Merge branch 'v2' into 8768-avoid-pulling-same-image-multiple-times

This commit is contained in:
Vedant Koditkar 2022-07-04 22:00:04 +05:30
commit 960453fa22
186 changed files with 5452 additions and 2006 deletions

View File

@ -1,3 +1,2 @@
.git/
bin/ bin/
dist/ dist/

View File

@ -7,10 +7,10 @@ jobs:
if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '/generate-artifacts') if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '/generate-artifacts')
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Set up Go 1.17 - name: Set up Go 1.18
uses: actions/setup-go@v2 uses: actions/setup-go@v2
with: with:
go-version: 1.17 go-version: 1.18.3
id: go id: go
- name: Checkout code into the Go module directory - name: Checkout code into the Go module directory
@ -42,6 +42,12 @@ jobs:
name: docker-compose-linux-amd64 name: docker-compose-linux-amd64
path: ${{ github.workspace }}/bin/docker-compose-linux-amd64 path: ${{ github.workspace }}/bin/docker-compose-linux-amd64
- name: Upload linux-ppc64le binary
uses: actions/upload-artifact@v2
with:
name: docker-compose-linux-ppc64le
path: ${{ github.workspace }}/bin/docker-compose-linux-ppc64le
- name: Upload windows-amd64 binary - name: Upload windows-amd64 binary
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2
with: with:

View File

@ -5,6 +5,12 @@ on:
branches: branches:
- v2 - v2
pull_request: pull_request:
workflow_dispatch:
inputs:
debug_enabled:
description: 'To run with tmate enter "debug_enabled"'
required: false
default: "false"
jobs: jobs:
lint: lint:
@ -13,16 +19,16 @@ jobs:
env: env:
GO111MODULE: "on" GO111MODULE: "on"
steps: steps:
- name: Set up Go 1.17 - name: Set up Go 1.18
uses: actions/setup-go@v2 uses: actions/setup-go@v2
with: with:
go-version: 1.17 go-version: 1.18.3
id: go id: go
- name: Checkout code into the Go module directory - name: Checkout code into the Go module directory
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Validate go-mod is up-to-date and license headers - name: Validate go-mod, license headers and docs are up-to-date
run: make validate run: make validate
- name: Run golangci-lint - name: Run golangci-lint
@ -40,10 +46,10 @@ jobs:
env: env:
GO111MODULE: "on" GO111MODULE: "on"
steps: steps:
- name: Set up Go 1.17 - name: Set up Go 1.18
uses: actions/setup-go@v2 uses: actions/setup-go@v2
with: with:
go-version: 1.17 go-version: 1.18.3
id: go id: go
- name: Checkout code into the Go module directory - name: Checkout code into the Go module directory
@ -65,10 +71,10 @@ jobs:
env: env:
GO111MODULE: "on" GO111MODULE: "on"
steps: steps:
- name: Set up Go 1.17 - name: Set up Go 1.18
uses: actions/setup-go@v2 uses: actions/setup-go@v2
with: with:
go-version: 1.17 go-version: 1.18.3
id: go id: go
- name: Setup docker CLI - name: Setup docker CLI
@ -90,7 +96,7 @@ jobs:
- name: Build for local E2E - name: Build for local E2E
env: env:
BUILD_TAGS: e2e BUILD_TAGS: e2e
run: make -f builder.Makefile compose-plugin run: make GIT_TAG=e2e-PR-${{ github.event.pull_request.number }}-${{ github.event.pull_request.head.sha }} -f builder.Makefile compose-plugin
- name: E2E Test in plugin mode - name: E2E Test in plugin mode
run: make e2e-compose run: make e2e-compose
@ -101,10 +107,10 @@ jobs:
env: env:
GO111MODULE: "on" GO111MODULE: "on"
steps: steps:
- name: Set up Go 1.17 - name: Set up Go 1.18
uses: actions/setup-go@v2 uses: actions/setup-go@v2
with: with:
go-version: 1.17 go-version: 1.18.3
id: go id: go
- name: Setup docker CLI - name: Setup docker CLI
@ -123,7 +129,17 @@ jobs:
- name: Build for local E2E - name: Build for local E2E
env: env:
BUILD_TAGS: e2e BUILD_TAGS: e2e
run: make -f builder.Makefile compose-plugin run: make GIT_TAG=e2e-PR-${{ github.event.pull_request.number }}-${{ github.event.pull_request.head.sha }} -f builder.Makefile compose-plugin
- name: Setup tmate session
uses: mxschmitt/action-tmate@v3
with:
limit-access-to-actor: true
github-token: ${{ secrets.GITHUB_TOKEN }}
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled }}
- name: E2E Test in standalone mode - name: E2E Test in standalone mode
run: make e2e-compose-standalone run: |
rm -f /usr/local/bin/docker-compose
cp bin/docker-compose /usr/local/bin
make e2e-compose-standalone

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

