diff --git a/.github/workflows/deploy-npm.yml b/.github/workflows/deploy-npm.yml
index f2b8144a6..c0315c8ac 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@v7
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
@@ -36,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
@@ -64,9 +58,9 @@ 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 dist-tag add "$PACKAGE@$VERSION" edge
+ npm publish --access public --tag latest --dry-run
+ npm publish --access public --tag latest
+ # 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"
@@ -78,16 +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
- env:
- NODE_AUTH_TOKEN: ${{ secrets.npm-token }}
- - 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 c0e1ffaea..8cd3e2c7a 100644
--- a/.github/workflows/dev-release.yml
+++ b/.github/workflows/dev-release.yml
@@ -29,41 +29,41 @@ jobs:
os: ${{ matrix.os }}
version: dev
- sign:
- permissions:
- contents: read
- id-token: write
- 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:
+ # permissions:
+ # contents: read
+ # id-token: write
+ # 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 }}
- azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}
- azure-subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
- certificate-data: ${{ contains(matrix.file, 'macos') && secrets.APPLE_CERT_DATA || '' }}
- certificate-password: ${{ contains(matrix.file, 'macos') && secrets.APPLE_CERT_PASSWORD || '' }}
+ #with:
+ # download-pattern: packaged-lando-*
+ # file: ${{ matrix.file }}
+ #secrets:
+ # apple-notary-user: ${{ secrets.APPLE_NOTARY_USER }}
+ # apple-notary-password: ${{ secrets.APPLE_NOTARY_PASSWORD }}
+ # azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}
+ # azure-subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+ # azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
+ # certificate-data: ${{ contains(matrix.file, 'macos') && secrets.APPLE_CERT_DATA || '' }}
+ # certificate-password: ${{ contains(matrix.file, 'macos') && secrets.APPLE_CERT_PASSWORD || '' }}
build-release-binary-alias:
uses: ./.github/workflows/release-rename-binary.yml
needs:
- - sign
+ - package
strategy:
fail-fast: false
matrix:
@@ -80,11 +80,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:
@@ -99,7 +99,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
@@ -119,16 +119,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 7d6b4ae3b..9cd7f11ea 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 bb54c1c55..3057a7927 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -31,41 +31,41 @@ jobs:
os: ${{ matrix.os }}
version: ${{ github.event.release.tag_name }}
- sign:
- permissions:
- contents: read
- id-token: write
- 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 }}
- azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}
- azure-subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
- certificate-data: ${{ contains(matrix.file, 'macos') && secrets.APPLE_CERT_DATA || '' }}
- certificate-password: ${{ contains(matrix.file, 'macos') && secrets.APPLE_CERT_PASSWORD || '' }}
+ #sign:
+ # permissions:
+ # contents: read
+ # id-token: write
+ # 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 }}
+ # azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}
+ # azure-subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+ # azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
+ # certificate-data: ${{ contains(matrix.file, 'macos') && secrets.APPLE_CERT_DATA || '' }}
+ # certificate-password: ${{ contains(matrix.file, 'macos') && secrets.APPLE_CERT_PASSWORD || '' }}
build-release-binary-alias:
uses: ./.github/workflows/release-rename-binary.yml
needs:
- - sign
+ - package
strategy:
fail-fast: false
matrix:
@@ -81,11 +81,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:
@@ -99,7 +99,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
@@ -130,17 +130,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:
@@ -159,52 +159,51 @@ jobs:
- checksum
secrets:
github-token: ${{ secrets.RTFM47_COAXIUM_INJECTOR }}
- npm-token: ${{ secrets.NPM_DEPLOY_TOKEN }}
- 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@v3
- 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@v3
- 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@v3
+# 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@v3
+# 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/app.js b/app.js
index 920f230b3..2d496f45d 100644
--- a/app.js
+++ b/app.js
@@ -26,8 +26,10 @@ module.exports = async (app, lando) => {
allServices: app.allServices,
compose: app.compose,
containers: app.containers,
+ hostLandoInternal: _.get(lando, 'config.hostLandoInternal.extraHost', null),
info: _.cloneDeep(app.info).map(service => ({...service, hostname: [], urls: []})),
name: app.name,
+ coreTooling: app._coreDbTooling,
overrides: {
tooling: app._coreToolingOverrides,
},
@@ -54,6 +56,7 @@ module.exports = async (app, lando) => {
allServices: app.allServices,
compose: app.compose,
containers: app.containers,
+ hostLandoInternal: _.get(lando, 'config.hostLandoInternal.extraHost', null),
info: _.cloneDeep(app.info).map(service => ({...service, hostname: [], urls: []})),
name: app.name,
mounts: require('./utils/get-mounts')(_.get(app, 'v4.services', {})),
@@ -61,10 +64,10 @@ module.exports = async (app, lando) => {
project: app.project,
root: app.root,
sapis: require('./utils/get-service-apis')(app),
+ coreTooling: app._coreDbTooling,
overrides: {
tooling: app._coreToolingOverrides,
},
-
}, {persist: true});
};
@@ -100,6 +103,9 @@ module.exports = async (app, lando) => {
// add in hostname
app.events.on('post-init', 1, async () => await require('./hooks/app-add-hostnames')(app, lando));
+ // add default database tooling eg db-import, db-export, note that this needs to run before app-add-tooling
+ app.events.on('post-init', 2, async () => await require('./hooks/app-add-db-tooling')(app, lando));
+
// run v3 build steps
app.events.on('post-init', async () => await require('./hooks/app-run-v3-build-steps')(app, lando));
@@ -124,6 +130,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));
@@ -135,18 +144,35 @@ 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));
+ // 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));
// 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));
@@ -174,9 +200,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));
@@ -199,7 +222,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));
@@ -231,6 +254,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/bin/lando b/bin/lando
index 693186d77..b4eda8fb0 100755
--- a/bin/lando
+++ b/bin/lando
@@ -52,6 +52,8 @@ if (process.env.LANDO_DEBUG) {
// }
// and finally prefer --debug
+// @NOTE: --debug is a boolean flag so only an explicit "--debug=" sets the scope. without this
+// "lando --debug start" would enable the "start" namespace and print nothing at all
if (argv.hasOption('--debug')) {
require('debug').enable(argv.getOption('--debug', {defaultValue: 'lando*'}));
}
@@ -68,7 +70,7 @@ debug('starting %o version %o runtime selector...', id, pjson.version);
// allow envvars to override a few core things
// @NOTE: we've kept these around for backwards compatibility, you probably shouldnt use them though
-const LOGLEVELCONSOLE = process.env.LANDO_CORE_LOGLEVELCONSOLE || debug.enabled ? 4 : undefined;
+const LOGLEVELCONSOLE = process.env.LANDO_CORE_LOGLEVELCONSOLE || (debug.enabled ? 4 : undefined);
const ENVPREFIX = process.env.LANDO_CORE_ENVPREFIX;
const USERCONFROOT = process.env.LANDO_CORE_USERCONFROOT;
const RUNTIME = process.env.LANDO_CORE_RUNTIME;
@@ -108,6 +110,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']));
@@ -137,6 +143,26 @@ if (appConfig.recipe && !fs.existsSync(appConfig.recipeCache)) {
if (fs.existsSync(process.landoAppCacheFile)) fs.unlinkSync(process.landoAppCacheFile);
}
+// host.lando.internal is baked into the generated compose files so if it has moved on us eg the wsl2 vm restarted and
+// got a new gateway then what we have cached is stale and needs to be regenerated
+if (fs.existsSync(process.landoAppCacheFile ?? '')) {
+ try {
+ const {extraHost} = require(`${COREBASE}/utils/get-host-lando-internal`)({
+ cacheDir: path.join(config.userConfRoot, 'cache'),
+ ideLocation: config.xdebugIdeLocation,
+ });
+ const cached = JSON.parse(fs.readFileSync(process.landoAppCacheFile, {encoding: 'utf-8'}));
+
+ if (typeof cached.hostLandoInternal === 'string' && cached.hostLandoInternal !== extraHost) {
+ debug('host.lando.internal moved from %o to %o, purging compose cache', cached.hostLandoInternal, extraHost);
+ fs.unlinkSync(process.landoAppCacheFile);
+ if (fs.existsSync(process.landoTaskCacheFile)) fs.unlinkSync(process.landoTaskCacheFile);
+ }
+ } catch (error) {
+ debug('could not check whether host.lando.internal is still valid %o', error.message);
+ }
+}
+
// determine bs level
const bsLevel = !_.isEmpty(appConfig) && !fs.existsSync(process.landoAppCacheFile) ? 'APP' : 'TASKS';
diff --git a/builders/_init.js b/builders/_init.js
index a86cf3b04..5b240f560 100644
--- a/builders/_init.js
+++ b/builders/_init.js
@@ -13,9 +13,11 @@ 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') {
+ constructor(userConfRoot, home, app, _app, env = {}, labels = {}, image = 'devwithlando/util:4') {
// Basic Init service
const initService = {
services: {
@@ -34,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/builders/_lando.js b/builders/_lando.js
index 3ef39dc51..85f1f328e 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,10 @@ module.exports = {
const environment = {
LANDO_SERVICE_NAME: name,
LANDO_SERVICE_TYPE: type,
+ LANDO_WEBROOT_USER: meUser,
+ LANDO_WEBROOT_GROUP: meUser,
+ LANDO_MOUNT: appMount,
+ LANDO_SERVICE_API: 3,
};
// Handle labels
@@ -110,10 +116,9 @@ module.exports = {
// Handle volumes
const volumes = [
- `${userConfRoot}:/lando:cached`,
+ `${userConfRoot}/keys:/lando/keys:cached`,
`${globalScriptsDir}:/helpers`,
`${entrypointScript}:/lando-entrypoint.sh`,
- `${dataHome}:/var/www`,
];
// add in service helpers if we have them
@@ -132,10 +137,15 @@ 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) 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
@@ -178,14 +188,21 @@ 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,
environment,
- extra_hosts: ['host.lando.internal:host-gateway'],
+ extra_hosts: require('../utils/get-host-lando-internal-hosts')({config: _.get(_app, '_config', {})}),
labels,
logging,
ports,
@@ -210,6 +227,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/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/builders/lando-compose.js b/builders/lando-compose.js
new file mode 100644
index 000000000..dc521b9bb
--- /dev/null
+++ b/builders/lando-compose.js
@@ -0,0 +1,51 @@
+'use strict';
+
+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 = {}) {
+ // 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,
+ sslExpose: false,
+ ssl: true,
+ }, options);
+
+ if ((options.landoEntrypoint ?? true) && command.length > 0) {
+ opts.overrides = _.merge({}, {command}, options.overrides);
+ } else {
+ opts.entrypoint = undefined;
+ }
+
+ super(id, opts);
+ }
+ },
+};
diff --git a/builders/lando-v4.js b/builders/lando-v4.js
index e8108a56b..4ec8c22b8 100644
--- a/builders/lando-v4.js
+++ b/builders/lando-v4.js
@@ -436,7 +436,7 @@ module.exports = {
// add it all 2getha
this.addLandoServiceData({
environment,
- extra_hosts: ['host.lando.internal:host-gateway'],
+ extra_hosts: require('../utils/get-host-lando-internal-hosts')(lando),
labels,
logging: {driver: 'json-file', options: {'max-file': '3', 'max-size': '10m'}},
networks: {[this.network]: {aliases: this.hostnames}},
diff --git a/components/l337-v4.js b/components/l337-v4.js
index 9820db680..8ba13350e 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() {
@@ -164,7 +164,7 @@ class L337ServiceV4 extends EventEmitter {
// add in the l337 spec config
this.addServiceData({
...config,
- extra_hosts: ['host.lando.internal:host-gateway'],
+ extra_hosts: require('../utils/get-host-lando-internal-hosts')(lando),
ports,
});
this.addServiceData({ports});
@@ -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/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/docs/.vitepress/config.mjs b/docs/.vitepress/config.mjs
index 4b3aea3d9..6b343164e 100644
--- a/docs/.vitepress/config.mjs
+++ b/docs/.vitepress/config.mjs
@@ -206,6 +206,9 @@ function cliBar() {
collapsed: false,
items: [
{text: 'lando config', link: '/cli/config'},
+ {text: 'lando db NEW!', link: '/cli/db'},
+ {text: 'lando db-export NEW!', link: '/cli/db-export'},
+ {text: 'lando db-import NEW!', link: '/cli/db-import'},
{text: 'lando destroy', link: '/cli/destroy'},
{text: 'lando exec NEW!', link: '/cli/exec'},
{text: 'lando info', link: '/cli/info'},
diff --git a/docs/cli/db-export.md b/docs/cli/db-export.md
new file mode 100644
index 000000000..f88998ea4
--- /dev/null
+++ b/docs/cli/db-export.md
@@ -0,0 +1,50 @@
+---
+title: lando db-export
+description: lando db-export exports a database from a database service to a file
+---
+
+# lando db-export
+
+Exports a database from a database service to a file.
+
+This command is provided automatically for any app that has a service that looks like a database. That means a
+service whose type is a known database eg `mysql`, `mariadb` or `postgres`, or a service literally called
+`database`. If your recipe or Landofile already declares its own `db-export` then that one is used instead.
+
+## Usage
+
+```sh
+lando db-export [file] [--host ] [--stdout]
+```
+
+## Arguments
+
+```sh
+file The file to dump to, defaults to a timestamped file in the current directory
+```
+
+### Options
+
+```sh
+--host, -h The database service to use [default: "database"]
+--stdout Dump database to stdout
+```
+
+## Examples
+
+```sh
+# Dump the default database service to a timestamped file
+lando db-export
+
+# Dump to a specific file
+lando db-export dump.sql
+
+# Dump a specific database service to stdout
+lando db-export --host mydb --stdout
+```
+
+::: warning File paths are resolved inside the container!
+A relative path is resolved against the working directory *inside* the service, which is normally your app mount
+eg `/app`. If a service has its app mount disabled then the dump lands somewhere inside the container and is lost
+on the next rebuild, so pass a path that is bind mounted from your host.
+:::
diff --git a/docs/cli/db-import.md b/docs/cli/db-import.md
new file mode 100644
index 000000000..d4ea89592
--- /dev/null
+++ b/docs/cli/db-import.md
@@ -0,0 +1,52 @@
+---
+title: lando db-import
+description: lando db-import imports a dump file into a database service
+---
+
+# lando db-import
+
+Imports a dump file into a database service.
+
+This command is provided automatically for any app that has a service that looks like a database. That means a
+service whose type is a known database eg `mysql`, `mariadb` or `postgres`, or a service literally called
+`database`. If your recipe or Landofile already declares its own `db-import` then that one is used instead.
+
+## Usage
+
+```sh
+lando db-import [--host ] [--no-wipe]
+```
+
+## Arguments
+
+```sh
+file The dump file to import
+```
+
+### Options
+
+```sh
+--host, -h The database service to use [default: "database"]
+--no-wipe Do not destroy the existing database before an import [boolean]
+```
+
+## Examples
+
+```sh
+# Import a dump into the default database service
+lando db-import dump.sql
+
+# Import a gzipped dump without wiping the database first
+lando db-import dump.sql.gz --no-wipe
+
+# Import into a specific database service
+lando db-import dump.sql --host mydb
+
+# Import from stdin
+cat dump.sql | lando db-import
+```
+
+::: warning File paths are resolved inside the container!
+A relative path is resolved against the working directory *inside* the service, which is normally your app mount
+eg `/app`. If a service has its app mount disabled then only paths that exist inside that container will work.
+:::
diff --git a/docs/cli/db.md b/docs/cli/db.md
new file mode 100644
index 000000000..6de0dfa5c
--- /dev/null
+++ b/docs/cli/db.md
@@ -0,0 +1,44 @@
+---
+title: lando db
+description: lando db drops into a database shell on a database service
+---
+
+# lando db
+
+Drops into a database shell on a database service.
+
+`lando db` works out which client to use at runtime by inspecting the service, so it does the right thing whether
+the service is MySQL, MariaDB or PostgreSQL. If Lando can determine the flavor of your database service up front
+it will *also* give you the conventional alias for it eg `lando mysql`, `lando mariadb` or `lando psql`.
+
+This command is provided automatically for any app that has a service that looks like a database. That means a
+service whose type is a known database eg `mysql`, `mariadb` or `postgres`, or a service literally called
+`database`. If your recipe or Landofile already declares its own then that one is used instead.
+
+## Usage
+
+```sh
+lando db [--host ]
+```
+
+### Options
+
+```sh
+--host, -h The database service to use [default: "database"]
+```
+
+## Examples
+
+```sh
+# Drop into a shell on the default database service
+lando db
+
+# Same thing, if Lando could detect the flavor
+lando mysql
+
+# Drop into a shell on a specific database service
+lando db --host mydb
+
+# Run a single query
+lando db -- -e "SELECT 1"
+```
diff --git a/docs/config/networking.md b/docs/config/networking.md
index 25a836934..3003c61bc 100644
--- a/docs/config/networking.md
+++ b/docs/config/networking.md
@@ -59,6 +59,47 @@ You can also use the environment variable `LANDO_HOST_IP`.
lando exec my-service -- ping "\$LANDO_HOST_IP" -c 3
```
+### WSL2 and where your IDE lives
+
+On most platforms `host.lando.internal` just points at the machine Lando is running on and that is the end of it. WSL2 is the exception because there are _two_ candidate hosts: the Linux distro your containers are in, and the Windows side.
+
+This matters most for step debugging. If your containers run on a `docker-ce` you installed *inside* WSL2 but PhpStorm or VS Code is listening on Windows, then the Linux side is the wrong target and Xdebug will never connect.
+
+Lando works this out for you at start time:
+
+| Situation | `host.lando.internal` resolves to |
+| :-- | :-- |
+| Not WSL2 | the Docker `host-gateway` |
+| WSL2 + Docker Desktop | the Docker `host-gateway`, Docker Desktop proxies it through to Windows |
+| WSL2 + `docker-ce`, `nat` mode (the default) | the WSL2 default gateway, which is the Windows `vEthernet (WSL)` adapter |
+| WSL2 + `docker-ce`, `mirrored` or `bridged` mode | the Windows IP on its best default route |
+| WSL2 + `docker-ce`, `virtioproxy` mode | the Windows `vEthernet (WSL)` Hyper-V switch |
+| WSL2 with `networkingMode=none` | nothing, there is no network path to Windows |
+
+If the autodetection gets it wrong you can pin it with `xdebugIdeLocation` in [Lando's global config](./global.html):
+
+```yaml
+# ~/.lando/config.yml
+
+# "auto" autodetect, the default
+# "wsl2" your IDE runs inside WSL2, use normal Linux behavior
+# "container" your IDE listens inside the container itself
+# an IP use this address verbatim
+xdebugIdeLocation: wsl2
+```
+
+You can check what Lando decided and why with:
+
+```sh
+lando config | grep -A1 hostLandoInternal
+```
+
+::: tip Xdebug still not connecting?
+Windows Defender blocks inbound connections on the `vEthernet (WSL)` network by default, so port `9003` needs an exception even once `host.lando.internal` is correct. PhpStorm on Windows also listens on IPv6 first, add `-Djava.net.preferIPv4Stack=true` under **Help → Edit Custom VM Options**.
+
+For `mirrored` mode you additionally need `hostAddressLoopback=true` under `[experimental]` in your Windows `.wslconfig`.
+:::
+
## Network Limits
By default Docker has a limit of 32 networks. If you're running a large number of sites, you'll see a message `Lando has detected you are at Docker's network limit`, after which Lando will attempt to clean up unused networks to put you below the network limit.
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/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/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/hooks/app-add-db-tooling.js b/hooks/app-add-db-tooling.js
new file mode 100644
index 000000000..c24304d49
--- /dev/null
+++ b/hooks/app-add-db-tooling.js
@@ -0,0 +1,23 @@
+'use strict';
+
+const _ = require('lodash');
+
+module.exports = async app => {
+ // bail if we cannot find anything that looks like a database
+ const images = require('../utils/get-service-images')(_.get(app, 'composeData', []));
+ const services = require('../utils/get-db-services')(_.get(app, 'info', []), images);
+ if (_.isEmpty(services)) return;
+
+ // prefer a service actually called "database" to preserve the historical recipe default
+ const host = _.find(services, {service: 'database'}) ? 'database' : _.first(services).service;
+ const defaults = require('../utils/get-db-tooling-defaults')(services, host);
+
+ // only add tooling the user or their recipe has not already claimed
+ const additions = require('../utils/get-core-tooling-additions')(defaults, _.get(app, 'config.tooling', {}));
+ if (_.isEmpty(additions)) return;
+
+ // stash these so we can persist them into the compose cache and use them on the faster "engine" bootstrap path
+ app._coreDbTooling = additions;
+ app.config.tooling = _.merge({}, additions, _.get(app, 'config.tooling', {}));
+ app.log.verbose('added default database tooling %o', _.keys(additions));
+};
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/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));
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);
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/app-purge-compose-cache.js b/hooks/app-purge-compose-cache.js
index f9e70ef5c..415b2e166 100644
--- a/hooks/app-purge-compose-cache.js
+++ b/hooks/app-purge-compose-cache.js
@@ -11,8 +11,13 @@ module.exports = async (app, lando) => {
// reset tooling overrides
app._coreToolingOverrides = {};
- // remove compose cache danglerz
+ // reset core provided database tooling
+ app._coreDbTooling = {};
+
+ // remove compose cache danglerz, note that the combined file is generated and not a dangler
+ const combined = require('../utils/dump-compose-config').combinedFile;
fs.readdirSync(app._dir)
+ .filter(dangler => dangler !== combined)
.map(dangler => path.join(app._dir, dangler))
.filter(dangler => !app.compose.includes(dangler))
.map(dangler => {
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);
+ }
+ });
+ }
+};
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/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/hooks/app-start-proxy.js b/hooks/app-start-proxy.js
index 3db065cfb..498ed28b5 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});
}
@@ -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
@@ -323,29 +322,29 @@ 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}),
};
})
// Add to our app
// @NOTE: we can't add this in the normal way since this happens AFTER our app
// has been initialized
- .then(result => {
+ .then(async result => {
app.add(new app.ComposeService('proxy', {}, ...result));
app.compose = lando.utils.dumpComposeData(app.composeData, app._dir);
app.log.debug('app now has proxy compose files', app.compose);
+ // redump the combined file so it also has the proxy stuff
+ await lando.utils.dumpComposeConfig(app.engine, app.compose, app.project, app._dir, app.log);
})
// Warn the user if this fails
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/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
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/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/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 053f6695c..312e72bd9 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',
@@ -40,6 +40,9 @@ const defaults = {
proxyHttpFallbacks: ['8000', '8080', '8888', '8008'],
proxyHttpsFallbacks: ['444', '4433', '4444', '4443'],
proxyPassThru: true,
+ // where the users ide/debugger is listening, this governs what host.lando.internal points at
+ // one of: "auto" (default), "wsl2", "container" or an explicit ip address
+ xdebugIdeLocation: 'auto',
},
};
@@ -74,6 +77,13 @@ module.exports = async lando => {
const platform = lando.config.os.landoPlatform;
+ // work out what host.lando.internal needs to resolve to, this is mostly a wsl2 concern eg the ide is over on
+ // windows but the containers are in wsl2 so "host-gateway" only ever gets us as far as the linux side
+ const hostLandoInternal = require('./utils/get-host-lando-internal')({
+ cache: lando.cache,
+ ideLocation: lando.config.xdebugIdeLocation,
+ });
+
// ensure some dirs exist before we start
_.forEach([binDir, caDir, sshDir], dir => fs.mkdirSync(dir, {recursive: true}));
@@ -143,6 +153,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: {
@@ -163,6 +175,7 @@ module.exports = async lando => {
caCert,
caDomain,
caKey,
+ hostLandoInternal,
maxKeyWarning: 10,
networkBridge: 'lando_bridge_network',
networkLimit: 32,
diff --git a/lib/app.js b/lib/app.js
index 5f4f224ab..cd2f0a307 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);
@@ -56,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';
@@ -64,6 +69,7 @@ module.exports = class App {
this._lando = lando;
this._name = name;
this._coreToolingOverrides = {};
+ this._coreDbTooling = {};
this.debuggy = lando.debuggy;
/**
@@ -256,6 +262,8 @@ module.exports = class App {
.then(() => this.log.info('destroyed app.'));
}
+ static isBootstrapCommand = undefined;
+
/**
* Initializes the app
*
@@ -272,14 +280,41 @@ 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);
- // 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);
+ if (App.isBootstrapCommand) {
+ 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)
+ *
+ * @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 []
+ .then(() => noEngine === true ? [] : require('../utils/load-compose-files')(
+ _.get(this, 'config.compose', []),
+ this.root,
+ this._dir,
+ this.engine,
+ this.project,
+ composeEnvFiles,
+ this.log,
+ _.get(this, '_config.orchestratorSeparator', '_'),
+ ))
+ .then(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 +327,7 @@ 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(() => this.events.emit('pre-init', this))
// Actually assemble this thing so its ready for that engine
.then(() => {
// Get all the services
@@ -364,9 +398,17 @@ module.exports = class App {
}
// Log
- this.initialized = true;
+ this.initialized = !!noEngine;
this.log.verbose('app is ready!');
})
+
+ .then(() => noEngine === true ? undefined : require('../utils/dump-compose-config')(
+ this.engine,
+ this.compose,
+ this.project,
+ this._dir,
+ this.log,
+ ))
/**
* Event that runs when the app is ready for action
*
@@ -378,7 +420,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);
}
/**
@@ -493,13 +536,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.
@@ -527,6 +576,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/cli.js b/lib/cli.js
index a37eccb38..661e9b3bf 100644
--- a/lib/cli.js
+++ b/lib/cli.js
@@ -83,6 +83,41 @@ module.exports = class Cli {
return require('yargs').help(false).version(false).argv;
}
+ /**
+ * Returns the parsed global options that belong to lando itself
+ *
+ * For passthrough commands eg tooling, everything after the command name belongs to that command and not to
+ * us. Without this `lando console my:command -vvv` would crank up landos verbosity when the user only wanted
+ * the underlying command to be verbose.
+ *
+ * @since 3.26.5
+ * @alias lando.cli.landoArgv
+ * @return {Object} Yarg parsed options
+ */
+ landoArgv() {
+ // reuse the last result if nothing has changed, defaultConfig alone asks for this twice
+ const key = `${this.passthrough}::${process.argv.join(' ')}`;
+ if (this._landoArgv && this._landoArgvFor === key) return this._landoArgv;
+
+ // if this is not a passthrough command then all global options are ours
+ if (!this.passthrough) return this.argv();
+
+ // otherwise only consider the args that come before the command
+ const args = process.argv.slice(2);
+ const index = args.indexOf(this.passthrough);
+ if (index === -1) return this.argv();
+
+ // note that we need a fresh yargs here because the singleton has demandCommand and middleware on it by now.
+ // we also drop "choices" because the validation for those is handled manually in run() and we do not want
+ // a bad value to make yargs bail out from in here
+ const parser = require('yargs/yargs')(args.slice(0, index)).help(false).version(false);
+ _.forEach(globalOptions, (config, name) => parser.option(name, _.omit(config, ['choices'])));
+
+ this._landoArgvFor = key;
+ this._landoArgv = parser.parse();
+ return this._landoArgv;
+ }
+
/**
* Checks to see if lando is running with sudo. If it is it
* will exit the process with a stern warning
@@ -181,7 +216,7 @@ module.exports = class Cli {
landoFile: '.lando.yml',
landoFileConfig: appConfig,
leia: _.has(process, 'env.LEIA_PARSER_RUNNING'),
- logLevelConsole: (this.argv().verbose) ? this.argv().verbose + 1 : this.logLevel,
+ logLevelConsole: (this.landoArgv().verbose) ? this.landoArgv().verbose + 1 : this.logLevel,
logDir: path.join(this.userConfRoot, 'logs'),
mode: 'cli',
packaged,
@@ -241,7 +276,7 @@ module.exports = class Cli {
}
isDebug() {
- const {debug, verbose} = this.argv();
+ const {debug, verbose} = this.landoArgv();
return debug ? 1 + verbose : 0 + verbose;
}
@@ -257,7 +292,8 @@ module.exports = class Cli {
* @param {Boolean} yes [yes=this.argv().yes] The auto yes value
* @return {Integer} The exit codes
*/
- handleError(error, handler, verbose = this.argv().verbose, lando = {}, yes = this.argv().yes) {
+ // note that "yes" stays on argv() because a "-y" after a command is still meant for us
+ handleError(error, handler, verbose = this.landoArgv().verbose, lando = {}, yes = this.argv().yes) {
// Set the verbosity
error.verbose = verbose;
@@ -320,6 +356,8 @@ module.exports = class Cli {
.option('help', globalOptions.help)
.option('verbose', globalOptions.verbose)
.version(false)
+ .env('lando_cli_')
+ .locale('en')
.middleware([(argv => {
argv._app = config;
argv._yargs = yargs;
@@ -393,9 +431,24 @@ module.exports = class Cli {
positionals = {},
run = {},
level = 'app',
+ dynamic = undefined,
+ passthrough = false,
usage = undefined,
} = {}, config = {}) {
const handler = argv => {
+ // if this command passes its args through then note that so we only claim global options that show up
+ // before it, this needs to happen before anything reads our config or verbosity
+ if (passthrough) {
+ this.passthrough = command.split(' ')[0];
+ // and drop the args that belong to the command so they cannot influence our own option handling
+ argv = require('../utils/strip-passthrough-args')(argv, {
+ options,
+ positionals,
+ dynamic,
+ globals: this.landoArgv(),
+ });
+ }
+
// Immediately build some arg data set opts and interactive options
const data = {options: argv, inquiry: formatters.getInteractive(options, argv)};
// Remove legacy secret toggle if still there
@@ -513,7 +566,12 @@ module.exports = class Cli {
const yargonaut = require('yargonaut');
yargonaut.style('green').errorsStyle('red');
const yargs = require('yargs');
- const {clear, channel, experimental, secretToggle} = yargs.argv;
+
+ // work out whether this command passes its args through before we look at any global option, otherwise
+ // something like "lando console my:command --clear" would wipe our caches and exit without ever running it
+ this.passthrough = require('../utils/get-passthrough-command')(tasks, yargs.argv._[0]);
+
+ const {clear, channel, experimental, secretToggle} = this.landoArgv();
// Handle global flag error conditions first
if (secretToggle && this.defaultConfig().packaged) {
diff --git a/lib/compose.js b/lib/compose.js
index 34cc67c18..f8dea8bc4 100644
--- a/lib/compose.js
+++ b/lib/compose.js
@@ -15,11 +15,26 @@ const composeFlags = {
noTTY: '-T',
pull: '--pull',
q: '--quiet',
+ quietPull: '--quiet-pull',
recreate: '--force-recreate',
removeOrphans: '--remove-orphans',
rm: '--rm',
timestamps: '--timestamps',
volumes: '-v',
+ ignoreBuildable: '--ignore-buildable',
+};
+
+const composeFlagOptionMapping = {
+ build: ['noCache', 'pull', 'q'],
+ down: ['removeOrphans', 'volumes'],
+ exec: ['background', 'detach', 'noTTY'],
+ kill: ['removeOrphans'],
+ logs: ['follow', 'timestamps'],
+ ps: ['q'],
+ pull: ['q', 'ignoreBuildable'],
+ rm: ['force', 'volumes'],
+ up: ['background', 'detach', 'noRecreate', 'noDeps', 'pull', 'q', 'recreate', 'removeOrphans', 'timestamps', 'quietPull'],
+ config: [],
};
// Default options nad things
@@ -30,21 +45,30 @@ 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: {},
};
/*
* 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(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] : [];
@@ -55,21 +79,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 options = parseOptions(opts);
+ const envFile = _.flatten(_.map(envFiles, unit => ['--env-file', unit]));
+ const options = parseOptions(run, 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 ?? []}, opts),
opts: {mode: 'spawn', cstdio: opts.cstdio, silent: opts.silent},
});
@@ -155,3 +180,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/daemon.js b/lib/daemon.js
index 938beff03..6a4c64d73 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.
@@ -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/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/lib/engine.js b/lib/engine.js
index b86c919b4..1c7eb33c5 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/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/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');
diff --git a/lib/router.js b/lib/router.js
index c8c314868..7b70a40a0 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);
@@ -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/lib/updates.js b/lib/updates.js
index 4871b8218..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
@@ -208,58 +209,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);
});
diff --git a/lib/utils.js b/lib/utils.js
index 9f9a80828..638673374 100644
--- a/lib/utils.js
+++ b/lib/utils.js
@@ -89,6 +89,7 @@ module.exports = {
// these all stay for backwards compatib
appMachineName: (...args) => require('../utils/slugify')(...args),
dockerComposify: (...args) => require('../utils/docker-composify')(...args),
+ dumpComposeConfig: (...args) => require('../utils/dump-compose-config')(...args),
dumpComposeData: (...args) => require('../utils/dump-compose-data')(...args),
getAppMounts: (...args) => require('../utils/get-app-mounts')(...args),
getCliEnvironment: (...args) => require('../utils/get-cli-env')(...args),
@@ -96,7 +97,7 @@ module.exports = {
getId: (...args) => require('../utils/get-container-id')(...args),
getInfoDefaults: (...args) => require('../utils/get-app-info-defaults')(...args),
getServices: (...args) => require('../utils/get-app-services')(...args),
- getUser: (...args) => ('../utils/get-user')(...args),
+ getUser: (...args) => require('../utils/get-user')(...args),
loadComposeFiles: (...args) => require('../utils/load-compose-files')(...args),
makeExecutable: (...args) => require('../utils/make-executable')(...args),
moveConfig: (...args) => require('../utils/move-config')(...args),
diff --git a/package-lock.json b/package-lock.json
index eaa8bef34..14581c2b4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
- "name": "@lando/core",
- "version": "3.26.7",
+ "name": "@florianpat/lando-core",
+ "version": "3.26.8-1florianPat.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
- "name": "@lando/core",
- "version": "3.26.7",
+ "name": "@florianpat/lando-core",
+ "version": "3.26.8-1florianPat.0",
"license": "MIT",
"dependencies": {
"@lando/argv": "^1.2.0",
@@ -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 2a02ad501..55a0a5f81 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.7",
- "author": "Mike Pirog @pirog",
+ "version": "3.26.8-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"
@@ -108,6 +111,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",
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/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/scripts/lando-entrypoint.sh b/scripts/lando-entrypoint.sh
index 5f37a4bad..ce8fb780b 100755
--- a/scripts/lando-entrypoint.sh
+++ b/scripts/lando-entrypoint.sh
@@ -2,6 +2,15 @@
set -e
+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
@@ -27,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;
@@ -57,8 +61,7 @@ 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" -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
@@ -73,6 +76,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}..."
@@ -85,3 +90,5 @@ elif [ ! -z ${LANDO_NEEDS_EXEC+x} ]; then
else
"$@" || tail -f /dev/null
fi;
+
+tail -f /dev/null
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/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..."
diff --git a/scripts/sql-cli.sh b/scripts/sql-cli.sh
new file mode 100755
index 000000000..52452823a
--- /dev/null
+++ b/scripts/sql-cli.sh
@@ -0,0 +1,66 @@
+#!/bin/bash
+set -e
+
+# Get the lando logger
+. /helpers/log.sh
+
+# Set the module
+LANDO_MODULE="sqlcli"
+
+# Set generic config
+HOST=localhost
+
+# Get type-specific config
+if [[ ${POSTGRES_DB} != '' ]]; then
+ DATABASE=${POSTGRES_DB:-database}
+ PORT=${LANDO_DB_CLI_PORT:-5432}
+ USER=${LANDO_DB_CLI_USER:-${POSTGRES_USER:-postgres}}
+else
+ DATABASE=${MYSQL_DATABASE:-database}
+ PORT=${LANDO_DB_CLI_PORT:-3306}
+ USER=${LANDO_DB_CLI_USER:-root}
+fi
+
+# PARSE THE ARGZZ
+# The --host option is handled by landos built in dynamic service resolution so we just need to drop it here,
+# everything else gets passed straight through to the underlying client
+ARGS=()
+while (( "$#" )); do
+ case "$1" in
+ -h|--host)
+ shift 2
+ ;;
+ --host=*)
+ shift
+ ;;
+ --)
+ shift
+ ;;
+ *)
+ ARGS+=("$1")
+ shift
+ ;;
+ esac
+done
+
+# Build DB specific connection command
+if [[ ${POSTGRES_DB} != '' ]]; then
+ if ! command -v psql >/dev/null 2>&1; then
+ lando_red "Could not find a psql client on service ${LANDO_SERVICE_NAME}!"
+ exit 1
+ fi
+ exec psql "postgresql://${USER}@${HOST}:${PORT}/${DATABASE}" ${LANDO_EXTRA_DB_CLI_ARGS} "${ARGS[@]}"
+fi
+
+# Newer MariaDB images have dropped the mysql symlink so prefer the mariadb client if mysql is not around
+CLIENT=mysql
+if ! command -v mysql >/dev/null 2>&1 && command -v mariadb >/dev/null 2>&1; then
+ CLIENT=mariadb
+fi
+
+if ! command -v "$CLIENT" >/dev/null 2>&1; then
+ lando_red "Could not find a mysql or mariadb client on service ${LANDO_SERVICE_NAME}!"
+ exit 1
+fi
+
+exec "$CLIENT" -h "$HOST" -P "$PORT" -u "$USER" ${LANDO_EXTRA_DB_CLI_ARGS} "$DATABASE" "${ARGS[@]}"
diff --git a/scripts/user-perm-helpers.sh b/scripts/user-perm-helpers.sh
index 28db38732..7f0545ae2 100755
--- a/scripts/user-perm-helpers.sh
+++ b/scripts/user-perm-helpers.sh
@@ -6,35 +6,19 @@
# Set the module
LANDO_MODULE="userperms"
-# Adding user if needed
-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;
-}
-
# 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;
}
@@ -44,31 +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
+ 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
- groupmod -g "$HOST_GID" "$GROUP" 2>/dev/null || true
- if [ "$(id -u $USER)" != "$HOST_UID" ]; then
- usermod -g "$HOST_GID" "$USER" 2>/dev/null || true
+ 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
- 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
- lando_warn "Looks like host/container user mapping was not possible! aborting..."
- exit 0
+ 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 [ "$(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
}
@@ -78,33 +75,44 @@ reset_user() {
perm_sweep() {
local USER=$1
local GROUP=$2
- local OTHER_DIR=$3
+ local USER_HOME=$3
+ local OTHER_DIR=$4
- # 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
# Do other dirs first if we have them
if [ ! -z "$OTHER_DIR" ]; then
- chown -R $USER:$GROUP $OTHER_DIR >/dev/null 2>&1 &
+ nohup chown -R $USER:$GROUP $OTHER_DIR >> /tmp/perms.out 2>> /tmp/perms.err && lando_info "chowned $OTHER_DIR" &
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 &
+ # 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
+ 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
- # 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 &
+ wait
+ lando_info "perm sweep complete"
- # 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 &
+ 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 215573859..377386bdc 100755
--- a/scripts/user-perms.sh
+++ b/scripts/user-perms.sh
@@ -26,7 +26,11 @@ 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'}
+
+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
@@ -47,25 +51,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
-# Symlink the gitconfig
-if [ -f "/user/.gitconfig" ]; then
- rm -f /var/www/.gitconfig
- ln -sf /user/.gitconfig /var/www/.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
- 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
@@ -86,19 +81,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) $LANDO_RESET_DIR
+perm_sweep $LANDO_WEBROOT_USER $(getent group "$LANDO_HOST_GID" | cut -d: -f1) $WEBROOT_HOME $LANDO_RESET_DIR
+
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/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/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/test/app-add-db-tooling.spec.js b/test/app-add-db-tooling.spec.js
new file mode 100644
index 000000000..23fbab6e7
--- /dev/null
+++ b/test/app-add-db-tooling.spec.js
@@ -0,0 +1,68 @@
+'use strict';
+
+const chai = require('chai');
+const expect = chai.expect;
+
+const addDbTooling = require('../hooks/app-add-db-tooling');
+
+// minimal app stub
+const getApp = (info = [], tooling = {}) => ({
+ info,
+ config: {tooling},
+ _coreDbTooling: {},
+ log: {verbose: () => {}},
+});
+
+describe('app-add-db-tooling', function() {
+ it('should do nothing if there is no database service', async function() {
+ const app = getApp([{service: 'appserver', type: 'php'}]);
+ await addDbTooling(app);
+ expect(app.config.tooling).to.deep.equal({});
+ expect(app._coreDbTooling).to.deep.equal({});
+ });
+
+ it('should add default tooling when it finds a database', async function() {
+ const app = getApp([{service: 'database', type: 'mysql'}]);
+ await addDbTooling(app);
+ expect(app.config.tooling).to.have.property('db-import ');
+ expect(app.config.tooling).to.have.property('db-export [file]');
+ expect(app.config.tooling).to.have.property('mysql');
+ expect(app._coreDbTooling).to.have.property('db-import ');
+ });
+
+ it('should default the host to the found service when it is not called "database"', async function() {
+ const app = getApp([{service: 'mydb', type: 'postgres'}]);
+ await addDbTooling(app);
+ expect(app.config.tooling['db-import '].options.host.default).to.equal('mydb');
+ expect(app.config.tooling).to.have.property('psql');
+ });
+
+ it('should prefer a service called "database" as the default host', async function() {
+ const app = getApp([{service: 'mydb', type: 'mysql'}, {service: 'database', type: 'mysql'}]);
+ await addDbTooling(app);
+ expect(app.config.tooling['db-import '].options.host.default).to.equal('database');
+ });
+
+ it('should not clobber tooling the user or recipe already declared', async function() {
+ const existing = {'db-import ': {service: 'database', cmd: 'my-importer'}};
+ const app = getApp([{service: 'database', type: 'mysql'}], existing);
+ await addDbTooling(app);
+ expect(app.config.tooling['db-import '].cmd).to.equal('my-importer');
+ expect(app._coreDbTooling).to.not.have.property('db-import ');
+ });
+
+ it('should match claimed tooling on id and not on key', async function() {
+ // a user declaring a plain "db-import" should not end up with two of them
+ const app = getApp([{service: 'database', type: 'mysql'}], {'db-import': {service: 'database', cmd: 'nope'}});
+ await addDbTooling(app);
+ expect(app.config.tooling).to.not.have.property('db-import ');
+ expect(app.config.tooling).to.have.property('db-import');
+ });
+
+ it('should respect tooling that has been disabled', async function() {
+ const app = getApp([{service: 'database', type: 'mysql'}], {mysql: false});
+ await addDbTooling(app);
+ expect(app.config.tooling.mysql).to.equal(false);
+ expect(app._coreDbTooling).to.not.have.property('mysql');
+ });
+});
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-core-tooling-additions.spec.js b/test/get-core-tooling-additions.spec.js
new file mode 100644
index 000000000..d5edbde79
--- /dev/null
+++ b/test/get-core-tooling-additions.spec.js
@@ -0,0 +1,40 @@
+'use strict';
+
+const chai = require('chai');
+const expect = chai.expect;
+
+const getAdditions = require('../utils/get-core-tooling-additions');
+
+describe('get-core-tooling-additions', function() {
+ const core = {'db-import ': {cmd: 'core-importer'}, 'db': {cmd: 'core-shell'}};
+
+ it('should return everything if nothing is claimed', function() {
+ expect(getAdditions(core, {})).to.deep.equal(core);
+ expect(getAdditions(core)).to.deep.equal(core);
+ });
+
+ it('should handle being given nothing', function() {
+ expect(getAdditions()).to.deep.equal({});
+ });
+
+ it('should drop commands claimed under the same key', function() {
+ const additions = getAdditions(core, {'db-import ': {cmd: 'mine'}});
+ expect(additions).to.not.have.property('db-import ');
+ expect(additions).to.have.property('db');
+ });
+
+ it('should drop commands claimed under a different key but the same id', function() {
+ const additions = getAdditions(core, {'db-import': {cmd: 'mine'}});
+ expect(additions).to.not.have.property('db-import ');
+ });
+
+ it('should drop commands the user has disabled', function() {
+ expect(getAdditions(core, {db: false})).to.not.have.property('db');
+ });
+
+ it('should never deep merge into a claimed command', function() {
+ const tooling = {'db-import ': {cmd: 'mine'}};
+ const merged = Object.assign({}, getAdditions(core, tooling), tooling);
+ expect(merged['db-import ']).to.deep.equal({cmd: 'mine'});
+ });
+});
diff --git a/test/get-db-services.spec.js b/test/get-db-services.spec.js
new file mode 100644
index 000000000..0f9931b26
--- /dev/null
+++ b/test/get-db-services.spec.js
@@ -0,0 +1,69 @@
+'use strict';
+
+const chai = require('chai');
+const expect = chai.expect;
+
+const getDbServices = require('../utils/get-db-services');
+const {getFlavor} = getDbServices;
+
+describe('get-db-services', function() {
+ it('should return an empty array if there is no info', function() {
+ expect(getDbServices()).to.deep.equal([]);
+ expect(getDbServices([])).to.deep.equal([]);
+ });
+
+ it('should ignore services that are not databases', function() {
+ const info = [{service: 'appserver', type: 'php'}, {service: 'cache', type: 'redis'}];
+ expect(getDbServices(info)).to.deep.equal([]);
+ });
+
+ it('should detect known database types', function() {
+ const info = [
+ {service: 'appserver', type: 'php'},
+ {service: 'db1', type: 'mysql'},
+ {service: 'db2', type: 'mariadb'},
+ {service: 'db3', type: 'postgres'},
+ ];
+ expect(getDbServices(info).map(service => service.service)).to.deep.equal(['db1', 'db2', 'db3']);
+ });
+
+ it('should detect prefixed and versioned database types', function() {
+ const info = [
+ {service: 'db1', type: 'pantheon-mariadb'},
+ {service: 'db2', type: 'mysql:8.0'},
+ {service: 'db3', type: 'lagoon_postgres'},
+ ];
+ expect(getDbServices(info).map(service => service.flavor)).to.deep.equal(['mariadb', 'mysql', 'postgres']);
+ });
+
+ it('should detect a service called "database" even if its flavor is unknown', function() {
+ const info = [{service: 'database', type: 'lando-compose'}];
+ expect(getDbServices(info)).to.deep.equal([{service: 'database', type: 'lando-compose', flavor: undefined}]);
+ });
+
+ it('should fall back to sniffing the service image', function() {
+ const info = [{service: 'db1', type: 'lando'}, {service: 'cache', type: 'lando'}];
+ const images = {db1: 'mariadb:10.4', cache: 'redis:7'};
+ expect(getDbServices(info, images)).to.deep.equal([{service: 'db1', type: 'lando', flavor: 'mariadb'}]);
+ });
+
+ it('should prefer the type over the image when sniffing', function() {
+ const info = [{service: 'db1', type: 'postgres'}];
+ expect(getDbServices(info, {db1: 'mariadb:10.4'})[0].flavor).to.equal('postgres');
+ });
+
+ it('should handle registries and tags in image names', function() {
+ expect(getFlavor('bitnami/postgresql:15')).to.equal('postgres');
+ expect(getFlavor('mysql:8.0')).to.equal('mysql');
+ expect(getFlavor('devwithlando/php:8.3-fpm-2')).to.equal(undefined);
+ expect(getFlavor('nginx:1.22.1')).to.equal(undefined);
+ });
+
+ it('should not confuse similarly named types', function() {
+ expect(getFlavor('mysql-proxy-thing')).to.equal('mysql');
+ expect(getFlavor('mysqlish')).to.equal(undefined);
+ expect(getFlavor('notmariadb')).to.equal(undefined);
+ expect(getFlavor(undefined)).to.equal(undefined);
+ expect(getFlavor(42)).to.equal(undefined);
+ });
+});
diff --git a/test/get-db-tooling-defaults.spec.js b/test/get-db-tooling-defaults.spec.js
new file mode 100644
index 000000000..c5b35afd2
--- /dev/null
+++ b/test/get-db-tooling-defaults.spec.js
@@ -0,0 +1,66 @@
+'use strict';
+
+const chai = require('chai');
+const expect = chai.expect;
+
+const getDbToolingDefaults = require('../utils/get-db-tooling-defaults');
+
+describe('get-db-tooling-defaults', function() {
+ it('should always provide import, export and a generic shell', function() {
+ const tooling = getDbToolingDefaults();
+ expect(tooling).to.have.property('db-import ');
+ expect(tooling).to.have.property('db-export [file]');
+ expect(tooling).to.have.property('db');
+ });
+
+ it('should point at the scripts core mounts into every api 3 service', function() {
+ const tooling = getDbToolingDefaults();
+ expect(tooling['db-import '].cmd).to.equal('/helpers/sql-import.sh');
+ expect(tooling['db-export [file]'].cmd).to.equal('/helpers/sql-export.sh');
+ expect(tooling['db'].cmd).to.equal('/helpers/sql-cli.sh');
+ });
+
+ it('should run import and export as root', function() {
+ const tooling = getDbToolingDefaults();
+ expect(tooling['db-import '].user).to.equal('root');
+ expect(tooling['db-export [file]'].user).to.equal('root');
+ });
+
+ it('should use a dynamic service that defaults to "database"', function() {
+ const tooling = getDbToolingDefaults();
+ expect(tooling['db-import '].service).to.equal(':host');
+ expect(tooling['db-import '].options.host.default).to.equal('database');
+ });
+
+ it('should honor a different default host', function() {
+ const tooling = getDbToolingDefaults([{service: 'mydb', flavor: 'mysql'}], 'mydb');
+ expect(tooling['db-import '].options.host.default).to.equal('mydb');
+ expect(tooling['db-export [file]'].options.host.default).to.equal('mydb');
+ expect(tooling['mysql'].options.host.default).to.equal('mydb');
+ });
+
+ it('should add a flavor specific shell for each detected flavor', function() {
+ const tooling = getDbToolingDefaults([
+ {service: 'db1', flavor: 'mariadb'},
+ {service: 'db2', flavor: 'postgres'},
+ ]);
+ expect(tooling).to.have.property('mariadb');
+ expect(tooling).to.have.property('psql');
+ expect(tooling).to.not.have.property('mysql');
+ });
+
+ it('should not add a shell for undetectable or unsupported flavors', function() {
+ const tooling = getDbToolingDefaults([{service: 'database'}, {service: 'db2', flavor: 'mssql'}]);
+ expect(tooling).to.have.property('db');
+ expect(tooling).to.not.have.property('mysql');
+ expect(tooling).to.not.have.property('mssql');
+ });
+
+ it('should not mutate shared option objects between commands', function() {
+ const tooling = getDbToolingDefaults([{service: 'database', flavor: 'mysql'}]);
+ expect(tooling['db-import '].options).to.have.property('no-wipe');
+ expect(tooling['db-export [file]'].options).to.not.have.property('no-wipe');
+ expect(tooling['db'].options).to.not.have.property('no-wipe');
+ expect(tooling['mysql'].options).to.not.have.property('stdout');
+ });
+});
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/test/get-host-lando-internal.spec.js b/test/get-host-lando-internal.spec.js
new file mode 100644
index 000000000..1e6c222be
--- /dev/null
+++ b/test/get-host-lando-internal.spec.js
@@ -0,0 +1,182 @@
+/*
+ * Tests for get-host-lando-internal.
+ * @file get-host-lando-internal.spec.js
+ */
+
+'use strict';
+
+const chai = require('chai');
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+
+const expect = chai.expect;
+chai.should();
+
+const MODULE = '../utils/get-host-lando-internal';
+
+// the sub utils we need to fake in order to exercise the wsl2 branches
+const STUBBABLE = {
+ isDockerDesktop: '../utils/is-docker-desktop',
+ mode: '../utils/get-wsl-networking-mode',
+ natIP: '../utils/get-wsl-nat-host-ip',
+ mirroredIP: '../utils/get-wsl-mirrored-host-ip',
+ virtioIP: '../utils/get-wsl-virtioproxy-host-ip',
+ physicalIP: '../utils/get-wsl-physical-ip',
+ loopback: '../utils/is-wsl-host-loopback-enabled',
+};
+
+const originalRelease = os.release;
+
+// stuffs a fake module into the require cache, the resolver requires these lazily so this works
+const stub = (id, value) => {
+ const resolved = require.resolve(id);
+ require.cache[resolved] = {id: resolved, filename: resolved, loaded: true, exports: () => value};
+};
+
+const unstub = id => delete require.cache[require.resolve(id)];
+
+// each test gets a throwaway cachedir so the on disk boot-id cache cannot leak between them
+const getCacheDir = () => fs.mkdtempSync(path.join(os.tmpdir(), 'hli-'));
+
+const getHostLandoInternal = opts => require(MODULE)({cacheDir: getCacheDir(), refresh: true, ...opts});
+
+describe('get-host-lando-internal', () => {
+ afterEach(() => {
+ os.release = originalRelease;
+ Object.values(STUBBABLE).forEach(unstub);
+ });
+
+ describe('explicit ideLocation', () => {
+ it('should use an explicit ip address as-is', () => {
+ const result = getHostLandoInternal({ideLocation: '192.168.1.50'});
+ expect(result.extraHost).to.equal('192.168.1.50');
+ expect(result.ip).to.equal('192.168.1.50');
+ });
+
+ it('should use loopback when the ide is in the container', () => {
+ const result = getHostLandoInternal({ideLocation: 'container'});
+ expect(result.extraHost).to.equal('127.0.0.1');
+ expect(result.ip).to.equal('127.0.0.1');
+ });
+ });
+
+ describe('not wsl2', () => {
+ it('should use host-gateway', () => {
+ os.release = () => '23.5.0';
+ expect(getHostLandoInternal().extraHost).to.equal('host-gateway');
+ });
+ });
+
+ describe('wsl2', () => {
+ beforeEach(() => {
+ os.release = () => '5.15.153.1-microsoft-standard-WSL2';
+ });
+
+ it('should use host-gateway when the ide is also in wsl2', () => {
+ const result = getHostLandoInternal({ideLocation: 'wsl2'});
+ expect(result.extraHost).to.equal('host-gateway');
+ });
+
+ it('should use host-gateway when docker desktop is in play', () => {
+ stub(STUBBABLE.isDockerDesktop, true);
+ expect(getHostLandoInternal().extraHost).to.equal('host-gateway');
+ });
+
+ it('should use the default gateway in nat mode', () => {
+ stub(STUBBABLE.isDockerDesktop, false);
+ stub(STUBBABLE.mode, 'nat');
+ stub(STUBBABLE.natIP, '172.28.128.1');
+
+ const result = getHostLandoInternal();
+ expect(result.extraHost).to.equal('172.28.128.1');
+ expect(result.mode).to.equal('nat');
+ });
+
+ it('should assume nat when wslinfo is unavailable', () => {
+ stub(STUBBABLE.isDockerDesktop, false);
+ stub(STUBBABLE.mode, undefined);
+ stub(STUBBABLE.natIP, '172.28.128.1');
+
+ expect(getHostLandoInternal().extraHost).to.equal('172.28.128.1');
+ });
+
+ it('should ask windows for a reachable ip in mirrored mode', () => {
+ stub(STUBBABLE.isDockerDesktop, false);
+ stub(STUBBABLE.mode, 'mirrored');
+ stub(STUBBABLE.mirroredIP, '10.1.2.3');
+ stub(STUBBABLE.loopback, true);
+
+ const result = getHostLandoInternal();
+ expect(result.extraHost).to.equal('10.1.2.3');
+ expect(result.message).to.not.contain('hostAddressLoopback');
+ });
+
+ it('should call out a missing hostAddressLoopback in mirrored mode', () => {
+ stub(STUBBABLE.isDockerDesktop, false);
+ stub(STUBBABLE.mode, 'mirrored');
+ stub(STUBBABLE.mirroredIP, '10.1.2.3');
+ stub(STUBBABLE.loopback, false);
+
+ expect(getHostLandoInternal().message).to.contain('hostAddressLoopback');
+ });
+
+ it('should use the windows reachable ip in bridged mode', () => {
+ stub(STUBBABLE.isDockerDesktop, false);
+ stub(STUBBABLE.mode, 'bridged');
+ stub(STUBBABLE.mirroredIP, '10.1.2.3');
+
+ expect(getHostLandoInternal().extraHost).to.equal('10.1.2.3');
+ });
+
+ it('should use the hyper-v switch in virtioproxy mode', () => {
+ stub(STUBBABLE.isDockerDesktop, false);
+ stub(STUBBABLE.mode, 'virtioproxy');
+ stub(STUBBABLE.virtioIP, '172.20.0.1');
+
+ expect(getHostLandoInternal().extraHost).to.equal('172.20.0.1');
+ });
+
+ it('should fall back to the wsl2 ip when there is no hyper-v switch', () => {
+ stub(STUBBABLE.isDockerDesktop, false);
+ stub(STUBBABLE.mode, 'virtioproxy');
+ stub(STUBBABLE.virtioIP, undefined);
+ stub(STUBBABLE.physicalIP, '10.0.0.42');
+
+ const result = getHostLandoInternal();
+ expect(result.extraHost).to.equal('10.0.0.42');
+ expect(result.message).to.contain('xdebugIdeLocation: wsl2');
+ });
+
+ it('should still fall back to host-gateway when networking is disabled', () => {
+ stub(STUBBABLE.isDockerDesktop, false);
+ stub(STUBBABLE.mode, 'none');
+
+ const result = getHostLandoInternal();
+ expect(result.extraHost).to.equal('host-gateway');
+ expect(result.mode).to.equal('none');
+ });
+
+ it('should give up when we cannot determine the windows ip', () => {
+ stub(STUBBABLE.isDockerDesktop, false);
+ stub(STUBBABLE.mode, 'nat');
+ stub(STUBBABLE.natIP, undefined);
+
+ expect(getHostLandoInternal().extraHost).to.be.undefined;
+ });
+ });
+});
+
+describe('get-host-lando-internal-hosts', () => {
+ const getHosts = require('../utils/get-host-lando-internal-hosts');
+
+ it('should build an extra_hosts entry from the resolved value', () => {
+ const lando = {config: {hostLandoInternal: {extraHost: '172.28.128.1'}}};
+ expect(getHosts(lando)).to.deep.equal(['host.lando.internal:172.28.128.1']);
+ });
+
+ it('should build an entry even when there is no resolved extraHost', () => {
+ const lando = {config: {hostLandoInternal: {message: 'nope'}}};
+ expect(getHosts(lando)).to.deep.equal(['host.lando.internal:undefined']);
+ });
+});
diff --git a/test/get-passthrough-command.spec.js b/test/get-passthrough-command.spec.js
new file mode 100644
index 000000000..3a41b56dc
--- /dev/null
+++ b/test/get-passthrough-command.spec.js
@@ -0,0 +1,36 @@
+'use strict';
+
+const chai = require('chai');
+const expect = chai.expect;
+
+const getPassthroughCommand = require('../utils/get-passthrough-command');
+
+const tasks = [
+ {command: 'start', describe: 'Starts your app'},
+ {command: 'db-import ', id: 'db-import', passthrough: true},
+ {command: 'console', id: 'console', passthrough: true},
+];
+
+describe('get-passthrough-command', function() {
+ it('should return the command when it is a passthrough', function() {
+ expect(getPassthroughCommand(tasks, 'console')).to.equal('console');
+ });
+
+ it('should match on id and not on the full command string', function() {
+ expect(getPassthroughCommand(tasks, 'db-import')).to.equal('db-import');
+ });
+
+ it('should return undefined for one of our own commands', function() {
+ expect(getPassthroughCommand(tasks, 'start')).to.equal(undefined);
+ });
+
+ it('should return undefined for an unknown command', function() {
+ expect(getPassthroughCommand(tasks, 'nope')).to.equal(undefined);
+ });
+
+ it('should return undefined when there is no command', function() {
+ expect(getPassthroughCommand(tasks)).to.equal(undefined);
+ expect(getPassthroughCommand(tasks, '')).to.equal(undefined);
+ expect(getPassthroughCommand()).to.equal(undefined);
+ });
+});
diff --git a/test/get-service-images.spec.js b/test/get-service-images.spec.js
new file mode 100644
index 000000000..1532642d4
--- /dev/null
+++ b/test/get-service-images.spec.js
@@ -0,0 +1,42 @@
+'use strict';
+
+const chai = require('chai');
+const expect = chai.expect;
+
+const getServiceImages = require('../utils/get-service-images');
+
+describe('get-service-images', function() {
+ it('should return an empty object if there is no compose data', function() {
+ expect(getServiceImages()).to.deep.equal({});
+ expect(getServiceImages([])).to.deep.equal({});
+ });
+
+ it('should collect images from all compose data', function() {
+ const composeData = [
+ {data: [{services: {appserver: {image: 'php:8.3-fpm'}, database: {image: 'mariadb:10.4'}}}]},
+ {data: [{services: {cache: {image: 'redis:7'}}}]},
+ ];
+ expect(getServiceImages(composeData)).to.deep.equal({
+ appserver: 'php:8.3-fpm',
+ database: 'mariadb:10.4',
+ cache: 'redis:7',
+ });
+ });
+
+ it('should skip services that have no image', function() {
+ const composeData = [{data: [{services: {appserver: {build: '.'}, database: {image: 'mysql:8.0'}}}]}];
+ expect(getServiceImages(composeData)).to.deep.equal({database: 'mysql:8.0'});
+ });
+
+ it('should let later definitions win', function() {
+ const composeData = [
+ {data: [{services: {database: {image: 'mysql:5.7'}}}]},
+ {data: [{services: {database: {image: 'mysql:8.0'}}}]},
+ ];
+ expect(getServiceImages(composeData)).to.deep.equal({database: 'mysql:8.0'});
+ });
+
+ it('should tolerate malformed compose data', function() {
+ expect(getServiceImages([{}, {data: []}, {data: [{}]}, {data: [{services: {}}]}])).to.deep.equal({});
+ });
+});
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/test/lando-argv.spec.js b/test/lando-argv.spec.js
new file mode 100644
index 000000000..e34fac77e
--- /dev/null
+++ b/test/lando-argv.spec.js
@@ -0,0 +1,98 @@
+'use strict';
+
+const chai = require('chai');
+const expect = chai.expect;
+
+const Cli = require('../lib/cli');
+
+// helper to run something with a faked process.argv
+const withArgv = (args, fn) => {
+ const original = process.argv;
+ process.argv = ['/usr/local/bin/node', '/usr/local/bin/lando', ...args];
+ try {
+ return fn();
+ } finally {
+ process.argv = original;
+ }
+};
+
+describe('cli.landoArgv', function() {
+ it('should fall back to argv() when the command is not a passthrough', function() {
+ const cli = new Cli();
+ withArgv(['info', '-vvv'], () => {
+ // no passthrough set, so we should get whatever the normal parse gives us
+ expect(cli.landoArgv()).to.deep.equal(cli.argv());
+ });
+ });
+
+ it('should ignore verbosity that comes after a passthrough command', function() {
+ const cli = new Cli();
+ cli.passthrough = 'console';
+ withArgv(['console', 'my:command', '-vvv'], () => {
+ expect(cli.landoArgv().verbose).to.equal(0);
+ });
+ });
+
+ it('should claim verbosity that comes before a passthrough command', function() {
+ const cli = new Cli();
+ cli.passthrough = 'console';
+ withArgv(['-vvv', 'console', 'my:command'], () => {
+ expect(cli.landoArgv().verbose).to.equal(3);
+ });
+ });
+
+ it('should count --verbose and -v the same way yargs does', function() {
+ const cli = new Cli();
+ cli.passthrough = 'console';
+ withArgv(['--verbose', '-v', 'console', 'my:command'], () => {
+ expect(cli.landoArgv().verbose).to.equal(2);
+ });
+ });
+
+ it('should ignore --debug that comes after a passthrough command', function() {
+ const cli = new Cli();
+ cli.passthrough = 'console';
+ withArgv(['console', 'my:command', '--debug'], () => {
+ expect(cli.landoArgv().debug).to.equal(undefined);
+ });
+ withArgv(['--debug', 'console', 'my:command'], () => {
+ expect(cli.landoArgv().debug).to.equal(true);
+ });
+ });
+
+ it('should not choke on a valued global option before the command', function() {
+ const cli = new Cli();
+ cli.passthrough = 'console';
+ withArgv(['--channel', 'edge', '-vv', 'console', 'my:command', '-vvv'], () => {
+ expect(cli.landoArgv().verbose).to.equal(2);
+ });
+ });
+
+ it('should fall back to argv() if the command cannot be found in argv', function() {
+ const cli = new Cli();
+ cli.passthrough = 'nope';
+ withArgv(['console', 'my:command'], () => {
+ expect(cli.landoArgv()).to.deep.equal(cli.argv());
+ });
+ });
+
+ it('should ignore globals that follow the command, bare -- or not', function() {
+ const cli = new Cli();
+ cli.passthrough = 'l4env';
+ withArgv(['l4env', '--debug', '--', 'env'], () => {
+ expect(cli.landoArgv().debug).to.equal(undefined);
+ });
+ withArgv(['l4env', '--', 'env', '--debug'], () => {
+ expect(cli.landoArgv().debug).to.equal(undefined);
+ });
+ });
+
+ it('should report zero verbosity when nothing precedes the command', function() {
+ const cli = new Cli();
+ cli.passthrough = 'console';
+ withArgv(['console'], () => {
+ expect(cli.landoArgv().verbose).to.equal(0);
+ expect(cli.landoArgv().debug).to.equal(undefined);
+ });
+ });
+});
diff --git a/test/parse-tooling-config.spec.js b/test/parse-tooling-config.spec.js
new file mode 100644
index 000000000..882869937
--- /dev/null
+++ b/test/parse-tooling-config.spec.js
@@ -0,0 +1,52 @@
+'use strict';
+
+const chai = require('chai');
+const expect = chai.expect;
+
+const parseToolingConfig = require('../utils/parse-tooling-config');
+
+// options as they would be declared by a dynamic service tooling command
+const options = {host: {description: 'The database service to use', default: 'database', alias: ['h']}};
+
+describe('parse-tooling-config', function() {
+ it('should resolve a dynamic service from the answers', function() {
+ const answers = {host: 'database', h: 'database', _eventArgs: ['node', 'lando', 'db']};
+ const config = parseToolingConfig(['/helpers/sql-cli.sh'], ':host', 'db', options, answers, {database: 3});
+ expect(config[0].service).to.equal('database');
+ expect(config[0].sapi).to.equal(3);
+ });
+
+ it('should strip the dynamic service option and its value from the args', function() {
+ const answers = {
+ host: 'database',
+ h: 'database',
+ _eventArgs: ['node', 'lando', 'mysql', '--host', 'database', '--', '-e', 'SELECT 1'],
+ };
+ const config = parseToolingConfig(['/helpers/sql-cli.sh'], ':host', 'mysql', options, answers, {database: 3});
+ expect(config[0].args).to.deep.equal(['--', '-e', 'SELECT 1']);
+ });
+
+ it('should not strip the command name when it matches the resolved service', function() {
+ // "lando db" on a service that is also called "db" used to remove its own command name from argv, which
+ // meant every arg including the node and lando binaries got passed through to the underlying command
+ const answers = {host: 'db', h: 'db', _eventArgs: ['node', 'lando', 'db', '--', '-e', 'SELECT 1']};
+ const config = parseToolingConfig(['/helpers/sql-cli.sh'], ':host', 'db', options, answers, {db: 3});
+ expect(config[0].service).to.equal('db');
+ expect(config[0].args).to.deep.equal(['--', '-e', 'SELECT 1']);
+ expect(config[0].args).to.not.include('node');
+ expect(config[0].args).to.not.include('lando');
+ });
+
+ it('should still strip the dynamic service option when the command name matches the service', function() {
+ const answers = {host: 'db', h: 'db', _eventArgs: ['node', 'lando', 'db', '-h', 'db', '--', '-e', 'SELECT 1']};
+ const config = parseToolingConfig(['/helpers/sql-cli.sh'], ':host', 'db', options, answers, {db: 3});
+ expect(config[0].args).to.deep.equal(['--', '-e', 'SELECT 1']);
+ });
+
+ it('should leave non dynamic services alone', function() {
+ const answers = {_eventArgs: ['node', 'lando', 'composer', 'install']};
+ const config = parseToolingConfig(['composer'], 'appserver', 'composer', {}, answers, {appserver: 3});
+ expect(config[0].service).to.equal('appserver');
+ expect(config[0].args).to.deep.equal(['install']);
+ });
+});
diff --git a/test/strip-passthrough-args.spec.js b/test/strip-passthrough-args.spec.js
new file mode 100644
index 000000000..7155df547
--- /dev/null
+++ b/test/strip-passthrough-args.spec.js
@@ -0,0 +1,78 @@
+'use strict';
+
+const chai = require('chai');
+const expect = chai.expect;
+
+const strip = require('../utils/strip-passthrough-args');
+
+// a tooling command as lando would describe it, roughly our own db-import
+const declared = {
+ options: {
+ 'host': {description: 'The database service to use', default: 'database', alias: ['h']},
+ 'no-wipe': {description: 'Do not wipe', boolean: true},
+ },
+ positionals: {file: {describe: 'the file'}},
+ dynamic: 'host',
+};
+
+// what landoArgv() would hand us for "lando -vv db-import ..."
+const globals = {_: ['db-import'], $0: 'lando', verbose: 2, debug: undefined};
+
+describe('strip-passthrough-args', function() {
+ it('should keep options the command declared', function() {
+ const argv = {host: 'db', h: 'db', file: 'dump.sql'};
+ const result = strip(argv, {...declared, globals});
+ expect(result.host).to.equal('db');
+ expect(result.h).to.equal('db');
+ expect(result.file).to.equal('dump.sql');
+ });
+
+ it('should keep both the kebab and camel forms yargs sets', function() {
+ const argv = {'no-wipe': true, 'noWipe': true};
+ const result = strip(argv, {...declared, globals});
+ expect(result['no-wipe']).to.equal(true);
+ expect(result.noWipe).to.equal(true);
+ });
+
+ it('should drop args the command did not declare', function() {
+ const argv = {'host': 'db', 'autoRemove': false, 'auto-remove': false, 'deps': true, 'coolFlag': true};
+ const result = strip(argv, {...declared, globals});
+ expect(result).to.not.have.property('autoRemove');
+ expect(result).to.not.have.property('auto-remove');
+ expect(result).to.not.have.property('deps');
+ expect(result).to.not.have.property('coolFlag');
+ expect(result.host).to.equal('db');
+ });
+
+ it('should keep the structural keys', function() {
+ const app = {root: '/app'};
+ const argv = {'_': ['db-import', 'dump.sql'], '--': ['-e', 'x'], '$0': 'lando', '_app': app, '_yargs': {}};
+ const result = strip(argv, {...declared, globals});
+ expect(result._).to.deep.equal(['db-import', 'dump.sql']);
+ expect(result['--']).to.deep.equal(['-e', 'x']);
+ expect(result._app).to.equal(app);
+ expect(result).to.have.property('_yargs');
+ });
+
+ it('should take our own global options from the pre command region', function() {
+ // -vvv after the command must not win over the -vv before it
+ const argv = {verbose: 3, v: 3, host: 'db'};
+ const result = strip(argv, {...declared, globals});
+ expect(result.verbose).to.equal(2);
+ });
+
+ it('should keep unknown flags that came before the command', function() {
+ // "lando --deps console foo" still means --deps for us
+ const result = strip({deps: true}, {options: {}, globals: {...globals, deps: true}});
+ expect(result.deps).to.equal(true);
+ });
+
+ it('should keep the dynamic service key even if it was not declared as an option', function() {
+ const result = strip({host: 'db'}, {options: {}, dynamic: 'host', globals});
+ expect(result.host).to.equal('db');
+ });
+
+ it('should cope with being given nothing', function() {
+ expect(strip()).to.deep.equal({});
+ });
+});
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;
diff --git a/utils/build-init-runner.js b/utils/build-init-runner.js
index 421a5a164..fb6c410d3 100644
--- a/utils/build-init-runner.js
+++ b/utils/build-init-runner.js
@@ -6,9 +6,12 @@ module.exports = config => ({
project: config.project,
cmd: config.cmd,
opts: {
+ environment: require('./get-cli-env')(config.env),
mode: 'attach',
user: config.user,
services: ['init'],
autoRemove: config.remove,
+ workdir: config.workdir,
+ prestart: config.prestart,
},
});
diff --git a/utils/build-tooling-runner.js b/utils/build-tooling-runner.js
index 6436548c7..15c1313f9 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) => {
@@ -20,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,
@@ -32,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 089a07067..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,29 +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, 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
- .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 {
@@ -47,5 +81,6 @@ module.exports = (config, injected) => {
describe,
run,
options,
+ service,
};
};
diff --git a/utils/dump-compose-config.js b/utils/dump-compose-config.js
new file mode 100644
index 000000000..8488447f9
--- /dev/null
+++ b/utils/dump-compose-config.js
@@ -0,0 +1,28 @@
+'use strict';
+
+const path = require('path');
+
+const combinedFile = 'docker-compose.yml';
+
+/*
+ * Dumps a single fully rendered compose file next to the individual "name-index" ones so things that can only
+ * handle one compose file eg IDEs, editors or just running `docker compose` directly have something to work with
+ *
+ * This is `docker compose config` so it is the canonical merge of all our files and not something we have to
+ * implement and maintain ourselves
+ *
+ * NOTE: this file is intentionally *not* part of `app.compose` or we would apply everything twice
+ * NOTE: the project name is not optional, it is what named volumes and networks get prefixed with
+ */
+module.exports = async (engine, compose = [], project, dir, log) => {
+ // we cannot do anything without an engine or compose files
+ if (!engine || compose.length === 0) return undefined;
+
+ const file = path.join(dir, combinedFile);
+
+ await engine.compose('config', {compose, project, opts: {cmd: ['-o', file]}});
+ log?.debug('dumped combined compose file to %s', file);
+ return file;
+};
+
+module.exports.combinedFile = combinedFile;
diff --git a/utils/filter-v3-build-steps.js b/utils/filter-v3-build-steps.js
index fc3a2ca7e..2190f9c97 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],
},
@@ -37,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;
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/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-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,
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);
+};
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: [],
diff --git a/utils/get-core-tooling-additions.js b/utils/get-core-tooling-additions.js
new file mode 100644
index 000000000..c6ffb03e7
--- /dev/null
+++ b/utils/get-core-tooling-additions.js
@@ -0,0 +1,15 @@
+'use strict';
+
+const _ = require('lodash');
+
+/*
+ * Helper to work out which of the tooling commands lando provides on the users behalf should actually be added
+ *
+ * Note that we compare ids eg "db-import" and not keys eg "db-import ". This means a user who declares a
+ * plain "db-import" _replaces_ our "db-import " instead of ending up with both of them. It also means we
+ * never deep merge our defaults into a command the user has redefined.
+ */
+module.exports = (core = {}, tooling = {}) => {
+ const claimed = _(tooling).keys().map(key => key.split(' ')[0]).value();
+ return _.omitBy(core, (task, key) => _.includes(claimed, key.split(' ')[0]));
+};
diff --git a/utils/get-db-services.js b/utils/get-db-services.js
new file mode 100644
index 000000000..2b4305182
--- /dev/null
+++ b/utils/get-db-services.js
@@ -0,0 +1,38 @@
+'use strict';
+
+const _ = require('lodash');
+
+/*
+ * Helper to get the database "flavor" from a service type or image
+ *
+ * We tokenize instead of matching the whole string because recipes routinely prefix their service types
+ * eg "pantheon-mariadb" or suffix them with a version eg "mysql:8.0", and because images come with registries
+ * and tags attached eg "bitnami/postgresql:15"
+ */
+const getFlavor = value => {
+ if (typeof value !== 'string') return undefined;
+ const tokens = value.toLowerCase().split(/[-_:/]/);
+ if (_.includes(tokens, 'mariadb')) return 'mariadb';
+ if (_.includes(tokens, 'mysql')) return 'mysql';
+ if (!_.isEmpty(_.intersection(tokens, ['postgres', 'postgresql', 'pgsql']))) return 'postgres';
+ if (_.includes(tokens, 'mssql')) return 'mssql';
+ return undefined;
+};
+
+/*
+ * Helper to find any services that look like they could be a database
+ *
+ * We consider a service to be a database if its type is a known database eg "mysql", if its image looks like a
+ * known database eg "mariadb:10.4", or if it is literally called "database". The last two are mostly so services
+ * that come from an external compose file still get the default database tooling.
+ */
+module.exports = (info = [], images = {}) => _(info)
+ .map(service => ({
+ service: service.service,
+ type: service.type,
+ flavor: getFlavor(service.type) ?? getFlavor(images[service.service]),
+ }))
+ .filter(service => service.flavor !== undefined || service.service === 'database')
+ .value();
+
+module.exports.getFlavor = getFlavor;
diff --git a/utils/get-db-tooling-defaults.js b/utils/get-db-tooling-defaults.js
new file mode 100644
index 000000000..586afc732
--- /dev/null
+++ b/utils/get-db-tooling-defaults.js
@@ -0,0 +1,74 @@
+'use strict';
+
+const _ = require('lodash');
+
+// map of database flavors to the shell command we should add for them
+const shells = {
+ mariadb: {command: 'mariadb', description: 'Drops into a MariaDB shell on a database service'},
+ mysql: {command: 'mysql', description: 'Drops into a MySQL shell on a database service'},
+ postgres: {command: 'psql', description: 'Drops into a PostgreSQL shell on a database service'},
+};
+
+/*
+ * Helper to get the default database tooling
+ *
+ * This is the same tooling every recipe has historically had to redeclare for itself. The scripts it points at
+ * are provided by this plugin and mounted into every api 3 service at /helpers.
+ */
+module.exports = (services = [], host = 'database') => {
+ const hostOption = {
+ host: {
+ description: 'The database service to use',
+ default: host,
+ alias: ['h'],
+ },
+ };
+
+ const tooling = {
+ 'db-import ': {
+ service: ':host',
+ description: 'Imports a dump file into a database service',
+ cmd: '/helpers/sql-import.sh',
+ user: 'root',
+ options: _.merge({}, hostOption, {
+ 'no-wipe': {
+ description: 'Do not destroy the existing database before an import',
+ boolean: true,
+ },
+ }),
+ },
+ 'db-export [file]': {
+ service: ':host',
+ description: 'Exports database from a database service to a file',
+ cmd: '/helpers/sql-export.sh',
+ user: 'root',
+ options: _.merge({}, hostOption, {
+ stdout: {
+ description: 'Dump database to stdout',
+ },
+ }),
+ },
+ 'db': {
+ service: ':host',
+ description: 'Drops into a database shell on a database service',
+ cmd: '/helpers/sql-cli.sh',
+ options: _.merge({}, hostOption),
+ },
+ };
+
+ // add a flavor specific shell eg "lando mysql" for each flavor we were able to detect
+ _(services)
+ .map('flavor')
+ .filter(flavor => _.has(shells, flavor))
+ .uniq()
+ .forEach(flavor => {
+ tooling[shells[flavor].command] = {
+ service: ':host',
+ description: shells[flavor].description,
+ cmd: '/helpers/sql-cli.sh',
+ options: _.merge({}, hostOption),
+ };
+ });
+
+ return tooling;
+};
diff --git a/utils/get-docker-bin-path.js b/utils/get-docker-bin-path.js
index ec4a00f42..3b3ecf904 100644
--- a/utils/get-docker-bin-path.js
+++ b/utils/get-docker-bin-path.js
@@ -5,9 +5,8 @@ const path = require('path');
module.exports = (platform = process.landoPlatform ?? process.platform) => {
switch (platform) {
- case 'darwin':
- return '/Applications/Docker.app/Contents/Resources/bin';
case 'linux':
+ case 'wsl':
return '/usr/share/lando/bin';
case 'win32': {
const programFiles = process.env.ProgramW6432 || process.env.ProgramFiles;
@@ -20,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';
}
diff --git a/utils/get-host-lando-internal-hosts.js b/utils/get-host-lando-internal-hosts.js
new file mode 100644
index 000000000..8c9de4cce
--- /dev/null
+++ b/utils/get-host-lando-internal-hosts.js
@@ -0,0 +1,19 @@
+'use strict';
+
+const path = require('path');
+
+/*
+ * Returns the docker compose "extra_hosts" list that maps host.lando.internal at whatever the users machine actually
+ * lives at, returns an empty list if we could not work that out eg wsl2 with networking disabled
+ *
+ * Prefers the value resolved at bootstrap but falls back to resolving it directly, get-host-lando-internal memoizes
+ * so the fallback is cheap
+ */
+module.exports = lando => {
+ const resolved = lando?.config?.hostLandoInternal ?? require('./get-host-lando-internal')({
+ cacheDir: lando?.config?.userConfRoot ? path.join(lando.config.userConfRoot, 'cache') : undefined,
+ ideLocation: lando?.config?.xdebugIdeLocation,
+ });
+
+ return [`host.lando.internal:${resolved.extraHost}`];
+};
diff --git a/utils/get-host-lando-internal.js b/utils/get-host-lando-internal.js
new file mode 100644
index 000000000..3390f2f22
--- /dev/null
+++ b/utils/get-host-lando-internal.js
@@ -0,0 +1,143 @@
+'use strict';
+
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+
+const {isIP} = require('net');
+
+const Cache = require('../lib/cache');
+const debug = require('debug')('@lando/host-lando-internal');
+
+// the cache is invalidated on this so we only pay for the expensive powershell lookups once per wsl boot
+const getBootID = () => {
+ try {
+ return fs.readFileSync('/proc/sys/kernel/random/boot_id', {encoding: 'utf-8'}).trim();
+ } catch {
+ return 'unknown';
+ }
+};
+
+/*
+ * Works out what "host.lando.internal" needs to point at so that things inside a container can reach things that are
+ * listening on the users machine eg an xdebug listener in their ide
+ *
+ * This is a port of how ddev handles the same problem
+ * @see https://github.com/ddev/ddev/blob/master/pkg/dockerutil/host_docker_internal.go
+ *
+ * @param {String} ideLocation one of "wsl2", "container", an ip address or falsy for autodetection
+ * @return {Object} {extraHost, ip, mode, message} where extraHost is what to feed docker compose extra_hosts
+ */
+const resolve = ideLocation => {
+ // 1. the user has told us exactly where to go
+ if (isIP(ideLocation)) {
+ return {extraHost: ideLocation, ip: ideLocation, message: `ideLocation is the ip ${ideLocation}`};
+ }
+
+ // 2. the ide is listening inside the container itself eg a vscode language server style setup
+ if (ideLocation === 'container') {
+ return {extraHost: '127.0.0.1', ip: '127.0.0.1', message: 'ideLocation is "container"'};
+ }
+
+ // 3. everywhere that is not wsl2, host-gateway does the right thing on linux, docker desktop and colima
+ if (!os.release().toLowerCase().includes('microsoft')) {
+ return {extraHost: 'host-gateway', message: 'not wsl2 so host-gateway is fine'};
+ }
+
+ // 4. the ide is running inside wsl2 as well, that is just normal linux behavior
+ if (ideLocation === 'wsl2') {
+ return {extraHost: 'host-gateway', mode: 'wsl2', message: 'ideLocation is "wsl2" so host-gateway is fine'};
+ }
+
+ // 5. docker desktop proxies host-gateway through to the windows host for us so there is nothing to do
+ if (require('./is-docker-desktop')('wsl')) {
+ return {extraHost: 'host-gateway', mode: 'wsl2', message: 'wsl2 with docker desktop so host-gateway is fine'};
+ }
+
+ // from here on out we assume docker is running _inside_ wsl2 and the ide is on windows, host-gateway would only
+ // ever get us as far as the linux side of the fence so we need to find the windows host ourselves
+
+ // @NOTE: wslinfo does not exist on older wsl releases and nat was the only mode back then
+ const mode = require('./get-wsl-networking-mode')() ?? 'nat';
+
+ switch (mode) {
+ // no network bridge at all, there is no internet and no path to windows, nothing we can do here
+ case 'none':
+ return {extraHost: 'host-gateway', mode, message: 'wsl2 networkingMode=none, there is no network path to the windows host'};
+
+ // wsl2 shares the windows network namespace so ask windows for its own reachable address
+ // @NOTE: we also send bridged down this path, in bridged mode the default gateway is the physical router rather
+ // than windows so the nat lookup below would be flat out wrong
+ case 'mirrored':
+ case 'bridged': {
+ const ip = require('./get-wsl-mirrored-host-ip')();
+ if (!ip) {
+ return {
+ extraHost: 'host-gateway',
+ mode,
+ message: `wsl2 networkingMode=${mode} but we could not determine the windows host ip`,
+ };
+ }
+
+ const loopback = mode === 'mirrored' && !require('./is-wsl-host-loopback-enabled')();
+ const caveat = loopback ? ', note that hostAddressLoopback=true is NOT set in .wslconfig' : '';
+ return {extraHost: ip, ip, mode, message: `wsl2 networkingMode=${mode} windows host is ${ip}${caveat}`};
+ }
+
+ // the default gateway is the network router so we need the hyper-v virtual switch that joins windows to wsl2
+ case 'virtioproxy': {
+ const ip = require('./get-wsl-virtioproxy-host-ip')();
+ if (ip) return {extraHost: ip, ip, mode, message: `wsl2 networkingMode=virtioproxy windows host is ${ip}`};
+
+ // no hyper-v virtual switch, common on arm64 windows. fall back to wsl2s own ip so that host.lando.internal at
+ // least resolves, that gets an ide running inside wsl2 working but never one on windows
+ const fallback = require('./get-wsl-physical-ip')();
+ const message = 'wsl2 networkingMode=virtioproxy but there is no "vEthernet (WSL)" hyper-v switch, a windows '
+ + 'side ide cannot be reached. run your ide inside wsl2 instead and set xdebugIdeLocation: wsl2';
+ return fallback ? {extraHost: fallback, ip: fallback, mode, message} : {mode, message};
+ }
+
+ // nat, the default. the wsl2 default gateway _is_ the windows host eg the "vEthernet (WSL)" adapter
+ default: {
+ const ip = require('./get-wsl-nat-host-ip')();
+ if (!ip) return {mode, message: 'wsl2 networkingMode=nat but we could not determine the windows host ip'};
+ return {extraHost: ip, ip, mode, message: `wsl2 networkingMode=nat windows host is ${ip}`};
+ }
+ }
+};
+
+/*
+ * @param {Object} [opts]
+ * @param {Object} [opts.cache] a lando cache eg lando.cache, one is created from cacheDir if not passed in
+ * @param {String} [opts.cacheDir] where to put the file cache if we need to make our own cache
+ * @param {String} [opts.ideLocation] one of "wsl2", "container", an ip address or falsy for autodetection
+ * @param {Boolean} [opts.refresh] ignore whatever is cached and resolve again
+ * @return {Object} see resolve() above
+ */
+module.exports = ({cache, cacheDir = path.join(os.homedir(), '.lando', 'cache'), ideLocation, refresh = false} = {}) => {
+ // normalize a few falsy-ish things the user might put in their config
+ if (!ideLocation || ideLocation === 'auto') ideLocation = undefined;
+
+ // resolution is free outside of wsl2 so there is nothing worth caching
+ if (!os.release().toLowerCase().includes('microsoft') || isIP(ideLocation) || ideLocation === 'container') {
+ return resolve(ideLocation);
+ }
+
+ // otherwise we cache because we would otherwise pay for a powershell spawn on every single lando command
+ cache = cache ?? new Cache({cacheDir});
+ const key = `_.host-lando-internal.${ideLocation ?? 'auto'}`;
+
+ // the cache is invalidated by boot id because the windows host ip can only really change when the wsl2 vm restarts
+ const bootID = getBootID();
+ const cached = refresh ? undefined : cache.get(key);
+ if (cached && cached.bootID === bootID) {
+ debug('using cached %o', cached.result);
+ return cached.result;
+ }
+
+ const result = resolve(ideLocation);
+ debug('resolved host.lando.internal to %o because %s', result.extraHost, result.message);
+ cache.set(key, {bootID, result}, {persist: true});
+
+ return result;
+};
diff --git a/utils/get-init-runner-defaults.js b/utils/get-init-runner-defaults.js
index d63666c68..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'),
@@ -21,10 +22,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-passthrough-command.js b/utils/get-passthrough-command.js
new file mode 100644
index 000000000..4af36293b
--- /dev/null
+++ b/utils/get-passthrough-command.js
@@ -0,0 +1,15 @@
+'use strict';
+
+const _ = require('lodash');
+
+/*
+ * Helper to work out whether the command being run is one that passes its args through
+ *
+ * Returns the command id if it does and undefined if it does not, which is exactly what cli.passthrough wants.
+ */
+module.exports = (tasks = [], command = undefined) => {
+ if (typeof command !== 'string' || command.length === 0) return undefined;
+
+ const task = _.find(tasks, task => (task.id ?? _.get(task, 'command', '').split(' ')[0]) === command);
+ return _.get(task, 'passthrough', false) ? command : undefined;
+};
diff --git a/utils/get-service-images.js b/utils/get-service-images.js
new file mode 100644
index 000000000..a83d195d9
--- /dev/null
+++ b/utils/get-service-images.js
@@ -0,0 +1,16 @@
+'use strict';
+
+const _ = require('lodash');
+
+/*
+ * Helper to get a map of service name to image from compose data
+ *
+ * Note that services built from an imagefile eg api 4 services will not show up here and that later definitions
+ * of the same service win, which matches how the compose data is merged downstream.
+ */
+module.exports = (composeData = []) => _(composeData)
+ .flatMap(data => _.get(data, 'data', []))
+ .flatMap(data => _.map(_.get(data, 'services', {}), (config, service) => ([service, _.get(config, 'image')])))
+ .filter(pair => typeof pair[1] === 'string')
+ .fromPairs()
+ .value();
diff --git a/utils/get-tasks.js b/utils/get-tasks.js
index 3ae593808..e3b495292 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)));
};
/*
@@ -53,6 +55,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);
@@ -122,6 +126,27 @@ module.exports = (config = {}, argv = {}, tasks = []) => {
}
}
+ // Load the compose cache if we have one, note that we need to do this _before_ we build our tooling tasks
+ // below so any core provided tooling eg database tooling is included
+ let composeCache = {};
+ if (fs.existsSync(config.composeCache)) {
+ try {
+ composeCache = JSON.parse(fs.readFileSync(config.composeCache, {encoding: 'utf-8'}));
+ } catch (e) {
+ throw new Error(`There was a problem with parsing ${config.composeCache}. Ensure it is valid JSON! ${e}`);
+ }
+
+ // add additional items
+ config.allServices = composeCache.allServices ?? [];
+ config.info = composeCache.info ?? [];
+ config.primary = composeCache.primary ?? 'appserver';
+ config.sapis = composeCache.sapis ?? {};
+
+ // mix in tooling core has added on our behalf, note that user and recipe tooling always wins
+ const additions = require('./get-core-tooling-additions')(composeCache.coreTooling ?? {}, config.tooling ?? {});
+ config.tooling = _.merge({}, additions, config.tooling ?? {});
+ }
+
// lets add ids to help match commands with args?
_.forEach(config.tooling, (task, command) => {
if (_.isObject(task) && typeof command === 'string') task.id = task.id || command.split(' ')[0];
@@ -129,7 +154,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) => {
@@ -141,6 +166,11 @@ module.exports = (config = {}, argv = {}, tasks = []) => {
describe: _.get(task, 'description', `Runs ${command} commands`),
examples: _.get(task, 'examples', []),
level,
+ // tooling commands pass their args straight through so any global options that show up after the
+ // command belong to the command and not to us
+ passthrough: true,
+ // dynamic services resolve their service from an answer eg ":host" so we need to keep that key around
+ dynamic: _.startsWith(_.get(task, 'service', ''), ':') ? _.trimStart(task.service, ':') : undefined,
options: _.get(task, 'options', {}),
positionals: _.get(task, 'positionals', {}),
usage: _.get(task, 'usage', command),
@@ -152,23 +182,8 @@ module.exports = (config = {}, argv = {}, tasks = []) => {
// get core tasks
const coreTasks = _(loadCacheFile(process.landoTaskCacheFile)).map(t => ([t.command, t])).fromPairs().value();
- // mix in any relevant compose cache things
- if (fs.existsSync(config.composeCache)) {
- try {
- const composeCache = JSON.parse(fs.readFileSync(config.composeCache, {encoding: 'utf-8'}));
-
- // merge in additional tooling;
- Object.assign(coreTasks, composeCache?.overrides?.tooling ?? {});
-
- // add additional items
- config.allServices = composeCache.allServices ?? [];
- config.info = composeCache.info ?? [];
- config.primary = composeCache.primary ?? 'appserver';
- config.sapis = composeCache.sapis ?? {};
- } catch (e) {
- throw new Error(`There was a problem with parsing ${config.composeCache}. Ensure it is valid JSON! ${e}`);
- }
- }
+ // merge in additional tooling;
+ Object.assign(coreTasks, composeCache?.overrides?.tooling ?? {});
// and combine
return tasks.concat(_.map(coreTasks, task => task));
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,
});
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/get-win32-ip-from-wsl.js b/utils/get-win32-ip-from-wsl.js
new file mode 100644
index 000000000..ea5182ede
--- /dev/null
+++ b/utils/get-win32-ip-from-wsl.js
@@ -0,0 +1,23 @@
+'use strict';
+
+const {isIP} = require('net');
+
+const stringer = require('./spawn-sync-stringer');
+
+/*
+ * Runs a powershell script that is expected to print a single ipv4 address and returns it
+ *
+ * Requires wsl interop eg the ability to invoke powershell.exe from inside the distro
+ */
+module.exports = script => {
+ try {
+ const args = ['-NoProfile', '-NonInteractive', '-Command', script];
+ const {status, stdout} = stringer('powershell.exe', args, {encoding: 'utf-8'});
+ if (status !== 0) return undefined;
+
+ const ip = stdout.split('\n')[0].trim();
+ return isIP(ip) === 4 ? ip : undefined;
+ } catch {
+ return undefined;
+ }
+};
diff --git a/utils/get-wsl-mirrored-host-ip.js b/utils/get-wsl-mirrored-host-ip.js
new file mode 100644
index 000000000..f8eb196ec
--- /dev/null
+++ b/utils/get-wsl-mirrored-host-ip.js
@@ -0,0 +1,36 @@
+'use strict';
+
+const getWinIP = require('./get-win32-ip-from-wsl');
+
+/*
+ * In mirrored mode the wsl2 vm shares the windows network namespace so there is no gateway that points at windows,
+ * instead we ask windows which of its own addresses is the one a container should be able to reach
+ *
+ * Rather than scanning every interface we
+ *
+ * 1. find the best default route (0.0.0.0/0) by RouteMetric/InterfaceMetric
+ * 2. take that routes InterfaceIndex
+ * 3. return the first non link local, non loopback ipv4 on that interface
+ *
+ * @NOTE: this also requires hostAddressLoopback=true in the users .wslconfig, see is-wsl-host-loopback-enabled
+ */
+const SCRIPT = `
+$bestRoute = Get-NetRoute -DestinationPrefix '0.0.0.0/0' -ErrorAction SilentlyContinue |
+ Where-Object { $_.NextHop -ne '0.0.0.0' } |
+ Sort-Object -Property RouteMetric, InterfaceMetric |
+ Select-Object -First 1
+
+if (-not $bestRoute) {
+ return
+}
+
+Get-NetIPAddress -AddressFamily IPv4 -InterfaceIndex $bestRoute.InterfaceIndex -ErrorAction SilentlyContinue |
+ Where-Object {
+ $_.IPAddress -and
+ $_.IPAddress -notlike '169.254*' -and
+ $_.IPAddress -ne '127.0.0.1'
+ } |
+ Select-Object -First 1 -ExpandProperty IPAddress
+`;
+
+module.exports = () => getWinIP(SCRIPT);
diff --git a/utils/get-wsl-nat-host-ip.js b/utils/get-wsl-nat-host-ip.js
new file mode 100644
index 000000000..0659f40da
--- /dev/null
+++ b/utils/get-wsl-nat-host-ip.js
@@ -0,0 +1,26 @@
+'use strict';
+
+const {isIP} = require('net');
+
+const stringer = require('./spawn-sync-stringer');
+
+/*
+ * Returns the windows host ip as seen from inside a wsl2 vm running in "nat" networking mode
+ *
+ * In nat mode the default gateway of the wsl2 vm _is_ the windows host eg the ip of the "vEthernet (WSL)" adapter
+ * on the windows side, this is the address a container needs in order to reach something listening on windows
+ *
+ * @see https://learn.microsoft.com/en-us/windows/wsl/networking#accessing-windows-networking-apps-from-linux-host-ip
+ */
+module.exports = () => {
+ try {
+ const {status, stdout} = stringer('ip', ['-4', 'route', 'show', 'default'], {encoding: 'utf-8'});
+ if (status !== 0) return undefined;
+
+ // output looks like "default via 172.28.128.1 dev eth0 proto kernel"
+ const ip = stdout.split(/\s+/)[2];
+ return isIP(ip) === 4 ? ip : undefined;
+ } catch {
+ return undefined;
+ }
+};
diff --git a/utils/get-wsl-networking-mode.js b/utils/get-wsl-networking-mode.js
new file mode 100644
index 000000000..f97f25392
--- /dev/null
+++ b/utils/get-wsl-networking-mode.js
@@ -0,0 +1,25 @@
+'use strict';
+
+const stringer = require('./spawn-sync-stringer');
+
+// the modes wsl currently knows about
+// @see https://learn.microsoft.com/en-us/windows/wsl/wsl-config#configuration-settings-for-wslconfig
+const MODES = ['nat', 'mirrored', 'virtioproxy', 'none', 'bridged'];
+
+/*
+ * Returns the networking mode of the wsl2 vm we are running in eg nat|mirrored|virtioproxy|none|bridged
+ *
+ * Returns undefined if we cannot determine it, note that `wslinfo` only exists on newer wsl releases so on older
+ * ones the caller should assume "nat" because that was the only option back then
+ */
+module.exports = () => {
+ try {
+ const {status, stdout} = stringer('wslinfo', ['--networking-mode'], {encoding: 'utf-8'});
+ if (status !== 0) return undefined;
+
+ const mode = stdout.toLowerCase();
+ return MODES.includes(mode) ? mode : undefined;
+ } catch {
+ return undefined;
+ }
+};
diff --git a/utils/get-wsl-physical-ip.js b/utils/get-wsl-physical-ip.js
new file mode 100644
index 000000000..ec1f5ac9d
--- /dev/null
+++ b/utils/get-wsl-physical-ip.js
@@ -0,0 +1,24 @@
+'use strict';
+
+const {isIP} = require('net');
+
+const stringer = require('./spawn-sync-stringer');
+
+/*
+ * Returns the wsl2 vms _own_ physical ip eg the source address it uses for outbound traffic
+ *
+ * This is only useful as a fallback, it gets containers to the wsl2 side of things but never to windows
+ */
+module.exports = () => {
+ try {
+ const {status, stdout} = stringer('ip', ['-4', 'route', 'get', '1.1.1.1'], {encoding: 'utf-8'});
+ if (status !== 0) return undefined;
+
+ // output looks like "1.1.1.1 via 10.0.0.1 dev eth0 src 10.0.0.42 uid 1000"
+ const parts = stdout.split(/\s+/);
+ const ip = parts[parts.indexOf('src') + 1];
+ return isIP(ip) === 4 ? ip : undefined;
+ } catch {
+ return undefined;
+ }
+};
diff --git a/utils/get-wsl-virtioproxy-host-ip.js b/utils/get-wsl-virtioproxy-host-ip.js
new file mode 100644
index 000000000..39f3417bc
--- /dev/null
+++ b/utils/get-wsl-virtioproxy-host-ip.js
@@ -0,0 +1,16 @@
+'use strict';
+
+const getWinIP = require('./get-win32-ip-from-wsl');
+
+/*
+ * In virtioproxy mode the default gateway is the actual network router and not the windows host so we need the ip of
+ * the "vEthernet (WSL)" hyper-v virtual switch instead
+ *
+ * @NOTE: this switch does not exist on every machine eg arm64 windows, in that case this returns undefined and a
+ * windows side ide simply is not reachable
+ */
+const SCRIPT = `Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue |
+ Where-Object { $_.InterfaceAlias -like 'vEthernet (WSL*' } |
+ Select-Object -First 1 -ExpandProperty IPAddress`;
+
+module.exports = () => getWinIP(SCRIPT);
diff --git a/utils/is-docker-desktop.js b/utils/is-docker-desktop.js
new file mode 100644
index 000000000..84ef9477f
--- /dev/null
+++ b/utils/is-docker-desktop.js
@@ -0,0 +1,32 @@
+'use strict';
+
+const fs = require('fs');
+
+const stringer = require('./spawn-sync-stringer');
+
+// docker desktops wsl2 integration always mounts itself here, if this is missing we definitely are not on it
+const DD_WSL_MOUNT = '/mnt/wsl/docker-desktop';
+
+/*
+ * Returns true if the build engine we are talking to is docker desktop
+ *
+ * This matters on wsl2 because docker desktop proxies "host-gateway" all the way through to the windows host while a
+ * docker-ce running inside the distro only ever gets you to the linux side
+ */
+module.exports = (platform = process.landoPlatform ?? process.platform) => {
+ // cheap and definitive negative on wsl
+ if (platform === 'wsl' && !fs.existsSync(DD_WSL_MOUNT)) return false;
+
+ // otherwise just ask the daemon
+ try {
+ const docker = require('./get-docker-x')();
+ if (!docker) return false;
+
+ const {status, stdout} = stringer(docker, ['info', '--format', '{{.OperatingSystem}}'], {encoding: 'utf-8'});
+ if (status !== 0) return false;
+
+ return stdout.toLowerCase().includes('docker desktop');
+ } catch {
+ return false;
+ }
+};
diff --git a/utils/is-wsl-host-loopback-enabled.js b/utils/is-wsl-host-loopback-enabled.js
new file mode 100644
index 000000000..a68b7feb6
--- /dev/null
+++ b/utils/is-wsl-host-loopback-enabled.js
@@ -0,0 +1,62 @@
+'use strict';
+
+const fs = require('fs');
+
+const getWinEnvar = require('./get-win32-envvar-from-wsl');
+const wslpath = require('./winpath-2-wslpath');
+
+/*
+ * Locates the users windows side .wslconfig, returns undefined if we cannot find it
+ */
+const getWslConfigPath = () => {
+ try {
+ const userProfile = process.env.USERPROFILE ?? getWinEnvar('USERPROFILE');
+ if (!userProfile) return undefined;
+
+ const configPath = `${wslpath(userProfile)}/.wslconfig`;
+ return fs.existsSync(configPath) ? configPath : undefined;
+ } catch {
+ return undefined;
+ }
+};
+
+/*
+ * Returns true if hostAddressLoopback=true is set under [experimental] in the users .wslconfig
+ *
+ * Mirrored networking mode needs this in order for anything inside wsl2 to be able to connect back to a listener on
+ * the windows host
+ */
+module.exports = () => {
+ const configPath = getWslConfigPath();
+ if (!configPath) return false;
+
+ let contents;
+ try {
+ contents = fs.readFileSync(configPath, {encoding: 'utf-8'});
+ } catch {
+ return false;
+ }
+
+ let experimental = false;
+
+ for (const raw of contents.split('\n')) {
+ const line = raw.trim().replace(/\r$/, '');
+
+ // skip comments and empties
+ if (line === '' || line.startsWith('#') || line.startsWith(';')) continue;
+
+ // track sections
+ if (line.startsWith('[') && line.endsWith(']')) {
+ experimental = line.slice(1, -1).trim().toLowerCase() === 'experimental';
+ continue;
+ }
+
+ if (!experimental) continue;
+
+ const [key, value] = line.split('=');
+ if (typeof value !== 'string') continue;
+ if (key.trim().toLowerCase() === 'hostaddressloopback' && value.trim().toLowerCase() === 'true') return true;
+ }
+
+ return false;
+};
diff --git a/utils/load-compose-files.js b/utils/load-compose-files.js
index f13bf4cec..e4ad0f072 100644
--- a/utils/load-compose-files.js
+++ b/utils/load-compose-files.js
@@ -2,8 +2,79 @@
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();
+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-name ${project} -f ${files} config -o ${outputFile}` to resolve
+// things like relative paths so they still work once we dump this into the lando config root
+module.exports = async (files, dir, landoComposeConfigDir, engine, project, envFiles, log, orchestratorSeperator) => {
+ const composeFilePaths = _(require('./normalize-files')(files, dir)).value();
+ if (_.isEmpty(composeFilePaths)) {
+ return [];
+ }
+
+ 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 engine.compose('config', {compose: composeFilePaths, project, opts: {envFiles, cmd: ['-o', outputFile]}});
+ const result = yaml.load(outputFile);
+ fs.unlinkSync(outputFile);
+ remove(path.dirname(outputFile));
+
+ return resolveServiceCommands([result], engine.docker, log, engine, composeFilePaths, project, orchestratorSeperator);
+};
diff --git a/utils/parse-events-config.js b/utils/parse-events-config.js
index 017fd3b62..8dc1fa07c 100644
--- a/utils/parse-events-config.js
+++ b/utils/parse-events-config.js
@@ -36,9 +36,10 @@ 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
const cstdio = _.get(app, '_config.orchestratorMV', 2) ? 'inherit' : ['inherit', 'pipe', 'pipe'];
@@ -53,6 +54,25 @@ module.exports = (cmds, app, data = {}) => _.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) {
@@ -76,6 +96,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, _app: app}),
+ {cmd, workdir: '/app'},
+ )),
+ {isInitEventCommand: true},
+ );
+ }
+
// Validate the service if we can
// @NOTE fast engine runs might not have this data yet
if (
@@ -95,6 +128,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: {
diff --git a/utils/parse-proxy-url.js b/utils/parse-proxy-url.js
index a7e244b19..eb99cf241 100644
--- a/utils/parse-proxy-url.js
+++ b/utils/parse-proxy-url.js
@@ -8,9 +8,16 @@ module.exports = data => {
// We add the protocol ourselves, so it can be parsed. We also change all *
// occurrences for our magic word __wildcard__, because otherwise the url parser
// won't parse wildcards in the hostname correctly.
- const parsedUrl = _.isString(data) ? url.parse(`http://${data}`.replace(/\*/g, '__wildcard__')) : _.merge({}, data, {
- hostname: data.hostname.replace(/\*/g, '__wildcard__'),
- });
+ // Object routes may carry a port or pathname inside the hostname (eg
+ // `hostname: foo.lndo.site:9000/path`) so we parse the hostname either way and
+ // then let any explicit keys on the object win over what we parsed out.
+ const hostname = _.isString(data) ? data : data.hostname;
+ const parsedUrl = url.parse(`http://${hostname}`.replace(/\*/g, '__wildcard__'));
+ if (!_.isString(data)) {
+ _.forEach(_.omit(data, ['hostname']), (value, key) => {
+ if (!_.isNil(value)) parsedUrl[key] = value;
+ });
+ }
// If the port is null then set it to 80
if (_.isNil(parsedUrl.port)) parsedUrl.port = '80';
diff --git a/utils/parse-tooling-config.js b/utils/parse-tooling-config.js
index e3e21cddd..06b42c592 100644
--- a/utils/parse-tooling-config.js
+++ b/utils/parse-tooling-config.js
@@ -20,11 +20,17 @@ 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 = {}, name = '') => {
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));
+ const noise = getDynamicKeys(answer, answers).concat(answer);
+ // Remove dynamic service option from argv, note that we only consider things _after_ the command itself
+ // because the rest of the pipeline does the same. Without this a command whose name happens to match the
+ // service it resolves to eg "lando db" on a service called "db" would strip out its own command name.
+ const start = argv.findIndex(value => value === name.split(' ')[0]);
+ for (let i = argv.length - 1; i > start; i--) {
+ if (_.includes(noise, argv[i])) argv.splice(i, 1);
+ }
// get the service
const service = answers[config.service.split(':')[1]];
// Return updated config
@@ -41,9 +47,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, argv, argopts = []) => {
// Append any user specificed opts
- argopts = argopts.concat(process.argv.slice(3));
+ 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
@@ -74,13 +80,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))
+ .map(config => handleDynamic(config, answers._eventArgs ?? process.argv, answers, sapis, name))
// Add in any argv extras if they've been passed in
- .map(config => handleOpts(config, 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
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({}, {
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])));
});
diff --git a/utils/strip-passthrough-args.js b/utils/strip-passthrough-args.js
new file mode 100644
index 000000000..ab0753ef1
--- /dev/null
+++ b/utils/strip-passthrough-args.js
@@ -0,0 +1,44 @@
+'use strict';
+
+const _ = require('lodash');
+
+// argv keys that are structural and must always survive
+const structural = ['_', '--', '$0', '_app', '_yargs', '_eventArgs'];
+
+/*
+ * Helper to expand an option name into every key yargs might set for it
+ */
+const expand = (name, config = {}) => _([name])
+ .concat(_.get(config, 'alias', []))
+ .flatten()
+ .filter(_.isString)
+ .flatMap(key => [key, _.camelCase(key)])
+ .uniq()
+ .value();
+
+/*
+ * Helper to remove the args that belong to a passthrough command from our own argv
+ *
+ * Tooling commands hand everything after the command name to the thing they wrap, but yargs still parses those
+ * args and we read a few things straight off the result eg "autoRemove" in utils/build-tooling-task.js. That
+ * means an "--auto-remove" meant for the wrapped command would quietly change how we run the container.
+ *
+ * So we keep only what we actually declared for the command plus whatever global options showed up before it.
+ * The upshot is that "lando console foo --deps" passes --deps to console while "lando --deps console foo"
+ * still means it for us.
+ */
+module.exports = (argv = {}, {options = {}, positionals = {}, dynamic = undefined, globals = {}} = {}) => {
+ // everything the command itself declared, including aliases and their camelCase forms
+ const declared = _([])
+ .concat(_.flatMap(options, (config, name) => expand(name, config)))
+ .concat(_.flatMap(positionals, (config, name) => expand(name, config)))
+ .concat(dynamic ? expand(dynamic) : [])
+ .concat(structural)
+ .uniq()
+ .value();
+
+ // our own options only ever come from the region before the command
+ const ours = _.omit(globals, ['_', '$0', '--']);
+
+ return {...ours, ..._.pick(argv, declared)};
+};