Compare commits

...

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

681 changed files with 55812 additions and 3496 deletions

1
.dockerignore Normal file
View File

@ -0,0 +1 @@
bin/

2
.gitattributes vendored Normal file
View File

@ -0,0 +1,2 @@
core.autocrlf false
*.golden text eol=lf

2
.github/CODEOWNERS vendored Normal file
View File

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

55
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@ -0,0 +1,55 @@
name: 🐞 Bug
description: File a bug/issue
title: "[BUG] <title>"
labels: ['status/0-triage', 'kind/bug']
body:
- type: textarea
attributes:
label: Description
description: |
Briefly describe the problem you are having.
Include both the current behavior (what you are seeing) as well as what you expected to happen.
validations:
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
attributes:
label: Steps To Reproduce
description: Steps to reproduce the behavior.
placeholder: |
1. In this environment...
2. With this config...
3. Run '...'
4. See error...
validations:
required: false
- type: textarea
attributes:
label: Compose Version
description: |
Paste output of `docker compose version` and `docker-compose version`.
render: Text
validations:
required: false
- type: textarea
attributes:
label: Docker Environment
description: Paste output of `docker info`.
render: Text
validations:
required: false
- type: textarea
attributes:
label: Anything else?
description: |
Links? References? Anything that will give us more context about the issue you are encountering!
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
validations:
required: false

11
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1,11 @@
blank_issues_enabled: true
contact_links:
- name: Docker Community Slack
url: https://dockr.ly/slack
about: 'Use the #docker-compose channel'
- name: Docker Support Forums
url: https://forums.docker.com/c/open-source-projects/compose/15
about: 'Use the "Open Source Projects > Compose" category'
- name: 'Ask on Stack Overflow'
url: https://stackoverflow.com/questions/tagged/docker-compose
about: 'Use the [docker-compose] tag when creating new questions'

View File

@ -0,0 +1,13 @@
name: Feature request
description: Missing functionality? Come tell us about it!
labels:
- kind/feature
- status/0-triage
body:
- type: textarea
id: description
attributes:
label: Description
description: What is the feature you want to see?
validations:
required: true

6
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,6 @@
**What I did**
**Related issue**
<!-- If this is a bug fix, make sure your description includes "fixes #xxxx", or "closes #xxxx" -->
**(not mandatory) A picture of a cute animal, if possible in relation to what you did**

44
.github/SECURITY.md vendored Normal file
View File

@ -0,0 +1,44 @@
# 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.

