diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e0cda4ed..6efcd1ede 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ## {{ UNRELEASED_VERSION }} - [{{ UNRELEASED_DATE }}]({{ UNRELEASED_LINK }}) +* Added `pre-` and `post-` as syntactical sugar for `L337` service `group` override syntax +* Fixed bug causing `v4` services to be unnecessarily rebuilt on non-rebuildy commands eg `lando start` + ## v3.24.0-beta.12 - [January 24, 2025](https://github.com/lando/core/releases/tag/v3.24.0-beta.12) * Merged in improvements from `@lando/core@v3.23.26` diff --git a/components/l337-v4.js b/components/l337-v4.js index 9820db680..f03c66e2f 100644 --- a/components/l337-v4.js +++ b/components/l337-v4.js @@ -27,6 +27,7 @@ const toPosixPath = require('../utils/to-posix-path'); class L337ServiceV4 extends EventEmitter { #app #data + #dgroups #lando static debug = require('debug')('@lando/l337-service-v4'); @@ -49,13 +50,15 @@ class L337ServiceV4 extends EventEmitter { groups: { context: { description: 'A group for adding and copying sources to the image', - weight: 0, + stage: 'image', user: 'root', + weight: 0, }, default: { description: 'A default general purpose build group around which other groups can be added', - weight: 1000, + stage: 'image', user: 'root', + weight: 1000, }, }, image: undefined, @@ -69,7 +72,6 @@ class L337ServiceV4 extends EventEmitter { }, sources: [], stages: { - default: 'image', image: 'Instructions to help generate an image', }, states: { @@ -151,9 +153,11 @@ class L337ServiceV4 extends EventEmitter { fs.mkdirSync(this.tmpdir, {recursive: true}); // initialize our private data + // note that you cannot override the default|context groups this.#app = app; + this.#dgroups = {groups: this.#init().groups ?? {}}; this.#lando = lando; - this.#data = merge(this.#init(), {groups}, {stages}, {states}, {volumes: Object.keys(tlvolumes)}); + this.#data = merge(this.#init(), {groups}, {stages}, {states}, {volumes: Object.keys(tlvolumes)}, this.#dgroups); // rework info based on whatever is passed in this.info = merge({}, {state: states}, {primary, service: id, type}, info); @@ -191,6 +195,11 @@ class L337ServiceV4 extends EventEmitter { // debug this.debug('%o autoset appmount to %o, did not select %o', this.id, this.appMount, appMounts); } + + // if the image is already built then lets set the tag and stuff here + if (this.info?.state?.IMAGE === 'BUILT' && this.tag) { + this.addComposeData({services: {[this.id]: {image: this.tag}}}); + } } // passed in build args that can be used @@ -346,11 +355,11 @@ class L337ServiceV4 extends EventEmitter { } // merge in - this.#data.groups = merge({}, this.#data.groups, {[group.id || group.name]: { - description: group.description || `Build group: ${group.id || group.name}`, - weight: group.weight || this.#data.groups.default.weight || 1000, - stage: group.stage || this.#data.stages.default || 'image', - user: group.user || 'root', + this.#data.groups = merge({}, this.#data.groups, {[group.id ?? group.name]: { + description: group.description ?? `Build group: ${group.id ?? group.name}`, + weight: group.weight ?? this.#data.groups.default.weight ?? 1000, + stage: group.stage ?? this.#data.groups.default.stage ?? 'image', + user: group.user ?? this.#data.groups.default.user ?? 'root', }}); this.debug('%o added build group %o', this.id, group); @@ -361,12 +370,14 @@ class L337ServiceV4 extends EventEmitter { // this handles our changes to docker-composes "image" key // @TODO: helper methods to add particular parts of build data eg image, files, steps, groups, etc addImageData(data) { - // make sure data is in object format if its a string then we assume it sets the "imagefile" value - if (typeof data === 'string') data = {imagefile: data}; + // make sure data is in object format if its stringy then we assume it sets the "imagefile" value + if (typeof data === 'string' || data?.constructor?.name === 'ImportString') data = {imagefile: data}; // map dockerfile key to image key if it is set and imagefile isnt if (!data.imagefile && data.dockerfile) data.imagefile = data.dockerfile; + // now pass the imagefile stuff into image parsing this.setBaseImage(data.imagefile); + // if the imageInstructions include COPY/ADD then make sure we are adding the dockerfile context directly as a // source so those instructions work // @NOTE: we are not adding a "context" because if this passes we have the instructions already and just need to make @@ -436,11 +447,12 @@ class L337ServiceV4 extends EventEmitter { // we should have stnadardized groups at this point so we can rebase on defaults as step = merge({}, - {stage: this.#data.stages.default}, + {stage: this.#data.groups.default.stage}, {weight: this.#data.groups.default.weight, user: this.#data.groups.default.user}, this.#data.groups[step.group], step, ); + // now lets modify the weight by the offset if we have one if (step.offset && Number(step.offset)) step.weight = step.weight + step.offset; // and finally lets rewrite the group for better instruction grouping @@ -539,6 +551,7 @@ class L337ServiceV4 extends EventEmitter { // add the final compose data with the updated image tag on success // @NOTE: ideally its sufficient for this to happen ONLY here but in v3 its not this.addComposeData({services: {[context.id]: {image: context.tag}}}); + // set the image stuff into the info this.info = {image: imagefile, state: {IMAGE: 'BUILT'}, tag: context.tag}; this.debug('image %o built successfully from %o', context.id, imagefile); @@ -662,10 +675,14 @@ class L337ServiceV4 extends EventEmitter { // gets group overrides or returns false if there are none getGroupOverrides(group) { - // break the group into parts - const parts = group.replace(`${this.getOverrideGroup(group)}`, '').split('-'); - // there will always be a leading '' element so dump it - parts.shift(); + // break the group into parts and remove empty strings + const parts = group.replace(`${this.getOverrideGroup(group)}`, '').split('-').filter(part => part && part !== ''); + + // translate pre|post syntax + if (['pre', 'post'].includes(parts[0])) { + parts[0] = parts[0] === 'pre' ? 'before' : 'after'; + if (parts.length === 1) parts.push('1'); + } // if we have nothing then lets return false at this point if (parts.length === 0) return false; @@ -697,7 +714,7 @@ class L337ServiceV4 extends EventEmitter { // this should ensure we end up with an ordered by closest match list const candidates = Object.keys(this.#data.groups) .sort((a, b) => b.length - a.length) - .filter(group => data.startsWith(group)); + .filter(group => data.startsWith(group) || data.startsWith(`pre-${group}`) || data.startsWith(`post-${group}`)); // if there is a closest match that is not the group itself then its an override otherwise fise return candidates.length > 0 && candidates[0] !== data ? candidates[0] : false; diff --git a/docs/services/l337.md b/docs/services/l337.md index 040271f4a..36b4da24d 100644 --- a/docs/services/l337.md +++ b/docs/services/l337.md @@ -513,7 +513,9 @@ While you _can_ use `COPY` and `ADD` instructions here we recommend you use [`co #### Override syntax -The group override syntax is flexible as long as the parent group is first. For example the following overrides are equivalent: +You can also override _any_ group with a `before|after`, `user` and `offset` to gain more flexible on _when_ and _who_ runs a given step. The group override syntax is flexible as long as the parent group is first. + +All the group overrides in the example below are equivalent. Any step using them will run as the `root` user at `+2` weight (eg after) to the weight provided by the `system` group. ```bash system-2 @@ -521,6 +523,15 @@ system-2-after system-root-after-2 system-2-after-root system-root-2-after +post-system-2 +``` + +Similarly, all of the below run as the `nginx` user but at `-2` weight (eg before) to the `system` group. + +```bash +system-2-before-nginx +system-nginx-before-2 +pre-system-2-nginx ``` That said, we like the `GROUP-OFFSET-DIRECTION-USER` format. ;) diff --git a/examples/l337/.lando.yml b/examples/l337/.lando.yml index 5561e5478..dd6d7f408 100644 --- a/examples/l337/.lando.yml +++ b/examples/l337/.lando.yml @@ -321,6 +321,14 @@ services: # direct group usage - instructions: RUN echo "${LANDO_IMAGE_USER}-${LANDO_IMAGE_GROUP}-$(whoami)" >> /tmp/groups group: "val-jean" + - instructions: RUN echo "${LANDO_IMAGE_USER}-${LANDO_IMAGE_GROUP}-$(whoami)" >> /tmp/groups + group: "pre-val-jean" + - instructions: RUN echo "${LANDO_IMAGE_USER}-${LANDO_IMAGE_GROUP}-$(whoami)" >> /tmp/groups + group: "post-val-jean" + - instructions: RUN echo "${LANDO_IMAGE_USER}-${LANDO_IMAGE_GROUP}-$(whoami)" >> /tmp/groups + group: "pre-val-jean-4" + - instructions: RUN echo "${LANDO_IMAGE_USER}-${LANDO_IMAGE_GROUP}-$(whoami)" >> /tmp/groups + group: "post-val-jean-5" - instructions: RUN echo "${LANDO_IMAGE_USER}-${LANDO_IMAGE_GROUP}-$(whoami)" >> /tmp/groups group: "user" - instructions: RUN echo "${LANDO_IMAGE_USER}-${LANDO_IMAGE_GROUP}-$(whoami)" >> /tmp/groups @@ -398,6 +406,19 @@ services: - instructions: RUN echo "first" >> /stuff weight: 1 + import: + api: 4 + type: l337 + image: + imagefile: !import Dockerfile + steps: + - instructions: !import instructions/Instructions1 + weight: 1 + - instructions: !import instructions/Instructions2 + weight: 2 + - instructions: !import instructions/Instructions3 + weight: 3 + networks: my-network: volumes: diff --git a/examples/l337/README.md b/examples/l337/README.md index feae568b0..18f2b1d14 100644 --- a/examples/l337/README.md +++ b/examples/l337/README.md @@ -78,7 +78,7 @@ lando info --service image-5 | grep tag: | grep "lando/l337\-" | grep "\-image-5 lando info --service image-6 | grep tag: | grep "lando/l337\-" | grep "\-image-6:latest" # should use web as the primary service for tooling and events -lando ssh --command "env" | grep SERVICE | grep web +lando exec web -- env | grep SERVICE | grep web lando env | grep SERVICE | grep web # should use the user as the default exec user @@ -122,21 +122,21 @@ lando ssh --service image-3 --command "stat /file1" # should run in working_dir if appMount is not set lando pwd --service db | grep /tmp -lando ssh --service db --command "pwd" | grep /tmp +lando exec db -- pwd | grep /tmp cd folder lando pwd --service db | grep -w /tmp -lando ssh --service db --command "pwd" | grep /tmp +lando exec db -- pwd | grep /tmp cd .. # should run in image working_dir as fallback lando pwd --service image-1 | grep -w / -lando ssh --service image-1 --command "pwd" | grep -w / +lando exec image-1 -- pwd | grep -w / lando pwd --service image-6 | grep /usr/share/nginx/html -lando ssh --service image-6 --command "pwd" | grep /usr/share/nginx/html +lando exec image-6 -- pwd | grep /usr/share/nginx/html # should correctly mount read-only volumes -lando ssh --command "test -r /file-ro" -lando ssh --command "test -w /file-ro" || echo $? | grep 1 +lando exec web -- test -r /file-ro +lando exec web -- test -w /file-ro || echo $? | grep 1 # should handle all context options correctly lando stat /folder @@ -178,12 +178,16 @@ lando groups | sed -n '11p' | grep root-default-1000-root-root lando groups | sed -n '12p' | grep root-default-1000-root-root lando groups | sed -n '13p' | grep nginx-user-10000-nginx-nginx lando groups | sed -n '14p' | grep nginx-val-jean-24591-nginx-nginx -lando groups | sed -n '15p' | grep root-val-jean-24601-root-root -lando groups | sed -n '16p' | grep root-val-jean-24603-root-root -lando groups | sed -n '17p' | grep root-val-jean-24604-root-root -lando groups | sed -n '18p' | grep nginx-val-jean-24605-nginx-nginx -lando groups | sed -n '19p' | grep nginx-val-jean-24606-nginx-nginx -lando groups | sed -n '20p' | grep root-val-jean-24701-root-root +lando groups | sed -n '15p' | grep root-val-jean-24597-root-root +lando groups | sed -n '16p' | grep root-val-jean-24600-root-root +lando groups | sed -n '17p' | grep root-val-jean-24601-root-root +lando groups | sed -n '18p' | grep root-val-jean-24602-root-root +lando groups | sed -n '19p' | grep root-val-jean-24603-root-root +lando groups | sed -n '20p' | grep root-val-jean-24604-root-root +lando groups | sed -n '21p' | grep nginx-val-jean-24605-nginx-nginx +lando groups | sed -n '22p' | grep root-val-jean-24606-root-root +lando groups | sed -n '23p' | grep nginx-val-jean-24606-nginx-nginx +lando groups | sed -n '24p' | grep root-val-jean-24701-root-root # should run build steps as the correct user lando groups | sed -n '1p' | grep root-default--99999999-root-root @@ -200,25 +204,32 @@ lando groups | sed -n '11p' | grep root-default-1000-root-root lando groups | sed -n '12p' | grep root-default-1000-root-root lando groups | sed -n '13p' | grep nginx-user-10000-nginx-nginx lando groups | sed -n '14p' | grep nginx-val-jean-24591-nginx-nginx -lando groups | sed -n '15p' | grep root-val-jean-24601-root-root -lando groups | sed -n '16p' | grep root-val-jean-24603-root-root -lando groups | sed -n '17p' | grep root-val-jean-24604-root-root -lando groups | sed -n '18p' | grep nginx-val-jean-24605-nginx-nginx -lando groups | sed -n '19p' | grep nginx-val-jean-24606-nginx-nginx -lando groups | sed -n '20p' | grep root-val-jean-24701-root-root +lando groups | sed -n '15p' | grep root-val-jean-24597-root-root +lando groups | sed -n '16p' | grep root-val-jean-24600-root-root +lando groups | sed -n '17p' | grep root-val-jean-24601-root-root +lando groups | sed -n '18p' | grep root-val-jean-24602-root-root +lando groups | sed -n '19p' | grep root-val-jean-24603-root-root +lando groups | sed -n '20p' | grep root-val-jean-24604-root-root +lando groups | sed -n '21p' | grep nginx-val-jean-24605-nginx-nginx +lando groups | sed -n '22p' | grep root-val-jean-24606-root-root +lando groups | sed -n '23p' | grep nginx-val-jean-24606-nginx-nginx +lando groups | sed -n '24p' | grep root-val-jean-24701-root-root # Should run steps in all allowed formats lando env --service steps-1 | grep VIBES | grep RISING lando env --service steps-1 | grep KIRK | grep wesley lando env --service steps-1 | grep SPOCK | grep peck +lando env --service import | grep LANDO_STRINGY_IMPORT_INSTRUCTIONS_1 | grep 1 +lando env --service import | grep LANDO_STRINGY_IMPORT_INSTRUCTIONS_2 | grep 1 +lando env --service import | grep LANDO_STRINGY_IMPORT_INSTRUCTIONS_3 | grep 1 # Should run unknown groups as the default group -lando ssh --service steps-1 --command "cat /tmp/val-jean-group" | grep default-1000-root +lando exec steps-1 -- cat /tmp/val-jean-group | grep default-1000-root # Should order detached groups by weight -lando ssh --service steps-1 --command "cat /stuff" | sed -n '1p' | grep first -lando ssh --service steps-1 --command "cat /stuff" | sed -n '2p' | grep middle -lando ssh --service steps-1 --command "cat /stuff" | sed -n '3p' | grep last +lando exec steps-1 -- cat /stuff | sed -n '1p' | grep first +lando exec steps-1 -- cat /stuff | sed -n '2p' | grep middle +lando exec steps-1 -- cat /stuff | sed -n '3p' | grep last ``` ## Destroy tests diff --git a/examples/l337/instructions/Instructions1 b/examples/l337/instructions/Instructions1 new file mode 100644 index 000000000..ce4afed36 --- /dev/null +++ b/examples/l337/instructions/Instructions1 @@ -0,0 +1,2 @@ +ENV MARIADB_ALLOW_EMPTY_ROOT_PASSWORD=1 +ENV LANDO_STRINGY_IMPORT_INSTRUCTIONS_1=1 diff --git a/examples/l337/instructions/Instructions2 b/examples/l337/instructions/Instructions2 new file mode 100644 index 000000000..505b0b34c --- /dev/null +++ b/examples/l337/instructions/Instructions2 @@ -0,0 +1 @@ +ENV LANDO_STRINGY_IMPORT_INSTRUCTIONS_2=1 diff --git a/examples/l337/instructions/Instructions3 b/examples/l337/instructions/Instructions3 new file mode 100644 index 000000000..f52130754 --- /dev/null +++ b/examples/l337/instructions/Instructions3 @@ -0,0 +1 @@ +ENV LANDO_STRINGY_IMPORT_INSTRUCTIONS_3=1 diff --git a/hooks/app-run-v4-build-image.js b/hooks/app-run-v4-build-image.js index 83852b80d..75e9e036d 100644 --- a/hooks/app-run-v4-build-image.js +++ b/hooks/app-run-v4-build-image.js @@ -13,6 +13,7 @@ module.exports = async (app, lando) => { const services = _(app.v4.services) .filter(service => _.includes(buildV4Services, service.id)) .filter(service => typeof service.buildImage === 'function') + .filter(service => service?.info?.state?.IMAGE !== 'BUILT') .value(); app.log.debug('going to build v4 images', services.map(service => service.id));