mirror of
https://github.com/docker/compose.git
synced 2026-02-11 11:09:23 +08:00
Compare commits
106 Commits
v2.18.0
...
multiplaye
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f06caeb844 | ||
|
|
49d1bc7524 | ||
|
|
827e864ed0 | ||
|
|
28301fb1a4 | ||
|
|
fa3e16c66b | ||
|
|
edd76bfd70 | ||
|
|
c496c23071 | ||
|
|
02284378bf | ||
|
|
10b290e682 | ||
|
|
3906a7a67c | ||
|
|
83671db3dd | ||
|
|
1a41678c58 | ||
|
|
035276e027 | ||
|
|
db24023884 | ||
|
|
c48f542962 | ||
|
|
42dc7a6a87 | ||
|
|
30b3b47383 | ||
|
|
061b52da9a | ||
|
|
04aa155878 | ||
|
|
2d4f8d31fc | ||
|
|
e1f8603a62 | ||
|
|
a2ce602f6c | ||
|
|
401334e03f | ||
|
|
93cf2b921a | ||
|
|
83b2433a27 | ||
|
|
c8d06137b5 | ||
|
|
c61b8aa5ac | ||
|
|
ff3984e609 | ||
|
|
2efea2e9f5 | ||
|
|
6a3a95c4a8 | ||
|
|
fee8a1c6c6 | ||
|
|
7ffe83dc95 | ||
|
|
d20c2551f2 | ||
|
|
0e9a5b6b78 | ||
|
|
6887a3fc3e | ||
|
|
586fe87f98 | ||
|
|
6d66130266 | ||
|
|
0f83a8630e | ||
|
|
26cb941f79 | ||
|
|
43783d36e2 | ||
|
|
08e6bfc859 | ||
|
|
7a870e2449 | ||
|
|
8cd8f08d77 | ||
|
|
cfe91becc7 | ||
|
|
9384e5f4d7 | ||
|
|
68bd0eb523 | ||
|
|
3fe665b93d | ||
|
|
508d71c5df | ||
|
|
58c5ea8217 | ||
|
|
ec31d3c2ac | ||
|
|
599723f890 | ||
|
|
0a9b9fd8fe | ||
|
|
3c8a56dbf3 | ||
|
|
e63ab14b1e | ||
|
|
32cf776ecd | ||
|
|
955784c406 | ||
|
|
2d22c2b5ce | ||
|
|
852c9e80b4 | ||
|
|
37850f7955 | ||
|
|
4bf2fe9fed | ||
|
|
e21a8d6293 | ||
|
|
f8b6459403 | ||
|
|
be6c9565e3 | ||
|
|
60fe97416c | ||
|
|
629c9f62e9 | ||
|
|
7c3fe359b7 | ||
|
|
d2aa15c06e | ||
|
|
6530880361 | ||
|
|
1bd8a773a7 | ||
|
|
fed8ef6b79 | ||
|
|
419fcdd6c8 | ||
|
|
65b714c108 | ||
|
|
44dd232e97 | ||
|
|
83ad5e97b7 | ||
|
|
b0a35ccc98 | ||
|
|
f5480ee3ed | ||
|
|
b4924dee83 | ||
|
|
2ca8ab914a | ||
|
|
3ec8c60657 | ||
|
|
06ec06472f | ||
|
|
466e1d3197 | ||
|
|
0d6b99e6f9 | ||
|
|
01d91c490c | ||
|
|
6f6e1635fd | ||
|
|
3d05a1becf | ||
|
|
42cd961d58 | ||
|
|
d15fcc6444 | ||
|
|
22c2471a08 | ||
|
|
29a1cc452d | ||
|
|
b05a94fd66 | ||
|
|
15cad92b61 | ||
|
|
c7afc6188b | ||
|
|
ca19b7fcc9 | ||
|
|
93bd27a0cc | ||
|
|
68c462e607 | ||
|
|
916aac6c27 | ||
|
|
eafcd1b35e | ||
|
|
1e399c271a | ||
|
|
544b579cb0 | ||
|
|
daa6bec80a | ||
|
|
34bd41cc0c | ||
|
|
70953b18c0 | ||
|
|
cfe1a860ff | ||
|
|
4dcda432cf | ||
|
|
5c2a885647 | ||
|
|
cd0fc214a5 |
18
.github/dependabot.yml
vendored
18
.github/dependabot.yml
vendored
@@ -5,14 +5,20 @@ updates:
|
||||
schedule:
|
||||
interval: daily
|
||||
ignore:
|
||||
# docker/buildx + docker/cli + docker/docker require coordination to
|
||||
# ensure compatibility between them
|
||||
# docker + moby deps require coordination
|
||||
- dependency-name: "github.com/docker/buildx"
|
||||
# buildx is still 0.x
|
||||
update-types: ["version-update:semver-minor"]
|
||||
- dependency-name: "github.com/moby/buildkit"
|
||||
# buildkit is still 0.x
|
||||
update-types: [ "version-update:semver-minor" ]
|
||||
- dependency-name: "github.com/docker/cli"
|
||||
# docker/cli uses CalVer rather than SemVer
|
||||
update-types: ["version-update:semver-major", "version-update:semver-minor"]
|
||||
update-types: ["version-update:semver-major"]
|
||||
- dependency-name: "github.com/docker/docker"
|
||||
# docker/docker uses CalVer rather than SemVer
|
||||
update-types: ["version-update:semver-major", "version-update:semver-minor"]
|
||||
update-types: ["version-update:semver-major"]
|
||||
- dependency-name: "github.com/containerd/containerd"
|
||||
# containerd major/minor must be kept in sync with moby
|
||||
update-types: [ "version-update:semver-major", "version-update:semver-minor" ]
|
||||
- dependency-name: "go.opentelemetry.io/otel/*"
|
||||
# OTEL is v1.x but has some parts that are not API stable yet
|
||||
update-types: [ "version-update:semver-major", "version-update:semver-minor"]
|
||||
|
||||
80
.github/workflows/ci.yml
vendored
80
.github/workflows/ci.yml
vendored
@@ -19,7 +19,6 @@ on:
|
||||
default: "false"
|
||||
|
||||
env:
|
||||
DESTDIR: "./bin"
|
||||
DOCKER_CLI_VERSION: "20.10.17"
|
||||
|
||||
permissions:
|
||||
@@ -103,7 +102,7 @@ jobs:
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: compose
|
||||
path: ${{ env.DESTDIR }}/*
|
||||
path: ./bin/release/*
|
||||
if-no-files-found: error
|
||||
|
||||
test:
|
||||
@@ -124,13 +123,15 @@ jobs:
|
||||
*.cache-from=type=gha,scope=test
|
||||
*.cache-to=type=gha,scope=test
|
||||
-
|
||||
name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
name: Gather coverage data
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: coverage-data-unit
|
||||
path: bin/coverage/unit/
|
||||
if-no-files-found: error
|
||||
|
||||
e2e:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
DESTDIR: "./bin/build"
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -179,11 +180,17 @@ jobs:
|
||||
name: Test plugin mode
|
||||
if: ${{ matrix.mode == 'plugin' }}
|
||||
run: |
|
||||
rm -rf ./covdatafiles
|
||||
mkdir ./covdatafiles
|
||||
make e2e-compose GOCOVERDIR=covdatafiles
|
||||
go tool covdata textfmt -i=covdatafiles -o=coverage.out
|
||||
|
||||
rm -rf ./bin/coverage/e2e
|
||||
mkdir -p ./bin/coverage/e2e
|
||||
make e2e-compose GOCOVERDIR=bin/coverage/e2e TEST_FLAGS="-v"
|
||||
-
|
||||
name: Gather coverage data
|
||||
if: ${{ matrix.mode == 'plugin' }}
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: coverage-data-e2e
|
||||
path: bin/coverage/e2e/
|
||||
if-no-files-found: error
|
||||
-
|
||||
name: Test standalone mode
|
||||
if: ${{ matrix.mode == 'standalone' }}
|
||||
@@ -196,9 +203,44 @@ jobs:
|
||||
if: ${{ matrix.mode == 'cucumber'}}
|
||||
run: |
|
||||
make test-cucumber
|
||||
-
|
||||
name: Upload coverage to Codecov
|
||||
|
||||
coverage:
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- test
|
||||
- e2e
|
||||
steps:
|
||||
# codecov won't process the report without the source code available
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
check-latest: true
|
||||
- name: Download unit test coverage
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: coverage-data-unit
|
||||
path: coverage/unit
|
||||
- name: Download E2E test coverage
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: coverage-data-e2e
|
||||
path: coverage/e2e
|
||||
- name: Merge coverage reports
|
||||
run: |
|
||||
go tool covdata textfmt -i=./coverage/unit,./coverage/e2e -o ./coverage.txt
|
||||
- name: Store coverage report in GitHub Actions
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: go-covdata-txt
|
||||
path: ./coverage.txt
|
||||
if-no-files-found: error
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
files: ./coverage.txt
|
||||
|
||||
release:
|
||||
permissions:
|
||||
@@ -216,10 +258,10 @@ jobs:
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: compose
|
||||
path: ${{ env.DESTDIR }}
|
||||
path: bin/release
|
||||
-
|
||||
name: Create checksums
|
||||
working-directory: ${{ env.DESTDIR }}
|
||||
working-directory: bin/release
|
||||
run: |
|
||||
find . -type f -print0 | sort -z | xargs -r0 shasum -a 256 -b | sed 's# \*\./# *#' > $RUNNER_TEMP/checksums.txt
|
||||
shasum -a 256 -U -c $RUNNER_TEMP/checksums.txt
|
||||
@@ -227,21 +269,21 @@ jobs:
|
||||
cat checksums.txt | while read sum file; do echo "$sum $file" > ${file#\*}.sha256; done
|
||||
-
|
||||
name: License
|
||||
run: cp packaging/* ${{ env.DESTDIR }}/
|
||||
run: cp packaging/* bin/release/
|
||||
-
|
||||
name: List artifacts
|
||||
run: |
|
||||
tree -nh ${{ env.DESTDIR }}
|
||||
tree -nh bin/release
|
||||
-
|
||||
name: Check artifacts
|
||||
run: |
|
||||
find ${{ env.DESTDIR }} -type f -exec file -e ascii -- {} +
|
||||
find bin/release -type f -exec file -e ascii -- {} +
|
||||
-
|
||||
name: GitHub Release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: ncipollo/release-action@58ae73b360456532aafd58ee170c045abbeaee37 # v1.10.0
|
||||
with:
|
||||
artifacts: ${{ env.DESTDIR }}/*
|
||||
artifacts: bin/release/*
|
||||
generateReleaseNotes: true
|
||||
draft: true
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
31
.github/workflows/merge.yml
vendored
31
.github/workflows/merge.yml
vendored
@@ -76,6 +76,8 @@ jobs:
|
||||
|
||||
bin-image:
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
digest: ${{ fromJSON(steps.bake.outputs.metadata).image-cross['containerimage.digest'] }}
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
@@ -107,6 +109,7 @@ jobs:
|
||||
-
|
||||
name: Build and push image
|
||||
uses: docker/bake-action@v2
|
||||
id: bake
|
||||
with:
|
||||
files: |
|
||||
./docker-bake.hcl
|
||||
@@ -118,3 +121,31 @@ jobs:
|
||||
*.cache-to=type=gha,scope=bin-image,mode=max
|
||||
*.attest=type=sbom
|
||||
*.attest=type=provenance,mode=max,builder-id=https://github.com/${{ env.GITHUB_REPOSITORY }}/actions/runs/${{ env.GITHUB_RUN_ID }}
|
||||
|
||||
desktop-edge-test:
|
||||
runs-on: ubuntu-latest
|
||||
needs: bin-image
|
||||
steps:
|
||||
-
|
||||
name: Generate Token
|
||||
id: generate_token
|
||||
uses: tibdex/github-app-token@v1
|
||||
with:
|
||||
app_id: ${{ vars.DOCKERDESKTOP_APP_ID }}
|
||||
private_key: ${{ secrets.DOCKERDESKTOP_APP_PRIVATEKEY }}
|
||||
repository: docker/${{ secrets.DOCKERDESKTOP_REPO }}
|
||||
-
|
||||
name: Trigger Docker Desktop e2e with edge version
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
github-token: ${{ steps.generate_token.outputs.token }}
|
||||
script: |
|
||||
await github.rest.actions.createWorkflowDispatch({
|
||||
owner: 'docker',
|
||||
repo: '${{ secrets.DOCKERDESKTOP_REPO }}',
|
||||
workflow_id: 'compose-edge-integration.yml',
|
||||
ref: 'main',
|
||||
inputs: {
|
||||
"image-tag": "${{ needs.bin-image.outputs.digest }}"
|
||||
}
|
||||
})
|
||||
|
||||
@@ -31,12 +31,11 @@ linters-settings:
|
||||
- name: package-comments
|
||||
disabled: true
|
||||
depguard:
|
||||
list-type: denylist
|
||||
include-go-root: true
|
||||
packages:
|
||||
# The io/ioutil package has been deprecated.
|
||||
# https://go.dev/doc/go1.16#ioutil
|
||||
- io/ioutil
|
||||
rules:
|
||||
all:
|
||||
deny:
|
||||
- pkg: io/ioutil
|
||||
desc: 'io/ioutil package has been deprecated'
|
||||
gomodguard:
|
||||
blocked:
|
||||
versions:
|
||||
|
||||
20
Dockerfile
20
Dockerfile
@@ -15,9 +15,9 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
ARG GO_VERSION=1.20.4
|
||||
ARG GO_VERSION=1.20.5
|
||||
ARG XX_VERSION=1.2.1
|
||||
ARG GOLANGCI_LINT_VERSION=v1.52.2
|
||||
ARG GOLANGCI_LINT_VERSION=v1.53.2
|
||||
ARG ADDLICENSE_VERSION=v1.0.0
|
||||
|
||||
ARG BUILD_TAGS="e2e"
|
||||
@@ -84,8 +84,8 @@ RUN --mount=type=bind,target=. \
|
||||
--mount=type=bind,from=osxcross,src=/osxsdk,target=/xx-sdk \
|
||||
xx-go --wrap && \
|
||||
if [ "$(xx-info os)" == "darwin" ]; then export CGO_ENABLED=1; fi && \
|
||||
make build GO_BUILDTAGS="$BUILD_TAGS" DESTDIR=/usr/bin && \
|
||||
xx-verify --static /usr/bin/docker-compose
|
||||
make build GO_BUILDTAGS="$BUILD_TAGS" DESTDIR=/out && \
|
||||
xx-verify --static /out/docker-compose
|
||||
|
||||
FROM build-base AS lint
|
||||
ARG BUILD_TAGS
|
||||
@@ -100,11 +100,13 @@ ARG BUILD_TAGS
|
||||
RUN --mount=type=bind,target=. \
|
||||
--mount=type=cache,target=/root/.cache \
|
||||
--mount=type=cache,target=/go/pkg/mod \
|
||||
go test -tags "$BUILD_TAGS" -v -coverprofile=/tmp/coverage.txt -covermode=atomic $(go list $(TAGS) ./... | grep -vE 'e2e') && \
|
||||
go tool cover -func=/tmp/coverage.txt
|
||||
rm -rf /tmp/coverage && \
|
||||
mkdir -p /tmp/coverage && \
|
||||
go test -tags "$BUILD_TAGS" -v -cover -covermode=atomic $(go list $(TAGS) ./... | grep -vE 'e2e') -args -test.gocoverdir="/tmp/coverage" && \
|
||||
go tool covdata percent -i=/tmp/coverage
|
||||
|
||||
FROM scratch AS test-coverage
|
||||
COPY --from=test /tmp/coverage.txt /coverage.txt
|
||||
COPY --from=test --link /tmp/coverage /
|
||||
|
||||
FROM base AS license-set
|
||||
ARG LICENSE_FILES
|
||||
@@ -162,11 +164,11 @@ RUN --mount=target=/context \
|
||||
EOT
|
||||
|
||||
FROM scratch AS binary-unix
|
||||
COPY --link --from=build /usr/bin/docker-compose /
|
||||
COPY --link --from=build /out/docker-compose /
|
||||
FROM binary-unix AS binary-darwin
|
||||
FROM binary-unix AS binary-linux
|
||||
FROM scratch AS binary-windows
|
||||
COPY --link --from=build /usr/bin/docker-compose /docker-compose.exe
|
||||
COPY --link --from=build /out/docker-compose /docker-compose.exe
|
||||
FROM binary-$TARGETOS AS binary
|
||||
# enable scanning for this stage
|
||||
ARG BUILDKIT_SBOM_SCAN_STAGE=true
|
||||
|
||||
35
Makefile
35
Makefile
@@ -17,29 +17,18 @@ VERSION ?= $(shell git describe --match 'v[0-9]*' --dirty='.m' --always --tags)
|
||||
|
||||
GO_LDFLAGS ?= -w -X ${PKG}/internal.Version=${VERSION}
|
||||
GO_BUILDTAGS ?= e2e
|
||||
|
||||
DRIVE_PREFIX?=
|
||||
ifeq ($(OS),Windows_NT)
|
||||
DETECTED_OS = Windows
|
||||
DRIVE_PREFIX=C:
|
||||
else
|
||||
DETECTED_OS = $(shell uname -s)
|
||||
endif
|
||||
|
||||
ifeq ($(DETECTED_OS),Linux)
|
||||
MOBY_DOCKER=/usr/bin/docker
|
||||
endif
|
||||
ifeq ($(DETECTED_OS),Darwin)
|
||||
MOBY_DOCKER=/Applications/Docker.app/Contents/Resources/bin/docker
|
||||
endif
|
||||
ifeq ($(DETECTED_OS),Windows)
|
||||
BINARY_EXT=.exe
|
||||
endif
|
||||
|
||||
TEST_COVERAGE_FLAGS = -coverprofile=coverage.out -covermode=atomic
|
||||
ifneq ($(DETECTED_OS),Windows)
|
||||
# go race detector requires gcc on Windows so not used by default
|
||||
# https://github.com/golang/go/issues/27089
|
||||
TEST_COVERAGE_FLAGS += -race
|
||||
endif
|
||||
BUILD_FLAGS?=
|
||||
TEST_FLAGS?=
|
||||
E2E_TEST?=
|
||||
@@ -49,13 +38,23 @@ else
|
||||
endif
|
||||
|
||||
BUILDX_CMD ?= docker buildx
|
||||
DESTDIR ?= ./bin/build
|
||||
|
||||
# DESTDIR overrides the output path for binaries and other artifacts
|
||||
# this is used by docker/docker-ce-packaging for the apt/rpm builds,
|
||||
# so it's important that the resulting binary ends up EXACTLY at the
|
||||
# path $DESTDIR/docker-compose when specified.
|
||||
#
|
||||
# See https://github.com/docker/docker-ce-packaging/blob/e43fbd37e48fde49d907b9195f23b13537521b94/rpm/SPECS/docker-compose-plugin.spec#L47
|
||||
#
|
||||
# By default, all artifacts go to subdirectories under ./bin/ in the
|
||||
# repo root, e.g. ./bin/build, ./bin/coverage, ./bin/release.
|
||||
DESTDIR ?=
|
||||
|
||||
all: build
|
||||
|
||||
.PHONY: build ## Build the compose cli-plugin
|
||||
build:
|
||||
GO111MODULE=on go build $(BUILD_FLAGS) -trimpath -tags "$(GO_BUILDTAGS)" -ldflags "$(GO_LDFLAGS)" -o "$(DESTDIR)/docker-compose$(BINARY_EXT)" ./cmd
|
||||
GO111MODULE=on go build $(BUILD_FLAGS) -trimpath -tags "$(GO_BUILDTAGS)" -ldflags "$(GO_LDFLAGS)" -o "$(or $(DESTDIR),./bin/build)/docker-compose$(BINARY_EXT)" ./cmd
|
||||
|
||||
.PHONY: binary
|
||||
binary:
|
||||
@@ -68,7 +67,7 @@ binary-with-coverage:
|
||||
.PHONY: install
|
||||
install: binary
|
||||
mkdir -p ~/.docker/cli-plugins
|
||||
install bin/build/docker-compose ~/.docker/cli-plugins/docker-compose
|
||||
install $(or $(DESTDIR),./bin/build)/docker-compose ~/.docker/cli-plugins/docker-compose
|
||||
|
||||
.PHONY: e2e-compose
|
||||
e2e-compose: ## Run end to end local tests in plugin mode. Set E2E_TEST=TestName to run a single test
|
||||
@@ -122,8 +121,8 @@ docs: ## generate documentation
|
||||
$(eval $@_TMP_OUT := $(shell mktemp -d -t compose-output.XXXXXXXXXX))
|
||||
$(BUILDX_CMD) bake --set "*.output=type=local,dest=$($@_TMP_OUT)" docs-update
|
||||
rm -rf ./docs/internal
|
||||
cp -R "$($@_TMP_OUT)"/out/* ./docs/
|
||||
rm -rf "$($@_TMP_OUT)"/*
|
||||
cp -R "$(DRIVE_PREFIX)$($@_TMP_OUT)"/out/* ./docs/
|
||||
rm -rf "$(DRIVE_PREFIX)$($@_TMP_OUT)"/*
|
||||
|
||||
.PHONY: validate-docs
|
||||
validate-docs: ## validate the doc does not change
|
||||
|
||||
131
cmd/cmdtrace/cmd_span.go
Normal file
131
cmd/cmdtrace/cmd_span.go
Normal file
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
Copyright 2023 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package cmdtrace
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
dockercli "github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
commands "github.com/docker/compose/v2/cmd/compose"
|
||||
"github.com/docker/compose/v2/internal/tracing"
|
||||
"github.com/spf13/cobra"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
// Setup should be called as part of the command's PersistentPreRunE
|
||||
// as soon as possible after initializing the dockerCli.
|
||||
//
|
||||
// It initializes the tracer for the CLI using both auto-detection
|
||||
// from the Docker context metadata as well as standard OTEL_ env
|
||||
// vars, creates a root span for the command, and wraps the actual
|
||||
// command invocation to ensure the span is properly finalized and
|
||||
// exported before exit.
|
||||
func Setup(cmd *cobra.Command, dockerCli command.Cli) error {
|
||||
tracingShutdown, err := tracing.InitTracing(dockerCli)
|
||||
if err != nil {
|
||||
return fmt.Errorf("initializing tracing: %w", err)
|
||||
}
|
||||
|
||||
ctx := cmd.Context()
|
||||
ctx, cmdSpan := tracing.Tracer.Start(
|
||||
ctx,
|
||||
"cli/"+strings.Join(commandName(cmd), "-"),
|
||||
)
|
||||
cmd.SetContext(ctx)
|
||||
wrapRunE(cmd, cmdSpan, tracingShutdown)
|
||||
return nil
|
||||
}
|
||||
|
||||
// wrapRunE injects a wrapper function around the command's actual RunE (or Run)
|
||||
// method. This is necessary to capture the command result for reporting as well
|
||||
// as flushing any spans before exit.
|
||||
//
|
||||
// Unfortunately, PersistentPostRun(E) can't be used for this purpose because it
|
||||
// only runs if RunE does _not_ return an error, but this should run unconditionally.
|
||||
func wrapRunE(c *cobra.Command, cmdSpan trace.Span, tracingShutdown tracing.ShutdownFunc) {
|
||||
origRunE := c.RunE
|
||||
if origRunE == nil {
|
||||
origRun := c.Run
|
||||
//nolint:unparam // wrapper function for RunE, always returns nil by design
|
||||
origRunE = func(cmd *cobra.Command, args []string) error {
|
||||
origRun(cmd, args)
|
||||
return nil
|
||||
}
|
||||
c.Run = nil
|
||||
}
|
||||
|
||||
c.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
cmdErr := origRunE(cmd, args)
|
||||
if cmdSpan != nil {
|
||||
if cmdErr != nil && !errors.Is(cmdErr, context.Canceled) {
|
||||
// default exit code is 1 if a more descriptive error
|
||||
// wasn't returned
|
||||
exitCode := 1
|
||||
var statusErr dockercli.StatusError
|
||||
if errors.As(cmdErr, &statusErr) {
|
||||
exitCode = statusErr.StatusCode
|
||||
}
|
||||
cmdSpan.SetStatus(codes.Error, "CLI command returned error")
|
||||
cmdSpan.RecordError(cmdErr, trace.WithAttributes(
|
||||
attribute.Int("exit_code", exitCode),
|
||||
))
|
||||
|
||||
} else {
|
||||
cmdSpan.SetStatus(codes.Ok, "")
|
||||
}
|
||||
cmdSpan.End()
|
||||
}
|
||||
if tracingShutdown != nil {
|
||||
// use background for root context because the cmd's context might have
|
||||
// been canceled already
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
// TODO(milas): add an env var to enable logging from the
|
||||
// OTel components for debugging purposes
|
||||
_ = tracingShutdown(ctx)
|
||||
}
|
||||
return cmdErr
|
||||
}
|
||||
}
|
||||
|
||||
// commandName returns the path components for a given command.
|
||||
//
|
||||
// The root Compose command and anything before (i.e. "docker")
|
||||
// are not included.
|
||||
//
|
||||
// For example:
|
||||
// - docker compose alpha watch -> [alpha, watch]
|
||||
// - docker-compose up -> [up]
|
||||
func commandName(cmd *cobra.Command) []string {
|
||||
var name []string
|
||||
for c := cmd; c != nil; c = c.Parent() {
|
||||
if c.Name() == commands.PluginName {
|
||||
break
|
||||
}
|
||||
name = append(name, c.Name())
|
||||
}
|
||||
sort.Sort(sort.Reverse(sort.StringSlice(name)))
|
||||
return name
|
||||
}
|
||||
@@ -62,10 +62,6 @@ func Convert(args []string) []string {
|
||||
continue
|
||||
}
|
||||
if len(arg) > 0 && arg[0] != '-' {
|
||||
// not a top-level flag anymore, keep the rest of the command unmodified
|
||||
if arg == compose.PluginName {
|
||||
i++
|
||||
}
|
||||
command = append(command, args[i:]...)
|
||||
break
|
||||
}
|
||||
|
||||
@@ -83,6 +83,11 @@ func Test_convert(t *testing.T) {
|
||||
args: []string{"--project-directory", "", "ps"},
|
||||
want: []string{"compose", "--project-directory", "", "ps"},
|
||||
},
|
||||
{
|
||||
name: "compose as project name",
|
||||
args: []string{"--project-name", "compose", "down", "--remove-orphans"},
|
||||
want: []string{"compose", "--project-name", "compose", "down", "--remove-orphans"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
@@ -27,8 +27,7 @@ import (
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
buildx "github.com/docker/buildx/util/progress"
|
||||
cliopts "github.com/docker/cli/opts"
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
"github.com/docker/compose/v2/pkg/utils"
|
||||
ui "github.com/docker/compose/v2/pkg/progress"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
@@ -37,14 +36,14 @@ import (
|
||||
type buildOptions struct {
|
||||
*ProjectOptions
|
||||
composeOptions
|
||||
quiet bool
|
||||
pull bool
|
||||
push bool
|
||||
progress string
|
||||
args []string
|
||||
noCache bool
|
||||
memory cliopts.MemBytes
|
||||
ssh string
|
||||
quiet bool
|
||||
pull bool
|
||||
push bool
|
||||
args []string
|
||||
noCache bool
|
||||
memory cliopts.MemBytes
|
||||
ssh string
|
||||
builder string
|
||||
}
|
||||
|
||||
func (opts buildOptions) toAPIBuildOptions(services []string) (api.BuildOptions, error) {
|
||||
@@ -56,27 +55,25 @@ func (opts buildOptions) toAPIBuildOptions(services []string) (api.BuildOptions,
|
||||
return api.BuildOptions{}, err
|
||||
}
|
||||
}
|
||||
builderName := opts.builder
|
||||
if builderName == "" {
|
||||
builderName = os.Getenv("BUILDX_BUILDER")
|
||||
}
|
||||
|
||||
return api.BuildOptions{
|
||||
Pull: opts.pull,
|
||||
Push: opts.push,
|
||||
Progress: opts.progress,
|
||||
Progress: ui.Mode,
|
||||
Args: types.NewMappingWithEquals(opts.args),
|
||||
NoCache: opts.noCache,
|
||||
Quiet: opts.quiet,
|
||||
Services: services,
|
||||
SSHs: SSHKeys,
|
||||
Builder: builderName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
var printerModes = []string{
|
||||
buildx.PrinterModeAuto,
|
||||
buildx.PrinterModeTty,
|
||||
buildx.PrinterModePlain,
|
||||
buildx.PrinterModeQuiet,
|
||||
}
|
||||
|
||||
func buildCommand(p *ProjectOptions, backend api.Service) *cobra.Command {
|
||||
func buildCommand(p *ProjectOptions, progress *string, backend api.Service) *cobra.Command {
|
||||
opts := buildOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
@@ -85,24 +82,21 @@ func buildCommand(p *ProjectOptions, backend api.Service) *cobra.Command {
|
||||
Short: "Build or rebuild services",
|
||||
PreRunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
if opts.quiet {
|
||||
opts.progress = buildx.PrinterModeQuiet
|
||||
ui.Mode = ui.ModeQuiet
|
||||
devnull, err := os.Open(os.DevNull)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
os.Stdout = devnull
|
||||
}
|
||||
if !utils.StringContains(printerModes, opts.progress) {
|
||||
return fmt.Errorf("unsupported --progress value %q", opts.progress)
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
RunE: AdaptCmd(func(ctx context.Context, cmd *cobra.Command, args []string) error {
|
||||
if cmd.Flags().Changed("ssh") && opts.ssh == "" {
|
||||
opts.ssh = "default"
|
||||
}
|
||||
if progress.Mode == progress.ModePlain && !cmd.Flags().Changed("progress") {
|
||||
opts.progress = buildx.PrinterModePlain
|
||||
if cmd.Flags().Changed("progress") && opts.ssh == "" {
|
||||
fmt.Fprint(os.Stderr, "--progress is a global compose flag, better use `docker compose --progress xx build ...")
|
||||
}
|
||||
return runBuild(ctx, backend, opts, args)
|
||||
}),
|
||||
@@ -111,9 +105,9 @@ func buildCommand(p *ProjectOptions, backend api.Service) *cobra.Command {
|
||||
cmd.Flags().BoolVar(&opts.push, "push", false, "Push service images.")
|
||||
cmd.Flags().BoolVarP(&opts.quiet, "quiet", "q", false, "Don't print anything to STDOUT")
|
||||
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().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().StringVar(&opts.builder, "builder", "", "Set builder to use.")
|
||||
cmd.Flags().Bool("parallel", true, "Build images in parallel. DEPRECATED")
|
||||
cmd.Flags().MarkHidden("parallel") //nolint:errcheck
|
||||
cmd.Flags().Bool("compress", true, "Compress the build context using gzip. DEPRECATED")
|
||||
@@ -124,6 +118,8 @@ func buildCommand(p *ProjectOptions, backend api.Service) *cobra.Command {
|
||||
cmd.Flags().Bool("no-rm", false, "Do not remove intermediate containers after a successful build. DEPRECATED")
|
||||
cmd.Flags().MarkHidden("no-rm") //nolint:errcheck
|
||||
cmd.Flags().VarP(&opts.memory, "memory", "m", "Set memory limit for the build container. Not supported by BuildKit.")
|
||||
cmd.Flags().StringVar(progress, "progress", buildx.PrinterModeAuto, fmt.Sprintf(`Set type of ui output (%s)`, strings.Join(printerModes, ", ")))
|
||||
cmd.Flags().MarkHidden("progress") //nolint:errcheck
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
buildx "github.com/docker/buildx/util/progress"
|
||||
"github.com/docker/cli/cli/command"
|
||||
|
||||
"github.com/compose-spec/compose-go/cli"
|
||||
@@ -43,7 +44,7 @@ import (
|
||||
"github.com/docker/compose/v2/cmd/formatter"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/compose"
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
ui "github.com/docker/compose/v2/pkg/progress"
|
||||
"github.com/docker/compose/v2/pkg/utils"
|
||||
)
|
||||
|
||||
@@ -175,7 +176,7 @@ func (o *ProjectOptions) toProjectName() (string, error) {
|
||||
return o.ProjectName, nil
|
||||
}
|
||||
|
||||
envProjectName := os.Getenv("ComposeProjectName")
|
||||
envProjectName := os.Getenv(ComposeProjectName)
|
||||
if envProjectName != "" {
|
||||
return envProjectName, nil
|
||||
}
|
||||
@@ -273,6 +274,7 @@ func RootCommand(streams command.Cli, backend api.Service) *cobra.Command { //no
|
||||
version bool
|
||||
parallel int
|
||||
dryRun bool
|
||||
progress string
|
||||
)
|
||||
c := &cobra.Command{
|
||||
Short: "Docker Compose",
|
||||
@@ -326,16 +328,36 @@ func RootCommand(streams command.Cli, backend api.Service) *cobra.Command { //no
|
||||
formatter.SetANSIMode(streams, ansi)
|
||||
|
||||
if noColor, ok := os.LookupEnv("NO_COLOR"); ok && noColor != "" {
|
||||
progress.NoColor()
|
||||
ui.NoColor()
|
||||
formatter.SetANSIMode(streams, formatter.Never)
|
||||
}
|
||||
|
||||
switch ansi {
|
||||
case "never":
|
||||
progress.Mode = progress.ModePlain
|
||||
ui.Mode = ui.ModePlain
|
||||
case "always":
|
||||
progress.Mode = progress.ModeTTY
|
||||
ui.Mode = ui.ModeTTY
|
||||
}
|
||||
|
||||
switch progress {
|
||||
case ui.ModeAuto:
|
||||
ui.Mode = ui.ModeAuto
|
||||
case ui.ModeTTY:
|
||||
if ansi == "never" {
|
||||
return fmt.Errorf("can't use --progress tty while ANSI support is disabled")
|
||||
}
|
||||
ui.Mode = ui.ModeTTY
|
||||
case ui.ModePlain:
|
||||
if ansi == "always" {
|
||||
return fmt.Errorf("can't use --progress plain while ANSI support is forced")
|
||||
}
|
||||
ui.Mode = ui.ModePlain
|
||||
case ui.ModeQuiet, "none":
|
||||
ui.Mode = ui.ModeQuiet
|
||||
default:
|
||||
return fmt.Errorf("unsupported --progress value %q", progress)
|
||||
}
|
||||
|
||||
if opts.WorkDir != "" {
|
||||
if opts.ProjectDir != "" {
|
||||
return errors.New(`cannot specify DEPRECATED "--workdir" and "--project-directory". Please use only "--project-directory" instead`)
|
||||
@@ -404,11 +426,13 @@ func RootCommand(streams command.Cli, backend api.Service) *cobra.Command { //no
|
||||
portCommand(&opts, streams, backend),
|
||||
imagesCommand(&opts, streams, backend),
|
||||
versionCommand(streams),
|
||||
buildCommand(&opts, backend),
|
||||
buildCommand(&opts, &progress, backend),
|
||||
publishCommand(&opts, backend),
|
||||
pushCommand(&opts, backend),
|
||||
pullCommand(&opts, backend),
|
||||
createCommand(&opts, backend),
|
||||
copyCommand(&opts, backend),
|
||||
waitCommand(&opts, backend),
|
||||
alphaCommand(&opts, backend),
|
||||
)
|
||||
|
||||
@@ -425,6 +449,8 @@ func RootCommand(streams command.Cli, backend api.Service) *cobra.Command { //no
|
||||
},
|
||||
)
|
||||
|
||||
c.Flags().StringVar(&progress, "progress", buildx.PrinterModeAuto, fmt.Sprintf(`Set type of progress output (%s)`, strings.Join(printerModes, ", ")))
|
||||
|
||||
c.Flags().StringVar(&ansi, "ansi", "auto", `Control when to print ANSI control characters ("never"|"always"|"auto")`)
|
||||
c.Flags().IntVar(¶llel, "parallel", -1, `Control max parallelism, -1 for unlimited`)
|
||||
c.Flags().BoolVarP(&version, "version", "v", false, "Show the Docker Compose version information")
|
||||
@@ -460,3 +486,10 @@ func setEnvWithDotEnv(prjOpts *ProjectOptions) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var printerModes = []string{
|
||||
ui.ModeAuto,
|
||||
ui.ModeTTY,
|
||||
ui.ModePlain,
|
||||
ui.ModeQuiet,
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ func downCommand(p *ProjectOptions, backend api.Service) *cobra.Command {
|
||||
ProjectOptions: p,
|
||||
}
|
||||
downCmd := &cobra.Command{
|
||||
Use: "down [OPTIONS]",
|
||||
Use: "down [OPTIONS] [SERVICES]",
|
||||
Short: "Stop and remove containers, networks",
|
||||
PreRunE: AdaptCmd(func(ctx context.Context, cmd *cobra.Command, args []string) error {
|
||||
opts.timeChanged = cmd.Flags().Changed("timeout")
|
||||
@@ -56,16 +56,15 @@ func downCommand(p *ProjectOptions, backend api.Service) *cobra.Command {
|
||||
return nil
|
||||
}),
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runDown(ctx, backend, opts)
|
||||
return runDown(ctx, backend, opts, args)
|
||||
}),
|
||||
Args: cobra.NoArgs,
|
||||
ValidArgsFunction: noCompletion(),
|
||||
}
|
||||
flags := downCmd.Flags()
|
||||
removeOrphans := utils.StringToBool(os.Getenv(ComposeRemoveOrphans))
|
||||
flags.BoolVar(&opts.removeOrphans, "remove-orphans", removeOrphans, "Remove containers for services not defined in the Compose file.")
|
||||
flags.IntVarP(&opts.timeout, "timeout", "t", 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.IntVarP(&opts.timeout, "timeout", "t", 0, "Specify a shutdown timeout in seconds")
|
||||
flags.BoolVarP(&opts.volumes, "volumes", "v", false, `Remove named volumes declared in the "volumes" section of the Compose file and anonymous volumes attached to containers.`)
|
||||
flags.StringVar(&opts.images, "rmi", "", `Remove images used by services. "local" remove only images that don't have a custom tag ("local"|"all")`)
|
||||
flags.SetNormalizeFunc(func(f *pflag.FlagSet, name string) pflag.NormalizedName {
|
||||
if name == "volume" {
|
||||
@@ -77,7 +76,7 @@ func downCommand(p *ProjectOptions, backend api.Service) *cobra.Command {
|
||||
return downCmd
|
||||
}
|
||||
|
||||
func runDown(ctx context.Context, backend api.Service, opts downOptions) error {
|
||||
func runDown(ctx context.Context, backend api.Service, opts downOptions, services []string) error {
|
||||
project, name, err := opts.projectOrName()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -94,5 +93,6 @@ func runDown(ctx context.Context, backend api.Service, opts downOptions) error {
|
||||
Timeout: timeout,
|
||||
Images: opts.images,
|
||||
Volumes: opts.volumes,
|
||||
Services: services,
|
||||
})
|
||||
}
|
||||
|
||||
55
cmd/compose/publish.go
Normal file
55
cmd/compose/publish.go
Normal file
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package compose
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
)
|
||||
|
||||
type publishOptions struct {
|
||||
*ProjectOptions
|
||||
composeOptions
|
||||
Repository string
|
||||
}
|
||||
|
||||
func publishCommand(p *ProjectOptions, backend api.Service) *cobra.Command {
|
||||
opts := pushOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
publishCmd := &cobra.Command{
|
||||
Use: "publish [OPTIONS] [REPOSITORY]",
|
||||
Short: "Publish compose application",
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runPublish(ctx, backend, opts, args[0])
|
||||
}),
|
||||
Args: cobra.ExactArgs(1),
|
||||
}
|
||||
return publishCmd
|
||||
}
|
||||
|
||||
func runPublish(ctx context.Context, backend api.Service, opts pushOptions, repository string) error {
|
||||
project, err := opts.ToProject(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return backend.Publish(ctx, project, repository)
|
||||
}
|
||||
@@ -31,6 +31,7 @@ type pushOptions struct {
|
||||
IncludeDeps bool
|
||||
Ignorefailures bool
|
||||
Quiet bool
|
||||
Repository string
|
||||
}
|
||||
|
||||
func pushCommand(p *ProjectOptions, backend api.Service) *cobra.Command {
|
||||
@@ -48,6 +49,7 @@ func pushCommand(p *ProjectOptions, backend api.Service) *cobra.Command {
|
||||
pushCmd.Flags().BoolVar(&opts.Ignorefailures, "ignore-push-failures", false, "Push what it can and ignores images with push failures")
|
||||
pushCmd.Flags().BoolVar(&opts.IncludeDeps, "include-deps", false, "Also push images of services declared as dependencies")
|
||||
pushCmd.Flags().BoolVarP(&opts.Quiet, "quiet", "q", false, "Push without printing progress information")
|
||||
pushCmd.Flags().StringVarP(&opts.Repository, "repository", "r", "", "Also publish the compose application in repository")
|
||||
|
||||
return pushCmd
|
||||
}
|
||||
@@ -68,5 +70,6 @@ func runPush(ctx context.Context, backend api.Service, opts pushOptions, service
|
||||
return backend.Push(ctx, project, api.PushOptions{
|
||||
IgnoreFailures: opts.Ignorefailures,
|
||||
Quiet: opts.Quiet,
|
||||
Repository: opts.Repository,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -28,8 +28,9 @@ import (
|
||||
|
||||
type restartOptions struct {
|
||||
*ProjectOptions
|
||||
timeout int
|
||||
noDeps bool
|
||||
timeChanged bool
|
||||
timeout int
|
||||
noDeps bool
|
||||
}
|
||||
|
||||
func restartCommand(p *ProjectOptions, backend api.Service) *cobra.Command {
|
||||
@@ -39,13 +40,16 @@ func restartCommand(p *ProjectOptions, backend api.Service) *cobra.Command {
|
||||
restartCmd := &cobra.Command{
|
||||
Use: "restart [OPTIONS] [SERVICE...]",
|
||||
Short: "Restart service containers",
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
opts.timeChanged = cmd.Flags().Changed("timeout")
|
||||
},
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runRestart(ctx, backend, opts, args)
|
||||
}),
|
||||
ValidArgsFunction: completeServiceNames(p),
|
||||
}
|
||||
flags := restartCmd.Flags()
|
||||
flags.IntVarP(&opts.timeout, "timeout", "t", 10, "Specify a shutdown timeout in seconds")
|
||||
flags.IntVarP(&opts.timeout, "timeout", "t", 0, "Specify a shutdown timeout in seconds")
|
||||
flags.BoolVar(&opts.noDeps, "no-deps", false, "Don't restart dependent services.")
|
||||
|
||||
return restartCmd
|
||||
@@ -57,6 +61,12 @@ func runRestart(ctx context.Context, backend api.Service, opts restartOptions, s
|
||||
return err
|
||||
}
|
||||
|
||||
var timeout *time.Duration
|
||||
if opts.timeChanged {
|
||||
timeoutValue := time.Duration(opts.timeout) * time.Second
|
||||
timeout = &timeoutValue
|
||||
}
|
||||
|
||||
if opts.noDeps {
|
||||
err := project.ForServices(services, types.IgnoreDependencies)
|
||||
if err != nil {
|
||||
@@ -64,9 +74,8 @@ func runRestart(ctx context.Context, backend api.Service, opts restartOptions, s
|
||||
}
|
||||
}
|
||||
|
||||
timeout := time.Duration(opts.timeout) * time.Second
|
||||
return backend.Restart(ctx, name, api.RestartOptions{
|
||||
Timeout: &timeout,
|
||||
Timeout: timeout,
|
||||
Services: services,
|
||||
Project: project,
|
||||
})
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
cgo "github.com/compose-spec/compose-go/cli"
|
||||
"github.com/compose-spec/compose-go/loader"
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/mattn/go-shellwords"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
@@ -48,6 +49,8 @@ type runOptions struct {
|
||||
workdir string
|
||||
entrypoint string
|
||||
entrypointCmd []string
|
||||
capAdd opts.ListOpts
|
||||
capDrop opts.ListOpts
|
||||
labels []string
|
||||
volumes []string
|
||||
publish []string
|
||||
@@ -59,20 +62,20 @@ type runOptions struct {
|
||||
quietPull bool
|
||||
}
|
||||
|
||||
func (opts runOptions) apply(project *types.Project) error {
|
||||
target, err := project.GetService(opts.Service)
|
||||
func (options runOptions) apply(project *types.Project) error {
|
||||
target, err := project.GetService(options.Service)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
target.Tty = !opts.noTty
|
||||
target.StdinOpen = opts.interactive
|
||||
if !opts.servicePorts {
|
||||
target.Tty = !options.noTty
|
||||
target.StdinOpen = options.interactive
|
||||
if !options.servicePorts {
|
||||
target.Ports = []types.ServicePortConfig{}
|
||||
}
|
||||
if len(opts.publish) > 0 {
|
||||
if len(options.publish) > 0 {
|
||||
target.Ports = []types.ServicePortConfig{}
|
||||
for _, p := range opts.publish {
|
||||
for _, p := range options.publish {
|
||||
config, err := types.ParsePortConfig(p)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -80,8 +83,8 @@ func (opts runOptions) apply(project *types.Project) error {
|
||||
target.Ports = append(target.Ports, config...)
|
||||
}
|
||||
}
|
||||
if len(opts.volumes) > 0 {
|
||||
for _, v := range opts.volumes {
|
||||
if len(options.volumes) > 0 {
|
||||
for _, v := range options.volumes {
|
||||
volume, err := loader.ParseVolume(v)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -90,15 +93,15 @@ func (opts runOptions) apply(project *types.Project) error {
|
||||
}
|
||||
}
|
||||
|
||||
if opts.noDeps {
|
||||
err := project.ForServices([]string{opts.Service}, types.IgnoreDependencies)
|
||||
if options.noDeps {
|
||||
err := project.ForServices([]string{options.Service}, types.IgnoreDependencies)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for i, s := range project.Services {
|
||||
if s.Name == opts.Service {
|
||||
if s.Name == options.Service {
|
||||
project.Services[i] = target
|
||||
break
|
||||
}
|
||||
@@ -107,10 +110,12 @@ func (opts runOptions) apply(project *types.Project) error {
|
||||
}
|
||||
|
||||
func runCommand(p *ProjectOptions, streams api.Streams, backend api.Service) *cobra.Command {
|
||||
opts := runOptions{
|
||||
options := runOptions{
|
||||
composeOptions: &composeOptions{
|
||||
ProjectOptions: p,
|
||||
},
|
||||
capAdd: opts.NewListOpts(nil),
|
||||
capDrop: opts.NewListOpts(nil),
|
||||
}
|
||||
createOpts := createOptions{}
|
||||
cmd := &cobra.Command{
|
||||
@@ -118,61 +123,63 @@ func runCommand(p *ProjectOptions, streams api.Streams, backend api.Service) *co
|
||||
Short: "Run a one-off command on a service.",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
PreRunE: AdaptCmd(func(ctx context.Context, cmd *cobra.Command, args []string) error {
|
||||
opts.Service = args[0]
|
||||
options.Service = args[0]
|
||||
if len(args) > 1 {
|
||||
opts.Command = args[1:]
|
||||
options.Command = args[1:]
|
||||
}
|
||||
if len(opts.publish) > 0 && opts.servicePorts {
|
||||
if len(options.publish) > 0 && options.servicePorts {
|
||||
return fmt.Errorf("--service-ports and --publish are incompatible")
|
||||
}
|
||||
if cmd.Flags().Changed("entrypoint") {
|
||||
command, err := shellwords.Parse(opts.entrypoint)
|
||||
command, err := shellwords.Parse(options.entrypoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opts.entrypointCmd = command
|
||||
options.entrypointCmd = command
|
||||
}
|
||||
if cmd.Flags().Changed("tty") {
|
||||
if cmd.Flags().Changed("no-TTY") {
|
||||
return fmt.Errorf("--tty and --no-TTY can't be used together")
|
||||
} else {
|
||||
opts.noTty = !opts.tty
|
||||
options.noTty = !options.tty
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
project, err := p.ToProject([]string{opts.Service}, cgo.WithResolvedPaths(true), cgo.WithDiscardEnvFile)
|
||||
project, err := p.ToProject([]string{options.Service}, cgo.WithResolvedPaths(true), cgo.WithDiscardEnvFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
opts.ignoreOrphans = utils.StringToBool(project.Environment[ComposeIgnoreOrphans])
|
||||
return runRun(ctx, backend, project, opts, createOpts, streams)
|
||||
options.ignoreOrphans = utils.StringToBool(project.Environment[ComposeIgnoreOrphans])
|
||||
return runRun(ctx, backend, project, options, createOpts, streams)
|
||||
}),
|
||||
ValidArgsFunction: completeServiceNames(p),
|
||||
}
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVarP(&opts.Detach, "detach", "d", false, "Run container in background and print container ID")
|
||||
flags.StringArrayVarP(&opts.environment, "env", "e", []string{}, "Set environment variables")
|
||||
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.BoolVarP(&opts.noTty, "no-TTY", "T", !streams.Out().IsTerminal(), "Disable pseudo-TTY allocation (default: auto-detected).")
|
||||
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.workdir, "workdir", "w", "", "Working directory inside the container")
|
||||
flags.StringVar(&opts.entrypoint, "entrypoint", "", "Override the entrypoint of the image")
|
||||
flags.BoolVar(&opts.noDeps, "no-deps", false, "Don't start linked services.")
|
||||
flags.StringArrayVarP(&opts.volumes, "volume", "v", []string{}, "Bind mount a volume.")
|
||||
flags.StringArrayVarP(&opts.publish, "publish", "p", []string{}, "Publish a container's port(s) to the host.")
|
||||
flags.BoolVar(&opts.useAliases, "use-aliases", false, "Use the service's network useAliases in the network(s) the container connects to.")
|
||||
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.BoolVarP(&options.Detach, "detach", "d", false, "Run container in background and print container ID")
|
||||
flags.StringArrayVarP(&options.environment, "env", "e", []string{}, "Set environment variables")
|
||||
flags.StringArrayVarP(&options.labels, "label", "l", []string{}, "Add or override a label")
|
||||
flags.BoolVar(&options.Remove, "rm", false, "Automatically remove the container when it exits")
|
||||
flags.BoolVarP(&options.noTty, "no-TTY", "T", !streams.Out().IsTerminal(), "Disable pseudo-TTY allocation (default: auto-detected).")
|
||||
flags.StringVar(&options.name, "name", "", "Assign a name to the container")
|
||||
flags.StringVarP(&options.user, "user", "u", "", "Run as specified username or uid")
|
||||
flags.StringVarP(&options.workdir, "workdir", "w", "", "Working directory inside the container")
|
||||
flags.StringVar(&options.entrypoint, "entrypoint", "", "Override the entrypoint of the image")
|
||||
flags.Var(&options.capAdd, "cap-add", "Add Linux capabilities")
|
||||
flags.Var(&options.capDrop, "cap-drop", "Drop Linux capabilities")
|
||||
flags.BoolVar(&options.noDeps, "no-deps", false, "Don't start linked services.")
|
||||
flags.StringArrayVarP(&options.volumes, "volume", "v", []string{}, "Bind mount a volume.")
|
||||
flags.StringArrayVarP(&options.publish, "publish", "p", []string{}, "Publish a container's port(s) to the host.")
|
||||
flags.BoolVar(&options.useAliases, "use-aliases", false, "Use the service's network useAliases in the network(s) the container connects to.")
|
||||
flags.BoolVar(&options.servicePorts, "service-ports", false, "Run command with the service's ports enabled and mapped to the host.")
|
||||
flags.BoolVar(&options.quietPull, "quiet-pull", false, "Pull without printing progress information.")
|
||||
flags.BoolVar(&createOpts.Build, "build", false, "Build image before starting container.")
|
||||
flags.BoolVar(&createOpts.removeOrphans, "remove-orphans", false, "Remove containers for services not defined in the Compose file.")
|
||||
|
||||
cmd.Flags().BoolVarP(&opts.interactive, "interactive", "i", true, "Keep STDIN open even if not attached.")
|
||||
cmd.Flags().BoolVarP(&opts.tty, "tty", "t", true, "Allocate a pseudo-TTY.")
|
||||
cmd.Flags().BoolVarP(&options.interactive, "interactive", "i", true, "Keep STDIN open even if not attached.")
|
||||
cmd.Flags().BoolVarP(&options.tty, "tty", "t", true, "Allocate a pseudo-TTY.")
|
||||
cmd.Flags().MarkHidden("tty") //nolint:errcheck
|
||||
|
||||
flags.SetNormalizeFunc(normalizeRunFlags)
|
||||
@@ -190,8 +197,8 @@ func normalizeRunFlags(f *pflag.FlagSet, name string) pflag.NormalizedName {
|
||||
return pflag.NormalizedName(name)
|
||||
}
|
||||
|
||||
func runRun(ctx context.Context, backend api.Service, project *types.Project, opts runOptions, createOpts createOptions, streams api.Streams) error {
|
||||
err := opts.apply(project)
|
||||
func runRun(ctx context.Context, backend api.Service, project *types.Project, options runOptions, createOpts createOptions, streams api.Streams) error {
|
||||
err := options.apply(project)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -202,14 +209,14 @@ func runRun(ctx context.Context, backend api.Service, project *types.Project, op
|
||||
}
|
||||
|
||||
err = progress.Run(ctx, func(ctx context.Context) error {
|
||||
return startDependencies(ctx, backend, *project, opts.Service, opts.ignoreOrphans)
|
||||
return startDependencies(ctx, backend, *project, options.Service, options.ignoreOrphans)
|
||||
}, streams.Err())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
labels := types.Labels{}
|
||||
for _, s := range opts.labels {
|
||||
for _, s := range options.labels {
|
||||
parts := strings.SplitN(s, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
return fmt.Errorf("label must be set as KEY=VALUE")
|
||||
@@ -219,27 +226,29 @@ func runRun(ctx context.Context, backend api.Service, project *types.Project, op
|
||||
|
||||
// start container and attach to container streams
|
||||
runOpts := api.RunOptions{
|
||||
Name: opts.name,
|
||||
Service: opts.Service,
|
||||
Command: opts.Command,
|
||||
Detach: opts.Detach,
|
||||
AutoRemove: opts.Remove,
|
||||
Tty: !opts.noTty,
|
||||
Interactive: opts.interactive,
|
||||
WorkingDir: opts.workdir,
|
||||
User: opts.user,
|
||||
Environment: opts.environment,
|
||||
Entrypoint: opts.entrypointCmd,
|
||||
Name: options.name,
|
||||
Service: options.Service,
|
||||
Command: options.Command,
|
||||
Detach: options.Detach,
|
||||
AutoRemove: options.Remove,
|
||||
Tty: !options.noTty,
|
||||
Interactive: options.interactive,
|
||||
WorkingDir: options.workdir,
|
||||
User: options.user,
|
||||
CapAdd: options.capAdd.GetAll(),
|
||||
CapDrop: options.capDrop.GetAll(),
|
||||
Environment: options.environment,
|
||||
Entrypoint: options.entrypointCmd,
|
||||
Labels: labels,
|
||||
UseNetworkAliases: opts.useAliases,
|
||||
NoDeps: opts.noDeps,
|
||||
UseNetworkAliases: options.useAliases,
|
||||
NoDeps: options.noDeps,
|
||||
Index: 0,
|
||||
QuietPull: opts.quietPull,
|
||||
QuietPull: options.quietPull,
|
||||
}
|
||||
|
||||
for i, service := range project.Services {
|
||||
if service.Name == opts.Service {
|
||||
service.StdinOpen = opts.interactive
|
||||
if service.Name == options.Service {
|
||||
service.StdinOpen = options.interactive
|
||||
project.Services[i] = service
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ func stopCommand(p *ProjectOptions, backend api.Service) *cobra.Command {
|
||||
ValidArgsFunction: completeServiceNames(p),
|
||||
}
|
||||
flags := cmd.Flags()
|
||||
flags.IntVarP(&opts.timeout, "timeout", "t", 10, "Specify a shutdown timeout in seconds")
|
||||
flags.IntVarP(&opts.timeout, "timeout", "t", 0, "Specify a shutdown timeout in seconds")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ func upCommand(p *ProjectOptions, streams api.Streams, backend api.Service) *cob
|
||||
Short: "Create and start containers",
|
||||
PreRunE: AdaptCmd(func(ctx context.Context, cmd *cobra.Command, args []string) error {
|
||||
create.pullChanged = cmd.Flags().Changed("pull")
|
||||
create.timeChanged = cmd.Flags().Changed("waitTimeout")
|
||||
create.timeChanged = cmd.Flags().Changed("timeout")
|
||||
return validateFlags(&up, &create)
|
||||
}),
|
||||
RunE: p.WithServices(func(ctx context.Context, project *types.Project, services []string) error {
|
||||
@@ -104,7 +104,7 @@ func upCommand(p *ProjectOptions, streams api.Streams, backend api.Service) *cob
|
||||
flags.BoolVar(&up.noStart, "no-start", false, "Don't start the services after creating them.")
|
||||
flags.BoolVar(&up.cascadeStop, "abort-on-container-exit", false, "Stops all containers if any container was stopped. Incompatible with -d")
|
||||
flags.StringVar(&up.exitCodeFrom, "exit-code-from", "", "Return the exit code of the selected service container. Implies --abort-on-container-exit")
|
||||
flags.IntVarP(&create.timeout, "timeout", "t", 10, "Use this timeout in seconds for container shutdown when attached or when containers are already running.")
|
||||
flags.IntVarP(&create.timeout, "timeout", "t", 0, "Use this timeout in seconds for container shutdown when attached or when containers are already running.")
|
||||
flags.BoolVar(&up.timestamp, "timestamps", false, "Show timestamps.")
|
||||
flags.BoolVar(&up.noDeps, "no-deps", false, "Don't start linked services.")
|
||||
flags.BoolVar(&create.recreateDeps, "always-recreate-deps", false, "Recreate dependent containers. Incompatible with --no-recreate.")
|
||||
|
||||
72
cmd/compose/wait.go
Normal file
72
cmd/compose/wait.go
Normal file
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
Copyright 2023 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package compose
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type waitOptions struct {
|
||||
*ProjectOptions
|
||||
|
||||
services []string
|
||||
|
||||
downProject bool
|
||||
}
|
||||
|
||||
func waitCommand(p *ProjectOptions, backend api.Service) *cobra.Command {
|
||||
opts := waitOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
|
||||
var statusCode int64
|
||||
var err error
|
||||
cmd := &cobra.Command{
|
||||
Use: "wait SERVICE [SERVICE...] [OPTIONS]",
|
||||
Short: "Block until the first service container stops",
|
||||
Args: cli.RequiresMinArgs(1),
|
||||
RunE: Adapt(func(ctx context.Context, services []string) error {
|
||||
opts.services = services
|
||||
statusCode, err = runWait(ctx, backend, &opts)
|
||||
return err
|
||||
}),
|
||||
PostRun: func(cmd *cobra.Command, args []string) {
|
||||
os.Exit(int(statusCode))
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVar(&opts.downProject, "down-project", false, "Drops project when the first container stops")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runWait(ctx context.Context, backend api.Service, opts *waitOptions) (int64, error) {
|
||||
_, name, err := opts.projectOrName()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return backend.Wait(ctx, name, api.WaitOptions{
|
||||
Services: opts.services,
|
||||
DownProjectOnContainerExit: opts.downProject,
|
||||
})
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"github.com/docker/cli/cli-plugins/manager"
|
||||
"github.com/docker/cli/cli-plugins/plugin"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/compose/v2/cmd/cmdtrace"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/compose/v2/cmd/compatibility"
|
||||
@@ -38,14 +39,20 @@ func pluginMain() {
|
||||
cmd := commands.RootCommand(dockerCli, serviceProxy)
|
||||
originalPreRun := cmd.PersistentPreRunE
|
||||
cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
|
||||
// initialize the dockerCli instance
|
||||
if err := plugin.PersistentPreRunE(cmd, args); err != nil {
|
||||
return err
|
||||
}
|
||||
// TODO(milas): add an env var to enable logging from the
|
||||
// OTel components for debugging purposes
|
||||
_ = cmdtrace.Setup(cmd, dockerCli)
|
||||
|
||||
if originalPreRun != nil {
|
||||
return originalPreRun(cmd, args)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
cmd.SetFlagErrorFunc(func(c *cobra.Command, err error) error {
|
||||
return dockercli.StatusError{
|
||||
StatusCode: compose.CommandSyntaxFailure.ExitCode,
|
||||
|
||||
21
codecov.yml
Normal file
21
codecov.yml
Normal file
@@ -0,0 +1,21 @@
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
informational: true
|
||||
target: auto
|
||||
threshold: 2%
|
||||
patch:
|
||||
default:
|
||||
informational: true
|
||||
|
||||
comment:
|
||||
require_changes: true
|
||||
|
||||
ignore:
|
||||
- "packaging"
|
||||
- "docs"
|
||||
- "bin"
|
||||
- "e2e"
|
||||
- "pkg/e2e"
|
||||
- "**/*_test.go"
|
||||
@@ -25,13 +25,16 @@ variable "DOCS_FORMATS" {
|
||||
default = "md,yaml"
|
||||
}
|
||||
|
||||
# Defines the output folder
|
||||
# Defines the output folder to override the default behavior.
|
||||
# See Makefile for details, this is generally only useful for
|
||||
# the packaging scripts and care should be taken to not break
|
||||
# them.
|
||||
variable "DESTDIR" {
|
||||
default = ""
|
||||
}
|
||||
function "bindir" {
|
||||
function "outdir" {
|
||||
params = [defaultdir]
|
||||
result = DESTDIR != "" ? DESTDIR : "./bin/${defaultdir}"
|
||||
result = DESTDIR != "" ? DESTDIR : "${defaultdir}"
|
||||
}
|
||||
|
||||
# Special target: https://github.com/docker/metadata-action#bake-definition
|
||||
@@ -84,23 +87,23 @@ target "vendor-update" {
|
||||
target "test" {
|
||||
inherits = ["_common"]
|
||||
target = "test-coverage"
|
||||
output = [bindir("coverage")]
|
||||
output = [outdir("./bin/coverage/unit")]
|
||||
}
|
||||
|
||||
target "binary-with-coverage" {
|
||||
inherits = ["_common"]
|
||||
target = "binary"
|
||||
args = {
|
||||
BUILD_FLAGS = "-cover"
|
||||
BUILD_FLAGS = "-cover -covermode=atomic"
|
||||
}
|
||||
output = [bindir("build")]
|
||||
output = [outdir("./bin/build")]
|
||||
platforms = ["local"]
|
||||
}
|
||||
|
||||
target "binary" {
|
||||
inherits = ["_common"]
|
||||
target = "binary"
|
||||
output = [bindir("build")]
|
||||
output = [outdir("./bin/build")]
|
||||
platforms = ["local"]
|
||||
}
|
||||
|
||||
@@ -124,7 +127,7 @@ target "binary-cross" {
|
||||
target "release" {
|
||||
inherits = ["binary-cross"]
|
||||
target = "release"
|
||||
output = [bindir("release")]
|
||||
output = [outdir("./bin/release")]
|
||||
}
|
||||
|
||||
target "docs-validate" {
|
||||
|
||||
@@ -33,6 +33,7 @@ Define and run multi-container applications with Docker.
|
||||
| [`unpause`](compose_unpause.md) | Unpause services |
|
||||
| [`up`](compose_up.md) | Create and start containers |
|
||||
| [`version`](compose_version.md) | Show the Docker Compose version information |
|
||||
| [`wait`](compose_wait.md) | Block until the first service container stops |
|
||||
|
||||
|
||||
### Options
|
||||
@@ -46,6 +47,7 @@ Define and run multi-container applications with Docker.
|
||||
| `-f`, `--file` | `stringArray` | | Compose configuration files |
|
||||
| `--parallel` | `int` | `-1` | Control max parallelism, -1 for unlimited |
|
||||
| `--profile` | `stringArray` | | Specify a profile to enable |
|
||||
| `--progress` | `string` | `auto` | Set type of progress output (auto, tty, plain, quiet) |
|
||||
| `--project-directory` | `string` | | Specify an alternate working directory<br>(default: the path of the, first specified, Compose file) |
|
||||
| `-p`, `--project-name` | `string` | | Project name |
|
||||
|
||||
@@ -171,7 +173,6 @@ If flags are explicitly set on the command line, the associated environment vari
|
||||
Setting the `COMPOSE_IGNORE_ORPHANS` environment variable to `true` will stop docker compose from detecting orphaned
|
||||
containers for the project.
|
||||
|
||||
|
||||
### Use Dry Run mode to test your command
|
||||
|
||||
Use `--dry-run` flag to test a command without changing your application stack state.
|
||||
@@ -195,24 +196,4 @@ $ docker compose --dry-run up --build -d
|
||||
From the example above, you can see that the first step is to pull the image defined by `db` service, then build the `backend` service.
|
||||
Next, the containers are created. The `db` service is started, and the `backend` and `proxy` wait until the `db` service is healthy before starting.
|
||||
|
||||
Dry Run mode does not currently work with all commands. In particular, you cannot use Dry Run mode with a command that doesn't change the state of a Compose stack
|
||||
such as `ps`, `ls`, `logs` for example.
|
||||
|
||||
Here the list of commands supporting `--dry-run` flag:
|
||||
* build
|
||||
* cp
|
||||
* create
|
||||
* down
|
||||
* exec
|
||||
* kill
|
||||
* pause
|
||||
* pull
|
||||
* push
|
||||
* remove
|
||||
* restart
|
||||
* run
|
||||
* start
|
||||
* stop
|
||||
* unpause
|
||||
* up
|
||||
|
||||
Dry Run mode works with almost all commands. You cannot use Dry Run mode with a command that doesn't change the state of a Compose stack such as `ps`, `ls`, `logs` for example.
|
||||
|
||||
@@ -8,10 +8,10 @@ Build or rebuild services
|
||||
| Name | Type | Default | Description |
|
||||
|:-----------------|:--------------|:--------|:------------------------------------------------------------------------------------------------------------|
|
||||
| `--build-arg` | `stringArray` | | Set build-time variables for services. |
|
||||
| `--builder` | `string` | | Set builder to use. |
|
||||
| `--dry-run` | | | Execute command in dry run mode |
|
||||
| `-m`, `--memory` | `bytes` | `0` | Set memory limit for the build container. Not supported by BuildKit. |
|
||||
| `--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. |
|
||||
| `--push` | | | Push service images. |
|
||||
| `-q`, `--quiet` | | | Don't print anything to STDOUT |
|
||||
|
||||
@@ -10,8 +10,8 @@ Stop and remove containers, networks
|
||||
| `--dry-run` | | | Execute command in dry run mode |
|
||||
| `--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. |
|
||||
| `-t`, `--timeout` | `int` | `0` | 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-->
|
||||
|
||||
@@ -9,7 +9,7 @@ Restart service containers
|
||||
|:------------------|:------|:--------|:--------------------------------------|
|
||||
| `--dry-run` | | | Execute command in dry run mode |
|
||||
| `--no-deps` | | | Don't restart dependent services. |
|
||||
| `-t`, `--timeout` | `int` | `10` | Specify a shutdown timeout in seconds |
|
||||
| `-t`, `--timeout` | `int` | `0` | Specify a shutdown timeout in seconds |
|
||||
|
||||
|
||||
<!---MARKER_GEN_END-->
|
||||
|
||||
@@ -8,6 +8,8 @@ Run a one-off command on a service.
|
||||
| Name | Type | Default | Description |
|
||||
|:----------------------|:--------------|:--------|:----------------------------------------------------------------------------------|
|
||||
| `--build` | | | Build image before starting container. |
|
||||
| `--cap-add` | `list` | | Add Linux capabilities |
|
||||
| `--cap-drop` | `list` | | Drop Linux capabilities |
|
||||
| `-d`, `--detach` | | | Run container in background and print container ID |
|
||||
| `--dry-run` | | | Execute command in dry run mode |
|
||||
| `--entrypoint` | `string` | | Override the entrypoint of the image |
|
||||
@@ -34,7 +36,7 @@ Run a one-off command on 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:
|
||||
|
||||
```console
|
||||
$ docker compose run web bash
|
||||
|
||||
@@ -8,7 +8,7 @@ Stop services
|
||||
| Name | Type | Default | Description |
|
||||
|:------------------|:------|:--------|:--------------------------------------|
|
||||
| `--dry-run` | | | Execute command in dry run mode |
|
||||
| `-t`, `--timeout` | `int` | `10` | Specify a shutdown timeout in seconds |
|
||||
| `-t`, `--timeout` | `int` | `0` | Specify a shutdown timeout in seconds |
|
||||
|
||||
|
||||
<!---MARKER_GEN_END-->
|
||||
|
||||
@@ -28,7 +28,7 @@ Create and start containers
|
||||
| `--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. |
|
||||
| `-t`, `--timeout` | `int` | `0` | Use this timeout in seconds for container shutdown when attached or when containers are already running. |
|
||||
| `--timestamps` | | | Show timestamps. |
|
||||
| `--wait` | | | Wait for services to be running\|healthy. Implies detached mode. |
|
||||
| `--wait-timeout` | `int` | `0` | timeout waiting for application to be running\|healthy. |
|
||||
|
||||
15
docs/reference/compose_wait.md
Normal file
15
docs/reference/compose_wait.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# docker compose wait
|
||||
|
||||
<!---MARKER_GEN_START-->
|
||||
Block until the first service container stops
|
||||
|
||||
### Options
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|:-----------------|:-----|:--------|:---------------------------------------------|
|
||||
| `--down-project` | | | Drops project when the first container stops |
|
||||
| `--dry-run` | | | Execute command in dry run mode |
|
||||
|
||||
|
||||
<!---MARKER_GEN_END-->
|
||||
|
||||
@@ -118,7 +118,6 @@ long: |-
|
||||
Setting the `COMPOSE_IGNORE_ORPHANS` environment variable to `true` will stop docker compose from detecting orphaned
|
||||
containers for the project.
|
||||
|
||||
|
||||
### Use Dry Run mode to test your command
|
||||
|
||||
Use `--dry-run` flag to test a command without changing your application stack state.
|
||||
@@ -142,26 +141,7 @@ long: |-
|
||||
From the example above, you can see that the first step is to pull the image defined by `db` service, then build the `backend` service.
|
||||
Next, the containers are created. The `db` service is started, and the `backend` and `proxy` wait until the `db` service is healthy before starting.
|
||||
|
||||
Dry Run mode does not currently work with all commands. In particular, you cannot use Dry Run mode with a command that doesn't change the state of a Compose stack
|
||||
such as `ps`, `ls`, `logs` for example.
|
||||
|
||||
Here the list of commands supporting `--dry-run` flag:
|
||||
* build
|
||||
* cp
|
||||
* create
|
||||
* down
|
||||
* exec
|
||||
* kill
|
||||
* pause
|
||||
* pull
|
||||
* push
|
||||
* remove
|
||||
* restart
|
||||
* run
|
||||
* start
|
||||
* stop
|
||||
* unpause
|
||||
* up
|
||||
Dry Run mode works with almost all commands. You cannot use Dry Run mode with a command that doesn't change the state of a Compose stack such as `ps`, `ls`, `logs` for example.
|
||||
usage: docker compose
|
||||
pname: docker
|
||||
plink: docker.yaml
|
||||
@@ -191,6 +171,7 @@ cname:
|
||||
- docker compose unpause
|
||||
- docker compose up
|
||||
- docker compose version
|
||||
- docker compose wait
|
||||
clink:
|
||||
- docker_compose_build.yaml
|
||||
- docker_compose_config.yaml
|
||||
@@ -217,6 +198,7 @@ clink:
|
||||
- docker_compose_unpause.yaml
|
||||
- docker_compose_up.yaml
|
||||
- docker_compose_version.yaml
|
||||
- docker_compose_wait.yaml
|
||||
options:
|
||||
- option: ansi
|
||||
value_type: string
|
||||
@@ -300,6 +282,16 @@ options:
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
- option: progress
|
||||
value_type: string
|
||||
default_value: auto
|
||||
description: Set type of progress output (auto, tty, plain, quiet)
|
||||
deprecated: false
|
||||
hidden: false
|
||||
experimental: false
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
- option: project-directory
|
||||
value_type: string
|
||||
description: |-
|
||||
|
||||
@@ -24,6 +24,15 @@ options:
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
- option: builder
|
||||
value_type: string
|
||||
description: Set builder to use.
|
||||
deprecated: false
|
||||
hidden: false
|
||||
experimental: false
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
- option: compress
|
||||
value_type: bool
|
||||
default_value: "true"
|
||||
@@ -90,9 +99,9 @@ options:
|
||||
- option: progress
|
||||
value_type: string
|
||||
default_value: auto
|
||||
description: Set type of progress output (auto, tty, plain, quiet)
|
||||
description: Set type of ui output (auto, tty, plain, quiet)
|
||||
deprecated: false
|
||||
hidden: false
|
||||
hidden: true
|
||||
experimental: false
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
|
||||
@@ -14,7 +14,7 @@ long: |-
|
||||
Anonymous volumes are not removed by default. However, as they don’t have a stable name, they will not be automatically
|
||||
mounted by a subsequent `up`. For data that needs to persist between updates, use explicit paths as bind mounts or
|
||||
named volumes.
|
||||
usage: docker compose down [OPTIONS]
|
||||
usage: docker compose down [OPTIONS] [SERVICES]
|
||||
pname: docker compose
|
||||
plink: docker_compose.yaml
|
||||
options:
|
||||
@@ -41,7 +41,7 @@ options:
|
||||
- option: timeout
|
||||
shorthand: t
|
||||
value_type: int
|
||||
default_value: "10"
|
||||
default_value: "0"
|
||||
description: Specify a shutdown timeout in seconds
|
||||
deprecated: false
|
||||
hidden: false
|
||||
@@ -54,7 +54,7 @@ options:
|
||||
value_type: bool
|
||||
default_value: "false"
|
||||
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
|
||||
hidden: false
|
||||
experimental: false
|
||||
|
||||
@@ -28,7 +28,7 @@ options:
|
||||
- option: timeout
|
||||
shorthand: t
|
||||
value_type: int
|
||||
default_value: "10"
|
||||
default_value: "0"
|
||||
description: Specify a shutdown timeout in seconds
|
||||
deprecated: false
|
||||
hidden: false
|
||||
|
||||
@@ -3,7 +3,7 @@ short: Run a one-off command on a service.
|
||||
long: |-
|
||||
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:
|
||||
|
||||
```console
|
||||
$ docker compose run web bash
|
||||
@@ -68,6 +68,24 @@ options:
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
- option: cap-add
|
||||
value_type: list
|
||||
description: Add Linux capabilities
|
||||
deprecated: false
|
||||
hidden: false
|
||||
experimental: false
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
- option: cap-drop
|
||||
value_type: list
|
||||
description: Drop Linux capabilities
|
||||
deprecated: false
|
||||
hidden: false
|
||||
experimental: false
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
- option: detach
|
||||
shorthand: d
|
||||
value_type: bool
|
||||
|
||||
@@ -9,7 +9,7 @@ options:
|
||||
- option: timeout
|
||||
shorthand: t
|
||||
value_type: int
|
||||
default_value: "10"
|
||||
default_value: "0"
|
||||
description: Specify a shutdown timeout in seconds
|
||||
deprecated: false
|
||||
hidden: false
|
||||
|
||||
@@ -234,7 +234,7 @@ options:
|
||||
- option: timeout
|
||||
shorthand: t
|
||||
value_type: int
|
||||
default_value: "10"
|
||||
default_value: "0"
|
||||
description: |
|
||||
Use this timeout in seconds for container shutdown when attached or when containers are already running.
|
||||
deprecated: false
|
||||
|
||||
34
docs/reference/docker_compose_wait.yaml
Normal file
34
docs/reference/docker_compose_wait.yaml
Normal file
@@ -0,0 +1,34 @@
|
||||
command: docker compose wait
|
||||
short: Block until the first service container stops
|
||||
long: Block until the first service container stops
|
||||
usage: docker compose wait SERVICE [SERVICE...] [OPTIONS]
|
||||
pname: docker compose
|
||||
plink: docker_compose.yaml
|
||||
options:
|
||||
- option: down-project
|
||||
value_type: bool
|
||||
default_value: "false"
|
||||
description: Drops project when the first container stops
|
||||
deprecated: false
|
||||
hidden: false
|
||||
experimental: false
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
inherited_options:
|
||||
- option: dry-run
|
||||
value_type: bool
|
||||
default_value: "false"
|
||||
description: Execute command in dry run mode
|
||||
deprecated: false
|
||||
hidden: false
|
||||
experimental: false
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
deprecated: false
|
||||
experimental: false
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
|
||||
28
e2e/cucumber-features/port-conflict.feature
Normal file
28
e2e/cucumber-features/port-conflict.feature
Normal file
@@ -0,0 +1,28 @@
|
||||
Feature: Report port conflicts
|
||||
|
||||
Background:
|
||||
Given a compose file
|
||||
"""
|
||||
services:
|
||||
web:
|
||||
image: nginx
|
||||
ports:
|
||||
- 31415:80
|
||||
"""
|
||||
And I run "docker rm -f nginx-pi-31415"
|
||||
|
||||
Scenario: Reports a port allocation conflict with another container
|
||||
Given I run "docker run -d -p 31415:80 --name nginx-pi-31415 nginx"
|
||||
When I run "compose up -d"
|
||||
Then the output contains "port is already allocated"
|
||||
And the exit code is 1
|
||||
|
||||
Scenario: Reports a port conflict with some other process
|
||||
Given a process listening on port 31415
|
||||
When I run "compose up -d"
|
||||
Then the output contains "address already in use"
|
||||
And the exit code is 1
|
||||
|
||||
Scenario: Cleanup
|
||||
Given I run "docker rm -f nginx-pi-31415"
|
||||
|
||||
@@ -16,6 +16,7 @@ Background:
|
||||
"""
|
||||
FROM golang:1.19-alpine
|
||||
"""
|
||||
And I run "docker rm -f external-test"
|
||||
|
||||
Scenario: external container from compose image exists
|
||||
When I run "compose build"
|
||||
@@ -24,4 +25,5 @@ Scenario: external container from compose image exists
|
||||
Then the exit code is 0
|
||||
And I run "compose ps -a"
|
||||
Then the output does not contain "external-test"
|
||||
And I run "docker rm -f external-test"
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ package cucumber
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
@@ -87,6 +88,7 @@ func setup(s *godog.ScenarioContext) {
|
||||
s.Step(`output contains "(.*)"$`, th.outputContains(true))
|
||||
s.Step(`output does not contain "(.*)"$`, th.outputContains(false))
|
||||
s.Step(`exit code is (\d+)$`, th.exitCodeIs)
|
||||
s.Step(`a process listening on port (\d+)$`, th.listenerOnPort)
|
||||
}
|
||||
|
||||
type testHelper struct {
|
||||
@@ -174,3 +176,16 @@ func (th *testHelper) setDockerfile(dockerfileString string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (th *testHelper) listenerOnPort(port int) error {
|
||||
l, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
th.T.Cleanup(func() {
|
||||
_ = l.Close()
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
186
go.mod
186
go.mod
@@ -3,103 +3,120 @@ module github.com/docker/compose/v2
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/AlecAivazis/survey/v2 v2.3.6
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7
|
||||
github.com/Microsoft/go-winio v0.6.1
|
||||
github.com/buger/goterm v1.0.4
|
||||
github.com/compose-spec/compose-go v1.13.5
|
||||
github.com/compose-spec/compose-go v1.15.1
|
||||
github.com/containerd/console v1.0.3
|
||||
github.com/containerd/containerd v1.6.21
|
||||
github.com/containerd/containerd v1.7.2
|
||||
github.com/cucumber/godog v0.0.0-00010101000000-000000000000 // replaced; see replace for the actual version used
|
||||
github.com/distribution/distribution/v3 v3.0.0-20230327091844-0c958010ace2
|
||||
github.com/docker/buildx v0.10.4
|
||||
github.com/docker/cli v23.0.6+incompatible
|
||||
github.com/distribution/distribution/v3 v3.0.0-20230601133803-97b1d649c493
|
||||
github.com/docker/buildx v0.11.0
|
||||
github.com/docker/cli v24.0.2+incompatible
|
||||
github.com/docker/cli-docs-tool v0.5.1
|
||||
github.com/docker/docker v23.0.6+incompatible
|
||||
github.com/docker/docker v24.0.2+incompatible
|
||||
github.com/docker/go-connections v0.4.0
|
||||
github.com/docker/go-units v0.5.0
|
||||
github.com/fsnotify/fsevents v0.1.1
|
||||
github.com/golang/mock v1.6.0
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
github.com/hashicorp/go-version v1.6.0
|
||||
github.com/jonboulle/clockwork v0.4.0
|
||||
github.com/mattn/go-shellwords v1.0.12
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/moby/buildkit v0.11.6
|
||||
github.com/moby/buildkit v0.11.0-rc3.0.20230609092854-67a08623b95a
|
||||
github.com/moby/patternmatcher v0.5.0
|
||||
github.com/moby/term v0.5.0
|
||||
github.com/morikuni/aec v1.0.0
|
||||
github.com/opencontainers/go-digest v1.0.0
|
||||
github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b
|
||||
github.com/opencontainers/image-spec v1.1.0-rc4
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/sirupsen/logrus v1.9.0
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/cobra v1.7.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/stretchr/testify v1.8.2
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/theupdateframework/notary v0.7.0
|
||||
github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375
|
||||
go.opentelemetry.io/otel v1.15.1
|
||||
go.opentelemetry.io/otel v1.14.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.14.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.14.0
|
||||
go.opentelemetry.io/otel/sdk v1.14.0
|
||||
go.opentelemetry.io/otel/trace v1.14.0
|
||||
go.uber.org/goleak v1.2.1
|
||||
golang.org/x/sync v0.2.0
|
||||
golang.org/x/sync v0.3.0
|
||||
google.golang.org/grpc v1.56.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
gotest.tools/v3 v3.4.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230106234847-43070de90fa1 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||
github.com/Microsoft/go-winio v0.5.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.16.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.15.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.12.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.11.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.16.4 // indirect
|
||||
github.com/aws/smithy-go v1.11.2 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.2.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.17.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.18.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.13.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.24 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.30 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.24 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.31 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.24 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.12.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.18.6 // indirect
|
||||
github.com/aws/smithy-go v1.13.5 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bugsnag/bugsnag-go v1.5.0 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.1.2 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/cloudflare/cfssl v1.4.1
|
||||
github.com/containerd/continuity v0.3.0 // indirect
|
||||
github.com/containerd/ttrpc v1.1.1 // indirect
|
||||
github.com/containerd/typeurl v1.0.2 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.2.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/cloudflare/cfssl v1.6.4 // indirect
|
||||
github.com/containerd/continuity v0.4.1 // indirect
|
||||
github.com/containerd/ttrpc v1.2.2 // indirect
|
||||
github.com/containerd/typeurl/v2 v2.1.1 // indirect
|
||||
github.com/cucumber/gherkin-go/v19 v19.0.3 // indirect
|
||||
github.com/cucumber/messages-go/v16 v16.0.1 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.2.3 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/docker/distribution v2.8.1+incompatible // indirect
|
||||
github.com/docker/distribution v2.8.2+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.7.0 // indirect
|
||||
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect
|
||||
github.com/docker/go-metrics v0.0.1 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.2 // indirect
|
||||
github.com/fsnotify/fsevents v0.1.1
|
||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.10.1 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.3 // indirect
|
||||
github.com/fvbommel/sortorder v1.0.2 // indirect
|
||||
github.com/go-logr/logr v1.2.4 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.0 // indirect
|
||||
github.com/go-openapi/swag v0.19.14 // indirect
|
||||
github.com/gofrs/flock v0.8.1 // indirect
|
||||
github.com/gofrs/uuid v4.2.0+incompatible // indirect
|
||||
github.com/gogo/googleapis v1.4.1 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/google/gnostic v0.5.7-v3refs // indirect
|
||||
github.com/google/go-cmp v0.5.9 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/googleapis/gnostic v0.5.5 // indirect
|
||||
github.com/gorilla/mux v1.8.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/v2 v2.11.3 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
||||
github.com/hashicorp/go-memdb v1.3.2 // indirect
|
||||
github.com/hashicorp/golang-lru v0.5.4 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/imdario/mergo v0.3.15 // indirect
|
||||
github.com/imdario/mergo v0.3.16 // indirect
|
||||
github.com/in-toto/in-toto-golang v0.5.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jinzhu/gorm v1.9.11 // indirect
|
||||
github.com/jonboulle/clockwork v0.4.0
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/klauspost/compress v1.15.12 // indirect
|
||||
github.com/klauspost/compress v1.16.5 // indirect
|
||||
github.com/kr/pretty v0.3.0 // indirect
|
||||
github.com/magiconair/properties v1.8.6 // indirect
|
||||
github.com/mailru/easyjson v0.7.6 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.14 // indirect
|
||||
@@ -107,7 +124,6 @@ require (
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
|
||||
github.com/miekg/pkcs11 v1.1.1 // indirect
|
||||
github.com/moby/locker v1.0.1 // indirect
|
||||
github.com/moby/patternmatcher v0.5.0
|
||||
github.com/moby/spdystream v0.2.0 // indirect
|
||||
github.com/moby/sys/mountinfo v0.6.2 // indirect
|
||||
github.com/moby/sys/sequential v0.5.0 // indirect
|
||||
@@ -115,75 +131,63 @@ require (
|
||||
github.com/moby/sys/symlink v0.2.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/opencontainers/runc v1.1.5 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/opencontainers/runc v1.1.7 // indirect
|
||||
github.com/pelletier/go-toml v1.9.5 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.5 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_golang v1.14.0 // indirect
|
||||
github.com/prometheus/client_model v0.3.0 // indirect
|
||||
github.com/prometheus/common v0.37.0 // indirect
|
||||
github.com/prometheus/procfs v0.8.0 // indirect
|
||||
github.com/prometheus/common v0.42.0 // indirect
|
||||
github.com/prometheus/procfs v0.9.0 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/secure-systems-lab/go-securesystemslib v0.4.0 // indirect
|
||||
github.com/serialx/hashring v0.0.0-20190422032157-8b2912629002 // indirect
|
||||
github.com/shibumi/go-pathspec v1.3.0 // indirect
|
||||
github.com/spf13/afero v1.9.2 // indirect
|
||||
github.com/spf13/cast v1.5.0 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/subosito/gotenv v1.4.1 // indirect
|
||||
github.com/tonistiigi/fsutil v0.0.0-20230105215944-fb433841cbfa // indirect
|
||||
github.com/tonistiigi/fsutil v0.0.0-20230407161946-9e7a6df48576 // indirect
|
||||
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea // indirect
|
||||
github.com/tonistiigi/vt100 v0.0.0-20210615222946-8066bb97264f // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||
github.com/zmap/zcrypto v0.0.0-20220605182715-4dfcec6e9a8c // indirect
|
||||
github.com/zmap/zlint v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.29.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.29.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.29.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.4.1 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.4.1 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.4.1 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.4.1 // indirect
|
||||
go.opentelemetry.io/otel/internal/metric v0.27.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v0.27.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.4.1 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.15.1 // indirect
|
||||
go.opentelemetry.io/proto/otlp v0.12.0 // indirect
|
||||
golang.org/x/crypto v0.2.0 // indirect
|
||||
golang.org/x/net v0.7.0 // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 // indirect
|
||||
golang.org/x/sys v0.5.0 // indirect
|
||||
golang.org/x/term v0.5.0 // indirect
|
||||
golang.org/x/text v0.7.0 // indirect
|
||||
golang.org/x/time v0.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.40.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.40.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.40.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.14.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.14.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v0.37.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v0.19.0 // indirect
|
||||
golang.org/x/crypto v0.7.0 // indirect
|
||||
golang.org/x/mod v0.9.0 // indirect
|
||||
golang.org/x/net v0.9.0 // indirect
|
||||
golang.org/x/oauth2 v0.7.0 // indirect
|
||||
golang.org/x/sys v0.7.0 // indirect
|
||||
golang.org/x/term v0.7.0 // indirect
|
||||
golang.org/x/text v0.9.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
golang.org/x/tools v0.7.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20221024183307-1bc688fe9f3e // indirect
|
||||
google.golang.org/grpc v1.50.1 // indirect
|
||||
google.golang.org/protobuf v1.28.1 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
|
||||
google.golang.org/protobuf v1.30.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/api v0.24.1 // indirect; replaced; see replace for the actual version used
|
||||
k8s.io/apimachinery v0.24.1 // indirect; replaced; see replace for the actual version used
|
||||
k8s.io/client-go v0.24.1 // indirect; replaced; see replace for the actual version used
|
||||
k8s.io/klog/v2 v2.60.1 // indirect
|
||||
k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect
|
||||
sigs.k8s.io/yaml v1.2.0 // indirect
|
||||
k8s.io/api v0.26.2 // indirect
|
||||
k8s.io/apimachinery v0.26.2 // indirect
|
||||
k8s.io/client-go v0.26.2 // indirect
|
||||
k8s.io/klog/v2 v2.90.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 // indirect
|
||||
k8s.io/utils v0.0.0-20230220204549-a5ecb0141aa5 // indirect
|
||||
oras.land/oras-go/v2 v2.2.0 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
|
||||
sigs.k8s.io/yaml v1.3.0 // indirect
|
||||
)
|
||||
|
||||
replace (
|
||||
// Override for e2e tests
|
||||
github.com/cucumber/godog => github.com/laurazard/godog v0.0.0-20220922095256-4c4b17abdae7
|
||||
|
||||
golang.org/x/oauth2 => golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783
|
||||
|
||||
// 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.9.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
|
||||
)
|
||||
// Override for e2e tests
|
||||
replace github.com/cucumber/godog => github.com/laurazard/godog v0.0.0-20220922095256-4c4b17abdae7
|
||||
|
||||
44
internal/tracing/conn_unix.go
Normal file
44
internal/tracing/conn_unix.go
Normal file
@@ -0,0 +1,44 @@
|
||||
//go:build !windows
|
||||
|
||||
/*
|
||||
Copyright 2023 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package tracing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
const maxUnixSocketPathSize = len(syscall.RawSockaddrUnix{}.Path)
|
||||
|
||||
func DialInMemory(ctx context.Context, addr string) (net.Conn, error) {
|
||||
if !strings.HasPrefix(addr, "unix://") {
|
||||
return nil, fmt.Errorf("not a Unix socket address: %s", addr)
|
||||
}
|
||||
addr = strings.TrimPrefix(addr, "unix://")
|
||||
|
||||
if len(addr) > maxUnixSocketPathSize {
|
||||
//goland:noinspection GoErrorStringFormat
|
||||
return nil, fmt.Errorf("Unix socket address is too long: %s", addr)
|
||||
}
|
||||
|
||||
var d net.Dialer
|
||||
return d.DialContext(ctx, "unix", addr)
|
||||
}
|
||||
35
internal/tracing/conn_windows.go
Normal file
35
internal/tracing/conn_windows.go
Normal file
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
Copyright 2023 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package tracing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/Microsoft/go-winio"
|
||||
)
|
||||
|
||||
func DialInMemory(ctx context.Context, addr string) (net.Conn, error) {
|
||||
if !strings.HasPrefix(addr, "npipe://") {
|
||||
return nil, fmt.Errorf("not a named pipe address: %s", addr)
|
||||
}
|
||||
addr = strings.TrimPrefix(addr, "npipe://")
|
||||
|
||||
return winio.DialPipeContext(ctx, addr)
|
||||
}
|
||||
126
internal/tracing/docker_context.go
Normal file
126
internal/tracing/docker_context.go
Normal file
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
Copyright 2023 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package tracing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/context/store"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
)
|
||||
|
||||
const otelConfigFieldName = "otel"
|
||||
|
||||
// traceClientFromDockerContext creates a gRPC OTLP client based on metadata
|
||||
// from the active Docker CLI context.
|
||||
func traceClientFromDockerContext(dockerCli command.Cli, otelEnv envMap) (otlptrace.Client, error) {
|
||||
// attempt to extract an OTEL config from the Docker context to enable
|
||||
// automatic integration with Docker Desktop;
|
||||
cfg, err := ConfigFromDockerContext(dockerCli.ContextStore(), dockerCli.CurrentContext())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("loading otel config from docker context metadata: %v", err)
|
||||
}
|
||||
|
||||
if cfg.Endpoint == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// HACK: unfortunately _all_ public OTEL initialization functions
|
||||
// implicitly read from the OS env, so temporarily unset them all and
|
||||
// restore afterwards
|
||||
defer func() {
|
||||
for k, v := range otelEnv {
|
||||
if err := os.Setenv(k, v); err != nil {
|
||||
panic(fmt.Errorf("restoring env for %q: %v", k, err))
|
||||
}
|
||||
}
|
||||
}()
|
||||
for k := range otelEnv {
|
||||
if err := os.Unsetenv(k); err != nil {
|
||||
return nil, fmt.Errorf("stashing env for %q: %v", k, err)
|
||||
}
|
||||
}
|
||||
|
||||
dialCtx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
||||
defer cancel()
|
||||
conn, err := grpc.DialContext(
|
||||
dialCtx,
|
||||
cfg.Endpoint,
|
||||
grpc.WithContextDialer(DialInMemory),
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
grpc.WithBlock(),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("initializing otel connection from docker context metadata: %v", err)
|
||||
}
|
||||
|
||||
client := otlptracegrpc.NewClient(otlptracegrpc.WithGRPCConn(conn))
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// ConfigFromDockerContext inspects extra metadata included as part of the
|
||||
// specified Docker context to try and extract a valid OTLP client configuration.
|
||||
func ConfigFromDockerContext(st store.Store, name string) (OTLPConfig, error) {
|
||||
meta, err := st.GetMetadata(name)
|
||||
if err != nil {
|
||||
return OTLPConfig{}, err
|
||||
}
|
||||
|
||||
var otelCfg interface{}
|
||||
switch m := meta.Metadata.(type) {
|
||||
case command.DockerContext:
|
||||
otelCfg = m.AdditionalFields[otelConfigFieldName]
|
||||
case map[string]interface{}:
|
||||
otelCfg = m[otelConfigFieldName]
|
||||
}
|
||||
if otelCfg == nil {
|
||||
return OTLPConfig{}, nil
|
||||
}
|
||||
|
||||
otelMap, ok := otelCfg.(map[string]interface{})
|
||||
if !ok {
|
||||
return OTLPConfig{}, fmt.Errorf(
|
||||
"unexpected type for field %q: %T (expected: %T)",
|
||||
otelConfigFieldName,
|
||||
otelCfg,
|
||||
otelMap,
|
||||
)
|
||||
}
|
||||
|
||||
// keys from https://opentelemetry.io/docs/concepts/sdk-configuration/otlp-exporter-configuration/
|
||||
cfg := OTLPConfig{
|
||||
Endpoint: valueOrDefault[string](otelMap, "OTEL_EXPORTER_OTLP_ENDPOINT"),
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// valueOrDefault returns the type-cast value at the specified key in the map
|
||||
// if present and the correct type; otherwise, it returns the default value for
|
||||
// T.
|
||||
func valueOrDefault[T any](m map[string]interface{}, key string) T {
|
||||
if v, ok := m[key].(T); ok {
|
||||
return v
|
||||
}
|
||||
return *new(T)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2020 Docker Compose CLI authors
|
||||
Copyright 2023 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -14,22 +14,16 @@
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package compose
|
||||
package tracing
|
||||
|
||||
import (
|
||||
"github.com/moby/buildkit/util/tracing/detect"
|
||||
"go.opentelemetry.io/otel"
|
||||
|
||||
_ "github.com/moby/buildkit/util/tracing/detect/delegated" //nolint:blank-imports
|
||||
_ "github.com/moby/buildkit/util/tracing/env" //nolint:blank-imports
|
||||
)
|
||||
|
||||
func init() {
|
||||
detect.ServiceName = "compose"
|
||||
// do not log tracing errors to stdio
|
||||
otel.SetErrorHandler(skipErrors{})
|
||||
}
|
||||
|
||||
// skipErrors is a no-op otel.ErrorHandler.
|
||||
type skipErrors struct{}
|
||||
|
||||
func (skipErrors) Handle(err error) {}
|
||||
// Handle does nothing, ignoring any errors passed to it.
|
||||
func (skipErrors) Handle(_ error) {}
|
||||
|
||||
var _ otel.ErrorHandler = skipErrors{}
|
||||
50
internal/tracing/mux.go
Normal file
50
internal/tracing/mux.go
Normal file
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
Copyright 2023 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package tracing
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
||||
)
|
||||
|
||||
type MuxExporter struct {
|
||||
exporters []sdktrace.SpanExporter
|
||||
}
|
||||
|
||||
func (m MuxExporter) ExportSpans(ctx context.Context, spans []sdktrace.ReadOnlySpan) error {
|
||||
var eg multierror.Group
|
||||
for i := range m.exporters {
|
||||
exporter := m.exporters[i]
|
||||
eg.Go(func() error {
|
||||
return exporter.ExportSpans(ctx, spans)
|
||||
})
|
||||
}
|
||||
return eg.Wait()
|
||||
}
|
||||
|
||||
func (m MuxExporter) Shutdown(ctx context.Context) error {
|
||||
var eg multierror.Group
|
||||
for i := range m.exporters {
|
||||
exporter := m.exporters[i]
|
||||
eg.Go(func() error {
|
||||
return exporter.Shutdown(ctx)
|
||||
})
|
||||
}
|
||||
return eg.Wait()
|
||||
}
|
||||
156
internal/tracing/tracing.go
Normal file
156
internal/tracing/tracing.go
Normal file
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
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 tracing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/compose/v2/internal"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/moby/buildkit/util/tracing/detect"
|
||||
_ "github.com/moby/buildkit/util/tracing/detect/delegated" //nolint:blank-imports
|
||||
_ "github.com/moby/buildkit/util/tracing/env" //nolint:blank-imports
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
|
||||
"go.opentelemetry.io/otel/propagation"
|
||||
"go.opentelemetry.io/otel/sdk/resource"
|
||||
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.18.0"
|
||||
)
|
||||
|
||||
func init() {
|
||||
detect.ServiceName = "compose"
|
||||
// do not log tracing errors to stdio
|
||||
otel.SetErrorHandler(skipErrors{})
|
||||
}
|
||||
|
||||
var Tracer = otel.Tracer("compose")
|
||||
|
||||
// OTLPConfig contains the necessary values to initialize an OTLP client
|
||||
// manually.
|
||||
//
|
||||
// This supports a minimal set of options based on what is necessary for
|
||||
// automatic OTEL configuration from Docker context metadata.
|
||||
type OTLPConfig struct {
|
||||
Endpoint string
|
||||
}
|
||||
|
||||
// ShutdownFunc flushes and stops an OTEL exporter.
|
||||
type ShutdownFunc func(ctx context.Context) error
|
||||
|
||||
// envMap is a convenience type for OS environment variables.
|
||||
type envMap map[string]string
|
||||
|
||||
func InitTracing(dockerCli command.Cli) (ShutdownFunc, error) {
|
||||
// set global propagator to tracecontext (the default is no-op).
|
||||
otel.SetTextMapPropagator(propagation.TraceContext{})
|
||||
|
||||
if v, _ := strconv.ParseBool(os.Getenv("COMPOSE_EXPERIMENTAL_OTEL")); !v {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return InitProvider(dockerCli)
|
||||
}
|
||||
|
||||
func InitProvider(dockerCli command.Cli) (ShutdownFunc, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
var errs []error
|
||||
var exporters []sdktrace.SpanExporter
|
||||
|
||||
envClient, otelEnv := traceClientFromEnv()
|
||||
if envClient != nil {
|
||||
if envExporter, err := otlptrace.New(ctx, envClient); err != nil {
|
||||
errs = append(errs, err)
|
||||
} else if envExporter != nil {
|
||||
exporters = append(exporters, envExporter)
|
||||
}
|
||||
}
|
||||
|
||||
if dcClient, err := traceClientFromDockerContext(dockerCli, otelEnv); err != nil {
|
||||
errs = append(errs, err)
|
||||
} else if dcClient != nil {
|
||||
if dcExporter, err := otlptrace.New(ctx, dcClient); err != nil {
|
||||
errs = append(errs, err)
|
||||
} else if dcExporter != nil {
|
||||
exporters = append(exporters, dcExporter)
|
||||
}
|
||||
}
|
||||
if len(errs) != 0 {
|
||||
return nil, errors.Join(errs...)
|
||||
}
|
||||
|
||||
res, err := resource.New(
|
||||
ctx,
|
||||
resource.WithAttributes(
|
||||
semconv.ServiceName("compose"),
|
||||
semconv.ServiceVersion(internal.Version),
|
||||
attribute.String("docker.context", dockerCli.CurrentContext()),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create resource: %v", err)
|
||||
}
|
||||
|
||||
muxExporter := MuxExporter{exporters: exporters}
|
||||
sp := sdktrace.NewSimpleSpanProcessor(muxExporter)
|
||||
tracerProvider := sdktrace.NewTracerProvider(
|
||||
sdktrace.WithSampler(sdktrace.AlwaysSample()),
|
||||
sdktrace.WithResource(res),
|
||||
sdktrace.WithSpanProcessor(sp),
|
||||
)
|
||||
otel.SetTracerProvider(tracerProvider)
|
||||
|
||||
// Shutdown will flush any remaining spans and shut down the exporter.
|
||||
return tracerProvider.Shutdown, nil
|
||||
}
|
||||
|
||||
// traceClientFromEnv creates a GRPC OTLP client based on OS environment
|
||||
// variables.
|
||||
//
|
||||
// https://opentelemetry.io/docs/concepts/sdk-configuration/otlp-exporter-configuration/
|
||||
func traceClientFromEnv() (otlptrace.Client, envMap) {
|
||||
hasOtelEndpointInEnv := false
|
||||
otelEnv := make(map[string]string)
|
||||
for _, kv := range os.Environ() {
|
||||
k, v, ok := strings.Cut(kv, "=")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(k, "OTEL_") {
|
||||
otelEnv[k] = v
|
||||
if strings.HasSuffix(k, "ENDPOINT") {
|
||||
hasOtelEndpointInEnv = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !hasOtelEndpointInEnv {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
client := otlptracegrpc.NewClient()
|
||||
return client, otelEnv
|
||||
}
|
||||
60
internal/tracing/tracing_test.go
Normal file
60
internal/tracing/tracing_test.go
Normal file
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
Copyright 2023 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package tracing_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/context/store"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/docker/compose/v2/internal/tracing"
|
||||
)
|
||||
|
||||
var testStoreCfg = store.NewConfig(
|
||||
func() interface{} {
|
||||
return &map[string]interface{}{}
|
||||
},
|
||||
)
|
||||
|
||||
func TestExtractOtelFromContext(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Requires filesystem access")
|
||||
}
|
||||
|
||||
dir := t.TempDir()
|
||||
|
||||
st := store.New(dir, testStoreCfg)
|
||||
err := st.CreateOrUpdate(store.Metadata{
|
||||
Name: "test",
|
||||
Metadata: command.DockerContext{
|
||||
Description: t.Name(),
|
||||
AdditionalFields: map[string]interface{}{
|
||||
"otel": map[string]interface{}{
|
||||
"OTEL_EXPORTER_OTLP_ENDPOINT": "localhost:1234",
|
||||
},
|
||||
},
|
||||
},
|
||||
Endpoints: make(map[string]interface{}),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
cfg, err := tracing.ConfigFromDockerContext(st, "test")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "localhost:1234", cfg.Endpoint)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2020 Docker Compose CLI authors
|
||||
Copyright 2023 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -74,6 +74,8 @@ type Service interface {
|
||||
Events(ctx context.Context, projectName string, options EventsOptions) error
|
||||
// Port executes the equivalent to a `compose port`
|
||||
Port(ctx context.Context, projectName string, service string, port uint16, options PortOptions) (string, int, error)
|
||||
// Publish executes the equivalent to a `compose publish`
|
||||
Publish(ctx context.Context, project *types.Project, repository string) error
|
||||
// Images executes the equivalent of a `compose images`
|
||||
Images(ctx context.Context, projectName string, options ImagesOptions) ([]ImageSummary, error)
|
||||
// MaxConcurrency defines upper limit for concurrent operations against engine API
|
||||
@@ -84,6 +86,15 @@ type Service interface {
|
||||
Watch(ctx context.Context, project *types.Project, services []string, options WatchOptions) error
|
||||
// Viz generates a graphviz graph of the project services
|
||||
Viz(ctx context.Context, project *types.Project, options VizOptions) (string, error)
|
||||
// Wait blocks until at least one of the services' container exits
|
||||
Wait(ctx context.Context, projectName string, options WaitOptions) (int64, error)
|
||||
}
|
||||
|
||||
type WaitOptions struct {
|
||||
// Services passed in the command line to be waited
|
||||
Services []string
|
||||
// Executes a down when a container exits
|
||||
DownProjectOnContainerExit bool
|
||||
}
|
||||
|
||||
type VizOptions struct {
|
||||
@@ -121,6 +132,8 @@ type BuildOptions struct {
|
||||
SSHs []types.SSHKey
|
||||
// Memory limit for the build container
|
||||
Memory int64
|
||||
// Builder name passed in the command line
|
||||
Builder string
|
||||
}
|
||||
|
||||
// Apply mutates project according to build options
|
||||
@@ -232,6 +245,8 @@ type DownOptions struct {
|
||||
Images string
|
||||
// Volumes remove volumes, both declared in the `volumes` section and anonymous ones
|
||||
Volumes bool
|
||||
// Services passed in the command line to be stopped
|
||||
Services []string
|
||||
}
|
||||
|
||||
// ConfigOptions group options of the Config API
|
||||
@@ -247,6 +262,7 @@ type ConfigOptions struct {
|
||||
// PushOptions group options of the Push API
|
||||
type PushOptions struct {
|
||||
Quiet bool
|
||||
Repository string
|
||||
IgnoreFailures bool
|
||||
}
|
||||
|
||||
@@ -303,6 +319,8 @@ type RunOptions struct {
|
||||
WorkingDir string
|
||||
User string
|
||||
Environment []string
|
||||
CapAdd []string
|
||||
CapDrop []string
|
||||
Labels types.Labels
|
||||
Privileged bool
|
||||
UseNetworkAliases bool
|
||||
|
||||
@@ -344,7 +344,7 @@ func (d *DryRunClient) ContainerCommit(ctx context.Context, container string, op
|
||||
return d.apiClient.ContainerCommit(ctx, container, options)
|
||||
}
|
||||
|
||||
func (d *DryRunClient) ContainerDiff(ctx context.Context, container string) ([]containerType.ContainerChangeResponseItem, error) {
|
||||
func (d *DryRunClient) ContainerDiff(ctx context.Context, container string) ([]containerType.FilesystemChange, error) {
|
||||
return d.apiClient.ContainerDiff(ctx, container)
|
||||
}
|
||||
|
||||
@@ -616,7 +616,7 @@ func (d *DryRunClient) Info(ctx context.Context) (moby.Info, error) {
|
||||
return d.apiClient.Info(ctx)
|
||||
}
|
||||
|
||||
func (d *DryRunClient) RegistryLogin(ctx context.Context, auth moby.AuthConfig) (registry.AuthenticateOKBody, error) {
|
||||
func (d *DryRunClient) RegistryLogin(ctx context.Context, auth registry.AuthConfig) (registry.AuthenticateOKBody, error) {
|
||||
return d.apiClient.RegistryLogin(ctx, auth)
|
||||
}
|
||||
|
||||
@@ -636,8 +636,8 @@ func (d *DryRunClient) VolumeInspectWithRaw(ctx context.Context, volumeID string
|
||||
return d.apiClient.VolumeInspectWithRaw(ctx, volumeID)
|
||||
}
|
||||
|
||||
func (d *DryRunClient) VolumeList(ctx context.Context, filter filters.Args) (volume.ListResponse, error) {
|
||||
return d.apiClient.VolumeList(ctx, filter)
|
||||
func (d *DryRunClient) VolumeList(ctx context.Context, opts volume.ListOptions) (volume.ListResponse, error) {
|
||||
return d.apiClient.VolumeList(ctx, opts)
|
||||
}
|
||||
|
||||
func (d *DryRunClient) VolumesPrune(ctx context.Context, pruneFilter filters.Args) (moby.VolumesPruneReport, error) {
|
||||
|
||||
@@ -54,6 +54,8 @@ type ServiceProxy struct {
|
||||
MaxConcurrencyFn func(parallel int)
|
||||
DryRunModeFn func(ctx context.Context, dryRun bool) (context.Context, error)
|
||||
VizFn func(ctx context.Context, project *types.Project, options VizOptions) (string, error)
|
||||
WaitFn func(ctx context.Context, projectName string, options WaitOptions) (int64, error)
|
||||
PublishFn func(ctx context.Context, project *types.Project, repository string) error
|
||||
interceptors []Interceptor
|
||||
}
|
||||
|
||||
@@ -90,11 +92,13 @@ func (s *ServiceProxy) WithService(service Service) *ServiceProxy {
|
||||
s.TopFn = service.Top
|
||||
s.EventsFn = service.Events
|
||||
s.PortFn = service.Port
|
||||
s.PublishFn = service.Publish
|
||||
s.ImagesFn = service.Images
|
||||
s.WatchFn = service.Watch
|
||||
s.MaxConcurrencyFn = service.MaxConcurrency
|
||||
s.DryRunModeFn = service.DryRunMode
|
||||
s.VizFn = service.Viz
|
||||
s.WaitFn = service.Wait
|
||||
return s
|
||||
}
|
||||
|
||||
@@ -309,6 +313,10 @@ func (s *ServiceProxy) Port(ctx context.Context, projectName string, service str
|
||||
return s.PortFn(ctx, projectName, service, port, options)
|
||||
}
|
||||
|
||||
func (s *ServiceProxy) Publish(ctx context.Context, project *types.Project, repository string) error {
|
||||
return s.PublishFn(ctx, project, repository)
|
||||
}
|
||||
|
||||
// Images implements Service interface
|
||||
func (s *ServiceProxy) Images(ctx context.Context, project string, options ImagesOptions) ([]ImageSummary, error) {
|
||||
if s.ImagesFn == nil {
|
||||
@@ -325,7 +333,7 @@ func (s *ServiceProxy) Watch(ctx context.Context, project *types.Project, servic
|
||||
return s.WatchFn(ctx, project, services, options)
|
||||
}
|
||||
|
||||
// Viz implements Viz interface
|
||||
// Viz implements Service interface
|
||||
func (s *ServiceProxy) Viz(ctx context.Context, project *types.Project, options VizOptions) (string, error) {
|
||||
if s.VizFn == nil {
|
||||
return "", ErrNotImplemented
|
||||
@@ -333,6 +341,14 @@ func (s *ServiceProxy) Viz(ctx context.Context, project *types.Project, options
|
||||
return s.VizFn(ctx, project, options)
|
||||
}
|
||||
|
||||
// Wait implements Service interface
|
||||
func (s *ServiceProxy) Wait(ctx context.Context, projectName string, options WaitOptions) (int64, error) {
|
||||
if s.WaitFn == nil {
|
||||
return 0, ErrNotImplemented
|
||||
}
|
||||
return s.WaitFn(ctx, projectName, options)
|
||||
}
|
||||
|
||||
func (s *ServiceProxy) MaxConcurrency(i int) {
|
||||
s.MaxConcurrencyFn(i)
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/docker/buildx/controller/pb"
|
||||
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
"github.com/containerd/containerd/platforms"
|
||||
"github.com/docker/buildx/build"
|
||||
@@ -38,6 +40,7 @@ import (
|
||||
"github.com/moby/buildkit/util/entitlements"
|
||||
specs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
@@ -68,6 +71,7 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti
|
||||
// build and will lock
|
||||
progressCtx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
w, err := xprogress.NewPrinter(progressCtx, s.stdout(), os.Stdout, options.Progress)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -112,11 +116,11 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti
|
||||
}
|
||||
buildOptions.BuildArgs = mergeArgs(buildOptions.BuildArgs, flatten(args))
|
||||
|
||||
ids, err := s.doBuildBuildkit(ctx, service.Name, buildOptions, w)
|
||||
digest, err := s.doBuildBuildkit(ctx, service.Name, buildOptions, w, options.Builder)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
builtIDs[idx] = ids[service.Name]
|
||||
builtIDs[idx] = digest
|
||||
|
||||
return nil
|
||||
}, func(traversal *graphTraversal) {
|
||||
@@ -175,20 +179,24 @@ func (s *composeService) ensureImagesExists(ctx context.Context, project *types.
|
||||
mode = xprogress.PrinterModeQuiet
|
||||
}
|
||||
|
||||
err = s.prepareProjectForBuild(project, images)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
builtImages, err := s.build(ctx, project, api.BuildOptions{
|
||||
Progress: mode,
|
||||
})
|
||||
buildRequired, err := s.prepareProjectForBuild(project, images)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for name, digest := range builtImages {
|
||||
images[name] = digest
|
||||
if buildRequired {
|
||||
builtImages, err := s.build(ctx, project, api.BuildOptions{
|
||||
Progress: mode,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for name, digest := range builtImages {
|
||||
images[name] = digest
|
||||
}
|
||||
}
|
||||
|
||||
// set digest as com.docker.compose.image label so we can detect outdated containers
|
||||
for i, service := range project.Services {
|
||||
image := api.GetImageNameOrDefault(service, project.Name)
|
||||
@@ -203,10 +211,11 @@ func (s *composeService) ensureImagesExists(ctx context.Context, project *types.
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *composeService) prepareProjectForBuild(project *types.Project, images map[string]string) error {
|
||||
func (s *composeService) prepareProjectForBuild(project *types.Project, images map[string]string) (bool, error) {
|
||||
buildRequired := false
|
||||
err := api.BuildOptions{}.Apply(project)
|
||||
if err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
for i, service := range project.Services {
|
||||
if service.Build == nil {
|
||||
@@ -227,8 +236,9 @@ func (s *composeService) prepareProjectForBuild(project *types.Project, images m
|
||||
service.Build.Platforms = []string{service.Platform}
|
||||
}
|
||||
project.Services[i] = service
|
||||
buildRequired = true
|
||||
}
|
||||
return nil
|
||||
return buildRequired, nil
|
||||
}
|
||||
|
||||
func (s *composeService) getLocalImagesDigests(ctx context.Context, project *types.Project) (map[string]string, error) {
|
||||
@@ -251,6 +261,9 @@ func (s *composeService) getLocalImagesDigests(ctx context.Context, project *typ
|
||||
for i, service := range project.Services {
|
||||
imgName := api.GetImageNameOrDefault(service, project.Name)
|
||||
digest, ok := images[imgName]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if service.Platform != "" {
|
||||
platform, err := platforms.Parse(service.Platform)
|
||||
if err != nil {
|
||||
@@ -271,9 +284,8 @@ func (s *composeService) getLocalImagesDigests(ctx context.Context, project *typ
|
||||
}
|
||||
}
|
||||
|
||||
if ok {
|
||||
project.Services[i].CustomLabels.Add(api.ImageDigestLabel, digest)
|
||||
}
|
||||
project.Services[i].CustomLabels.Add(api.ImageDigestLabel, digest)
|
||||
|
||||
}
|
||||
|
||||
return images, nil
|
||||
@@ -356,8 +368,8 @@ func (s *composeService) toBuildOptions(project *types.Project, service types.Se
|
||||
DockerfilePath: dockerFilePath(service.Build.Context, service.Build.Dockerfile),
|
||||
NamedContexts: toBuildContexts(service.Build.AdditionalContexts),
|
||||
},
|
||||
CacheFrom: cacheFrom,
|
||||
CacheTo: cacheTo,
|
||||
CacheFrom: pb.CreateCaches(cacheFrom),
|
||||
CacheTo: pb.CreateCaches(cacheTo),
|
||||
NoCache: service.Build.NoCache,
|
||||
Pull: service.Build.Pull,
|
||||
BuildArgs: buildArgs,
|
||||
@@ -440,6 +452,9 @@ func addSecretsConfig(project *types.Project, service types.ServiceConfig) (sess
|
||||
default:
|
||||
return nil, fmt.Errorf("build.secrets only supports environment or file-based secrets: %q", secret.Source)
|
||||
}
|
||||
if secret.UID != "" || secret.GID != "" || secret.Mode != nil {
|
||||
logrus.Warn("secrets `uid`, `gid` and `mode` are not supported by BuildKit, they will be ignored")
|
||||
}
|
||||
}
|
||||
store, err := secretsprovider.NewStore(sources)
|
||||
if err != nil {
|
||||
|
||||
@@ -20,44 +20,46 @@ import (
|
||||
"context"
|
||||
"crypto/sha1"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/docker/buildx/build"
|
||||
"github.com/docker/buildx/builder"
|
||||
_ "github.com/docker/buildx/driver/docker" //nolint:blank-imports
|
||||
_ "github.com/docker/buildx/driver/docker-container" //nolint:blank-imports
|
||||
_ "github.com/docker/buildx/driver/kubernetes" //nolint:blank-imports
|
||||
_ "github.com/docker/buildx/driver/remote" //nolint:blank-imports
|
||||
buildx "github.com/docker/buildx/util/progress"
|
||||
"github.com/moby/buildkit/client"
|
||||
|
||||
"github.com/docker/buildx/build"
|
||||
"github.com/docker/buildx/builder"
|
||||
"github.com/docker/buildx/util/confutil"
|
||||
"github.com/docker/buildx/util/dockerutil"
|
||||
buildx "github.com/docker/buildx/util/progress"
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
"github.com/moby/buildkit/client"
|
||||
)
|
||||
|
||||
func (s *composeService) doBuildBuildkit(ctx context.Context, service string, opts build.Options, p *buildx.Printer) (map[string]string, error) {
|
||||
b, err := builder.New(s.dockerCli)
|
||||
func (s *composeService) doBuildBuildkit(ctx context.Context, service string, opts build.Options, p *buildx.Printer, builderName string) (string, error) {
|
||||
b, err := builder.New(s.dockerCli, builder.WithName(builderName))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return "", err
|
||||
}
|
||||
|
||||
nodes, err := b.LoadNodes(ctx, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return "", err
|
||||
}
|
||||
|
||||
var response map[string]*client.SolveResponse
|
||||
if s.dryRun {
|
||||
response = s.dryRunBuildResponse(ctx, service, opts)
|
||||
} else {
|
||||
response, err = build.Build(ctx, nodes, map[string]build.Options{service: opts}, dockerutil.NewClient(s.dockerCli), filepath.Dir(s.configFile().Filename), buildx.WithPrefix(p, service, true))
|
||||
response, err = build.Build(ctx, nodes,
|
||||
map[string]build.Options{service: opts},
|
||||
dockerutil.NewClient(s.dockerCli),
|
||||
confutil.ConfigDir(s.dockerCli),
|
||||
buildx.WithPrefix(p, service, true))
|
||||
if err != nil {
|
||||
return nil, WrapCategorisedComposeError(err, BuildFailure)
|
||||
return "", WrapCategorisedComposeError(err, BuildFailure)
|
||||
}
|
||||
}
|
||||
|
||||
imagesBuilt := map[string]string{}
|
||||
for name, img := range response {
|
||||
for _, img := range response {
|
||||
if img == nil || len(img.ExporterResponse) == 0 {
|
||||
continue
|
||||
}
|
||||
@@ -65,10 +67,10 @@ func (s *composeService) doBuildBuildkit(ctx context.Context, service string, op
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
imagesBuilt[name] = digest
|
||||
return digest, nil
|
||||
}
|
||||
|
||||
return imagesBuilt, err
|
||||
return "", fmt.Errorf("buildkit response is missing expected result for %s", service)
|
||||
}
|
||||
|
||||
func (s composeService) dryRunBuildResponse(ctx context.Context, name string, options build.Options) map[string]*client.SolveResponse {
|
||||
|
||||
@@ -26,6 +26,8 @@ import (
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/api/types/registry"
|
||||
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command/image/build"
|
||||
@@ -153,9 +155,9 @@ func (s *composeService) doBuildClassic(ctx context.Context, service types.Servi
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
authConfigs := make(map[string]dockertypes.AuthConfig, len(creds))
|
||||
authConfigs := make(map[string]registry.AuthConfig, len(creds))
|
||||
for k, auth := range creds {
|
||||
authConfigs[k] = dockertypes.AuthConfig(auth)
|
||||
authConfigs[k] = registry.AuthConfig(auth)
|
||||
}
|
||||
buildOptions := imageBuildOptions(service.Build)
|
||||
buildOptions.Tags = append(buildOptions.Tags, service.Image)
|
||||
|
||||
@@ -44,7 +44,7 @@ func TestPrepareProjectForBuild(t *testing.T) {
|
||||
}
|
||||
|
||||
s := &composeService{}
|
||||
err := s.prepareProjectForBuild(&project, nil)
|
||||
_, err := s.prepareProjectForBuild(&project, nil)
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, project.Services[0].Build.Platforms, types.StringList{"alice/32"})
|
||||
})
|
||||
@@ -70,7 +70,7 @@ func TestPrepareProjectForBuild(t *testing.T) {
|
||||
}
|
||||
|
||||
s := &composeService{}
|
||||
err := s.prepareProjectForBuild(&project, nil)
|
||||
_, err := s.prepareProjectForBuild(&project, nil)
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, project.Services[0].Build.Platforms, types.StringList{"linux/amd64"})
|
||||
})
|
||||
@@ -89,7 +89,7 @@ func TestPrepareProjectForBuild(t *testing.T) {
|
||||
}
|
||||
|
||||
s := &composeService{}
|
||||
err := s.prepareProjectForBuild(&project, map[string]string{"foo": "exists"})
|
||||
_, err := s.prepareProjectForBuild(&project, map[string]string{"foo": "exists"})
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, project.Services[0].Build == nil)
|
||||
})
|
||||
@@ -115,7 +115,7 @@ func TestPrepareProjectForBuild(t *testing.T) {
|
||||
}
|
||||
|
||||
s := &composeService{}
|
||||
err := s.prepareProjectForBuild(&project, nil)
|
||||
_, err := s.prepareProjectForBuild(&project, nil)
|
||||
assert.Check(t, err != nil)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/docker/docker/api/types/volume"
|
||||
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
"github.com/distribution/distribution/v3/reference"
|
||||
"github.com/docker/cli/cli/command"
|
||||
@@ -230,7 +232,10 @@ SERVICES:
|
||||
}
|
||||
|
||||
func (s *composeService) actualVolumes(ctx context.Context, projectName string) (types.Volumes, error) {
|
||||
volumes, err := s.apiClient().VolumeList(ctx, filters.NewArgs(projectFilter(projectName)))
|
||||
opts := volume.ListOptions{
|
||||
Filters: filters.NewArgs(projectFilter(projectName)),
|
||||
}
|
||||
volumes, err := s.apiClient().VolumeList(ctx, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -277,8 +282,11 @@ func (s *composeService) isSWarmEnabled(ctx context.Context) (bool, error) {
|
||||
if err != nil {
|
||||
swarmEnabled.err = err
|
||||
}
|
||||
if info.Swarm.LocalNodeState == swarm.LocalNodeStateInactive {
|
||||
swarmEnabled.val = info.Swarm.LocalNodeState == swarm.LocalNodeStateInactive
|
||||
switch info.Swarm.LocalNodeState {
|
||||
case swarm.LocalNodeStateInactive, swarm.LocalNodeStateLocked:
|
||||
swarmEnabled.val = false
|
||||
default:
|
||||
swarmEnabled.val = true
|
||||
}
|
||||
})
|
||||
return swarmEnabled.val, swarmEnabled.err
|
||||
|
||||
@@ -19,6 +19,7 @@ package compose
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -28,7 +29,6 @@ import (
|
||||
"github.com/containerd/containerd/platforms"
|
||||
moby "github.com/docker/docker/api/types"
|
||||
containerType "github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
specs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
@@ -172,6 +172,9 @@ func (c *convergence) ensureService(ctx context.Context, project *types.Project,
|
||||
|
||||
eg, _ := errgroup.WithContext(ctx)
|
||||
|
||||
sort.Slice(containers, func(i, j int) bool {
|
||||
return containers[i].Created < containers[j].Created
|
||||
})
|
||||
for i, container := range containers {
|
||||
if i >= expected {
|
||||
// Scale Down
|
||||
@@ -229,7 +232,13 @@ func (c *convergence) ensureService(ctx context.Context, project *types.Project,
|
||||
name := getContainerName(project.Name, service, number)
|
||||
i := i
|
||||
eg.Go(func() error {
|
||||
container, err := c.service.createContainer(ctx, project, service, name, number, false, true, false)
|
||||
opts := createOptions{
|
||||
AutoRemove: false,
|
||||
AttachStdin: false,
|
||||
UseNetworkAliases: true,
|
||||
Labels: mergeLabels(service.Labels, service.CustomLabels),
|
||||
}
|
||||
container, err := c.service.createContainer(ctx, project, service, name, number, opts)
|
||||
updated[actual+i] = container
|
||||
return err
|
||||
})
|
||||
@@ -395,12 +404,11 @@ func getScale(config types.ServiceConfig) (int, error) {
|
||||
}
|
||||
|
||||
func (s *composeService) createContainer(ctx context.Context, project *types.Project, service types.ServiceConfig,
|
||||
name string, number int, autoRemove bool, useNetworkAliases bool, attachStdin bool) (container moby.Container, err error) {
|
||||
name string, number int, opts createOptions) (container moby.Container, err error) {
|
||||
w := progress.ContextWriter(ctx)
|
||||
eventName := "Container " + name
|
||||
w.Event(progress.CreatingEvent(eventName))
|
||||
container, err = s.createMobyContainer(ctx, project, service, name, number, nil,
|
||||
autoRemove, useNetworkAliases, attachStdin, w, mergeLabels(service.Labels, service.CustomLabels))
|
||||
container, err = s.createMobyContainer(ctx, project, service, name, number, nil, opts, w)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -425,9 +433,13 @@ func (s *composeService) recreateContainer(ctx context.Context, project *types.P
|
||||
}
|
||||
name := getContainerName(project.Name, service, number)
|
||||
tmpName := fmt.Sprintf("%s_%s", replaced.ID[:12], name)
|
||||
created, err = s.createMobyContainer(ctx, project, service, tmpName, number, inherited,
|
||||
false, true, false, w,
|
||||
mergeLabels(service.Labels, service.CustomLabels).Add(api.ContainerReplaceLabel, replaced.ID))
|
||||
opts := createOptions{
|
||||
AutoRemove: false,
|
||||
AttachStdin: false,
|
||||
UseNetworkAliases: true,
|
||||
Labels: mergeLabels(service.Labels, service.CustomLabels).Add(api.ContainerReplaceLabel, replaced.ID),
|
||||
}
|
||||
created, err = s.createMobyContainer(ctx, project, service, tmpName, number, inherited, opts, w)
|
||||
if err != nil {
|
||||
return created, err
|
||||
}
|
||||
@@ -486,13 +498,12 @@ func (s *composeService) createMobyContainer(ctx context.Context,
|
||||
name string,
|
||||
number int,
|
||||
inherit *moby.Container,
|
||||
autoRemove, useNetworkAliases, attachStdin bool,
|
||||
opts createOptions,
|
||||
w progress.Writer,
|
||||
labels types.Labels,
|
||||
) (moby.Container, error) {
|
||||
var created moby.Container
|
||||
containerConfig, hostConfig, networkingConfig, err := s.getCreateOptions(ctx, project, service, number, inherit,
|
||||
autoRemove, attachStdin, labels)
|
||||
cfgs, err := s.getCreateConfigs(ctx, project, service, number, inherit, opts)
|
||||
|
||||
if err != nil {
|
||||
return created, err
|
||||
}
|
||||
@@ -509,7 +520,8 @@ func (s *composeService) createMobyContainer(ctx context.Context,
|
||||
}
|
||||
plat = &p
|
||||
}
|
||||
response, err := s.apiClient().ContainerCreate(ctx, containerConfig, hostConfig, networkingConfig, plat, name)
|
||||
|
||||
response, err := s.apiClient().ContainerCreate(ctx, cfgs.Container, cfgs.Host, cfgs.Network, plat, name)
|
||||
if err != nil {
|
||||
return created, err
|
||||
}
|
||||
@@ -532,33 +544,19 @@ func (s *composeService) createMobyContainer(ctx context.Context,
|
||||
Networks: inspectedContainer.NetworkSettings.Networks,
|
||||
},
|
||||
}
|
||||
links, err := s.getLinks(ctx, project.Name, service, number)
|
||||
if err != nil {
|
||||
return created, err
|
||||
}
|
||||
for _, netName := range service.NetworksByPriority() {
|
||||
netwrk := project.Networks[netName]
|
||||
cfg := service.Networks[netName]
|
||||
aliases := []string{getContainerName(project.Name, service, number)}
|
||||
if useNetworkAliases {
|
||||
aliases = append(aliases, service.Name)
|
||||
if cfg != nil {
|
||||
aliases = append(aliases, cfg.Aliases...)
|
||||
}
|
||||
}
|
||||
if val, ok := created.NetworkSettings.Networks[netwrk.Name]; ok {
|
||||
if shortIDAliasExists(created.ID, val.Aliases...) {
|
||||
continue
|
||||
}
|
||||
err = s.apiClient().NetworkDisconnect(ctx, netwrk.Name, created.ID, false)
|
||||
if err != nil {
|
||||
|
||||
// the highest-priority network is the primary and is included in the ContainerCreate API
|
||||
// call via container.NetworkMode & network.NetworkingConfig
|
||||
// any remaining networks are connected one-by-one here after creation (but before start)
|
||||
serviceNetworks := service.NetworksByPriority()
|
||||
if len(serviceNetworks) > 1 {
|
||||
for _, networkKey := range serviceNetworks[1:] {
|
||||
mobyNetworkName := project.Networks[networkKey].Name
|
||||
epSettings := createEndpointSettings(project, service, number, networkKey, cfgs.Links, opts.UseNetworkAliases)
|
||||
if err := s.apiClient().NetworkConnect(ctx, mobyNetworkName, created.ID, epSettings); err != nil {
|
||||
return created, err
|
||||
}
|
||||
}
|
||||
err = s.connectContainerToNetwork(ctx, created.ID, netwrk.Name, cfg, links, aliases...)
|
||||
if err != nil {
|
||||
return created, err
|
||||
}
|
||||
}
|
||||
|
||||
err = s.injectSecrets(ctx, project, service, created.ID)
|
||||
@@ -623,43 +621,6 @@ func (s *composeService) getLinks(ctx context.Context, projectName string, servi
|
||||
return links, nil
|
||||
}
|
||||
|
||||
func shortIDAliasExists(containerID string, aliases ...string) bool {
|
||||
for _, alias := range aliases {
|
||||
if alias == containerID[:12] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *composeService) connectContainerToNetwork(ctx context.Context, id string, netwrk string, cfg *types.ServiceNetworkConfig, links []string, aliases ...string) error {
|
||||
var (
|
||||
ipv4Address string
|
||||
ipv6Address string
|
||||
ipam *network.EndpointIPAMConfig
|
||||
)
|
||||
if cfg != nil {
|
||||
ipv4Address = cfg.Ipv4Address
|
||||
ipv6Address = cfg.Ipv6Address
|
||||
ipam = &network.EndpointIPAMConfig{
|
||||
IPv4Address: ipv4Address,
|
||||
IPv6Address: ipv6Address,
|
||||
LinkLocalIPs: cfg.LinkLocalIPs,
|
||||
}
|
||||
}
|
||||
err := s.apiClient().NetworkConnect(ctx, netwrk, id, &network.EndpointSettings{
|
||||
Aliases: aliases,
|
||||
IPAddress: ipv4Address,
|
||||
GlobalIPv6Address: ipv6Address,
|
||||
Links: links,
|
||||
IPAMConfig: ipam,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *composeService) isServiceHealthy(ctx context.Context, containers Containers, fallbackRunning bool) (bool, error) {
|
||||
for _, c := range containers {
|
||||
container, err := s.apiClient().ContainerInspect(ctx, c.ID)
|
||||
|
||||
@@ -325,5 +325,5 @@ func resolveLocalPath(localPath string) (absPath string, err error) {
|
||||
if absPath, err = filepath.Abs(localPath); err != nil {
|
||||
return
|
||||
}
|
||||
return archive.PreserveTrailingDotOrSeparator(absPath, localPath, filepath.Separator), nil
|
||||
return archive.PreserveTrailingDotOrSeparator(absPath, localPath), nil
|
||||
}
|
||||
|
||||
@@ -48,6 +48,20 @@ import (
|
||||
"github.com/docker/compose/v2/pkg/utils"
|
||||
)
|
||||
|
||||
type createOptions struct {
|
||||
AutoRemove bool
|
||||
AttachStdin bool
|
||||
UseNetworkAliases bool
|
||||
Labels types.Labels
|
||||
}
|
||||
|
||||
type createConfigs struct {
|
||||
Container *container.Config
|
||||
Host *container.HostConfig
|
||||
Network *network.NetworkingConfig
|
||||
Links []string
|
||||
}
|
||||
|
||||
func (s *composeService) Create(ctx context.Context, project *types.Project, options api.CreateOptions) error {
|
||||
return progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
||||
return s.create(ctx, project, options)
|
||||
@@ -106,11 +120,6 @@ func (s *composeService) create(ctx context.Context, project *types.Project, opt
|
||||
}
|
||||
}
|
||||
|
||||
err = prepareServicesDependsOn(project)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return newConvergence(options.Services, observedState, s).apply(ctx, project, options)
|
||||
}
|
||||
|
||||
@@ -147,78 +156,13 @@ func prepareNetworks(project *types.Project) {
|
||||
}
|
||||
}
|
||||
|
||||
func prepareServicesDependsOn(p *types.Project) error {
|
||||
allServices := types.Project{}
|
||||
allServices.Services = p.AllServices()
|
||||
|
||||
for i, service := range p.Services {
|
||||
var dependencies []string
|
||||
networkDependency := getDependentServiceFromMode(service.NetworkMode)
|
||||
if networkDependency != "" {
|
||||
dependencies = append(dependencies, networkDependency)
|
||||
}
|
||||
|
||||
ipcDependency := getDependentServiceFromMode(service.Ipc)
|
||||
if ipcDependency != "" {
|
||||
dependencies = append(dependencies, ipcDependency)
|
||||
}
|
||||
|
||||
pidDependency := getDependentServiceFromMode(service.Pid)
|
||||
if pidDependency != "" {
|
||||
dependencies = append(dependencies, pidDependency)
|
||||
}
|
||||
|
||||
for _, vol := range service.VolumesFrom {
|
||||
spec := strings.Split(vol, ":")
|
||||
if len(spec) == 0 {
|
||||
continue
|
||||
}
|
||||
if spec[0] == "container" {
|
||||
continue
|
||||
}
|
||||
dependencies = append(dependencies, spec[0])
|
||||
}
|
||||
|
||||
for _, link := range service.Links {
|
||||
dependencies = append(dependencies, strings.Split(link, ":")[0])
|
||||
}
|
||||
|
||||
for d := range service.DependsOn {
|
||||
dependencies = append(dependencies, d)
|
||||
}
|
||||
|
||||
if len(dependencies) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Verify dependencies exist in the project, whether disabled or not
|
||||
deps, err := allServices.GetServices(dependencies...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if service.DependsOn == nil {
|
||||
service.DependsOn = make(types.DependsOnConfig)
|
||||
}
|
||||
|
||||
for _, d := range deps {
|
||||
if _, ok := service.DependsOn[d.Name]; !ok {
|
||||
service.DependsOn[d.Name] = types.ServiceDependency{
|
||||
Condition: types.ServiceConditionStarted,
|
||||
}
|
||||
}
|
||||
}
|
||||
p.Services[i] = service
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *composeService) ensureNetworks(ctx context.Context, networks types.Networks) error {
|
||||
for _, network := range networks {
|
||||
err := s.ensureNetwork(ctx, network)
|
||||
for i, network := range networks {
|
||||
err := s.ensureNetwork(ctx, &network)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
networks[i] = network
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -236,18 +180,16 @@ func (s *composeService) ensureProjectVolumes(ctx context.Context, project *type
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *composeService) getCreateOptions(ctx context.Context,
|
||||
func (s *composeService) getCreateConfigs(ctx context.Context,
|
||||
p *types.Project,
|
||||
service types.ServiceConfig,
|
||||
number int,
|
||||
inherit *moby.Container,
|
||||
autoRemove, attachStdin bool,
|
||||
labels types.Labels,
|
||||
) (*container.Config, *container.HostConfig, *network.NetworkingConfig, error) {
|
||||
|
||||
labels, err := s.prepareLabels(labels, service, number)
|
||||
opts createOptions,
|
||||
) (createConfigs, error) {
|
||||
labels, err := s.prepareLabels(opts.Labels, service, number)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
return createConfigs{}, err
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -266,11 +208,6 @@ func (s *composeService) getCreateOptions(ctx context.Context,
|
||||
stdinOpen = service.StdinOpen
|
||||
)
|
||||
|
||||
binds, mounts, err := s.buildContainerVolumes(ctx, *p, service, inherit)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
proxyConfig := types.MappingWithEquals(s.configFile().ParseProxyConfig(s.apiClient().DaemonHost(), nil))
|
||||
env := proxyConfig.OverrideBy(service.Environment)
|
||||
|
||||
@@ -281,8 +218,8 @@ func (s *composeService) getCreateOptions(ctx context.Context,
|
||||
ExposedPorts: buildContainerPorts(service),
|
||||
Tty: tty,
|
||||
OpenStdin: stdinOpen,
|
||||
StdinOnce: attachStdin && stdinOpen,
|
||||
AttachStdin: attachStdin,
|
||||
StdinOnce: opts.AttachStdin && stdinOpen,
|
||||
AttachStdin: opts.AttachStdin,
|
||||
AttachStderr: true,
|
||||
AttachStdout: true,
|
||||
Cmd: runCmd,
|
||||
@@ -298,20 +235,7 @@ func (s *composeService) getCreateOptions(ctx context.Context,
|
||||
StopTimeout: ToSeconds(service.StopGracePeriod),
|
||||
}
|
||||
|
||||
portBindings := buildContainerPortBindingOptions(service)
|
||||
|
||||
resources := getDeployResources(service)
|
||||
|
||||
if service.NetworkMode == "" {
|
||||
service.NetworkMode = getDefaultNetworkMode(p, service)
|
||||
}
|
||||
|
||||
var networkConfig *network.NetworkingConfig
|
||||
for _, id := range service.NetworksByPriority() {
|
||||
networkConfig = s.createNetworkConfig(p, service, id)
|
||||
break
|
||||
}
|
||||
|
||||
// VOLUMES/MOUNTS/FILESYSTEMS
|
||||
tmpfs := map[string]string{}
|
||||
for _, t := range service.Tmpfs {
|
||||
if arr := strings.SplitN(t, ":", 2); len(arr) > 1 {
|
||||
@@ -320,7 +244,28 @@ func (s *composeService) getCreateOptions(ctx context.Context,
|
||||
tmpfs[arr[0]] = ""
|
||||
}
|
||||
}
|
||||
binds, mounts, err := s.buildContainerVolumes(ctx, *p, service, inherit)
|
||||
if err != nil {
|
||||
return createConfigs{}, err
|
||||
}
|
||||
var volumesFrom []string
|
||||
for _, v := range service.VolumesFrom {
|
||||
if !strings.HasPrefix(v, "container:") {
|
||||
return createConfigs{}, fmt.Errorf("invalid volume_from: %s", v)
|
||||
}
|
||||
volumesFrom = append(volumesFrom, v[len("container:"):])
|
||||
}
|
||||
|
||||
// NETWORKING
|
||||
links, err := s.getLinks(ctx, p.Name, service, number)
|
||||
if err != nil {
|
||||
return createConfigs{}, err
|
||||
}
|
||||
networkMode, networkingConfig := defaultNetworkSettings(p, service, number, links, opts.UseNetworkAliases)
|
||||
portBindings := buildContainerPortBindingOptions(service)
|
||||
|
||||
// MISC
|
||||
resources := getDeployResources(service)
|
||||
var logConfig container.LogConfig
|
||||
if service.Logging != nil {
|
||||
logConfig = container.LogConfig{
|
||||
@@ -328,31 +273,18 @@ func (s *composeService) getCreateOptions(ctx context.Context,
|
||||
Config: service.Logging.Options,
|
||||
}
|
||||
}
|
||||
|
||||
var volumesFrom []string
|
||||
for _, v := range service.VolumesFrom {
|
||||
if !strings.HasPrefix(v, "container:") {
|
||||
return nil, nil, nil, fmt.Errorf("invalid volume_from: %s", v)
|
||||
}
|
||||
volumesFrom = append(volumesFrom, v[len("container:"):])
|
||||
}
|
||||
|
||||
links, err := s.getLinks(ctx, p.Name, service, number)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
securityOpts, unconfined, err := parseSecurityOpts(p, service.SecurityOpt)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
return createConfigs{}, err
|
||||
}
|
||||
|
||||
hostConfig := container.HostConfig{
|
||||
AutoRemove: autoRemove,
|
||||
AutoRemove: opts.AutoRemove,
|
||||
Binds: binds,
|
||||
Mounts: mounts,
|
||||
CapAdd: strslice.StrSlice(service.CapAdd),
|
||||
CapDrop: strslice.StrSlice(service.CapDrop),
|
||||
NetworkMode: container.NetworkMode(service.NetworkMode),
|
||||
NetworkMode: networkMode,
|
||||
Init: service.Init,
|
||||
IpcMode: container.IpcMode(service.Ipc),
|
||||
CgroupnsMode: container.CgroupnsMode(service.Cgroup),
|
||||
@@ -387,12 +319,28 @@ func (s *composeService) getCreateOptions(ctx context.Context,
|
||||
hostConfig.ReadonlyPaths = []string{}
|
||||
}
|
||||
|
||||
return &containerConfig, &hostConfig, networkConfig, nil
|
||||
cfgs := createConfigs{
|
||||
Container: &containerConfig,
|
||||
Host: &hostConfig,
|
||||
Network: networkingConfig,
|
||||
Links: links,
|
||||
}
|
||||
return cfgs, nil
|
||||
}
|
||||
|
||||
func (s *composeService) createNetworkConfig(p *types.Project, service types.ServiceConfig, networkID string) *network.NetworkingConfig {
|
||||
net := p.Networks[networkID]
|
||||
config := service.Networks[networkID]
|
||||
func getAliases(project *types.Project, service types.ServiceConfig, serviceIndex int, networkKey string, useNetworkAliases bool) []string {
|
||||
aliases := []string{getContainerName(project.Name, service, serviceIndex)}
|
||||
if useNetworkAliases {
|
||||
aliases = append(aliases, service.Name)
|
||||
if cfg := service.Networks[networkKey]; cfg != nil {
|
||||
aliases = append(aliases, cfg.Aliases...)
|
||||
}
|
||||
}
|
||||
return aliases
|
||||
}
|
||||
|
||||
func createEndpointSettings(p *types.Project, service types.ServiceConfig, serviceIndex int, networkKey string, links []string, useNetworkAliases bool) *network.EndpointSettings {
|
||||
config := service.Networks[networkKey]
|
||||
var ipam *network.EndpointIPAMConfig
|
||||
var (
|
||||
ipv4Address string
|
||||
@@ -407,15 +355,12 @@ func (s *composeService) createNetworkConfig(p *types.Project, service types.Ser
|
||||
LinkLocalIPs: config.LinkLocalIPs,
|
||||
}
|
||||
}
|
||||
return &network.NetworkingConfig{
|
||||
EndpointsConfig: map[string]*network.EndpointSettings{
|
||||
net.Name: {
|
||||
Aliases: getAliases(service, config),
|
||||
IPAddress: ipv4Address,
|
||||
IPv6Gateway: ipv6Address,
|
||||
IPAMConfig: ipam,
|
||||
},
|
||||
},
|
||||
return &network.EndpointSettings{
|
||||
Aliases: getAliases(p, service, serviceIndex, networkKey, useNetworkAliases),
|
||||
Links: links,
|
||||
IPAddress: ipv4Address,
|
||||
IPv6Gateway: ipv6Address,
|
||||
IPAMConfig: ipam,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -474,17 +419,39 @@ func (s *composeService) prepareLabels(labels types.Labels, service types.Servic
|
||||
return labels, nil
|
||||
}
|
||||
|
||||
func getDefaultNetworkMode(project *types.Project, service types.ServiceConfig) string {
|
||||
// defaultNetworkSettings determines the container.NetworkMode and corresponding network.NetworkingConfig (nil if not applicable).
|
||||
func defaultNetworkSettings(
|
||||
project *types.Project,
|
||||
service types.ServiceConfig,
|
||||
serviceIndex int,
|
||||
links []string,
|
||||
useNetworkAliases bool,
|
||||
) (container.NetworkMode, *network.NetworkingConfig) {
|
||||
if service.NetworkMode != "" {
|
||||
return container.NetworkMode(service.NetworkMode), nil
|
||||
}
|
||||
|
||||
if len(project.Networks) == 0 {
|
||||
return "none"
|
||||
return "none", nil
|
||||
}
|
||||
|
||||
var networkKey string
|
||||
if len(service.Networks) > 0 {
|
||||
name := service.NetworksByPriority()[0]
|
||||
return project.Networks[name].Name
|
||||
networkKey = service.NetworksByPriority()[0]
|
||||
} else {
|
||||
networkKey = "default"
|
||||
}
|
||||
|
||||
return project.Networks["default"].Name
|
||||
mobyNetworkName := project.Networks[networkKey].Name
|
||||
epSettings := createEndpointSettings(project, service, serviceIndex, networkKey, links, useNetworkAliases)
|
||||
networkConfig := &network.NetworkingConfig{
|
||||
EndpointsConfig: map[string]*network.EndpointSettings{
|
||||
mobyNetworkName: epSettings,
|
||||
},
|
||||
}
|
||||
// From the Engine API docs:
|
||||
// > Supported standard values are: bridge, host, none, and container:<name|id>.
|
||||
// > Any other value is taken as a custom network's name to which this container should connect to.
|
||||
return container.NetworkMode(mobyNetworkName), networkConfig
|
||||
}
|
||||
|
||||
func getRestartPolicy(service types.ServiceConfig) container.RestartPolicy {
|
||||
@@ -640,8 +607,8 @@ func setLimits(limits *types.Resource, resources *container.Resources) {
|
||||
resources.NanoCPUs = int64(f * 1e9)
|
||||
}
|
||||
}
|
||||
if limits.PIds > 0 {
|
||||
resources.PidsLimit = &limits.PIds
|
||||
if limits.Pids > 0 {
|
||||
resources.PidsLimit = &limits.Pids
|
||||
}
|
||||
}
|
||||
|
||||
@@ -743,7 +710,10 @@ func getVolumesFrom(project *types.Project, volumesFrom []string) ([]string, []s
|
||||
}
|
||||
|
||||
func getDependentServiceFromMode(mode string) string {
|
||||
if strings.HasPrefix(mode, types.NetworkModeServicePrefix) {
|
||||
if strings.HasPrefix(
|
||||
mode,
|
||||
types.NetworkModeServicePrefix,
|
||||
) {
|
||||
return mode[len(types.NetworkModeServicePrefix):]
|
||||
}
|
||||
return ""
|
||||
@@ -1069,15 +1039,133 @@ func buildTmpfsOptions(tmpfs *types.ServiceVolumeTmpfs) *mount.TmpfsOptions {
|
||||
}
|
||||
}
|
||||
|
||||
func getAliases(s types.ServiceConfig, c *types.ServiceNetworkConfig) []string {
|
||||
aliases := []string{s.Name}
|
||||
if c != nil {
|
||||
aliases = append(aliases, c.Aliases...)
|
||||
func (s *composeService) ensureNetwork(ctx context.Context, n *types.NetworkConfig) error {
|
||||
if n.External.External {
|
||||
return s.resolveExternalNetwork(ctx, n)
|
||||
}
|
||||
return aliases
|
||||
|
||||
err := s.resolveOrCreateNetwork(ctx, n)
|
||||
if errdefs.IsConflict(err) {
|
||||
// Maybe another execution of `docker compose up|run` created same network
|
||||
// let's retry once
|
||||
return s.resolveOrCreateNetwork(ctx, n)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *composeService) ensureNetwork(ctx context.Context, n types.NetworkConfig) error {
|
||||
func (s *composeService) resolveOrCreateNetwork(ctx context.Context, n *types.NetworkConfig) error { //nolint:gocyclo
|
||||
expectedNetworkLabel := n.Labels[api.NetworkLabel]
|
||||
expectedProjectLabel := n.Labels[api.ProjectLabel]
|
||||
|
||||
// First, try to find a unique network matching by name or ID
|
||||
inspect, err := s.apiClient().NetworkInspect(ctx, n.Name, moby.NetworkInspectOptions{})
|
||||
if err == nil {
|
||||
// NetworkInspect will match on ID prefix, so double check we get the expected one
|
||||
// as looking for network named `db` we could erroneously matched network ID `db9086999caf`
|
||||
if inspect.Name == n.Name || inspect.ID == n.Name {
|
||||
p, ok := inspect.Labels[api.ProjectLabel]
|
||||
if !ok {
|
||||
logrus.Warnf("a network with name %s exists but was not created by compose.\n"+
|
||||
"Set `external: true` to use an existing network", n.Name)
|
||||
} else if p != expectedProjectLabel {
|
||||
logrus.Warnf("a network with name %s exists but was not created for project %q.\n"+
|
||||
"Set `external: true` to use an existing network", n.Name, expectedProjectLabel)
|
||||
}
|
||||
if inspect.Labels[api.NetworkLabel] != expectedNetworkLabel {
|
||||
return fmt.Errorf("network %s was found but has incorrect label %s set to %q", n.Name, api.NetworkLabel, inspect.Labels[api.NetworkLabel])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
// ignore other errors. Typically, an ambiguous request by name results in some generic `invalidParameter` error
|
||||
|
||||
// Either not found, or name is ambiguous - use NetworkList to list by name
|
||||
networks, err := s.apiClient().NetworkList(ctx, moby.NetworkListOptions{
|
||||
Filters: filters.NewArgs(filters.Arg("name", n.Name)),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// NetworkList Matches all or part of a network name, so we have to filter for a strict match
|
||||
networks = utils.Filter(networks, func(net moby.NetworkResource) bool {
|
||||
return net.Name == n.Name
|
||||
})
|
||||
|
||||
for _, net := range networks {
|
||||
if net.Labels[api.ProjectLabel] == expectedProjectLabel &&
|
||||
net.Labels[api.NetworkLabel] == expectedNetworkLabel {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// we could have set NetworkList with a projectFilter and networkFilter but not doing so allows to catch this
|
||||
// scenario were a network with same name exists but doesn't have label, and use of `CheckDuplicate: true`
|
||||
// prevents to create another one.
|
||||
if len(networks) > 0 {
|
||||
logrus.Warnf("a network with name %s exists but was not created by compose.\n"+
|
||||
"Set `external: true` to use an existing network", n.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
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,
|
||||
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,
|
||||
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))
|
||||
|
||||
_, err = s.apiClient().NetworkCreate(ctx, n.Name, createOpts)
|
||||
if 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
|
||||
}
|
||||
|
||||
func (s *composeService) resolveExternalNetwork(ctx context.Context, n *types.NetworkConfig) error {
|
||||
// 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
|
||||
@@ -1088,87 +1176,35 @@ func (s *composeService) ensureNetwork(ctx context.Context, n types.NetworkConfi
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
networkNotFound := true
|
||||
for _, net := range networks {
|
||||
if net.Name == n.Name {
|
||||
networkNotFound = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if networkNotFound {
|
||||
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
|
||||
enabled, err := s.isSWarmEnabled(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if enabled {
|
||||
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,
|
||||
}
|
||||
|
||||
if n.Ipam.Driver != "" || len(n.Ipam.Config) > 0 {
|
||||
createOpts.IPAM = &network.IPAM{}
|
||||
}
|
||||
// NetworkList API doesn't return the exact name match, so we can retrieve more than one network with a request
|
||||
networks = utils.Filter(networks, func(net moby.NetworkResource) bool {
|
||||
return net.Name == n.Name
|
||||
})
|
||||
|
||||
if n.Ipam.Driver != "" {
|
||||
createOpts.IPAM.Driver = n.Ipam.Driver
|
||||
}
|
||||
|
||||
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))
|
||||
switch len(networks) {
|
||||
case 1:
|
||||
n.Name = networks[0].ID
|
||||
return nil
|
||||
case 0:
|
||||
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
|
||||
enabled, err := s.isSWarmEnabled(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if enabled {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("network %s declared as external, but could not be found", n.Name)
|
||||
default:
|
||||
return fmt.Errorf("multiple networks with name %q were found. Use network ID as `name` to avoid ambiguity", n.Name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *composeService) ensureVolume(ctx context.Context, volume types.VolumeConfig, project string) error {
|
||||
@@ -1194,7 +1230,7 @@ func (s *composeService) ensureVolume(ctx context.Context, volume types.VolumeCo
|
||||
logrus.Warnf("volume %q already exists but was not created by Docker Compose. Use `external: true` to use an existing volume", volume.Name)
|
||||
}
|
||||
if ok && p != project {
|
||||
logrus.Warnf("volume %q already exists but was not created for project %q. Use `external: true` to use an existing volume", volume.Name, p)
|
||||
logrus.Warnf("volume %q already exists but was created for project %q (expected %q). Use `external: true` to use an existing volume", volume.Name, p, project)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ import (
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"gotest.tools/v3/assert/cmp"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
|
||||
composetypes "github.com/compose-spec/compose-go/types"
|
||||
@@ -203,7 +205,7 @@ func TestBuildContainerMountOptions(t *testing.T) {
|
||||
assert.Equal(t, mounts[2].Target, "\\\\.\\pipe\\docker_engine")
|
||||
}
|
||||
|
||||
func TestGetDefaultNetworkMode(t *testing.T) {
|
||||
func TestDefaultNetworkSettings(t *testing.T) {
|
||||
t.Run("returns the network with the highest priority when service has multiple networks", func(t *testing.T) {
|
||||
service := composetypes.ServiceConfig{
|
||||
Name: "myService",
|
||||
@@ -231,7 +233,10 @@ func TestGetDefaultNetworkMode(t *testing.T) {
|
||||
}),
|
||||
}
|
||||
|
||||
assert.Equal(t, getDefaultNetworkMode(&project, service), "myProject_myNetwork2")
|
||||
networkMode, networkConfig := defaultNetworkSettings(&project, service, 1, nil, true)
|
||||
assert.Equal(t, string(networkMode), "myProject_myNetwork2")
|
||||
assert.Check(t, cmp.Len(networkConfig.EndpointsConfig, 1))
|
||||
assert.Check(t, cmp.Contains(networkConfig.EndpointsConfig, "myProject_myNetwork2"))
|
||||
})
|
||||
|
||||
t.Run("returns default network when service has no networks", func(t *testing.T) {
|
||||
@@ -256,7 +261,10 @@ func TestGetDefaultNetworkMode(t *testing.T) {
|
||||
}),
|
||||
}
|
||||
|
||||
assert.Equal(t, getDefaultNetworkMode(&project, service), "myProject_default")
|
||||
networkMode, networkConfig := defaultNetworkSettings(&project, service, 1, nil, true)
|
||||
assert.Equal(t, string(networkMode), "myProject_default")
|
||||
assert.Check(t, cmp.Len(networkConfig.EndpointsConfig, 1))
|
||||
assert.Check(t, cmp.Contains(networkConfig.EndpointsConfig, "myProject_default"))
|
||||
})
|
||||
|
||||
t.Run("returns none if project has no networks", func(t *testing.T) {
|
||||
@@ -270,6 +278,28 @@ func TestGetDefaultNetworkMode(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, getDefaultNetworkMode(&project, service), "none")
|
||||
networkMode, networkConfig := defaultNetworkSettings(&project, service, 1, nil, true)
|
||||
assert.Equal(t, string(networkMode), "none")
|
||||
assert.Check(t, cmp.Nil(networkConfig))
|
||||
})
|
||||
|
||||
t.Run("returns defined network mode if explicitly set", func(t *testing.T) {
|
||||
service := composetypes.ServiceConfig{
|
||||
Name: "myService",
|
||||
NetworkMode: "host",
|
||||
}
|
||||
project := composetypes.Project{
|
||||
Name: "myProject",
|
||||
Services: []composetypes.ServiceConfig{service},
|
||||
Networks: composetypes.Networks(map[string]composetypes.NetworkConfig{
|
||||
"default": {
|
||||
Name: "myProject_default",
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
networkMode, networkConfig := defaultNetworkSettings(&project, service, 1, nil, true)
|
||||
assert.Equal(t, string(networkMode), "host")
|
||||
assert.Check(t, cmp.Nil(networkConfig))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/utils"
|
||||
@@ -38,8 +40,9 @@ const (
|
||||
)
|
||||
|
||||
type graphTraversal struct {
|
||||
mu sync.Mutex
|
||||
seen map[string]struct{}
|
||||
mu sync.Mutex
|
||||
seen map[string]struct{}
|
||||
ignored map[string]struct{}
|
||||
|
||||
extremityNodesFn func(*Graph) []*Vertex // leaves or roots
|
||||
adjacentNodesFn func(*Vertex) []*Vertex // getParents or getChildren
|
||||
@@ -75,7 +78,7 @@ func downDirectionTraversal(visitorFn func(context.Context, string) error) *grap
|
||||
|
||||
// InDependencyOrder applies the function to the services of the project taking in account the dependency order
|
||||
func InDependencyOrder(ctx context.Context, project *types.Project, fn func(context.Context, string) error, options ...func(*graphTraversal)) error {
|
||||
graph, err := NewGraph(project.Services, ServiceStopped)
|
||||
graph, err := NewGraph(project, ServiceStopped)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -87,15 +90,46 @@ func InDependencyOrder(ctx context.Context, project *types.Project, fn func(cont
|
||||
}
|
||||
|
||||
// InReverseDependencyOrder applies the function to the services of the project in reverse order of dependencies
|
||||
func InReverseDependencyOrder(ctx context.Context, project *types.Project, fn func(context.Context, string) error) error {
|
||||
graph, err := NewGraph(project.Services, ServiceStarted)
|
||||
func InReverseDependencyOrder(ctx context.Context, project *types.Project, fn func(context.Context, string) error, options ...func(*graphTraversal)) error {
|
||||
graph, err := NewGraph(project, ServiceStarted)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t := downDirectionTraversal(fn)
|
||||
for _, option := range options {
|
||||
option(t)
|
||||
}
|
||||
return t.visit(ctx, graph)
|
||||
}
|
||||
|
||||
func WithRootNodesAndDown(nodes []string) func(*graphTraversal) {
|
||||
return func(t *graphTraversal) {
|
||||
if len(nodes) == 0 {
|
||||
return
|
||||
}
|
||||
originalFn := t.extremityNodesFn
|
||||
t.extremityNodesFn = func(graph *Graph) []*Vertex {
|
||||
var want []string
|
||||
for _, node := range nodes {
|
||||
vertex := graph.Vertices[node]
|
||||
want = append(want, vertex.Service)
|
||||
for _, v := range getAncestors(vertex) {
|
||||
want = append(want, v.Service)
|
||||
}
|
||||
}
|
||||
|
||||
t.ignored = map[string]struct{}{}
|
||||
for k := range graph.Vertices {
|
||||
if !utils.Contains(want, k) {
|
||||
t.ignored[k] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
return originalFn(graph)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *graphTraversal) visit(ctx context.Context, g *Graph) error {
|
||||
expect := len(g.Vertices)
|
||||
if expect == 0 {
|
||||
@@ -106,24 +140,28 @@ func (t *graphTraversal) visit(ctx context.Context, g *Graph) error {
|
||||
if t.maxConcurrency > 0 {
|
||||
eg.SetLimit(t.maxConcurrency + 1)
|
||||
}
|
||||
nodeCh := make(chan *Vertex)
|
||||
nodeCh := make(chan *Vertex, expect)
|
||||
defer close(nodeCh)
|
||||
// nodeCh need to allow n=expect writers while reader goroutine could have returner after ctx.Done
|
||||
eg.Go(func() error {
|
||||
for node := range nodeCh {
|
||||
expect--
|
||||
if expect == 0 {
|
||||
close(nodeCh)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case node := <-nodeCh:
|
||||
expect--
|
||||
if expect == 0 {
|
||||
return nil
|
||||
}
|
||||
t.run(ctx, g, eg, t.adjacentNodesFn(node), nodeCh)
|
||||
}
|
||||
t.run(ctx, g, eg, t.adjacentNodesFn(node), nodeCh)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
nodes := t.extremityNodesFn(g)
|
||||
t.run(ctx, g, eg, nodes, nodeCh)
|
||||
|
||||
err := eg.Wait()
|
||||
return err
|
||||
return eg.Wait()
|
||||
}
|
||||
|
||||
// Note: this could be `graph.walk` or whatever
|
||||
@@ -142,7 +180,10 @@ func (t *graphTraversal) run(ctx context.Context, graph *Graph, eg *errgroup.Gro
|
||||
}
|
||||
|
||||
eg.Go(func() error {
|
||||
err := t.visitorFn(ctx, node.Service)
|
||||
var err error
|
||||
if _, ignore := t.ignored[node.Service]; !ignore {
|
||||
err = t.visitorFn(ctx, node.Service)
|
||||
}
|
||||
if err == nil {
|
||||
graph.UpdateStatus(node.Key, t.targetServiceStatus)
|
||||
}
|
||||
@@ -197,6 +238,16 @@ func getChildren(v *Vertex) []*Vertex {
|
||||
return v.GetChildren()
|
||||
}
|
||||
|
||||
// getAncestors return all descendents for a vertex, might contain duplicates
|
||||
func getAncestors(v *Vertex) []*Vertex {
|
||||
var descendents []*Vertex
|
||||
for _, parent := range v.GetParents() {
|
||||
descendents = append(descendents, parent)
|
||||
descendents = append(descendents, getAncestors(parent)...)
|
||||
}
|
||||
return descendents
|
||||
}
|
||||
|
||||
// GetChildren returns a slice with the child vertices of the a Vertex
|
||||
func (v *Vertex) GetChildren() []*Vertex {
|
||||
var res []*Vertex
|
||||
@@ -207,19 +258,28 @@ func (v *Vertex) GetChildren() []*Vertex {
|
||||
}
|
||||
|
||||
// NewGraph returns the dependency graph of the services
|
||||
func NewGraph(services types.Services, initialStatus ServiceStatus) (*Graph, error) {
|
||||
func NewGraph(project *types.Project, initialStatus ServiceStatus) (*Graph, error) {
|
||||
graph := &Graph{
|
||||
lock: sync.RWMutex{},
|
||||
Vertices: map[string]*Vertex{},
|
||||
}
|
||||
|
||||
for _, s := range services {
|
||||
for _, s := range project.Services {
|
||||
graph.AddVertex(s.Name, s.Name, initialStatus)
|
||||
}
|
||||
|
||||
for _, s := range services {
|
||||
for _, s := range project.Services {
|
||||
for _, name := range s.GetDependencies() {
|
||||
_ = graph.AddEdge(s.Name, name)
|
||||
err := graph.AddEdge(s.Name, name)
|
||||
if err != nil {
|
||||
if api.IsNotFoundError(err) {
|
||||
ds, err := project.GetDisabledService(name)
|
||||
if err == nil {
|
||||
return nil, fmt.Errorf("service %s is required by %s but is disabled. Can be enabled by profiles %s", name, s.Name, ds.Profiles)
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,10 +319,10 @@ func (g *Graph) AddEdge(source string, destination string) error {
|
||||
destinationVertex := g.Vertices[destination]
|
||||
|
||||
if sourceVertex == nil {
|
||||
return fmt.Errorf("could not find %s", source)
|
||||
return errors.Wrapf(api.ErrNotFound, "could not find %s", source)
|
||||
}
|
||||
if destinationVertex == nil {
|
||||
return fmt.Errorf("could not find %s", destination)
|
||||
return errors.Wrapf(api.ErrNotFound, "could not find %s", destination)
|
||||
}
|
||||
|
||||
// If they are already connected
|
||||
|
||||
@@ -19,9 +19,12 @@ package compose
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
"github.com/docker/compose/v2/pkg/utils"
|
||||
testify "github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gotest.tools/v3/assert"
|
||||
@@ -267,7 +270,7 @@ func TestBuildGraph(t *testing.T) {
|
||||
Services: tC.services,
|
||||
}
|
||||
|
||||
graph, err := NewGraph(project.Services, ServiceStopped)
|
||||
graph, err := NewGraph(&project, ServiceStopped)
|
||||
assert.NilError(t, err, fmt.Sprintf("failed to build graph for: %s", tC.desc))
|
||||
|
||||
for k, vertex := range graph.Vertices {
|
||||
@@ -297,3 +300,88 @@ func isVertexEqual(a, b Vertex) bool {
|
||||
childrenEquality &&
|
||||
parentEquality
|
||||
}
|
||||
|
||||
func TestWith_RootNodesAndUp(t *testing.T) {
|
||||
graph := &Graph{
|
||||
lock: sync.RWMutex{},
|
||||
Vertices: map[string]*Vertex{},
|
||||
}
|
||||
|
||||
/** graph topology:
|
||||
A B
|
||||
/ \ / \
|
||||
G C E
|
||||
\ /
|
||||
D
|
||||
|
|
||||
F
|
||||
*/
|
||||
|
||||
graph.AddVertex("A", "A", 0)
|
||||
graph.AddVertex("B", "B", 0)
|
||||
graph.AddVertex("C", "C", 0)
|
||||
graph.AddVertex("D", "D", 0)
|
||||
graph.AddVertex("E", "E", 0)
|
||||
graph.AddVertex("F", "F", 0)
|
||||
graph.AddVertex("G", "G", 0)
|
||||
|
||||
_ = graph.AddEdge("C", "A")
|
||||
_ = graph.AddEdge("C", "B")
|
||||
_ = graph.AddEdge("E", "B")
|
||||
_ = graph.AddEdge("D", "C")
|
||||
_ = graph.AddEdge("D", "E")
|
||||
_ = graph.AddEdge("F", "D")
|
||||
_ = graph.AddEdge("G", "A")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
nodes []string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "whole graph",
|
||||
nodes: []string{"A", "B"},
|
||||
want: []string{"A", "B", "C", "D", "E", "F", "G"},
|
||||
},
|
||||
{
|
||||
name: "only leaves",
|
||||
nodes: []string{"F", "G"},
|
||||
want: []string{"F", "G"},
|
||||
},
|
||||
{
|
||||
name: "simple dependent",
|
||||
nodes: []string{"D"},
|
||||
want: []string{"D", "F"},
|
||||
},
|
||||
{
|
||||
name: "diamond dependents",
|
||||
nodes: []string{"B"},
|
||||
want: []string{"B", "C", "D", "E", "F"},
|
||||
},
|
||||
{
|
||||
name: "partial graph",
|
||||
nodes: []string{"A"},
|
||||
want: []string{"A", "C", "D", "F", "G"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mx := sync.Mutex{}
|
||||
expected := utils.Set[string]{}
|
||||
expected.AddAll("C", "G", "D", "F")
|
||||
var visited []string
|
||||
|
||||
gt := downDirectionTraversal(func(ctx context.Context, s string) error {
|
||||
mx.Lock()
|
||||
defer mx.Unlock()
|
||||
visited = append(visited, s)
|
||||
return nil
|
||||
})
|
||||
WithRootNodesAndDown(tt.nodes)(gt)
|
||||
err := gt.visit(context.TODO(), graph)
|
||||
assert.NilError(t, err)
|
||||
sort.Strings(visited)
|
||||
assert.DeepEqual(t, tt.want, visited)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ func (s *composeService) Down(ctx context.Context, projectName string, options a
|
||||
}, s.stdinfo())
|
||||
}
|
||||
|
||||
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 { //nolint:gocyclo
|
||||
w := progress.ContextWriter(ctx)
|
||||
resourceToRemove := false
|
||||
|
||||
@@ -65,6 +65,12 @@ func (s *composeService) down(ctx context.Context, projectName string, options a
|
||||
}
|
||||
}
|
||||
|
||||
// Check requested services exists in model
|
||||
options.Services, err = checkSelectedServices(options, project)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(containers) > 0 {
|
||||
resourceToRemove = true
|
||||
}
|
||||
@@ -73,7 +79,7 @@ func (s *composeService) down(ctx context.Context, projectName string, options a
|
||||
serviceContainers := containers.filter(isService(service))
|
||||
err := s.removeContainers(ctx, w, serviceContainers, options.Timeout, options.Volumes)
|
||||
return err
|
||||
})
|
||||
}, WithRootNodesAndDown(options.Services))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -111,6 +117,23 @@ func (s *composeService) down(ctx context.Context, projectName string, options a
|
||||
return eg.Wait()
|
||||
}
|
||||
|
||||
func checkSelectedServices(options api.DownOptions, project *types.Project) ([]string, error) {
|
||||
var services []string
|
||||
for _, service := range options.Services {
|
||||
_, err := project.GetService(service)
|
||||
if err != nil {
|
||||
if options.Project != nil {
|
||||
// ran with an explicit compose.yaml file, so we should not ignore
|
||||
return nil, err
|
||||
}
|
||||
// ran without an explicit compose.yaml file, so can't distinguish typo vs container already removed
|
||||
} else {
|
||||
services = append(services, service)
|
||||
}
|
||||
}
|
||||
return services, nil
|
||||
}
|
||||
|
||||
func (s *composeService) ensureVolumesDown(ctx context.Context, project *types.Project, w progress.Writer) []downOp {
|
||||
var ops []downOp
|
||||
for _, vol := range project.Volumes {
|
||||
@@ -148,32 +171,30 @@ func (s *composeService) ensureImagesDown(ctx context.Context, project *types.Pr
|
||||
|
||||
func (s *composeService) ensureNetworksDown(ctx context.Context, project *types.Project, w progress.Writer) []downOp {
|
||||
var ops []downOp
|
||||
for _, n := range project.Networks {
|
||||
for key, n := range project.Networks {
|
||||
if n.External.External {
|
||||
continue
|
||||
}
|
||||
// loop capture variable for op closure
|
||||
networkName := n.Name
|
||||
networkKey := key
|
||||
idOrName := n.Name
|
||||
ops = append(ops, func() error {
|
||||
return s.removeNetwork(ctx, networkName, w)
|
||||
return s.removeNetwork(ctx, networkKey, project.Name, idOrName, w)
|
||||
})
|
||||
}
|
||||
return ops
|
||||
}
|
||||
|
||||
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)
|
||||
func (s *composeService) removeNetwork(ctx context.Context, composeNetworkName string, projectName string, name string, w progress.Writer) error {
|
||||
networks, err := s.apiClient().NetworkList(ctx, moby.NetworkListOptions{
|
||||
Filters: filters.NewArgs(filters.Arg("name", name)),
|
||||
Filters: filters.NewArgs(
|
||||
projectFilter(projectName),
|
||||
networkFilter(composeNetworkName)),
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, fmt.Sprintf("failed to inspect network %s", name))
|
||||
return errors.Wrapf(err, "failed to list networks")
|
||||
}
|
||||
|
||||
if len(networks) == 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -229,6 +250,10 @@ func (s *composeService) removeImage(ctx context.Context, image string, w progre
|
||||
w.Event(progress.NewEvent(id, progress.Done, "Removed"))
|
||||
return nil
|
||||
}
|
||||
if errdefs.IsConflict(err) {
|
||||
w.Event(progress.NewEvent(id, progress.Warning, "Resource is still in use"))
|
||||
return nil
|
||||
}
|
||||
if errdefs.IsNotFound(err) {
|
||||
w.Event(progress.NewEvent(id, progress.Done, "Warning: No resource found to remove"))
|
||||
return nil
|
||||
@@ -244,6 +269,10 @@ func (s *composeService) removeVolume(ctx context.Context, id string, w progress
|
||||
w.Event(progress.NewEvent(resource, progress.Done, "Removed"))
|
||||
return nil
|
||||
}
|
||||
if errdefs.IsConflict(err) {
|
||||
w.Event(progress.NewEvent(resource, progress.Warning, "Resource is still in use"))
|
||||
return nil
|
||||
}
|
||||
if errdefs.IsNotFound(err) {
|
||||
w.Event(progress.NewEvent(resource, progress.Done, "Warning: No resource found to remove"))
|
||||
return nil
|
||||
|
||||
@@ -53,15 +53,19 @@ func TestDown(t *testing.T) {
|
||||
testContainer("service2", "789", false),
|
||||
testContainer("service_orphan", "321", true),
|
||||
}, nil)
|
||||
api.EXPECT().VolumeList(gomock.Any(), filters.NewArgs(projectFilter(strings.ToLower(testProject)))).
|
||||
api.EXPECT().VolumeList(
|
||||
gomock.Any(),
|
||||
volume.ListOptions{
|
||||
Filters: filters.NewArgs(projectFilter(strings.ToLower(testProject))),
|
||||
}).
|
||||
Return(volume.ListResponse{}, 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"},
|
||||
{ID: "abc123", Name: "myProject_default", Labels: map[string]string{compose.NetworkLabel: "default"}},
|
||||
{ID: "def456", Name: "myProject_default", Labels: map[string]string{compose.NetworkLabel: "default"}},
|
||||
}, nil)
|
||||
|
||||
stopOptions := containerType.StopOptions{}
|
||||
@@ -74,7 +78,9 @@ func TestDown(t *testing.T) {
|
||||
api.EXPECT().ContainerRemove(gomock.Any(), "789", moby.ContainerRemoveOptions{Force: true}).Return(nil)
|
||||
|
||||
api.EXPECT().NetworkList(gomock.Any(), moby.NetworkListOptions{
|
||||
Filters: filters.NewArgs(filters.Arg("name", "myProject_default")),
|
||||
Filters: filters.NewArgs(
|
||||
projectFilter(strings.ToLower(testProject)),
|
||||
networkFilter("default")),
|
||||
}).Return([]moby.NetworkResource{
|
||||
{ID: "abc123", Name: "myProject_default"},
|
||||
{ID: "def456", Name: "myProject_default"},
|
||||
@@ -103,10 +109,18 @@ func TestDownRemoveOrphans(t *testing.T) {
|
||||
testContainer("service2", "789", false),
|
||||
testContainer("service_orphan", "321", true),
|
||||
}, nil)
|
||||
api.EXPECT().VolumeList(gomock.Any(), filters.NewArgs(projectFilter(strings.ToLower(testProject)))).
|
||||
api.EXPECT().VolumeList(
|
||||
gomock.Any(),
|
||||
volume.ListOptions{
|
||||
Filters: filters.NewArgs(projectFilter(strings.ToLower(testProject))),
|
||||
}).
|
||||
Return(volume.ListResponse{}, nil)
|
||||
api.EXPECT().NetworkList(gomock.Any(), moby.NetworkListOptions{Filters: filters.NewArgs(projectFilter(strings.ToLower(testProject)))}).
|
||||
Return([]moby.NetworkResource{{Name: "myProject_default"}}, nil)
|
||||
Return([]moby.NetworkResource{
|
||||
{
|
||||
Name: "myProject_default",
|
||||
Labels: map[string]string{compose.NetworkLabel: "default"},
|
||||
}}, nil)
|
||||
|
||||
stopOptions := containerType.StopOptions{}
|
||||
api.EXPECT().ContainerStop(gomock.Any(), "123", stopOptions).Return(nil)
|
||||
@@ -118,7 +132,10 @@ func TestDownRemoveOrphans(t *testing.T) {
|
||||
api.EXPECT().ContainerRemove(gomock.Any(), "321", moby.ContainerRemoveOptions{Force: true}).Return(nil)
|
||||
|
||||
api.EXPECT().NetworkList(gomock.Any(), moby.NetworkListOptions{
|
||||
Filters: filters.NewArgs(filters.Arg("name", "myProject_default")),
|
||||
Filters: filters.NewArgs(
|
||||
networkFilter("default"),
|
||||
projectFilter(strings.ToLower(testProject)),
|
||||
),
|
||||
}).Return([]moby.NetworkResource{{ID: "abc123", Name: "myProject_default"}}, nil)
|
||||
api.EXPECT().NetworkInspect(gomock.Any(), "abc123", gomock.Any()).Return(moby.NetworkResource{ID: "abc123"}, nil)
|
||||
api.EXPECT().NetworkRemove(gomock.Any(), "abc123").Return(nil)
|
||||
@@ -138,7 +155,11 @@ func TestDownRemoveVolumes(t *testing.T) {
|
||||
|
||||
api.EXPECT().ContainerList(gomock.Any(), projectFilterListOpt(false)).Return(
|
||||
[]moby.Container{testContainer("service1", "123", false)}, nil)
|
||||
api.EXPECT().VolumeList(gomock.Any(), filters.NewArgs(projectFilter(strings.ToLower(testProject)))).
|
||||
api.EXPECT().VolumeList(
|
||||
gomock.Any(),
|
||||
volume.ListOptions{
|
||||
Filters: filters.NewArgs(projectFilter(strings.ToLower(testProject))),
|
||||
}).
|
||||
Return(volume.ListResponse{
|
||||
Volumes: []*volume.Volume{{Name: "myProject_volume"}},
|
||||
}, nil)
|
||||
@@ -271,7 +292,11 @@ func TestDownRemoveImages_NoLabel(t *testing.T) {
|
||||
api.EXPECT().ContainerList(gomock.Any(), projectFilterListOpt(false)).Return(
|
||||
[]moby.Container{container}, nil)
|
||||
|
||||
api.EXPECT().VolumeList(gomock.Any(), filters.NewArgs(projectFilter(strings.ToLower(testProject)))).
|
||||
api.EXPECT().VolumeList(
|
||||
gomock.Any(),
|
||||
volume.ListOptions{
|
||||
Filters: filters.NewArgs(projectFilter(strings.ToLower(testProject))),
|
||||
}).
|
||||
Return(volume.ListResponse{
|
||||
Volumes: []*volume.Volume{{Name: "myProject_volume"}},
|
||||
}, nil)
|
||||
|
||||
@@ -25,7 +25,6 @@ import (
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/utils"
|
||||
)
|
||||
|
||||
@@ -67,8 +66,8 @@ func (s *composeService) Events(ctx context.Context, projectName string, options
|
||||
err := options.Consumer(api.Event{
|
||||
Timestamp: timestamp,
|
||||
Service: service,
|
||||
Container: event.ID,
|
||||
Status: event.Status,
|
||||
Container: event.Actor.ID,
|
||||
Status: event.Action,
|
||||
Attributes: attributes,
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -31,6 +31,10 @@ func serviceFilter(serviceName string) filters.KeyValuePair {
|
||||
return filters.Arg("label", fmt.Sprintf("%s=%s", api.ServiceLabel, serviceName))
|
||||
}
|
||||
|
||||
func networkFilter(name string) filters.KeyValuePair {
|
||||
return filters.Arg("label", fmt.Sprintf("%s=%s", api.NetworkLabel, name))
|
||||
}
|
||||
|
||||
func oneOffFilter(b bool) filters.KeyValuePair {
|
||||
v := "False"
|
||||
if b {
|
||||
|
||||
@@ -28,11 +28,12 @@ func ServiceHash(o types.ServiceConfig) (string, error) {
|
||||
// remove the Build config when generating the service hash
|
||||
o.Build = nil
|
||||
o.PullPolicy = ""
|
||||
o.Scale = 1
|
||||
if o.Deploy != nil {
|
||||
var one uint64 = 1
|
||||
o.Deploy.Replicas = &one
|
||||
if o.Deploy == nil {
|
||||
o.Deploy = &types.DeployConfig{}
|
||||
}
|
||||
o.Scale = 1
|
||||
var one uint64 = 1
|
||||
o.Deploy.Replicas = &one
|
||||
bytes, err := json.Marshal(o)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
||||
@@ -50,7 +50,11 @@ func TestKillAll(t *testing.T) {
|
||||
Filters: filters.NewArgs(projectFilter(name), hasConfigHashLabel()),
|
||||
}).Return(
|
||||
[]moby.Container{testContainer("service1", "123", false), testContainer("service1", "456", false), testContainer("service2", "789", false)}, nil)
|
||||
api.EXPECT().VolumeList(gomock.Any(), filters.NewArgs(projectFilter(strings.ToLower(testProject)))).
|
||||
api.EXPECT().VolumeList(
|
||||
gomock.Any(),
|
||||
volume.ListOptions{
|
||||
Filters: filters.NewArgs(projectFilter(strings.ToLower(testProject))),
|
||||
}).
|
||||
Return(volume.ListResponse{}, nil)
|
||||
api.EXPECT().NetworkList(gomock.Any(), moby.NetworkListOptions{Filters: filters.NewArgs(projectFilter(strings.ToLower(testProject)))}).
|
||||
Return([]moby.NetworkResource{
|
||||
@@ -81,7 +85,11 @@ func TestKillSignal(t *testing.T) {
|
||||
|
||||
ctx := context.Background()
|
||||
api.EXPECT().ContainerList(ctx, listOptions).Return([]moby.Container{testContainer(serviceName, "123", false)}, nil)
|
||||
api.EXPECT().VolumeList(gomock.Any(), filters.NewArgs(projectFilter(strings.ToLower(testProject)))).
|
||||
api.EXPECT().VolumeList(
|
||||
gomock.Any(),
|
||||
volume.ListOptions{
|
||||
Filters: filters.NewArgs(projectFilter(strings.ToLower(testProject))),
|
||||
}).
|
||||
Return(volume.ListResponse{}, nil)
|
||||
api.EXPECT().NetworkList(gomock.Any(), moby.NetworkListOptions{Filters: filters.NewArgs(projectFilter(strings.ToLower(testProject)))}).
|
||||
Return([]moby.NetworkResource{
|
||||
|
||||
@@ -45,19 +45,12 @@ func (s *composeService) Logs(
|
||||
return err
|
||||
}
|
||||
|
||||
project := options.Project
|
||||
if project == nil {
|
||||
project, err = s.getProjectWithResources(ctx, containers, projectName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if options.Project != nil && len(options.Services) == 0 {
|
||||
// we run with an explicit compose.yaml, so only consider services defined in this file
|
||||
options.Services = options.Project.ServiceNames()
|
||||
containers = containers.filter(isService(options.Services...))
|
||||
}
|
||||
|
||||
if len(options.Services) == 0 {
|
||||
options.Services = project.ServiceNames()
|
||||
}
|
||||
|
||||
containers = containers.filter(isService(options.Services...))
|
||||
eg, ctx := errgroup.WithContext(ctx)
|
||||
for _, c := range containers {
|
||||
c := c
|
||||
|
||||
@@ -71,9 +71,9 @@ func TestComposeService_Logs_Demux(t *testing.T) {
|
||||
c1Stdout := stdcopy.NewStdWriter(c1Writer, stdcopy.Stdout)
|
||||
c1Stderr := stdcopy.NewStdWriter(c1Writer, stdcopy.Stderr)
|
||||
go func() {
|
||||
_, err := c1Stdout.Write([]byte("hello stdout\n"))
|
||||
_, err := c1Stdout.Write([]byte("hello\n stdout"))
|
||||
assert.NoError(t, err, "Writing to fake stdout")
|
||||
_, err = c1Stderr.Write([]byte("hello stderr\n"))
|
||||
_, err = c1Stderr.Write([]byte("hello\n stderr"))
|
||||
assert.NoError(t, err, "Writing to fake stderr")
|
||||
_ = c1Writer.Close()
|
||||
}()
|
||||
@@ -94,7 +94,7 @@ func TestComposeService_Logs_Demux(t *testing.T) {
|
||||
|
||||
require.Equal(
|
||||
t,
|
||||
[]string{"hello stdout", "hello stderr"},
|
||||
[]string{"hello", " stdout", "hello", " stderr"},
|
||||
consumer.LogsForContainer("c"),
|
||||
)
|
||||
}
|
||||
|
||||
185
pkg/compose/publish.go
Normal file
185
pkg/compose/publish.go
Normal file
@@ -0,0 +1,185 @@
|
||||
/*
|
||||
Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package compose
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
"github.com/distribution/distribution/v3/reference"
|
||||
client2 "github.com/docker/cli/cli/registry/client"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/opencontainers/go-digest"
|
||||
"github.com/opencontainers/image-spec/specs-go"
|
||||
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
func (s *composeService) Publish(ctx context.Context, project *types.Project, repository string) error {
|
||||
err := s.Push(ctx, project, api.PushOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
target, err := reference.ParseDockerRef(repository)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client := s.dockerCli.RegistryClient(false)
|
||||
for i, service := range project.Services {
|
||||
ref, err := reference.ParseDockerRef(service.Image)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
auth, err := encodedAuth(ref, s.configFile())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
inspect, err := s.apiClient().DistributionInspect(ctx, ref.String(), auth)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
canonical, err := reference.WithDigest(ref, inspect.Descriptor.Digest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
to, err := reference.WithDigest(target, inspect.Descriptor.Digest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = client.MountBlob(ctx, canonical, to)
|
||||
switch err.(type) {
|
||||
case client2.ErrBlobCreated:
|
||||
default:
|
||||
return err
|
||||
}
|
||||
service.Image = to.String()
|
||||
project.Services[i] = service
|
||||
}
|
||||
|
||||
err = s.publishComposeYaml(ctx, project, repository)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *composeService) publishComposeYaml(ctx context.Context, project *types.Project, repository string) error {
|
||||
ref, err := reference.ParseDockerRef(repository)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var manifests []v1.Descriptor
|
||||
|
||||
for _, composeFile := range project.ComposeFiles {
|
||||
stat, err := os.Stat(composeFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, "oras", "push", "--artifact-type", "application/vnd.docker.compose.yaml", ref.String(), composeFile)
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd.Stderr = s.stderr()
|
||||
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
out, err := io.ReadAll(stdout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var composeFileDigest string
|
||||
for _, line := range strings.Split(string(out), "\n") {
|
||||
if strings.HasPrefix(line, "Digest: ") {
|
||||
composeFileDigest = line[len("Digest: "):]
|
||||
}
|
||||
fmt.Fprintln(s.stdout(), line)
|
||||
}
|
||||
if composeFileDigest == "" {
|
||||
return fmt.Errorf("expected oras to display `Digest: xxx`")
|
||||
}
|
||||
|
||||
err = cmd.Wait()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
manifests = append(manifests, v1.Descriptor{
|
||||
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
Digest: digest.Digest(composeFileDigest),
|
||||
Size: stat.Size(),
|
||||
ArtifactType: "application/vnd.docker.compose.yaml",
|
||||
})
|
||||
}
|
||||
|
||||
for _, service := range project.Services {
|
||||
dockerRef, err := reference.ParseDockerRef(service.Image)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
manifests = append(manifests, v1.Descriptor{
|
||||
MediaType: v1.MediaTypeImageIndex,
|
||||
Digest: dockerRef.(reference.Digested).Digest(),
|
||||
Annotations: map[string]string{
|
||||
"com.docker.compose.service": service.Name,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
manifest := v1.Index{
|
||||
Versioned: specs.Versioned{
|
||||
SchemaVersion: 2,
|
||||
},
|
||||
MediaType: v1.MediaTypeImageIndex,
|
||||
Manifests: manifests,
|
||||
Annotations: map[string]string{
|
||||
"com.docker.compose": api.ComposeVersion,
|
||||
},
|
||||
}
|
||||
manifestContent, err := json.Marshal(manifest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
temp, err := os.CreateTemp(os.TempDir(), "compose")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.WriteFile(temp.Name(), manifestContent, 0o700)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(temp.Name())
|
||||
|
||||
cmd := exec.CommandContext(ctx, "oras", "manifest", "push", ref.String(), temp.Name())
|
||||
cmd.Stdout = s.stdout()
|
||||
cmd.Stderr = s.stderr()
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
package compose
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
@@ -26,14 +27,17 @@ import (
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
"github.com/distribution/distribution/v3/reference"
|
||||
"github.com/docker/buildx/driver"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
moby "github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/pkg/jsonmessage"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/opencontainers/go-digest"
|
||||
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
"oras.land/oras-go/v2/content"
|
||||
"oras.land/oras-go/v2/registry/remote"
|
||||
)
|
||||
|
||||
func (s *composeService) Push(ctx context.Context, project *types.Project, options api.PushOptions) error {
|
||||
@@ -45,8 +49,8 @@ func (s *composeService) Push(ctx context.Context, project *types.Project, optio
|
||||
}, s.stdinfo(), "Pushing")
|
||||
}
|
||||
|
||||
func (s *composeService) push(ctx context.Context, project *types.Project, options api.PushOptions) error {
|
||||
eg, ctx := errgroup.WithContext(ctx)
|
||||
func (s *composeService) push(upctx context.Context, project *types.Project, options api.PushOptions) error {
|
||||
eg, ctx := errgroup.WithContext(upctx)
|
||||
eg.SetLimit(s.maxConcurrency)
|
||||
|
||||
info, err := s.apiClient().Info(ctx)
|
||||
@@ -79,7 +83,66 @@ func (s *composeService) push(ctx context.Context, project *types.Project, optio
|
||||
return nil
|
||||
})
|
||||
}
|
||||
return eg.Wait()
|
||||
|
||||
err = eg.Wait()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx = upctx
|
||||
|
||||
if options.Repository != "" {
|
||||
repository, err := remote.NewRepository(options.Repository)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
yaml, err := project.MarshalYAML()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
manifests := []v1.Descriptor{
|
||||
{
|
||||
MediaType: "application/vnd.oci.artifact.manifest.v1+json",
|
||||
Digest: digest.FromBytes(yaml),
|
||||
Size: int64(len(yaml)),
|
||||
Data: yaml,
|
||||
ArtifactType: "application/vnd.docker.compose.yaml",
|
||||
},
|
||||
}
|
||||
for _, service := range project.Services {
|
||||
inspected, _, err := s.dockerCli.Client().ImageInspectWithRaw(ctx, service.Image)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
manifests = append(manifests, v1.Descriptor{
|
||||
MediaType: v1.MediaTypeImageIndex,
|
||||
Digest: digest.Digest(inspected.RepoDigests[0]),
|
||||
Size: inspected.Size,
|
||||
Annotations: map[string]string{
|
||||
"com.docker.compose.service": service.Name,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
manifest := v1.Index{
|
||||
MediaType: v1.MediaTypeImageIndex,
|
||||
Manifests: manifests,
|
||||
Annotations: map[string]string{
|
||||
"com.docker.compose": api.ComposeVersion,
|
||||
},
|
||||
}
|
||||
manifestContent, err := json.Marshal(manifest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
manifestDescriptor := content.NewDescriptorFromBytes(v1.MediaTypeImageIndex, manifestContent)
|
||||
|
||||
err = repository.Push(ctx, manifestDescriptor, bytes.NewReader(manifestContent))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *composeService) pushServiceImage(ctx context.Context, service types.ServiceConfig, info moby.Info, configFile driver.Auth, w progress.Writer, quietPush bool) error {
|
||||
|
||||
@@ -19,11 +19,14 @@ package compose
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
"github.com/docker/cli/cli"
|
||||
cmd "github.com/docker/cli/cli/command/container"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/utils"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
)
|
||||
|
||||
@@ -38,6 +41,14 @@ func (s *composeService) RunOneOffContainer(ctx context.Context, project *types.
|
||||
start.Attach = !opts.Detach
|
||||
start.Containers = []string{containerID}
|
||||
|
||||
// remove cancellable context signal handler so we can forward signals to container without compose to exit
|
||||
signal.Reset()
|
||||
|
||||
sigc := make(chan os.Signal, 128)
|
||||
signal.Notify(sigc)
|
||||
go cmd.ForwardAllSignals(ctx, s.dockerCli, containerID, sigc)
|
||||
defer signal.Stop(sigc)
|
||||
|
||||
err = cmd.RunStart(s.dockerCli, &start)
|
||||
if sterr, ok := err.(cli.StatusError); ok {
|
||||
return sterr.StatusCode, nil
|
||||
@@ -88,8 +99,14 @@ func (s *composeService) prepareRun(ctx context.Context, project *types.Project,
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
created, err := s.createContainer(ctx, project, service, service.ContainerName, 1,
|
||||
opts.AutoRemove, opts.UseNetworkAliases, opts.Interactive)
|
||||
createOpts := createOptions{
|
||||
AutoRemove: opts.AutoRemove,
|
||||
AttachStdin: opts.Interactive,
|
||||
UseNetworkAliases: opts.UseNetworkAliases,
|
||||
Labels: mergeLabels(service.Labels, service.CustomLabels),
|
||||
}
|
||||
|
||||
created, err := s.createContainer(ctx, project, service, service.ContainerName, 1, createOpts)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -107,6 +124,15 @@ func applyRunOptions(project *types.Project, service *types.ServiceConfig, opts
|
||||
if len(opts.User) > 0 {
|
||||
service.User = opts.User
|
||||
}
|
||||
|
||||
if len(opts.CapAdd) > 0 {
|
||||
service.CapAdd = append(service.CapAdd, opts.CapAdd...)
|
||||
service.CapDrop = utils.Remove(service.CapDrop, opts.CapAdd...)
|
||||
}
|
||||
if len(opts.CapDrop) > 0 {
|
||||
service.CapDrop = append(service.CapDrop, opts.CapDrop...)
|
||||
service.CapAdd = utils.Remove(service.CapAdd, opts.CapDrop...)
|
||||
}
|
||||
if len(opts.WorkingDir) > 0 {
|
||||
service.WorkingDir = opts.WorkingDir
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ func (s *composeService) injectSecrets(ctx context.Context, project *types.Proje
|
||||
}
|
||||
|
||||
err = s.apiClient().CopyToContainer(ctx, id, "/", &b, moby.CopyToContainerOptions{
|
||||
CopyUIDGID: true,
|
||||
CopyUIDGID: config.UID != "" || config.GID != "",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -155,13 +155,31 @@ func (s *composeService) watchContainers(ctx context.Context, //nolint:gocyclo
|
||||
required = services
|
||||
}
|
||||
|
||||
// predicate to tell if a container we receive event for should be considered or ignored
|
||||
ofInterest := func(c moby.Container) bool {
|
||||
if len(services) > 0 {
|
||||
// we only watch some services
|
||||
return utils.Contains(services, c.Labels[api.ServiceLabel])
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// predicate to tell if a container we receive event for should be watched until termination
|
||||
isRequired := func(c moby.Container) bool {
|
||||
if len(services) > 0 && len(required) > 0 {
|
||||
// we only watch some services
|
||||
return utils.Contains(required, c.Labels[api.ServiceLabel])
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
var (
|
||||
expected []string
|
||||
watched = map[string]int{}
|
||||
replaced []string
|
||||
)
|
||||
for _, c := range containers {
|
||||
if utils.Contains(required, c.Labels[api.ServiceLabel]) {
|
||||
if isRequired(c) {
|
||||
expected = append(expected, c.ID)
|
||||
}
|
||||
watched[c.ID] = 0
|
||||
@@ -265,6 +283,11 @@ func (s *composeService) watchContainers(ctx context.Context, //nolint:gocyclo
|
||||
if utils.Contains(expected, id) {
|
||||
expected = append(expected, container.ID)
|
||||
}
|
||||
} else if ofInterest(container) {
|
||||
watched[container.ID] = 1
|
||||
if isRequired(container) {
|
||||
expected = append(expected, container.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(expected) == 0 {
|
||||
|
||||
@@ -50,7 +50,11 @@ func TestStopTimeout(t *testing.T) {
|
||||
testContainer("service1", "456", false),
|
||||
testContainer("service2", "789", false),
|
||||
}, nil)
|
||||
api.EXPECT().VolumeList(gomock.Any(), filters.NewArgs(projectFilter(strings.ToLower(testProject)))).
|
||||
api.EXPECT().VolumeList(
|
||||
gomock.Any(),
|
||||
volume.ListOptions{
|
||||
Filters: filters.NewArgs(projectFilter(strings.ToLower(testProject))),
|
||||
}).
|
||||
Return(volume.ListResponse{}, nil)
|
||||
api.EXPECT().NetworkList(gomock.Any(), moby.NetworkListOptions{Filters: filters.NewArgs(projectFilter(strings.ToLower(testProject)))}).
|
||||
Return([]moby.NetworkResource{}, nil)
|
||||
|
||||
67
pkg/compose/wait.go
Normal file
67
pkg/compose/wait.go
Normal file
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package compose
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
func (s *composeService) Wait(ctx context.Context, projectName string, options api.WaitOptions) (int64, error) {
|
||||
containers, err := s.getContainers(ctx, projectName, oneOffInclude, false, options.Services...)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if len(containers) == 0 {
|
||||
return 0, fmt.Errorf("no containers for project %q", projectName)
|
||||
}
|
||||
|
||||
eg, waitCtx := errgroup.WithContext(ctx)
|
||||
var statusCode int64
|
||||
for _, c := range containers {
|
||||
c := c
|
||||
eg.Go(func() error {
|
||||
var err error
|
||||
resultC, errC := s.dockerCli.Client().ContainerWait(waitCtx, c.ID, "")
|
||||
|
||||
select {
|
||||
case result := <-resultC:
|
||||
fmt.Fprintf(s.dockerCli.Out(), "container %q exited with status code %d\n", c.ID, result.StatusCode)
|
||||
statusCode = result.StatusCode
|
||||
case err = <-errC:
|
||||
}
|
||||
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
err = eg.Wait()
|
||||
if err != nil {
|
||||
return 42, err // Ignore abort flag in case of error in wait
|
||||
}
|
||||
|
||||
if options.DownProjectOnContainerExit {
|
||||
return statusCode, s.Down(ctx, projectName, api.DownOptions{
|
||||
RemoveOrphans: true,
|
||||
})
|
||||
}
|
||||
|
||||
return statusCode, err
|
||||
}
|
||||
@@ -79,7 +79,7 @@ func (s *composeService) Watch(ctx context.Context, project *types.Project, serv
|
||||
needRebuild := make(chan fileMapping)
|
||||
needSync := make(chan fileMapping)
|
||||
|
||||
err := s.prepareProjectForBuild(project, nil)
|
||||
_, err := s.prepareProjectForBuild(project, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -104,7 +104,11 @@ func (s *composeService) Watch(ctx context.Context, project *types.Project, serv
|
||||
return err
|
||||
}
|
||||
|
||||
if config != nil && len(config.Watch) > 0 && service.Build == nil {
|
||||
if config == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(config.Watch) > 0 && service.Build == nil {
|
||||
// service configured with watchers but no build section
|
||||
return fmt.Errorf("can't watch service %q without a build context", service.Name)
|
||||
}
|
||||
@@ -118,21 +122,8 @@ func (s *composeService) Watch(ctx context.Context, project *types.Project, serv
|
||||
continue
|
||||
}
|
||||
|
||||
if config == nil {
|
||||
config = &DevelopmentConfig{
|
||||
Watch: []Trigger{
|
||||
{
|
||||
Path: service.Build.Context,
|
||||
Action: WatchActionRebuild,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
name := service.Name
|
||||
bc := service.Build.Context
|
||||
|
||||
dockerIgnores, err := watch.LoadDockerIgnore(bc)
|
||||
dockerIgnores, err := watch.LoadDockerIgnore(service.Build.Context)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -150,12 +141,21 @@ func (s *composeService) Watch(ctx context.Context, project *types.Project, serv
|
||||
dotGitIgnore,
|
||||
)
|
||||
|
||||
watcher, err := watch.NewWatcher([]string{bc}, ignore)
|
||||
var paths []string
|
||||
for _, trigger := range config.Watch {
|
||||
if checkIfPathAlreadyBindMounted(trigger.Path, service.Volumes) {
|
||||
logrus.Warnf("path '%s' also declared by a bind mount volume, this path won't be monitored!\n", trigger.Path)
|
||||
continue
|
||||
}
|
||||
paths = append(paths, trigger.Path)
|
||||
}
|
||||
|
||||
watcher, err := watch.NewWatcher(paths, ignore)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintf(s.stdinfo(), "watching %s\n", bc)
|
||||
fmt.Fprintf(s.stdinfo(), "watching %s\n", paths)
|
||||
err = watcher.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -311,25 +311,50 @@ func (s *composeService) makeSyncFn(ctx context.Context, project *types.Project,
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case opt := <-needSync:
|
||||
if fi, statErr := os.Stat(opt.HostPath); statErr == nil && !fi.IsDir() {
|
||||
err := s.Copy(ctx, project.Name, api.CopyOptions{
|
||||
Source: opt.HostPath,
|
||||
Destination: fmt.Sprintf("%s:%s", opt.Service, opt.ContainerPath),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
service, err := project.GetService(opt.Service)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
scale := 1
|
||||
if service.Deploy != nil && service.Deploy.Replicas != nil {
|
||||
scale = int(*service.Deploy.Replicas)
|
||||
}
|
||||
|
||||
if fi, statErr := os.Stat(opt.HostPath); statErr == nil {
|
||||
if fi.IsDir() {
|
||||
for i := 1; i <= scale; i++ {
|
||||
_, err := s.Exec(ctx, project.Name, api.RunOptions{
|
||||
Service: opt.Service,
|
||||
Command: []string{"mkdir", "-p", opt.ContainerPath},
|
||||
Index: i,
|
||||
})
|
||||
if err != nil {
|
||||
logrus.Warnf("failed to create %q from %s: %v", opt.ContainerPath, opt.Service, err)
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(s.stdinfo(), "%s created\n", opt.ContainerPath)
|
||||
} else {
|
||||
err := s.Copy(ctx, project.Name, api.CopyOptions{
|
||||
Source: opt.HostPath,
|
||||
Destination: fmt.Sprintf("%s:%s", opt.Service, opt.ContainerPath),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(s.stdinfo(), "%s updated\n", opt.ContainerPath)
|
||||
}
|
||||
fmt.Fprintf(s.stdinfo(), "%s updated\n", opt.ContainerPath)
|
||||
} else if errors.Is(statErr, fs.ErrNotExist) {
|
||||
_, err := s.Exec(ctx, project.Name, api.RunOptions{
|
||||
Service: opt.Service,
|
||||
Command: []string{"rm", "-rf", opt.ContainerPath},
|
||||
Index: 1,
|
||||
})
|
||||
if err != nil {
|
||||
logrus.Warnf("failed to delete %q from %s: %v", opt.ContainerPath, opt.Service, err)
|
||||
for i := 1; i <= scale; i++ {
|
||||
_, err := s.Exec(ctx, project.Name, api.RunOptions{
|
||||
Service: opt.Service,
|
||||
Command: []string{"rm", "-rf", opt.ContainerPath},
|
||||
Index: i,
|
||||
})
|
||||
if err != nil {
|
||||
logrus.Warnf("failed to delete %q from %s: %v", opt.ContainerPath, opt.Service, err)
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(s.stdinfo(), "%s deleted from container\n", opt.ContainerPath)
|
||||
fmt.Fprintf(s.stdinfo(), "%s deleted from service\n", opt.ContainerPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -362,3 +387,12 @@ func debounce(ctx context.Context, clock clockwork.Clock, delay time.Duration, i
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkIfPathAlreadyBindMounted(watchPath string, volumes []types.ServiceVolumeConfig) bool {
|
||||
for _, volume := range volumes {
|
||||
if volume.Bind != nil && strings.HasPrefix(watchPath, volume.Source) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"strings"
|
||||
@@ -386,7 +387,7 @@ func TestBuildPlatformsStandardErrors(t *testing.T) {
|
||||
res := c.RunDockerComposeCmdNoCheck(t, "--project-directory", "fixtures/build-test/platforms", "build")
|
||||
res.Assert(t, icmd.Expected{
|
||||
ExitCode: 17,
|
||||
Err: `multiple platforms feature is currently not supported for docker driver. Please switch to a different driver (eg. "docker buildx create --use")`,
|
||||
Err: `Multiple platforms feature is currently not supported for docker driver. Please switch to a different driver (eg. "docker buildx create --use")`,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -423,3 +424,30 @@ func TestBuildPlatformsStandardErrors(t *testing.T) {
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func TestBuildBuilder(t *testing.T) {
|
||||
c := NewParallelCLI(t)
|
||||
builderName := "build-with-builder"
|
||||
// declare builder
|
||||
result := c.RunDockerCmd(t, "buildx", "create", "--name", builderName, "--use", "--bootstrap")
|
||||
assert.NilError(t, result.Error)
|
||||
|
||||
t.Cleanup(func() {
|
||||
c.RunDockerComposeCmd(t, "--project-directory", "fixtures/build-test/", "down")
|
||||
_ = c.RunDockerCmd(t, "buildx", "rm", "-f", builderName)
|
||||
})
|
||||
|
||||
t.Run("use specific builder to run build command", func(t *testing.T) {
|
||||
res := c.RunDockerComposeCmdNoCheck(t, "--project-directory", "fixtures/build-test", "build", "--builder", builderName)
|
||||
assert.NilError(t, res.Error, res.Stderr())
|
||||
})
|
||||
|
||||
t.Run("error when using specific builder to run build command", func(t *testing.T) {
|
||||
res := c.RunDockerComposeCmdNoCheck(t, "--project-directory", "fixtures/build-test", "build", "--builder", "unknown-builder")
|
||||
res.Assert(t, icmd.Expected{
|
||||
ExitCode: 1,
|
||||
Err: fmt.Sprintf(`no builder %q found`, "unknown-builder"),
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
@@ -102,6 +102,14 @@ func TestLocalComposeUp(t *testing.T) {
|
||||
res.Assert(t, icmd.Expected{Out: `compose-e2e-demo-words-1 gtardif/sentences-api latest`})
|
||||
})
|
||||
|
||||
t.Run("down SERVICE", func(t *testing.T) {
|
||||
_ = c.RunDockerComposeCmd(t, "--project-name", projectName, "down", "web")
|
||||
|
||||
res := c.RunDockerComposeCmd(t, "--project-name", projectName, "ps")
|
||||
assert.Assert(t, !strings.Contains(res.Combined(), "compose-e2e-demo-web-1"), res.Combined())
|
||||
assert.Assert(t, strings.Contains(res.Combined(), "compose-e2e-demo-db-1"), res.Combined())
|
||||
})
|
||||
|
||||
t.Run("down", func(t *testing.T) {
|
||||
_ = c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
|
||||
})
|
||||
@@ -269,7 +277,7 @@ networks:
|
||||
})
|
||||
}
|
||||
|
||||
func TestStopWithDependeciesAttached(t *testing.T) {
|
||||
func TestStopWithDependenciesAttached(t *testing.T) {
|
||||
const projectName = "compose-e2e-stop-with-deps"
|
||||
c := NewParallelCLI(t, WithEnv("COMMAND=echo hello"))
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
services:
|
||||
ping:
|
||||
image: alpine
|
||||
command: ping localhost -c 1
|
||||
command: ping localhost -c ${REPEAT:-1}
|
||||
hello:
|
||||
image: alpine
|
||||
command: echo hello
|
||||
|
||||
11
pkg/e2e/fixtures/wait/compose.yaml
Normal file
11
pkg/e2e/fixtures/wait/compose.yaml
Normal file
@@ -0,0 +1,11 @@
|
||||
services:
|
||||
faster:
|
||||
image: alpine
|
||||
command: sleep 2
|
||||
slower:
|
||||
image: alpine
|
||||
command: sleep 5
|
||||
infinity:
|
||||
image: alpine
|
||||
command: sleep infinity
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -134,7 +135,7 @@ func initializePlugins(t testing.TB, configDir string) {
|
||||
require.NoError(t, os.MkdirAll(filepath.Join(configDir, "cli-plugins"), 0o755),
|
||||
"Failed to create cli-plugins directory")
|
||||
composePlugin, err := findExecutable(DockerComposeExecutableName)
|
||||
if os.IsNotExist(err) {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
t.Logf("WARNING: docker-compose cli-plugin not found")
|
||||
}
|
||||
|
||||
@@ -161,20 +162,21 @@ func dirContents(dir string) []string {
|
||||
}
|
||||
|
||||
func findExecutable(executableName string) (string, error) {
|
||||
_, filename, _, _ := runtime.Caller(0)
|
||||
root := filepath.Join(filepath.Dir(filename), "..", "..")
|
||||
buildPath := filepath.Join(root, "bin", "build")
|
||||
|
||||
bin, err := filepath.Abs(filepath.Join(buildPath, executableName))
|
||||
if err != nil {
|
||||
return "", err
|
||||
bin := os.Getenv("COMPOSE_E2E_BIN_PATH")
|
||||
if bin == "" {
|
||||
_, filename, _, _ := runtime.Caller(0)
|
||||
buildPath := filepath.Join(filepath.Dir(filename), "..", "..", "bin", "build")
|
||||
var err error
|
||||
bin, err = filepath.Abs(filepath.Join(buildPath, executableName))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := os.Stat(bin); err == nil {
|
||||
return bin, nil
|
||||
}
|
||||
|
||||
return "", errors.Wrap(os.ErrNotExist, "executable not found")
|
||||
return "", fmt.Errorf("looking for %q: %w", bin, fs.ErrNotExist)
|
||||
}
|
||||
|
||||
func findPluginExecutable(pluginExecutableName string) (string, error) {
|
||||
|
||||
@@ -17,10 +17,13 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
"gotest.tools/v3/poll"
|
||||
|
||||
"gotest.tools/v3/icmd"
|
||||
)
|
||||
@@ -56,3 +59,42 @@ func TestLocalComposeLogs(t *testing.T) {
|
||||
_ = c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
|
||||
})
|
||||
}
|
||||
|
||||
func TestLocalComposeLogsFollow(t *testing.T) {
|
||||
c := NewCLI(t, WithEnv("REPEAT=20"))
|
||||
const projectName = "compose-e2e-logs"
|
||||
t.Cleanup(func() {
|
||||
c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
|
||||
})
|
||||
|
||||
c.RunDockerComposeCmd(t, "-f", "./fixtures/logs-test/compose.yaml", "--project-name", projectName, "up", "-d", "ping")
|
||||
|
||||
cmd := c.NewDockerComposeCmd(t, "--project-name", projectName, "logs", "-f")
|
||||
res := icmd.StartCmd(cmd)
|
||||
t.Cleanup(func() {
|
||||
_ = res.Cmd.Process.Kill()
|
||||
})
|
||||
|
||||
expected := fmt.Sprintf("%s-ping-1 ", projectName)
|
||||
poll.WaitOn(t, expectOutput(res, expected), poll.WithDelay(100*time.Millisecond), poll.WithTimeout(1*time.Second))
|
||||
|
||||
c.RunDockerComposeCmd(t, "-f", "./fixtures/logs-test/compose.yaml", "--project-name", projectName, "up", "-d")
|
||||
|
||||
expected = fmt.Sprintf("%s-hello-1 ", projectName)
|
||||
poll.WaitOn(t, expectOutput(res, expected), poll.WithDelay(100*time.Millisecond), poll.WithTimeout(1*time.Second))
|
||||
|
||||
c.RunDockerComposeCmd(t, "-f", "./fixtures/logs-test/compose.yaml", "--project-name", projectName, "up", "-d", "--scale", "ping=2", "ping")
|
||||
|
||||
expected = fmt.Sprintf("%s-ping-2 ", projectName)
|
||||
poll.WaitOn(t, expectOutput(res, expected), poll.WithDelay(100*time.Millisecond), poll.WithTimeout(20*time.Second))
|
||||
}
|
||||
|
||||
func expectOutput(res *icmd.Result, expected string) func(t poll.LogT) poll.Result {
|
||||
return func(t poll.LogT) poll.Result {
|
||||
if strings.Contains(res.Stdout(), expected) {
|
||||
return poll.Success()
|
||||
}
|
||||
return poll.Continue("condition not met")
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,13 +23,14 @@ import (
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/utils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gotest.tools/v3/assert"
|
||||
"gotest.tools/v3/icmd"
|
||||
)
|
||||
|
||||
@@ -79,7 +80,7 @@ func TestUpDependenciesNotStopped(t *testing.T) {
|
||||
defer cancel()
|
||||
|
||||
cmd, err := StartWithNewGroupID(ctx, testCmd, upOut, nil)
|
||||
assert.NoError(t, err, "Failed to run compose up")
|
||||
assert.NilError(t, err, "Failed to run compose up")
|
||||
|
||||
t.Log("Waiting for containers to be in running state")
|
||||
upOut.RequireEventuallyContains(t, "hello app")
|
||||
@@ -138,3 +139,17 @@ func TestUpWithDependencyExit(t *testing.T) {
|
||||
res.Assert(t, icmd.Expected{ExitCode: 1, Err: "dependency failed to start: container dependencies-db-1 exited (1)"})
|
||||
})
|
||||
}
|
||||
|
||||
func TestScaleDoesntRecreate(t *testing.T) {
|
||||
c := NewCLI(t)
|
||||
const projectName = "compose-e2e-scale"
|
||||
t.Cleanup(func() {
|
||||
c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
|
||||
})
|
||||
|
||||
c.RunDockerComposeCmd(t, "-f", "fixtures/simple-composefile/compose.yaml", "--project-name", projectName, "up", "-d")
|
||||
|
||||
res := c.RunDockerComposeCmd(t, "-f", "fixtures/simple-composefile/compose.yaml", "--project-name", projectName, "up", "--scale", "simple=2", "-d")
|
||||
assert.Check(t, !strings.Contains(res.Combined(), "Recreated"))
|
||||
|
||||
}
|
||||
|
||||
72
pkg/e2e/wait_test.go
Normal file
72
pkg/e2e/wait_test.go
Normal file
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestWaitOnFaster(t *testing.T) {
|
||||
const projectName = "e2e-wait-faster"
|
||||
c := NewParallelCLI(t)
|
||||
|
||||
c.RunDockerComposeCmd(t, "-f", "./fixtures/wait/compose.yaml", "--project-name", projectName, "up", "-d")
|
||||
c.RunDockerComposeCmd(t, "--project-name", projectName, "wait", "faster")
|
||||
}
|
||||
|
||||
func TestWaitOnSlower(t *testing.T) {
|
||||
const projectName = "e2e-wait-slower"
|
||||
c := NewParallelCLI(t)
|
||||
|
||||
c.RunDockerComposeCmd(t, "-f", "./fixtures/wait/compose.yaml", "--project-name", projectName, "up", "-d")
|
||||
c.RunDockerComposeCmd(t, "--project-name", projectName, "wait", "slower")
|
||||
}
|
||||
|
||||
func TestWaitOnInfinity(t *testing.T) {
|
||||
const projectName = "e2e-wait-infinity"
|
||||
c := NewParallelCLI(t)
|
||||
|
||||
c.RunDockerComposeCmd(t, "-f", "./fixtures/wait/compose.yaml", "--project-name", projectName, "up", "-d")
|
||||
|
||||
finished := make(chan struct{})
|
||||
ticker := time.NewTicker(7 * time.Second)
|
||||
go func() {
|
||||
c.RunDockerComposeCmd(t, "--project-name", projectName, "wait", "infinity")
|
||||
finished <- struct{}{}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-finished:
|
||||
t.Fatal("wait infinity should not finish")
|
||||
case <-ticker.C:
|
||||
}
|
||||
}
|
||||
|
||||
func TestWaitAndDrop(t *testing.T) {
|
||||
const projectName = "e2e-wait-and-drop"
|
||||
c := NewParallelCLI(t)
|
||||
|
||||
c.RunDockerComposeCmd(t, "-f", "./fixtures/wait/compose.yaml", "--project-name", projectName, "up", "-d")
|
||||
c.RunDockerComposeCmd(t, "--project-name", projectName, "wait", "--down-project", "faster")
|
||||
|
||||
res := c.RunDockerCmd(t, "ps", "--all")
|
||||
assert.Assert(t, !strings.Contains(res.Combined(), projectName), res.Combined())
|
||||
}
|
||||
@@ -267,10 +267,10 @@ func (mr *MockAPIClientMockRecorder) ContainerCreate(arg0, arg1, arg2, arg3, arg
|
||||
}
|
||||
|
||||
// ContainerDiff mocks base method.
|
||||
func (m *MockAPIClient) ContainerDiff(arg0 context.Context, arg1 string) ([]container.ContainerChangeResponseItem, error) {
|
||||
func (m *MockAPIClient) ContainerDiff(arg0 context.Context, arg1 string) ([]container.FilesystemChange, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ContainerDiff", arg0, arg1)
|
||||
ret0, _ := ret[0].([]container.ContainerChangeResponseItem)
|
||||
ret0, _ := ret[0].([]container.FilesystemChange)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
@@ -1381,7 +1381,7 @@ func (mr *MockAPIClientMockRecorder) PluginUpgrade(arg0, arg1, arg2 interface{})
|
||||
}
|
||||
|
||||
// RegistryLogin mocks base method.
|
||||
func (m *MockAPIClient) RegistryLogin(arg0 context.Context, arg1 types.AuthConfig) (registry.AuthenticateOKBody, error) {
|
||||
func (m *MockAPIClient) RegistryLogin(arg0 context.Context, arg1 registry.AuthConfig) (registry.AuthenticateOKBody, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "RegistryLogin", arg0, arg1)
|
||||
ret0, _ := ret[0].(registry.AuthenticateOKBody)
|
||||
@@ -1768,7 +1768,7 @@ func (mr *MockAPIClientMockRecorder) VolumeInspectWithRaw(arg0, arg1 interface{}
|
||||
}
|
||||
|
||||
// VolumeList mocks base method.
|
||||
func (m *MockAPIClient) VolumeList(arg0 context.Context, arg1 filters.Args) (volume.ListResponse, error) {
|
||||
func (m *MockAPIClient) VolumeList(arg0 context.Context, arg1 volume.ListOptions) (volume.ListResponse, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "VolumeList", arg0, arg1)
|
||||
ret0, _ := ret[0].(volume.ListResponse)
|
||||
|
||||
@@ -423,6 +423,21 @@ func (mr *MockServiceMockRecorder) Viz(ctx, project, options interface{}) *gomoc
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Viz", reflect.TypeOf((*MockService)(nil).Viz), ctx, project, options)
|
||||
}
|
||||
|
||||
// Wait mocks base method.
|
||||
func (m *MockService) Wait(ctx context.Context, projectName string, options api.WaitOptions) (int64, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Wait", ctx, projectName, options)
|
||||
ret0, _ := ret[0].(int64)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Wait indicates an expected call of Wait.
|
||||
func (mr *MockServiceMockRecorder) Wait(ctx, projectName, options interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Wait", reflect.TypeOf((*MockService)(nil).Wait), ctx, projectName, options)
|
||||
}
|
||||
|
||||
// Watch mocks base method.
|
||||
func (m *MockService) Watch(ctx context.Context, project *types.Project, services []string, options api.WatchOptions) error {
|
||||
m.ctrl.T.Helper()
|
||||
|
||||
@@ -153,6 +153,15 @@ func RemovedEvent(id string) Event {
|
||||
return NewEvent(id, Done, "Removed")
|
||||
}
|
||||
|
||||
// SkippedEvent creates a new Skipped Event
|
||||
func SkippedEvent(id string, reason string) Event {
|
||||
return Event{
|
||||
ID: id,
|
||||
Status: Warning,
|
||||
StatusText: "Skipped: " + reason,
|
||||
}
|
||||
}
|
||||
|
||||
// NewEvent new event
|
||||
func NewEvent(id string, status EventStatus, statusText string) Event {
|
||||
return Event{
|
||||
|
||||
37
pkg/progress/quiet.go
Normal file
37
pkg/progress/quiet.go
Normal file
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
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 progress
|
||||
|
||||
import "context"
|
||||
|
||||
type quiet struct{}
|
||||
|
||||
func (q quiet) Start(_ context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (q quiet) Stop() {
|
||||
}
|
||||
|
||||
func (q quiet) Event(_ Event) {
|
||||
}
|
||||
|
||||
func (q quiet) Events(_ []Event) {
|
||||
}
|
||||
|
||||
func (q quiet) TailMsgf(_ string, _ ...interface{}) {
|
||||
}
|
||||
@@ -21,12 +21,12 @@ import (
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"github.com/cloudflare/cfssl/log"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
|
||||
"github.com/containerd/console"
|
||||
"github.com/moby/term"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
)
|
||||
|
||||
// Writer can write multiple progress events
|
||||
@@ -107,6 +107,8 @@ const (
|
||||
ModeTTY = "tty"
|
||||
// ModePlain dump raw events to output
|
||||
ModePlain = "plain"
|
||||
// ModeQuiet don't display events
|
||||
ModeQuiet = "quiet"
|
||||
)
|
||||
|
||||
// Mode define how progress should be rendered, either as ModePlain or ModeTTY
|
||||
@@ -119,13 +121,16 @@ func NewWriter(ctx context.Context, out io.Writer, progressTitle string) (Writer
|
||||
if !ok {
|
||||
dryRun = false
|
||||
}
|
||||
if Mode == ModeQuiet {
|
||||
return quiet{}, nil
|
||||
}
|
||||
f, isConsole := out.(console.File) // see https://github.com/docker/compose/issues/10560
|
||||
if Mode == ModeAuto && isTerminal && isConsole {
|
||||
return newTTYWriter(f, dryRun, progressTitle)
|
||||
}
|
||||
if Mode == ModeTTY {
|
||||
if !isConsole {
|
||||
log.Warning("Terminal is not a POSIX console")
|
||||
logrus.Warn("Terminal is not a POSIX console")
|
||||
} else {
|
||||
return newTTYWriter(f, dryRun, progressTitle)
|
||||
}
|
||||
|
||||
@@ -20,8 +20,18 @@ func (s Set[T]) Add(v T) {
|
||||
s[v] = struct{}{}
|
||||
}
|
||||
|
||||
func (s Set[T]) Remove(v T) {
|
||||
delete(s, v)
|
||||
func (s Set[T]) AddAll(v ...T) {
|
||||
for _, e := range v {
|
||||
s[e] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
func (s Set[T]) Remove(v T) bool {
|
||||
_, ok := s[v]
|
||||
if ok {
|
||||
delete(s, v)
|
||||
}
|
||||
return ok
|
||||
}
|
||||
|
||||
func (s Set[T]) Clear() {
|
||||
|
||||
@@ -39,3 +39,13 @@ func Remove[T any](origin []T, elements ...T) []T {
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
func Filter[T any](elements []T, predicate func(T) bool) []T {
|
||||
var filtered []T
|
||||
for _, v := range elements {
|
||||
if predicate(v) {
|
||||
filtered = append(filtered, v)
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
@@ -43,11 +43,17 @@ func (s *splitWriter) Write(b []byte) (int, error) {
|
||||
for {
|
||||
b = s.buffer.Bytes()
|
||||
index := bytes.Index(b, []byte{'\n'})
|
||||
if index < 0 {
|
||||
if index > 0 {
|
||||
line := s.buffer.Next(index + 1)
|
||||
s.consumer(string(line[:len(line)-1]))
|
||||
} else {
|
||||
line := s.buffer.String()
|
||||
s.buffer.Reset()
|
||||
if len(line) > 0 {
|
||||
s.consumer(line)
|
||||
}
|
||||
break
|
||||
}
|
||||
line := s.buffer.Next(index + 1)
|
||||
s.consumer(string(line[:len(line)-1]))
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
@@ -28,13 +28,21 @@ func TestSplitWriter(t *testing.T) {
|
||||
w := GetWriter(func(line string) {
|
||||
lines = append(lines, line)
|
||||
})
|
||||
w.Write([]byte("h"))
|
||||
w.Write([]byte("e"))
|
||||
w.Write([]byte("l"))
|
||||
w.Write([]byte("l"))
|
||||
w.Write([]byte("o"))
|
||||
w.Write([]byte("\n"))
|
||||
w.Write([]byte("world!\n"))
|
||||
w.Write([]byte("hello\n"))
|
||||
w.Write([]byte("world\n"))
|
||||
w.Write([]byte("!"))
|
||||
assert.DeepEqual(t, lines, []string{"hello", "world", "!"})
|
||||
|
||||
}
|
||||
|
||||
//nolint:errcheck
|
||||
func TestSplitWriterNoEOL(t *testing.T) {
|
||||
var lines []string
|
||||
w := GetWriter(func(line string) {
|
||||
lines = append(lines, line)
|
||||
})
|
||||
w.Write([]byte("hello\n"))
|
||||
w.Write([]byte("world!"))
|
||||
assert.DeepEqual(t, lines, []string{"hello", "world!"})
|
||||
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ type Notify interface {
|
||||
// - Watch /src/repo, but ignore /src/repo/.git
|
||||
// - Watch /src/repo, but ignore everything in /src/repo/bazel-bin except /src/repo/bazel-bin/app-binary
|
||||
//
|
||||
// The PathMatcher inteface helps us manage these ignores.
|
||||
// The PathMatcher interface helps us manage these ignores.
|
||||
type PathMatcher interface {
|
||||
Matches(file string) (bool, error)
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ func dedupePathsForRecursiveWatcher(paths []string) []string {
|
||||
}
|
||||
|
||||
if IsChild(current, existing) {
|
||||
// Mark the element empty fo removal.
|
||||
// Mark the element empty for removal.
|
||||
result[i] = ""
|
||||
hasRemovals = true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user