23
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,23 @@
version: 2
updates:
- package-ecosystem: gomod
directory: /
schedule:
interval: daily
ignore:
# docker + moby deps require coordination
- dependency-name: "github.com/docker/buildx"
# buildx is still 0.x
update-types: ["version-update:semver-minor"]
- dependency-name: "github.com/moby/buildkit"
# buildkit is still 0.x
update-types: [ "version-update:semver-minor" ]
- dependency-name: "github.com/docker/cli"
update-types: ["version-update:semver-major"]
- dependency-name: "github.com/docker/docker"
update-types: ["version-update:semver-major"]
- dependency-name: "github.com/containerd/containerd"
# containerd major/minor must be kept in sync with moby
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/*"

59
.github/stale.yml vendored Normal file
View File

@ -0,0 +1,59 @@
# Configuration for probot-stale - https://github.com/probot/stale
# Number of days of inactivity before an Issue or Pull Request becomes stale
daysUntilStale: 90
# 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.
daysUntilClose: 7
# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled)
onlyLabels: []
# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
exemptLabels:
- "kind/feature"
# Set to true to ignore issues in a project (defaults to false)
exemptProjects: false
# Set to true to ignore issues in a milestone (defaults to false)
exemptMilestones: false
# Set to true to ignore issues with an assignee (defaults to false)
exemptAssignees: true
# Label to use when marking as stale
staleLabel: stale
# Comment to post when marking as stale. Set to `false` to disable
markComment: >
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.
# Comment to post when removing the stale label.
unmarkComment: >
This issue has been automatically marked as not stale anymore due to the recent activity.
# Comment to post when closing a stale Issue or Pull Request.
closeComment: >
This issue has been automatically closed because it had not recent activity during the stale period.
# Limit the number of actions per hour, from 1-30. Default is 30
limitPerRun: 30
# Limit to only `issues` or `pulls`
only: issues
# Optionally, specify configuration settings that are specific to just 'issues' or 'pulls':
# pulls:
# daysUntilStale: 30
# markComment: >
# This pull request 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.
# issues:
# exemptLabels:
# - confirmed

333
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,333 @@
name: ci
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
push:
branches:
- 'main'
tags:
- 'v*'
pull_request:
workflow_dispatch:
inputs:
debug_enabled:
description: 'To run with tmate enter "debug_enabled"'
required: false
default: "false"
permissions:
contents: read # to fetch code (actions/checkout)
jobs:
prepare:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.platforms.outputs.matrix }}
steps:
-
name: Checkout
uses: actions/checkout@v4
-
name: Create matrix
id: platforms
run: |
echo matrix=$(docker buildx bake binary-cross --print | jq -cr '.target."binary-cross".platforms') >> $GITHUB_OUTPUT
-
name: Show matrix
run: |
echo ${{ steps.platforms.outputs.matrix }}
validate:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
target:
- lint
- validate-go-mod
- validate-headers
- validate-docs
steps:
-
name: Checkout
uses: actions/checkout@v4
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
-
name: Run
run: |
make ${{ matrix.target }}
binary:
runs-on: ubuntu-latest
needs:
- prepare
strategy:
fail-fast: false
matrix:
platform: ${{ fromJson(needs.prepare.outputs.matrix) }}
steps:
-
name: Checkout
uses: actions/checkout@v4
-
name: Prepare
run: |
platform=${MATRIX_PLATFORM}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
env:
MATRIX_PLATFORM: ${{ matrix.platform }}
-
name: Set up QEMU
uses: docker/setup-qemu-action@v3
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
-
name: Build
uses: docker/bake-action@v6
with:
source: .
targets: release
provenance: mode=max
sbom: true
set: |
*.platform=${{ matrix.platform }}
*.cache-from=type=gha,scope=binary-${{ env.PLATFORM_PAIR }}
*.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
uses: actions/upload-artifact@v4
with:
name: compose-${{ env.PLATFORM_PAIR }}
path: ./bin/release
if-no-files-found: error
test:
runs-on: ubuntu-latest
steps:
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
-
name: Test
uses: docker/bake-action@v6
with:
targets: test
set: |
*.cache-from=type=gha,scope=test
*.cache-to=type=gha,scope=test
-
name: Gather coverage data
uses: actions/upload-artifact@v4
with:
name: coverage-data-unit
path: bin/coverage/unit/
if-no-files-found: error
-
name: Unit Test Summary
uses: test-summary/action@v2
with:
paths: bin/coverage/unit/report.xml
if: always()
e2e:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
mode:
- plugin
- standalone
engine:
- 26
- 27
- 28
steps:
- name: Prepare
run: |
mode=${{ matrix.mode }}
engine=${{ matrix.engine }}
echo "MODE_ENGINE_PAIR=${mode}-${engine}" >> $GITHUB_ENV
- name: Checkout
uses: actions/checkout@v4
- 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:
go-version-file: 'go.mod'
check-latest: true
cache: true
- name: Build example provider
run: make example-provider
- name: Build
uses: docker/bake-action@v6
with:
source: .
targets: binary-with-coverage
set: |
*.cache-from=type=gha,scope=binary-linux-amd64
*.cache-from=type=gha,scope=binary-e2e-${{ matrix.mode }}
*.cache-to=type=gha,scope=binary-e2e-${{ matrix.mode }},mode=max
env:
BUILD_TAGS: e2e
- name: Setup tmate session
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled }}
uses: mxschmitt/action-tmate@8b4e4ac71822ed7e0ad5fb3d1c33483e9e8fb270 # v3.11
with:
limit-access-to-actor: true
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Test plugin mode
if: ${{ matrix.mode == 'plugin' }}
run: |
rm -rf ./bin/coverage/e2e
mkdir -p ./bin/coverage/e2e
make e2e-compose GOCOVERDIR=bin/coverage/e2e TEST_FLAGS="-v"
- name: Gather coverage data
if: ${{ matrix.mode == 'plugin' }}
uses: actions/upload-artifact@v4
with:
name: coverage-data-e2e-${{ env.MODE_ENGINE_PAIR }}
path: bin/coverage/e2e/
if-no-files-found: error
- name: Test standalone mode
if: ${{ matrix.mode == 'standalone' }}
run: |
rm -f /usr/local/bin/docker-compose
cp bin/build/docker-compose /usr/local/bin
make e2e-compose-standalone
- name: e2e Test Summary
uses: test-summary/action@v2
with:
paths: /tmp/report/report.xml
if: always()
coverage:
runs-on: ubuntu-latest
needs:
- test
- e2e
steps:
# codecov won't process the report without the source code available
- name: Checkout
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: 'go.mod'
check-latest: true
- name: Download unit test coverage
uses: actions/download-artifact@v4
with:
name: coverage-data-unit
path: coverage/unit
merge-multiple: true
- name: Download E2E test coverage
uses: actions/download-artifact@v4
with:
pattern: coverage-data-e2e-*
path: coverage/e2e
merge-multiple: true
- name: Merge coverage reports
run: |
go tool covdata textfmt -i=./coverage/unit,./coverage/e2e -o ./coverage.txt
- name: Store coverage report in GitHub Actions
uses: actions/upload-artifact@v4
with:
name: go-covdata-txt
path: ./coverage.txt
if-no-files-found: error
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
files: ./coverage.txt
release:
permissions:
contents: write # to create a release (ncipollo/release-action)
runs-on: ubuntu-latest
needs:
- binary
steps:
-
name: Checkout
uses: actions/checkout@v4
-
name: Download artifacts
uses: actions/download-artifact@v4
with:
pattern: compose-*
path: ./bin/release
merge-multiple: true
-
name: Create checksums
working-directory: ./bin/release
run: |
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
mv $RUNNER_TEMP/checksums.txt .
cat checksums.txt | while read sum file; do
if [[ "${file#\*}" == docker-compose-* && "${file#\*}" != *.provenance.json && "${file#\*}" != *.sbom.json ]]; then
echo "$sum $file" > ${file#\*}.sha256
fi
done
-
name: List artifacts
run: |
tree -nh ./bin/release
-
name: Check artifacts
run: |
find bin/release -type f -exec file -e ascii -- {} +
-
name: GitHub Release
if: startsWith(github.ref, 'refs/tags/v')
uses: ncipollo/release-action@58ae73b360456532aafd58ee170c045abbeaee37 # v1.10.0
with:
artifacts: ./bin/release/*
generateReleaseNotes: true
draft: true
token: ${{ secrets.GITHUB_TOKEN }}

51
.github/workflows/docs-upstream.yml vendored Normal file
View File

@ -0,0 +1,51 @@
# this workflow runs the remote validate bake target from docker/docs
# to check if yaml reference docs used in this repo are valid
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:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
push:
branches:
- 'main'
- 'v[0-9]*'
paths:
- '.github/workflows/docs-upstream.yml'
- 'docs/**'
pull_request:
paths:
- '.github/workflows/docs-upstream.yml'
- 'docs/**'
jobs:
docs-yaml:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v4
-
name: Upload reference YAML docs
uses: actions/upload-artifact@v4
with:
name: docs-yaml
path: docs/reference
retention-days: 1
validate:
uses: docker/docs/.github/workflows/validate-upstream.yml@main
needs:
- docs-yaml
with:
module-name: docker/compose

163
.github/workflows/merge.yml vendored Normal file
View File

@ -0,0 +1,163 @@
name: merge
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
push:
branches:
- 'main'
tags:
- 'v*'
permissions:
contents: read # to fetch code (actions/checkout)
env:
REPO_SLUG: "docker/compose-bin"
jobs:
e2e:
name: Build and test
runs-on: ${{ matrix.os }}
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
os: [desktop-windows, desktop-macos, desktop-m1]
# mode: [plugin, standalone]
mode: [plugin]
env:
GO111MODULE: "on"
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true
check-latest: true
- name: List Docker resources on machine
run: |
docker ps --all
docker volume ls
docker network ls
docker image ls
- name: Remove Docker resources on machine
continue-on-error: true
run: |
docker kill $(docker ps -q)
docker rm -f $(docker ps -aq)
docker volume rm -f $(docker volume ls -q)
docker ps --all
- name: Unit tests
run: make test
- name: Build binaries
run: |
make
- name: Check arch of go compose binary
run: |
file ./bin/build/docker-compose
if: ${{ !contains(matrix.os, 'desktop-windows') }}
-
name: Test plugin mode
if: ${{ matrix.mode == 'plugin' }}
run: |
make e2e-compose
-
name: Test standalone mode
if: ${{ matrix.mode == 'standalone' }}
run: |
make e2e-compose-standalone
bin-image:
runs-on: ubuntu-22.04
outputs:
digest: ${{ fromJSON(steps.bake.outputs.metadata).image-cross['containerimage.digest'] }}
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
uses: actions/checkout@v4
-
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
uses: docker/setup-qemu-action@v3
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
-
name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
${{ env.REPO_SLUG }}
tags: |
type=ref,event=tag
type=edge
bake-target: meta-helper
-
name: Build and push image
uses: docker/bake-action@v6
id: bake
with:
source: .
files: |
./docker-bake.hcl
${{ steps.meta.outputs.bake-file }}
targets: image-cross
push: ${{ github.event_name != 'pull_request' }}
sbom: true
provenance: mode=max
set: |
*.cache-from=type=gha,scope=bin-image
*.cache-to=type=gha,scope=bin-image,mode=max
desktop-edge-test:
runs-on: ubuntu-latest
needs: bin-image
steps:
-
name: Generate Token
id: generate_token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ vars.DOCKERDESKTOP_APP_ID }}
private-key: ${{ secrets.DOCKERDESKTOP_APP_PRIVATEKEY }}
owner: docker
repositories: |
${{ secrets.DOCKERDESKTOP_REPO }}
-
name: Trigger Docker Desktop e2e with edge version
uses: actions/github-script@v7
with:
github-token: ${{ steps.generate_token.outputs.token }}
script: |
await github.rest.actions.createWorkflowDispatch({
owner: 'docker',
repo: '${{ secrets.DOCKERDESKTOP_REPO }}',
workflow_id: 'compose-edge-integration.yml',
ref: 'main',
inputs: {
"image-tag": "${{ needs.bin-image.outputs.digest }}"
}
})

63
.github/workflows/scorecards.yml vendored Normal file
View File

@ -0,0 +1,63 @@
name: Scorecards supply-chain security
on:
# Only the default branch is supported.
branch_protection_rule:
schedule:
- cron: '44 9 * * 4'
push:
branches: [ "main" ]
jobs:
analysis:
name: Scorecards analysis
runs-on: ubuntu-latest
permissions:
# Needed to upload the results to code-scanning dashboard.
security-events: write
# Used to receive a badge.
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:
- name: "Checkout code"
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # tag=v4.4.2
with:
persist-credentials: false
- name: "Run analysis"
uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # tag=v2.4.0
with:
results_file: results.sarif
results_format: sarif
# Publish the results for public repositories to enable scorecard badges. For more details, see
# https://github.com/ossf/scorecard-action#publishing-results.
# For private repositories, `publish_results` will automatically be set to `false`, regardless
# of the value entered here.
publish_results: true
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
# format to the repository Actions tab.
- name: "Upload artifact"
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # tag=v4.5.0
with:
name: SARIF file
path: results.sarif
retention-days: 5
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@3096afedf9873361b2b2f65e1445b13272c83eb8 # tag=v2.20.00
with:
sarif_file: results.sarif

33
.github/workflows/stale.yml vendored Normal file
View File

@ -0,0 +1,33 @@
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"

9
.gitignore vendored
View File

@ -1,4 +1,5 @@
*.egg-info
*.pyc
/dist
/_site
bin/
/.vscode/
coverage.out
covdatafiles/
.DS_Store

89
.golangci.yml Normal file
View File

@ -0,0 +1,89 @@
version: "2"
run:
concurrency: 2
linters:
default: none
enable:
- copyloopvar
- depguard
- errcheck
- errorlint
- gocritic
- gocyclo
- gomodguard
- govet
- ineffassign
- lll
- misspell
- nakedret
- nolintlint
- revive
- staticcheck
- testifylint
- unconvert
- unparam
- unused
settings:
depguard:
rules:
all:
deny:
- pkg: io/ioutil
desc: io/ioutil package has been deprecated
- pkg: github.com/docker/docker/errdefs
desc: use github.com/containerd/errdefs instead.
- pkg: golang.org/x/exp/maps
desc: use stdlib maps package
- pkg: golang.org/x/exp/slices
desc: use stdlib slices package
- pkg: gopkg.in/yaml.v2
desc: compose-go uses yaml.v3
gocritic:
disabled-checks:
- paramTypeCombine
- unnamedResult
- whyNoLint
enabled-tags:
- diagnostic
- opinionated
- style
gocyclo:
min-complexity: 16
gomodguard:
blocked:
modules:
- github.com/pkg/errors:
recommendations:
- errors
- fmt
versions:
- github.com/distribution/distribution:
reason: use distribution/reference
- gotest.tools:
version: < 3.0.0
reason: deprecated, pre-modules version
lll:
line-length: 200
revive:
rules:
- name: package-comments
disabled: true
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
issues:
max-issues-per-linter: 0
max-same-issues: 0
formatters:
enable:
- gofumpt
- goimports
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$

View File

@ -1,19 +0,0 @@
language: python
python:
- "2.6"
- "2.7"
- "3.2"
- "3.3"
matrix:
allow_failures:
- python: "3.2"
- python: "3.3"
install: script/travis-install
script:
- pwd
- env
- sekexe/run "`pwd`/script/travis $TRAVIS_PYTHON_VERSION"

95
BUILDING.md Normal file
View File

@ -0,0 +1,95 @@
### Prerequisites
* Windows:
* [Docker Desktop](https://docs.docker.com/desktop/setup/install/windows-install/)
* make
* go (see [go.mod](go.mod) for minimum version)
* macOS:
* [Docker Desktop](https://docs.docker.com/desktop/setup/install/mac-install/)
* make
* go (see [go.mod](go.mod) for minimum version)
* Linux:
* [Docker 20.10 or later](https://docs.docker.com/engine/install/)
* make
* go (see [go.mod](go.mod) for minimum version)
### Building the CLI
Once you have the prerequisites installed, you can build the CLI using:
```console
make
```
This will output a `docker-compose` CLI plugin for your host machine in
`./bin/build`.
You can statically cross compile the CLI for Windows, macOS, and Linux using the
`cross` target.
### Unit tests
To run all of the unit tests, run:
```console
make test
```
If you need to update a golden file simply do `go test ./... -test.update-golden`.
### End-to-end tests
To run e2e tests, the Compose CLI binary needs to be built. All the commands to run e2e tests propose a version
with the prefix `build-and-e2e` to first build the CLI before executing tests.
Note that this requires a local Docker Engine to be running.
#### Whole end-to-end tests suite
To execute both CLI and standalone e2e tests, run :
```console
make e2e
```
Or if you need to build the CLI, run:
```console
make build-and-e2e
```
#### Plugin end-to-end tests suite
To execute CLI plugin e2e tests, run :
```console
make e2e-compose
```
Or if you need to build the CLI, run:
```console
make build-and-e2e-compose
```
#### Standalone end-to-end tests suite
To execute the standalone CLI e2e tests, run :
```console
make e2e-compose-standalone
```
Or if you need to build the CLI, run:
```console
make build-and-e2e-compose-standalone
```
## Releases
To create a new 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.
This will automatically create a new tag, release and make binaries for
Windows, macOS, and Linux available for download on the
[releases page](https://github.com/docker/compose/releases).

View File

@ -1,32 +0,0 @@
Change log
==========
0.1.0 (2014-01-16)
------------------
- Containers are recreated on each `fig up`, ensuring config is up-to-date with `fig.yml` (#2)
- Add `fig scale` command (#9)
- Use DOCKER_HOST environment variable to find Docker daemon (#19)
- Truncate long commands in `fig ps` (#18)
- Fill out CLI help banners for commands (#15, #16)
- Show a friendlier error when `fig.yml` is missing (#4)
- Fix bug with `fig build` logging (#3)
- Fix bug where builds would time out if a step took a long time without generating output (#6)
- Fix bug where streaming container output over the Unix socket raised an error (#7)
0.0.2 (2014-01-02)
------------------
- Improve documentation
- Try to connect to Docker on `tcp://localdocker:4243` and a UNIX socket in addition to `localhost`.
- Improve `fig up` behaviour
- Add confirmation prompt to `fig rm`
- Add `fig build` command
0.0.1 (2013-12-20)
------------------
Initial release.

339
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,339 @@
# Contributing to Docker
Want to hack on Docker? Awesome! We have a contributor's guide that explains
[setting up a Docker development environment and the contribution
process](https://docs.docker.com/contribute/).
This page contains information about reporting issues as well as some tips and
guidelines useful to experienced open source contributors. Finally, make sure
you read our [community guidelines](#docker-community-guidelines) before you
start participating.
## Topics
- [Contributing to Docker](#contributing-to-docker)
- [Topics](#topics)
- [Reporting security issues](#reporting-security-issues)
- [Reporting other issues](#reporting-other-issues)
- [Quick contribution tips and guidelines](#quick-contribution-tips-and-guidelines)
- [Pull requests are always welcome](#pull-requests-are-always-welcome)
- [Talking to other Docker users and contributors](#talking-to-other-docker-users-and-contributors)
- [Conventions](#conventions)
- [Merge approval](#merge-approval)
- [Sign your work](#sign-your-work)
- [How can I become a maintainer?](#how-can-i-become-a-maintainer)
- [Docker community guidelines](#docker-community-guidelines)
- [Coding Style](#coding-style)
## Reporting security issues
The Docker maintainers take security seriously. If you discover a security
issue, please bring it to their attention right away!
Please **DO NOT** file a public issue, instead, send your report privately to
[security@docker.com](mailto:security@docker.com).
Security reports are greatly appreciated and we will publicly thank you for them.
We also like to send gifts&mdash;if you're into Docker swag, make sure to let
us know. We currently do not offer a paid security bounty program but are not
ruling it out in the future.
## Reporting other issues
A great way to contribute to the project is to send a detailed report when you
encounter an issue. We always appreciate a well-written, thorough bug report,
and will thank you for it!
Check that [our issue database](https://github.com/docker/compose/labels/Docker%20Compose%20V2)
doesn't already include that problem or suggestion before submitting an issue.
If you find a match, you can use the "subscribe" button to get notified of
updates. Do *not* leave random "+1" or "I have this too" comments, as they
only clutter the discussion, and don't help to resolve it. However, if you
have ways to reproduce the issue or have additional information that may help
resolve the issue, please leave a comment.
When reporting issues, always include:
* The output of `docker version`.
* The output of `docker context show`.
* The output of `docker info`.
Also, include the steps required to reproduce the problem if possible and
applicable. This information will help us review and fix your issue faster.
When sending lengthy log files, consider posting them as a gist
(https://gist.github.com).
Don't forget to remove sensitive data from your log files before posting (you
can replace those parts with "REDACTED").
_Note:_
Maintainers might request additional information to diagnose an issue,
if initial reporter doesn't answer within a reasonable delay (a few weeks),
issue will be closed.
## Quick contribution tips and guidelines
This section gives the experienced contributor some tips and guidelines.
### Pull requests are always welcome
Not sure if that typo is worth a pull request? Found a bug and know how to fix
it? Do it! We will appreciate it. Any significant change, like adding a backend,
should be documented as
[a GitHub issue](https://github.com/docker/compose/issues)
before anybody starts working on it.
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,
don't get discouraged!
### Talking to other Docker users and contributors
<table class="tg">
<col width="45%">
<col width="65%">
<tr>
<td>Community Slack</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>.
</td>
</tr>
<tr>
<td>Forums</td>
<td>
A public forum for users to discuss questions and explore current design patterns and
best practices about Docker and related projects in the Docker Ecosystem. To participate,
just log in with your Docker Hub account on <a href="https://forums.docker.com" target="_blank">https://forums.docker.com</a>.
</td>
</tr>
<tr>
<td>Twitter</td>
<td>
You can follow <a href="https://twitter.com/docker/" target="_blank">Docker's Twitter feed</a>
to get updates on our products. You can also tweet us questions or just
share blogs or stories.
</td>
</tr>
<tr>
<td>Stack Overflow</td>
<td>
Stack Overflow has over 17000 Docker questions listed. We regularly
monitor <a href="https://stackoverflow.com/questions/tagged/docker" target="_blank">Docker questions</a>
and so do many other knowledgeable Docker users.
</td>
</tr>
</table>
### Conventions
Fork the repository and make changes on your fork in a feature branch:
- If it's a bug fix branch, name it XXXX-something where XXXX is the number of
the issue.
- If it's a feature branch, create an enhancement issue to announce
your intentions, and name it XXXX-something where XXXX is the number of the
issue.
Submit unit tests for your changes. Go has a great test framework built in; use
it! Take a look at existing tests for inspiration. Also, end-to-end tests are
available. Run the full test suite, both unit tests and e2e tests on your
branch before submitting a pull request. See [BUILDING.md](BUILDING.md) for
instructions to build and run tests.
Write clean code. Universally formatted code promotes ease of writing, reading,
and maintenance. Always run `gofmt -s -w file.go` on each changed file before
committing your changes. Most editors have plug-ins that do this automatically.
Pull request descriptions should be as clear as possible and include a reference
to all the issues that they address.
Commit messages must start with a capitalized and short summary (max. 50 chars)
written in the imperative, followed by an optional, more detailed explanatory
text which is separated from the summary by an empty line.
Code review comments may be added to your pull request. Discuss, then make the
suggested modifications and push additional commits to your feature branch. Post
a comment after pushing. New commits show up in the pull request automatically,
but the reviewers are notified only when you comment.
Pull requests must be cleanly rebased on top of the base branch without multiple branches
mixed into the PR.
**Git tip**: If your PR no longer merges cleanly, use `rebase master` in your
feature branch to update your pull request rather than `merge master`.
Before you make a pull request, squash your commits into logical units of work
using `git rebase -i` and `git push -f`. A logical unit of work is a consistent
set of patches that should be reviewed together: for example, upgrading the
version of a vendored dependency and taking advantage of its now available new
feature constitute two separate units of work. Implementing a new function and
calling it in another file constitute a single logical unit of work. The very
high majority of submissions should have a single commit, so if in doubt: squash
down to one.
After every commit, make sure the test suite passes. Include documentation
changes in the same pull request so that a revert would remove all traces of
the feature or fix.
Include an issue reference like `Closes #XXXX` or `Fixes #XXXX` in the pull
request description that closes an issue. Including references automatically
closes the issue on a merge.
Please do not add yourself to the `AUTHORS` file, as it is regenerated regularly
from the Git history.
Please see the [Coding Style](#coding-style) for further guidelines.
### Merge approval
Docker maintainers use LGTM (Looks Good To Me) in comments on the code review to
indicate acceptance.
A change requires at least 2 LGTMs from the maintainers of each
component affected.
For more details, see the [MAINTAINERS](MAINTAINERS) page.
### Sign your work
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
it on as an open-source patch. The rules are pretty simple: if you can certify
the below (from [developercertificate.org](https://developercertificate.org/)):
```
Developer Certificate of Origin
Version 1.1
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
660 York Street, Suite 102,
San Francisco, CA 94110 USA
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
```
Then you just add a line to every git commit message:
Signed-off-by: Joe Smith <joe.smith@email.com>
Use your real name (sorry, no pseudonyms or anonymous contributions.)
If you set your `user.name` and `user.email` git configs, you can sign your
commit automatically with `git commit -s`.
### How can I become a maintainer?
The procedures for adding new maintainers are explained in the global
[MAINTAINERS](https://github.com/docker/opensource/blob/main/MAINTAINERS)
file in the
[https://github.com/docker/opensource/](https://github.com/docker/opensource/)
repository.
Don't forget: being a maintainer is a time investment. Make sure you
will have time to make yourself available. You don't have to be a
maintainer to make a difference on the project!
## Docker community guidelines
We want to keep the Docker community awesome, growing and collaborative. We need
your help to keep it that way. To help with this we've come up with some general
guidelines for the community as a whole:
* Be nice: Be courteous, respectful and polite to fellow community members:
no regional, racial, gender or other abuse will be tolerated. We like
nice people way better than mean ones!
* Encourage diversity and participation: Make everyone in our community feel
welcome, regardless of their background and the extent of their
contributions, and do everything possible to encourage participation in
our community.
* Keep it legal: Basically, don't get us in trouble. Share only content that
you own, do not share private or sensitive information, and don't break
the law.
* Stay on topic: Make sure that you are posting to the correct channel and
avoid off-topic discussions. Remember when you update an issue or respond
to an email you are potentially sending it to a large number of people. Please
consider this before you update. Also, remember that nobody likes spam.
* Don't send emails to the maintainers: There's no need to send emails to the
maintainers to ask them to investigate an issue or to take a look at a
pull request. Instead of sending an email, GitHub mentions should be
used to ping maintainers to review a pull request, a proposal or an
issue.
## Coding Style
Unless explicitly stated, we follow all coding guidelines from the Go
community. While some of these standards may seem arbitrary, they somehow seem
to result in a solid, consistent codebase.
It is possible that the code base does not currently comply with these
guidelines. We are not looking for a massive PR that fixes this, since that
goes against the spirit of the guidelines. All new contributors should make their
best effort to clean up and make the code base better than they left it.
Obviously, apply your best judgement. Remember, the goal here is to make the
code base easier for humans to navigate and understand. Always keep that in
mind when nudging others to comply.
The rules:
1. All code should be formatted with `gofmt -s`.
2. All code should pass the default levels of
[`golint`](https://github.com/golang/lint).
3. All code should follow the guidelines covered in [Effective
Go](https://go.dev/doc/effective_go) and [Go Code Review
Comments](https://go.dev/wiki/CodeReviewComments).
4. Include code comments. Tell us the why, the history and the context.
5. Document _all_ declarations and methods, even private ones. Declare
expectations, caveats and anything else that may be important. If a type
gets exported, having the comments already there will ensure it's ready.
6. Variable name length should be proportional to its context and no longer.
`noCommaALongVariableNameLikeThisIsNotMoreClearWhenASimpleCommentWouldDo`.
In practice, short methods will have short variable names and globals will
have longer names.
7. No underscores in package names. If you need a compound name, step back,
and re-examine why you need a compound name. If you still think you need a
compound name, lose the underscore.
8. No utils or helpers packages. If a function is not general enough to
warrant its own package, it has not been written generally enough to be a
part of a util package. Just leave it unexported and well-documented.
9. All tests should run with `go test` and outside tooling should not be
required. No, we don't need another unit testing framework. Assertion
packages are acceptable if they provide _real_ incremental value.
10. Even though we call these "rules" above, they are actually just
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
reading through [Effective Go](https://go.dev/doc/effective_go). The
[Go Blog](https://go.dev/blog/) is also a great resource. Drinking the
kool-aid is a lot easier than going thirsty.

View File

@ -1,9 +1,197 @@
FROM stackbrew/ubuntu:12.04
RUN apt-get update -qq
RUN apt-get install -y python python-pip
ADD requirements.txt /code/
WORKDIR /code/
RUN pip install -r requirements.txt
ADD requirements-dev.txt /code/
RUN pip install -r requirements-dev.txt
ADD . /code/
# syntax=docker/dockerfile:1
# 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.
ARG GO_VERSION=1.23.12
ARG XX_VERSION=1.6.1
ARG GOLANGCI_LINT_VERSION=v2.0.2
ARG ADDLICENSE_VERSION=v1.0.0
ARG BUILD_TAGS="e2e"
ARG DOCS_FORMATS="md,yaml"
ARG LICENSE_FILES=".*\(Dockerfile\|Makefile\|\.go\|\.hcl\|\.sh\)"
# xx is a helper for cross-compilation
FROM --platform=${BUILDPLATFORM} tonistiigi/xx:${XX_VERSION} AS xx
# osxcross contains the MacOSX cross toolchain for xx
FROM crazymax/osxcross:11.3-alpine AS osxcross
FROM golangci/golangci-lint:${GOLANGCI_LINT_VERSION}-alpine AS golangci-lint
FROM ghcr.io/google/addlicense:${ADDLICENSE_VERSION} AS addlicense
FROM --platform=${BUILDPLATFORM} golang:${GO_VERSION}-alpine AS base
COPY --from=xx / /
RUN apk add --no-cache \
clang \
docker \
file \
findutils \
git \
make \
protoc \
protobuf-dev
WORKDIR /src
ENV CGO_ENABLED=0
FROM base AS build-base
COPY go.* .
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
go mod download
FROM build-base AS vendored
RUN --mount=type=bind,target=.,rw \
--mount=type=cache,target=/go/pkg/mod \
go mod tidy && mkdir /out && cp go.mod go.sum /out
FROM scratch AS vendor-update
COPY --from=vendored /out /
FROM vendored AS vendor-validate
RUN --mount=type=bind,target=.,rw <<EOT
set -e
git add -A
cp -rf /out/* .
diff=$(git status --porcelain -- go.mod go.sum)
if [ -n "$diff" ]; then
echo >&2 'ERROR: Vendor result differs. Please vendor your package with "make go-mod-tidy"'
echo "$diff"
exit 1
fi
EOT
FROM build-base AS build
ARG BUILD_TAGS
ARG BUILD_FLAGS
ARG TARGETPLATFORM
RUN --mount=type=bind,target=. \
--mount=type=cache,target=/root/.cache \
--mount=type=cache,target=/go/pkg/mod \
--mount=type=bind,from=osxcross,src=/osxsdk,target=/xx-sdk \
xx-go --wrap && \
if [ "$(xx-info os)" == "darwin" ]; then export CGO_ENABLED=1; fi && \
make build GO_BUILDTAGS="$BUILD_TAGS" DESTDIR=/out && \
xx-verify --static /out/docker-compose
FROM build-base AS lint
ARG BUILD_TAGS
ENV GOLANGCI_LINT_CACHE=/cache/golangci-lint
RUN --mount=type=bind,target=. \
--mount=type=cache,target=/root/.cache \
--mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/cache/golangci-lint \
--mount=from=golangci-lint,source=/usr/bin/golangci-lint,target=/usr/bin/golangci-lint \
golangci-lint cache status && \
golangci-lint run --build-tags "$BUILD_TAGS" ./...
FROM build-base AS test
ARG CGO_ENABLED=0
ARG BUILD_TAGS
RUN --mount=type=bind,target=. \
--mount=type=cache,target=/root/.cache \
--mount=type=cache,target=/go/pkg/mod \
rm -rf /tmp/coverage && \
mkdir -p /tmp/coverage && \
rm -rf /tmp/report && \
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
FROM scratch AS test-coverage
COPY --from=test --link /tmp/coverage /
COPY --from=test --link /tmp/report /
FROM base AS license-set
ARG LICENSE_FILES
RUN --mount=type=bind,target=.,rw \
--mount=from=addlicense,source=/app/addlicense,target=/usr/bin/addlicense \
find . -regex "${LICENSE_FILES}" | xargs addlicense -c 'Docker Compose CLI' -l apache && \
mkdir /out && \
find . -regex "${LICENSE_FILES}" | cpio -pdm /out
FROM scratch AS license-update
COPY --from=set /out /
FROM base AS license-validate
ARG LICENSE_FILES
RUN --mount=type=bind,target=. \
--mount=from=addlicense,source=/app/addlicense,target=/usr/bin/addlicense \
find . -regex "${LICENSE_FILES}" | xargs addlicense -check -c 'Docker Compose CLI' -l apache -ignore validate -ignore testdata -ignore resolvepath -v
FROM base AS docsgen
WORKDIR /src
RUN --mount=target=. \
--mount=target=/root/.cache,type=cache \
--mount=type=cache,target=/go/pkg/mod \
go build -o /out/docsgen ./docs/yaml/main/generate.go
FROM --platform=${BUILDPLATFORM} alpine AS docs-build
RUN apk add --no-cache rsync git
WORKDIR /src
COPY --from=docsgen /out/docsgen /usr/bin
ARG DOCS_FORMATS
RUN --mount=target=/context \
--mount=target=.,type=tmpfs <<EOT
set -e
rsync -a /context/. .
docsgen --formats "$DOCS_FORMATS" --source "docs/reference"
mkdir /out
cp -r docs/reference /out
EOT
FROM scratch AS docs-update
COPY --from=docs-build /out /out
FROM docs-build AS docs-validate
RUN --mount=target=/context \
--mount=target=.,type=tmpfs <<EOT
set -e
rsync -a /context/. .
git add -A
rm -rf docs/reference/*
cp -rf /out/* ./docs/
if [ -n "$(git status --porcelain -- docs/reference)" ]; then
echo >&2 'ERROR: Docs result differs. Please update with "make docs"'
git status --porcelain -- docs/reference
exit 1
fi
EOT
FROM scratch AS binary-unix
COPY --link --from=build /out/docker-compose /
FROM binary-unix AS binary-darwin
FROM binary-unix AS binary-linux
FROM scratch AS binary-windows
COPY --link --from=build /out/docker-compose /docker-compose.exe
FROM binary-$TARGETOS AS binary
# enable scanning for this stage
ARG BUILDKIT_SBOM_SCAN_STAGE=true
FROM --platform=$BUILDPLATFORM alpine AS releaser
WORKDIR /work
ARG TARGETOS
ARG TARGETARCH
ARG TARGETVARIANT
RUN --mount=from=binary \
mkdir -p /out && \
# TODO: should just use standard arch
TARGETARCH=$([ "$TARGETARCH" = "amd64" ] && echo "x86_64" || echo "$TARGETARCH"); \
TARGETARCH=$([ "$TARGETARCH" = "arm64" ] && echo "aarch64" || echo "$TARGETARCH"); \
cp docker-compose* "/out/docker-compose-${TARGETOS}-${TARGETARCH}${TARGETVARIANT}$(ls docker-compose* | sed -e 's/^docker-compose//')"
FROM scratch AS release
COPY --from=releaser /out/ /

226
LICENSE
View File

@ -1,24 +1,202 @@
Copyright (c) 2013, Orchard Laboratories Ltd.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* The names of its contributors may not be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.

124
MAINTAINERS Normal file
View File

@ -0,0 +1,124 @@
# Docker maintainers file
#
# This file describes who runs the docker/compose project and how.
# This is a living document - if you see something out of date or missing, speak up!
#
# It is structured to be consumable by both humans and programs.
# To extract its contents programmatically, use any TOML-compliant
# parser.
#
# This file is compiled into the MAINTAINERS file in docker/opensource.
#
[Org]
[Org."Core maintainers"]
# The Core maintainers are the ghostbusters of the project: when there's a problem others
# can't solve, they show up and fix it with bizarre devices and weaponry.
# They have final say on technical implementation and coding style.
# They are ultimately responsible for quality in all its forms: usability polish,
# bugfixes, performance, stability, etc. When ownership can cleanly be passed to
# a subsystem, they are responsible for doing so and holding the
# subsystem maintainers accountable. If ownership is unclear, they are the de facto owners.
people = [
"glours",
"jhrotko",
"milas",
"ndeloof",
"nicksieger",
"StefanScherer",
"ulyssessouza"
]
[Org."Regular maintainers"]
# The Regular maintainers are people who aren't Core maintainers but are around
# to help reviewing and fixing bugs, just on a less regular basis than previously.
# Most of them were previously Core maintainers of Compose.
people = [
"aiordache",
"chris-crone",
"gtardif",
"laurazard",
"maxcleme",
"rumpl",
"thaJeztah"
]
[people]
# A reference list of all people associated with the project.
# All other sections should refer to people by their canonical key
# in the people section.
# ADD YOURSELF HERE IN ALPHABETICAL ORDER
[people.aiordache]
Name = "Anca Iordache"
Email = "anca.iordache@docker.com"
GitHub = "aiordache "
[people.chris-crone]
Name = "Christopher Crone"
Email = "christopher.crone@docker.com"
GitHub = "chris-crone"
[people.glours]
Name = "Guillaume Lours"
Email = "guillaume.lours@docker.com"
GitHub = "glours"
[people.gtardif]
Name = "Guillaume Tardif"
Email = "guillaume.tardif@docker.com"
GitHub = "gtardif"
[people.jhrotko]
Name = "Joana Hrotko"
Email = "joana.hrotko@docker.com"
Github = "jhrotko"
[people.laurazard]
Name = "Laura Brehm"
Email = "laura.brehm@docker.com"
GitHub = "laurazard"
[people.maxcleme]
Name = "Maxime Clement"
Email = "maxime.clement@docker.com"
GitHub = "maxcleme"
[people.milas]
Name = "Milas Bowman"
Email = "milas.bowman@docker.com"
GitHub = "milas"
[people.nicksieger]
Name = "Nick Sieger"
Email = "nick.sieger@docker.com"
GitHub = "nicksieger"
[people.ndeloof]
Name = "Nicolas Deloof"
Email = "nicolas.deloof@docker.com"
GitHub = "ndeloof"
[people.rumpl]
Name = "Djordje Lukic"
Email = "djordje.lukic@docker.com"
GitHub = "rumpl"
[people.thaJeztah]
Name = "Sebastiaan van Stijn"
Email = "sebastiaan.vanstijn@docker.com"
GitHub = "thaJeztah "
[people.StefanScherer]
Name = "Stefan Scherer"
Email = "stefan.scherer@docker.com"
GitHub = "StefanScherer"
[people.ulyssessouza]
Name = "Ulysses Souza"
Email = "<ulysses.souza@docker.com"
Github = "ulyssessouza"

View File

@ -1,10 +0,0 @@
include Dockerfile
include LICENSE
include requirements.txt
include requirements-dev.txt
tox.ini
include *.md
recursive-include tests *
global-exclude *.pyc
global-exclude *.pyo
global-exclude *.un~

162
Makefile Normal file
View File

@ -0,0 +1,162 @@
# 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.
PKG := github.com/docker/compose/v2
VERSION ?= $(shell git describe --match 'v[0-9]*' --dirty='.m' --always --tags)
GO_LDFLAGS ?= -w -X ${PKG}/internal.Version=${VERSION}
GO_BUILDTAGS ?= e2e
DRIVE_PREFIX?=
ifeq ($(OS),Windows_NT)
DETECTED_OS = Windows
DRIVE_PREFIX=C:
else
DETECTED_OS = $(shell uname -s)
endif
ifeq ($(DETECTED_OS),Windows)
BINARY_EXT=.exe
endif
BUILD_FLAGS?=
TEST_FLAGS?=
E2E_TEST?=
ifneq ($(E2E_TEST),)
TEST_FLAGS:=$(TEST_FLAGS) -run '$(E2E_TEST)'
endif
EXCLUDE_E2E_TESTS?=
ifneq ($(EXCLUDE_E2E_TESTS),)
TEST_FLAGS:=$(TEST_FLAGS) -skip '$(EXCLUDE_E2E_TESTS)'
endif
BUILDX_CMD ?= docker buildx
# DESTDIR overrides the output path for binaries and other artifacts
# this is used by docker/docker-ce-packaging for the apt/rpm builds,
# so it's important that the resulting binary ends up EXACTLY at the
# path $DESTDIR/docker-compose when specified.
#
# See https://github.com/docker/docker-ce-packaging/blob/e43fbd37e48fde49d907b9195f23b13537521b94/rpm/SPECS/docker-compose-plugin.spec#L47
#
# By default, all artifacts go to subdirectories under ./bin/ in the
# repo root, e.g. ./bin/build, ./bin/coverage, ./bin/release.
DESTDIR ?=
all: build
.PHONY: build ## Build the compose cli-plugin
build:
GO111MODULE=on go build $(BUILD_FLAGS) -trimpath -tags "$(GO_BUILDTAGS)" -ldflags "$(GO_LDFLAGS)" -o "$(or $(DESTDIR),./bin/build)/docker-compose$(BINARY_EXT)" ./cmd
.PHONY: binary
binary:
$(BUILDX_CMD) bake binary
.PHONY: binary-with-coverage
binary-with-coverage:
$(BUILDX_CMD) bake binary-with-coverage
.PHONY: install
install: binary
mkdir -p ~/.docker/cli-plugins
install $(or $(DESTDIR),./bin/build)/docker-compose ~/.docker/cli-plugins/docker-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
go run gotest.tools/gotestsum@latest --format testname --junitfile "/tmp/report/report.xml" -- -v $(TEST_FLAGS) -count=1 ./pkg/e2e
.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
go run gotest.tools/gotestsum@latest --format testname --junitfile "/tmp/report/report.xml" -- $(TEST_FLAGS) -v -count=1 -parallel=1 --tags=standalone ./pkg/e2e
.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
.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
.PHONY: example-provider
example-provider: ## build example provider for e2e tests
go build -o bin/build/example-provider docs/examples/provider.go
.PHONY: mocks
mocks:
mockgen --version >/dev/null 2>&1 || go install go.uber.org/mock/mockgen@v0.4.0
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_compose_api.go -package mocks -source=./pkg/api/api.go Service
.PHONY: e2e
e2e: e2e-compose e2e-compose-standalone ## Run end to end local tests in both modes. Set E2E_TEST=TestName to run a single test
.PHONY: build-and-e2e
build-and-e2e: build e2e-compose e2e-compose-standalone ## Compile the compose cli-plugin and run end to end local tests in both modes. Set E2E_TEST=TestName to run a single test
.PHONY: cross
cross: ## Compile the CLI for linux, darwin and windows
$(BUILDX_CMD) bake binary-cross
.PHONY: test
test: ## Run unit tests
$(BUILDX_CMD) bake test
.PHONY: cache-clear
cache-clear: ## Clear the builder cache
$(BUILDX_CMD) prune --force --filter type=exec.cachemount --filter=unused-for=24h
.PHONY: lint
lint: ## run linter(s)
$(BUILDX_CMD) bake lint
.PHONY: fmt
fmt:
gofumpt --version >/dev/null 2>&1 || go install mvdan.cc/gofumpt@latest
gofumpt -w .
.PHONY: docs
docs: ## generate documentation
$(eval $@_TMP_OUT := $(shell mktemp -d -t compose-output.XXXXXXXXXX))
$(BUILDX_CMD) bake --set "*.output=type=local,dest=$($@_TMP_OUT)" docs-update
rm -rf ./docs/internal
cp -R "$(DRIVE_PREFIX)$($@_TMP_OUT)"/out/* ./docs/
rm -rf "$(DRIVE_PREFIX)$($@_TMP_OUT)"/*
.PHONY: validate-docs
validate-docs: ## validate the doc does not change
$(BUILDX_CMD) bake docs-validate
.PHONY: check-dependencies
check-dependencies: ## check dependency updates
go list -u -m -f '{{if not .Indirect}}{{if .Update}}{{.}}{{end}}{{end}}' all
.PHONY: validate-headers
validate-headers: ## Check license header for all files
$(BUILDX_CMD) bake license-validate
.PHONY: go-mod-tidy
go-mod-tidy: ## Run go mod tidy in a container and output resulting go.mod and go.sum
$(BUILDX_CMD) bake vendor-update
.PHONY: validate-go-mod
validate-go-mod: ## Validate go.mod and go.sum are up-to-date
$(BUILDX_CMD) bake vendor-validate
validate: validate-go-mod validate-headers validate-docs ## Validate sources
pre-commit: validate check-dependencies lint build test e2e-compose
help: ## Show help
@echo Please specify a build target. The choices are:
@grep -E '^[0-9a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'

4
NOTICE Normal file
View File

@ -0,0 +1,4 @@
Docker Compose V2
Copyright 2020 Docker Compose authors
This product includes software developed at Docker, Inc. (https://www.docker.com).

355
README.md
View File

@ -1,285 +1,92 @@
Fig
===
# Table of Contents
- [Docker Compose v2](#docker-compose-v2)
- [Where to get Docker Compose](#where-to-get-docker-compose)
+ [Windows and macOS](#windows-and-macos)
+ [Linux](#linux)
- [Quick Start](#quick-start)
- [Contributing](#contributing)
- [Legacy](#legacy)
# Docker Compose v2
[![Build Status](https://travis-ci.org/orchardup/fig.png?branch=master)](https://travis-ci.org/orchardup/fig)
[![GitHub release](https://img.shields.io/github/v/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)
[![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)
[![Codecov](https://codecov.io/gh/docker/compose/branch/main/graph/badge.svg?token=HP3K4Y4ctu)](https://codecov.io/gh/docker/compose)
[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/docker/compose/badge)](https://api.securityscorecards.dev/projects/github.com/docker/compose)
![Docker Compose](logo.png?raw=true "Docker Compose Logo")
Punctual, lightweight development environments using Docker.
Docker Compose is a tool for running multi-container applications on Docker
defined using the [Compose file format](https://compose-spec.io).
A Compose file is used to define how one or more containers that make up
your application are configured.
Once you have a Compose file, you can create and start your application with a
single command: `docker compose up`.
Fig is a tool for defining and running isolated application environments. You define the services which comprise your app in a simple, version-controllable YAML configuration file that looks like this:
> **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
### Windows and macOS
Docker Compose is included in
[Docker Desktop](https://www.docker.com/products/docker-desktop/)
for Windows and macOS.
### Linux
You can download Docker Compose binaries from the
[release page](https://github.com/docker/compose/releases) on this repository.
Rename the relevant binary for your OS to `docker-compose` and copy it to `$HOME/.docker/cli-plugins`
Or copy it into one of these folders to install it system-wide:
* `/usr/local/lib/docker/cli-plugins` OR `/usr/local/libexec/docker/cli-plugins`
* `/usr/lib/docker/cli-plugins` OR `/usr/libexec/docker/cli-plugins`
(might require making the downloaded file executable with `chmod +x`)
Quick Start
-----------
Using Docker Compose is a three-step process:
1. Define your app's environment with a `Dockerfile` so it can be
reproduced anywhere.
2. Define the services that make up your app in `compose.yaml` so
they can be run together in an isolated environment.
3. Lastly, run `docker compose up` and Compose will start and run your entire
app.
A Compose file looks like this:
```yaml
web:
build: .
links:
- db
ports:
- 8000:8000
db:
image: orchardup/postgresql
services:
web:
build: .
ports:
- "5000:5000"
volumes:
- .:/code
redis:
image: redis
```
Then type `fig up`, and Fig will start and run your entire app:
Contributing
------------
![example fig run](https://orchardup.com/static/images/fig-example-large.f96065fc9e22.gif)
Want to help develop Docker Compose? Check out our
[contributing documentation](CONTRIBUTING.md).
There are commands to:
If you find an issue, please report it on the
[issue tracker](https://github.com/docker/compose/issues/new/choose).
- start, stop and rebuild services
- view the status of running services
- tail running services' log output
- run a one-off command on a service
Legacy
-------------
Fig is a project from [Orchard](https://orchardup.com), a Docker hosting service. [Follow us on Twitter](https://twitter.com/orchardup) to keep up to date with Fig and other Docker news.
Getting started
---------------
Let's get a basic Python web app running on Fig. It assumes a little knowledge of Python, but the concepts should be clear if you're not familiar with it.
First, install Docker. If you're on OS X, you can use [docker-osx](https://github.com/noplay/docker-osx):
$ curl https://raw.github.com/noplay/docker-osx/master/docker-osx > /usr/local/bin/docker-osx
$ chmod +x /usr/local/bin/docker-osx
$ docker-osx shell
Docker has guides for [Ubuntu](http://docs.docker.io/en/latest/installation/ubuntulinux/) and [other platforms](http://docs.docker.io/en/latest/installation/) in their documentation.
Next, install Fig:
$ sudo pip install fig
(If you dont have pip installed, try `brew install python` or `apt-get install python-pip`.)
You'll want to make a directory for the project:
$ mkdir figtest
$ cd figtest
Inside this directory, create `app.py`, a simple web app that uses the Flask framework and increments a value in Redis:
```python
from flask import Flask
from redis import Redis
import os
app = Flask(__name__)
redis = Redis(
host=os.environ.get('FIGTEST_REDIS_1_PORT_6379_TCP_ADDR'),
port=int(os.environ.get('FIGTEST_REDIS_1_PORT_6379_TCP_PORT'))
)
@app.route('/')
def hello():
redis.incr('hits')
return 'Hello World! I have been seen %s times.' % redis.get('hits')
if __name__ == "__main__":
app.run(host="0.0.0.0", debug=True)
```
We define our Python dependencies in a file called `requirements.txt`:
flask
redis
And we define how to build this into a Docker image using a file called `Dockerfile`:
FROM stackbrew/ubuntu:13.10
RUN apt-get -qq update
RUN apt-get install -y python python-pip
ADD . /code
WORKDIR /code
RUN pip install -r requirements.txt
EXPOSE 5000
CMD python app.py
That tells Docker to create an image with Python and Flask installed on it, run the command `python app.py`, and open port 5000 (the port that Flask listens on).
We then define a set of services using `fig.yml`:
web:
build: .
ports:
- 5000:5000
volumes:
- .:/code
links:
- redis
redis:
image: orchardup/redis
This defines two services:
- `web`, which is built from `Dockerfile` in the current directory. It also says to forward the exposed port 5000 on the container to port 5000 on the host machine, connect up the Redis service, and mount the current directory inside the container so we can work on code without having to rebuild the image.
- `redis`, which uses the public image [orchardup/redis](https://index.docker.io/u/orchardup/redis/).
Now if we run `fig up`, it'll pull a Redis image, build an image for our own code, and start everything up:
$ fig up
Pulling image orchardup/redis...
Building web...
Starting figtest_redis_1...
Starting figtest_web_1...
figtest_redis_1 | [8] 02 Jan 18:43:35.576 # Server started, Redis version 2.8.3
figtest_web_1 | * Running on http://0.0.0.0:5000/
Open up [http://localhost:5000](http://localhost:5000) in your browser (or [http://localdocker:5000](http://localdocker:5000) if you're using [docker-osx](https://github.com/noplay/docker-osx)) and you should see it running!
If you want to run your services in the background, you can pass the `-d` flag to `fig up` and use `fig ps` to see what is currently running:
$ fig up -d
Starting figtest_redis_1...
Starting figtest_web_1...
$ fig ps
Name Command State Ports
-------------------------------------------------------------------
figtest_redis_1 /usr/local/bin/run Up
figtest_web_1 /bin/sh -c python app.py Up 5000->5000/tcp
`fig run` allows you to run one-off commands for your services. For example, to see what environment variables are available to the `web` service:
$ fig run web env
See `fig --help` other commands that are available.
If you started Fig with `fig up -d`, you'll probably want to stop your services once you've finished with them:
$ fig stop
That's more-or-less how Fig works. See the reference section below for full details on the commands, configuration file and environment variables. If you have any thoughts or suggestions, [open an issue on GitHub](https://github.com/orchardup/fig) or [email us](mailto:hello@orchardup.com).
Reference
---------
### fig.yml
Each service defined in `fig.yml` must specify exactly one of `image` or `build`. Other keys are optional, and are analogous to their `docker run` command-line counterparts.
As with `docker run`, options specified in the Dockerfile (e.g. `CMD`, `EXPOSE`, `VOLUME`, `ENV`) are respected by default - you don't need to specify them again in `fig.yml`.
```yaml
-- Tag or partial image ID. Can be local or remote - Fig will attempt to pull if it doesn't exist locally.
image: ubuntu
image: orchardup/postgresql
image: a4bc65fd
-- Path to a directory containing a Dockerfile. Fig will build and tag it with a generated name, and use that image thereafter.
build: /path/to/build/dir
-- Override the default command.
command: bundle exec thin -p 3000
-- Link to containers in another service (see "Communicating between containers").
links:
- db
- redis
-- Expose ports. Either specify both ports (HOST:CONTAINER), or just the container port (a random host port will be chosen).
ports:
- 3000
- 8000:8000
-- Map volumes from the host machine (HOST:CONTAINER).
volumes:
- cache/:/tmp/cache
-- Add environment variables.
environment:
RACK_ENV: development
```
### Commands
Most commands are run against one or more services. If the service is omitted, it will apply to all services.
Run `fig [COMMAND] --help` for full usage.
#### build
Build or rebuild services.
Services are built once and then tagged as `project_service`, e.g. `figtest_db`. If you change a service's `Dockerfile` or the contents of its build directory, you can run `fig build` to rebuild it.
#### help
Get help on a command.
#### kill
Force stop service containers.
#### logs
View output from services.
#### ps
List containers.
#### rm
Remove stopped service containers.
#### run
Run a one-off command on a service.
For example:
$ fig run web python manage.py shell
Note that this will not start any services that the command's service links to. So if, for example, your one-off command talks to your database, you will need to run `fig up -d db` first.
#### scale
Set number of containers to run for a service.
Numbers are specified in the form `service=num` as arguments.
For example:
$ fig scale web=2 worker=3
#### start
Start existing containers for a service.
#### stop
Stop running containers without removing them. They can be started again with `fig start`.
#### up
Build, (re)create, start and attach to containers for a service.
By default, `fig up` will aggregate the output of each container, and when it exits, all containers will be stopped. If you run `fig up -d`, it'll start the containers in the background and leave them running.
If there are existing containers for a service, `fig up` will stop and recreate them (preserving mounted volumes with [volumes-from]), so that changes in `fig.yml` are picked up.
### Environment variables
Fig uses [Docker links] to expose services' containers to one another. Each linked container injects a set of environment variables, each of which begins with the uppercase name of the container.
<b><i>name</i>\_PORT</b><br>
Full URL, e.g. `MYAPP_DB_1_PORT=tcp://172.17.0.5:5432`
<b><i>name</i>\_PORT\_<i>num</i>\_<i>protocol</i></b><br>
Full URL, e.g. `MYAPP_DB_1_PORT_5432_TCP=tcp://172.17.0.5:5432`
<b><i>name</i>\_PORT\_<i>num</i>\_<i>protocol</i>\_ADDR</b><br>
Container's IP address, e.g. `MYAPP_DB_1_PORT_5432_TCP_ADDR=172.17.0.5`
<b><i>name</i>\_PORT\_<i>num</i>\_<i>protocol</i>\_PORT</b><br>
Exposed port number, e.g. `MYAPP_DB_1_PORT_5432_TCP_PORT=5432`
<b><i>name</i>\_PORT\_<i>num</i>\_<i>protocol</i>\_PROTO</b><br>
Protocol (tcp or udp), e.g. `MYAPP_DB_1_PORT_5432_TCP_PROTO=tcp`
<b><i>name</i>\_NAME</b><br>
Fully qualified container name, e.g. `MYAPP_DB_1_NAME=/myapp_web_1/myapp_db_1`
[Docker links]: http://docs.docker.io/en/latest/use/port_redirection/#linking-a-container
[volumes-from]: http://docs.docker.io/en/latest/use/working_with_volumes/
The Python version of Compose is available under the `v1` [branch](https://github.com/docker/compose/tree/v1).

147
cmd/cmdtrace/cmd_span.go Normal file
View File

@ -0,0 +1,147 @@
/*
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 cmdtrace
import (
"context"
"errors"
"fmt"
"sort"
"strings"
"time"
dockercli "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
commands "github.com/docker/compose/v2/cmd/compose"
"github.com/docker/compose/v2/internal/tracing"
"github.com/spf13/cobra"
flag "github.com/spf13/pflag"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
)
// Setup should be called as part of the command's PersistentPreRunE
// as soon as possible after initializing the dockerCli.
//
// It initializes the tracer for the CLI using both auto-detection
// from the Docker context metadata as well as standard OTEL_ env
// vars, creates a root span for the command, and wraps the actual
// command invocation to ensure the span is properly finalized and
// exported before exit.
func Setup(cmd *cobra.Command, dockerCli command.Cli, args []string) error {
tracingShutdown, err := tracing.InitTracing(dockerCli)
if err != nil {
return fmt.Errorf("initializing tracing: %w", err)
}
ctx := cmd.Context()
ctx, cmdSpan := otel.Tracer("").Start(
ctx,
"cli/"+strings.Join(commandName(cmd), "-"),
)
cmdSpan.SetAttributes(
attribute.StringSlice("cli.flags", getFlags(cmd.Flags())),
attribute.Bool("cli.isatty", dockerCli.In().IsTerminal()),
)
cmd.SetContext(ctx)
wrapRunE(cmd, cmdSpan, tracingShutdown)
return nil
}
// wrapRunE injects a wrapper function around the command's actual RunE (or Run)
// method. This is necessary to capture the command result for reporting as well
// as flushing any spans before exit.
//
// Unfortunately, PersistentPostRun(E) can't be used for this purpose because it
// only runs if RunE does _not_ return an error, but this should run unconditionally.
func wrapRunE(c *cobra.Command, cmdSpan trace.Span, tracingShutdown tracing.ShutdownFunc) {
origRunE := c.RunE
if origRunE == nil {
origRun := c.Run
//nolint:unparam // wrapper function for RunE, always returns nil by design
origRunE = func(cmd *cobra.Command, args []string) error {
origRun(cmd, args)
return nil
}
c.Run = nil
}
c.RunE = func(cmd *cobra.Command, args []string) error {
cmdErr := origRunE(cmd, args)
if cmdSpan != nil {
if cmdErr != nil && !errors.Is(cmdErr, context.Canceled) {
// default exit code is 1 if a more descriptive error
// wasn't returned
exitCode := 1
var statusErr dockercli.StatusError
if errors.As(cmdErr, &statusErr) {
exitCode = statusErr.StatusCode
}
cmdSpan.SetStatus(codes.Error, "CLI command returned error")
cmdSpan.RecordError(cmdErr, trace.WithAttributes(
attribute.Int("exit_code", exitCode),
))
} else {
cmdSpan.SetStatus(codes.Ok, "")
}
cmdSpan.End()
}
if tracingShutdown != nil {
// use background for root context because the cmd's context might have
// been canceled already
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
// TODO(milas): add an env var to enable logging from the
// OTel components for debugging purposes
_ = tracingShutdown(ctx)
}
return cmdErr
}
}
// 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")
// are not included.
//
// For example:
// - docker compose alpha watch -> [watch, alpha]
// - docker-compose up -> [up]
func commandName(cmd *cobra.Command) []string {
var name []string
for c := cmd; c != nil; c = c.Parent() {
if c.Name() == commands.PluginName {
break
}
name = append(name, c.Name())
}
sort.Sort(sort.Reverse(sort.StringSlice(name)))
return name
}
func getFlags(fs *flag.FlagSet) []string {
var result []string
fs.Visit(func(flag *flag.Flag) {
result = append(result, flag.Name)
})
return result
}

View File

@ -0,0 +1,112 @@
/*
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 cmdtrace
import (
"reflect"
"testing"
commands "github.com/docker/compose/v2/cmd/compose"
"github.com/spf13/cobra"
flag "github.com/spf13/pflag"
)
func TestGetFlags(t *testing.T) {
// Initialize flagSet with flags
fs := flag.NewFlagSet("up", flag.ContinueOnError)
var (
detach string
timeout string
)
fs.StringVar(&detach, "detach", "d", "")
fs.StringVar(&timeout, "timeout", "t", "")
_ = fs.Set("detach", "detach")
_ = fs.Set("timeout", "timeout")
tests := []struct {
name string
input *flag.FlagSet
expected []string
}{
{
name: "NoFlags",
input: flag.NewFlagSet("NoFlags", flag.ContinueOnError),
expected: nil,
},
{
name: "Flags",
input: fs,
expected: []string{"detach", "timeout"},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result := getFlags(test.input)
if !reflect.DeepEqual(result, test.expected) {
t.Errorf("Expected %v, but got %v", test.expected, result)
}
})
}
}
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

@ -0,0 +1,116 @@
/*
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 compatibility
import (
"fmt"
"os"
"strings"
"github.com/docker/compose/v2/cmd/compose"
)
func getCompletionCommands() []string {
return []string{
"__complete",
"__completeNoDesc",
}
}
func getBoolFlags() []string {
return []string{
"--debug", "-D",
"--verbose",
"--tls",
"--tlsverify",
}
}
func getStringFlags() []string {
return []string{
"--tlscacert",
"--tlscert",
"--tlskey",
"--host", "-H",
"--context",
"--log-level",
}
}
// Convert transforms standalone docker-compose args into CLI plugin compliant ones
func Convert(args []string) []string {
var rootFlags []string
command := []string{compose.PluginName}
l := len(args)
ARGS:
for i := 0; i < l; i++ {
arg := args[i]
if contains(getCompletionCommands(), arg) {
command = append([]string{arg}, command...)
continue
}
if arg != "" && arg[0] != '-' {
command = append(command, args[i:]...)
break
}
switch arg {
case "--verbose":
arg = "--debug"
case "-h":
// docker cli has deprecated -h to avoid ambiguity with -H, while docker-compose still support it
arg = "--help"
case "--version", "-v":
// redirect --version pseudo-command to actual command
arg = "version"
}
if contains(getBoolFlags(), arg) {
rootFlags = append(rootFlags, arg)
continue
}
for _, flag := range getStringFlags() {
if arg == flag {
i++
if i >= l {
fmt.Fprintf(os.Stderr, "flag needs an argument: '%s'\n", arg)
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
}
}
}
command = append(command, arg)
}
return append(rootFlags, command...)
}
func contains(array []string, needle string) bool {
for _, val := range array {
if val == needle {
return true
}
}
return false
}

View File

@ -0,0 +1,132 @@
/*
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 compatibility
import (
"errors"
"os"
"os/exec"
"testing"
"gotest.tools/v3/assert"
)
func Test_convert(t *testing.T) {
tests := []struct {
name string
args []string
want []string
wantErr bool
}{
{
name: "compose only",
args: []string{"up"},
want: []string{"compose", "up"},
},
{
name: "with context",
args: []string{"--context", "foo", "-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",
args: []string{"--host", "tcp://1.2.3.4", "up"},
want: []string{"--host", "tcp://1.2.3.4", "compose", "up"},
},
{
name: "compose --verbose",
args: []string{"--verbose"},
want: []string{"--debug", "compose"},
},
{
name: "compose --version",
args: []string{"--version"},
want: []string{"compose", "version"},
},
{
name: "compose -v",
args: []string{"-v"},
want: []string{"compose", "version"},
},
{
name: "help",
args: []string{"-h"},
want: []string{"compose", "--help"},
},
{
name: "issues/1962",
args: []string{"psql", "-h", "postgres"},
want: []string{"compose", "psql", "-h", "postgres"}, // -h should not be converted to --help
},
{
name: "issues/8648",
args: []string{"exec", "mongo", "mongo", "--host", "mongo"},
want: []string{"compose", "exec", "mongo", "mongo", "--host", "mongo"}, // --host is passed to exec
},
{
name: "issues/12",
args: []string{"--log-level", "INFO", "up"},
want: []string{"--log-level", "INFO", "compose", "up"},
},
{
name: "empty string argument",
args: []string{"--project-directory", "", "ps"},
want: []string{"compose", "--project-directory", "", "ps"},
},
{
name: "compose as project name",
args: []string{"--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 {
t.Run(tt.name, func(t *testing.T) {
if tt.wantErr {
if os.Getenv("BE_CRASHER") == "1" {
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)
}
})
}
}

39
cmd/compose/alpha.go Normal file
View File

@ -0,0 +1,39 @@
/*
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 (
"github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/pkg/api"
"github.com/spf13/cobra"
)
// alphaCommand groups all experimental subcommands
func alphaCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
cmd := &cobra.Command{
Short: "Experimental commands",
Use: "alpha [COMMAND]",
Hidden: true,
Annotations: map[string]string{
"experimentalCLI": "true",
},
}
cmd.AddCommand(
vizCommand(p, dockerCli, backend),
publishCommand(p, dockerCli, backend),
generateCommand(p, backend),
)
return cmd
}

80
cmd/compose/attach.go Normal file
View File

@ -0,0 +1,80 @@
/*
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/compose/v2/pkg/api"
"github.com/spf13/cobra"
)
type attachOpts struct {
*composeOptions
service string
index int
detachKeys string
noStdin bool
proxy bool
}
func attachCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
opts := attachOpts{
composeOptions: &composeOptions{
ProjectOptions: p,
},
}
runCmd := &cobra.Command{
Use: "attach [OPTIONS] SERVICE",
Short: "Attach local standard input, output, and error streams to a service's running container",
Args: cobra.MinimumNArgs(1),
PreRunE: Adapt(func(ctx context.Context, args []string) error {
opts.service = args[0]
return nil
}),
RunE: Adapt(func(ctx context.Context, args []string) error {
return runAttach(ctx, dockerCli, backend, opts)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
runCmd.Flags().IntVar(&opts.index, "index", 0, "index of the container if service has multiple replicas.")
runCmd.Flags().StringVarP(&opts.detachKeys, "detach-keys", "", "", "Override the key sequence for detaching from a container.")
runCmd.Flags().BoolVar(&opts.noStdin, "no-stdin", false, "Do not attach STDIN")
runCmd.Flags().BoolVar(&opts.proxy, "sig-proxy", true, "Proxy all received signals to the process")
return runCmd
}
func runAttach(ctx context.Context, dockerCli command.Cli, backend api.Service, opts attachOpts) error {
projectName, err := opts.toProjectName(ctx, dockerCli)
if err != nil {
return err
}
attachOpts := api.AttachOptions{
Service: opts.service,
Index: opts.index,
DetachKeys: opts.detachKeys,
NoStdin: opts.noStdin,
Proxy: opts.proxy,
}
return backend.Attach(ctx, projectName, attachOpts)
}

149
cmd/compose/bridge.go Normal file
View File

@ -0,0 +1,149 @@
/*
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
}

169
cmd/compose/build.go Normal file
View File

@ -0,0 +1,169 @@
/*
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"
"os"
"strings"
"github.com/compose-spec/compose-go/v2/cli"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli/command"
cliopts "github.com/docker/cli/opts"
ui "github.com/docker/compose/v2/pkg/progress"
"github.com/spf13/cobra"
"github.com/docker/compose/v2/pkg/api"
)
type buildOptions struct {
*ProjectOptions
quiet bool
pull bool
push bool
args []string
noCache bool
memory cliopts.MemBytes
ssh string
builder string
deps bool
print bool
check bool
sbom string
provenance string
}
func (opts buildOptions) toAPIBuildOptions(services []string) (api.BuildOptions, error) {
var SSHKeys []types.SSHKey
if opts.ssh != "" {
id, path, found := strings.Cut(opts.ssh, "=")
if !found && id != "default" {
return api.BuildOptions{}, fmt.Errorf("invalid ssh key %q", opts.ssh)
}
SSHKeys = append(SSHKeys, types.SSHKey{
ID: id,
Path: path,
})
}
builderName := opts.builder
if builderName == "" {
builderName = os.Getenv("BUILDX_BUILDER")
}
uiMode := ui.Mode
if uiMode == ui.ModeJSON {
uiMode = "rawjson"
}
return api.BuildOptions{
Pull: opts.pull,
Push: opts.push,
Progress: uiMode,
Args: types.NewMappingWithEquals(opts.args),
NoCache: opts.noCache,
Quiet: opts.quiet,
Services: services,
Deps: opts.deps,
Memory: int64(opts.memory),
Print: opts.print,
Check: opts.check,
SSHs: SSHKeys,
Builder: builderName,
SBOM: opts.sbom,
Provenance: opts.provenance,
}, nil
}
func buildCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
opts := buildOptions{
ProjectOptions: p,
}
cmd := &cobra.Command{
Use: "build [OPTIONS] [SERVICE...]",
Short: "Build or rebuild services",
PreRunE: Adapt(func(ctx context.Context, args []string) error {
if opts.quiet {
ui.Mode = ui.ModeQuiet
devnull, err := os.Open(os.DevNull)
if err != nil {
return err
}
os.Stdout = devnull
}
return nil
}),
RunE: AdaptCmd(func(ctx context.Context, cmd *cobra.Command, args []string) error {
if cmd.Flags().Changed("ssh") && opts.ssh == "" {
opts.ssh = "default"
}
if cmd.Flags().Changed("progress") && opts.ssh == "" {
fmt.Fprint(os.Stderr, "--progress is a global compose flag, better use `docker compose --progress xx build ...\n")
}
return runBuild(ctx, dockerCli, backend, opts, args)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
flags := cmd.Flags()
flags.BoolVar(&opts.push, "push", false, "Push service images")
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Suppress the build output")
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.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.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.MarkHidden("parallel") //nolint:errcheck
flags.Bool("compress", true, "Compress the build context using gzip. DEPRECATED")
flags.MarkHidden("compress") //nolint:errcheck
flags.Bool("force-rm", true, "Always remove intermediate containers. DEPRECATED")
flags.MarkHidden("force-rm") //nolint:errcheck
flags.BoolVar(&opts.noCache, "no-cache", false, "Do not use cache when building the image")
flags.Bool("no-rm", false, "Do not remove intermediate containers after a successful build. DEPRECATED")
flags.MarkHidden("no-rm") //nolint:errcheck
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.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
}
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(ctx, dockerCli, nil, cli.WithResolvedPaths(true), cli.WithoutEnvironmentResolution)
if err != nil {
return err
}
if err := applyPlatforms(project, false); err != nil {
return err
}
apiBuildOptions, err := opts.toAPIBuildOptions(services)
apiBuildOptions.Attestations = true
if err != nil {
return err
}
return backend.Build(ctx, project, apiBuildOptions)
}

93
cmd/compose/commit.go Normal file
View File

@ -0,0 +1,93 @@
/*
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)
}

102
cmd/compose/completion.go Normal file
View File

@ -0,0 +1,102 @@
/*
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 (
"sort"
"strings"
"github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/pkg/api"
"github.com/spf13/cobra"
)
// validArgsFn defines a completion func to be returned to fetch completion options
type validArgsFn func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective)
func noCompletion() validArgsFn {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{}, cobra.ShellCompDirectiveNoSpace
}
}
func completeServiceNames(dockerCli command.Cli, p *ProjectOptions) validArgsFn {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
p.Offline = true
project, _, err := p.ToProject(cmd.Context(), dockerCli, nil)
if err != nil {
return nil, cobra.ShellCompDirectiveNoFileComp
}
var values []string
serviceNames := append(project.ServiceNames(), project.DisabledServiceNames()...)
for _, s := range serviceNames {
if toComplete == "" || strings.HasPrefix(s, toComplete) {
values = append(values, s)
}
}
return values, cobra.ShellCompDirectiveNoFileComp
}
}
func completeProjectNames(backend api.Service) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
list, err := backend.List(cmd.Context(), api.ListOptions{
All: true,
})
if err != nil {
return nil, cobra.ShellCompDirectiveError
}
var values []string
for _, stack := range list {
if strings.HasPrefix(stack.Name, toComplete) {
values = append(values, stack.Name)
}
}
return values, cobra.ShellCompDirectiveNoFileComp
}
}
func completeProfileNames(dockerCli command.Cli, p *ProjectOptions) validArgsFn {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
p.Offline = true
project, _, err := p.ToProject(cmd.Context(), dockerCli, nil)
if err != nil {
return nil, cobra.ShellCompDirectiveNoFileComp
}
allProfileNames := project.AllServices().GetProfiles()
sort.Strings(allProfileNames)
var values []string
for _, profileName := range allProfileNames {
if strings.HasPrefix(profileName, toComplete) {
values = append(values, profileName)
}
}
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
}
}

723
cmd/compose/compose.go Normal file
View File

@ -0,0 +1,723 @@
/*
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"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"os/signal"
"path/filepath"
"strconv"
"strings"
"syscall"
"github.com/compose-spec/compose-go/v2/cli"
"github.com/compose-spec/compose-go/v2/dotenv"
"github.com/compose-spec/compose-go/v2/loader"
"github.com/compose-spec/compose-go/v2/types"
composegoutils "github.com/compose-spec/compose-go/v2/utils"
"github.com/docker/buildx/util/logutil"
dockercli "github.com/docker/cli/cli"
"github.com/docker/cli/cli-plugins/metadata"
"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/utils"
"github.com/morikuni/aec"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
const (
// ComposeParallelLimit set the limit running concurrent operation on docker engine
ComposeParallelLimit = "COMPOSE_PARALLEL_LIMIT"
// ComposeProjectName define the project name to be used, instead of guessing from parent directory
ComposeProjectName = "COMPOSE_PROJECT_NAME"
// ComposeCompatibility try to mimic compose v1 as much as possible
ComposeCompatibility = "COMPOSE_COMPATIBILITY"
// ComposeRemoveOrphans remove "orphaned" containers, i.e. containers tagged for current project but not declared as service
ComposeRemoveOrphans = "COMPOSE_REMOVE_ORPHANS"
// ComposeIgnoreOrphans ignore "orphaned" containers
ComposeIgnoreOrphans = "COMPOSE_IGNORE_ORPHANS"
// ComposeEnvFiles defines the env files to use if --env-file isn't used
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
type Command func(context.Context, []string) error
// CobraCommand defines a cobra command function
type CobraCommand func(context.Context, *cobra.Command, []string) error
// AdaptCmd adapt a CobraCommand func to cobra library
func AdaptCmd(fn CobraCommand) func(cmd *cobra.Command, args []string) error {
return func(cmd *cobra.Command, args []string) error {
ctx, cancel := context.WithCancel(cmd.Context())
s := make(chan os.Signal, 1)
signal.Notify(s, syscall.SIGTERM, syscall.SIGINT)
go func() {
<-s
cancel()
signal.Stop(s)
close(s)
}()
err := fn(ctx, cmd, args)
if api.IsErrCanceled(err) || errors.Is(ctx.Err(), context.Canceled) {
err = dockercli.StatusError{
StatusCode: 130,
}
}
if ui.Mode == ui.ModeJSON {
err = makeJSONError(err)
}
return err
}
}
// Adapt a Command func to cobra library
func Adapt(fn Command) func(cmd *cobra.Command, args []string) error {
return AdaptCmd(func(ctx context.Context, cmd *cobra.Command, args []string) error {
return fn(ctx, args)
})
}
type ProjectOptions struct {
ProjectName string
Profiles []string
ConfigPaths []string
WorkDir string
ProjectDir string
EnvFiles []string
Compatibility bool
Progress string
Offline bool
All bool
}
// ProjectFunc does stuff within a types.Project
type ProjectFunc func(ctx context.Context, project *types.Project) error
// ProjectServicesFunc does stuff within a types.Project and a selection of services
type ProjectServicesFunc func(ctx context.Context, project *types.Project, services []string) error
// WithProject creates a cobra run command from a ProjectFunc based on configured project options and selected services
func (o *ProjectOptions) WithProject(fn ProjectFunc, dockerCli command.Cli) func(cmd *cobra.Command, args []string) error {
return o.WithServices(dockerCli, func(ctx context.Context, project *types.Project, services []string) error {
return fn(ctx, project)
})
}
// WithServices creates a cobra run command from a ProjectFunc based on configured project options and selected services
func (o *ProjectOptions) WithServices(dockerCli command.Cli, fn ProjectServicesFunc) func(cmd *cobra.Command, args []string) error {
return Adapt(func(ctx context.Context, args []string) error {
options := []cli.ProjectOptionsFn{
cli.WithResolvedPaths(true),
cli.WithoutEnvironmentResolution,
}
project, metrics, err := o.ToProject(ctx, dockerCli, args, options...)
if err != nil {
return err
}
ctx = context.WithValue(ctx, tracing.MetricsKey{}, metrics)
project, err = project.WithServicesEnvironmentResolved(true)
if err != nil {
return err
}
return fn(ctx, project, args)
})
}
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) {
f.StringArrayVar(&o.Profiles, "profile", []string{}, "Specify a profile to enable")
f.StringVarP(&o.ProjectName, "project-name", "p", "", "Project name")
f.StringArrayVarP(&o.ConfigPaths, "file", "f", []string{}, "Compose configuration files")
f.StringArrayVar(&o.EnvFiles, "env-file", defaultStringArrayVar(ComposeEnvFiles), "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.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.StringVar(&o.Progress, "progress", os.Getenv(ComposeProgress), 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")
}
// get default value for a command line flag that is set by a coma-separated value in environment variable
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
var project *types.Project
if len(o.ConfigPaths) > 0 || o.ProjectName == "" {
p, _, err := o.ToProject(ctx, dockerCli, services, cli.WithDiscardEnvFile, cli.WithoutEnvironmentResolution)
if err != nil {
envProjectName := os.Getenv(ComposeProjectName)
if envProjectName != "" {
return nil, envProjectName, nil
}
return nil, "", err
}
project = p
name = p.Name
}
return project, name, nil
}
func (o *ProjectOptions) toProjectName(ctx context.Context, dockerCli command.Cli) (string, error) {
if o.ProjectName != "" {
return o.ProjectName, nil
}
envProjectName := os.Getenv(ComposeProjectName)
if envProjectName != "" {
return envProjectName, nil
}
project, _, err := o.ToProject(ctx, dockerCli, nil)
if err != nil {
return "", err
}
return project.Name, nil
}
func (o *ProjectOptions) ToModel(ctx context.Context, dockerCli command.Cli, services []string, po ...cli.ProjectOptionsFn) (map[string]any, error) {
remotes := o.remoteLoaders(dockerCli)
for _, r := range remotes {
po = append(po, cli.WithResourceLoader(r))
}
options, err := o.toProjectOptions(po...)
if err != nil {
return nil, err
}
if o.Compatibility || utils.StringToBool(options.Environment[ComposeCompatibility]) {
api.Separator = "_"
}
return options.LoadModel(ctx)
}
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 {
return nil, metrics, 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 == "" {
return nil, metrics, errors.New("project name can't be empty. Use `--project-name` to set a valid name")
}
project, err = project.WithServicesEnabled(services...)
if err != nil {
return nil, metrics, err
}
for name, s := range project.Services {
s.CustomLabels = map[string]string{
api.ProjectLabel: project.Name,
api.ServiceLabel: name,
api.VersionLabel: api.ComposeVersion,
api.WorkingDirLabel: project.WorkingDir,
api.ConfigFilesLabel: strings.Join(project.ComposeFiles, ","),
api.OneoffLabel: "False", // default, will be overridden by `run` command
}
if len(o.EnvFiles) != 0 {
s.CustomLabels[api.EnvironmentFileLabel] = strings.Join(o.EnvFiles, ",")
}
project.Services[name] = s
}
project, err = project.WithSelectedServices(services)
if err != nil {
return nil, tracing.Metrics{}, err
}
if !o.All {
project = project.WithoutUnnecessaryResources()
}
return project, metrics, err
}
func (o *ProjectOptions) remoteLoaders(dockerCli command.Cli) []loader.ResourceLoader {
if o.Offline {
return nil
}
git := remote.NewGitRemoteLoader(dockerCli, o.Offline)
oci := remote.NewOCIRemoteLoader(dockerCli, o.Offline)
return []loader.ResourceLoader{git, oci}
}
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,
append(po,
cli.WithWorkingDirectory(o.ProjectDir),
// First apply os.Environment, always win
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...),
// read dot env file to populate project environment
cli.WithDotEnv,
// get compose file path set by COMPOSE_FILE
cli.WithConfigFileEnv,
// if none was selected, get default compose.yaml file from current dir or parent folder
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.WithName(o.ProjectName))...)
}
// PluginName is the name of the plugin
const PluginName = "compose"
// RunningAsStandalone detects when running as a standalone program
func RunningAsStandalone() bool {
return len(os.Args) < 2 || os.Args[1] != metadata.MetadataSubcommandName && os.Args[1] != PluginName
}
// RootCommand returns the compose command with its child commands
func RootCommand(dockerCli command.Cli, backend Backend) *cobra.Command { //nolint:gocyclo
// 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"
// https://github.com/docker/cli/blob/e1f24d3c93df6752d3c27c8d61d18260f141310c/cli/connhelper/commandconn/commandconn.go#L203-L215
logrus.AddHook(logutil.NewFilter([]logrus.Level{
logrus.WarnLevel,
},
"commandConn.CloseWrite:",
"commandConn.CloseRead:",
))
experiments := experimental.NewState()
opts := ProjectOptions{}
var (
ansi string
noAnsi bool
verbose bool
version bool
parallel int
dryRun bool
)
c := &cobra.Command{
Short: "Docker Compose",
Long: "Define and run multi-container applications with Docker",
Use: PluginName,
TraverseChildren: true,
// By default (no Run/RunE in parent c) for typos in subcommands, cobra displays the help of parent c but exit(0) !
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return cmd.Help()
}
if version {
return versionCommand(dockerCli).Execute()
}
_ = cmd.Help()
return dockercli.StatusError{
StatusCode: 1,
Status: fmt.Sprintf("unknown docker command: %q", "compose "+args[0]),
}
},
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
parent := cmd.Root()
if parent != nil {
parentPrerun := parent.PersistentPreRunE
if parentPrerun != nil {
err := parentPrerun(cmd, args)
if err != nil {
return err
}
}
}
if verbose {
logrus.SetLevel(logrus.TraceLevel)
}
err := setEnvWithDotEnv(opts)
if err != nil {
return err
}
if noAnsi {
if ansi != "auto" {
return errors.New(`cannot specify DEPRECATED "--no-ansi" and "--ansi". Please use only "--ansi"`)
}
ansi = "never"
fmt.Fprint(os.Stderr, "option '--no-ansi' is DEPRECATED ! Please use '--ansi' instead.\n")
}
if v, ok := os.LookupEnv("COMPOSE_ANSI"); ok && !cmd.Flags().Changed("ansi") {
ansi = v
}
formatter.SetANSIMode(dockerCli, ansi)
if noColor, ok := os.LookupEnv("NO_COLOR"); ok && noColor != "" {
ui.NoColor()
formatter.SetANSIMode(dockerCli, formatter.Never)
}
switch ansi {
case "never":
ui.Mode = ui.ModePlain
case "always":
ui.Mode = ui.ModeTTY
}
switch opts.Progress {
case "", ui.ModeAuto:
if ansi == "never" {
ui.Mode = ui.ModePlain
}
case ui.ModeTTY:
if ansi == "never" {
return fmt.Errorf("can't use --progress tty while ANSI support is disabled")
}
ui.Mode = ui.ModeTTY
case ui.ModePlain:
if ansi == "always" {
return fmt.Errorf("can't use --progress plain while ANSI support is forced")
}
ui.Mode = ui.ModePlain
case ui.ModeQuiet, "none":
ui.Mode = ui.ModeQuiet
case ui.ModeJSON:
ui.Mode = ui.ModeJSON
logrus.SetFormatter(&logrus.JSONFormatter{})
default:
return fmt.Errorf("unsupported --progress value %q", opts.Progress)
}
// (4) options validation / normalization
if opts.WorkDir != "" {
if opts.ProjectDir != "" {
return errors.New(`cannot specify DEPRECATED "--workdir" and "--project-directory". Please use only "--project-directory" instead`)
}
opts.ProjectDir = opts.WorkDir
fmt.Fprint(os.Stderr, aec.Apply("option '--workdir' is DEPRECATED at root level! Please use '--project-directory' instead.\n", aec.RedF))
}
for i, file := range opts.EnvFiles {
if !filepath.IsAbs(file) {
file, err := filepath.Abs(file)
if err != nil {
return err
}
opts.EnvFiles[i] = file
}
}
composeCmd := cmd
for composeCmd.Name() != PluginName {
if !composeCmd.HasParent() {
return fmt.Errorf("error parsing command line, expected %q", PluginName)
}
composeCmd = composeCmd.Parent()
}
if v, ok := os.LookupEnv(ComposeParallelLimit); ok && !composeCmd.Flags().Changed("parallel") {
i, err := strconv.Atoi(v)
if err != nil {
return fmt.Errorf("%s must be an integer (found: %q)", ComposeParallelLimit, v)
}
parallel = i
}
if parallel > 0 {
logrus.Debugf("Limiting max concurrency to %d jobs", parallel)
backend.MaxConcurrency(parallel)
}
// dry run detection
ctx, err = backend.DryRunMode(ctx, dryRun)
if err != nil {
return err
}
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
},
}
c.AddCommand(
upCommand(&opts, dockerCli, backend),
downCommand(&opts, dockerCli, backend),
startCommand(&opts, dockerCli, backend),
restartCommand(&opts, dockerCli, backend),
stopCommand(&opts, dockerCli, backend),
psCommand(&opts, dockerCli, backend),
listCommand(dockerCli, backend),
logsCommand(&opts, dockerCli, backend),
configCommand(&opts, dockerCli),
killCommand(&opts, dockerCli, backend),
runCommand(&opts, dockerCli, backend),
removeCommand(&opts, dockerCli, backend),
execCommand(&opts, dockerCli, backend),
attachCommand(&opts, dockerCli, backend),
exportCommand(&opts, dockerCli, backend),
commitCommand(&opts, dockerCli, backend),
pauseCommand(&opts, dockerCli, backend),
unpauseCommand(&opts, dockerCli, backend),
topCommand(&opts, dockerCli, backend),
eventsCommand(&opts, dockerCli, backend),
portCommand(&opts, dockerCli, backend),
imagesCommand(&opts, dockerCli, backend),
versionCommand(dockerCli),
buildCommand(&opts, dockerCli, backend),
pushCommand(&opts, dockerCli, backend),
pullCommand(&opts, dockerCli, backend),
createCommand(&opts, dockerCli, backend),
copyCommand(&opts, dockerCli, backend),
waitCommand(&opts, dockerCli, backend),
scaleCommand(&opts, dockerCli, backend),
statsCommand(&opts, dockerCli),
watchCommand(&opts, dockerCli, backend),
publishCommand(&opts, dockerCli, backend),
alphaCommand(&opts, dockerCli, backend),
bridgeCommand(&opts, dockerCli),
volumesCommand(&opts, dockerCli, backend),
)
c.Flags().SetInterspersed(false)
opts.addProjectFlags(c.Flags())
c.RegisterFlagCompletionFunc( //nolint:errcheck
"project-name",
completeProjectNames(backend),
)
c.RegisterFlagCompletionFunc( //nolint:errcheck
"project-directory",
func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{}, cobra.ShellCompDirectiveFilterDirs
},
)
c.RegisterFlagCompletionFunc( //nolint:errcheck
"file",
func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{"yaml", "yml"}, cobra.ShellCompDirectiveFilterFileExt
},
)
c.RegisterFlagCompletionFunc( //nolint:errcheck
"profile",
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().IntVar(&parallel, "parallel", -1, `Control max parallelism, -1 for unlimited`)
c.Flags().BoolVarP(&version, "version", "v", false, "Show the Docker Compose version information")
c.PersistentFlags().BoolVar(&dryRun, "dry-run", false, "Execute command in dry run mode")
c.Flags().MarkHidden("version") //nolint:errcheck
c.Flags().BoolVar(&noAnsi, "no-ansi", false, `Do not print ANSI control characters (DEPRECATED)`)
c.Flags().MarkHidden("no-ansi") //nolint:errcheck
c.Flags().BoolVar(&verbose, "verbose", false, "Show more output")
c.Flags().MarkHidden("verbose") //nolint:errcheck
return c
}
func setEnvWithDotEnv(opts ProjectOptions) error {
options, err := cli.NewProjectOptions(opts.ConfigPaths,
cli.WithWorkingDirectory(opts.ProjectDir),
cli.WithOsEnv,
cli.WithEnvFiles(opts.EnvFiles...),
cli.WithDotEnv,
)
if err != nil {
return nil
}
envFromFile, err := dotenv.GetEnvFromFile(composegoutils.GetAsEqualsMap(os.Environ()), options.EnvFiles)
if err != nil {
return nil
}
for k, v := range envFromFile {
if _, ok := os.LookupEnv(k); !ok {
if err = os.Setenv(k, v); err != nil {
return nil
}
}
}
return err
}
var printerModes = []string{
ui.ModeAuto,
ui.ModeTTY,
ui.ModePlain,
ui.ModeJSON,
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

@ -0,0 +1,55 @@
/*
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 (
"testing"
"github.com/compose-spec/compose-go/v2/types"
"gotest.tools/v3/assert"
)
func TestFilterServices(t *testing.T) {
p := &types.Project{
Services: types.Services{
"foo": {
Name: "foo",
Links: []string{"bar"},
},
"bar": {
Name: "bar",
DependsOn: map[string]types.ServiceDependency{
"zot": {},
},
},
"zot": {
Name: "zot",
},
"qix": {
Name: "qix",
},
},
}
p, err := p.WithSelectedServices([]string{"bar"})
assert.NilError(t, err)
assert.Equal(t, len(p.Services), 2)
_, err = p.GetService("bar")
assert.NilError(t, err)
_, err = p.GetService("zot")
assert.NilError(t, err)
}

517
cmd/compose/config.go Normal file
View File

@ -0,0 +1,517 @@
/*
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 (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"os"
"sort"
"strings"
"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/docker/cli/cli/command"
"github.com/docker/compose/v2/cmd/formatter"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/compose"
)
type configOptions struct {
*ProjectOptions
Format string
Output string
quiet bool
resolveImageDigests bool
noInterpolate bool
noNormalize bool
noResolvePath bool
noResolveEnv bool
services bool
volumes bool
networks bool
models bool
profiles bool
images bool
hash string
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) {
po = append(po, o.ToProjectOptions()...)
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.WithResolvedPaths(!o.noResolvePath),
cli.WithNormalization(!o.noNormalize),
cli.WithConsistency(!o.noConsistency),
cli.WithDefaultProfiles(o.Profiles...),
cli.WithDiscardEnvFile,
}
}
func configCommand(p *ProjectOptions, dockerCli command.Cli) *cobra.Command {
opts := configOptions{
ProjectOptions: p,
}
cmd := &cobra.Command{
Use: "config [OPTIONS] [SERVICE...]",
Short: "Parse, resolve and render compose file in canonical format",
PreRunE: Adapt(func(ctx context.Context, args []string) error {
if opts.quiet {
devnull, err := os.Open(os.DevNull)
if err != nil {
return err
}
os.Stdout = devnull
}
if p.Compatibility {
opts.noNormalize = true
}
if opts.lockImageDigests {
opts.resolveImageDigests = true
}
return nil
}),
RunE: Adapt(func(ctx context.Context, args []string) error {
if opts.services {
return runServices(ctx, dockerCli, opts)
}
if opts.volumes {
return runVolumes(ctx, dockerCli, opts)
}
if opts.networks {
return runNetworks(ctx, dockerCli, opts)
}
if opts.models {
return runModels(ctx, dockerCli, opts)
}
if opts.hash != "" {
return runHash(ctx, dockerCli, opts)
}
if opts.profiles {
return runProfiles(ctx, dockerCli, opts, args)
}
if opts.images {
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 == "" {
opts.Format = "yaml"
}
return runConfig(ctx, dockerCli, opts, args)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
flags := cmd.Flags()
flags.StringVar(&opts.Format, "format", "", "Format the output. Values: [yaml | json]")
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.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.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.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.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.images, "images", false, "Print the image names, 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)")
return cmd
}
func runConfig(ctx context.Context, dockerCli command.Cli, opts configOptions, services []string) (err error) {
var content []byte
if opts.noInterpolate {
content, err = runConfigNoInterpolate(ctx, dockerCli, opts, services)
if err != nil {
return err
}
} else {
content, err = runConfigInterpolate(ctx, dockerCli, opts, services)
if err != nil {
return err
}
}
if !opts.noInterpolate {
content = escapeDollarSign(content)
}
if opts.quiet {
return nil
}
if opts.Output != "" && len(content) > 0 {
return os.WriteFile(opts.Output, content, 0o666)
}
_, err = fmt.Fprint(dockerCli.Out(), string(content))
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 {
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)
if err != nil {
return err
}
err = project.ForEachService(project.ServiceNames(), func(serviceName string, _ *types.ServiceConfig) error {
_, _ = fmt.Fprintln(dockerCli.Out(), serviceName)
return nil
})
return err
}
func runVolumes(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.Volumes {
_, _ = 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
}
func runHash(ctx context.Context, dockerCli command.Cli, opts configOptions) error {
var services []string
if opts.hash != "*" {
services = append(services, strings.Split(opts.hash, ",")...)
}
project, err := opts.ToProject(ctx, dockerCli, nil, cli.WithoutEnvironmentResolution)
if err != nil {
return err
}
if err := applyPlatforms(project, true); err != nil {
return err
}
if len(services) == 0 {
services = project.ServiceNames()
}
sorted := services
sort.Slice(sorted, func(i, j int) bool {
return sorted[i] < sorted[j]
})
for _, name := range sorted {
s, err := project.GetService(name)
if err != nil {
return err
}
hash, err := compose.ServiceHash(s)
if err != nil {
return err
}
_, _ = fmt.Fprintf(dockerCli.Out(), "%s %s\n", name, hash)
}
return nil
}
func runProfiles(ctx context.Context, dockerCli command.Cli, opts configOptions, services []string) error {
set := map[string]struct{}{}
project, err := opts.ToProject(ctx, dockerCli, services, cli.WithoutEnvironmentResolution)
if err != nil {
return err
}
for _, s := range project.AllServices() {
for _, p := range s.Profiles {
set[p] = struct{}{}
}
}
profiles := make([]string, 0, len(set))
for p := range set {
profiles = append(profiles, p)
}
sort.Strings(profiles)
for _, p := range profiles {
_, _ = fmt.Fprintln(dockerCli.Out(), p)
}
return nil
}
func runConfigImages(ctx context.Context, dockerCli command.Cli, opts configOptions, services []string) error {
project, err := opts.ToProject(ctx, dockerCli, services, cli.WithoutEnvironmentResolution)
if err != nil {
return err
}
for _, s := range project.Services {
_, _ = 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
}
func escapeDollarSign(marshal []byte) []byte {
dollar := []byte{'$'}
escDollar := []byte{'$', '$'}
return bytes.ReplaceAll(marshal, dollar, escDollar)
}

90
cmd/compose/cp.go Normal file
View File

@ -0,0 +1,90 @@
/*
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"
"errors"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
"github.com/docker/compose/v2/pkg/api"
)
type copyOptions struct {
*ProjectOptions
source string
destination string
index int
all bool
followLink bool
copyUIDGID bool
}
func copyCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
opts := copyOptions{
ProjectOptions: p,
}
copyCmd := &cobra.Command{
Use: `cp [OPTIONS] SERVICE:SRC_PATH DEST_PATH|-
docker compose cp [OPTIONS] SRC_PATH|- SERVICE:DEST_PATH`,
Short: "Copy files/folders between a service container and the local filesystem",
Args: cli.ExactArgs(2),
PreRunE: Adapt(func(ctx context.Context, args []string) error {
if args[0] == "" {
return errors.New("source can not be empty")
}
if args[1] == "" {
return errors.New("destination can not be empty")
}
return nil
}),
RunE: AdaptCmd(func(ctx context.Context, cmd *cobra.Command, args []string) error {
opts.source = args[0]
opts.destination = args[1]
return runCopy(ctx, dockerCli, backend, opts)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
flags := copyCmd.Flags()
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.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)")
return copyCmd
}
func runCopy(ctx context.Context, dockerCli command.Cli, backend api.Service, opts copyOptions) error {
name, err := opts.toProjectName(ctx, dockerCli)
if err != nil {
return err
}
return backend.Copy(ctx, name, api.CopyOptions{
Source: opts.source,
Destination: opts.destination,
All: opts.all,
Index: opts.index,
FollowLink: opts.followLink,
CopyUIDGID: opts.copyUIDGID,
})
}

216
cmd/compose/create.go Normal file
View File

@ -0,0 +1,216 @@
/*
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"
"strconv"
"strings"
"time"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli/command"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/docker/compose/v2/pkg/api"
)
type createOptions struct {
Build bool
noBuild bool
Pull string
pullChanged bool
removeOrphans bool
ignoreOrphans bool
forceRecreate bool
noRecreate bool
recreateDeps bool
noInherit bool
timeChanged bool
timeout int
quietPull bool
scale []string
AssumeYes bool
}
func createCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
opts := createOptions{}
buildOpts := buildOptions{
ProjectOptions: p,
}
cmd := &cobra.Command{
Use: "create [OPTIONS] [SERVICE...]",
Short: "Creates containers for a service",
PreRunE: AdaptCmd(func(ctx context.Context, cmd *cobra.Command, args []string) error {
opts.pullChanged = cmd.Flags().Changed("pull")
if opts.Build && opts.noBuild {
return fmt.Errorf("--build and --no-build are incompatible")
}
if opts.forceRecreate && opts.noRecreate {
return fmt.Errorf("--force-recreate and --no-recreate are incompatible")
}
return nil
}),
RunE: p.WithServices(dockerCli, func(ctx context.Context, project *types.Project, services []string) error {
return runCreate(ctx, dockerCli, backend, opts, buildOpts, project, services)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
flags := cmd.Flags()
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.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.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.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
}
func runCreate(ctx context.Context, _ command.Cli, backend api.Service, createOpts createOptions, buildOpts buildOptions, project *types.Project, services []string) error {
if err := createOpts.Apply(project); err != nil {
return err
}
var build *api.BuildOptions
if !createOpts.noBuild {
bo, err := buildOpts.toAPIBuildOptions(services)
if err != nil {
return err
}
build = &bo
}
return backend.Create(ctx, project, api.CreateOptions{
Build: build,
Services: services,
RemoveOrphans: createOpts.removeOrphans,
IgnoreOrphans: createOpts.ignoreOrphans,
Recreate: createOpts.recreateStrategy(),
RecreateDependencies: createOpts.dependenciesRecreateStrategy(),
Inherit: !createOpts.noInherit,
Timeout: createOpts.GetTimeout(),
QuietPull: createOpts.quietPull,
AssumeYes: createOpts.AssumeYes,
})
}
func (opts createOptions) recreateStrategy() string {
if opts.noRecreate {
return api.RecreateNever
}
if opts.forceRecreate {
return api.RecreateForce
}
if opts.noInherit {
return api.RecreateForce
}
return api.RecreateDiverged
}
func (opts createOptions) dependenciesRecreateStrategy() string {
if opts.noRecreate {
return api.RecreateNever
}
if opts.recreateDeps {
return api.RecreateForce
}
return api.RecreateDiverged
}
func (opts createOptions) GetTimeout() *time.Duration {
if opts.timeChanged {
t := time.Duration(opts.timeout) * time.Second
return &t
}
return nil
}
func (opts createOptions) Apply(project *types.Project) error {
if opts.pullChanged {
if !opts.isPullPolicyValid() {
return fmt.Errorf("invalid --pull option %q", opts.Pull)
}
for i, service := range project.Services {
service.PullPolicy = opts.Pull
project.Services[i] = service
}
}
// N.B. opts.Build means "force build all", but images can still be built
// when this is false
// e.g. if a service has pull_policy: build or its local image is policy
if opts.Build {
for i, service := range project.Services {
if service.Build == nil {
continue
}
service.PullPolicy = types.PullPolicyBuild
project.Services[i] = service
}
}
if err := applyPlatforms(project, true); err != nil {
return err
}
err := applyScaleOpts(project, opts.scale)
if err != nil {
return err
}
return nil
}
func applyScaleOpts(project *types.Project, opts []string) error {
for _, scale := range opts {
split := strings.Split(scale, "=")
if len(split) != 2 {
return fmt.Errorf("invalid --scale option %q. Should be SERVICE=NUM", scale)
}
name := split[0]
replicas, err := strconv.Atoi(split[1])
if err != nil {
return err
}
err = setServiceScale(project, name, replicas)
if err != nil {
return err
}
}
return nil
}
func (opts createOptions) isPullPolicyValid() bool {
pullPolicies := []string{
types.PullPolicyAlways, types.PullPolicyNever, types.PullPolicyBuild,
types.PullPolicyMissing, types.PullPolicyIfNotPresent,
}
return slices.Contains(pullPolicies, opts.Pull)
}

174
cmd/compose/create_test.go Normal file
View File

@ -0,0 +1,174 @@
/*
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"
"testing"
"github.com/compose-spec/compose-go/v2/types"
"github.com/davecgh/go-spew/spew"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/mocks"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
)
func TestRunCreate(t *testing.T) {
ctrl, ctx := gomock.WithContext(context.Background(), t)
backend := mocks.NewMockService(ctrl)
backend.EXPECT().Create(
gomock.Eq(ctx),
pullPolicy(""),
deepEqual(defaultCreateOptions(true)),
)
createOpts := createOptions{}
buildOpts := buildOptions{
ProjectOptions: &ProjectOptions{},
}
project := sampleProject()
err := runCreate(ctx, nil, backend, createOpts, buildOpts, project, nil)
require.NoError(t, err)
}
func TestRunCreate_Build(t *testing.T) {
ctrl, ctx := gomock.WithContext(context.Background(), t)
backend := mocks.NewMockService(ctrl)
backend.EXPECT().Create(
gomock.Eq(ctx),
pullPolicy("build"),
deepEqual(defaultCreateOptions(true)),
)
createOpts := createOptions{
Build: true,
}
buildOpts := buildOptions{
ProjectOptions: &ProjectOptions{},
}
project := sampleProject()
err := runCreate(ctx, nil, backend, createOpts, buildOpts, project, nil)
require.NoError(t, err)
}
func TestRunCreate_NoBuild(t *testing.T) {
ctrl, ctx := gomock.WithContext(context.Background(), t)
backend := mocks.NewMockService(ctrl)
backend.EXPECT().Create(
gomock.Eq(ctx),
pullPolicy(""),
deepEqual(defaultCreateOptions(false)),
)
createOpts := createOptions{
noBuild: true,
}
buildOpts := buildOptions{}
project := sampleProject()
err := runCreate(ctx, nil, backend, createOpts, buildOpts, project, nil)
require.NoError(t, err)
}
func sampleProject() *types.Project {
return &types.Project{
Name: "test",
Services: types.Services{
"svc": {
Name: "svc",
Build: &types.BuildConfig{
Context: ".",
},
},
},
}
}
func defaultCreateOptions(includeBuild bool) api.CreateOptions {
var build *api.BuildOptions
if includeBuild {
bo := defaultBuildOptions()
build = &bo
}
return api.CreateOptions{
Build: build,
Services: nil,
RemoveOrphans: false,
IgnoreOrphans: false,
Recreate: "diverged",
RecreateDependencies: "diverged",
Inherit: true,
Timeout: nil,
QuietPull: false,
}
}
func defaultBuildOptions() api.BuildOptions {
return api.BuildOptions{
Args: make(types.MappingWithEquals),
Progress: "auto",
}
}
// deepEqual returns a nice diff on failure vs gomock.Eq when used
// on structs.
func deepEqual(x interface{}) gomock.Matcher {
return gomock.GotFormatterAdapter(
gomock.GotFormatterFunc(func(got interface{}) string {
return cmp.Diff(x, got)
}),
gomock.Eq(x),
)
}
func spewAdapter(m gomock.Matcher) gomock.Matcher {
return gomock.GotFormatterAdapter(
gomock.GotFormatterFunc(func(got interface{}) string {
return spew.Sdump(got)
}),
m,
)
}
type withPullPolicy struct {
policy string
}
func pullPolicy(policy string) gomock.Matcher {
return spewAdapter(withPullPolicy{policy: policy})
}
func (w withPullPolicy) Matches(x interface{}) bool {
proj, ok := x.(*types.Project)
if !ok || proj == nil || len(proj.Services) == 0 {
return false
}
for _, svc := range proj.Services {
if svc.PullPolicy != w.policy {
return false
}
}
return true
}
func (w withPullPolicy) String() string {
return fmt.Sprintf("has pull policy %q for all services", w.policy)
}

99
cmd/compose/down.go Normal file
View File

@ -0,0 +1,99 @@
/*
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"
"os"
"time"
"github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/pkg/utils"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/docker/compose/v2/pkg/api"
)
type downOptions struct {
*ProjectOptions
removeOrphans bool
timeChanged bool
timeout int
volumes bool
images string
}
func downCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
opts := downOptions{
ProjectOptions: p,
}
downCmd := &cobra.Command{
Use: "down [OPTIONS] [SERVICES]",
Short: "Stop and remove containers, networks",
PreRunE: AdaptCmd(func(ctx context.Context, cmd *cobra.Command, args []string) error {
opts.timeChanged = cmd.Flags().Changed("timeout")
if opts.images != "" {
if opts.images != "all" && opts.images != "local" {
return fmt.Errorf("invalid value for --rmi: %q", opts.images)
}
}
return nil
}),
RunE: Adapt(func(ctx context.Context, args []string) error {
return runDown(ctx, dockerCli, backend, opts, args)
}),
ValidArgsFunction: noCompletion(),
}
flags := downCmd.Flags()
removeOrphans := utils.StringToBool(os.Getenv(ComposeRemoveOrphans))
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.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.SetNormalizeFunc(func(f *pflag.FlagSet, name string) pflag.NormalizedName {
if name == "volume" {
name = "volumes"
logrus.Warn("--volume is deprecated, please use --volumes")
}
return pflag.NormalizedName(name)
})
return downCmd
}
func runDown(ctx context.Context, dockerCli command.Cli, backend api.Service, opts downOptions, services []string) error {
project, name, err := opts.projectOrName(ctx, dockerCli, services...)
if err != nil {
return err
}
var timeout *time.Duration
if opts.timeChanged {
timeoutValue := time.Duration(opts.timeout) * time.Second
timeout = &timeoutValue
}
return backend.Down(ctx, name, api.DownOptions{
RemoveOrphans: opts.removeOrphans,
Project: project,
Timeout: timeout,
Images: opts.images,
Volumes: opts.volumes,
Services: services,
})
}

88
cmd/compose/events.go Normal file
View File

@ -0,0 +1,88 @@
/*
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"
"encoding/json"
"fmt"
"github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/pkg/api"
"github.com/spf13/cobra"
)
type eventsOpts struct {
*composeOptions
json bool
since string
until string
}
func eventsCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
opts := eventsOpts{
composeOptions: &composeOptions{
ProjectOptions: p,
},
}
cmd := &cobra.Command{
Use: "events [OPTIONS] [SERVICE...]",
Short: "Receive real time events from containers",
RunE: Adapt(func(ctx context.Context, args []string) error {
return runEvents(ctx, dockerCli, backend, opts, args)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
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
}
func runEvents(ctx context.Context, dockerCli command.Cli, backend api.Service, opts eventsOpts, services []string) error {
name, err := opts.toProjectName(ctx, dockerCli)
if err != nil {
return err
}
return backend.Events(ctx, name, api.EventsOptions{
Services: services,
Since: opts.since,
Until: opts.until,
Consumer: func(event api.Event) error {
if opts.json {
marshal, err := json.Marshal(map[string]interface{}{
"time": event.Timestamp,
"type": "container",
"service": event.Service,
"id": event.Container,
"action": event.Status,
"attributes": event.Attributes,
})
if err != nil {
return err
}
_, _ = fmt.Fprintln(dockerCli.Out(), string(marshal))
} else {
_, _ = fmt.Fprintln(dockerCli.Out(), event)
}
return nil
},
})
}

131
cmd/compose/exec.go Normal file
View File

@ -0,0 +1,131 @@
/*
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"
"errors"
"fmt"
"os"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/compose"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
type execOpts struct {
*composeOptions
service string
command []string
environment []string
workingDir string
noTty bool
user string
detach bool
index int
privileged bool
interactive bool
}
func execCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
opts := execOpts{
composeOptions: &composeOptions{
ProjectOptions: p,
},
}
runCmd := &cobra.Command{
Use: "exec [OPTIONS] SERVICE COMMAND [ARGS...]",
Short: "Execute a command in a running container",
Args: cobra.MinimumNArgs(2),
PreRunE: Adapt(func(ctx context.Context, args []string) error {
opts.service = args[0]
opts.command = args[1:]
return nil
}),
RunE: Adapt(func(ctx context.Context, args []string) error {
err := 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),
}
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().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().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().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().MarkHidden("interactive") //nolint:errcheck
runCmd.Flags().BoolP("tty", "t", true, "Allocate a pseudo-TTY")
runCmd.Flags().MarkHidden("tty") //nolint:errcheck
runCmd.Flags().SetInterspersed(false)
return runCmd
}
func runExec(ctx context.Context, dockerCli command.Cli, backend api.Service, opts execOpts) error {
projectName, err := opts.toProjectName(ctx, dockerCli)
if err != nil {
return err
}
projectOptions, err := opts.composeOptions.toProjectOptions() //nolint:staticcheck
if err != nil {
return err
}
lookupFn := func(k string) (string, bool) {
v, ok := projectOptions.Environment[k]
return v, ok
}
execOpts := api.RunOptions{
Service: opts.service,
Command: opts.command,
Environment: compose.ToMobyEnv(types.NewMappingWithEquals(opts.environment).Resolve(lookupFn)),
Tty: !opts.noTty,
User: opts.user,
Privileged: opts.privileged,
Index: opts.index,
Detach: opts.detach,
WorkingDir: opts.workingDir,
Interactive: opts.interactive,
}
exitCode, err := backend.Exec(ctx, projectName, execOpts)
if exitCode != 0 {
errMsg := fmt.Sprintf("exit status %d", exitCode)
if err != nil && err.Error() != "" {
errMsg = err.Error()
}
return cli.StatusError{StatusCode: exitCode, Status: errMsg}
}
return err
}

74
cmd/compose/export.go Normal file
View File

@ -0,0 +1,74 @@
/*
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)
}

82
cmd/compose/generate.go Normal file
View File

@ -0,0 +1,82 @@
/*
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
}

146
cmd/compose/images.go Normal file
View File

@ -0,0 +1,146 @@
/*
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"
"maps"
"slices"
"strings"
"time"
"github.com/containerd/platforms"
"github.com/docker/cli/cli/command"
"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/api"
)
type imageOptions struct {
*ProjectOptions
Quiet bool
Format string
}
func imagesCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
opts := imageOptions{
ProjectOptions: p,
}
imgCmd := &cobra.Command{
Use: "images [OPTIONS] [SERVICE...]",
Short: "List images used by the created containers",
RunE: Adapt(func(ctx context.Context, args []string) error {
return runImages(ctx, dockerCli, backend, opts, args)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
imgCmd.Flags().StringVar(&opts.Format, "format", "table", "Format the output. Values: [table | json]")
imgCmd.Flags().BoolVarP(&opts.Quiet, "quiet", "q", false, "Only display IDs")
return imgCmd
}
func runImages(ctx context.Context, dockerCli command.Cli, backend api.Service, opts imageOptions, services []string) error {
projectName, err := opts.toProjectName(ctx, dockerCli)
if err != nil {
return err
}
images, err := backend.Images(ctx, projectName, api.ImagesOptions{
Services: services,
})
if err != nil {
return err
}
if opts.Quiet {
ids := []string{}
for _, img := range images {
id := img.ID
if i := strings.IndexRune(img.ID, ':'); i >= 0 {
id = id[i+1:]
}
if !slices.Contains(ids, id) {
ids = append(ids, id)
}
}
for _, img := range ids {
_, _ = fmt.Fprintln(dockerCli.Out(), img)
}
return nil
}
if opts.Format == "json" {
type img struct {
ID string `json:"ID"`
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(),
func(w io.Writer) {
for _, container := range slices.Sorted(maps.Keys(images)) {
img := images[container]
id := stringid.TruncateID(img.ID)
size := units.HumanSizeWithPrecision(float64(img.Size), 3)
repo := img.Repository
if repo == "" {
repo = "<none>"
}
tag := img.Tag
if tag == "" {
tag = "<none>"
}
created := units.HumanDuration(time.Now().UTC().Sub(img.LastTagTime)) + " ago"
_, _ = 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")
}

69
cmd/compose/kill.go Normal file
View File

@ -0,0 +1,69 @@
/*
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"
"os"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/utils"
)
type killOptions struct {
*ProjectOptions
removeOrphans bool
signal string
}
func killCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
opts := killOptions{
ProjectOptions: p,
}
cmd := &cobra.Command{
Use: "kill [OPTIONS] [SERVICE...]",
Short: "Force stop service containers",
RunE: Adapt(func(ctx context.Context, args []string) error {
return runKill(ctx, dockerCli, backend, opts, args)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
flags := cmd.Flags()
removeOrphans := utils.StringToBool(os.Getenv(ComposeRemoveOrphans))
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")
return cmd
}
func runKill(ctx context.Context, dockerCli command.Cli, backend api.Service, opts killOptions, services []string) error {
project, name, err := opts.projectOrName(ctx, dockerCli, services...)
if err != nil {
return err
}
return backend.Kill(ctx, name, api.KillOptions{
RemoveOrphans: opts.removeOrphans,
Project: project,
Services: services,
Signal: opts.signal,
})
}

118
cmd/compose/list.go Normal file
View File

@ -0,0 +1,118 @@
/*
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"
"strings"
"github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/cmd/formatter"
"github.com/docker/cli/opts"
"github.com/spf13/cobra"
"github.com/docker/compose/v2/pkg/api"
)
type lsOptions struct {
Format string
Quiet bool
All bool
Filter opts.FilterOpt
}
func listCommand(dockerCli command.Cli, backend api.Service) *cobra.Command {
lsOpts := lsOptions{Filter: opts.NewFilterOpt()}
lsCmd := &cobra.Command{
Use: "ls [OPTIONS]",
Short: "List running compose projects",
RunE: Adapt(func(ctx context.Context, args []string) error {
return runList(ctx, dockerCli, backend, lsOpts)
}),
Args: cobra.NoArgs,
ValidArgsFunction: noCompletion(),
}
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().Var(&lsOpts.Filter, "filter", "Filter output based on conditions provided")
lsCmd.Flags().BoolVarP(&lsOpts.All, "all", "a", false, "Show all stopped Compose projects")
return lsCmd
}
var acceptedListFilters = map[string]bool{
"name": true,
}
func runList(ctx context.Context, dockerCli command.Cli, backend api.Service, lsOpts lsOptions) error {
filters := lsOpts.Filter.Value()
err := filters.Validate(acceptedListFilters)
if err != nil {
return err
}
stackList, err := backend.List(ctx, api.ListOptions{All: lsOpts.All})
if err != nil {
return err
}
if filters.Len() > 0 {
var filtered []api.Stack
for _, s := range stackList {
if filters.Contains("name") && !filters.Match("name", s.Name) {
continue
}
filtered = append(filtered, s)
}
stackList = filtered
}
if lsOpts.Quiet {
for _, s := range stackList {
_, _ = fmt.Fprintln(dockerCli.Out(), s.Name)
}
return nil
}
view := viewFromStackList(stackList)
return formatter.Print(view, lsOpts.Format, dockerCli.Out(), func(w io.Writer) {
for _, stack := range view {
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\n", stack.Name, stack.Status, stack.ConfigFiles)
}
}, "NAME", "STATUS", "CONFIG FILES")
}
type stackView struct {
Name string
Status string
ConfigFiles string
}
func viewFromStackList(stackList []api.Stack) []stackView {
retList := make([]stackView, len(stackList))
for i, s := range stackList {
retList[i] = stackView{
Name: s.Name,
Status: strings.TrimSpace(fmt.Sprintf("%s %s", s.Status, s.Reason)),
ConfigFiles: s.ConfigFiles,
}
}
return retList
}

99
cmd/compose/logs.go Normal file
View File

@ -0,0 +1,99 @@
/*
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"
"errors"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
"github.com/docker/compose/v2/cmd/formatter"
"github.com/docker/compose/v2/pkg/api"
)
type logsOptions struct {
*ProjectOptions
composeOptions
follow bool
index int
tail string
since string
until string
noColor bool
noPrefix bool
timestamps bool
}
func logsCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
opts := logsOptions{
ProjectOptions: p,
}
logsCmd := &cobra.Command{
Use: "logs [OPTIONS] [SERVICE...]",
Short: "View output from containers",
RunE: Adapt(func(ctx context.Context, args []string) error {
return runLogs(ctx, dockerCli, backend, opts, args)
}),
PreRunE: func(cmd *cobra.Command, args []string) error {
if opts.index > 0 && len(args) != 1 {
return errors.New("--index requires one service to be selected")
}
return nil
},
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
flags := logsCmd.Flags()
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.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.BoolVar(&opts.noColor, "no-color", false, "Produce monochrome output")
flags.BoolVar(&opts.noPrefix, "no-log-prefix", false, "Don't print prefix in logs")
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")
return logsCmd
}
func runLogs(ctx context.Context, dockerCli command.Cli, backend api.Service, opts logsOptions, services []string) error {
project, name, err := opts.projectOrName(ctx, dockerCli, services...)
if err != nil {
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)
return backend.Logs(ctx, name, consumer, api.LogOptions{
Project: project,
Services: services,
Follow: opts.follow,
Index: opts.index,
Tail: opts.tail,
Since: opts.since,
Until: opts.until,
Timestamps: opts.timestamps,
})
}

291
cmd/compose/options.go Normal file
View File

@ -0,0 +1,291 @@
/*
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"
"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/docker/cli/cli/command"
"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 {
defaultPlatform := project.Environment["DOCKER_DEFAULT_PLATFORM"]
for name, service := range project.Services {
if service.Build == nil {
continue
}
// default platform only applies if the service doesn't specify
if defaultPlatform != "" && service.Platform == "" {
if len(service.Build.Platforms) > 0 && !slices.Contains(service.Build.Platforms, defaultPlatform) {
return fmt.Errorf("service %q build.platforms does not support value set by DOCKER_DEFAULT_PLATFORM: %s", name, defaultPlatform)
}
service.Platform = defaultPlatform
}
if service.Platform != "" {
if len(service.Build.Platforms) > 0 {
if !slices.Contains(service.Build.Platforms, service.Platform) {
return fmt.Errorf("service %q build configuration does not support platform: %s", name, service.Platform)
}
}
if buildForSinglePlatform || len(service.Build.Platforms) == 0 {
// if we're building for a single platform, we want to build for the platform we'll use to run the image
// similarly, if no build platforms were explicitly specified, it makes sense to build for the platform
// the image is designed for rather than allowing the builder to infer the platform
service.Build.Platforms = []string{service.Platform}
}
}
// services can specify that they should be built for multiple platforms, which can be used
// with `docker compose build` to produce a multi-arch image
// other cases, such as `up` and `run`, need a single architecture to actually run
// if there is only a single platform present (which might have been inferred
// from service.Platform above), it will be used, even if it requires emulation.
// if there's more than one platform, then the list is cleared so that the builder
// can decide.
// TODO(milas): there's no validation that the platform the builder will pick is actually one
// of the supported platforms from the build definition
// e.g. `build.platforms: [linux/arm64, linux/amd64]` on a `linux/ppc64` machine would build
// for `linux/ppc64` instead of returning an error that it's not a valid platform for the service.
if buildForSinglePlatform && len(service.Build.Platforms) > 1 {
// empty indicates that the builder gets to decide
service.Build.Platforms = nil
}
project.Services[name] = service
}
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
}

394
cmd/compose/options_test.go Normal file
View File

@ -0,0 +1,394 @@
/*
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 (
"bytes"
"context"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"testing"
"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"
"go.uber.org/mock/gomock"
)
func TestApplyPlatforms_InferFromRuntime(t *testing.T) {
makeProject := func() *types.Project {
return &types.Project{
Services: types.Services{
"test": {
Name: "test",
Image: "foo",
Build: &types.BuildConfig{
Context: ".",
Platforms: []string{
"linux/amd64",
"linux/arm64",
"alice/32",
},
},
Platform: "alice/32",
},
},
}
}
t.Run("SinglePlatform", func(t *testing.T) {
project := makeProject()
require.NoError(t, applyPlatforms(project, true))
require.EqualValues(t, []string{"alice/32"}, project.Services["test"].Build.Platforms)
})
t.Run("MultiPlatform", func(t *testing.T) {
project := makeProject()
require.NoError(t, applyPlatforms(project, false))
require.EqualValues(t, []string{"linux/amd64", "linux/arm64", "alice/32"},
project.Services["test"].Build.Platforms)
})
}
func TestApplyPlatforms_DockerDefaultPlatform(t *testing.T) {
makeProject := func() *types.Project {
return &types.Project{
Environment: map[string]string{
"DOCKER_DEFAULT_PLATFORM": "linux/amd64",
},
Services: types.Services{
"test": {
Name: "test",
Image: "foo",
Build: &types.BuildConfig{
Context: ".",
Platforms: []string{
"linux/amd64",
"linux/arm64",
},
},
},
},
}
}
t.Run("SinglePlatform", func(t *testing.T) {
project := makeProject()
require.NoError(t, applyPlatforms(project, true))
require.EqualValues(t, []string{"linux/amd64"}, project.Services["test"].Build.Platforms)
})
t.Run("MultiPlatform", func(t *testing.T) {
project := makeProject()
require.NoError(t, applyPlatforms(project, false))
require.EqualValues(t, []string{"linux/amd64", "linux/arm64"},
project.Services["test"].Build.Platforms)
})
}
func TestApplyPlatforms_UnsupportedPlatform(t *testing.T) {
makeProject := func() *types.Project {
return &types.Project{
Environment: map[string]string{
"DOCKER_DEFAULT_PLATFORM": "commodore/64",
},
Services: types.Services{
"test": {
Name: "test",
Image: "foo",
Build: &types.BuildConfig{
Context: ".",
Platforms: []string{
"linux/amd64",
"linux/arm64",
},
},
},
},
}
}
t.Run("SinglePlatform", func(t *testing.T) {
project := makeProject()
require.EqualError(t, applyPlatforms(project, true),
`service "test" build.platforms does not support value set by DOCKER_DEFAULT_PLATFORM: commodore/64`)
})
t.Run("MultiPlatform", func(t *testing.T) {
project := makeProject()
require.EqualError(t, applyPlatforms(project, false),
`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()
})
}
}

88
cmd/compose/pause.go Normal file
View File

@ -0,0 +1,88 @@
/*
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 pauseOptions struct {
*ProjectOptions
}
func pauseCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
opts := pauseOptions{
ProjectOptions: p,
}
cmd := &cobra.Command{
Use: "pause [SERVICE...]",
Short: "Pause services",
RunE: Adapt(func(ctx context.Context, args []string) error {
return runPause(ctx, dockerCli, backend, opts, args)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
return cmd
}
func runPause(ctx context.Context, dockerCli command.Cli, backend api.Service, opts pauseOptions, services []string) error {
project, name, err := opts.projectOrName(ctx, dockerCli, services...)
if err != nil {
return err
}
return backend.Pause(ctx, name, api.PauseOptions{
Services: services,
Project: project,
})
}
type unpauseOptions struct {
*ProjectOptions
}
func unpauseCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
opts := unpauseOptions{
ProjectOptions: p,
}
cmd := &cobra.Command{
Use: "unpause [SERVICE...]",
Short: "Unpause services",
RunE: Adapt(func(ctx context.Context, args []string) error {
return runUnPause(ctx, dockerCli, backend, opts, args)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
return cmd
}
func runUnPause(ctx context.Context, dockerCli command.Cli, backend api.Service, opts unpauseOptions, services []string) error {
project, name, err := opts.projectOrName(ctx, dockerCli, services...)
if err != nil {
return err
}
return backend.UnPause(ctx, name, api.PauseOptions{
Services: services,
Project: project,
})
}

80
cmd/compose/port.go Normal file
View File

@ -0,0 +1,80 @@
/*
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"
"strconv"
"strings"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
"github.com/docker/compose/v2/pkg/api"
)
type portOptions struct {
*ProjectOptions
port uint16
protocol string
index int
}
func portCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
opts := portOptions{
ProjectOptions: p,
}
cmd := &cobra.Command{
Use: "port [OPTIONS] SERVICE PRIVATE_PORT",
Short: "Print the public port for a port binding",
Args: cobra.MinimumNArgs(2),
PreRunE: Adapt(func(ctx context.Context, args []string) error {
port, err := strconv.ParseUint(args[1], 10, 16)
if err != nil {
return err
}
opts.port = uint16(port)
opts.protocol = strings.ToLower(opts.protocol)
return nil
}),
RunE: Adapt(func(ctx context.Context, args []string) error {
return runPort(ctx, dockerCli, backend, opts, args[0])
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
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")
return cmd
}
func runPort(ctx context.Context, dockerCli command.Cli, backend api.Service, opts portOptions, service string) error {
projectName, err := opts.toProjectName(ctx, dockerCli)
if err != nil {
return err
}
ip, port, err := backend.Port(ctx, projectName, service, opts.port, api.PortOptions{
Protocol: opts.protocol,
Index: opts.index,
})
if err != nil {
return err
}
_, _ = fmt.Fprintf(dockerCli.Out(), "%s:%d\n", ip, port)
return nil
}

179
cmd/compose/ps.go Normal file
View File

@ -0,0 +1,179 @@
/*
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"
"errors"
"fmt"
"slices"
"sort"
"strings"
"github.com/docker/compose/v2/cmd/formatter"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/cli/cli/command"
cliformatter "github.com/docker/cli/cli/command/formatter"
cliflags "github.com/docker/cli/cli/flags"
"github.com/spf13/cobra"
)
type psOptions struct {
*ProjectOptions
Format string
All bool
Quiet bool
Services bool
Filter string
Status []string
noTrunc bool
Orphans bool
}
func (p *psOptions) parseFilter() error {
if p.Filter == "" {
return nil
}
parts := strings.SplitN(p.Filter, "=", 2)
if len(parts) != 2 {
return errors.New("arguments to --filter should be in form KEY=VAL")
}
switch parts[0] {
case "status":
p.Status = append(p.Status, parts[1])
case "source":
return api.ErrNotImplemented
default:
return fmt.Errorf("unknown filter %s", parts[0])
}
return nil
}
func psCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
opts := psOptions{
ProjectOptions: p,
}
psCmd := &cobra.Command{
Use: "ps [OPTIONS] [SERVICE...]",
Short: "List containers",
PreRunE: func(cmd *cobra.Command, args []string) error {
return opts.parseFilter()
},
RunE: Adapt(func(ctx context.Context, args []string) error {
return runPs(ctx, dockerCli, backend, args, opts)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
flags := psCmd.Flags()
flags.StringVar(&opts.Format, "format", "table", cliflags.FormatHelp)
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.BoolVarP(&opts.Quiet, "quiet", "q", false, "Only display IDs")
flags.BoolVar(&opts.Services, "services", false, "Display services")
flags.BoolVar(&opts.Orphans, "orphans", true, "Include orphaned services (not declared by project)")
flags.BoolVarP(&opts.All, "all", "a", false, "Show all stopped containers (including those created by the run command)")
flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Don't truncate output")
return psCmd
}
func runPs(ctx context.Context, dockerCli command.Cli, backend api.Service, services []string, opts psOptions) error {
project, name, err := opts.projectOrName(ctx, dockerCli, services...)
if err != nil {
return err
}
if project != nil {
names := project.ServiceNames()
if len(services) > 0 {
for _, service := range services {
if !slices.Contains(names, service) {
return fmt.Errorf("no such service: %s", service)
}
}
} else if !opts.Orphans {
// until user asks to list orphaned services, we only include those declared in project
services = names
}
}
containers, err := backend.Ps(ctx, name, api.PsOptions{
Project: project,
All: opts.All || len(opts.Status) != 0,
Services: services,
})
if err != nil {
return err
}
if len(opts.Status) != 0 {
containers = filterByStatus(containers, opts.Status)
}
sort.Slice(containers, func(i, j int) bool {
return containers[i].Name < containers[j].Name
})
if opts.Quiet {
for _, c := range containers {
_, _ = fmt.Fprintln(dockerCli.Out(), c.ID)
}
return nil
}
if opts.Services {
services := []string{}
for _, c := range containers {
s := c.Service
if !slices.Contains(services, s) {
services = append(services, s)
}
}
_, _ = fmt.Fprintln(dockerCli.Out(), strings.Join(services, "\n"))
return nil
}
if opts.Format == "" {
opts.Format = dockerCli.ConfigFile().PsFormat
}
containerCtx := cliformatter.Context{
Output: dockerCli.Out(),
Format: formatter.NewContainerFormat(opts.Format, opts.Quiet, false),
Trunc: !opts.noTrunc,
}
return formatter.ContainerWrite(containerCtx, containers)
}
func filterByStatus(containers []api.ContainerSummary, statuses []string) []api.ContainerSummary {
var filtered []api.ContainerSummary
for _, c := range containers {
if hasStatus(c, statuses) {
filtered = append(filtered, c)
}
}
return filtered
}
func hasStatus(c api.ContainerSummary, statuses []string) bool {
for _, status := range statuses {
if c.State == status {
return true
}
}
return false
}

87
cmd/compose/ps_test.go Normal file
View File

@ -0,0 +1,87 @@
/*
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"
"os"
"path/filepath"
"testing"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/streams"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/mocks"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
)
func TestPsTable(t *testing.T) {
ctx := context.Background()
dir := t.TempDir()
out := filepath.Join(dir, "output.txt")
f, err := os.Create(out)
if err != nil {
t.Fatal("could not create output file")
}
defer func() { _ = f.Close() }()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
backend := mocks.NewMockService(ctrl)
backend.EXPECT().
Ps(gomock.Eq(ctx), gomock.Any(), gomock.Any()).
DoAndReturn(func(ctx context.Context, projectName string, options api.PsOptions) ([]api.ContainerSummary, error) {
return []api.ContainerSummary{
{
ID: "abc123",
Name: "ABC",
Image: "foo/bar",
Publishers: api.PortPublishers{
{
TargetPort: 8080,
PublishedPort: 8080,
Protocol: "tcp",
},
{
TargetPort: 8443,
PublishedPort: 8443,
Protocol: "tcp",
},
},
},
}, nil
}).AnyTimes()
opts := psOptions{ProjectOptions: &ProjectOptions{ProjectName: "test"}}
stdout := streams.NewOut(f)
cli := mocks.NewMockCli(ctrl)
cli.EXPECT().Out().Return(stdout).AnyTimes()
cli.EXPECT().ConfigFile().Return(&configfile.ConfigFile{}).AnyTimes()
err = runPs(ctx, cli, backend, nil, opts)
require.NoError(t, err)
_, err = f.Seek(0, 0)
require.NoError(t, err)
output, err := os.ReadFile(out)
require.NoError(t, err)
assert.Contains(t, string(output), "8080/tcp, 8443/tcp")
}

84
cmd/compose/publish.go Normal file
View File

@ -0,0 +1,84 @@
/*
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"
"errors"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/pkg/api"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
type publishOptions struct {
*ProjectOptions
resolveImageDigests bool
ociVersion string
withEnvironment bool
assumeYes bool
}
func publishCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
opts := publishOptions{
ProjectOptions: p,
}
cmd := &cobra.Command{
Use: "publish [OPTIONS] REPOSITORY[:TAG]",
Short: "Publish compose application",
RunE: Adapt(func(ctx context.Context, args []string) error {
return runPublish(ctx, dockerCli, backend, opts, args[0])
}),
Args: cli.ExactArgs(1),
}
flags := cmd.Flags()
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.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
}
func runPublish(ctx context.Context, dockerCli command.Cli, backend api.Service, opts publishOptions, repository string) error {
project, metrics, err := opts.ToProject(ctx, dockerCli, nil)
if err != nil {
return err
}
if metrics.CountIncludesLocal > 0 {
return errors.New("cannot publish compose file with local includes")
}
return backend.Publish(ctx, project, repository, api.PublishOptions{
ResolveImageDigests: opts.resolveImageDigests,
OCIVersion: api.OCIVersion(opts.ociVersion),
WithEnvironment: opts.withEnvironment,
AssumeYes: opts.assumeYes,
})
}

116
cmd/compose/pull.go Normal file
View File

@ -0,0 +1,116 @@
/*
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"
"os"
"github.com/compose-spec/compose-go/v2/cli"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli/command"
"github.com/morikuni/aec"
"github.com/spf13/cobra"
"github.com/docker/compose/v2/pkg/api"
)
type pullOptions struct {
*ProjectOptions
composeOptions
quiet bool
parallel bool
noParallel bool
includeDeps bool
ignorePullFailures bool
noBuildable bool
policy string
}
func pullCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
opts := pullOptions{
ProjectOptions: p,
}
cmd := &cobra.Command{
Use: "pull [OPTIONS] [SERVICE...]",
Short: "Pull service images",
PreRunE: func(cmd *cobra.Command, args []string) error {
if cmd.Flags().Changed("no-parallel") {
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
},
RunE: Adapt(func(ctx context.Context, args []string) error {
return runPull(ctx, dockerCli, backend, opts, args)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
flags := cmd.Flags()
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.parallel, "parallel", true, "DEPRECATED pull multiple images in parallel")
flags.MarkHidden("parallel") //nolint:errcheck
cmd.Flags().BoolVar(&opts.noParallel, "no-parallel", true, "DEPRECATED disable parallel pulling")
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.noBuildable, "ignore-buildable", false, "Ignore images that can be built")
cmd.Flags().StringVar(&opts.policy, "policy", "", `Apply pull policy ("missing"|"always")`)
return cmd
}
func (opts pullOptions) apply(project *types.Project, services []string) (*types.Project, error) {
if !opts.includeDeps {
var err error
project, err = project.WithSelectedServices(services, types.IgnoreDependencies)
if err != nil {
return nil, err
}
}
if opts.policy != "" {
for i, service := range project.Services {
if service.Image == "" {
continue
}
service.PullPolicy = opts.policy
project.Services[i] = service
}
}
return project, nil
}
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)
if err != nil {
return err
}
project, err = opts.apply(project, services)
if err != nil {
return err
}
return backend.Pull(ctx, project, api.PullOptions{
Quiet: opts.quiet,
IgnoreFailures: opts.ignorePullFailures,
IgnoreBuildable: opts.noBuildable,
})
}

View File

@ -0,0 +1,57 @@
/*
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 (
"testing"
"github.com/compose-spec/compose-go/v2/types"
"gotest.tools/v3/assert"
)
func TestApplyPullOptions(t *testing.T) {
project := &types.Project{
Services: types.Services{
"must-build": {
Name: "must-build",
// No image, local build only
Build: &types.BuildConfig{
Context: ".",
},
},
"has-build": {
Name: "has-build",
Image: "registry.example.com/myservice",
Build: &types.BuildConfig{
Context: ".",
},
},
"must-pull": {
Name: "must-pull",
Image: "registry.example.com/another-service",
},
},
}
project, err := pullOptions{
policy: types.PullPolicyMissing,
}.apply(project, nil)
assert.NilError(t, err)
assert.Equal(t, project.Services["must-build"].PullPolicy, "") // still default
assert.Equal(t, project.Services["has-build"].PullPolicy, types.PullPolicyMissing)
assert.Equal(t, project.Services["must-pull"].PullPolicy, types.PullPolicyMissing)
}

73
cmd/compose/push.go Normal file
View File

@ -0,0 +1,73 @@
/*
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/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
"github.com/docker/compose/v2/pkg/api"
)
type pushOptions struct {
*ProjectOptions
composeOptions
IncludeDeps bool
Ignorefailures bool
Quiet bool
}
func pushCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
opts := pushOptions{
ProjectOptions: p,
}
pushCmd := &cobra.Command{
Use: "push [OPTIONS] [SERVICE...]",
Short: "Push service images",
RunE: Adapt(func(ctx context.Context, args []string) error {
return runPush(ctx, dockerCli, backend, opts, args)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
pushCmd.Flags().BoolVar(&opts.Ignorefailures, "ignore-push-failures", false, "Push what it can and ignores images with push failures")
pushCmd.Flags().BoolVar(&opts.IncludeDeps, "include-deps", false, "Also push images of services declared as dependencies")
pushCmd.Flags().BoolVarP(&opts.Quiet, "quiet", "q", false, "Push without printing progress information")
return pushCmd
}
func runPush(ctx context.Context, dockerCli command.Cli, backend api.Service, opts pushOptions, services []string) error {
project, _, err := opts.ToProject(ctx, dockerCli, services)
if err != nil {
return err
}
if !opts.IncludeDeps {
project, err = project.WithSelectedServices(services, types.IgnoreDependencies)
if err != nil {
return err
}
}
return backend.Push(ctx, project, api.PushOptions{
IgnoreFailures: opts.Ignorefailures,
Quiet: opts.Quiet,
})
}

75
cmd/compose/remove.go Normal file
View File

@ -0,0 +1,75 @@
/*
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/compose/v2/pkg/api"
"github.com/spf13/cobra"
)
type removeOptions struct {
*ProjectOptions
force bool
stop bool
volumes bool
}
func removeCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
opts := removeOptions{
ProjectOptions: p,
}
cmd := &cobra.Command{
Use: "rm [OPTIONS] [SERVICE...]",
Short: "Removes stopped service containers",
Long: `Removes stopped service containers
By default, anonymous volumes attached to containers will not be removed. You
can override this with -v. To list all volumes, use "docker volume ls".
Any data which is not in a volume will be lost.`,
RunE: Adapt(func(ctx context.Context, args []string) error {
return runRemove(ctx, dockerCli, backend, opts, args)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
f := cmd.Flags()
f.BoolVarP(&opts.force, "force", "f", false, "Don't ask to confirm removal")
f.BoolVarP(&opts.stop, "stop", "s", false, "Stop the containers, if required, before removing")
f.BoolVarP(&opts.volumes, "volumes", "v", false, "Remove any anonymous volumes attached to containers")
f.BoolP("all", "a", false, "Deprecated - no effect")
f.MarkHidden("all") //nolint:errcheck
return cmd
}
func runRemove(ctx context.Context, dockerCli command.Cli, backend api.Service, opts removeOptions, services []string) error {
project, name, err := opts.projectOrName(ctx, dockerCli, services...)
if err != nil {
return err
}
return backend.Remove(ctx, name, api.RemoveOptions{
Services: services,
Force: opts.force,
Volumes: opts.volumes,
Project: project,
Stop: opts.stop,
})
}

83
cmd/compose/restart.go Normal file
View File

@ -0,0 +1,83 @@
/*
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"
"time"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
"github.com/docker/compose/v2/pkg/api"
)
type restartOptions struct {
*ProjectOptions
timeChanged bool
timeout int
noDeps bool
}
func restartCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
opts := restartOptions{
ProjectOptions: p,
}
restartCmd := &cobra.Command{
Use: "restart [OPTIONS] [SERVICE...]",
Short: "Restart service containers",
PreRun: func(cmd *cobra.Command, args []string) {
opts.timeChanged = cmd.Flags().Changed("timeout")
},
RunE: Adapt(func(ctx context.Context, args []string) error {
return runRestart(ctx, dockerCli, backend, opts, args)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
flags := restartCmd.Flags()
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")
return restartCmd
}
func runRestart(ctx context.Context, dockerCli command.Cli, backend api.Service, opts restartOptions, services []string) error {
project, name, err := opts.projectOrName(ctx, dockerCli)
if err != nil {
return err
}
if project != nil && len(services) > 0 {
project, err = project.WithServicesEnabled(services...)
if err != nil {
return err
}
}
var timeout *time.Duration
if opts.timeChanged {
timeoutValue := time.Duration(opts.timeout) * time.Second
timeout = &timeoutValue
}
return backend.Restart(ctx, name, api.RestartOptions{
Timeout: timeout,
Services: services,
Project: project,
NoDeps: opts.noDeps,
})
}

340
cmd/compose/run.go Normal file
View File

@ -0,0 +1,340 @@
/*
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"
"os"
"strings"
"github.com/compose-spec/compose-go/v2/dotenv"
"github.com/compose-spec/compose-go/v2/format"
xprogress "github.com/moby/buildkit/util/progress/progressui"
"github.com/sirupsen/logrus"
cgo "github.com/compose-spec/compose-go/v2/cli"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/opts"
"github.com/mattn/go-shellwords"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/docker/cli/cli"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/progress"
"github.com/docker/compose/v2/pkg/utils"
)
type runOptions struct {
*composeOptions
Service string
Command []string
environment []string
envFiles []string
Detach bool
Remove bool
noTty bool
tty bool
interactive bool
user string
workdir string
entrypoint string
entrypointCmd []string
capAdd opts.ListOpts
capDrop opts.ListOpts
labels []string
volumes []string
publish []string
useAliases bool
servicePorts bool
name string
noDeps bool
ignoreOrphans bool
removeOrphans bool
quiet bool
quietPull bool
}
func (options runOptions) apply(project *types.Project) (*types.Project, error) {
if options.noDeps {
var err error
project, err = project.WithSelectedServices([]string{options.Service}, types.IgnoreDependencies)
if err != nil {
return nil, err
}
}
target, err := project.GetService(options.Service)
if err != nil {
return nil, err
}
target.Tty = !options.noTty
target.StdinOpen = options.interactive
// --service-ports and --publish are incompatible
if !options.servicePorts {
if len(target.Ports) > 0 {
logrus.Debug("Running service without ports exposed as --service-ports=false")
}
target.Ports = []types.ServicePortConfig{}
for _, p := range options.publish {
config, err := types.ParsePortConfig(p)
if err != nil {
return nil, err
}
target.Ports = append(target.Ports, config...)
}
}
for _, v := range options.volumes {
volume, err := format.ParseVolume(v)
if err != nil {
return nil, err
}
target.Volumes = append(target.Volumes, volume)
}
for name := range project.Services {
if name == options.Service {
project.Services[name] = target
break
}
}
return project, nil
}
func (options runOptions) getEnvironment() (types.Mapping, error) {
environment := types.NewMappingWithEquals(options.environment).Resolve(os.LookupEnv).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 {
options := runOptions{
composeOptions: &composeOptions{
ProjectOptions: p,
},
capAdd: opts.NewListOpts(nil),
capDrop: opts.NewListOpts(nil),
}
createOpts := createOptions{}
buildOpts := buildOptions{
ProjectOptions: p,
}
cmd := &cobra.Command{
Use: "run [OPTIONS] SERVICE [COMMAND] [ARGS...]",
Short: "Run a one-off command on a service",
Args: cobra.MinimumNArgs(1),
PreRunE: AdaptCmd(func(ctx context.Context, cmd *cobra.Command, args []string) error {
options.Service = args[0]
if len(args) > 1 {
options.Command = args[1:]
}
if len(options.publish) > 0 && options.servicePorts {
return fmt.Errorf("--service-ports and --publish are incompatible")
}
if cmd.Flags().Changed("entrypoint") {
command, err := shellwords.Parse(options.entrypoint)
if err != nil {
return err
}
options.entrypointCmd = command
}
if cmd.Flags().Changed("tty") {
if cmd.Flags().Changed("no-TTY") {
return fmt.Errorf("--tty and --no-TTY can't be used together")
} else {
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
}),
RunE: Adapt(func(ctx context.Context, args []string) error {
project, _, err := p.ToProject(ctx, dockerCli, []string{options.Service}, cgo.WithResolvedPaths(true), cgo.WithoutEnvironmentResolution)
if err != nil {
return err
}
project, err = project.WithServicesEnvironmentResolved(true)
if err != nil {
return err
}
if createOpts.quietPull {
buildOpts.Progress = string(xprogress.QuietMode)
}
options.ignoreOrphans = utils.StringToBool(project.Environment[ComposeIgnoreOrphans])
return runRun(ctx, backend, project, options, createOpts, buildOpts, dockerCli)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
flags := cmd.Flags()
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.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.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.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.workdir, "workdir", "w", "", "Working directory inside the container")
flags.StringVar(&options.entrypoint, "entrypoint", "", "Override the entrypoint of the image")
flags.Var(&options.capAdd, "cap-add", "Add Linux capabilities")
flags.Var(&options.capDrop, "cap-drop", "Drop Linux capabilities")
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.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.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.BoolVarP(&options.quiet, "quiet", "q", false, "Don't print anything to STDOUT")
flags.BoolVar(&buildOpts.quiet, "quiet-build", false, "Suppress progress output from the build process")
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.tty, "tty", "t", true, "Allocate a pseudo-TTY")
cmd.Flags().MarkHidden("tty") //nolint:errcheck
flags.SetNormalizeFunc(normalizeRunFlags)
flags.SetInterspersed(false)
return cmd
}
func normalizeRunFlags(f *pflag.FlagSet, name string) pflag.NormalizedName {
switch name {
case "volumes":
name = "volume"
case "labels":
name = "label"
}
return pflag.NormalizedName(name)
}
func runRun(ctx context.Context, backend api.Service, project *types.Project, options runOptions, createOpts createOptions, buildOpts buildOptions, dockerCli command.Cli) error {
project, err := options.apply(project)
if err != nil {
return err
}
err = createOpts.Apply(project)
if err != nil {
return err
}
if err := checksForRemoteStack(ctx, dockerCli, project, buildOpts, createOpts.AssumeYes, []string{}); err != nil {
return err
}
labels := types.Labels{}
for _, s := range options.labels {
parts := strings.SplitN(s, "=", 2)
if len(parts) != 2 {
return fmt.Errorf("label must be set as KEY=VALUE")
}
labels[parts[0]] = parts[1]
}
var buildForRun *api.BuildOptions
if !createOpts.noBuild {
bo, err := buildOpts.toAPIBuildOptions(nil)
if err != nil {
return err
}
buildForRun = &bo
}
environment, err := options.getEnvironment()
if err != nil {
return err
}
// start container and attach to container streams
runOpts := api.RunOptions{
CreateOptions: api.CreateOptions{
Build: buildForRun,
RemoveOrphans: options.removeOrphans,
IgnoreOrphans: options.ignoreOrphans,
QuietPull: options.quietPull,
},
Name: options.name,
Service: options.Service,
Command: options.Command,
Detach: options.Detach,
AutoRemove: options.Remove,
Tty: !options.noTty,
Interactive: options.interactive,
WorkingDir: options.workdir,
User: options.user,
CapAdd: options.capAdd.GetSlice(),
CapDrop: options.capDrop.GetSlice(),
Environment: environment.Values(),
Entrypoint: options.entrypointCmd,
Labels: labels,
UseNetworkAliases: options.useAliases,
NoDeps: options.noDeps,
Index: 0,
}
for name, service := range project.Services {
if name == options.Service {
service.StdinOpen = options.interactive
project.Services[name] = service
}
}
exitCode, err := backend.RunOneOffContainer(ctx, project, runOpts)
if exitCode != 0 {
errMsg := ""
if err != nil {
errMsg = err.Error()
}
return cli.StatusError{StatusCode: exitCode, Status: errMsg}
}
return err
}

100
cmd/compose/scale.go Normal file
View File

@ -0,0 +1,100 @@
/*
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"
"maps"
"slices"
"strconv"
"strings"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/pkg/api"
"github.com/spf13/cobra"
)
type scaleOptions struct {
*ProjectOptions
noDeps bool
}
func scaleCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
opts := scaleOptions{
ProjectOptions: p,
}
scaleCmd := &cobra.Command{
Use: "scale [SERVICE=REPLICAS...]",
Short: "Scale services ",
Args: cobra.MinimumNArgs(1),
RunE: Adapt(func(ctx context.Context, args []string) error {
serviceTuples, err := parseServicesReplicasArgs(args)
if err != nil {
return err
}
return runScale(ctx, dockerCli, backend, opts, serviceTuples)
}),
ValidArgsFunction: completeScaleArgs(dockerCli, p),
}
flags := scaleCmd.Flags()
flags.BoolVar(&opts.noDeps, "no-deps", false, "Don't start linked services")
return scaleCmd
}
func runScale(ctx context.Context, dockerCli command.Cli, backend api.Service, opts scaleOptions, serviceReplicaTuples map[string]int) error {
services := slices.Sorted(maps.Keys(serviceReplicaTuples))
project, _, err := opts.ToProject(ctx, dockerCli, services)
if err != nil {
return err
}
if opts.noDeps {
if project, err = project.WithSelectedServices(services, types.IgnoreDependencies); err != nil {
return err
}
}
for key, value := range serviceReplicaTuples {
service, err := project.GetService(key)
if err != nil {
return err
}
service.SetScale(value)
project.Services[key] = service
}
return backend.Scale(ctx, project, api.ScaleOptions{Services: services})
}
func parseServicesReplicasArgs(args []string) (map[string]int, error) {
serviceReplicaTuples := map[string]int{}
for _, arg := range args {
key, val, ok := strings.Cut(arg, "=")
if !ok || key == "" || val == "" {
return nil, fmt.Errorf("invalid scale specifier: %s", arg)
}
intValue, err := strconv.Atoi(val)
if err != nil {
return nil, fmt.Errorf("invalid scale specifier: can't parse replica value as int: %v", arg)
}
serviceReplicaTuples[key] = intValue
}
return serviceReplicaTuples, nil
}

57
cmd/compose/start.go Normal file
View File

@ -0,0 +1,57 @@
/*
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/compose/v2/pkg/api"
"github.com/spf13/cobra"
)
type startOptions struct {
*ProjectOptions
}
func startCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
opts := startOptions{
ProjectOptions: p,
}
startCmd := &cobra.Command{
Use: "start [SERVICE...]",
Short: "Start services",
RunE: Adapt(func(ctx context.Context, args []string) error {
return runStart(ctx, dockerCli, backend, opts, args)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
return startCmd
}
func runStart(ctx context.Context, dockerCli command.Cli, backend api.Service, opts startOptions, services []string) error {
project, name, err := opts.projectOrName(ctx, dockerCli, services...)
if err != nil {
return err
}
return backend.Start(ctx, name, api.StartOptions{
AttachTo: services,
Project: project,
Services: services,
})
}

84
cmd/compose/stats.go Normal file
View File

@ -0,0 +1,84 @@
/*
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"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/container"
"github.com/docker/docker/api/types/filters"
"github.com/spf13/cobra"
"github.com/docker/compose/v2/pkg/api"
)
type statsOptions struct {
ProjectOptions *ProjectOptions
all bool
format string
noStream bool
noTrunc bool
}
func statsCommand(p *ProjectOptions, dockerCli command.Cli) *cobra.Command {
opts := statsOptions{
ProjectOptions: p,
}
cmd := &cobra.Command{
Use: "stats [OPTIONS] [SERVICE]",
Short: "Display a live stream of container(s) resource usage statistics",
Args: cobra.MaximumNArgs(1),
RunE: Adapt(func(ctx context.Context, args []string) error {
return runStats(ctx, dockerCli, opts, args)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
flags := cmd.Flags()
flags.BoolVarP(&opts.all, "all", "a", false, "Show all containers (default shows just running)")
flags.StringVar(&opts.format, "format", "", `Format output using a custom template:
'table': Print output in table format with column headers (default)
'table TEMPLATE': Print output in table format using the given Go template
'json': Print in JSON format
'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`)
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")
return cmd
}
func runStats(ctx context.Context, dockerCli command.Cli, opts statsOptions, service []string) error {
name, err := opts.ProjectOptions.toProjectName(ctx, dockerCli)
if err != nil {
return err
}
filter := []filters.KeyValuePair{
filters.Arg("label", fmt.Sprintf("%s=%s", api.ProjectLabel, name)),
}
if len(service) > 0 {
filter = append(filter, filters.Arg("label", fmt.Sprintf("%s=%s", api.ServiceLabel, service[0])))
}
args := filters.NewArgs(filter...)
return container.RunStats(ctx, dockerCli, &container.StatsOptions{
All: opts.all,
NoStream: opts.noStream,
NoTrunc: opts.noTrunc,
Format: opts.format,
Filters: &args,
})
}

72
cmd/compose/stop.go Normal file
View File

@ -0,0 +1,72 @@
/*
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"
"time"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
"github.com/docker/compose/v2/pkg/api"
)
type stopOptions struct {
*ProjectOptions
timeChanged bool
timeout int
}
func stopCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
opts := stopOptions{
ProjectOptions: p,
}
cmd := &cobra.Command{
Use: "stop [OPTIONS] [SERVICE...]",
Short: "Stop services",
PreRun: func(cmd *cobra.Command, args []string) {
opts.timeChanged = cmd.Flags().Changed("timeout")
},
RunE: Adapt(func(ctx context.Context, args []string) error {
return runStop(ctx, dockerCli, backend, opts, args)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
flags := cmd.Flags()
flags.IntVarP(&opts.timeout, "timeout", "t", 0, "Specify a shutdown timeout in seconds")
return cmd
}
func runStop(ctx context.Context, dockerCli command.Cli, backend api.Service, opts stopOptions, services []string) error {
project, name, err := opts.projectOrName(ctx, dockerCli, services...)
if err != nil {
return err
}
var timeout *time.Duration
if opts.timeChanged {
timeoutValue := time.Duration(opts.timeout) * time.Second
timeout = &timeoutValue
}
return backend.Stop(ctx, name, api.StopOptions{
Timeout: timeout,
Services: services,
Project: project,
})
}

143
cmd/compose/top.go Normal file
View File

@ -0,0 +1,143 @@
/*
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"
"sort"
"strings"
"text/tabwriter"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
"github.com/docker/compose/v2/pkg/api"
)
type topOptions struct {
*ProjectOptions
}
func topCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
opts := topOptions{
ProjectOptions: p,
}
topCmd := &cobra.Command{
Use: "top [SERVICES...]",
Short: "Display the running processes",
RunE: Adapt(func(ctx context.Context, args []string) error {
return runTop(ctx, dockerCli, backend, opts, args)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
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 {
projectName, err := opts.toProjectName(ctx, dockerCli)
if err != nil {
return err
}
containers, err := backend.Top(ctx, projectName, services)
if err != nil {
return err
}
sort.Slice(containers, func(i, j int) bool {
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 _, proc := range container.Processes {
entry := topEntries{
"SERVICE": container.Service,
"#": container.Replica,
}
for i, title := range container.Titles {
if _, exists := header[title]; !exists {
header[title] = len(header)
}
entry[title] = proc[i]
}
entries = append(entries, entry)
}
}
// 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 {
if len(rows) == 0 {
return nil
}
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()
}

329
cmd/compose/top_test.go Normal file
View File

@ -0,0 +1,329 @@
/*
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()
}

348
cmd/compose/up.go Normal file
View File

@ -0,0 +1,348 @@
/*
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"
"errors"
"fmt"
"os"
"strings"
"time"
"github.com/compose-spec/compose-go/v2/types"
"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/pkg/api"
ui "github.com/docker/compose/v2/pkg/progress"
"github.com/docker/compose/v2/pkg/utils"
)
// composeOptions hold options common to `up` and `run` to run compose project
type composeOptions struct {
*ProjectOptions
}
type upOptions struct {
*composeOptions
Detach bool
noStart bool
noDeps bool
cascadeStop bool
cascadeFail bool
exitCodeFrom string
noColor bool
noPrefix bool
attachDependencies bool
attach []string
noAttach []string
timestamp bool
wait bool
waitTimeout int
watch bool
navigationMenu bool
navigationMenuChanged bool
}
func (opts upOptions) apply(project *types.Project, services []string) (*types.Project, error) {
if opts.noDeps {
var err error
project, err = project.WithSelectedServices(services, types.IgnoreDependencies)
if err != nil {
return nil, err
}
}
if opts.exitCodeFrom != "" {
_, err := project.GetService(opts.exitCodeFrom)
if err != nil {
return nil, err
}
}
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 {
up := upOptions{}
create := createOptions{}
build := buildOptions{ProjectOptions: p}
upCmd := &cobra.Command{
Use: "up [OPTIONS] [SERVICE...]",
Short: "Create and start containers",
PreRunE: AdaptCmd(func(ctx context.Context, cmd *cobra.Command, args []string) error {
create.pullChanged = cmd.Flags().Changed("pull")
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)
}),
RunE: p.WithServices(dockerCli, func(ctx context.Context, project *types.Project, services []string) error {
create.ignoreOrphans = utils.StringToBool(project.Environment[ComposeIgnoreOrphans])
if create.ignoreOrphans && create.removeOrphans {
return fmt.Errorf("cannot combine %s and --remove-orphans", ComposeIgnoreOrphans)
}
if len(up.attach) != 0 && up.attachDependencies {
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)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
flags := upCmd.Flags()
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.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.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.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(&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(&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.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.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.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.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(&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.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.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.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
}
//nolint:gocyclo
func validateFlags(up *upOptions, create *createOptions) error {
if up.exitCodeFrom != "" && !up.cascadeFail {
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.attachDependencies || up.cascadeStop || len(up.attach) > 0 {
return fmt.Errorf("--wait cannot be combined with --abort-on-container-exit, --attach or --attach-dependencies")
}
up.Detach = true
}
if create.Build && create.noBuild {
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.wait {
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 {
return fmt.Errorf("--force-recreate and --no-recreate are incompatible")
}
if create.recreateDeps && create.noRecreate {
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
}
//nolint:gocyclo
func runUp(
ctx context.Context,
dockerCli command.Cli,
backend api.Service,
createOptions createOptions,
upOptions upOptions,
buildOptions buildOptions,
project *types.Project,
services []string,
) error {
if err := checksForRemoteStack(ctx, dockerCli, project, buildOptions, createOptions.AssumeYes, []string{}); err != nil {
return err
}
err := createOptions.Apply(project)
if err != nil {
return err
}
project, err = upOptions.apply(project, services)
if err != nil {
return err
}
var build *api.BuildOptions
if !createOptions.noBuild {
if createOptions.quietPull {
buildOptions.Progress = string(xprogress.QuietMode)
}
// BuildOptions here is nested inside CreateOptions, so
// no service list is passed, it will implicitly pick all
// services being created, which includes any explicitly
// specified via "services" arg here as well as deps
bo, err := buildOptions.toAPIBuildOptions(nil)
if err != nil {
return err
}
bo.Services = services
bo.Deps = !upOptions.noDeps
build = &bo
}
create := api.CreateOptions{
Build: build,
Services: services,
RemoveOrphans: createOptions.removeOrphans,
IgnoreOrphans: createOptions.ignoreOrphans,
Recreate: createOptions.recreateStrategy(),
RecreateDependencies: createOptions.dependenciesRecreateStrategy(),
Inherit: !createOptions.noInherit,
Timeout: createOptions.GetTimeout(),
QuietPull: createOptions.quietPull,
AssumeYes: createOptions.AssumeYes,
}
if upOptions.noStart {
return backend.Create(ctx, project, create)
}
var consumer api.LogConsumer
var attach []string
if !upOptions.Detach {
consumer = formatter.NewLogConsumer(ctx, dockerCli.Out(), dockerCli.Err(), !upOptions.noColor, !upOptions.noPrefix, upOptions.timestamp)
var attachSet utils.Set[string]
if len(upOptions.attach) != 0 {
// services are passed explicitly with --attach, verify they're valid and then use them as-is
attachSet = utils.NewSet(upOptions.attach...)
unexpectedSvcs := attachSet.Diff(utils.NewSet(project.ServiceNames()...))
if len(unexpectedSvcs) != 0 {
return fmt.Errorf("cannot attach to services not included in up: %s", strings.Join(unexpectedSvcs.Elements(), ", "))
}
} else {
// mark services being launched (and potentially their deps) for attach
// if they didn't opt-out via Compose YAML
attachSet = utils.NewSet[string]()
var dependencyOpt types.DependencyOption = types.IgnoreDependencies
if upOptions.attachDependencies {
dependencyOpt = types.IncludeDependencies
}
if err := project.ForEachService(services, func(serviceName string, s *types.ServiceConfig) error {
if s.Attach == nil || *s.Attach {
attachSet.Add(serviceName)
}
return nil
}, dependencyOpt); err != nil {
return err
}
}
// filter out any services that have been explicitly marked for ignore with `--no-attach`
attachSet.RemoveAll(upOptions.noAttach...)
attach = attachSet.Elements()
}
timeout := time.Duration(upOptions.waitTimeout) * time.Second
return backend.Up(ctx, project, api.UpOptions{
Create: create,
Start: api.StartOptions{
Project: project,
Attach: consumer,
AttachTo: attach,
ExitCodeFrom: upOptions.exitCodeFrom,
OnExit: upOptions.OnExit(),
Wait: upOptions.wait,
WaitTimeout: timeout,
Watch: upOptions.watch,
Services: services,
NavigationMenu: upOptions.navigationMenu && ui.Mode != "plain" && dockerCli.In().IsTerminal(),
},
})
}
func setServiceScale(project *types.Project, name string, replicas int) error {
service, err := project.GetService(name)
if err != nil {
return err
}
service.SetScale(replicas)
project.Services[name] = service
return nil
}

50
cmd/compose/up_test.go Normal file
View File

@ -0,0 +1,50 @@
/*
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 (
"testing"
"github.com/compose-spec/compose-go/v2/types"
"gotest.tools/v3/assert"
)
func TestApplyScaleOpt(t *testing.T) {
p := types.Project{
Services: types.Services{
"foo": {
Name: "foo",
},
"bar": {
Name: "bar",
Deploy: &types.DeployConfig{
Mode: "test",
},
},
},
}
err := applyScaleOpts(&p, []string{"foo=2", "bar=3"})
assert.NilError(t, err)
foo, err := p.GetService("foo")
assert.NilError(t, err)
assert.Equal(t, *foo.Scale, 2)
bar, err := p.GetService("bar")
assert.NilError(t, err)
assert.Equal(t, *bar.Scale, 3)
assert.Equal(t, *bar.Deploy.Replicas, 3)
}

70
cmd/compose/version.go Normal file
View File

@ -0,0 +1,70 @@
/*
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 (
"fmt"
"strings"
"github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/cmd/formatter"
"github.com/spf13/cobra"
"github.com/docker/compose/v2/internal"
)
type versionOptions struct {
format string
short bool
}
func versionCommand(dockerCli command.Cli) *cobra.Command {
opts := versionOptions{}
cmd := &cobra.Command{
Use: "version [OPTIONS]",
Short: "Show the Docker Compose version information",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
runVersion(opts, dockerCli)
return nil
},
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
// overwrite parent PersistentPreRunE to avoid trying to load
// compose file on version command if COMPOSE_FILE is set
return nil
},
}
// define flags for backward compatibility with com.docker.cli
flags := cmd.Flags()
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")
return cmd
}
func runVersion(opts versionOptions, dockerCli command.Cli) {
if opts.short {
_, _ = fmt.Fprintln(dockerCli.Out(), strings.TrimPrefix(internal.Version, "v"))
return
}
if opts.format == formatter.JSON {
_, _ = fmt.Fprintf(dockerCli.Out(), "{\"version\":%q}\n", internal.Version)
return
}
_, _ = fmt.Fprintln(dockerCli.Out(), "Docker Compose version", internal.Version)
}

View File

@ -0,0 +1,76 @@
/*
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())
})
}
}

97
cmd/compose/viz.go Normal file
View File

@ -0,0 +1,97 @@
/*
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"
"strings"
"github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/pkg/api"
"github.com/spf13/cobra"
)
type vizOptions struct {
*ProjectOptions
includeNetworks bool
includePorts bool
includeImageName bool
indentationStr string
}
func vizCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
opts := vizOptions{
ProjectOptions: p,
}
var indentationSize int
var useSpaces bool
cmd := &cobra.Command{
Use: "viz [OPTIONS]",
Short: "EXPERIMENTAL - Generate a graphviz graph from your compose file",
PreRunE: Adapt(func(ctx context.Context, args []string) error {
var err error
opts.indentationStr, err = preferredIndentationStr(indentationSize, useSpaces)
return err
}),
RunE: Adapt(func(ctx context.Context, args []string) error {
return runViz(ctx, dockerCli, backend, &opts)
}),
}
cmd.Flags().BoolVar(&opts.includePorts, "ports", false, "Include service's exposed ports in output graph")
cmd.Flags().BoolVar(&opts.includeNetworks, "networks", false, "Include service's attached networks in output graph")
cmd.Flags().BoolVar(&opts.includeImageName, "image", false, "Include service's image name in output graph")
cmd.Flags().IntVar(&indentationSize, "indentation-size", 1, "Number of tabs or spaces to use for indentation")
cmd.Flags().BoolVar(&useSpaces, "spaces", false, "If given, space character ' ' will be used to indent,\notherwise tab character '\\t' will be used")
return cmd
}
func runViz(ctx context.Context, dockerCli command.Cli, backend api.Service, opts *vizOptions) error {
_, _ = fmt.Fprintln(os.Stderr, "viz command is EXPERIMENTAL")
project, _, err := opts.ToProject(ctx, dockerCli, nil)
if err != nil {
return err
}
// build graph
graphStr, _ := backend.Viz(ctx, project, api.VizOptions{
IncludeNetworks: opts.includeNetworks,
IncludePorts: opts.includePorts,
IncludeImageName: opts.includeImageName,
Indentation: opts.indentationStr,
})
fmt.Println(graphStr)
return nil
}
// preferredIndentationStr returns a single string given the indentation preference
func preferredIndentationStr(size int, useSpace bool) (string, error) {
if size < 0 {
return "", fmt.Errorf("invalid indentation size: %d", size)
}
indentationStr := "\t"
if useSpace {
indentationStr = " "
}
return strings.Repeat(indentationStr, size), nil
}

94
cmd/compose/viz_test.go Normal file
View File

@ -0,0 +1,94 @@
/*
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 (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPreferredIndentationStr(t *testing.T) {
type args struct {
size int
useSpace bool
}
tests := []struct {
name string
args args
want string
wantErr bool
}{
{
name: "should return '\\t\\t'",
args: args{
size: 2,
useSpace: false,
},
want: "\t\t",
wantErr: false,
},
{
name: "should return ' '",
args: args{
size: 4,
useSpace: true,
},
want: " ",
wantErr: false,
},
{
name: "should return ''",
args: args{
size: 0,
useSpace: false,
},
want: "",
wantErr: false,
},
{
name: "should return ''",
args: args{
size: 0,
useSpace: true,
},
want: "",
wantErr: false,
},
{
name: "should throw error because indentation size < 0",
args: args{
size: -1,
useSpace: false,
},
want: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := preferredIndentationStr(tt.args.size, tt.args.useSpace)
if tt.wantErr {
require.Errorf(t, err, "preferredIndentationStr(%v, %v)", tt.args.size, tt.args.useSpace)
} else {
require.NoError(t, err)
assert.Equalf(t, tt.want, got, "preferredIndentationStr(%v, %v)", tt.args.size, tt.args.useSpace)
}
})
}
}

95
cmd/compose/volumes.go Normal file
View File

@ -0,0 +1,95 @@
/*
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, _, err := options.projectOrName(ctx, dockerCli, services...)
if err != nil {
return err
}
names := project.ServiceNames()
if len(services) == 0 {
services = names
}
for _, service := range services {
if !slices.Contains(names, service) {
return fmt.Errorf("no such service: %s", service)
}
}
volumes, err := backend.Volumes(ctx, project, 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)
}

73
cmd/compose/wait.go Normal file
View File

@ -0,0 +1,73 @@
/*
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"
"os"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/pkg/api"
"github.com/spf13/cobra"
)
type waitOptions struct {
*ProjectOptions
services []string
downProject bool
}
func waitCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
opts := waitOptions{
ProjectOptions: p,
}
var statusCode int64
var err error
cmd := &cobra.Command{
Use: "wait SERVICE [SERVICE...] [OPTIONS]",
Short: "Block until containers of all (or specified) services stop.",
Args: cli.RequiresMinArgs(1),
RunE: Adapt(func(ctx context.Context, services []string) error {
opts.services = services
statusCode, err = runWait(ctx, dockerCli, backend, &opts)
return err
}),
PostRun: func(cmd *cobra.Command, args []string) {
os.Exit(int(statusCode))
},
}
cmd.Flags().BoolVar(&opts.downProject, "down-project", false, "Drops project when the first container stops")
return cmd
}
func runWait(ctx context.Context, dockerCli command.Cli, backend api.Service, opts *waitOptions) (int64, error) {
_, name, err := opts.projectOrName(ctx, dockerCli)
if err != nil {
return 0, err
}
return backend.Wait(ctx, name, api.WaitOptions{
Services: opts.services,
DownProjectOnContainerExit: opts.downProject,
})
}

126
cmd/compose/watch.go Normal file
View File

@ -0,0 +1,126 @@
/*
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"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/compose/v2/cmd/formatter"
"github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/internal/locker"
"github.com/docker/compose/v2/pkg/api"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
type watchOptions struct {
*ProjectOptions
prune bool
noUp bool
}
func watchCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
watchOpts := watchOptions{
ProjectOptions: p,
}
buildOpts := buildOptions{
ProjectOptions: p,
}
cmd := &cobra.Command{
Use: "watch [SERVICE...]",
Short: "Watch build context for service and rebuild/refresh containers when files are updated",
PreRunE: Adapt(func(ctx context.Context, args []string) error {
return nil
}),
RunE: AdaptCmd(func(ctx context.Context, cmd *cobra.Command, args []string) error {
if cmd.Parent().Name() == "alpha" {
logrus.Warn("watch command is now available as a top level command")
}
return runWatch(ctx, dockerCli, backend, watchOpts, buildOpts, args)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
cmd.Flags().BoolVar(&buildOpts.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")
return cmd
}
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)
if err != nil {
return err
}
if err := applyPlatforms(project, true); err != nil {
return err
}
build, err := buildOpts.toAPIBuildOptions(nil)
if err != nil {
return err
}
// validation done -- ensure we have the lockfile for this project before doing work
l, err := locker.NewPidfile(project.Name)
if err != nil {
return fmt.Errorf("cannot take exclusive lock for project %q: %w", project.Name, err)
}
if err := l.Lock(); err != nil {
return fmt.Errorf("cannot take exclusive lock for project %q: %w", project.Name, err)
}
if !watchOpts.noUp {
for index, service := range project.Services {
if service.Build != nil && service.Develop != nil {
service.PullPolicy = types.PullPolicyBuild
}
project.Services[index] = service
}
upOpts := api.UpOptions{
Create: api.CreateOptions{
Build: &build,
Services: services,
RemoveOrphans: false,
Recreate: api.RecreateDiverged,
RecreateDependencies: api.RecreateNever,
Inherit: true,
QuietPull: buildOpts.quiet,
},
Start: api.StartOptions{
Project: project,
Attach: nil,
Services: services,
},
}
if err := backend.Up(ctx, project, upOpts); err != nil {
return err
}
}
consumer := formatter.NewLogConsumer(ctx, dockerCli.Out(), dockerCli.Err(), false, false, false)
return backend.Watch(ctx, project, api.WatchOptions{
Build: &build,
LogTo: consumer,
Prune: watchOpts.prune,
Services: services,
})
}

100
cmd/formatter/ansi.go Normal file
View File

@ -0,0 +1,100 @@
/*
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))
}

148
cmd/formatter/colors.go Normal file
View File

@ -0,0 +1,148 @@
/*
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 formatter
import (
"fmt"
"strconv"
"strings"
"sync"
"github.com/docker/cli/cli/command"
)
var names = []string{
"grey",
"red",
"green",
"yellow",
"blue",
"magenta",
"cyan",
"white",
}
const (
BOLD = "1"
FAINT = "2"
ITALIC = "3"
UNDERLINE = "4"
)
const (
RESET = "0"
CYAN = "36"
)
const (
// Never use ANSI codes
Never = "never"
// Always use ANSI codes
Always = "always"
// Auto detect terminal is a tty and can use ANSI codes
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
func SetANSIMode(streams command.Streams, ansi string) {
if !useAnsi(streams, ansi) {
nextColor = func() colorFunc {
return monochrome
}
disableAnsi = true
}
}
func useAnsi(streams command.Streams, ansi string) bool {
switch ansi {
case Always:
return true
case Auto:
return streams.Out().IsTerminal()
}
return false
}
// colorFunc use ANSI codes to render colored text on console
type colorFunc func(s string) string
var monochrome = func(s string) string {
return s
}
func ansiColor(code, s string, formatOpts ...string) string {
return fmt.Sprintf("%s%s%s", ansiColorCode(code, formatOpts...), s, ansiColorCode("0"))
}
// Everything about ansiColorCode color https://hyperskill.org/learn/step/18193
func ansiColorCode(code string, formatOpts ...string) string {
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 {
return func(s string) string {
return ansiColor(code, s)
}
}
var (
nextColor = rainbowColor
rainbow []colorFunc
currentIndex = 0
mutex sync.Mutex
)
func rainbowColor() colorFunc {
mutex.Lock()
defer mutex.Unlock()
result := rainbow[currentIndex]
currentIndex = (currentIndex + 1) % len(rainbow)
return result
}
func init() {
colors := map[string]colorFunc{}
for i, name := range names {
colors[name] = makeColorFunc(strconv.Itoa(ansiColorOffset + i))
colors["intense_"+name] = makeColorFunc(strconv.Itoa(ansiColorOffset+i) + ";1")
}
rainbow = []colorFunc{
colors["cyan"],
colors["yellow"],
colors["green"],
colors["magenta"],
colors["blue"],
colors["intense_cyan"],
colors["intense_yellow"],
colors["intense_green"],
colors["intense_magenta"],
colors["intense_blue"],
}
}

29
cmd/formatter/consts.go Normal file
View File

@ -0,0 +1,29 @@
/*
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 formatter
const (
// JSON Print in JSON format
JSON = "json"
// TemplateLegacyJSON the legacy json formatting value using go template
TemplateLegacyJSON = "{{json.}}"
// PRETTY is the constant for default formats on list commands
// Deprecated: use TABLE
PRETTY = "pretty"
// TABLE Print output in table format with column headers (default)
TABLE = "table"
)

287
cmd/formatter/container.go Normal file
View File

@ -0,0 +1,287 @@
/*
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 formatter
import (
"fmt"
"strconv"
"strings"
"time"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/go-units"
)
const (
defaultContainerTableFormat = "table {{.Name}}\t{{.Image}}\t{{.Command}}\t{{.Service}}\t{{.RunningFor}}\t{{.Status}}\t{{.Ports}}"
nameHeader = "NAME"
projectHeader = "PROJECT"
serviceHeader = "SERVICE"
commandHeader = "COMMAND"
runningForHeader = "CREATED"
mountsHeader = "MOUNTS"
localVolumes = "LOCAL VOLUMES"
networksHeader = "NETWORKS"
)
// NewContainerFormat returns a Format for rendering using a Context
func NewContainerFormat(source string, quiet bool, size bool) formatter.Format {
switch source {
case formatter.TableFormatKey, "": // table formatting is the default if none is set.
if quiet {
return formatter.DefaultQuietFormat
}
format := defaultContainerTableFormat
if size {
format += `\t{{.Size}}`
}
return formatter.Format(format)
case formatter.RawFormatKey:
if quiet {
return `container_id: {{.ID}}`
}
format := `container_id: {{.ID}}
image: {{.Image}}
command: {{.Command}}
created_at: {{.CreatedAt}}
state: {{- pad .State 1 0}}
status: {{- pad .Status 1 0}}
names: {{.Names}}
labels: {{- pad .Labels 1 0}}
ports: {{- pad .Ports 1 0}}
`
if size {
format += `size: {{.Size}}\n`
}
return formatter.Format(format)
default: // custom format
if quiet {
return formatter.DefaultQuietFormat
}
return formatter.Format(source)
}
}
// ContainerWrite renders the context for a list of containers
func ContainerWrite(ctx formatter.Context, containers []api.ContainerSummary) error {
render := func(format func(subContext formatter.SubContext) error) error {
for _, container := range containers {
err := format(&ContainerContext{trunc: ctx.Trunc, c: container})
if err != nil {
return err
}
}
return nil
}
return ctx.Write(NewContainerContext(), render)
}
// ContainerContext is a struct used for rendering a list of containers in a Go template.
type ContainerContext struct {
formatter.HeaderContext
trunc bool
c api.ContainerSummary
// FieldsUsed is used in the pre-processing step to detect which fields are
// used in the template. It's currently only used to detect use of the .Size
// field which (if used) automatically sets the '--size' option when making
// the API call.
FieldsUsed map[string]interface{}
}
// NewContainerContext creates a new context for rendering containers
func NewContainerContext() *ContainerContext {
containerCtx := ContainerContext{}
containerCtx.Header = formatter.SubHeaderContext{
"ID": formatter.ContainerIDHeader,
"Name": nameHeader,
"Project": projectHeader,
"Service": serviceHeader,
"Image": formatter.ImageHeader,
"Command": commandHeader,
"CreatedAt": formatter.CreatedAtHeader,
"RunningFor": runningForHeader,
"Ports": formatter.PortsHeader,
"State": formatter.StateHeader,
"Status": formatter.StatusHeader,
"Size": formatter.SizeHeader,
"Labels": formatter.LabelsHeader,
}
return &containerCtx
}
// MarshalJSON makes ContainerContext implement json.Marshaler
func (c *ContainerContext) MarshalJSON() ([]byte, error) {
return formatter.MarshalJSON(c)
}
// ID returns the container's ID as a string. Depending on the `--no-trunc`
// option being set, the full or truncated ID is returned.
func (c *ContainerContext) ID() string {
if c.trunc {
return stringid.TruncateID(c.c.ID)
}
return c.c.ID
}
func (c *ContainerContext) Name() string {
return c.c.Name
}
// Names returns a comma-separated string of the container's names, with their
// slash (/) prefix stripped. Additional names for the container (related to the
// legacy `--link` feature) are omitted.
func (c *ContainerContext) Names() string {
names := formatter.StripNamePrefix(c.c.Names)
if c.trunc {
for _, name := range names {
if len(strings.Split(name, "/")) == 1 {
names = []string{name}
break
}
}
}
return strings.Join(names, ",")
}
func (c *ContainerContext) Service() string {
return c.c.Service
}
func (c *ContainerContext) Project() string {
return c.c.Project
}
func (c *ContainerContext) Image() string {
return c.c.Image
}
func (c *ContainerContext) Command() string {
command := c.c.Command
if c.trunc {
command = formatter.Ellipsis(command, 20)
}
return strconv.Quote(command)
}
func (c *ContainerContext) CreatedAt() string {
return time.Unix(c.c.Created, 0).String()
}
func (c *ContainerContext) RunningFor() string {
createdAt := time.Unix(c.c.Created, 0)
return units.HumanDuration(time.Now().UTC().Sub(createdAt)) + " ago"
}
func (c *ContainerContext) ExitCode() int {
return c.c.ExitCode
}
func (c *ContainerContext) State() string {
return c.c.State
}
func (c *ContainerContext) Status() string {
return c.c.Status
}
func (c *ContainerContext) Health() string {
return c.c.Health
}
func (c *ContainerContext) Publishers() api.PortPublishers {
return c.c.Publishers
}
func (c *ContainerContext) Ports() string {
var ports []container.Port
for _, publisher := range c.c.Publishers {
ports = append(ports, container.Port{
IP: publisher.URL,
PrivatePort: uint16(publisher.TargetPort),
PublicPort: uint16(publisher.PublishedPort),
Type: publisher.Protocol,
})
}
return formatter.DisplayablePorts(ports)
}
// Labels returns a comma-separated string of labels present on the container.
func (c *ContainerContext) Labels() string {
if c.c.Labels == nil {
return ""
}
var joinLabels []string
for k, v := range c.c.Labels {
joinLabels = append(joinLabels, fmt.Sprintf("%s=%s", k, v))
}
return strings.Join(joinLabels, ",")
}
// Label returns the value of the label with the given name or an empty string
// if the given label does not exist.
func (c *ContainerContext) Label(name string) string {
if c.c.Labels == nil {
return ""
}
return c.c.Labels[name]
}
// Mounts returns a comma-separated string of mount names present on the container.
// If the trunc option is set, names can be truncated (ellipsized).
func (c *ContainerContext) Mounts() string {
var mounts []string
for _, name := range c.c.Mounts {
if c.trunc {
name = formatter.Ellipsis(name, 15)
}
mounts = append(mounts, name)
}
return strings.Join(mounts, ",")
}
// LocalVolumes returns the number of volumes using the "local" volume driver.
func (c *ContainerContext) LocalVolumes() string {
return fmt.Sprintf("%d", c.c.LocalVolumes)
}
// Networks returns a comma-separated string of networks that the container is
// attached to.
func (c *ContainerContext) Networks() string {
return strings.Join(c.c.Networks, ",")
}
// Size returns the container's size and virtual size (e.g. "2B (virtual 21.5MB)")
func (c *ContainerContext) Size() string {
if c.FieldsUsed == nil {
c.FieldsUsed = map[string]interface{}{}
}
c.FieldsUsed["Size"] = struct{}{}
srw := units.HumanSizeWithPrecision(float64(c.c.SizeRw), 3)
sv := units.HumanSizeWithPrecision(float64(c.c.SizeRootFs), 3)
sf := srw
if c.c.SizeRootFs > 0 {
sf = fmt.Sprintf("%s (virtual %s)", srw, sv)
}
return sf
}

View File

@ -0,0 +1,71 @@
/*
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 formatter
import (
"fmt"
"io"
"reflect"
"strings"
"github.com/docker/compose/v2/pkg/api"
)
// Print prints formatted lists in different formats
func Print(toJSON interface{}, format string, outWriter io.Writer, writerFn func(w io.Writer), headers ...string) error {
switch strings.ToLower(format) {
case TABLE, PRETTY, "":
return PrintPrettySection(outWriter, writerFn, headers...)
case TemplateLegacyJSON:
switch reflect.TypeOf(toJSON).Kind() {
case reflect.Slice:
s := reflect.ValueOf(toJSON)
for i := 0; i < s.Len(); i++ {
obj := s.Index(i).Interface()
outJSON, err := ToJSON(obj, "", "")
if err != nil {
return err
}
_, _ = fmt.Fprint(outWriter, outJSON)
}
default:
outJSON, err := ToStandardJSON(toJSON)
if err != nil {
return err
}
_, _ = fmt.Fprintln(outWriter, outJSON)
}
case JSON:
switch reflect.TypeOf(toJSON).Kind() {
case reflect.Slice:
outJSON, err := ToJSON(toJSON, "", "")
if err != nil {
return err
}
_, _ = fmt.Fprint(outWriter, outJSON)
default:
outJSON, err := ToStandardJSON(toJSON)
if err != nil {
return err
}
_, _ = fmt.Fprintln(outWriter, outJSON)
}
default:
return fmt.Errorf("format value %q could not be parsed: %w", format, api.ErrParsingFailed)
}
return nil
}

View File

@ -0,0 +1,78 @@
/*
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 formatter
import (
"bytes"
"fmt"
"io"
"testing"
"go.uber.org/goleak"
"gotest.tools/v3/assert"
)
type testStruct struct {
Name string
Status string
}
// Print prints formatted lists in different formats
func TestPrint(t *testing.T) {
testList := []testStruct{
{
Name: "myName1",
Status: "myStatus1",
},
{
Name: "myName2",
Status: "myStatus2",
},
}
b := &bytes.Buffer{}
assert.NilError(t, Print(testList, PRETTY, b, func(w io.Writer) {
for _, t := range testList {
_, _ = fmt.Fprintf(w, "%s\t%s\n", t.Name, t.Status)
}
}, "NAME", "STATUS"))
assert.Equal(t, b.String(), "NAME STATUS\nmyName1 myStatus1\nmyName2 myStatus2\n")
b.Reset()
assert.NilError(t, Print(testList, JSON, b, func(w io.Writer) {
for _, t := range testList {
_, _ = fmt.Fprintf(w, "%s\t%s\n", t.Name, t.Status)
}
}, "NAME", "STATUS"))
assert.Equal(t, b.String(), `[{"Name":"myName1","Status":"myStatus1"},{"Name":"myName2","Status":"myStatus2"}]
`)
b.Reset()
assert.NilError(t, Print(testList, TemplateLegacyJSON, b, func(w io.Writer) {
for _, t := range testList {
_, _ = fmt.Fprintf(w, "%s\t%s\n", t.Name, t.Status)
}
}, "NAME", "STATUS"))
json := b.String()
assert.Equal(t, json, `{"Name":"myName1","Status":"myStatus1"}
{"Name":"myName2","Status":"myStatus2"}
`)
}
func TestColorsGoroutinesLeak(t *testing.T) {
goleak.VerifyNone(t)
}

39
cmd/formatter/json.go Normal file
View File

@ -0,0 +1,39 @@
/*
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 formatter
import (
"bytes"
"encoding/json"
)
const standardIndentation = " "
// ToStandardJSON return a string with the JSON representation of the interface{}
func ToStandardJSON(i interface{}) (string, error) {
return ToJSON(i, "", standardIndentation)
}
// ToJSON return a string with the JSON representation of the interface{}
func ToJSON(i interface{}, prefix string, indentation string) (string, error) {
buffer := &bytes.Buffer{}
encoder := json.NewEncoder(buffer)
encoder.SetEscapeHTML(false)
encoder.SetIndent(prefix, indentation)
err := encoder.Encode(i)
return buffer.String(), err
}

185
cmd/formatter/logs.go Normal file
View File

@ -0,0 +1,185 @@
/*
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 formatter
import (
"context"
"fmt"
"io"
"strconv"
"strings"
"sync"
"time"
"github.com/buger/goterm"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/docker/pkg/jsonmessage"
)
// LogConsumer consume logs from services and format them
type logConsumer struct {
ctx context.Context
presenters sync.Map // map[string]*presenter
width int
stdout io.Writer
stderr io.Writer
color bool
prefix bool
timestamp bool
}
// NewLogConsumer creates a new LogConsumer
func NewLogConsumer(ctx context.Context, stdout, stderr io.Writer, color, prefix, timestamp bool) api.LogConsumer {
return &logConsumer{
ctx: ctx,
presenters: sync.Map{},
width: 0,
stdout: stdout,
stderr: stderr,
color: color,
prefix: prefix,
timestamp: timestamp,
}
}
func (l *logConsumer) register(name string) *presenter {
var p *presenter
root, _, found := strings.Cut(name, " ")
if found {
parent := l.getPresenter(root)
p = &presenter{
colors: parent.colors,
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.computeWidth()
if l.prefix {
l.presenters.Range(func(key, value interface{}) bool {
p := value.(*presenter)
p.setPrefix(l.width)
return true
})
}
return p
}
func (l *logConsumer) getPresenter(container string) *presenter {
p, ok := l.presenters.Load(container)
if !ok { // should have been registered, but ¯\_(ツ)_/¯
return l.register(container)
}
return p.(*presenter)
}
// Log formats a log message as received from name/container
func (l *logConsumer) Log(container, message string) {
l.write(l.stdout, container, message)
}
// Err formats a log message as received from name/container
func (l *logConsumer) Err(container, message string) {
l.write(l.stderr, container, message)
}
func (l *logConsumer) write(w io.Writer, container, message string) {
if l.ctx.Err() != nil {
return
}
p := l.getPresenter(container)
timestamp := time.Now().Format(jsonmessage.RFC3339NanoFixed)
for _, line := range strings.Split(message, "\n") {
if l.timestamp {
_, _ = fmt.Fprintf(w, "%s%s %s\n", p.prefix, timestamp, line)
} else {
_, _ = fmt.Fprintf(w, "%s%s\n", p.prefix, line)
}
}
}
func (l *logConsumer) Status(container, msg string) {
p := l.getPresenter(container)
s := p.colors(fmt.Sprintf("%s%s %s\n", goterm.RESET_LINE, container, msg))
l.stdout.Write([]byte(s)) //nolint:errcheck
}
func (l *logConsumer) computeWidth() {
width := 0
l.presenters.Range(func(key, value interface{}) bool {
p := value.(*presenter)
if len(p.name) > width {
width = len(p.name)
}
return true
})
l.width = width + 1
}
type presenter struct {
colors colorFunc
name string
prefix string
}
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))
}
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()
}

32
cmd/formatter/pretty.go Normal file
View File

@ -0,0 +1,32 @@
/*
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 formatter
import (
"fmt"
"io"
"strings"
"text/tabwriter"
)
// PrintPrettySection prints a tabbed section on the writer parameter
func PrintPrettySection(out io.Writer, printer func(writer io.Writer), headers ...string) error {
w := tabwriter.NewWriter(out, 20, 1, 3, ' ', 0)
_, _ = fmt.Fprintln(w, strings.Join(headers, "\t"))
printer(w)
return w.Flush()
}

359
cmd/formatter/shortcut.go Normal file
View File

@ -0,0 +1,359 @@
/*
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)
}

99
cmd/main.go Normal file
View File

@ -0,0 +1,99 @@
/*
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 (
"os"
dockercli "github.com/docker/cli/cli"
"github.com/docker/cli/cli-plugins/metadata"
"github.com/docker/cli/cli-plugins/plugin"
"github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/cmd/cmdtrace"
"github.com/docker/docker/client"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/docker/compose/v2/cmd/compatibility"
commands "github.com/docker/compose/v2/cmd/compose"
"github.com/docker/compose/v2/internal"
"github.com/docker/compose/v2/pkg/compose"
)
func pluginMain() {
plugin.Run(func(dockerCli command.Cli) *cobra.Command {
// TODO(milas): this cast is safe but we should not need to do this,
// 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)
originalPreRunE := cmd.PersistentPreRunE
cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
// initialize the dockerCli instance
if err := plugin.PersistentPreRunE(cmd, args); err != nil {
return err
}
// compose-specific initialization
dockerCliPostInitialize(dockerCli)
if err := cmdtrace.Setup(cmd, dockerCli, os.Args[1:]); err != nil {
logrus.Debugf("failed to enable tracing: %v", err)
}
if originalPreRunE != nil {
return originalPreRunE(cmd, args)
}
return nil
}
cmd.SetFlagErrorFunc(func(c *cobra.Command, err error) error {
return dockercli.StatusError{
StatusCode: 1,
Status: err.Error(),
}
})
return cmd
},
metadata.Metadata{
SchemaVersion: "0.1.0",
Vendor: "Docker Inc.",
Version: internal.Version,
})
}
// dockerCliPostInitialize performs Compose-specific configuration for the
// command.Cli instance provided by the plugin.Run() initialization.
//
// NOTE: This must be called AFTER plugin.PersistentPreRunE.
func dockerCliPostInitialize(dockerCli command.Cli) {
// HACK(milas): remove once docker/cli#4574 is merged; for now,
// set it in a rather roundabout way by grabbing the underlying
// concrete client and manually invoking an option on it
_ = dockerCli.Apply(func(cli *command.DockerCli) error {
if mobyClient, ok := cli.Client().(*client.Client); ok {
_ = client.WithUserAgent("compose/" + internal.Version)(mobyClient)
}
return nil
})
}
func main() {
if plugin.RunningStandalone() {
os.Args = append([]string{"docker"}, compatibility.Convert(os.Args[1:])...)
}
pluginMain()
}

21
codecov.yml Normal file
View File

@ -0,0 +1,21 @@
coverage:
status:
project:
default:
informational: true
target: auto
threshold: 2%
patch:
default:
informational: true
comment:
require_changes: true
ignore:
- "packaging"
- "docs"
- "bin"
- "e2e"
- "pkg/e2e"
- "**/*_test.go"

