Compare commits

..

No commits in common. "main" and "v2.24.5" have entirely different histories.

479 changed files with 5703 additions and 18764 deletions

2
.github/CODEOWNERS vendored
View File

@ -1,2 +0,0 @@
# global rules
* @docker/compose-maintainers

View File

@ -12,12 +12,6 @@ body:
Include both the current behavior (what you are seeing) as well as what you expected to happen. Include both the current behavior (what you are seeing) as well as what you expected to happen.
validations: validations:
required: true required: true
- type: markdown
attributes:
value: |
[Docker Swarm](https://www.mirantis.com/software/swarm/) uses a distinct compose file parser and
as such doesn't support some of the recent features of Docker Compose. Please contact Mirantis
if you need assistance with compose file support in Docker Swarm.
- type: textarea - type: textarea
attributes: attributes:
label: Steps To Reproduce label: Steps To Reproduce

44
.github/SECURITY.md vendored
View File

@ -1,44 +0,0 @@
# Security Policy
The maintainers of Docker Compose take security seriously. If you discover
a security issue, please bring it to their attention right away!
## Reporting a Vulnerability
Please **DO NOT** file a public issue, instead send your report privately
to [security@docker.com](mailto:security@docker.com).
Reporter(s) can expect a response within 72 hours, acknowledging the issue was
received.
## Review Process
After receiving the report, an initial triage and technical analysis is
performed to confirm the report and determine its scope. We may request
additional information in this stage of the process.
Once a reviewer has confirmed the relevance of the report, a draft security
advisory will be created on GitHub. The draft advisory will be used to discuss
the issue with maintainers, the reporter(s), and where applicable, other
affected parties under embargo.
If the vulnerability is accepted, a timeline for developing a patch, public
disclosure, and patch release will be determined. If there is an embargo period
on public disclosure before the patch release, the reporter(s) are expected to
participate in the discussion of the timeline and abide by agreed upon dates
for public disclosure.
## Accreditation
Security reports are greatly appreciated and we will publicly thank you,
although we will keep your name confidential if you request it. We also like to
send gifts - if you're into swag, make sure to let us know. We do not currently
offer a paid security bounty program at this time.
## Supported Versions
This project docs not provide long-term supported versions, and only the current
release and `main` branch are actively maintained. Docker Compose v1, and the
corresponding [v1 branch](https://github.com/docker/compose/tree/v1) reached
EOL and are no longer supported.

View File

@ -19,5 +19,6 @@ updates:
- dependency-name: "github.com/containerd/containerd" - dependency-name: "github.com/containerd/containerd"
# containerd major/minor must be kept in sync with moby # containerd major/minor must be kept in sync with moby
update-types: [ "version-update:semver-major", "version-update:semver-minor" ] update-types: [ "version-update:semver-major", "version-update:semver-minor" ]
# OTEL dependencies should be upgraded in sync with engine, cli, buildkit and buildx projects - dependency-name: "go.opentelemetry.io/otel/*"
- dependency-name: "go.opentelemetry.io/*" # OTEL is v1.x but has some parts that are not API stable yet
update-types: [ "version-update:semver-major", "version-update:semver-minor"]

2
.github/stale.yml vendored
View File

@ -1,7 +1,7 @@
# Configuration for probot-stale - https://github.com/probot/stale # Configuration for probot-stale - https://github.com/probot/stale
# Number of days of inactivity before an Issue or Pull Request becomes stale # Number of days of inactivity before an Issue or Pull Request becomes stale
daysUntilStale: 90 daysUntilStale: 180
# Number of days of inactivity before an Issue or Pull Request with the stale label is closed. # Number of days of inactivity before an Issue or Pull Request with the stale label is closed.
# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. # Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.

View File

@ -18,6 +18,9 @@ on:
required: false required: false
default: "false" default: "false"
env:
DOCKER_CLI_VERSION: "25.0.1"
permissions: permissions:
contents: read # to fetch code (actions/checkout) contents: read # to fetch code (actions/checkout)
@ -29,7 +32,7 @@ jobs:
steps: steps:
- -
name: Checkout name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v3
- -
name: Create matrix name: Create matrix
id: platforms id: platforms
@ -53,10 +56,10 @@ jobs:
steps: steps:
- -
name: Checkout name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v3
- -
name: Set up Docker Buildx name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v2
- -
name: Run name: Run
run: | run: |
@ -71,64 +74,49 @@ jobs:
matrix: matrix:
platform: ${{ fromJson(needs.prepare.outputs.matrix) }} platform: ${{ fromJson(needs.prepare.outputs.matrix) }}
steps: steps:
-
name: Checkout
uses: actions/checkout@v4
- -
name: Prepare name: Prepare
run: | run: |
platform=${MATRIX_PLATFORM} platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
env: -
MATRIX_PLATFORM: ${{ matrix.platform }} name: Checkout
uses: actions/checkout@v3
- -
name: Set up QEMU name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v2
- -
name: Set up Docker Buildx name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v2
- -
name: Build name: Build
uses: docker/bake-action@v6 uses: docker/bake-action@v2
with: with:
source: .
targets: release targets: release
provenance: mode=max
sbom: true
set: | set: |
*.platform=${{ matrix.platform }} *.platform=${{ matrix.platform }}
*.cache-from=type=gha,scope=binary-${{ env.PLATFORM_PAIR }} *.cache-from=type=gha,scope=binary-${{ env.PLATFORM_PAIR }}
*.cache-to=type=gha,scope=binary-${{ env.PLATFORM_PAIR }},mode=max *.cache-to=type=gha,scope=binary-${{ env.PLATFORM_PAIR }},mode=max
-
name: Rename provenance and sbom
working-directory: ./bin/release
run: |
binname=$(find . -name 'docker-compose-*')
filename=$(basename "$binname" | sed -E 's/\.exe$//')
mv "provenance.json" "${filename}.provenance.json"
mv "sbom-binary.spdx.json" "${filename}.sbom.json"
find . -name 'sbom*.json' -exec rm {} \;
-
name: List artifacts
run: |
tree -nh ./bin/release
- -
name: Upload artifacts name: Upload artifacts
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: compose-${{ env.PLATFORM_PAIR }} name: compose
path: ./bin/release path: ./bin/release/*
if-no-files-found: error if-no-files-found: error
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
-
name: Checkout
uses: actions/checkout@v3
- -
name: Set up Docker Buildx name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v2
- -
name: Test name: Test
uses: docker/bake-action@v6 uses: docker/bake-action@v2
with: with:
targets: test targets: test
set: | set: |
@ -136,17 +124,12 @@ jobs:
*.cache-to=type=gha,scope=test *.cache-to=type=gha,scope=test
- -
name: Gather coverage data name: Gather coverage data
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: coverage-data-unit name: coverage-data-unit
path: bin/coverage/unit/ path: bin/coverage/unit/
if-no-files-found: error if-no-files-found: error
-
name: Unit Test Summary
uses: test-summary/action@v2
with:
paths: bin/coverage/unit/report.xml
if: always()
e2e: e2e:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
@ -155,53 +138,29 @@ jobs:
mode: mode:
- plugin - plugin
- standalone - standalone
engine:
- 26
- 27
- 28
steps: steps:
- name: Prepare -
run: | name: Checkout
mode=${{ matrix.mode }} uses: actions/checkout@v3
engine=${{ matrix.engine }} -
echo "MODE_ENGINE_PAIR=${mode}-${engine}" >> $GITHUB_ENV name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Checkout -
uses: actions/checkout@v4 name: Set up Go
uses: actions/setup-go@v3
- name: Install Docker ${{ matrix.engine }}
run: |
sudo systemctl stop docker.service
sudo apt-get purge docker-ce docker-ce-cli containerd.io docker-compose-plugin docker-ce-rootless-extras docker-buildx-plugin
sudo apt-get install curl
curl -fsSL https://test.docker.com -o get-docker.sh
sudo sh ./get-docker.sh --version ${{ matrix.engine }}
- name: Check Docker Version
run: docker --version
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Set up Docker Model
run: |
sudo apt-get install docker-model-plugin
docker model version
- name: Set up Go
uses: actions/setup-go@v5
with: with:
go-version-file: 'go.mod' go-version-file: 'go.mod'
check-latest: true check-latest: true
cache: true cache: true
-
- name: Build example provider name: Setup docker CLI
run: make example-provider run: |
curl https://download.docker.com/linux/static/stable/x86_64/docker-${DOCKER_CLI_VERSION}.tgz | tar xz
- name: Build sudo cp ./docker/docker /usr/bin/ && rm -rf docker && docker version
uses: docker/bake-action@v6 -
name: Build
uses: docker/bake-action@v2
with: with:
source: .
targets: binary-with-coverage targets: binary-with-coverage
set: | set: |
*.cache-from=type=gha,scope=binary-linux-amd64 *.cache-from=type=gha,scope=binary-linux-amd64
@ -209,72 +168,65 @@ jobs:
*.cache-to=type=gha,scope=binary-e2e-${{ matrix.mode }},mode=max *.cache-to=type=gha,scope=binary-e2e-${{ matrix.mode }},mode=max
env: env:
BUILD_TAGS: e2e BUILD_TAGS: e2e
-
- name: Setup tmate session name: Setup tmate session
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled }} if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled }}
uses: mxschmitt/action-tmate@8b4e4ac71822ed7e0ad5fb3d1c33483e9e8fb270 # v3.11 uses: mxschmitt/action-tmate@8b4e4ac71822ed7e0ad5fb3d1c33483e9e8fb270 # v3.11
with: with:
limit-access-to-actor: true limit-access-to-actor: true
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
-
- name: Test plugin mode name: Test plugin mode
if: ${{ matrix.mode == 'plugin' }} if: ${{ matrix.mode == 'plugin' }}
run: | run: |
rm -rf ./bin/coverage/e2e rm -rf ./bin/coverage/e2e
mkdir -p ./bin/coverage/e2e mkdir -p ./bin/coverage/e2e
make e2e-compose GOCOVERDIR=bin/coverage/e2e TEST_FLAGS="-v" make e2e-compose GOCOVERDIR=bin/coverage/e2e TEST_FLAGS="-v"
-
- name: Gather coverage data name: Gather coverage data
if: ${{ matrix.mode == 'plugin' }} if: ${{ matrix.mode == 'plugin' }}
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: coverage-data-e2e-${{ env.MODE_ENGINE_PAIR }} name: coverage-data-e2e
path: bin/coverage/e2e/ path: bin/coverage/e2e/
if-no-files-found: error if-no-files-found: error
-
- name: Test standalone mode name: Test standalone mode
if: ${{ matrix.mode == 'standalone' }} if: ${{ matrix.mode == 'standalone' }}
run: | run: |
rm -f /usr/local/bin/docker-compose rm -f /usr/local/bin/docker-compose
cp bin/build/docker-compose /usr/local/bin cp bin/build/docker-compose /usr/local/bin
make e2e-compose-standalone make e2e-compose-standalone
- name: e2e Test Summary
uses: test-summary/action@v2
with:
paths: /tmp/report/report.xml
if: always()
coverage: coverage:
runs-on: ubuntu-latest runs-on: ubuntu-22.04
needs: needs:
- test - test
- e2e - e2e
steps: steps:
# codecov won't process the report without the source code available # codecov won't process the report without the source code available
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v3
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v5 uses: actions/setup-go@v4
with: with:
go-version-file: 'go.mod' go-version-file: 'go.mod'
check-latest: true check-latest: true
- name: Download unit test coverage - name: Download unit test coverage
uses: actions/download-artifact@v4 uses: actions/download-artifact@v3
with: with:
name: coverage-data-unit name: coverage-data-unit
path: coverage/unit path: coverage/unit
merge-multiple: true
- name: Download E2E test coverage - name: Download E2E test coverage
uses: actions/download-artifact@v4 uses: actions/download-artifact@v3
with: with:
pattern: coverage-data-e2e-* name: coverage-data-e2e
path: coverage/e2e path: coverage/e2e
merge-multiple: true
- name: Merge coverage reports - name: Merge coverage reports
run: | run: |
go tool covdata textfmt -i=./coverage/unit,./coverage/e2e -o ./coverage.txt go tool covdata textfmt -i=./coverage/unit,./coverage/e2e -o ./coverage.txt
- name: Store coverage report in GitHub Actions - name: Store coverage report in GitHub Actions
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: go-covdata-txt name: go-covdata-txt
path: ./coverage.txt path: ./coverage.txt
@ -294,30 +246,28 @@ jobs:
steps: steps:
- -
name: Checkout name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v3
- -
name: Download artifacts name: Download artifacts
uses: actions/download-artifact@v4 uses: actions/download-artifact@v3
with: with:
pattern: compose-* name: compose
path: ./bin/release path: bin/release
merge-multiple: true
- -
name: Create checksums name: Create checksums
working-directory: ./bin/release working-directory: bin/release
run: | run: |
find . -type f -print0 | sort -z | xargs -r0 shasum -a 256 -b | sed 's# \*\./# *#' > $RUNNER_TEMP/checksums.txt find . -type f -print0 | sort -z | xargs -r0 shasum -a 256 -b | sed 's# \*\./# *#' > $RUNNER_TEMP/checksums.txt
shasum -a 256 -U -c $RUNNER_TEMP/checksums.txt shasum -a 256 -U -c $RUNNER_TEMP/checksums.txt
mv $RUNNER_TEMP/checksums.txt . mv $RUNNER_TEMP/checksums.txt .
cat checksums.txt | while read sum file; do cat checksums.txt | while read sum file; do echo "$sum $file" > ${file#\*}.sha256; done
if [[ "${file#\*}" == docker-compose-* && "${file#\*}" != *.provenance.json && "${file#\*}" != *.sbom.json ]]; then -
echo "$sum $file" > ${file#\*}.sha256 name: License
fi run: cp packaging/* bin/release/
done
- -
name: List artifacts name: List artifacts
run: | run: |
tree -nh ./bin/release tree -nh bin/release
- -
name: Check artifacts name: Check artifacts
run: | run: |
@ -327,7 +277,7 @@ jobs:
if: startsWith(github.ref, 'refs/tags/v') if: startsWith(github.ref, 'refs/tags/v')
uses: ncipollo/release-action@58ae73b360456532aafd58ee170c045abbeaee37 # v1.10.0 uses: ncipollo/release-action@58ae73b360456532aafd58ee170c045abbeaee37 # v1.10.0
with: with:
artifacts: ./bin/release/* artifacts: bin/release/*
generateReleaseNotes: true generateReleaseNotes: true
draft: true draft: true
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}

58
.github/workflows/codeql.yml vendored Normal file
View File

@ -0,0 +1,58 @@
name: codeql
on:
push:
branches:
- 'main'
paths-ignore:
- '**/*.md'
- '**/*.txt'
- '**/*.yaml'
- '**/*_test.go'
pull_request:
branches:
- 'main'
paths-ignore:
- '**/*.md'
- '**/*.txt'
- '**/*.yaml'
- '**/*_test.go'
jobs:
analyze:
name: Analyze
runs-on: 'ubuntu-latest'
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language:
- go
steps:
-
name: Checkout
uses: actions/checkout@v4
-
name: Set up Go
uses: actions/setup-go@v4
with:
go-version-file: go.mod
check-latest: true
-
name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
-
name: Autobuild
uses: github/codeql-action/autobuild@v2
-
name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
with:
category: "/language:${{matrix.language}}"

View File

@ -2,15 +2,6 @@
# to check if yaml reference docs used in this repo are valid # to check if yaml reference docs used in this repo are valid
name: docs-upstream name: docs-upstream
# Default to 'contents: read', which grants actions to read commits.
#
# If any permission is set, any permission not included in the list is
# implicitly set to "none".
#
# see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions
permissions:
contents: read
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
@ -30,7 +21,7 @@ on:
jobs: jobs:
docs-yaml: docs-yaml:
runs-on: ubuntu-latest runs-on: ubuntu-22.04
steps: steps:
- -
name: Checkout name: Checkout
@ -44,7 +35,7 @@ jobs:
retention-days: 1 retention-days: 1
validate: validate:
uses: docker/docs/.github/workflows/validate-upstream.yml@main uses: docker/docs/.github/workflows/validate-upstream.yml@919a9b9104a34a40b30d116529bcce589a544d1c # pin for artifact v4 support: https://github.com/docker/docs/pull/19220
needs: needs:
- docs-yaml - docs-yaml
with: with:

View File

@ -31,9 +31,9 @@ jobs:
env: env:
GO111MODULE: "on" GO111MODULE: "on"
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- uses: actions/setup-go@v5 - uses: actions/setup-go@v3
with: with:
go-version-file: go.mod go-version-file: go.mod
cache: true cache: true
@ -79,35 +79,19 @@ jobs:
outputs: outputs:
digest: ${{ fromJSON(steps.bake.outputs.metadata).image-cross['containerimage.digest'] }} digest: ${{ fromJSON(steps.bake.outputs.metadata).image-cross['containerimage.digest'] }}
steps: steps:
-
name: Free disk space
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1
with:
android: true
dotnet: true
haskell: true
large-packages: true
swap-storage: true
- -
name: Checkout name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v3
-
name: Login to DockerHub
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERPUBLICBOT_USERNAME }}
password: ${{ secrets.DOCKERPUBLICBOT_WRITE_PAT }}
- -
name: Set up QEMU name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v2
- -
name: Set up Docker Buildx name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v2
- -
name: Docker meta name: Docker meta
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v4
with: with:
images: | images: |
${{ env.REPO_SLUG }} ${{ env.REPO_SLUG }}
@ -115,22 +99,28 @@ jobs:
type=ref,event=tag type=ref,event=tag
type=edge type=edge
bake-target: meta-helper bake-target: meta-helper
-
name: Login to DockerHub
if: github.event_name != 'pull_request'
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERPUBLICBOT_USERNAME }}
password: ${{ secrets.DOCKERPUBLICBOT_WRITE_PAT }}
- -
name: Build and push image name: Build and push image
uses: docker/bake-action@v6 uses: docker/bake-action@v2
id: bake id: bake
with: with:
source: .
files: | files: |
./docker-bake.hcl ./docker-bake.hcl
${{ steps.meta.outputs.bake-file }} ${{ steps.meta.outputs.bake-file }}
targets: image-cross targets: image-cross
push: ${{ github.event_name != 'pull_request' }} push: ${{ github.event_name != 'pull_request' }}
sbom: true
provenance: mode=max
set: | set: |
*.cache-from=type=gha,scope=bin-image *.cache-from=type=gha,scope=bin-image
*.cache-to=type=gha,scope=bin-image,mode=max *.cache-to=type=gha,scope=bin-image,mode=max
*.attest=type=sbom
*.attest=type=provenance,mode=max,builder-id=https://github.com/${{ env.GITHUB_REPOSITORY }}/actions/runs/${{ env.GITHUB_RUN_ID }}
desktop-edge-test: desktop-edge-test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -139,16 +129,14 @@ jobs:
- -
name: Generate Token name: Generate Token
id: generate_token id: generate_token
uses: actions/create-github-app-token@v1 uses: tibdex/github-app-token@v1
with: with:
app-id: ${{ vars.DOCKERDESKTOP_APP_ID }} app_id: ${{ vars.DOCKERDESKTOP_APP_ID }}
private-key: ${{ secrets.DOCKERDESKTOP_APP_PRIVATEKEY }} private_key: ${{ secrets.DOCKERDESKTOP_APP_PRIVATEKEY }}
owner: docker repository: docker/${{ secrets.DOCKERDESKTOP_REPO }}
repositories: |
${{ secrets.DOCKERDESKTOP_REPO }}
- -
name: Trigger Docker Desktop e2e with edge version name: Trigger Docker Desktop e2e with edge version
uses: actions/github-script@v7 uses: actions/github-script@v6
with: with:
github-token: ${{ steps.generate_token.outputs.token }} github-token: ${{ steps.generate_token.outputs.token }}
script: | script: |

View File

@ -7,6 +7,9 @@ on:
push: push:
branches: [ "main" ] branches: [ "main" ]
# Declare default permissions as read only.
permissions: read-all
jobs: jobs:
analysis: analysis:
name: Scorecards analysis name: Scorecards analysis
@ -16,27 +19,15 @@ jobs:
security-events: write security-events: write
# Used to receive a badge. # Used to receive a badge.
id-token: write id-token: write
# read permissions to all the other objects
actions: read
attestations: read
checks: read
contents: read
deployments: read
issues: read
discussions: read
packages: read
pages: read
pull-requests: read
statuses: read
steps: steps:
- name: "Checkout code" - name: "Checkout code"
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # tag=v4.4.2 uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 # tag=v3.0.0
with: with:
persist-credentials: false persist-credentials: false
- name: "Run analysis" - name: "Run analysis"
uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # tag=v2.4.0 uses: ossf/scorecard-action@99c53751e09b9529366343771cc321ec74e9bd3d # tag=v2.0.6
with: with:
results_file: results.sarif results_file: results.sarif
results_format: sarif results_format: sarif
@ -50,7 +41,7 @@ jobs:
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
# format to the repository Actions tab. # format to the repository Actions tab.
- name: "Upload artifact" - name: "Upload artifact"
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # tag=v4.5.0 uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535 # tag=v3.0.0
with: with:
name: SARIF file name: SARIF file
path: results.sarif path: results.sarif
@ -58,6 +49,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard. # Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning" - name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@3096afedf9873361b2b2f65e1445b13272c83eb8 # tag=v2.20.00 uses: github/codeql-action/upload-sarif@5f532563584d71fdef14ee64d17bafb34f751ce5 # tag=v1.0.26
with: with:
sarif_file: results.sarif sarif_file: results.sarif

View File

@ -1,33 +0,0 @@
name: 'Close stale issues'
# Default to 'contents: read', which grants actions to read commits.
#
# If any permission is set, any permission not included in the list is
# implicitly set to "none".
#
# see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions
permissions:
contents: read
on:
schedule:
- cron: '0 0 * * 0,3' # at midnight UTC every Sunday and Wednesday
jobs:
stale:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v9
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
days-before-issue-stale: 150 # marks stale after 5 months
days-before-issue-close: 30 # closes 1 month after being marked with no action
stale-issue-label: "stale"
exempt-issue-labels: "kind/feature,kind/enhancement"

View File

@ -1,89 +1,73 @@
version: "2"
run: run:
concurrency: 2 concurrency: 2
timeout: 10m
linters: linters:
default: none enable-all: false
disable-all: true
enable: enable:
- copyloopvar
- depguard - depguard
- errcheck - errcheck
- errorlint - errorlint
- gocritic - gocritic
- gocyclo - gocyclo
- gofmt
- goimports
- gomodguard - gomodguard
- revive
- gosimple
- govet - govet
- ineffassign - ineffassign
- lll - lll
- misspell - misspell
- nakedret - nakedret
- nolintlint - nolintlint
- revive
- staticcheck - staticcheck
- testifylint - typecheck
- unconvert - unconvert
- unparam - unparam
- unused - unused
settings: linters-settings:
depguard: revive:
rules: rules:
all: - name: package-comments
deny: disabled: true
- pkg: io/ioutil depguard:
desc: io/ioutil package has been deprecated rules:
- pkg: github.com/docker/docker/errdefs all:
desc: use github.com/containerd/errdefs instead. deny:
- pkg: golang.org/x/exp/maps - pkg: io/ioutil
desc: use stdlib maps package desc: 'io/ioutil package has been deprecated'
- pkg: golang.org/x/exp/slices - pkg: gopkg.in/yaml.v2
desc: use stdlib slices package desc: 'compose-go uses yaml.v3'
- pkg: gopkg.in/yaml.v2 gomodguard:
desc: compose-go uses yaml.v3 blocked:
gocritic: modules:
disabled-checks: - github.com/pkg/errors:
- paramTypeCombine recommendations:
- unnamedResult - errors
- whyNoLint - fmt
enabled-tags: versions:
- diagnostic - github.com/distribution/distribution:
- opinionated reason: "use distribution/reference"
- style - gotest.tools:
gocyclo: version: "< 3.0.0"
min-complexity: 16 reason: "deprecated, pre-modules version"
gomodguard: gocritic:
blocked: # Enable multiple checks by tags, run `GL_DEBUG=gocritic golangci-lint run` to see all tags and checks.
modules: # Empty list by default. See https://github.com/go-critic/go-critic#usage -> section "Tags".
- github.com/pkg/errors: enabled-tags:
recommendations: - diagnostic
- errors - opinionated
- fmt - style
versions: disabled-checks:
- github.com/distribution/distribution: - paramTypeCombine
reason: use distribution/reference - unnamedResult
- gotest.tools: - whyNoLint
version: < 3.0.0 gocyclo:
reason: deprecated, pre-modules version min-complexity: 16
lll: lll:
line-length: 200 line-length: 200
revive:
rules:
- name: package-comments
disabled: true
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
issues: issues:
max-issues-per-linter: 0 # golangci hides some golint warnings (the warning about exported things
max-same-issues: 0 # withtout documentation for example), this will make it show them anyway.
formatters: exclude-use-default: false
enable:
- gofumpt
- goimports
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$

View File