@ -0,0 +1,51 @@
name: Docs
on:
release:
types: [published]
jobs:
open-pr:
runs-on: ubuntu-latest
steps:
-
name: Checkout docs repo
uses: actions/checkout@v3
with:
token: ${{ secrets.GHPAT_DOCS_DISPATCH }}
repository: docker/docker.github.io
ref: master
-
name: Prepare
run: |
rm -rf ./_data/compose-cli/*
-
name: Build
uses: docker/build-push-action@v3
with:
context: ${{ github.server_url }}/${{ github.repository }}.git#${{ github.event.release.name }}
target: docs-reference
outputs: ./_data/compose-cli
-
name: Update compose_version in _config.yml
run: |
sed -i "s|^compose_version\:.*|compose_version\: \"${{ github.event.release.name }}\"|g" _config.yml
cat _config.yml | yq .compose_version
-
name: Commit changes
run: |
git add -A .
-
name: Create PR on docs repo
uses: peter-evans/create-pull-request@923ad837f191474af6b1721408744feb989a4c27 # v4.0.4
with:
token: ${{ secrets.GHPAT_DOCS_DISPATCH }}
commit-message: Update Compose reference API to ${{ github.event.release.name }}
signoff: true
branch: dispatch/compose-api-reference-${{ github.event.release.name }}
delete-branch: true
title: Update Compose reference API to ${{ github.event.release.name }}
body: |
Update the Compose reference API documentation to keep in sync with the latest release `${{ github.event.release.name }}`
labels: area/Compose
draft: false

View File

@ -11,10 +11,10 @@ jobs:
upload-release: upload-release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Set up Go 1.17 - name: Set up Go 1.18
uses: actions/setup-go@v2 uses: actions/setup-go@v2
with: with:
go-version: 1.17 go-version: 1.18.3
id: go id: go
- name: Setup docker CLI - name: Setup docker CLI
@ -36,7 +36,7 @@ jobs:
run: make GIT_TAG=${{ github.event.inputs.tag }} -f builder.Makefile cross run: make GIT_TAG=${{ github.event.inputs.tag }} -f builder.Makefile cross
- name: Compute checksums - name: Compute checksums
run: cd bin; for f in *; do shasum --algorithm 256 $f > $f.sha256; done run: cd bin; for f in *; do shasum --binary --algorithm 256 $f | tee -a checksums.txt > $f.sha256; done
- name: License - name: License
run: cp packaging/* bin/ run: cp packaging/* bin/
@ -44,7 +44,8 @@ jobs:
- uses: ncipollo/release-action@v1 - uses: ncipollo/release-action@v1
with: with:
artifacts: "bin/*" artifacts: "bin/*"
prerelease: true generateReleaseNotes: true
draft: true
commit: "v2" commit: "v2"
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.event.inputs.tag }} tag: ${{ github.event.inputs.tag }}

View File

@ -1,12 +1,11 @@
run:
concurrency: 2
linters: linters:
run:
concurrency: 2
skip-dirs:
- tests/composefiles
enable-all: false enable-all: false
disable-all: true disable-all: true
enable: enable:
- deadcode - deadcode
- depguard
- errcheck - errcheck
- gocyclo - gocyclo
- gofmt - gofmt
@ -26,6 +25,13 @@ linters:
- unused - unused
- varcheck - varcheck
linters-settings: linters-settings:
depguard:
list-type: blacklist
include-go-root: true
packages:
# The io/ioutil package has been deprecated.
# https://go.dev/doc/go1.16#ioutil
- io/ioutil
gocyclo: gocyclo:
min-complexity: 16 min-complexity: 16
lll: lll:

View File

@ -8,7 +8,7 @@
* [Docker Desktop](https://hub.docker.com/editions/community/docker-ce-desktop-mac) * [Docker Desktop](https://hub.docker.com/editions/community/docker-ce-desktop-mac)
* make * make
* Linux: * Linux:
* [Docker 19.03 or later](https://docs.docker.com/engine/install/) * [Docker 20.10 or later](https://docs.docker.com/engine/install/)
* make * make
### Building the CLI ### Building the CLI

View File

@ -83,7 +83,7 @@ don't get discouraged! Our contributor's guide explains
<tr> <tr>
<td>Community Slack</td> <td>Community Slack</td>
<td> <td>
The Docker Community has a dedicated Slack chat to discuss features and issues. You can sign-up <a href="https://dockercommunity.slack.com/ssb/redirect" target="_blank">with this link</a>. The Docker Community has a dedicated Slack chat to discuss features and issues. You can sign-up <a href="https://www.docker.com/docker-community" target="_blank">with this link</a>.
</td> </td>
</tr> </tr>
<tr> <tr>

View File

@ -15,7 +15,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
ARG GO_VERSION=1.17-alpine ARG GO_VERSION=1.18.3-alpine
ARG GOLANGCI_LINT_VERSION=v1.40.1-alpine ARG GOLANGCI_LINT_VERSION=v1.40.1-alpine
ARG PROTOC_GEN_GO_VERSION=v1.4.3 ARG PROTOC_GEN_GO_VERSION=v1.4.3
@ -88,7 +88,7 @@ RUN --mount=target=. \
make -f builder.Makefile test make -f builder.Makefile test
FROM base AS check-license-headers FROM base AS check-license-headers
RUN go get -u github.com/kunalkushwaha/ltag RUN go install github.com/kunalkushwaha/ltag@latest
RUN --mount=target=. \ RUN --mount=target=. \
make -f builder.Makefile check-license-headers make -f builder.Makefile check-license-headers
@ -105,3 +105,9 @@ COPY --from=make-go-mod-tidy /compose-cli/go.sum .
FROM base AS check-go-mod FROM base AS check-go-mod
COPY . . COPY . .
RUN make -f builder.Makefile check-go-mod RUN make -f builder.Makefile check-go-mod
# docs-reference is a target used as remote context to update docs on release
# with latest changes on docker.github.io.
# see open-pr job in .github/workflows/docs.yml for more details
FROM scratch AS docs-reference
COPY docs/reference/*.yaml .

View File

@ -43,12 +43,19 @@ compose-plugin: ## Compile the compose cli-plugin
.PHONY: e2e-compose .PHONY: e2e-compose
e2e-compose: ## Run end to end local tests in plugin mode. Set E2E_TEST=TestName to run a single test e2e-compose: ## Run end to end local tests in plugin mode. Set E2E_TEST=TestName to run a single test
docker compose version
go test $(TEST_FLAGS) -count=1 ./pkg/e2e go test $(TEST_FLAGS) -count=1 ./pkg/e2e
.PHONY: e2e-compose-standalone .PHONY: e2e-compose-standalone
e2e-compose-standalone: ## Run End to end local tests in standalone mode. Set E2E_TEST=TestName to run a single test e2e-compose-standalone: ## Run End to end local tests in standalone mode. Set E2E_TEST=TestName to run a single test
go test $(TEST_FLAGS) -count=1 --tags=standalone ./pkg/e2e docker-compose version
go test $(TEST_FLAGS) -v -count=1 -parallel=1 --tags=standalone ./pkg/e2e
.PHONY: mocks
mocks:
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 .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 e2e: e2e-compose e2e-compose-standalone ## Run end to end local tests in both modes. Set E2E_TEST=TestName to run a single test
@ -78,6 +85,23 @@ lint: ## run linter(s)
--build-arg GIT_TAG=$(GIT_TAG) \ --build-arg GIT_TAG=$(GIT_TAG) \
--target lint --target lint
.PHONY: docs
docs: ## generate documentation
$(eval $@_TMP_OUT := $(shell mktemp -d -t dockercli-output.XXXXXXXXXX))
docker build . \
--output type=local,dest=$($@_TMP_OUT) \
-f ./docs/docs.Dockerfile \
--target update
rm -rf ./docs/internal
cp -R "$($@_TMP_OUT)"/out/* ./docs/
rm -rf "$($@_TMP_OUT)"/*
.PHONY: validate-docs
validate-docs: ## validate the doc does not change
@docker build . \
-f ./docs/docs.Dockerfile \
--target validate
.PHONY: check-dependencies .PHONY: check-dependencies
check-dependencies: ## check dependency updates check-dependencies: ## check dependency updates
go list -u -m -f '{{if not .Indirect}}{{if .Update}}{{.}}{{end}}{{end}}' all go list -u -m -f '{{if not .Indirect}}{{if .Update}}{{.}}{{end}}{{end}}' all
@ -94,7 +118,7 @@ go-mod-tidy: ## Run go mod tidy in a container and output resulting go.mod and g
validate-go-mod: ## Validate go.mod and go.sum are up-to-date validate-go-mod: ## Validate go.mod and go.sum are up-to-date
@docker build . --target check-go-mod @docker build . --target check-go-mod
validate: validate-go-mod validate-headers ## Validate sources validate: validate-go-mod validate-headers validate-docs ## Validate sources
pre-commit: validate check-dependencies lint compose-plugin test e2e-compose pre-commit: validate check-dependencies lint compose-plugin test e2e-compose

View File

@ -47,6 +47,7 @@ compose-plugin:
.PHONY: cross .PHONY: cross
cross: cross:
GOOS=linux GOARCH=amd64 $(GO_BUILD) $(TAGS) -o $(COMPOSE_BINARY)-linux-x86_64 ./cmd GOOS=linux GOARCH=amd64 $(GO_BUILD) $(TAGS) -o $(COMPOSE_BINARY)-linux-x86_64 ./cmd
GOOS=linux GOARCH=ppc64le $(GO_BUILD) $(TAGS) -o $(COMPOSE_BINARY)-linux-ppc64le ./cmd
GOOS=linux GOARCH=arm64 $(GO_BUILD) $(TAGS) -o $(COMPOSE_BINARY)-linux-aarch64 ./cmd GOOS=linux GOARCH=arm64 $(GO_BUILD) $(TAGS) -o $(COMPOSE_BINARY)-linux-aarch64 ./cmd
GOOS=linux GOARM=6 GOARCH=arm $(GO_BUILD) $(TAGS) -o $(COMPOSE_BINARY)-linux-armv6 ./cmd GOOS=linux GOARM=6 GOARCH=arm $(GO_BUILD) $(TAGS) -o $(COMPOSE_BINARY)-linux-armv6 ./cmd
GOOS=linux GOARM=7 GOARCH=arm $(GO_BUILD) $(TAGS) -o $(COMPOSE_BINARY)-linux-armv7 ./cmd GOOS=linux GOARM=7 GOARCH=arm $(GO_BUILD) $(TAGS) -o $(COMPOSE_BINARY)-linux-armv7 ./cmd
@ -70,7 +71,3 @@ check-license-headers:
.PHONY: check-go-mod .PHONY: check-go-mod
check-go-mod: check-go-mod:
./scripts/validate/check-go-mod ./scripts/validate/check-go-mod
.PHONY: yamldocs
yamldocs:
go run docs/yaml/main/generate.go

View File

@ -50,7 +50,7 @@ func Convert(args []string) []string {
l := len(args) l := len(args)
for i := 0; i < l; i++ { for i := 0; i < l; i++ {
arg := args[i] arg := args[i]
if arg[0] != '-' { if len(arg) > 0 && arg[0] != '-' {
// not a top-level flag anymore, keep the rest of the command unmodified // not a top-level flag anymore, keep the rest of the command unmodified
if arg == compose.PluginName { if arg == compose.PluginName {
i++ i++
@ -58,17 +58,18 @@ func Convert(args []string) []string {
command = append(command, args[i:]...) command = append(command, args[i:]...)
break break
} }
if arg == "--verbose" {
switch arg {
case "--verbose":
arg = "--debug" arg = "--debug"
} case "-h":
if arg == "-h" {
// docker cli has deprecated -h to avoid ambiguity with -H, while docker-compose still support it // docker cli has deprecated -h to avoid ambiguity with -H, while docker-compose still support it
arg = "--help" arg = "--help"
} case "--version", "-v":
if arg == "--version" || arg == "-v" {
// redirect --version pseudo-command to actual command // redirect --version pseudo-command to actual command
arg = "version" arg = "version"
} }
if contains(getBoolFlags(), arg) { if contains(getBoolFlags(), arg) {
rootFlags = append(rootFlags, arg) rootFlags = append(rootFlags, arg)
continue continue

View File

@ -43,11 +43,21 @@ func Test_convert(t *testing.T) {
args: []string{"--host", "tcp://1.2.3.4", "up"}, args: []string{"--host", "tcp://1.2.3.4", "up"},
want: []string{"--host", "tcp://1.2.3.4", "compose", "up"}, want: []string{"--host", "tcp://1.2.3.4", "compose", "up"},
}, },
{
name: "compose --verbose",
args: []string{"--verbose"},
want: []string{"--debug", "compose"},
},
{ {
name: "compose --version", name: "compose --version",
args: []string{"--version"}, args: []string{"--version"},
want: []string{"compose", "version"}, want: []string{"compose", "version"},
}, },
{
name: "compose -v",
args: []string{"-v"},
want: []string{"compose", "version"},
},
{ {
name: "help", name: "help",
args: []string{"-h"}, args: []string{"-h"},
@ -68,6 +78,11 @@ func Test_convert(t *testing.T) {
args: []string{"--log-level", "INFO", "up"}, args: []string{"--log-level", "INFO", "up"},
want: []string{"--log-level", "INFO", "compose", "up"}, want: []string{"--log-level", "INFO", "compose", "up"},
}, },
{
name: "empty string argument",
args: []string{"--project-directory", "", "ps"},
want: []string{"compose", "--project-directory", "", "ps"},
},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {

View File

@ -23,6 +23,7 @@ import (
"strings" "strings"
"github.com/compose-spec/compose-go/cli" "github.com/compose-spec/compose-go/cli"
"github.com/compose-spec/compose-go/loader"
"github.com/compose-spec/compose-go/types" "github.com/compose-spec/compose-go/types"
buildx "github.com/docker/buildx/util/progress" buildx "github.com/docker/buildx/util/progress"
"github.com/docker/compose/v2/pkg/utils" "github.com/docker/compose/v2/pkg/utils"
@ -40,6 +41,28 @@ type buildOptions struct {
args []string args []string
noCache bool noCache bool
memory string memory string
ssh string
}
func (opts buildOptions) toAPIBuildOptions(services []string) (api.BuildOptions, error) {
var SSHKeys []types.SSHKey
var err error
if opts.ssh != "" {
SSHKeys, err = loader.ParseShortSSHSyntax(opts.ssh)
if err != nil {
return api.BuildOptions{}, err
}
}
return api.BuildOptions{
Pull: opts.pull,
Progress: opts.progress,
Args: types.NewMappingWithEquals(opts.args),
NoCache: opts.noCache,
Quiet: opts.quiet,
Services: services,
SSHs: SSHKeys,
}, nil
} }
var printerModes = []string{ var printerModes = []string{
@ -73,7 +96,10 @@ func buildCommand(p *projectOptions, backend api.Service) *cobra.Command {
} }
return nil return nil
}), }),
RunE: Adapt(func(ctx context.Context, args []string) error { RunE: AdaptCmd(func(ctx context.Context, cmd *cobra.Command, args []string) error {
if cmd.Flags().Changed("ssh") && opts.ssh == "" {
opts.ssh = "default"
}
return runBuild(ctx, backend, opts, args) return runBuild(ctx, backend, opts, args)
}), }),
ValidArgsFunction: serviceCompletion(p), ValidArgsFunction: serviceCompletion(p),
@ -82,6 +108,7 @@ func buildCommand(p *projectOptions, backend api.Service) *cobra.Command {
cmd.Flags().BoolVar(&opts.pull, "pull", false, "Always attempt to pull a newer version of the image.") cmd.Flags().BoolVar(&opts.pull, "pull", false, "Always attempt to pull a newer version of the image.")
cmd.Flags().StringVar(&opts.progress, "progress", buildx.PrinterModeAuto, fmt.Sprintf(`Set type of progress output (%s)`, strings.Join(printerModes, ", "))) cmd.Flags().StringVar(&opts.progress, "progress", buildx.PrinterModeAuto, fmt.Sprintf(`Set type of progress output (%s)`, strings.Join(printerModes, ", ")))
cmd.Flags().StringArrayVar(&opts.args, "build-arg", []string{}, "Set build-time variables for services.") cmd.Flags().StringArrayVar(&opts.args, "build-arg", []string{}, "Set build-time variables for services.")
cmd.Flags().StringVar(&opts.ssh, "ssh", "", "Set SSH authentications used when building service images. (use 'default' for using your default SSH Agent)")
cmd.Flags().Bool("parallel", true, "Build images in parallel. DEPRECATED") cmd.Flags().Bool("parallel", true, "Build images in parallel. DEPRECATED")
cmd.Flags().MarkHidden("parallel") //nolint:errcheck cmd.Flags().MarkHidden("parallel") //nolint:errcheck
cmd.Flags().Bool("compress", true, "Compress the build context using gzip. DEPRECATED") cmd.Flags().Bool("compress", true, "Compress the build context using gzip. DEPRECATED")
@ -103,12 +130,9 @@ func runBuild(ctx context.Context, backend api.Service, opts buildOptions, servi
return err return err
} }
return backend.Build(ctx, project, api.BuildOptions{ apiBuildOptions, err := opts.toAPIBuildOptions(services)
Pull: opts.pull, if err != nil {
Progress: opts.progress, return err
Args: types.NewMappingWithEquals(opts.args), }
NoCache: opts.noCache, return backend.Build(ctx, project, apiBuildOptions)
Quiet: opts.quiet,
Services: services,
})
} }

View File

@ -27,17 +27,21 @@ import (
"github.com/compose-spec/compose-go/cli" "github.com/compose-spec/compose-go/cli"
"github.com/compose-spec/compose-go/types" "github.com/compose-spec/compose-go/types"
composegoutils "github.com/compose-spec/compose-go/utils"
dockercli "github.com/docker/cli/cli" dockercli "github.com/docker/cli/cli"
"github.com/docker/cli/cli-plugins/manager" "github.com/docker/cli/cli-plugins/manager"
"github.com/docker/compose/v2/cmd/formatter" "github.com/docker/cli/cli/command"
"github.com/morikuni/aec" "github.com/morikuni/aec"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag" "github.com/spf13/pflag"
"github.com/docker/compose/v2/cmd/formatter"
"github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/compose" "github.com/docker/compose/v2/pkg/compose"
"github.com/docker/compose/v2/pkg/progress"
"github.com/docker/compose/v2/pkg/utils"
) )
// Command defines a compose CLI command as a func with args // Command defines a compose CLI command as a func with args
@ -86,9 +90,6 @@ func Adapt(fn Command) func(cmd *cobra.Command, args []string) error {
}) })
} }
// Warning is a global warning to be displayed to user on command failure
var Warning string
type projectOptions struct { type projectOptions struct {
ProjectName string ProjectName string
Profiles []string Profiles []string
@ -129,8 +130,8 @@ func (o *projectOptions) addProjectFlags(f *pflag.FlagSet) {
f.StringVarP(&o.ProjectName, "project-name", "p", "", "Project name") f.StringVarP(&o.ProjectName, "project-name", "p", "", "Project name")
f.StringArrayVarP(&o.ConfigPaths, "file", "f", []string{}, "Compose configuration files") f.StringArrayVarP(&o.ConfigPaths, "file", "f", []string{}, "Compose configuration files")
f.StringVar(&o.EnvFile, "env-file", "", "Specify an alternate environment file.") f.StringVar(&o.EnvFile, "env-file", "", "Specify an alternate environment file.")
f.StringVar(&o.ProjectDir, "project-directory", "", "Specify an alternate working directory\n(default: the path of the Compose file)") f.StringVar(&o.ProjectDir, "project-directory", "", "Specify an alternate working directory\n(default: the path of the, first specified, Compose file)")
f.StringVar(&o.WorkDir, "workdir", "", "DEPRECATED! USE --project-directory INSTEAD.\nSpecify an alternate working directory\n(default: the path of the Compose file)") f.StringVar(&o.WorkDir, "workdir", "", "DEPRECATED! USE --project-directory INSTEAD.\nSpecify an alternate working directory\n(default: the path of the, first specified, Compose file)")
f.BoolVar(&o.Compatibility, "compatibility", false, "Run compose in backward compatibility mode") f.BoolVar(&o.Compatibility, "compatibility", false, "Run compose in backward compatibility mode")
_ = f.MarkHidden("workdir") _ = f.MarkHidden("workdir")
} }
@ -140,6 +141,11 @@ func (o *projectOptions) toProjectName() (string, error) {
return o.ProjectName, nil return o.ProjectName, nil
} }
envProjectName := os.Getenv("COMPOSE_PROJECT_NAME")
if envProjectName != "" {
return envProjectName, nil
}
project, err := o.toProject(nil) project, err := o.toProject(nil)
if err != nil { if err != nil {
return "", err return "", err
@ -158,13 +164,16 @@ func (o *projectOptions) toProject(services []string, po ...cli.ProjectOptionsFn
return nil, compose.WrapComposeError(err) return nil, compose.WrapComposeError(err)
} }
if o.Compatibility || project.Environment["COMPOSE_COMPATIBILITY"] == "true" { if o.Compatibility || utils.StringToBool(project.Environment["COMPOSE_COMPATIBILITY"]) {
compose.Separator = "_" compose.Separator = "_"
} }
ef := o.EnvFile ef := o.EnvFile
if ef != "" && !filepath.IsAbs(ef) { if ef != "" && !filepath.IsAbs(ef) {
ef = filepath.Join(project.WorkingDir, o.EnvFile) ef, err = filepath.Abs(ef)
if err != nil {
return nil, err
}
} }
for i, s := range project.Services { for i, s := range project.Services {
s.CustomLabels = map[string]string{ s.CustomLabels = map[string]string{
@ -205,9 +214,9 @@ func (o *projectOptions) toProjectOptions(po ...cli.ProjectOptionsFn) (*cli.Proj
return cli.NewProjectOptions(o.ConfigPaths, return cli.NewProjectOptions(o.ConfigPaths,
append(po, append(po,
cli.WithWorkingDirectory(o.ProjectDir), cli.WithWorkingDirectory(o.ProjectDir),
cli.WithOsEnv,
cli.WithEnvFile(o.EnvFile), cli.WithEnvFile(o.EnvFile),
cli.WithDotEnv, cli.WithDotEnv,
cli.WithOsEnv,
cli.WithConfigFileEnv, cli.WithConfigFileEnv,
cli.WithDefaultConfigPath, cli.WithDefaultConfigPath,
cli.WithName(o.ProjectName))...) cli.WithName(o.ProjectName))...)
@ -222,7 +231,7 @@ func RunningAsStandalone() bool {
} }
// RootCommand returns the compose command with its child commands // RootCommand returns the compose command with its child commands
func RootCommand(backend api.Service) *cobra.Command { func RootCommand(dockerCli command.Cli, backend api.Service) *cobra.Command {
opts := projectOptions{} opts := projectOptions{}
var ( var (
ansi string ansi string
@ -249,6 +258,10 @@ func RootCommand(backend api.Service) *cobra.Command {
} }
}, },
PersistentPreRunE: func(cmd *cobra.Command, args []string) error { PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
err := setEnvWithDotEnv(&opts)
if err != nil {
return err
}
parent := cmd.Root() parent := cmd.Root()
if parent != nil { if parent != nil {
parentPrerun := parent.PersistentPreRunE parentPrerun := parent.PersistentPreRunE
@ -264,12 +277,18 @@ func RootCommand(backend api.Service) *cobra.Command {
return errors.New(`cannot specify DEPRECATED "--no-ansi" and "--ansi". Please use only "--ansi"`) return errors.New(`cannot specify DEPRECATED "--no-ansi" and "--ansi". Please use only "--ansi"`)
} }
ansi = "never" ansi = "never"
fmt.Fprint(os.Stderr, aec.Apply("option '--no-ansi' is DEPRECATED ! Please use '--ansi' instead.\n", aec.RedF)) fmt.Fprint(os.Stderr, "option '--no-ansi' is DEPRECATED ! Please use '--ansi' instead.\n")
} }
if verbose { if verbose {
logrus.SetLevel(logrus.TraceLevel) logrus.SetLevel(logrus.TraceLevel)
} }
formatter.SetANSIMode(ansi) formatter.SetANSIMode(ansi)
switch ansi {
case "never":
progress.Mode = progress.ModePlain
case "tty":
progress.Mode = progress.ModeTTY
}
if opts.WorkDir != "" { if opts.WorkDir != "" {
if opts.ProjectDir != "" { if opts.ProjectDir != "" {
return errors.New(`cannot specify DEPRECATED "--workdir" and "--project-directory". Please use only "--project-directory" instead`) return errors.New(`cannot specify DEPRECATED "--workdir" and "--project-directory". Please use only "--project-directory" instead`)
@ -292,9 +311,9 @@ func RootCommand(backend api.Service) *cobra.Command {
logsCommand(&opts, backend), logsCommand(&opts, backend),
convertCommand(&opts, backend), convertCommand(&opts, backend),
killCommand(&opts, backend), killCommand(&opts, backend),
runCommand(&opts, backend), runCommand(&opts, dockerCli, backend),
removeCommand(&opts, backend), removeCommand(&opts, backend),
execCommand(&opts, backend), execCommand(&opts, dockerCli, backend),
pauseCommand(&opts, backend), pauseCommand(&opts, backend),
unpauseCommand(&opts, backend), unpauseCommand(&opts, backend),
topCommand(&opts, backend), topCommand(&opts, backend),
@ -319,3 +338,27 @@ func RootCommand(backend api.Service) *cobra.Command {
command.Flags().MarkHidden("verbose") //nolint:errcheck command.Flags().MarkHidden("verbose") //nolint:errcheck
return command return command
} }
func setEnvWithDotEnv(prjOpts *projectOptions) error {
options, err := prjOpts.toProjectOptions()
if err != nil {
return compose.WrapComposeError(err)
}
workingDir, err := options.GetWorkingDir()
if err != nil {
return err
}
envFromFile, err := cli.GetEnvFromFile(composegoutils.GetAsEqualsMap(os.Environ()), workingDir, options.EnvFile)
if err != nil {
return err
}
for k, v := range envFromFile {
if _, ok := os.LookupEnv(k); !ok {
if err = os.Setenv(k, v); err != nil {
return err
}
}
}
return nil
}

View File

@ -55,7 +55,7 @@ func copyCommand(p *projectOptions, backend api.Service) *cobra.Command {
} }
return nil return nil
}), }),
RunE: Adapt(func(ctx context.Context, args []string) error { RunE: AdaptCmd(func(ctx context.Context, cmd *cobra.Command, args []string) error {
opts.source = args[0] opts.source = args[0]
opts.destination = args[1] opts.destination = args[1]
return runCopy(ctx, backend, opts) return runCopy(ctx, backend, opts)
@ -64,8 +64,10 @@ func copyCommand(p *projectOptions, backend api.Service) *cobra.Command {
} }
flags := copyCmd.Flags() flags := copyCmd.Flags()
flags.IntVar(&opts.index, "index", 1, "Index of the container if there are multiple instances of a service [default: 1].") flags.IntVar(&opts.index, "index", 0, "Index of the container if there are multiple instances of a service .")
flags.BoolVar(&opts.all, "all", false, "Copy to all the containers of the service.") flags.BoolVar(&opts.all, "all", false, "Copy to all the containers of the service.")
flags.MarkHidden("all") //nolint:errcheck
flags.MarkDeprecated("all", "By default all the containers of the service will get the source file/directory to be copied.") //nolint:errcheck
flags.BoolVarP(&opts.followLink, "follow-link", "L", false, "Always follow symbol link in SRC_PATH") flags.BoolVarP(&opts.followLink, "follow-link", "L", false, "Always follow symbol link in SRC_PATH")
flags.BoolVarP(&opts.copyUIDGID, "archive", "a", false, "Archive mode (copy all uid/gid information)") flags.BoolVarP(&opts.copyUIDGID, "archive", "a", false, "Archive mode (copy all uid/gid information)")

View File

@ -20,10 +20,10 @@ import (
"context" "context"
"fmt" "fmt"
"os" "os"
"strings"
"time" "time"
"github.com/compose-spec/compose-go/types" "github.com/compose-spec/compose-go/types"
"github.com/docker/compose/v2/pkg/utils"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag" "github.com/spf13/pflag"
@ -59,10 +59,11 @@ func downCommand(p *projectOptions, backend api.Service) *cobra.Command {
RunE: Adapt(func(ctx context.Context, args []string) error { RunE: Adapt(func(ctx context.Context, args []string) error {
return runDown(ctx, backend, opts) return runDown(ctx, backend, opts)
}), }),
Args: cobra.NoArgs,
ValidArgsFunction: noCompletion(), ValidArgsFunction: noCompletion(),
} }
flags := downCmd.Flags() flags := downCmd.Flags()
removeOrphans := strings.ToLower(os.Getenv("COMPOSE_REMOVE_ORPHANS ")) == "true" removeOrphans := utils.StringToBool(os.Getenv("COMPOSE_REMOVE_ORPHANS"))
flags.BoolVar(&opts.removeOrphans, "remove-orphans", removeOrphans, "Remove containers for services not defined in the Compose file.") flags.BoolVar(&opts.removeOrphans, "remove-orphans", removeOrphans, "Remove containers for services not defined in the Compose file.")
flags.IntVarP(&opts.timeout, "timeout", "t", 10, "Specify a shutdown timeout in seconds") flags.IntVarP(&opts.timeout, "timeout", "t", 10, "Specify a shutdown timeout in seconds")
flags.BoolVarP(&opts.volumes, "volumes", "v", false, " Remove named volumes declared in the `volumes` section of the Compose file and anonymous volumes attached to containers.") flags.BoolVarP(&opts.volumes, "volumes", "v", false, " Remove named volumes declared in the `volumes` section of the Compose file and anonymous volumes attached to containers.")

View File

@ -18,12 +18,10 @@ package compose
import ( import (
"context" "context"
"fmt"
"os"
"github.com/compose-spec/compose-go/types" "github.com/compose-spec/compose-go/types"
"github.com/containerd/console"
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/compose" "github.com/docker/compose/v2/pkg/compose"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -37,14 +35,15 @@ type execOpts struct {
environment []string environment []string
workingDir string workingDir string
noTty bool noTty bool
user string user string
detach bool detach bool
index int index int
privileged bool privileged bool
interactive bool
} }
func execCommand(p *projectOptions, backend api.Service) *cobra.Command { func execCommand(p *projectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
opts := execOpts{ opts := execOpts{
composeOptions: &composeOptions{ composeOptions: &composeOptions{
projectOptions: p, projectOptions: p,
@ -70,9 +69,14 @@ func execCommand(p *projectOptions, backend api.Service) *cobra.Command {
runCmd.Flags().IntVar(&opts.index, "index", 1, "index of the container if there are multiple instances of a service [default: 1].") runCmd.Flags().IntVar(&opts.index, "index", 1, "index of the container if there are multiple instances of a service [default: 1].")
runCmd.Flags().BoolVarP(&opts.privileged, "privileged", "", false, "Give extended privileges to the process.") runCmd.Flags().BoolVarP(&opts.privileged, "privileged", "", false, "Give extended privileges to the process.")
runCmd.Flags().StringVarP(&opts.user, "user", "u", "", "Run the command as this user.") runCmd.Flags().StringVarP(&opts.user, "user", "u", "", "Run the command as this user.")
runCmd.Flags().BoolVarP(&opts.noTty, "no-TTY", "T", false, "Disable pseudo-TTY allocation. By default `docker compose exec` allocates a TTY.") runCmd.Flags().BoolVarP(&opts.noTty, "no-TTY", "T", !dockerCli.Out().IsTerminal(), "Disable pseudo-TTY allocation. By default `docker compose exec` allocates a TTY.")
runCmd.Flags().StringVarP(&opts.workingDir, "workdir", "w", "", "Path to workdir directory for this command.") runCmd.Flags().StringVarP(&opts.workingDir, "workdir", "w", "", "Path to workdir directory for this command.")
runCmd.Flags().BoolVarP(&opts.interactive, "interactive", "i", true, "Keep STDIN open even if not attached.")
runCmd.Flags().MarkHidden("interactive") //nolint:errcheck
runCmd.Flags().BoolP("tty", "t", true, "Allocate a pseudo-TTY.")
runCmd.Flags().MarkHidden("tty") //nolint:errcheck
runCmd.Flags().SetInterspersed(false) runCmd.Flags().SetInterspersed(false)
return runCmd return runCmd
} }
@ -100,27 +104,9 @@ func runExec(ctx context.Context, backend api.Service, opts execOpts) error {
Index: opts.index, Index: opts.index,
Detach: opts.detach, Detach: opts.detach,
WorkingDir: opts.workingDir, WorkingDir: opts.workingDir,
Interactive: opts.interactive,
Stdin: os.Stdin,
Stdout: os.Stdout,
Stderr: os.Stderr,
} }
if execOpts.Tty {
con := console.Current()
if err := con.SetRaw(); err != nil {
return err
}
defer func() {
if err := con.Reset(); err != nil {
fmt.Println("Unable to close the console")
}
}()
execOpts.Stdin = con
execOpts.Stdout = con
execOpts.Stderr = con
}
exitCode, err := backend.Exec(ctx, projectName, execOpts) exitCode, err := backend.Exec(ctx, projectName, execOpts)
if exitCode != 0 { if exitCode != 0 {
errMsg := "" errMsg := ""

View File

@ -19,25 +19,44 @@ package compose
import ( import (
"context" "context"
"github.com/compose-spec/compose-go/types"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/api"
) )
type killOptions struct {
*projectOptions
signal string
}
func killCommand(p *projectOptions, backend api.Service) *cobra.Command { func killCommand(p *projectOptions, backend api.Service) *cobra.Command {
var opts api.KillOptions opts := killOptions{
projectOptions: p,
}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "kill [options] [SERVICE...]", Use: "kill [options] [SERVICE...]",
Short: "Force stop service containers.", Short: "Force stop service containers.",
RunE: p.WithProject(func(ctx context.Context, project *types.Project) error { RunE: Adapt(func(ctx context.Context, args []string) error {
return backend.Kill(ctx, project, opts) return runKill(ctx, backend, opts, args)
}), }),
ValidArgsFunction: serviceCompletion(p), ValidArgsFunction: serviceCompletion(p),
} }
flags := cmd.Flags() flags := cmd.Flags()
flags.StringVarP(&opts.Signal, "signal", "s", "SIGKILL", "SIGNAL to send to the container.") flags.StringVarP(&opts.signal, "signal", "s", "SIGKILL", "SIGNAL to send to the container.")
return cmd return cmd
} }
func runKill(ctx context.Context, backend api.Service, opts killOptions, services []string) error {
projectName, err := opts.toProjectName()
if err != nil {
return err
}
return backend.Kill(ctx, projectName, api.KillOptions{
Services: services,
Signal: opts.signal,
})
}

View File

@ -27,6 +27,7 @@ import (
"github.com/docker/compose/v2/cmd/formatter" "github.com/docker/compose/v2/cmd/formatter"
"github.com/docker/compose/v2/pkg/utils" "github.com/docker/compose/v2/pkg/utils"
"github.com/docker/docker/api/types"
formatter2 "github.com/docker/cli/cli/command/formatter" formatter2 "github.com/docker/cli/cli/command/formatter"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -81,12 +82,11 @@ func psCommand(p *projectOptions, backend api.Service) *cobra.Command {
} }
flags := psCmd.Flags() flags := psCmd.Flags()
flags.StringVar(&opts.Format, "format", "pretty", "Format the output. Values: [pretty | json]") flags.StringVar(&opts.Format, "format", "pretty", "Format the output. Values: [pretty | json]")
flags.StringVar(&opts.Filter, "filter", "", "Filter services by a property. Deprecated, use --status instead") flags.StringVar(&opts.Filter, "filter", "", "Filter services by a property (supported filters: status).")
flags.StringArrayVar(&opts.Status, "status", []string{}, "Filter services by status. Values: [paused | restarting | removing | running | dead | created | exited]") flags.StringArrayVar(&opts.Status, "status", []string{}, "Filter services by status. Values: [paused | restarting | removing | running | dead | created | exited]")
flags.BoolVarP(&opts.Quiet, "quiet", "q", false, "Only display IDs") flags.BoolVarP(&opts.Quiet, "quiet", "q", false, "Only display IDs")
flags.BoolVar(&opts.Services, "services", false, "Display services") flags.BoolVar(&opts.Services, "services", false, "Display services")
flags.BoolVarP(&opts.All, "all", "a", false, "Show all stopped containers (including those created by the run command)") flags.BoolVarP(&opts.All, "all", "a", false, "Show all stopped containers (including those created by the run command)")
flags.Lookup("filter").Hidden = true
return psCmd return psCmd
} }
@ -140,14 +140,14 @@ SERVICES:
} }
return formatter.Print(containers, opts.Format, os.Stdout, return formatter.Print(containers, opts.Format, os.Stdout,
writter(containers), writer(containers),
"NAME", "COMMAND", "SERVICE", "STATUS", "PORTS") "NAME", "COMMAND", "SERVICE", "STATUS", "PORTS")
} }
func writter(containers []api.ContainerSummary) func(w io.Writer) { func writer(containers []api.ContainerSummary) func(w io.Writer) {
return func(w io.Writer) { return func(w io.Writer) {
for _, container := range containers { for _, container := range containers {
ports := DisplayablePorts(container) ports := displayablePorts(container)
status := container.State status := container.State
if status == "running" && container.Health != "" { if status == "running" && container.Health != "" {
status = fmt.Sprintf("%s (%s)", container.State, container.Health) status = fmt.Sprintf("%s (%s)", container.State, container.Health)
@ -179,72 +179,20 @@ func hasStatus(c api.ContainerSummary, statuses []string) bool {
return false return false
} }
type portRange struct { func displayablePorts(c api.ContainerSummary) string {
pStart int
pEnd int
tStart int
tEnd int
IP string
protocol string
}
func (pr portRange) String() string {
var (
pub string
tgt string
)
if pr.pEnd > pr.pStart {
pub = fmt.Sprintf("%s:%d-%d->", pr.IP, pr.pStart, pr.pEnd)
} else if pr.pStart > 0 {
pub = fmt.Sprintf("%s:%d->", pr.IP, pr.pStart)
}
if pr.tEnd > pr.tStart {
tgt = fmt.Sprintf("%d-%d", pr.tStart, pr.tEnd)
} else {
tgt = fmt.Sprintf("%d", pr.tStart)
}
return fmt.Sprintf("%s%s/%s", pub, tgt, pr.protocol)
}
// DisplayablePorts is copy pasted from https://github.com/docker/cli/pull/581/files
func DisplayablePorts(c api.ContainerSummary) string {
if c.Publishers == nil { if c.Publishers == nil {
return "" return ""
} }
sort.Sort(c.Publishers) ports := make([]types.Port, len(c.Publishers))
for i, pub := range c.Publishers {
pr := portRange{} ports[i] = types.Port{
ports := []string{} IP: pub.URL,
for _, p := range c.Publishers { PrivatePort: uint16(pub.TargetPort),
prIsRange := pr.tEnd != pr.tStart PublicPort: uint16(pub.PublishedPort),
tOverlaps := p.TargetPort <= pr.tEnd Type: pub.Protocol,
// Start a new port-range if:
// - the protocol is different from the current port-range
// - published or target port are not consecutive to the current port-range
// - the current port-range is a _range_, and the target port overlaps with the current range's target-ports
if p.Protocol != pr.protocol || p.URL != pr.IP || p.PublishedPort-pr.pEnd > 1 || p.TargetPort-pr.tEnd > 1 || prIsRange && tOverlaps {
// start a new port-range, and print the previous port-range (if any)
if pr.pStart > 0 {
ports = append(ports, pr.String())
}
pr = portRange{
pStart: p.PublishedPort,
pEnd: p.PublishedPort,
tStart: p.TargetPort,
tEnd: p.TargetPort,
protocol: p.Protocol,
IP: p.URL,
}
continue
} }
pr.pEnd = p.PublishedPort
pr.tEnd = p.TargetPort
} }
if pr.tStart > 0 {
ports = append(ports, pr.String()) return formatter2.DisplayablePorts(ports)
}
return strings.Join(ports, ", ")
} }

84
cmd/compose/ps_test.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"
"os"
"path/filepath"
"testing"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/mocks"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
)
func TestPsPretty(t *testing.T) {
ctx := context.Background()
origStdout := os.Stdout
t.Cleanup(func() {
os.Stdout = origStdout
})
dir := t.TempDir()
f, err := os.Create(filepath.Join(dir, "output.txt"))
if err != nil {
t.Fatal("could not create output file")
}
defer func() { _ = f.Close() }()
os.Stdout = f
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",
Publishers: api.PortPublishers{
{
TargetPort: 8080,
PublishedPort: 8080,
Protocol: "tcp",
},
{
TargetPort: 8443,
PublishedPort: 8443,
Protocol: "tcp",
},
},
},
}, nil
}).AnyTimes()
opts := psOptions{projectOptions: &projectOptions{ProjectName: "test"}}
err = runPs(ctx, backend, nil, opts)
assert.NoError(t, err)
_, err = f.Seek(0, 0)
assert.NoError(t, err)
output := make([]byte, 256)
_, err = f.Read(output)
assert.NoError(t, err)
assert.Contains(t, string(output), "8080/tcp, 8443/tcp")
}

View File

@ -59,13 +59,13 @@ Any data which is not in a volume will be lost.`,
} }
func runRemove(ctx context.Context, backend api.Service, opts removeOptions, services []string) error { func runRemove(ctx context.Context, backend api.Service, opts removeOptions, services []string) error {
project, err := opts.toProject(services) project, err := opts.toProjectName()
if err != nil { if err != nil {
return err return err
} }
if opts.stop { if opts.stop {
err := backend.Stop(ctx, project.Name, api.StopOptions{ err := backend.Stop(ctx, project, api.StopOptions{
Services: services, Services: services,
}) })
if err != nil { if err != nil {

View File

@ -19,12 +19,12 @@ package compose
import ( import (
"context" "context"
"fmt" "fmt"
"os"
"strings" "strings"
cgo "github.com/compose-spec/compose-go/cli" cgo "github.com/compose-spec/compose-go/cli"
"github.com/compose-spec/compose-go/loader" "github.com/compose-spec/compose-go/loader"
"github.com/compose-spec/compose-go/types" "github.com/compose-spec/compose-go/types"
"github.com/docker/cli/cli/command"
"github.com/mattn/go-shellwords" "github.com/mattn/go-shellwords"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag" "github.com/spf13/pflag"
@ -42,6 +42,7 @@ type runOptions struct {
Detach bool Detach bool
Remove bool Remove bool
noTty bool noTty bool
interactive bool
user string user string
workdir string workdir string
entrypoint string entrypoint string
@ -53,6 +54,7 @@ type runOptions struct {
servicePorts bool servicePorts bool
name string name string
noDeps bool noDeps bool
ignoreOrphans bool
quietPull bool quietPull bool
} }
@ -61,6 +63,9 @@ func (opts runOptions) apply(project *types.Project) error {
if err != nil { if err != nil {
return err return err
} }
target.Tty = !opts.noTty
target.StdinOpen = opts.interactive
if !opts.servicePorts { if !opts.servicePorts {
target.Ports = []types.ServicePortConfig{} target.Ports = []types.ServicePortConfig{}
} }
@ -102,7 +107,7 @@ func (opts runOptions) apply(project *types.Project) error {
return nil return nil
} }
func runCommand(p *projectOptions, backend api.Service) *cobra.Command { func runCommand(p *projectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
opts := runOptions{ opts := runOptions{
composeOptions: &composeOptions{ composeOptions: &composeOptions{
projectOptions: p, projectOptions: p,
@ -134,6 +139,8 @@ func runCommand(p *projectOptions, backend api.Service) *cobra.Command {
if err != nil { if err != nil {
return err return err
} }
ignore := project.Environment["COMPOSE_IGNORE_ORPHANS"]
opts.ignoreOrphans = strings.ToLower(ignore) == "true"
return runRun(ctx, backend, project, opts) return runRun(ctx, backend, project, opts)
}), }),
ValidArgsFunction: serviceCompletion(p), ValidArgsFunction: serviceCompletion(p),
@ -143,7 +150,7 @@ func runCommand(p *projectOptions, backend api.Service) *cobra.Command {
flags.StringArrayVarP(&opts.environment, "env", "e", []string{}, "Set environment variables") flags.StringArrayVarP(&opts.environment, "env", "e", []string{}, "Set environment variables")
flags.StringArrayVarP(&opts.labels, "label", "l", []string{}, "Add or override a label") flags.StringArrayVarP(&opts.labels, "label", "l", []string{}, "Add or override a label")
flags.BoolVar(&opts.Remove, "rm", false, "Automatically remove the container when it exits") flags.BoolVar(&opts.Remove, "rm", false, "Automatically remove the container when it exits")
flags.BoolVarP(&opts.noTty, "no-TTY", "T", false, "Disable pseudo-noTty allocation. By default docker compose run allocates a TTY") flags.BoolVarP(&opts.noTty, "no-TTY", "T", !dockerCli.Out().IsTerminal(), "Disable pseudo-TTY allocation (default: auto-detected).")
flags.StringVar(&opts.name, "name", "", " Assign a name to the container") flags.StringVar(&opts.name, "name", "", " Assign a name to the container")
flags.StringVarP(&opts.user, "user", "u", "", "Run as specified username or uid") flags.StringVarP(&opts.user, "user", "u", "", "Run as specified username or uid")
flags.StringVarP(&opts.workdir, "workdir", "w", "", "Working directory inside the container") flags.StringVarP(&opts.workdir, "workdir", "w", "", "Working directory inside the container")
@ -155,6 +162,10 @@ func runCommand(p *projectOptions, backend api.Service) *cobra.Command {
flags.BoolVar(&opts.servicePorts, "service-ports", false, "Run command with the service's ports enabled and mapped to the host.") flags.BoolVar(&opts.servicePorts, "service-ports", false, "Run command with the service's ports enabled and mapped to the host.")
flags.BoolVar(&opts.quietPull, "quiet-pull", false, "Pull without printing progress information.") flags.BoolVar(&opts.quietPull, "quiet-pull", false, "Pull without printing progress information.")
cmd.Flags().BoolVarP(&opts.interactive, "interactive", "i", true, "Keep STDIN open even if not attached.")
cmd.Flags().BoolP("tty", "t", true, "Allocate a pseudo-TTY.")
cmd.Flags().MarkHidden("tty") //nolint:errcheck
flags.SetNormalizeFunc(normalizeRunFlags) flags.SetNormalizeFunc(normalizeRunFlags)
flags.SetInterspersed(false) flags.SetInterspersed(false)
return cmd return cmd
@ -177,7 +188,7 @@ func runRun(ctx context.Context, backend api.Service, project *types.Project, op
} }
err = progress.Run(ctx, func(ctx context.Context) error { err = progress.Run(ctx, func(ctx context.Context) error {
return startDependencies(ctx, backend, *project, opts.Service) return startDependencies(ctx, backend, *project, opts.Service, opts.ignoreOrphans)
}) })
if err != nil { if err != nil {
return err return err
@ -199,10 +210,8 @@ func runRun(ctx context.Context, backend api.Service, project *types.Project, op
Command: opts.Command, Command: opts.Command,
Detach: opts.Detach, Detach: opts.Detach,
AutoRemove: opts.Remove, AutoRemove: opts.Remove,
Stdin: os.Stdin,
Stdout: os.Stdout,
Stderr: os.Stderr,
Tty: !opts.noTty, Tty: !opts.noTty,
Interactive: opts.interactive,
WorkingDir: opts.workdir, WorkingDir: opts.workdir,
User: opts.user, User: opts.user,
Environment: opts.environment, Environment: opts.environment,
@ -213,6 +222,14 @@ func runRun(ctx context.Context, backend api.Service, project *types.Project, op
Index: 0, Index: 0,
QuietPull: opts.quietPull, QuietPull: opts.quietPull,
} }
for i, service := range project.Services {
if service.Name == opts.Service {
service.StdinOpen = opts.interactive
project.Services[i] = service
}
}
exitCode, err := backend.RunOneOffContainer(ctx, project, runOpts) exitCode, err := backend.RunOneOffContainer(ctx, project, runOpts)
if exitCode != 0 { if exitCode != 0 {
errMsg := "" errMsg := ""
@ -224,7 +241,7 @@ func runRun(ctx context.Context, backend api.Service, project *types.Project, op
return err return err
} }
func startDependencies(ctx context.Context, backend api.Service, project types.Project, requestedServiceName string) error { func startDependencies(ctx context.Context, backend api.Service, project types.Project, requestedServiceName string, ignoreOrphans bool) error {
dependencies := types.Services{} dependencies := types.Services{}
var requestedService types.ServiceConfig var requestedService types.ServiceConfig
for _, service := range project.Services { for _, service := range project.Services {
@ -237,8 +254,17 @@ func startDependencies(ctx context.Context, backend api.Service, project types.P
project.Services = dependencies project.Services = dependencies
project.DisabledServices = append(project.DisabledServices, requestedService) project.DisabledServices = append(project.DisabledServices, requestedService)
if err := backend.Create(ctx, &project, api.CreateOptions{}); err != nil { err := backend.Create(ctx, &project, api.CreateOptions{
IgnoreOrphans: ignoreOrphans,
})
if err != nil {
return err return err
} }
return backend.Start(ctx, project.Name, api.StartOptions{})
if len(dependencies) > 0 {
return backend.Start(ctx, project.Name, api.StartOptions{
Project: &project,
})
}
return nil
} }

View File

@ -103,8 +103,7 @@ func upCommand(p *projectOptions, backend api.Service) *cobra.Command {
return validateFlags(&up, &create) return validateFlags(&up, &create)
}), }),
RunE: p.WithServices(func(ctx context.Context, project *types.Project, services []string) error { RunE: p.WithServices(func(ctx context.Context, project *types.Project, services []string) error {
ignore := project.Environment["COMPOSE_IGNORE_ORPHANS"] create.ignoreOrphans = utils.StringToBool(project.Environment["COMPOSE_IGNORE_ORPHANS"])
create.ignoreOrphans = strings.ToLower(ignore) == "true"
if create.ignoreOrphans && create.removeOrphans { if create.ignoreOrphans && create.removeOrphans {
return fmt.Errorf("COMPOSE_IGNORE_ORPHANS and --remove-orphans cannot be combined") return fmt.Errorf("COMPOSE_IGNORE_ORPHANS and --remove-orphans cannot be combined")
} }
@ -186,6 +185,9 @@ func runUp(ctx context.Context, backend api.Service, createOptions createOptions
if upOptions.attachDependencies { if upOptions.attachDependencies {
attachTo = project.ServiceNames() attachTo = project.ServiceNames()
} }
if len(attachTo) == 0 {
attachTo = project.ServiceNames()
}
create := api.CreateOptions{ create := api.CreateOptions{
Services: services, Services: services,
@ -205,6 +207,7 @@ func runUp(ctx context.Context, backend api.Service, createOptions createOptions
return backend.Up(ctx, project, api.UpOptions{ return backend.Up(ctx, project, api.UpOptions{
Create: create, Create: create,
Start: api.StartOptions{ Start: api.StartOptions{
Project: project,
Attach: consumer, Attach: consumer,
AttachTo: attachTo, AttachTo: attachTo,
ExitCodeFrom: upOptions.exitCodeFrom, ExitCodeFrom: upOptions.exitCodeFrom,

View File

@ -57,7 +57,7 @@ func runVersion(opts versionOptions) {
return return
} }
if opts.format == formatter.JSON { if opts.format == formatter.JSON {
fmt.Printf(`{"version":%q}\n`, internal.Version) fmt.Printf("{\"version\":%q}\n", internal.Version)
return return
} }
fmt.Println("Docker Compose version", internal.Version) fmt.Println("Docker Compose version", internal.Version)

View File

@ -79,7 +79,7 @@ func (l *logConsumer) Log(container, service, message string) {
} }
p := l.getPresenter(container) p := l.getPresenter(container)
for _, line := range strings.Split(message, "\n") { for _, line := range strings.Split(message, "\n") {
fmt.Fprintf(l.writer, "%s %s\n", p.prefix, line) // nolint:errcheck fmt.Fprintf(l.writer, "%s%s\n", p.prefix, line) // nolint:errcheck
} }
} }
@ -118,5 +118,5 @@ type presenter struct {
} }
func (p *presenter) setPrefix(width int) { func (p *presenter) setPrefix(width int) {
p.prefix = p.colors(fmt.Sprintf("%-"+strconv.Itoa(width)+"s |", p.name)) p.prefix = p.colors(fmt.Sprintf("%-"+strconv.Itoa(width)+"s | ", p.name))
} }

View File

@ -32,21 +32,16 @@ import (
"github.com/docker/compose/v2/pkg/compose" "github.com/docker/compose/v2/pkg/compose"
) )
func init() {
commands.Warning = "The new 'docker compose' command is currently experimental. " +
"To provide feedback or request new features please open issues at https://github.com/docker/compose"
}
func pluginMain() { func pluginMain() {
plugin.Run(func(dockerCli command.Cli) *cobra.Command { plugin.Run(func(dockerCli command.Cli) *cobra.Command {
lazyInit := api.NewServiceProxy() lazyInit := api.NewServiceProxy()
cmd := commands.RootCommand(lazyInit) cmd := commands.RootCommand(dockerCli, lazyInit)
originalPreRun := cmd.PersistentPreRunE originalPreRun := cmd.PersistentPreRunE
cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
if err := plugin.PersistentPreRunE(cmd, args); err != nil { if err := plugin.PersistentPreRunE(cmd, args); err != nil {
return err return err
} }
lazyInit.WithService(compose.NewComposeService(dockerCli.Client(), dockerCli.ConfigFile())) lazyInit.WithService(compose.NewComposeService(dockerCli))
if originalPreRun != nil { if originalPreRun != nil {
return originalPreRun(cmd, args) return originalPreRun(cmd, args)
} }
@ -68,7 +63,7 @@ func pluginMain() {
} }
func main() { func main() {
if commands.RunningAsStandalone() { if plugin.RunningStandalone() {
os.Args = append([]string{"docker"}, compatibility.Convert(os.Args[1:])...) os.Args = append([]string{"docker"}, compatibility.Convert(os.Args[1:])...)
} }
pluginMain() pluginMain()

57
docs/docs.Dockerfile Normal file
View File

@ -0,0 +1,57 @@
# syntax=docker/dockerfile:1.3-labs
# 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.18.3
ARG FORMATS=md,yaml
FROM --platform=${BUILDPLATFORM} golang:${GO_VERSION}-alpine AS docsgen
WORKDIR /src
RUN --mount=target=. \
--mount=target=/root/.cache,type=cache \
go build -o /out/docsgen ./docs/yaml/main/generate.go
FROM --platform=${BUILDPLATFORM} alpine AS gen
RUN apk add --no-cache rsync git
WORKDIR /src
COPY --from=docsgen /out/docsgen /usr/bin
ARG FORMATS
RUN --mount=target=/context \
--mount=target=.,type=tmpfs <<EOT
set -e
rsync -a /context/. .
docsgen --formats "$FORMATS" --source "docs/reference"
mkdir /out
cp -r docs/reference /out
EOT
FROM scratch AS update
COPY --from=gen /out /out
FROM gen AS 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

View File

@ -1,4 +1,54 @@
# docker compose
<!---MARKER_GEN_START-->
Docker Compose
### Subcommands
| Name | Description |
| --- | --- |
| [`build`](compose_build.md) | Build or rebuild services |
| [`convert`](compose_convert.md) | Converts the compose file to platform's 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. |
| [`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 |
| [`pull`](compose_pull.md) | Pull service images |
| [`push`](compose_push.md) | Push service images |
| [`restart`](compose_restart.md) | Restart containers |
| [`rm`](compose_rm.md) | Removes stopped service containers |
| [`run`](compose_run.md) | Run a one-off command on a service. |
| [`start`](compose_start.md) | Start services |
| [`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 |
### Options
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `--ansi` | `string` | `auto` | Control when to print ANSI control characters ("never"\|"always"\|"auto") |
| `--compatibility` | | | Run compose in backward compatibility mode |
| `--env-file` | `string` | | Specify an alternate environment file. |
| `-f`, `--file` | `stringArray` | | Compose configuration files |
| `--profile` | `stringArray` | | Specify a profile to enable |
| `--project-directory` | `string` | | Specify an alternate working directory
(default: the path of the, first specified, Compose file) |
| `-p`, `--project-name` | `string` | | Project name |
<!---MARKER_GEN_END-->
## Description ## Description
@ -9,8 +59,8 @@ multiple services in Docker containers.
Use the `-f` flag to specify the location of a Compose configuration file. Use the `-f` flag to specify the location of a Compose configuration file.
#### Specifying multiple Compose files #### Specifying multiple Compose files
You can supply multiple `-f` configuration files. When you supply multiple files, Compose combines them into a single You can supply multiple `-f` configuration files. When you supply multiple files, Compose combines them into a single
configuration. Compose builds the configuration in the order you supply the files. Subsequent files override and add configuration. Compose builds the configuration in the order you supply the files. Subsequent files override and add
to their predecessors. to their predecessors.
For example, consider this command line: For example, consider this command line:
@ -30,7 +80,7 @@ services:
volumes: volumes:
- "/data" - "/data"
``` ```
If the `docker-compose.admin.yml` also specifies this same service, any matching fields override the previous file. If the `docker-compose.admin.yml` also specifies this same service, any matching fields override the previous file.
New values, add to the `webapp` service configuration. New values, add to the `webapp` service configuration.
```yaml ```yaml
@ -41,22 +91,22 @@ services:
- DEBUG=1 - DEBUG=1
``` ```
When you use multiple Compose files, all paths in the files are relative to the first configuration file specified 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. 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 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. 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 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. and its parent directories looking for a `compose.yaml` or `docker-compose.yaml` file.
#### Specifying a path to a single Compose 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 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. 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 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 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: get the postgres image for the db service from anywhere by using the `-f` flag as follows:
```console ```console
$ docker compose -f ~/sandbox/rails/compose.yaml pull db $ docker compose -f ~/sandbox/rails/compose.yaml pull db
@ -64,17 +114,17 @@ $ docker compose -f ~/sandbox/rails/compose.yaml pull db
### Use `-p` to specify a project name ### Use `-p` to specify a project name
Each configuration has a project name. If you supply a `-p` flag, you can specify a project name. If you dont Each configuration has a project name. If you supply a `-p` flag, you can specify a project name. If you dont
specify the flag, Compose uses the current directory name. specify the flag, Compose uses the current directory name.
Project name can also be set by `COMPOSE_PROJECT_NAME` environment variable. Project name can also be set by `COMPOSE_PROJECT_NAME` environment variable.
Most compose subcommand can be ran without a compose file, just passing Most compose subcommand can be ran without a compose file, just passing
project name to retrieve the relevant resources. project name to retrieve the relevant resources.
```console ```console
$ docker compose -p my_project ps -a $ docker compose -p my_project ps -a
NAME SERVICE STATUS PORTS NAME SERVICE STATUS PORTS
my_project_demo_1 demo running my_project_demo_1 demo running
$ docker compose -p my_project logs $ docker compose -p my_project logs
demo_1 | PING localhost (127.0.0.1): 56 data bytes demo_1 | PING localhost (127.0.0.1): 56 data bytes
@ -84,8 +134,8 @@ demo_1 | 64 bytes from 127.0.0.1: seq=0 ttl=64 time=0.095 ms
### Use profiles to enable optional services ### Use profiles to enable optional services
Use `--profile` to specify one or more active profiles Use `--profile` to specify one or more active profiles
Calling `docker compose --profile frontend up` will start the services with the profile `frontend` and services Calling `docker compose --profile frontend up` will start the services with the profile `frontend` and services
without any specified profiles. 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` will be enabled. You can also enable multiple profiles, e.g. with `docker compose --profile frontend --profile debug up` the profiles `frontend` and `debug` will be enabled.
Profiles can also be set by `COMPOSE_PROFILES` environment variable. Profiles can also be set by `COMPOSE_PROFILES` environment variable.
@ -99,3 +149,6 @@ Setting the `COMPOSE_FILE` environment variable is equivalent to passing the `-f
and so does `COMPOSE_PROFILES` environment variable for to the `--profiles` flag. and so does `COMPOSE_PROFILES` environment variable for to the `--profiles` flag.
If flags are explicitly set on command line, associated environment variable is ignored If flags are explicitly set on command line, associated environment variable is ignored
Setting the `COMPOSE_IGNORE_ORPHANS` environment variable to `true` will stop docker compose from detecting orphaned
containers for the project.

View File

@ -1,12 +1,30 @@
# docker compose build
<!---MARKER_GEN_START-->
Build or rebuild services
### Options
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `--build-arg` | `stringArray` | | Set build-time variables for services. |
| `--no-cache` | | | Do not use cache when building the image |
| `--progress` | `string` | `auto` | Set type of progress output (auto, tty, plain, quiet) |
| `--pull` | | | Always attempt to pull a newer version of the image. |
| `-q`, `--quiet` | | | Don't print anything to STDOUT |
| `--ssh` | `string` | | Set SSH authentications used when building service images. (use 'default' for using your default SSH Agent) |
<!---MARKER_GEN_END-->
## Description ## Description
Services are built once and then tagged, by default as `project_service`. Services are built once and then tagged, by default as `project_service`.
If the Compose file specifies an If the Compose file specifies an
[image](https://github.com/compose-spec/compose-spec/blob/master/spec.md#image) name, [image](https://github.com/compose-spec/compose-spec/blob/master/spec.md#image) name,
the image is tagged with that name, substituting any variables beforehand. See the image is tagged with that name, substituting any variables beforehand. See
[variable interpolation](https://github.com/compose-spec/compose-spec/blob/master/spec.md#interpolation). [variable interpolation](https://github.com/compose-spec/compose-spec/blob/master/spec.md#interpolation).
If you change a service's `Dockerfile` or the contents of its build directory, If you change a service's `Dockerfile` or the contents of its build directory,
run `docker compose build` to rebuild it. run `docker compose build` to rebuild it.

View File

@ -1,9 +1,35 @@
# docker compose convert
<!---MARKER_GEN_START-->
Converts the compose file to platform's canonical format
### Aliases
`convert`, `config`
### Options
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `--format` | `string` | `yaml` | Format the output. Values: [yaml \| json] |
| `--hash` | `string` | | Print the service config hash, one per line. |
| `--images` | | | Print the image names, one per line. |
| `--no-interpolate` | | | Don't interpolate environment variables. |
| `--no-normalize` | | | Don't normalize compose model. |
| `-o`, `--output` | `string` | | Save to file (default to stdout) |
| `--profiles` | | | Print the profile names, one per line. |
| `-q`, `--quiet` | | | Only validate the configuration, don't print anything. |
| `--resolve-image-digests` | | | Pin image tags to digests. |
| `--services` | | | Print the service names, one per line. |
| `--volumes` | | | Print the volume names, one per line. |
<!---MARKER_GEN_END-->
## Description ## Description
`docker compose convert` render the actual data model to be applied on target platform. When used with Docker engine, `docker compose convert` render the actual data model to be applied on target platform. When used with Docker engine,
it merges the Compose files set by `-f` flags, resolves variables in Compose file, and expands short-notation into it merges the Compose files set by `-f` flags, resolves variables in Compose file, and expands short-notation into
fully defined Compose model. fully defined Compose model.
To allow smooth migration from docker-compose, this subcommand declares alias `docker compose config` To allow smooth migration from docker-compose, this subcommand declares alias `docker compose config`

View File

@ -0,0 +1,16 @@
# docker compose cp
<!---MARKER_GEN_START-->
Copy files/folders between a service container and the local filesystem
### Options
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `-a`, `--archive` | | | Archive mode (copy all uid/gid information) |
| `-L`, `--follow-link` | | | Always follow symbol link in SRC_PATH |
| `--index` | `int` | `0` | Index of the container if there are multiple instances of a service . |
<!---MARKER_GEN_END-->

View File

@ -0,0 +1,17 @@
# docker compose create
<!---MARKER_GEN_START-->
Creates containers for a service.
### Options
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `--build` | | | Build images before starting containers. |
| `--force-recreate` | | | Recreate containers even if their configuration and image haven't changed. |
| `--no-build` | | | Don't build an image, even if it's missing. |
| `--no-recreate` | | | If containers already exist, don't recreate them. Incompatible with --force-recreate. |
<!---MARKER_GEN_END-->

View File

@ -1,4 +1,19 @@
# docker compose down
<!---MARKER_GEN_START-->
Stop and remove containers, networks
### Options
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `--remove-orphans` | | | Remove containers for services not defined in the Compose file. |
| `--rmi` | `string` | | Remove images used by services. "local" remove only images that don't have a custom tag ("local"\|"all") |
| `-t`, `--timeout` | `int` | `10` | Specify a shutdown timeout in seconds |
| `-v`, `--volumes` | | | Remove named volumes declared in the `volumes` section of the Compose file and anonymous volumes attached to containers. |
<!---MARKER_GEN_END-->
## Description ## Description

View File

@ -1,3 +1,16 @@
# docker compose events
<!---MARKER_GEN_START-->
Receive real time events from containers.
### Options
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `--json` | | | Output events as a stream of json objects |
<!---MARKER_GEN_END-->
## Description ## Description

View File

@ -1,7 +1,26 @@
# docker compose exec
<!---MARKER_GEN_START-->
Execute a command in a running container.
### Options
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `-d`, `--detach` | | | Detached mode: Run command in the background. |
| `-e`, `--env` | `stringArray` | | Set environment variables |
| `--index` | `int` | `1` | index of the container if there are multiple instances of a service [default: 1]. |
| `-T`, `--no-TTY` | | | Disable pseudo-TTY allocation. By default `docker compose exec` allocates a TTY. |
| `--privileged` | | | Give extended privileges to the process. |
| `-u`, `--user` | `string` | | Run the command as this user. |
| `-w`, `--workdir` | `string` | | Path to workdir directory for this command. |
<!---MARKER_GEN_END-->
## Description ## Description
This is the equivalent of `docker exec` targeting a Compose service. This is the equivalent of `docker exec` targeting a Compose service.
With this subcommand you can run arbitrary commands in your services. Commands are by default allocating a TTY, so With this subcommand you can run arbitrary commands in your services. Commands are by default allocating a TTY, so
you can use a command such as `docker compose exec web sh` to get an interactive prompt. you can use a command such as `docker compose exec web sh` to get an interactive prompt.

View File

@ -0,0 +1,14 @@
# docker compose images
<!---MARKER_GEN_START-->
List images used by the created containers
### Options
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `-q`, `--quiet` | | | Only display IDs |
<!---MARKER_GEN_END-->

View File

@ -1,3 +1,16 @@
# docker compose kill
<!---MARKER_GEN_START-->
Force stop service containers.
### Options
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `-s`, `--signal` | `string` | `SIGKILL` | SIGNAL to send to the container. |
<!---MARKER_GEN_END-->
## Description ## Description

View File

@ -1,3 +1,22 @@
# docker compose logs
<!---MARKER_GEN_START-->
View output from containers
### Options
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `-f`, `--follow` | | | Follow log output. |
| `--no-color` | | | Produce monochrome output. |
| `--no-log-prefix` | | | Don't print prefix in logs. |
| `--since` | `string` | | Show logs since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes) |
| `--tail` | `string` | `all` | Number of lines to show from the end of the logs for each container. |
| `-t`, `--timestamps` | | | Show timestamps. |
| `--until` | `string` | | Show logs before a timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes) |
<!---MARKER_GEN_END-->
## Description ## Description

View File

@ -1,3 +1,19 @@
# docker compose ls
<!---MARKER_GEN_START-->
List running compose projects
### Options
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `-a`, `--all` | | | Show all stopped Compose projects |
| `--filter` | `filter` | | Filter output based on conditions provided. |
| `--format` | `string` | `pretty` | Format the output. Values: [pretty \| json]. |
| `-q`, `--quiet` | | | Only display IDs. |
<!---MARKER_GEN_END-->
## Description ## Description

View File

@ -1,3 +1,10 @@
# docker compose pause
<!---MARKER_GEN_START-->
Pause services
<!---MARKER_GEN_END-->
## Description ## Description

View File

@ -1,3 +1,17 @@
# docker compose port
<!---MARKER_GEN_START-->
Print the public port for a port binding.
### Options
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `--index` | `int` | `1` | index of the container if service has multiple replicas |
| `--protocol` | `string` | `tcp` | tcp or udp |
<!---MARKER_GEN_END-->
## Description ## Description

View File

@ -1,11 +1,117 @@
# docker compose ps
<!---MARKER_GEN_START-->
List containers
### Options
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `-a`, `--all` | | | Show all stopped containers (including those created by the run command) |
| [`--filter`](#filter) | `string` | | Filter services by a property (supported filters: status). |
| [`--format`](#format) | `string` | `pretty` | Format the output. Values: [pretty \| json] |
| `-q`, `--quiet` | | | Only display IDs |
| `--services` | | | Display services |
| [`--status`](#status) | `stringArray` | | Filter services by status. Values: [paused \| restarting \| removing \| running \| dead \| created \| exited] |
<!---MARKER_GEN_END-->
## Description ## Description
Lists containers for a Compose project, with current status and exposed ports. Lists containers for a Compose project, with current status and exposed ports.
By default, both running and stopped containers are shown:
```console ```console
$ docker compose ps $ docker compose ps
NAME SERVICE STATUS PORTS NAME COMMAND SERVICE STATUS PORTS
example_foo_1 foo running (healthy) 0.0.0.0:8000->80/tcp example-bar-1 "/docker-entrypoint.…" bar exited (0)
example_bar_1 bar exited (1) example-foo-1 "/docker-entrypoint.…" foo running 0.0.0.0:8080->80/tcp
``` ```
## Examples
### <a name="format"></a> Format the output (--format)
By default, the `docker compose ps` command uses a table ("pretty") format to
show the containers. The `--format` flag allows you to specify alternative
presentations for the output. Currently supported options are `pretty` (default),
and `json`, which outputs information about the containers as a JSON array:
```console
$ docker compose ps --format json
[{"ID":"1553b0236cf4d2715845f053a4ee97042c4f9a2ef655731ee34f1f7940eaa41a","Name":"example-bar-1","Command":"/docker-entrypoint.sh nginx -g 'daemon off;'","Project":"example","Service":"bar","State":"exited","Health":"","ExitCode":0,"Publishers":null},{"ID":"f02a4efaabb67416e1ff127d51c4b5578634a0ad5743bd65225ff7d1909a3fa0","Name":"example-foo-1","Command":"/docker-entrypoint.sh nginx -g 'daemon off;'","Project":"example","Service":"foo","State":"running","Health":"","ExitCode":0,"Publishers":[{"URL":"0.0.0.0","TargetPort":80,"PublishedPort":8080,"Protocol":"tcp"}]}]
```
The JSON output allows you to use the information in other tools for further
processing, for example, using the [`jq` utility](https://stedolan.github.io/jq/){:target="_blank" rel="noopener" class="_"}
to pretty-print the JSON:
```console
$ docker compose ps --format json | jq .
[
{
"ID": "1553b0236cf4d2715845f053a4ee97042c4f9a2ef655731ee34f1f7940eaa41a",
"Name": "example-bar-1",
"Command": "/docker-entrypoint.sh nginx -g 'daemon off;'",
"Project": "example",
"Service": "bar",
"State": "exited",
"Health": "",
"ExitCode": 0,
"Publishers": null
},
{
"ID": "f02a4efaabb67416e1ff127d51c4b5578634a0ad5743bd65225ff7d1909a3fa0",
"Name": "example-foo-1",
"Command": "/docker-entrypoint.sh nginx -g 'daemon off;'",
"Project": "example",
"Service": "foo",
"State": "running",
"Health": "",
"ExitCode": 0,
"Publishers": [
{
"URL": "0.0.0.0",
"TargetPort": 80,
"PublishedPort": 8080,
"Protocol": "tcp"
}
]
}
]
```
### <a name="status"></a> Filter containers by status (--status)
Use the `--status` flag to filter the list of containers by status. For example,
to show only containers that are running, or only containers that have exited:
```console
$ docker compose ps --status=running
NAME COMMAND SERVICE STATUS PORTS
example-foo-1 "/docker-entrypoint.…" foo running 0.0.0.0:8080->80/tcp
$ docker compose ps --status=exited
NAME COMMAND SERVICE STATUS PORTS
example-bar-1 "/docker-entrypoint.…" bar exited (0)
```
### <a name="filter"></a> Filter containers by status (--filter)
The [`--status` flag](#status) is a convenience shorthand for the `--filter status=<status>`
flag. The example below is the equivalent to the example from the previous section,
this time using the `--filter` flag:
```console
$ docker compose ps --filter status=running
NAME COMMAND SERVICE STATUS PORTS
example-foo-1 "/docker-entrypoint.…" foo running 0.0.0.0:8080->80/tcp
$ docker compose ps --filter status=running
NAME COMMAND SERVICE STATUS PORTS
example-bar-1 "/docker-entrypoint.…" bar exited (0)
```
The `docker compose ps` command currently only supports the `--filter status=<status>`
option, but additional filter options may be added in future.

View File

@ -1,11 +1,26 @@
# docker compose pull
<!---MARKER_GEN_START-->
Pull service images
### Options
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `--ignore-pull-failures` | | | Pull what it can and ignores images with pull failures |
| `--include-deps` | | | Also pull services declared as dependencies |
| `-q`, `--quiet` | | | Pull without printing progress information |
<!---MARKER_GEN_END-->
## Description ## Description
Pulls an image associated with a service defined in a `compose.yaml` file, but does not start containers based on Pulls an image associated with a service defined in a `compose.yaml` file, but does not start containers based on
those images. those images.
## Examples ## Examples
suppose you have this `compose.yaml`: suppose you have this `compose.yaml`:
@ -24,8 +39,8 @@ services:
- db - db
``` ```
If you run `docker compose pull ServiceName` in the same directory as the `compose.yaml` file that defines the service, If you run `docker compose pull ServiceName` in the same directory as the `compose.yaml` file that defines the service,
Docker pulls the associated image. For example, to call the postgres image configured as the db service in our example, Docker pulls the associated image. For example, to call the postgres image configured as the db service in our example,
you would run `docker compose pull db`. you would run `docker compose pull db`.
```console ```console
@ -46,4 +61,4 @@ $ docker compose pull db
⠹ f63c47038e66 Waiting 9.3s ⠹ f63c47038e66 Waiting 9.3s
⠹ 77a0c198cde5 Waiting 9.3s ⠹ 77a0c198cde5 Waiting 9.3s
⠹ c8752d5b785c Waiting 9.3s ⠹ c8752d5b785c Waiting 9.3s
``̀ ``̀`

View File

@ -1,3 +1,16 @@
# docker compose push
<!---MARKER_GEN_START-->
Push service images
### Options
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `--ignore-push-failures` | | | Push what it can and ignores images with push failures |
<!---MARKER_GEN_END-->
## Description ## Description

View File

@ -1,8 +1,24 @@
# docker compose restart
<!---MARKER_GEN_START-->
Restart containers
### Options
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `-t`, `--timeout` | `int` | `10` | Specify a shutdown timeout in seconds |
<!---MARKER_GEN_END-->
## Description
Restarts all stopped and running services. Restarts all stopped and running services.
If you make changes to your `compose.yml` configuration, these changes are not reflected If you make changes to your `compose.yml` configuration, these changes are not reflected
after running this command. For example, changes to environment variables (which are added after running this command. For example, changes to environment variables (which are added
after a container is built, but before the container's command is executed) are not updated after a container is built, but before the container's command is executed) are not updated
after restarting. after restarting.
If you are looking to configure a service's restart policy, please refer to If you are looking to configure a service's restart policy, please refer to

View File

@ -1,3 +1,23 @@
# docker compose rm
<!---MARKER_GEN_START-->
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.
### Options
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `-f`, `--force` | | | Don't ask to confirm removal |
| `-s`, `--stop` | | | Stop the containers, if required, before removing |
| `-v`, `--volumes` | | | Remove any anonymous volumes attached to containers |
<!---MARKER_GEN_END-->
## Description ## Description

View File

@ -1,7 +1,35 @@
# docker compose run
<!---MARKER_GEN_START-->
Run a one-off command on a service.
### Options
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `-d`, `--detach` | | | Run container in background and print container ID |
| `--entrypoint` | `string` | | Override the entrypoint of the image |
| `-e`, `--env` | `stringArray` | | Set environment variables |
| `-i`, `--interactive` | | | Keep STDIN open even if not attached. |
| `-l`, `--label` | `stringArray` | | Add or override a label |
| `--name` | `string` | | Assign a name to the container |
| `-T`, `--no-TTY` | | | Disable pseudo-TTY allocation (default: auto-detected). |
| `--no-deps` | | | Don't start linked services. |
| `-p`, `--publish` | `stringArray` | | Publish a container's port(s) to the host. |
| `--quiet-pull` | | | Pull without printing progress information. |
| `--rm` | | | Automatically remove the container when it exits |
| `--service-ports` | | | Run command with the service's ports enabled and mapped to the host. |
| `--use-aliases` | | | Use the service's network useAliases in the network(s) the container connects to. |
| `-u`, `--user` | `string` | | Run as specified username or uid |
| `-v`, `--volume` | `stringArray` | | Bind mount a volume. |
| `-w`, `--workdir` | `string` | | Working directory inside the container |
<!---MARKER_GEN_END-->
## Description ## Description
Runs a one-time command against a service. Runs a one-time command against a service.
the following command starts the `web` service and runs `bash` as its command: the following command starts the `web` service and runs `bash` as its command:
@ -12,12 +40,12 @@ $ docker compose run web bash
Commands you use with run start in new containers with configuration defined by that of the service, Commands you use with run start in new containers with configuration defined by that of the service,
including volumes, links, and other details. However, there are two important differences: including volumes, links, and other details. However, there are two important differences:
First, the command passed by `run` overrides the command defined in the service configuration. For example, if the First, the command passed by `run` overrides the command defined in the service configuration. For example, if the
`web` service configuration is started with `bash`, then `docker compose run web python app.py` overrides it with `web` service configuration is started with `bash`, then `docker compose run web python app.py` overrides it with
`python app.py`. `python app.py`.
The second difference is that the `docker compose run` command does not create any of the ports specified in the The second difference is that the `docker compose run` command does not create any of the ports specified in the
service configuration. This prevents port collisions with already-open ports. If you do want the services ports service configuration. This prevents port collisions with already-open ports. If you do want the services ports
to be created and mapped to the host, specify the `--service-ports` to be created and mapped to the host, specify the `--service-ports`
```console ```console
@ -30,8 +58,8 @@ Alternatively, manual port mapping can be specified with the `--publish` or `-p`
$ docker compose run --publish 8080:80 -p 2022:22 -p 127.0.0.1:2021:21 web python manage.py shell $ docker compose run --publish 8080:80 -p 2022:22 -p 127.0.0.1:2021:21 web python manage.py shell
``` ```
If you start a service configured with links, the run command first checks to see if the linked service is running If you start a service configured with links, the run command first checks to see if the linked service is running
and starts the service if it is stopped. Once all the linked services are running, the run executes the command you and starts the service if it is stopped. Once all the linked services are running, the run executes the command you
passed it. For example, you could run: passed it. For example, you could run:
```console ```console
@ -52,5 +80,5 @@ If you want to remove the container after running while overriding the container
$ docker compose run --rm web python manage.py db upgrade $ docker compose run --rm web python manage.py db upgrade
``` ```
This runs a database upgrade script, and removes the container when finished running, even if a restart policy is This runs a database upgrade script, and removes the container when finished running, even if a restart policy is
specified in the service configuration. specified in the service configuration.

View File

@ -1,3 +1,10 @@
# docker compose start
<!---MARKER_GEN_START-->
Start services
<!---MARKER_GEN_END-->
## Description ## Description

View File

@ -1,3 +1,16 @@
# docker compose stop
<!---MARKER_GEN_START-->
Stop services
### Options
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `-t`, `--timeout` | `int` | `10` | Specify a shutdown timeout in seconds |
<!---MARKER_GEN_END-->
## Description ## Description

View File

@ -1,3 +1,10 @@
# docker compose top
<!---MARKER_GEN_START-->
Display the running processes
<!---MARKER_GEN_END-->
## Description ## Description
@ -9,5 +16,5 @@ Displays the running processes.
$ docker compose top $ docker compose top
example_foo_1 example_foo_1
UID PID PPID C STIME TTY TIME CMD UID PID PPID C STIME TTY TIME CMD
root 142353 142331 2 15:33 ? 00:00:00 ping localhost -c 5 root 142353 142331 2 15:33 ? 00:00:00 ping localhost -c 5
``` ```

View File

@ -1,3 +1,10 @@
# docker compose unpause
<!---MARKER_GEN_START-->
Unpause services
<!---MARKER_GEN_END-->
## Description ## Description

View File

@ -1,3 +1,35 @@
# docker compose up
<!---MARKER_GEN_START-->
Create and start containers
### Options
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `--abort-on-container-exit` | | | Stops all containers if any container was stopped. Incompatible with -d |
| `--always-recreate-deps` | | | Recreate dependent containers. Incompatible with --no-recreate. |
| `--attach` | `stringArray` | | Attach to service output. |
| `--attach-dependencies` | | | Attach to dependent containers. |
| `--build` | | | Build images before starting containers. |
| `-d`, `--detach` | | | Detached mode: Run containers in the background |
| `--exit-code-from` | `string` | | Return the exit code of the selected service container. Implies --abort-on-container-exit |
| `--force-recreate` | | | Recreate containers even if their configuration and image haven't changed. |
| `--no-build` | | | Don't build an image, even if it's missing. |
| `--no-color` | | | Produce monochrome output. |
| `--no-deps` | | | Don't start linked services. |
| `--no-log-prefix` | | | Don't print prefix in logs. |
| `--no-recreate` | | | If containers already exist, don't recreate them. Incompatible with --force-recreate. |
| `--no-start` | | | Don't start the services after creating them. |
| `--quiet-pull` | | | Pull without printing progress information. |
| `--remove-orphans` | | | Remove containers for services not defined in the Compose file. |
| `-V`, `--renew-anon-volumes` | | | Recreate anonymous volumes instead of retrieving data from the previous containers. |
| `--scale` | `stringArray` | | Scale SERVICE to NUM instances. Overrides the `scale` setting in the Compose file if present. |
| `-t`, `--timeout` | `int` | `10` | Use this timeout in seconds for container shutdown when attached or when containers are already running. |
| `--wait` | | | Wait for services to be running\|healthy. Implies detached mode. |
<!---MARKER_GEN_END-->
## Description ## Description
@ -5,12 +37,12 @@ Builds, (re)creates, starts, and attaches to containers for a service.
Unless they are already running, this command also starts any linked services. Unless they are already running, this command also starts any linked services.
The `docker compose up` command aggregates the output of each container (like `docker compose logs --follow` does). The `docker compose up` command aggregates the output of each container (like `docker compose logs --follow` does).
When the command exits, all containers are stopped. Running `docker compose up --detach` starts the containers in the When the command exits, all containers are stopped. Running `docker compose up --detach` starts the containers in the
background and leaves them running. background and leaves them running.
If there are existing containers for a service, and the services configuration or image was changed after the If there are existing containers for a service, and the services configuration or image was changed after the
containers creation, `docker compose up` picks up the changes by stopping and recreating the containers containers creation, `docker compose up` picks up the changes by stopping and recreating the containers
(preserving mounted volumes). To prevent Compose from picking up changes, use the `--no-recreate` flag. (preserving mounted volumes). To prevent Compose from picking up changes, use the `--no-recreate` flag.
If you want to force Compose to stop and recreate all containers, use the `--force-recreate` flag. If you want to force Compose to stop and recreate all containers, use the `--force-recreate` flag.

View File

@ -0,0 +1,14 @@
# docker compose version
<!---MARKER_GEN_START-->
Show the Docker Compose version information
### Options
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `-f`, `--format` | `string` | | Format the output. Values: [pretty \| json]. (Default: pretty) |
| `--short` | | | Shows only Compose's version number. |
<!---MARKER_GEN_END-->

View File

@ -98,6 +98,9 @@ long: |-
and so does `COMPOSE_PROFILES` environment variable for to the `--profiles` flag. and so does `COMPOSE_PROFILES` environment variable for to the `--profiles` flag.
If flags are explicitly set on command line, associated environment variable is ignored If flags are explicitly set on command line, associated environment variable is ignored
Setting the `COMPOSE_IGNORE_ORPHANS` environment variable to `true` will stop docker compose from detecting orphaned
containers for the project.
usage: docker compose usage: docker compose
pname: docker pname: docker
plink: docker.yaml plink: docker.yaml
@ -126,6 +129,7 @@ cname:
- docker compose top - docker compose top
- docker compose unpause - docker compose unpause
- docker compose up - docker compose up
- docker compose version
clink: clink:
- docker_compose_build.yaml - docker_compose_build.yaml
- docker_compose_convert.yaml - docker_compose_convert.yaml
@ -151,6 +155,7 @@ clink:
- docker_compose_top.yaml - docker_compose_top.yaml
- docker_compose_unpause.yaml - docker_compose_unpause.yaml
- docker_compose_up.yaml - docker_compose_up.yaml
- docker_compose_version.yaml
options: options:
- option: ansi - option: ansi
value_type: string value_type: string
@ -158,6 +163,17 @@ options:
description: | description: |
Control when to print ANSI control characters ("never"|"always"|"auto") Control when to print ANSI control characters ("never"|"always"|"auto")
deprecated: false deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: compatibility
value_type: bool
default_value: "false"
description: Run compose in backward compatibility mode
deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -166,6 +182,7 @@ options:
value_type: string value_type: string
description: Specify an alternate environment file. description: Specify an alternate environment file.
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -176,6 +193,7 @@ options:
default_value: '[]' default_value: '[]'
description: Compose configuration files description: Compose configuration files
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -185,6 +203,7 @@ options:
default_value: "false" default_value: "false"
description: Do not print ANSI control characters (DEPRECATED) description: Do not print ANSI control characters (DEPRECATED)
deprecated: false deprecated: false
hidden: true
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -194,6 +213,7 @@ options:
default_value: '[]' default_value: '[]'
description: Specify a profile to enable description: Specify a profile to enable
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -202,8 +222,9 @@ options:
value_type: string value_type: string
description: |- description: |-
Specify an alternate working directory Specify an alternate working directory
(default: the path of the Compose file) (default: the path of the, first specified, Compose file)
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -213,6 +234,7 @@ options:
value_type: string value_type: string
description: Project name description: Project name
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -222,6 +244,18 @@ options:
default_value: "false" default_value: "false"
description: Show more output description: Show more output
deprecated: false deprecated: false
hidden: true
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: version
shorthand: v
value_type: bool
default_value: "false"
description: Show the Docker Compose version information
deprecated: false
hidden: true
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -231,8 +265,9 @@ options:
description: |- description: |-
DEPRECATED! USE --project-directory INSTEAD. DEPRECATED! USE --project-directory INSTEAD.
Specify an alternate working directory Specify an alternate working directory
(default: the path of the Compose file) (default: the path of the, first specified, Compose file)
deprecated: false deprecated: false
hidden: true
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false

View File

@ -19,6 +19,7 @@ options:
default_value: '[]' default_value: '[]'
description: Set build-time variables for services. description: Set build-time variables for services.
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -28,6 +29,7 @@ options:
default_value: "true" default_value: "true"
description: Compress the build context using gzip. DEPRECATED description: Compress the build context using gzip. DEPRECATED
deprecated: false deprecated: false
hidden: true
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -37,6 +39,7 @@ options:
default_value: "true" default_value: "true"
description: Always remove intermediate containers. DEPRECATED description: Always remove intermediate containers. DEPRECATED
deprecated: false deprecated: false
hidden: true
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -47,6 +50,7 @@ options:
description: | description: |
Set memory limit for the build container. Not supported on buildkit yet. Set memory limit for the build container. Not supported on buildkit yet.
deprecated: false deprecated: false
hidden: true
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -56,6 +60,7 @@ options:
default_value: "false" default_value: "false"
description: Do not use cache when building the image description: Do not use cache when building the image
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -66,6 +71,7 @@ options:
description: | description: |
Do not remove intermediate containers after a successful build. DEPRECATED Do not remove intermediate containers after a successful build. DEPRECATED
deprecated: false deprecated: false
hidden: true
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -75,6 +81,7 @@ options:
default_value: "true" default_value: "true"
description: Build images in parallel. DEPRECATED description: Build images in parallel. DEPRECATED
deprecated: false deprecated: false
hidden: true
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -82,8 +89,9 @@ options:
- option: progress - option: progress
value_type: string value_type: string
default_value: auto default_value: auto
description: Set type of progress output ("auto", "plain", "noTty") description: Set type of progress output (auto, tty, plain, quiet)
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -93,6 +101,7 @@ options:
default_value: "false" default_value: "false"
description: Always attempt to pull a newer version of the image. description: Always attempt to pull a newer version of the image.
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -103,6 +112,17 @@ options:
default_value: "false" default_value: "false"
description: Don't print anything to STDOUT description: Don't print anything to STDOUT
deprecated: false deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: ssh
value_type: string
description: |
Set SSH authentications used when building service images. (use 'default' for using your default SSH Agent)
deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false

View File

@ -16,6 +16,7 @@ options:
default_value: yaml default_value: yaml
description: 'Format the output. Values: [yaml | json]' description: 'Format the output. Values: [yaml | json]'
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -24,6 +25,17 @@ options:
value_type: string value_type: string
description: Print the service config hash, one per line. description: Print the service config hash, one per line.
deprecated: false deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: images
value_type: bool
default_value: "false"
description: Print the image names, one per line.
deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -33,6 +45,27 @@ options:
default_value: "false" default_value: "false"
description: Don't interpolate environment variables. description: Don't interpolate environment variables.
deprecated: false deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: no-normalize
value_type: bool
default_value: "false"
description: Don't normalize compose model.
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: output
shorthand: o
value_type: string
description: Save to file (default to stdout)
deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -42,6 +75,7 @@ options:
default_value: "false" default_value: "false"
description: Print the profile names, one per line. description: Print the profile names, one per line.
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -52,6 +86,7 @@ options:
default_value: "false" default_value: "false"
description: Only validate the configuration, don't print anything. description: Only validate the configuration, don't print anything.
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -61,6 +96,7 @@ options:
default_value: "false" default_value: "false"
description: Pin image tags to digests. description: Pin image tags to digests.
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -70,6 +106,7 @@ options:
default_value: "false" default_value: "false"
description: Print the service names, one per line. description: Print the service names, one per line.
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -79,6 +116,7 @@ options:
default_value: "false" default_value: "false"
description: Print the volume names, one per line. description: Print the volume names, one per line.
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false

View File

@ -10,7 +10,8 @@ options:
value_type: bool value_type: bool
default_value: "false" default_value: "false"
description: Copy to all the containers of the service. description: Copy to all the containers of the service.
deprecated: false deprecated: true
hidden: true
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -21,6 +22,7 @@ options:
default_value: "false" default_value: "false"
description: Archive mode (copy all uid/gid information) description: Archive mode (copy all uid/gid information)
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -31,16 +33,18 @@ options:
default_value: "false" default_value: "false"
description: Always follow symbol link in SRC_PATH description: Always follow symbol link in SRC_PATH
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
swarm: false swarm: false
- option: index - option: index
value_type: int value_type: int
default_value: "1" default_value: "0"
description: | description: |
Index of the container if there are multiple instances of a service [default: 1]. Index of the container if there are multiple instances of a service .
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false

View File

@ -10,6 +10,7 @@ options:
default_value: "false" default_value: "false"
description: Build images before starting containers. description: Build images before starting containers.
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -20,6 +21,7 @@ options:
description: | description: |
Recreate containers even if their configuration and image haven't changed. Recreate containers even if their configuration and image haven't changed.
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -29,6 +31,7 @@ options:
default_value: "false" default_value: "false"
description: Don't build an image, even if it's missing. description: Don't build an image, even if it's missing.
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -39,6 +42,7 @@ options:
description: | description: |
If containers already exist, don't recreate them. Incompatible with --force-recreate. If containers already exist, don't recreate them. Incompatible with --force-recreate.
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false

View File

@ -23,6 +23,7 @@ options:
default_value: "false" default_value: "false"
description: Remove containers for services not defined in the Compose file. description: Remove containers for services not defined in the Compose file.
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -32,6 +33,7 @@ options:
description: | description: |
Remove images used by services. "local" remove only images that don't have a custom tag ("local"|"all") Remove images used by services. "local" remove only images that don't have a custom tag ("local"|"all")
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -42,6 +44,7 @@ options:
default_value: "10" default_value: "10"
description: Specify a shutdown timeout in seconds description: Specify a shutdown timeout in seconds
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -53,6 +56,7 @@ options:
description: | description: |
Remove named volumes declared in the `volumes` section of the Compose file and anonymous volumes attached to containers. Remove named volumes declared in the `volumes` section of the Compose file and anonymous volumes attached to containers.
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false

View File

@ -29,6 +29,7 @@ options:
default_value: "false" default_value: "false"
description: Output events as a stream of json objects description: Output events as a stream of json objects
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false

View File

@ -15,6 +15,7 @@ options:
default_value: "false" default_value: "false"
description: 'Detached mode: Run command in the background.' description: 'Detached mode: Run command in the background.'
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -25,6 +26,7 @@ options:
default_value: '[]' default_value: '[]'
description: Set environment variables description: Set environment variables
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -35,6 +37,18 @@ options:
description: | description: |
index of the container if there are multiple instances of a service [default: 1]. index of the container if there are multiple instances of a service [default: 1].
deprecated: false deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: interactive
shorthand: i
value_type: bool
default_value: "true"
description: Keep STDIN open even if not attached.
deprecated: false
hidden: true
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -42,10 +56,11 @@ options:
- option: no-TTY - option: no-TTY
shorthand: T shorthand: T
value_type: bool value_type: bool
default_value: "false" default_value: "true"
description: | description: |
Disable pseudo-TTY allocation. By default `docker compose exec` allocates a TTY. Disable pseudo-TTY allocation. By default `docker compose exec` allocates a TTY.
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -55,6 +70,18 @@ options:
default_value: "false" default_value: "false"
description: Give extended privileges to the process. description: Give extended privileges to the process.
deprecated: false deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: tty
shorthand: t
value_type: bool
default_value: "true"
description: Allocate a pseudo-TTY.
deprecated: false
hidden: true
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -64,6 +91,7 @@ options:
value_type: string value_type: string
description: Run the command as this user. description: Run the command as this user.
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -73,6 +101,7 @@ options:
value_type: string value_type: string
description: Path to workdir directory for this command. description: Path to workdir directory for this command.
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false

View File

@ -11,6 +11,7 @@ options:
default_value: "false" default_value: "false"
description: Only display IDs description: Only display IDs
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false

View File

@ -16,6 +16,7 @@ options:
default_value: SIGKILL default_value: SIGKILL
description: SIGNAL to send to the container. description: SIGNAL to send to the container.
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false

View File

@ -11,6 +11,7 @@ options:
default_value: "false" default_value: "false"
description: Follow log output. description: Follow log output.
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -20,6 +21,7 @@ options:
default_value: "false" default_value: "false"
description: Produce monochrome output. description: Produce monochrome output.
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -29,6 +31,7 @@ options:
default_value: "false" default_value: "false"
description: Don't print prefix in logs. description: Don't print prefix in logs.
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -38,6 +41,7 @@ options:
description: | description: |
Show logs since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes) Show logs since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -48,6 +52,7 @@ options:
description: | description: |
Number of lines to show from the end of the logs for each container. Number of lines to show from the end of the logs for each container.
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -58,6 +63,7 @@ options:
default_value: "false" default_value: "false"
description: Show timestamps. description: Show timestamps.
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -67,6 +73,7 @@ options:
description: | description: |
Show logs before a timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes) Show logs before a timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false

View File

@ -11,6 +11,7 @@ options:
default_value: "false" default_value: "false"
description: Show all stopped Compose projects description: Show all stopped Compose projects
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -19,6 +20,7 @@ options:
value_type: filter value_type: filter
description: Filter output based on conditions provided. description: Filter output based on conditions provided.
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -28,6 +30,7 @@ options:
default_value: pretty default_value: pretty
description: 'Format the output. Values: [pretty | json].' description: 'Format the output. Values: [pretty | json].'
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -38,6 +41,7 @@ options:
default_value: "false" default_value: "false"
description: Only display IDs. description: Only display IDs.
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false

View File

@ -1,5 +1,5 @@
command: docker compose pause command: docker compose pause
short: pause services short: Pause services
long: | long: |
Pauses running containers of a service. They can be unpaused with `docker compose unpause`. Pauses running containers of a service. They can be unpaused with `docker compose unpause`.
usage: docker compose pause [SERVICE...] usage: docker compose pause [SERVICE...]

View File

@ -10,6 +10,7 @@ options:
default_value: "1" default_value: "1"
description: index of the container if service has multiple replicas description: index of the container if service has multiple replicas
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -19,6 +20,7 @@ options:
default_value: tcp default_value: tcp
description: tcp or udp description: tcp or udp
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false

View File

@ -2,12 +2,13 @@ command: docker compose ps
short: List containers short: List containers
long: |- long: |-
Lists containers for a Compose project, with current status and exposed ports. Lists containers for a Compose project, with current status and exposed ports.
By default, both running and stopped containers are shown:
```console ```console
$ docker compose ps $ docker compose ps
NAME SERVICE STATUS PORTS NAME COMMAND SERVICE STATUS PORTS
example_foo_1 foo running (healthy) 0.0.0.0:8000->80/tcp example-bar-1 "/docker-entrypoint.…" bar exited (0)
example_bar_1 bar exited (1) example-foo-1 "/docker-entrypoint.…" foo running 0.0.0.0:8080->80/tcp
``` ```
usage: docker compose ps [SERVICE...] usage: docker compose ps [SERVICE...]
pname: docker compose pname: docker compose
@ -20,14 +21,17 @@ options:
description: | description: |
Show all stopped containers (including those created by the run command) Show all stopped containers (including those created by the run command)
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
swarm: false swarm: false
- option: filter - option: filter
value_type: string value_type: string
description: Filter services by a property description: 'Filter services by a property (supported filters: status).'
details_url: '#filter'
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -36,7 +40,9 @@ options:
value_type: string value_type: string
default_value: pretty default_value: pretty
description: 'Format the output. Values: [pretty | json]' description: 'Format the output. Values: [pretty | json]'
details_url: '#format'
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -47,6 +53,7 @@ options:
default_value: "false" default_value: "false"
description: Only display IDs description: Only display IDs
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -56,18 +63,108 @@ options:
default_value: "false" default_value: "false"
description: Display services description: Display services
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
swarm: false swarm: false
- option: status - option: status
value_type: string value_type: stringArray
description: Filter services by status default_value: '[]'
description: |
Filter services by status. Values: [paused | restarting | removing | running | dead | created | exited]
details_url: '#status'
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
swarm: false swarm: false
examples: |-
### Format the output (--format) {#format}
By default, the `docker compose ps` command uses a table ("pretty") format to
show the containers. The `--format` flag allows you to specify alternative
presentations for the output. Currently supported options are `pretty` (default),
and `json`, which outputs information about the containers as a JSON array:
```console
$ docker compose ps --format json
[{"ID":"1553b0236cf4d2715845f053a4ee97042c4f9a2ef655731ee34f1f7940eaa41a","Name":"example-bar-1","Command":"/docker-entrypoint.sh nginx -g 'daemon off;'","Project":"example","Service":"bar","State":"exited","Health":"","ExitCode":0,"Publishers":null},{"ID":"f02a4efaabb67416e1ff127d51c4b5578634a0ad5743bd65225ff7d1909a3fa0","Name":"example-foo-1","Command":"/docker-entrypoint.sh nginx -g 'daemon off;'","Project":"example","Service":"foo","State":"running","Health":"","ExitCode":0,"Publishers":[{"URL":"0.0.0.0","TargetPort":80,"PublishedPort":8080,"Protocol":"tcp"}]}]
```
The JSON output allows you to use the information in other tools for further
processing, for example, using the [`jq` utility](https://stedolan.github.io/jq/){:target="_blank" rel="noopener" class="_"}
to pretty-print the JSON:
```console
$ docker compose ps --format json | jq .
[
{
"ID": "1553b0236cf4d2715845f053a4ee97042c4f9a2ef655731ee34f1f7940eaa41a",
"Name": "example-bar-1",
"Command": "/docker-entrypoint.sh nginx -g 'daemon off;'",
"Project": "example",
"Service": "bar",
"State": "exited",
"Health": "",
"ExitCode": 0,
"Publishers": null
},
{
"ID": "f02a4efaabb67416e1ff127d51c4b5578634a0ad5743bd65225ff7d1909a3fa0",
"Name": "example-foo-1",
"Command": "/docker-entrypoint.sh nginx -g 'daemon off;'",
"Project": "example",
"Service": "foo",
"State": "running",
"Health": "",
"ExitCode": 0,
"Publishers": [
{
"URL": "0.0.0.0",
"TargetPort": 80,
"PublishedPort": 8080,
"Protocol": "tcp"
}
]
}
]
```
### Filter containers by status (--status) {#status}
Use the `--status` flag to filter the list of containers by status. For example,
to show only containers that are running, or only containers that have exited:
```console
$ docker compose ps --status=running
NAME COMMAND SERVICE STATUS PORTS
example-foo-1 "/docker-entrypoint.…" foo running 0.0.0.0:8080->80/tcp
$ docker compose ps --status=exited
NAME COMMAND SERVICE STATUS PORTS
example-bar-1 "/docker-entrypoint.…" bar exited (0)
```
### Filter containers by status (--filter) {#filter}
The [`--status` flag](#status) is a convenience shorthand for the `--filter status=<status>`
flag. The example below is the equivalent to the example from the previous section,
this time using the `--filter` flag:
```console
$ docker compose ps --filter status=running
NAME COMMAND SERVICE STATUS PORTS
example-foo-1 "/docker-entrypoint.…" foo running 0.0.0.0:8080->80/tcp
$ docker compose ps --filter status=running
NAME COMMAND SERVICE STATUS PORTS
example-bar-1 "/docker-entrypoint.…" bar exited (0)
```
The `docker compose ps` command currently only supports the `--filter status=<status>`
option, but additional filter options may be added in future.
deprecated: false deprecated: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false

View File

@ -12,6 +12,7 @@ options:
default_value: "false" default_value: "false"
description: Pull what it can and ignores images with pull failures description: Pull what it can and ignores images with pull failures
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -21,6 +22,7 @@ options:
default_value: "false" default_value: "false"
description: Also pull services declared as dependencies description: Also pull services declared as dependencies
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -30,6 +32,7 @@ options:
default_value: "true" default_value: "true"
description: DEPRECATED disable parallel pulling. description: DEPRECATED disable parallel pulling.
deprecated: false deprecated: false
hidden: true
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -39,6 +42,7 @@ options:
default_value: "true" default_value: "true"
description: DEPRECATED pull multiple images in parallel. description: DEPRECATED pull multiple images in parallel.
deprecated: false deprecated: false
hidden: true
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -49,10 +53,52 @@ options:
default_value: "false" default_value: "false"
description: Pull without printing progress information description: Pull without printing progress information
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
swarm: false swarm: false
examples: |-
suppose you have this `compose.yaml`:
```yaml
services:
db:
image: postgres
web:
build: .
command: bundle exec rails s -p 3000 -b '0.0.0.0'
volumes:
- .:/myapp
ports:
- "3000:3000"
depends_on:
- db
```
If you run `docker compose pull ServiceName` in the same directory as the `compose.yaml` file that defines the service,
Docker pulls the associated image. For example, to call the postgres image configured as the db service in our example,
you would run `docker compose pull db`.
```console
$ docker compose pull db
[+] Running 1/15
⠸ db Pulling 12.4s
⠿ 45b42c59be33 Already exists 0.0s
⠹ 40adec129f1a Downloading 3.374MB/4.178MB 9.3s
⠹ b4c431d00c78 Download complete 9.3s
⠹ 2696974e2815 Download complete 9.3s
⠹ 564b77596399 Downloading 5.622MB/7.965MB 9.3s
⠹ 5044045cf6f2 Downloading 216.7kB/391.1kB 9.3s
⠹ d736e67e6ac3 Waiting 9.3s
⠹ 390c1c9a5ae4 Waiting 9.3s
⠹ c0e62f172284 Waiting 9.3s
⠹ ebcdc659c5bf Waiting 9.3s
⠹ 29be22cb3acc Waiting 9.3s
⠹ f63c47038e66 Waiting 9.3s
⠹ 77a0c198cde5 Waiting 9.3s
⠹ c8752d5b785c Waiting 9.3s
``̀`
deprecated: false deprecated: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false

View File

@ -28,6 +28,7 @@ options:
default_value: "false" default_value: "false"
description: Push what it can and ignores images with push failures description: Push what it can and ignores images with push failures
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false

View File

@ -1,6 +1,16 @@
command: docker compose restart command: docker compose restart
short: Restart containers short: Restart containers
long: Restart containers long: |-
Restarts all stopped and running services.
If you make changes to your `compose.yml` configuration, these changes are not reflected
after running this command. For example, changes to environment variables (which are added
after a container is built, but before the container's command is executed) are not updated
after restarting.
If you are looking to configure a service's restart policy, please refer to
[restart](https://github.com/compose-spec/compose-spec/blob/master/spec.md#restart)
or [restart_policy](https://github.com/compose-spec/compose-spec/blob/master/deploy.md#restart_policy).
usage: docker compose restart usage: docker compose restart
pname: docker compose pname: docker compose
plink: docker_compose.yaml plink: docker_compose.yaml
@ -11,6 +21,7 @@ options:
default_value: "10" default_value: "10"
description: Specify a shutdown timeout in seconds description: Specify a shutdown timeout in seconds
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false

View File

@ -26,6 +26,7 @@ options:
default_value: "false" default_value: "false"
description: Deprecated - no effect description: Deprecated - no effect
deprecated: false deprecated: false
hidden: true
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -36,6 +37,7 @@ options:
default_value: "false" default_value: "false"
description: Don't ask to confirm removal description: Don't ask to confirm removal
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -46,6 +48,7 @@ options:
default_value: "false" default_value: "false"
description: Stop the containers, if required, before removing description: Stop the containers, if required, before removing
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -56,6 +59,7 @@ options:
default_value: "false" default_value: "false"
description: Remove any anonymous volumes attached to containers description: Remove any anonymous volumes attached to containers
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false

View File

@ -65,6 +65,7 @@ options:
default_value: "false" default_value: "false"
description: Run container in background and print container ID description: Run container in background and print container ID
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -73,6 +74,7 @@ options:
value_type: string value_type: string
description: Override the entrypoint of the image description: Override the entrypoint of the image
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -83,16 +85,29 @@ options:
default_value: '[]' default_value: '[]'
description: Set environment variables description: Set environment variables
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
swarm: false swarm: false
- option: labels - option: interactive
shorthand: i
value_type: bool
default_value: "true"
description: Keep STDIN open even if not attached.
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: label
shorthand: l shorthand: l
value_type: stringArray value_type: stringArray
default_value: '[]' default_value: '[]'
description: Add or override a label description: Add or override a label
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -101,6 +116,7 @@ options:
value_type: string value_type: string
description: Assign a name to the container description: Assign a name to the container
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -108,10 +124,10 @@ options:
- option: no-TTY - option: no-TTY
shorthand: T shorthand: T
value_type: bool value_type: bool
default_value: "false" default_value: "true"
description: | description: 'Disable pseudo-TTY allocation (default: auto-detected).'
Disable pseudo-noTty allocation. By default docker compose run allocates a TTY
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -121,6 +137,7 @@ options:
default_value: "false" default_value: "false"
description: Don't start linked services. description: Don't start linked services.
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -131,6 +148,17 @@ options:
default_value: '[]' default_value: '[]'
description: Publish a container's port(s) to the host. description: Publish a container's port(s) to the host.
deprecated: false deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: quiet-pull
value_type: bool
default_value: "false"
description: Pull without printing progress information.
deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -140,6 +168,7 @@ options:
default_value: "false" default_value: "false"
description: Automatically remove the container when it exits description: Automatically remove the container when it exits
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -150,6 +179,18 @@ options:
description: | description: |
Run command with the service's ports enabled and mapped to the host. Run command with the service's ports enabled and mapped to the host.
deprecated: false deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: tty
shorthand: t
value_type: bool
default_value: "true"
description: Allocate a pseudo-TTY.
deprecated: false
hidden: true
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -160,6 +201,7 @@ options:
description: | description: |
Use the service's network useAliases in the network(s) the container connects to. Use the service's network useAliases in the network(s) the container connects to.
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -169,6 +211,7 @@ options:
value_type: string value_type: string
description: Run as specified username or uid description: Run as specified username or uid
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -179,6 +222,7 @@ options:
default_value: '[]' default_value: '[]'
description: Bind mount a volume. description: Bind mount a volume.
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -188,6 +232,7 @@ options:
value_type: string value_type: string
description: Working directory inside the container description: Working directory inside the container
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false

View File

@ -12,6 +12,7 @@ options:
default_value: "10" default_value: "10"
description: Specify a shutdown timeout in seconds description: Specify a shutdown timeout in seconds
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false

View File

@ -1,5 +1,5 @@
command: docker compose unpause command: docker compose unpause
short: unpause services short: Unpause services
long: Unpauses paused containers of a service. long: Unpauses paused containers of a service.
usage: docker compose unpause [SERVICE...] usage: docker compose unpause [SERVICE...]
pname: docker compose pname: docker compose

View File

@ -27,6 +27,7 @@ options:
description: | description: |
Stops all containers if any container was stopped. Incompatible with -d Stops all containers if any container was stopped. Incompatible with -d
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -36,6 +37,7 @@ options:
default_value: "false" default_value: "false"
description: Recreate dependent containers. Incompatible with --no-recreate. description: Recreate dependent containers. Incompatible with --no-recreate.
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -45,6 +47,7 @@ options:
default_value: '[]' default_value: '[]'
description: Attach to service output. description: Attach to service output.
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -54,6 +57,7 @@ options:
default_value: "false" default_value: "false"
description: Attach to dependent containers. description: Attach to dependent containers.
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -63,6 +67,7 @@ options:
default_value: "false" default_value: "false"
description: Build images before starting containers. description: Build images before starting containers.
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -73,16 +78,7 @@ options:
default_value: "false" default_value: "false"
description: 'Detached mode: Run containers in the background' description: 'Detached mode: Run containers in the background'
deprecated: false deprecated: false
experimental: false hidden: false
experimentalcli: false
kubernetes: false
swarm: false
- option: environment
shorthand: e
value_type: stringArray
default_value: '[]'
description: Environment variables
deprecated: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -92,6 +88,7 @@ options:
description: | description: |
Return the exit code of the selected service container. Implies --abort-on-container-exit Return the exit code of the selected service container. Implies --abort-on-container-exit
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -102,6 +99,7 @@ options:
description: | description: |
Recreate containers even if their configuration and image haven't changed. Recreate containers even if their configuration and image haven't changed.
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -111,6 +109,7 @@ options:
default_value: "false" default_value: "false"
description: Don't build an image, even if it's missing. description: Don't build an image, even if it's missing.
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -120,6 +119,7 @@ options:
default_value: "false" default_value: "false"
description: Produce monochrome output. description: Produce monochrome output.
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -129,6 +129,7 @@ options:
default_value: "false" default_value: "false"
description: Don't start linked services. description: Don't start linked services.
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -138,6 +139,7 @@ options:
default_value: "false" default_value: "false"
description: Don't print prefix in logs. description: Don't print prefix in logs.
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -148,6 +150,7 @@ options:
description: | description: |
If containers already exist, don't recreate them. Incompatible with --force-recreate. If containers already exist, don't recreate them. Incompatible with --force-recreate.
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -157,6 +160,7 @@ options:
default_value: "false" default_value: "false"
description: Don't start the services after creating them. description: Don't start the services after creating them.
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -166,6 +170,7 @@ options:
default_value: "false" default_value: "false"
description: Pull without printing progress information. description: Pull without printing progress information.
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -175,6 +180,7 @@ options:
default_value: "false" default_value: "false"
description: Remove containers for services not defined in the Compose file. description: Remove containers for services not defined in the Compose file.
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -186,6 +192,7 @@ options:
description: | description: |
Recreate anonymous volumes instead of retrieving data from the previous containers. Recreate anonymous volumes instead of retrieving data from the previous containers.
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -196,6 +203,7 @@ options:
description: | description: |
Scale SERVICE to NUM instances. Overrides the `scale` setting in the Compose file if present. Scale SERVICE to NUM instances. Overrides the `scale` setting in the Compose file if present.
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -207,6 +215,17 @@ options:
description: | description: |
Use this timeout in seconds for container shutdown when attached or when containers are already running. Use this timeout in seconds for container shutdown when attached or when containers are already running.
deprecated: false deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: wait
value_type: bool
default_value: "false"
description: Wait for services to be running|healthy. Implies detached mode.
deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false

View File

@ -10,6 +10,7 @@ options:
value_type: string value_type: string
description: 'Format the output. Values: [pretty | json]. (Default: pretty)' description: 'Format the output. Values: [pretty | json]. (Default: pretty)'
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
@ -19,6 +20,7 @@ options:
default_value: "false" default_value: "false"
description: Shows only Compose's version number. description: Shows only Compose's version number.
deprecated: false deprecated: false
hidden: false
experimental: false experimental: false
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false

View File

@ -22,16 +22,23 @@ import (
"path/filepath" "path/filepath"
clidocstool "github.com/docker/cli-docs-tool" clidocstool "github.com/docker/cli-docs-tool"
"github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/cmd/compose" "github.com/docker/compose/v2/cmd/compose"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
func generateCliYaml(opts *options) error { func generateDocs(opts *options) error {
cmd := &cobra.Command{Use: "docker"} dockerCLI, err := command.NewDockerCli()
cmd.AddCommand(compose.RootCommand(nil)) if err != nil {
return err
}
cmd := &cobra.Command{
Use: "docker",
DisableAutoGenTag: true,
}
cmd.AddCommand(compose.RootCommand(dockerCLI, nil))
disableFlagsInUseLine(cmd) disableFlagsInUseLine(cmd)
cmd.DisableAutoGenTag = true
tool, err := clidocstool.New(clidocstool.Options{ tool, err := clidocstool.New(clidocstool.Options{
Root: cmd, Root: cmd,
SourceDir: opts.source, SourceDir: opts.source,
@ -41,7 +48,7 @@ func generateCliYaml(opts *options) error {
if err != nil { if err != nil {
return err return err
} }
return tool.GenYamlTree(cmd) return tool.GenAllTree()
} }
func disableFlagsInUseLine(cmd *cobra.Command) { func disableFlagsInUseLine(cmd *cobra.Command) {
@ -69,12 +76,12 @@ type options struct {
func main() { func main() {
cwd, _ := os.Getwd() cwd, _ := os.Getwd()
opts := &options{ opts := &options{
source: cwd, source: filepath.Join(cwd, "docs", "reference"),
target: filepath.Join(cwd, "docs", "reference"), target: filepath.Join(cwd, "docs", "reference"),
} }
fmt.Printf("Project root: %s\n", opts.source) fmt.Printf("Project root: %s\n", opts.source)
fmt.Printf("Generating yaml files into %s\n", opts.target) fmt.Printf("Generating yaml files into %s\n", opts.target)
if err := generateCliYaml(opts); err != nil { if err := generateDocs(opts); err != nil {
fmt.Fprintf(os.Stderr, "Failed to generate yaml files: %s\n", err.Error()) _, _ = fmt.Fprintf(os.Stderr, "Failed to generate documentation: %s\n", err.Error())
} }
} }

143
go.mod
View File

@ -1,144 +1,153 @@
module github.com/docker/compose/v2 module github.com/docker/compose/v2
go 1.17 go 1.18
require ( require (
github.com/AlecAivazis/survey/v2 v2.3.2 github.com/AlecAivazis/survey/v2 v2.3.5
github.com/buger/goterm v1.0.4 github.com/buger/goterm v1.0.4
github.com/cnabio/cnab-to-oci v0.3.1-beta1 github.com/cnabio/cnab-to-oci v0.3.4
github.com/compose-spec/compose-go v1.1.0 github.com/compose-spec/compose-go v1.2.8
github.com/containerd/console v1.0.3 github.com/containerd/console v1.0.3
github.com/containerd/containerd v1.6.0 github.com/containerd/containerd v1.6.6
github.com/distribution/distribution/v3 v3.0.0-20210316161203-a01c71e2477e github.com/distribution/distribution/v3 v3.0.0-20210316161203-a01c71e2477e
github.com/docker/buildx v0.7.1 github.com/docker/buildx v0.8.2 // when updating, also update the replace rules accordingly
github.com/docker/cli v20.10.12+incompatible github.com/docker/cli v20.10.17+incompatible
github.com/docker/cli-docs-tool v0.2.1 github.com/docker/cli-docs-tool v0.4.0
github.com/docker/docker v20.10.7+incompatible github.com/docker/docker v20.10.17+incompatible
github.com/docker/go-connections v0.4.0 github.com/docker/go-connections v0.4.0
github.com/docker/go-units v0.4.0 github.com/docker/go-units v0.4.0
github.com/golang/mock v1.6.0 github.com/golang/mock v1.6.0
github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/go-version v1.3.0 github.com/hashicorp/go-version v1.6.0
github.com/mattn/go-isatty v0.0.14 github.com/mattn/go-isatty v0.0.14
github.com/mattn/go-shellwords v1.0.12 github.com/mattn/go-shellwords v1.0.12
github.com/moby/buildkit v0.9.1-0.20211019185819-8778943ac3da github.com/moby/buildkit v0.10.1-0.20220403220257-10e6f94bf90d
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6
github.com/morikuni/aec v1.0.0 github.com/morikuni/aec v1.0.0
github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.0.2 github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/sanathkr/go-yaml v0.0.0-20170819195128-ed9d249f429b github.com/sanathkr/go-yaml v0.0.0-20170819195128-ed9d249f429b
github.com/sirupsen/logrus v1.8.1 github.com/sirupsen/logrus v1.8.1
github.com/spf13/cobra v1.3.0 github.com/spf13/cobra v1.5.0
github.com/spf13/pflag v1.0.5 github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.7.0 github.com/stretchr/testify v1.8.0
github.com/theupdateframework/notary v0.7.0
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
gotest.tools v2.2.0+incompatible gotest.tools v2.2.0+incompatible
gotest.tools/v3 v3.1.0 gotest.tools/v3 v3.3.0
) )
require ( require (
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/Masterminds/semver v1.5.0 // indirect github.com/Masterminds/semver v1.5.0 // indirect
github.com/Microsoft/go-winio v0.5.1 // indirect github.com/Microsoft/go-winio v0.5.2 // indirect
github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/cnabio/cnab-go v0.10.0-beta1 // indirect github.com/cnabio/cnab-go v0.23.4 // indirect
github.com/containerd/continuity v0.2.2 // indirect github.com/containerd/continuity v0.2.2 // indirect
github.com/containerd/ttrpc v1.1.0 // indirect
github.com/containerd/typeurl v1.0.2 // indirect github.com/containerd/typeurl v1.0.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/distribution v2.8.0+incompatible // indirect github.com/docker/distribution v2.8.1+incompatible // indirect
github.com/docker/docker-credential-helpers v0.6.4 // indirect github.com/docker/docker-credential-helpers v0.6.4 // indirect
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect
github.com/docker/go-metrics v0.0.1 // indirect github.com/docker/go-metrics v0.0.1 // indirect
github.com/felixge/httpsnoop v1.0.2 // indirect github.com/felixge/httpsnoop v1.0.2 // indirect
github.com/fvbommel/sortorder v1.0.1 // indirect github.com/fvbommel/sortorder v1.0.2 // indirect
github.com/go-logr/logr v1.2.2 // indirect github.com/go-logr/logr v1.2.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
github.com/gofrs/flock v0.8.0 // indirect github.com/gofrs/flock v0.8.0 // indirect
github.com/gogo/googleapis v1.4.0 // indirect github.com/gogo/googleapis v1.4.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-cmp v0.5.6 // indirect github.com/google/go-cmp v0.5.7 // indirect
github.com/google/gofuzz v1.2.0 // indirect github.com/google/gofuzz v1.2.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/gorilla/mux v1.8.0 // indirect github.com/gorilla/mux v1.8.0 // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/imdario/mergo v0.3.12 // indirect github.com/imdario/mergo v0.3.13 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/klauspost/compress v1.13.5 // indirect github.com/klauspost/compress v1.15.1 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-colorable v0.1.12 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
github.com/miekg/pkcs11 v1.0.3 // indirect github.com/miekg/pkcs11 v1.1.1 // indirect
github.com/mitchellh/mapstructure v1.4.3 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/moby/locker v1.0.1 // indirect github.com/moby/locker v1.0.1 // indirect
github.com/moby/sys/mount v0.2.0 // indirect
github.com/moby/sys/mountinfo v0.5.0 // indirect
github.com/moby/sys/signal v0.6.0 // indirect github.com/moby/sys/signal v0.6.0 // indirect
github.com/moby/sys/symlink v0.2.0 // indirect github.com/moby/sys/symlink v0.2.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/opencontainers/runc v1.1.0 // indirect github.com/opencontainers/runc v1.1.2 // indirect
github.com/pelletier/go-toml v1.9.4 // indirect github.com/pelletier/go-toml v1.9.4 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.11.0 // indirect github.com/prometheus/client_golang v1.12.1 // indirect
github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.30.0 // indirect github.com/prometheus/common v0.32.1 // indirect
github.com/prometheus/procfs v0.7.3 // indirect github.com/prometheus/procfs v0.7.3 // indirect
github.com/qri-io/jsonpointer v0.1.0 // indirect github.com/qri-io/jsonpointer v0.1.1 // indirect
github.com/qri-io/jsonschema v0.1.1 // indirect github.com/qri-io/jsonschema v0.2.2-0.20210831022256-780655b2ba0e // indirect
github.com/sergi/go-diff v1.1.0 // indirect github.com/tonistiigi/fsutil v0.0.0-20220315205639-9ed612626da3 // indirect
github.com/theupdateframework/notary v0.6.1 // indirect
github.com/tonistiigi/fsutil v0.0.0-20210818161904-4442383b5028 // indirect
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea // indirect github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea // indirect
github.com/tonistiigi/vt100 v0.0.0-20210615222946-8066bb97264f // indirect github.com/tonistiigi/vt100 v0.0.0-20210615222946-8066bb97264f // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect
go.opentelemetry.io/contrib v0.21.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.29.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.28.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.29.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.21.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.29.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.21.0 // indirect go.opentelemetry.io/otel v1.4.1 // indirect
go.opentelemetry.io/otel v1.3.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.4.1 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.3.0 // indirect go.opentelemetry.io/otel/internal/metric v0.27.0 // indirect
go.opentelemetry.io/otel/internal/metric v0.21.0 // indirect go.opentelemetry.io/otel/metric v0.27.0 // indirect
go.opentelemetry.io/otel/metric v0.21.0 // indirect go.opentelemetry.io/otel/sdk v1.4.1 // indirect
go.opentelemetry.io/otel/sdk v1.3.0 // indirect go.opentelemetry.io/otel/trace v1.4.1 // indirect
go.opentelemetry.io/otel/trace v1.3.0 // indirect go.opentelemetry.io/proto/otlp v0.12.0 // indirect
go.opentelemetry.io/proto/otlp v0.11.0 // indirect golang.org/x/crypto v0.0.0-20220214200702-86341886e292 // indirect
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect golang.org/x/net v0.0.0-20220225172249-27dd8689420f // indirect
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f // indirect
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 // indirect
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
golang.org/x/text v0.3.7 // indirect golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect
google.golang.org/appengine v1.6.7 // indirect google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa // indirect google.golang.org/genproto v0.0.0-20220314164441-57ef72a4c106 // indirect
google.golang.org/grpc v1.43.0 // indirect google.golang.org/grpc v1.45.0 // indirect
google.golang.org/protobuf v1.27.1 // indirect google.golang.org/protobuf v1.27.1 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/apimachinery v0.22.5 // indirect k8s.io/apimachinery v0.24.1 // indirect; see replace for the actual version used
k8s.io/client-go v0.22.5 // indirect k8s.io/client-go v0.24.1 // indirect; see replace for the actual version used
k8s.io/klog/v2 v2.30.0 // indirect k8s.io/klog/v2 v2.60.1 // indirect
k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b // indirect k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.1.2 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect
sigs.k8s.io/yaml v1.2.0 // indirect sigs.k8s.io/yaml v1.2.0 // indirect
) )
// (for buildx) require (
replace ( github.com/cyberphone/json-canonicalization v0.0.0-20210303052042-6bc126869bf4 // indirect
github.com/docker/cli => github.com/docker/cli v20.10.3-0.20210702143511-f782d1355eff+incompatible github.com/zmap/zcrypto v0.0.0-20220605182715-4dfcec6e9a8c // indirect
github.com/docker/docker => github.com/docker/docker v20.10.3-0.20220121014307-40bb9831756f+incompatible github.com/zmap/zlint v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc => github.com/tonistiigi/opentelemetry-go-contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.0.0-20210714055410-d010b05b4939 )
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace => github.com/tonistiigi/opentelemetry-go-contrib/instrumentation/net/http/httptrace/otelhttptrace v0.0.0-20210714055410-d010b05b4939
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp => github.com/tonistiigi/opentelemetry-go-contrib/instrumentation/net/http/otelhttp v0.0.0-20210714055410-d010b05b4939 replace (
github.com/docker/cli => github.com/docker/cli v20.10.3-0.20220309205733-2b52f62e9627+incompatible
github.com/docker/docker => github.com/docker/docker v20.10.3-0.20220309172631-83b51522df43+incompatible
github.com/opencontainers/runc => github.com/opencontainers/runc v1.1.2 // Can be removed on next bump of containerd to > 1.6.4
// For k8s dependencies, we use a replace directive, to prevent them being
// upgraded to the version specified in containerd, which is not relevant to the
// version needed.
// See https://github.com/docker/buildx/pull/948 for details.
// https://github.com/docker/buildx/blob/v0.8.1/go.mod#L62-L64
k8s.io/api => k8s.io/api v0.22.4
k8s.io/apimachinery => k8s.io/apimachinery v0.22.4
k8s.io/client-go => k8s.io/client-go v0.22.4
) )

601
go.sum

File diff suppressed because it is too large Load Diff

View File

@ -19,7 +19,6 @@ package api
import ( import (
"context" "context"
"fmt" "fmt"
"io"
"strings" "strings"
"time" "time"
@ -33,9 +32,9 @@ type Service interface {
// Push executes the equivalent ot a `compose push` // Push executes the equivalent ot a `compose push`
Push(ctx context.Context, project *types.Project, options PushOptions) error Push(ctx context.Context, project *types.Project, options PushOptions) error
// Pull executes the equivalent of a `compose pull` // Pull executes the equivalent of a `compose pull`
Pull(ctx context.Context, project *types.Project, opts PullOptions) error Pull(ctx context.Context, project *types.Project, options PullOptions) error
// Create executes the equivalent to a `compose create` // Create executes the equivalent to a `compose create`
Create(ctx context.Context, project *types.Project, opts CreateOptions) error Create(ctx context.Context, project *types.Project, options CreateOptions) error
// Start executes the equivalent to a `compose start` // Start executes the equivalent to a `compose start`
Start(ctx context.Context, projectName string, options StartOptions) error Start(ctx context.Context, projectName string, options StartOptions) error
// Restart restarts containers // Restart restarts containers
@ -55,25 +54,25 @@ type Service interface {
// Convert translate compose model into backend's native format // Convert translate compose model into backend's native format
Convert(ctx context.Context, project *types.Project, options ConvertOptions) ([]byte, error) Convert(ctx context.Context, project *types.Project, options ConvertOptions) ([]byte, error)
// Kill executes the equivalent to a `compose kill` // Kill executes the equivalent to a `compose kill`
Kill(ctx context.Context, project *types.Project, options KillOptions) error Kill(ctx context.Context, projectName string, options KillOptions) error
// RunOneOffContainer creates a service oneoff container and starts its dependencies // RunOneOffContainer creates a service oneoff container and starts its dependencies
RunOneOffContainer(ctx context.Context, project *types.Project, opts RunOptions) (int, error) RunOneOffContainer(ctx context.Context, project *types.Project, opts RunOptions) (int, error)
// Remove executes the equivalent to a `compose rm` // Remove executes the equivalent to a `compose rm`
Remove(ctx context.Context, project *types.Project, options RemoveOptions) error Remove(ctx context.Context, projectName string, options RemoveOptions) error
// Exec executes a command in a running service container // Exec executes a command in a running service container
Exec(ctx context.Context, project string, opts RunOptions) (int, error) Exec(ctx context.Context, projectName string, options RunOptions) (int, error)
// Copy copies a file/folder between a service container and the local filesystem // Copy copies a file/folder between a service container and the local filesystem
Copy(ctx context.Context, project string, options CopyOptions) error Copy(ctx context.Context, projectName string, options CopyOptions) error
// Pause executes the equivalent to a `compose pause` // Pause executes the equivalent to a `compose pause`
Pause(ctx context.Context, project string, options PauseOptions) error Pause(ctx context.Context, projectName string, options PauseOptions) error
// UnPause executes the equivalent to a `compose unpause` // UnPause executes the equivalent to a `compose unpause`
UnPause(ctx context.Context, project string, options PauseOptions) error UnPause(ctx context.Context, projectName string, options PauseOptions) error
// Top executes the equivalent to a `compose top` // Top executes the equivalent to a `compose top`
Top(ctx context.Context, projectName string, services []string) ([]ContainerProcSummary, error) Top(ctx context.Context, projectName string, services []string) ([]ContainerProcSummary, error)
// Events executes the equivalent to a `compose events` // Events executes the equivalent to a `compose events`
Events(ctx context.Context, project string, options EventsOptions) error Events(ctx context.Context, projectName string, options EventsOptions) error
// Port executes the equivalent to a `compose port` // Port executes the equivalent to a `compose port`
Port(ctx context.Context, project string, service string, port int, options PortOptions) (string, int, error) Port(ctx context.Context, projectName string, service string, port int, options PortOptions) (string, int, error)
// Images executes the equivalent of a `compose images` // Images executes the equivalent of a `compose images`
Images(ctx context.Context, projectName string, options ImagesOptions) ([]ImageSummary, error) Images(ctx context.Context, projectName string, options ImagesOptions) ([]ImageSummary, error)
} }
@ -92,6 +91,8 @@ type BuildOptions struct {
Quiet bool Quiet bool
// Services passed in the command line to be built // Services passed in the command line to be built
Services []string Services []string
// Ssh authentications passed in the command line
SSHs []types.SSHKey
} }
// CreateOptions group options of the Create API // CreateOptions group options of the Create API
@ -116,6 +117,8 @@ type CreateOptions struct {
// StartOptions group options of the Start API // StartOptions group options of the Start API
type StartOptions struct { type StartOptions struct {
// Project is the compose project used to define this app. Might be nil if user ran `start` just with project name
Project *types.Project
// Attach to container and forward logs if not nil // Attach to container and forward logs if not nil
Attach LogConsumer Attach LogConsumer
// AttachTo set the services to attach to // AttachTo set the services to attach to
@ -216,10 +219,8 @@ type RunOptions struct {
Entrypoint []string Entrypoint []string
Detach bool Detach bool
AutoRemove bool AutoRemove bool
Stdin io.ReadCloser
Stdout io.WriteCloser
Stderr io.WriteCloser
Tty bool Tty bool
Interactive bool
WorkingDir string WorkingDir string
User string User string
Environment []string Environment []string

View File

@ -37,9 +37,9 @@ type ServiceProxy struct {
PsFn func(ctx context.Context, projectName string, options PsOptions) ([]ContainerSummary, error) PsFn func(ctx context.Context, projectName string, options PsOptions) ([]ContainerSummary, error)
ListFn func(ctx context.Context, options ListOptions) ([]Stack, error) ListFn func(ctx context.Context, options ListOptions) ([]Stack, error)
ConvertFn func(ctx context.Context, project *types.Project, options ConvertOptions) ([]byte, error) ConvertFn func(ctx context.Context, project *types.Project, options ConvertOptions) ([]byte, error)
KillFn func(ctx context.Context, project *types.Project, options KillOptions) error KillFn func(ctx context.Context, project string, options KillOptions) error
RunOneOffContainerFn func(ctx context.Context, project *types.Project, opts RunOptions) (int, error) RunOneOffContainerFn func(ctx context.Context, project *types.Project, opts RunOptions) (int, error)
RemoveFn func(ctx context.Context, project *types.Project, options RemoveOptions) error RemoveFn func(ctx context.Context, project string, options RemoveOptions) error
ExecFn func(ctx context.Context, project string, opts RunOptions) (int, error) ExecFn func(ctx context.Context, project string, opts RunOptions) (int, error)
CopyFn func(ctx context.Context, project string, options CopyOptions) error CopyFn func(ctx context.Context, project string, options CopyOptions) error
PauseFn func(ctx context.Context, project string, options PauseOptions) error PauseFn func(ctx context.Context, project string, options PauseOptions) error
@ -219,14 +219,11 @@ func (s *ServiceProxy) Convert(ctx context.Context, project *types.Project, opti
} }
// Kill implements Service interface // Kill implements Service interface
func (s *ServiceProxy) Kill(ctx context.Context, project *types.Project, options KillOptions) error { func (s *ServiceProxy) Kill(ctx context.Context, projectName string, options KillOptions) error {
if s.KillFn == nil { if s.KillFn == nil {
return ErrNotImplemented return ErrNotImplemented
} }
for _, i := range s.interceptors { return s.KillFn(ctx, projectName, options)
i(ctx, project)
}
return s.KillFn(ctx, project, options)
} }
// RunOneOffContainer implements Service interface // RunOneOffContainer implements Service interface
@ -241,46 +238,43 @@ func (s *ServiceProxy) RunOneOffContainer(ctx context.Context, project *types.Pr
} }
// Remove implements Service interface // Remove implements Service interface
func (s *ServiceProxy) Remove(ctx context.Context, project *types.Project, options RemoveOptions) error { func (s *ServiceProxy) Remove(ctx context.Context, projectName string, options RemoveOptions) error {
if s.RemoveFn == nil { if s.RemoveFn == nil {
return ErrNotImplemented return ErrNotImplemented
} }
for _, i := range s.interceptors { return s.RemoveFn(ctx, projectName, options)
i(ctx, project)
}
return s.RemoveFn(ctx, project, options)
} }
// Exec implements Service interface // Exec implements Service interface
func (s *ServiceProxy) Exec(ctx context.Context, project string, options RunOptions) (int, error) { func (s *ServiceProxy) Exec(ctx context.Context, projectName string, options RunOptions) (int, error) {
if s.ExecFn == nil { if s.ExecFn == nil {
return 0, ErrNotImplemented return 0, ErrNotImplemented
} }
return s.ExecFn(ctx, project, options) return s.ExecFn(ctx, projectName, options)
} }
// Copy implements Service interface // Copy implements Service interface
func (s *ServiceProxy) Copy(ctx context.Context, project string, options CopyOptions) error { func (s *ServiceProxy) Copy(ctx context.Context, projectName string, options CopyOptions) error {
if s.CopyFn == nil { if s.CopyFn == nil {
return ErrNotImplemented return ErrNotImplemented
} }
return s.CopyFn(ctx, project, options) return s.CopyFn(ctx, projectName, options)
} }
// Pause implements Service interface // Pause implements Service interface
func (s *ServiceProxy) Pause(ctx context.Context, project string, options PauseOptions) error { func (s *ServiceProxy) Pause(ctx context.Context, projectName string, options PauseOptions) error {
if s.PauseFn == nil { if s.PauseFn == nil {
return ErrNotImplemented return ErrNotImplemented
} }
return s.PauseFn(ctx, project, options) return s.PauseFn(ctx, projectName, options)
} }
// UnPause implements Service interface // UnPause implements Service interface
func (s *ServiceProxy) UnPause(ctx context.Context, project string, options PauseOptions) error { func (s *ServiceProxy) UnPause(ctx context.Context, projectName string, options PauseOptions) error {
if s.UnPauseFn == nil { if s.UnPauseFn == nil {
return ErrNotImplemented return ErrNotImplemented
} }
return s.UnPauseFn(ctx, project, options) return s.UnPauseFn(ctx, projectName, options)
} }
// Top implements Service interface // Top implements Service interface
@ -292,19 +286,19 @@ func (s *ServiceProxy) Top(ctx context.Context, project string, services []strin
} }
// Events implements Service interface // Events implements Service interface
func (s *ServiceProxy) Events(ctx context.Context, project string, options EventsOptions) error { func (s *ServiceProxy) Events(ctx context.Context, projectName string, options EventsOptions) error {
if s.EventsFn == nil { if s.EventsFn == nil {
return ErrNotImplemented return ErrNotImplemented
} }
return s.EventsFn(ctx, project, options) return s.EventsFn(ctx, projectName, options)
} }
// Port implements Service interface // Port implements Service interface
func (s *ServiceProxy) Port(ctx context.Context, project string, service string, port int, options PortOptions) (string, int, error) { func (s *ServiceProxy) Port(ctx context.Context, projectName string, service string, port int, options PortOptions) (string, int, error) {
if s.PortFn == nil { if s.PortFn == nil {
return "", 0, ErrNotImplemented return "", 0, ErrNotImplemented
} }
return s.PortFn(ctx, project, service, port, options) return s.PortFn(ctx, projectName, service, port, options)
} }
// Images implements Service interface // Images implements Service interface

View File

@ -48,7 +48,7 @@ func (s *composeService) attach(ctx context.Context, project *types.Project, lis
fmt.Printf("Attaching to %s\n", strings.Join(names, ", ")) fmt.Printf("Attaching to %s\n", strings.Join(names, ", "))
for _, container := range containers { for _, container := range containers {
err := s.attachContainer(ctx, container, listener, project) err := s.attachContainer(ctx, container, listener)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -56,13 +56,9 @@ func (s *composeService) attach(ctx context.Context, project *types.Project, lis
return containers, err return containers, err
} }
func (s *composeService) attachContainer(ctx context.Context, container moby.Container, listener api.ContainerEventListener, project *types.Project) error { func (s *composeService) attachContainer(ctx context.Context, container moby.Container, listener api.ContainerEventListener) error {
serviceName := container.Labels[api.ServiceLabel] serviceName := container.Labels[api.ServiceLabel]
containerName := getContainerNameWithoutProject(container) containerName := getContainerNameWithoutProject(container)
service, err := project.GetService(serviceName)
if err != nil {
return err
}
listener(api.ContainerEvent{ listener(api.ContainerEvent{
Type: api.ContainerEventAttach, Type: api.ContainerEventAttach,
@ -78,7 +74,13 @@ func (s *composeService) attachContainer(ctx context.Context, container moby.Con
Line: line, Line: line,
}) })
}) })
_, _, err = s.attachContainerStreams(ctx, container.ID, service.Tty, nil, w, w)
inspect, err := s.dockerCli.Client().ContainerInspect(ctx, container.ID)
if err != nil {
return err
}
_, _, err = s.attachContainerStreams(ctx, container.ID, inspect.Config.Tty, nil, w, w)
return err return err
} }
@ -137,7 +139,7 @@ func (s *composeService) attachContainerStreams(ctx context.Context, container s
func (s *composeService) getContainerStreams(ctx context.Context, container string) (io.WriteCloser, io.ReadCloser, error) { func (s *composeService) getContainerStreams(ctx context.Context, container string) (io.WriteCloser, io.ReadCloser, error) {
var stdout io.ReadCloser var stdout io.ReadCloser
var stdin io.WriteCloser var stdin io.WriteCloser
cnx, err := s.apiClient.ContainerAttach(ctx, container, moby.ContainerAttachOptions{ cnx, err := s.apiClient().ContainerAttach(ctx, container, moby.ContainerAttachOptions{
Stream: true, Stream: true,
Stdin: true, Stdin: true,
Stdout: true, Stdout: true,
@ -151,7 +153,7 @@ func (s *composeService) getContainerStreams(ctx context.Context, container stri
} }
// Fallback to logs API // Fallback to logs API
logs, err := s.apiClient.ContainerLogs(ctx, container, moby.ContainerLogsOptions{ logs, err := s.apiClient().ContainerLogs(ctx, container, moby.ContainerLogsOptions{
ShowStdout: true, ShowStdout: true,
ShowStderr: true, ShowStderr: true,
Follow: true, Follow: true,

View File

@ -19,7 +19,6 @@ package compose
import ( import (
"context" "context"
"fmt" "fmt"
"os"
"path/filepath" "path/filepath"
"github.com/compose-spec/compose-go/types" "github.com/compose-spec/compose-go/types"
@ -28,11 +27,12 @@ import (
_ "github.com/docker/buildx/driver/docker" // required to get default driver registered _ "github.com/docker/buildx/driver/docker" // required to get default driver registered
"github.com/docker/buildx/util/buildflags" "github.com/docker/buildx/util/buildflags"
xprogress "github.com/docker/buildx/util/progress" xprogress "github.com/docker/buildx/util/progress"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/pkg/urlutil" "github.com/docker/docker/pkg/urlutil"
bclient "github.com/moby/buildkit/client" bclient "github.com/moby/buildkit/client"
"github.com/moby/buildkit/session" "github.com/moby/buildkit/session"
"github.com/moby/buildkit/session/auth/authprovider" "github.com/moby/buildkit/session/auth/authprovider"
"github.com/moby/buildkit/session/secrets/secretsprovider"
"github.com/moby/buildkit/session/sshforward/sshprovider"
specs "github.com/opencontainers/image-spec/specs-go/v1" specs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/api"
@ -64,7 +64,7 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti
if service.Build != nil { if service.Build != nil {
imageName := getImageName(service, project.Name) imageName := getImageName(service, project.Name)
imagesToBuild = append(imagesToBuild, imageName) imagesToBuild = append(imagesToBuild, imageName)
buildOptions, err := s.toBuildOptions(project, service, imageName) buildOptions, err := s.toBuildOptions(project, service, imageName, options.SSHs)
if err != nil { if err != nil {
return err return err
} }
@ -82,7 +82,6 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti
Attrs: map[string]string{"ref": image}, Attrs: map[string]string{"ref": image},
}) })
} }
opts[imageName] = buildOptions opts[imageName] = buildOptions
} }
} }
@ -161,7 +160,7 @@ func (s *composeService) getBuildOptions(project *types.Project, images map[stri
if localImagePresent && service.PullPolicy != types.PullPolicyBuild { if localImagePresent && service.PullPolicy != types.PullPolicyBuild {
continue continue
} }
opt, err := s.toBuildOptions(project, service, imageName) opt, err := s.toBuildOptions(project, service, imageName, []types.SSHKey{})
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -189,37 +188,29 @@ func (s *composeService) getLocalImagesDigests(ctx context.Context, project *typ
for name, info := range imgs { for name, info := range imgs {
images[name] = info.ID images[name] = info.ID
} }
return images, nil
}
func (s *composeService) serverInfo(ctx context.Context) (command.ServerInfo, error) { for _, s := range project.Services {
ping, err := s.apiClient.Ping(ctx) imgName := getImageName(s, project.Name)
if err != nil { digest, ok := images[imgName]
return command.ServerInfo{}, err if ok {
s.CustomLabels[api.ImageDigestLabel] = digest
}
} }
serverInfo := command.ServerInfo{
HasExperimental: ping.Experimental, return images, nil
OSType: ping.OSType,
BuildkitVersion: ping.BuilderVersion,
}
return serverInfo, err
} }
func (s *composeService) doBuild(ctx context.Context, project *types.Project, opts map[string]build.Options, mode string) (map[string]string, error) { func (s *composeService) doBuild(ctx context.Context, project *types.Project, opts map[string]build.Options, mode string) (map[string]string, error) {
if len(opts) == 0 { if len(opts) == 0 {
return nil, nil return nil, nil
} }
serverInfo, err := s.serverInfo(ctx) if buildkitEnabled, err := s.dockerCli.BuildKitEnabled(); err != nil || !buildkitEnabled {
if err != nil { return s.doBuildClassic(ctx, project, opts)
return nil, err
}
if buildkitEnabled, err := command.BuildKitEnabled(serverInfo); err != nil || !buildkitEnabled {
return s.doBuildClassic(ctx, opts)
} }
return s.doBuildBuildkit(ctx, project, opts, mode) return s.doBuildBuildkit(ctx, project, opts, mode)
} }
func (s *composeService) toBuildOptions(project *types.Project, service types.ServiceConfig, imageTag string) (build.Options, error) { func (s *composeService) toBuildOptions(project *types.Project, service types.ServiceConfig, imageTag string, sshKeys []types.SSHKey) (build.Options, error) {
var tags []string var tags []string
tags = append(tags, imageTag) tags = append(tags, imageTag)
@ -244,11 +235,59 @@ func (s *composeService) toBuildOptions(project *types.Project, service types.Se
plats = append(plats, p) plats = append(plats, p)
} }
cacheFrom, err := buildflags.ParseCacheEntry(service.Build.CacheFrom)
if err != nil {
return build.Options{}, err
}
cacheTo, err := buildflags.ParseCacheEntry(service.Build.CacheTo)
if err != nil {
return build.Options{}, err
}
sessionConfig := []session.Attachable{
authprovider.NewDockerAuthProvider(s.stderr()),
}
if len(sshKeys) > 0 || len(service.Build.SSH) > 0 {
sshAgentProvider, err := sshAgentProvider(append(service.Build.SSH, sshKeys...))
if err != nil {
return build.Options{}, err
}
sessionConfig = append(sessionConfig, sshAgentProvider)
}
if len(service.Build.Secrets) > 0 {
var sources []secretsprovider.Source
for _, secret := range service.Build.Secrets {
config := project.Secrets[secret.Source]
if config.File == "" {
return build.Options{}, fmt.Errorf("build.secrets only supports file-based secrets: %q", secret.Source)
}
sources = append(sources, secretsprovider.Source{
ID: secret.Source,
FilePath: config.File,
})
}
store, err := secretsprovider.NewStore(sources)
if err != nil {
return build.Options{}, err
}
p := secretsprovider.NewSecretProvider(store)
sessionConfig = append(sessionConfig, p)
}
if len(service.Build.Tags) > 0 {
tags = append(tags, service.Build.Tags...)
}
return build.Options{ return build.Options{
Inputs: build.Inputs{ Inputs: build.Inputs{
ContextPath: service.Build.Context, ContextPath: service.Build.Context,
DockerfilePath: dockerFilePath(service.Build.Context, service.Build.Dockerfile), DockerfilePath: dockerFilePath(service.Build.Context, service.Build.Dockerfile),
}, },
CacheFrom: cacheFrom,
CacheTo: cacheTo,
NoCache: service.Build.NoCache,
Pull: service.Build.Pull,
BuildArgs: buildArgs, BuildArgs: buildArgs,
Tags: tags, Tags: tags,
Target: service.Build.Target, Target: service.Build.Target,
@ -256,10 +295,8 @@ func (s *composeService) toBuildOptions(project *types.Project, service types.Se
Platforms: plats, Platforms: plats,
Labels: service.Build.Labels, Labels: service.Build.Labels,
NetworkMode: service.Build.Network, NetworkMode: service.Build.Network,
ExtraHosts: service.Build.ExtraHosts, ExtraHosts: service.Build.ExtraHosts.AsList(),
Session: []session.Attachable{ Session: sessionConfig,
authprovider.NewDockerAuthProvider(os.Stderr),
},
}, nil }, nil
} }
@ -293,3 +330,14 @@ func dockerFilePath(context string, dockerfile string) string {
} }
return filepath.Join(context, dockerfile) return filepath.Join(context, dockerfile)
} }
func sshAgentProvider(sshKeys types.SSHConfig) (session.Attachable, error) {
sshConfig := make([]sshprovider.AgentConfig, 0, len(sshKeys))
for _, sshKey := range sshKeys {
sshConfig = append(sshConfig, sshprovider.AgentConfig{
ID: sshKey.ID,
Paths: []string{sshKey.Path},
})
}
return sshprovider.NewSSHAgentProvider(sshConfig)
}

View File

@ -29,7 +29,7 @@ import (
func (s *composeService) doBuildBuildkit(ctx context.Context, project *types.Project, opts map[string]build.Options, mode string) (map[string]string, error) { func (s *composeService) doBuildBuildkit(ctx context.Context, project *types.Project, opts map[string]build.Options, mode string) (map[string]string, error) {
const drivername = "default" const drivername = "default"
d, err := driver.GetDriver(ctx, drivername, nil, s.apiClient, s.configFile, nil, nil, nil, nil, nil, project.WorkingDir) d, err := driver.GetDriver(ctx, drivername, nil, s.apiClient(), s.configFile(), nil, nil, nil, nil, nil, project.WorkingDir)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -45,10 +45,10 @@ func (s *composeService) doBuildBuildkit(ctx context.Context, project *types.Pro
// build and will lock // build and will lock
progressCtx, cancel := context.WithCancel(context.Background()) progressCtx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
w := xprogress.NewPrinter(progressCtx, os.Stdout, mode) w := xprogress.NewPrinter(progressCtx, s.stdout(), os.Stdout, mode)
// We rely on buildx "docker" builder integrated in docker engine, so don't need a DockerAPI here // We rely on buildx "docker" builder integrated in docker engine, so don't need a DockerAPI here
response, err := build.Build(ctx, driverInfo, opts, nil, filepath.Dir(s.configFile.Filename), w) response, err := build.Build(ctx, driverInfo, opts, nil, filepath.Dir(s.configFile().Filename), w)
errW := w.Wait() errW := w.Wait()
if err == nil { if err == nil {
err = errW err = errW

View File

@ -21,12 +21,12 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings" "strings"
"github.com/compose-spec/compose-go/types"
buildx "github.com/docker/buildx/build" buildx "github.com/docker/buildx/build"
"github.com/docker/cli/cli/command/image/build" "github.com/docker/cli/cli/command/image/build"
dockertypes "github.com/docker/docker/api/types" dockertypes "github.com/docker/docker/api/types"
@ -41,15 +41,24 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
) )
func (s *composeService) doBuildClassic(ctx context.Context, opts map[string]buildx.Options) (map[string]string, error) { func (s *composeService) doBuildClassic(ctx context.Context, project *types.Project, opts map[string]buildx.Options) (map[string]string, error) {
var nameDigests = make(map[string]string) var nameDigests = make(map[string]string)
var errs error var errs error
for name, o := range opts { err := project.WithServices(nil, func(service types.ServiceConfig) error {
imageName := getImageName(service, project.Name)
o, ok := opts[imageName]
if !ok {
return nil
}
digest, err := s.doBuildClassicSimpleImage(ctx, o) digest, err := s.doBuildClassicSimpleImage(ctx, o)
if err != nil { if err != nil {
errs = multierror.Append(errs, err).ErrorOrNil() errs = multierror.Append(errs, err).ErrorOrNil()
} }
nameDigests[name] = digest nameDigests[imageName] = digest
return nil
})
if err != nil {
return nil, err
} }
return nameDigests, errs return nameDigests, errs
@ -69,8 +78,8 @@ func (s *composeService) doBuildClassicSimpleImage(ctx context.Context, options
dockerfileName := options.Inputs.DockerfilePath dockerfileName := options.Inputs.DockerfilePath
specifiedContext := options.Inputs.ContextPath specifiedContext := options.Inputs.ContextPath
progBuff := os.Stdout progBuff := s.stdout()
buildBuff := os.Stdout buildBuff := s.stdout()
if options.ImageIDFile != "" { if options.ImageIDFile != "" {
// Avoid leaving a stale file if we eventually fail // Avoid leaving a stale file if we eventually fail
if err := os.Remove(options.ImageIDFile); err != nil && !os.IsNotExist(err) { if err := os.Remove(options.ImageIDFile); err != nil && !os.IsNotExist(err) {
@ -143,19 +152,10 @@ func (s *composeService) doBuildClassicSimpleImage(ctx context.Context, options
return "", err return "", err
} }
// if up to this point nothing has set the context then we must have another
// way for sending it(streaming) and set the context to the Dockerfile
if dockerfileCtx != nil && buildCtx == nil {
buildCtx = dockerfileCtx
}
progressOutput := streamformatter.NewProgressOutput(progBuff) progressOutput := streamformatter.NewProgressOutput(progBuff)
var body io.Reader body := progress.NewProgressReader(buildCtx, progressOutput, 0, "", "Sending build context to Docker daemon")
if buildCtx != nil {
body = progress.NewProgressReader(buildCtx, progressOutput, 0, "", "Sending build context to Docker daemon")
}
configFile := s.configFile configFile := s.configFile()
creds, err := configFile.GetAllCredentials() creds, err := configFile.GetAllCredentials()
if err != nil { if err != nil {
return "", err return "", err
@ -171,7 +171,7 @@ func (s *composeService) doBuildClassicSimpleImage(ctx context.Context, options
ctx, cancel := context.WithCancel(ctx) ctx, cancel := context.WithCancel(ctx)
defer cancel() defer cancel()
response, err := s.apiClient.ImageBuild(ctx, body, buildOptions) response, err := s.apiClient().ImageBuild(ctx, body, buildOptions)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -181,13 +181,13 @@ func (s *composeService) doBuildClassicSimpleImage(ctx context.Context, options
aux := func(msg jsonmessage.JSONMessage) { aux := func(msg jsonmessage.JSONMessage) {
var result dockertypes.BuildResult var result dockertypes.BuildResult
if err := json.Unmarshal(*msg.Aux, &result); err != nil { if err := json.Unmarshal(*msg.Aux, &result); err != nil {
fmt.Fprintf(os.Stderr, "Failed to parse aux message: %s", err) fmt.Fprintf(s.stderr(), "Failed to parse aux message: %s", err)
} else { } else {
imageID = result.ID imageID = result.ID
} }
} }
err = jsonmessage.DisplayJSONMessagesStream(response.Body, buildBuff, progBuff.Fd(), true, aux) err = jsonmessage.DisplayJSONMessagesStream(response.Body, buildBuff, progBuff.FD(), true, aux)
if err != nil { if err != nil {
if jerr, ok := err.(*jsonmessage.JSONError); ok { if jerr, ok := err.(*jsonmessage.JSONError); ok {
// If no error code is set, default to 1 // If no error code is set, default to 1
@ -203,7 +203,7 @@ func (s *composeService) doBuildClassicSimpleImage(ctx context.Context, options
// daemon isn't running Windows. // daemon isn't running Windows.
if response.OSType != "windows" && runtime.GOOS == "windows" { if response.OSType != "windows" && runtime.GOOS == "windows" {
// if response.OSType != "windows" && runtime.GOOS == "windows" && !options.quiet { // if response.OSType != "windows" && runtime.GOOS == "windows" && !options.quiet {
fmt.Fprintln(os.Stdout, "SECURITY WARNING: You are building a Docker "+ fmt.Fprintln(s.stdout(), "SECURITY WARNING: You are building a Docker "+
"image from Windows against a non-Windows Docker host. All files and "+ "image from Windows against a non-Windows Docker host. All files and "+
"directories added to build context will have '-rwxr-xr-x' permissions. "+ "directories added to build context will have '-rwxr-xr-x' permissions. "+
"It is recommended to double check and reset permissions for sensitive "+ "It is recommended to double check and reset permissions for sensitive "+
@ -214,7 +214,7 @@ func (s *composeService) doBuildClassicSimpleImage(ctx context.Context, options
if imageID == "" { if imageID == "" {
return "", errors.Errorf("Server did not provide an image ID. Cannot write %s", options.ImageIDFile) return "", errors.Errorf("Server did not provide an image ID. Cannot write %s", options.ImageIDFile)
} }
if err := ioutil.WriteFile(options.ImageIDFile, []byte(imageID), 0666); err != nil { if err := os.WriteFile(options.ImageIDFile, []byte(imageID), 0666); err != nil {
return "", err return "", err
} }
} }

View File

@ -21,15 +21,18 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"strings" "strings"
"github.com/docker/compose/v2/pkg/api"
"github.com/pkg/errors"
"github.com/compose-spec/compose-go/types" "github.com/compose-spec/compose-go/types"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/streams"
"github.com/docker/compose/v2/pkg/api"
moby "github.com/docker/docker/api/types" moby "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/pkg/errors"
"github.com/sanathkr/go-yaml" "github.com/sanathkr/go-yaml"
) )
@ -37,19 +40,41 @@ import (
var Separator = "-" var Separator = "-"
// NewComposeService create a local implementation of the compose.Service API // NewComposeService create a local implementation of the compose.Service API
func NewComposeService(apiClient client.APIClient, configFile *configfile.ConfigFile) api.Service { func NewComposeService(dockerCli command.Cli) api.Service {
return &composeService{ return &composeService{
apiClient: apiClient, dockerCli: dockerCli,
configFile: configFile,
} }
} }
type composeService struct { type composeService struct {
apiClient client.APIClient dockerCli command.Cli
configFile *configfile.ConfigFile }
func (s *composeService) apiClient() client.APIClient {
return s.dockerCli.Client()
}
func (s *composeService) configFile() *configfile.ConfigFile {
return s.dockerCli.ConfigFile()
}
func (s *composeService) stdout() *streams.Out {
return s.dockerCli.Out()
}
func (s *composeService) stdin() *streams.In {
return s.dockerCli.In()
}
func (s *composeService) stderr() io.Writer {
return s.dockerCli.Err()
} }
func getCanonicalContainerName(c moby.Container) string { func getCanonicalContainerName(c moby.Container) string {
if len(c.Names) == 0 {
// corner case, sometime happens on removal. return short ID as a safeguard value
return c.ID[:12]
}
// Names return container canonical name /foo + link aliases /linked_by/foo // Names return container canonical name /foo + link aliases /linked_by/foo
for _, name := range c.Names { for _, name := range c.Names {
if strings.LastIndex(name, "/") == 0 { if strings.LastIndex(name, "/") == 0 {
@ -100,7 +125,7 @@ func (s *composeService) projectFromName(containers Containers, projectName stri
Name: projectName, Name: projectName,
} }
if len(containers) == 0 { if len(containers) == 0 {
return project, errors.New("no such project: " + projectName) return project, errors.Wrap(api.ErrNotFound, fmt.Sprintf("no container found for project %q", projectName))
} }
set := map[string]*types.ServiceConfig{} set := map[string]*types.ServiceConfig{}
for _, c := range containers { for _, c := range containers {
@ -140,7 +165,7 @@ SERVICES:
continue SERVICES continue SERVICES
} }
} }
return project, errors.New("no such service: " + qs) return project, errors.Wrapf(api.ErrNotFound, "no such service: %q", qs)
} }
err := project.ForServices(services) err := project.ForServices(services)
if err != nil { if err != nil {
@ -149,3 +174,59 @@ SERVICES:
return project, nil return project, nil
} }
// actualState list resources labelled by projectName to rebuild compose project model
func (s *composeService) actualState(ctx context.Context, projectName string, services []string) (Containers, *types.Project, error) {
var containers Containers
// don't filter containers by options.Services so projectFromName can rebuild project with all existing resources
containers, err := s.getContainers(ctx, projectName, oneOffInclude, true)
if err != nil {
return nil, nil, err
}
project, err := s.projectFromName(containers, projectName, services...)
if err != nil && !api.IsNotFoundError(err) {
return nil, nil, err
}
if len(services) > 0 {
containers = containers.filter(isService(services...))
}
return containers, project, nil
}
func (s *composeService) actualVolumes(ctx context.Context, projectName string) (types.Volumes, error) {
volumes, err := s.apiClient().VolumeList(ctx, filters.NewArgs(projectFilter(projectName)))
if err != nil {
return nil, err
}
actual := types.Volumes{}
for _, vol := range volumes.Volumes {
actual[vol.Labels[api.VolumeLabel]] = types.VolumeConfig{
Name: vol.Name,
Driver: vol.Driver,
Labels: vol.Labels,
}
}
return actual, nil
}
func (s *composeService) actualNetworks(ctx context.Context, projectName string) (types.Networks, error) {
networks, err := s.apiClient().NetworkList(ctx, moby.NetworkListOptions{
Filters: filters.NewArgs(projectFilter(projectName)),
})
if err != nil {
return nil, err
}
actual := types.Networks{}
for _, net := range networks {
actual[net.Labels[api.NetworkLabel]] = types.NetworkConfig{
Name: net.Name,
Driver: net.Driver,
Labels: net.Labels,
}
}
return actual, nil
}

View File

@ -18,14 +18,13 @@ package compose
import ( import (
"context" "context"
"fmt"
"sort" "sort"
"strconv"
moby "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/utils" "github.com/docker/compose/v2/pkg/utils"
moby "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
) )
// Containers is a set of moby Container // Containers is a set of moby Container
@ -41,18 +40,8 @@ const (
func (s *composeService) getContainers(ctx context.Context, project string, oneOff oneOff, stopped bool, selectedServices ...string) (Containers, error) { func (s *composeService) getContainers(ctx context.Context, project string, oneOff oneOff, stopped bool, selectedServices ...string) (Containers, error) {
var containers Containers var containers Containers
f := []filters.KeyValuePair{projectFilter(project)} f := getDefaultFilters(project, oneOff, selectedServices...)
if len(selectedServices) == 1 { containers, err := s.apiClient().ContainerList(ctx, moby.ContainerListOptions{
f = append(f, serviceFilter(selectedServices[0]))
}
switch oneOff {
case oneOffOnly:
f = append(f, oneOffFilter(true))
case oneOffExclude:
f = append(f, oneOffFilter(false))
case oneOffInclude:
}
containers, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{
Filters: filters.NewArgs(f...), Filters: filters.NewArgs(f...),
All: stopped, All: stopped,
}) })
@ -65,6 +54,40 @@ func (s *composeService) getContainers(ctx context.Context, project string, oneO
return containers, nil return containers, nil
} }
func getDefaultFilters(projectName string, oneOff oneOff, selectedServices ...string) []filters.KeyValuePair {
f := []filters.KeyValuePair{projectFilter(projectName)}
if len(selectedServices) == 1 {
f = append(f, serviceFilter(selectedServices[0]))
}
switch oneOff {
case oneOffOnly:
f = append(f, oneOffFilter(true))
case oneOffExclude:
f = append(f, oneOffFilter(false))
case oneOffInclude:
}
return f
}
func (s *composeService) getSpecifiedContainer(ctx context.Context, projectName string, oneOff oneOff, stopped bool, serviceName string, containerIndex int) (moby.Container, error) {
defaultFilters := getDefaultFilters(projectName, oneOff, serviceName)
defaultFilters = append(defaultFilters, containerNumberFilter(containerIndex))
containers, err := s.apiClient().ContainerList(ctx, moby.ContainerListOptions{
Filters: filters.NewArgs(
defaultFilters...,
),
All: stopped,
})
if err != nil {
return moby.Container{}, err
}
if len(containers) < 1 {
return moby.Container{}, fmt.Errorf("service %q is not running container #%d", serviceName, containerIndex)
}
container := containers[0]
return container, nil
}
// containerPredicate define a predicate we want container to satisfy for filtering operations // containerPredicate define a predicate we want container to satisfy for filtering operations
type containerPredicate func(c moby.Container) bool type containerPredicate func(c moby.Container) bool
@ -87,14 +110,6 @@ func isNotOneOff(c moby.Container) bool {
return !ok || v == "False" return !ok || v == "False"
} }
func indexed(index int) containerPredicate {
return func(c moby.Container) bool {
number := c.Labels[api.ContainerNumberLabel]
idx, err := strconv.Atoi(number)
return err == nil && index == idx
}
}
// filter return Containers with elements to match predicate // filter return Containers with elements to match predicate
func (containers Containers) filter(predicate containerPredicate) Containers { func (containers Containers) filter(predicate containerPredicate) Containers {
var filtered Containers var filtered Containers

View File

@ -180,26 +180,20 @@ func (c *convergence) ensureService(ctx context.Context, project *types.Project,
// Scale Down // Scale Down
container := container container := container
eg.Go(func() error { eg.Go(func() error {
err := c.service.apiClient.ContainerStop(ctx, container.ID, timeout) err := c.service.apiClient().ContainerStop(ctx, container.ID, timeout)
if err != nil { if err != nil {
return err return err
} }
return c.service.apiClient.ContainerRemove(ctx, container.ID, moby.ContainerRemoveOptions{}) return c.service.apiClient().ContainerRemove(ctx, container.ID, moby.ContainerRemoveOptions{})
}) })
continue continue
} }
if recreate == api.RecreateNever { mustRecreate, err := mustRecreate(service, container, recreate)
continue
}
// Re-create diverged containers
configHash, err := ServiceHash(service)
if err != nil { if err != nil {
return err return err
} }
name := getContainerProgressName(container) if mustRecreate {
diverged := container.Labels[api.ConfigHashLabel] != configHash
if diverged || recreate == api.RecreateForce || service.Extensions[extLifecycle] == forceRecreate {
i, container := i, container i, container := i, container
eg.Go(func() error { eg.Go(func() error {
recreated, err := c.service.recreateContainer(ctx, project, service, container, inherit, timeout) recreated, err := c.service.recreateContainer(ctx, project, service, container, inherit, timeout)
@ -211,6 +205,7 @@ func (c *convergence) ensureService(ctx context.Context, project *types.Project,
// Enforce non-diverged containers are running // Enforce non-diverged containers are running
w := progress.ContextWriter(ctx) w := progress.ContextWriter(ctx)
name := getContainerProgressName(container)
switch container.State { switch container.State {
case ContainerRunning: case ContainerRunning:
w.Event(progress.RunningEvent(name)) w.Event(progress.RunningEvent(name))
@ -249,6 +244,22 @@ func (c *convergence) ensureService(ctx context.Context, project *types.Project,
return err return err
} }
func mustRecreate(expected types.ServiceConfig, actual moby.Container, policy string) (bool, error) {
if policy == api.RecreateNever {
return false, nil
}
if policy == api.RecreateForce || expected.Extensions[extLifecycle] == forceRecreate {
return true, nil
}
configHash, err := ServiceHash(expected)
if err != nil {
return false, err
}
configChanged := actual.Labels[api.ConfigHashLabel] != configHash
imageUpdated := actual.Labels[api.ImageDigestLabel] != expected.CustomLabels[api.ImageDigestLabel]
return configChanged || imageUpdated, nil
}
func getContainerName(projectName string, service types.ServiceConfig, number int) string { func getContainerName(projectName string, service types.ServiceConfig, number int) string {
name := strings.Join([]string{projectName, service.Name, strconv.Itoa(number)}, Separator) name := strings.Join([]string{projectName, service.Name, strconv.Itoa(number)}, Separator)
if service.ContainerName != "" { if service.ContainerName != "" {
@ -395,13 +406,13 @@ func (s *composeService) recreateContainer(ctx context.Context, project *types.P
var created moby.Container var created moby.Container
w := progress.ContextWriter(ctx) w := progress.ContextWriter(ctx)
w.Event(progress.NewEvent(getContainerProgressName(replaced), progress.Working, "Recreate")) w.Event(progress.NewEvent(getContainerProgressName(replaced), progress.Working, "Recreate"))
err := s.apiClient.ContainerStop(ctx, replaced.ID, timeout) err := s.apiClient().ContainerStop(ctx, replaced.ID, timeout)
if err != nil { if err != nil {
return created, err return created, err
} }
name := getCanonicalContainerName(replaced) name := getCanonicalContainerName(replaced)
tmpName := fmt.Sprintf("%s_%s", replaced.ID[:12], name) tmpName := fmt.Sprintf("%s_%s", replaced.ID[:12], name)
err = s.apiClient.ContainerRename(ctx, replaced.ID, tmpName) err = s.apiClient().ContainerRename(ctx, replaced.ID, tmpName)
if err != nil { if err != nil {
return created, err return created, err
} }
@ -419,7 +430,7 @@ func (s *composeService) recreateContainer(ctx context.Context, project *types.P
if err != nil { if err != nil {
return created, err return created, err
} }
err = s.apiClient.ContainerRemove(ctx, replaced.ID, moby.ContainerRemoveOptions{}) err = s.apiClient().ContainerRemove(ctx, replaced.ID, moby.ContainerRemoveOptions{})
if err != nil { if err != nil {
return created, err return created, err
} }
@ -444,7 +455,7 @@ func setDependentLifecycle(project *types.Project, service string, strategy stri
func (s *composeService) startContainer(ctx context.Context, container moby.Container) error { func (s *composeService) startContainer(ctx context.Context, container moby.Container) error {
w := progress.ContextWriter(ctx) w := progress.ContextWriter(ctx)
w.Event(progress.NewEvent(getContainerProgressName(container), progress.Working, "Restart")) w.Event(progress.NewEvent(getContainerProgressName(container), progress.Working, "Restart"))
err := s.apiClient.ContainerStart(ctx, container.ID, moby.ContainerStartOptions{}) err := s.apiClient().ContainerStart(ctx, container.ID, moby.ContainerStartOptions{})
if err != nil { if err != nil {
return err return err
} }
@ -468,11 +479,11 @@ func (s *composeService) createMobyContainer(ctx context.Context, project *types
} }
plat = &p plat = &p
} }
response, err := s.apiClient.ContainerCreate(ctx, containerConfig, hostConfig, networkingConfig, plat, name) response, err := s.apiClient().ContainerCreate(ctx, containerConfig, hostConfig, networkingConfig, plat, name)
if err != nil { if err != nil {
return created, err return created, err
} }
inspectedContainer, err := s.apiClient.ContainerInspect(ctx, response.ID) inspectedContainer, err := s.apiClient().ContainerInspect(ctx, response.ID)
if err != nil { if err != nil {
return created, err return created, err
} }
@ -502,7 +513,7 @@ func (s *composeService) createMobyContainer(ctx context.Context, project *types
if shortIDAliasExists(created.ID, val.Aliases...) { if shortIDAliasExists(created.ID, val.Aliases...) {
continue continue
} }
err = s.apiClient.NetworkDisconnect(ctx, netwrk.Name, created.ID, false) err = s.apiClient().NetworkDisconnect(ctx, netwrk.Name, created.ID, false)
if err != nil { if err != nil {
return created, err return created, err
} }
@ -512,6 +523,8 @@ func (s *composeService) createMobyContainer(ctx context.Context, project *types
return created, err return created, err
} }
} }
err = s.injectSecrets(ctx, project, service, created.ID)
return created, err return created, err
} }
@ -596,7 +609,7 @@ func (s *composeService) connectContainerToNetwork(ctx context.Context, id strin
IPv6Address: ipv6Address, IPv6Address: ipv6Address,
} }
} }
err := s.apiClient.NetworkConnect(ctx, netwrk, id, &network.EndpointSettings{ err := s.apiClient().NetworkConnect(ctx, netwrk, id, &network.EndpointSettings{
Aliases: aliases, Aliases: aliases,
IPAddress: ipv4Address, IPAddress: ipv4Address,
GlobalIPv6Address: ipv6Address, GlobalIPv6Address: ipv6Address,
@ -619,7 +632,7 @@ func (s *composeService) isServiceHealthy(ctx context.Context, project *types.Pr
return false, nil return false, nil
} }
for _, c := range containers { for _, c := range containers {
container, err := s.apiClient.ContainerInspect(ctx, c.ID) container, err := s.apiClient().ContainerInspect(ctx, c.ID)
if err != nil { if err != nil {
return false, err return false, err
} }
@ -651,7 +664,7 @@ func (s *composeService) isServiceCompleted(ctx context.Context, project *types.
return false, 0, err return false, 0, err
} }
for _, c := range containers { for _, c := range containers {
container, err := s.apiClient.ContainerInspect(ctx, c.ID) container, err := s.apiClient().ContainerInspect(ctx, c.ID)
if err != nil { if err != nil {
return false, 0, err return false, 0, err
} }
@ -671,7 +684,7 @@ func (s *composeService) startService(ctx context.Context, project *types.Projec
if err != nil { if err != nil {
return err return err
} }
containers, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{ containers, err := s.apiClient().ContainerList(ctx, moby.ContainerListOptions{
Filters: filters.NewArgs( Filters: filters.NewArgs(
projectFilter(project.Name), projectFilter(project.Name),
serviceFilter(service.Name), serviceFilter(service.Name),
@ -700,7 +713,7 @@ func (s *composeService) startService(ctx context.Context, project *types.Projec
eg.Go(func() error { eg.Go(func() error {
eventName := getContainerProgressName(container) eventName := getContainerProgressName(container)
w.Event(progress.StartingEvent(eventName)) w.Event(progress.StartingEvent(eventName))
err := s.apiClient.ContainerStart(ctx, container.ID, moby.ContainerStartOptions{}) err := s.apiClient().ContainerStart(ctx, container.ID, moby.ContainerStartOptions{})
if err == nil { if err == nil {
w.Event(progress.StartedEvent(eventName)) w.Event(progress.StartedEvent(eventName))
} }

View File

@ -74,8 +74,11 @@ func TestServiceLinks(t *testing.T) {
t.Run("service links default", func(t *testing.T) { t.Run("service links default", func(t *testing.T) {
mockCtrl := gomock.NewController(t) mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish() defer mockCtrl.Finish()
apiClient := mocks.NewMockAPIClient(mockCtrl) apiClient := mocks.NewMockAPIClient(mockCtrl)
tested.apiClient = apiClient cli := mocks.NewMockCli(mockCtrl)
tested.dockerCli = cli
cli.EXPECT().Client().Return(apiClient).AnyTimes()
s.Links = []string{"db"} s.Links = []string{"db"}
@ -95,7 +98,9 @@ func TestServiceLinks(t *testing.T) {
mockCtrl := gomock.NewController(t) mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish() defer mockCtrl.Finish()
apiClient := mocks.NewMockAPIClient(mockCtrl) apiClient := mocks.NewMockAPIClient(mockCtrl)
tested.apiClient = apiClient cli := mocks.NewMockCli(mockCtrl)
tested.dockerCli = cli
cli.EXPECT().Client().Return(apiClient).AnyTimes()
s.Links = []string{"db:db"} s.Links = []string{"db:db"}
@ -115,7 +120,9 @@ func TestServiceLinks(t *testing.T) {
mockCtrl := gomock.NewController(t) mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish() defer mockCtrl.Finish()
apiClient := mocks.NewMockAPIClient(mockCtrl) apiClient := mocks.NewMockAPIClient(mockCtrl)
tested.apiClient = apiClient cli := mocks.NewMockCli(mockCtrl)
tested.dockerCli = cli
cli.EXPECT().Client().Return(apiClient).AnyTimes()
s.Links = []string{"db:dbname"} s.Links = []string{"db:dbname"}
@ -135,7 +142,9 @@ func TestServiceLinks(t *testing.T) {
mockCtrl := gomock.NewController(t) mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish() defer mockCtrl.Finish()
apiClient := mocks.NewMockAPIClient(mockCtrl) apiClient := mocks.NewMockAPIClient(mockCtrl)
tested.apiClient = apiClient cli := mocks.NewMockCli(mockCtrl)
tested.dockerCli = cli
cli.EXPECT().Client().Return(apiClient).AnyTimes()
s.Links = []string{"db:dbname"} s.Links = []string{"db:dbname"}
s.ExternalLinks = []string{"db1:db2"} s.ExternalLinks = []string{"db1:db2"}
@ -159,7 +168,9 @@ func TestServiceLinks(t *testing.T) {
mockCtrl := gomock.NewController(t) mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish() defer mockCtrl.Finish()
apiClient := mocks.NewMockAPIClient(mockCtrl) apiClient := mocks.NewMockAPIClient(mockCtrl)
tested.apiClient = apiClient cli := mocks.NewMockCli(mockCtrl)
tested.dockerCli = cli
cli.EXPECT().Client().Return(apiClient).AnyTimes()
s.Links = []string{} s.Links = []string{}
s.ExternalLinks = []string{} s.ExternalLinks = []string{}
@ -189,8 +200,11 @@ func TestServiceLinks(t *testing.T) {
func TestWaitDependencies(t *testing.T) { func TestWaitDependencies(t *testing.T) {
mockCtrl := gomock.NewController(t) mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish() defer mockCtrl.Finish()
api := mocks.NewMockAPIClient(mockCtrl)
tested.apiClient = api apiClient := mocks.NewMockAPIClient(mockCtrl)
cli := mocks.NewMockCli(mockCtrl)
tested.dockerCli = cli
cli.EXPECT().Client().Return(apiClient).AnyTimes()
t.Run("should skip dependencies with scale 0", func(t *testing.T) { t.Run("should skip dependencies with scale 0", func(t *testing.T) {
dbService := types.ServiceConfig{Name: "db", Scale: 0} dbService := types.ServiceConfig{Name: "db", Scale: 0}

View File

@ -42,59 +42,80 @@ const (
acrossServices = fromService | toService acrossServices = fromService | toService
) )
func (s *composeService) Copy(ctx context.Context, project string, opts api.CopyOptions) error { func (s *composeService) Copy(ctx context.Context, projectName string, options api.CopyOptions) error {
srcService, srcPath := splitCpArg(opts.Source) projectName = strings.ToLower(projectName)
destService, dstPath := splitCpArg(opts.Destination) srcService, srcPath := splitCpArg(options.Source)
destService, dstPath := splitCpArg(options.Destination)
var direction copyDirection var direction copyDirection
var serviceName string var serviceName string
var copyFunc func(ctx context.Context, containerID string, srcPath string, dstPath string, opts api.CopyOptions) error
if srcService != "" { if srcService != "" {
direction |= fromService direction |= fromService
serviceName = srcService serviceName = srcService
copyFunc = s.copyFromContainer
// copying from multiple containers of a services doesn't make sense. // copying from multiple containers of a services doesn't make sense.
if opts.All { if options.All {
return errors.New("cannot use the --all flag when copying from a service") return errors.New("cannot use the --all flag when copying from a service")
} }
} }
if destService != "" { if destService != "" {
direction |= toService direction |= toService
serviceName = destService serviceName = destService
copyFunc = s.copyToContainer
}
if direction == acrossServices {
return errors.New("copying between services is not supported")
} }
containers, err := s.getContainers(ctx, project, oneOffExclude, true, serviceName) if direction == 0 {
return errors.New("unknown copy direction")
}
containers, err := s.listContainersTargetedForCopy(ctx, projectName, options.Index, direction, serviceName)
if err != nil { if err != nil {
return err return err
} }
if len(containers) < 1 {
return fmt.Errorf("no container found for service %q", serviceName)
}
if !opts.All {
containers = containers.filter(indexed(opts.Index))
}
g := errgroup.Group{} g := errgroup.Group{}
for _, container := range containers { for _, container := range containers {
containerID := container.ID containerID := container.ID
g.Go(func() error { g.Go(func() error {
switch direction { return copyFunc(ctx, containerID, srcPath, dstPath, options)
case fromService:
return s.copyFromContainer(ctx, containerID, srcPath, dstPath, opts)
case toService:
return s.copyToContainer(ctx, containerID, srcPath, dstPath, opts)
case acrossServices:
return errors.New("copying between services is not supported")
default:
return errors.New("unknown copy direction")
}
}) })
} }
return g.Wait() return g.Wait()
} }
func (s *composeService) listContainersTargetedForCopy(ctx context.Context, projectName string, index int, direction copyDirection, serviceName string) (Containers, error) {
var containers Containers
var err error
switch {
case index > 0:
container, err := s.getSpecifiedContainer(ctx, projectName, oneOffExclude, true, serviceName, index)
if err != nil {
return nil, err
}
return append(containers, container), nil
default:
containers, err = s.getContainers(ctx, projectName, oneOffExclude, true, serviceName)
if err != nil {
return nil, err
}
if len(containers) < 1 {
return nil, fmt.Errorf("no container found for service %q", serviceName)
}
if direction == fromService {
return containers[:1], err
}
return containers, err
}
}
func (s *composeService) copyToContainer(ctx context.Context, containerID string, srcPath string, dstPath string, opts api.CopyOptions) error { func (s *composeService) copyToContainer(ctx context.Context, containerID string, srcPath string, dstPath string, opts api.CopyOptions) error {
var err error var err error
if srcPath != "-" { if srcPath != "-" {
@ -107,7 +128,7 @@ func (s *composeService) copyToContainer(ctx context.Context, containerID string
// Prepare destination copy info by stat-ing the container path. // Prepare destination copy info by stat-ing the container path.
dstInfo := archive.CopyInfo{Path: dstPath} dstInfo := archive.CopyInfo{Path: dstPath}
dstStat, err := s.apiClient.ContainerStatPath(ctx, containerID, dstPath) dstStat, err := s.apiClient().ContainerStatPath(ctx, containerID, dstPath)
// If the destination is a symbolic link, we should evaluate it. // If the destination is a symbolic link, we should evaluate it.
if err == nil && dstStat.Mode&os.ModeSymlink != 0 { if err == nil && dstStat.Mode&os.ModeSymlink != 0 {
@ -119,7 +140,7 @@ func (s *composeService) copyToContainer(ctx context.Context, containerID string
} }
dstInfo.Path = linkTarget dstInfo.Path = linkTarget
dstStat, err = s.apiClient.ContainerStatPath(ctx, containerID, linkTarget) dstStat, err = s.apiClient().ContainerStatPath(ctx, containerID, linkTarget)
} }
// Validate the destination path // Validate the destination path
@ -143,7 +164,7 @@ func (s *composeService) copyToContainer(ctx context.Context, containerID string
) )
if srcPath == "-" { if srcPath == "-" {
content = os.Stdin content = s.stdin()
resolvedDstPath = dstInfo.Path resolvedDstPath = dstInfo.Path
if !dstInfo.IsDir { if !dstInfo.IsDir {
return errors.Errorf("destination \"%s:%s\" must be a directory", containerID, dstPath) return errors.Errorf("destination \"%s:%s\" must be a directory", containerID, dstPath)
@ -187,7 +208,7 @@ func (s *composeService) copyToContainer(ctx context.Context, containerID string
AllowOverwriteDirWithFile: false, AllowOverwriteDirWithFile: false,
CopyUIDGID: opts.CopyUIDGID, CopyUIDGID: opts.CopyUIDGID,
} }
return s.apiClient.CopyToContainer(ctx, containerID, resolvedDstPath, content, options) return s.apiClient().CopyToContainer(ctx, containerID, resolvedDstPath, content, options)
} }
func (s *composeService) copyFromContainer(ctx context.Context, containerID, srcPath, dstPath string, opts api.CopyOptions) error { func (s *composeService) copyFromContainer(ctx context.Context, containerID, srcPath, dstPath string, opts api.CopyOptions) error {
@ -207,7 +228,7 @@ func (s *composeService) copyFromContainer(ctx context.Context, containerID, src
// if client requests to follow symbol link, then must decide target file to be copied // if client requests to follow symbol link, then must decide target file to be copied
var rebaseName string var rebaseName string
if opts.FollowLink { if opts.FollowLink {
srcStat, err := s.apiClient.ContainerStatPath(ctx, containerID, srcPath) srcStat, err := s.apiClient().ContainerStatPath(ctx, containerID, srcPath)
// If the destination is a symbolic link, we should follow it. // If the destination is a symbolic link, we should follow it.
if err == nil && srcStat.Mode&os.ModeSymlink != 0 { if err == nil && srcStat.Mode&os.ModeSymlink != 0 {
@ -223,14 +244,14 @@ func (s *composeService) copyFromContainer(ctx context.Context, containerID, src
} }
} }
content, stat, err := s.apiClient.CopyFromContainer(ctx, containerID, srcPath) content, stat, err := s.apiClient().CopyFromContainer(ctx, containerID, srcPath)
if err != nil { if err != nil {
return err return err
} }
defer content.Close() //nolint:errcheck defer content.Close() //nolint:errcheck
if dstPath == "-" { if dstPath == "-" {
_, err = io.Copy(os.Stdout, content) _, err = io.Copy(s.stdout(), content)
return err return err
} }

View File

@ -21,7 +21,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "os"
"path" "path"
"path/filepath" "path/filepath"
"strconv" "strconv"
@ -31,6 +31,7 @@ import (
moby "github.com/docker/docker/api/types" moby "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/blkiodev" "github.com/docker/docker/api/types/blkiodev"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/mount" "github.com/docker/docker/api/types/mount"
"github.com/docker/docker/api/types/network" "github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/strslice" "github.com/docker/docker/api/types/strslice"
@ -173,13 +174,21 @@ func prepareServicesDependsOn(p *types.Project) error {
dependencies = append(dependencies, spec[0]) dependencies = append(dependencies, spec[0])
} }
for _, link := range service.Links {
dependencies = append(dependencies, strings.Split(link, ":")[0])
}
if len(dependencies) == 0 { if len(dependencies) == 0 {
continue continue
} }
if service.DependsOn == nil { if service.DependsOn == nil {
service.DependsOn = make(types.DependsOnConfig) service.DependsOn = make(types.DependsOnConfig)
} }
deps, err := p.GetServices(dependencies...)
// Verify dependencies exist in the project, whether disabled or not
projAllServices := types.Project{}
projAllServices.Services = p.AllServices()
deps, err := projAllServices.GetServices(dependencies...)
if err != nil { if err != nil {
return err return err
} }
@ -255,7 +264,7 @@ func (s *composeService) getCreateOptions(ctx context.Context, p *types.Project,
return nil, nil, nil, err return nil, nil, nil, err
} }
proxyConfig := types.MappingWithEquals(s.configFile.ParseProxyConfig(s.apiClient.DaemonHost(), nil)) proxyConfig := types.MappingWithEquals(s.configFile().ParseProxyConfig(s.apiClient().DaemonHost(), nil))
env := proxyConfig.OverrideBy(service.Environment) env := proxyConfig.OverrideBy(service.Environment)
containerConfig := container.Config{ containerConfig := container.Config{
@ -347,6 +356,11 @@ func (s *composeService) getCreateOptions(ctx context.Context, p *types.Project,
volumesFrom = append(volumesFrom, v[len("container:"):]) volumesFrom = append(volumesFrom, v[len("container:"):])
} }
links, err := s.getLinks(ctx, p.Name, service, number)
if err != nil {
return nil, nil, nil, err
}
securityOpts, err := parseSecurityOpts(p, service.SecurityOpt) securityOpts, err := parseSecurityOpts(p, service.SecurityOpt)
if err != nil { if err != nil {
return nil, nil, nil, err return nil, nil, nil, err
@ -371,7 +385,7 @@ func (s *composeService) getCreateOptions(ctx context.Context, p *types.Project,
DNS: service.DNS, DNS: service.DNS,
DNSSearch: service.DNSSearch, DNSSearch: service.DNSSearch,
DNSOptions: service.DNSOpts, DNSOptions: service.DNSOpts,
ExtraHosts: service.ExtraHosts, ExtraHosts: service.ExtraHosts.AsList(),
SecurityOpt: securityOpts, SecurityOpt: securityOpts,
UsernsMode: container.UsernsMode(service.UserNSMode), UsernsMode: container.UsernsMode(service.UserNSMode),
Privileged: service.Privileged, Privileged: service.Privileged,
@ -381,6 +395,7 @@ func (s *composeService) getCreateOptions(ctx context.Context, p *types.Project,
Runtime: service.Runtime, Runtime: service.Runtime,
LogConfig: logConfig, LogConfig: logConfig,
GroupAdd: service.GroupAdd, GroupAdd: service.GroupAdd,
Links: links,
} }
return &containerConfig, &hostConfig, networkConfig, nil return &containerConfig, &hostConfig, networkConfig, nil
@ -399,7 +414,7 @@ func parseSecurityOpts(p *types.Project, securityOpts []string) ([]string, error
} }
} }
if con[0] == "seccomp" && con[1] != "unconfined" { if con[0] == "seccomp" && con[1] != "unconfined" {
f, err := ioutil.ReadFile(p.RelativePath(con[1])) f, err := os.ReadFile(p.RelativePath(con[1]))
if err != nil { if err != nil {
return securityOpts, errors.Errorf("opening seccomp profile (%s) failed: %v", con[1], err) return securityOpts, errors.Errorf("opening seccomp profile (%s) failed: %v", con[1], err)
} }
@ -500,6 +515,7 @@ func getDeployResources(s types.ServiceConfig) container.Resources {
CPUShares: s.CPUShares, CPUShares: s.CPUShares,
CPUPercent: int64(s.CPUS * 100), CPUPercent: int64(s.CPUS * 100),
CpusetCpus: s.CPUSet, CpusetCpus: s.CPUSet,
DeviceCgroupRules: s.DeviceCgroupRules,
} }
if s.PidsLimit != 0 { if s.PidsLimit != 0 {
@ -579,8 +595,12 @@ func setLimits(limits *types.Resource, resources *container.Resources) {
resources.Memory = int64(limits.MemoryBytes) resources.Memory = int64(limits.MemoryBytes)
} }
if limits.NanoCPUs != "" { if limits.NanoCPUs != "" {
i, _ := strconv.ParseInt(limits.NanoCPUs, 10, 64) if f, err := strconv.ParseFloat(limits.NanoCPUs, 64); err == nil {
resources.NanoCPUs = i resources.NanoCPUs = int64(f * 1e9)
}
}
if limits.PIds > 0 {
resources.PidsLimit = &limits.PIds
} }
} }
@ -693,7 +713,7 @@ func (s *composeService) buildContainerVolumes(ctx context.Context, p types.Proj
var mounts = []mount.Mount{} var mounts = []mount.Mount{}
image := getImageName(service, p.Name) image := getImageName(service, p.Name)
imgInspect, _, err := s.apiClient.ImageInspectWithRaw(ctx, image) imgInspect, _, err := s.apiClient().ImageInspectWithRaw(ctx, image)
if err != nil { if err != nil {
return nil, nil, nil, err return nil, nil, nil, err
} }
@ -708,12 +728,20 @@ func (s *composeService) buildContainerVolumes(ctx context.Context, p types.Proj
MOUNTS: MOUNTS:
for _, m := range mountOptions { for _, m := range mountOptions {
volumeMounts[m.Target] = struct{}{} volumeMounts[m.Target] = struct{}{}
// `Bind` API is used when host path need to be created if missing, `Mount` is preferred otherwise
if m.Type == mount.TypeBind || m.Type == mount.TypeNamedPipe { if m.Type == mount.TypeBind || m.Type == mount.TypeNamedPipe {
// `Mount` is preferred but does not offer option to created host path if missing
// so `Bind` API is used here with raw volume string
// see https://github.com/moby/moby/issues/43483
for _, v := range service.Volumes { for _, v := range service.Volumes {
if v.Target == m.Target && v.Bind != nil && v.Bind.CreateHostPath { if v.Target == m.Target {
binds = append(binds, fmt.Sprintf("%s:%s:%s", m.Source, m.Target, getBindMode(v.Bind, m.ReadOnly))) switch {
continue MOUNTS case string(m.Type) != v.Type:
v.Source = m.Source
fallthrough
case v.Bind != nil && v.Bind.CreateHostPath:
binds = append(binds, v.String())
continue MOUNTS
}
} }
} }
} }
@ -722,23 +750,6 @@ MOUNTS:
return volumeMounts, binds, mounts, nil return volumeMounts, binds, mounts, nil
} }
func getBindMode(bind *types.ServiceVolumeBind, readOnly bool) string {
mode := "rw"
if readOnly {
mode = "ro"
}
switch bind.SELinux {
case types.SELinuxShared:
mode += ",z"
case types.SELinuxPrivate:
mode += ",Z"
}
return mode
}
func buildContainerMountOptions(p types.Project, s types.ServiceConfig, img moby.ImageInspect, inherit *moby.Container) ([]mount.Mount, error) { func buildContainerMountOptions(p types.Project, s types.ServiceConfig, img moby.ImageInspect, inherit *moby.Container) ([]mount.Mount, error) {
var mounts = map[string]mount.Mount{} var mounts = map[string]mount.Mount{}
if inherit != nil { if inherit != nil {
@ -878,6 +889,10 @@ func buildContainerSecretMounts(p types.Project, s types.ServiceConfig) ([]mount
return nil, fmt.Errorf("unsupported external secret %s", definedSecret.Name) return nil, fmt.Errorf("unsupported external secret %s", definedSecret.Name)
} }
if definedSecret.Environment != "" {
continue
}
mount, err := buildMount(p, types.ServiceVolumeConfig{ mount, err := buildMount(p, types.ServiceVolumeConfig{
Type: types.VolumeTypeBind, Type: types.VolumeTypeBind,
Source: definedSecret.File, Source: definedSecret.File,
@ -921,10 +936,14 @@ func buildMount(project types.Project, volume types.ServiceVolumeConfig) (mount.
} }
} }
bind, vol, tmpfs := buildMountOptions(volume) bind, vol, tmpfs := buildMountOptions(project, volume)
volume.Target = path.Clean(volume.Target) volume.Target = path.Clean(volume.Target)
if bind != nil {
volume.Type = types.VolumeTypeBind
}
return mount.Mount{ return mount.Mount{
Type: mount.Type(volume.Type), Type: mount.Type(volume.Type),
Source: source, Source: source,
@ -937,7 +956,7 @@ func buildMount(project types.Project, volume types.ServiceVolumeConfig) (mount.
}, nil }, nil
} }
func buildMountOptions(volume types.ServiceVolumeConfig) (*mount.BindOptions, *mount.VolumeOptions, *mount.TmpfsOptions) { func buildMountOptions(project types.Project, volume types.ServiceVolumeConfig) (*mount.BindOptions, *mount.VolumeOptions, *mount.TmpfsOptions) {
switch volume.Type { switch volume.Type {
case "bind": case "bind":
if volume.Volume != nil { if volume.Volume != nil {
@ -954,6 +973,11 @@ func buildMountOptions(volume types.ServiceVolumeConfig) (*mount.BindOptions, *m
if volume.Tmpfs != nil { if volume.Tmpfs != nil {
logrus.Warnf("mount of type `volume` should not define `tmpfs` option") logrus.Warnf("mount of type `volume` should not define `tmpfs` option")
} }
if v, ok := project.Volumes[volume.Source]; ok && v.DriverOpts["o"] == types.VolumeTypeBind {
return buildBindOption(&types.ServiceVolumeBind{
CreateHostPath: true,
}), nil, nil
}
return nil, buildVolumeOptions(volume.Volume), nil return nil, buildVolumeOptions(volume.Volume), nil
case "tmpfs": case "tmpfs":
if volume.Bind != nil { if volume.Bind != nil {
@ -1007,92 +1031,88 @@ func getAliases(s types.ServiceConfig, c *types.ServiceNetworkConfig) []string {
} }
func (s *composeService) ensureNetwork(ctx context.Context, n types.NetworkConfig) error { func (s *composeService) ensureNetwork(ctx context.Context, n types.NetworkConfig) error {
_, err := s.apiClient.NetworkInspect(ctx, n.Name, moby.NetworkInspectOptions{}) // NetworkInspect will match on ID prefix, so NetworkList with a name
// filter is used to look for an exact match to prevent e.g. a network
// named `db` from getting erroneously matched to a network with an ID
// like `db9086999caf`
networks, err := s.apiClient().NetworkList(ctx, moby.NetworkListOptions{
Filters: filters.NewArgs(filters.Arg("name", n.Name)),
})
if err != nil { if err != nil {
if errdefs.IsNotFound(err) {
if n.External.External {
if n.Driver == "overlay" {
// Swarm nodes do not register overlay networks that were
// created on a different node unless they're in use.
// Here we assume `driver` is relevant for a network we don't manage
// which is a non-sense, but this is our legacy ¯\(ツ)/¯
// networkAttach will later fail anyway if network actually doesn't exists
return nil
}
return fmt.Errorf("network %s declared as external, but could not be found", n.Name)
}
var ipam *network.IPAM
if n.Ipam.Config != nil {
var config []network.IPAMConfig
for _, pool := range n.Ipam.Config {
config = append(config, network.IPAMConfig{
Subnet: pool.Subnet,
IPRange: pool.IPRange,
Gateway: pool.Gateway,
AuxAddress: pool.AuxiliaryAddresses,
})
}
ipam = &network.IPAM{
Driver: n.Ipam.Driver,
Config: config,
}
}
createOpts := moby.NetworkCreate{
// TODO NameSpace Labels
Labels: n.Labels,
Driver: n.Driver,
Options: n.DriverOpts,
Internal: n.Internal,
Attachable: n.Attachable,
IPAM: ipam,
EnableIPv6: n.EnableIPv6,
}
if n.Ipam.Driver != "" || len(n.Ipam.Config) > 0 {
createOpts.IPAM = &network.IPAM{}
}
if n.Ipam.Driver != "" {
createOpts.IPAM.Driver = n.Ipam.Driver
}
for _, ipamConfig := range n.Ipam.Config {
config := network.IPAMConfig{
Subnet: ipamConfig.Subnet,
}
createOpts.IPAM.Config = append(createOpts.IPAM.Config, config)
}
networkEventName := fmt.Sprintf("Network %s", n.Name)
w := progress.ContextWriter(ctx)
w.Event(progress.CreatingEvent(networkEventName))
if _, err := s.apiClient.NetworkCreate(ctx, n.Name, createOpts); err != nil {
w.Event(progress.ErrorEvent(networkEventName))
return errors.Wrapf(err, "failed to create network %s", n.Name)
}
w.Event(progress.CreatedEvent(networkEventName))
return nil
}
return err return err
} }
return nil if len(networks) == 0 {
} if n.External.External {
if n.Driver == "overlay" {
// Swarm nodes do not register overlay networks that were
// created on a different node unless they're in use.
// Here we assume `driver` is relevant for a network we don't manage
// which is a non-sense, but this is our legacy ¯\(ツ)/¯
// networkAttach will later fail anyway if network actually doesn't exists
return nil
}
return fmt.Errorf("network %s declared as external, but could not be found", n.Name)
}
var ipam *network.IPAM
if n.Ipam.Config != nil {
var config []network.IPAMConfig
for _, pool := range n.Ipam.Config {
config = append(config, network.IPAMConfig{
Subnet: pool.Subnet,
IPRange: pool.IPRange,
Gateway: pool.Gateway,
AuxAddress: pool.AuxiliaryAddresses,
})
}
ipam = &network.IPAM{
Driver: n.Ipam.Driver,
Config: config,
}
}
createOpts := moby.NetworkCreate{
CheckDuplicate: true,
// TODO NameSpace Labels
Labels: n.Labels,
Driver: n.Driver,
Options: n.DriverOpts,
Internal: n.Internal,
Attachable: n.Attachable,
IPAM: ipam,
EnableIPv6: n.EnableIPv6,
}
func (s *composeService) removeNetwork(ctx context.Context, networkID string, networkName string) error { if n.Ipam.Driver != "" || len(n.Ipam.Config) > 0 {
w := progress.ContextWriter(ctx) createOpts.IPAM = &network.IPAM{}
eventName := fmt.Sprintf("Network %s", networkName) }
w.Event(progress.RemovingEvent(eventName))
if err := s.apiClient.NetworkRemove(ctx, networkID); err != nil { if n.Ipam.Driver != "" {
w.Event(progress.ErrorEvent(eventName)) createOpts.IPAM.Driver = n.Ipam.Driver
return errors.Wrapf(err, fmt.Sprintf("failed to remove network %s", networkID)) }
for _, ipamConfig := range n.Ipam.Config {
config := network.IPAMConfig{
Subnet: ipamConfig.Subnet,
IPRange: ipamConfig.IPRange,
Gateway: ipamConfig.Gateway,
AuxAddress: ipamConfig.AuxiliaryAddresses,
}
createOpts.IPAM.Config = append(createOpts.IPAM.Config, config)
}
networkEventName := fmt.Sprintf("Network %s", n.Name)
w := progress.ContextWriter(ctx)
w.Event(progress.CreatingEvent(networkEventName))
if _, err := s.apiClient().NetworkCreate(ctx, n.Name, createOpts); err != nil {
w.Event(progress.ErrorEvent(networkEventName))
return errors.Wrapf(err, "failed to create network %s", n.Name)
}
w.Event(progress.CreatedEvent(networkEventName))
return nil
} }
w.Event(progress.RemovedEvent(eventName))
return nil return nil
} }
func (s *composeService) ensureVolume(ctx context.Context, volume types.VolumeConfig, project string) error { func (s *composeService) ensureVolume(ctx context.Context, volume types.VolumeConfig, project string) error {
inspected, err := s.apiClient.VolumeInspect(ctx, volume.Name) inspected, err := s.apiClient().VolumeInspect(ctx, volume.Name)
if err != nil { if err != nil {
if !errdefs.IsNotFound(err) { if !errdefs.IsNotFound(err) {
return err return err
@ -1123,7 +1143,7 @@ func (s *composeService) createVolume(ctx context.Context, volume types.VolumeCo
eventName := fmt.Sprintf("Volume %q", volume.Name) eventName := fmt.Sprintf("Volume %q", volume.Name)
w := progress.ContextWriter(ctx) w := progress.ContextWriter(ctx)
w.Event(progress.CreatingEvent(eventName)) w.Event(progress.CreatingEvent(eventName))
_, err := s.apiClient.VolumeCreate(ctx, volume_api.VolumeCreateBody{ _, err := s.apiClient().VolumeCreate(ctx, volume_api.VolumeCreateBody{
Labels: volume.Labels, Labels: volume.Labels,
Name: volume.Name, Name: volume.Name,
Driver: volume.Driver, Driver: volume.Driver,

View File

@ -143,15 +143,6 @@ func TestBuildContainerMountOptions(t *testing.T) {
assert.Equal(t, mounts[1].Target, "/var/myvolume2") assert.Equal(t, mounts[1].Target, "/var/myvolume2")
} }
func TestGetBindMode(t *testing.T) {
assert.Equal(t, getBindMode(&composetypes.ServiceVolumeBind{}, false), "rw")
assert.Equal(t, getBindMode(&composetypes.ServiceVolumeBind{}, true), "ro")
assert.Equal(t, getBindMode(&composetypes.ServiceVolumeBind{SELinux: composetypes.SELinuxShared}, false), "rw,z")
assert.Equal(t, getBindMode(&composetypes.ServiceVolumeBind{SELinux: composetypes.SELinuxPrivate}, false), "rw,Z")
assert.Equal(t, getBindMode(&composetypes.ServiceVolumeBind{SELinux: composetypes.SELinuxShared}, true), "ro,z")
assert.Equal(t, getBindMode(&composetypes.ServiceVolumeBind{SELinux: composetypes.SELinuxPrivate}, true), "ro,Z")
}
func TestGetDefaultNetworkMode(t *testing.T) { func TestGetDefaultNetworkMode(t *testing.T) {
t.Run("returns the network with the highest priority when service has multiple networks", func(t *testing.T) { t.Run("returns the network with the highest priority when service has multiple networks", func(t *testing.T) {
service := composetypes.ServiceConfig{ service := composetypes.ServiceConfig{

View File

@ -132,7 +132,7 @@ func getParents(v *Vertex) []*Vertex {
return v.GetParents() return v.GetParents()
} }
// GetParents returns a slice with the parent vertexes of the a Vertex // GetParents returns a slice with the parent vertices of the a Vertex
func (v *Vertex) GetParents() []*Vertex { func (v *Vertex) GetParents() []*Vertex {
var res []*Vertex var res []*Vertex
for _, p := range v.Parents { for _, p := range v.Parents {
@ -145,7 +145,7 @@ func getChildren(v *Vertex) []*Vertex {
return v.GetChildren() return v.GetChildren()
} }
// GetChildren returns a slice with the child vertexes of the a Vertex // GetChildren returns a slice with the child vertices of the a Vertex
func (v *Vertex) GetChildren() []*Vertex { func (v *Vertex) GetChildren() []*Vertex {
var res []*Vertex var res []*Vertex
for _, p := range v.Children { for _, p := range v.Children {
@ -194,7 +194,7 @@ func (g *Graph) AddVertex(key string, service string, initialStatus ServiceStatu
g.Vertices[key] = v g.Vertices[key] = v
} }
// AddEdge adds a relationship of dependency between vertexes `source` and `destination` // AddEdge adds a relationship of dependency between vertices `source` and `destination`
func (g *Graph) AddEdge(source string, destination string) error { func (g *Graph) AddEdge(source string, destination string) error {
g.lock.Lock() g.lock.Lock()
defer g.lock.Unlock() defer g.lock.Unlock()

View File

@ -26,6 +26,7 @@ import (
moby "github.com/docker/docker/api/types" moby "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
"github.com/docker/docker/errdefs" "github.com/docker/docker/errdefs"
"github.com/pkg/errors"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
"github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/api"
@ -41,7 +42,6 @@ func (s *composeService) Down(ctx context.Context, projectName string, options a
} }
func (s *composeService) down(ctx context.Context, projectName string, options api.DownOptions) error { func (s *composeService) down(ctx context.Context, projectName string, options api.DownOptions) error {
builtFromResources := options.Project == nil
w := progress.ContextWriter(ctx) w := progress.ContextWriter(ctx)
resourceToRemove := false resourceToRemove := false
@ -51,8 +51,9 @@ func (s *composeService) down(ctx context.Context, projectName string, options a
return err return err
} }
if builtFromResources { project := options.Project
options.Project, err = s.getProjectWithVolumes(ctx, containers, projectName) if project == nil {
project, err = s.getProjectWithResources(ctx, containers, projectName)
if err != nil { if err != nil {
return err return err
} }
@ -62,7 +63,7 @@ func (s *composeService) down(ctx context.Context, projectName string, options a
resourceToRemove = true resourceToRemove = true
} }
err = InReverseDependencyOrder(ctx, options.Project, func(c context.Context, service string) error { err = InReverseDependencyOrder(ctx, project, func(c context.Context, service string) error {
serviceContainers := containers.filter(isService(service)) serviceContainers := containers.filter(isService(service))
err := s.removeContainers(ctx, w, serviceContainers, options.Timeout, options.Volumes) err := s.removeContainers(ctx, w, serviceContainers, options.Timeout, options.Volumes)
return err return err
@ -71,7 +72,7 @@ func (s *composeService) down(ctx context.Context, projectName string, options a
return err return err
} }
orphans := containers.filter(isNotService(options.Project.ServiceNames()...)) orphans := containers.filter(isNotService(project.ServiceNames()...))
if options.RemoveOrphans && len(orphans) > 0 { if options.RemoveOrphans && len(orphans) > 0 {
err := s.removeContainers(ctx, w, orphans, options.Timeout, false) err := s.removeContainers(ctx, w, orphans, options.Timeout, false)
if err != nil { if err != nil {
@ -79,21 +80,18 @@ func (s *composeService) down(ctx context.Context, projectName string, options a
} }
} }
ops, err := s.ensureNetworksDown(ctx, projectName) ops := s.ensureNetworksDown(ctx, project, w)
if err != nil {
return err
}
if options.Images != "" { if options.Images != "" {
ops = append(ops, s.ensureImagesDown(ctx, projectName, options, w)...) ops = append(ops, s.ensureImagesDown(ctx, project, options, w)...)
} }
if options.Volumes { if options.Volumes {
ops = append(ops, s.ensureVolumesDown(ctx, options.Project, w)...) ops = append(ops, s.ensureVolumesDown(ctx, project, w)...)
} }
if !resourceToRemove && len(ops) == 0 { if !resourceToRemove && len(ops) == 0 {
w.Event(progress.NewEvent(projectName, progress.Done, "Warning: No resource found to remove")) fmt.Fprintf(s.stderr(), "Warning: No resource found to remove for project %q.\n", projectName)
} }
eg, _ := errgroup.WithContext(ctx) eg, _ := errgroup.WithContext(ctx)
@ -106,6 +104,9 @@ func (s *composeService) down(ctx context.Context, projectName string, options a
func (s *composeService) ensureVolumesDown(ctx context.Context, project *types.Project, w progress.Writer) []downOp { func (s *composeService) ensureVolumesDown(ctx context.Context, project *types.Project, w progress.Writer) []downOp {
var ops []downOp var ops []downOp
for _, vol := range project.Volumes { for _, vol := range project.Volumes {
if vol.External.External {
continue
}
volumeName := vol.Name volumeName := vol.Name
ops = append(ops, func() error { ops = append(ops, func() error {
return s.removeVolume(ctx, volumeName, w) return s.removeVolume(ctx, volumeName, w)
@ -114,9 +115,9 @@ func (s *composeService) ensureVolumesDown(ctx context.Context, project *types.P
return ops return ops
} }
func (s *composeService) ensureImagesDown(ctx context.Context, projectName string, options api.DownOptions, w progress.Writer) []downOp { func (s *composeService) ensureImagesDown(ctx context.Context, project *types.Project, options api.DownOptions, w progress.Writer) []downOp {
var ops []downOp var ops []downOp
for image := range s.getServiceImages(options, projectName) { for image := range s.getServiceImages(options, project) {
image := image image := image
ops = append(ops, func() error { ops = append(ops, func() error {
return s.removeImage(ctx, image, w) return s.removeImage(ctx, image, w)
@ -125,31 +126,74 @@ func (s *composeService) ensureImagesDown(ctx context.Context, projectName strin
return ops return ops
} }
func (s *composeService) ensureNetworksDown(ctx context.Context, projectName string) ([]downOp, error) { func (s *composeService) ensureNetworksDown(ctx context.Context, project *types.Project, w progress.Writer) []downOp {
var ops []downOp var ops []downOp
networks, err := s.apiClient.NetworkList(ctx, moby.NetworkListOptions{Filters: filters.NewArgs(projectFilter(projectName))}) for _, n := range project.Networks {
if err != nil { if n.External.External {
return ops, err continue
} }
for _, n := range networks { // loop capture variable for op closure
networkID := n.ID
networkName := n.Name networkName := n.Name
ops = append(ops, func() error { ops = append(ops, func() error {
return s.removeNetwork(ctx, networkID, networkName) return s.removeNetwork(ctx, networkName, w)
}) })
} }
return ops, nil return ops
} }
func (s *composeService) getServiceImages(options api.DownOptions, projectName string) map[string]struct{} { func (s *composeService) removeNetwork(ctx context.Context, name string, w progress.Writer) error {
// networks are guaranteed to have unique IDs but NOT names, so it's
// possible to get into a situation where a compose down will fail with
// an error along the lines of:
// failed to remove network test: Error response from daemon: network test is ambiguous (2 matches found based on name)
// as a workaround here, the delete is done by ID after doing a list using
// the name as a filter (99.9% of the time this will return a single result)
networks, err := s.apiClient().NetworkList(ctx, moby.NetworkListOptions{
Filters: filters.NewArgs(filters.Arg("name", name)),
})
if err != nil {
return errors.Wrapf(err, fmt.Sprintf("failed to inspect network %s", name))
}
if len(networks) == 0 {
return nil
}
eventName := fmt.Sprintf("Network %s", name)
w.Event(progress.RemovingEvent(eventName))
var removed int
for _, net := range networks {
if err := s.apiClient().NetworkRemove(ctx, net.ID); err != nil {
if errdefs.IsNotFound(err) {
continue
}
w.Event(progress.ErrorEvent(eventName))
return errors.Wrapf(err, fmt.Sprintf("failed to remove network %s", name))
}
removed++
}
if removed == 0 {
// in practice, it's extremely unlikely for this to ever occur, as it'd
// mean the network was present when we queried at the start of this
// method but was then deleted by something else in the interim
w.Event(progress.NewEvent(eventName, progress.Done, "Warning: No resource found to remove"))
return nil
}
w.Event(progress.RemovedEvent(eventName))
return nil
}
func (s *composeService) getServiceImages(options api.DownOptions, project *types.Project) map[string]struct{} {
images := map[string]struct{}{} images := map[string]struct{}{}
for _, service := range options.Project.Services { for _, service := range project.Services {
image := service.Image image := service.Image
if options.Images == "local" && image != "" { if options.Images == "local" && image != "" {
continue continue
} }
if image == "" { if image == "" {
image = getImageName(service, projectName) image = getImageName(service, project.Name)
} }
images[image] = struct{}{} images[image] = struct{}{}
} }
@ -159,7 +203,7 @@ func (s *composeService) getServiceImages(options api.DownOptions, projectName s
func (s *composeService) removeImage(ctx context.Context, image string, w progress.Writer) error { func (s *composeService) removeImage(ctx context.Context, image string, w progress.Writer) error {
id := fmt.Sprintf("Image %s", image) id := fmt.Sprintf("Image %s", image)
w.Event(progress.NewEvent(id, progress.Working, "Removing")) w.Event(progress.NewEvent(id, progress.Working, "Removing"))
_, err := s.apiClient.ImageRemove(ctx, image, moby.ImageRemoveOptions{}) _, err := s.apiClient().ImageRemove(ctx, image, moby.ImageRemoveOptions{})
if err == nil { if err == nil {
w.Event(progress.NewEvent(id, progress.Done, "Removed")) w.Event(progress.NewEvent(id, progress.Done, "Removed"))
return nil return nil
@ -174,7 +218,7 @@ func (s *composeService) removeImage(ctx context.Context, image string, w progre
func (s *composeService) removeVolume(ctx context.Context, id string, w progress.Writer) error { func (s *composeService) removeVolume(ctx context.Context, id string, w progress.Writer) error {
resource := fmt.Sprintf("Volume %s", id) resource := fmt.Sprintf("Volume %s", id)
w.Event(progress.NewEvent(resource, progress.Working, "Removing")) w.Event(progress.NewEvent(resource, progress.Working, "Removing"))
err := s.apiClient.VolumeRemove(ctx, id, true) err := s.apiClient().VolumeRemove(ctx, id, true)
if err == nil { if err == nil {
w.Event(progress.NewEvent(resource, progress.Done, "Removed")) w.Event(progress.NewEvent(resource, progress.Done, "Removed"))
return nil return nil
@ -193,7 +237,7 @@ func (s *composeService) stopContainers(ctx context.Context, w progress.Writer,
eg.Go(func() error { eg.Go(func() error {
eventName := getContainerProgressName(container) eventName := getContainerProgressName(container)
w.Event(progress.StoppingEvent(eventName)) w.Event(progress.StoppingEvent(eventName))
err := s.apiClient.ContainerStop(ctx, container.ID, timeout) err := s.apiClient().ContainerStop(ctx, container.ID, timeout)
if err != nil { if err != nil {
w.Event(progress.ErrorMessageEvent(eventName, "Error while Stopping")) w.Event(progress.ErrorMessageEvent(eventName, "Error while Stopping"))
return err return err
@ -218,7 +262,7 @@ func (s *composeService) removeContainers(ctx context.Context, w progress.Writer
return err return err
} }
w.Event(progress.RemovingEvent(eventName)) w.Event(progress.RemovingEvent(eventName))
err = s.apiClient.ContainerRemove(ctx, container.ID, moby.ContainerRemoveOptions{ err = s.apiClient().ContainerRemove(ctx, container.ID, moby.ContainerRemoveOptions{
Force: true, Force: true,
RemoveVolumes: volumes, RemoveVolumes: volumes,
}) })
@ -233,21 +277,23 @@ func (s *composeService) removeContainers(ctx context.Context, w progress.Writer
return eg.Wait() return eg.Wait()
} }
func (s *composeService) getProjectWithVolumes(ctx context.Context, containers Containers, projectName string) (*types.Project, error) { func (s *composeService) getProjectWithResources(ctx context.Context, containers Containers, projectName string) (*types.Project, error) {
containers = containers.filter(isNotOneOff) containers = containers.filter(isNotOneOff)
project, _ := s.projectFromName(containers, projectName) project, err := s.projectFromName(containers, projectName)
volumes, err := s.apiClient.VolumeList(ctx, filters.NewArgs(projectFilter(projectName))) if err != nil && !api.IsNotFoundError(err) {
if err != nil {
return nil, err return nil, err
} }
project.Volumes = types.Volumes{} volumes, err := s.actualVolumes(ctx, projectName)
for _, vol := range volumes.Volumes { if err != nil {
project.Volumes[vol.Labels[api.VolumeLabel]] = types.VolumeConfig{ return nil, err
Name: vol.Name,
Driver: vol.Driver,
Labels: vol.Labels,
}
} }
project.Volumes = volumes
networks, err := s.actualNetworks(ctx, projectName)
if err != nil {
return nil, err
}
project.Networks = networks
return project, nil return project, nil
} }

View File

@ -21,21 +21,24 @@ import (
"strings" "strings"
"testing" "testing"
compose "github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/mocks"
moby "github.com/docker/docker/api/types" moby "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/volume" "github.com/docker/docker/api/types/volume"
"github.com/golang/mock/gomock" "github.com/golang/mock/gomock"
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
compose "github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/mocks"
) )
func TestDown(t *testing.T) { func TestDown(t *testing.T) {
mockCtrl := gomock.NewController(t) mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish() defer mockCtrl.Finish()
api := mocks.NewMockAPIClient(mockCtrl) api := mocks.NewMockAPIClient(mockCtrl)
tested.apiClient = api cli := mocks.NewMockCli(mockCtrl)
tested.dockerCli = cli
cli.EXPECT().Client().Return(api).AnyTimes()
api.EXPECT().ContainerList(gomock.Any(), projectFilterListOpt()).Return( api.EXPECT().ContainerList(gomock.Any(), projectFilterListOpt()).Return(
[]moby.Container{ []moby.Container{
@ -47,6 +50,14 @@ func TestDown(t *testing.T) {
api.EXPECT().VolumeList(gomock.Any(), filters.NewArgs(projectFilter(strings.ToLower(testProject)))). api.EXPECT().VolumeList(gomock.Any(), filters.NewArgs(projectFilter(strings.ToLower(testProject)))).
Return(volume.VolumeListOKBody{}, nil) Return(volume.VolumeListOKBody{}, nil)
// network names are not guaranteed to be unique, ensure Compose handles
// cleanup properly if duplicates are inadvertently created
api.EXPECT().NetworkList(gomock.Any(), moby.NetworkListOptions{Filters: filters.NewArgs(projectFilter(strings.ToLower(testProject)))}).
Return([]moby.NetworkResource{
{ID: "abc123", Name: "myProject_default"},
{ID: "def456", Name: "myProject_default"},
}, nil)
api.EXPECT().ContainerStop(gomock.Any(), "123", nil).Return(nil) api.EXPECT().ContainerStop(gomock.Any(), "123", nil).Return(nil)
api.EXPECT().ContainerStop(gomock.Any(), "456", nil).Return(nil) api.EXPECT().ContainerStop(gomock.Any(), "456", nil).Return(nil)
api.EXPECT().ContainerStop(gomock.Any(), "789", nil).Return(nil) api.EXPECT().ContainerStop(gomock.Any(), "789", nil).Return(nil)
@ -55,10 +66,14 @@ func TestDown(t *testing.T) {
api.EXPECT().ContainerRemove(gomock.Any(), "456", moby.ContainerRemoveOptions{Force: true}).Return(nil) api.EXPECT().ContainerRemove(gomock.Any(), "456", moby.ContainerRemoveOptions{Force: true}).Return(nil)
api.EXPECT().ContainerRemove(gomock.Any(), "789", moby.ContainerRemoveOptions{Force: true}).Return(nil) api.EXPECT().ContainerRemove(gomock.Any(), "789", moby.ContainerRemoveOptions{Force: true}).Return(nil)
api.EXPECT().NetworkList(gomock.Any(), moby.NetworkListOptions{Filters: filters.NewArgs(projectFilter(strings.ToLower(testProject)))}).Return([]moby.NetworkResource{{ID: "myProject_default"}}, api.EXPECT().NetworkList(gomock.Any(), moby.NetworkListOptions{
nil) Filters: filters.NewArgs(filters.Arg("name", "myProject_default")),
}).Return([]moby.NetworkResource{
api.EXPECT().NetworkRemove(gomock.Any(), "myProject_default").Return(nil) {ID: "abc123", Name: "myProject_default"},
{ID: "def456", Name: "myProject_default"},
}, nil)
api.EXPECT().NetworkRemove(gomock.Any(), "abc123").Return(nil)
api.EXPECT().NetworkRemove(gomock.Any(), "def456").Return(nil)
err := tested.Down(context.Background(), strings.ToLower(testProject), compose.DownOptions{}) err := tested.Down(context.Background(), strings.ToLower(testProject), compose.DownOptions{})
assert.NilError(t, err) assert.NilError(t, err)
@ -67,8 +82,11 @@ func TestDown(t *testing.T) {
func TestDownRemoveOrphans(t *testing.T) { func TestDownRemoveOrphans(t *testing.T) {
mockCtrl := gomock.NewController(t) mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish() defer mockCtrl.Finish()
api := mocks.NewMockAPIClient(mockCtrl) api := mocks.NewMockAPIClient(mockCtrl)
tested.apiClient = api cli := mocks.NewMockCli(mockCtrl)
tested.dockerCli = cli
cli.EXPECT().Client().Return(api).AnyTimes()
api.EXPECT().ContainerList(gomock.Any(), projectFilterListOpt()).Return( api.EXPECT().ContainerList(gomock.Any(), projectFilterListOpt()).Return(
[]moby.Container{ []moby.Container{
@ -78,6 +96,8 @@ func TestDownRemoveOrphans(t *testing.T) {
}, nil) }, nil)
api.EXPECT().VolumeList(gomock.Any(), filters.NewArgs(projectFilter(strings.ToLower(testProject)))). api.EXPECT().VolumeList(gomock.Any(), filters.NewArgs(projectFilter(strings.ToLower(testProject)))).
Return(volume.VolumeListOKBody{}, nil) Return(volume.VolumeListOKBody{}, nil)
api.EXPECT().NetworkList(gomock.Any(), moby.NetworkListOptions{Filters: filters.NewArgs(projectFilter(strings.ToLower(testProject)))}).
Return([]moby.NetworkResource{{Name: "myProject_default"}}, nil)
api.EXPECT().ContainerStop(gomock.Any(), "123", nil).Return(nil) api.EXPECT().ContainerStop(gomock.Any(), "123", nil).Return(nil)
api.EXPECT().ContainerStop(gomock.Any(), "789", nil).Return(nil) api.EXPECT().ContainerStop(gomock.Any(), "789", nil).Return(nil)
@ -87,10 +107,10 @@ func TestDownRemoveOrphans(t *testing.T) {
api.EXPECT().ContainerRemove(gomock.Any(), "789", moby.ContainerRemoveOptions{Force: true}).Return(nil) api.EXPECT().ContainerRemove(gomock.Any(), "789", moby.ContainerRemoveOptions{Force: true}).Return(nil)
api.EXPECT().ContainerRemove(gomock.Any(), "321", moby.ContainerRemoveOptions{Force: true}).Return(nil) api.EXPECT().ContainerRemove(gomock.Any(), "321", moby.ContainerRemoveOptions{Force: true}).Return(nil)
api.EXPECT().NetworkList(gomock.Any(), moby.NetworkListOptions{Filters: filters.NewArgs(projectFilter(strings.ToLower(testProject)))}).Return([]moby.NetworkResource{{ID: "myProject_default"}}, api.EXPECT().NetworkList(gomock.Any(), moby.NetworkListOptions{
nil) Filters: filters.NewArgs(filters.Arg("name", "myProject_default")),
}).Return([]moby.NetworkResource{{ID: "abc123", Name: "myProject_default"}}, nil)
api.EXPECT().NetworkRemove(gomock.Any(), "myProject_default").Return(nil) api.EXPECT().NetworkRemove(gomock.Any(), "abc123").Return(nil)
err := tested.Down(context.Background(), strings.ToLower(testProject), compose.DownOptions{RemoveOrphans: true}) err := tested.Down(context.Background(), strings.ToLower(testProject), compose.DownOptions{RemoveOrphans: true})
assert.NilError(t, err) assert.NilError(t, err)
@ -99,8 +119,11 @@ func TestDownRemoveOrphans(t *testing.T) {
func TestDownRemoveVolumes(t *testing.T) { func TestDownRemoveVolumes(t *testing.T) {
mockCtrl := gomock.NewController(t) mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish() defer mockCtrl.Finish()
api := mocks.NewMockAPIClient(mockCtrl) api := mocks.NewMockAPIClient(mockCtrl)
tested.apiClient = api cli := mocks.NewMockCli(mockCtrl)
tested.dockerCli = cli
cli.EXPECT().Client().Return(api).AnyTimes()
api.EXPECT().ContainerList(gomock.Any(), projectFilterListOpt()).Return( api.EXPECT().ContainerList(gomock.Any(), projectFilterListOpt()).Return(
[]moby.Container{testContainer("service1", "123", false)}, nil) []moby.Container{testContainer("service1", "123", false)}, nil)
@ -108,12 +131,12 @@ func TestDownRemoveVolumes(t *testing.T) {
Return(volume.VolumeListOKBody{ Return(volume.VolumeListOKBody{
Volumes: []*moby.Volume{{Name: "myProject_volume"}}, Volumes: []*moby.Volume{{Name: "myProject_volume"}},
}, nil) }, nil)
api.EXPECT().NetworkList(gomock.Any(), moby.NetworkListOptions{Filters: filters.NewArgs(projectFilter(strings.ToLower(testProject)))}).
Return(nil, nil)
api.EXPECT().ContainerStop(gomock.Any(), "123", nil).Return(nil) api.EXPECT().ContainerStop(gomock.Any(), "123", nil).Return(nil)
api.EXPECT().ContainerRemove(gomock.Any(), "123", moby.ContainerRemoveOptions{Force: true, RemoveVolumes: true}).Return(nil) api.EXPECT().ContainerRemove(gomock.Any(), "123", moby.ContainerRemoveOptions{Force: true, RemoveVolumes: true}).Return(nil)
api.EXPECT().NetworkList(gomock.Any(), moby.NetworkListOptions{Filters: filters.NewArgs(projectFilter(strings.ToLower(testProject)))}).Return(nil, nil)
api.EXPECT().VolumeRemove(gomock.Any(), "myProject_volume", true).Return(nil) api.EXPECT().VolumeRemove(gomock.Any(), "myProject_volume", true).Return(nil)
err := tested.Down(context.Background(), strings.ToLower(testProject), compose.DownOptions{Volumes: true}) err := tested.Down(context.Background(), strings.ToLower(testProject), compose.DownOptions{Volumes: true})

View File

@ -29,9 +29,10 @@ import (
"github.com/docker/compose/v2/pkg/utils" "github.com/docker/compose/v2/pkg/utils"
) )
func (s *composeService) Events(ctx context.Context, project string, options api.EventsOptions) error { func (s *composeService) Events(ctx context.Context, projectName string, options api.EventsOptions) error {
events, errors := s.apiClient.Events(ctx, moby.EventsOptions{ projectName = strings.ToLower(projectName)
Filters: filters.NewArgs(projectFilter(project)), events, errors := s.apiClient().Events(ctx, moby.EventsOptions{
Filters: filters.NewArgs(projectFilter(projectName)),
}) })
for { for {
select { select {

View File

@ -18,149 +18,44 @@ package compose
import ( import (
"context" "context"
"fmt" "strings"
"io"
"github.com/docker/cli/cli/streams"
moby "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/pkg/stdcopy"
"github.com/moby/term"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command/container"
"github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/api"
moby "github.com/docker/docker/api/types"
) )
func (s *composeService) Exec(ctx context.Context, project string, opts api.RunOptions) (int, error) { func (s *composeService) Exec(ctx context.Context, projectName string, options api.RunOptions) (int, error) {
container, err := s.getExecTarget(ctx, project, opts) projectName = strings.ToLower(projectName)
target, err := s.getExecTarget(ctx, projectName, options)
if err != nil { if err != nil {
return 0, err return 0, err
} }
exec, err := s.apiClient.ContainerExecCreate(ctx, container.ID, moby.ExecConfig{ exec := container.NewExecOptions()
Cmd: opts.Command, exec.Interactive = options.Interactive
Env: opts.Environment, exec.TTY = options.Tty
User: opts.User, exec.Detach = options.Detach
Privileged: opts.Privileged, exec.User = options.User
Tty: opts.Tty, exec.Privileged = options.Privileged
Detach: opts.Detach, exec.Workdir = options.WorkingDir
WorkingDir: opts.WorkingDir, exec.Container = target.ID
exec.Command = options.Command
AttachStdin: true, for _, v := range options.Environment {
AttachStdout: true, err := exec.Env.Set(v)
AttachStderr: true,
})
if err != nil {
return 0, err
}
if opts.Detach {
return 0, s.apiClient.ContainerExecStart(ctx, exec.ID, moby.ExecStartCheck{
Detach: true,
Tty: opts.Tty,
})
}
resp, err := s.apiClient.ContainerExecAttach(ctx, exec.ID, moby.ExecStartCheck{
Tty: opts.Tty,
})
if err != nil {
return 0, err
}
defer resp.Close() //nolint:errcheck
if opts.Tty {
s.monitorTTySize(ctx, exec.ID, s.apiClient.ContainerExecResize)
if err != nil { if err != nil {
return 0, err return 0, err
} }
} }
err = s.interactiveExec(ctx, opts, resp) err = container.RunExec(s.dockerCli, exec)
if err != nil { if sterr, ok := err.(cli.StatusError); ok {
return 0, err return sterr.StatusCode, nil
}
return s.getExecExitStatus(ctx, exec.ID)
}
// inspired by https://github.com/docker/cli/blob/master/cli/command/container/exec.go#L116
func (s *composeService) interactiveExec(ctx context.Context, opts api.RunOptions, resp moby.HijackedResponse) error {
outputDone := make(chan error)
inputDone := make(chan error)
stdout := ContainerStdout{HijackedResponse: resp}
stdin := ContainerStdin{HijackedResponse: resp}
r, err := s.getEscapeKeyProxy(opts.Stdin, opts.Tty)
if err != nil {
return err
}
in := streams.NewIn(opts.Stdin)
if in.IsTerminal() && opts.Tty {
state, err := term.SetRawTerminal(in.FD())
if err != nil {
return err
}
defer term.RestoreTerminal(in.FD(), state) //nolint:errcheck
}
go func() {
if opts.Tty {
_, err := io.Copy(opts.Stdout, stdout)
outputDone <- err
} else {
_, err := stdcopy.StdCopy(opts.Stdout, opts.Stderr, stdout)
outputDone <- err
}
stdout.Close() //nolint:errcheck
}()
go func() {
_, err := io.Copy(stdin, r)
inputDone <- err
stdin.Close() //nolint:errcheck
}()
for {
select {
case err := <-outputDone:
return err
case err := <-inputDone:
if _, ok := err.(term.EscapeError); ok {
return nil
}
if err != nil {
return err
}
// Wait for output to complete streaming
case <-ctx.Done():
return ctx.Err()
}
} }
return 0, err
} }
func (s *composeService) getExecTarget(ctx context.Context, projectName string, opts api.RunOptions) (moby.Container, error) { func (s *composeService) getExecTarget(ctx context.Context, projectName string, opts api.RunOptions) (moby.Container, error) {
containers, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{ return s.getSpecifiedContainer(ctx, projectName, oneOffInclude, false, opts.Service, opts.Index)
Filters: filters.NewArgs(
projectFilter(projectName),
serviceFilter(opts.Service),
containerNumberFilter(opts.Index),
),
})
if err != nil {
return moby.Container{}, err
}
if len(containers) < 1 {
return moby.Container{}, fmt.Errorf("service %q is not running container #%d", opts.Service, opts.Index)
}
container := containers[0]
return container, nil
}
func (s *composeService) getExecExitStatus(ctx context.Context, execID string) (int, error) {
resp, err := s.apiClient.ContainerExecInspect(ctx, execID)
if err != nil {
return 0, err
}
return resp.ExitCode, nil
} }

View File

@ -32,7 +32,8 @@ import (
) )
func (s *composeService) Images(ctx context.Context, projectName string, options api.ImagesOptions) ([]api.ImageSummary, error) { func (s *composeService) Images(ctx context.Context, projectName string, options api.ImagesOptions) ([]api.ImageSummary, error) {
allContainers, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{ projectName = strings.ToLower(projectName)
allContainers, err := s.apiClient().ContainerList(ctx, moby.ContainerListOptions{
All: true, All: true,
Filters: filters.NewArgs(projectFilter(projectName)), Filters: filters.NewArgs(projectFilter(projectName)),
}) })
@ -83,7 +84,7 @@ func (s *composeService) getImages(ctx context.Context, images []string) (map[st
for _, img := range images { for _, img := range images {
img := img img := img
eg.Go(func() error { eg.Go(func() error {
inspect, _, err := s.apiClient.ImageInspectWithRaw(ctx, img) inspect, _, err := s.apiClient().ImageInspectWithRaw(ctx, img)
if err != nil { if err != nil {
if errdefs.IsNotFound(err) { if errdefs.IsNotFound(err) {
return nil return nil
@ -93,7 +94,6 @@ func (s *composeService) getImages(ctx context.Context, images []string) (map[st
tag := "" tag := ""
repository := "" repository := ""
if len(inspect.RepoTags) > 0 { if len(inspect.RepoTags) > 0 {
repotag := strings.Split(inspect.RepoTags[0], ":") repotag := strings.Split(inspect.RepoTags[0], ":")
repository = repotag[0] repository = repotag[0]
if len(repotag) > 1 { if len(repotag) > 1 {

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