148
docker-bake.hcl Normal file
View File

@ -0,0 +1,148 @@
// Copyright 2022 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.
variable "GO_VERSION" {
# default ARG value set in Dockerfile
default = null
}
variable "BUILD_TAGS" {
default = "e2e"
}
variable "DOCS_FORMATS" {
default = "md,yaml"
}
# Defines the output folder to override the default behavior.
# See Makefile for details, this is generally only useful for
# the packaging scripts and care should be taken to not break
# them.
variable "DESTDIR" {
default = ""
}
function "outdir" {
params = [defaultdir]
result = DESTDIR != "" ? DESTDIR : "${defaultdir}"
}
# Special target: https://github.com/docker/metadata-action#bake-definition
target "meta-helper" {}
target "_common" {
args = {
GO_VERSION = GO_VERSION
BUILD_TAGS = BUILD_TAGS
BUILDKIT_CONTEXT_KEEP_GIT_DIR = 1
}
}
group "default" {
targets = ["binary"]
}
group "validate" {
targets = ["lint", "vendor-validate", "license-validate"]
}
target "lint" {
inherits = ["_common"]
target = "lint"
output = ["type=cacheonly"]
}
target "license-validate" {
target = "license-validate"
output = ["type=cacheonly"]
}
target "license-update" {
target = "license-update"
output = ["."]
}
target "vendor-validate" {
inherits = ["_common"]
target = "vendor-validate"
output = ["type=cacheonly"]
}
target "vendor-update" {
inherits = ["_common"]
target = "vendor-update"
output = ["."]
}
target "test" {
inherits = ["_common"]
target = "test-coverage"
output = [outdir("./bin/coverage/unit")]
}
target "binary-with-coverage" {
inherits = ["_common"]
target = "binary"
args = {
BUILD_FLAGS = "-cover -covermode=atomic"
}
output = [outdir("./bin/build")]
platforms = ["local"]
}
target "binary" {
inherits = ["_common"]
target = "binary"
output = [outdir("./bin/build")]
platforms = ["local"]
}
target "binary-cross" {
inherits = ["binary"]
platforms = [
"darwin/amd64",
"darwin/arm64",
"linux/amd64",
"linux/arm/v6",
"linux/arm/v7",
"linux/arm64",
"linux/ppc64le",
"linux/riscv64",
"linux/s390x",
"windows/amd64",
"windows/arm64"
]
}
target "release" {
inherits = ["binary-cross"]
target = "release"
output = [outdir("./bin/release")]
}
target "docs-validate" {
inherits = ["_common"]
target = "docs-validate"
output = ["type=cacheonly"]
}
target "docs-update" {
inherits = ["_common"]
target = "docs-update"
output = ["./docs"]
}
target "image-cross" {
inherits = ["meta-helper", "binary-cross"]
output = ["type=image"]
}

