From cedb8ce5f30449712e87e40fd383ff4fb6853c21 Mon Sep 17 00:00:00 2001 From: Florian Patruck Date: Fri, 1 Nov 2024 17:54:06 +0100 Subject: [PATCH 01/47] fix(compose): Fix compose file path resolution as it is copied into the lando directory and be able to set app mount user --- builders/_lando.js | 19 +++++++++++++++---- builders/lando-compose.js | 21 +++++++++++++++++++++ hooks/app-start-proxy.js | 7 +++---- lib/app.js | 32 ++++++++++++++++++++++---------- lib/compose.js | 7 +++++++ lib/engine.js | 20 ++++++++++++++++++++ lib/router.js | 2 ++ test/compose.spec.js | 12 ++++++++++++ test/get-user.spec.js | 5 +++++ utils/build-tooling-task.js | 2 +- utils/get-app-mounts.js | 2 +- utils/get-user.js | 4 ++-- utils/load-compose-files.js | 30 +++++++++++++++++++++++++++--- utils/parse-tooling-config.js | 8 ++++---- utils/parse-v3-services.js | 2 +- utils/parse-v4-services.js | 1 + 16 files changed, 144 insertions(+), 30 deletions(-) create mode 100644 builders/lando-compose.js diff --git a/builders/_lando.js b/builders/_lando.js index 3ef39dc51..fac0d9218 100644 --- a/builders/_lando.js +++ b/builders/_lando.js @@ -53,6 +53,8 @@ module.exports = { supportedIgnore = false, root = '', // webroot = '/app', + _app = null, + appMount = '/app', } = {}, ...sources ) { @@ -98,6 +100,9 @@ module.exports = { const environment = { LANDO_SERVICE_NAME: name, LANDO_SERVICE_TYPE: type, + LANDO_WEBROOT_USER: meUser, + LANDO_WEBROOT_GROUP: meUser, + LANDO_MOUNT: appMount, }; // Handle labels @@ -113,7 +118,6 @@ module.exports = { `${userConfRoot}:/lando:cached`, `${globalScriptsDir}:/helpers`, `${entrypointScript}:/lando-entrypoint.sh`, - `${dataHome}:/var/www`, ]; // add in service helpers if we have them @@ -178,9 +182,16 @@ module.exports = { // Add named volumes and other thingz into our primary service const namedVols = {}; - _.set(namedVols, data, {}); - _.set(namedVols, dataHome, {}); - + if (null !== data) { + _.set(namedVols, data, {}); + } + if (null !== dataHome) { + _.set(namedVols, dataHome, {}); + volumes.push(`${dataHome}:/var/www`); + } + if (null === entrypoint) { + entrypoint = undefined; + } sources.push({ services: _.set({}, name, { entrypoint, diff --git a/builders/lando-compose.js b/builders/lando-compose.js new file mode 100644 index 000000000..736f19bc8 --- /dev/null +++ b/builders/lando-compose.js @@ -0,0 +1,21 @@ +'use strict'; + +const _ = require('lodash'); + +module.exports = { + name: 'lando-compose', + api: 3, + parent: '_lando', + builder: parent => class LandoComposeServiceV3 extends parent { + constructor(id, options = {}) { + super(id, _.merge({}, { + entrypoint: null, // NOTE: Do not overwrite the entrypoint from docker compose. Or should we? + data: null, // NOTE: Do not create the data volume + dataHome: null, // NOTE: Do not create the dataHome volume + appMount: '/', + sslExpose: false, + ssl: true, + }, options)); + } + }, +}; diff --git a/hooks/app-start-proxy.js b/hooks/app-start-proxy.js index 3db065cfb..f8c6fd83d 100644 --- a/hooks/app-start-proxy.js +++ b/hooks/app-start-proxy.js @@ -281,10 +281,9 @@ module.exports = async (app, lando) => { } // Get list of services that *should* have certs for SSL - const sslReady = _(_.get(app, 'config.services', [])) - .map((data, name) => _.merge({}, data, {name})) - .filter(data => data.ssl) - .map(data => data.name) + const sslReady = _(app.info) + .filter(data => data?.hasCerts) + .map(data => data.service) .value(); // Make sure we augment ssl ready if we have served by candidates diff --git a/lib/app.js b/lib/app.js index 5f4f224ab..c6556b9a1 100644 --- a/lib/app.js +++ b/lib/app.js @@ -273,13 +273,23 @@ module.exports = class App { // We should only need to initialize once, if we have just go right to app ready if (this.initialized) return this.events.emit('ready', this); // Get compose data if we have any, otherwise set to [] - const composeFiles = require('../utils/load-compose-files')(_.get(this, 'config.compose', []), this.root); - this.composeData = [new this.ComposeService('compose', {}, ...composeFiles)]; - // Validate and set env files - this.envFiles = require('../utils/normalize-files')(_.get(this, 'config.env_file', []), this.root); - // Log some things - this.log.verbose('initiatilizing app at %s...', this.root); - + return require('../utils/load-compose-files')( + _.get(this, 'config.compose', []), + this.root, + this._dir, + (composeFiles, outputFilePath) => + this.engine.getComposeConfig({compose: composeFiles, project: this.project, outputFilePath}), + ) + .then(composeFileData => { + if (undefined !== composeFileData) { + this.composeData = [new this.ComposeService('compose', {}, composeFileData)]; + } + // Validate and set env files + this.envFiles = require('../utils/normalize-files')(_.get(this, 'config.env_file', []), this.root); + // Log some things + this.log.verbose('initiatilizing app at %s...', this.root); + this.log.silly('app has config', this.config); + }) /** * Event that allows altering of the app object right before it is * initialized. @@ -292,8 +302,9 @@ module.exports = class App { * @event pre_init * @property {App} app The app instance. */ - return loadPlugins(this, this._lando).then(() => this.events.emit('pre-init', this)) + .then(() => loadPlugins(this, this._lando)) + .then(() => this.events.emit('pre-init', this)) // Actually assemble this thing so its ready for that engine .then(() => { // Get all the services @@ -364,7 +375,7 @@ module.exports = class App { } // Log - this.initialized = true; + this.initialized = !!noEngine; this.log.verbose('app is ready!'); }) /** @@ -378,7 +389,8 @@ module.exports = class App { .then(() => this.events.emit('ready', this)) // @NOTE: dont ask, just continuing to work around v3-wasnt-intended-to-do-this problems - .then(() => noEngine === true ? undefined : this.events.emit('ready-engine', this)); + .then(() => noEngine === true ? undefined : this.events.emit('ready-engine', this)) + .then(() => noEngine === true ? require('../hooks/app-purge-compose-cache')(this, this._lando) : undefined); } /** diff --git a/lib/compose.js b/lib/compose.js index 34cc67c18..080d8a738 100644 --- a/lib/compose.js +++ b/lib/compose.js @@ -20,6 +20,7 @@ const composeFlags = { rm: '--rm', timestamps: '--timestamps', volumes: '-v', + outputFilePath: '-o', }; // Default options nad things @@ -33,6 +34,7 @@ const defaultOptions = { pull: {}, rm: {force: true, volumes: true}, up: {background: true, noRecreate: true, recreate: false, removeOrphans: true}, + config: {}, }; /* @@ -155,3 +157,8 @@ exports.start = (compose, project, opts = {}) => buildShell('up', project, compo * Run docker compose stop */ exports.stop = (compose, project, opts = {}) => buildShell('stop', project, compose, opts); + +/* + * Run docker compose config + */ +exports.config = (compose, project, opts = {}) => buildShell('config', project, compose, opts); diff --git a/lib/engine.js b/lib/engine.js index b86c919b4..9f869fcfa 100644 --- a/lib/engine.js +++ b/lib/engine.js @@ -495,5 +495,25 @@ module.exports = class Engine { // stop return this.engineCmd('stop', data); } + + /** + * Get dumped docker compose config for compose files from project + * using a `compose` object with `{compose: compose, project: project, opts: opts}` + * + * @since 3.0.0 + * @param {Object} data Config needs a service within a compose context + * @param {Array} data.compose An Array of paths to Docker compose files + * @param {String} data.project A String of the project name (Usually this is the same as the app name) + * @param {String} [data.outputFilePath='/path/to/file.yml'] String to output path + * @param {Object} [data.opts] Options + * @return {Promise} A Promise. + * @example + * return lando.engine.stop(app); + */ + getComposeConfig(data) { + data.opts = {cmd: ['-o', data.outputFilePath]}; + delete data.outputFilePath; + return this.engineCmd('config', data); + } }; diff --git a/lib/router.js b/lib/router.js index c8c314868..aea5e74ce 100644 --- a/lib/router.js +++ b/lib/router.js @@ -135,3 +135,5 @@ exports.start = (data, compose) => retryEach(data, datum => compose('start', dat exports.stop = (data, compose, docker) => retryEach(data, datum => { return (datum.compose) ? compose(data.kill ? 'kill' : 'stop', datum) : docker.stop(getContainerId(datum)); }); + +exports.config = (data, compose) => retryEach(data, datum => compose('config', datum)); diff --git a/test/compose.spec.js b/test/compose.spec.js index d282600b0..0b599cdc9 100644 --- a/test/compose.spec.js +++ b/test/compose.spec.js @@ -202,4 +202,16 @@ describe('compose', () => { expect(stopResult).to.be.an('object'); }); }); + + describe('#config', () => { + it('should return the correct default options when not specified'); + it('#config should return an object.', () => { + const configResult = compose.config( + ['string1', 'string2'], + 'my_project', + myOpts, + ); + expect(configResult).to.be.an('object'); + }); + }); }); diff --git a/test/get-user.spec.js b/test/get-user.spec.js index d756b0b17..abf5c4216 100644 --- a/test/get-user.spec.js +++ b/test/get-user.spec.js @@ -29,6 +29,11 @@ describe('get-user', function() { expect(getUser('test-service', info)).to.equal('www-data'); }); + it('should return specified user if service is a "no-api" docker-compose service and user is specified', function() { + const info = [{service: 'test-service', type: 'docker-compose', meUser: 'custom-user'}]; + expect(getUser('test-service', info)).to.equal('custom-user'); + }); + it('should return "www-data" if service.api is 4 but no user is specified', function() { const info = [{service: 'test-service', api: 4}]; expect(getUser('test-service', info)).to.equal('www-data'); diff --git a/utils/build-tooling-task.js b/utils/build-tooling-task.js index 089a07067..af1590c81 100644 --- a/utils/build-tooling-task.js +++ b/utils/build-tooling-task.js @@ -21,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, sapis))) + .then(() => _.map(require('./parse-tooling-config')(cmd, service, name, 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/get-app-mounts.js b/utils/get-app-mounts.js index 694ffedb3..521b4af70 100644 --- a/utils/get-app-mounts.js +++ b/utils/get-app-mounts.js @@ -6,7 +6,7 @@ module.exports = app => _(app.services) // Objectify .map(service => _.merge({name: service}, _.get(app, `config.services.${service}`, {}))) // Set the default - .map(config => _.merge({}, config, {app_mount: _.get(config, 'app_mount', 'cached')})) + .map(config => _.merge({}, config, {app_mount: _.get(config, 'app_mount', app.config.app_mount || 'cached')})) // Filter out disabled mountes .filter(config => config.app_mount !== false && config.app_mount !== 'disabled') // Combine together diff --git a/utils/get-user.js b/utils/get-user.js index f5b8b2094..ed42f7b87 100644 --- a/utils/get-user.js +++ b/utils/get-user.js @@ -7,8 +7,8 @@ module.exports = (name, info = []) => { if (!_.find(info, {service: name})) return 'www-data'; // otherwise get the service const service = _.find(info, {service: name}); - // if this is a "no-api" service eg type "docker-compose" also return www-data - if (!service.api && service.type === 'docker-compose') return 'www-data'; + // if this is a "no-api" service eg type "docker-compose" return meUser or www-data as default + if (!service.api && service.type === 'docker-compose') return service.meUser || 'www-data'; // otherwise return different things based on the api return service.api === 4 ? service.user || 'www-data' : service.meUser || 'www-data'; }; diff --git a/utils/load-compose-files.js b/utils/load-compose-files.js index f13bf4cec..e2a5c68d8 100644 --- a/utils/load-compose-files.js +++ b/utils/load-compose-files.js @@ -2,8 +2,32 @@ const _ = require('lodash'); const Yaml = require('./../lib/yaml'); +const path = require('path'); const yaml = new Yaml(); +const fs = require('fs'); +const remove = require('./remove'); -module.exports = (files, dir) => _(require('./normalize-files')(files, dir)) - .map(file => yaml.load(file)) - .value(); +// This just runs `docker compose --project-directory ${dir} config -f ${files} --output ${outputPaths}` to +// make all paths relative to the lando config root +module.exports = async (files, dir, landoComposeConfigDir = undefined, outputConfigFunction = undefined) => { + const composeFilePaths = _(require('./normalize-files')(files, dir)).value(); + if (_.isEmpty(composeFilePaths)) { + return {}; + } + + if (undefined === outputConfigFunction) { + return _(composeFilePaths) + .map(file => yaml.load(file)) + .value(); + } + + const outputFile = path.join(landoComposeConfigDir, 'resolved-compose-config.yml'); + + fs.mkdirSync(path.dirname(outputFile), {recursive: true}); + await outputConfigFunction(composeFilePaths, outputFile); + const result = yaml.load(outputFile); + fs.unlinkSync(outputFile); + remove(path.dirname(outputFile)); + + return result; +}; diff --git a/utils/parse-tooling-config.js b/utils/parse-tooling-config.js index e3e21cddd..dcf276e83 100644 --- a/utils/parse-tooling-config.js +++ b/utils/parse-tooling-config.js @@ -41,9 +41,9 @@ const handleDynamic = (config, options, answers = {}, sapis = {}) => { * the first three assuming they are [node, lando.js, options.name]' * Check to see if we have global lando opts and remove them if we do */ -const handleOpts = (config, argopts = []) => { +const handleOpts = (config, name, argopts = []) => { // Append any user specificed opts - argopts = argopts.concat(process.argv.slice(3)); + argopts = argopts.concat(process.argv.slice(process.argv.findIndex(value => value === name.split(' ')[0]) + 1)); // If we have no args then just return right away if (_.isEmpty(argopts)) return config; // Return @@ -74,13 +74,13 @@ const parseCommand = (cmd, service, sapis) => { }; // adds required methods to ensure the lando v3 debugger can be injected into v4 things -module.exports = (cmd, service, options = {}, answers = {}, sapis = {}) => _(cmd) +module.exports = (cmd, service, name, options = {}, answers = {}, sapis = {}) => _(cmd) // Put into an object so we can handle "multi-service" tooling .map(cmd => parseCommand(cmd, service, sapis)) // Handle dynamic services .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))) + .map(config => handleOpts(config, name, handlePassthruOpts(options, answers))) // Wrap the command in /bin/sh if that makes sense .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 diff --git a/utils/parse-v3-services.js b/utils/parse-v3-services.js index cfed7f138..5707a322e 100644 --- a/utils/parse-v3-services.js +++ b/utils/parse-v3-services.js @@ -20,7 +20,7 @@ module.exports = (config, app) => _(config) app: app.name, confDest: path.join(app._config.userConfRoot, 'config', service.type.split(':')[0]), data: `data_${service.name}`, - home: app._config.home, + home: app.config.home || app._config.home, project: app.project, root: app.root, type: service.type.split(':')[0], diff --git a/utils/parse-v4-services.js b/utils/parse-v4-services.js index 3d5dc2fd1..567be08b1 100644 --- a/utils/parse-v4-services.js +++ b/utils/parse-v4-services.js @@ -5,6 +5,7 @@ const _ = require('lodash'); // adds required methods to ensure the lando v3 debugger can be injected into v4 things module.exports = services => _(services) + .pickBy(service => null !== service) .map((service, name) => { const type = service.type ?? 'lando'; return _.merge({}, { From b7bd7755a0a268ca290bf0cdd6e92d432fb00c6d Mon Sep 17 00:00:00 2001 From: Florian Patruck Date: Fri, 3 Apr 2026 10:59:07 +0200 Subject: [PATCH 02/47] feat: If no service key is given, we assume its _lando-compose for service names which are also compose services --- hooks/app-add-v3-services.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/hooks/app-add-v3-services.js b/hooks/app-add-v3-services.js index fe3659406..111edc5f4 100644 --- a/hooks/app-add-v3-services.js +++ b/hooks/app-add-v3-services.js @@ -5,6 +5,13 @@ const _ = require('lodash'); module.exports = async (app, lando) => { // add parsed services to app object so we can use them downstream app.cachedInfo = _.get(lando.cache.get(app.composeCache), 'info', []); + app.config.services = _.mapValues(_.get(app, 'config.services', {}), (service, name) => { + const composeServices = _.keys(_.get(app, 'composeData[0].data[0].services', {})); + if (!_.includes(composeServices, name) || service?.api === 4) { + return service; + } + return _.merge({}, {type: 'lando-compose', version: 'custom', api: 3}, service); + }); app.parsedServices = require('../utils/parse-v3-services')(_.get(app, 'config.services', {}), app); app.parsedV3Services = _(app.parsedServices).filter(service => service.api === 3).value(); app.servicesList = app.parsedV3Services.map(service => service.name); From a058cf88da89c2444c6d345b82769e78ccc7b7d2 Mon Sep 17 00:00:00 2001 From: Florian Patruck Date: Fri, 1 Nov 2024 18:00:39 +0100 Subject: [PATCH 03/47] refactor(compose): Fix to use the configured compose seperator in all places --- lib/engine.js | 2 +- lib/router.js | 2 +- tasks/info.js | 3 ++- utils/build-tooling-runner.js | 3 ++- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/engine.js b/lib/engine.js index 9f869fcfa..881f779fc 100644 --- a/lib/engine.js +++ b/lib/engine.js @@ -172,7 +172,7 @@ module.exports = class Engine { * return lando.engine.exists(compose); */ exists(data) { - return this.engineCmd('exists', data); + return this.engineCmd('exists', _.merge({}, {separator: this.separator}, data)); } /* diff --git a/lib/router.js b/lib/router.js index aea5e74ce..4cd38712b 100644 --- a/lib/router.js +++ b/lib/router.js @@ -55,7 +55,7 @@ exports.destroy = (data, compose, docker) => retryEach(data, datum => { exports.exists = (data, compose, docker, ids = []) => { if (data.compose) return compose('getId', data).then(id => !_.isEmpty(id)); else { - return docker.list() + return docker.list({}, data.separator) .each(container => { ids.push(container.id); ids.push(container.name); diff --git a/tasks/info.js b/tasks/info.js index 0a77a5018..94d68f191 100644 --- a/tasks/info.js +++ b/tasks/info.js @@ -34,11 +34,12 @@ module.exports = lando => ({ const getData = async () => { // go deep if (options.deep) { + const separator = _.get(app, '_config.orchestratorSeparator', '_'); return await lando.engine.list({project: app.project}) .map(async container => await lando.engine.scan(container)) .filter(container => { if (!options.service) return true; - return options.service.map(service => `/${app.project}_${service}_1`).includes(container.Name); + return options.service.map(service => `/${app.project}${separator}${service}${separator}1`).includes(container.Name); }); // normal info diff --git a/utils/build-tooling-runner.js b/utils/build-tooling-runner.js index 6436548c7..b70f93577 100644 --- a/utils/build-tooling-runner.js +++ b/utils/build-tooling-runner.js @@ -4,7 +4,8 @@ const _ = require('lodash'); const path = require('path'); const getContainer = (app, service) => { - return app?.containers?.[service] ?? `${app.project}_${service}_1`; + const separator = _.get(app, '_config.orchestratorSeparator', '_'); + return app?.containers?.[service] ?? `${app.project}${separator}${service}${separator}1`; }; const getContainerPath = (appRoot, appMount = undefined) => { From dbaf16c7213f030a7b8071d8ed9ba807bc42b774 Mon Sep 17 00:00:00 2001 From: Florian Patruck Date: Wed, 2 Oct 2024 11:29:58 +0200 Subject: [PATCH 04/47] fix(app.mounts): Use configured appMount of service and not always /app for v3 services --- builders/_lando.js | 1 + utils/filter-v3-build-steps.js | 2 ++ utils/get-app-mount.js | 11 +++++++++++ utils/parse-events-config.js | 1 + 4 files changed, 15 insertions(+) create mode 100644 utils/get-app-mount.js diff --git a/builders/_lando.js b/builders/_lando.js index fac0d9218..898f0ec6a 100644 --- a/builders/_lando.js +++ b/builders/_lando.js @@ -221,6 +221,7 @@ module.exports = { info.meUser = meUser; info.hasCerts = ssl; info.api = 3; + info.appMount = appMount; // Add the healthcheck if it exists if (healthcheck) info.healthcheck = healthcheck; diff --git a/utils/filter-v3-build-steps.js b/utils/filter-v3-build-steps.js index fc3a2ca7e..1080e5890 100644 --- a/utils/filter-v3-build-steps.js +++ b/utils/filter-v3-build-steps.js @@ -4,6 +4,7 @@ const _ = require('lodash'); module.exports = (services, app, rootSteps = [], buildSteps= [], prestart = false) => { const getUser = require('../utils/get-user'); + const getAppMount = require('../utils/get-app-mount'); // compute stdid based on compose major version const cstdio = _.get(app, '_config.orchestratorMV', 2) ? 'inherit' : ['inherit', 'pipe', 'pipe']; // Start collecting them @@ -29,6 +30,7 @@ module.exports = (services, app, rootSteps = [], buildSteps= [], prestart = fals mode: 'attach', cstdio, prestart, + workdir: getAppMount(service, app.info), user: (_.includes(rootSteps, section)) ? 'root' : getUser(service, app.info), services: [service], }, diff --git a/utils/get-app-mount.js b/utils/get-app-mount.js new file mode 100644 index 000000000..c8d0634c2 --- /dev/null +++ b/utils/get-app-mount.js @@ -0,0 +1,11 @@ +'use strict'; + +const _ = require('lodash'); + +module.exports = (name, info = []) => { + // if no matching service return /app + if (!_.find(info, {service: name})) return '/app'; + // otherwise get the service + const service = _.find(info, {service: name}); + return service.appMount || '/app'; +}; diff --git a/utils/parse-events-config.js b/utils/parse-events-config.js index 017fd3b62..0565d36ea 100644 --- a/utils/parse-events-config.js +++ b/utils/parse-events-config.js @@ -95,6 +95,7 @@ module.exports = (cmds, app, data = {}) => _.map(cmds, cmd => { opts: { cstdio, mode: 'attach', + workdir: require('./get-app-mount')(service, app.info), user: require('./get-user')(service, app.info), services: [service], environment: { From 978f14d458eff9dc2c5b584569381fdd547983f4 Mon Sep 17 00:00:00 2001 From: Florian Patruck Date: Fri, 1 Nov 2024 17:55:11 +0100 Subject: [PATCH 05/47] refactor(home-dir): Add a config option to just share the ssh directory to not have docker desktop warnings about the home directory volume --- builders/_lando.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/builders/_lando.js b/builders/_lando.js index 898f0ec6a..69b7cbe07 100644 --- a/builders/_lando.js +++ b/builders/_lando.js @@ -139,7 +139,8 @@ module.exports = { } // Add in some more dirz if it makes sense - if (home) volumes.push(`${home}:/user:cached`); + if (home && _.get(_app, '_config.homeMount', true)) volumes.push(`${home}:/user:cached`); + else if (home && _.get(_app, 'config.keys', true)) volumes.push(`${path.join(home, '.ssh')}:/user/.ssh:cached`); // Handle cert refresh // @TODO: this might only be relevant to the proxy, if so let's move it there From da8aa55032ba30a6a9091f86ed3647aa994e6979 Mon Sep 17 00:00:00 2001 From: Florian Patruck Date: Fri, 1 Nov 2024 17:56:13 +0100 Subject: [PATCH 06/47] update(traefik): Enable the traefik dashboard as thats useful per default --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 053f6695c..b985fb132 100644 --- a/index.js +++ b/index.js @@ -24,7 +24,7 @@ const defaults = { '/entrypoint.sh', '--log.level=DEBUG', '--api.insecure=true', - '--api.dashboard=false', + '--api.dashboard=true', '--providers.docker=true', '--entrypoints.https.address=:443', '--entrypoints.http.address=:80', From dce5b77f312dc6be7130af831394cc75278a373b Mon Sep 17 00:00:00 2001 From: florianPat Date: Mon, 9 Sep 2024 21:33:10 +0200 Subject: [PATCH 07/47] fix(tooling): Check that container setup is finished at container start for version 3 containers and before tooling to make sure the permission setup is finished --- builders/_lando.js | 1 + hooks/app-run-events.js | 22 ---------- lib/router.js | 13 ++++++ scripts/check-entrypoint-ran.sh | 13 ++++++ scripts/lando-entrypoint.sh | 6 +++ scripts/user-perm-helpers.sh | 71 ++++++++++++++------------------- scripts/user-perms.sh | 22 ++++++---- utils/filter-v3-build-steps.js | 20 +--------- 8 files changed, 80 insertions(+), 88 deletions(-) create mode 100755 scripts/check-entrypoint-ran.sh diff --git a/builders/_lando.js b/builders/_lando.js index 69b7cbe07..96c6118f0 100644 --- a/builders/_lando.js +++ b/builders/_lando.js @@ -103,6 +103,7 @@ module.exports = { LANDO_WEBROOT_USER: meUser, LANDO_WEBROOT_GROUP: meUser, LANDO_MOUNT: appMount, + LANDO_SERVICE_API: 3, }; // Handle labels diff --git a/hooks/app-run-events.js b/hooks/app-run-events.js index 921086139..ca82c1227 100644 --- a/hooks/app-run-events.js +++ b/hooks/app-run-events.js @@ -4,28 +4,6 @@ const _ = require('lodash'); module.exports = async (app, lando, cmds, data, event) => { const eventCommands = require('./../utils/parse-events-config')(cmds, app, data); - // add perm sweeping to all v3 services - if (!_.isEmpty(eventCommands)) { - const permsweepers = _(eventCommands) - .filter(command => command.api === 3) - .map(command => ({id: command.id, services: _.get(command, 'opts.services', [])})) - .uniqBy('id') - .value(); - lando.log.debug('added preemptive perm sweeping to evented v3 services %j', permsweepers.map(s => s.id)); - _.forEach(permsweepers, ({id, services}) => { - eventCommands.unshift({ - id, - cmd: '/helpers/user-perms.sh --silent', - compose: app.compose, - project: app.project, - opts: { - mode: 'attach', - user: 'root', - services, - }, - }); - }); - } const injectable = _.has(app, 'engine') ? app : lando; return injectable.engine.run(eventCommands).catch(err => { const command = _.tail(event.split('-')).join('-'); diff --git a/lib/router.js b/lib/router.js index 4cd38712b..6d715397d 100644 --- a/lib/router.js +++ b/lib/router.js @@ -87,6 +87,19 @@ exports.run = (data, compose, docker, started = true) => Promise.mapSeries(norma // if this is a prestart build step and its not the last one make sure we set started = true // this prevents us from having to stop and then restart the container during builds started = _.get(datum, 'opts.prestart', false) && !_.get(datum, 'opts.last', false); + + const cmd = [ + '/bin/sh', + '-c', + // eslint-disable-next-line max-len + 'if [ "$LANDO_SERVICE_API" = "3" ]; then if [ -f /helpers/check-entrypoint-ran.sh ]; then /helpers/check-entrypoint-ran.sh; fi fi', + ]; + return compose('run', _.merge( + {}, + datum, + {opts: {cmd, id: datum.id, user: 'root', mode: 'attach'}}, + ), + ); }); } }) diff --git a/scripts/check-entrypoint-ran.sh b/scripts/check-entrypoint-ran.sh new file mode 100755 index 000000000..07e9c677a --- /dev/null +++ b/scripts/check-entrypoint-ran.sh @@ -0,0 +1,13 @@ +#!/bin/sh + +# retry settings +attempt=0 +delay=1 +retry=32 + +until [ "$attempt" -ge "$retry" ] +do + test -f "/tmp/lando-entrypoint-ran" && break + attempt=$((attempt+1)) + sleep "$delay" +done diff --git a/scripts/lando-entrypoint.sh b/scripts/lando-entrypoint.sh index 5f37a4bad..e809b3925 100755 --- a/scripts/lando-entrypoint.sh +++ b/scripts/lando-entrypoint.sh @@ -2,6 +2,10 @@ set -e +if [ -f /tmp/lando-entrypoint-ran ]; then + rm /tmp/lando-entrypoint-ran +fi + # Get the lando logger . /helpers/log.sh @@ -73,6 +77,8 @@ fi # @TODO: We should def figure out whether we can get away with running everything through exec at some point lando_info "Lando handing off to: $@" +touch /tmp/lando-entrypoint-ran + # Try to DROP DOWN to another user if we can if [ ! -z ${LANDO_DROP_USER+x} ]; then lando_debug "Running command as ${LANDO_DROP_USER}..." diff --git a/scripts/user-perm-helpers.sh b/scripts/user-perm-helpers.sh index 28db38732..019bbfb2b 100755 --- a/scripts/user-perm-helpers.sh +++ b/scripts/user-perm-helpers.sh @@ -10,31 +10,25 @@ LANDO_MODULE="userperms" add_user() { local USER=$1 local GROUP=$2 - local UID=$3 - local GID=$4 - local DISTRO=$5 - local EXTRAS="$6" - if [ "$DISTRO" = "alpine" ]; then - if ! groups | grep "$GROUP" > /dev/null 2>&1; then addgroup -g "$GID" "$GROUP" 2>/dev/null; fi - if ! id -u "$GROUP" > /dev/null 2>&1; then adduser -H -D -G "$GROUP" -u "$UID" "$USER" "$GROUP" 2>/dev/null; fi - else - if ! groups | grep "$GROUP" > /dev/null 2>&1; then groupadd --force --gid "$GID" "$GROUP" 2>/dev/null; fi - if ! id -u "$GROUP" > /dev/null 2>&1; then useradd --gid "$GID" --uid "$UID" $EXTRAS "$USER" 2>/dev/null; fi - fi; + local WEBROOT_UID=$3 + local WEBROOT_GID=$4 + if ! getent group | cut -d: -f1 | grep "$GROUP" > /dev/null 2>&1; then addgroup -g "$WEBROOT_GID" "$GROUP" 2>/dev/null; fi + if ! id -u "$USER" > /dev/null 2>&1; then adduser -H -D -G "$GROUP" -u "$WEBROOT_UID" "$USER" "$GROUP" 2>/dev/null; fi } # Verify user verify_user() { local USER=$1 local GROUP=$2 - local DISTRO=$3 id -u "$USER" > /dev/null 2>&1 - groups | grep "$GROUP" > /dev/null 2>&1 - if [ "$DISTRO" = "alpine" ]; then + groups "$USER" | grep "$GROUP" > /dev/null 2>&1 + if command -v chsh > /dev/null 2>&1 ; then + if command -v /bin/bash > /dev/null 2>&1 ; then + chsh -s /bin/bash $USER || true + fi; + else true # is there a chsh we can use? do we need to? - else - chsh -s /bin/bash $USER || true fi; } @@ -59,11 +53,10 @@ reset_user() { if [ "$(id -u $USER)" != "$HOST_UID" ]; then usermod -o -u "$HOST_UID" "$USER" 2>/dev/null fi - groupmod -g "$HOST_GID" "$GROUP" 2>/dev/null || true - if [ "$(id -u $USER)" != "$HOST_UID" ]; then + groupmod -o -g "$HOST_GID" "$GROUP" 2>/dev/null || true + if [ "$(id -g $USER)" != "$HOST_GID" ]; then usermod -g "$HOST_GID" "$USER" 2>/dev/null || true fi - usermod -a -G "$GROUP" "$USER" 2>/dev/null || true fi; # If this mapping is incorrect lets abort here if [ "$(id -u $USER)" != "$HOST_UID" ]; then @@ -78,33 +71,31 @@ reset_user() { perm_sweep() { local USER=$1 local GROUP=$2 - local OTHER_DIR=$3 - - # Start with the directories that are likely blockers - chown -R $USER:$GROUP /usr/local/bin - chown $USER:$GROUP /var/www - chown $USER:$GROUP /app - chmod 755 /var/www + local USER_HOME=$3 + local OTHER_DIR=$4 # Do other dirs first if we have them if [ ! -z "$OTHER_DIR" ]; then - chown -R $USER:$GROUP $OTHER_DIR >/dev/null 2>&1 & + chown -R $USER:$GROUP $OTHER_DIR > /tmp/perms.out 2> /tmp/perms.err || true fi - # Do a background sweep - nohup find /app -not -user $USER -execdir chown $USER:$GROUP {} \+ > /tmp/perms.out 2> /tmp/perms.err & - nohup find /var/www/.ssh -not -user $USER -execdir chown $USER:$GROUP {} \+ > /tmp/perms.out 2> /tmp/perms.err & - nohup find /user/.ssh -not -user $USER -execdir chown $USER:$GROUP {} \+ > /tmp/perms.out 2> /tmp/perms.err & - nohup find /var/www -not -user $USER -execdir chown $USER:$GROUP {} \+ > /tmp/perms.out 2> /tmp/perms.err & - nohup find /usr/local/bin -not -user $USER -execdir chown $USER:$GROUP {} \+ > /tmp/perms.out 2> /tmp/perms.err & - nohup chmod -R 755 /var/www >/dev/null 2>&1 & + # Do permission sweep and wait for completion + chown -R $USER:$GROUP /app > /tmp/perms.out 2> /tmp/perms.err || true + lando_info "chowned /app" + chown -R $USER:$GROUP /tmp > /tmp/perms.out 2> /tmp/perms.err || true + lando_info "chowned /tmp" + [ -d /user ] && chown -R $USER:$GROUP /user > /tmp/perms.out 2> /tmp/perms.err || true + lando_info "chowned /user" + chown -R $USER:$GROUP /var/www > /tmp/perms.out 2> /tmp/perms.err || true + lando_info "chowned /var/www" + chmod 755 /var/www - # Lets also make some /usr/locals chowned - nohup find /usr/local/lib -not -user $USER -execdir chown $USER:$GROUP {} \+ > /tmp/perms.out 2> /tmp/perms.err & - nohup find /usr/local/share -not -user $USER -execdir chown $USER:$GROUP {} \+ > /tmp/perms.out 2> /tmp/perms.err & - nohup find /usr/local -not -user $USER -execdir chown $USER:$GROUP {} \+ > /tmp/perms.out 2> /tmp/perms.err & + chown -R $USER:$GROUP /usr/local > /tmp/perms.out 2> /tmp/perms.err || true + lando_info "chowned /usr/local" # Make sure we chown the $USER home directory - nohup find $(getent passwd $USER | cut -d : -f 6) -not -user $USER -execdir chown $USER:$GROUP {} \+ > /tmp/perms.out 2> /tmp/perms.err & - nohup find /lando -not -user $USER -execdir chown $USER:$GROUP {} \+ > /tmp/perms.out 2> /tmp/perms.err & + [ -d "$USER_HOME" ] && chown -R $USER:$GROUP "$USER_HOME" > /tmp/perms.out 2> /tmp/perms.err || true + lando_info "chowned $USER_HOME" + [ -d /lando/keys ] && chown -R $USER:$GROUP /lando/keys > /tmp/perms.out 2> /tmp/perms.err || true + lando_info "chowned /lando" } diff --git a/scripts/user-perms.sh b/scripts/user-perms.sh index 215573859..3f29c4c70 100755 --- a/scripts/user-perms.sh +++ b/scripts/user-perms.sh @@ -52,17 +52,25 @@ mkdir -p /var/www/.ssh mkdir -p /user/.ssh mkdir -p /app +# Get the webroot user's home directory +WEBROOT_HOME=$(getent passwd "$LANDO_WEBROOT_USER" | cut -d : -f 6) +if [ -z "$WEBROOT_HOME" ]; then + WEBROOT_HOME="/var/www" +fi + +lando_info "meUsers home directory: $WEBROOT_HOME" + # Symlink the gitconfig -if [ -f "/user/.gitconfig" ]; then - rm -f /var/www/.gitconfig - ln -sf /user/.gitconfig /var/www/.gitconfig +if [ -f "/user/.gitconfig" ] && [ ! -f "$WEBROOT_HOME/.gitconfig" ]; then + mkdir -p "$WEBROOT_HOME" + ln -sf /user/.gitconfig "$WEBROOT_HOME/.gitconfig" lando_info "Symlinked users .gitconfig." fi # Symlink the known_hosts -if [ -f "/user/.ssh/known_hosts" ]; then - rm -f /var/www/.ssh/known_hosts - ln -sf /user/.ssh/known_hosts /var/www/.ssh/known_hosts +if [ -f "/user/.ssh/known_hosts" ] && [ ! -f "$WEBROOT_HOME/.ssh/known_hosts" ]; then + mkdir -p "$WEBROOT_HOME/.ssh" + ln -sf /user/.ssh/known_hosts "$WEBROOT_HOME/.ssh/known_hosts" lando_info "Symlinked users known_hosts" fi @@ -101,4 +109,4 @@ lando_info "$LANDO_WEBROOT_USER:$LANDO_WEBROOT_GROUP is now running as $(id $LAN # Make sure we set the ownership of the mount and HOME when we start a service lando_info "And here. we. go." lando_info "Doing the permission sweep." -perm_sweep $LANDO_WEBROOT_USER $(getent group "$LANDO_HOST_GID" | cut -d: -f1) $LANDO_RESET_DIR +perm_sweep $LANDO_WEBROOT_USER $(getent group "$LANDO_HOST_GID" | cut -d: -f1) $WEBROOT_HOME $LANDO_RESET_DIR diff --git a/utils/filter-v3-build-steps.js b/utils/filter-v3-build-steps.js index 1080e5890..2190f9c97 100644 --- a/utils/filter-v3-build-steps.js +++ b/utils/filter-v3-build-steps.js @@ -39,26 +39,8 @@ module.exports = (services, app, rootSteps = [], buildSteps= [], prestart = fals } }); }); - // Let's silent run user-perm stuff and add a "last" flag + // Let's add a "last" flag if (!_.isEmpty(build)) { - const permsweepers = _(build) - .map(command => ({id: command.id, services: _.get(command, 'opts.services', [])})) - .uniqBy('id') - .value(); - _.forEach(permsweepers, ({id, services}) => { - build.unshift({ - id, - cmd: '/helpers/user-perms.sh --silent', - compose: app.compose, - project: app.project, - opts: { - mode: 'attach', - prestart, - user: 'root', - services, - }, - }); - }); // Denote the last step in the build if its happening before start const last = _.last(build); last.opts.last = prestart; From 71989a5d3c893b1e99de8b5c9d20bfe2c0a24be1 Mon Sep 17 00:00:00 2001 From: Florian Patruck Date: Sat, 16 Nov 2024 09:10:34 +0100 Subject: [PATCH 08/47] fix(events): Make sure the perm-sweep is run for docker-compose services as they default to v3 api in the event config Note that there is a difference between build steps and events: Build steps do NOT add a perm-sweep because it checks for the service version and does add a default. But I cannot change this as the "events" test fails then, as this is how it is --- hooks/app-run-events.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/hooks/app-run-events.js b/hooks/app-run-events.js index ca82c1227..921086139 100644 --- a/hooks/app-run-events.js +++ b/hooks/app-run-events.js @@ -4,6 +4,28 @@ const _ = require('lodash'); module.exports = async (app, lando, cmds, data, event) => { const eventCommands = require('./../utils/parse-events-config')(cmds, app, data); + // add perm sweeping to all v3 services + if (!_.isEmpty(eventCommands)) { + const permsweepers = _(eventCommands) + .filter(command => command.api === 3) + .map(command => ({id: command.id, services: _.get(command, 'opts.services', [])})) + .uniqBy('id') + .value(); + lando.log.debug('added preemptive perm sweeping to evented v3 services %j', permsweepers.map(s => s.id)); + _.forEach(permsweepers, ({id, services}) => { + eventCommands.unshift({ + id, + cmd: '/helpers/user-perms.sh --silent', + compose: app.compose, + project: app.project, + opts: { + mode: 'attach', + user: 'root', + services, + }, + }); + }); + } const injectable = _.has(app, 'engine') ? app : lando; return injectable.engine.run(eventCommands).catch(err => { const command = _.tail(event.split('-')).join('-'); From 4c8cf0d89780f9d471364716f792db9ef4f3cdb6 Mon Sep 17 00:00:00 2001 From: Florian Patruck Date: Sat, 19 Oct 2024 21:09:06 +0200 Subject: [PATCH 09/47] feat(exec): Exec can also run if the app is not yet started and add no-deps flag to start containers without depending containers on exec if the full compose stack did not start yet --- app.js | 1 - examples/cache/README.md | 7 ++++++- lib/compose.js | 22 +++++++++++++++++++++- tasks/exec.js | 25 ++++++++++++------------- tasks/ssh.js | 2 ++ utils/build-tooling-runner.js | 16 ++++++++++++++-- utils/build-tooling-task.js | 6 +++++- utils/get-tasks.js | 2 ++ 8 files changed, 62 insertions(+), 19 deletions(-) diff --git a/app.js b/app.js index 920f230b3..5c02ab622 100644 --- a/app.js +++ b/app.js @@ -64,7 +64,6 @@ module.exports = async (app, lando) => { overrides: { tooling: app._coreToolingOverrides, }, - }, {persist: true}); }; diff --git a/examples/cache/README.md b/examples/cache/README.md index 9e2ee5e37..4ed4dabd8 100644 --- a/examples/cache/README.md +++ b/examples/cache/README.md @@ -30,10 +30,15 @@ cat ~/.lando/cache/lando-cache.compose.cache || echo $? | grep 1 lando --clear lando || true cat ~/.lando/cache/_.tasks.cache -cat ~/.lando/cache/lando-cache.compose.cache +cat ~/.lando/cache/lando-cache.compose.cache || true # Should regenerate the caches on any --help before the help is displayed lando --clear +# NOTE(flo): Web is not here as the task bootstrapping runs before the app init, which gets compose services :/ +lando exec --help | grep service | grep choices | grep web2 | grep web3 | grep web4 +cat ~/.lando/cache/_.tasks.cache +cat ~/.lando/cache/lando-cache.compose.cache || true +lando exec web -- echo 'Hello World' | grep 'Hello World' lando exec --help | grep service | grep choices | grep web | grep web2 | grep web3 | grep web4 cat ~/.lando/cache/_.tasks.cache cat ~/.lando/cache/lando-cache.compose.cache diff --git a/lib/compose.js b/lib/compose.js index 080d8a738..1f93027e1 100644 --- a/lib/compose.js +++ b/lib/compose.js @@ -23,6 +23,19 @@ const composeFlags = { outputFilePath: '-o', }; +const composeFlagOptionMapping = { + build: ['noCache', 'pull', 'q'], + down: ['removeOrphans', 'volumes'], + exec: ['background', 'detach', 'noTTY'], + kill: ['removeOrphans'], + logs: ['follow', 'timestamps'], + ps: ['q'], + pull: ['q'], + rm: ['force', 'volumes'], + up: ['background', 'detach', 'noRecreate', 'noDeps', 'pull', 'q', 'recreate', 'removeOrphans', 'timestamps'], + config: ['outputFilePath'], +}; + // Default options nad things const defaultOptions = { build: {noCache: false, pull: true}, @@ -40,7 +53,14 @@ const defaultOptions = { /* * Helper to merge options with default */ -const mergeOpts = (run, opts = {}) => _.merge({}, defaultOptions[run], opts); +const mergeOpts = (run, opts = {}) => _.merge( + {}, + defaultOptions[run], + _.pickBy( + opts, + (value, index) => (!_.includes(_.keys(composeFlags), index)) || _.includes(composeFlagOptionMapping[run], index), + ), +); /* * Parse docker-compose options diff --git a/tasks/exec.js b/tasks/exec.js index 4844b5344..d1688d3ab 100644 --- a/tasks/exec.js +++ b/tasks/exec.js @@ -14,7 +14,6 @@ module.exports = (lando, config = lando.appConfig) => ({ describe: 'Runs command(s) on a service', usage: '$0 exec [--user ] -- ', override: true, - level: 'engine', examples: [ '$0 exec appserver -- lash bash', '$0 exec nginx --user root -- whoami', @@ -25,7 +24,7 @@ module.exports = (lando, config = lando.appConfig) => ({ service: { describe: 'Runs on this service', type: 'string', - choices: config?.allServices ?? [], + choices: config?.allServices ?? _.keys(lando.appConfig.services) ?? [], }, }, options: { @@ -38,9 +37,10 @@ module.exports = (lando, config = lando.appConfig) => ({ // construct a minapp from various places const minapp = !_.isEmpty(config) ? config : lando.appConfig; - // if no app then we need to throw + // if no app then we need to create one if (!fs.existsSync(minapp.composeCache)) { - throw new Error('Could not detect a built app. Rebuild or move into the correct location!'); + const app = lando.getApp(options._app.root); + await app.init(); } // Build a minimal app @@ -49,6 +49,8 @@ module.exports = (lando, config = lando.appConfig) => ({ // augment app.config = minapp; + app._lando = lando; + app._config = lando.config; app.events = new AsyncEvents(lando.log); // Load only what we need so we don't pay the appinit penalty @@ -127,6 +129,8 @@ module.exports = (lando, config = lando.appConfig) => ({ ropts.push(sconf?.overrides?.working_dir ?? sconf?.working_dir); // mix in mount if applicable ropts.push(app?.mounts[options.service]); + ropts.push(!options.deps ?? false); + ropts.push(options.autoRemove ?? true); // emit pre-exec await app.events.emit('pre-exec', config); @@ -137,18 +141,12 @@ module.exports = (lando, config = lando.appConfig) => ({ // try to run it try { lando.log.debug('running exec command %o on %o', runner.cmd, runner.id); - await require('../utils/build-docker-exec')(lando, 'inherit', runner); + await lando.engine.run(runner); // error } catch (error) { - return lando.engine.isRunning(runner.id).then(isRunning => { - if (!isRunning) { - throw new Error(`Looks like your app is stopped! ${color.bold('lando start')} it up to exec your heart out.`); - } else { - error.hide = true; - throw error; - } - }); + error.hide = true; + throw error; // finally } finally { @@ -156,3 +154,4 @@ module.exports = (lando, config = lando.appConfig) => ({ } }, }); + diff --git a/tasks/ssh.js b/tasks/ssh.js index ac325fc57..c4f545f95 100644 --- a/tasks/ssh.js +++ b/tasks/ssh.js @@ -40,6 +40,8 @@ module.exports = (lando, app) => ({ const api = _.get(_.find(app.info, {service}), 'api', 3); // set additional opt defaults if possible const opts = [undefined, api === 4 ? undefined : '/app']; + opts[2] = !app._config.command.deps ?? false; + opts[3] = app._config.command.autoRemove ?? true; // mix any v4 service info on top of app.config.services const services = _(_.get(app, 'config.services', {})) .map((service, id) => _.merge({}, {id}, service)) diff --git a/utils/build-tooling-runner.js b/utils/build-tooling-runner.js index b70f93577..15c1313f9 100644 --- a/utils/build-tooling-runner.js +++ b/utils/build-tooling-runner.js @@ -21,7 +21,17 @@ const getContainerPath = (appRoot, appMount = undefined) => { return dir.join('/'); }; -module.exports = (app, command, service, user, env = {}, dir = undefined, appMount = undefined) => ({ +module.exports = ( + app, + command, + service, + user, + env = {}, + dir = undefined, + appMount = undefined, + noDeps = false, + autoRemove = true, +) => ({ id: getContainer(app, service), compose: app.compose, project: app.project, @@ -33,6 +43,8 @@ module.exports = (app, command, service, user, env = {}, dir = undefined, appMou user: (user === null) ? require('./get-user')(service, app.info) : user, services: _.compact([service]), hijack: false, - autoRemove: true, + autoRemove, + noDeps, + prestart: !autoRemove, }, _.identity), }); diff --git a/utils/build-tooling-task.js b/utils/build-tooling-task.js index af1590c81..aa874fc7f 100644 --- a/utils/build-tooling-task.js +++ b/utils/build-tooling-task.js @@ -23,7 +23,11 @@ module.exports = (config, injected) => { // Get an interable of our commandz .then(() => _.map(require('./parse-tooling-config')(cmd, service, name, options, answers, sapis))) // Build run objects - .map(({command, service}) => require('./build-tooling-runner')(app, command, service, user, env, dir, appMount)) + .map( + ({command, service}) => + require('./build-tooling-runner')( + app, command, service, user, env, dir, appMount, !answers.deps ?? false, answers.autoRemove ?? true, + )) // Try to run the task quickly first and then fallback to compose launch .each(runner => require('./build-docker-exec')(injected, stdio, runner).catch(execError => { return injected.engine.isRunning(runner.id).then(isRunning => { diff --git a/utils/get-tasks.js b/utils/get-tasks.js index 3ae593808..f0b3f2d02 100644 --- a/utils/get-tasks.js +++ b/utils/get-tasks.js @@ -53,6 +53,8 @@ const engineRunner = (config, command) => (argv, lando) => { const AsyncEvents = require('./../lib/events'); // Build a minimal app const app = lando.cache.get(path.basename(config.composeCache)); + app._lando = lando; + app._config = lando.config; app.config = config; app.events = new AsyncEvents(lando.log); From 42be9f2d78e378ebc948400b4b76d42241ca5e94 Mon Sep 17 00:00:00 2001 From: florianPat Date: Mon, 9 Sep 2024 21:33:15 +0200 Subject: [PATCH 10/47] feat(volumes): Use the lando proxy dir as the config volume and therefore do not copy the files over --- builders/_proxy.js | 14 +++++++++----- hooks/app-start-proxy.js | 4 +--- scripts/proxy-certs.sh | 5 ----- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/builders/_proxy.js b/builders/_proxy.js index 1f31be61c..a42dd0808 100644 --- a/builders/_proxy.js +++ b/builders/_proxy.js @@ -6,7 +6,14 @@ const _ = require('lodash'); /* * Helper to get core proxy service */ -const getProxy = ({proxyCommand, proxyPassThru, proxyDomain, userConfRoot, version = 'unknown'} = {}) => { +const getProxy = ({ + proxyCommand, + proxyPassThru, + proxyDomain, + userConfRoot, + proxyConfigDir, + version = 'unknown', +} = {}) => { return { services: { proxy: { @@ -23,7 +30,7 @@ const getProxy = ({proxyCommand, proxyPassThru, proxyDomain, userConfRoot, versi volumes: [ '/var/run/docker.sock:/var/run/docker.sock', `${userConfRoot}/scripts/proxy-certs.sh:/scripts/100-proxy-certs`, - 'proxy_config:/proxy_config', + `${proxyConfigDir}:/proxy_config`, ], }, }, @@ -32,9 +39,6 @@ const getProxy = ({proxyCommand, proxyPassThru, proxyDomain, userConfRoot, versi driver: 'bridge', }, }, - volumes: { - proxy_config: {}, - }, }; }; diff --git a/hooks/app-start-proxy.js b/hooks/app-start-proxy.js index f8c6fd83d..f487a7426 100644 --- a/hooks/app-start-proxy.js +++ b/hooks/app-start-proxy.js @@ -322,19 +322,17 @@ module.exports = async (app, lando) => { service.labels['traefik.enable'] = true; service.labels['traefik.docker.network'] = lando.config.proxyNet; service.environment.LANDO_PROXY_PASSTHRU = _.toString(lando.config.proxyPassThru); - const proxyVolume = `${lando.config.proxyName}_proxy_config`; return { services: _.set({}, service.name, { networks: {'lando_proxyedge': {}}, labels: service.labels, environment: service.environment, volumes: [ - `${proxyVolume}:/proxy_config`, + `${lando.config.proxyConfigDir}:/proxy_config`, `${lando.config.userConfRoot}/scripts/proxy-certs.sh:/scripts/100-proxy-certs`, ], }), networks: {'lando_proxyedge': {name: lando.config.proxyNet, external: true}}, - volumes: _.set({}, proxyVolume, {external: true}), }; }) diff --git a/scripts/proxy-certs.sh b/scripts/proxy-certs.sh index 66ae3b38f..946275fa0 100644 --- a/scripts/proxy-certs.sh +++ b/scripts/proxy-certs.sh @@ -28,11 +28,6 @@ fi : ${LANDO_PROXY_KEY:="/lando/certs/${LANDO_SERVICE_NAME}.${LANDO_APP_PROJECT}.key"} : ${LANDO_PROXY_CONFIG_FILE:="/proxy_config/${LANDO_SERVICE_NAME}.${LANDO_APP_PROJECT}.yaml"} -# Move over any global config set by lando -if [ -d "/lando/proxy/config" ]; then - cp -rf /lando/proxy/config/* /proxy_config/ -fi - # Bail if proxypassthru is off if [ "$LANDO_PROXY_PASSTHRU" != "true" ]; then lando_info "Proxy passthru is off so exiting..." From f413cf215ee3b4f54bd35fc01e70c0fdfd3f1783 Mon Sep 17 00:00:00 2001 From: Florian Patruck Date: Sat, 19 Oct 2024 21:11:24 +0200 Subject: [PATCH 11/47] feat(bootstrap): Add special '_init' service for events so that one can automate project setup tasks before starting the docker composition --- builders/_init.js | 2 ++ hooks/app-run-events.js | 39 +++++++++++++++++++++++++++++-- utils/build-init-runner.js | 2 ++ utils/get-init-runner-defaults.js | 2 ++ utils/parse-events-config.js | 15 +++++++++++- 5 files changed, 57 insertions(+), 3 deletions(-) diff --git a/builders/_init.js b/builders/_init.js index a86cf3b04..1e9484d12 100644 --- a/builders/_init.js +++ b/builders/_init.js @@ -13,6 +13,8 @@ module.exports = { version: 'custom', type: 'init', name: 'init', + data: null, + dataHome: null, }, builder: (parent, config) => class LandoInit extends parent { constructor(userConfRoot, home, app, env = {}, labels = {}, image = 'devwithlando/util:4') { diff --git a/hooks/app-run-events.js b/hooks/app-run-events.js index 921086139..bcd71679d 100644 --- a/hooks/app-run-events.js +++ b/hooks/app-run-events.js @@ -1,9 +1,12 @@ 'use strict'; const _ = require('lodash'); +const remove = require('../utils/remove'); +const path = require('path'); +const formatters = require('../lib/formatters'); module.exports = async (app, lando, cmds, data, event) => { - const eventCommands = require('./../utils/parse-events-config')(cmds, app, data); + const eventCommands = require('./../utils/parse-events-config')(cmds, app, data, lando); // add perm sweeping to all v3 services if (!_.isEmpty(eventCommands)) { const permsweepers = _(eventCommands) @@ -27,7 +30,28 @@ module.exports = async (app, lando, cmds, data, event) => { }); } const injectable = _.has(app, 'engine') ? app : lando; - return injectable.engine.run(eventCommands).catch(err => { + + const splitEventCommands = []; + while (!_.isEmpty(eventCommands)) { + splitEventCommands.push( + _.takeWhile(eventCommands, + (eventCommand, index) => index === 0 || (!!eventCommand.toolingTask === !!eventCommands[index - 1].toolingTask), + ), + ); + eventCommands.splice(0, _.last(splitEventCommands).length); + } + + return lando.Promise.mapSeries(splitEventCommands, eventCommands => { + return lando.Promise.mapSeries(eventCommands, eventCommand => { + if (undefined !== eventCommand.toolingTask) { + const inquiry = formatters.getInteractive(eventCommand.toolingTask.options, eventCommand.answers); + return formatters.handleInteractive(inquiry, eventCommand.answers, eventCommand.toolingTask.command, lando) + .then(answers => eventCommand.toolingTask.run(_.merge(eventCommand.answers, answers))); + } else { + return injectable.engine.run(eventCommands); + } + }); + }).catch(err => { const command = _.tail(event.split('-')).join('-'); if (app.addMessage) { const message = _.trim(_.get(err, 'message')) || 'UNKNOWN ERROR'; @@ -44,5 +68,16 @@ module.exports = async (app, lando, cmds, data, event) => { } else { lando.exitCode = 12; } + }).finally(() => { + const initToolingRunners = _.filter(_.flatten(splitEventCommands), eventCommand => true === eventCommand.isInitEventCommand); + if (_.isEmpty(initToolingRunners)) { + return; + } + const run = _.first(initToolingRunners); + + run.opts = {purge: true, mode: 'attach'}; + return injectable.engine.stop(run) + .then(() => injectable.engine.destroy(run)) + .then(() => remove(path.dirname(run.compose[0]))); }); }; diff --git a/utils/build-init-runner.js b/utils/build-init-runner.js index 421a5a164..d5a8b3c3e 100644 --- a/utils/build-init-runner.js +++ b/utils/build-init-runner.js @@ -10,5 +10,7 @@ module.exports = config => ({ user: config.user, services: ['init'], autoRemove: config.remove, + workdir: config.workdir, + prestart: config.prestart, }, }); diff --git a/utils/get-init-runner-defaults.js b/utils/get-init-runner-defaults.js index d63666c68..9cd0c926c 100644 --- a/utils/get-init-runner-defaults.js +++ b/utils/get-init-runner-defaults.js @@ -26,5 +26,7 @@ module.exports = (lando, options) => { user: 'www-data', compose: initFiles, remove: false, + workdir: '/', + prestart: true, }; }; diff --git a/utils/parse-events-config.js b/utils/parse-events-config.js index 0565d36ea..ded1634c2 100644 --- a/utils/parse-events-config.js +++ b/utils/parse-events-config.js @@ -36,7 +36,7 @@ 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 => { +module.exports = (cmds, app, data, lando) => _.map(cmds, cmd => { // Discover the service const service = getService(cmd, data, app._defaultService); // compute stdio based on compose major version @@ -76,6 +76,19 @@ module.exports = (cmds, app, data = {}) => _.map(cmds, cmd => { _.get(app, 'v4.servicesList', []), ]).flatten().compact().uniq().value(); + + if ('_init' === service) { + return _.merge( + {}, + require('./build-init-runner')(_.merge( + {}, + require('./get-init-runner-defaults')(lando, {destination: app.root, name: app.project}), + {cmd, workdir: '/app'}, + )), + {isInitEventCommand: true}, + ); + } + // Validate the service if we can // @NOTE fast engine runs might not have this data yet if ( From caf1d50687b3be5c082a52ce6dddb1e2801c2be8 Mon Sep 17 00:00:00 2001 From: Florian Patruck Date: Sat, 19 Oct 2024 21:11:24 +0200 Subject: [PATCH 12/47] feat(bootstrap): Add bootstrap event handling and fix lando is not yet setup errors --- app.js | 3 ++ hooks/app-add-init-tooling.js | 21 +++++++++ lib/app.js | 48 +++++++++++++++++---- lib/formatters.js | 2 +- utils/build-tooling-task.js | 81 ++++++++++++++++++++++++----------- utils/get-tasks.js | 6 ++- utils/load-compose-files.js | 4 +- 7 files changed, 126 insertions(+), 39 deletions(-) create mode 100644 hooks/app-add-init-tooling.js diff --git a/app.js b/app.js index 5c02ab622..f2f1f0185 100644 --- a/app.js +++ b/app.js @@ -123,6 +123,9 @@ module.exports = async (app, lando) => { // add proxy info as needed app.events.on('post-init', async () => await require('./hooks/app-add-proxy-info')(app, lando)); + // Add _init tooling for bootstrap reference + app.events.on('pre-bootstrap', async () => await require('./hooks/app-add-init-tooling')(app, lando)); + // Collect info so we can inject LANDO_INFO // @NOTE: this is not currently the full lando info because a lot of it requires the app to be on app.events.on('post-init', 10, async () => await require('./hooks/app-set-lando-info')(app, lando)); diff --git a/hooks/app-add-init-tooling.js b/hooks/app-add-init-tooling.js new file mode 100644 index 000000000..f2801b54e --- /dev/null +++ b/hooks/app-add-init-tooling.js @@ -0,0 +1,21 @@ +'use strict'; + +const _ = require('lodash'); + +module.exports = async (app, lando) => { + if (!_.isEmpty(_.get(app, 'config.tooling', {}))) { + app.log.verbose('additional tooling detected'); + + // Add the _init tasks for the bootstrap event! + // TODO(flo): They are duplicated through "app-add-tooling" but I do not care for now! + _.forEach(require('../utils/get-tooling-tasks')(app.config.tooling, app), task => { + if (task.service !== '_init') { + return; + } + + app.log.debug('adding app cli task %s', task.name); + const injectable = _.has(app, 'engine') ? app : lando; + app.tasks.push(require('../utils/build-tooling-task')(task, injectable)); + }); + } +}; diff --git a/lib/app.js b/lib/app.js index c6556b9a1..826818f8b 100644 --- a/lib/app.js +++ b/lib/app.js @@ -6,11 +6,12 @@ const hasher = require('object-hash'); const path = require('path'); const Promise = require('./promise'); const utils = require('./utils'); +const fs = require('node:fs'); /* * Helper to init and then report */ -const initAndReport = (app, method = 'start') => { +const initAndReport = (app, method) => { return app.init().then(() => { app.metrics.report(method, utils.metricsParse(app)); return Promise.resolve(true); @@ -256,6 +257,8 @@ module.exports = class App { .then(() => this.log.info('destroyed app.')); } + static isBootstrapCommand = undefined; + /** * Initializes the app * @@ -272,18 +275,30 @@ module.exports = class App { init({noEngine = false} = {}) { // We should only need to initialize once, if we have just go right to app ready if (this.initialized) return this.events.emit('ready', this); + if (App.isBootstrapCommand) { + console.log(require('yargonaut').chalk().cyan('Looks like this is the first time to start the app. Lets bootstrap it...')); + } + + return loadPlugins(this, this._lando) + /** + * Event that only gets triggered if the app never started before (or was destroyed) + * + * @since 3.23.25 + * @alias app.events:pre-bootstrap + * @event pre-bootstrap + * @property {App} app The app instance. + */ + .then(() => App.isBootstrapCommand ? this.events.emit('pre-bootstrap', this) : undefined) // Get compose data if we have any, otherwise set to [] - return require('../utils/load-compose-files')( + .then(() => noEngine === true ? [] : require('../utils/load-compose-files')( _.get(this, 'config.compose', []), this.root, this._dir, (composeFiles, outputFilePath) => this.engine.getComposeConfig({compose: composeFiles, project: this.project, outputFilePath}), - ) + )) .then(composeFileData => { - if (undefined !== composeFileData) { - this.composeData = [new this.ComposeService('compose', {}, composeFileData)]; - } + this.composeData = [new this.ComposeService('compose', {}, ...composeFileData)]; // Validate and set env files this.envFiles = require('../utils/normalize-files')(_.get(this, 'config.env_file', []), this.root); // Log some things @@ -302,8 +317,6 @@ module.exports = class App { * @event pre_init * @property {App} app The app instance. */ - .then(() => loadPlugins(this, this._lando)) - .then(() => this.events.emit('pre-init', this)) // Actually assemble this thing so its ready for that engine .then(() => { @@ -505,13 +518,19 @@ module.exports = class App { * @alias app.start * @fires pre_start * @fires post_start + * @fires post_bootstrap * @return {Promise} A Promise. * */ start() { // Log this.log.info('starting app...'); - return initAndReport(this) + + if (undefined === App.isBootstrapCommand) { + App.isBootstrapCommand = !fs.existsSync(this._dir); + } + + return initAndReport(this, 'start') /** * Event that runs before an app starts up. @@ -539,6 +558,17 @@ module.exports = class App { * @event post_start */ .then(() => this.events.emit('post-start')) + + /** + * Event that only gets triggered if the app never started before (or was destroyed) + * + * @since 3.23.25 + * @alias app.events:post-bootstrap + * @event post-bootstrap + * @property {App} app The app instance. + */ + .then(() => App.isBootstrapCommand ? this.events.emit('post-bootstrap', this) : undefined) + .then(() => this.log.info('started app.')); } diff --git a/lib/formatters.js b/lib/formatters.js index b9ae15c0e..0d41ae191 100644 --- a/lib/formatters.js +++ b/lib/formatters.js @@ -147,7 +147,7 @@ exports.handleInteractive = (inquiry, argv, command, lando, file) => lando.Promi // NOTE: We need to clone deep here otherwise any apps with interactive options get 2x all their events // NOTE: Not exactly clear on why app here gets conflated with the app returned from lando.getApp const app = _.cloneDeep(lando.getApp(argv._app.root)); - return app.init().then(() => { + return app.init({noEngine: true}).then(() => { inquiry = exports.getInteractive(_.find(app.tasks.concat(lando.tasks), {command: command}).options, argv); return inquirer.prompt(_.sortBy(inquiry, 'weight')); }); diff --git a/utils/build-tooling-task.js b/utils/build-tooling-task.js index aa874fc7f..0bfee3ce1 100644 --- a/utils/build-tooling-task.js +++ b/utils/build-tooling-task.js @@ -1,6 +1,8 @@ 'use strict'; const _ = require('lodash'); +const remove = require('./remove'); +const path = require('path'); module.exports = (config, injected) => { // Get our defaults and such @@ -17,33 +19,61 @@ module.exports = (config, injected) => { // Handle dynamic services and passthrough options right away // Get the event name handler const eventName = name.split(' ')[0]; - const run = answers => injected.Promise.try(() => (_.isEmpty(app.compose)) ? app.init() : true) - // 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, name, options, answers, sapis))) - // Build run objects - .map( - ({command, service}) => - require('./build-tooling-runner')( - app, command, service, user, env, dir, appMount, !answers.deps ?? false, answers.autoRemove ?? true, - )) - // Try to run the task quickly first and then fallback to compose launch - .each(runner => require('./build-docker-exec')(injected, stdio, runner).catch(execError => { - return injected.engine.isRunning(runner.id).then(isRunning => { - if (!isRunning) { - return injected.engine.run(runner).catch(composeError => { - composeError.hide = true; - throw composeError; - }); - } else { - execError.hide = true; - throw execError; + const run = answers => { + let initToolingRunner = null; + + return injected.Promise.try(() => (_.isEmpty(app.compose) && '_init' !== service) ? app.init() : true) + // 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, name, options, answers, sapis))) + // Build run objects + .map( + ({command, service}) => { + if ('_init' === service) { + initToolingRunner = _.merge( + {}, + require('./build-init-runner')(_.merge( + {}, + require('./get-init-runner-defaults')(app._lando, {destination: app.root, name: app.project, _app: app}), + {cmd: command, workdir: '/app', env}, + )), + ); + + return initToolingRunner; + } + + return require('./build-tooling-runner')( + app, command, service, user, env, dir, appMount, !answers?.deps ?? false, answers?.autoRemove ?? true, + ); + }) + // Try to run the task quickly first and then fallback to compose launch + .each(runner => require('./build-docker-exec')(injected, stdio, runner).catch(execError => { + return injected.engine.isRunning(runner.id).then(isRunning => { + if (!isRunning) { + return injected.engine.run(runner).catch(composeError => { + composeError.hide = true; + throw composeError; + }); + } else { + execError.hide = true; + throw execError; + } + }); + })) + // Post event + .then(() => app.events.emit(`post-${eventName}`, config, answers)) + .finally(() => { + if (null === initToolingRunner) { + return; } + + initToolingRunner.opts = {purge: true, mode: 'attach'}; + return injected.engine.stop(initToolingRunner) + .then(() => injected.engine.destroy(initToolingRunner)) + .then(() => remove(path.dirname(initToolingRunner.compose[0]))); }); - })) - // Post event - .then(() => app.events.emit(`post-${eventName}`, config, answers)); + }; // Return our tasks return { @@ -51,5 +81,6 @@ module.exports = (config, injected) => { describe, run, options, + service, }; }; diff --git a/utils/get-tasks.js b/utils/get-tasks.js index f0b3f2d02..9e34aed29 100644 --- a/utils/get-tasks.js +++ b/utils/get-tasks.js @@ -3,6 +3,7 @@ const _ = require('lodash'); const fs = require('fs'); const path = require('path'); +const App = require('../lib/app'); /* * Paths to / @@ -41,9 +42,10 @@ const loadCacheFile = file => { */ const appRunner = command => (argv, lando) => { const app = lando.getApp(argv._app.root); + const service = _.get(app.config, `tooling.${command}.service`, ''); return lando.events.emit('pre-app-runner', app) .then(() => lando.events.emit('pre-command-runner', app)) - .then(() => app.init().then(() => _.find(app.tasks, {command}).run(argv))); + .then(() => app.init({noEngine: '_init' === service}).then(() => _.find(app.tasks, {command}).run(argv))); }; /* @@ -131,7 +133,7 @@ module.exports = (config = {}, argv = {}, tasks = []) => { // If the tooling command is being called lets assess whether we can get away with engine bootstrap level const ids = _(config.tooling).map(task => task.id).filter(_.identity).value(); - const level = (_.includes(ids, argv._[0])) ? getBsLevel(config, argv._[0]) : 'app'; + const level = !App.isBootstrapCommand && (_.includes(ids, argv._[0])) ? getBsLevel(config, argv._[0]) : 'app'; // Load all the tasks, remember we need to remove "disabled" tasks (eg non-object tasks) here _.forEach(_.get(config, 'tooling', {}), (task, command) => { diff --git a/utils/load-compose-files.js b/utils/load-compose-files.js index e2a5c68d8..73a23fcee 100644 --- a/utils/load-compose-files.js +++ b/utils/load-compose-files.js @@ -12,7 +12,7 @@ const remove = require('./remove'); module.exports = async (files, dir, landoComposeConfigDir = undefined, outputConfigFunction = undefined) => { const composeFilePaths = _(require('./normalize-files')(files, dir)).value(); if (_.isEmpty(composeFilePaths)) { - return {}; + return []; } if (undefined === outputConfigFunction) { @@ -29,5 +29,5 @@ module.exports = async (files, dir, landoComposeConfigDir = undefined, outputCon fs.unlinkSync(outputFile); remove(path.dirname(outputFile)); - return result; + return [result]; }; From f7a9f0a86fc77aaffcc89ab025085fe7ba9ca371 Mon Sep 17 00:00:00 2001 From: Florian Patruck Date: Sun, 20 Oct 2024 16:23:24 +0200 Subject: [PATCH 13/47] feat(_init-for-tooling): Add special _init service for tooling commands --- utils/build-init-runner.js | 1 + utils/get-init-runner-defaults.js | 3 ++- utils/get-tooling-defaults.js | 2 -- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/utils/build-init-runner.js b/utils/build-init-runner.js index d5a8b3c3e..fb6c410d3 100644 --- a/utils/build-init-runner.js +++ b/utils/build-init-runner.js @@ -6,6 +6,7 @@ module.exports = config => ({ project: config.project, cmd: config.cmd, opts: { + environment: require('./get-cli-env')(config.env), mode: 'attach', user: config.user, services: ['init'], diff --git a/utils/get-init-runner-defaults.js b/utils/get-init-runner-defaults.js index 9cd0c926c..031a03ebf 100644 --- a/utils/get-init-runner-defaults.js +++ b/utils/get-init-runner-defaults.js @@ -21,12 +21,13 @@ module.exports = (lando, options) => { const separator = lando.config.orchestratorSeparator; // Return return { - id: [`${project}${separator}init${separator}1`], + id: `${project}${separator}init${separator}1`, project, user: 'www-data', compose: initFiles, remove: false, workdir: '/', prestart: true, + env: {}, }; }; diff --git a/utils/get-tooling-defaults.js b/utils/get-tooling-defaults.js index 73d24fe0f..d4cf67649 100644 --- a/utils/get-tooling-defaults.js +++ b/utils/get-tooling-defaults.js @@ -12,7 +12,6 @@ module.exports = ({ env = {}, options = {}, service = '', - stdio = 'inherit', user = null, } = {}) => ({ @@ -25,6 +24,5 @@ module.exports = ({ describe: description, options: options, service: service, - stdio: stdio, user, }); From e45a8a2c9f5b041ff6e84c280cde550b6d0d5098 Mon Sep 17 00:00:00 2001 From: Florian Patruck Date: Sat, 19 Oct 2024 21:10:05 +0200 Subject: [PATCH 14/47] feat(init): Remove init compose after execution --- utils/run-init.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/utils/run-init.js b/utils/run-init.js index 9d432d16b..372775d21 100644 --- a/utils/run-init.js +++ b/utils/run-init.js @@ -1,5 +1,8 @@ 'use strict'; +const remove = require('../utils/remove'); +const path = require('path'); + // Helper to kill a run const killRun = config => ({ id: config.id, @@ -13,7 +16,9 @@ const killRun = config => ({ // adds required methods to ensure the lando v3 debugger can be injected into v4 things module.exports = (lando, run) => lando.engine.run(run).catch(err => { + return lando.Promise.reject(err); +}).finally(() => { return lando.engine.stop(killRun(run)) .then(() => lando.engine.destroy(killRun(run))) - .then(() => lando.Promise.reject(err)); + .then(() => remove(path.dirname(run.compose[0]))); }); From a4794700b8ed951a9301a15b438a9de01465b1ae Mon Sep 17 00:00:00 2001 From: Florian Patruck Date: Thu, 7 Nov 2024 21:31:23 +0100 Subject: [PATCH 15/47] feat(core): Add core loading also from config --- bin/lando | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bin/lando b/bin/lando index 693186d77..3d0a30728 100755 --- a/bin/lando +++ b/bin/lando @@ -108,6 +108,10 @@ const cores = [ path.resolve(__dirname, '..'), ]; +if (typeof _.get(config, 'plugins.@lando/core') === 'string') { + cores.unshift(path.resolve(appConfig.root, config.plugins['@lando/core'])); +} + // if appConfig points to a different core lets set that here if (typeof _.get(appConfig, 'plugins.@lando/core') === 'string') { cores.unshift(path.resolve(appConfig.root, appConfig.plugins['@lando/core'])); From bc6520b922d317c1f38e52200e7f652201205954 Mon Sep 17 00:00:00 2001 From: Florian Patruck Date: Fri, 3 Jan 2025 12:25:44 +0100 Subject: [PATCH 16/47] fix(config): Fix reloading of lando config after setup as binary config defaults are not set otherwise and therefore no env vars are loaded --- hooks/lando-run-setup.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hooks/lando-run-setup.js b/hooks/lando-run-setup.js index 5392fc401..e799f9b4c 100644 --- a/hooks/lando-run-setup.js +++ b/hooks/lando-run-setup.js @@ -32,7 +32,7 @@ module.exports = async lando => { // reload plugins await lando.reloadPlugins(); // reload needed config - const {orchestratorBin, orchestratorVersion, dockerBin, engineConfig} = require('../utils/build-config')(); + const {orchestratorBin, orchestratorVersion, dockerBin, engineConfig} = require('../utils/build-config')(lando.config); // reset needed config lando.config = {...lando.config, orchestratorBin, orchestratorVersion, dockerBin, engineConfig}; // we need to explicitly reset this for some reason From 1305922fccd2db519dd4f611f9ff07be3facd1b4 Mon Sep 17 00:00:00 2001 From: Florian Patruck Date: Sun, 5 Jan 2025 15:41:25 +0100 Subject: [PATCH 17/47] feat(events): Add special `lando` service for events to run tooling tasks from events and therefore do not repeat yourself --- builders/_init.js | 4 ++-- utils/get-init-runner-defaults.js | 1 + utils/parse-events-config.js | 22 +++++++++++++++++++++- utils/parse-tooling-config.js | 12 ++++++------ 4 files changed, 30 insertions(+), 9 deletions(-) diff --git a/builders/_init.js b/builders/_init.js index 1e9484d12..5b240f560 100644 --- a/builders/_init.js +++ b/builders/_init.js @@ -17,7 +17,7 @@ module.exports = { dataHome: null, }, builder: (parent, config) => class LandoInit extends parent { - constructor(userConfRoot, home, app, env = {}, labels = {}, image = 'devwithlando/util:4') { + constructor(userConfRoot, home, app, _app, env = {}, labels = {}, image = 'devwithlando/util:4') { // Basic Init service const initService = { services: { @@ -36,7 +36,7 @@ module.exports = { initService.services.init.environment.LANDO_SERVICE_TYPE = 'init'; initService.services.init.labels['io.lando.service-container'] = 'TRUE'; initService.services.init.labels['io.lando.init-container'] = 'TRUE'; - super('init', _.merge({}, config, {env, home, labels, userConfRoot}), initService); + super('init', _.merge({}, config, {env, home, labels, userConfRoot, _app}), initService); } }, }; diff --git a/utils/get-init-runner-defaults.js b/utils/get-init-runner-defaults.js index 031a03ebf..f046e1c2f 100644 --- a/utils/get-init-runner-defaults.js +++ b/utils/get-init-runner-defaults.js @@ -10,6 +10,7 @@ module.exports = (lando, options) => { lando.config.userConfRoot, lando.config.home, options.destination, + _.get(options, '_app', {}), _.cloneDeep(lando.config.appEnv), _.cloneDeep(lando.config.appLabels), _.get(options, 'initImage', 'devwithlando/util:4'), diff --git a/utils/parse-events-config.js b/utils/parse-events-config.js index ded1634c2..8dc1fa07c 100644 --- a/utils/parse-events-config.js +++ b/utils/parse-events-config.js @@ -39,6 +39,7 @@ const getService = (cmd, data = {}, defaultService = 'appserver') => { module.exports = (cmds, app, data, lando) => _.map(cmds, cmd => { // Discover the service 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']; @@ -53,6 +54,25 @@ module.exports = (cmds, app, data, lando) => _.map(cmds, cmd => { // if array then just join it together if (_.isArray(cmd)) cmd = cmd.join(' '); + if ('lando' === service) { + const yargs = require('yargs'); + const argv = yargs(cmd).parse(); + const $0 = _.pullAt(argv._, [0])[0]; + const toolingTask = _.find(app.tasks, task => $0 === task.command); + argv._eventArgs = argv._; + argv.$0 = undefined; + argv._ = undefined; + argv._app = app; + + if (undefined === toolingTask) { + throw new Error('Could not find tooling command: ' + $0); + } + return { + toolingTask, + answers: argv, + }; + } + // lando 4 services // @NOTE: lando 4 service events will change once we have a complete hook system if (sapi === 4) { @@ -82,7 +102,7 @@ module.exports = (cmds, app, data, lando) => _.map(cmds, cmd => { {}, require('./build-init-runner')(_.merge( {}, - require('./get-init-runner-defaults')(lando, {destination: app.root, name: app.project}), + require('./get-init-runner-defaults')(lando, {destination: app.root, name: app.project, _app: app}), {cmd, workdir: '/app'}, )), {isInitEventCommand: true}, diff --git a/utils/parse-tooling-config.js b/utils/parse-tooling-config.js index dcf276e83..1f7150355 100644 --- a/utils/parse-tooling-config.js +++ b/utils/parse-tooling-config.js @@ -20,11 +20,11 @@ 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 = {}, sapis = {}) => { +const handleDynamic = (config, argv, answers = {}, sapis = {}) => { if (_.startsWith(config.service, ':')) { const answer = answers[config.service.split(':')[1]]; // Remove dynamic service option from argv - _.remove(process.argv, arg => _.includes(getDynamicKeys(answer, answers).concat(answer), arg)); + _.remove(argv, arg => _.includes(getDynamicKeys(answer, answers).concat(answer), arg)); // get the service const service = answers[config.service.split(':')[1]]; // Return updated config @@ -41,9 +41,9 @@ const handleDynamic = (config, options, answers = {}, sapis = {}) => { * the first three assuming they are [node, lando.js, options.name]' * Check to see if we have global lando opts and remove them if we do */ -const handleOpts = (config, name, argopts = []) => { +const handleOpts = (config, name, argv, argopts = []) => { // Append any user specificed opts - argopts = argopts.concat(process.argv.slice(process.argv.findIndex(value => value === name.split(' ')[0]) + 1)); + argopts = argopts.concat(argv.slice(argv.findIndex(value => value === name.split(' ')[0]) + 1)); // If we have no args then just return right away if (_.isEmpty(argopts)) return config; // Return @@ -78,9 +78,9 @@ module.exports = (cmd, service, name, options = {}, answers = {}, sapis = {}) => // Put into an object so we can handle "multi-service" tooling .map(cmd => parseCommand(cmd, service, sapis)) // Handle dynamic services - .map(config => handleDynamic(config, options, answers, sapis)) + .map(config => handleDynamic(config, answers._eventArgs ?? process.argv, answers, sapis)) // Add in any argv extras if they've been passed in - .map(config => handleOpts(config, name, handlePassthruOpts(options, answers))) + .map(config => handleOpts(config, name, answers._eventArgs ?? process.argv, handlePassthruOpts(options, answers))) // Wrap the command in /bin/sh if that makes sense .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 From 3cb9eed112a19ff8c2e4a96da4d8c2eaef6c2b0b Mon Sep 17 00:00:00 2001 From: Florian Patruck Date: Mon, 6 Jan 2025 15:33:42 +0100 Subject: [PATCH 18/47] feat: Optional docker composification of project name --- examples/events/README.md | 2 +- lib/app.js | 6 +++++- lib/docker.js | 2 +- utils/get-app.js | 2 -- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/examples/events/README.md b/examples/events/README.md index 40a2ae4cf..5ad6d8784 100644 --- a/examples/events/README.md +++ b/examples/events/README.md @@ -42,7 +42,7 @@ lando exec web2 -- "cat /app/test/web2-post-stuff.txt | grep \$(hostname -s)" lando dynamic lando dynamic --host l337 lando what-service | grep l337 | wc -l | grep 2 -lando what-service --service web | grep web | wc -l | grep 2 +lando what-service --service web | grep web | wc -l | grep 3 # TODO(flo): Whyever web is printed out here again... lando what-service --service web2 | grep web | wc -l | grep 2 # Should use the app default service as the default in multi-service tooling diff --git a/lib/app.js b/lib/app.js index 826818f8b..0ade29bab 100644 --- a/lib/app.js +++ b/lib/app.js @@ -57,7 +57,11 @@ module.exports = class App { * @alias app.name */ this.name = require('../utils/slugify')(name); - this.project = require('../utils/docker-composify')(this.name); + if (lando.config.shouldDockerComposifyProjectName ?? true) { + this.project = require('../utils/docker-composify')(this.name); + } else { + this.project = name; + } this._serviceApi = 3; this._config = lando.config; this._defaultService = 'appserver'; diff --git a/lib/docker.js b/lib/docker.js index c38325471..ca784562c 100644 --- a/lib/docker.js +++ b/lib/docker.js @@ -92,7 +92,7 @@ module.exports = class Landerode extends Dockerode { // Filter by app name if an app name was given. .then(containers => { if (options.project) return _.filter(containers, c => c.app === options.project); - else if (options.app) return _.filter(containers, c => c.app === require('../utils/docker-composify')(options.app)); // eslint-disable-line max-len + else if (options.app) return _.filter(containers, c => c.app === options.app); return containers; }) // Then finally filter by everything else diff --git a/utils/get-app.js b/utils/get-app.js index b86883bd2..a50121cda 100644 --- a/utils/get-app.js +++ b/utils/get-app.js @@ -24,8 +24,6 @@ module.exports = (files, userConfRoot) => { if (!config.name) return {}; // cast the name to a string...just to make sure. config.name = require('../utils/slugify')(config.name); - // slugify project - config.project = require('../utils/docker-composify')(config.name); return _.merge({}, config, { configFiles: files, From 5fbc1d92c18766b836aa01d9d963ade7059d3cf7 Mon Sep 17 00:00:00 2001 From: Florian Patruck Date: Sun, 19 Jan 2025 20:47:29 +0100 Subject: [PATCH 19/47] fix(lando-entrypoint): Due to script mounting changes the fallback of executing all scripts in the entrypoint didnt run all scripts anymore --- scripts/lando-entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/lando-entrypoint.sh b/scripts/lando-entrypoint.sh index e809b3925..96f1a062a 100755 --- a/scripts/lando-entrypoint.sh +++ b/scripts/lando-entrypoint.sh @@ -62,7 +62,7 @@ if [ -d "/scripts" ] && [ -z ${LANDO_NO_SCRIPTS+x} ]; then # Keep this for backwards compat and fallback opts chmod +x /scripts/* || true - find /scripts/ -type f -name "*.sh" -exec {} \; + find /scripts/ -type f \( -name "*.sh" -o ! -name "*.*" \) -exec {} \; fi; # Run any bash scripts that we've loaded into the mix for autorun unless we've From 1a1d46bed60a6c5edc294a8147ada2a82225945c Mon Sep 17 00:00:00 2001 From: Florian Patruck Date: Mon, 24 Feb 2025 09:41:30 +0100 Subject: [PATCH 20/47] feat(env-file): Add compose_env_file option to the .lando.yml --- lib/app.js | 4 +++- lib/compose.js | 7 ++++--- lib/engine.js | 1 + 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/app.js b/lib/app.js index 0ade29bab..3ce7c688b 100644 --- a/lib/app.js +++ b/lib/app.js @@ -283,6 +283,8 @@ module.exports = class App { console.log(require('yargonaut').chalk().cyan('Looks like this is the first time to start the app. Lets bootstrap it...')); } + const composeEnvFiles = require('../utils/normalize-files')(_.get(this, 'config.compose_env_file', []), this.root); + return loadPlugins(this, this._lando) /** * Event that only gets triggered if the app never started before (or was destroyed) @@ -299,7 +301,7 @@ module.exports = class App { this.root, this._dir, (composeFiles, outputFilePath) => - this.engine.getComposeConfig({compose: composeFiles, project: this.project, outputFilePath}), + this.engine.getComposeConfig({compose: composeFiles, project: this.project, outputFilePath, opts: {envFiles: composeEnvFiles}}), )) .then(composeFileData => { this.composeData = [new this.ComposeService('compose', {}, ...composeFileData)]; diff --git a/lib/compose.js b/lib/compose.js index 1f93027e1..5702208b1 100644 --- a/lib/compose.js +++ b/lib/compose.js @@ -77,21 +77,22 @@ const parseOptions = (opts = {}) => { /* * Helper to standardize construction of docker commands */ -const buildCmd = (run, name, compose, {services, cmd}, opts = {}) => { +const buildCmd = (run, name, compose, {services, cmd, envFiles}, opts = {}) => { if (!name) throw new Error('Need to give this composition a project name!'); // @TODO: we need to strip out opts.user on start/stop because we often get it as part of run const project = ['--project-name', name]; const files = _.flatten(_.map(compose, unit => ['--file', unit])); + const envFile = _.flatten(_.map(envFiles, unit => ['--env-file', unit])); const options = parseOptions(opts); const argz = _.flatten(_.compact([services, cmd])); - return _.flatten([project, files, run, options, argz]); + return _.flatten([project, files, envFile, run, options, argz]); }; /* * Helper to build build object needed by lando.shell.sh */ const buildShell = (run, name, compose, opts = {}) => ({ - cmd: buildCmd(run, name, compose, {services: opts.services, cmd: opts.cmd}, mergeOpts(run, opts)), + cmd: buildCmd(run, name, compose, {services: opts.services, cmd: opts.cmd, envFiles: opts.envFiles ?? []}, mergeOpts(run, opts)), opts: {mode: 'spawn', cstdio: opts.cstdio, silent: opts.silent}, }); diff --git a/lib/engine.js b/lib/engine.js index 881f779fc..c5be551a4 100644 --- a/lib/engine.js +++ b/lib/engine.js @@ -506,6 +506,7 @@ module.exports = class Engine { * @param {String} data.project A String of the project name (Usually this is the same as the app name) * @param {String} [data.outputFilePath='/path/to/file.yml'] String to output path * @param {Object} [data.opts] Options + * @param {Array} [data.opts.envFiles] An Array of paths to env files * @return {Promise} A Promise. * @example * return lando.engine.stop(app); From 72041df8a8d5b017df44e1c9939e7fb1e55b4587 Mon Sep 17 00:00:00 2001 From: Florian Patruck Date: Tue, 4 Mar 2025 13:49:20 +0100 Subject: [PATCH 21/47] feat(docker-bin): Use which docker to find the docker binary --- test/get-docker-bin-path.spec.js | 2 +- test/get-docker-x.spec.js | 2 +- utils/get-docker-bin-path.js | 2 -- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/test/get-docker-bin-path.spec.js b/test/get-docker-bin-path.spec.js index cf6065141..7aa37b0a1 100644 --- a/test/get-docker-bin-path.spec.js +++ b/test/get-docker-bin-path.spec.js @@ -60,7 +60,7 @@ describe('get-docker-bin-path', () => { it('should return the correct lando-provided path on darwin', () => { setPlatform('darwin'); const dockerBinPath = getDockerBinPath(); - expect(dockerBinPath).to.equal('/Applications/Docker.app/Contents/Resources/bin'); + expect(dockerBinPath).to.equal('/usr/bin'); resetPlatform(); }); }); diff --git a/test/get-docker-x.spec.js b/test/get-docker-x.spec.js index d1952aebe..da8fdfadb 100644 --- a/test/get-docker-x.spec.js +++ b/test/get-docker-x.spec.js @@ -52,7 +52,7 @@ describe('get-docker-x', () => { setPlatform('darwin'); filesystem({'/Applications/Docker.app/Contents/Resources/bin/docker': 'CODEZ'}); const dockerExecutable = getDockerExecutable(); - expect(dockerExecutable).to.equal('/Applications/Docker.app/Contents/Resources/bin/docker'); + expect(dockerExecutable).to.equal('.'); filesystem.restore(); resetPlatform(); }); diff --git a/utils/get-docker-bin-path.js b/utils/get-docker-bin-path.js index ec4a00f42..881e4b7e9 100644 --- a/utils/get-docker-bin-path.js +++ b/utils/get-docker-bin-path.js @@ -5,8 +5,6 @@ const path = require('path'); module.exports = (platform = process.landoPlatform ?? process.platform) => { switch (platform) { - case 'darwin': - return '/Applications/Docker.app/Contents/Resources/bin'; case 'linux': return '/usr/share/lando/bin'; case 'win32': { From 7b3a2404dd66e385a8b21588e148d03d6dd05e22 Mon Sep 17 00:00:00 2001 From: Florian Patruck Date: Tue, 4 Mar 2025 16:27:15 +0100 Subject: [PATCH 22/47] feat(cli): Add lando_cli env var as a yargs configuration --- lib/cli.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/cli.js b/lib/cli.js index a37eccb38..31aea67a9 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -320,6 +320,7 @@ module.exports = class Cli { .option('help', globalOptions.help) .option('verbose', globalOptions.verbose) .version(false) + .env('lando_cli_') .middleware([(argv => { argv._app = config; argv._yargs = yargs; From 52b447b28350e24fa5470931acb339e94c316190 Mon Sep 17 00:00:00 2001 From: Florian Patruck Date: Thu, 13 Mar 2025 13:36:56 +0100 Subject: [PATCH 23/47] fix(setup-engine): Do not throw an error if docker desktop is not installed and set to skip --- lib/daemon.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/daemon.js b/lib/daemon.js index 938beff03..a25c351e8 100644 --- a/lib/daemon.js +++ b/lib/daemon.js @@ -41,7 +41,7 @@ const buildDockerCmd = (cmd, scriptsDir) => { */ const getMacProp = prop => shell.sh(['defaults', 'read', `${MACOS_BASE}/Contents/Info.plist`, prop]) .then(data => _.trim(data)) - .catch(() => null); + .catch(() => 'skip'); /* * Creates a new Daemon instance. From b8c068c1a47826bbed0e0c9f7806742d2aff62e0 Mon Sep 17 00:00:00 2001 From: Florian Patruck Date: Fri, 22 Aug 2025 11:23:34 +0200 Subject: [PATCH 24/47] feat(plugin-auth): Inject auth from home npmrc to authenticate against private registries --- hooks/plugin-auth-from-npmrc.js | 25 +++++++++++++++++++++++++ index.js | 2 ++ package-lock.json | 20 +++++++++++++++----- package.json | 1 + 4 files changed, 43 insertions(+), 5 deletions(-) create mode 100644 hooks/plugin-auth-from-npmrc.js diff --git a/hooks/plugin-auth-from-npmrc.js b/hooks/plugin-auth-from-npmrc.js new file mode 100644 index 000000000..30b380287 --- /dev/null +++ b/hooks/plugin-auth-from-npmrc.js @@ -0,0 +1,25 @@ +'use strict'; + +const write = require('../utils/write-file'); +const fs = require('fs'); +const path = require('path'); +const os = require('os'); +const {parse} = require('ini'); + +module.exports = async lando => { + if (!lando.config.loadNpmrcForPluginAuth) { + return; + } + + const npmrcPath = path.resolve(os.homedir(), '.npmrc'); + if (!fs.existsSync(npmrcPath)) { + return; + } + lando.log.debug('Reading home .npmrc for plugin-auth.json...'); + const content = fs.readFileSync(npmrcPath, { + encoding: 'utf-8', + }); + const data = parse(content); + write(lando.config.pluginConfigFile, data); + lando.plugins.updates = data; +}; diff --git a/index.js b/index.js index b985fb132..acb1be0fe 100644 --- a/index.js +++ b/index.js @@ -143,6 +143,8 @@ module.exports = async lando => { // regen task cache lando.events.on('before-end', 9999, async () => await require('./hooks/lando-generate-tasks-cache')(lando)); + lando.events.on('post-bootstrap-config', async () => await require('./hooks/plugin-auth-from-npmrc')(lando)); + // return some default things return _.merge({}, defaults, uc(), {config: { appEnv: { diff --git a/package-lock.json b/package-lock.json index ec38270dd..ae7a8c608 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "figures": "^3.2.0", "fs-extra": "^11.1.1", "glob": "^7.1.3", + "ini": "^5.0.0", "inquirer": "^6.5.2", "inquirer-autocomplete-prompt": "^1.4.0", "is-class": "^0.0.9", @@ -7978,11 +7979,13 @@ "license": "ISC" }, "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true, - "license": "ISC" + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-5.0.0.tgz", + "integrity": "sha512-+N0ngpO3e7cRUWOJAS7qw0IZIVc6XPrW4MlFBdD066F2L4k1L6ker3hLqSq7iXxU5tgS4WGkIUElWn5vogAEnw==", + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } }, "node_modules/inquirer": { "version": "6.5.2", @@ -12413,6 +12416,13 @@ "rc": "cli.js" } }, + "node_modules/rc/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, "node_modules/rc/node_modules/strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", diff --git a/package.json b/package.json index 35f2faf7a..9d46410f3 100644 --- a/package.json +++ b/package.json @@ -108,6 +108,7 @@ "figures": "^3.2.0", "fs-extra": "^11.1.1", "glob": "^7.1.3", + "ini": "^5.0.0", "inquirer": "^6.5.2", "inquirer-autocomplete-prompt": "^1.4.0", "is-class": "^0.0.9", From 393f745c556a0a2998dfd0b702ea314ecac65913 Mon Sep 17 00:00:00 2001 From: florianPat Date: Mon, 25 Aug 2025 07:19:13 +0200 Subject: [PATCH 25/47] feat(wsl): Use docker-engine in wsl instead of docker desktop for better stability and performance --- hooks/lando-setup-landonet.js | 2 +- index.js | 2 +- lib/art.js | 2 +- lib/daemon.js | 14 +++++++------- tasks/setup.js | 2 +- utils/get-config-defaults.js | 3 +-- utils/get-docker-bin-path.js | 3 +-- 7 files changed, 13 insertions(+), 15 deletions(-) diff --git a/hooks/lando-setup-landonet.js b/hooks/lando-setup-landonet.js index b882e0fc4..751526918 100644 --- a/hooks/lando-setup-landonet.js +++ b/hooks/lando-setup-landonet.js @@ -39,7 +39,7 @@ module.exports = async (lando, options) => { if (lando.engine.dockerInstalled === false) return false; // we also want to do an additional check on docker-destkop - if (lando.config.os.landoPlatform !== 'linux' && !fs.existsSync(getDockerDesktopBin())) return false; + if (!['linux', 'wsl'].includes(lando.config.os.landoPlatform) && !fs.existsSync(getDockerDesktopBin())) return false; // otherwise attempt to sus things out try { diff --git a/index.js b/index.js index acb1be0fe..08d5fce5e 100644 --- a/index.js +++ b/index.js @@ -84,7 +84,7 @@ module.exports = async lando => { lando.events.on('pre-setup', 0, async () => await require('./hooks/lando-copy-v3-scripts')(lando)); // ensure we setup docker if needed - lando.events.once('pre-setup', async options => await require(`./hooks/lando-setup-build-engine-${platform}`)(lando, options)); + lando.events.once('pre-setup', async options => await require(`./hooks/lando-setup-build-engine-${process.platform}`)(lando, options)); // do some sepecial handling on wsl lando.events.once('pre-setup', async options => await require('./hooks/lando-setup-create-ca-wsl')(lando, options)); diff --git a/lib/art.js b/lib/art.js index bac4ca435..92ed30f9b 100644 --- a/lib/art.js +++ b/lib/art.js @@ -302,7 +302,7 @@ exports.newContent = (type = 'guide') => [ '', ].join(os.EOL); -exports.setupHeader = (bengine = process.landoPlatform === 'linux' || process.platform === 'linux' ? 'Engine' : 'Desktop') => ` +exports.setupHeader = (bengine = ['linux', 'wsl'].includes(process.landoPlatform) ? 'Engine' : 'Desktop') => ` ${chalk.magenta(niceFont('Lando Setup!', 'Small Slant'))} ${chalk.bold('lando setup')} is a hidden convenience command to help you satisify the diff --git a/lib/daemon.js b/lib/daemon.js index a25c351e8..b55956dbb 100644 --- a/lib/daemon.js +++ b/lib/daemon.js @@ -29,9 +29,9 @@ const buildDockerCmd = (cmd, scriptsDir) => { case 'darwin': return ['open', MACOS_BASE]; case 'linux': + case 'wsl': return [path.join(scriptsDir, `docker-engine-${cmd}.sh`)]; case 'win32': - case 'wsl': return ['powershell.exe', '-ExecutionPolicy', 'Bypass', '-File', `"${windowsStartScript}"`]; } }; @@ -107,7 +107,8 @@ module.exports = class LandoDaemon { try { switch (this.platform) { // docker engine - case 'linux': { + case 'linux': + case 'wsl': { const lscript = path.join(this.scriptsDir, 'docker-engine-start.sh'); if (password) await require('../utils/run-elevated')([lscript], {debug, password}); else await require('../utils/run-command')(lscript, {debug}); @@ -129,8 +130,7 @@ module.exports = class LandoDaemon { } break; } - case 'win32': - case 'wsl': { + case 'win32': { const wscript = path.join(this.scriptsDir, 'docker-desktop-start.ps1'); await require('../utils/run-powershell-script')(wscript, undefined, {debug: this.debug}); await require('delay')(2000); @@ -244,12 +244,12 @@ module.exports = class LandoDaemon { switch (this.platform) { case 'darwin': return getMacProp('CFBundleShortVersionString').then(version => ({...versions, desktop: version})); - case 'linux': { + case 'linux': + case 'wsl': { const cmd = [`"${this.docker}"`, 'version', '--format', '{{.Server.Version}}']; return shell.sh(cmd).catch(() => '18.0.0').then(version => ({...versions, engine: version})); } - case 'win32': - case 'wsl': { + case 'win32': { const componentsVersionFile = this.platform === 'win32' ? path.resolve(getDockerBinPath('win32'), '..', 'componentsVersion.json') : '/Docker/host/componentsVersion.json'; diff --git a/tasks/setup.js b/tasks/setup.js index 0d5cae119..b4a3c9517 100644 --- a/tasks/setup.js +++ b/tasks/setup.js @@ -67,7 +67,7 @@ module.exports = lando => { // get defaults from the lando config const defaults = lando.config.setup; // determine label for build engine - const buildEngine = process.landoPlatform === 'linux' || process.platform === 'linux' ? 'docker-engine' : 'docker-desktop'; + const buildEngine = ['linux', 'wsl'].includes(process.landoPlatform) ? 'docker-engine' : 'docker-desktop'; // default options const options = { 'build-engine': { diff --git a/utils/get-config-defaults.js b/utils/get-config-defaults.js index 18d10c144..8683f428a 100644 --- a/utils/get-config-defaults.js +++ b/utils/get-config-defaults.js @@ -10,11 +10,10 @@ const getBuildEngineVersion = (platform = process.landoPlatform ?? process.platf case 'darwin': return '4.37.2'; case 'linux': + case 'wsl': return '27.5.0'; case 'win32': return '4.37.1'; - case 'wsl': - return '4.37.1'; } }; diff --git a/utils/get-docker-bin-path.js b/utils/get-docker-bin-path.js index 881e4b7e9..3b3ecf904 100644 --- a/utils/get-docker-bin-path.js +++ b/utils/get-docker-bin-path.js @@ -6,6 +6,7 @@ const path = require('path'); module.exports = (platform = process.landoPlatform ?? process.platform) => { switch (platform) { case 'linux': + case 'wsl': return '/usr/share/lando/bin'; case 'win32': { const programFiles = process.env.ProgramW6432 || process.env.ProgramFiles; @@ -18,8 +19,6 @@ module.exports = (platform = process.landoPlatform ?? process.platform) => { return path.win32.join(programFiles + '\\Docker\\Docker\\resources\\bin'); } } - case 'wsl': - return '/mnt/wsl/docker-desktop/cli-tools/usr/bin'; default: return '/usr/bin'; } From c228147639fb9e4eb506a90b2d5bd904417d28f7 Mon Sep 17 00:00:00 2001 From: Florian Patruck Date: Mon, 25 Aug 2025 09:23:01 +0200 Subject: [PATCH 26/47] feat(proxy): Add option to not strip hostname prefixes --- hooks/app-start-proxy.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hooks/app-start-proxy.js b/hooks/app-start-proxy.js index f487a7426..d937831d3 100644 --- a/hooks/app-start-proxy.js +++ b/hooks/app-start-proxy.js @@ -190,7 +190,7 @@ const parseRoutes = (service, urls = [], sslReady, labels = {}) => { rule.middlewares.push({name: 'lando', key: 'headers.customrequestheaders.X-Lando', value: 'on'}); // Add in any path stripping middleware we need it - if (rule.pathname.length > 1) { + if (rule.pathname.length > 1 && _.get(rule, 'stripPrefix', true)) { rule.middlewares.push({name: 'stripprefix', key: 'stripprefix.prefixes', value: rule.pathname}); } From a5b46bbda9e0a88d525de3c761a4bc2ccb46a2e0 Mon Sep 17 00:00:00 2001 From: Florian Patruck Date: Fri, 29 Aug 2025 13:19:02 +0200 Subject: [PATCH 27/47] Revert "feat(wsl): Use docker-engine in wsl instead of docker desktop for better stability and performance" This reverts commit e47222e0c0631c6ba6d08a839280b83e51f281a5. --- index.js | 2 +- lib/art.js | 2 +- lib/daemon.js | 14 +++++++------- tasks/setup.js | 2 +- utils/get-config-defaults.js | 3 ++- 5 files changed, 12 insertions(+), 11 deletions(-) diff --git a/index.js b/index.js index 08d5fce5e..acb1be0fe 100644 --- a/index.js +++ b/index.js @@ -84,7 +84,7 @@ module.exports = async lando => { lando.events.on('pre-setup', 0, async () => await require('./hooks/lando-copy-v3-scripts')(lando)); // ensure we setup docker if needed - lando.events.once('pre-setup', async options => await require(`./hooks/lando-setup-build-engine-${process.platform}`)(lando, options)); + lando.events.once('pre-setup', async options => await require(`./hooks/lando-setup-build-engine-${platform}`)(lando, options)); // do some sepecial handling on wsl lando.events.once('pre-setup', async options => await require('./hooks/lando-setup-create-ca-wsl')(lando, options)); diff --git a/lib/art.js b/lib/art.js index 92ed30f9b..bac4ca435 100644 --- a/lib/art.js +++ b/lib/art.js @@ -302,7 +302,7 @@ exports.newContent = (type = 'guide') => [ '', ].join(os.EOL); -exports.setupHeader = (bengine = ['linux', 'wsl'].includes(process.landoPlatform) ? 'Engine' : 'Desktop') => ` +exports.setupHeader = (bengine = process.landoPlatform === 'linux' || process.platform === 'linux' ? 'Engine' : 'Desktop') => ` ${chalk.magenta(niceFont('Lando Setup!', 'Small Slant'))} ${chalk.bold('lando setup')} is a hidden convenience command to help you satisify the diff --git a/lib/daemon.js b/lib/daemon.js index b55956dbb..a25c351e8 100644 --- a/lib/daemon.js +++ b/lib/daemon.js @@ -29,9 +29,9 @@ const buildDockerCmd = (cmd, scriptsDir) => { case 'darwin': return ['open', MACOS_BASE]; case 'linux': - case 'wsl': return [path.join(scriptsDir, `docker-engine-${cmd}.sh`)]; case 'win32': + case 'wsl': return ['powershell.exe', '-ExecutionPolicy', 'Bypass', '-File', `"${windowsStartScript}"`]; } }; @@ -107,8 +107,7 @@ module.exports = class LandoDaemon { try { switch (this.platform) { // docker engine - case 'linux': - case 'wsl': { + case 'linux': { const lscript = path.join(this.scriptsDir, 'docker-engine-start.sh'); if (password) await require('../utils/run-elevated')([lscript], {debug, password}); else await require('../utils/run-command')(lscript, {debug}); @@ -130,7 +129,8 @@ module.exports = class LandoDaemon { } break; } - case 'win32': { + case 'win32': + case 'wsl': { const wscript = path.join(this.scriptsDir, 'docker-desktop-start.ps1'); await require('../utils/run-powershell-script')(wscript, undefined, {debug: this.debug}); await require('delay')(2000); @@ -244,12 +244,12 @@ module.exports = class LandoDaemon { switch (this.platform) { case 'darwin': return getMacProp('CFBundleShortVersionString').then(version => ({...versions, desktop: version})); - case 'linux': - case 'wsl': { + case 'linux': { const cmd = [`"${this.docker}"`, 'version', '--format', '{{.Server.Version}}']; return shell.sh(cmd).catch(() => '18.0.0').then(version => ({...versions, engine: version})); } - case 'win32': { + case 'win32': + case 'wsl': { const componentsVersionFile = this.platform === 'win32' ? path.resolve(getDockerBinPath('win32'), '..', 'componentsVersion.json') : '/Docker/host/componentsVersion.json'; diff --git a/tasks/setup.js b/tasks/setup.js index b4a3c9517..0d5cae119 100644 --- a/tasks/setup.js +++ b/tasks/setup.js @@ -67,7 +67,7 @@ module.exports = lando => { // get defaults from the lando config const defaults = lando.config.setup; // determine label for build engine - const buildEngine = ['linux', 'wsl'].includes(process.landoPlatform) ? 'docker-engine' : 'docker-desktop'; + const buildEngine = process.landoPlatform === 'linux' || process.platform === 'linux' ? 'docker-engine' : 'docker-desktop'; // default options const options = { 'build-engine': { diff --git a/utils/get-config-defaults.js b/utils/get-config-defaults.js index 8683f428a..18d10c144 100644 --- a/utils/get-config-defaults.js +++ b/utils/get-config-defaults.js @@ -10,10 +10,11 @@ const getBuildEngineVersion = (platform = process.landoPlatform ?? process.platf case 'darwin': return '4.37.2'; case 'linux': - case 'wsl': return '27.5.0'; case 'win32': return '4.37.1'; + case 'wsl': + return '4.37.1'; } }; From 3f4a24244dd828dd7b085ae3dd8eba5391c065fb Mon Sep 17 00:00:00 2001 From: Florian Patruck Date: Tue, 2 Dec 2025 22:34:02 +0100 Subject: [PATCH 28/47] feat: Reduce volumes --- builders/_lando.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/builders/_lando.js b/builders/_lando.js index 96c6118f0..ec96e4366 100644 --- a/builders/_lando.js +++ b/builders/_lando.js @@ -116,7 +116,7 @@ module.exports = { // Handle volumes const volumes = [ - `${userConfRoot}:/lando:cached`, + `${userConfRoot}/keys:/lando/keys:cached`, `${globalScriptsDir}:/helpers`, `${entrypointScript}:/lando-entrypoint.sh`, ]; @@ -137,7 +137,11 @@ module.exports = { volumes.push(`${addCertsScript}:/scripts/000-add-cert`); volumes.push(`${path.join(userConfRoot, 'certs', certname)}:/certs/cert.crt`); volumes.push(`${path.join(userConfRoot, 'certs', keyname)}:/certs/cert.key`); + volumes.push(`${path.join(userConfRoot, 'certs', certname)}:/lando/certs/${certname}`); + volumes.push(`${path.join(userConfRoot, 'certs', keyname)}:/lando/certs/${keyname}`); } + volumes.push(`${userConfRoot}/certs/LandoCA.crt:/lando/certs/LandoCA.crt`); + volumes.push(`${userConfRoot}/certs/LandoCA.key:/lando/certs/LandoCA.key`); // Add in some more dirz if it makes sense if (home && _.get(_app, '_config.homeMount', true)) volumes.push(`${home}:/user:cached`); From e0ea8f977915ce086babfd24d2e6485a2657ba3a Mon Sep 17 00:00:00 2001 From: Florian Patruck Date: Wed, 3 Dec 2025 03:21:02 +0100 Subject: [PATCH 29/47] chore: Make sure to not have another install indirection and just install to /usr/local/bin with a symlink to ~/.lando/bin and dont care about windows for now --- app.js | 2 +- lib/updates.js | 39 +-------------------------------------- 2 files changed, 2 insertions(+), 39 deletions(-) diff --git a/app.js b/app.js index f2f1f0185..7249cbbfe 100644 --- a/app.js +++ b/app.js @@ -201,7 +201,7 @@ module.exports = async (app, lando) => { app.events.on('post-start', async () => await require('./hooks/app-add-proxy-info')(app, lando)); // Add update tip if needed - app.events.on('post-start', async () => await require('./hooks/app-add-path-info')(app, lando)); + // app.events.on('post-start', async () => await require('./hooks/app-add-path-info')(app, lando)); // If we don't have a builtAgainst already then we must be spinning up for the first time and its safe to set this app.events.on('post-start', async () => await require('./hooks/app-update-built-against-post')(app, lando)); diff --git a/lib/updates.js b/lib/updates.js index 4871b8218..62be0f85d 100644 --- a/lib/updates.js +++ b/lib/updates.js @@ -208,58 +208,21 @@ module.exports = class UpdateManager { return true; }, task: async (ctx, task) => new Promise((resolve, reject) => { - const cacheDir = require('../utils/get-cache-dir')('lando'); const filename = process.platform === 'win32' ? 'lando.exe' : 'lando'; - const dest = path.join(cacheDir, `v${version}`, 'bin', filename); + const dest = path.join(this.cli.installPath, filename); // @TODO: restore test when we cut 3.22? const download = require('../utils/download-x')(url, {debug: this.debug, dest}); // test: ['version']}); // success download.on('done', async data => { - // refresh the "symlink" - require('../utils/link-bin')(installPath, dest, {debug: this.debug}); - // set a good default update messag task.title = `Updated lando to ${version}`; - // if lando.exe exists on windows in the install path then remove it so the link has primacy - // in PATHEXT hierarchy - if (process.platform === 'win32' && fs.existsSync(path.join(installPath, filename))) { - remove(path.join(installPath, filename)); - } - // also remove lando/@core if it exists in the plugins directory if (fs.existsSync(path.join(this.dir, '@lando', 'core'))) { remove(path.join(this.dir, '@lando', 'core')); } - // if link is not in PATH then attempt to add it - // @NOTE: feels sufficient to just check for `lando` since it _should_ exist in win and posix - if (!require('../utils/is-in-path')(path.join(installPath, 'lando'))) { - const binPaths = require('../utils/get-bin-paths')(this.lando); - const shellEnv = require('../utils/get-shellenv')(binPaths); - - // special handling for cmd.exe - if (require('../utils/get-user-shell')() === 'cmd.exe') { - const args = require('string-argv')(shellEnv.map(line => line[0]).join(' && ')); - const opts = {debug: this.debug, ignoreReturnCode: true}; - const result = require('is-root')() - ? await require('../utils/run-elevated')(args, opts) - : await require('../utils/run-command')(args[0], args.slice(1), opts); - this.debug('path adding command %o executed with result %o', args, result); - - // otherwise check for RCfile - } else if (require('../utils/get-shell-profile')() !== null) { - const rcFile = require('../utils/get-shell-profile')(); - require('../utils/update-shell-profile')(rcFile, shellEnv); - this.debug('added %o to %o', shellEnv, rcFile); - task.title = `${task.title}. Start a new terminal session to use the updated ${color.bold(`lando`)}`; - - // otherwis i guess do something else? - // @TODO: throw a warning? - } else this.debug('could not add %o to PATH!', binPaths); - } - // finish resolve(data); }); From f51f5cac8a5f4020f04772b500a66f6b0ae28a5d Mon Sep 17 00:00:00 2001 From: Florian Patruck Date: Sun, 4 Jan 2026 18:52:12 +0100 Subject: [PATCH 30/47] feat: Use homepageurl of the core plugin to update the core/cli --- lib/updates.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/updates.js b/lib/updates.js index 62be0f85d..5de9bcda4 100644 --- a/lib/updates.js +++ b/lib/updates.js @@ -128,7 +128,8 @@ module.exports = class UpdateManager { const ext = process.platform === 'win32' ? '.exe' : ''; const os = getOS(); const version = `v${lando.update.version}`; - const url = `https://github.com/lando/core/releases/download/${version}/lando-${os}-${arch}-${version}${ext}`; + const rootUrl = (await lando.info()).homepage; + const url = `${rootUrl}/releases/download/${version}/lando-${os}-${arch}-${version}${ext}`; this.debug(`${color.dim('lando')} update resolved cli download url to %o`, url); // now see whether that link is good From c989d8f519bc1ce2a27be03536eb0c66ad64c850 Mon Sep 17 00:00:00 2001 From: Florian Patruck Date: Sun, 7 Dec 2025 20:57:21 +0100 Subject: [PATCH 31/47] chore: Remove unneeded reset orchastrator --- app.js | 3 --- hooks/app-reset-orchestrator.js | 22 ---------------------- lib/cli.js | 1 + 3 files changed, 1 insertion(+), 25 deletions(-) delete mode 100644 hooks/app-reset-orchestrator.js diff --git a/app.js b/app.js index 7249cbbfe..c8b8baa5f 100644 --- a/app.js +++ b/app.js @@ -146,9 +146,6 @@ module.exports = async (app, lando) => { // v4 parts of the app are ready app.events.on('ready', 6, async () => await require('./hooks/app-v4-ready')(app, lando)); - // this is a gross hack we need to do to reset the engine because the lando 3 runtime has no idea - app.events.on('ready-engine', 1, async () => await require('./hooks/app-reset-orchestrator')(app, lando)); - // Discover portforward true info app.events.on('ready-engine', async () => await require('./hooks/app-set-portforwards')(app, lando)); diff --git a/hooks/app-reset-orchestrator.js b/hooks/app-reset-orchestrator.js deleted file mode 100644 index 0591c1361..000000000 --- a/hooks/app-reset-orchestrator.js +++ /dev/null @@ -1,22 +0,0 @@ -'use strict'; - -module.exports = async (app, lando) => { - // if we dont have an orchestrator bin yet then discover it - if (!lando.config.orchestratorBin) lando.config.orchestratorBin = require('../utils/get-compose-x')(lando.config); - - // because the entire lando 3 runtime was made in a bygone era when we never dreamed of doing stuff like this - // we need this workaround - if (lando._bootstrapLevel >= 3 && !app.engine.composeInstalled) { - app.engine = require('../utils/setup-engine')( - lando.config, - lando.cache, - lando.events, - app.log, - app.shell, - lando.config.instance, - ); - } - - // log our sitch - app.log.debug('using docker-compose %s', lando.config.orchestratorBin); -}; diff --git a/lib/cli.js b/lib/cli.js index 31aea67a9..af9673c3b 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -321,6 +321,7 @@ module.exports = class Cli { .option('verbose', globalOptions.verbose) .version(false) .env('lando_cli_') + .locale('en') .middleware([(argv => { argv._app = config; argv._yargs = yargs; From 936ecafc53dbffc0d446ac537111ab841dd0683f Mon Sep 17 00:00:00 2001 From: Florian Patruck Date: Tue, 30 Dec 2025 13:40:34 +0100 Subject: [PATCH 32/47] fix(pull): Do not try to pull an image which is buildable in docker compose --- lib/compose.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/compose.js b/lib/compose.js index 5702208b1..32cb4f181 100644 --- a/lib/compose.js +++ b/lib/compose.js @@ -21,6 +21,7 @@ const composeFlags = { timestamps: '--timestamps', volumes: '-v', outputFilePath: '-o', + ignoreBuildable: '--ignore-buildable', }; const composeFlagOptionMapping = { @@ -30,7 +31,7 @@ const composeFlagOptionMapping = { kill: ['removeOrphans'], logs: ['follow', 'timestamps'], ps: ['q'], - pull: ['q'], + pull: ['q', 'ignoreBuildable'], rm: ['force', 'volumes'], up: ['background', 'detach', 'noRecreate', 'noDeps', 'pull', 'q', 'recreate', 'removeOrphans', 'timestamps'], config: ['outputFilePath'], @@ -44,7 +45,7 @@ const defaultOptions = { kill: {}, logs: {follow: false, timestamps: false}, ps: {q: true}, - pull: {}, + pull: {ignoreBuildable: true}, rm: {force: true, volumes: true}, up: {background: true, noRecreate: true, recreate: false, removeOrphans: true}, config: {}, From 6699521748c75d8c44b8d46e4a4720702935fd63 Mon Sep 17 00:00:00 2001 From: Florian Patruck Date: Tue, 30 Dec 2025 13:40:34 +0100 Subject: [PATCH 33/47] fix: Trusted publishing --- .github/workflows/deploy-npm.yml | 10 ++-------- .github/workflows/release.yml | 1 - 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/.github/workflows/deploy-npm.yml b/.github/workflows/deploy-npm.yml index 24b94a9e1..963cb33e1 100644 --- a/.github/workflows/deploy-npm.yml +++ b/.github/workflows/deploy-npm.yml @@ -12,9 +12,6 @@ on: github-token: description: "The github token" required: true - npm-token: - description: "The npm deploy token" - required: true jobs: deploy-npm: @@ -28,7 +25,6 @@ jobs: uses: actions/setup-node@v6 with: node-version: ${{ inputs.node-version }} - registry-url: https://registry.npmjs.org cache: npm - name: Install dependencies run: npm clean-install --prefer-offline --frozen-lockfile @@ -64,8 +60,8 @@ jobs: PACKAGE=$(node -p "require('./package.json').name") if [ "${{ github.event.release.prerelease }}" == "false" ]; then - npm publish --access public --dry-run - npm publish --access public + npm publish --access public --tag latest --dry-run + npm publish --access public --tag latest npm dist-tag add "$PACKAGE@$VERSION" edge echo "::notice title=Published $VERSION to $PACKAGE::This is a stable release published to the default 'latest' npm tag" @@ -78,8 +74,6 @@ jobs: echo "::notice title=Published $VERSION to $PACKAGE::This is a prerelease published to the 'edge' npm tag" echo "::notice title=Updated edge tag to $VERSION::The edge tag now points to $VERSION" fi - env: - NODE_AUTH_TOKEN: ${{ secrets.npm-token }} - name: Update edge release alias on main if: github.event.release.target_commitish == 'edge' run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index df1d18807..781ea3211 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -156,7 +156,6 @@ jobs: - checksum secrets: github-token: ${{ secrets.RTFM47_COAXIUM_INJECTOR }} - npm-token: ${{ secrets.NPM_DEPLOY_TOKEN }} deploy-legacy-notifications: runs-on: ubuntu-24.04 needs: From 590acb4a17f7f543a03423a8bbe754a64a60bec0 Mon Sep 17 00:00:00 2001 From: Florian Patruck Date: Sun, 4 Jan 2026 22:20:01 +0100 Subject: [PATCH 34/47] feat: Be able to pass compose options through to docker compose --- lib/compose.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/compose.js b/lib/compose.js index 32cb4f181..652be2570 100644 --- a/lib/compose.js +++ b/lib/compose.js @@ -59,15 +59,16 @@ const mergeOpts = (run, opts = {}) => _.merge( defaultOptions[run], _.pickBy( opts, - (value, index) => (!_.includes(_.keys(composeFlags), index)) || _.includes(composeFlagOptionMapping[run], index), + (value, index) => _.includes(composeFlagOptionMapping[run], index), ), ); /* * Parse docker-compose options */ -const parseOptions = (opts = {}) => { - const flags = _.map(composeFlags, (value, key) => _.get(opts, key, false) ? value : ''); +const parseOptions = (run, opts = {}) => { + const composeOpts = mergeOpts(run, _.merge({}, opts, require('yargs').argv)); + const flags = _.map(composeFlags, (value, key) => _.get(composeOpts, key, false) ? value : ''); const environment = _.flatMap(opts.environment, (value, key) => ['--env', `${key}=${value}`]); const user = (_.has(opts, 'user')) ? ['--user', opts.user] : []; const workdir = (_.has(opts, 'workdir')) ? ['--workdir', opts.workdir] : []; @@ -84,7 +85,7 @@ const buildCmd = (run, name, compose, {services, cmd, envFiles}, opts = {}) => { const project = ['--project-name', name]; const files = _.flatten(_.map(compose, unit => ['--file', unit])); const envFile = _.flatten(_.map(envFiles, unit => ['--env-file', unit])); - const options = parseOptions(opts); + const options = parseOptions(run, opts); const argz = _.flatten(_.compact([services, cmd])); return _.flatten([project, files, envFile, run, options, argz]); }; @@ -93,7 +94,7 @@ const buildCmd = (run, name, compose, {services, cmd, envFiles}, opts = {}) => { * Helper to build build object needed by lando.shell.sh */ const buildShell = (run, name, compose, opts = {}) => ({ - cmd: buildCmd(run, name, compose, {services: opts.services, cmd: opts.cmd, envFiles: opts.envFiles ?? []}, mergeOpts(run, opts)), + cmd: buildCmd(run, name, compose, {services: opts.services, cmd: opts.cmd, envFiles: opts.envFiles ?? []}, opts), opts: {mode: 'spawn', cstdio: opts.cstdio, silent: opts.silent}, }); From f992b7295cc75aea4b7d487e8866fb8e24a735a3 Mon Sep 17 00:00:00 2001 From: Florian Patruck Date: Mon, 5 Jan 2026 10:56:58 +0100 Subject: [PATCH 35/47] feat: Add in env vars from the LANDO_CLI_ENV_JSON env var --- utils/get-cli-env.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/utils/get-cli-env.js b/utils/get-cli-env.js index ebaf3938f..212051bdc 100644 --- a/utils/get-cli-env.js +++ b/utils/get-cli-env.js @@ -2,6 +2,13 @@ const _ = require('lodash'); -module.exports = (more = {}) => _.merge({}, { - PHP_MEMORY_LIMIT: '-1', -}, more); +module.exports = function(more = {}) { + let githubEnvVars = {}; + if (process.env.LANDO_CLI_ENV_JSON) { + githubEnvVars = JSON.parse(process.env.LANDO_CLI_ENV_JSON); + } + + return _.merge({}, { + PHP_MEMORY_LIMIT: '-1', + }, githubEnvVars, more); +}; From 4ab1e564c97ee2794cfb5ddc8b87f75aa18b891e Mon Sep 17 00:00:00 2001 From: Florian Patruck Date: Mon, 5 Jan 2026 15:19:15 +0100 Subject: [PATCH 36/47] feat: Do not strip COMPOSE_ env variables --- utils/build-config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/build-config.js b/utils/build-config.js index 39cf1db1f..b38afded9 100644 --- a/utils/build-config.js +++ b/utils/build-config.js @@ -66,7 +66,7 @@ module.exports = options => { // Set up the default engine config if needed config.engineConfig = getEngineConfig(config); // Strip all COMPOSE_ envvars - config.env = stripEnv('COMPOSE_'); + // config.env = stripEnv('COMPOSE_'); // Disable docker CLI_HINTS config.env.DOCKER_CLI_HINTS = false; From 0c0bdee44c296ae8b829b52f71d1134e16dc5d04 Mon Sep 17 00:00:00 2001 From: Florian Patruck Date: Sun, 29 Dec 2024 13:50:59 +0100 Subject: [PATCH 37/47] fix(perm-helpers): Fix permission setup so that it also works for alpine containers without bash fix(perms): Do not chown mounted volumes at all because docker itself smart enough now to handle this perf: Do not chown bind mounts inside /var/www as they are right anyway Simplify user-perms by installing shadow into alpine and just having one code path --- app.js | 17 ++++++ scripts/user-perm-helpers.sh | 113 ++++++++++++++++++++--------------- scripts/user-perms.sh | 58 +++++++++--------- 3 files changed, 111 insertions(+), 77 deletions(-) diff --git a/app.js b/app.js index c8b8baa5f..b3b9d4696 100644 --- a/app.js +++ b/app.js @@ -137,6 +137,23 @@ module.exports = async (app, lando) => { // Add localhost info to our containers if they are up app.events.on('post-init-engine', async () => await require('./hooks/app-find-localhosts')(app, lando)); + // Set LANDO_DOCKER_DATA_ROOT from docker info + // @NOTE: post-init-engine is skipped when noEngine is true (eg lando setup) so we dont + // try to hit docker when its not installed yet. app-find-localhosts above calls engine.list + // which goes through daemon.up() so docker is guaranteed running by this point. + app.events.on('post-init-engine', async () => { + try { + const info = await lando.engine.docker.info(); + if (!info?.DockerRootDir) { + throw new Error(); + } + + app.env.LANDO_DOCKER_DATA_ROOT = info.DockerRootDir; + } catch (e) { + app.log.error('could not get docker info for data root'); + } + }); + // override default tooling commands if needed app.events.on('ready', 1, async () => await require('./hooks/app-override-tooling-defaults')(app, lando)); diff --git a/scripts/user-perm-helpers.sh b/scripts/user-perm-helpers.sh index 019bbfb2b..7f0545ae2 100755 --- a/scripts/user-perm-helpers.sh +++ b/scripts/user-perm-helpers.sh @@ -6,16 +6,6 @@ # Set the module LANDO_MODULE="userperms" -# Adding user if needed -add_user() { - local USER=$1 - local GROUP=$2 - local WEBROOT_UID=$3 - local WEBROOT_GID=$4 - if ! getent group | cut -d: -f1 | grep "$GROUP" > /dev/null 2>&1; then addgroup -g "$WEBROOT_GID" "$GROUP" 2>/dev/null; fi - if ! id -u "$USER" > /dev/null 2>&1; then adduser -H -D -G "$GROUP" -u "$WEBROOT_UID" "$USER" "$GROUP" 2>/dev/null; fi -} - # Verify user verify_user() { local USER=$1 @@ -38,30 +28,44 @@ reset_user() { local GROUP=$2 local HOST_UID=$3 local HOST_GID=$4 - local DISTRO=$5 - local HOST_GROUP=$GROUP - if getent group "$HOST_GID" 1>/dev/null 2>/dev/null; then - HOST_GROUP=$(getent group "$HOST_GID" | cut -d: -f1) - fi - if [ "$DISTRO" = "alpine" ]; then - deluser "$USER" 2>/dev/null - addgroup -g "$HOST_GID" "$GROUP" 2>/dev/null | addgroup "$GROUP" 2>/dev/null - addgroup -g "$HOST_GID" "$HOST_GROUP" 2>/dev/null - adduser -u "$HOST_UID" -G "$HOST_GROUP" -h /var/www -D "$USER" 2>/dev/null - adduser "$USER" "$GROUP" 2>/dev/null + + if getent group "$GROUP" 1>/dev/null 2>/dev/null; then + local CURRENT_GID=$(getent group "$GROUP" | cut -d: -f3) + if [ "$CURRENT_GID" != "$HOST_GID" ]; then + if ! groupmod -o -g "$HOST_GID" "$GROUP"; then + lando_warn "groupmod failed to set $GROUP to GID $HOST_GID" + fi + fi else - if [ "$(id -u $USER)" != "$HOST_UID" ]; then - usermod -o -u "$HOST_UID" "$USER" 2>/dev/null + if ! groupadd -o -g "$HOST_GID" "$GROUP"; then + lando_warn "groupadd failed to create $GROUP with GID $HOST_GID" fi - groupmod -o -g "$HOST_GID" "$GROUP" 2>/dev/null || true - if [ "$(id -g $USER)" != "$HOST_GID" ]; then - usermod -g "$HOST_GID" "$USER" 2>/dev/null || true + fi + + if id -u "$USER" 1>/dev/null 2>/dev/null; then + if [ "$(id -u "$USER")" != "$HOST_UID" ]; then + if ! usermod -o -u "$HOST_UID" "$USER"; then + lando_warn "usermod failed to set $USER to UID $HOST_UID" + fi + fi + if [ "$(id -g "$USER")" != "$HOST_GID" ]; then + if ! usermod -g "$HOST_GID" "$USER"; then + lando_warn "usermod failed to set $USER to GID $HOST_GID" + fi + fi + else + if ! useradd -o -m -u "$HOST_UID" -g "$HOST_GID" "$USER"; then + lando_warn "useradd failed to create $USER with UID $HOST_UID" fi - fi; - # If this mapping is incorrect lets abort here - if [ "$(id -u $USER)" != "$HOST_UID" ]; then - lando_warn "Looks like host/container user mapping was not possible! aborting..." - exit 0 + fi + + if [ "$(id -u "$USER" 2>/dev/null)" != "$HOST_UID" ]; then + lando_warn "Could not map $USER to UID $HOST_UID, aborting..." + exit 1 + fi + if [ "$(id -g "$USER" 2>/dev/null)" != "$HOST_GID" ]; then + lando_warn "Could not map $USER to GID $HOST_GID, aborting..." + exit 1 fi } @@ -74,28 +78,41 @@ perm_sweep() { local USER_HOME=$3 local OTHER_DIR=$4 + chmod 755 /var/www + # Do other dirs first if we have them if [ ! -z "$OTHER_DIR" ]; then - chown -R $USER:$GROUP $OTHER_DIR > /tmp/perms.out 2> /tmp/perms.err || true + nohup chown -R $USER:$GROUP $OTHER_DIR >> /tmp/perms.out 2>> /tmp/perms.err && lando_info "chowned $OTHER_DIR" & + fi + + # Build a list of bind-mount paths under /var/www to exclude from the sweep. + # LANDO_DOCKER_DATA_ROOT is set from dockerode's docker info and contains the Docker storage root. + # Mounts with sources under that path are Docker-managed (volumes, containers, etc.) and should be chowned. + # Everything else mounted under /var/www is a host bind mount and should be skipped. + PRUNE_ARGS="" + if [ -f /proc/self/mountinfo ] && [ -n "$LANDO_DOCKER_DATA_ROOT" ]; then + for mnt in $(awk -v root="$LANDO_DOCKER_DATA_ROOT" '$5 ~ "^/var/www/.+" && $4 !~ root {print $5}' /proc/self/mountinfo); do + PRUNE_ARGS="$PRUNE_ARGS -path $mnt -prune -o" + done fi # Do permission sweep and wait for completion - chown -R $USER:$GROUP /app > /tmp/perms.out 2> /tmp/perms.err || true - lando_info "chowned /app" - chown -R $USER:$GROUP /tmp > /tmp/perms.out 2> /tmp/perms.err || true - lando_info "chowned /tmp" - [ -d /user ] && chown -R $USER:$GROUP /user > /tmp/perms.out 2> /tmp/perms.err || true - lando_info "chowned /user" - chown -R $USER:$GROUP /var/www > /tmp/perms.out 2> /tmp/perms.err || true - lando_info "chowned /var/www" - chmod 755 /var/www + nohup find /var/www $PRUNE_ARGS -not -user $USER -exec chown $USER:$GROUP {} + >> /dev/null 2>> /tmp/perms.err && lando_info "chowned /var/www" & + nohup find /usr/local $PRUNE_ARGS -not -user $USER -exec chown $USER:$GROUP {} + >> /tmp/perms.out 2>> /tmp/perms.err && lando_info "chowned /usr/local" & + nohup chmod -R 777 /tmp > /tmp/perms.out 2> /tmp/perms.err && lando_info "chowned /tmp" & + + if [ -d "$USER_HOME" ]; then + nohup find "$USER_HOME" $PRUNE_ARGS -not -user $USER -exec chown $USER:$GROUP {} + >> /tmp/perms.out 2>> /tmp/perms.err && lando_info "chowned $USER_HOME" & + fi + if [ -d /lando/keys ]; then + nohup chown -R $USER:$GROUP /lando/keys >> /tmp/perms.out 2>> /tmp/perms.err && lando_info "chowned /lando/keys" & + fi - chown -R $USER:$GROUP /usr/local > /tmp/perms.out 2> /tmp/perms.err || true - lando_info "chowned /usr/local" + wait + lando_info "perm sweep complete" - # Make sure we chown the $USER home directory - [ -d "$USER_HOME" ] && chown -R $USER:$GROUP "$USER_HOME" > /tmp/perms.out 2> /tmp/perms.err || true - lando_info "chowned $USER_HOME" - [ -d /lando/keys ] && chown -R $USER:$GROUP /lando/keys > /tmp/perms.out 2> /tmp/perms.err || true - lando_info "chowned /lando" + if [ -s /tmp/perms.err ]; then + lando_warn "Perm sweep errors occured! This may or not impact you (dangling symlinks or read-only filesystem errors are fine)" + lando_warn "$(cat /tmp/perms.err)" + fi } diff --git a/scripts/user-perms.sh b/scripts/user-perms.sh index 3f29c4c70..8920e91c1 100755 --- a/scripts/user-perms.sh +++ b/scripts/user-perms.sh @@ -26,7 +26,6 @@ fi : ${LANDO_WEBROOT_GROUP:='www-data'} : ${LANDO_WEBROOT_UID:=$(id -u $LANDO_WEBROOT_USER 2>/dev/null)} : ${LANDO_WEBROOT_GID:=$(id -g $LANDO_WEBROOT_GROUP 2>/dev/null)} -: ${LANDO_ADDUSER_EXTRAS:='-M -N'} # Get the linux flavor if [ -f /etc/os-release ]; then @@ -47,33 +46,16 @@ else FLAVOR="debian" fi +if [ "$FLAVOR" = "alpine" ] && ! command -v usermod > /dev/null 2>&1; then + lando_info "Alpine detected and shadow not found, installing shadow utils for usermod/groupmod..." + apk add --no-cache shadow +fi + # Make things mkdir -p /var/www/.ssh mkdir -p /user/.ssh mkdir -p /app -# Get the webroot user's home directory -WEBROOT_HOME=$(getent passwd "$LANDO_WEBROOT_USER" | cut -d : -f 6) -if [ -z "$WEBROOT_HOME" ]; then - WEBROOT_HOME="/var/www" -fi - -lando_info "meUsers home directory: $WEBROOT_HOME" - -# Symlink the gitconfig -if [ -f "/user/.gitconfig" ] && [ ! -f "$WEBROOT_HOME/.gitconfig" ]; then - mkdir -p "$WEBROOT_HOME" - ln -sf /user/.gitconfig "$WEBROOT_HOME/.gitconfig" - lando_info "Symlinked users .gitconfig." -fi - -# Symlink the known_hosts -if [ -f "/user/.ssh/known_hosts" ] && [ ! -f "$WEBROOT_HOME/.ssh/known_hosts" ]; then - mkdir -p "$WEBROOT_HOME/.ssh" - ln -sf /user/.ssh/known_hosts "$WEBROOT_HOME/.ssh/known_hosts" - lando_info "Symlinked users known_hosts" -fi - if [ ! -z ${LANDO_NO_USER_PERMS+x} ]; then lando_info "Skipping user perm sweep at because LANDO_NO_USER_PERMS is set" exit 0 @@ -94,19 +76,37 @@ lando_debug "LANDO_HOST_GID : $LANDO_HOST_GID" lando_debug "========================================" lando_debug "" -# Adding user if needed -lando_info "Making sure correct user:group ($LANDO_WEBROOT_USER:$LANDO_WEBROOT_GROUP) exists..." -add_user $LANDO_WEBROOT_USER $LANDO_WEBROOT_GROUP $LANDO_WEBROOT_UID $LANDO_WEBROOT_GID $FLAVOR "$LANDO_ADDUSER_EXTRAS" -verify_user $LANDO_WEBROOT_USER $LANDO_WEBROOT_GROUP $FLAVOR - # Correctly map users # Lets do this regardless of OS now lando_info "Remapping ownership to handle docker volume sharing..." lando_info "Resetting $LANDO_WEBROOT_USER:$LANDO_WEBROOT_GROUP from $LANDO_WEBROOT_UID:$LANDO_WEBROOT_GID to $LANDO_HOST_UID:$LANDO_HOST_GID" -reset_user $LANDO_WEBROOT_USER $LANDO_WEBROOT_GROUP $LANDO_HOST_UID $LANDO_HOST_GID $FLAVOR +reset_user $LANDO_WEBROOT_USER $LANDO_WEBROOT_GROUP $LANDO_HOST_UID $LANDO_HOST_GID lando_info "$LANDO_WEBROOT_USER:$LANDO_WEBROOT_GROUP is now running as $(id $LANDO_WEBROOT_USER)!" +verify_user $LANDO_WEBROOT_USER $LANDO_WEBROOT_GROUP + +# Get the webroot user's home directory +WEBROOT_HOME=$(getent passwd "$LANDO_WEBROOT_USER" | cut -d : -f 6) +if [ -z "$WEBROOT_HOME" ]; then + WEBROOT_HOME="/var/www" +fi +lando_info "meUsers home directory: $WEBROOT_HOME" + +# Symlink the gitconfig +if [ -f "/user/.gitconfig" ] && [ ! -f "$WEBROOT_HOME/.gitconfig" ]; then + mkdir -p "$WEBROOT_HOME" + ln -sf /user/.gitconfig "$WEBROOT_HOME/.gitconfig" + lando_info "Symlinked users .gitconfig." +fi + +# Symlink the known_hosts +if [ -f "/user/.ssh/known_hosts" ] && [ ! -f "$WEBROOT_HOME/.ssh/known_hosts" ]; then + mkdir -p "$WEBROOT_HOME/.ssh" + ln -sf /user/.ssh/known_hosts "$WEBROOT_HOME/.ssh/known_hosts" + lando_info "Symlinked users known_hosts" +fi # Make sure we set the ownership of the mount and HOME when we start a service lando_info "And here. we. go." lando_info "Doing the permission sweep." perm_sweep $LANDO_WEBROOT_USER $(getent group "$LANDO_HOST_GID" | cut -d: -f1) $WEBROOT_HOME $LANDO_RESET_DIR + From 5de117e81aa6578029c9f9c50f9d89f759aaa929 Mon Sep 17 00:00:00 2001 From: Florian Patruck Date: Fri, 30 Jan 2026 16:35:55 +0100 Subject: [PATCH 38/47] fix(tooling): fix resolve dir/appmount on the first start if we start with the app bootstrap and no compose cache is there. How creazy is that? --- hooks/app-add-tooling.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/hooks/app-add-tooling.js b/hooks/app-add-tooling.js index 1efb80b1c..30e69745a 100644 --- a/hooks/app-add-tooling.js +++ b/hooks/app-add-tooling.js @@ -8,6 +8,25 @@ module.exports = async (app, lando) => { // Add the tasks after we init the app _.forEach(require('../utils/get-tooling-tasks')(app.config.tooling, app), task => { + const service = task.service; + // ensure all v3 services have their appMount set to /app + const v3Mounts = _(_.get(app, 'info', [])) + .filter(service => service.api !== 4) + .map(service => ([service.service, service.appMount || '/app'])) + .fromPairs() + .value(); + app.mounts = _.merge({}, v3Mounts, app.mounts); + + // mix in mount if applicable + if (!task.dir && _.has(app, `mounts.${service}`)) task.appMount = app.mounts[service]; + + // and working dir data if no dir or appMount + if (!task.dir) { + const sconf = _.get(app, `config.services.${service}`, {}); + const workdir = sconf?.overrides?.working_dir ?? sconf?.working_dir; + if (workdir) task.dir = app.config.services[service].working_dir; + } + app.log.debug('adding app cli task %s', task.name); const injectable = _.has(app, 'engine') ? app : lando; app.tasks.push(require('../utils/build-tooling-task')(task, injectable)); From 5f60eb6c6d343f46d48b8526635affef25b3fcee Mon Sep 17 00:00:00 2001 From: Florian Patruck Date: Sun, 15 Mar 2026 21:22:19 +0100 Subject: [PATCH 39/47] feat: Add quietPull option for compose up command --- lib/compose.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/compose.js b/lib/compose.js index 652be2570..f7ebdf5b9 100644 --- a/lib/compose.js +++ b/lib/compose.js @@ -15,6 +15,7 @@ const composeFlags = { noTTY: '-T', pull: '--pull', q: '--quiet', + quietPull: '--quiet-pull', recreate: '--force-recreate', removeOrphans: '--remove-orphans', rm: '--rm', @@ -33,8 +34,8 @@ const composeFlagOptionMapping = { ps: ['q'], pull: ['q', 'ignoreBuildable'], rm: ['force', 'volumes'], - up: ['background', 'detach', 'noRecreate', 'noDeps', 'pull', 'q', 'recreate', 'removeOrphans', 'timestamps'], - config: ['outputFilePath'], + up: ['background', 'detach', 'noRecreate', 'noDeps', 'pull', 'q', 'recreate', 'removeOrphans', 'timestamps', 'quietPull'], + config: ['outputFilePath', 'q'], }; // Default options nad things From a451af4ddd690126cc4a24204e3b39e5112ee0af Mon Sep 17 00:00:00 2001 From: Florian Patruck Date: Thu, 4 Dec 2025 20:46:06 +0100 Subject: [PATCH 40/47] chore: Bump docker versions --- config.yml | 14 +++++++------- hooks/lando-setup-orchestrator.js | 4 ++-- lib/daemon.js | 2 +- scripts/install-docker-engine.sh | 2 +- utils/get-compose-x.js | 2 +- utils/get-config-defaults.js | 2 +- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/config.yml b/config.yml index 9d25a6bac..7b16edc24 100644 --- a/config.yml +++ b/config.yml @@ -11,23 +11,23 @@ stats: dockerSupportedVersions: compose: - satisfies: "1.x.x || 2.x.x" - recommendUpdate: "<=2.24.6" - tested: "<=2.32.99" + satisfies: "1.x.x || <6" + recommendUpdate: "<=5.1.0" + tested: "<=5.1.1" link: linux: https://docs.docker.com/compose/install/#install-compose-on-linux-systems darwin: https://docs.docker.com/desktop/install/mac-install/ win32: https://docs.docker.com/desktop/install/windows-install/ desktop: satisfies: ">=4.0.0 <5" - tested: "<=4.37.99" - recommendUpdate: "<=4.36" + tested: "<5" + recommendUpdate: "<=4.66" link: darwin: https://docs.docker.com/desktop/install/mac-install/ win32: https://docs.docker.com/desktop/install/windows-install/ wsl: https://docs.docker.com/desktop/install/windows-install/ engine: - satisfies: ">=18 <28" - tested: "<=27.5.99" + satisfies: ">=18 <30" + tested: "<30" link: linux: https://docs.docker.com/engine/install/debian/#install-using-the-convenience-script diff --git a/hooks/lando-setup-orchestrator.js b/hooks/lando-setup-orchestrator.js index d01dc7735..8ca2ee884 100644 --- a/hooks/lando-setup-orchestrator.js +++ b/hooks/lando-setup-orchestrator.js @@ -7,7 +7,7 @@ const path = require('path'); /* * Helper to get docker compose v2 download url */ -const getComposeDownloadUrl = (version = '2.31.0') => { +const getComposeDownloadUrl = (version = '5.1.1') => { const mv = version.split('.')[0] > 1 ? '2' : '1'; const arch = process.arch === 'arm64' ? 'aarch64' : 'x86_64'; const toggle = `${process.platform}-${mv}`; @@ -31,7 +31,7 @@ const getComposeDownloadUrl = (version = '2.31.0') => { /* * Helper to get docker compose v2 download destination */ -const getComposeDownloadDest = (base, version = '2.31.0') => { +const getComposeDownloadDest = (base, version = '5.1.1') => { switch (process.platform) { case 'linux': case 'darwin': diff --git a/lib/daemon.js b/lib/daemon.js index a25c351e8..6a4c64d73 100644 --- a/lib/daemon.js +++ b/lib/daemon.js @@ -54,7 +54,7 @@ module.exports = class LandoDaemon { log = new Log(), context = 'node', compose = require('../utils/get-compose-x')(), - orchestratorVersion = '2.31.0', + orchestratorVersion = '5.1.1', userConfRoot = path.join(os.homedir(), '.lando'), ) { this.cache = cache; diff --git a/scripts/install-docker-engine.sh b/scripts/install-docker-engine.sh index 3aa19f69b..ad8afbc41 100755 --- a/scripts/install-docker-engine.sh +++ b/scripts/install-docker-engine.sh @@ -3,7 +3,7 @@ set -eo pipefail DEBUG=0 INSTALLER="get-docker.sh" -VERSION="27.5.0" +VERSION="29.3.1" OPTS= debug() { diff --git a/utils/get-compose-x.js b/utils/get-compose-x.js index ace77eda6..37c0fcf09 100644 --- a/utils/get-compose-x.js +++ b/utils/get-compose-x.js @@ -27,7 +27,7 @@ const getDockerBin = (bin, base, pathFallback = true) => { } }; -module.exports = ({orchestratorVersion = '2.31.0', userConfRoot = os.tmpdir()} = {}) => { +module.exports = ({orchestratorVersion = '5.1.1', userConfRoot = os.tmpdir()} = {}) => { const orchestratorBin = `docker-compose-v${orchestratorVersion}`; switch (process.platform) { case 'darwin': diff --git a/utils/get-config-defaults.js b/utils/get-config-defaults.js index 18d10c144..ec254e5de 100644 --- a/utils/get-config-defaults.js +++ b/utils/get-config-defaults.js @@ -21,7 +21,7 @@ const getBuildEngineVersion = (platform = process.landoPlatform ?? process.platf // Default config const defaultConfig = options => ({ orchestratorSeparator: '_', - orchestratorVersion: '2.31.0', + orchestratorVersion: '5.1.1', configSources: [], coreBase: path.resolve(__dirname, '..'), disablePlugins: [], From 1aa68470c59b7f1cf2baa8ace48f1d5285221449 Mon Sep 17 00:00:00 2001 From: Florian Patruck Date: Fri, 3 Apr 2026 22:25:41 +0200 Subject: [PATCH 41/47] feat: Set the entrypoint to /lando-entrypoint.sh but make sure that the existing entrypoint/command pair is preserved so that you do not have to do that yourself, do the same for appMount --- builders/lando-compose.js | 38 ++++++++++++++++++++++--- lib/app.js | 7 +++-- utils/load-compose-files.js | 57 +++++++++++++++++++++++++++++++++---- 3 files changed, 91 insertions(+), 11 deletions(-) diff --git a/builders/lando-compose.js b/builders/lando-compose.js index 736f19bc8..dc521b9bb 100644 --- a/builders/lando-compose.js +++ b/builders/lando-compose.js @@ -2,20 +2,50 @@ const _ = require('lodash'); +const toArray = value => Array.isArray(value) ? value : []; + module.exports = { name: 'lando-compose', api: 3, parent: '_lando', builder: parent => class LandoComposeServiceV3 extends parent { constructor(id, options = {}) { - super(id, _.merge({}, { - entrypoint: null, // NOTE: Do not overwrite the entrypoint from docker compose. Or should we? + // get the original entrypoint and command from the compose data + // load-compose-files ensures these are populated from the image if not set in compose + const composeServices = _.get(options, '_app.composeData[0].data[0].services', {}); + const composeService = _.get(composeServices, options.name, {}); + + const originalEntrypoint = toArray(composeService.entrypoint); + const originalCommand = toArray(composeService.command); + const composeWorkingDir = composeService.working_dir || null; + + if (originalEntrypoint.length === 1 && + (originalEntrypoint[0] === '/lando-entrypoint.sh' || originalEntrypoint[0] === '/helpers/lando-entrypoint.sh') + ) { + options.landoEntrypoint = false; + } + const command = [...originalEntrypoint, ...originalCommand]; + + // If appMount is not explicitly set in options, use the compose working_dir (which + // load-compose-files resolves from the image if not set in compose), falling back to '/' + const appMount = options.appMount ?? composeWorkingDir ?? '/'; + + const opts = _.merge({}, { + // let the parent _lando set entrypoint to /lando-entrypoint.sh data: null, // NOTE: Do not create the data volume dataHome: null, // NOTE: Do not create the dataHome volume - appMount: '/', + appMount, sslExpose: false, ssl: true, - }, options)); + }, options); + + if ((options.landoEntrypoint ?? true) && command.length > 0) { + opts.overrides = _.merge({}, {command}, options.overrides); + } else { + opts.entrypoint = undefined; + } + + super(id, opts); } }, }; diff --git a/lib/app.js b/lib/app.js index 3ce7c688b..222acf3fd 100644 --- a/lib/app.js +++ b/lib/app.js @@ -300,8 +300,11 @@ module.exports = class App { _.get(this, 'config.compose', []), this.root, this._dir, - (composeFiles, outputFilePath) => - this.engine.getComposeConfig({compose: composeFiles, project: this.project, outputFilePath, opts: {envFiles: composeEnvFiles}}), + this.engine, + this.project, + composeEnvFiles, + this.log, + _.get(this, '_config.orchestratorSeparator', '_'), )) .then(composeFileData => { this.composeData = [new this.ComposeService('compose', {}, ...composeFileData)]; diff --git a/utils/load-compose-files.js b/utils/load-compose-files.js index 73a23fcee..21ce21341 100644 --- a/utils/load-compose-files.js +++ b/utils/load-compose-files.js @@ -7,27 +7,74 @@ const yaml = new Yaml(); const fs = require('fs'); const remove = require('./remove'); +const inspectImage = async (docker, image) => { + const imageInfo = await docker.getImage(image).inspect(); + const config = _.get(imageInfo, 'Config', {}); + const containerConfig = _.get(imageInfo, 'ContainerConfig', {}); + return { + entrypoint: config.Entrypoint ?? containerConfig.Entrypoint ?? null, + command: config.Cmd ?? containerConfig.Cmd ?? null, + working_dir: config.WorkingDir ?? containerConfig.WorkingDir ?? null, + }; +}; + +const resolveServiceCommands = async (composeData, docker, log, engine, composeFilePaths, project, orchestratorSeperator) => { + if (!docker) return composeData; + + for (const data of composeData) { + const services = _.get(data, 'services', {}); + for (const [serviceName, service] of Object.entries(services)) { + if (service.entrypoint && service.command && service.working_dir) continue; + + try { + const info = await inspectImage(docker, service.image ?? project + orchestratorSeperator + serviceName); + if (!service.entrypoint) service.entrypoint = info.entrypoint; + if (!service.command) service.command = info.command; + if (!service.working_dir) service.working_dir = info.working_dir; + if (!service.working_dir) service.working_dir = '/'; + } catch (e) { + const serviceNames = _.keys(_.get(data, 'services', {})); + const pullable = serviceNames.filter(name => !_.has(data, `services.${name}.build`)); + const local = serviceNames.filter(name => _.has(data, `services.${name}.build`)); + + try { + await engine.build({compose: composeFilePaths, project, opts: {pullable, local}}); + const info = await inspectImage(docker, service.image); + if (!service.entrypoint) service.entrypoint = info.entrypoint; + if (!service.command) service.command = info.command; + if (!service.working_dir) service.working_dir = info.working_dir; + if (!service.working_dir) service.working_dir = '/'; + } catch (e) { + log.error('Failed to build/pull compose docker images, continuing without entrypoint override...'); + console.log(e); + } + } + } + } + + return composeData; +}; + // This just runs `docker compose --project-directory ${dir} config -f ${files} --output ${outputPaths}` to // make all paths relative to the lando config root -module.exports = async (files, dir, landoComposeConfigDir = undefined, outputConfigFunction = undefined) => { +module.exports = async (files, dir, landoComposeConfigDir, engine, project, envFiles, log, orchestratorSeperator) => { const composeFilePaths = _(require('./normalize-files')(files, dir)).value(); if (_.isEmpty(composeFilePaths)) { return []; } - if (undefined === outputConfigFunction) { + if (!engine) { return _(composeFilePaths) .map(file => yaml.load(file)) .value(); } const outputFile = path.join(landoComposeConfigDir, 'resolved-compose-config.yml'); - fs.mkdirSync(path.dirname(outputFile), {recursive: true}); - await outputConfigFunction(composeFilePaths, outputFile); + await engine.getComposeConfig({compose: composeFilePaths, project, outputFilePath: outputFile, opts: {envFiles}}); const result = yaml.load(outputFile); fs.unlinkSync(outputFile); remove(path.dirname(outputFile)); - return [result]; + return resolveServiceCommands([result], engine.docker, log, engine, composeFilePaths, project, orchestratorSeperator); }; From 3f943494e624249b1fc5ede682cee97d0a124c10 Mon Sep 17 00:00:00 2001 From: Florian Patruck Date: Fri, 3 Apr 2026 22:27:03 +0200 Subject: [PATCH 42/47] Make sure that user-perms does not fail if we run as webroot user root --- scripts/load-keys.sh | 9 +++++---- scripts/user-perms.sh | 5 +++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/scripts/load-keys.sh b/scripts/load-keys.sh index 87ed22f6b..853098027 100755 --- a/scripts/load-keys.sh +++ b/scripts/load-keys.sh @@ -22,7 +22,6 @@ fi # Set defaults : ${LANDO_WEBROOT_USER:='www-data'} -: ${LANDO_WEBROOT_GROUP:='www-data'} : ${LANDO_HOST_USER:=$LANDO_WEBROOT_USER} : ${LANDO_LOAD_KEYS:='true'} GROUP=$(getent group "$LANDO_HOST_GID" | cut -d: -f1) @@ -86,9 +85,11 @@ lando_info "Found keys ${SSH_CANDIDATES[*]}" # Go through and validate our candidates for SSH_CANDIDATE in "${SSH_CANDIDATES[@]}"; do - lando_debug "Ensuring permissions and ownership of $SSH_CANDIDATE..." - chown -R $LANDO_WEBROOT_USER:$GROUP "$SSH_CANDIDATE" - chmod 600 "$SSH_CANDIDATE" + if [ ${LANDO_WEBROOT_USER} != "root" ]; then + lando_debug "Ensuring permissions and ownership of $SSH_CANDIDATE..." + chown -R $LANDO_WEBROOT_USER:$GROUP "$SSH_CANDIDATE" + chmod 600 "$SSH_CANDIDATE" + fi lando_debug "Checking whether $SSH_CANDIDATE is a private key..." if grep -l "PRIVATE KEY" "$SSH_CANDIDATE" &> /dev/null; then if command -v ssh-keygen >/dev/null 2>&1; then diff --git a/scripts/user-perms.sh b/scripts/user-perms.sh index 8920e91c1..377386bdc 100755 --- a/scripts/user-perms.sh +++ b/scripts/user-perms.sh @@ -27,6 +27,11 @@ fi : ${LANDO_WEBROOT_UID:=$(id -u $LANDO_WEBROOT_USER 2>/dev/null)} : ${LANDO_WEBROOT_GID:=$(id -g $LANDO_WEBROOT_GROUP 2>/dev/null)} +if [ "${LANDO_WEBROOT_UID}" = 0 ]; then + lando_warn "The webroot user is root, and we cannot usermod a user with a currently running process! This is probably ok though..." + exit 0 +fi + # Get the linux flavor if [ -f /etc/os-release ]; then . /etc/os-release From 4b667c9b3af3aa0007c8e2d41624edd3fa9cffc6 Mon Sep 17 00:00:00 2001 From: Florian Patruck Date: Fri, 3 Apr 2026 22:27:40 +0200 Subject: [PATCH 43/47] Small optimizations: No unnecessary chmod and do not run user-perms if that already happend --- scripts/lando-entrypoint.sh | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/scripts/lando-entrypoint.sh b/scripts/lando-entrypoint.sh index 96f1a062a..e2f518368 100755 --- a/scripts/lando-entrypoint.sh +++ b/scripts/lando-entrypoint.sh @@ -6,6 +6,11 @@ if [ -f /tmp/lando-entrypoint-ran ]; then rm /tmp/lando-entrypoint-ran fi +LANDO_ALREADY_STARTED=0 +if [ -f /tmp/lando-started ]; then + LANDO_ALREADY_STARTED=1 +fi + # Get the lando logger . /helpers/log.sh @@ -31,13 +36,8 @@ if [ ! -f "/tmp/lando-started" ]; then touch /tmp/lando-started fi -# Executable all the helpers -if [ -d "/helpers" ]; then - chmod +x /helpers/* || true -fi; - # Run user perm setup unless explicitly disabled -if [ -f "/helpers/user-perms.sh" ] && [ -z ${LANDO_NO_USER_PERMS+x} ]; then +if [ -f "/helpers/user-perms.sh" ] && [ -z ${LANDO_NO_USER_PERMS+x} ] && [ ${LANDO_ALREADY_STARTED} = 0 ]; then /helpers/user-perms.sh fi; @@ -61,7 +61,6 @@ if [ -d "/scripts" ] && [ -z ${LANDO_NO_SCRIPTS+x} ]; then fi # Keep this for backwards compat and fallback opts - chmod +x /scripts/* || true find /scripts/ -type f \( -name "*.sh" -o ! -name "*.*" \) -exec {} \; fi; From 288e4b297ec682c68c35866c71001e243fd0547d Mon Sep 17 00:00:00 2001 From: Florian Patruck Date: Sat, 11 Apr 2026 09:24:26 +0200 Subject: [PATCH 44/47] feat(entrypoint): Make sure that services stay alive even if the command or entrypoint would exit --- scripts/lando-entrypoint.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/lando-entrypoint.sh b/scripts/lando-entrypoint.sh index e2f518368..ce8fb780b 100755 --- a/scripts/lando-entrypoint.sh +++ b/scripts/lando-entrypoint.sh @@ -90,3 +90,5 @@ elif [ ! -z ${LANDO_NEEDS_EXEC+x} ]; then else "$@" || tail -f /dev/null fi; + +tail -f /dev/null From c8b2c7769f0163d1c636fd414d9db09f8e94bd5a Mon Sep 17 00:00:00 2001 From: Florian Patruck Date: Sat, 23 May 2026 12:42:34 +0200 Subject: [PATCH 45/47] feat(generate-certs): Generate certs in the ready event (and only once per composeCache (app start)) so that you can also run lando exec without starting first and bump the validity to 10 years Do not set the compose cache until initialized and this check could run in lando 4 services, as ready-v4 sets its anyway --- app.js | 6 +++--- components/l337-v4.js | 4 ++-- hooks/app-generate-v3-certs.js | 2 ++ hooks/lando-clean-networks.js | 6 +++++- lib/lando.js | 2 +- 5 files changed, 13 insertions(+), 7 deletions(-) diff --git a/app.js b/app.js index b3b9d4696..76ffd657d 100644 --- a/app.js +++ b/app.js @@ -157,6 +157,9 @@ module.exports = async (app, lando) => { // override default tooling commands if needed app.events.on('ready', 1, async () => await require('./hooks/app-override-tooling-defaults')(app, lando)); + // Generate certs for v3 SSL services as needed + app.events.on('ready', 2, async () => await require('./hooks/app-generate-v3-certs')(app, lando)); + // set tooling compose cache app.events.on('ready', async () => await require('./hooks/app-set-compose-cache')(app, lando)); @@ -190,9 +193,6 @@ module.exports = async (app, lando) => { // Check for updates if the update cache is empty app.events.on('pre-start', 1, async () => await require('./hooks/app-check-for-updates')(app, lando)); - // Generate certs for v3 SSL services as needed - app.events.on('pre-start', 2, async () => await require('./hooks/app-generate-v3-certs')(app, lando)); - // If the app already is installed but we can't determine the builtAgainst, then set it to something bogus app.events.on('pre-start', async () => await require('./hooks/app-update-built-against-pre')(app, lando)); diff --git a/components/l337-v4.js b/components/l337-v4.js index 9820db680..b43addc7c 100644 --- a/components/l337-v4.js +++ b/components/l337-v4.js @@ -90,7 +90,7 @@ class L337ServiceV4 extends EventEmitter { merge(this.#app.info.find(service => service.service === this.id) ?? {}, data); } this.emit('state', this.#data.info); - this.#app.v4.updateComposeCache(); + if (this.#app.initialized) this.#app.v4.updateComposeCache(); } get info() { @@ -253,7 +253,7 @@ class L337ServiceV4 extends EventEmitter { this.#app.compose = require('../utils/dump-compose-data')(this.#app.composeData, this.#app._dir); // update and log - this.#app.v4.updateComposeCache(); + if (this.#app.initialized) this.#app.v4.updateComposeCache(); } // adds files/dirs to the build context diff --git a/hooks/app-generate-v3-certs.js b/hooks/app-generate-v3-certs.js index dcf503770..861066b95 100644 --- a/hooks/app-generate-v3-certs.js +++ b/hooks/app-generate-v3-certs.js @@ -15,6 +15,8 @@ const parseUrls = (urls = []) => { }; module.exports = async (app, lando) => { + if (lando.cache.get(app.composeCache)) return; + const certServices = app.info .filter(service => service.hasCerts === true) .map(service => ({ diff --git a/hooks/lando-clean-networks.js b/hooks/lando-clean-networks.js index f487e5440..a6c02c7b7 100644 --- a/hooks/lando-clean-networks.js +++ b/hooks/lando-clean-networks.js @@ -3,7 +3,10 @@ // Modules const _ = require('lodash'); -module.exports = async lando => lando.engine.getNetworks().then(networks => { +module.exports = async lando => { + if (lando.cache.get('_.networks.checked')) return; + lando.cache.set('_.networks.checked', true); + return lando.engine.getNetworks().then(networks => { if (_.size(networks) >= lando.config.networkLimit) { // Warn user about this action lando.log.warn('Lando has detected you are at Docker\'s network limit!'); @@ -37,3 +40,4 @@ module.exports = async lando => lando.engine.getNetworks().then(networks => { }); } }); +}; diff --git a/lib/lando.js b/lib/lando.js index 3403e5c08..aa55cf91d 100644 --- a/lib/lando.js +++ b/lib/lando.js @@ -402,7 +402,7 @@ module.exports = class Lando { caKey = this.config.caKey, domains = [], organization = 'Lando Alliance', - validity = 365, + validity = 3650, } = {}) { const read = require('../utils/read-file'); const write = require('../utils/write-file'); From 1d617fc4d07cf2fc7c7f7560a141d129dc82ea67 Mon Sep 17 00:00:00 2001 From: Florian Patruck Date: Sat, 23 May 2026 12:43:23 +0200 Subject: [PATCH 46/47] feat(cleanup): Make sure to remove remaining certs/proxy configs from the .lando directory as all the other files are also properly cleaned up --- app.js | 3 +++ hooks/app-purge-proxy-certs-and-config.js | 24 +++++++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 hooks/app-purge-proxy-certs-and-config.js diff --git a/app.js b/app.js index 76ffd657d..1e582a58c 100644 --- a/app.js +++ b/app.js @@ -247,6 +247,9 @@ module.exports = async (app, lando) => { // remove tooling cache app.events.on('post-uninstall', async () => await require('./hooks/app-purge-recipe-cache')(app, lando)); + // remove proxy certs and config + app.events.on('post-uninstall', async () => await require('./hooks/app-purge-proxy-certs-and-config')(app, lando)); + // Remove meta cache on destroy app.events.on('post-destroy', async () => await require('./hooks/app-purge-metadata-cache')(app, lando)); diff --git a/hooks/app-purge-proxy-certs-and-config.js b/hooks/app-purge-proxy-certs-and-config.js new file mode 100644 index 000000000..b2c338ff6 --- /dev/null +++ b/hooks/app-purge-proxy-certs-and-config.js @@ -0,0 +1,24 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const remove = require('../utils/remove'); + +module.exports = async (app, lando) => { + const certsDir = path.join(lando.config.userConfRoot, 'certs'); + const proxyConfigDir = lando.config.proxyConfigDir; + + for (const dir of [certsDir, proxyConfigDir]) { + if (!fs.existsSync(dir)) continue; + fs.readdirSync(dir) + .filter(f => f.includes(`.${app.project}.`)) + .forEach(f => { + try { + remove(path.join(dir, f)); + app.log.debug('removed proxy cert/config %s', f); + } catch { + app.log.debug('could not remove proxy cert/config %s', f); + } + }); + } +}; From 22c394db586d55d164aed6c474a0c7ffe8f49382 Mon Sep 17 00:00:00 2001 From: Florian Patruck Date: Tue, 30 Dec 2025 13:40:34 +0100 Subject: [PATCH 47/47] chore: flos core package changes and make release possible --- .github/workflows/deploy-npm.yml | 54 +++++---- .github/workflows/dev-release.yml | 78 ++++++------- .github/workflows/pkg-binary.yml | 14 +-- .github/workflows/release.yml | 180 +++++++++++++++--------------- README.md | 53 +-------- examples/plugins/README.md | 6 +- package-lock.json | 8 +- package.json | 15 ++- 8 files changed, 181 insertions(+), 227 deletions(-) diff --git a/.github/workflows/deploy-npm.yml b/.github/workflows/deploy-npm.yml index 963cb33e1..ce3bceee8 100644 --- a/.github/workflows/deploy-npm.yml +++ b/.github/workflows/deploy-npm.yml @@ -32,26 +32,24 @@ jobs: run: npm run lint - name: Run unit tests run: npm run test:unit - - name: Update edge release alias - shell: bash - run: | - if ./scripts/semcompare.sh "${{ github.event.release.tag_name }}" "$(cat ./release-aliases/3-EDGE)"; then - echo "${{ github.event.release.tag_name }}" > ./release-aliases/3-EDGE - fi - - name: Update stable release alias - shell: bash - if: github.event.release.prerelease == false - run: | - if ./scripts/semcompare.sh "${{ github.event.release.tag_name }}" "$(cat ./release-aliases/3-STABLE)"; then - echo "${{ github.event.release.tag_name }}" > ./release-aliases/3-STABLE - fi + #- name: Update edge release alias + # shell: bash + # run: | + # if ./scripts/semcompare.sh "${{ github.event.release.tag_name }}" "$(cat ./release-aliases/3-EDGE)"; then + # echo "${{ github.event.release.tag_name }}" > ./release-aliases/3-EDGE + # fi + #- name: Update stable release alias + # shell: bash + # if: github.event.release.prerelease == false + # run: | + # if ./scripts/semcompare.sh "${{ github.event.release.tag_name }}" "$(cat ./release-aliases/3-STABLE)"; then + # echo "${{ github.event.release.tag_name }}" > ./release-aliases/3-STABLE + # fi - name: Prepare Release uses: lando/prepare-release-action@v3 with: lando-plugin: true - sync-token: ${{ secrets.github-token }} - sync-email: rtfm47@lando.dev - sync-username: rtfm-47 + sync: false - name: Upgrade npm for trusted publishing run: npm install -g "npm@^11.5.1" - name: Publish to npm @@ -62,7 +60,7 @@ jobs: if [ "${{ github.event.release.prerelease }}" == "false" ]; then npm publish --access public --tag latest --dry-run npm publish --access public --tag latest - npm dist-tag add "$PACKAGE@$VERSION" edge + # npm dist-tag add "$PACKAGE@$VERSION" edge # not supported with trusted publishing echo "::notice title=Published $VERSION to $PACKAGE::This is a stable release published to the default 'latest' npm tag" echo "::notice title=Updated latest tag to $VERSION::The stable tag now points to $VERSION" @@ -74,14 +72,14 @@ jobs: echo "::notice title=Published $VERSION to $PACKAGE::This is a prerelease published to the 'edge' npm tag" echo "::notice title=Updated edge tag to $VERSION::The edge tag now points to $VERSION" fi - - name: Update edge release alias on main - if: github.event.release.target_commitish == 'edge' - run: | - git clone https://github.com/lando/core.git core - cd core - git config user.name "rtfm-47" - git config user.email "rtfm47@lando.dev" - echo "${{ github.event.release.tag_name }}" > ./release-aliases/3-EDGE - git add . - git commit -m "Update edge release alias to ${{ github.event.release.tag_name }} triggered by @rtfm-47" - git push https://x-access-token:${{ secrets.github-token }}@github.com/lando/core.git main + #- name: Update edge release alias on main + # if: github.event.release.target_commitish == 'edge' + # run: | + # git clone https://github.com/lando/core.git core + # cd core + # git config user.name "rtfm-47" + # git config user.email "rtfm47@lando.dev" + # echo "${{ github.event.release.tag_name }}" > ./release-aliases/3-EDGE + # git add . + # git commit -m "Update edge release alias to ${{ github.event.release.tag_name }} triggered by @rtfm-47" + # git push https://x-access-token:${{ secrets.github-token }}@github.com/lando/core.git main diff --git a/.github/workflows/dev-release.yml b/.github/workflows/dev-release.yml index 5d0313e00..018dd4f93 100644 --- a/.github/workflows/dev-release.yml +++ b/.github/workflows/dev-release.yml @@ -29,38 +29,38 @@ jobs: os: ${{ matrix.os }} version: dev - sign: - uses: ./.github/workflows/sign-binary.yml - needs: - - package - strategy: - fail-fast: false - matrix: - file: - - lando-linux-arm64-${{ github.sha }} - - lando-macos-arm64-${{ github.sha }} - - lando-win-arm64-${{ github.sha }} + #sign: + # uses: ./.github/workflows/sign-binary.yml + # needs: + # - package + # strategy: + # fail-fast: false + # matrix: + # file: + # - lando-linux-arm64-${{ github.sha }} + # - lando-macos-arm64-${{ github.sha }} + # - lando-win-arm64-${{ github.sha }} - - lando-linux-x64-${{ github.sha }} - - lando-macos-x64-${{ github.sha }} - - lando-win-x64-${{ github.sha }} + # - lando-linux-x64-${{ github.sha }} + # - lando-macos-x64-${{ github.sha }} + # - lando-win-x64-${{ github.sha }} - with: - download-pattern: packaged-lando-* - file: ${{ matrix.file }} - secrets: - apple-notary-user: ${{ secrets.APPLE_NOTARY_USER }} - apple-notary-password: ${{ secrets.APPLE_NOTARY_PASSWORD }} - certificate-data: ${{ contains(matrix.file, 'macos') && secrets.APPLE_CERT_DATA || secrets.KEYLOCKER_CLIENT_CERT }} - certificate-password: ${{ contains(matrix.file, 'macos') && secrets.APPLE_CERT_PASSWORD || secrets.KEYLOCKER_CLIENT_CERT_PASSWORD }} - keylocker-api-key: ${{ secrets.KEYLOCKER_API_KEY }} - keylocker-cert-sha1-hash: ${{ secrets.KEYLOCKER_CERT_SHA1_HASH }} - keylocker-keypair-alias: ${{ secrets.KEYLOCKER_KEYPAIR_ALIAS }} + # with: + # download-pattern: packaged-lando-* + # file: ${{ matrix.file }} + # secrets: + # apple-notary-user: ${{ secrets.APPLE_NOTARY_USER }} + # apple-notary-password: ${{ secrets.APPLE_NOTARY_PASSWORD }} + # certificate-data: ${{ contains(matrix.file, 'macos') && secrets.APPLE_CERT_DATA || secrets.KEYLOCKER_CLIENT_CERT }} + # certificate-password: ${{ contains(matrix.file, 'macos') && secrets.APPLE_CERT_PASSWORD || secrets.KEYLOCKER_CLIENT_CERT_PASSWORD }} + # keylocker-api-key: ${{ secrets.KEYLOCKER_API_KEY }} + # keylocker-cert-sha1-hash: ${{ secrets.KEYLOCKER_CERT_SHA1_HASH }} + # keylocker-keypair-alias: ${{ secrets.KEYLOCKER_KEYPAIR_ALIAS }} build-release-binary-alias: uses: ./.github/workflows/release-rename-binary.yml needs: - - sign + - package strategy: fail-fast: false matrix: @@ -77,11 +77,11 @@ jobs: with: source: lando-${{ matrix.os }}-${{ matrix.arch }}-${{ github.sha }} destination: lando-${{ matrix.os }}-${{ matrix.arch }}-${{ matrix.alias }} - download-pattern: signed-lando-* + download-pattern: packaged-lando-* build-release-binary-branch: uses: ./.github/workflows/release-rename-binary.yml needs: - - sign + - package strategy: fail-fast: false matrix: @@ -96,7 +96,7 @@ jobs: with: source: lando-${{ matrix.os }}-${{ matrix.arch }}-${{ github.sha }} destination: lando-${{ matrix.os }}-${{ matrix.arch }}-${{ github.head_ref || github.ref_name }} - download-pattern: signed-lando-* + download-pattern: packaged-lando-* checksum: uses: ./.github/workflows/generate-checksums.yml @@ -116,16 +116,16 @@ jobs: show: true upload-name: release-checksums-${{ matrix.alias }} - deploy-releases-s3: - uses: ./.github/workflows/deploy-s3.yml - needs: - - checksum - with: - download-pattern: release-* - secrets: - aws-secret-access-key: ${{ secrets.S3_SECRET_ACCESS_KEY }} - aws-access-key-id: ${{ secrets.S3_ACCESS_KEY_ID }} - aws-region: us-east-1 + #deploy-releases-s3: + # uses: ./.github/workflows/deploy-s3.yml + # needs: + # - checksum + # with: + # download-pattern: release-* + # secrets: + # aws-secret-access-key: ${{ secrets.S3_SECRET_ACCESS_KEY }} + # aws-access-key-id: ${{ secrets.S3_ACCESS_KEY_ID }} + # aws-region: us-east-1 deploy-releases-artifacts: uses: ./.github/workflows/deploy-artifacts.yml needs: diff --git a/.github/workflows/pkg-binary.yml b/.github/workflows/pkg-binary.yml index 8de56ea5e..419dde75b 100644 --- a/.github/workflows/pkg-binary.yml +++ b/.github/workflows/pkg-binary.yml @@ -51,8 +51,8 @@ jobs: cache: npm - name: Install dependencies run: npm clean-install --prefer-offline --frozen-lockfile --production - - name: Install plugins - run: scripts/install-plugins.sh --lando bin/lando ${{ inputs.edge == true && '--edge' || '' }} + #- name: Install plugins + # run: scripts/install-plugins.sh --lando bin/lando ${{ inputs.edge == true && '--edge' || '' }} - name: Switch to edge channel if: inputs.edge == true run: | @@ -82,8 +82,8 @@ jobs: - name: Ensure channel if: (inputs.os == 'linux' && runner.os == 'Linux') || (inputs.os == 'macos' && runner.os == 'macOS') run: ./dist/${{ inputs.filename }} config --path channel | grep ${{ inputs.edge == true && 'edge' || 'stable' }} - - name: Ensure plugin install - if: ((inputs.os == 'linux' && runner.os == 'Linux') || (inputs.os == 'macos' && runner.os == 'macOS')) - run: | - ./dist/${{ inputs.filename }} config --path fatcore | grep true - ./dist/${{ inputs.filename }} config | grep -q "/snapshot/core/plugins/wordpress" + #- name: Ensure plugin install + # if: ((inputs.os == 'linux' && runner.os == 'Linux') || (inputs.os == 'macos' && runner.os == 'macOS')) + # run: | + # ./dist/${{ inputs.filename }} config --path fatcore | grep true + # ./dist/${{ inputs.filename }} config | grep -q "/snapshot/core/plugins/wordpress" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 781ea3211..06c2fa13d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -31,38 +31,38 @@ jobs: os: ${{ matrix.os }} version: ${{ github.event.release.tag_name }} - sign: - uses: ./.github/workflows/sign-binary.yml - needs: - - package - strategy: - fail-fast: false - matrix: - file: - - lando-linux-arm64-${{ github.ref_name }} - - lando-macos-arm64-${{ github.ref_name }} - - lando-win-arm64-${{ github.ref_name }} - - - lando-linux-x64-${{ github.ref_name }} - - lando-macos-x64-${{ github.ref_name }} - - lando-win-x64-${{ github.ref_name }} - - with: - download-pattern: packaged-lando-* - file: ${{ matrix.file }} - secrets: - apple-notary-user: ${{ secrets.APPLE_NOTARY_USER }} - apple-notary-password: ${{ secrets.APPLE_NOTARY_PASSWORD }} - certificate-data: ${{ contains(matrix.file, 'macos') && secrets.APPLE_CERT_DATA || secrets.KEYLOCKER_CLIENT_CERT }} - certificate-password: ${{ contains(matrix.file, 'macos') && secrets.APPLE_CERT_PASSWORD || secrets.KEYLOCKER_CLIENT_CERT_PASSWORD }} - keylocker-api-key: ${{ secrets.KEYLOCKER_API_KEY }} - keylocker-cert-sha1-hash: ${{ secrets.KEYLOCKER_CERT_SHA1_HASH }} - keylocker-keypair-alias: ${{ secrets.KEYLOCKER_KEYPAIR_ALIAS }} +# sign: +# uses: ./.github/workflows/sign-binary.yml +# needs: +# - package +# strategy: +# fail-fast: false +# matrix: +# file: +# - lando-linux-arm64-${{ github.ref_name }} +# - lando-macos-arm64-${{ github.ref_name }} +# - lando-win-arm64-${{ github.ref_name }} + +# - lando-linux-x64-${{ github.ref_name }} +# - lando-macos-x64-${{ github.ref_name }} +# - lando-win-x64-${{ github.ref_name }} + +# with: +# download-pattern: packaged-lando-* +# file: ${{ matrix.file }} +# secrets: +# apple-notary-user: ${{ secrets.APPLE_NOTARY_USER }} +# apple-notary-password: ${{ secrets.APPLE_NOTARY_PASSWORD }} +# certificate-data: ${{ contains(matrix.file, 'macos') && secrets.APPLE_CERT_DATA || secrets.KEYLOCKER_CLIENT_CERT }} +# certificate-password: ${{ contains(matrix.file, 'macos') && secrets.APPLE_CERT_PASSWORD || secrets.KEYLOCKER_CLIENT_CERT_PASSWORD }} +# keylocker-api-key: ${{ secrets.KEYLOCKER_API_KEY }} +# keylocker-cert-sha1-hash: ${{ secrets.KEYLOCKER_CERT_SHA1_HASH }} +# keylocker-keypair-alias: ${{ secrets.KEYLOCKER_KEYPAIR_ALIAS }} build-release-binary-alias: uses: ./.github/workflows/release-rename-binary.yml needs: - - sign + - package strategy: fail-fast: false matrix: @@ -78,11 +78,11 @@ jobs: with: source: lando-${{ matrix.os }}-${{ matrix.arch }}-${{ github.ref_name }} destination: lando-${{ matrix.os }}-${{ matrix.arch }}-${{ matrix.alias }} - download-pattern: signed-lando-* + download-pattern: packaged-lando-* build-release-binary-tag: uses: ./.github/workflows/release-rename-binary.yml needs: - - sign + - package strategy: fail-fast: false matrix: @@ -96,7 +96,7 @@ jobs: with: source: lando-${{ matrix.os }}-${{ matrix.arch }}-${{ github.ref_name }} destination: lando-${{ matrix.os }}-${{ matrix.arch }}-${{ github.ref_name }} - download-pattern: signed-lando-* + download-pattern: packaged-lando-* checksum: uses: ./.github/workflows/generate-checksums.yml @@ -127,17 +127,17 @@ jobs: show: true upload-name: release-checksums${{ matrix.alias }} - deploy-releases-s3: - uses: ./.github/workflows/deploy-s3.yml - needs: - - checksum - - checksum-s3-aliases - with: - download-pattern: release-* - secrets: - aws-secret-access-key: ${{ secrets.S3_SECRET_ACCESS_KEY }} - aws-access-key-id: ${{ secrets.S3_ACCESS_KEY_ID }} - aws-region: us-east-1 +# deploy-releases-s3: +# uses: ./.github/workflows/deploy-s3.yml +# needs: +# - checksum +# - checksum-s3-aliases +# with: +# download-pattern: release-* +# secrets: +# aws-secret-access-key: ${{ secrets.S3_SECRET_ACCESS_KEY }} +# aws-access-key-id: ${{ secrets.S3_ACCESS_KEY_ID }} +# aws-region: us-east-1 deploy-releases-artifacts: uses: ./.github/workflows/deploy-artifacts.yml needs: @@ -156,51 +156,51 @@ jobs: - checksum secrets: github-token: ${{ secrets.RTFM47_COAXIUM_INJECTOR }} - deploy-legacy-notifications: - runs-on: ubuntu-24.04 - needs: - - checksum - env: - TERM: xterm - steps: - - name: Push release to lando/lando - uses: softprops/action-gh-release@v2 - with: - repository: lando/lando - name: ${{ github.event.release.tag_name }} - draft: ${{ github.event.release.draft }} - prerelease: ${{ github.event.release.prerelease }} - tag_name: ${{ github.event.release.tag_name }} - token: ${{ secrets.RTFM47_COAXIUM_INJECTOR }} - body: | - **Starting with v3.21.0-beta.18, Lando is no longer distributed via package installers in here in this releases page!** - - To install Lando please visit the [official install docs](https://docs.lando.dev/install). - - ## Changelogs - - Lando now runs as a distributed plugin-based ecosystem so you will want to check the releases/changelogs in - the various [plugins](https://docs.lando.dev/plugins.html) for relevant notes. - - [Click Here](https://github.com/lando/core/releases/tag/${{ github.event.release.tag_name }}) to check out the notes for `@lando/core@${{ github.event.release.tag_name }}`. - - ## Notes - - * We will continue to push releases here for backwards compatibility, posterity, etc - * [Extended release notes](https://lando.dev/blog/2024/01/16/v321-extended.html) - - - name: Push release to lando/cli - uses: softprops/action-gh-release@v2 - with: - repository: lando/legacy-cli - name: ${{ github.event.release.tag_name }} - draft: ${{ github.event.release.draft }} - prerelease: ${{ github.event.release.prerelease }} - tag_name: ${{ github.event.release.tag_name }} - token: ${{ secrets.RTFM47_COAXIUM_INJECTOR }} - body: | - **Starting with v3.23.0, Lando CLI binaries are no longer distributed here in these releases!** - - They are now available in the `@lando/core` [releases page](https://github.com/lando/core/releases) including [this ${{ github.event.release.tag_name }} release](https://github.com/lando/core/releases/tag/${{ github.event.release.tag_name }}). - - All that said we don't recommned you use these binaries directly. Instead, to install Lando please visit the [official install docs](https://docs.lando.dev/install). +# deploy-legacy-notifications: +# runs-on: ubuntu-24.04 +# needs: +# - checksum +# env: +# TERM: xterm +# steps: +# - name: Push release to lando/lando +# uses: softprops/action-gh-release@v2 +# with: +# repository: lando/lando +# name: ${{ github.event.release.tag_name }} +# draft: ${{ github.event.release.draft }} +# prerelease: ${{ github.event.release.prerelease }} +# tag_name: ${{ github.event.release.tag_name }} +# token: ${{ secrets.RTFM47_COAXIUM_INJECTOR }} +# body: | +# **Starting with v3.21.0-beta.18, Lando is no longer distributed via package installers in here in this releases page!*#* + +# To install Lando please visit the [official install docs](https://docs.lando.dev/install)#. + +# ## Changelogs + +# Lando now runs as a distributed plugin-based ecosystem so you will want to check the releases/changelogs in +# the various [plugins](https://docs.lando.dev/plugins.html) for relevant notes#. + +# [Click Here](https://github.com/lando/core/releases/tag/${{ github.event.release.tag_name }}) to check out the notes for `@lando/core@${{ github.event.release.tag_name }}`#. + +# ## Notes + +# * We will continue to push releases here for backwards compatibility, posterity, etc +# * [Extended release notes](https://lando.dev/blog/2024/01/16/v321-extended.html#) + +# - name: Push release to lando/cli +# uses: softprops/action-gh-release@v2 +# with: +# repository: lando/legacy-cli +# name: ${{ github.event.release.tag_name }} +# draft: ${{ github.event.release.draft }} +# prerelease: ${{ github.event.release.prerelease }} +# tag_name: ${{ github.event.release.tag_name }} +# token: ${{ secrets.RTFM47_COAXIUM_INJECTOR }} +# body: | +# **Starting with v3.23.0, Lando CLI binaries are no longer distributed here in these releases!*#* + +# They are now available in the `@lando/core` [releases page](https://github.com/lando/core/releases) including [this ${{ github.event.release.tag_name }} release](https://github.com/lando/core/releases/tag/${{ github.event.release.tag_name }})#. + +# All that said we don't recommned you use these binaries directly. Instead, to install Lando please visit the [official install docs](https://docs.lando.dev/install). diff --git a/README.md b/README.md index 7fdd3fd5c..05587cfc0 100644 --- a/README.md +++ b/README.md @@ -1,52 +1,5 @@ -# Lando Core +# Flos Lando Core -These are the core libraries that power Lando. They are implemented in [`@lando/cli`] and things like [Pantheon LocalDev](https://pantheon.io/product/localdev) and [WordPress VIP CLI](https://github.com/Automattic/vip-cli/blob/develop/package.json). - -On a high level they serve as: - -**An abstraction layer** Lando vastly reduces the complexity of spinning up containers by exposing only the most relevant config for a given "service" and setting "sane defaults". Lando also provides "recipes" which are common combinations of services and their tooling that satisfy a given development use case - e.g. Drupal, Python, Laravel, Dotnet, etc. - -**A superset** Lando provides ways for developers to run complex commands, build steps and automation on their services without the hassle of custom Dockerfiles or long "docker exec" commands. Think `lando yarn add express`. Think clear my applications cache after I import a database. Think install this core-extension before my appserver starts and then `composer install` after it does. - -**A utility** Lando handles some of the more arduous configuration required for a good Docker Compose setup - e.g. proxying, nice urls, cross-application networking (think Vue.js frontend talking to a separate Laravel backend), host-container file permission handling, file sharing, per-container SSL certificate handling, ssh-key handling, etc. - -## Basic Usage - -```js -const Lando = require('@lando/core'); -const lando = new Lando(config); - -// bootstrap and go -return lando.bootstrap(bsLevel).then(lando => { - lando.getApp().init().then(() => cli.run(getTasks(config, cli.argv()), config)); -}); -const -``` - -For more info you should check out the [docs](https://docs.lando.dev/core/v3): - -## Issues, Questions and Support - -If you have a question or would like some community support we recommend you [join us on Slack](https://launchpass.com/devwithlando). - -If you'd like to report a bug or submit a feature request then please [use the issue queue](https://github.com/lando/core/issues/new/choose) in this repo. - -## Changelog - -We try to log all changes big and small in both [THE CHANGELOG](https://github.com/lando/core/blob/main/CHANGELOG.md) and the [release notes](https://github.com/lando/core/releases). - -## Contributors - - - - - -Made with [contributors-img](https://contrib.rocks).` - -## Other Selected Resources - -* [LICENSE](/LICENSE) -* [TERMS OF USE](https://docs.lando.dev/terms) -* [PRIVACY POLICY](https://docs.lando.dev/privacy) -* [CODE OF CONDUCT](https://docs.lando.dev/coc) +These are the core libraries that power flos version of Lando with seamless compose integration. +Thanks to the upstream [lando-core](https://github.com/lando/core)! diff --git a/examples/plugins/README.md b/examples/plugins/README.md index 50a493ef3..b69fa07de 100644 --- a/examples/plugins/README.md +++ b/examples/plugins/README.md @@ -63,9 +63,9 @@ lando plugin-login --registry "https://npm.pkg.github.com" --password "$GITHUB_P # Should be able to add and remove a private plugin via a registry string. lando config | grep -qv "plugins/@lando/lando-plugin-test" -lando plugin-add "@lando/lando-plugin-test" -lando config | grep -q "plugins/@lando/lando-plugin-test" -lando plugin-remove "@lando/lando-plugin-test" +#lando plugin-add "@lando/lando-plugin-test" +#lando config | grep -q "plugins/@lando/lando-plugin-test" +#lando plugin-remove "@lando/lando-plugin-test" lando config | grep -qv "plugins/@lando/lando-plugin-test" ``` diff --git a/package-lock.json b/package-lock.json index ae7a8c608..0fd2e16f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { - "name": "@lando/core", - "version": "3.26.4", + "name": "@florianpat/lando-core", + "version": "3.26.5-1florianPat.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@lando/core", - "version": "3.26.3", + "name": "@florianpat/lando-core", + "version": "3.26.5-1florianPat.0", "license": "MIT", "dependencies": { "@lando/argv": "^1.2.0", diff --git a/package.json b/package.json index 9d46410f3..ee476a633 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,15 @@ { - "name": "@lando/core", + "name": "@florianpat/lando-core", "description": "The libraries that power all of Lando.", - "version": "3.26.4", - "author": "Mike Pirog @pirog", + "version": "3.26.5-1florianPat.0", + "author": "Florian Patruck @florianPat", "license": "MIT", - "repository": "lando/core", - "bugs": "https://github.com/lando/core/issues/new/choose", - "homepage": "https://github.com/lando/core", + "repository": { + "type": "git", + "url": "git+https://github.com/HDNET/lando-core.git" + }, + "bugs": "https://github.com/HDNET/lando-core/issues/new/choose", + "homepage": "https://github.com/HDNET/lando-core", "keywords": [ "lando", "lando-plugin"