@ -2,17 +2,14 @@
### Prerequisites ### Prerequisites
* Windows: * Windows:
* [Docker Desktop](https://docs.docker.com/desktop/setup/install/windows-install/) * [Docker Desktop](https://hub.docker.com/editions/community/docker-ce-desktop-windows)
* make * make
* go (see [go.mod](go.mod) for minimum version)
* macOS: * macOS:
* [Docker Desktop](https://docs.docker.com/desktop/setup/install/mac-install/) * [Docker Desktop](https://hub.docker.com/editions/community/docker-ce-desktop-mac)
* make * make
* go (see [go.mod](go.mod) for minimum version)
* Linux: * Linux:
* [Docker 20.10 or later](https://docs.docker.com/engine/install/) * [Docker 20.10 or later](https://docs.docker.com/engine/install/)
* make * make
* go (see [go.mod](go.mod) for minimum version)
### Building the CLI ### Building the CLI
@ -52,7 +49,7 @@ To execute both CLI and standalone e2e tests, run :
make e2e make e2e
``` ```
Or if you need to build the CLI, run: Or if you need to build the CLI, run:
```console ```console
make build-and-e2e make build-and-e2e
``` ```
@ -88,7 +85,7 @@ make build-and-e2e-compose-standalone
To create a new release: To create a new release:
* Check that the CI is green on the main branch for the commit you want to release * Check that the CI is green on the main branch for the commit you want to release
* Run the release GitHub Actions workflow with a tag of form vx.y.z following existing tags. * Run the release Github Actions workflow with a tag of form vx.y.z following existing tags.
This will automatically create a new tag, release and make binaries for This will automatically create a new tag, release and make binaries for
Windows, macOS, and Linux available for download on the Windows, macOS, and Linux available for download on the

View File

@ -2,7 +2,7 @@
Want to hack on Docker? Awesome! We have a contributor's guide that explains Want to hack on Docker? Awesome! We have a contributor's guide that explains
[setting up a Docker development environment and the contribution [setting up a Docker development environment and the contribution
process](https://docs.docker.com/contribute/). process](https://docs.docker.com/contribute/overview/).
This page contains information about reporting issues as well as some tips and This page contains information about reporting issues as well as some tips and
guidelines useful to experienced open source contributors. Finally, make sure guidelines useful to experienced open source contributors. Finally, make sure
@ -85,7 +85,8 @@ before anybody starts working on it.
We are always thrilled to receive pull requests. We do our best to process them We are always thrilled to receive pull requests. We do our best to process them
quickly. If your pull request is not accepted on the first try, quickly. If your pull request is not accepted on the first try,
don't get discouraged! don't get discouraged! Our contributor's guide explains
[the review process we use for simple changes](https://docs.docker.com/opensource/workflow/make-a-contribution/).
### Talking to other Docker users and contributors ### Talking to other Docker users and contributors
@ -95,7 +96,7 @@ don't get discouraged!
<tr> <tr>
<td>Community Slack</td> <td>Community Slack</td>
<td> <td>
The Docker Community has a dedicated Slack chat to discuss features and issues. You can sign-up <a href="https://www.docker.com/community/" target="_blank">with this link</a>. The Docker Community has a dedicated Slack chat to discuss features and issues. You can sign-up <a href="https://www.docker.com/docker-community" target="_blank">with this link</a>.
</td> </td>
</tr> </tr>
<tr> <tr>
@ -118,7 +119,7 @@ don't get discouraged!
<td>Stack Overflow</td> <td>Stack Overflow</td>
<td> <td>
Stack Overflow has over 17000 Docker questions listed. We regularly Stack Overflow has over 17000 Docker questions listed. We regularly
monitor <a href="https://stackoverflow.com/questions/tagged/docker" target="_blank">Docker questions</a> monitor <a href="https://stackoverflow.com/search?tab=newest&q=docker" target="_blank">Docker questions</a>
and so do many other knowledgeable Docker users. and so do many other knowledgeable Docker users.
</td> </td>
</tr> </tr>
@ -200,7 +201,7 @@ For more details, see the [MAINTAINERS](MAINTAINERS) page.
The sign-off is a simple line at the end of the explanation for the patch. Your The sign-off is a simple line at the end of the explanation for the patch. Your
signature certifies that you wrote the patch or otherwise have the right to pass signature certifies that you wrote the patch or otherwise have the right to pass
it on as an open-source patch. The rules are pretty simple: if you can certify it on as an open-source patch. The rules are pretty simple: if you can certify
the below (from [developercertificate.org](https://developercertificate.org/)): the below (from [developercertificate.org](http://developercertificate.org/)):
``` ```
Developer Certificate of Origin Developer Certificate of Origin
@ -252,7 +253,7 @@ commit automatically with `git commit -s`.
### How can I become a maintainer? ### How can I become a maintainer?
The procedures for adding new maintainers are explained in the global The procedures for adding new maintainers are explained in the global
[MAINTAINERS](https://github.com/docker/opensource/blob/main/MAINTAINERS) [MAINTAINERS](https://github.com/docker/opensource/blob/master/MAINTAINERS)
file in the file in the
[https://github.com/docker/opensource/](https://github.com/docker/opensource/) [https://github.com/docker/opensource/](https://github.com/docker/opensource/)
repository. repository.
@ -311,8 +312,8 @@ The rules:
2. All code should pass the default levels of 2. All code should pass the default levels of
[`golint`](https://github.com/golang/lint). [`golint`](https://github.com/golang/lint).
3. All code should follow the guidelines covered in [Effective 3. All code should follow the guidelines covered in [Effective
Go](https://go.dev/doc/effective_go) and [Go Code Review Go](http://golang.org/doc/effective_go.html) and [Go Code Review
Comments](https://go.dev/wiki/CodeReviewComments). Comments](https://github.com/golang/go/wiki/CodeReviewComments).
4. Include code comments. Tell us the why, the history and the context. 4. Include code comments. Tell us the why, the history and the context.
5. Document _all_ declarations and methods, even private ones. Declare 5. Document _all_ declarations and methods, even private ones. Declare
expectations, caveats and anything else that may be important. If a type expectations, caveats and anything else that may be important. If a type
@ -334,6 +335,6 @@ The rules:
guidelines. Since you've read all the rules, you now know that. guidelines. Since you've read all the rules, you now know that.
If you are having trouble getting into the mood of idiomatic Go, we recommend If you are having trouble getting into the mood of idiomatic Go, we recommend
reading through [Effective Go](https://go.dev/doc/effective_go). The reading through [Effective Go](https://golang.org/doc/effective_go.html). The
[Go Blog](https://go.dev/blog/) is also a great resource. Drinking the [Go Blog](https://blog.golang.org) is also a great resource. Drinking the
kool-aid is a lot easier than going thirsty. kool-aid is a lot easier than going thirsty.

View File

@ -15,9 +15,9 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
ARG GO_VERSION=1.24.7 ARG GO_VERSION=1.21.6
ARG XX_VERSION=1.6.1 ARG XX_VERSION=1.2.1
ARG GOLANGCI_LINT_VERSION=v2.0.2 ARG GOLANGCI_LINT_VERSION=v1.55.2
ARG ADDLICENSE_VERSION=v1.0.0 ARG ADDLICENSE_VERSION=v1.0.0
ARG BUILD_TAGS="e2e" ARG BUILD_TAGS="e2e"
@ -106,14 +106,11 @@ RUN --mount=type=bind,target=. \
--mount=type=cache,target=/go/pkg/mod \ --mount=type=cache,target=/go/pkg/mod \
rm -rf /tmp/coverage && \ rm -rf /tmp/coverage && \
mkdir -p /tmp/coverage && \ mkdir -p /tmp/coverage && \
rm -rf /tmp/report && \ go test -tags "$BUILD_TAGS" -v -cover -covermode=atomic $(go list $(TAGS) ./... | grep -vE 'e2e') -args -test.gocoverdir="/tmp/coverage" && \
mkdir -p /tmp/report && \
go run gotest.tools/gotestsum@latest --format testname --junitfile "/tmp/report/report.xml" -- -tags "$BUILD_TAGS" -v -cover -covermode=atomic $(go list $(TAGS) ./... | grep -vE 'e2e') -args -test.gocoverdir="/tmp/coverage" && \
go tool covdata percent -i=/tmp/coverage go tool covdata percent -i=/tmp/coverage
FROM scratch AS test-coverage FROM scratch AS test-coverage
COPY --from=test --link /tmp/coverage / COPY --from=test --link /tmp/coverage /
COPY --from=test --link /tmp/report /
FROM base AS license-set FROM base AS license-set
ARG LICENSE_FILES ARG LICENSE_FILES

View File

@ -23,7 +23,6 @@
people = [ people = [
"glours", "glours",
"jhrotko",
"milas", "milas",
"ndeloof", "ndeloof",
"nicksieger", "nicksieger",
@ -73,11 +72,6 @@
Email = "guillaume.tardif@docker.com" Email = "guillaume.tardif@docker.com"
GitHub = "gtardif" GitHub = "gtardif"
[people.jhrotko]
Name = "Joana Hrotko"
Email = "joana.hrotko@docker.com"
Github = "jhrotko"
[people.laurazard] [people.laurazard]
Name = "Laura Brehm" Name = "Laura Brehm"
Email = "laura.brehm@docker.com" Email = "laura.brehm@docker.com"

View File

@ -74,12 +74,12 @@ install: binary
install $(or $(DESTDIR),./bin/build)/docker-compose ~/.docker/cli-plugins/docker-compose install $(or $(DESTDIR),./bin/build)/docker-compose ~/.docker/cli-plugins/docker-compose
.PHONY: e2e-compose .PHONY: e2e-compose
e2e-compose: example-provider ## Run end to end local tests in plugin mode. Set E2E_TEST=TestName to run a single test e2e-compose: ## Run end to end local tests in plugin mode. Set E2E_TEST=TestName to run a single test
go run gotest.tools/gotestsum@latest --format testname --junitfile "/tmp/report/report.xml" -- -v $(TEST_FLAGS) -count=1 ./pkg/e2e go test -v $(TEST_FLAGS) -count=1 ./pkg/e2e
.PHONY: e2e-compose-standalone .PHONY: e2e-compose-standalone
e2e-compose-standalone: ## Run End to end local tests in standalone mode. Set E2E_TEST=TestName to run a single test e2e-compose-standalone: ## Run End to end local tests in standalone mode. Set E2E_TEST=TestName to run a single test
go run gotest.tools/gotestsum@latest --format testname --junitfile "/tmp/report/report.xml" -- $(TEST_FLAGS) -v -count=1 -parallel=1 --tags=standalone ./pkg/e2e go test $(TEST_FLAGS) -v -count=1 -parallel=1 --tags=standalone ./pkg/e2e
.PHONY: build-and-e2e-compose .PHONY: build-and-e2e-compose
build-and-e2e-compose: build e2e-compose ## Compile the compose cli-plugin and run end to end local tests in plugin mode. Set E2E_TEST=TestName to run a single test build-and-e2e-compose: build e2e-compose ## Compile the compose cli-plugin and run end to end local tests in plugin mode. Set E2E_TEST=TestName to run a single test
@ -87,13 +87,9 @@ build-and-e2e-compose: build e2e-compose ## Compile the compose cli-plugin and r
.PHONY: build-and-e2e-compose-standalone .PHONY: build-and-e2e-compose-standalone
build-and-e2e-compose-standalone: build e2e-compose-standalone ## Compile the compose cli-plugin and run End to end local tests in standalone mode. Set E2E_TEST=TestName to run a single test build-and-e2e-compose-standalone: build e2e-compose-standalone ## Compile the compose cli-plugin and run End to end local tests in standalone mode. Set E2E_TEST=TestName to run a single test
.PHONY: example-provider
example-provider: ## build example provider for e2e tests
go build -o bin/build/example-provider docs/examples/provider.go
.PHONY: mocks .PHONY: mocks
mocks: mocks:
mockgen --version >/dev/null 2>&1 || go install go.uber.org/mock/mockgen@v0.4.0 mockgen --version >/dev/null 2>&1 || go install go.uber.org/mock/mockgen@v0.3.0
mockgen -destination pkg/mocks/mock_docker_cli.go -package mocks github.com/docker/cli/cli/command Cli mockgen -destination pkg/mocks/mock_docker_cli.go -package mocks github.com/docker/cli/cli/command Cli
mockgen -destination pkg/mocks/mock_docker_api.go -package mocks github.com/docker/docker/client APIClient mockgen -destination pkg/mocks/mock_docker_api.go -package mocks github.com/docker/docker/client APIClient
mockgen -destination pkg/mocks/mock_docker_compose_api.go -package mocks -source=./pkg/api/api.go Service mockgen -destination pkg/mocks/mock_docker_compose_api.go -package mocks -source=./pkg/api/api.go Service
@ -120,11 +116,6 @@ cache-clear: ## Clear the builder cache
lint: ## run linter(s) lint: ## run linter(s)
$(BUILDX_CMD) bake lint $(BUILDX_CMD) bake lint
.PHONY: fmt
fmt:
gofumpt --version >/dev/null 2>&1 || go install mvdan.cc/gofumpt@latest
gofumpt -w .
.PHONY: docs .PHONY: docs
docs: ## generate documentation docs: ## generate documentation
$(eval $@_TMP_OUT := $(shell mktemp -d -t compose-output.XXXXXXXXXX)) $(eval $@_TMP_OUT := $(shell mktemp -d -t compose-output.XXXXXXXXXX))

View File

@ -8,7 +8,7 @@
- [Legacy](#legacy) - [Legacy](#legacy)
# Docker Compose v2 # Docker Compose v2
[![GitHub release](https://img.shields.io/github/v/release/docker/compose.svg?style=flat-square)](https://github.com/docker/compose/releases/latest) [![GitHub release](https://img.shields.io/github/release/docker/compose.svg?style=flat-square)](https://github.com/docker/compose/releases/latest)
[![PkgGoDev](https://img.shields.io/badge/go.dev-docs-007d9c?style=flat-square&logo=go&logoColor=white)](https://pkg.go.dev/github.com/docker/compose/v2) [![PkgGoDev](https://img.shields.io/badge/go.dev-docs-007d9c?style=flat-square&logo=go&logoColor=white)](https://pkg.go.dev/github.com/docker/compose/v2)
[![Build Status](https://img.shields.io/github/actions/workflow/status/docker/compose/ci.yml?label=ci&logo=github&style=flat-square)](https://github.com/docker/compose/actions?query=workflow%3Aci) [![Build Status](https://img.shields.io/github/actions/workflow/status/docker/compose/ci.yml?label=ci&logo=github&style=flat-square)](https://github.com/docker/compose/actions?query=workflow%3Aci)
[![Go Report Card](https://goreportcard.com/badge/github.com/docker/compose/v2?style=flat-square)](https://goreportcard.com/report/github.com/docker/compose/v2) [![Go Report Card](https://goreportcard.com/badge/github.com/docker/compose/v2?style=flat-square)](https://goreportcard.com/report/github.com/docker/compose/v2)
@ -23,18 +23,12 @@ your application are configured.
Once you have a Compose file, you can create and start your application with a Once you have a Compose file, you can create and start your application with a
single command: `docker compose up`. single command: `docker compose up`.
> **Note**: About Docker Swarm
> Docker Swarm used to rely on the legacy compose file format but did not adopted the compose specification
> so is missing some of the recent enhancements in the compose syntax. After
> [acquisition by Mirantis](https://www.mirantis.com/software/swarm/) swarm isn't maintained by Docker Inc, and
> as such some Docker Compose features aren't accessible to swarm users.
# Where to get Docker Compose # Where to get Docker Compose
### Windows and macOS ### Windows and macOS
Docker Compose is included in Docker Compose is included in
[Docker Desktop](https://www.docker.com/products/docker-desktop/) [Docker Desktop](https://www.docker.com/products/docker-desktop)
for Windows and macOS. for Windows and macOS.
### Linux ### Linux

View File

@ -30,7 +30,6 @@ import (
"github.com/docker/compose/v2/internal/tracing" "github.com/docker/compose/v2/internal/tracing"
"github.com/spf13/cobra" "github.com/spf13/cobra"
flag "github.com/spf13/pflag" flag "github.com/spf13/pflag"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
@ -51,14 +50,12 @@ func Setup(cmd *cobra.Command, dockerCli command.Cli, args []string) error {
} }
ctx := cmd.Context() ctx := cmd.Context()
ctx, cmdSpan := otel.Tracer("").Start( ctx, cmdSpan := tracing.Tracer.Start(
ctx, ctx,
"cli/"+strings.Join(commandName(cmd), "-"), "cli/"+strings.Join(commandName(cmd), "-"),
) )
cmdSpan.SetAttributes( cmdSpan.SetAttributes(attribute.StringSlice("cli.args", args))
attribute.StringSlice("cli.flags", getFlags(cmd.Flags())), cmdSpan.SetAttributes(attribute.StringSlice("cli.flags", getFlags(cmd.Flags())))
attribute.Bool("cli.isatty", dockerCli.In().IsTerminal()),
)
cmd.SetContext(ctx) cmd.SetContext(ctx)
wrapRunE(cmd, cmdSpan, tracingShutdown) wrapRunE(cmd, cmdSpan, tracingShutdown)
@ -107,7 +104,7 @@ func wrapRunE(c *cobra.Command, cmdSpan trace.Span, tracingShutdown tracing.Shut
if tracingShutdown != nil { if tracingShutdown != nil {
// use background for root context because the cmd's context might have // use background for root context because the cmd's context might have
// been canceled already // been canceled already
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel() defer cancel()
// TODO(milas): add an env var to enable logging from the // TODO(milas): add an env var to enable logging from the
// OTel components for debugging purposes // OTel components for debugging purposes
@ -117,14 +114,13 @@ func wrapRunE(c *cobra.Command, cmdSpan trace.Span, tracingShutdown tracing.Shut
} }
} }
// commandName returns the path components for a given command, // commandName returns the path components for a given command.
// in reverse alphabetical order for consistent usage metrics.
// //
// The root Compose command and anything before (i.e. "docker") // The root Compose command and anything before (i.e. "docker")
// are not included. // are not included.
// //
// For example: // For example:
// - docker compose alpha watch -> [watch, alpha] // - docker compose alpha watch -> [alpha, watch]
// - docker-compose up -> [up] // - docker-compose up -> [up]
func commandName(cmd *cobra.Command) []string { func commandName(cmd *cobra.Command) []string {
var name []string var name []string

View File

@ -20,8 +20,6 @@ import (
"reflect" "reflect"
"testing" "testing"
commands "github.com/docker/compose/v2/cmd/compose"
"github.com/spf13/cobra"
flag "github.com/spf13/pflag" flag "github.com/spf13/pflag"
) )
@ -62,51 +60,5 @@ func TestGetFlags(t *testing.T) {
} }
}) })
} }
}
func TestCommandName(t *testing.T) {
tests := []struct {
name string
setupCmd func() *cobra.Command
want []string
}{
{
name: "docker compose alpha watch -> [watch, alpha]",
setupCmd: func() *cobra.Command {
dockerCmd := &cobra.Command{Use: "docker"}
composeCmd := &cobra.Command{Use: commands.PluginName}
alphaCmd := &cobra.Command{Use: "alpha"}
watchCmd := &cobra.Command{Use: "watch"}
dockerCmd.AddCommand(composeCmd)
composeCmd.AddCommand(alphaCmd)
alphaCmd.AddCommand(watchCmd)
return watchCmd
},
want: []string{"watch", "alpha"},
},
{
name: "docker-compose up -> [up]",
setupCmd: func() *cobra.Command {
dockerComposeCmd := &cobra.Command{Use: commands.PluginName}
upCmd := &cobra.Command{Use: "up"}
dockerComposeCmd.AddCommand(upCmd)
return upCmd
},
want: []string{"up"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cmd := tt.setupCmd()
got := commandName(cmd)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("commandName() = %v, want %v", got, tt.want)
}
})
}
} }

View File

@ -19,7 +19,6 @@ package compatibility
import ( import (
"fmt" "fmt"
"os" "os"
"strings"
"github.com/docker/compose/v2/cmd/compose" "github.com/docker/compose/v2/cmd/compose"
) )
@ -56,14 +55,13 @@ func Convert(args []string) []string {
var rootFlags []string var rootFlags []string
command := []string{compose.PluginName} command := []string{compose.PluginName}
l := len(args) l := len(args)
ARGS:
for i := 0; i < l; i++ { for i := 0; i < l; i++ {
arg := args[i] arg := args[i]
if contains(getCompletionCommands(), arg) { if contains(getCompletionCommands(), arg) {
command = append([]string{arg}, command...) command = append([]string{arg}, command...)
continue continue
} }
if arg != "" && arg[0] != '-' { if len(arg) > 0 && arg[0] != '-' {
command = append(command, args[i:]...) command = append(command, args[i:]...)
break break
} }
@ -83,23 +81,14 @@ ARGS:
rootFlags = append(rootFlags, arg) rootFlags = append(rootFlags, arg)
continue continue
} }
for _, flag := range getStringFlags() { if contains(getStringFlags(), arg) {
if arg == flag { i++
i++ if i >= l {
if i >= l { fmt.Fprintf(os.Stderr, "flag needs an argument: '%s'\n", arg)
fmt.Fprintf(os.Stderr, "flag needs an argument: '%s'\n", arg) os.Exit(1)
os.Exit(1)
}
rootFlags = append(rootFlags, arg, args[i])
continue ARGS
}
if strings.HasPrefix(arg, flag) {
_, val, found := strings.Cut(arg, "=")
if found {
rootFlags = append(rootFlags, flag, val)
continue ARGS
}
} }
rootFlags = append(rootFlags, arg, args[i])
continue
} }
command = append(command, arg) command = append(command, arg)
} }

View File

@ -17,9 +17,6 @@
package compatibility package compatibility
import ( import (
"errors"
"os"
"os/exec"
"testing" "testing"
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
@ -27,10 +24,9 @@ import (
func Test_convert(t *testing.T) { func Test_convert(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
args []string args []string
want []string want []string
wantErr bool
}{ }{
{ {
name: "compose only", name: "compose only",
@ -42,11 +38,6 @@ func Test_convert(t *testing.T) {
args: []string{"--context", "foo", "-f", "compose.yaml", "up"}, args: []string{"--context", "foo", "-f", "compose.yaml", "up"},
want: []string{"--context", "foo", "compose", "-f", "compose.yaml", "up"}, want: []string{"--context", "foo", "compose", "-f", "compose.yaml", "up"},
}, },
{
name: "with context arg",
args: []string{"--context=foo", "-f", "compose.yaml", "up"},
want: []string{"--context", "foo", "compose", "-f", "compose.yaml", "up"},
},
{ {
name: "with host", name: "with host",
args: []string{"--host", "tcp://1.2.3.4", "up"}, args: []string{"--host", "tcp://1.2.3.4", "up"},
@ -97,36 +88,11 @@ func Test_convert(t *testing.T) {
args: []string{"--project-name", "compose", "down", "--remove-orphans"}, args: []string{"--project-name", "compose", "down", "--remove-orphans"},
want: []string{"compose", "--project-name", "compose", "down", "--remove-orphans"}, want: []string{"compose", "--project-name", "compose", "down", "--remove-orphans"},
}, },
{
name: "completion command",
args: []string{"__complete", "up"},
want: []string{"__complete", "compose", "up"},
},
{
name: "string flag without argument",
args: []string{"--log-level"},
wantErr: true,
},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
if tt.wantErr { got := Convert(tt.args)
if os.Getenv("BE_CRASHER") == "1" { assert.DeepEqual(t, tt.want, got)
Convert(tt.args)
return
}
cmd := exec.Command(os.Args[0], "-test.run=^"+t.Name()+"$")
cmd.Env = append(os.Environ(), "BE_CRASHER=1")
err := cmd.Run()
var e *exec.ExitError
if errors.As(err, &e) && !e.Success() {
return
}
t.Fatalf("process ran with err %v, want exit status 1", err)
} else {
got := Convert(tt.args)
assert.DeepEqual(t, tt.want, got)
}
}) })
} }
} }

View File

@ -33,7 +33,6 @@ func alphaCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
cmd.AddCommand( cmd.AddCommand(
vizCommand(p, dockerCli, backend), vizCommand(p, dockerCli, backend),
publishCommand(p, dockerCli, backend), publishCommand(p, dockerCli, backend),
generateCommand(p, backend),
) )
return cmd return cmd
} }

View File

@ -43,7 +43,7 @@ func attachCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service
} }
runCmd := &cobra.Command{ runCmd := &cobra.Command{
Use: "attach [OPTIONS] SERVICE", Use: "attach [OPTIONS] SERVICE",
Short: "Attach local standard input, output, and error streams to a service's running container", Short: "Attach local standard input, output, and error streams to a service's running container.",
Args: cobra.MinimumNArgs(1), Args: cobra.MinimumNArgs(1),
PreRunE: Adapt(func(ctx context.Context, args []string) error { PreRunE: Adapt(func(ctx context.Context, args []string) error {
opts.service = args[0] opts.service = args[0]
@ -64,7 +64,7 @@ func attachCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service
} }
func runAttach(ctx context.Context, dockerCli command.Cli, backend api.Service, opts attachOpts) error { func runAttach(ctx context.Context, dockerCli command.Cli, backend api.Service, opts attachOpts) error {
projectName, err := opts.toProjectName(ctx, dockerCli) projectName, err := opts.toProjectName(dockerCli)
if err != nil { if err != nil {
return err return err
} }

View File

@ -1,149 +0,0 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"fmt"
"io"
"github.com/distribution/reference"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/go-units"
"github.com/spf13/cobra"
"github.com/docker/compose/v2/cmd/formatter"
"github.com/docker/compose/v2/pkg/bridge"
)
func bridgeCommand(p *ProjectOptions, dockerCli command.Cli) *cobra.Command {
cmd := &cobra.Command{
Use: "bridge CMD [OPTIONS]",
Short: "Convert compose files into another model",
TraverseChildren: true,
}
cmd.AddCommand(
convertCommand(p, dockerCli),
transformersCommand(dockerCli),
)
return cmd
}
func convertCommand(p *ProjectOptions, dockerCli command.Cli) *cobra.Command {
convertOpts := bridge.ConvertOptions{}
cmd := &cobra.Command{
Use: "convert",
Short: "Convert compose files to Kubernetes manifests, Helm charts, or another model",
RunE: Adapt(func(ctx context.Context, args []string) error {
return runConvert(ctx, dockerCli, p, convertOpts)
}),
}
flags := cmd.Flags()
flags.StringVarP(&convertOpts.Output, "output", "o", "out", "The output directory for the Kubernetes resources")
flags.StringArrayVarP(&convertOpts.Transformations, "transformation", "t", nil, "Transformation to apply to compose model (default: docker/compose-bridge-kubernetes)")
flags.StringVar(&convertOpts.Templates, "templates", "", "Directory containing transformation templates")
return cmd
}
func runConvert(ctx context.Context, dockerCli command.Cli, p *ProjectOptions, opts bridge.ConvertOptions) error {
project, _, err := p.ToProject(ctx, dockerCli, nil)
if err != nil {
return err
}
return bridge.Convert(ctx, dockerCli, project, opts)
}
func transformersCommand(dockerCli command.Cli) *cobra.Command {
cmd := &cobra.Command{
Use: "transformations CMD [OPTIONS]",
Short: "Manage transformation images",
}
cmd.AddCommand(
listTransformersCommand(dockerCli),
createTransformerCommand(dockerCli),
)
return cmd
}
func listTransformersCommand(dockerCli command.Cli) *cobra.Command {
options := lsOptions{}
cmd := &cobra.Command{
Use: "list",
Aliases: []string{"ls"},
Short: "List available transformations",
RunE: Adapt(func(ctx context.Context, args []string) error {
transformers, err := bridge.ListTransformers(ctx, dockerCli)
if err != nil {
return err
}
return displayTransformer(dockerCli, transformers, options)
}),
}
cmd.Flags().StringVar(&options.Format, "format", "table", "Format the output. Values: [table | json]")
cmd.Flags().BoolVarP(&options.Quiet, "quiet", "q", false, "Only display transformer names")
return cmd
}
func displayTransformer(dockerCli command.Cli, transformers []image.Summary, options lsOptions) error {
if options.Quiet {
for _, t := range transformers {
if len(t.RepoTags) > 0 {
_, _ = fmt.Fprintln(dockerCli.Out(), t.RepoTags[0])
} else {
_, _ = fmt.Fprintln(dockerCli.Out(), t.ID)
}
}
return nil
}
return formatter.Print(transformers, options.Format, dockerCli.Out(),
func(w io.Writer) {
for _, img := range transformers {
id := stringid.TruncateID(img.ID)
size := units.HumanSizeWithPrecision(float64(img.Size), 3)
repo, tag := "<none>", "<none>"
if len(img.RepoTags) > 0 {
ref, err := reference.ParseDockerRef(img.RepoTags[0])
if err == nil {
// ParseDockerRef will reject a local image ID
repo = reference.FamiliarName(ref)
if tagged, ok := ref.(reference.Tagged); ok {
tag = tagged.Tag()
}
}
}
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", id, repo, tag, size)
}
},
"IMAGE ID", "REPO", "TAGS", "SIZE")
}
func createTransformerCommand(dockerCli command.Cli) *cobra.Command {
var opts bridge.CreateTransformerOptions
cmd := &cobra.Command{
Use: "create [OPTION] PATH",
Short: "Create a new transformation",
RunE: Adapt(func(ctx context.Context, args []string) error {
opts.Dest = args[0]
return bridge.CreateTransformer(ctx, dockerCli, opts)
}),
}
cmd.Flags().StringVarP(&opts.From, "from", "f", "", "Existing transformation to copy (default: docker/compose-bridge-kubernetes)")
return cmd
}

View File

@ -27,6 +27,7 @@ import (
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
cliopts "github.com/docker/cli/opts" cliopts "github.com/docker/cli/opts"
ui "github.com/docker/compose/v2/pkg/progress" ui "github.com/docker/compose/v2/pkg/progress"
buildkit "github.com/moby/buildkit/util/progress/progressui"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/api"
@ -34,23 +35,20 @@ import (
type buildOptions struct { type buildOptions struct {
*ProjectOptions *ProjectOptions
quiet bool quiet bool
pull bool pull bool
push bool push bool
args []string args []string
noCache bool noCache bool
memory cliopts.MemBytes memory cliopts.MemBytes
ssh string ssh string
builder string builder string
deps bool deps bool
print bool
check bool
sbom string
provenance string
} }
func (opts buildOptions) toAPIBuildOptions(services []string) (api.BuildOptions, error) { func (opts buildOptions) toAPIBuildOptions(services []string) (api.BuildOptions, error) {
var SSHKeys []types.SSHKey var SSHKeys []types.SSHKey
var err error
if opts.ssh != "" { if opts.ssh != "" {
id, path, found := strings.Cut(opts.ssh, "=") id, path, found := strings.Cut(opts.ssh, "=")
if !found && id != "default" { if !found && id != "default" {
@ -60,33 +58,26 @@ func (opts buildOptions) toAPIBuildOptions(services []string) (api.BuildOptions,
ID: id, ID: id,
Path: path, Path: path,
}) })
if err != nil {
return api.BuildOptions{}, err
}
} }
builderName := opts.builder builderName := opts.builder
if builderName == "" { if builderName == "" {
builderName = os.Getenv("BUILDX_BUILDER") builderName = os.Getenv("BUILDX_BUILDER")
} }
uiMode := ui.Mode
if uiMode == ui.ModeJSON {
uiMode = "rawjson"
}
return api.BuildOptions{ return api.BuildOptions{
Pull: opts.pull, Pull: opts.pull,
Push: opts.push, Push: opts.push,
Progress: uiMode, Progress: ui.Mode,
Args: types.NewMappingWithEquals(opts.args), Args: types.NewMappingWithEquals(opts.args),
NoCache: opts.noCache, NoCache: opts.noCache,
Quiet: opts.quiet, Quiet: opts.quiet,
Services: services, Services: services,
Deps: opts.deps, Deps: opts.deps,
Memory: int64(opts.memory), SSHs: SSHKeys,
Print: opts.print, Builder: builderName,
Check: opts.check,
SSHs: SSHKeys,
Builder: builderName,
SBOM: opts.sbom,
Provenance: opts.provenance,
}, nil }, nil
} }
@ -120,15 +111,13 @@ func buildCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
ValidArgsFunction: completeServiceNames(dockerCli, p), ValidArgsFunction: completeServiceNames(dockerCli, p),
} }
flags := cmd.Flags() flags := cmd.Flags()
flags.BoolVar(&opts.push, "push", false, "Push service images") flags.BoolVar(&opts.push, "push", false, "Push service images.")
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Suppress the build output") flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Don't print anything to STDOUT")
flags.BoolVar(&opts.pull, "pull", false, "Always attempt to pull a newer version of the image") flags.BoolVar(&opts.pull, "pull", false, "Always attempt to pull a newer version of the image.")
flags.StringArrayVar(&opts.args, "build-arg", []string{}, "Set build-time variables for services") flags.StringArrayVar(&opts.args, "build-arg", []string{}, "Set build-time variables for services.")
flags.StringVar(&opts.ssh, "ssh", "", "Set SSH authentications used when building service images. (use 'default' for using your default SSH Agent)") flags.StringVar(&opts.ssh, "ssh", "", "Set SSH authentications used when building service images. (use 'default' for using your default SSH Agent)")
flags.StringVar(&opts.builder, "builder", "", "Set builder to use") flags.StringVar(&opts.builder, "builder", "", "Set builder to use.")
flags.BoolVar(&opts.deps, "with-dependencies", false, "Also build dependencies (transitively)") flags.BoolVar(&opts.deps, "with-dependencies", false, "Also build dependencies (transitively).")
flags.StringVar(&opts.provenance, "provenance", "", `Add a provenance attestation`)
flags.StringVar(&opts.sbom, "sbom", "", `Add a SBOM attestation`)
flags.Bool("parallel", true, "Build images in parallel. DEPRECATED") flags.Bool("parallel", true, "Build images in parallel. DEPRECATED")
flags.MarkHidden("parallel") //nolint:errcheck flags.MarkHidden("parallel") //nolint:errcheck
@ -140,17 +129,14 @@ func buildCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
flags.Bool("no-rm", false, "Do not remove intermediate containers after a successful build. DEPRECATED") flags.Bool("no-rm", false, "Do not remove intermediate containers after a successful build. DEPRECATED")
flags.MarkHidden("no-rm") //nolint:errcheck flags.MarkHidden("no-rm") //nolint:errcheck
flags.VarP(&opts.memory, "memory", "m", "Set memory limit for the build container. Not supported by BuildKit.") flags.VarP(&opts.memory, "memory", "m", "Set memory limit for the build container. Not supported by BuildKit.")
flags.StringVar(&p.Progress, "progress", "", fmt.Sprintf(`Set type of ui output (%s)`, strings.Join(printerModes, ", "))) flags.StringVar(&p.Progress, "progress", string(buildkit.AutoMode), fmt.Sprintf(`Set type of ui output (%s)`, strings.Join(printerModes, ", ")))
flags.MarkHidden("progress") //nolint:errcheck flags.MarkHidden("progress") //nolint:errcheck
flags.BoolVar(&opts.print, "print", false, "Print equivalent bake file")
flags.BoolVar(&opts.check, "check", false, "Check build configuration")
return cmd return cmd
} }
func runBuild(ctx context.Context, dockerCli command.Cli, backend api.Service, opts buildOptions, services []string) error { func runBuild(ctx context.Context, dockerCli command.Cli, backend api.Service, opts buildOptions, services []string) error {
opts.All = true // do not drop resources as build may involve some dependencies by additional_contexts project, err := opts.ToProject(dockerCli, services, cli.WithResolvedPaths(true), cli.WithoutEnvironmentResolution)
project, _, err := opts.ToProject(ctx, dockerCli, nil, cli.WithResolvedPaths(true), cli.WithoutEnvironmentResolution)
if err != nil { if err != nil {
return err return err
} }
@ -160,10 +146,10 @@ func runBuild(ctx context.Context, dockerCli command.Cli, backend api.Service, o
} }
apiBuildOptions, err := opts.toAPIBuildOptions(services) apiBuildOptions, err := opts.toAPIBuildOptions(services)
apiBuildOptions.Attestations = true
if err != nil { if err != nil {
return err return err
} }
apiBuildOptions.Memory = int64(opts.memory)
return backend.Build(ctx, project, apiBuildOptions) return backend.Build(ctx, project, apiBuildOptions)
} }

View File

@ -1,93 +0,0 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/opts"
"github.com/docker/compose/v2/pkg/api"
"github.com/spf13/cobra"
)
type commitOptions struct {
*ProjectOptions
service string
reference string
pause bool
comment string
author string
changes opts.ListOpts
index int
}
func commitCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
options := commitOptions{
ProjectOptions: p,
}
cmd := &cobra.Command{
Use: "commit [OPTIONS] SERVICE [REPOSITORY[:TAG]]",
Short: "Create a new image from a service container's changes",
Args: cobra.RangeArgs(1, 2),
PreRunE: Adapt(func(ctx context.Context, args []string) error {
options.service = args[0]
if len(args) > 1 {
options.reference = args[1]
}
return nil
}),
RunE: Adapt(func(ctx context.Context, args []string) error {
return runCommit(ctx, dockerCli, backend, options)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
flags := cmd.Flags()
flags.IntVar(&options.index, "index", 0, "index of the container if service has multiple replicas.")
flags.BoolVarP(&options.pause, "pause", "p", true, "Pause container during commit")
flags.StringVarP(&options.comment, "message", "m", "", "Commit message")
flags.StringVarP(&options.author, "author", "a", "", `Author (e.g., "John Hannibal Smith <hannibal@a-team.com>")`)
options.changes = opts.NewListOpts(nil)
flags.VarP(&options.changes, "change", "c", "Apply Dockerfile instruction to the created image")
return cmd
}
func runCommit(ctx context.Context, dockerCli command.Cli, backend api.Service, options commitOptions) error {
projectName, err := options.toProjectName(ctx, dockerCli)
if err != nil {
return err
}
commitOptions := api.CommitOptions{
Service: options.service,
Reference: options.reference,
Pause: options.pause,
Comment: options.comment,
Author: options.author,
Changes: options.changes,
Index: options.index,
}
return backend.Commit(ctx, projectName, commitOptions)
}

View File

@ -37,18 +37,17 @@ func noCompletion() validArgsFn {
func completeServiceNames(dockerCli command.Cli, p *ProjectOptions) validArgsFn { func completeServiceNames(dockerCli command.Cli, p *ProjectOptions) validArgsFn {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
p.Offline = true p.Offline = true
project, _, err := p.ToProject(cmd.Context(), dockerCli, nil) project, err := p.ToProject(dockerCli, nil)
if err != nil { if err != nil {
return nil, cobra.ShellCompDirectiveNoFileComp return nil, cobra.ShellCompDirectiveNoFileComp
} }
var values []string
serviceNames := append(project.ServiceNames(), project.DisabledServiceNames()...) serviceNames := append(project.ServiceNames(), project.DisabledServiceNames()...)
for _, s := range serviceNames { for _, s := range serviceNames {
if toComplete == "" || strings.HasPrefix(s, toComplete) { if toComplete == "" || strings.HasPrefix(s, toComplete) {
values = append(values, s) serviceNames = append(serviceNames, s)
} }
} }
return values, cobra.ShellCompDirectiveNoFileComp return serviceNames, cobra.ShellCompDirectiveNoFileComp
} }
} }
@ -73,7 +72,7 @@ func completeProjectNames(backend api.Service) func(cmd *cobra.Command, args []s
func completeProfileNames(dockerCli command.Cli, p *ProjectOptions) validArgsFn { func completeProfileNames(dockerCli command.Cli, p *ProjectOptions) validArgsFn {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
p.Offline = true p.Offline = true
project, _, err := p.ToProject(cmd.Context(), dockerCli, nil) project, err := p.ToProject(dockerCli, nil)
if err != nil { if err != nil {
return nil, cobra.ShellCompDirectiveNoFileComp return nil, cobra.ShellCompDirectiveNoFileComp
} }
@ -90,13 +89,3 @@ func completeProfileNames(dockerCli command.Cli, p *ProjectOptions) validArgsFn
return values, cobra.ShellCompDirectiveNoFileComp return values, cobra.ShellCompDirectiveNoFileComp
} }
} }
func completeScaleArgs(cli command.Cli, p *ProjectOptions) cobra.CompletionFunc {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
completions, directive := completeServiceNames(cli, p)(cmd, args, toComplete)
for i, completion := range completions {
completions[i] = completion + "="
}
return completions, directive
}
}

View File

@ -18,10 +18,8 @@ package compose
import ( import (
"context" "context"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"io"
"os" "os"
"os/signal" "os/signal"
"path/filepath" "path/filepath"
@ -31,26 +29,24 @@ import (
"github.com/compose-spec/compose-go/v2/cli" "github.com/compose-spec/compose-go/v2/cli"
"github.com/compose-spec/compose-go/v2/dotenv" "github.com/compose-spec/compose-go/v2/dotenv"
"github.com/compose-spec/compose-go/v2/loader"
"github.com/compose-spec/compose-go/v2/types" "github.com/compose-spec/compose-go/v2/types"
composegoutils "github.com/compose-spec/compose-go/v2/utils" composegoutils "github.com/compose-spec/compose-go/v2/utils"
"github.com/docker/buildx/util/logutil" "github.com/docker/buildx/util/logutil"
dockercli "github.com/docker/cli/cli" dockercli "github.com/docker/cli/cli"
"github.com/docker/cli/cli-plugins/metadata" "github.com/docker/cli/cli-plugins/manager"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/pkg/kvfile"
"github.com/docker/compose/v2/cmd/formatter"
"github.com/docker/compose/v2/internal/desktop"
"github.com/docker/compose/v2/internal/experimental"
"github.com/docker/compose/v2/internal/tracing"
"github.com/docker/compose/v2/pkg/api"
ui "github.com/docker/compose/v2/pkg/progress"
"github.com/docker/compose/v2/pkg/remote" "github.com/docker/compose/v2/pkg/remote"
"github.com/docker/compose/v2/pkg/utils" buildkit "github.com/moby/buildkit/util/progress/progressui"
"github.com/morikuni/aec" "github.com/morikuni/aec"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag" "github.com/spf13/pflag"
"github.com/docker/compose/v2/cmd/formatter"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/compose"
ui "github.com/docker/compose/v2/pkg/progress"
"github.com/docker/compose/v2/pkg/utils"
) )
const ( const (
@ -60,45 +56,14 @@ const (
ComposeProjectName = "COMPOSE_PROJECT_NAME" ComposeProjectName = "COMPOSE_PROJECT_NAME"
// ComposeCompatibility try to mimic compose v1 as much as possible // ComposeCompatibility try to mimic compose v1 as much as possible
ComposeCompatibility = "COMPOSE_COMPATIBILITY" ComposeCompatibility = "COMPOSE_COMPATIBILITY"
// ComposeRemoveOrphans remove "orphaned" containers, i.e. containers tagged for current project but not declared as service // ComposeRemoveOrphans remove orphaned" containers, i.e. containers tagged for current project but not declared as service
ComposeRemoveOrphans = "COMPOSE_REMOVE_ORPHANS" ComposeRemoveOrphans = "COMPOSE_REMOVE_ORPHANS"
// ComposeIgnoreOrphans ignore "orphaned" containers // ComposeIgnoreOrphans ignore "orphaned" containers
ComposeIgnoreOrphans = "COMPOSE_IGNORE_ORPHANS" ComposeIgnoreOrphans = "COMPOSE_IGNORE_ORPHANS"
// ComposeEnvFiles defines the env files to use if --env-file isn't used // ComposeEnvFiles defines the env files to use if --env-file isn't used
ComposeEnvFiles = "COMPOSE_ENV_FILES" ComposeEnvFiles = "COMPOSE_ENV_FILES"
// ComposeMenu defines if the navigation menu should be rendered. Can be also set via --menu
ComposeMenu = "COMPOSE_MENU"
// ComposeProgress defines type of progress output, if --progress isn't used
ComposeProgress = "COMPOSE_PROGRESS"
) )
// rawEnv load a dot env file using docker/cli key=value parser, without attempt to interpolate or evaluate values
func rawEnv(r io.Reader, filename string, vars map[string]string, lookup func(key string) (string, bool)) error {
lines, err := kvfile.ParseFromReader(r, lookup)
if err != nil {
return fmt.Errorf("failed to parse env_file %s: %w", filename, err)
}
for _, line := range lines {
key, value, _ := strings.Cut(line, "=")
vars[key] = value
}
return nil
}
func init() {
// compose evaluates env file values for interpolation
// `raw` format allows to load env_file with the same parser used by docker run --env-file
dotenv.RegisterFormat("raw", rawEnv)
}
type Backend interface {
api.Service
SetDesktopClient(cli *desktop.Client)
SetExperiments(experiments *experimental.State)
}
// Command defines a compose CLI command as a func with args // Command defines a compose CLI command as a func with args
type Command func(context.Context, []string) error type Command func(context.Context, []string) error
@ -120,13 +85,18 @@ func AdaptCmd(fn CobraCommand) func(cmd *cobra.Command, args []string) error {
}() }()
err := fn(ctx, cmd, args) err := fn(ctx, cmd, args)
var composeErr compose.Error
if api.IsErrCanceled(err) || errors.Is(ctx.Err(), context.Canceled) { if api.IsErrCanceled(err) || errors.Is(ctx.Err(), context.Canceled) {
err = dockercli.StatusError{ err = dockercli.StatusError{
StatusCode: 130, StatusCode: 130,
Status: compose.CanceledStatus,
} }
} }
if ui.Mode == ui.ModeJSON { if errors.As(err, &composeErr) {
err = makeJSONError(err) err = dockercli.StatusError{
StatusCode: composeErr.GetMetricsFailureCategory().ExitCode,
Status: err.Error(),
}
} }
return err return err
} }
@ -149,7 +119,6 @@ type ProjectOptions struct {
Compatibility bool Compatibility bool
Progress string Progress string
Offline bool Offline bool
All bool
} }
// ProjectFunc does stuff within a types.Project // ProjectFunc does stuff within a types.Project
@ -170,17 +139,11 @@ func (o *ProjectOptions) WithServices(dockerCli command.Cli, fn ProjectServicesF
return Adapt(func(ctx context.Context, args []string) error { return Adapt(func(ctx context.Context, args []string) error {
options := []cli.ProjectOptionsFn{ options := []cli.ProjectOptionsFn{
cli.WithResolvedPaths(true), cli.WithResolvedPaths(true),
cli.WithoutEnvironmentResolution, cli.WithDiscardEnvFile,
cli.WithContext(ctx),
} }
project, metrics, err := o.ToProject(ctx, dockerCli, args, options...) project, err := o.ToProject(dockerCli, args, options...)
if err != nil {
return err
}
ctx = context.WithValue(ctx, tracing.MetricsKey{}, metrics)
project, err = project.WithServicesEnvironmentResolved(true)
if err != nil { if err != nil {
return err return err
} }
@ -189,63 +152,23 @@ func (o *ProjectOptions) WithServices(dockerCli command.Cli, fn ProjectServicesF
}) })
} }
type jsonErrorData struct {
Error bool `json:"error,omitempty"`
Message string `json:"message,omitempty"`
}
func errorAsJSON(message string) string {
errorMessage := &jsonErrorData{
Error: true,
Message: message,
}
marshal, err := json.Marshal(errorMessage)
if err == nil {
return string(marshal)
} else {
return message
}
}
func makeJSONError(err error) error {
if err == nil {
return nil
}
var statusErr dockercli.StatusError
if errors.As(err, &statusErr) {
return dockercli.StatusError{
StatusCode: statusErr.StatusCode,
Status: errorAsJSON(statusErr.Status),
}
}
return fmt.Errorf("%s", errorAsJSON(err.Error()))
}
func (o *ProjectOptions) addProjectFlags(f *pflag.FlagSet) { func (o *ProjectOptions) addProjectFlags(f *pflag.FlagSet) {
f.StringArrayVar(&o.Profiles, "profile", []string{}, "Specify a profile to enable") f.StringArrayVar(&o.Profiles, "profile", []string{}, "Specify a profile to enable")
f.StringVarP(&o.ProjectName, "project-name", "p", "", "Project name") f.StringVarP(&o.ProjectName, "project-name", "p", "", "Project name")
f.StringArrayVarP(&o.ConfigPaths, "file", "f", []string{}, "Compose configuration files") f.StringArrayVarP(&o.ConfigPaths, "file", "f", []string{}, "Compose configuration files")
f.StringArrayVar(&o.EnvFiles, "env-file", defaultStringArrayVar(ComposeEnvFiles), "Specify an alternate environment file") f.StringArrayVar(&o.EnvFiles, "env-file", nil, "Specify an alternate environment file.")
f.StringVar(&o.ProjectDir, "project-directory", "", "Specify an alternate working directory\n(default: the path of the, first specified, Compose file)") f.StringVar(&o.ProjectDir, "project-directory", "", "Specify an alternate working directory\n(default: the path of the, first specified, Compose file)")
f.StringVar(&o.WorkDir, "workdir", "", "DEPRECATED! USE --project-directory INSTEAD.\nSpecify an alternate working directory\n(default: the path of the, first specified, Compose file)") f.StringVar(&o.WorkDir, "workdir", "", "DEPRECATED! USE --project-directory INSTEAD.\nSpecify an alternate working directory\n(default: the path of the, first specified, Compose file)")
f.BoolVar(&o.Compatibility, "compatibility", false, "Run compose in backward compatibility mode") f.BoolVar(&o.Compatibility, "compatibility", false, "Run compose in backward compatibility mode")
f.StringVar(&o.Progress, "progress", os.Getenv(ComposeProgress), fmt.Sprintf(`Set type of progress output (%s)`, strings.Join(printerModes, ", "))) f.StringVar(&o.Progress, "progress", string(buildkit.AutoMode), fmt.Sprintf(`Set type of progress output (%s)`, strings.Join(printerModes, ", ")))
f.BoolVar(&o.All, "all-resources", false, "Include all resources, even those not used by services")
_ = f.MarkHidden("workdir") _ = f.MarkHidden("workdir")
} }
// get default value for a command line flag that is set by a coma-separated value in environment variable func (o *ProjectOptions) projectOrName(dockerCli command.Cli, services ...string) (*types.Project, string, error) {
func defaultStringArrayVar(env string) []string {
return strings.FieldsFunc(os.Getenv(env), func(c rune) bool {
return c == ','
})
}
func (o *ProjectOptions) projectOrName(ctx context.Context, dockerCli command.Cli, services ...string) (*types.Project, string, error) {
name := o.ProjectName name := o.ProjectName
var project *types.Project var project *types.Project
if len(o.ConfigPaths) > 0 || o.ProjectName == "" { if len(o.ConfigPaths) > 0 || o.ProjectName == "" {
p, _, err := o.ToProject(ctx, dockerCli, services, cli.WithDiscardEnvFile, cli.WithoutEnvironmentResolution) p, err := o.ToProject(dockerCli, services, cli.WithDiscardEnvFile)
if err != nil { if err != nil {
envProjectName := os.Getenv(ComposeProjectName) envProjectName := os.Getenv(ComposeProjectName)
if envProjectName != "" { if envProjectName != "" {
@ -259,7 +182,7 @@ func (o *ProjectOptions) projectOrName(ctx context.Context, dockerCli command.Cl
return project, name, nil return project, name, nil
} }
func (o *ProjectOptions) toProjectName(ctx context.Context, dockerCli command.Cli) (string, error) { func (o *ProjectOptions) toProjectName(dockerCli command.Cli) (string, error) {
if o.ProjectName != "" { if o.ProjectName != "" {
return o.ProjectName, nil return o.ProjectName, nil
} }
@ -269,82 +192,39 @@ func (o *ProjectOptions) toProjectName(ctx context.Context, dockerCli command.Cl
return envProjectName, nil return envProjectName, nil
} }
project, _, err := o.ToProject(ctx, dockerCli, nil) project, err := o.ToProject(dockerCli, nil)
if err != nil { if err != nil {
return "", err return "", err
} }
return project.Name, nil return project.Name, nil
} }
func (o *ProjectOptions) ToModel(ctx context.Context, dockerCli command.Cli, services []string, po ...cli.ProjectOptionsFn) (map[string]any, error) { func (o *ProjectOptions) ToProject(dockerCli command.Cli, services []string, po ...cli.ProjectOptionsFn) (*types.Project, error) {
remotes := o.remoteLoaders(dockerCli) if !o.Offline {
for _, r := range remotes { po = o.configureRemoteLoaders(dockerCli, po)
po = append(po, cli.WithResourceLoader(r))
} }
options, err := o.toProjectOptions(po...) options, err := o.toProjectOptions(po...)
if err != nil { if err != nil {
return nil, err return nil, compose.WrapComposeError(err)
} }
if o.Compatibility || utils.StringToBool(options.Environment[ComposeCompatibility]) { if o.Compatibility || utils.StringToBool(options.Environment[ComposeCompatibility]) {
api.Separator = "_" api.Separator = "_"
} }
return options.LoadModel(ctx) project, err := cli.ProjectFromOptions(options)
}
func (o *ProjectOptions) ToProject(ctx context.Context, dockerCli command.Cli, services []string, po ...cli.ProjectOptionsFn) (*types.Project, tracing.Metrics, error) { //nolint:gocyclo
var metrics tracing.Metrics
remotes := o.remoteLoaders(dockerCli)
for _, r := range remotes {
po = append(po, cli.WithResourceLoader(r))
}
options, err := o.toProjectOptions(po...)
if err != nil { if err != nil {
return nil, metrics, err return nil, compose.WrapComposeError(err)
}
options.WithListeners(func(event string, metadata map[string]any) {
switch event {
case "extends":
metrics.CountExtends++
case "include":
paths := metadata["path"].(types.StringList)
for _, path := range paths {
var isRemote bool
for _, r := range remotes {
if r.Accept(path) {
isRemote = true
break
}
}
if isRemote {
metrics.CountIncludesRemote++
} else {
metrics.CountIncludesLocal++
}
}
}
})
if o.Compatibility || utils.StringToBool(options.Environment[ComposeCompatibility]) {
api.Separator = "_"
}
project, err := options.LoadProject(ctx)
if err != nil {
return nil, metrics, err
} }
if project.Name == "" { if project.Name == "" {
return nil, metrics, errors.New("project name can't be empty. Use `--project-name` to set a valid name") return nil, errors.New("project name can't be empty. Use `--project-name` to set a valid name")
} }
project, err = project.WithServicesEnabled(services...) project, err = project.WithServicesEnabled(services...)
if err != nil { if err != nil {
return nil, metrics, err return nil, err
} }
for name, s := range project.Services { for name, s := range project.Services {
@ -362,51 +242,29 @@ func (o *ProjectOptions) ToProject(ctx context.Context, dockerCli command.Cli, s
project.Services[name] = s project.Services[name] = s
} }
project, err = project.WithSelectedServices(services) project = project.WithoutUnnecessaryResources()
if err != nil {
return nil, tracing.Metrics{}, err
}
if !o.All { project, err = project.WithSelectedServices(services)
project = project.WithoutUnnecessaryResources() return project, err
}
return project, metrics, err
} }
func (o *ProjectOptions) remoteLoaders(dockerCli command.Cli) []loader.ResourceLoader { func (o *ProjectOptions) configureRemoteLoaders(dockerCli command.Cli, po []cli.ProjectOptionsFn) []cli.ProjectOptionsFn {
if o.Offline { git := remote.NewGitRemoteLoader(o.Offline)
return nil
}
git := remote.NewGitRemoteLoader(dockerCli, o.Offline)
oci := remote.NewOCIRemoteLoader(dockerCli, o.Offline) oci := remote.NewOCIRemoteLoader(dockerCli, o.Offline)
return []loader.ResourceLoader{git, oci}
po = append(po, cli.WithResourceLoader(git), cli.WithResourceLoader(oci))
return po
} }
func (o *ProjectOptions) toProjectOptions(po ...cli.ProjectOptionsFn) (*cli.ProjectOptions, error) { func (o *ProjectOptions) toProjectOptions(po ...cli.ProjectOptionsFn) (*cli.ProjectOptions, error) {
pwd, err := os.Getwd()
if err != nil {
return nil, err
}
return cli.NewProjectOptions(o.ConfigPaths, return cli.NewProjectOptions(o.ConfigPaths,
append(po, append(po,
cli.WithWorkingDirectory(o.ProjectDir), cli.WithWorkingDirectory(o.ProjectDir),
// First apply os.Environment, always win
cli.WithOsEnv, cli.WithOsEnv,
// set PWD as this variable is not consistently supported on Windows
cli.WithEnv([]string{"PWD=" + pwd}),
// Load PWD/.env if present and no explicit --env-file has been set
cli.WithEnvFiles(o.EnvFiles...), cli.WithEnvFiles(o.EnvFiles...),
// read dot env file to populate project environment
cli.WithDotEnv, cli.WithDotEnv,
// get compose file path set by COMPOSE_FILE
cli.WithConfigFileEnv, cli.WithConfigFileEnv,
// if none was selected, get default compose.yaml file from current dir or parent folder
cli.WithDefaultConfigPath, cli.WithDefaultConfigPath,
// .. and then, a project directory != PWD maybe has been set so let's load .env file
cli.WithEnvFiles(o.EnvFiles...),
cli.WithDotEnv,
// eventually COMPOSE_PROFILES should have been set
cli.WithDefaultProfiles(o.Profiles...), cli.WithDefaultProfiles(o.Profiles...),
cli.WithName(o.ProjectName))...) cli.WithName(o.ProjectName))...)
} }
@ -416,11 +274,11 @@ const PluginName = "compose"
// RunningAsStandalone detects when running as a standalone program // RunningAsStandalone detects when running as a standalone program
func RunningAsStandalone() bool { func RunningAsStandalone() bool {
return len(os.Args) < 2 || os.Args[1] != metadata.MetadataSubcommandName && os.Args[1] != PluginName return len(os.Args) < 2 || os.Args[1] != manager.MetadataSubcommandName && os.Args[1] != PluginName
} }
// RootCommand returns the compose command with its child commands // RootCommand returns the compose command with its child commands
func RootCommand(dockerCli command.Cli, backend Backend) *cobra.Command { //nolint:gocyclo func RootCommand(dockerCli command.Cli, backend api.Service) *cobra.Command { //nolint:gocyclo
// filter out useless commandConn.CloseWrite warning message that can occur // filter out useless commandConn.CloseWrite warning message that can occur
// when using a remote context that is unreachable: "commandConn.CloseWrite: commandconn: failed to wait: signal: killed" // when using a remote context that is unreachable: "commandConn.CloseWrite: commandconn: failed to wait: signal: killed"
// https://github.com/docker/cli/blob/e1f24d3c93df6752d3c27c8d61d18260f141310c/cli/connhelper/commandconn/commandconn.go#L203-L215 // https://github.com/docker/cli/blob/e1f24d3c93df6752d3c27c8d61d18260f141310c/cli/connhelper/commandconn/commandconn.go#L203-L215
@ -431,7 +289,6 @@ func RootCommand(dockerCli command.Cli, backend Backend) *cobra.Command { //noli
"commandConn.CloseRead:", "commandConn.CloseRead:",
)) ))
experiments := experimental.NewState()
opts := ProjectOptions{} opts := ProjectOptions{}
var ( var (
ansi string ansi string
@ -443,7 +300,7 @@ func RootCommand(dockerCli command.Cli, backend Backend) *cobra.Command { //noli
) )
c := &cobra.Command{ c := &cobra.Command{
Short: "Docker Compose", Short: "Docker Compose",
Long: "Define and run multi-container applications with Docker", Long: "Define and run multi-container applications with Docker.",
Use: PluginName, Use: PluginName,
TraverseChildren: true, TraverseChildren: true,
// By default (no Run/RunE in parent c) for typos in subcommands, cobra displays the help of parent c but exit(0) ! // By default (no Run/RunE in parent c) for typos in subcommands, cobra displays the help of parent c but exit(0) !
@ -456,14 +313,16 @@ func RootCommand(dockerCli command.Cli, backend Backend) *cobra.Command { //noli
} }
_ = cmd.Help() _ = cmd.Help()
return dockercli.StatusError{ return dockercli.StatusError{
StatusCode: 1, StatusCode: compose.CommandSyntaxFailure.ExitCode,
Status: fmt.Sprintf("unknown docker command: %q", "compose "+args[0]), Status: fmt.Sprintf("unknown docker command: %q", "compose "+args[0]),
} }
}, },
PersistentPreRunE: func(cmd *cobra.Command, args []string) error { PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context() err := setEnvWithDotEnv(&opts)
if err != nil {
return err
}
parent := cmd.Root() parent := cmd.Root()
if parent != nil { if parent != nil {
parentPrerun := parent.PersistentPreRunE parentPrerun := parent.PersistentPreRunE
if parentPrerun != nil { if parentPrerun != nil {
@ -473,15 +332,6 @@ func RootCommand(dockerCli command.Cli, backend Backend) *cobra.Command { //noli
} }
} }
} }
if verbose {
logrus.SetLevel(logrus.TraceLevel)
}
err := setEnvWithDotEnv(opts)
if err != nil {
return err
}
if noAnsi { if noAnsi {
if ansi != "auto" { if ansi != "auto" {
return errors.New(`cannot specify DEPRECATED "--no-ansi" and "--ansi". Please use only "--ansi"`) return errors.New(`cannot specify DEPRECATED "--no-ansi" and "--ansi". Please use only "--ansi"`)
@ -489,9 +339,14 @@ func RootCommand(dockerCli command.Cli, backend Backend) *cobra.Command { //noli
ansi = "never" ansi = "never"
fmt.Fprint(os.Stderr, "option '--no-ansi' is DEPRECATED ! Please use '--ansi' instead.\n") fmt.Fprint(os.Stderr, "option '--no-ansi' is DEPRECATED ! Please use '--ansi' instead.\n")
} }
if verbose {
logrus.SetLevel(logrus.TraceLevel)
}
if v, ok := os.LookupEnv("COMPOSE_ANSI"); ok && !cmd.Flags().Changed("ansi") { if v, ok := os.LookupEnv("COMPOSE_ANSI"); ok && !cmd.Flags().Changed("ansi") {
ansi = v ansi = v
} }
formatter.SetANSIMode(dockerCli, ansi) formatter.SetANSIMode(dockerCli, ansi)
if noColor, ok := os.LookupEnv("NO_COLOR"); ok && noColor != "" { if noColor, ok := os.LookupEnv("NO_COLOR"); ok && noColor != "" {
@ -507,10 +362,8 @@ func RootCommand(dockerCli command.Cli, backend Backend) *cobra.Command { //noli
} }
switch opts.Progress { switch opts.Progress {
case "", ui.ModeAuto: case ui.ModeAuto:
if ansi == "never" { ui.Mode = ui.ModeAuto
ui.Mode = ui.ModePlain
}
case ui.ModeTTY: case ui.ModeTTY:
if ansi == "never" { if ansi == "never" {
return fmt.Errorf("can't use --progress tty while ANSI support is disabled") return fmt.Errorf("can't use --progress tty while ANSI support is disabled")
@ -523,14 +376,10 @@ func RootCommand(dockerCli command.Cli, backend Backend) *cobra.Command { //noli
ui.Mode = ui.ModePlain ui.Mode = ui.ModePlain
case ui.ModeQuiet, "none": case ui.ModeQuiet, "none":
ui.Mode = ui.ModeQuiet ui.Mode = ui.ModeQuiet
case ui.ModeJSON:
ui.Mode = ui.ModeJSON
logrus.SetFormatter(&logrus.JSONFormatter{})
default: default:
return fmt.Errorf("unsupported --progress value %q", opts.Progress) return fmt.Errorf("unsupported --progress value %q", opts.Progress)
} }
// (4) options validation / normalization
if opts.WorkDir != "" { if opts.WorkDir != "" {
if opts.ProjectDir != "" { if opts.ProjectDir != "" {
return errors.New(`cannot specify DEPRECATED "--workdir" and "--project-directory". Please use only "--project-directory" instead`) return errors.New(`cannot specify DEPRECATED "--workdir" and "--project-directory". Please use only "--project-directory" instead`)
@ -540,7 +389,7 @@ func RootCommand(dockerCli command.Cli, backend Backend) *cobra.Command { //noli
} }
for i, file := range opts.EnvFiles { for i, file := range opts.EnvFiles {
if !filepath.IsAbs(file) { if !filepath.IsAbs(file) {
file, err := filepath.Abs(file) file, err = filepath.Abs(file)
if err != nil { if err != nil {
return err return err
} }
@ -549,7 +398,10 @@ func RootCommand(dockerCli command.Cli, backend Backend) *cobra.Command { //noli
} }
composeCmd := cmd composeCmd := cmd
for composeCmd.Name() != PluginName { for {
if composeCmd.Name() == PluginName {
break
}
if !composeCmd.HasParent() { if !composeCmd.HasParent() {
return fmt.Errorf("error parsing command line, expected %q", PluginName) return fmt.Errorf("error parsing command line, expected %q", PluginName)
} }
@ -564,38 +416,13 @@ func RootCommand(dockerCli command.Cli, backend Backend) *cobra.Command { //noli
parallel = i parallel = i
} }
if parallel > 0 { if parallel > 0 {
logrus.Debugf("Limiting max concurrency to %d jobs", parallel)
backend.MaxConcurrency(parallel) backend.MaxConcurrency(parallel)
} }
ctx, err := backend.DryRunMode(cmd.Context(), dryRun)
// dry run detection
ctx, err = backend.DryRunMode(ctx, dryRun)
if err != nil { if err != nil {
return err return err
} }
cmd.SetContext(ctx) cmd.SetContext(ctx)
// (6) Desktop integration
var desktopCli *desktop.Client
if !dryRun {
if desktopCli, err = desktop.NewFromDockerClient(ctx, dockerCli); desktopCli != nil {
logrus.Debugf("Enabled Docker Desktop integration (experimental) @ %s", desktopCli.Endpoint())
backend.SetDesktopClient(desktopCli)
} else if err != nil {
// not fatal, Compose will still work but behave as though
// it's not running as part of Docker Desktop
logrus.Debugf("failed to enable Docker Desktop integration: %v", err)
} else {
logrus.Trace("Docker Desktop integration not enabled")
}
}
// (7) experimental features
if err := experiments.Load(ctx, desktopCli); err != nil {
logrus.Debugf("Failed to query feature flags from Desktop: %v", err)
}
backend.SetExperiments(experiments)
return nil return nil
}, },
} }
@ -609,14 +436,12 @@ func RootCommand(dockerCli command.Cli, backend Backend) *cobra.Command { //noli
psCommand(&opts, dockerCli, backend), psCommand(&opts, dockerCli, backend),
listCommand(dockerCli, backend), listCommand(dockerCli, backend),
logsCommand(&opts, dockerCli, backend), logsCommand(&opts, dockerCli, backend),
configCommand(&opts, dockerCli), configCommand(&opts, dockerCli, backend),
killCommand(&opts, dockerCli, backend), killCommand(&opts, dockerCli, backend),
runCommand(&opts, dockerCli, backend), runCommand(&opts, dockerCli, backend),
removeCommand(&opts, dockerCli, backend), removeCommand(&opts, dockerCli, backend),
execCommand(&opts, dockerCli, backend), execCommand(&opts, dockerCli, backend),
attachCommand(&opts, dockerCli, backend), attachCommand(&opts, dockerCli, backend),
exportCommand(&opts, dockerCli, backend),
commitCommand(&opts, dockerCli, backend),
pauseCommand(&opts, dockerCli, backend), pauseCommand(&opts, dockerCli, backend),
unpauseCommand(&opts, dockerCli, backend), unpauseCommand(&opts, dockerCli, backend),
topCommand(&opts, dockerCli, backend), topCommand(&opts, dockerCli, backend),
@ -633,10 +458,7 @@ func RootCommand(dockerCli command.Cli, backend Backend) *cobra.Command { //noli
scaleCommand(&opts, dockerCli, backend), scaleCommand(&opts, dockerCli, backend),
statsCommand(&opts, dockerCli), statsCommand(&opts, dockerCli),
watchCommand(&opts, dockerCli, backend), watchCommand(&opts, dockerCli, backend),
publishCommand(&opts, dockerCli, backend),
alphaCommand(&opts, dockerCli, backend), alphaCommand(&opts, dockerCli, backend),
bridgeCommand(&opts, dockerCli),
volumesCommand(&opts, dockerCli, backend),
) )
c.Flags().SetInterspersed(false) c.Flags().SetInterspersed(false)
@ -661,10 +483,6 @@ func RootCommand(dockerCli command.Cli, backend Backend) *cobra.Command { //noli
"profile", "profile",
completeProfileNames(dockerCli, &opts), completeProfileNames(dockerCli, &opts),
) )
c.RegisterFlagCompletionFunc( //nolint:errcheck
"progress",
cobra.FixedCompletions(printerModes, cobra.ShellCompDirectiveNoFileComp),
)
c.Flags().StringVar(&ansi, "ansi", "auto", `Control when to print ANSI control characters ("never"|"always"|"auto")`) c.Flags().StringVar(&ansi, "ansi", "auto", `Control when to print ANSI control characters ("never"|"always"|"auto")`)
c.Flags().IntVar(&parallel, "parallel", -1, `Control max parallelism, -1 for unlimited`) c.Flags().IntVar(&parallel, "parallel", -1, `Control max parallelism, -1 for unlimited`)
@ -678,46 +496,34 @@ func RootCommand(dockerCli command.Cli, backend Backend) *cobra.Command { //noli
return c return c
} }
func setEnvWithDotEnv(opts ProjectOptions) error { func setEnvWithDotEnv(prjOpts *ProjectOptions) error {
options, err := cli.NewProjectOptions(opts.ConfigPaths, if len(prjOpts.EnvFiles) == 0 {
cli.WithWorkingDirectory(opts.ProjectDir), if envFiles := os.Getenv(ComposeEnvFiles); envFiles != "" {
cli.WithOsEnv, prjOpts.EnvFiles = strings.Split(envFiles, ",")
cli.WithEnvFiles(opts.EnvFiles...), }
cli.WithDotEnv,
)
if err != nil {
return nil
} }
options, err := prjOpts.toProjectOptions()
if err != nil {
return compose.WrapComposeError(err)
}
envFromFile, err := dotenv.GetEnvFromFile(composegoutils.GetAsEqualsMap(os.Environ()), options.EnvFiles) envFromFile, err := dotenv.GetEnvFromFile(composegoutils.GetAsEqualsMap(os.Environ()), options.EnvFiles)
if err != nil { if err != nil {
return nil return err
} }
for k, v := range envFromFile { for k, v := range envFromFile {
if _, ok := os.LookupEnv(k); !ok && strings.HasPrefix(k, "COMPOSE_") { if _, ok := os.LookupEnv(k); !ok { // Precedence to OS Env
if err = os.Setenv(k, v); err != nil { if err := os.Setenv(k, v); err != nil {
return nil return err
} }
} }
} }
return err return nil
} }
var printerModes = []string{ var printerModes = []string{
ui.ModeAuto, ui.ModeAuto,
ui.ModeTTY, ui.ModeTTY,
ui.ModePlain, ui.ModePlain,
ui.ModeJSON,
ui.ModeQuiet, ui.ModeQuiet,
} }
func SetUnchangedOption(name string, experimentalFlag bool) bool {
var value bool
// If the var is defined we use that value first
if envVar, ok := os.LookupEnv(name); ok {
value = utils.StringToBool(envVar)
} else {
// if not, we try to get it from experimental feature flag
value = experimentalFlag
}
return value
}

View File

@ -19,20 +19,15 @@ package compose
import ( import (
"bytes" "bytes"
"context" "context"
"encoding/json"
"fmt" "fmt"
"io"
"os" "os"
"sort" "sort"
"strings" "strings"
"github.com/compose-spec/compose-go/v2/cli" "github.com/compose-spec/compose-go/v2/cli"
"github.com/compose-spec/compose-go/v2/template"
"github.com/compose-spec/compose-go/v2/types" "github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/cmd/formatter"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"gopkg.in/yaml.v3"
"github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/compose" "github.com/docker/compose/v2/pkg/compose"
@ -47,49 +42,34 @@ type configOptions struct {
noInterpolate bool noInterpolate bool
noNormalize bool noNormalize bool
noResolvePath bool noResolvePath bool
noResolveEnv bool
services bool services bool
volumes bool volumes bool
networks bool
models bool
profiles bool profiles bool
images bool images bool
hash string hash string
noConsistency bool noConsistency bool
variables bool
environment bool
lockImageDigests bool
} }
func (o *configOptions) ToProject(ctx context.Context, dockerCli command.Cli, services []string, po ...cli.ProjectOptionsFn) (*types.Project, error) { func (o *configOptions) ToProject(ctx context.Context, dockerCli command.Cli, services []string, po ...cli.ProjectOptionsFn) (*types.Project, error) {
po = append(po, o.ToProjectOptions()...) po = append(po,
project, _, err := o.ProjectOptions.ToProject(ctx, dockerCli, services, po...)
return project, err
}
func (o *configOptions) ToModel(ctx context.Context, dockerCli command.Cli, services []string, po ...cli.ProjectOptionsFn) (map[string]any, error) {
po = append(po, o.ToProjectOptions()...)
return o.ProjectOptions.ToModel(ctx, dockerCli, services, po...)
}
func (o *configOptions) ToProjectOptions() []cli.ProjectOptionsFn {
return []cli.ProjectOptionsFn{
cli.WithInterpolation(!o.noInterpolate), cli.WithInterpolation(!o.noInterpolate),
cli.WithResolvedPaths(!o.noResolvePath), cli.WithResolvedPaths(!o.noResolvePath),
cli.WithNormalization(!o.noNormalize), cli.WithNormalization(!o.noNormalize),
cli.WithConsistency(!o.noConsistency), cli.WithConsistency(!o.noConsistency),
cli.WithDefaultProfiles(o.Profiles...), cli.WithDefaultProfiles(o.Profiles...),
cli.WithDiscardEnvFile, cli.WithDiscardEnvFile,
} cli.WithContext(ctx))
return o.ProjectOptions.ToProject(dockerCli, services, po...)
} }
func configCommand(p *ProjectOptions, dockerCli command.Cli) *cobra.Command { func configCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
opts := configOptions{ opts := configOptions{
ProjectOptions: p, ProjectOptions: p,
} }
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "config [OPTIONS] [SERVICE...]", Aliases: []string{"convert"}, // for backward compatibility with Cloud integrations
Short: "Parse, resolve and render compose file in canonical format", Use: "config [OPTIONS] [SERVICE...]",
Short: "Parse, resolve and render compose file in canonical format",
PreRunE: Adapt(func(ctx context.Context, args []string) error { PreRunE: Adapt(func(ctx context.Context, args []string) error {
if opts.quiet { if opts.quiet {
devnull, err := os.Open(os.DevNull) devnull, err := os.Open(os.DevNull)
@ -101,9 +81,6 @@ func configCommand(p *ProjectOptions, dockerCli command.Cli) *cobra.Command {
if p.Compatibility { if p.Compatibility {
opts.noNormalize = true opts.noNormalize = true
} }
if opts.lockImageDigests {
opts.resolveImageDigests = true
}
return nil return nil
}), }),
RunE: Adapt(func(ctx context.Context, args []string) error { RunE: Adapt(func(ctx context.Context, args []string) error {
@ -113,12 +90,6 @@ func configCommand(p *ProjectOptions, dockerCli command.Cli) *cobra.Command {
if opts.volumes { if opts.volumes {
return runVolumes(ctx, dockerCli, opts) return runVolumes(ctx, dockerCli, opts)
} }
if opts.networks {
return runNetworks(ctx, dockerCli, opts)
}
if opts.models {
return runModels(ctx, dockerCli, opts)
}
if opts.hash != "" { if opts.hash != "" {
return runHash(ctx, dockerCli, opts) return runHash(ctx, dockerCli, opts)
} }
@ -128,57 +99,44 @@ func configCommand(p *ProjectOptions, dockerCli command.Cli) *cobra.Command {
if opts.images { if opts.images {
return runConfigImages(ctx, dockerCli, opts, args) return runConfigImages(ctx, dockerCli, opts, args)
} }
if opts.variables {
return runVariables(ctx, dockerCli, opts, args)
}
if opts.environment {
return runEnvironment(ctx, dockerCli, opts, args)
}
if opts.Format == "" { return runConfig(ctx, dockerCli, backend, opts, args)
opts.Format = "yaml"
}
return runConfig(ctx, dockerCli, opts, args)
}), }),
ValidArgsFunction: completeServiceNames(dockerCli, p), ValidArgsFunction: completeServiceNames(dockerCli, p),
} }
flags := cmd.Flags() flags := cmd.Flags()
flags.StringVar(&opts.Format, "format", "", "Format the output. Values: [yaml | json]") flags.StringVar(&opts.Format, "format", "yaml", "Format the output. Values: [yaml | json]")
flags.BoolVar(&opts.resolveImageDigests, "resolve-image-digests", false, "Pin image tags to digests") flags.BoolVar(&opts.resolveImageDigests, "resolve-image-digests", false, "Pin image tags to digests.")
flags.BoolVar(&opts.lockImageDigests, "lock-image-digests", false, "Produces an override file with image digests") flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only validate the configuration, don't print anything.")
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only validate the configuration, don't print anything") flags.BoolVar(&opts.noInterpolate, "no-interpolate", false, "Don't interpolate environment variables.")
flags.BoolVar(&opts.noInterpolate, "no-interpolate", false, "Don't interpolate environment variables") flags.BoolVar(&opts.noNormalize, "no-normalize", false, "Don't normalize compose model.")
flags.BoolVar(&opts.noNormalize, "no-normalize", false, "Don't normalize compose model") flags.BoolVar(&opts.noResolvePath, "no-path-resolution", false, "Don't resolve file paths.")
flags.BoolVar(&opts.noResolvePath, "no-path-resolution", false, "Don't resolve file paths")
flags.BoolVar(&opts.noConsistency, "no-consistency", false, "Don't check model consistency - warning: may produce invalid Compose output") flags.BoolVar(&opts.noConsistency, "no-consistency", false, "Don't check model consistency - warning: may produce invalid Compose output")
flags.BoolVar(&opts.noResolveEnv, "no-env-resolution", false, "Don't resolve service env files")
flags.BoolVar(&opts.services, "services", false, "Print the service names, one per line.") flags.BoolVar(&opts.services, "services", false, "Print the service names, one per line.")
flags.BoolVar(&opts.volumes, "volumes", false, "Print the volume names, one per line.") flags.BoolVar(&opts.volumes, "volumes", false, "Print the volume names, one per line.")
flags.BoolVar(&opts.networks, "networks", false, "Print the network names, one per line.")
flags.BoolVar(&opts.models, "models", false, "Print the model names, one per line.")
flags.BoolVar(&opts.profiles, "profiles", false, "Print the profile names, one per line.") flags.BoolVar(&opts.profiles, "profiles", false, "Print the profile names, one per line.")
flags.BoolVar(&opts.images, "images", false, "Print the image names, one per line.") flags.BoolVar(&opts.images, "images", false, "Print the image names, one per line.")
flags.StringVar(&opts.hash, "hash", "", "Print the service config hash, one per line.") flags.StringVar(&opts.hash, "hash", "", "Print the service config hash, one per line.")
flags.BoolVar(&opts.variables, "variables", false, "Print model variables and default values.")
flags.BoolVar(&opts.environment, "environment", false, "Print environment used for interpolation.")
flags.StringVarP(&opts.Output, "output", "o", "", "Save to file (default to stdout)") flags.StringVarP(&opts.Output, "output", "o", "", "Save to file (default to stdout)")
return cmd return cmd
} }
func runConfig(ctx context.Context, dockerCli command.Cli, opts configOptions, services []string) (err error) { func runConfig(ctx context.Context, dockerCli command.Cli, backend api.Service, opts configOptions, services []string) error {
var content []byte var content []byte
if opts.noInterpolate { project, err := opts.ToProject(ctx, dockerCli, services)
content, err = runConfigNoInterpolate(ctx, dockerCli, opts, services) if err != nil {
if err != nil { return err
return err }
}
} else { content, err = backend.Config(ctx, project, api.ConfigOptions{
content, err = runConfigInterpolate(ctx, dockerCli, opts, services) Format: opts.Format,
if err != nil { Output: opts.Output,
return err ResolveImageDigests: opts.resolveImageDigests,
} })
if err != nil {
return err
} }
if !opts.noInterpolate { if !opts.noInterpolate {
@ -196,173 +154,15 @@ func runConfig(ctx context.Context, dockerCli command.Cli, opts configOptions, s
return err return err
} }
func runConfigInterpolate(ctx context.Context, dockerCli command.Cli, opts configOptions, services []string) ([]byte, error) {
project, err := opts.ToProject(ctx, dockerCli, services)
if err != nil {
return nil, err
}
if opts.resolveImageDigests {
project, err = project.WithImagesResolved(compose.ImageDigestResolver(ctx, dockerCli.ConfigFile(), dockerCli.Client()))
if err != nil {
return nil, err
}
}
if !opts.noResolveEnv {
project, err = project.WithServicesEnvironmentResolved(true)
if err != nil {
return nil, err
}
}
if !opts.noConsistency {
err := project.CheckContainerNameUnicity()
if err != nil {
return nil, err
}
}
if opts.lockImageDigests {
project = imagesOnly(project)
}
var content []byte
switch opts.Format {
case "json":
content, err = project.MarshalJSON()
case "yaml":
content, err = project.MarshalYAML()
default:
return nil, fmt.Errorf("unsupported format %q", opts.Format)
}
if err != nil {
return nil, err
}
return content, nil
}
// imagesOnly return project with all attributes removed but service.images
func imagesOnly(project *types.Project) *types.Project {
digests := types.Services{}
for name, config := range project.Services {
digests[name] = types.ServiceConfig{
Image: config.Image,
}
}
project = &types.Project{Services: digests}
return project
}
func runConfigNoInterpolate(ctx context.Context, dockerCli command.Cli, opts configOptions, services []string) ([]byte, error) {
// we can't use ToProject, so the model we render here is only partially resolved
model, err := opts.ToModel(ctx, dockerCli, services)
if err != nil {
return nil, err
}
if opts.resolveImageDigests {
err = resolveImageDigests(ctx, dockerCli, model)
if err != nil {
return nil, err
}
}
if opts.lockImageDigests {
for key, e := range model {
if key != "services" {
delete(model, key)
} else {
for _, s := range e.(map[string]any) {
service := s.(map[string]any)
for key := range service {
if key != "image" {
delete(service, key)
}
}
}
}
}
}
return formatModel(model, opts.Format)
}
func resolveImageDigests(ctx context.Context, dockerCli command.Cli, model map[string]any) (err error) {
// create a pseudo-project so we can rely on WithImagesResolved to resolve images
p := &types.Project{
Services: types.Services{},
}
services := model["services"].(map[string]any)
for name, s := range services {
service := s.(map[string]any)
if image, ok := service["image"]; ok {
p.Services[name] = types.ServiceConfig{
Image: image.(string),
}
}
}
p, err = p.WithImagesResolved(compose.ImageDigestResolver(ctx, dockerCli.ConfigFile(), dockerCli.Client()))
if err != nil {
return err
}
// Collect image resolved with digest and update model accordingly
for name, s := range services {
service := s.(map[string]any)
config := p.Services[name]
if config.Image != "" {
service["image"] = config.Image
}
services[name] = service
}
model["services"] = services
return nil
}
func formatModel(model map[string]any, format string) (content []byte, err error) {
switch format {
case "json":
content, err = json.MarshalIndent(model, "", " ")
case "yaml":
buf := bytes.NewBuffer([]byte{})
encoder := yaml.NewEncoder(buf)
encoder.SetIndent(2)
err = encoder.Encode(model)
content = buf.Bytes()
default:
return nil, fmt.Errorf("unsupported format %q", format)
}
return
}
func runServices(ctx context.Context, dockerCli command.Cli, opts configOptions) error { func runServices(ctx context.Context, dockerCli command.Cli, opts configOptions) error {
if opts.noInterpolate {
// we can't use ToProject, so the model we render here is only partially resolved
data, err := opts.ToModel(ctx, dockerCli, nil, cli.WithoutEnvironmentResolution)
if err != nil {
return err
}
if _, ok := data["services"]; ok {
for serviceName := range data["services"].(map[string]any) {
_, _ = fmt.Fprintln(dockerCli.Out(), serviceName)
}
}
return nil
}
project, err := opts.ToProject(ctx, dockerCli, nil, cli.WithoutEnvironmentResolution) project, err := opts.ToProject(ctx, dockerCli, nil, cli.WithoutEnvironmentResolution)
if err != nil { if err != nil {
return err return err
} }
err = project.ForEachService(project.ServiceNames(), func(serviceName string, _ *types.ServiceConfig) error { err = project.ForEachService(project.ServiceNames(), func(serviceName string, _ *types.ServiceConfig) error {
_, _ = fmt.Fprintln(dockerCli.Out(), serviceName) fmt.Fprintln(dockerCli.Out(), serviceName)
return nil return nil
}) })
return err return err
} }
@ -372,31 +172,7 @@ func runVolumes(ctx context.Context, dockerCli command.Cli, opts configOptions)
return err return err
} }
for n := range project.Volumes { for n := range project.Volumes {
_, _ = fmt.Fprintln(dockerCli.Out(), n) fmt.Fprintln(dockerCli.Out(), n)
}
return nil
}
func runNetworks(ctx context.Context, dockerCli command.Cli, opts configOptions) error {
project, err := opts.ToProject(ctx, dockerCli, nil, cli.WithoutEnvironmentResolution)
if err != nil {
return err
}
for n := range project.Networks {
_, _ = fmt.Fprintln(dockerCli.Out(), n)
}
return nil
}
func runModels(ctx context.Context, dockerCli command.Cli, opts configOptions) error {
project, err := opts.ToProject(ctx, dockerCli, nil, cli.WithoutEnvironmentResolution)
if err != nil {
return err
}
for _, model := range project.Models {
if model.Model != "" {
_, _ = fmt.Fprintln(dockerCli.Out(), model.Model)
}
} }
return nil return nil
} }
@ -431,10 +207,11 @@ func runHash(ctx context.Context, dockerCli command.Cli, opts configOptions) err
} }
hash, err := compose.ServiceHash(s) hash, err := compose.ServiceHash(s)
if err != nil { if err != nil {
return err return err
} }
_, _ = fmt.Fprintf(dockerCli.Out(), "%s %s\n", name, hash) fmt.Fprintf(dockerCli.Out(), "%s %s\n", name, hash)
} }
return nil return nil
} }
@ -456,7 +233,7 @@ func runProfiles(ctx context.Context, dockerCli command.Cli, opts configOptions,
} }
sort.Strings(profiles) sort.Strings(profiles)
for _, p := range profiles { for _, p := range profiles {
_, _ = fmt.Fprintln(dockerCli.Out(), p) fmt.Fprintln(dockerCli.Out(), p)
} }
return nil return nil
} }
@ -466,46 +243,8 @@ func runConfigImages(ctx context.Context, dockerCli command.Cli, opts configOpti
if err != nil { if err != nil {
return err return err
} }
for _, s := range project.Services { for _, s := range project.Services {
_, _ = fmt.Fprintln(dockerCli.Out(), api.GetImageNameOrDefault(s, project.Name)) fmt.Fprintln(dockerCli.Out(), api.GetImageNameOrDefault(s, project.Name))
}
return nil
}
func runVariables(ctx context.Context, dockerCli command.Cli, opts configOptions, services []string) error {
opts.noInterpolate = true
model, err := opts.ToModel(ctx, dockerCli, services, cli.WithoutEnvironmentResolution)
if err != nil {
return err
}
variables := template.ExtractVariables(model, template.DefaultPattern)
if opts.Format == "yaml" {
result, err := yaml.Marshal(variables)
if err != nil {
return err
}
fmt.Print(string(result))
return nil
}
return formatter.Print(variables, opts.Format, dockerCli.Out(), func(w io.Writer) {
for name, variable := range variables {
_, _ = fmt.Fprintf(w, "%s\t%t\t%s\t%s\n", name, variable.Required, variable.DefaultValue, variable.PresenceValue)
}
}, "NAME", "REQUIRED", "DEFAULT VALUE", "ALTERNATE VALUE")
}
func runEnvironment(ctx context.Context, dockerCli command.Cli, opts configOptions, services []string) error {
project, err := opts.ToProject(ctx, dockerCli, services)
if err != nil {
return err
}
for _, v := range project.Environment.Values() {
fmt.Println(v)
} }
return nil return nil
} }

View File

@ -65,8 +65,10 @@ func copyCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
} }
flags := copyCmd.Flags() flags := copyCmd.Flags()
flags.IntVar(&opts.index, "index", 0, "Index of the container if service has multiple replicas") flags.IntVar(&opts.index, "index", 0, "index of the container if service has multiple replicas")
flags.BoolVar(&opts.all, "all", false, "Include containers created by the run command") flags.BoolVar(&opts.all, "all", false, "copy to all the containers of the service.")
flags.MarkHidden("all") //nolint:errcheck
flags.MarkDeprecated("all", "by default all the containers of the service will get the source file/directory to be copied.") //nolint:errcheck
flags.BoolVarP(&opts.followLink, "follow-link", "L", false, "Always follow symbol link in SRC_PATH") flags.BoolVarP(&opts.followLink, "follow-link", "L", false, "Always follow symbol link in SRC_PATH")
flags.BoolVarP(&opts.copyUIDGID, "archive", "a", false, "Archive mode (copy all uid/gid information)") flags.BoolVarP(&opts.copyUIDGID, "archive", "a", false, "Archive mode (copy all uid/gid information)")
@ -74,7 +76,7 @@ func copyCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
} }
func runCopy(ctx context.Context, dockerCli command.Cli, backend api.Service, opts copyOptions) error { func runCopy(ctx context.Context, dockerCli command.Cli, backend api.Service, opts copyOptions) error {
name, err := opts.toProjectName(ctx, dockerCli) name, err := opts.toProjectName(dockerCli)
if err != nil { if err != nil {
return err return err
} }

View File

@ -26,9 +26,7 @@ import (
"github.com/compose-spec/compose-go/v2/types" "github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/api"
) )
@ -48,7 +46,6 @@ type createOptions struct {
timeout int timeout int
quietPull bool quietPull bool
scale []string scale []string
AssumeYes bool
} }
func createCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command { func createCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
@ -58,7 +55,7 @@ func createCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service
} }
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "create [OPTIONS] [SERVICE...]", Use: "create [OPTIONS] [SERVICE...]",
Short: "Creates containers for a service", Short: "Creates containers for a service.",
PreRunE: AdaptCmd(func(ctx context.Context, cmd *cobra.Command, args []string) error { PreRunE: AdaptCmd(func(ctx context.Context, cmd *cobra.Command, args []string) error {
opts.pullChanged = cmd.Flags().Changed("pull") opts.pullChanged = cmd.Flags().Changed("pull")
if opts.Build && opts.noBuild { if opts.Build && opts.noBuild {
@ -75,23 +72,13 @@ func createCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service
ValidArgsFunction: completeServiceNames(dockerCli, p), ValidArgsFunction: completeServiceNames(dockerCli, p),
} }
flags := cmd.Flags() flags := cmd.Flags()
flags.BoolVar(&opts.Build, "build", false, "Build images before starting containers") flags.BoolVar(&opts.Build, "build", false, "Build images before starting containers.")
flags.BoolVar(&opts.noBuild, "no-build", false, "Don't build an image, even if it's policy") flags.BoolVar(&opts.noBuild, "no-build", false, "Don't build an image, even if it's policy.")
flags.StringVar(&opts.Pull, "pull", "policy", `Pull image before running ("always"|"missing"|"never"|"build")`) flags.StringVar(&opts.Pull, "pull", "policy", `Pull image before running ("always"|"missing"|"never"|"build")`)
flags.BoolVar(&opts.quietPull, "quiet-pull", false, "Pull without printing progress information") flags.BoolVar(&opts.forceRecreate, "force-recreate", false, "Recreate containers even if their configuration and image haven't changed.")
flags.BoolVar(&opts.forceRecreate, "force-recreate", false, "Recreate containers even if their configuration and image haven't changed")
flags.BoolVar(&opts.noRecreate, "no-recreate", false, "If containers already exist, don't recreate them. Incompatible with --force-recreate.") flags.BoolVar(&opts.noRecreate, "no-recreate", false, "If containers already exist, don't recreate them. Incompatible with --force-recreate.")
flags.BoolVar(&opts.removeOrphans, "remove-orphans", false, "Remove containers for services not defined in the Compose file") flags.BoolVar(&opts.removeOrphans, "remove-orphans", false, "Remove containers for services not defined in the Compose file.")
flags.StringArrayVar(&opts.scale, "scale", []string{}, "Scale SERVICE to NUM instances. Overrides the `scale` setting in the Compose file if present.") flags.StringArrayVar(&opts.scale, "scale", []string{}, "Scale SERVICE to NUM instances. Overrides the `scale` setting in the Compose file if present.")
flags.BoolVarP(&opts.AssumeYes, "yes", "y", false, `Assume "yes" as answer to all prompts and run non-interactively`)
flags.SetNormalizeFunc(func(f *pflag.FlagSet, name string) pflag.NormalizedName {
// assumeYes was introduced by mistake as `--y`
if name == "y" {
logrus.Warn("--y is deprecated, please use --yes instead")
name = "yes"
}
return pflag.NormalizedName(name)
})
return cmd return cmd
} }
@ -118,8 +105,7 @@ func runCreate(ctx context.Context, _ command.Cli, backend api.Service, createOp
RecreateDependencies: createOpts.dependenciesRecreateStrategy(), RecreateDependencies: createOpts.dependenciesRecreateStrategy(),
Inherit: !createOpts.noInherit, Inherit: !createOpts.noInherit,
Timeout: createOpts.GetTimeout(), Timeout: createOpts.GetTimeout(),
QuietPull: createOpts.quietPull, QuietPull: false,
AssumeYes: createOpts.AssumeYes,
}) })
} }
@ -130,9 +116,6 @@ func (opts createOptions) recreateStrategy() string {
if opts.forceRecreate { if opts.forceRecreate {
return api.RecreateForce return api.RecreateForce
} }
if opts.noInherit {
return api.RecreateForce
}
return api.RecreateDiverged return api.RecreateDiverged
} }
@ -208,9 +191,7 @@ func applyScaleOpts(project *types.Project, opts []string) error {
} }
func (opts createOptions) isPullPolicyValid() bool { func (opts createOptions) isPullPolicyValid() bool {
pullPolicies := []string{ pullPolicies := []string{types.PullPolicyAlways, types.PullPolicyNever, types.PullPolicyBuild,
types.PullPolicyAlways, types.PullPolicyNever, types.PullPolicyBuild, types.PullPolicyMissing, types.PullPolicyIfNotPresent}
types.PullPolicyMissing, types.PullPolicyIfNotPresent,
}
return slices.Contains(pullPolicies, opts.Pull) return slices.Contains(pullPolicies, opts.Pull)
} }

View File

@ -40,9 +40,7 @@ func TestRunCreate(t *testing.T) {
) )
createOpts := createOptions{} createOpts := createOptions{}
buildOpts := buildOptions{ buildOpts := buildOptions{}
ProjectOptions: &ProjectOptions{},
}
project := sampleProject() project := sampleProject()
err := runCreate(ctx, nil, backend, createOpts, buildOpts, project, nil) err := runCreate(ctx, nil, backend, createOpts, buildOpts, project, nil)
require.NoError(t, err) require.NoError(t, err)
@ -60,9 +58,7 @@ func TestRunCreate_Build(t *testing.T) {
createOpts := createOptions{ createOpts := createOptions{
Build: true, Build: true,
} }
buildOpts := buildOptions{ buildOpts := buildOptions{}
ProjectOptions: &ProjectOptions{},
}
project := sampleProject() project := sampleProject()
err := runCreate(ctx, nil, backend, createOpts, buildOpts, project, nil) err := runCreate(ctx, nil, backend, createOpts, buildOpts, project, nil)
require.NoError(t, err) require.NoError(t, err)

View File

@ -63,9 +63,9 @@ func downCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
} }
flags := downCmd.Flags() flags := downCmd.Flags()
removeOrphans := utils.StringToBool(os.Getenv(ComposeRemoveOrphans)) removeOrphans := utils.StringToBool(os.Getenv(ComposeRemoveOrphans))
flags.BoolVar(&opts.removeOrphans, "remove-orphans", removeOrphans, "Remove containers for services not defined in the Compose file") flags.BoolVar(&opts.removeOrphans, "remove-orphans", removeOrphans, "Remove containers for services not defined in the Compose file.")
flags.IntVarP(&opts.timeout, "timeout", "t", 0, "Specify a shutdown timeout in seconds") flags.IntVarP(&opts.timeout, "timeout", "t", 0, "Specify a shutdown timeout in seconds")
flags.BoolVarP(&opts.volumes, "volumes", "v", false, `Remove named volumes declared in the "volumes" section of the Compose file and anonymous volumes attached to containers`) flags.BoolVarP(&opts.volumes, "volumes", "v", false, `Remove named volumes declared in the "volumes" section of the Compose file and anonymous volumes attached to containers.`)
flags.StringVar(&opts.images, "rmi", "", `Remove images used by services. "local" remove only images that don't have a custom tag ("local"|"all")`) flags.StringVar(&opts.images, "rmi", "", `Remove images used by services. "local" remove only images that don't have a custom tag ("local"|"all")`)
flags.SetNormalizeFunc(func(f *pflag.FlagSet, name string) pflag.NormalizedName { flags.SetNormalizeFunc(func(f *pflag.FlagSet, name string) pflag.NormalizedName {
if name == "volume" { if name == "volume" {
@ -78,7 +78,7 @@ func downCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
} }
func runDown(ctx context.Context, dockerCli command.Cli, backend api.Service, opts downOptions, services []string) error { func runDown(ctx context.Context, dockerCli command.Cli, backend api.Service, opts downOptions, services []string) error {
project, name, err := opts.projectOrName(ctx, dockerCli, services...) project, name, err := opts.projectOrName(dockerCli, services...)
if err != nil { if err != nil {
return err return err
} }

View File

@ -29,9 +29,7 @@ import (
type eventsOpts struct { type eventsOpts struct {
*composeOptions *composeOptions
json bool json bool
since string
until string
} }
func eventsCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command { func eventsCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
@ -42,7 +40,7 @@ func eventsCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service
} }
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "events [OPTIONS] [SERVICE...]", Use: "events [OPTIONS] [SERVICE...]",
Short: "Receive real time events from containers", Short: "Receive real time events from containers.",
RunE: Adapt(func(ctx context.Context, args []string) error { RunE: Adapt(func(ctx context.Context, args []string) error {
return runEvents(ctx, dockerCli, backend, opts, args) return runEvents(ctx, dockerCli, backend, opts, args)
}), }),
@ -50,21 +48,17 @@ func eventsCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service
} }
cmd.Flags().BoolVar(&opts.json, "json", false, "Output events as a stream of json objects") cmd.Flags().BoolVar(&opts.json, "json", false, "Output events as a stream of json objects")
cmd.Flags().StringVar(&opts.since, "since", "", "Show all events created since timestamp")
cmd.Flags().StringVar(&opts.until, "until", "", "Stream events until this timestamp")
return cmd return cmd
} }
func runEvents(ctx context.Context, dockerCli command.Cli, backend api.Service, opts eventsOpts, services []string) error { func runEvents(ctx context.Context, dockerCli command.Cli, backend api.Service, opts eventsOpts, services []string) error {
name, err := opts.toProjectName(ctx, dockerCli) name, err := opts.toProjectName(dockerCli)
if err != nil { if err != nil {
return err return err
} }
return backend.Events(ctx, name, api.EventsOptions{ return backend.Events(ctx, name, api.EventsOptions{
Services: services, Services: services,
Since: opts.since,
Until: opts.until,
Consumer: func(event api.Event) error { Consumer: func(event api.Event) error {
if opts.json { if opts.json {
marshal, err := json.Marshal(map[string]interface{}{ marshal, err := json.Marshal(map[string]interface{}{
@ -78,9 +72,9 @@ func runEvents(ctx context.Context, dockerCli command.Cli, backend api.Service,
if err != nil { if err != nil {
return err return err
} }
_, _ = fmt.Fprintln(dockerCli.Out(), string(marshal)) fmt.Fprintln(dockerCli.Out(), string(marshal))
} else { } else {
_, _ = fmt.Fprintln(dockerCli.Out(), event) fmt.Fprintln(dockerCli.Out(), event)
} }
return nil return nil
}, },

View File

@ -18,18 +18,13 @@ package compose
import ( import (
"context" "context"
"errors"
"fmt"
"os"
"github.com/compose-spec/compose-go/v2/types" "github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/compose" "github.com/docker/compose/v2/pkg/compose"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag"
) )
type execOpts struct { type execOpts struct {
@ -56,7 +51,7 @@ func execCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
} }
runCmd := &cobra.Command{ runCmd := &cobra.Command{
Use: "exec [OPTIONS] SERVICE COMMAND [ARGS...]", Use: "exec [OPTIONS] SERVICE COMMAND [ARGS...]",
Short: "Execute a command in a running container", Short: "Execute a command in a running container.",
Args: cobra.MinimumNArgs(2), Args: cobra.MinimumNArgs(2),
PreRunE: Adapt(func(ctx context.Context, args []string) error { PreRunE: Adapt(func(ctx context.Context, args []string) error {
opts.service = args[0] opts.service = args[0]
@ -64,48 +59,34 @@ func execCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
return nil return nil
}), }),
RunE: Adapt(func(ctx context.Context, args []string) error { RunE: Adapt(func(ctx context.Context, args []string) error {
err := runExec(ctx, dockerCli, backend, opts) return runExec(ctx, dockerCli, backend, opts)
if err != nil {
logrus.Debugf("%v", err)
var cliError cli.StatusError
if ok := errors.As(err, &cliError); ok {
os.Exit(err.(cli.StatusError).StatusCode) //nolint: errorlint
}
}
return err
}), }),
ValidArgsFunction: completeServiceNames(dockerCli, p), ValidArgsFunction: completeServiceNames(dockerCli, p),
} }
runCmd.Flags().BoolVarP(&opts.detach, "detach", "d", false, "Detached mode: Run command in the background") runCmd.Flags().BoolVarP(&opts.detach, "detach", "d", false, "Detached mode: Run command in the background.")
runCmd.Flags().StringArrayVarP(&opts.environment, "env", "e", []string{}, "Set environment variables") runCmd.Flags().StringArrayVarP(&opts.environment, "env", "e", []string{}, "Set environment variables")
runCmd.Flags().IntVar(&opts.index, "index", 0, "Index of the container if service has multiple replicas") runCmd.Flags().IntVar(&opts.index, "index", 0, "index of the container if service has multiple replicas")
runCmd.Flags().BoolVarP(&opts.privileged, "privileged", "", false, "Give extended privileges to the process") runCmd.Flags().BoolVarP(&opts.privileged, "privileged", "", false, "Give extended privileges to the process.")
runCmd.Flags().StringVarP(&opts.user, "user", "u", "", "Run the command as this user") runCmd.Flags().StringVarP(&opts.user, "user", "u", "", "Run the command as this user.")
runCmd.Flags().BoolVarP(&opts.noTty, "no-tty", "T", !dockerCli.Out().IsTerminal(), "Disable pseudo-TTY allocation. By default `docker compose exec` allocates a TTY.") runCmd.Flags().BoolVarP(&opts.noTty, "no-TTY", "T", !dockerCli.Out().IsTerminal(), "Disable pseudo-TTY allocation. By default `docker compose exec` allocates a TTY.")
runCmd.Flags().StringVarP(&opts.workingDir, "workdir", "w", "", "Path to workdir directory for this command") runCmd.Flags().StringVarP(&opts.workingDir, "workdir", "w", "", "Path to workdir directory for this command.")
runCmd.Flags().BoolVarP(&opts.interactive, "interactive", "i", true, "Keep STDIN open even if not attached") runCmd.Flags().BoolVarP(&opts.interactive, "interactive", "i", true, "Keep STDIN open even if not attached.")
runCmd.Flags().MarkHidden("interactive") //nolint:errcheck runCmd.Flags().MarkHidden("interactive") //nolint:errcheck
runCmd.Flags().BoolP("tty", "t", true, "Allocate a pseudo-TTY") runCmd.Flags().BoolP("tty", "t", true, "Allocate a pseudo-TTY.")
runCmd.Flags().MarkHidden("tty") //nolint:errcheck runCmd.Flags().MarkHidden("tty") //nolint:errcheck
runCmd.Flags().SetInterspersed(false) runCmd.Flags().SetInterspersed(false)
runCmd.Flags().SetNormalizeFunc(func(f *pflag.FlagSet, name string) pflag.NormalizedName {
if name == "no-TTY" { // legacy
name = "no-tty"
}
return pflag.NormalizedName(name)
})
return runCmd return runCmd
} }
func runExec(ctx context.Context, dockerCli command.Cli, backend api.Service, opts execOpts) error { func runExec(ctx context.Context, dockerCli command.Cli, backend api.Service, opts execOpts) error {
projectName, err := opts.toProjectName(ctx, dockerCli) projectName, err := opts.toProjectName(dockerCli)
if err != nil { if err != nil {
return err return err
} }
projectOptions, err := opts.composeOptions.toProjectOptions() //nolint:staticcheck projectOptions, err := opts.composeOptions.toProjectOptions()
if err != nil { if err != nil {
return err return err
} }
@ -128,8 +109,8 @@ func runExec(ctx context.Context, dockerCli command.Cli, backend api.Service, op
exitCode, err := backend.Exec(ctx, projectName, execOpts) exitCode, err := backend.Exec(ctx, projectName, execOpts)
if exitCode != 0 { if exitCode != 0 {
errMsg := fmt.Sprintf("exit status %d", exitCode) errMsg := ""
if err != nil && err.Error() != "" { if err != nil {
errMsg = err.Error() errMsg = err.Error()
} }
return cli.StatusError{StatusCode: exitCode, Status: errMsg} return cli.StatusError{StatusCode: exitCode, Status: errMsg}

View File

@ -1,74 +0,0 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
"github.com/docker/compose/v2/pkg/api"
)
type exportOptions struct {
*ProjectOptions
service string
output string
index int
}
func exportCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
options := exportOptions{
ProjectOptions: p,
}
cmd := &cobra.Command{
Use: "export [OPTIONS] SERVICE",
Short: "Export a service container's filesystem as a tar archive",
Args: cobra.MinimumNArgs(1),
PreRunE: Adapt(func(ctx context.Context, args []string) error {
options.service = args[0]
return nil
}),
RunE: Adapt(func(ctx context.Context, args []string) error {
return runExport(ctx, dockerCli, backend, options)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
flags := cmd.Flags()
flags.IntVar(&options.index, "index", 0, "index of the container if service has multiple replicas.")
flags.StringVarP(&options.output, "output", "o", "", "Write to a file, instead of STDOUT")
return cmd
}
func runExport(ctx context.Context, dockerCli command.Cli, backend api.Service, options exportOptions) error {
projectName, err := options.toProjectName(ctx, dockerCli)
if err != nil {
return err
}
exportOptions := api.ExportOptions{
Service: options.service,
Index: options.index,
Output: options.output,
}
return backend.Export(ctx, projectName, exportOptions)
}

View File

@ -1,82 +0,0 @@
/*
Copyright 2023 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"fmt"
"os"
"github.com/docker/compose/v2/pkg/api"
"github.com/spf13/cobra"
)
type generateOptions struct {
*ProjectOptions
Format string
}
func generateCommand(p *ProjectOptions, backend api.Service) *cobra.Command {
opts := generateOptions{
ProjectOptions: p,
}
cmd := &cobra.Command{
Use: "generate [OPTIONS] [CONTAINERS...]",
Short: "EXPERIMENTAL - Generate a Compose file from existing containers",
PreRunE: Adapt(func(ctx context.Context, args []string) error {
return nil
}),
RunE: Adapt(func(ctx context.Context, args []string) error {
return runGenerate(ctx, backend, opts, args)
}),
}
cmd.Flags().StringVar(&opts.ProjectName, "name", "", "Project name to set in the Compose file")
cmd.Flags().StringVar(&opts.ProjectDir, "project-dir", "", "Directory to use for the project")
cmd.Flags().StringVar(&opts.Format, "format", "yaml", "Format the output. Values: [yaml | json]")
return cmd
}
func runGenerate(ctx context.Context, backend api.Service, opts generateOptions, containers []string) error {
_, _ = fmt.Fprintln(os.Stderr, "generate command is EXPERIMENTAL")
if len(containers) == 0 {
return fmt.Errorf("at least one container must be specified")
}
project, err := backend.Generate(ctx, api.GenerateOptions{
Containers: containers,
ProjectName: opts.ProjectName,
})
if err != nil {
return err
}
var content []byte
switch opts.Format {
case "json":
content, err = project.MarshalJSON()
case "yaml":
content, err = project.MarshalYAML()
default:
return fmt.Errorf("unsupported format %q", opts.Format)
}
if err != nil {
return err
}
fmt.Println(string(content))
return nil
}

View File

@ -20,12 +20,9 @@ import (
"context" "context"
"fmt" "fmt"
"io" "io"
"maps" "sort"
"slices"
"strings" "strings"
"time"
"github.com/containerd/platforms"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/docker/pkg/stringid" "github.com/docker/docker/pkg/stringid"
"github.com/docker/go-units" "github.com/docker/go-units"
@ -33,6 +30,7 @@ import (
"github.com/docker/compose/v2/cmd/formatter" "github.com/docker/compose/v2/cmd/formatter"
"github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/utils"
) )
type imageOptions struct { type imageOptions struct {
@ -53,13 +51,13 @@ func imagesCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service
}), }),
ValidArgsFunction: completeServiceNames(dockerCli, p), ValidArgsFunction: completeServiceNames(dockerCli, p),
} }
imgCmd.Flags().StringVar(&opts.Format, "format", "table", "Format the output. Values: [table | json]") imgCmd.Flags().StringVar(&opts.Format, "format", "table", "Format the output. Values: [table | json].")
imgCmd.Flags().BoolVarP(&opts.Quiet, "quiet", "q", false, "Only display IDs") imgCmd.Flags().BoolVarP(&opts.Quiet, "quiet", "q", false, "Only display IDs")
return imgCmd return imgCmd
} }
func runImages(ctx context.Context, dockerCli command.Cli, backend api.Service, opts imageOptions, services []string) error { func runImages(ctx context.Context, dockerCli command.Cli, backend api.Service, opts imageOptions, services []string) error {
projectName, err := opts.toProjectName(ctx, dockerCli) projectName, err := opts.toProjectName(dockerCli)
if err != nil { if err != nil {
return err return err
} }
@ -78,55 +76,23 @@ func runImages(ctx context.Context, dockerCli command.Cli, backend api.Service,
if i := strings.IndexRune(img.ID, ':'); i >= 0 { if i := strings.IndexRune(img.ID, ':'); i >= 0 {
id = id[i+1:] id = id[i+1:]
} }
if !slices.Contains(ids, id) { if !utils.StringContains(ids, id) {
ids = append(ids, id) ids = append(ids, id)
} }
} }
for _, img := range ids { for _, img := range ids {
_, _ = fmt.Fprintln(dockerCli.Out(), img) fmt.Fprintln(dockerCli.Out(), img)
} }
return nil return nil
} }
if opts.Format == "json" {
type img struct { sort.Slice(images, func(i, j int) bool {
ID string `json:"ID"` return images[i].ContainerName < images[j].ContainerName
ContainerName string `json:"ContainerName"` })
Repository string `json:"Repository"`
Tag string `json:"Tag"`
Platform string `json:"Platform"`
Size int64 `json:"Size"`
LastTagTime time.Time `json:"LastTagTime"`
}
// Convert map to slice
var imageList []img
for ctr, i := range images {
lastTagTime := i.LastTagTime
if lastTagTime.IsZero() {
lastTagTime = i.Created
}
imageList = append(imageList, img{
ContainerName: ctr,
ID: i.ID,
Repository: i.Repository,
Tag: i.Tag,
Platform: platforms.Format(i.Platform),
Size: i.Size,
LastTagTime: lastTagTime,
})
}
json, err := formatter.ToJSON(imageList, "", "")
if err != nil {
return err
}
_, err = fmt.Fprintln(dockerCli.Out(), json)
return err
}
return formatter.Print(images, opts.Format, dockerCli.Out(), return formatter.Print(images, opts.Format, dockerCli.Out(),
func(w io.Writer) { func(w io.Writer) {
for _, container := range slices.Sorted(maps.Keys(images)) { for _, img := range images {
img := images[container]
id := stringid.TruncateID(img.ID) id := stringid.TruncateID(img.ID)
size := units.HumanSizeWithPrecision(float64(img.Size), 3) size := units.HumanSizeWithPrecision(float64(img.Size), 3)
repo := img.Repository repo := img.Repository
@ -137,10 +103,8 @@ func runImages(ctx context.Context, dockerCli command.Cli, backend api.Service,
if tag == "" { if tag == "" {
tag = "<none>" tag = "<none>"
} }
created := units.HumanDuration(time.Now().UTC().Sub(img.LastTagTime)) + " ago" _, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", img.ContainerName, repo, tag, id, size)
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
container, repo, tag, platforms.Format(img.Platform), id, size, created)
} }
}, },
"CONTAINER", "REPOSITORY", "TAG", "PLATFORM", "IMAGE ID", "SIZE", "CREATED") "CONTAINER", "REPOSITORY", "TAG", "IMAGE ID", "SIZE")
} }

View File

@ -39,7 +39,7 @@ func killCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
} }
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "kill [OPTIONS] [SERVICE...]", Use: "kill [OPTIONS] [SERVICE...]",
Short: "Force stop service containers", Short: "Force stop service containers.",
RunE: Adapt(func(ctx context.Context, args []string) error { RunE: Adapt(func(ctx context.Context, args []string) error {
return runKill(ctx, dockerCli, backend, opts, args) return runKill(ctx, dockerCli, backend, opts, args)
}), }),
@ -48,14 +48,14 @@ func killCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
flags := cmd.Flags() flags := cmd.Flags()
removeOrphans := utils.StringToBool(os.Getenv(ComposeRemoveOrphans)) removeOrphans := utils.StringToBool(os.Getenv(ComposeRemoveOrphans))
flags.BoolVar(&opts.removeOrphans, "remove-orphans", removeOrphans, "Remove containers for services not defined in the Compose file") flags.BoolVar(&opts.removeOrphans, "remove-orphans", removeOrphans, "Remove containers for services not defined in the Compose file.")
flags.StringVarP(&opts.signal, "signal", "s", "SIGKILL", "SIGNAL to send to the container") flags.StringVarP(&opts.signal, "signal", "s", "SIGKILL", "SIGNAL to send to the container.")
return cmd return cmd
} }
func runKill(ctx context.Context, dockerCli command.Cli, backend api.Service, opts killOptions, services []string) error { func runKill(ctx context.Context, dockerCli command.Cli, backend api.Service, opts killOptions, services []string) error {
project, name, err := opts.projectOrName(ctx, dockerCli, services...) project, name, err := opts.projectOrName(dockerCli, services...)
if err != nil { if err != nil {
return err return err
} }

View File

@ -49,9 +49,9 @@ func listCommand(dockerCli command.Cli, backend api.Service) *cobra.Command {
Args: cobra.NoArgs, Args: cobra.NoArgs,
ValidArgsFunction: noCompletion(), ValidArgsFunction: noCompletion(),
} }
lsCmd.Flags().StringVar(&lsOpts.Format, "format", "table", "Format the output. Values: [table | json]") lsCmd.Flags().StringVar(&lsOpts.Format, "format", "table", "Format the output. Values: [table | json].")
lsCmd.Flags().BoolVarP(&lsOpts.Quiet, "quiet", "q", false, "Only display project names") lsCmd.Flags().BoolVarP(&lsOpts.Quiet, "quiet", "q", false, "Only display IDs.")
lsCmd.Flags().Var(&lsOpts.Filter, "filter", "Filter output based on conditions provided") lsCmd.Flags().Var(&lsOpts.Filter, "filter", "Filter output based on conditions provided.")
lsCmd.Flags().BoolVarP(&lsOpts.All, "all", "a", false, "Show all stopped Compose projects") lsCmd.Flags().BoolVarP(&lsOpts.All, "all", "a", false, "Show all stopped Compose projects")
return lsCmd return lsCmd
@ -86,7 +86,7 @@ func runList(ctx context.Context, dockerCli command.Cli, backend api.Service, ls
if lsOpts.Quiet { if lsOpts.Quiet {
for _, s := range stackList { for _, s := range stackList {
_, _ = fmt.Fprintln(dockerCli.Out(), s.Name) fmt.Fprintln(dockerCli.Out(), s.Name)
} }
return nil return nil
} }

View File

@ -59,32 +59,22 @@ func logsCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
ValidArgsFunction: completeServiceNames(dockerCli, p), ValidArgsFunction: completeServiceNames(dockerCli, p),
} }
flags := logsCmd.Flags() flags := logsCmd.Flags()
flags.BoolVarP(&opts.follow, "follow", "f", false, "Follow log output") flags.BoolVarP(&opts.follow, "follow", "f", false, "Follow log output.")
flags.IntVar(&opts.index, "index", 0, "index of the container if service has multiple replicas") flags.IntVar(&opts.index, "index", 0, "index of the container if service has multiple replicas")
flags.StringVar(&opts.since, "since", "", "Show logs since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)") flags.StringVar(&opts.since, "since", "", "Show logs since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)")
flags.StringVar(&opts.until, "until", "", "Show logs before a timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)") flags.StringVar(&opts.until, "until", "", "Show logs before a timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)")
flags.BoolVar(&opts.noColor, "no-color", false, "Produce monochrome output") flags.BoolVar(&opts.noColor, "no-color", false, "Produce monochrome output.")
flags.BoolVar(&opts.noPrefix, "no-log-prefix", false, "Don't print prefix in logs") flags.BoolVar(&opts.noPrefix, "no-log-prefix", false, "Don't print prefix in logs.")
flags.BoolVarP(&opts.timestamps, "timestamps", "t", false, "Show timestamps") flags.BoolVarP(&opts.timestamps, "timestamps", "t", false, "Show timestamps.")
flags.StringVarP(&opts.tail, "tail", "n", "all", "Number of lines to show from the end of the logs for each container") flags.StringVarP(&opts.tail, "tail", "n", "all", "Number of lines to show from the end of the logs for each container.")
return logsCmd return logsCmd
} }
func runLogs(ctx context.Context, dockerCli command.Cli, backend api.Service, opts logsOptions, services []string) error { func runLogs(ctx context.Context, dockerCli command.Cli, backend api.Service, opts logsOptions, services []string) error {
project, name, err := opts.projectOrName(ctx, dockerCli, services...) project, name, err := opts.projectOrName(dockerCli, services...)
if err != nil { if err != nil {
return err return err
} }
// exclude services configured to ignore output (attach: false), until explicitly selected
if project != nil && len(services) == 0 {
for n, service := range project.Services {
if service.Attach == nil || *service.Attach {
services = append(services, n)
}
}
}
consumer := formatter.NewLogConsumer(ctx, dockerCli.Out(), dockerCli.Err(), !opts.noColor, !opts.noPrefix, false) consumer := formatter.NewLogConsumer(ctx, dockerCli.Out(), dockerCli.Err(), !opts.noColor, !opts.noPrefix, false)
return backend.Logs(ctx, name, consumer, api.LogOptions{ return backend.Logs(ctx, name, consumer, api.LogOptions{
Project: project, Project: project,

View File

@ -17,22 +17,10 @@
package compose package compose
import ( import (
"context"
"fmt" "fmt"
"io"
"os"
"slices"
"sort"
"strings"
"text/tabwriter"
"github.com/compose-spec/compose-go/v2/cli"
"github.com/compose-spec/compose-go/v2/template"
"github.com/compose-spec/compose-go/v2/types" "github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli/command" "github.com/docker/compose/v2/pkg/utils"
"github.com/docker/compose/v2/internal/tracing"
ui "github.com/docker/compose/v2/pkg/progress"
"github.com/docker/compose/v2/pkg/prompt"
) )
func applyPlatforms(project *types.Project, buildForSinglePlatform bool) error { func applyPlatforms(project *types.Project, buildForSinglePlatform bool) error {
@ -44,7 +32,7 @@ func applyPlatforms(project *types.Project, buildForSinglePlatform bool) error {
// default platform only applies if the service doesn't specify // default platform only applies if the service doesn't specify
if defaultPlatform != "" && service.Platform == "" { if defaultPlatform != "" && service.Platform == "" {
if len(service.Build.Platforms) > 0 && !slices.Contains(service.Build.Platforms, defaultPlatform) { if len(service.Build.Platforms) > 0 && !utils.StringContains(service.Build.Platforms, defaultPlatform) {
return fmt.Errorf("service %q build.platforms does not support value set by DOCKER_DEFAULT_PLATFORM: %s", name, defaultPlatform) return fmt.Errorf("service %q build.platforms does not support value set by DOCKER_DEFAULT_PLATFORM: %s", name, defaultPlatform)
} }
service.Platform = defaultPlatform service.Platform = defaultPlatform
@ -52,7 +40,7 @@ func applyPlatforms(project *types.Project, buildForSinglePlatform bool) error {
if service.Platform != "" { if service.Platform != "" {
if len(service.Build.Platforms) > 0 { if len(service.Build.Platforms) > 0 {
if !slices.Contains(service.Build.Platforms, service.Platform) { if !utils.StringContains(service.Build.Platforms, service.Platform) {
return fmt.Errorf("service %q build configuration does not support platform: %s", name, service.Platform) return fmt.Errorf("service %q build configuration does not support platform: %s", name, service.Platform)
} }
} }
@ -84,208 +72,3 @@ func applyPlatforms(project *types.Project, buildForSinglePlatform bool) error {
} }
return nil return nil
} }
// isRemoteConfig checks if the main compose file is from a remote source (OCI or Git)
func isRemoteConfig(dockerCli command.Cli, options buildOptions) bool {
if len(options.ConfigPaths) == 0 {
return false
}
remoteLoaders := options.remoteLoaders(dockerCli)
for _, loader := range remoteLoaders {
if loader.Accept(options.ConfigPaths[0]) {
return true
}
}
return false
}
// checksForRemoteStack handles environment variable prompts for remote configurations
func checksForRemoteStack(ctx context.Context, dockerCli command.Cli, project *types.Project, options buildOptions, assumeYes bool, cmdEnvs []string) error {
if !isRemoteConfig(dockerCli, options) {
return nil
}
if metrics, ok := ctx.Value(tracing.MetricsKey{}).(tracing.Metrics); ok && metrics.CountIncludesRemote > 0 {
if err := confirmRemoteIncludes(dockerCli, options, assumeYes); err != nil {
return err
}
}
displayLocationRemoteStack(dockerCli, project, options)
return promptForInterpolatedVariables(ctx, dockerCli, options.ProjectOptions, assumeYes, cmdEnvs)
}
// Prepare the values map and collect all variables info
type varInfo struct {
name string
value string
source string
required bool
defaultValue string
}
// promptForInterpolatedVariables displays all variables and their values at once,
// then prompts for confirmation
func promptForInterpolatedVariables(ctx context.Context, dockerCli command.Cli, projectOptions *ProjectOptions, assumeYes bool, cmdEnvs []string) error {
if assumeYes {
return nil
}
varsInfo, noVariables, err := extractInterpolationVariablesFromModel(ctx, dockerCli, projectOptions, cmdEnvs)
if err != nil {
return err
}
if noVariables {
return nil
}
displayInterpolationVariables(dockerCli.Out(), varsInfo)
// Prompt for confirmation
userInput := prompt.NewPrompt(dockerCli.In(), dockerCli.Out())
msg := "\nDo you want to proceed with these variables? [Y/n]: "
confirmed, err := userInput.Confirm(msg, true)
if err != nil {
return err
}
if !confirmed {
return fmt.Errorf("operation cancelled by user")
}
return nil
}
func extractInterpolationVariablesFromModel(ctx context.Context, dockerCli command.Cli, projectOptions *ProjectOptions, cmdEnvs []string) ([]varInfo, bool, error) {
cmdEnvMap := extractEnvCLIDefined(cmdEnvs)
// Create a model without interpolation to extract variables
opts := configOptions{
noInterpolate: true,
ProjectOptions: projectOptions,
}
model, err := opts.ToModel(ctx, dockerCli, nil, cli.WithoutEnvironmentResolution)
if err != nil {
return nil, false, err
}
// Extract variables that need interpolation
variables := template.ExtractVariables(model, template.DefaultPattern)
if len(variables) == 0 {
return nil, true, nil
}
var varsInfo []varInfo
proposedValues := make(map[string]string)
for name, variable := range variables {
info := varInfo{
name: name,
required: variable.Required,
defaultValue: variable.DefaultValue,
}
// Determine value and source based on priority
if value, exists := cmdEnvMap[name]; exists {
info.value = value
info.source = "command-line"
proposedValues[name] = value
} else if value, exists := os.LookupEnv(name); exists {
info.value = value
info.source = "environment"
proposedValues[name] = value
} else if variable.DefaultValue != "" {
info.value = variable.DefaultValue
info.source = "compose file"
proposedValues[name] = variable.DefaultValue
} else {
info.value = "<unset>"
info.source = "none"
}
varsInfo = append(varsInfo, info)
}
return varsInfo, false, nil
}
func extractEnvCLIDefined(cmdEnvs []string) map[string]string {
// Parse command-line environment variables
cmdEnvMap := make(map[string]string)
for _, env := range cmdEnvs {
parts := strings.SplitN(env, "=", 2)
if len(parts) == 2 {
cmdEnvMap[parts[0]] = parts[1]
}
}
return cmdEnvMap
}
func displayInterpolationVariables(writer io.Writer, varsInfo []varInfo) {
// Display all variables in a table format
_, _ = fmt.Fprintln(writer, "\nFound the following variables in configuration:")
w := tabwriter.NewWriter(writer, 0, 0, 3, ' ', 0)
_, _ = fmt.Fprintln(w, "VARIABLE\tVALUE\tSOURCE\tREQUIRED\tDEFAULT")
sort.Slice(varsInfo, func(a, b int) bool {
return varsInfo[a].name < varsInfo[b].name
})
for _, info := range varsInfo {
required := "no"
if info.required {
required = "yes"
}
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n",
info.name,
info.value,
info.source,
required,
info.defaultValue,
)
}
_ = w.Flush()
}
func displayLocationRemoteStack(dockerCli command.Cli, project *types.Project, options buildOptions) {
mainComposeFile := options.ProjectOptions.ConfigPaths[0] //nolint:staticcheck
if ui.Mode != ui.ModeQuiet && ui.Mode != ui.ModeJSON {
_, _ = fmt.Fprintf(dockerCli.Out(), "Your compose stack %q is stored in %q\n", mainComposeFile, project.WorkingDir)
}
}
func confirmRemoteIncludes(dockerCli command.Cli, options buildOptions, assumeYes bool) error {
if assumeYes {
return nil
}
var remoteIncludes []string
remoteLoaders := options.ProjectOptions.remoteLoaders(dockerCli) //nolint:staticcheck
for _, cf := range options.ProjectOptions.ConfigPaths { //nolint:staticcheck
for _, loader := range remoteLoaders {
if loader.Accept(cf) {
remoteIncludes = append(remoteIncludes, cf)
break
}
}
}
if len(remoteIncludes) == 0 {
return nil
}
_, _ = fmt.Fprintln(dockerCli.Out(), "\nWarning: This Compose project includes files from remote sources:")
for _, include := range remoteIncludes {
_, _ = fmt.Fprintf(dockerCli.Out(), " - %s\n", include)
}
_, _ = fmt.Fprintln(dockerCli.Out(), "\nRemote includes could potentially be malicious. Make sure you trust the source.")
msg := "Do you want to continue? [y/N]: "
confirmed, err := prompt.NewPrompt(dockerCli.In(), dockerCli.Out()).Confirm(msg, false)
if err != nil {
return err
}
if !confirmed {
return fmt.Errorf("operation cancelled by user")
}
return nil
}

View File

@ -17,20 +17,10 @@
package compose package compose
import ( import (
"bytes"
"context"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"testing" "testing"
"github.com/compose-spec/compose-go/v2/types" "github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli/streams"
"github.com/docker/compose/v2/pkg/mocks"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
) )
func TestApplyPlatforms_InferFromRuntime(t *testing.T) { func TestApplyPlatforms_InferFromRuntime(t *testing.T) {
@ -138,257 +128,3 @@ func TestApplyPlatforms_UnsupportedPlatform(t *testing.T) {
`service "test" build.platforms does not support value set by DOCKER_DEFAULT_PLATFORM: commodore/64`) `service "test" build.platforms does not support value set by DOCKER_DEFAULT_PLATFORM: commodore/64`)
}) })
} }
func TestIsRemoteConfig(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cli := mocks.NewMockCli(ctrl)
tests := []struct {
name string
configPaths []string
want bool
}{
{
name: "empty config paths",
configPaths: []string{},
want: false,
},
{
name: "local file",
configPaths: []string{"docker-compose.yaml"},
want: false,
},
{
name: "OCI reference",
configPaths: []string{"oci://registry.example.com/stack:latest"},
want: true,
},
{
name: "GIT reference",
configPaths: []string{"git://github.com/user/repo.git"},
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
opts := buildOptions{
ProjectOptions: &ProjectOptions{
ConfigPaths: tt.configPaths,
},
}
got := isRemoteConfig(cli, opts)
require.Equal(t, tt.want, got)
})
}
}
func TestDisplayLocationRemoteStack(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cli := mocks.NewMockCli(ctrl)
buf := new(bytes.Buffer)
cli.EXPECT().Out().Return(streams.NewOut(buf)).AnyTimes()
project := &types.Project{
Name: "test-project",
WorkingDir: "/tmp/test",
}
options := buildOptions{
ProjectOptions: &ProjectOptions{
ConfigPaths: []string{"oci://registry.example.com/stack:latest"},
},
}
displayLocationRemoteStack(cli, project, options)
output := buf.String()
require.Equal(t, output, fmt.Sprintf("Your compose stack %q is stored in %q\n", "oci://registry.example.com/stack:latest", "/tmp/test"))
}
func TestDisplayInterpolationVariables(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
// Create a temporary directory for the test
tmpDir, err := os.MkdirTemp("", "compose-test")
require.NoError(t, err)
defer func() { _ = os.RemoveAll(tmpDir) }()
// Create a temporary compose file
composeContent := `
services:
app:
image: nginx
environment:
- TEST_VAR=${TEST_VAR:?required} # required with default
- API_KEY=${API_KEY:?} # required without default
- DEBUG=${DEBUG:-true} # optional with default
- UNSET_VAR # optional without default
`
composePath := filepath.Join(tmpDir, "docker-compose.yml")
err = os.WriteFile(composePath, []byte(composeContent), 0o644)
require.NoError(t, err)
buf := new(bytes.Buffer)
cli := mocks.NewMockCli(ctrl)
cli.EXPECT().Out().Return(streams.NewOut(buf)).AnyTimes()
// Create ProjectOptions with the temporary compose file
projectOptions := &ProjectOptions{
ConfigPaths: []string{composePath},
}
// Set up the context with necessary environment variables
ctx := context.Background()
_ = os.Setenv("TEST_VAR", "test-value")
_ = os.Setenv("API_KEY", "123456")
defer func() {
_ = os.Unsetenv("TEST_VAR")
_ = os.Unsetenv("API_KEY")
}()
// Extract variables from the model
info, noVariables, err := extractInterpolationVariablesFromModel(ctx, cli, projectOptions, []string{})
require.NoError(t, err)
require.False(t, noVariables)
// Display the variables
displayInterpolationVariables(cli.Out(), info)
// Expected output format with proper spacing
expected := "\nFound the following variables in configuration:\n" +
"VARIABLE VALUE SOURCE REQUIRED DEFAULT\n" +
"API_KEY 123456 environment yes \n" +
"DEBUG true compose file no true\n" +
"TEST_VAR test-value environment yes \n"
// Normalize spaces and newlines for comparison
normalizeSpaces := func(s string) string {
// Replace multiple spaces with a single space
s = strings.Join(strings.Fields(strings.TrimSpace(s)), " ")
return s
}
actualOutput := buf.String()
// Compare normalized strings
require.Equal(t,
normalizeSpaces(expected),
normalizeSpaces(actualOutput),
"\nExpected:\n%s\nGot:\n%s", expected, actualOutput)
}
func TestConfirmRemoteIncludes(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cli := mocks.NewMockCli(ctrl)
tests := []struct {
name string
opts buildOptions
assumeYes bool
userInput string
wantErr bool
errMessage string
wantPrompt bool
wantOutput string
}{
{
name: "no remote includes",
opts: buildOptions{
ProjectOptions: &ProjectOptions{
ConfigPaths: []string{
"docker-compose.yaml",
"./local/path/compose.yaml",
},
},
},
assumeYes: false,
wantErr: false,
wantPrompt: false,
},
{
name: "assume yes with remote includes",
opts: buildOptions{
ProjectOptions: &ProjectOptions{
ConfigPaths: []string{
"oci://registry.example.com/stack:latest",
"git://github.com/user/repo.git",
},
},
},
assumeYes: true,
wantErr: false,
wantPrompt: false,
},
{
name: "user confirms remote includes",
opts: buildOptions{
ProjectOptions: &ProjectOptions{
ConfigPaths: []string{
"oci://registry.example.com/stack:latest",
"git://github.com/user/repo.git",
},
},
},
assumeYes: false,
userInput: "y\n",
wantErr: false,
wantPrompt: true,
wantOutput: "\nWarning: This Compose project includes files from remote sources:\n" +
" - oci://registry.example.com/stack:latest\n" +
" - git://github.com/user/repo.git\n" +
"\nRemote includes could potentially be malicious. Make sure you trust the source.\n" +
"Do you want to continue? [y/N]: ",
},
{
name: "user rejects remote includes",
opts: buildOptions{
ProjectOptions: &ProjectOptions{
ConfigPaths: []string{
"oci://registry.example.com/stack:latest",
},
},
},
assumeYes: false,
userInput: "n\n",
wantErr: true,
errMessage: "operation cancelled by user",
wantPrompt: true,
wantOutput: "\nWarning: This Compose project includes files from remote sources:\n" +
" - oci://registry.example.com/stack:latest\n" +
"\nRemote includes could potentially be malicious. Make sure you trust the source.\n" +
"Do you want to continue? [y/N]: ",
},
}
buf := new(bytes.Buffer)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cli.EXPECT().Out().Return(streams.NewOut(buf)).AnyTimes()
if tt.wantPrompt {
inbuf := io.NopCloser(bytes.NewBufferString(tt.userInput))
cli.EXPECT().In().Return(streams.NewIn(inbuf)).AnyTimes()
}
err := confirmRemoteIncludes(cli, tt.opts, tt.assumeYes)
if tt.wantErr {
require.Error(t, err)
require.Equal(t, tt.errMessage, err.Error())
} else {
require.NoError(t, err)
}
if tt.wantOutput != "" {
require.Equal(t, tt.wantOutput, buf.String())
}
buf.Reset()
})
}
}

View File

@ -45,7 +45,7 @@ func pauseCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
} }
func runPause(ctx context.Context, dockerCli command.Cli, backend api.Service, opts pauseOptions, services []string) error { func runPause(ctx context.Context, dockerCli command.Cli, backend api.Service, opts pauseOptions, services []string) error {
project, name, err := opts.projectOrName(ctx, dockerCli, services...) project, name, err := opts.projectOrName(dockerCli, services...)
if err != nil { if err != nil {
return err return err
} }
@ -76,7 +76,7 @@ func unpauseCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Servic
} }
func runUnPause(ctx context.Context, dockerCli command.Cli, backend api.Service, opts unpauseOptions, services []string) error { func runUnPause(ctx context.Context, dockerCli command.Cli, backend api.Service, opts unpauseOptions, services []string) error {
project, name, err := opts.projectOrName(ctx, dockerCli, services...) project, name, err := opts.projectOrName(dockerCli, services...)
if err != nil { if err != nil {
return err return err
} }

View File

@ -41,7 +41,7 @@ func portCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
} }
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "port [OPTIONS] SERVICE PRIVATE_PORT", Use: "port [OPTIONS] SERVICE PRIVATE_PORT",
Short: "Print the public port for a port binding", Short: "Print the public port for a port binding.",
Args: cobra.MinimumNArgs(2), Args: cobra.MinimumNArgs(2),
PreRunE: Adapt(func(ctx context.Context, args []string) error { PreRunE: Adapt(func(ctx context.Context, args []string) error {
port, err := strconv.ParseUint(args[1], 10, 16) port, err := strconv.ParseUint(args[1], 10, 16)
@ -58,12 +58,12 @@ func portCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
ValidArgsFunction: completeServiceNames(dockerCli, p), ValidArgsFunction: completeServiceNames(dockerCli, p),
} }
cmd.Flags().StringVar(&opts.protocol, "protocol", "tcp", "tcp or udp") cmd.Flags().StringVar(&opts.protocol, "protocol", "tcp", "tcp or udp")
cmd.Flags().IntVar(&opts.index, "index", 0, "Index of the container if service has multiple replicas") cmd.Flags().IntVar(&opts.index, "index", 0, "index of the container if service has multiple replicas")
return cmd return cmd
} }
func runPort(ctx context.Context, dockerCli command.Cli, backend api.Service, opts portOptions, service string) error { func runPort(ctx context.Context, dockerCli command.Cli, backend api.Service, opts portOptions, service string) error {
projectName, err := opts.toProjectName(ctx, dockerCli) projectName, err := opts.toProjectName(dockerCli)
if err != nil { if err != nil {
return err return err
} }
@ -75,6 +75,6 @@ func runPort(ctx context.Context, dockerCli command.Cli, backend api.Service, op
return err return err
} }
_, _ = fmt.Fprintf(dockerCli.Out(), "%s:%d\n", ip, port) fmt.Fprintf(dockerCli.Out(), "%s:%d\n", ip, port)
return nil return nil
} }

View File

@ -20,12 +20,12 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"slices"
"sort" "sort"
"strings" "strings"
"github.com/docker/compose/v2/cmd/formatter" "github.com/docker/compose/v2/cmd/formatter"
"github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/utils"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
cliformatter "github.com/docker/cli/cli/command/formatter" cliformatter "github.com/docker/cli/cli/command/formatter"
@ -81,7 +81,7 @@ func psCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *c
} }
flags := psCmd.Flags() flags := psCmd.Flags()
flags.StringVar(&opts.Format, "format", "table", cliflags.FormatHelp) flags.StringVar(&opts.Format, "format", "table", cliflags.FormatHelp)
flags.StringVar(&opts.Filter, "filter", "", "Filter services by a property (supported filters: status)") flags.StringVar(&opts.Filter, "filter", "", "Filter services by a property (supported filters: status).")
flags.StringArrayVar(&opts.Status, "status", []string{}, "Filter services by status. Values: [paused | restarting | removing | running | dead | created | exited]") flags.StringArrayVar(&opts.Status, "status", []string{}, "Filter services by status. Values: [paused | restarting | removing | running | dead | created | exited]")
flags.BoolVarP(&opts.Quiet, "quiet", "q", false, "Only display IDs") flags.BoolVarP(&opts.Quiet, "quiet", "q", false, "Only display IDs")
flags.BoolVar(&opts.Services, "services", false, "Display services") flags.BoolVar(&opts.Services, "services", false, "Display services")
@ -92,7 +92,7 @@ func psCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *c
} }
func runPs(ctx context.Context, dockerCli command.Cli, backend api.Service, services []string, opts psOptions) error { func runPs(ctx context.Context, dockerCli command.Cli, backend api.Service, services []string, opts psOptions) error {
project, name, err := opts.projectOrName(ctx, dockerCli, services...) project, name, err := opts.projectOrName(dockerCli, services...)
if err != nil { if err != nil {
return err return err
} }
@ -101,7 +101,7 @@ func runPs(ctx context.Context, dockerCli command.Cli, backend api.Service, serv
names := project.ServiceNames() names := project.ServiceNames()
if len(services) > 0 { if len(services) > 0 {
for _, service := range services { for _, service := range services {
if !slices.Contains(names, service) { if !utils.StringContains(names, service) {
return fmt.Errorf("no such service: %s", service) return fmt.Errorf("no such service: %s", service)
} }
} }
@ -113,7 +113,7 @@ func runPs(ctx context.Context, dockerCli command.Cli, backend api.Service, serv
containers, err := backend.Ps(ctx, name, api.PsOptions{ containers, err := backend.Ps(ctx, name, api.PsOptions{
Project: project, Project: project,
All: opts.All || len(opts.Status) != 0, All: opts.All,
Services: services, Services: services,
}) })
if err != nil { if err != nil {
@ -130,7 +130,7 @@ func runPs(ctx context.Context, dockerCli command.Cli, backend api.Service, serv
if opts.Quiet { if opts.Quiet {
for _, c := range containers { for _, c := range containers {
_, _ = fmt.Fprintln(dockerCli.Out(), c.ID) fmt.Fprintln(dockerCli.Out(), c.ID)
} }
return nil return nil
} }
@ -139,11 +139,11 @@ func runPs(ctx context.Context, dockerCli command.Cli, backend api.Service, serv
services := []string{} services := []string{}
for _, c := range containers { for _, c := range containers {
s := c.Service s := c.Service
if !slices.Contains(services, s) { if !utils.StringContains(services, s) {
services = append(services, s) services = append(services, s)
} }
} }
_, _ = fmt.Fprintln(dockerCli.Out(), strings.Join(services, "\n")) fmt.Fprintln(dockerCli.Out(), strings.Join(services, "\n"))
return nil return nil
} }

View File

@ -27,7 +27,6 @@ import (
"github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/mocks" "github.com/docker/compose/v2/pkg/mocks"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock" "go.uber.org/mock/gomock"
) )
@ -75,13 +74,13 @@ func TestPsTable(t *testing.T) {
cli.EXPECT().Out().Return(stdout).AnyTimes() cli.EXPECT().Out().Return(stdout).AnyTimes()
cli.EXPECT().ConfigFile().Return(&configfile.ConfigFile{}).AnyTimes() cli.EXPECT().ConfigFile().Return(&configfile.ConfigFile{}).AnyTimes()
err = runPs(ctx, cli, backend, nil, opts) err = runPs(ctx, cli, backend, nil, opts)
require.NoError(t, err) assert.NoError(t, err)
_, err = f.Seek(0, 0) _, err = f.Seek(0, 0)
require.NoError(t, err) assert.NoError(t, err)
output, err := os.ReadFile(out) output, err := os.ReadFile(out)
require.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, string(output), "8080/tcp, 8443/tcp") assert.Contains(t, string(output), "8080/tcp, 8443/tcp")
} }

View File

@ -18,22 +18,17 @@ package compose
import ( import (
"context" "context"
"errors"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/pkg/api"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/docker/compose/v2/pkg/api"
) )
type publishOptions struct { type publishOptions struct {
*ProjectOptions *ProjectOptions
resolveImageDigests bool resolveImageDigests bool
ociVersion string ociVersion string
withEnvironment bool
assumeYes bool
} }
func publishCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command { func publishCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
@ -41,44 +36,27 @@ func publishCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Servic
ProjectOptions: p, ProjectOptions: p,
} }
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "publish [OPTIONS] REPOSITORY[:TAG]", Use: "publish [OPTIONS] [REPOSITORY]",
Short: "Publish compose application", Short: "Publish compose application",
RunE: Adapt(func(ctx context.Context, args []string) error { RunE: Adapt(func(ctx context.Context, args []string) error {
return runPublish(ctx, dockerCli, backend, opts, args[0]) return runPublish(ctx, dockerCli, backend, opts, args[0])
}), }),
Args: cli.ExactArgs(1), Args: cobra.ExactArgs(1),
} }
flags := cmd.Flags() flags := cmd.Flags()
flags.BoolVar(&opts.resolveImageDigests, "resolve-image-digests", false, "Pin image tags to digests") flags.BoolVar(&opts.resolveImageDigests, "resolve-image-digests", false, "Pin image tags to digests.")
flags.StringVar(&opts.ociVersion, "oci-version", "", "OCI image/artifact specification version (automatically determined by default)") flags.StringVar(&opts.ociVersion, "oci-version", "", "OCI Image/Artifact specification version (automatically determined by default)")
flags.BoolVar(&opts.withEnvironment, "with-env", false, "Include environment variables in the published OCI artifact")
flags.BoolVarP(&opts.assumeYes, "yes", "y", false, `Assume "yes" as answer to all prompts`)
flags.SetNormalizeFunc(func(f *pflag.FlagSet, name string) pflag.NormalizedName {
// assumeYes was introduced by mistake as `--y`
if name == "y" {
logrus.Warn("--y is deprecated, please use --yes instead")
name = "yes"
}
return pflag.NormalizedName(name)
})
return cmd return cmd
} }
func runPublish(ctx context.Context, dockerCli command.Cli, backend api.Service, opts publishOptions, repository string) error { func runPublish(ctx context.Context, dockerCli command.Cli, backend api.Service, opts publishOptions, repository string) error {
project, metrics, err := opts.ToProject(ctx, dockerCli, nil) project, err := opts.ToProject(dockerCli, nil)
if err != nil { if err != nil {
return err return err
} }
if metrics.CountIncludesLocal > 0 {
return errors.New("cannot publish compose file with local includes")
}
return backend.Publish(ctx, project, repository, api.PublishOptions{ return backend.Publish(ctx, project, repository, api.PublishOptions{
ResolveImageDigests: opts.resolveImageDigests, ResolveImageDigests: opts.resolveImageDigests,
OCIVersion: api.OCIVersion(opts.ociVersion), OCIVersion: api.OCIVersion(opts.ociVersion),
WithEnvironment: opts.withEnvironment,
AssumeYes: opts.assumeYes,
}) })
} }

View File

@ -21,7 +21,6 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/compose-spec/compose-go/v2/cli"
"github.com/compose-spec/compose-go/v2/types" "github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/morikuni/aec" "github.com/morikuni/aec"
@ -49,30 +48,27 @@ func pullCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "pull [OPTIONS] [SERVICE...]", Use: "pull [OPTIONS] [SERVICE...]",
Short: "Pull service images", Short: "Pull service images",
PreRunE: func(cmd *cobra.Command, args []string) error { PreRunE: Adapt(func(ctx context.Context, args []string) error {
if cmd.Flags().Changed("no-parallel") { if opts.noParallel {
fmt.Fprint(os.Stderr, aec.Apply("option '--no-parallel' is DEPRECATED and will be ignored.\n", aec.RedF)) fmt.Fprint(os.Stderr, aec.Apply("option '--no-parallel' is DEPRECATED and will be ignored.\n", aec.RedF))
} }
if cmd.Flags().Changed("parallel") {
fmt.Fprint(os.Stderr, aec.Apply("option '--parallel' is DEPRECATED and will be ignored.\n", aec.RedF))
}
return nil return nil
}, }),
RunE: Adapt(func(ctx context.Context, args []string) error { RunE: Adapt(func(ctx context.Context, args []string) error {
return runPull(ctx, dockerCli, backend, opts, args) return runPull(ctx, dockerCli, backend, opts, args)
}), }),
ValidArgsFunction: completeServiceNames(dockerCli, p), ValidArgsFunction: completeServiceNames(dockerCli, p),
} }
flags := cmd.Flags() flags := cmd.Flags()
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Pull without printing progress information") flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Pull without printing progress information.")
cmd.Flags().BoolVar(&opts.includeDeps, "include-deps", false, "Also pull services declared as dependencies") cmd.Flags().BoolVar(&opts.includeDeps, "include-deps", false, "Also pull services declared as dependencies.")
cmd.Flags().BoolVar(&opts.parallel, "parallel", true, "DEPRECATED pull multiple images in parallel") cmd.Flags().BoolVar(&opts.parallel, "parallel", true, "DEPRECATED pull multiple images in parallel.")
flags.MarkHidden("parallel") //nolint:errcheck flags.MarkHidden("parallel") //nolint:errcheck
cmd.Flags().BoolVar(&opts.noParallel, "no-parallel", true, "DEPRECATED disable parallel pulling") cmd.Flags().BoolVar(&opts.parallel, "no-parallel", true, "DEPRECATED disable parallel pulling.")
flags.MarkHidden("no-parallel") //nolint:errcheck flags.MarkHidden("no-parallel") //nolint:errcheck
cmd.Flags().BoolVar(&opts.ignorePullFailures, "ignore-pull-failures", false, "Pull what it can and ignores images with pull failures") cmd.Flags().BoolVar(&opts.ignorePullFailures, "ignore-pull-failures", false, "Pull what it can and ignores images with pull failures.")
cmd.Flags().BoolVar(&opts.noBuildable, "ignore-buildable", false, "Ignore images that can be built") cmd.Flags().BoolVar(&opts.noBuildable, "ignore-buildable", false, "Ignore images that can be built.")
cmd.Flags().StringVar(&opts.policy, "policy", "", `Apply pull policy ("missing"|"always")`) cmd.Flags().StringVar(&opts.policy, "policy", "", `Apply pull policy ("missing"|"always").`)
return cmd return cmd
} }
@ -98,7 +94,7 @@ func (opts pullOptions) apply(project *types.Project, services []string) (*types
} }
func runPull(ctx context.Context, dockerCli command.Cli, backend api.Service, opts pullOptions, services []string) error { func runPull(ctx context.Context, dockerCli command.Cli, backend api.Service, opts pullOptions, services []string) error {
project, _, err := opts.ToProject(ctx, dockerCli, services, cli.WithoutEnvironmentResolution) project, err := opts.ToProject(dockerCli, services)
if err != nil { if err != nil {
return err return err
} }

View File

@ -54,7 +54,7 @@ func pushCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
} }
func runPush(ctx context.Context, dockerCli command.Cli, backend api.Service, opts pushOptions, services []string) error { func runPush(ctx context.Context, dockerCli command.Cli, backend api.Service, opts pushOptions, services []string) error {
project, _, err := opts.ToProject(ctx, dockerCli, services) project, err := opts.ToProject(dockerCli, services)
if err != nil { if err != nil {
return err return err
} }

View File

@ -60,7 +60,7 @@ Any data which is not in a volume will be lost.`,
} }
func runRemove(ctx context.Context, dockerCli command.Cli, backend api.Service, opts removeOptions, services []string) error { func runRemove(ctx context.Context, dockerCli command.Cli, backend api.Service, opts removeOptions, services []string) error {
project, name, err := opts.projectOrName(ctx, dockerCli, services...) project, name, err := opts.projectOrName(dockerCli, services...)
if err != nil { if err != nil {
return err return err
} }

View File

@ -50,13 +50,13 @@ func restartCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Servic
} }
flags := restartCmd.Flags() flags := restartCmd.Flags()
flags.IntVarP(&opts.timeout, "timeout", "t", 0, "Specify a shutdown timeout in seconds") flags.IntVarP(&opts.timeout, "timeout", "t", 0, "Specify a shutdown timeout in seconds")
flags.BoolVar(&opts.noDeps, "no-deps", false, "Don't restart dependent services") flags.BoolVar(&opts.noDeps, "no-deps", false, "Don't restart dependent services.")
return restartCmd return restartCmd
} }
func runRestart(ctx context.Context, dockerCli command.Cli, backend api.Service, opts restartOptions, services []string) error { func runRestart(ctx context.Context, dockerCli command.Cli, backend api.Service, opts restartOptions, services []string) error {
project, name, err := opts.projectOrName(ctx, dockerCli) project, name, err := opts.projectOrName(dockerCli)
if err != nil { if err != nil {
return err return err
} }

View File

@ -19,10 +19,8 @@ package compose
import ( import (
"context" "context"
"fmt" "fmt"
"os"
"strings" "strings"
"github.com/compose-spec/compose-go/v2/dotenv"
"github.com/compose-spec/compose-go/v2/format" "github.com/compose-spec/compose-go/v2/format"
xprogress "github.com/moby/buildkit/util/progress/progressui" xprogress "github.com/moby/buildkit/util/progress/progressui"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -46,7 +44,6 @@ type runOptions struct {
Service string Service string
Command []string Command []string
environment []string environment []string
envFiles []string
Detach bool Detach bool
Remove bool Remove bool
noTty bool noTty bool
@ -66,8 +63,6 @@ type runOptions struct {
name string name string
noDeps bool noDeps bool
ignoreOrphans bool ignoreOrphans bool
removeOrphans bool
quiet bool
quietPull bool quietPull bool
} }
@ -120,29 +115,6 @@ func (options runOptions) apply(project *types.Project) (*types.Project, error)
return project, nil return project, nil
} }
func (options runOptions) getEnvironment(resolve func(string) (string, bool)) (types.Mapping, error) {
environment := types.NewMappingWithEquals(options.environment).Resolve(resolve).ToMapping()
for _, file := range options.envFiles {
f, err := os.Open(file)
if err != nil {
return nil, err
}
vars, err := dotenv.ParseWithLookup(f, func(k string) (string, bool) {
value, ok := environment[k]
return value, ok
})
if err != nil {
return nil, nil
}
for k, v := range vars {
if _, ok := environment[k]; !ok {
environment[k] = v
}
}
}
return environment, nil
}
func runCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command { func runCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
options := runOptions{ options := runOptions{
composeOptions: &composeOptions{ composeOptions: &composeOptions{
@ -157,7 +129,7 @@ func runCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *
} }
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "run [OPTIONS] SERVICE [COMMAND] [ARGS...]", Use: "run [OPTIONS] SERVICE [COMMAND] [ARGS...]",
Short: "Run a one-off command on a service", Short: "Run a one-off command on a service.",
Args: cobra.MinimumNArgs(1), Args: cobra.MinimumNArgs(1),
PreRunE: AdaptCmd(func(ctx context.Context, cmd *cobra.Command, args []string) error { PreRunE: AdaptCmd(func(ctx context.Context, cmd *cobra.Command, args []string) error {
options.Service = args[0] options.Service = args[0]
@ -181,24 +153,10 @@ func runCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *
options.noTty = !options.tty options.noTty = !options.tty
} }
} }
if options.quiet {
progress.Mode = progress.ModeQuiet
devnull, err := os.Open(os.DevNull)
if err != nil {
return err
}
os.Stdout = devnull
}
createOpts.pullChanged = cmd.Flags().Changed("pull")
return nil return nil
}), }),
RunE: Adapt(func(ctx context.Context, args []string) error { RunE: Adapt(func(ctx context.Context, args []string) error {
project, _, err := p.ToProject(ctx, dockerCli, []string{options.Service}, cgo.WithResolvedPaths(true), cgo.WithoutEnvironmentResolution) project, err := p.ToProject(dockerCli, []string{options.Service}, cgo.WithResolvedPaths(true), cgo.WithDiscardEnvFile)
if err != nil {
return err
}
project, err = project.WithServicesEnvironmentResolved(true)
if err != nil { if err != nil {
return err return err
} }
@ -215,30 +173,26 @@ func runCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *
flags := cmd.Flags() flags := cmd.Flags()
flags.BoolVarP(&options.Detach, "detach", "d", false, "Run container in background and print container ID") flags.BoolVarP(&options.Detach, "detach", "d", false, "Run container in background and print container ID")
flags.StringArrayVarP(&options.environment, "env", "e", []string{}, "Set environment variables") flags.StringArrayVarP(&options.environment, "env", "e", []string{}, "Set environment variables")
flags.StringArrayVar(&options.envFiles, "env-from-file", []string{}, "Set environment variables from file")
flags.StringArrayVarP(&options.labels, "label", "l", []string{}, "Add or override a label") flags.StringArrayVarP(&options.labels, "label", "l", []string{}, "Add or override a label")
flags.BoolVar(&options.Remove, "rm", false, "Automatically remove the container when it exits") flags.BoolVar(&options.Remove, "rm", false, "Automatically remove the container when it exits")
flags.BoolVarP(&options.noTty, "no-TTY", "T", !dockerCli.Out().IsTerminal(), "Disable pseudo-TTY allocation (default: auto-detected)") flags.BoolVarP(&options.noTty, "no-TTY", "T", !dockerCli.Out().IsTerminal(), "Disable pseudo-TTY allocation (default: auto-detected).")
flags.StringVar(&options.name, "name", "", "Assign a name to the container") flags.StringVar(&options.name, "name", "", "Assign a name to the container")
flags.StringVarP(&options.user, "user", "u", "", "Run as specified username or uid") flags.StringVarP(&options.user, "user", "u", "", "Run as specified username or uid")
flags.StringVarP(&options.workdir, "workdir", "w", "", "Working directory inside the container") flags.StringVarP(&options.workdir, "workdir", "w", "", "Working directory inside the container")
flags.StringVar(&options.entrypoint, "entrypoint", "", "Override the entrypoint of the image") flags.StringVar(&options.entrypoint, "entrypoint", "", "Override the entrypoint of the image")
flags.Var(&options.capAdd, "cap-add", "Add Linux capabilities") flags.Var(&options.capAdd, "cap-add", "Add Linux capabilities")
flags.Var(&options.capDrop, "cap-drop", "Drop Linux capabilities") flags.Var(&options.capDrop, "cap-drop", "Drop Linux capabilities")
flags.BoolVar(&options.noDeps, "no-deps", false, "Don't start linked services") flags.BoolVar(&options.noDeps, "no-deps", false, "Don't start linked services.")
flags.StringArrayVarP(&options.volumes, "volume", "v", []string{}, "Bind mount a volume") flags.StringArrayVarP(&options.volumes, "volume", "v", []string{}, "Bind mount a volume.")
flags.StringArrayVarP(&options.publish, "publish", "p", []string{}, "Publish a container's port(s) to the host") flags.StringArrayVarP(&options.publish, "publish", "p", []string{}, "Publish a container's port(s) to the host.")
flags.BoolVar(&options.useAliases, "use-aliases", false, "Use the service's network useAliases in the network(s) the container connects to") flags.BoolVar(&options.useAliases, "use-aliases", false, "Use the service's network useAliases in the network(s) the container connects to.")
flags.BoolVarP(&options.servicePorts, "service-ports", "P", false, "Run command with all service's ports enabled and mapped to the host") flags.BoolVarP(&options.servicePorts, "service-ports", "P", false, "Run command with all service's ports enabled and mapped to the host.")
flags.StringVar(&createOpts.Pull, "pull", "policy", `Pull image before running ("always"|"missing"|"never")`) flags.BoolVar(&options.quietPull, "quiet-pull", false, "Pull without printing progress information.")
flags.BoolVarP(&options.quiet, "quiet", "q", false, "Don't print anything to STDOUT") flags.BoolVar(&createOpts.Build, "build", false, "Build image before starting container.")
flags.BoolVar(&buildOpts.quiet, "quiet-build", false, "Suppress progress output from the build process") flags.BoolVar(&createOpts.removeOrphans, "remove-orphans", false, "Remove containers for services not defined in the Compose file.")
flags.BoolVar(&options.quietPull, "quiet-pull", false, "Pull without printing progress information")
flags.BoolVar(&createOpts.Build, "build", false, "Build image before starting container")
flags.BoolVar(&options.removeOrphans, "remove-orphans", false, "Remove containers for services not defined in the Compose file")
cmd.Flags().BoolVarP(&options.interactive, "interactive", "i", true, "Keep STDIN open even if not attached") cmd.Flags().BoolVarP(&options.interactive, "interactive", "i", true, "Keep STDIN open even if not attached.")
cmd.Flags().BoolVarP(&options.tty, "tty", "t", true, "Allocate a pseudo-TTY") cmd.Flags().BoolVarP(&options.tty, "tty", "t", true, "Allocate a pseudo-TTY.")
cmd.Flags().MarkHidden("tty") //nolint:errcheck cmd.Flags().MarkHidden("tty") //nolint:errcheck
flags.SetNormalizeFunc(normalizeRunFlags) flags.SetNormalizeFunc(normalizeRunFlags)
@ -267,7 +221,19 @@ func runRun(ctx context.Context, backend api.Service, project *types.Project, op
return err return err
} }
if err := checksForRemoteStack(ctx, dockerCli, project, buildOpts, createOpts.AssumeYes, []string{}); err != nil { err = progress.Run(ctx, func(ctx context.Context) error {
var buildForDeps *api.BuildOptions
if !createOpts.noBuild {
// allow dependencies needing build to be implicitly selected
bo, err := buildOpts.toAPIBuildOptions(nil)
if err != nil {
return err
}
buildForDeps = &bo
}
return startDependencies(ctx, backend, *project, buildForDeps, options.Service, options.ignoreOrphans)
}, dockerCli.Err())
if err != nil {
return err return err
} }
@ -282,26 +248,18 @@ func runRun(ctx context.Context, backend api.Service, project *types.Project, op
var buildForRun *api.BuildOptions var buildForRun *api.BuildOptions
if !createOpts.noBuild { if !createOpts.noBuild {
bo, err := buildOpts.toAPIBuildOptions(nil) // dependencies have already been started above, so only the service
// being run might need to be built at this point
bo, err := buildOpts.toAPIBuildOptions([]string{options.Service})
if err != nil { if err != nil {
return err return err
} }
buildForRun = &bo buildForRun = &bo
} }
environment, err := options.getEnvironment(project.Environment.Resolve)
if err != nil {
return err
}
// start container and attach to container streams // start container and attach to container streams
runOpts := api.RunOptions{ runOpts := api.RunOptions{
CreateOptions: api.CreateOptions{ Build: buildForRun,
Build: buildForRun,
RemoveOrphans: options.removeOrphans,
IgnoreOrphans: options.ignoreOrphans,
QuietPull: options.quietPull,
},
Name: options.name, Name: options.name,
Service: options.Service, Service: options.Service,
Command: options.Command, Command: options.Command,
@ -311,14 +269,15 @@ func runRun(ctx context.Context, backend api.Service, project *types.Project, op
Interactive: options.interactive, Interactive: options.interactive,
WorkingDir: options.workdir, WorkingDir: options.workdir,
User: options.user, User: options.user,
CapAdd: options.capAdd.GetSlice(), CapAdd: options.capAdd.GetAll(),
CapDrop: options.capDrop.GetSlice(), CapDrop: options.capDrop.GetAll(),
Environment: environment.Values(), Environment: options.environment,
Entrypoint: options.entrypointCmd, Entrypoint: options.entrypointCmd,
Labels: labels, Labels: labels,
UseNetworkAliases: options.useAliases, UseNetworkAliases: options.useAliases,
NoDeps: options.noDeps, NoDeps: options.noDeps,
Index: 0, Index: 0,
QuietPull: options.quietPull,
} }
for name, service := range project.Services { for name, service := range project.Services {
@ -338,3 +297,32 @@ func runRun(ctx context.Context, backend api.Service, project *types.Project, op
} }
return err return err
} }
func startDependencies(ctx context.Context, backend api.Service, project types.Project, buildOpts *api.BuildOptions, requestedServiceName string, ignoreOrphans bool) error {
dependencies := types.Services{}
var requestedService types.ServiceConfig
for name, service := range project.Services {
if name != requestedServiceName {
dependencies[name] = service
} else {
requestedService = service
}
}
project.Services = dependencies
project.DisabledServices[requestedServiceName] = requestedService
err := backend.Create(ctx, &project, api.CreateOptions{
Build: buildOpts,
IgnoreOrphans: ignoreOrphans,
})
if err != nil {
return err
}
if len(dependencies) > 0 {
return backend.Start(ctx, project.Name, api.StartOptions{
Project: &project,
})
}
return nil
}

View File

@ -19,13 +19,14 @@ package compose
import ( import (
"context" "context"
"fmt" "fmt"
"maps"
"slices"
"strconv" "strconv"
"strings" "strings"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/compose-spec/compose-go/v2/types"
"golang.org/x/exp/maps"
"github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/api"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -50,17 +51,17 @@ func scaleCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
} }
return runScale(ctx, dockerCli, backend, opts, serviceTuples) return runScale(ctx, dockerCli, backend, opts, serviceTuples)
}), }),
ValidArgsFunction: completeScaleArgs(dockerCli, p), ValidArgsFunction: completeServiceNames(dockerCli, p),
} }
flags := scaleCmd.Flags() flags := scaleCmd.Flags()
flags.BoolVar(&opts.noDeps, "no-deps", false, "Don't start linked services") flags.BoolVar(&opts.noDeps, "no-deps", false, "Don't start linked services.")
return scaleCmd return scaleCmd
} }
func runScale(ctx context.Context, dockerCli command.Cli, backend api.Service, opts scaleOptions, serviceReplicaTuples map[string]int) error { func runScale(ctx context.Context, dockerCli command.Cli, backend api.Service, opts scaleOptions, serviceReplicaTuples map[string]int) error {
services := slices.Sorted(maps.Keys(serviceReplicaTuples)) services := maps.Keys(serviceReplicaTuples)
project, _, err := opts.ToProject(ctx, dockerCli, services) project, err := opts.ToProject(dockerCli, services)
if err != nil { if err != nil {
return err return err
} }
@ -91,6 +92,7 @@ func parseServicesReplicasArgs(args []string) (map[string]int, error) {
return nil, fmt.Errorf("invalid scale specifier: %s", arg) return nil, fmt.Errorf("invalid scale specifier: %s", arg)
} }
intValue, err := strconv.Atoi(val) intValue, err := strconv.Atoi(val)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid scale specifier: can't parse replica value as int: %v", arg) return nil, fmt.Errorf("invalid scale specifier: can't parse replica value as int: %v", arg)
} }

View File

@ -44,7 +44,7 @@ func startCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
} }
func runStart(ctx context.Context, dockerCli command.Cli, backend api.Service, opts startOptions, services []string) error { func runStart(ctx context.Context, dockerCli command.Cli, backend api.Service, opts startOptions, services []string) error {
project, name, err := opts.projectOrName(ctx, dockerCli, services...) project, name, err := opts.projectOrName(dockerCli, services...)
if err != nil { if err != nil {
return err return err
} }

View File

@ -56,14 +56,14 @@ func statsCommand(p *ProjectOptions, dockerCli command.Cli) *cobra.Command {
'table TEMPLATE': Print output in table format using the given Go template 'table TEMPLATE': Print output in table format using the given Go template
'json': Print in JSON format 'json': Print in JSON format
'TEMPLATE': Print output using the given Go template. 'TEMPLATE': Print output using the given Go template.
Refer to https://docs.docker.com/engine/cli/formatting/ for more information about formatting output with templates`) Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates`)
flags.BoolVar(&opts.noStream, "no-stream", false, "Disable streaming stats and only pull the first result") flags.BoolVar(&opts.noStream, "no-stream", false, "Disable streaming stats and only pull the first result")
flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate output") flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate output")
return cmd return cmd
} }
func runStats(ctx context.Context, dockerCli command.Cli, opts statsOptions, service []string) error { func runStats(ctx context.Context, dockerCli command.Cli, opts statsOptions, service []string) error {
name, err := opts.ProjectOptions.toProjectName(ctx, dockerCli) name, err := opts.ProjectOptions.toProjectName(dockerCli)
if err != nil { if err != nil {
return err return err
} }

View File

@ -54,7 +54,7 @@ func stopCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
} }
func runStop(ctx context.Context, dockerCli command.Cli, backend api.Service, opts stopOptions, services []string) error { func runStop(ctx context.Context, dockerCli command.Cli, backend api.Service, opts stopOptions, services []string) error {
project, name, err := opts.projectOrName(ctx, dockerCli, services...) project, name, err := opts.projectOrName(dockerCli, services...)
if err != nil { if err != nil {
return err return err
} }

View File

@ -49,13 +49,8 @@ func topCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *
return topCmd return topCmd
} }
type (
topHeader map[string]int // maps a proc title to its output index
topEntries map[string]string
)
func runTop(ctx context.Context, dockerCli command.Cli, backend api.Service, opts topOptions, services []string) error { func runTop(ctx context.Context, dockerCli command.Cli, backend api.Service, opts topOptions, services []string) error {
projectName, err := opts.toProjectName(ctx, dockerCli) projectName, err := opts.toProjectName(dockerCli)
if err != nil { if err != nil {
return err return err
} }
@ -68,76 +63,30 @@ func runTop(ctx context.Context, dockerCli command.Cli, backend api.Service, opt
return containers[i].Name < containers[j].Name return containers[i].Name < containers[j].Name
}) })
header, entries := collectTop(containers)
return topPrint(dockerCli.Out(), header, entries)
}
func collectTop(containers []api.ContainerProcSummary) (topHeader, []topEntries) {
// map column name to its header (should keep working if backend.Top returns
// varying columns for different containers)
header := topHeader{"SERVICE": 0, "#": 1}
// assume one process per container and grow if needed
entries := make([]topEntries, 0, len(containers))
for _, container := range containers { for _, container := range containers {
for _, proc := range container.Processes { fmt.Fprintf(dockerCli.Out(), "%s\n", container.Name)
entry := topEntries{ err := psPrinter(dockerCli.Out(), func(w io.Writer) {
"SERVICE": container.Service, for _, proc := range container.Processes {
"#": container.Replica, info := []interface{}{}
} for _, p := range proc {
for i, title := range container.Titles { info = append(info, p)
if _, exists := header[title]; !exists {
header[title] = len(header)
} }
entry[title] = proc[i] _, _ = fmt.Fprintf(w, strings.Repeat("%s\t", len(info))+"\n", info...)
} }
entries = append(entries, entry) fmt.Fprintln(w)
},
container.Titles...)
if err != nil {
return err
} }
} }
return nil
// ensure CMD is the right-most column
if pos, ok := header["CMD"]; ok {
maxPos := pos
for h, i := range header {
if i > maxPos {
maxPos = i
}
if i > pos {
header[h] = i - 1
}
}
header["CMD"] = maxPos
}
return header, entries
} }
func topPrint(out io.Writer, headers topHeader, rows []topEntries) error { func psPrinter(out io.Writer, printer func(writer io.Writer), headers ...string) error {
if len(rows) == 0 { w := tabwriter.NewWriter(out, 5, 1, 3, ' ', 0)
return nil _, _ = fmt.Fprintln(w, strings.Join(headers, "\t"))
} printer(w)
w := tabwriter.NewWriter(out, 4, 1, 2, ' ', 0)
// write headers in the order we've encountered them
h := make([]string, len(headers))
for title, index := range headers {
h[index] = title
}
_, _ = fmt.Fprintln(w, strings.Join(h, "\t"))
for _, row := range rows {
// write proc data in header order
r := make([]string, len(headers))
for title, index := range headers {
if v, ok := row[title]; ok {
r[index] = v
} else {
r[index] = "-"
}
}
_, _ = fmt.Fprintln(w, strings.Join(r, "\t"))
}
return w.Flush() return w.Flush()
} }

View File

@ -1,329 +0,0 @@
/*
Copyright 2024 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"bytes"
"strings"
"testing"
"github.com/docker/compose/v2/pkg/api"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var topTestCases = []struct {
name string
titles []string
procs [][]string
header topHeader
entries []topEntries
output string
}{
{
name: "noprocs",
titles: []string{"UID", "PID", "PPID", "C", "STIME", "TTY", "TIME", "CMD"},
procs: [][]string{},
header: topHeader{"SERVICE": 0, "#": 1},
entries: []topEntries{},
output: "",
},
{
name: "simple",
titles: []string{"UID", "PID", "PPID", "C", "STIME", "TTY", "TIME", "CMD"},
procs: [][]string{{"root", "1", "1", "0", "12:00", "?", "00:00:01", "/entrypoint"}},
header: topHeader{
"SERVICE": 0,
"#": 1,
"UID": 2,
"PID": 3,
"PPID": 4,
"C": 5,
"STIME": 6,
"TTY": 7,
"TIME": 8,
"CMD": 9,
},
entries: []topEntries{
{
"SERVICE": "simple",
"#": "1",
"UID": "root",
"PID": "1",
"PPID": "1",
"C": "0",
"STIME": "12:00",
"TTY": "?",
"TIME": "00:00:01",
"CMD": "/entrypoint",
},
},
output: trim(`
SERVICE # UID PID PPID C STIME TTY TIME CMD
simple 1 root 1 1 0 12:00 ? 00:00:01 /entrypoint
`),
},
{
name: "noppid",
titles: []string{"UID", "PID", "C", "STIME", "TTY", "TIME", "CMD"},
procs: [][]string{{"root", "1", "0", "12:00", "?", "00:00:02", "/entrypoint"}},
header: topHeader{
"SERVICE": 0,
"#": 1,
"UID": 2,
"PID": 3,
"C": 4,
"STIME": 5,
"TTY": 6,
"TIME": 7,
"CMD": 8,
},
entries: []topEntries{
{
"SERVICE": "noppid",
"#": "1",
"UID": "root",
"PID": "1",
"C": "0",
"STIME": "12:00",
"TTY": "?",
"TIME": "00:00:02",
"CMD": "/entrypoint",
},
},
output: trim(`
SERVICE # UID PID C STIME TTY TIME CMD
noppid 1 root 1 0 12:00 ? 00:00:02 /entrypoint
`),
},
{
name: "extra-hdr",
titles: []string{"UID", "GID", "PID", "PPID", "C", "STIME", "TTY", "TIME", "CMD"},
procs: [][]string{{"root", "1", "1", "1", "0", "12:00", "?", "00:00:03", "/entrypoint"}},
header: topHeader{
"SERVICE": 0,
"#": 1,
"UID": 2,
"GID": 3,
"PID": 4,
"PPID": 5,
"C": 6,
"STIME": 7,
"TTY": 8,
"TIME": 9,
"CMD": 10,
},
entries: []topEntries{
{
"SERVICE": "extra-hdr",
"#": "1",
"UID": "root",
"GID": "1",
"PID": "1",
"PPID": "1",
"C": "0",
"STIME": "12:00",
"TTY": "?",
"TIME": "00:00:03",
"CMD": "/entrypoint",
},
},
output: trim(`
SERVICE # UID GID PID PPID C STIME TTY TIME CMD
extra-hdr 1 root 1 1 1 0 12:00 ? 00:00:03 /entrypoint
`),
},
{
name: "multiple",
titles: []string{"UID", "PID", "PPID", "C", "STIME", "TTY", "TIME", "CMD"},
procs: [][]string{
{"root", "1", "1", "0", "12:00", "?", "00:00:04", "/entrypoint"},
{"root", "123", "1", "0", "12:00", "?", "00:00:42", "sleep infinity"},
},
header: topHeader{
"SERVICE": 0,
"#": 1,
"UID": 2,
"PID": 3,
"PPID": 4,
"C": 5,
"STIME": 6,
"TTY": 7,
"TIME": 8,
"CMD": 9,
},
entries: []topEntries{
{
"SERVICE": "multiple",
"#": "1",
"UID": "root",
"PID": "1",
"PPID": "1",
"C": "0",
"STIME": "12:00",
"TTY": "?",
"TIME": "00:00:04",
"CMD": "/entrypoint",
},
{
"SERVICE": "multiple",
"#": "1",
"UID": "root",
"PID": "123",
"PPID": "1",
"C": "0",
"STIME": "12:00",
"TTY": "?",
"TIME": "00:00:42",
"CMD": "sleep infinity",
},
},
output: trim(`
SERVICE # UID PID PPID C STIME TTY TIME CMD
multiple 1 root 1 1 0 12:00 ? 00:00:04 /entrypoint
multiple 1 root 123 1 0 12:00 ? 00:00:42 sleep infinity
`),
},
}
// TestRunTopCore only tests the core functionality of runTop: formatting
// and printing of the output of (api.Service).Top().
func TestRunTopCore(t *testing.T) {
t.Parallel()
all := []api.ContainerProcSummary{}
for _, tc := range topTestCases {
summary := api.ContainerProcSummary{
Name: "not used",
Titles: tc.titles,
Processes: tc.procs,
Service: tc.name,
Replica: "1",
}
all = append(all, summary)
t.Run(tc.name, func(t *testing.T) {
header, entries := collectTop([]api.ContainerProcSummary{summary})
assert.Equal(t, tc.header, header)
assert.Equal(t, tc.entries, entries)
var buf bytes.Buffer
err := topPrint(&buf, header, entries)
require.NoError(t, err)
assert.Equal(t, tc.output, buf.String())
})
}
t.Run("all", func(t *testing.T) {
header, entries := collectTop(all)
assert.Equal(t, topHeader{
"SERVICE": 0,
"#": 1,
"UID": 2,
"PID": 3,
"PPID": 4,
"C": 5,
"STIME": 6,
"TTY": 7,
"TIME": 8,
"GID": 9,
"CMD": 10,
}, header)
assert.Equal(t, []topEntries{
{
"SERVICE": "simple",
"#": "1",
"UID": "root",
"PID": "1",
"PPID": "1",
"C": "0",
"STIME": "12:00",
"TTY": "?",
"TIME": "00:00:01",
"CMD": "/entrypoint",
}, {
"SERVICE": "noppid",
"#": "1",
"UID": "root",
"PID": "1",
"C": "0",
"STIME": "12:00",
"TTY": "?",
"TIME": "00:00:02",
"CMD": "/entrypoint",
}, {
"SERVICE": "extra-hdr",
"#": "1",
"UID": "root",
"GID": "1",
"PID": "1",
"PPID": "1",
"C": "0",
"STIME": "12:00",
"TTY": "?",
"TIME": "00:00:03",
"CMD": "/entrypoint",
}, {
"SERVICE": "multiple",
"#": "1",
"UID": "root",
"PID": "1",
"PPID": "1",
"C": "0",
"STIME": "12:00",
"TTY": "?",
"TIME": "00:00:04",
"CMD": "/entrypoint",
}, {
"SERVICE": "multiple",
"#": "1",
"UID": "root",
"PID": "123",
"PPID": "1",
"C": "0",
"STIME": "12:00",
"TTY": "?",
"TIME": "00:00:42",
"CMD": "sleep infinity",
},
}, entries)
var buf bytes.Buffer
err := topPrint(&buf, header, entries)
require.NoError(t, err)
assert.Equal(t, trim(`
SERVICE # UID PID PPID C STIME TTY TIME GID CMD
simple 1 root 1 1 0 12:00 ? 00:00:01 - /entrypoint
noppid 1 root 1 - 0 12:00 ? 00:00:02 - /entrypoint
extra-hdr 1 root 1 1 0 12:00 ? 00:00:03 1 /entrypoint
multiple 1 root 1 1 0 12:00 ? 00:00:04 - /entrypoint
multiple 1 root 123 1 0 12:00 ? 00:00:42 - sleep infinity
`), buf.String())
})
}
func trim(s string) string {
var out bytes.Buffer
for _, line := range strings.Split(strings.TrimSpace(s), "\n") {
out.WriteString(strings.TrimSpace(line))
out.WriteRune('\n')
}
return out.String()
}

View File

@ -20,20 +20,17 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"os"
"strings" "strings"
"time" "time"
xprogress "github.com/moby/buildkit/util/progress/progressui"
"github.com/compose-spec/compose-go/v2/types" "github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
xprogress "github.com/moby/buildkit/util/progress/progressui"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/docker/compose/v2/cmd/formatter" "github.com/docker/compose/v2/cmd/formatter"
"github.com/spf13/cobra"
"github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/api"
ui "github.com/docker/compose/v2/pkg/progress"
"github.com/docker/compose/v2/pkg/utils" "github.com/docker/compose/v2/pkg/utils"
) )
@ -44,23 +41,19 @@ type composeOptions struct {
type upOptions struct { type upOptions struct {
*composeOptions *composeOptions
Detach bool Detach bool
noStart bool noStart bool
noDeps bool noDeps bool
cascadeStop bool cascadeStop bool
cascadeFail bool exitCodeFrom string
exitCodeFrom string noColor bool
noColor bool noPrefix bool
noPrefix bool attachDependencies bool
attachDependencies bool attach []string
attach []string noAttach []string
noAttach []string timestamp bool
timestamp bool wait bool
wait bool waitTimeout int
waitTimeout int
watch bool
navigationMenu bool
navigationMenuChanged bool
} }
func (opts upOptions) apply(project *types.Project, services []string) (*types.Project, error) { func (opts upOptions) apply(project *types.Project, services []string) (*types.Project, error) {
@ -82,33 +75,6 @@ func (opts upOptions) apply(project *types.Project, services []string) (*types.P
return project, nil return project, nil
} }
func (opts *upOptions) validateNavigationMenu(dockerCli command.Cli) {
if !dockerCli.Out().IsTerminal() {
opts.navigationMenu = false
return
}
// If --menu flag was not set
if !opts.navigationMenuChanged {
if envVar, ok := os.LookupEnv(ComposeMenu); ok {
opts.navigationMenu = utils.StringToBool(envVar)
return
}
// ...and COMPOSE_MENU env var is not defined we want the default value to be true
opts.navigationMenu = true
}
}
func (opts upOptions) OnExit() api.Cascade {
switch {
case opts.cascadeStop:
return api.CascadeStop
case opts.cascadeFail:
return api.CascadeFail
default:
return api.CascadeIgnore
}
}
func upCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command { func upCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
up := upOptions{} up := upOptions{}
create := createOptions{} create := createOptions{}
@ -119,10 +85,6 @@ func upCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *c
PreRunE: AdaptCmd(func(ctx context.Context, cmd *cobra.Command, args []string) error { PreRunE: AdaptCmd(func(ctx context.Context, cmd *cobra.Command, args []string) error {
create.pullChanged = cmd.Flags().Changed("pull") create.pullChanged = cmd.Flags().Changed("pull")
create.timeChanged = cmd.Flags().Changed("timeout") create.timeChanged = cmd.Flags().Changed("timeout")
up.navigationMenuChanged = cmd.Flags().Changed("menu")
if !cmd.Flags().Changed("remove-orphans") {
create.removeOrphans = utils.StringToBool(os.Getenv(ComposeRemoveOrphans))
}
return validateFlags(&up, &create) return validateFlags(&up, &create)
}), }),
RunE: p.WithServices(dockerCli, func(ctx context.Context, project *types.Project, services []string) error { RunE: p.WithServices(dockerCli, func(ctx context.Context, project *types.Project, services []string) error {
@ -133,66 +95,43 @@ func upCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *c
if len(up.attach) != 0 && up.attachDependencies { if len(up.attach) != 0 && up.attachDependencies {
return errors.New("cannot combine --attach and --attach-dependencies") return errors.New("cannot combine --attach and --attach-dependencies")
} }
up.validateNavigationMenu(dockerCli)
if !p.All && len(project.Services) == 0 {
return fmt.Errorf("no service selected")
}
return runUp(ctx, dockerCli, backend, create, up, build, project, services) return runUp(ctx, dockerCli, backend, create, up, build, project, services)
}), }),
ValidArgsFunction: completeServiceNames(dockerCli, p), ValidArgsFunction: completeServiceNames(dockerCli, p),
} }
flags := upCmd.Flags() flags := upCmd.Flags()
flags.BoolVarP(&up.Detach, "detach", "d", false, "Detached mode: Run containers in the background") flags.BoolVarP(&up.Detach, "detach", "d", false, "Detached mode: Run containers in the background")
flags.BoolVar(&create.Build, "build", false, "Build images before starting containers") flags.BoolVar(&create.Build, "build", false, "Build images before starting containers.")
flags.BoolVar(&create.noBuild, "no-build", false, "Don't build an image, even if it's policy") flags.BoolVar(&create.noBuild, "no-build", false, "Don't build an image, even if it's policy.")
flags.StringVar(&create.Pull, "pull", "policy", `Pull image before running ("always"|"missing"|"never")`) flags.StringVar(&create.Pull, "pull", "policy", `Pull image before running ("always"|"missing"|"never")`)
flags.BoolVar(&create.removeOrphans, "remove-orphans", false, "Remove containers for services not defined in the Compose file") flags.BoolVar(&create.removeOrphans, "remove-orphans", false, "Remove containers for services not defined in the Compose file.")
flags.StringArrayVar(&create.scale, "scale", []string{}, "Scale SERVICE to NUM instances. Overrides the `scale` setting in the Compose file if present.") flags.StringArrayVar(&create.scale, "scale", []string{}, "Scale SERVICE to NUM instances. Overrides the `scale` setting in the Compose file if present.")
flags.BoolVar(&up.noColor, "no-color", false, "Produce monochrome output") flags.BoolVar(&up.noColor, "no-color", false, "Produce monochrome output.")
flags.BoolVar(&up.noPrefix, "no-log-prefix", false, "Don't print prefix in logs") flags.BoolVar(&up.noPrefix, "no-log-prefix", false, "Don't print prefix in logs.")
flags.BoolVar(&create.forceRecreate, "force-recreate", false, "Recreate containers even if their configuration and image haven't changed") flags.BoolVar(&create.forceRecreate, "force-recreate", false, "Recreate containers even if their configuration and image haven't changed.")
flags.BoolVar(&create.noRecreate, "no-recreate", false, "If containers already exist, don't recreate them. Incompatible with --force-recreate.") flags.BoolVar(&create.noRecreate, "no-recreate", false, "If containers already exist, don't recreate them. Incompatible with --force-recreate.")
flags.BoolVar(&up.noStart, "no-start", false, "Don't start the services after creating them") flags.BoolVar(&up.noStart, "no-start", false, "Don't start the services after creating them.")
flags.BoolVar(&up.cascadeStop, "abort-on-container-exit", false, "Stops all containers if any container was stopped. Incompatible with -d") flags.BoolVar(&up.cascadeStop, "abort-on-container-exit", false, "Stops all containers if any container was stopped. Incompatible with -d")
flags.BoolVar(&up.cascadeFail, "abort-on-container-failure", false, "Stops all containers if any container exited with failure. Incompatible with -d")
flags.StringVar(&up.exitCodeFrom, "exit-code-from", "", "Return the exit code of the selected service container. Implies --abort-on-container-exit") flags.StringVar(&up.exitCodeFrom, "exit-code-from", "", "Return the exit code of the selected service container. Implies --abort-on-container-exit")
flags.IntVarP(&create.timeout, "timeout", "t", 0, "Use this timeout in seconds for container shutdown when attached or when containers are already running") flags.IntVarP(&create.timeout, "timeout", "t", 0, "Use this timeout in seconds for container shutdown when attached or when containers are already running.")
flags.BoolVar(&up.timestamp, "timestamps", false, "Show timestamps") flags.BoolVar(&up.timestamp, "timestamps", false, "Show timestamps.")
flags.BoolVar(&up.noDeps, "no-deps", false, "Don't start linked services") flags.BoolVar(&up.noDeps, "no-deps", false, "Don't start linked services.")
flags.BoolVar(&create.recreateDeps, "always-recreate-deps", false, "Recreate dependent containers. Incompatible with --no-recreate.") flags.BoolVar(&create.recreateDeps, "always-recreate-deps", false, "Recreate dependent containers. Incompatible with --no-recreate.")
flags.BoolVarP(&create.noInherit, "renew-anon-volumes", "V", false, "Recreate anonymous volumes instead of retrieving data from the previous containers") flags.BoolVarP(&create.noInherit, "renew-anon-volumes", "V", false, "Recreate anonymous volumes instead of retrieving data from the previous containers.")
flags.BoolVar(&create.quietPull, "quiet-pull", false, "Pull without printing progress information") flags.BoolVar(&create.quietPull, "quiet-pull", false, "Pull without printing progress information.")
flags.BoolVar(&build.quiet, "quiet-build", false, "Suppress the build output")
flags.StringArrayVar(&up.attach, "attach", []string{}, "Restrict attaching to the specified services. Incompatible with --attach-dependencies.") flags.StringArrayVar(&up.attach, "attach", []string{}, "Restrict attaching to the specified services. Incompatible with --attach-dependencies.")
flags.StringArrayVar(&up.noAttach, "no-attach", []string{}, "Do not attach (stream logs) to the specified services") flags.StringArrayVar(&up.noAttach, "no-attach", []string{}, "Do not attach (stream logs) to the specified services.")
flags.BoolVar(&up.attachDependencies, "attach-dependencies", false, "Automatically attach to log output of dependent services") flags.BoolVar(&up.attachDependencies, "attach-dependencies", false, "Automatically attach to log output of dependent services.")
flags.BoolVar(&up.wait, "wait", false, "Wait for services to be running|healthy. Implies detached mode.") flags.BoolVar(&up.wait, "wait", false, "Wait for services to be running|healthy. Implies detached mode.")
flags.IntVar(&up.waitTimeout, "wait-timeout", 0, "Maximum duration in seconds to wait for the project to be running|healthy") flags.IntVar(&up.waitTimeout, "wait-timeout", 0, "Maximum duration to wait for the project to be running|healthy.")
flags.BoolVarP(&up.watch, "watch", "w", false, "Watch source code and rebuild/refresh containers when files are updated.")
flags.BoolVar(&up.navigationMenu, "menu", false, "Enable interactive shortcuts when running attached. Incompatible with --detach. Can also be enable/disable by setting COMPOSE_MENU environment var.")
flags.BoolVarP(&create.AssumeYes, "yes", "y", false, `Assume "yes" as answer to all prompts and run non-interactively`)
flags.SetNormalizeFunc(func(f *pflag.FlagSet, name string) pflag.NormalizedName {
// assumeYes was introduced by mistake as `--y`
if name == "y" {
logrus.Warn("--y is deprecated, please use --yes instead")
name = "yes"
}
return pflag.NormalizedName(name)
})
return upCmd return upCmd
} }
//nolint:gocyclo
func validateFlags(up *upOptions, create *createOptions) error { func validateFlags(up *upOptions, create *createOptions) error {
if up.exitCodeFrom != "" && !up.cascadeFail { if up.exitCodeFrom != "" {
up.cascadeStop = true up.cascadeStop = true
} }
if up.cascadeStop && up.cascadeFail {
return fmt.Errorf("--abort-on-container-failure cannot be combined with --abort-on-container-exit")
}
if up.wait { if up.wait {
if up.attachDependencies || up.cascadeStop || len(up.attach) > 0 { if up.attachDependencies || up.cascadeStop || len(up.attach) > 0 {
return fmt.Errorf("--wait cannot be combined with --abort-on-container-exit, --attach or --attach-dependencies") return fmt.Errorf("--wait cannot be combined with --abort-on-container-exit, --attach or --attach-dependencies")
@ -202,15 +141,8 @@ func validateFlags(up *upOptions, create *createOptions) error {
if create.Build && create.noBuild { if create.Build && create.noBuild {
return fmt.Errorf("--build and --no-build are incompatible") return fmt.Errorf("--build and --no-build are incompatible")
} }
if up.Detach && (up.attachDependencies || up.cascadeStop || up.cascadeFail || len(up.attach) > 0 || up.watch) { if up.Detach && (up.attachDependencies || up.cascadeStop || len(up.attach) > 0) {
if up.wait { return fmt.Errorf("--detach cannot be combined with --abort-on-container-exit, --attach or --attach-dependencies")
return fmt.Errorf("--wait cannot be combined with --abort-on-container-exit, --abort-on-container-failure, --attach, --attach-dependencies or --watch")
} else {
return fmt.Errorf("--detach cannot be combined with --abort-on-container-exit, --abort-on-container-failure, --attach, --attach-dependencies or --watch")
}
}
if create.noInherit && create.noRecreate {
return fmt.Errorf("--no-recreate and --renew-anon-volumes are incompatible")
} }
if create.forceRecreate && create.noRecreate { if create.forceRecreate && create.noRecreate {
return fmt.Errorf("--force-recreate and --no-recreate are incompatible") return fmt.Errorf("--force-recreate and --no-recreate are incompatible")
@ -218,13 +150,9 @@ func validateFlags(up *upOptions, create *createOptions) error {
if create.recreateDeps && create.noRecreate { if create.recreateDeps && create.noRecreate {
return fmt.Errorf("--always-recreate-deps and --no-recreate are incompatible") return fmt.Errorf("--always-recreate-deps and --no-recreate are incompatible")
} }
if create.noBuild && up.watch {
return fmt.Errorf("--no-build and --watch are incompatible")
}
return nil return nil
} }
//nolint:gocyclo
func runUp( func runUp(
ctx context.Context, ctx context.Context,
dockerCli command.Cli, dockerCli command.Cli,
@ -235,8 +163,8 @@ func runUp(
project *types.Project, project *types.Project,
services []string, services []string,
) error { ) error {
if err := checksForRemoteStack(ctx, dockerCli, project, buildOptions, createOptions.AssumeYes, []string{}); err != nil { if len(project.Services) == 0 {
return err return fmt.Errorf("no service selected")
} }
err := createOptions.Apply(project) err := createOptions.Apply(project)
@ -262,8 +190,6 @@ func runUp(
if err != nil { if err != nil {
return err return err
} }
bo.Services = services
bo.Deps = !upOptions.noDeps
build = &bo build = &bo
} }
@ -277,7 +203,6 @@ func runUp(
Inherit: !createOptions.noInherit, Inherit: !createOptions.noInherit,
Timeout: createOptions.GetTimeout(), Timeout: createOptions.GetTimeout(),
QuietPull: createOptions.quietPull, QuietPull: createOptions.quietPull,
AssumeYes: createOptions.AssumeYes,
} }
if upOptions.noStart { if upOptions.noStart {
@ -323,16 +248,14 @@ func runUp(
return backend.Up(ctx, project, api.UpOptions{ return backend.Up(ctx, project, api.UpOptions{
Create: create, Create: create,
Start: api.StartOptions{ Start: api.StartOptions{
Project: project, Project: project,
Attach: consumer, Attach: consumer,
AttachTo: attach, AttachTo: attach,
ExitCodeFrom: upOptions.exitCodeFrom, ExitCodeFrom: upOptions.exitCodeFrom,
OnExit: upOptions.OnExit(), CascadeStop: upOptions.cascadeStop,
Wait: upOptions.wait, Wait: upOptions.wait,
WaitTimeout: timeout, WaitTimeout: timeout,
Watch: upOptions.watch, Services: services,
Services: services,
NavigationMenu: upOptions.navigationMenu && ui.Mode != "plain" && dockerCli.In().IsTerminal(),
}, },
}) })
} }

View File

@ -47,4 +47,5 @@ func TestApplyScaleOpt(t *testing.T) {
assert.NilError(t, err) assert.NilError(t, err)
assert.Equal(t, *bar.Scale, 3) assert.Equal(t, *bar.Scale, 3)
assert.Equal(t, *bar.Deploy.Replicas, 3) assert.Equal(t, *bar.Deploy.Replicas, 3)
} }

View File

@ -52,19 +52,19 @@ func versionCommand(dockerCli command.Cli) *cobra.Command {
// define flags for backward compatibility with com.docker.cli // define flags for backward compatibility with com.docker.cli
flags := cmd.Flags() flags := cmd.Flags()
flags.StringVarP(&opts.format, "format", "f", "", "Format the output. Values: [pretty | json]. (Default: pretty)") flags.StringVarP(&opts.format, "format", "f", "", "Format the output. Values: [pretty | json]. (Default: pretty)")
flags.BoolVar(&opts.short, "short", false, "Shows only Compose's version number") flags.BoolVar(&opts.short, "short", false, "Shows only Compose's version number.")
return cmd return cmd
} }
func runVersion(opts versionOptions, dockerCli command.Cli) { func runVersion(opts versionOptions, dockerCli command.Cli) {
if opts.short { if opts.short {
_, _ = fmt.Fprintln(dockerCli.Out(), strings.TrimPrefix(internal.Version, "v")) fmt.Fprintln(dockerCli.Out(), strings.TrimPrefix(internal.Version, "v"))
return return
} }
if opts.format == formatter.JSON { if opts.format == formatter.JSON {
_, _ = fmt.Fprintf(dockerCli.Out(), "{\"version\":%q}\n", internal.Version) fmt.Fprintf(dockerCli.Out(), "{\"version\":%q}\n", internal.Version)
return return
} }
_, _ = fmt.Fprintln(dockerCli.Out(), "Docker Compose version", internal.Version) fmt.Fprintln(dockerCli.Out(), "Docker Compose version", internal.Version)
} }

View File

@ -1,76 +0,0 @@
/*
Copyright 2025 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"bytes"
"testing"
"github.com/docker/cli/cli/streams"
"github.com/docker/compose/v2/internal"
"github.com/docker/compose/v2/pkg/mocks"
"go.uber.org/mock/gomock"
"gotest.tools/v3/assert"
)
func TestVersionCommand(t *testing.T) {
originalVersion := internal.Version
defer func() {
internal.Version = originalVersion
}()
internal.Version = "v9.9.9-test"
tests := []struct {
name string
args []string
want string
}{
{
name: "default",
args: []string{},
want: "Docker Compose version v9.9.9-test\n",
},
{
name: "short flag",
args: []string{"--short"},
want: "9.9.9-test\n",
},
{
name: "json flag",
args: []string{"--format", "json"},
want: `{"version":"v9.9.9-test"}` + "\n",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
buf := new(bytes.Buffer)
cli := mocks.NewMockCli(ctrl)
cli.EXPECT().Out().Return(streams.NewOut(buf)).AnyTimes()
cmd := versionCommand(cli)
cmd.SetArgs(test.args)
err := cmd.Execute()
assert.NilError(t, err)
assert.Equal(t, test.want, buf.String())
})
}
}

View File

@ -65,7 +65,7 @@ func vizCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *
func runViz(ctx context.Context, dockerCli command.Cli, backend api.Service, opts *vizOptions) error { func runViz(ctx context.Context, dockerCli command.Cli, backend api.Service, opts *vizOptions) error {
_, _ = fmt.Fprintln(os.Stderr, "viz command is EXPERIMENTAL") _, _ = fmt.Fprintln(os.Stderr, "viz command is EXPERIMENTAL")
project, _, err := opts.ToProject(ctx, dockerCli, nil) project, err := opts.ToProject(dockerCli, nil)
if err != nil { if err != nil {
return err return err
} }

View File

@ -17,10 +17,10 @@
package compose package compose
import ( import (
"fmt"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestPreferredIndentationStr(t *testing.T) { func TestPreferredIndentationStr(t *testing.T) {
@ -83,12 +83,10 @@ func TestPreferredIndentationStr(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
got, err := preferredIndentationStr(tt.args.size, tt.args.useSpace) got, err := preferredIndentationStr(tt.args.size, tt.args.useSpace)
if tt.wantErr { if tt.wantErr && assert.NotNilf(t, err, fmt.Sprintf("preferredIndentationStr(%v, %v)", tt.args.size, tt.args.useSpace)) {
require.Errorf(t, err, "preferredIndentationStr(%v, %v)", tt.args.size, tt.args.useSpace) return
} else {
require.NoError(t, err)
assert.Equalf(t, tt.want, got, "preferredIndentationStr(%v, %v)", tt.args.size, tt.args.useSpace)
} }
assert.Equalf(t, tt.want, got, "preferredIndentationStr(%v, %v)", tt.args.size, tt.args.useSpace)
}) })
} }
} }

View File

@ -1,92 +0,0 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"fmt"
"slices"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/cli/flags"
"github.com/docker/compose/v2/pkg/api"
"github.com/spf13/cobra"
)
type volumesOptions struct {
*ProjectOptions
Quiet bool
Format string
}
func volumesCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
options := volumesOptions{
ProjectOptions: p,
}
cmd := &cobra.Command{
Use: "volumes [OPTIONS] [SERVICE...]",
Short: "List volumes",
RunE: Adapt(func(ctx context.Context, args []string) error {
return runVol(ctx, dockerCli, backend, args, options)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
cmd.Flags().BoolVarP(&options.Quiet, "quiet", "q", false, "Only display volume names")
cmd.Flags().StringVar(&options.Format, "format", "table", flags.FormatHelp)
return cmd
}
func runVol(ctx context.Context, dockerCli command.Cli, backend api.Service, services []string, options volumesOptions) error {
project, name, err := options.projectOrName(ctx, dockerCli, services...)
if err != nil {
return err
}
if project != nil {
names := project.ServiceNames()
for _, service := range services {
if !slices.Contains(names, service) {
return fmt.Errorf("no such service: %s", service)
}
}
}
volumes, err := backend.Volumes(ctx, name, api.VolumesOptions{
Services: services,
})
if err != nil {
return err
}
if options.Quiet {
for _, v := range volumes {
_, _ = fmt.Fprintln(dockerCli.Out(), v.Name)
}
return nil
}
volumeCtx := formatter.Context{
Output: dockerCli.Out(),
Format: formatter.NewVolumeFormat(options.Format, options.Quiet),
}
return formatter.VolumeWrite(volumeCtx, volumes)
}

View File

@ -43,7 +43,7 @@ func waitCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
var err error var err error
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "wait SERVICE [SERVICE...] [OPTIONS]", Use: "wait SERVICE [SERVICE...] [OPTIONS]",
Short: "Block until containers of all (or specified) services stop.", Short: "Block until the first service container stops",
Args: cli.RequiresMinArgs(1), Args: cli.RequiresMinArgs(1),
RunE: Adapt(func(ctx context.Context, services []string) error { RunE: Adapt(func(ctx context.Context, services []string) error {
opts.services = services opts.services = services
@ -61,7 +61,7 @@ func waitCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
} }
func runWait(ctx context.Context, dockerCli command.Cli, backend api.Service, opts *waitOptions) (int64, error) { func runWait(ctx context.Context, dockerCli command.Cli, backend api.Service, opts *waitOptions) (int64, error) {
_, name, err := opts.projectOrName(ctx, dockerCli) _, name, err := opts.projectOrName(dockerCli)
if err != nil { if err != nil {
return 0, err return 0, err
} }

View File

@ -21,7 +21,6 @@ import (
"fmt" "fmt"
"github.com/compose-spec/compose-go/v2/types" "github.com/compose-spec/compose-go/v2/types"
"github.com/docker/compose/v2/cmd/formatter"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/internal/locker" "github.com/docker/compose/v2/internal/locker"
@ -32,7 +31,7 @@ import (
type watchOptions struct { type watchOptions struct {
*ProjectOptions *ProjectOptions
prune bool quiet bool
noUp bool noUp bool
} }
@ -58,14 +57,13 @@ func watchCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
ValidArgsFunction: completeServiceNames(dockerCli, p), ValidArgsFunction: completeServiceNames(dockerCli, p),
} }
cmd.Flags().BoolVar(&buildOpts.quiet, "quiet", false, "hide build output") cmd.Flags().BoolVar(&watchOpts.quiet, "quiet", false, "hide build output")
cmd.Flags().BoolVar(&watchOpts.prune, "prune", true, "Prune dangling images on rebuild")
cmd.Flags().BoolVar(&watchOpts.noUp, "no-up", false, "Do not build & start services before watching") cmd.Flags().BoolVar(&watchOpts.noUp, "no-up", false, "Do not build & start services before watching")
return cmd return cmd
} }
func runWatch(ctx context.Context, dockerCli command.Cli, backend api.Service, watchOpts watchOptions, buildOpts buildOptions, services []string) error { func runWatch(ctx context.Context, dockerCli command.Cli, backend api.Service, watchOpts watchOptions, buildOpts buildOptions, services []string) error {
project, _, err := watchOpts.ToProject(ctx, dockerCli, services) project, err := watchOpts.ToProject(dockerCli, nil)
if err != nil { if err != nil {
return err return err
} }
@ -103,24 +101,20 @@ func runWatch(ctx context.Context, dockerCli command.Cli, backend api.Service, w
Recreate: api.RecreateDiverged, Recreate: api.RecreateDiverged,
RecreateDependencies: api.RecreateNever, RecreateDependencies: api.RecreateNever,
Inherit: true, Inherit: true,
QuietPull: buildOpts.quiet, QuietPull: watchOpts.quiet,
}, },
Start: api.StartOptions{ Start: api.StartOptions{
Project: project, Project: project,
Attach: nil, Attach: nil,
Services: services, CascadeStop: false,
Services: services,
}, },
} }
if err := backend.Up(ctx, project, upOpts); err != nil { if err := backend.Up(ctx, project, upOpts); err != nil {
return err return err
} }
} }
return backend.Watch(ctx, project, services, api.WatchOptions{
consumer := formatter.NewLogConsumer(ctx, dockerCli.Out(), dockerCli.Err(), false, false, false) Build: build,
return backend.Watch(ctx, project, api.WatchOptions{
Build: &build,
LogTo: consumer,
Prune: watchOpts.prune,
Services: services,
}) })
} }

View File

@ -1,100 +0,0 @@
/*
Copyright 2024 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package formatter
import (
"fmt"
"github.com/acarl005/stripansi"
)
var disableAnsi bool
func ansi(code string) string {
return fmt.Sprintf("\033%s", code)
}
func saveCursor() {
if disableAnsi {
return
}
fmt.Print(ansi("7"))
}
func restoreCursor() {
if disableAnsi {
return
}
fmt.Print(ansi("8"))
}
func showCursor() {
if disableAnsi {
return
}
fmt.Print(ansi("[?25h"))
}
func moveCursor(y, x int) {
if disableAnsi {
return
}
fmt.Print(ansi(fmt.Sprintf("[%d;%dH", y, x)))
}
func carriageReturn() {
if disableAnsi {
return
}
fmt.Print(ansi(fmt.Sprintf("[%dG", 0)))
}
func clearLine() {
if disableAnsi {
return
}
// Does not move cursor from its current position
fmt.Print(ansi("[2K"))
}
func moveCursorUp(lines int) {
if disableAnsi {
return
}
// Does not add new lines
fmt.Print(ansi(fmt.Sprintf("[%dA", lines)))
}
func moveCursorDown(lines int) {
if disableAnsi {
return
}
// Does not add new lines
fmt.Print(ansi(fmt.Sprintf("[%dB", lines)))
}
func newLine() {
// Like \n
fmt.Print("\012")
}
func lenAnsi(s string) int {
// len has into consideration ansi codes, if we want
// the len of the actual len(string) we need to strip
// all ansi codes
return len(stripansi.Strip(s))
}

View File

@ -19,10 +19,9 @@ package formatter
import ( import (
"fmt" "fmt"
"strconv" "strconv"
"strings"
"sync" "sync"
"github.com/docker/cli/cli/command" "github.com/docker/compose/v2/pkg/api"
) )
var names = []string{ var names = []string{
@ -36,18 +35,6 @@ var names = []string{
"white", "white",
} }
const (
BOLD = "1"
FAINT = "2"
ITALIC = "3"
UNDERLINE = "4"
)
const (
RESET = "0"
CYAN = "36"
)
const ( const (
// Never use ANSI codes // Never use ANSI codes
Never = "never" Never = "never"
@ -59,20 +46,16 @@ const (
Auto = "auto" Auto = "auto"
) )
// ansiColorOffset is the offset for basic foreground colors in ANSI escape codes.
const ansiColorOffset = 30
// SetANSIMode configure formatter for colored output on ANSI-compliant console // SetANSIMode configure formatter for colored output on ANSI-compliant console
func SetANSIMode(streams command.Streams, ansi string) { func SetANSIMode(streams api.Streams, ansi string) {
if !useAnsi(streams, ansi) { if !useAnsi(streams, ansi) {
nextColor = func() colorFunc { nextColor = func() colorFunc {
return monochrome return monochrome
} }
disableAnsi = true
} }
} }
func useAnsi(streams command.Streams, ansi string) bool { func useAnsi(streams api.Streams, ansi string) bool {
switch ansi { switch ansi {
case Always: case Always:
return true return true
@ -89,21 +72,12 @@ var monochrome = func(s string) string {
return s return s
} }
func ansiColor(code, s string, formatOpts ...string) string { func ansiColor(code, s string) string {
return fmt.Sprintf("%s%s%s", ansiColorCode(code, formatOpts...), s, ansiColorCode("0")) return fmt.Sprintf("%s%s%s", ansi(code), s, ansi("0"))
} }
// Everything about ansiColorCode color https://hyperskill.org/learn/step/18193 func ansi(code string) string {
func ansiColorCode(code string, formatOpts ...string) string { return fmt.Sprintf("\033[%sm", code)
var sb strings.Builder
sb.WriteString("\033[")
for _, c := range formatOpts {
sb.WriteString(c)
sb.WriteString(";")
}
sb.WriteString(code)
sb.WriteString("m")
return sb.String()
} }
func makeColorFunc(code string) colorFunc { func makeColorFunc(code string) colorFunc {
@ -112,12 +86,10 @@ func makeColorFunc(code string) colorFunc {
} }
} }
var ( var nextColor = rainbowColor
nextColor = rainbowColor var rainbow []colorFunc
rainbow []colorFunc var currentIndex = 0
currentIndex = 0 var mutex sync.Mutex
mutex sync.Mutex
)
func rainbowColor() colorFunc { func rainbowColor() colorFunc {
mutex.Lock() mutex.Lock()
@ -130,8 +102,8 @@ func rainbowColor() colorFunc {
func init() { func init() {
colors := map[string]colorFunc{} colors := map[string]colorFunc{}
for i, name := range names { for i, name := range names {
colors[name] = makeColorFunc(strconv.Itoa(ansiColorOffset + i)) colors[name] = makeColorFunc(strconv.Itoa(30 + i))
colors["intense_"+name] = makeColorFunc(strconv.Itoa(ansiColorOffset+i) + ";1") colors["intense_"+name] = makeColorFunc(strconv.Itoa(30+i) + ";1")
} }
rainbow = []colorFunc{ rainbow = []colorFunc{
colors["cyan"], colors["cyan"],

View File

@ -24,7 +24,7 @@ import (
"github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/command/formatter"
"github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/api"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/stringid" "github.com/docker/docker/pkg/stringid"
"github.com/docker/go-units" "github.com/docker/go-units"
) )
@ -212,9 +212,9 @@ func (c *ContainerContext) Publishers() api.PortPublishers {
} }
func (c *ContainerContext) Ports() string { func (c *ContainerContext) Ports() string {
var ports []container.Port var ports []types.Port
for _, publisher := range c.c.Publishers { for _, publisher := range c.c.Publishers {
ports = append(ports, container.Port{ ports = append(ports, types.Port{
IP: publisher.URL, IP: publisher.URL,
PrivatePort: uint16(publisher.TargetPort), PrivatePort: uint16(publisher.TargetPort),
PublicPort: uint16(publisher.PublishedPort), PublicPort: uint16(publisher.PublishedPort),

View File

@ -25,7 +25,6 @@ import (
"sync" "sync"
"time" "time"
"github.com/buger/goterm"
"github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/api"
"github.com/docker/docker/pkg/jsonmessage" "github.com/docker/docker/pkg/jsonmessage"
) )
@ -56,36 +55,22 @@ func NewLogConsumer(ctx context.Context, stdout, stderr io.Writer, color, prefix
} }
} }
func (l *logConsumer) Register(name string) {
l.register(name)
}
func (l *logConsumer) register(name string) *presenter { func (l *logConsumer) register(name string) *presenter {
var p *presenter cf := monochrome
root, _, found := strings.Cut(name, " ") if l.color {
if found { cf = nextColor()
parent := l.getPresenter(root) }
p = &presenter{ p := &presenter{
colors: parent.colors, colors: cf,
name: name, name: name,
prefix: parent.prefix,
}
} else {
cf := monochrome
if l.color {
switch name {
case "":
cf = monochrome
case api.WatchLogger:
cf = makeColorFunc("92")
default:
cf = nextColor()
}
}
p = &presenter{
colors: cf,
name: name,
}
} }
l.presenters.Store(name, p) l.presenters.Store(name, p)
l.computeWidth()
if l.prefix { if l.prefix {
l.computeWidth()
l.presenters.Range(func(key, value interface{}) bool { l.presenters.Range(func(key, value interface{}) bool {
p := value.(*presenter) p := value.(*presenter)
p.setPrefix(l.width) p.setPrefix(l.width)
@ -108,7 +93,7 @@ func (l *logConsumer) Log(container, message string) {
l.write(l.stdout, container, message) l.write(l.stdout, container, message)
} }
// Err formats a log message as received from name/container // Log formats a log message as received from name/container
func (l *logConsumer) Err(container, message string) { func (l *logConsumer) Err(container, message string) {
l.write(l.stderr, container, message) l.write(l.stderr, container, message)
} }
@ -121,16 +106,16 @@ func (l *logConsumer) write(w io.Writer, container, message string) {
timestamp := time.Now().Format(jsonmessage.RFC3339NanoFixed) timestamp := time.Now().Format(jsonmessage.RFC3339NanoFixed)
for _, line := range strings.Split(message, "\n") { for _, line := range strings.Split(message, "\n") {
if l.timestamp { if l.timestamp {
_, _ = fmt.Fprintf(w, "%s%s %s\n", p.prefix, timestamp, line) fmt.Fprintf(w, "%s%s%s\n", p.prefix, timestamp, line)
} else { } else {
_, _ = fmt.Fprintf(w, "%s%s\n", p.prefix, line) fmt.Fprintf(w, "%s%s\n", p.prefix, line)
} }
} }
} }
func (l *logConsumer) Status(container, msg string) { func (l *logConsumer) Status(container, msg string) {
p := l.getPresenter(container) p := l.getPresenter(container)
s := p.colors(fmt.Sprintf("%s%s %s\n", goterm.RESET_LINE, container, msg)) s := p.colors(fmt.Sprintf("%s %s\n", container, msg))
l.stdout.Write([]byte(s)) //nolint:errcheck l.stdout.Write([]byte(s)) //nolint:errcheck
} }
@ -153,33 +138,5 @@ type presenter struct {
} }
func (p *presenter) setPrefix(width int) { func (p *presenter) setPrefix(width int) {
if p.name == api.WatchLogger {
p.prefix = p.colors(strings.Repeat(" ", width) + " ⦿ ")
return
}
p.prefix = p.colors(fmt.Sprintf("%-"+strconv.Itoa(width)+"s | ", p.name)) p.prefix = p.colors(fmt.Sprintf("%-"+strconv.Itoa(width)+"s | ", p.name))
} }
type logDecorator struct {
decorated api.LogConsumer
Before func()
After func()
}
func (l logDecorator) Log(containerName, message string) {
l.Before()
l.decorated.Log(containerName, message)
l.After()
}
func (l logDecorator) Err(containerName, message string) {
l.Before()
l.decorated.Err(containerName, message)
l.After()
}
func (l logDecorator) Status(container, msg string) {
l.Before()
l.decorated.Status(container, msg)
l.After()
}

View File

@ -14,16 +14,25 @@
limitations under the License. limitations under the License.
*/ */
package e2e package formatter
import ( import (
"testing" "strings"
"github.com/hashicorp/go-multierror"
) )
func TestComposeModel(t *testing.T) { // SetMultiErrorFormat set cli default format for multi-errors
t.Skip("waiting for docker-model release") func SetMultiErrorFormat(errs *multierror.Error) {
c := NewParallelCLI(t) if errs != nil {
defer c.cleanupWithDown(t, "model-test") errs.ErrorFormat = formatErrors
}
c.RunDockerComposeCmd(t, "-f", "./fixtures/model/compose.yaml", "run", "test", "sh", "-c", "curl ${FOO_URL}") }
func formatErrors(errs []error) string {
messages := make([]string, len(errs))
for i, err := range errs {
messages[i] = "Error: " + err.Error()
}
return strings.Join(messages, "\n")
} }

View File

@ -1,359 +0,0 @@
/*
Copyright 2024 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package formatter
import (
"context"
"errors"
"fmt"
"math"
"os"
"syscall"
"time"
"github.com/buger/goterm"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/compose/v2/internal/tracing"
"github.com/docker/compose/v2/pkg/api"
"github.com/eiannone/keyboard"
"github.com/skratchdot/open-golang/open"
)
const DISPLAY_ERROR_TIME = 10
type KeyboardError struct {
err error
timeStart time.Time
}
func (ke *KeyboardError) shouldDisplay() bool {
return ke.err != nil && int(time.Since(ke.timeStart).Seconds()) < DISPLAY_ERROR_TIME
}
func (ke *KeyboardError) printError(height int, info string) {
if ke.shouldDisplay() {
errMessage := ke.err.Error()
moveCursor(height-1-extraLines(info)-extraLines(errMessage), 0)
clearLine()
fmt.Print(errMessage)
}
}
func (ke *KeyboardError) addError(prefix string, err error) {
ke.timeStart = time.Now()
prefix = ansiColor(CYAN, fmt.Sprintf("%s →", prefix), BOLD)
errorString := fmt.Sprintf("%s %s", prefix, err.Error())
ke.err = errors.New(errorString)
}
func (ke *KeyboardError) error() string {
return ke.err.Error()
}
type KeyboardWatch struct {
Watching bool
Watcher Feature
}
// Feature is an compose feature that can be started/stopped by a menu command
type Feature interface {
Start(context.Context) error
Stop() error
}
type KEYBOARD_LOG_LEVEL int
const (
NONE KEYBOARD_LOG_LEVEL = 0
INFO KEYBOARD_LOG_LEVEL = 1
DEBUG KEYBOARD_LOG_LEVEL = 2
)
type LogKeyboard struct {
kError KeyboardError
Watch *KeyboardWatch
IsDockerDesktopActive bool
logLevel KEYBOARD_LOG_LEVEL
signalChannel chan<- os.Signal
}
func NewKeyboardManager(isDockerDesktopActive bool, sc chan<- os.Signal) *LogKeyboard {
return &LogKeyboard{
IsDockerDesktopActive: isDockerDesktopActive,
logLevel: INFO,
signalChannel: sc,
}
}
func (lk *LogKeyboard) Decorate(l api.LogConsumer) api.LogConsumer {
return logDecorator{
decorated: l,
Before: lk.clearNavigationMenu,
After: lk.PrintKeyboardInfo,
}
}
func (lk *LogKeyboard) PrintKeyboardInfo() {
if lk.logLevel == INFO {
lk.printNavigationMenu()
}
}
// Creates space to print error and menu string
func (lk *LogKeyboard) createBuffer(lines int) {
if lk.kError.shouldDisplay() {
extraLines := extraLines(lk.kError.error()) + 1
lines += extraLines
}
// get the string
infoMessage := lk.navigationMenu()
// calculate how many lines we need to display the menu info
// might be needed a line break
extraLines := extraLines(infoMessage) + 1
lines += extraLines
if lines > 0 {
allocateSpace(lines)
moveCursorUp(lines)
}
}
func (lk *LogKeyboard) printNavigationMenu() {
offset := 1
lk.clearNavigationMenu()
lk.createBuffer(offset)
if lk.logLevel == INFO {
height := goterm.Height()
menu := lk.navigationMenu()
carriageReturn()
saveCursor()
lk.kError.printError(height, menu)
moveCursor(height-extraLines(menu), 0)
clearLine()
fmt.Print(menu)
carriageReturn()
restoreCursor()
}
}
func (lk *LogKeyboard) navigationMenu() string {
var openDDInfo string
if lk.IsDockerDesktopActive {
openDDInfo = shortcutKeyColor("v") + navColor(" View in Docker Desktop")
}
var openDDUI string
if openDDInfo != "" {
openDDUI = navColor(" ")
}
if lk.IsDockerDesktopActive {
openDDUI = openDDUI + shortcutKeyColor("o") + navColor(" View Config")
}
var watchInfo string
if openDDInfo != "" || openDDUI != "" {
watchInfo = navColor(" ")
}
isEnabled := " Enable"
if lk.Watch != nil && lk.Watch.Watching {
isEnabled = " Disable"
}
watchInfo = watchInfo + shortcutKeyColor("w") + navColor(isEnabled+" Watch")
return openDDInfo + openDDUI + watchInfo
}
func (lk *LogKeyboard) clearNavigationMenu() {
height := goterm.Height()
carriageReturn()
saveCursor()
// clearLine()
for i := 0; i < height; i++ {
moveCursorDown(1)
clearLine()
}
restoreCursor()
}
func (lk *LogKeyboard) openDockerDesktop(ctx context.Context, project *types.Project) {
if !lk.IsDockerDesktopActive {
return
}
go func() {
_ = tracing.EventWrapFuncForErrGroup(ctx, "menu/gui", tracing.SpanOptions{},
func(ctx context.Context) error {
link := fmt.Sprintf("docker-desktop://dashboard/apps/%s", project.Name)
err := open.Run(link)
if err != nil {
err = fmt.Errorf("could not open Docker Desktop")
lk.keyboardError("View", err)
}
return err
})()
}()
}
func (lk *LogKeyboard) openDDComposeUI(ctx context.Context, project *types.Project) {
if !lk.IsDockerDesktopActive {
return
}
go func() {
_ = tracing.EventWrapFuncForErrGroup(ctx, "menu/gui/composeview", tracing.SpanOptions{},
func(ctx context.Context) error {
link := fmt.Sprintf("docker-desktop://dashboard/docker-compose/%s", project.Name)
err := open.Run(link)
if err != nil {
err = fmt.Errorf("could not open Docker Desktop Compose UI")
lk.keyboardError("View Config", err)
}
return err
})()
}()
}
func (lk *LogKeyboard) openDDWatchDocs(ctx context.Context, project *types.Project) {
go func() {
_ = tracing.EventWrapFuncForErrGroup(ctx, "menu/gui/watch", tracing.SpanOptions{},
func(ctx context.Context) error {
link := fmt.Sprintf("docker-desktop://dashboard/docker-compose/%s/watch", project.Name)
err := open.Run(link)
if err != nil {
err = fmt.Errorf("could not open Docker Desktop Compose UI")
lk.keyboardError("Watch Docs", err)
}
return err
})()
}()
}
func (lk *LogKeyboard) keyboardError(prefix string, err error) {
lk.kError.addError(prefix, err)
lk.printNavigationMenu()
timer1 := time.NewTimer((DISPLAY_ERROR_TIME + 1) * time.Second)
go func() {
<-timer1.C
lk.printNavigationMenu()
}()
}
func (lk *LogKeyboard) ToggleWatch(ctx context.Context, options api.UpOptions) {
if lk.Watch == nil {
return
}
if lk.Watch.Watching {
err := lk.Watch.Watcher.Stop()
if err != nil {
options.Start.Attach.Err(api.WatchLogger, err.Error())
} else {
lk.Watch.Watching = false
}
} else {
go func() {
_ = tracing.EventWrapFuncForErrGroup(ctx, "menu/watch", tracing.SpanOptions{},
func(ctx context.Context) error {
err := lk.Watch.Watcher.Start(ctx)
if err != nil {
options.Start.Attach.Err(api.WatchLogger, err.Error())
} else {
lk.Watch.Watching = true
}
return err
})()
}()
}
}
func (lk *LogKeyboard) HandleKeyEvents(ctx context.Context, event keyboard.KeyEvent, project *types.Project, options api.UpOptions) {
switch kRune := event.Rune; kRune {
case 'v':
lk.openDockerDesktop(ctx, project)
case 'w':
if lk.Watch == nil {
// we try to open watch docs if DD is installed
if lk.IsDockerDesktopActive {
lk.openDDWatchDocs(ctx, project)
}
// either way we mark menu/watch as an error
go func() {
_ = tracing.EventWrapFuncForErrGroup(ctx, "menu/watch", tracing.SpanOptions{},
func(ctx context.Context) error {
err := fmt.Errorf("watch is not yet configured. Learn more: %s", ansiColor(CYAN, "https://docs.docker.com/compose/file-watch/"))
lk.keyboardError("Watch", err)
return err
})()
}()
}
lk.ToggleWatch(ctx, options)
case 'o':
lk.openDDComposeUI(ctx, project)
}
switch key := event.Key; key {
case keyboard.KeyCtrlC:
_ = keyboard.Close()
lk.clearNavigationMenu()
showCursor()
lk.logLevel = NONE
// will notify main thread to kill and will handle gracefully
lk.signalChannel <- syscall.SIGINT
case keyboard.KeyEnter:
newLine()
lk.printNavigationMenu()
}
}
func (lk *LogKeyboard) EnableWatch(enabled bool, watcher Feature) {
lk.Watch = &KeyboardWatch{
Watching: enabled,
Watcher: watcher,
}
}
func allocateSpace(lines int) {
for i := 0; i < lines; i++ {
clearLine()
newLine()
carriageReturn()
}
}
func extraLines(s string) int {
return int(math.Floor(float64(lenAnsi(s)) / float64(goterm.Width())))
}
func shortcutKeyColor(key string) string {
foreground := "38;2"
black := "0;0;0"
background := "48;2"
white := "255;255;255"
return ansiColor(foreground+";"+black+";"+background+";"+white, key, BOLD)
}
func navColor(key string) string {
return ansiColor(FAINT, key)
}

View File

@ -20,12 +20,11 @@ import (
"os" "os"
dockercli "github.com/docker/cli/cli" dockercli "github.com/docker/cli/cli"
"github.com/docker/cli/cli-plugins/metadata" "github.com/docker/cli/cli-plugins/manager"
"github.com/docker/cli/cli-plugins/plugin" "github.com/docker/cli/cli-plugins/plugin"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/cmd/cmdtrace" "github.com/docker/compose/v2/cmd/cmdtrace"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/docker/compose/v2/cmd/compatibility" "github.com/docker/compose/v2/cmd/compatibility"
@ -36,12 +35,9 @@ import (
func pluginMain() { func pluginMain() {
plugin.Run(func(dockerCli command.Cli) *cobra.Command { plugin.Run(func(dockerCli command.Cli) *cobra.Command {
// TODO(milas): this cast is safe but we should not need to do this, backend := compose.NewComposeService(dockerCli)
// we should expose the concrete service type so that we do not need
// to rely on the `api.Service` interface internally
backend := compose.NewComposeService(dockerCli).(commands.Backend)
cmd := commands.RootCommand(dockerCli, backend) cmd := commands.RootCommand(dockerCli, backend)
originalPreRunE := cmd.PersistentPreRunE originalPreRun := cmd.PersistentPreRunE
cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
// initialize the dockerCli instance // initialize the dockerCli instance
if err := plugin.PersistentPreRunE(cmd, args); err != nil { if err := plugin.PersistentPreRunE(cmd, args); err != nil {
@ -50,25 +46,25 @@ func pluginMain() {
// compose-specific initialization // compose-specific initialization
dockerCliPostInitialize(dockerCli) dockerCliPostInitialize(dockerCli)
if err := cmdtrace.Setup(cmd, dockerCli, os.Args[1:]); err != nil { // TODO(milas): add an env var to enable logging from the
logrus.Debugf("failed to enable tracing: %v", err) // OTel components for debugging purposes
} _ = cmdtrace.Setup(cmd, dockerCli, os.Args[1:])
if originalPreRunE != nil { if originalPreRun != nil {
return originalPreRunE(cmd, args) return originalPreRun(cmd, args)
} }
return nil return nil
} }
cmd.SetFlagErrorFunc(func(c *cobra.Command, err error) error { cmd.SetFlagErrorFunc(func(c *cobra.Command, err error) error {
return dockercli.StatusError{ return dockercli.StatusError{
StatusCode: 1, StatusCode: compose.CommandSyntaxFailure.ExitCode,
Status: err.Error(), Status: err.Error(),
} }
}) })
return cmd return cmd
}, },
metadata.Metadata{ manager.Metadata{
SchemaVersion: "0.1.0", SchemaVersion: "0.1.0",
Vendor: "Docker Inc.", Vendor: "Docker Inc.",
Version: internal.Version, Version: internal.Version,

View File

@ -1,152 +0,0 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"encoding/json"
"fmt"
"os"
"time"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
func main() {
cmd := &cobra.Command{
Short: "Compose Provider Example",
Use: "demo",
}
cmd.AddCommand(composeCommand())
err := cmd.Execute()
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
type options struct {
db string
size int
}
func composeCommand() *cobra.Command {
c := &cobra.Command{
Use: "compose EVENT",
TraverseChildren: true,
}
c.PersistentFlags().String("project-name", "", "compose project name") // unused
var options options
upCmd := &cobra.Command{
Use: "up",
Run: func(_ *cobra.Command, args []string) {
up(options, args)
},
Args: cobra.ExactArgs(1),
}
upCmd.Flags().StringVar(&options.db, "type", "", "Database type (mysql, postgres, etc.)")
_ = upCmd.MarkFlagRequired("type")
upCmd.Flags().IntVar(&options.size, "size", 10, "Database size in GB")
upCmd.Flags().String("name", "", "Name of the database to be created")
_ = upCmd.MarkFlagRequired("name")
downCmd := &cobra.Command{
Use: "down",
Run: down,
Args: cobra.ExactArgs(1),
}
downCmd.Flags().String("name", "", "Name of the database to be deleted")
_ = downCmd.MarkFlagRequired("name")
c.AddCommand(upCmd, downCmd)
c.AddCommand(metadataCommand(upCmd, downCmd))
return c
}
const lineSeparator = "\n"
func up(options options, args []string) {
servicename := args[0]
fmt.Printf(`{ "type": "debug", "message": "Starting %s" }%s`, servicename, lineSeparator)
for i := 0; i < options.size; i += 10 {
time.Sleep(1 * time.Second)
fmt.Printf(`{ "type": "info", "message": "Processing ... %d%%" }%s`, i*100/options.size, lineSeparator)
}
fmt.Printf(`{ "type": "setenv", "message": "URL=https://magic.cloud/%s" }%s`, servicename, lineSeparator)
}
func down(_ *cobra.Command, _ []string) {
fmt.Printf(`{ "type": "error", "message": "Permission error" }%s`, lineSeparator)
}
func metadataCommand(upCmd, downCmd *cobra.Command) *cobra.Command {
return &cobra.Command{
Use: "metadata",
Run: func(cmd *cobra.Command, _ []string) {
metadata(upCmd, downCmd)
},
Args: cobra.NoArgs,
}
}
func metadata(upCmd, downCmd *cobra.Command) {
metadata := ProviderMetadata{}
metadata.Description = "Manage services on AwesomeCloud"
metadata.Up = commandParameters(upCmd)
metadata.Down = commandParameters(downCmd)
jsonMetadata, err := json.Marshal(metadata)
if err != nil {
panic(err)
}
fmt.Println(string(jsonMetadata))
}
func commandParameters(cmd *cobra.Command) CommandMetadata {
cmdMetadata := CommandMetadata{}
cmd.Flags().VisitAll(func(f *pflag.Flag) {
_, isRequired := f.Annotations[cobra.BashCompOneRequiredFlag]
cmdMetadata.Parameters = append(cmdMetadata.Parameters, Metadata{
Name: f.Name,
Description: f.Usage,
Required: isRequired,
Type: f.Value.Type(),
Default: f.DefValue,
})
})
return cmdMetadata
}
type ProviderMetadata struct {
Description string `json:"description"`
Up CommandMetadata `json:"up"`
Down CommandMetadata `json:"down"`
}
type CommandMetadata struct {
Parameters []Metadata `json:"parameters"`
}
type Metadata struct {
Name string `json:"name"`
Description string `json:"description"`
Required bool `json:"required"`
Type string `json:"type"`
Default string `json:"default,omitempty"`
}

View File

@ -1,176 +0,0 @@
# About
The Compose application model defines `service` as an abstraction for a computing unit managing (a subset of)
application needs, which can interact with other service by relying on network(s). Docker Compose is designed
to use the Docker Engine ("Moby") API to manage services as containers, but the abstraction _could_ also cover
many other runtimes, typically cloud services or services natively provided by host.
The Compose extensibility model has been designed to extend the `service` support to runtimes accessible through
third-party tooling.
# Architecture
Compose extensibility relies on the `provider` attribute to select the actual binary responsible for managing
the resource(s) needed to run a service.
```yaml
database:
provider:
type: awesomecloud
options:
type: mysql
size: 256
name: myAwesomeCloudDB
```
`provider.type` tells Compose the binary to run, which can be either:
- Another Docker CLI plugin (typically, `model` to run `docker-model`)
- An executable in user's `PATH`
If `provider.type` doesn't resolve into any of those, Compose will report an error and interrupt the `up` command.
To be a valid Compose extension, provider command *MUST* accept a `compose` command (which can be hidden)
with subcommands `up` and `down`.
## Up lifecycle
To execute an application's `up` lifecycle, Compose executes the provider's `compose up` command, passing
the project name, service name, and additional options. The `provider.options` are translated
into command line flags. For example:
```console
awesomecloud compose --project-name <NAME> up --type=mysql --size=256 "database"
```
> __Note:__ `project-name` _should_ be used by the provider to tag resources
> set for project, so that later execution with `down` subcommand releases
> all allocated resources set for the project.
## Communication with Compose
Providers can interact with Compose using `stdout` as a channel, sending JSON line delimited messages.
JSON messages MUST include a `type` and a `message` attribute.
```json
{ "type": "info", "message": "preparing mysql ..." }
```
`type` can be either:
- `info`: Reports status updates to the user. Compose will render message as the service state in the progress UI
- `error`: Let's the user know something went wrong with details about the error. Compose will render the message as the reason for the service failure.
- `setenv`: Let's the plugin tell Compose how dependent services can access the created resource. See next section for further details.
- `debug`: Those messages could help debugging the provider, but are not rendered to the user by default. They are rendered when Compose is started with `--verbose` flag.
```mermaid
sequenceDiagram
Shell->>Compose: docker compose up
Compose->>Provider: compose up --project-name=xx --foo=bar "database"
Provider--)Compose: json { "info": "pulling 25%" }
Compose-)Shell: pulling 25%
Provider--)Compose: json { "info": "pulling 50%" }
Compose-)Shell: pulling 50%
Provider--)Compose: json { "info": "pulling 75%" }
Compose-)Shell: pulling 75%
Provider--)Compose: json { "setenv": "URL=http://cloud.com/abcd:1234" }
Compose-)Compose: set DATABASE_URL
Provider-)Compose: EOF (command complete) exit 0
Compose-)Shell: service started
```
## Connection to a service managed by a provider
A service in the Compose application can declare dependency on a service managed by an external provider:
```yaml
services:
app:
image: myapp
depends_on:
- database
database:
provider:
type: awesomecloud
```
When the provider command sends a `setenv` JSON message, Compose injects the specified variable into any dependent service,
automatically prefixing it with the service name. For example, if `awesomecloud compose up` returns:
```json
{"type": "setenv", "message": "URL=https://awesomecloud.com/db:1234"}
```
Then the `app` service, which depends on the service managed by the provider, will receive a `DATABASE_URL` environment variable injected
into its runtime environment.
> __Note:__ The `compose up` provider command _MUST_ be idempotent. If resource is already running, the command _MUST_ set
> the same environment variables to ensure consistent configuration of dependent services.
## Down lifecycle
`down` lifecycle is equivalent to `up` with the `<provider> compose --project-name <NAME> down <SERVICE>` command.
The provider is responsible for releasing all resources associated with the service.
## Provide metadata about options
Compose extensions *MAY* optionally implement a `metadata` subcommand to provide information about the parameters accepted by the `up` and `down` commands.
The `metadata` subcommand takes no parameters and returns a JSON structure on the `stdout` channel that describes the parameters accepted by both the `up` and `down` commands, including whether each parameter is mandatory or optional.
```console
awesomecloud compose metadata
```
The expected JSON output format is:
```json
{
"description": "Manage services on AwesomeCloud",
"up": {
"parameters": [
{
"name": "type",
"description": "Database type (mysql, postgres, etc.)",
"required": true,
"type": "string"
},
{
"name": "size",
"description": "Database size in GB",
"required": false,
"type": "integer",
"default": "10"
},
{
"name": "name",
"description": "Name of the database to be created",
"required": true,
"type": "string"
}
]
},
"down": {
"parameters": [
{
"name": "name",
"description": "Name of the database to be removed",
"required": true,
"type": "string"
}
]
}
}
```
The top elements are:
- `description`: Human-readable description of the provider
- `up`: Object describing the parameters accepted by the `up` command
- `down`: Object describing the parameters accepted by the `down` command
And for each command parameter, you should include the following properties:
- `name`: The parameter name (without `--` prefix)
- `description`: Human-readable description of the parameter
- `required`: Boolean indicating if the parameter is mandatory
- `type`: Parameter type (`string`, `integer`, `boolean`, etc.)
- `default`: Default value (optional, only for non-required parameters)
- `enum`: List of possible values supported by the parameter separated by `,` (optional, only for parameters with a limited set of values)
This metadata allows Compose and other tools to understand the provider's interface and provide better user experience, such as validation, auto-completion, and documentation generation.
## Examples
See [example](examples/provider.go) for illustration on implementing this API in a command line

View File

@ -1,76 +1,69 @@
# docker compose # docker compose
```text
docker compose [-f <arg>...] [options] [COMMAND] [ARGS...]
```
<!---MARKER_GEN_START--> <!---MARKER_GEN_START-->
Define and run multi-container applications with Docker Define and run multi-container applications with Docker.
### Subcommands ### Subcommands
| Name | Description | | Name | Description |
|:--------------------------------|:----------------------------------------------------------------------------------------| |:--------------------------------|:-----------------------------------------------------------------------------------------|
| [`attach`](compose_attach.md) | Attach local standard input, output, and error streams to a service's running container | | [`attach`](compose_attach.md) | Attach local standard input, output, and error streams to a service's running container. |
| [`bridge`](compose_bridge.md) | Convert compose files into another model | | [`build`](compose_build.md) | Build or rebuild services |
| [`build`](compose_build.md) | Build or rebuild services | | [`config`](compose_config.md) | Parse, resolve and render compose file in canonical format |
| [`commit`](compose_commit.md) | Create a new image from a service container's changes | | [`cp`](compose_cp.md) | Copy files/folders between a service container and the local filesystem |
| [`config`](compose_config.md) | Parse, resolve and render compose file in canonical format | | [`create`](compose_create.md) | Creates containers for a service. |
| [`cp`](compose_cp.md) | Copy files/folders between a service container and the local filesystem | | [`down`](compose_down.md) | Stop and remove containers, networks |
| [`create`](compose_create.md) | Creates containers for a service | | [`events`](compose_events.md) | Receive real time events from containers. |
| [`down`](compose_down.md) | Stop and remove containers, networks | | [`exec`](compose_exec.md) | Execute a command in a running container. |
| [`events`](compose_events.md) | Receive real time events from containers | | [`images`](compose_images.md) | List images used by the created containers |
| [`exec`](compose_exec.md) | Execute a command in a running container | | [`kill`](compose_kill.md) | Force stop service containers. |
| [`export`](compose_export.md) | Export a service container's filesystem as a tar archive | | [`logs`](compose_logs.md) | View output from containers |
| [`images`](compose_images.md) | List images used by the created containers | | [`ls`](compose_ls.md) | List running compose projects |
| [`kill`](compose_kill.md) | Force stop service containers | | [`pause`](compose_pause.md) | Pause services |
| [`logs`](compose_logs.md) | View output from containers | | [`port`](compose_port.md) | Print the public port for a port binding. |
| [`ls`](compose_ls.md) | List running compose projects | | [`ps`](compose_ps.md) | List containers |
| [`pause`](compose_pause.md) | Pause services | | [`pull`](compose_pull.md) | Pull service images |
| [`port`](compose_port.md) | Print the public port for a port binding | | [`push`](compose_push.md) | Push service images |
| [`ps`](compose_ps.md) | List containers | | [`restart`](compose_restart.md) | Restart service containers |
| [`publish`](compose_publish.md) | Publish compose application | | [`rm`](compose_rm.md) | Removes stopped service containers |
| [`pull`](compose_pull.md) | Pull service images | | [`run`](compose_run.md) | Run a one-off command on a service. |
| [`push`](compose_push.md) | Push service images | | [`scale`](compose_scale.md) | Scale services |
| [`restart`](compose_restart.md) | Restart service containers | | [`start`](compose_start.md) | Start services |
| [`rm`](compose_rm.md) | Removes stopped service containers | | [`stats`](compose_stats.md) | Display a live stream of container(s) resource usage statistics |
| [`run`](compose_run.md) | Run a one-off command on a service | | [`stop`](compose_stop.md) | Stop services |
| [`scale`](compose_scale.md) | Scale services | | [`top`](compose_top.md) | Display the running processes |
| [`start`](compose_start.md) | Start services | | [`unpause`](compose_unpause.md) | Unpause services |
| [`stats`](compose_stats.md) | Display a live stream of container(s) resource usage statistics | | [`up`](compose_up.md) | Create and start containers |
| [`stop`](compose_stop.md) | Stop services | | [`version`](compose_version.md) | Show the Docker Compose version information |
| [`top`](compose_top.md) | Display the running processes | | [`wait`](compose_wait.md) | Block until the first service container stops |
| [`unpause`](compose_unpause.md) | Unpause services | | [`watch`](compose_watch.md) | Watch build context for service and rebuild/refresh containers when files are updated |
| [`up`](compose_up.md) | Create and start containers |
| [`version`](compose_version.md) | Show the Docker Compose version information |
| [`volumes`](compose_volumes.md) | List volumes |
| [`wait`](compose_wait.md) | Block until containers of all (or specified) services stop. |
| [`watch`](compose_watch.md) | Watch build context for service and rebuild/refresh containers when files are updated |
### Options ### Options
| Name | Type | Default | Description | | Name | Type | Default | Description |
|:-----------------------|:--------------|:--------|:----------------------------------------------------------------------------------------------------| |:-----------------------|:--------------|:--------|:----------------------------------------------------------------------------------------------------|
| `--all-resources` | `bool` | | Include all resources, even those not used by services |
| `--ansi` | `string` | `auto` | Control when to print ANSI control characters ("never"\|"always"\|"auto") | | `--ansi` | `string` | `auto` | Control when to print ANSI control characters ("never"\|"always"\|"auto") |
| `--compatibility` | `bool` | | Run compose in backward compatibility mode | | `--compatibility` | | | Run compose in backward compatibility mode |
| `--dry-run` | `bool` | | Execute command in dry run mode | | `--dry-run` | | | Execute command in dry run mode |
| `--env-file` | `stringArray` | | Specify an alternate environment file | | `--env-file` | `stringArray` | | Specify an alternate environment file. |
| `-f`, `--file` | `stringArray` | | Compose configuration files | | `-f`, `--file` | `stringArray` | | Compose configuration files |
| `--parallel` | `int` | `-1` | Control max parallelism, -1 for unlimited | | `--parallel` | `int` | `-1` | Control max parallelism, -1 for unlimited |
| `--profile` | `stringArray` | | Specify a profile to enable | | `--profile` | `stringArray` | | Specify a profile to enable |
| `--progress` | `string` | | Set type of progress output (auto, tty, plain, json, quiet) | | `--progress` | `string` | `auto` | Set type of progress output (auto, tty, plain, quiet) |
| `--project-directory` | `string` | | Specify an alternate working directory<br>(default: the path of the, first specified, Compose file) | | `--project-directory` | `string` | | Specify an alternate working directory<br>(default: the path of the, first specified, Compose file) |
| `-p`, `--project-name` | `string` | | Project name | | `-p`, `--project-name` | `string` | | Project name |
<!---MARKER_GEN_END--> <!---MARKER_GEN_END-->
## Examples ## Description
You can use the compose subcommand, `docker compose [-f <arg>...] [options] [COMMAND] [ARGS...]`, to build and manage
multiple services in Docker containers.
### Use `-f` to specify the name and path of one or more Compose files ### Use `-f` to specify the name and path of one or more Compose files
Use the `-f` flag to specify the location of a Compose [configuration file](/reference/compose-file/). Use the `-f` flag to specify the location of a Compose configuration file.
#### Specifying multiple Compose files #### Specifying multiple Compose files
You can supply multiple `-f` configuration files. When you supply multiple files, Compose combines them into a single You can supply multiple `-f` configuration files. When you supply multiple files, Compose combines them into a single
@ -80,10 +73,10 @@ to their predecessors.
For example, consider this command line: For example, consider this command line:
```console ```console
$ docker compose -f compose.yaml -f compose.admin.yaml run backup_db $ docker compose -f docker-compose.yml -f docker-compose.admin.yml run backup_db
``` ```
The `compose.yaml` file might specify a `webapp` service. The `docker-compose.yml` file might specify a `webapp` service.
```yaml ```yaml
services: services:
@ -94,7 +87,7 @@ services:
volumes: volumes:
- "/data" - "/data"
``` ```
If the `compose.admin.yaml` also specifies this same service, any matching fields override the previous file. If the `docker-compose.admin.yml` also specifies this same service, any matching fields override the previous file.
New values, add to the `webapp` service configuration. New values, add to the `webapp` service configuration.
```yaml ```yaml
@ -183,9 +176,6 @@ If flags are explicitly set on the command line, the associated environment vari
Setting the `COMPOSE_IGNORE_ORPHANS` environment variable to `true` stops docker compose from detecting orphaned Setting the `COMPOSE_IGNORE_ORPHANS` environment variable to `true` stops docker compose from detecting orphaned
containers for the project. containers for the project.
Setting the `COMPOSE_MENU` environment variable to `false` disables the helper menu when running `docker compose up`
in attached mode. Alternatively, you can also run `docker compose up --menu=false` to disable the helper menu.
### Use Dry Run mode to test your command ### Use Dry Run mode to test your command
Use `--dry-run` flag to test a command without changing your application stack state. Use `--dry-run` flag to test a command without changing your application stack state.
@ -209,4 +199,4 @@ $ docker compose --dry-run up --build -d
From the example above, you can see that the first step is to pull the image defined by `db` service, then build the `backend` service. From the example above, you can see that the first step is to pull the image defined by `db` service, then build the `backend` service.
Next, the containers are created. The `db` service is started, and the `backend` and `proxy` wait until the `db` service is healthy before starting. Next, the containers are created. The `db` service is started, and the `backend` and `proxy` wait until the `db` service is healthy before starting.
Dry Run mode works with almost all commands. You cannot use Dry Run mode with a command that doesn't change the state of a Compose stack such as `ps`, `ls`, `logs` for example. Dry Run mode works with almost all commands. You cannot use Dry Run mode with a command that doesn't change the state of a Compose stack such as `ps`, `ls`, `logs` for example.

View File

@ -1,7 +1,7 @@
# docker compose alpha dry-run # docker compose alpha dry-run
<!---MARKER_GEN_START--> <!---MARKER_GEN_START-->
Dry run command allows you to test a command without applying changes Dry run command allows you to test a command without applying changes.
<!---MARKER_GEN_END--> <!---MARKER_GEN_END-->

View File

@ -1,17 +0,0 @@
# docker compose alpha generate
<!---MARKER_GEN_START-->
EXPERIMENTAL - Generate a Compose file from existing containers
### Options
| Name | Type | Default | Description |
|:----------------|:---------|:--------|:------------------------------------------|
| `--dry-run` | `bool` | | Execute command in dry run mode |
| `--format` | `string` | `yaml` | Format the output. Values: [yaml \| json] |
| `--name` | `string` | | Project name to set in the Compose file |
| `--project-dir` | `string` | | Directory to use for the project |
<!---MARKER_GEN_END-->

View File

@ -7,11 +7,9 @@ Publish compose application
| Name | Type | Default | Description | | Name | Type | Default | Description |
|:--------------------------|:---------|:--------|:-------------------------------------------------------------------------------| |:--------------------------|:---------|:--------|:-------------------------------------------------------------------------------|
| `--dry-run` | `bool` | | Execute command in dry run mode | | `--dry-run` | | | Execute command in dry run mode |
| `--oci-version` | `string` | | OCI image/artifact specification version (automatically determined by default) | | `--oci-version` | `string` | | OCI Image/Artifact specification version (automatically determined by default) |
| `--resolve-image-digests` | `bool` | | Pin image tags to digests | | `--resolve-image-digests` | | | Pin image tags to digests. |
| `--with-env` | `bool` | | Include environment variables in the published OCI artifact |
| `-y`, `--yes` | `bool` | | Assume "yes" as answer to all prompts |
<!---MARKER_GEN_END--> <!---MARKER_GEN_END-->

View File

@ -1,14 +1,14 @@
# docker compose alpha scale # docker compose alpha scale
<!---MARKER_GEN_START--> <!---MARKER_GEN_START-->
Scale services Scale services.
### Options ### Options
| Name | Type | Default | Description | | Name | Type | Default | Description |
|:------------|:-----|:--------|:--------------------------------| |:------------|:-----|:--------|:--------------------------------|
| `--dry-run` | | | Execute command in dry run mode | | `--dry-run` | | | Execute command in dry run mode |
| `--no-deps` | | | Don't start linked services | | `--no-deps` | | | Don't start linked services |
<!---MARKER_GEN_END--> <!---MARKER_GEN_END-->

View File

@ -5,14 +5,14 @@ EXPERIMENTAL - Generate a graphviz graph from your compose file
### Options ### Options
| Name | Type | Default | Description | | Name | Type | Default | Description |
|:---------------------|:-------|:--------|:---------------------------------------------------------------------------------------------------| |:---------------------|:------|:--------|:---------------------------------------------------------------------------------------------------|
| `--dry-run` | `bool` | | Execute command in dry run mode | | `--dry-run` | | | Execute command in dry run mode |
| `--image` | `bool` | | Include service's image name in output graph | | `--image` | | | Include service's image name in output graph |
| `--indentation-size` | `int` | `1` | Number of tabs or spaces to use for indentation | | `--indentation-size` | `int` | `1` | Number of tabs or spaces to use for indentation |
| `--networks` | `bool` | | Include service's attached networks in output graph | | `--networks` | | | Include service's attached networks in output graph |
| `--ports` | `bool` | | Include service's exposed ports in output graph | | `--ports` | | | Include service's exposed ports in output graph |
| `--spaces` | `bool` | | If given, space character ' ' will be used to indent,<br>otherwise tab character '\t' will be used | | `--spaces` | | | If given, space character ' ' will be used to indent,<br>otherwise tab character '\t' will be used |
<!---MARKER_GEN_END--> <!---MARKER_GEN_END-->

View File

@ -1,17 +1,18 @@
# docker compose attach # docker compose attach
<!---MARKER_GEN_START--> <!---MARKER_GEN_START-->
Attach local standard input, output, and error streams to a service's running container Attach local standard input, output, and error streams to a service's running container.
### Options ### Options
| Name | Type | Default | Description | | Name | Type | Default | Description |
|:----------------|:---------|:--------|:----------------------------------------------------------| |:----------------|:---------|:--------|:----------------------------------------------------------|
| `--detach-keys` | `string` | | Override the key sequence for detaching from a container. | | `--detach-keys` | `string` | | Override the key sequence for detaching from a container. |
| `--dry-run` | `bool` | | Execute command in dry run mode | | `--dry-run` | | | Execute command in dry run mode |
| `--index` | `int` | `0` | index of the container if service has multiple replicas. | | `--index` | `int` | `0` | index of the container if service has multiple replicas. |
| `--no-stdin` | `bool` | | Do not attach STDIN | | `--no-stdin` | | | Do not attach STDIN |
| `--sig-proxy` | `bool` | `true` | Proxy all received signals to the process | | `--sig-proxy` | | | Proxy all received signals to the process |
<!---MARKER_GEN_END--> <!---MARKER_GEN_END-->

View File

@ -1,22 +0,0 @@
# docker compose bridge
<!---MARKER_GEN_START-->
Convert compose files into another model
### Subcommands
| Name | Description |
|:-------------------------------------------------------|:-----------------------------------------------------------------------------|
| [`convert`](compose_bridge_convert.md) | Convert compose files to Kubernetes manifests, Helm charts, or another model |
| [`transformations`](compose_bridge_transformations.md) | Manage transformation images |
### Options
| Name | Type | Default | Description |
|:------------|:-------|:--------|:--------------------------------|
| `--dry-run` | `bool` | | Execute command in dry run mode |
<!---MARKER_GEN_END-->

View File

@ -1,17 +0,0 @@
# docker compose bridge convert
<!---MARKER_GEN_START-->
Convert compose files to Kubernetes manifests, Helm charts, or another model
### Options
| Name | Type | Default | Description |
|:-------------------------|:--------------|:--------|:-------------------------------------------------------------------------------------|
| `--dry-run` | `bool` | | Execute command in dry run mode |
| `-o`, `--output` | `string` | `out` | The output directory for the Kubernetes resources |
| `--templates` | `string` | | Directory containing transformation templates |
| `-t`, `--transformation` | `stringArray` | | Transformation to apply to compose model (default: docker/compose-bridge-kubernetes) |
<!---MARKER_GEN_END-->

View File

@ -1,22 +0,0 @@
# docker compose bridge transformations
<!---MARKER_GEN_START-->
Manage transformation images
### Subcommands
| Name | Description |
|:-----------------------------------------------------|:-------------------------------|
| [`create`](compose_bridge_transformations_create.md) | Create a new transformation |
| [`list`](compose_bridge_transformations_list.md) | List available transformations |
### Options
| Name | Type | Default | Description |
|:------------|:-------|:--------|:--------------------------------|
| `--dry-run` | `bool` | | Execute command in dry run mode |
<!---MARKER_GEN_END-->

View File

@ -1,15 +0,0 @@
# docker compose bridge transformations create
<!---MARKER_GEN_START-->
Create a new transformation
### Options
| Name | Type | Default | Description |
|:---------------|:---------|:--------|:----------------------------------------------------------------------------|
| `--dry-run` | `bool` | | Execute command in dry run mode |
| `-f`, `--from` | `string` | | Existing transformation to copy (default: docker/compose-bridge-kubernetes) |
<!---MARKER_GEN_END-->

View File

@ -1,20 +0,0 @@
# docker compose bridge transformations list
<!---MARKER_GEN_START-->
List available transformations
### Aliases
`docker compose bridge transformations list`, `docker compose bridge transformations ls`
### Options
| Name | Type | Default | Description |
|:----------------|:---------|:--------|:-------------------------------------------|
| `--dry-run` | `bool` | | Execute command in dry run mode |
| `--format` | `string` | `table` | Format the output. Values: [table \| json] |
| `-q`, `--quiet` | `bool` | | Only display transformer names |
<!---MARKER_GEN_END-->

View File

@ -1,46 +1,34 @@
# docker compose build # docker compose build
<!---MARKER_GEN_START--> <!---MARKER_GEN_START-->
Services are built once and then tagged, by default as `project-service`. Build or rebuild services
If the Compose file specifies an
[image](https://github.com/compose-spec/compose-spec/blob/main/spec.md#image) name,
the image is tagged with that name, substituting any variables beforehand. See
[variable interpolation](https://github.com/compose-spec/compose-spec/blob/main/spec.md#interpolation).
If you change a service's `Dockerfile` or the contents of its build directory,
run `docker compose build` to rebuild it.
### Options ### Options
| Name | Type | Default | Description | | Name | Type | Default | Description |
|:----------------------|:--------------|:--------|:------------------------------------------------------------------------------------------------------------| |:----------------------|:--------------|:--------|:------------------------------------------------------------------------------------------------------------|
| `--build-arg` | `stringArray` | | Set build-time variables for services | | `--build-arg` | `stringArray` | | Set build-time variables for services. |
| `--builder` | `string` | | Set builder to use | | `--builder` | `string` | | Set builder to use. |
| `--check` | `bool` | | Check build configuration | | `--dry-run` | | | Execute command in dry run mode |
| `--dry-run` | `bool` | | Execute command in dry run mode |
| `-m`, `--memory` | `bytes` | `0` | Set memory limit for the build container. Not supported by BuildKit. | | `-m`, `--memory` | `bytes` | `0` | Set memory limit for the build container. Not supported by BuildKit. |
| `--no-cache` | `bool` | | Do not use cache when building the image | | `--no-cache` | | | Do not use cache when building the image |
| `--print` | `bool` | | Print equivalent bake file | | `--pull` | | | Always attempt to pull a newer version of the image. |
| `--provenance` | `string` | | Add a provenance attestation | | `--push` | | | Push service images. |
| `--pull` | `bool` | | Always attempt to pull a newer version of the image | | `-q`, `--quiet` | | | Don't print anything to STDOUT |
| `--push` | `bool` | | Push service images |
| `-q`, `--quiet` | `bool` | | Suppress the build output |
| `--sbom` | `string` | | Add a SBOM attestation |
| `--ssh` | `string` | | Set SSH authentications used when building service images. (use 'default' for using your default SSH Agent) | | `--ssh` | `string` | | Set SSH authentications used when building service images. (use 'default' for using your default SSH Agent) |
| `--with-dependencies` | `bool` | | Also build dependencies (transitively) | | `--with-dependencies` | | | Also build dependencies (transitively). |
<!---MARKER_GEN_END--> <!---MARKER_GEN_END-->
## Description ## Description
Services are built once and then tagged, by default as `project-service`. Services are built once and then tagged, by default as `project_service`.
If the Compose file specifies an If the Compose file specifies an
[image](https://github.com/compose-spec/compose-spec/blob/main/spec.md#image) name, [image](https://github.com/compose-spec/compose-spec/blob/master/spec.md#image) name,
the image is tagged with that name, substituting any variables beforehand. See the image is tagged with that name, substituting any variables beforehand. See
[variable interpolation](https://github.com/compose-spec/compose-spec/blob/main/spec.md#interpolation). [variable interpolation](https://github.com/compose-spec/compose-spec/blob/master/spec.md#interpolation).
If you change a service's `Dockerfile` or the contents of its build directory, If you change a service's `Dockerfile` or the contents of its build directory,
run `docker compose build` to rebuild it. run `docker compose build` to rebuild it.

View File

@ -1,19 +0,0 @@
# docker compose commit
<!---MARKER_GEN_START-->
Create a new image from a service container's changes
### Options
| Name | Type | Default | Description |
|:------------------|:---------|:--------|:-----------------------------------------------------------|
| `-a`, `--author` | `string` | | Author (e.g., "John Hannibal Smith <hannibal@a-team.com>") |
| `-c`, `--change` | `list` | | Apply Dockerfile instruction to the created image |
| `--dry-run` | `bool` | | Execute command in dry run mode |
| `--index` | `int` | `0` | index of the container if service has multiple replicas. |
| `-m`, `--message` | `string` | | Commit message |
| `-p`, `--pause` | `bool` | `true` | Pause container during commit |
<!---MARKER_GEN_END-->

View File

@ -1,34 +1,30 @@
# docker compose convert # docker compose convert
<!---MARKER_GEN_START--> <!---MARKER_GEN_START-->
`docker compose config` renders the actual data model to be applied on the Docker Engine. Parse, resolve and render compose file in canonical format
It merges the Compose files set by `-f` flags, resolves variables in the Compose file, and expands short-notation into
the canonical format. ### Aliases
`docker compose config`, `docker compose convert`
### Options ### Options
| Name | Type | Default | Description | | Name | Type | Default | Description |
|:--------------------------|:---------|:--------|:----------------------------------------------------------------------------| |:--------------------------|:---------|:--------|:----------------------------------------------------------------------------|
| `--dry-run` | `bool` | | Execute command in dry run mode | | `--dry-run` | | | Execute command in dry run mode |
| `--environment` | `bool` | | Print environment used for interpolation. | | `--format` | `string` | `yaml` | Format the output. Values: [yaml \| json] |
| `--format` | `string` | | Format the output. Values: [yaml \| json] |
| `--hash` | `string` | | Print the service config hash, one per line. | | `--hash` | `string` | | Print the service config hash, one per line. |
| `--images` | `bool` | | Print the image names, one per line. | | `--images` | | | Print the image names, one per line. |
| `--lock-image-digests` | `bool` | | Produces an override file with image digests | | `--no-consistency` | | | Don't check model consistency - warning: may produce invalid Compose output |
| `--models` | `bool` | | Print the model names, one per line. | | `--no-interpolate` | | | Don't interpolate environment variables. |
| `--networks` | `bool` | | Print the network names, one per line. | | `--no-normalize` | | | Don't normalize compose model. |
| `--no-consistency` | `bool` | | Don't check model consistency - warning: may produce invalid Compose output | | `--no-path-resolution` | | | Don't resolve file paths. |
| `--no-env-resolution` | `bool` | | Don't resolve service env files |
| `--no-interpolate` | `bool` | | Don't interpolate environment variables |
| `--no-normalize` | `bool` | | Don't normalize compose model |
| `--no-path-resolution` | `bool` | | Don't resolve file paths |
| `-o`, `--output` | `string` | | Save to file (default to stdout) | | `-o`, `--output` | `string` | | Save to file (default to stdout) |
| `--profiles` | `bool` | | Print the profile names, one per line. | | `--profiles` | | | Print the profile names, one per line. |
| `-q`, `--quiet` | `bool` | | Only validate the configuration, don't print anything | | `-q`, `--quiet` | | | Only validate the configuration, don't print anything. |
| `--resolve-image-digests` | `bool` | | Pin image tags to digests | | `--resolve-image-digests` | | | Pin image tags to digests. |
| `--services` | `bool` | | Print the service names, one per line. | | `--services` | | | Print the service names, one per line. |
| `--variables` | `bool` | | Print model variables and default values. | | `--volumes` | | | Print the volume names, one per line. |
| `--volumes` | `bool` | | Print the volume names, one per line. |
<!---MARKER_GEN_END--> <!---MARKER_GEN_END-->

View File

@ -5,13 +5,12 @@ Copy files/folders between a service container and the local filesystem
### Options ### Options
| Name | Type | Default | Description | | Name | Type | Default | Description |
|:----------------------|:-------|:--------|:--------------------------------------------------------| |:----------------------|:------|:--------|:--------------------------------------------------------|
| `--all` | `bool` | | Include containers created by the run command | | `-a`, `--archive` | | | Archive mode (copy all uid/gid information) |
| `-a`, `--archive` | `bool` | | Archive mode (copy all uid/gid information) | | `--dry-run` | | | Execute command in dry run mode |
| `--dry-run` | `bool` | | Execute command in dry run mode | | `-L`, `--follow-link` | | | Always follow symbol link in SRC_PATH |
| `-L`, `--follow-link` | `bool` | | Always follow symbol link in SRC_PATH | | `--index` | `int` | `0` | index of the container if service has multiple replicas |
| `--index` | `int` | `0` | Index of the container if service has multiple replicas |
<!---MARKER_GEN_END--> <!---MARKER_GEN_END-->

View File

@ -1,22 +1,20 @@
# docker compose create # docker compose create
<!---MARKER_GEN_START--> <!---MARKER_GEN_START-->
Creates containers for a service Creates containers for a service.
### Options ### Options
| Name | Type | Default | Description | | Name | Type | Default | Description |
|:-------------------|:--------------|:---------|:----------------------------------------------------------------------------------------------| |:-------------------|:--------------|:---------|:----------------------------------------------------------------------------------------------|
| `--build` | `bool` | | Build images before starting containers | | `--build` | | | Build images before starting containers. |
| `--dry-run` | `bool` | | Execute command in dry run mode | | `--dry-run` | | | Execute command in dry run mode |
| `--force-recreate` | `bool` | | Recreate containers even if their configuration and image haven't changed | | `--force-recreate` | | | Recreate containers even if their configuration and image haven't changed. |
| `--no-build` | `bool` | | Don't build an image, even if it's policy | | `--no-build` | | | Don't build an image, even if it's policy. |
| `--no-recreate` | `bool` | | If containers already exist, don't recreate them. Incompatible with --force-recreate. | | `--no-recreate` | | | If containers already exist, don't recreate them. Incompatible with --force-recreate. |
| `--pull` | `string` | `policy` | Pull image before running ("always"\|"missing"\|"never"\|"build") | | `--pull` | `string` | `policy` | Pull image before running ("always"\|"missing"\|"never"\|"build") |
| `--quiet-pull` | `bool` | | Pull without printing progress information | | `--remove-orphans` | | | Remove containers for services not defined in the Compose file. |
| `--remove-orphans` | `bool` | | Remove containers for services not defined in the Compose file |
| `--scale` | `stringArray` | | Scale SERVICE to NUM instances. Overrides the `scale` setting in the Compose file if present. | | `--scale` | `stringArray` | | Scale SERVICE to NUM instances. Overrides the `scale` setting in the Compose file if present. |
| `-y`, `--yes` | `bool` | | Assume "yes" as answer to all prompts and run non-interactively |
<!---MARKER_GEN_END--> <!---MARKER_GEN_END-->

View File

@ -1,29 +1,17 @@
# docker compose down # docker compose down
<!---MARKER_GEN_START--> <!---MARKER_GEN_START-->
Stops containers and removes containers, networks, volumes, and images created by `up`. Stop and remove containers, networks
By default, the only things removed are:
- Containers for services defined in the Compose file.
- Networks defined in the networks section of the Compose file.
- The default network, if one is used.
Networks and volumes defined as external are never removed.
Anonymous volumes are not removed by default. However, as they dont have a stable name, they are not automatically
mounted by a subsequent `up`. For data that needs to persist between updates, use explicit paths as bind mounts or
named volumes.
### Options ### Options
| Name | Type | Default | Description | | Name | Type | Default | Description |
|:-------------------|:---------|:--------|:------------------------------------------------------------------------------------------------------------------------| |:-------------------|:---------|:--------|:-------------------------------------------------------------------------------------------------------------------------|
| `--dry-run` | `bool` | | Execute command in dry run mode | | `--dry-run` | | | Execute command in dry run mode |
| `--remove-orphans` | `bool` | | Remove containers for services not defined in the Compose file | | `--remove-orphans` | | | Remove containers for services not defined in the Compose file. |
| `--rmi` | `string` | | Remove images used by services. "local" remove only images that don't have a custom tag ("local"\|"all") | | `--rmi` | `string` | | Remove images used by services. "local" remove only images that don't have a custom tag ("local"\|"all") |
| `-t`, `--timeout` | `int` | `0` | Specify a shutdown timeout in seconds | | `-t`, `--timeout` | `int` | `0` | Specify a shutdown timeout in seconds |
| `-v`, `--volumes` | `bool` | | Remove named volumes declared in the "volumes" section of the Compose file and anonymous volumes attached to containers | | `-v`, `--volumes` | | | Remove named volumes declared in the "volumes" section of the Compose file and anonymous volumes attached to containers. |
<!---MARKER_GEN_END--> <!---MARKER_GEN_END-->

View File

@ -1,34 +1,14 @@
# docker compose events # docker compose events
<!---MARKER_GEN_START--> <!---MARKER_GEN_START-->
Stream container events for every container in the project. Receive real time events from containers.
With the `--json` flag, a json object is printed one per line with the format:
```json
{
"time": "2015-11-20T18:01:03.615550",
"type": "container",
"action": "create",
"id": "213cf7...5fc39a",
"service": "web",
"attributes": {
"name": "application_web_1",
"image": "alpine:edge"
}
}
```
The events that can be received using this can be seen [here](/reference/cli/docker/system/events/#object-types).
### Options ### Options
| Name | Type | Default | Description | | Name | Type | Default | Description |
|:------------|:---------|:--------|:------------------------------------------| |:------------|:-----|:--------|:------------------------------------------|
| `--dry-run` | `bool` | | Execute command in dry run mode | | `--dry-run` | | | Execute command in dry run mode |
| `--json` | `bool` | | Output events as a stream of json objects | | `--json` | | | Output events as a stream of json objects |
| `--since` | `string` | | Show all events created since timestamp |
| `--until` | `string` | | Stream events until this timestamp |
<!---MARKER_GEN_END--> <!---MARKER_GEN_END-->
@ -53,4 +33,4 @@ With the `--json` flag, a json object is printed one per line with the format:
} }
``` ```
The events that can be received using this can be seen [here](https://docs.docker.com/reference/cli/docker/system/events/#object-types). The events that can be received using this can be seen [here](https://docs.docker.com/engine/reference/commandline/system_events/#object-types).

View File

@ -1,29 +1,20 @@
# docker compose exec # docker compose exec
<!---MARKER_GEN_START--> <!---MARKER_GEN_START-->
This is the equivalent of `docker exec` targeting a Compose service. Execute a command in a running container.
With this subcommand, you can run arbitrary commands in your services. Commands allocate a TTY by default, so
you can use a command such as `docker compose exec web sh` to get an interactive prompt.
By default, Compose will enter container in interactive mode and allocate a TTY, while the equivalent `docker exec`
command requires passing `--interactive --tty` flags to get the same behavior. Compose also support those two flags
to offer a smooth migration between commands, whenever they are no-op by default. Still, `interactive` can be used to
force disabling interactive mode (`--interactive=false`), typically when `docker compose exec` command is used inside
a script.
### Options ### Options
| Name | Type | Default | Description | | Name | Type | Default | Description |
|:------------------|:--------------|:--------|:---------------------------------------------------------------------------------| |:------------------|:--------------|:--------|:---------------------------------------------------------------------------------|
| `-d`, `--detach` | `bool` | | Detached mode: Run command in the background | | `-d`, `--detach` | | | Detached mode: Run command in the background. |
| `--dry-run` | `bool` | | Execute command in dry run mode | | `--dry-run` | | | Execute command in dry run mode |
| `-e`, `--env` | `stringArray` | | Set environment variables | | `-e`, `--env` | `stringArray` | | Set environment variables |
| `--index` | `int` | `0` | Index of the container if service has multiple replicas | | `--index` | `int` | `0` | index of the container if service has multiple replicas |
| `-T`, `--no-tty` | `bool` | `true` | Disable pseudo-TTY allocation. By default `docker compose exec` allocates a TTY. | | `-T`, `--no-TTY` | | | Disable pseudo-TTY allocation. By default `docker compose exec` allocates a TTY. |
| `--privileged` | `bool` | | Give extended privileges to the process | | `--privileged` | | | Give extended privileges to the process. |
| `-u`, `--user` | `string` | | Run the command as this user | | `-u`, `--user` | `string` | | Run the command as this user. |
| `-w`, `--workdir` | `string` | | Path to workdir directory for this command | | `-w`, `--workdir` | `string` | | Path to workdir directory for this command. |
<!---MARKER_GEN_END--> <!---MARKER_GEN_END-->
@ -34,9 +25,3 @@ This is the equivalent of `docker exec` targeting a Compose service.
With this subcommand, you can run arbitrary commands in your services. Commands allocate a TTY by default, so With this subcommand, you can run arbitrary commands in your services. Commands allocate a TTY by default, so
you can use a command such as `docker compose exec web sh` to get an interactive prompt. you can use a command such as `docker compose exec web sh` to get an interactive prompt.
By default, Compose will enter container in interactive mode and allocate a TTY, while the equivalent `docker exec`
command requires passing `--interactive --tty` flags to get the same behavior. Compose also support those two flags
to offer a smooth migration between commands, whenever they are no-op by default. Still, `interactive` can be used to
force disabling interactive mode (`--interactive=false`), typically when `docker compose exec` command is used inside
a script.

View File

@ -1,16 +0,0 @@
# docker compose export
<!---MARKER_GEN_START-->
Export a service container's filesystem as a tar archive
### Options
| Name | Type | Default | Description |
|:-----------------|:---------|:--------|:---------------------------------------------------------|
| `--dry-run` | `bool` | | Execute command in dry run mode |
| `--index` | `int` | `0` | index of the container if service has multiple replicas. |
| `-o`, `--output` | `string` | | Write to a file, instead of STDOUT |
<!---MARKER_GEN_END-->

View File

@ -5,11 +5,11 @@ List images used by the created containers
### Options ### Options
| Name | Type | Default | Description | | Name | Type | Default | Description |
|:----------------|:---------|:--------|:-------------------------------------------| |:----------------|:---------|:--------|:--------------------------------------------|
| `--dry-run` | `bool` | | Execute command in dry run mode | | `--dry-run` | | | Execute command in dry run mode |
| `--format` | `string` | `table` | Format the output. Values: [table \| json] | | `--format` | `string` | `table` | Format the output. Values: [table \| json]. |
| `-q`, `--quiet` | `bool` | | Only display IDs | | `-q`, `--quiet` | | | Only display IDs |
<!---MARKER_GEN_END--> <!---MARKER_GEN_END-->

Some files were not shown because too many files have changed in this diff Show More