152
docs/examples/provider.go Normal file
View File

@ -0,0 +1,152 @@
/*
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"`
}

176
docs/extension.md Normal file
View File

@ -0,0 +1,176 @@
# 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

212
docs/reference/compose.md Normal file
View File

@ -0,0 +1,212 @@
# docker compose
```text
docker compose [-f <arg>...] [options] [COMMAND] [ARGS...]
```
<!---MARKER_GEN_START-->
Define and run multi-container applications with Docker
### Subcommands
| Name | Description |
|:--------------------------------|:----------------------------------------------------------------------------------------|
| [`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 |
| [`commit`](compose_commit.md) | Create a new image from a service container's changes |
| [`config`](compose_config.md) | Parse, resolve and render compose file in canonical format |
| [`cp`](compose_cp.md) | Copy files/folders between a service container and the local filesystem |
| [`create`](compose_create.md) | Creates containers for a service |
| [`down`](compose_down.md) | Stop and remove containers, networks |
| [`events`](compose_events.md) | Receive real time events from containers |
| [`exec`](compose_exec.md) | Execute a command in a running container |
| [`export`](compose_export.md) | Export a service container's filesystem as a tar archive |
| [`images`](compose_images.md) | List images used by the created containers |
| [`kill`](compose_kill.md) | Force stop service containers |
| [`logs`](compose_logs.md) | View output from containers |
| [`ls`](compose_ls.md) | List running compose projects |
| [`pause`](compose_pause.md) | Pause services |
| [`port`](compose_port.md) | Print the public port for a port binding |
| [`ps`](compose_ps.md) | List containers |
| [`publish`](compose_publish.md) | Publish compose application |
| [`pull`](compose_pull.md) | Pull service images |
| [`push`](compose_push.md) | Push service images |
| [`restart`](compose_restart.md) | Restart service containers |
| [`rm`](compose_rm.md) | Removes stopped service containers |
| [`run`](compose_run.md) | Run a one-off command on a service |
| [`scale`](compose_scale.md) | Scale services |
| [`start`](compose_start.md) | Start services |
| [`stats`](compose_stats.md) | Display a live stream of container(s) resource usage statistics |
| [`stop`](compose_stop.md) | Stop services |
| [`top`](compose_top.md) | Display the running processes |
| [`unpause`](compose_unpause.md) | Unpause services |
| [`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
| 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") |
| `--compatibility` | `bool` | | Run compose in backward compatibility mode |
| `--dry-run` | `bool` | | Execute command in dry run mode |
| `--env-file` | `stringArray` | | Specify an alternate environment file |
| `-f`, `--file` | `stringArray` | | Compose configuration files |
| `--parallel` | `int` | `-1` | Control max parallelism, -1 for unlimited |
| `--profile` | `stringArray` | | Specify a profile to enable |
| `--progress` | `string` | | Set type of progress output (auto, tty, plain, json, quiet) |
| `--project-directory` | `string` | | Specify an alternate working directory<br>(default: the path of the, first specified, Compose file) |
| `-p`, `--project-name` | `string` | | Project name |
<!---MARKER_GEN_END-->
## Examples
### 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/).
#### Specifying multiple Compose files
You can supply multiple `-f` configuration files. When you supply multiple files, Compose combines them into a single
configuration. Compose builds the configuration in the order you supply the files. Subsequent files override and add
to their predecessors.
For example, consider this command line:
```console
$ docker compose -f compose.yaml -f compose.admin.yaml run backup_db
```
The `compose.yaml` file might specify a `webapp` service.
```yaml
services:
webapp:
image: examples/web
ports:
- "8000:8000"
volumes:
- "/data"
```
If the `compose.admin.yaml` also specifies this same service, any matching fields override the previous file.
New values, add to the `webapp` service configuration.
```yaml
services:
webapp:
build: .
environment:
- DEBUG=1
```
When you use multiple Compose files, all paths in the files are relative to the first configuration file specified
with `-f`. You can use the `--project-directory` option to override this base path.
Use a `-f` with `-` (dash) as the filename to read the configuration from stdin. When stdin is used all paths in the
configuration are relative to the current working directory.
The `-f` flag is optional. If you dont provide this flag on the command line, Compose traverses the working directory
and its parent directories looking for a `compose.yaml` or `docker-compose.yaml` file.
#### Specifying a path to a single Compose file
You can use the `-f` flag to specify a path to a Compose file that is not located in the current directory, either
from the command line or by setting up a `COMPOSE_FILE` environment variable in your shell or in an environment file.
For an example of using the `-f` option at the command line, suppose you are running the Compose Rails sample, and
have a `compose.yaml` file in a directory called `sandbox/rails`. You can use a command like `docker compose pull` to
get the postgres image for the db service from anywhere by using the `-f` flag as follows:
```console
$ docker compose -f ~/sandbox/rails/compose.yaml pull db
```
### Use `-p` to specify a project name
Each configuration has a project name. Compose sets the project name using
the following mechanisms, in order of precedence:
- The `-p` command line flag
- The `COMPOSE_PROJECT_NAME` environment variable
- The top level `name:` variable from the config file (or the last `name:`
from a series of config files specified using `-f`)
- The `basename` of the project directory containing the config file (or
containing the first config file specified using `-f`)
- The `basename` of the current directory if no config file is specified
Project names must contain only lowercase letters, decimal digits, dashes,
and underscores, and must begin with a lowercase letter or decimal digit. If
the `basename` of the project directory or current directory violates this
constraint, you must use one of the other mechanisms.
```console
$ docker compose -p my_project ps -a
NAME SERVICE STATUS PORTS
my_project_demo_1 demo running
$ docker compose -p my_project logs
demo_1 | PING localhost (127.0.0.1): 56 data bytes
demo_1 | 64 bytes from 127.0.0.1: seq=0 ttl=64 time=0.095 ms
```
### Use profiles to enable optional services
Use `--profile` to specify one or more active profiles
Calling `docker compose --profile frontend up` starts the services with the profile `frontend` and services
without any specified profiles.
You can also enable multiple profiles, e.g. with `docker compose --profile frontend --profile debug up` the profiles `frontend` and `debug` is enabled.
Profiles can also be set by `COMPOSE_PROFILES` environment variable.
### Configuring parallelism
Use `--parallel` to specify the maximum level of parallelism for concurrent engine calls.
Calling `docker compose --parallel 1 pull` pulls the pullable images defined in the Compose file
one at a time. This can also be used to control build concurrency.
Parallelism can also be set by the `COMPOSE_PARALLEL_LIMIT` environment variable.
### Set up environment variables
You can set environment variables for various docker compose options, including the `-f`, `-p` and `--profiles` flags.
Setting the `COMPOSE_FILE` environment variable is equivalent to passing the `-f` flag,
`COMPOSE_PROJECT_NAME` environment variable does the same as the `-p` flag,
`COMPOSE_PROFILES` environment variable is equivalent to the `--profiles` flag
and `COMPOSE_PARALLEL_LIMIT` does the same as the `--parallel` flag.
If flags are explicitly set on the command line, the associated environment variable is ignored.
Setting the `COMPOSE_IGNORE_ORPHANS` environment variable to `true` stops docker compose from detecting orphaned
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` flag to test a command without changing your application stack state.
Dry Run mode shows you all the steps Compose applies when executing a command, for example:
```console
$ docker compose --dry-run up --build -d
[+] Pulling 1/1
✔ DRY-RUN MODE - db Pulled 0.9s
[+] Running 10/8
✔ DRY-RUN MODE - build service backend 0.0s
✔ DRY-RUN MODE - ==> ==> writing image dryRun-754a08ddf8bcb1cf22f310f09206dd783d42f7dd 0.0s
✔ DRY-RUN MODE - ==> ==> naming to nginx-golang-mysql-backend 0.0s
✔ DRY-RUN MODE - Network nginx-golang-mysql_default Created 0.0s
✔ DRY-RUN MODE - Container nginx-golang-mysql-db-1 Created 0.0s
✔ DRY-RUN MODE - Container nginx-golang-mysql-backend-1 Created 0.0s
✔ DRY-RUN MODE - Container nginx-golang-mysql-proxy-1 Created 0.0s
✔ DRY-RUN MODE - Container nginx-golang-mysql-db-1 Healthy 0.5s
✔ DRY-RUN MODE - Container nginx-golang-mysql-backend-1 Started 0.0s
✔ DRY-RUN MODE - Container nginx-golang-mysql-proxy-1 Started Started
```
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.
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

@ -0,0 +1,22 @@
# docker compose alpha
<!---MARKER_GEN_START-->
Experimental commands
### Subcommands
| Name | Description |
|:----------------------------------|:-----------------------------------------------------------------------------------------------------|
| [`viz`](compose_alpha_viz.md) | EXPERIMENTAL - Generate a graphviz graph from your compose file |
| [`watch`](compose_alpha_watch.md) | EXPERIMENTAL - Watch build context for service and rebuild/refresh containers when files are updated |
### Options
| Name | Type | Default | Description |
|:------------|:-----|:--------|:--------------------------------|
| `--dry-run` | | | Execute command in dry run mode |
<!---MARKER_GEN_END-->

View File

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

View File

@ -0,0 +1,17 @@
# 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-->

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