From 76c2446d9af79a4aaaa367d11c327b01f3c3a3e8 Mon Sep 17 00:00:00 2001 From: Mike Pirog Date: Thu, 19 Dec 2024 11:07:36 -0500 Subject: [PATCH] Enable multiline and import|load support for a bunch of stuff (#311) * chill debugger and get it to match better with lando4, also make security MLC * multiline support for v3 build steps * add multiline !load support to events * use sapi as a more holistic measure of the service api * uh * fix write-file to be more targetted re: ImportString * fix write-file to be more targetted re: ImportString part 2 * fix write-file to be more targetted re: ImportString part 3 * fix other SAPI related issues re: multipass * improve !load resolution when base is unknown * fix incorrect SAPI usage * #244: Variable for setting the network limit. * #244: Add test for the networkLimit config var. * #297: fixed incorrect -EncodedCommand fallback detection for powershell.exe script execution * cl * release v3.23.22 generated by @lando/prepare-release-action * multipass access to healthchecks * multipass access to tooling * multipass access for config --------- Co-authored-by: Alec Reynolds Co-authored-by: Alec Reynolds <1153738+reynoldsalec@users.noreply.github.com> Co-authored-by: rtfm-47 --- CHANGELOG.md | 5 +++ app.js | 8 ++-- builders/_lando.js | 30 ++++++++++--- builders/lando-v4.js | 19 ++++----- components/docker-engine.js | 4 -- components/l337-v4.js | 38 +++++++++++++---- components/yaml.js | 18 +++++++- docs/config/networking.md | 6 +++ examples/build/.lando.yml | 5 +++ examples/build/paperback-writer.sh | 32 ++++++++++++++ examples/config/.lando.yml | 6 +++ examples/config/README.md | 6 +++ examples/config/rooster | 1 + examples/events/.lando.yml | 6 +++ examples/events/README.md | 24 +++++------ examples/events/paperback-writer.sh | 32 ++++++++++++++ examples/healthcheck/.lando.yml | 19 +++++++++ examples/healthcheck/README.md | 9 ++-- examples/healthcheck/healthcheck.sh | 10 ++--- examples/networking/README.md | 8 ++++ examples/networking/config.yml | 1 + examples/security/.lando.yml | 59 +++++++++++++++++++++++--- examples/security/README.md | 14 ++++++ examples/tooling/.lando.yml | 50 ++++++++++++++++++++++ examples/tooling/README.md | 13 ++++++ hooks/app-add-healthchecks.js | 13 +++++- hooks/app-override-tooling-defaults.js | 1 - hooks/lando-clean-networks.js | 2 +- index.js | 1 + lib/app.js | 4 +- lib/router.js | 2 + packages/security/security.js | 26 ++++++++++-- release-aliases/3-STABLE | 2 +- scripts/exec-multiliner.sh | 32 ++++++++++++++ tasks/exec.js | 2 +- utils/build-tooling-task.js | 7 ++- utils/debug-shim.js | 38 +++++++++++++++-- utils/filter-v3-build-steps.js | 6 ++- utils/get-executors.js | 9 ---- utils/get-service-apis.js | 16 +++++++ utils/get-tasks.js | 1 + utils/is-stringy.js | 3 ++ utils/normalize-healthcheck.js | 2 + utils/parse-events-config.js | 44 ++++++++++++------- utils/parse-tooling-config.js | 30 +++++++------ utils/run-command.js | 12 +++--- utils/run-powershell-script.js | 4 +- utils/shell-escape.js | 23 +++++++++- utils/write-file.js | 7 ++- 49 files changed, 586 insertions(+), 124 deletions(-) create mode 100755 examples/build/paperback-writer.sh create mode 100644 examples/config/rooster create mode 100755 examples/events/paperback-writer.sh create mode 100644 examples/networking/config.yml create mode 100755 scripts/exec-multiliner.sh delete mode 100644 utils/get-executors.js create mode 100644 utils/get-service-apis.js create mode 100644 utils/is-stringy.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 7eea76769..319c6e015 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,11 @@ * Fixed bug causing auto setup to not correctly reset the orchestrator binary path * Improved `lando init` so that it can auto setup if needed +## v3.23.22 - [December 17, 2024](https://github.com/lando/core/releases/tag/v3.23.22) + +* Added ability to customize `networkLimit` [#245](https://github.com/lando/core/pull/245) +* Fixed incorrect `-EncodedCommand` fallback detection for `powershell.exe` script execution [#297](https://github.com/lando/core/issues/297) + ## v3.23.21 - [December 14, 2024](https://github.com/lando/core/releases/tag/v3.23.21) * Fixed `powershell` scripts from failing when user cannot set `-ExecutionPolicy` to `Bypass` for `Process` scope [#297](https://github.com/lando/core/issues/297) diff --git a/app.js b/app.js index 24c1e9a4f..2fc3a0237 100644 --- a/app.js +++ b/app.js @@ -34,6 +34,7 @@ module.exports = async (app, lando) => { primary: app._defaultService, project: app.project, root: app.root, + sapis: require('./utils/get-service-apis')(app), }, {persist: true}); }; @@ -54,15 +55,16 @@ module.exports = async (app, lando) => { compose: app.compose, containers: app.containers, info: _.cloneDeep(app.info).map(service => ({...service, hostname: [], urls: []})), - executors: require('./utils/get-executors')(_.get(app, 'v4.services', {})), name: app.name, mounts: require('./utils/get-mounts')(_.get(app, 'v4.services', {})), + primary: app._defaultService, + project: app.project, root: app.root, + sapis: require('./utils/get-service-apis')(app), overrides: { tooling: app._coreToolingOverrides, }, - primary: app._defaultService, - project: app.project, + }, {persist: true}); }; diff --git a/builders/_lando.js b/builders/_lando.js index 14be7773c..3b3855b7a 100644 --- a/builders/_lando.js +++ b/builders/_lando.js @@ -3,8 +3,12 @@ // Modules const _ = require('lodash'); const fs = require('fs'); +const os = require('os'); const path = require('path'); +const write = require('../utils/write-file'); + const {color} = require('listr2'); +const {nanoid} = require('nanoid'); /* * The lowest level lando service, this is where a lot of the deep magic lives @@ -128,13 +132,29 @@ module.exports = { volumes.push(`${local}:${remote}`); }); + // rebase remoteFiles + remoteFiles = _.merge({}, {'_lando_': '/tmp/rooster'}, remoteFiles); + // Handle custom config files - _.forEach(config, (file, type) => { - if (_.has(remoteFiles, type)) { - const local = path.resolve(root, config[type]); - const remote = remoteFiles[type]; - volumes.push(`${local}:${remote}`); + _.forEach(config, (local, remote) => { + // if this is special type then get it from remoteFile + remote = _.has(remoteFiles, remote) ? remoteFiles[remote] : path.resolve('/', remote); + + // if file is an imported string lets just get the file path instead + if (local?.constructor?.name === 'ImportString') { + const meta = local.getMetadata(); + if (meta.file) local = meta.file; + else local = local.toString(); } + + // if file is still a multiline string then dump to tmp and use that + if (typeof local === 'string' && local.split('\n').length > 1) { + const contents = local; + local = path.join(os.tmpdir(), nanoid()); + write(local, contents, {forcePosixLineEndings: true}); + } + + volumes.push(`${path.resolve(root, local)}:${remote}`); }); // Add named volumes and other thingz into our primary service diff --git a/builders/lando-v4.js b/builders/lando-v4.js index 0d82fcb0f..737863989 100644 --- a/builders/lando-v4.js +++ b/builders/lando-v4.js @@ -151,6 +151,7 @@ module.exports = { this.addLSF(path.join(__dirname, '..', 'scripts', 'boot.sh')); this.addLSF(path.join(__dirname, '..', 'scripts', 'entrypoint.sh')); this.addLSF(path.join(__dirname, '..', 'scripts', 'exec.sh')); + this.addLSF(path.join(__dirname, '..', 'scripts', 'exec-multiliner.sh')); this.addLSF(path.join(__dirname, '..', 'scripts', 'run-hooks.sh')); this.addLSF(path.join(__dirname, '..', 'scripts', 'landorc.sh'), 'landorc'); this.addLSF(path.join(__dirname, '..', 'scripts', 'utils.sh')); @@ -303,7 +304,7 @@ module.exports = { super(id, merge({}, {stages}, {groups}, {states}, upstream), app, lando); // props - this.canExec = true; + this.api = 4; this.canHealthcheck = true; this.isInteractive = lando.config.isInteractive; this.generateCert = lando.generateCert.bind(lando); @@ -401,7 +402,6 @@ module.exports = { this.setNPMRC(lando.config.pluginConfigFile); // add in top level things - this.debug('adding top level volumes %o and networks %o', this.tlvolumes, {networks: this.tlnetworks}); this.addComposeData({networks: this.tlnetworks, volumes: this.tlvolumes}); // environment @@ -505,6 +505,10 @@ module.exports = { } addLSF(source, dest, {context = 'context'} = {}) { + // normalize file input + source = this.normalizeFileInput(source, {dest}); + + // then do the rest if (dest === undefined) dest = path.basename(source); this.addContext(`${source}:/etc/lando/${dest}`, context); return `/etc/lando/${dest}`; @@ -673,15 +677,10 @@ module.exports = { } mountScript(contents, {dest = `tmp/${nanoid()}.sh`} = {}) { - // @TODO: check if contents is a string? - - // compute hostside file - const file = path.join(this.tmpdir, `${nanoid()}.sh`); - - // dump contents to service tmpdir and make executable - write(file, contents, {forcePosixLineEndings: true}); + // normalize to a file + const file = this.normalizeFileInput(contents); + // make executable fs.chmodSync(file, '755'); - // now complete the final mapping for container injection return this.addLSF(file, dest, 'user'); } diff --git a/components/docker-engine.js b/components/docker-engine.js index 7d4bb8f9b..617a4a6ea 100644 --- a/components/docker-engine.js +++ b/components/docker-engine.js @@ -128,7 +128,6 @@ class DockerEngine extends Dockerode { for (const source of sources) { try { fs.copySync(source.source, path.join(context, source.target), {dereference: true}); - debug('copied %o into build context %o', source.source, path.join(context, source.target)); } catch (error) { error.message = `Failed to copy ${source.source} into build context at ${source.target}!: ${error.message}`; throw error; @@ -139,7 +138,6 @@ class DockerEngine extends Dockerode { // @NOTE: we do this last to ensure we overwrite any dockerfile that may happenstance end up in the build-context // from source above fs.copySync(dockerfile, path.join(context, 'Dockerfile')); - debug('copied Imagefile from %o to %o', dockerfile, path.join(context, 'Dockerfile')); // on windows we want to ensure the build context has linux line endings if (process.platform === 'win32') { @@ -229,7 +227,6 @@ class DockerEngine extends Dockerode { for (const source of sources) { try { fs.copySync(source.source, path.join(context, source.target), {dereference: true}); - debug('copied %o into build context %o', source.source, path.join(context, source.target)); } catch (error) { error.message = `Failed to copy ${source.source} into build context at ${source.target}!: ${error.message}`; throw error; @@ -245,7 +242,6 @@ class DockerEngine extends Dockerode { // copy the dockerfile to the correct place and reset fs.copySync(dockerfile, path.join(context, 'Dockerfile')); - debug('copied Imagefile from %o to %o', dockerfile, path.join(context, 'Dockerfile')); dockerfile = path.join(context, 'Dockerfile'); // build initial buildx command diff --git a/components/l337-v4.js b/components/l337-v4.js index f80688bfa..9820db680 100644 --- a/components/l337-v4.js +++ b/components/l337-v4.js @@ -3,6 +3,7 @@ const fs = require('fs'); const groupBy = require('lodash/groupBy'); const isObject = require('lodash/isPlainObject'); +const isStringy = require('../utils/is-stringy'); const os = require('os'); const merge = require('lodash/merge'); const path = require('path'); @@ -90,7 +91,6 @@ class L337ServiceV4 extends EventEmitter { } this.emit('state', this.#data.info); this.#app.v4.updateComposeCache(); - this.debug('updated app info to %o', this.#data.info); } get info() { @@ -127,11 +127,12 @@ class L337ServiceV4 extends EventEmitter { // set top level required stuff this.id = id; + this.api = 'l337'; this.appRoot = appRoot; this.buildkit = true; this.config = config; this.context = context; - this.debug = debug; + this.debug = debug.extend(id); this.name = name ?? id; this.primary = primary; this.project = project; @@ -253,7 +254,6 @@ class L337ServiceV4 extends EventEmitter { // update and log this.#app.v4.updateComposeCache(); - this.debug('%o added top level compose data %o', this.id, JSON.parse(JSON.stringify(data))); } // adds files/dirs to the build context @@ -327,7 +327,6 @@ class L337ServiceV4 extends EventEmitter { if (group) this.addSteps({group, instructions: file.instructions.join('\n'), contexted: true}); // return normalized data - this.debug('%o added %o to the build context', this.id, file); return file; })); } @@ -446,8 +445,6 @@ class L337ServiceV4 extends EventEmitter { if (step.offset && Number(step.offset)) step.weight = step.weight + step.offset; // and finally lets rewrite the group for better instruction grouping step.group = `${step.group}-${step.weight}-${step.user}`; - // log - this.debug('%o added build step %o', this.id, step); // push this.#data.steps.push(step); }); @@ -576,8 +573,8 @@ class L337ServiceV4 extends EventEmitter { // remove build contexts and tmp remove(this.context); remove(this.tmpdir); - this.debug('removed %o build-context %o', `${this.project}-${this.id}`, this.context); - this.debug('removed %o tmpdir %o', `${this.project}-${this.id}`, this.id, this.tmpdir); + this.debug('removed build-context %o', this.context); + this.debug('removed tmpdir %o', this.tmpdir); } generateBuildContext() { @@ -706,6 +703,29 @@ class L337ServiceV4 extends EventEmitter { return candidates.length > 0 && candidates[0] !== data ? candidates[0] : false; } + normalizeFileInput(data, {dest = undefined} = {}) { + // if data is not a stringy then do something else? + if (!isStringy(data)) { + this.debug('%o does not seem to be valid file input data', data); + return data; + } + + // if data is a single line string then just return it + if (data.split('\n').length === 1) return path.resolve(this.appRoot, data); + + // if dest is undefined and we have ImportString then lets use that filename + if (dest === undefined && data?.constructor?.name === 'ImportString') { + const {file} = data.getMetadata(); + if (file) dest = path.basename(file); + } + + // if we are here it is multiline and lets dump to a tmp file and return that + const file = path.join(this.tmpdir, dest ?? nanoid()); + write(file, data, {forcePosixLineEndings: true}); + + return file; + } + normalizeVolumes(volumes = []) { if (!Array.isArray) return []; @@ -791,7 +811,7 @@ class L337ServiceV4 extends EventEmitter { } // log - this.debug('%o set base image to %o with instructions %o', this.id, this.#data.image, this.#data.imageInstructions); + this.debug('set base image to %o with instructions %o', this.#data.image, this.#data.imageInstructions); } } diff --git a/components/yaml.js b/components/yaml.js index 76b83c36f..43037be82 100644 --- a/components/yaml.js +++ b/components/yaml.js @@ -22,6 +22,13 @@ const parseFileTypeInput = input => { }; }; +// helper to find file +const findFile = (file, base = undefined) => { + return require('../utils/traverse-up')([file], path.resolve(base)) + .map(candidate => path.join(path.dirname(candidate), file)) + .find(candidate => fs.existsSync(candidate)); +}; + // file loader options const fileloader = { kind: 'scalar', @@ -33,7 +40,7 @@ const fileloader = { const input = parseFileTypeInput(data); // if data is not an absolute path then resolve with base - if (!path.isAbsolute(input.file)) input.file = path.resolve(this.base, input.file); + if (!path.isAbsolute(input.file)) input.file = findFile(input.file, this.base); // Otherwise check the path exists return fs.existsSync(input.file); @@ -42,7 +49,7 @@ const fileloader = { // transform data data = {raw: data, ...parseFileTypeInput(data)}; // normalize if needed - data.file = !path.isAbsolute(data.file) ? path.resolve(this.base, data.file) : data.file; + data.file = !path.isAbsolute(data.file) ? findFile(data.file, this.base) : data.file; // switch based on type switch (data.type) { @@ -100,6 +107,13 @@ class ImportString extends String { getDumper() { return this.#metadata.raw; } + + [Symbol.toPrimitive](hint) { + if (hint === 'string') { + return this.toString(); + } + return this.toString(); + } } class ImportObject extends Object { diff --git a/docs/config/networking.md b/docs/config/networking.md index b40f0c281..25a836934 100644 --- a/docs/config/networking.md +++ b/docs/config/networking.md @@ -58,3 +58,9 @@ You can also use the environment variable `LANDO_HOST_IP`. ```sh lando exec my-service -- ping "\$LANDO_HOST_IP" -c 3 ``` + +## Network Limits + +By default Docker has a limit of 32 networks. If you're running a large number of sites, you'll see a message `Lando has detected you are at Docker's network limit`, after which Lando will attempt to clean up unused networks to put you below the network limit. + +If you've [modified your Docker daemon](https://discussion.fedoraproject.org/t/increase-limit-of-30-docker-networks-in-a-clean-way/96622/4) to allow more networks, you can set Lando's network limit to a higher number by setting the `networkLimit` variable in [Lando's global config](./global.html). diff --git a/examples/build/.lando.yml b/examples/build/.lando.yml index 5f02cb5a0..ed281c575 100644 --- a/examples/build/.lando.yml +++ b/examples/build/.lando.yml @@ -14,6 +14,8 @@ events: - cat /var/www/run_internal.txt | grep www-data - cat /run_as_root.txt | grep root - cat /var/www/run.txt | grep www-data + - cat /tmp/famous-blogger | grep famous-blogger + - cat /tmp/paperback-writer | grep paperback-writer - /bin/sh -c 'echo "$LANDO_APP_PROJECT" | grep landobuild' # uncomment below to test out https://github.com/lando/core/issues/70 # this is commented out by default because of https://github.com/actions/runner/issues/241 @@ -40,6 +42,9 @@ services: # - bash /app/build_as_root.bash build: - echo "$(id)" > /var/www/build.txt + - !load paperback-writer.sh + - /app/paperback-writer.sh --writer famous-blogger + run_as_root: - echo "$(id)" > /run_as_root.txt run: diff --git a/examples/build/paperback-writer.sh b/examples/build/paperback-writer.sh new file mode 100755 index 000000000..9e89d88a9 --- /dev/null +++ b/examples/build/paperback-writer.sh @@ -0,0 +1,32 @@ +#!/bin/bash +set -eo pipefail + +WRITER=paperback-writer + +# PARSE THE ARGZZ +while (( "$#" )); do + case "$1" in + --writer) + WRITER="$2" + shift 2 + ;; + --writer=*) + WRITER="${1#*=}" + shift + ;; + --) + shift + break + ;; + -*|--*=) + shift + ;; + *) + shift + ;; + esac +done + +echo "$WRITER" > "/tmp/$WRITER" + +cat "/tmp/$WRITER" diff --git a/examples/config/.lando.yml b/examples/config/.lando.yml index 1b4fcfbcb..cd59f47d4 100644 --- a/examples/config/.lando.yml +++ b/examples/config/.lando.yml @@ -5,6 +5,12 @@ services: web2: api: 3 type: lando + config: + _lando_: rooster + /tmp/somewhere: rooster + /tmp/somewhere-else: !import rooster + /tmp/somewhere-else-else: | + here they come to snuff the rooster services: image: nginx:1.22.1 command: /docker-entrypoint.sh nginx -g "daemon off;" diff --git a/examples/config/README.md b/examples/config/README.md index 3f86fd8fa..d3ae6a014 100644 --- a/examples/config/README.md +++ b/examples/config/README.md @@ -41,6 +41,12 @@ lando config --format json | grep "^{\"" # Should output in table format lando config --format table | grep landoFileConfig.name | grep lando-config + +# Should be able to mount config files in a few different ways +lando exec web2 -- cat /tmp/rooster | grep "here they come to snuff the rooster" +lando exec web2 -- cat /tmp/somewhere | grep "here they come to snuff the rooster" +lando exec web2 -- cat /tmp/somewhere-else | grep "here they come to snuff the rooster" +lando exec web2 -- cat /tmp/somewhere-else-else | grep "here they come to snuff the rooster" ``` ## Destroy tests diff --git a/examples/config/rooster b/examples/config/rooster new file mode 100644 index 000000000..6d6b7160c --- /dev/null +++ b/examples/config/rooster @@ -0,0 +1 @@ +here they come to snuff the rooster diff --git a/examples/events/.lando.yml b/examples/events/.lando.yml index a60eb3815..af8ea58bb 100644 --- a/examples/events/.lando.yml +++ b/examples/events/.lando.yml @@ -39,8 +39,14 @@ events: - web2: env > /app/test/web2-event-env.txt post-start: - web: id && mkdir -p /app/test && echo "$(hostname -s)" > /app/test/web-post-start.txt + - web: !import paperback-writer.sh + - web: cat /tmp/paperback-writer | grep paperback-writer - web2: id && mkdir -p /app/test && echo "$(hostname -s)" > /app/test/web2-post-start.txt + - web2: !import paperback-writer.sh + - web2: cat /tmp/paperback-writer | grep paperback-writer - l337: id && mkdir -p /app/test && echo "$(hostname -s)" > /app/test/l337-post-start.txt + - l337: !import paperback-writer.sh + - l337: cat /tmp/paperback-writer | grep paperback-writer post-thing: - web: mkdir -p /app/test && echo "$(hostname -s)" > /app/test/web-post-thing.txt - web2: mkdir -p /app/test && echo "$(hostname -s)" > /app/test/web2-post-thing.txt diff --git a/examples/events/README.md b/examples/events/README.md index df2c5f61a..40a2ae4cf 100644 --- a/examples/events/README.md +++ b/examples/events/README.md @@ -20,22 +20,22 @@ Run the following commands to verify things work as expected ```bash # Should run events on the primary service by default -lando ssh -s appserver -c "cat /app/test/appserver-pre-start.txt | grep \$(hostname -s)" +lando exec appserver -- "cat /app/test/appserver-pre-start.txt | grep \$(hostname -s)" # Should run events on the specified service -lando ssh -s web -c "cat /app/test/web-pre-start.txt | grep \$(hostname -s)" -lando ssh -s web -c "cat /app/test/web-post-start.txt | grep \$(hostname -s)" -lando ssh -s l337 -c "cat /app/test/l337-pre-start.txt | grep \$(hostname -s)" -lando ssh -s l337 -c "cat /app/test/l337-post-start.txt | grep \$(hostname -s)" +lando exec web -- "cat /app/test/web-pre-start.txt | grep \$(hostname -s)" +lando exec web -- "cat /app/test/web-post-start.txt | grep \$(hostname -s)" +lando exec l337 -- "cat /app/test/l337-pre-start.txt | grep \$(hostname -s)" +lando exec l337 -- "cat /app/test/l337-post-start.txt | grep \$(hostname -s)" lando exec web2 -- "cat /app/test/web2-pre-start.txt | grep \$(hostname -s)" lando exec web2 -- "cat /app/test/web2-post-start.txt | grep \$(hostname -s)" # Should run tooling command events using the tooling command service as the default lando thing -lando ssh -s web -c "cat /app/test/web-post-thing.txt | grep \$(hostname -s)" +lando exec web -- "cat /app/test/web-post-thing.txt | grep \$(hostname -s)" lando exec web2 -- "cat /app/test/web2-post-thing.txt | grep \$(hostname -s)" lando stuff -lando ssh -s l337 -c "cat /app/test/l337-post-stuff.txt | grep \$(hostname -s)" +lando exec l337 -- "cat /app/test/l337-post-stuff.txt | grep \$(hostname -s)" lando exec web2 -- "cat /app/test/web2-post-stuff.txt | grep \$(hostname -s)" # Should run dynamic tooling command events using argv if set or option default otherwise @@ -50,14 +50,14 @@ lando multi-pass # Should run on rebuild without failing and trigger pre-rebuild event lando rebuild -y | grep "ET TU, BRUT" -lando ssh -s web -c "cat /app/test/web-pre-rebuild.txt | grep rebuilding" -lando ssh -s l337 -c "cat /app/test/l337-pre-rebuild.txt | grep rebuilding" +lando exec web -- cat /app/test/web-pre-rebuild.txt | grep rebuilding +lando exec l337 -- cat /app/test/l337-pre-rebuild.txt | grep rebuilding lando exec web2 -- cat /app/test/web2-pre-rebuild.txt | grep rebuilding # Should run events as the correct user -lando ssh -s appserver -c "cat /app/test/appserver-user.txt" | grep www-data -lando ssh -s web -c "cat /app/test/web-user.txt" | grep www-data -lando ssh -s l337 -c "cat /app/test/l337-user.txt" | grep root +lando exec appserver -- cat /app/test/appserver-user.txt | grep www-data +lando exec web -- cat /app/test/web-user.txt | grep www-data +lando exec l337 -- cat /app/test/l337-user.txt | grep root lando exec web2 -- cat /app/test/web2-user.txt | grep nginx # Should load the correct environment for lando 4 service events diff --git a/examples/events/paperback-writer.sh b/examples/events/paperback-writer.sh new file mode 100755 index 000000000..9e89d88a9 --- /dev/null +++ b/examples/events/paperback-writer.sh @@ -0,0 +1,32 @@ +#!/bin/bash +set -eo pipefail + +WRITER=paperback-writer + +# PARSE THE ARGZZ +while (( "$#" )); do + case "$1" in + --writer) + WRITER="$2" + shift 2 + ;; + --writer=*) + WRITER="${1#*=}" + shift + ;; + --) + shift + break + ;; + -*|--*=) + shift + ;; + *) + shift + ;; + esac +done + +echo "$WRITER" > "/tmp/$WRITER" + +cat "/tmp/$WRITER" diff --git a/examples/healthcheck/.lando.yml b/examples/healthcheck/.lando.yml index a8bc0ca61..2a8527ba4 100755 --- a/examples/healthcheck/.lando.yml +++ b/examples/healthcheck/.lando.yml @@ -12,6 +12,18 @@ services: volumes: - "./healthcheck.sh:/usr/local/bin/healthcheck" healthcheck: healthcheck + appserver2: + api: 3 + type: lando + services: + image: php:8.2-fpm + command: docker-php-entrypoint sleep infinity + healthcheck: | + #!/bin/bash + set -eo pipefail + touch /tmp/hi + exit 0 + nginx: api: 3 type: lando @@ -49,6 +61,13 @@ services: command: healthcheck retry: 10 delay: 1000 + nginx3: + api: 4 + image: + imagefile: nginxinc/nginx-unprivileged:1.26.1 + user: nginx + healthcheck: !load healthcheck.sh + database1: api: 3 type: lando diff --git a/examples/healthcheck/README.md b/examples/healthcheck/README.md index a7c3bbb17..d792b881b 100755 --- a/examples/healthcheck/README.md +++ b/examples/healthcheck/README.md @@ -32,14 +32,17 @@ lando info -s disablebase | grep healthy: | grep unknown lando start # Should have passed all the healthchecks -lando ssh -s appserver -c "stat /healthy" -lando ssh -s database1 -c "mysql -uroot --silent --execute \"SHOW DATABASES;\"" -lando ssh -s database2 -c "mysql -uroot --silent --execute \"SHOW DATABASES;\"" +lando exec appserver -- "stat /healthy" +lando exec appserver -- "stat /healthy" +lando exec database1 -- "mysql -uroot --silent --execute \"SHOW DATABASES;\"" +lando exec database2 -- "mysql -uroot --silent --execute \"SHOW DATABASES;\"" # Should set healthy status to true if applicable lando info -s appserver | grep healthy: | grep true +lando info -s appserver2 | grep healthy: | grep true lando info -s nginx | grep healthy: | grep unknown lando info -s nginx2 | grep healthy: | grep true +lando info -s nginx3 | grep healthy: | grep true lando info -s database1 | grep healthy: | grep true lando info -s database2 | grep healthy: | grep true lando info -s disablebase | grep healthy: | grep unknown diff --git a/examples/healthcheck/healthcheck.sh b/examples/healthcheck/healthcheck.sh index 364e03ee5..01a5b2dcc 100755 --- a/examples/healthcheck/healthcheck.sh +++ b/examples/healthcheck/healthcheck.sh @@ -4,16 +4,16 @@ set -eo pipefail rm -rf /healthy main () { - local healthfile=/tmp/healthfile + local healthfile="/tmp/healthfile" # if healthcheck file does not exist then create it - if [ ! -f $healthfile ]; then - touch $healthfile + if [ ! -f "$healthfile" ]; then + touch "$healthfile" fi # append an X to the healthfile - echo -n "X" >> $healthfile + echo -n "X" >> "$healthfile" # run our "healthcheck" - cat $healthfile | grep -w "XXXXX" && rm -rf $healthfile + cat "$healthfile" | grep -w "XXXXX" && rm -rf "$healthfile" } main diff --git a/examples/networking/README.md b/examples/networking/README.md index 5b4e507e9..08b97334b 100644 --- a/examples/networking/README.md +++ b/examples/networking/README.md @@ -63,6 +63,14 @@ lando exec appserver_nginx -- curl https://appserver.landolamp.internal # Should even be able to connect to a database in a different app cd lamp lando exec database -- mysql -uroot -h database.landolemp.internal -e "quit" + +# Should have 32 networks by default +lando config | grep "networkLimit" | grep 32 + +# Should be able to set the networkLimit in config +cp config.yml ~/.lando/config.yml +lando --clear +lando config | grep "networkLimit" | grep 64 ``` ## Destroy tests diff --git a/examples/networking/config.yml b/examples/networking/config.yml new file mode 100644 index 000000000..558339625 --- /dev/null +++ b/examples/networking/config.yml @@ -0,0 +1 @@ +networkLimit: 64 diff --git a/examples/security/.lando.yml b/examples/security/.lando.yml index cc3678f5f..650847199 100644 --- a/examples/security/.lando.yml +++ b/examples/security/.lando.yml @@ -2,17 +2,18 @@ name: lando-security proxy: web: - web.lndo.site:8080 + web2: + - web2.lndo.site:8080 + web3: + - web3.lndo.site:8080 -services: - web: +x-web: + &default-web api: 4 image: imagefile: nginxinc/nginx-unprivileged:1.26.1 context: - ./default-ssl-3.conf:/etc/nginx/conf.d/default.conf - security: - ca: - - SoloCA.crt certs: cert: /frank/cert.crt key: /bob/key.key @@ -20,6 +21,54 @@ services: ports: - 8080/http - 8443/https + +services: + web: + << : *default-web + security: + ca: + - SoloCA.crt + web2: + << : *default-web + security: + ca: + - !load SoloCA.crt + web3: + << : *default-web + security: + ca: | + -----BEGIN CERTIFICATE----- + MIIFZDCCA0wCCQDKgPmqD5wcHzANBgkqhkiG9w0BAQsFADB0MQswCQYDVQQGEwJV + UzELMAkGA1UECAwCQ0ExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDzANBgNVBAoM + BlNvbG9yZzERMA8GA1UECwwIQ29yZWxsaWExHDAaBgNVBAMME1NvbG8gRGV2ZWxv + cG1lbnQgQ0EwHhcNMjQwNzI0MTIyMzExWhcNMzQwNzIyMTIyMzExWjB0MQswCQYD + VQQGEwJVUzELMAkGA1UECAwCQ0ExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDzAN + BgNVBAoMBlNvbG9yZzERMA8GA1UECwwIQ29yZWxsaWExHDAaBgNVBAMME1NvbG8g + RGV2ZWxvcG1lbnQgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCV + 5j3/rBOGfBbmdnS/gnkF9IubvlngTTDcEHrZcDsvf48DyhhEw9bhP6CDiaHYeUAe + LCwChZujehDOdkJJINMh894HlgVqMDoT6m3CCwAIdHbGKUVHHyGKh0iJmKPby/eP + fv6pcfN5RBAbuzp+CheD8d+Ja6WjDRqCgO8vCTm9fIlFRMbyYcva7rrGZSY5aslD + D26jJl5/DlvLBAhmlpXoQLH2sLc6ywV58/S1tfinDyeOXOdji8nOTRPcpCT6xY+8 + d8sFWL+TMzEiG24CBmVs6mmBRxGVkTaIsQgDB2nnNBVRaX6eu2Zw8wZQGYONZshg + gPqAHrIRPVkXfuxMygb0WrURKuFys6GyVGZ/iv0aj6UYK44AT0ygjfx3XUx7kgmV + Nc7vX5xFJYfyrM2W3Vg85N1qcSlIISie/tEA9ovgBSYkGAVA+ooOFVzB1zcpvBud + QxhauYFv/sshojrRgnM14I4xGxwqU+P2eZbdAZoyaAMjB6cdzBHTwhhHGUdEIS5x + R8U/sgLRcFZwqWH2ggAxa8uAEcBB137rSqYJmJZ51HXgG3pCM/9H44BfDS8P+6dg + cYKZqyqDMm5Vso6PUwwzL5BoYN+aPcBSpB3X1n3X/E4h7tzLnMeo2ri/4xPc/Var + P0mxKd11EKRjg6FlnxgVKFiqNBj3jAWXKPRlwCDIEwIDAQABMA0GCSqGSIb3DQEB + CwUAA4ICAQAooTHp89bhm/9Y+7jSUl6lyLdOBQm+dgKg4jUAyXtXFNi0niFj0coa + zwEXx00YB28ucsfSM5fATWnoM7oMO6fTZtXx9vlTCnJ5DcW/Yg8XgtLYiYDlwcQn + 1mb0FUwYofEYut3dXFMHKYuxc6qp1dQsoHhZP0WnJnSKTpBGm2AnXMHx1bVJ08CY + 5T56Wq+5P70+LNRPdBNT9A1UL1ey35NgW8wtT5nV6T/PkS3Isu0s4dMAm8xhYT9k + UwXkaWtBCDdFkpVLW2W2QvhM9yKNBbIz9oxpCg4Tj0hQWQpDck1yxxasCQ/wiiyI + /A3H+sY/W0L/vDKX8v4fbSc3fGqOOCJbAOxQA2tzUK26PI1vaIScXkyzmOmqw1ij + G/JfHf1ZsHpopCbhIvdBA/wp3iSyN6fN4KnC+3SUqtAXKXaEHHs7oSYbpfYTobRk + nUnNP7Hp9MCkToGHBDAK/J47Uz6UlYp3KR8YlR8mlItdvEmIEvf/A+cmy/ya9uz+ + Owp7Rl6tbmkLtu7s7OC3kaACsYoSXUHh/pMzhjV+mwQozOlp6kZfDv3aAAu0/6zg + AtcHpcb2kU2gzxz4DfucBMBsLkoqfP7ULS4hYLkJQ+A+zv9nCEYtfztFAcR05Hpu + fz9hmLfYUMztFjBqhK0oR7XGt+g9Jj/dTXcB4RMx+w44QNq/eKGo5A== + -----END CERTIFICATE----- + arch: api: 4 image: | diff --git a/examples/security/README.md b/examples/security/README.md index 3d993174a..6074a1bf4 100644 --- a/examples/security/README.md +++ b/examples/security/README.md @@ -23,6 +23,8 @@ Run the following commands to verify things work as expected lando certinfo | grep Issuer | grep "Lando Development CA" lando certinfo --service arch | grep Issuer | grep "Lando Development CA" lando certinfo --service fedora | grep Issuer | grep "Lando Development CA" +lando certinfo --service web2 | grep Issuer | grep "Lando Development CA" +lando certinfo --service web3 | grep Issuer | grep "Lando Development CA" # Should set the environment variables correctly lando exec arch -- env | grep LANDO_CA_DIR | grep /etc/ca-certificates/trust-source/anchors @@ -39,14 +41,26 @@ lando exec fedora -- "cat \$LANDO_CA_BUNDLE" | grep "Lando Development CA" lando exec fedora -- "cat \$LANDO_CA_BUNDLE" | grep "Solo Development CA" lando exec web -- "cat \$LANDO_CA_BUNDLE" lando exec web -- "ls -lsa \$LANDO_CA_DIR" | grep LandoCA.pem +lando exec web2 -- "cat \$LANDO_CA_BUNDLE" +lando exec web2 -- "ls -lsa \$LANDO_CA_DIR" | grep LandoCA.pem +lando exec web2 -- "ls -lsa \$LANDO_CA_DIR" | grep SoloCA.pem +lando exec web3 -- "cat \$LANDO_CA_BUNDLE" +lando exec web3 -- "ls -lsa \$LANDO_CA_DIR" | grep LandoCA.pem +lando exec web3 -- "ls -lsa \$LANDO_CA_DIR" | grep LandoCA- # Should use additional CAs if specified lando exec web -- "ls -lsa \$LANDO_CA_DIR" | grep SoloCA.crt +lando exec web2 -- "ls -lsa \$LANDO_CA_DIR" | grep SoloCA.crt +lando exec web3 -- "ls -lsa \$LANDO_CA_DIR" | grep LandoCA- lando exec fedora -- "ls -lsa \$LANDO_CA_DIR" | grep SoloCA.crt # Should trust CA signed web traffic on host and in container curl https://web.lndo.site +curl https://web2.lndo.site +curl https://web3.lndo.site lando exec web -- curl https://localhost:8443 +lando exec web2 -- curl https://localhost:8443 +lando exec web3 -- curl https://localhost:8443 # Should have the correct cert issuer if LANDO_CA_CERT and LANDO_CA_KEY are set differently LANDO_CA_CERT="$(pwd)/SoloCA.crt" LANDO_CA_KEY="$(pwd)/SoloCA.key" lando config --path caCert | grep "SoloCA.crt" diff --git a/examples/tooling/.lando.yml b/examples/tooling/.lando.yml index 6e5ab1a05..54ec51180 100644 --- a/examples/tooling/.lando.yml +++ b/examples/tooling/.lando.yml @@ -135,6 +135,56 @@ tooling: message: What is the word? default: bird weight: 600 + word-imported: + service: :service + cmd: !import word.sh + level: app + options: + service: + default: web + alias: + - s + describe: Runs in this service + word: + passthrough: true + alias: + - w + describe: Print what the word is + interactive: + type: input + message: What is the word? + default: bird + weight: 600 + all-the-words: + cmd: + - web: !import word.sh + - lando4: | + /app/word.sh + - l337-node: /app/word.sh + + word-wrapped: + service: :service + cmd: | + #!/bin/bash + /app/word.sh "$@" + level: engine + options: + service: + default: web + alias: + - s + describe: Runs in this service + word: + passthrough: true + alias: + - w + describe: Print what the word is + interactive: + type: input + message: What is the word? + default: bird + weight: 600 + word-engine [word]: service: :service cmd: /app/word-engine.sh diff --git a/examples/tooling/README.md b/examples/tooling/README.md index e8991e604..188f5aaa4 100644 --- a/examples/tooling/README.md +++ b/examples/tooling/README.md @@ -61,6 +61,19 @@ lando word-engine --random 1 --service l337-node gird | grep "gird is the word" lando word-engine --random 1 --service l337-node "this is actually a phrase" | grep "this is actually a phrase" lando lonely-bird +# Should be able to run multiline tooling commands on all sapis +lando word-imported --word larrybird | grep "larrybird is the word" +lando word-imported --service l337-node --word larrybird | grep "larrybird is the word" +lando word-imported --service lando4 --word larrybird | grep "larrybird is the word" + +# Should be able to use multiline wrapped scripts on all sapis +lando word-wrapped --word larrybird | grep "larrybird is the word" +lando word-wrapped --service l337-node --word larrybird | grep "larrybird is the word" +lando word-wrapped --service lando4 --word larrybird | grep "larrybird is the word" + +# Should be able run mutliple multiline command on multiple services regardless of sapi +lando all-the-words | wc -l | grep 3 + # Should be able to run multiple commands on multiple services lando env diff --git a/hooks/app-add-healthchecks.js b/hooks/app-add-healthchecks.js index fd711ec87..4694865a2 100644 --- a/hooks/app-add-healthchecks.js +++ b/hooks/app-add-healthchecks.js @@ -2,12 +2,20 @@ const _ = require('lodash'); const debug = require('debug')('@lando/core:healthcheck'); +const isStringy = require('../utils/is-stringy'); const {color} = require('listr2'); module.exports = async app => { - const exec = (command, container, {service, log = debug, user = 'root'} = {}) => { + const exec = (command, container, {api = 3, service, log = debug, user = 'root'} = {}) => { log('running %o healthcheck %o...', service, command); + + // if is stringy then multipass + if (isStringy(command)) { + command = Buffer.from(command, 'utf8').toString('base64'); + command = api === 3 ? ['/helpers/exec-multiliner.sh', command] : ['/etc/lando/exec-multiliner.sh', command]; + } + return app.engine.run({ id: container, cmd: command, @@ -49,6 +57,7 @@ module.exports = async app => { const newV3Healthchecks = _(_.get(app, 'parsedV3Services', [])) .filter(service => _.has(service, 'healthcheck')) .map(service => ({ + api: 3, container: app.containers[service.name], name: service.name, service: service.name, @@ -59,6 +68,7 @@ module.exports = async app => { .filter(service => _.has(service, 'healthcheck')) .filter(service => service.canHealthcheck) .map(service => ({ + api: 4, container: app.containers[service.name], name: service.name, service: service.name, @@ -87,6 +97,7 @@ module.exports = async app => { retry: healthcheck.retry, title: healthcheck.service, args: [healthcheck.command, healthcheck.container, { + api: healthcheck.api, log: app.log.debug, service: healthcheck.service, user: healthcheck.user, diff --git a/hooks/app-override-tooling-defaults.js b/hooks/app-override-tooling-defaults.js index 2818fef47..6184a4a16 100644 --- a/hooks/app-override-tooling-defaults.js +++ b/hooks/app-override-tooling-defaults.js @@ -4,7 +4,6 @@ const merge = require('lodash/merge'); module.exports = async (app, lando) => { for (const task of lando.tasks.filter(task => task.override)) { - app.log.debug('overriding task %s with dynamic app options', task.command); app._coreToolingOverrides = merge({}, app._coreToolingOverrides, { [task.command]: {...require(task.file)(lando, app), file: task.file}, }); diff --git a/hooks/lando-clean-networks.js b/hooks/lando-clean-networks.js index f5a001d35..f487e5440 100644 --- a/hooks/lando-clean-networks.js +++ b/hooks/lando-clean-networks.js @@ -4,7 +4,7 @@ const _ = require('lodash'); module.exports = async lando => lando.engine.getNetworks().then(networks => { - if (_.size(networks) >= 32) { + if (_.size(networks) >= lando.config.networkLimit) { // Warn user about this action lando.log.warn('Lando has detected you are at Docker\'s network limit!'); lando.log.warn('Give us a moment as we try to make space by cleaning up old networks...'); diff --git a/index.js b/index.js index 11cc01e7b..090bed015 100644 --- a/index.js +++ b/index.js @@ -162,6 +162,7 @@ module.exports = async lando => { caKey, maxKeyWarning: 10, networkBridge: 'lando_bridge_network', + networkLimit: 32, proxyBindAddress: _.get(lando, 'config.bindAddress', '127.0.0.1'), proxyDomain: lando.config.domain, proxyIp: _.get(lando.config, 'engineConfig.host', '127.0.0.1'), diff --git a/lib/app.js b/lib/app.js index 327f1a916..f1dc77a76 100644 --- a/lib/app.js +++ b/lib/app.js @@ -298,8 +298,9 @@ module.exports = class App { .then(() => { // Get all the services this.services = require('../utils/get-app-services')(this.composeData); - // add a double property because we have weird differnces between cli getApp and core getApp + // add double properties because we have weird differnces between cli getApp and core getApp this.allServices = this.services; + this.sapis = require('../utils/get-service-apis')(this); // Merge whatever we have thus far together this.info = require('../utils/get-app-info-defaults')(this); // finally just make a list of containers @@ -340,6 +341,7 @@ module.exports = class App { this.serviceGroups['4'] = _.get(this, 'serviceGroups.4', []); delete this.serviceGroups.undefined; + // accomodate a higher service API if we can if (this.serviceGroups.compose.length === 0 && this.serviceGroups['3'].length === 0 diff --git a/lib/router.js b/lib/router.js index ca844fbf9..c8c314868 100644 --- a/lib/router.js +++ b/lib/router.js @@ -76,8 +76,10 @@ exports.run = (data, compose, docker, started = true) => Promise.mapSeries(norma // Merge in default cli envars datum.opts.environment = require('../utils/get-cli-env')(datum.opts.environment); datum.kill = true; + // Escape command if it is still a string if (_.isString(datum.cmd)) datum.cmd = require('../utils/shell-escape')(datum.cmd, true); + return docker.isRunning(getContainerId(datum)).then(isRunning => { started = isRunning; if (!isRunning) { diff --git a/packages/security/security.js b/packages/security/security.js index 1799a32a3..31cabf2a0 100644 --- a/packages/security/security.js +++ b/packages/security/security.js @@ -2,13 +2,30 @@ const fs = require('fs'); const path = require('path'); +const isStringy = require('../../utils/is-stringy'); + +const {nanoid} = require('nanoid'); module.exports = async (service, security) => { // right now this is mostly just CA setup, lets munge it all together and normalize and whatever const cas = [security.ca, security.cas, security['certificate-authority'], security['certificate-authorities']] .flat(Number.POSITIVE_INFINITY) - .filter(cert => fs.existsSync(cert)) - .map(cert => path.isAbsolute(cert) ? cert : path.resolve(service.appRoot, cert)); + .filter(cert => isStringy(cert)) + .map(cert => { + // if ImportString then just return the filename + if (cert?.constructor?.name === 'ImportString') { + const {file} = cert.getMetadata(); + cert = file; + } + + // if a single liner then resolve the path + if (cert.split('\n').length === 1) { + cert = path.resolve(service.appRoot, cert); + } + + return cert; + }) + .filter(cert => cert.split('\n').length > 1 || fs.existsSync(cert)); // add ca-cert install hook if we have some to add if (cas.length > 0) { @@ -16,5 +33,8 @@ module.exports = async (service, security) => { } // inject them - for (const ca of cas) service.addLSF(ca, `ca-certificates/${path.basename(ca)}`); + for (const ca of cas) { + const file = ca.split('\n').length > 1 ? `LandoCA-${nanoid()}.crt` : path.basename(ca); + service.addLSF(ca, `ca-certificates/${file}`); + } }; diff --git a/release-aliases/3-STABLE b/release-aliases/3-STABLE index ba4bef1ab..71f6f0f88 100644 --- a/release-aliases/3-STABLE +++ b/release-aliases/3-STABLE @@ -1 +1 @@ -v3.23.21 +v3.23.22 diff --git a/scripts/exec-multiliner.sh b/scripts/exec-multiliner.sh new file mode 100755 index 000000000..cf4805f4a --- /dev/null +++ b/scripts/exec-multiliner.sh @@ -0,0 +1,32 @@ +#!/bin/sh + +# ensure at least one argument is provided +if [ $# -lt 1 ]; then + echo "Usage: $0 [args...]" + exit 1 +fi + +# assign the first argument to ENCODED_SCRIPT and remove it from the arguments list +ENCODED_SCRIPT_CONTENTS="$1" +shift + +# create a temporary file with a unique name in /tmp and a .sh extension +SCRIPT=$(mktemp /tmp/lando.XXXXXX.sh || mktemp) + +# decode the Base64 script and write it to the temporary file +echo "$ENCODED_SCRIPT_CONTENTS" | base64 -d > "$SCRIPT" +if [ $? -ne 0 ]; then + echo "Error: Failed to decode the script." + rm -f "$SCRIPT" + exit 1 +fi + +# make the temporary script executable +chmod +x "$SCRIPT" + +# execute the decoded script with the remaining arguments +if [ -f "/etc/lando/exec.sh" ]; then + /etc/lando/exec.sh "$SCRIPT" "$@" +else + "$SCRIPT" "$@" +fi diff --git a/tasks/exec.js b/tasks/exec.js index 89385aaae..4844b5344 100644 --- a/tasks/exec.js +++ b/tasks/exec.js @@ -99,7 +99,7 @@ module.exports = (lando, config = lando.appConfig) => ({ } // if this service has /etc/lando/exec then prepend - if (app?.executors?.[options.service]) options.command.unshift('/etc/lando/exec.sh'); + if (app?.sapis?.[options.service] === 4) options.command.unshift('/etc/lando/exec.sh'); // spoof options we can pass into build tooling runner const ropts = [ diff --git a/utils/build-tooling-task.js b/utils/build-tooling-task.js index bf808dcf3..089a07067 100644 --- a/utils/build-tooling-task.js +++ b/utils/build-tooling-task.js @@ -11,9 +11,8 @@ module.exports = (config, injected) => { env.DEBUG = injected.debuggy ? '1' : ''; env.LANDO_DEBUG = injected.debuggy ? '1' : ''; - // service api 4 services that canExec - const canExec = Object.fromEntries((config?.app?.info ?? []) - .map(service => ([service.service, config?.app?.executors?.[service.service] ?? false]))); + // get the service api + const sapis = config?.app?.sapis ?? {}; // Handle dynamic services and passthrough options right away // Get the event name handler @@ -22,7 +21,7 @@ module.exports = (config, injected) => { // Kick off the pre event wrappers .then(() => app.events.emit(`pre-${eventName}`, config, answers)) // Get an interable of our commandz - .then(() => _.map(require('./parse-tooling-config')(cmd, service, options, answers, canExec))) + .then(() => _.map(require('./parse-tooling-config')(cmd, service, options, answers, sapis))) // Build run objects .map(({command, service}) => require('./build-tooling-runner')(app, command, service, user, env, dir, appMount)) // Try to run the task quickly first and then fallback to compose launch diff --git a/utils/debug-shim.js b/utils/debug-shim.js index 13726e0e7..b7239e2d1 100644 --- a/utils/debug-shim.js +++ b/utils/debug-shim.js @@ -1,5 +1,7 @@ 'use strict'; +const _debug = require('debug'); + const Log = require('./../lib/logger'); // adds required methods to ensure the lando v3 debugger can be injected into v4 things @@ -12,11 +14,41 @@ module.exports = (log, {namespace} = {}) => { fresh.alsoSanitize(/_password$/); fresh.alsoSanitize('forceAuth'); + // spoofer + const spoofer = _debug(namespace ?? 'lando'); + spoofer.diff = 0; + // we need to start with the function itself and then augment it - const debug = fresh.debug; + const debug = (...args) => { + args[0] = _debug.coerce(args[0]); + + if (typeof args[0] !== 'string') args.unshift('%O'); + + // Apply any `formatters` transformations + let index = 0; + args[0] = args[0].replace(/%([a-zA-Z%])/g, (match, format) => { + // If we encounter an escaped % then don't increase the array index + if (match === '%%') { + return '%'; + } + index++; + const formatter = _debug.formatters[format]; + if (typeof formatter === 'function') { + const val = args[index]; + match = formatter.call(spoofer, val); + + args.splice(index, 1); + index--; + } + return match; + }); + + fresh.debug(...args); + }; + // contract and replace should do nothing - debug.contract = () => fresh.debug; - debug.replace = () => fresh.debug; + debug.contract = () => debug; + debug.replace = () => debug; // extend should just return a new logger debug.extend = name => module.exports(log, {namespace: name}); diff --git a/utils/filter-v3-build-steps.js b/utils/filter-v3-build-steps.js index b722a5bb7..fc3a2ca7e 100644 --- a/utils/filter-v3-build-steps.js +++ b/utils/filter-v3-build-steps.js @@ -16,9 +16,13 @@ module.exports = (services, app, rootSteps = [], buildSteps= [], prestart = fals if (!_.isEmpty(_.get(app, `config.services.${service}.${section}`, []))) { // Run each command _.forEach(app.config.services[service][section], cmd => { + // if array then just join it together + // @NOTE: this cant possibly work correctly in many situations? + if (_.isArray(cmd)) cmd = cmd.join(' '); + build.push({ id: app.containers[service], - cmd: ['/bin/sh', '-c', _.isArray(cmd) ? cmd.join(' ') : cmd], + cmd: ['/helpers/exec-multiliner.sh', Buffer.from(cmd, 'utf8').toString('base64')], compose: app.compose, project: app.project, opts: { diff --git a/utils/get-executors.js b/utils/get-executors.js deleted file mode 100644 index 14e912cac..000000000 --- a/utils/get-executors.js +++ /dev/null @@ -1,9 +0,0 @@ -'use strict'; - -const _ = require('lodash'); - -module.exports = (services = {}) => _(services) - .map((service, id) => _.merge({}, {id}, service)) - .map(service => ([service.id, service.canExec ?? false])) - .fromPairs() - .value(); diff --git a/utils/get-service-apis.js b/utils/get-service-apis.js new file mode 100644 index 000000000..eca754fb4 --- /dev/null +++ b/utils/get-service-apis.js @@ -0,0 +1,16 @@ +'use strict'; + +const _ = require('lodash'); + +module.exports = app => { + const v4 = _.get(app, 'v4.services', []); + const v3 = _.get(app, 'parsedV3Services', []); + + return _([...v3, ...v4]) + .map(service => { + if (service.api === 4 && service.type === 'l337') return [service.name, 'l337']; + return [service.name, service.api]; + }) + .fromPairs() + .value(); +}; diff --git a/utils/get-tasks.js b/utils/get-tasks.js index 9a8a25ada..3ae593808 100644 --- a/utils/get-tasks.js +++ b/utils/get-tasks.js @@ -164,6 +164,7 @@ module.exports = (config = {}, argv = {}, tasks = []) => { config.allServices = composeCache.allServices ?? []; config.info = composeCache.info ?? []; config.primary = composeCache.primary ?? 'appserver'; + config.sapis = composeCache.sapis ?? {}; } catch (e) { throw new Error(`There was a problem with parsing ${config.composeCache}. Ensure it is valid JSON! ${e}`); } diff --git a/utils/is-stringy.js b/utils/is-stringy.js new file mode 100644 index 000000000..579b03955 --- /dev/null +++ b/utils/is-stringy.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = data => typeof data === 'string' || data?.constructor?.name == 'ImportString'; diff --git a/utils/normalize-healthcheck.js b/utils/normalize-healthcheck.js index a026f9d06..1542cfb97 100644 --- a/utils/normalize-healthcheck.js +++ b/utils/normalize-healthcheck.js @@ -7,6 +7,8 @@ module.exports = healthcheck => { if (typeof healthcheck === 'string') healthcheck = {command: healthcheck}; // ditto if its an array else if (Array.isArray(healthcheck)) healthcheck = {command: healthcheck}; + // ditto if its an import string + else if (healthcheck?.constructor?.name === 'ImportString') healthcheck = {command: healthcheck}; // allow cmd shorthand if (!healthcheck.command && healthcheck.cmd) healthcheck.command = healthcheck.cmd; // merge in defaults and return diff --git a/utils/parse-events-config.js b/utils/parse-events-config.js index 412d182e4..017fd3b62 100644 --- a/utils/parse-events-config.js +++ b/utils/parse-events-config.js @@ -3,6 +3,8 @@ // Modules const _ = require('lodash'); +const {nanoid} = require('nanoid'); + // Helper to find the default service const getDefaultService = (data = {}, defaultService = 'appserver') => { // if this is an event built on a service-dynamic command @@ -36,11 +38,37 @@ const getService = (cmd, data = {}, defaultService = 'appserver') => { // adds required methods to ensure the lando v3 debugger can be injected into v4 things module.exports = (cmds, app, data = {}) => _.map(cmds, cmd => { // Discover the service - const command = getCommand(cmd); const service = getService(cmd, data, app._defaultService); // compute stdio based on compose major version const cstdio = _.get(app, '_config.orchestratorMV', 2) ? 'inherit' : ['inherit', 'pipe', 'pipe']; + // attempt to ascertain the SAPI + const sapi = _.get(app, 'v4.services', []).find(s => s.id === service)?.api + ?? _.get(app, `sapis.${service}`, undefined) + ?? _.get(data, `sapis.${service}`, undefined); + + // normalize cmd + cmd = getCommand(cmd); + + // if array then just join it together + if (_.isArray(cmd)) cmd = cmd.join(' '); + + // lando 4 services + // @NOTE: lando 4 service events will change once we have a complete hook system + if (sapi === 4) { + cmd = ['/etc/lando/exec-multiliner.sh', Buffer.from(cmd, 'utf8').toString('base64')]; + + // lando 3 + } else if (sapi === 3) { + cmd = ['/helpers/exec-multiliner.sh', Buffer.from(cmd, 'utf8').toString('base64')]; + + // this should be all "compose-y" services + } else { + const file = `/tmp/${nanoid()}.sh`; + const script = Buffer.from(cmd, 'utf8').toString('base64'); + cmd = `echo ${script} | base64 -d > ${file} && chmod +x ${file} && ${file}`; + } + // try to get a list of v4 services a few ways, we have to look at different places because the event could // run at various points in the bootstrap const v4s = _([ @@ -48,20 +76,6 @@ module.exports = (cmds, app, data = {}) => _.map(cmds, cmd => { _.get(app, 'v4.servicesList', []), ]).flatten().compact().uniq().value(); - // attempt to ascertain whether this is a v4 "exec" service - const canExec = _.get(app, 'v4.services', []).find(s => s.id === service)?.canExec - ?? _.get(app, `executors.${service}`, undefined) - ?? _.get(data, `executors.${service}`, undefined) - ?? false; - - // reset the cmd based on exec situation - // @TODO: replace this with better command scripting stuff when command scripting is done? - if (canExec) { - cmd = ['/etc/lando/exec.sh', 'sh', '-c', _.isArray(command) ? command.join(' ') : command]; - } else { - cmd = ['/bin/sh', '-c', _.isArray(command) ? command.join(' ') : command]; - } - // Validate the service if we can // @NOTE fast engine runs might not have this data yet if ( diff --git a/utils/parse-tooling-config.js b/utils/parse-tooling-config.js index 9d79a6272..e3e21cddd 100644 --- a/utils/parse-tooling-config.js +++ b/utils/parse-tooling-config.js @@ -2,6 +2,7 @@ // Modules const _ = require('lodash'); +const isStringy = require('./is-stringy'); /* * Helper to get dynamic service keys for stripping @@ -19,7 +20,7 @@ const getDynamicKeys = (answer, answers = {}) => _(answers) * Set SERVICE from answers and strip out that noise from the rest of * stuff, check answers/argv for --service or -s, validate and then remove */ -const handleDynamic = (config, options, answers = {}, execs = {}) => { +const handleDynamic = (config, options, answers = {}, sapis = {}) => { if (_.startsWith(config.service, ':')) { const answer = answers[config.service.split(':')[1]]; // Remove dynamic service option from argv @@ -27,7 +28,7 @@ const handleDynamic = (config, options, answers = {}, execs = {}) => { // get the service const service = answers[config.service.split(':')[1]]; // Return updated config - return _.merge({}, config, {exec: execs[service] ?? false, service}); + return _.merge({}, config, {sapi: sapis[service] ?? false, service}); } else { return config; } @@ -61,23 +62,28 @@ const handlePassthruOpts = (options = {}, answers = {}) => _(options) /* * Helper to convert a command into config object */ -const parseCommand = (cmd, service, execs) => ({ - exec: execs[service] ?? false, - command: (_.isObject(cmd)) ? cmd[_.first(_.keys(cmd))] : cmd, - service: (_.isObject(cmd)) ? _.first(_.keys(cmd)) : service, -}); +const parseCommand = (cmd, service, sapis) => { + const command = (_.isObject(cmd) && !isStringy(cmd)) ? cmd[_.first(_.keys(cmd))] : cmd; + service = (_.isObject(cmd) && !isStringy(cmd)) ? _.first(_.keys(cmd)) : service; + + return { + command, + sapi: sapis[service] ?? undefined, + service, + }; +}; // adds required methods to ensure the lando v3 debugger can be injected into v4 things -module.exports = (cmd, service, options = {}, answers = {}, execs = {}) => _(cmd) +module.exports = (cmd, service, options = {}, answers = {}, sapis = {}) => _(cmd) // Put into an object so we can handle "multi-service" tooling - .map(cmd => parseCommand(cmd, service, execs)) + .map(cmd => parseCommand(cmd, service, sapis)) // Handle dynamic services - .map(config => handleDynamic(config, options, answers, execs)) + .map(config => handleDynamic(config, options, answers, sapis)) // Add in any argv extras if they've been passed in .map(config => handleOpts(config, handlePassthruOpts(options, answers))) // Wrap the command in /bin/sh if that makes sense - .map(config => _.merge({}, config, {command: require('./shell-escape')(config.command, true, config.args, config.exec)})) // eslint-disable-line max-len + .map(config => ({...config, command: require('./shell-escape')(config.command, true, config.args, config.sapi)})) // Add any args to the command and compact to remove undefined - .map(config => _.merge({}, config, {command: _.compact(config.command.concat(config.args))})) + .map(config => ({...config, command: _.compact(config.command.concat(config.args))})) // Put into an object .value(); diff --git a/utils/run-command.js b/utils/run-command.js index 9fd2ae04e..a50c73db4 100644 --- a/utils/run-command.js +++ b/utils/run-command.js @@ -4,6 +4,7 @@ // Modules const merge = require('lodash/merge'); +const {color} = require('listr2'); const {spawn} = require('child_process'); @@ -21,28 +22,29 @@ module.exports = (command, args = [], options = {}, stdout = '', stderr = '') => const {debug} = options; // birth - debug('running command %o %o', command, args); const child = spawn(command, args, options); + debug('running command pid=%o %o %o', child.pid, command, args); + return require('./merge-promise')(child, async () => { return new Promise((resolve, reject) => { child.on('error', error => { - debug('command %o error %o', command, error?.message); + debug('command pid=$o %o error %o', child.pid, command, error?.message); stderr += error?.message ?? error; }); child.stdout.on('data', data => { - debug('stdout %o', data.toString().trim()); + debug('stdout %s', color.dim(data.toString().trim())); stdout += data; }); child.stderr.on('data', data => { - debug('stderr %o', data.toString().trim()); + debug('stderr %s', color.dim(data.toString().trim())); stderr += data; }); child.on('close', code => { - debug('command %o done with code %o', command, code); + debug('command pid=%o %o done with code %o', child.pid, command, code); // if code is non-zero and we arent ignoring then reject here if (code !== 0 && !options.ignoreReturnCode) { const error = new Error(stderr); diff --git a/utils/run-powershell-script.js b/utils/run-powershell-script.js index 820ee8977..dfbdb9dd0 100644 --- a/utils/run-powershell-script.js +++ b/utils/run-powershell-script.js @@ -24,9 +24,9 @@ module.exports = (script, args = [], options = {}, stdout = '', stderr = '', car // if encode is not explicitly set then we need to pick a good value if (options.encode === undefined) { - const bargs = ['Set-ExecutionPolicy', '-Scope', 'UserPolicy', '-ExecutionPolicy', 'Bypass']; + const bargs = ['Set-ExecutionPolicy', '-Scope', 'Process', '-ExecutionPolicy', 'Bypass']; const {status} = spawnSync('powershell.exe', bargs, options); - options.encode === status !== 0; + options.encode = status !== 0; } // if encode is true we need to do a bunch of other stuff diff --git a/utils/shell-escape.js b/utils/shell-escape.js index c0c7496f0..30354ae39 100644 --- a/utils/shell-escape.js +++ b/utils/shell-escape.js @@ -1,10 +1,29 @@ 'use strict'; const _ = require('lodash'); +const isStringy = require('./is-stringy'); +const {nanoid} = require('nanoid'); + +module.exports = (command, wrap = false, args = process.argv.slice(3), sapi = 3) => { + // if stringy and multiline and + if (isStringy(command) && command.split('\n').length > 1) { + // prep for multipass + const script = Buffer.from(command, 'utf8').toString('base64'); + // different strokes + if (sapi === 4) return ['/etc/lando/exec-multiliner.sh', script, ...args]; + else if (sapi === 3) return ['/helpers/exec-multiliner.sh', script, ...args]; + else { + const file = `/tmp/${nanoid()}.sh`; + return [ + '/bin/sh', + '-c', + `echo ${script} | base64 -d > ${file} && chmod +x ${file} && ${file} ${args.join(' ')}`, + ]; + } + } -module.exports = (command, wrap = false, args = process.argv.slice(3), v4Exec = false) => { // if api 4 then just prepend and we will handle it downstream - if (v4Exec) { + if (sapi === 4) { if (_.isString(command)) command = require('string-argv')(command); return ['/etc/lando/exec.sh', ...command]; } diff --git a/utils/write-file.js b/utils/write-file.js index 83c305d34..5d79e22ac 100644 --- a/utils/write-file.js +++ b/utils/write-file.js @@ -7,15 +7,14 @@ const path = require('path'); // @TODO: maybe extension should be in {options}? // @TODO: error handling, defaults etc? module.exports = (file, data, options = {}) => { - // if data is an ImportString coming from !import in a yaml file then toString it - if (data?.constructor?.name === 'ImportString') data = data.toString(); - // set extension if not set const extension = options.extension || path.extname(file); - // linux line endings const forcePosixLineEndings = options.forcePosixLineEndings ?? false; + // special handling for ImportString + if (typeof data !== 'string' && data?.constructor?.name === 'ImportString') data = data.toString(); + // data is a string and posixOnly then replace if (typeof data === 'string' && forcePosixLineEndings) data = data.replace(/\r\n/g, '\n');