mirror of
https://github.com/docker/compose.git
synced 2026-02-11 11:09:23 +08:00
Compare commits
56 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7abaa06617 | ||
|
|
3b0e8f538e | ||
|
|
af376603c3 | ||
|
|
7f8814f4c5 | ||
|
|
af0029afe1 | ||
|
|
b76feb66e1 | ||
|
|
9dc7f1e70c | ||
|
|
03205124fe | ||
|
|
8b769bad6b | ||
|
|
671507a8b3 | ||
|
|
56ab28aef3 | ||
|
|
e7d870a106 | ||
|
|
d5bb3387ca | ||
|
|
d91fc63813 | ||
|
|
c51b1fea29 | ||
|
|
fa7549a851 | ||
|
|
a061c17736 | ||
|
|
c5e7d9158c | ||
|
|
3783b8ada3 | ||
|
|
c428a77111 | ||
|
|
04b4a832dc | ||
|
|
27faa3b84e | ||
|
|
bcc0401e0e | ||
|
|
093205121c | ||
|
|
b92b87dd9c | ||
|
|
06e1287483 | ||
|
|
d7bdb34ff5 | ||
|
|
79d7a8acd6 | ||
|
|
abd99be4fd | ||
|
|
2672d34217 | ||
|
|
27bf40357a | ||
|
|
c8d687599a | ||
|
|
2f108ffaa8 | ||
|
|
0a07df0e5b | ||
|
|
02b606ef8e | ||
|
|
9856802945 | ||
|
|
63ae7eb0fa | ||
|
|
f17d0dfc61 | ||
|
|
ef14cfcfea | ||
|
|
b760afaf9f | ||
|
|
a2a5c86f53 | ||
|
|
98e82127b3 | ||
|
|
03e19e4a84 | ||
|
|
b2c17ff118 | ||
|
|
ec88588cd8 | ||
|
|
7d5913403a | ||
|
|
d95aa57f01 | ||
|
|
ee4c01b66b | ||
|
|
d7a65f53f8 | ||
|
|
4520bcbaf6 | ||
|
|
327be1fcd5 | ||
|
|
59f04b85af | ||
|
|
b4574c8bd6 | ||
|
|
29d6c918c4 | ||
|
|
58403169f3 | ||
|
|
6aee7f8370 |
153
.github/workflows/ci.yml
vendored
153
.github/workflows/ci.yml
vendored
@@ -22,24 +22,6 @@ permissions:
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.platforms.outputs.matrix }}
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Create matrix
|
||||
id: platforms
|
||||
run: |
|
||||
echo matrix=$(docker buildx bake binary-cross --print | jq -cr '.target."binary-cross".platforms') >> $GITHUB_OUTPUT
|
||||
-
|
||||
name: Show matrix
|
||||
run: |
|
||||
echo ${{ steps.platforms.outputs.matrix }}
|
||||
|
||||
validate:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
@@ -63,63 +45,88 @@ jobs:
|
||||
make ${{ matrix.target }}
|
||||
|
||||
binary:
|
||||
uses: docker/github-builder/.github/workflows/bake.yml@v1
|
||||
permissions:
|
||||
contents: read # same as global permission
|
||||
id-token: write # for signing attestation(s) with GitHub OIDC Token
|
||||
with:
|
||||
runner: amd64
|
||||
artifact-name: compose
|
||||
artifact-upload: true
|
||||
cache: true
|
||||
cache-scope: binary
|
||||
target: release
|
||||
output: local
|
||||
sbom: true
|
||||
sign: ${{ github.event_name != 'pull_request' }}
|
||||
|
||||
binary-finalize:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- prepare
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: ${{ fromJson(needs.prepare.outputs.matrix) }}
|
||||
- binary
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Prepare
|
||||
run: |
|
||||
platform=${MATRIX_PLATFORM}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
env:
|
||||
MATRIX_PLATFORM: ${{ matrix.platform }}
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
-
|
||||
name: Build
|
||||
uses: docker/bake-action@v6
|
||||
name: Download artifacts
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
source: .
|
||||
targets: release
|
||||
provenance: mode=max
|
||||
sbom: true
|
||||
set: |
|
||||
*.platform=${{ matrix.platform }}
|
||||
*.cache-from=type=gha,scope=binary-${{ env.PLATFORM_PAIR }}
|
||||
*.cache-to=type=gha,scope=binary-${{ env.PLATFORM_PAIR }},mode=max
|
||||
path: /tmp/compose-output
|
||||
name: ${{ needs.binary.outputs.artifact-name }}
|
||||
-
|
||||
name: Rename provenance and sbom
|
||||
run: |
|
||||
for pdir in /tmp/compose-output/*/; do
|
||||
(
|
||||
cd "$pdir"
|
||||
binname=$(find . -name 'docker-compose-*')
|
||||
filename=$(basename "${binname%.exe}")
|
||||
mv "provenance.json" "${filename}.provenance.json"
|
||||
mv "sbom-binary.spdx.json" "${filename}.sbom.json"
|
||||
find . -name 'sbom*.json' -exec rm {} \;
|
||||
if [ -f "provenance.sigstore.json" ]; then
|
||||
mv "provenance.sigstore.json" "${filename}.sigstore.json"
|
||||
fi
|
||||
)
|
||||
done
|
||||
mkdir -p "./bin/release"
|
||||
mv /tmp/compose-output/**/* "./bin/release/"
|
||||
-
|
||||
name: Create checksum file
|
||||
working-directory: ./bin/release
|
||||
run: |
|
||||
binname=$(find . -name 'docker-compose-*')
|
||||
filename=$(basename "$binname" | sed -E 's/\.exe$//')
|
||||
mv "provenance.json" "${filename}.provenance.json"
|
||||
mv "sbom-binary.spdx.json" "${filename}.sbom.json"
|
||||
find . -name 'sbom*.json' -exec rm {} \;
|
||||
-
|
||||
name: List artifacts
|
||||
run: |
|
||||
tree -nh ./bin/release
|
||||
find . -type f -print0 | sort -z | xargs -r0 shasum -a 256 -b | sed 's# \*\./# *#' > $RUNNER_TEMP/checksums.txt
|
||||
shasum -a 256 -U -c $RUNNER_TEMP/checksums.txt
|
||||
mv $RUNNER_TEMP/checksums.txt .
|
||||
cat checksums.txt | while read sum file; do
|
||||
if [[ "${file#\*}" == docker-compose-* && "${file#\*}" != *.provenance.json && "${file#\*}" != *.sbom.json && "${file#\*}" != *.sigstore.json ]]; then
|
||||
echo "$sum $file" > ${file#\*}.sha256
|
||||
fi
|
||||
done
|
||||
-
|
||||
name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: compose-${{ env.PLATFORM_PAIR }}
|
||||
path: ./bin/release
|
||||
name: release
|
||||
path: ./bin/release/*
|
||||
if-no-files-found: error
|
||||
|
||||
bin-image-test:
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: docker/github-builder/.github/workflows/bake.yml@v1
|
||||
with:
|
||||
runner: amd64
|
||||
target: image-cross
|
||||
cache: true
|
||||
cache-scope: bin-image-test
|
||||
output: image
|
||||
push: false
|
||||
sbom: true
|
||||
set-meta-labels: true
|
||||
meta-images: |
|
||||
compose-bin
|
||||
meta-tags: |
|
||||
type=ref,event=pr
|
||||
meta-bake-target: meta-helper
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -147,6 +154,7 @@ jobs:
|
||||
with:
|
||||
paths: bin/coverage/unit/report.xml
|
||||
if: always()
|
||||
|
||||
e2e:
|
||||
runs-on: ubuntu-latest
|
||||
name: e2e (${{ matrix.mode }}, ${{ matrix.channel }})
|
||||
@@ -254,6 +262,7 @@ jobs:
|
||||
with:
|
||||
paths: /tmp/report/report.xml
|
||||
if: always()
|
||||
|
||||
coverage:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
@@ -290,40 +299,26 @@ jobs:
|
||||
path: ./coverage.txt
|
||||
if-no-files-found: error
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
files: ./coverage.txt
|
||||
|
||||
release:
|
||||
permissions:
|
||||
contents: write # to create a release (ncipollo/release-action)
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- binary
|
||||
- binary-finalize
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
pattern: compose-*
|
||||
path: ./bin/release
|
||||
merge-multiple: true
|
||||
-
|
||||
name: Create checksums
|
||||
working-directory: ./bin/release
|
||||
run: |
|
||||
find . -type f -print0 | sort -z | xargs -r0 shasum -a 256 -b | sed 's# \*\./# *#' > $RUNNER_TEMP/checksums.txt
|
||||
shasum -a 256 -U -c $RUNNER_TEMP/checksums.txt
|
||||
mv $RUNNER_TEMP/checksums.txt .
|
||||
cat checksums.txt | while read sum file; do
|
||||
if [[ "${file#\*}" == docker-compose-* && "${file#\*}" != *.provenance.json && "${file#\*}" != *.sbom.json ]]; then
|
||||
echo "$sum $file" > ${file#\*}.sha256
|
||||
fi
|
||||
done
|
||||
name: release
|
||||
-
|
||||
name: List artifacts
|
||||
run: |
|
||||
|
||||
86
.github/workflows/merge.yml
vendored
86
.github/workflows/merge.yml
vendored
@@ -74,63 +74,41 @@ jobs:
|
||||
run: |
|
||||
make e2e-compose-standalone
|
||||
|
||||
bin-image:
|
||||
runs-on: ubuntu-22.04
|
||||
bin-image-prepare:
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
digest: ${{ fromJSON(steps.bake.outputs.metadata).image-cross['containerimage.digest'] }}
|
||||
repo-slug: ${{ env.REPO_SLUG }}
|
||||
steps:
|
||||
-
|
||||
name: Free disk space
|
||||
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1
|
||||
with:
|
||||
android: true
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: true
|
||||
swap-storage: true
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
# FIXME: can't use env object in reusable workflow inputs: https://github.com/orgs/community/discussions/26671
|
||||
- run: echo "Exposing env vars for reusable workflow"
|
||||
|
||||
bin-image:
|
||||
uses: docker/github-builder/.github/workflows/bake.yml@v1
|
||||
needs:
|
||||
- bin-image-prepare
|
||||
permissions:
|
||||
contents: read # same as global permission
|
||||
id-token: write # for signing attestation(s) with GitHub OIDC Token
|
||||
with:
|
||||
runner: amd64
|
||||
target: image-cross
|
||||
cache: true
|
||||
cache-scope: bin-image
|
||||
output: image
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
sbom: true
|
||||
set-meta-labels: true
|
||||
meta-images: |
|
||||
${{ needs.bin-image-prepare.outputs.repo-slug }}
|
||||
meta-tags: |
|
||||
type=ref,event=tag
|
||||
type=edge
|
||||
meta-bake-target: meta-helper
|
||||
secrets:
|
||||
registry-auths: |
|
||||
- registry: docker.io
|
||||
username: ${{ secrets.DOCKERPUBLICBOT_USERNAME }}
|
||||
password: ${{ secrets.DOCKERPUBLICBOT_WRITE_PAT }}
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
-
|
||||
name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
${{ env.REPO_SLUG }}
|
||||
tags: |
|
||||
type=ref,event=tag
|
||||
type=edge
|
||||
bake-target: meta-helper
|
||||
-
|
||||
name: Build and push image
|
||||
uses: docker/bake-action@v6
|
||||
id: bake
|
||||
with:
|
||||
source: .
|
||||
files: |
|
||||
./docker-bake.hcl
|
||||
${{ steps.meta.outputs.bake-file }}
|
||||
targets: image-cross
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
sbom: true
|
||||
provenance: mode=max
|
||||
set: |
|
||||
*.cache-from=type=gha,scope=bin-image
|
||||
*.cache-to=type=gha,scope=bin-image,mode=max
|
||||
|
||||
desktop-edge-test:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -158,6 +136,6 @@ jobs:
|
||||
workflow_id: 'compose-edge-integration.yml',
|
||||
ref: 'main',
|
||||
inputs: {
|
||||
"image-tag": "${{ needs.bin-image.outputs.digest }}"
|
||||
"image-tag": "${{ env.REPO_SLUG }}:edge"
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.24.11
|
||||
1.25.7
|
||||
@@ -8,6 +8,7 @@ linters:
|
||||
- depguard
|
||||
- errcheck
|
||||
- errorlint
|
||||
- forbidigo
|
||||
- gocritic
|
||||
- gocyclo
|
||||
- gomodguard
|
||||
@@ -38,6 +39,15 @@ linters:
|
||||
desc: use stdlib slices package
|
||||
- pkg: gopkg.in/yaml.v2
|
||||
desc: compose-go uses yaml.v3
|
||||
forbidigo:
|
||||
analyze-types: true
|
||||
forbid:
|
||||
- pattern: 'context\.Background'
|
||||
pkg: '^context$'
|
||||
msg: "in tests, use t.Context() instead of context.Background()"
|
||||
- pattern: 'context\.TODO'
|
||||
pkg: '^context$'
|
||||
msg: "in tests, use t.Context() instead of context.TODO()"
|
||||
gocritic:
|
||||
disabled-checks:
|
||||
- paramTypeCombine
|
||||
@@ -74,6 +84,10 @@ linters:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
rules:
|
||||
- path-except: '_test\.go'
|
||||
linters:
|
||||
- forbidigo
|
||||
issues:
|
||||
max-issues-per-linter: 0
|
||||
max-same-issues: 0
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
ARG GO_VERSION=1.24.11
|
||||
ARG GO_VERSION=1.25.7
|
||||
ARG XX_VERSION=1.9.0
|
||||
ARG GOLANGCI_LINT_VERSION=v2.6.2
|
||||
ARG GOLANGCI_LINT_VERSION=v2.8.0
|
||||
ARG ADDLICENSE_VERSION=v1.0.0
|
||||
|
||||
ARG BUILD_TAGS="e2e"
|
||||
@@ -83,7 +83,7 @@ RUN --mount=type=bind,target=. \
|
||||
--mount=type=cache,target=/go/pkg/mod \
|
||||
--mount=type=bind,from=osxcross,src=/osxsdk,target=/xx-sdk \
|
||||
xx-go --wrap && \
|
||||
if [ "$(xx-info os)" == "darwin" ]; then export CGO_ENABLED=1; fi && \
|
||||
if [ "$(xx-info os)" == "darwin" ]; then export CGO_ENABLED=1; export BUILD_TAGS=fsnotify,$BUILD_TAGS; fi && \
|
||||
make build GO_BUILDTAGS="$BUILD_TAGS" DESTDIR=/out && \
|
||||
xx-verify --static /out/docker-compose
|
||||
|
||||
|
||||
4
Makefile
4
Makefile
@@ -29,10 +29,6 @@ ifeq ($(DETECTED_OS),Windows)
|
||||
BINARY_EXT=.exe
|
||||
endif
|
||||
|
||||
ifeq ($(DETECTED_OS),Darwin)
|
||||
GO_BUILDTAGS += fsnotify
|
||||
endif
|
||||
|
||||
BUILD_FLAGS?=
|
||||
TEST_FLAGS?=
|
||||
E2E_TEST?=
|
||||
|
||||
@@ -32,6 +32,7 @@ import (
|
||||
"github.com/compose-spec/compose-go/v2/cli"
|
||||
"github.com/compose-spec/compose-go/v2/dotenv"
|
||||
"github.com/compose-spec/compose-go/v2/loader"
|
||||
composepaths "github.com/compose-spec/compose-go/v2/paths"
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
composegoutils "github.com/compose-spec/compose-go/v2/utils"
|
||||
"github.com/docker/buildx/util/logutil"
|
||||
@@ -476,7 +477,7 @@ func RootCommand(dockerCli command.Cli, backendOptions *BackendOptions) *cobra.C
|
||||
logrus.SetLevel(logrus.TraceLevel)
|
||||
}
|
||||
|
||||
err := setEnvWithDotEnv(opts)
|
||||
err := setEnvWithDotEnv(opts, dockerCli)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -504,6 +505,7 @@ func RootCommand(dockerCli command.Cli, backendOptions *BackendOptions) *cobra.C
|
||||
display.Mode = display.ModeTTY
|
||||
}
|
||||
|
||||
detached, _ := cmd.Flags().GetBool("detach")
|
||||
var ep api.EventProcessor
|
||||
switch opts.Progress {
|
||||
case "", display.ModeAuto:
|
||||
@@ -512,7 +514,7 @@ func RootCommand(dockerCli command.Cli, backendOptions *BackendOptions) *cobra.C
|
||||
display.Mode = display.ModePlain
|
||||
ep = display.Plain(dockerCli.Err())
|
||||
case dockerCli.Out().IsTerminal():
|
||||
ep = display.Full(dockerCli.Err(), stdinfo(dockerCli))
|
||||
ep = display.Full(dockerCli.Err(), stdinfo(dockerCli), detached)
|
||||
default:
|
||||
ep = display.Plain(dockerCli.Err())
|
||||
}
|
||||
@@ -521,7 +523,7 @@ func RootCommand(dockerCli command.Cli, backendOptions *BackendOptions) *cobra.C
|
||||
return fmt.Errorf("can't use --progress tty while ANSI support is disabled")
|
||||
}
|
||||
display.Mode = display.ModeTTY
|
||||
ep = display.Full(dockerCli.Err(), stdinfo(dockerCli))
|
||||
ep = display.Full(dockerCli.Err(), stdinfo(dockerCli), detached)
|
||||
|
||||
case display.ModePlain:
|
||||
if ansi == "always" {
|
||||
@@ -550,12 +552,15 @@ func RootCommand(dockerCli command.Cli, backendOptions *BackendOptions) *cobra.C
|
||||
fmt.Fprint(os.Stderr, aec.Apply("option '--workdir' is DEPRECATED at root level! Please use '--project-directory' instead.\n", aec.RedF))
|
||||
}
|
||||
for i, file := range opts.EnvFiles {
|
||||
file = composepaths.ExpandUser(file)
|
||||
if !filepath.IsAbs(file) {
|
||||
file, err := filepath.Abs(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opts.EnvFiles[i] = file
|
||||
} else {
|
||||
opts.EnvFiles[i] = file
|
||||
}
|
||||
}
|
||||
|
||||
@@ -672,7 +677,21 @@ func stdinfo(dockerCli command.Cli) io.Writer {
|
||||
return dockerCli.Err()
|
||||
}
|
||||
|
||||
func setEnvWithDotEnv(opts ProjectOptions) error {
|
||||
func setEnvWithDotEnv(opts ProjectOptions, dockerCli command.Cli) error {
|
||||
// Check if we're using a remote config (OCI or Git)
|
||||
// If so, skip env loading as remote loaders haven't been initialized yet
|
||||
// and trying to process the path would fail
|
||||
remoteLoaders := opts.remoteLoaders(dockerCli)
|
||||
for _, path := range opts.ConfigPaths {
|
||||
for _, loader := range remoteLoaders {
|
||||
if loader.Accept(path) {
|
||||
// Remote config - skip env loading for now
|
||||
// It will be loaded later when the project is fully initialized
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
options, err := cli.NewProjectOptions(opts.ConfigPaths,
|
||||
cli.WithWorkingDirectory(opts.ProjectDir),
|
||||
cli.WithOsEnv,
|
||||
|
||||
76
cmd/compose/compose_oci_test.go
Normal file
76
cmd/compose/compose_oci_test.go
Normal file
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package compose
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"go.uber.org/mock/gomock"
|
||||
"gotest.tools/v3/assert"
|
||||
|
||||
"github.com/docker/compose/v5/pkg/mocks"
|
||||
)
|
||||
|
||||
func TestSetEnvWithDotEnv_WithOCIArtifact(t *testing.T) {
|
||||
// Test that setEnvWithDotEnv doesn't fail when using OCI artifacts
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
cli := mocks.NewMockCli(ctrl)
|
||||
|
||||
opts := ProjectOptions{
|
||||
ConfigPaths: []string{"oci://docker.io/dockersamples/welcome-to-docker"},
|
||||
ProjectDir: "",
|
||||
EnvFiles: []string{},
|
||||
}
|
||||
|
||||
err := setEnvWithDotEnv(opts, cli)
|
||||
assert.NilError(t, err, "setEnvWithDotEnv should not fail with OCI artifact path")
|
||||
}
|
||||
|
||||
func TestSetEnvWithDotEnv_WithGitRemote(t *testing.T) {
|
||||
// Test that setEnvWithDotEnv doesn't fail when using Git remotes
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
cli := mocks.NewMockCli(ctrl)
|
||||
|
||||
opts := ProjectOptions{
|
||||
ConfigPaths: []string{"https://github.com/docker/compose.git"},
|
||||
ProjectDir: "",
|
||||
EnvFiles: []string{},
|
||||
}
|
||||
|
||||
err := setEnvWithDotEnv(opts, cli)
|
||||
assert.NilError(t, err, "setEnvWithDotEnv should not fail with Git remote path")
|
||||
}
|
||||
|
||||
func TestSetEnvWithDotEnv_WithLocalPath(t *testing.T) {
|
||||
// Test that setEnvWithDotEnv still works with local paths
|
||||
// This will fail if the file doesn't exist, but it should not panic
|
||||
// or produce invalid paths
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
cli := mocks.NewMockCli(ctrl)
|
||||
|
||||
opts := ProjectOptions{
|
||||
ConfigPaths: []string{"compose.yaml"},
|
||||
ProjectDir: "",
|
||||
EnvFiles: []string{},
|
||||
}
|
||||
|
||||
// This may error if files don't exist, but should not panic
|
||||
_ = setEnvWithDotEnv(opts, cli)
|
||||
}
|
||||
@@ -198,12 +198,11 @@ func (opts createOptions) Apply(project *types.Project) error {
|
||||
|
||||
func applyScaleOpts(project *types.Project, opts []string) error {
|
||||
for _, scale := range opts {
|
||||
split := strings.Split(scale, "=")
|
||||
if len(split) != 2 {
|
||||
name, val, ok := strings.Cut(scale, "=")
|
||||
if !ok || val == "" {
|
||||
return fmt.Errorf("invalid --scale option %q. Should be SERVICE=NUM", scale)
|
||||
}
|
||||
name := split[0]
|
||||
replicas, err := strconv.Atoi(split[1])
|
||||
replicas, err := strconv.Atoi(val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ func downCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *Backe
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runDown(ctx, dockerCli, backendOptions, opts, args)
|
||||
}),
|
||||
ValidArgsFunction: noCompletion(),
|
||||
ValidArgsFunction: completeServiceNames(dockerCli, p),
|
||||
}
|
||||
flags := downCmd.Flags()
|
||||
removeOrphans := utils.StringToBool(os.Getenv(ComposeRemoveOrphans))
|
||||
|
||||
@@ -213,9 +213,9 @@ func extractEnvCLIDefined(cmdEnvs []string) map[string]string {
|
||||
// Parse command-line environment variables
|
||||
cmdEnvMap := make(map[string]string)
|
||||
for _, env := range cmdEnvs {
|
||||
parts := strings.SplitN(env, "=", 2)
|
||||
if len(parts) == 2 {
|
||||
cmdEnvMap[parts[0]] = parts[1]
|
||||
key, val, ok := strings.Cut(env, "=")
|
||||
if ok {
|
||||
cmdEnvMap[key] = val
|
||||
}
|
||||
}
|
||||
return cmdEnvMap
|
||||
|
||||
@@ -18,7 +18,6 @@ package compose
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@@ -214,10 +213,7 @@ func TestDisplayInterpolationVariables(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
// Create a temporary directory for the test
|
||||
tmpDir, err := os.MkdirTemp("", "compose-test")
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = os.RemoveAll(tmpDir) }()
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Create a temporary compose file
|
||||
composeContent := `
|
||||
@@ -231,8 +227,7 @@ services:
|
||||
- UNSET_VAR # optional without default
|
||||
`
|
||||
composePath := filepath.Join(tmpDir, "docker-compose.yml")
|
||||
err = os.WriteFile(composePath, []byte(composeContent), 0o644)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, os.WriteFile(composePath, []byte(composeContent), 0o644))
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
cli := mocks.NewMockCli(ctrl)
|
||||
@@ -244,16 +239,11 @@ services:
|
||||
}
|
||||
|
||||
// Set up the context with necessary environment variables
|
||||
ctx := context.Background()
|
||||
_ = os.Setenv("TEST_VAR", "test-value")
|
||||
_ = os.Setenv("API_KEY", "123456")
|
||||
defer func() {
|
||||
_ = os.Unsetenv("TEST_VAR")
|
||||
_ = os.Unsetenv("API_KEY")
|
||||
}()
|
||||
t.Setenv("TEST_VAR", "test-value")
|
||||
t.Setenv("API_KEY", "123456")
|
||||
|
||||
// Extract variables from the model
|
||||
info, noVariables, err := extractInterpolationVariablesFromModel(ctx, cli, projectOptions, []string{})
|
||||
info, noVariables, err := extractInterpolationVariablesFromModel(t.Context(), cli, projectOptions, []string{})
|
||||
require.NoError(t, err)
|
||||
require.False(t, noVariables)
|
||||
|
||||
|
||||
@@ -50,19 +50,19 @@ func (p *psOptions) parseFilter() error {
|
||||
if p.Filter == "" {
|
||||
return nil
|
||||
}
|
||||
parts := strings.SplitN(p.Filter, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
key, val, ok := strings.Cut(p.Filter, "=")
|
||||
if !ok {
|
||||
return errors.New("arguments to --filter should be in form KEY=VAL")
|
||||
}
|
||||
switch parts[0] {
|
||||
switch key {
|
||||
case "status":
|
||||
p.Status = append(p.Status, parts[1])
|
||||
p.Status = append(p.Status, val)
|
||||
return nil
|
||||
case "source":
|
||||
return api.ErrNotImplemented
|
||||
default:
|
||||
return fmt.Errorf("unknown filter %s", parts[0])
|
||||
return fmt.Errorf("unknown filter %s", key)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func psCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
|
||||
|
||||
@@ -284,11 +284,11 @@ func runRun(ctx context.Context, backend api.Compose, project *types.Project, op
|
||||
|
||||
labels := types.Labels{}
|
||||
for _, s := range options.labels {
|
||||
parts := strings.SplitN(s, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
key, val, ok := strings.Cut(s, "=")
|
||||
if !ok {
|
||||
return fmt.Errorf("label must be set as KEY=VALUE")
|
||||
}
|
||||
labels[parts[0]] = parts[1]
|
||||
labels[key] = val
|
||||
}
|
||||
|
||||
var buildForRun *api.BuildOptions
|
||||
|
||||
@@ -63,7 +63,10 @@ func runStart(ctx context.Context, dockerCli command.Cli, backendOptions *Backen
|
||||
return err
|
||||
}
|
||||
|
||||
timeout := time.Duration(opts.waitTimeout) * time.Second
|
||||
var timeout time.Duration
|
||||
if opts.waitTimeout > 0 {
|
||||
timeout = time.Duration(opts.waitTimeout) * time.Second
|
||||
}
|
||||
return backend.Start(ctx, name, api.StartOptions{
|
||||
AttachTo: services,
|
||||
Project: project,
|
||||
|
||||
@@ -188,6 +188,9 @@ func upCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *Backend
|
||||
|
||||
//nolint:gocyclo
|
||||
func validateFlags(up *upOptions, create *createOptions) error {
|
||||
if up.waitTimeout < 0 {
|
||||
return fmt.Errorf("--wait-timeout must be a non-negative integer")
|
||||
}
|
||||
if up.exitCodeFrom != "" && !up.cascadeFail {
|
||||
up.cascadeStop = true
|
||||
}
|
||||
@@ -328,7 +331,10 @@ func runUp(
|
||||
attach = attachSet.Elements()
|
||||
}
|
||||
|
||||
timeout := time.Duration(upOptions.waitTimeout) * time.Second
|
||||
var timeout time.Duration
|
||||
if upOptions.waitTimeout > 0 {
|
||||
timeout = time.Duration(upOptions.waitTimeout) * time.Second
|
||||
}
|
||||
return backend.Up(ctx, project, api.UpOptions{
|
||||
Create: create,
|
||||
Start: api.StartOptions{
|
||||
|
||||
@@ -21,6 +21,8 @@ import (
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"gotest.tools/v3/assert"
|
||||
|
||||
"github.com/docker/compose/v5/pkg/api"
|
||||
)
|
||||
|
||||
func TestApplyScaleOpt(t *testing.T) {
|
||||
@@ -48,3 +50,42 @@ func TestApplyScaleOpt(t *testing.T) {
|
||||
assert.Equal(t, *bar.Scale, 3)
|
||||
assert.Equal(t, *bar.Deploy.Replicas, 3)
|
||||
}
|
||||
|
||||
func TestUpOptions_OnExit(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args upOptions
|
||||
want api.Cascade
|
||||
}{
|
||||
{
|
||||
name: "no cascade",
|
||||
args: upOptions{},
|
||||
want: api.CascadeIgnore,
|
||||
},
|
||||
{
|
||||
name: "cascade stop",
|
||||
args: upOptions{cascadeStop: true},
|
||||
want: api.CascadeStop,
|
||||
},
|
||||
{
|
||||
name: "cascade fail",
|
||||
args: upOptions{cascadeFail: true},
|
||||
want: api.CascadeFail,
|
||||
},
|
||||
{
|
||||
name: "both set - stop takes precedence",
|
||||
args: upOptions{
|
||||
cascadeStop: true,
|
||||
cascadeFail: true,
|
||||
},
|
||||
want: api.CascadeStop,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := tt.args.OnExit()
|
||||
assert.Equal(t, got, tt.want)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,9 +21,11 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"iter"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/buger/goterm"
|
||||
"github.com/docker/go-units"
|
||||
@@ -35,13 +37,14 @@ import (
|
||||
|
||||
// Full creates an EventProcessor that render advanced UI within a terminal.
|
||||
// On Start, TUI lists task with a progress timer
|
||||
func Full(out io.Writer, info io.Writer) api.EventProcessor {
|
||||
func Full(out io.Writer, info io.Writer, detached bool) api.EventProcessor {
|
||||
return &ttyWriter{
|
||||
out: out,
|
||||
info: info,
|
||||
tasks: map[string]*task{},
|
||||
done: make(chan bool),
|
||||
mtx: &sync.Mutex{},
|
||||
out: out,
|
||||
info: info,
|
||||
tasks: map[string]*task{},
|
||||
done: make(chan bool),
|
||||
mtx: &sync.Mutex{},
|
||||
detached: detached,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +61,7 @@ type ttyWriter struct {
|
||||
ticker *time.Ticker
|
||||
suspended bool
|
||||
info io.Writer
|
||||
detached bool
|
||||
}
|
||||
|
||||
type task struct {
|
||||
@@ -188,7 +192,7 @@ func (w *ttyWriter) On(events ...api.Resource) {
|
||||
continue
|
||||
}
|
||||
|
||||
if w.operation != "start" && (e.Text == api.StatusStarted || e.Text == api.StatusStarting) {
|
||||
if w.operation != "start" && (e.Text == api.StatusStarted || e.Text == api.StatusStarting) && !w.detached {
|
||||
// skip those events to avoid mix with container logs
|
||||
continue
|
||||
}
|
||||
@@ -258,13 +262,39 @@ func (w *ttyWriter) childrenTasks(parent string) iter.Seq[*task] {
|
||||
}
|
||||
}
|
||||
|
||||
// lineData holds pre-computed formatting for a task line
|
||||
type lineData struct {
|
||||
spinner string // rendered spinner with color
|
||||
prefix string // dry-run prefix if any
|
||||
taskID string // possibly abbreviated
|
||||
progress string // progress bar and size info
|
||||
status string // rendered status with color
|
||||
details string // possibly abbreviated
|
||||
timer string // rendered timer with color
|
||||
statusPad int // padding before status to align
|
||||
timerPad int // padding before timer to align
|
||||
statusColor colorFunc
|
||||
}
|
||||
|
||||
func (w *ttyWriter) print() {
|
||||
terminalWidth := goterm.Width()
|
||||
terminalHeight := goterm.Height()
|
||||
if terminalWidth <= 0 {
|
||||
terminalWidth = 80
|
||||
}
|
||||
if terminalHeight <= 0 {
|
||||
terminalHeight = 24
|
||||
}
|
||||
w.printWithDimensions(terminalWidth, terminalHeight)
|
||||
}
|
||||
|
||||
func (w *ttyWriter) printWithDimensions(terminalWidth, terminalHeight int) {
|
||||
w.mtx.Lock()
|
||||
defer w.mtx.Unlock()
|
||||
if len(w.tasks) == 0 {
|
||||
return
|
||||
}
|
||||
terminalWidth := goterm.Width()
|
||||
|
||||
up := w.numLines + 1
|
||||
if !w.repeated {
|
||||
up--
|
||||
@@ -283,39 +313,208 @@ func (w *ttyWriter) print() {
|
||||
firstLine := fmt.Sprintf("[+] %s %d/%d", w.operation, numDone(w.tasks), len(w.tasks))
|
||||
_, _ = fmt.Fprintln(w.out, firstLine)
|
||||
|
||||
var statusPadding int
|
||||
for _, t := range w.tasks {
|
||||
l := len(t.ID)
|
||||
if len(t.parents) == 0 && statusPadding < l {
|
||||
statusPadding = l
|
||||
// Collect parent tasks in original order
|
||||
allTasks := slices.Collect(w.parentTasks())
|
||||
|
||||
// Available lines: terminal height - 2 (header line + potential "more" line)
|
||||
maxLines := terminalHeight - 2
|
||||
if maxLines < 1 {
|
||||
maxLines = 1
|
||||
}
|
||||
|
||||
showMore := len(allTasks) > maxLines
|
||||
tasksToShow := allTasks
|
||||
if showMore {
|
||||
tasksToShow = allTasks[:maxLines-1] // Reserve one line for "more" message
|
||||
}
|
||||
|
||||
// collect line data and compute timerLen
|
||||
lines := make([]lineData, len(tasksToShow))
|
||||
var timerLen int
|
||||
for i, t := range tasksToShow {
|
||||
lines[i] = w.prepareLineData(t)
|
||||
if len(lines[i].timer) > timerLen {
|
||||
timerLen = len(lines[i].timer)
|
||||
}
|
||||
}
|
||||
|
||||
skipChildEvents := len(w.tasks) > goterm.Height()-2
|
||||
// shorten details/taskID to fit terminal width
|
||||
w.adjustLineWidth(lines, timerLen, terminalWidth)
|
||||
|
||||
// compute padding
|
||||
w.applyPadding(lines, terminalWidth, timerLen)
|
||||
|
||||
// Render lines
|
||||
numLines := 0
|
||||
for t := range w.parentTasks() {
|
||||
line := w.lineText(t, "", terminalWidth, statusPadding, w.dryRun)
|
||||
_, _ = fmt.Fprint(w.out, line)
|
||||
for _, l := range lines {
|
||||
_, _ = fmt.Fprint(w.out, lineText(l))
|
||||
numLines++
|
||||
if skipChildEvents {
|
||||
continue
|
||||
}
|
||||
for child := range w.childrenTasks(t.ID) {
|
||||
line := w.lineText(child, " ", terminalWidth, statusPadding-2, w.dryRun)
|
||||
_, _ = fmt.Fprint(w.out, line)
|
||||
numLines++
|
||||
}
|
||||
}
|
||||
for i := numLines; i < w.numLines; i++ {
|
||||
if numLines < goterm.Height()-2 {
|
||||
_, _ = fmt.Fprintln(w.out, strings.Repeat(" ", terminalWidth))
|
||||
numLines++
|
||||
|
||||
if showMore {
|
||||
moreCount := len(allTasks) - len(tasksToShow)
|
||||
moreText := fmt.Sprintf(" ... %d more", moreCount)
|
||||
pad := terminalWidth - len(moreText)
|
||||
if pad < 0 {
|
||||
pad = 0
|
||||
}
|
||||
_, _ = fmt.Fprintf(w.out, "%s%s\n", moreText, strings.Repeat(" ", pad))
|
||||
numLines++
|
||||
}
|
||||
|
||||
// Clear any remaining lines from previous render
|
||||
for i := numLines; i < w.numLines; i++ {
|
||||
_, _ = fmt.Fprintln(w.out, strings.Repeat(" ", terminalWidth))
|
||||
numLines++
|
||||
}
|
||||
w.numLines = numLines
|
||||
}
|
||||
|
||||
func (w *ttyWriter) lineText(t *task, pad string, terminalWidth, statusPadding int, dryRun bool) string {
|
||||
func (w *ttyWriter) applyPadding(lines []lineData, terminalWidth int, timerLen int) {
|
||||
var maxBeforeStatus int
|
||||
for i := range lines {
|
||||
l := &lines[i]
|
||||
// Width before statusPad: space(1) + spinner(1) + prefix + space(1) + taskID + progress
|
||||
beforeStatus := 3 + lenAnsi(l.prefix) + utf8.RuneCountInString(l.taskID) + lenAnsi(l.progress)
|
||||
if beforeStatus > maxBeforeStatus {
|
||||
maxBeforeStatus = beforeStatus
|
||||
}
|
||||
}
|
||||
|
||||
for i, l := range lines {
|
||||
// Position before statusPad: space(1) + spinner(1) + prefix + space(1) + taskID + progress
|
||||
beforeStatus := 3 + lenAnsi(l.prefix) + utf8.RuneCountInString(l.taskID) + lenAnsi(l.progress)
|
||||
// statusPad aligns status; lineText adds 1 more space after statusPad
|
||||
l.statusPad = maxBeforeStatus - beforeStatus
|
||||
|
||||
// Format: beforeStatus + statusPad + space(1) + status
|
||||
lineLen := beforeStatus + l.statusPad + 1 + utf8.RuneCountInString(l.status)
|
||||
if l.details != "" {
|
||||
lineLen += 1 + utf8.RuneCountInString(l.details)
|
||||
}
|
||||
l.timerPad = terminalWidth - lineLen - timerLen
|
||||
if l.timerPad < 1 {
|
||||
l.timerPad = 1
|
||||
}
|
||||
lines[i] = l
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func (w *ttyWriter) adjustLineWidth(lines []lineData, timerLen int, terminalWidth int) {
|
||||
const minIDLen = 10
|
||||
maxStatusLen := maxStatusLength(lines)
|
||||
|
||||
// Iteratively truncate until all lines fit
|
||||
for range 100 { // safety limit
|
||||
maxBeforeStatus := maxBeforeStatusWidth(lines)
|
||||
overflow := computeOverflow(lines, maxBeforeStatus, maxStatusLen, timerLen, terminalWidth)
|
||||
|
||||
if overflow <= 0 {
|
||||
break
|
||||
}
|
||||
|
||||
// First try to truncate details, then taskID
|
||||
if !truncateDetails(lines, overflow) && !truncateLongestTaskID(lines, overflow, minIDLen) {
|
||||
break // Can't truncate further
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// maxStatusLength returns the maximum status text length across all lines.
|
||||
func maxStatusLength(lines []lineData) int {
|
||||
var maxLen int
|
||||
for i := range lines {
|
||||
if len(lines[i].status) > maxLen {
|
||||
maxLen = len(lines[i].status)
|
||||
}
|
||||
}
|
||||
return maxLen
|
||||
}
|
||||
|
||||
// maxBeforeStatusWidth computes the maximum width before statusPad across all lines.
|
||||
// This is: space(1) + spinner(1) + prefix + space(1) + taskID + progress
|
||||
func maxBeforeStatusWidth(lines []lineData) int {
|
||||
var maxWidth int
|
||||
for i := range lines {
|
||||
l := &lines[i]
|
||||
width := 3 + lenAnsi(l.prefix) + len(l.taskID) + lenAnsi(l.progress)
|
||||
if width > maxWidth {
|
||||
maxWidth = width
|
||||
}
|
||||
}
|
||||
return maxWidth
|
||||
}
|
||||
|
||||
// computeOverflow calculates how many characters the widest line exceeds the terminal width.
|
||||
// Returns 0 or negative if all lines fit.
|
||||
func computeOverflow(lines []lineData, maxBeforeStatus, maxStatusLen, timerLen, terminalWidth int) int {
|
||||
var maxOverflow int
|
||||
for i := range lines {
|
||||
l := &lines[i]
|
||||
detailsLen := len(l.details)
|
||||
if detailsLen > 0 {
|
||||
detailsLen++ // space before details
|
||||
}
|
||||
// Line width: maxBeforeStatus + space(1) + status + details + minTimerPad(1) + timer
|
||||
lineWidth := maxBeforeStatus + 1 + maxStatusLen + detailsLen + 1 + timerLen
|
||||
overflow := lineWidth - terminalWidth
|
||||
if overflow > maxOverflow {
|
||||
maxOverflow = overflow
|
||||
}
|
||||
}
|
||||
return maxOverflow
|
||||
}
|
||||
|
||||
// truncateDetails tries to truncate the first line's details to reduce overflow.
|
||||
// Returns true if any truncation was performed.
|
||||
func truncateDetails(lines []lineData, overflow int) bool {
|
||||
for i := range lines {
|
||||
l := &lines[i]
|
||||
if len(l.details) > 3 {
|
||||
reduction := overflow
|
||||
if reduction > len(l.details)-3 {
|
||||
reduction = len(l.details) - 3
|
||||
}
|
||||
l.details = l.details[:len(l.details)-reduction-3] + "..."
|
||||
return true
|
||||
} else if l.details != "" {
|
||||
l.details = ""
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// truncateLongestTaskID truncates the longest taskID to reduce overflow.
|
||||
// Returns true if truncation was performed.
|
||||
func truncateLongestTaskID(lines []lineData, overflow, minIDLen int) bool {
|
||||
longestIdx := -1
|
||||
longestLen := minIDLen
|
||||
for i := range lines {
|
||||
if len(lines[i].taskID) > longestLen {
|
||||
longestLen = len(lines[i].taskID)
|
||||
longestIdx = i
|
||||
}
|
||||
}
|
||||
|
||||
if longestIdx < 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
l := &lines[longestIdx]
|
||||
reduction := overflow + 3 // account for "..."
|
||||
newLen := len(l.taskID) - reduction
|
||||
if newLen < minIDLen-3 {
|
||||
newLen = minIDLen - 3
|
||||
}
|
||||
if newLen > 0 {
|
||||
l.taskID = l.taskID[:newLen] + "..."
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (w *ttyWriter) prepareLineData(t *task) lineData {
|
||||
endTime := time.Now()
|
||||
if t.status != api.Working {
|
||||
endTime = t.startTime
|
||||
@@ -323,8 +522,9 @@ func (w *ttyWriter) lineText(t *task, pad string, terminalWidth, statusPadding i
|
||||
endTime = t.endTime
|
||||
}
|
||||
}
|
||||
|
||||
prefix := ""
|
||||
if dryRun {
|
||||
if w.dryRun {
|
||||
prefix = PrefixColor(DRYRUN_PREFIX)
|
||||
}
|
||||
|
||||
@@ -338,11 +538,9 @@ func (w *ttyWriter) lineText(t *task, pad string, terminalWidth, statusPadding i
|
||||
)
|
||||
|
||||
// only show the aggregated progress while the root operation is in-progress
|
||||
if parent := t; parent.status == api.Working {
|
||||
for child := range w.childrenTasks(parent.ID) {
|
||||
if t.status == api.Working {
|
||||
for child := range w.childrenTasks(t.ID) {
|
||||
if child.status == api.Working && child.total == 0 {
|
||||
// we don't have totals available for all the child events
|
||||
// so don't show the total progress yet
|
||||
hideDetails = true
|
||||
}
|
||||
total += child.total
|
||||
@@ -356,49 +554,49 @@ func (w *ttyWriter) lineText(t *task, pad string, terminalWidth, statusPadding i
|
||||
}
|
||||
}
|
||||
|
||||
// don't try to show detailed progress if we don't have any idea
|
||||
if total == 0 {
|
||||
hideDetails = true
|
||||
}
|
||||
|
||||
txt := t.ID
|
||||
var progress string
|
||||
if len(completion) > 0 {
|
||||
var progress string
|
||||
progress = " [" + SuccessColor(strings.Join(completion, "")) + "]"
|
||||
if !hideDetails {
|
||||
progress = fmt.Sprintf(" %7s / %-7s", units.HumanSize(float64(current)), units.HumanSize(float64(total)))
|
||||
progress += fmt.Sprintf(" %7s / %-7s", units.HumanSize(float64(current)), units.HumanSize(float64(total)))
|
||||
}
|
||||
txt = fmt.Sprintf("%s [%s]%s",
|
||||
t.ID,
|
||||
SuccessColor(strings.Join(completion, "")),
|
||||
progress,
|
||||
)
|
||||
}
|
||||
textLen := len(txt)
|
||||
padding := statusPadding - textLen
|
||||
if padding < 0 {
|
||||
padding = 0
|
||||
}
|
||||
// calculate the max length for the status text, on errors it
|
||||
// is 2-3 lines long and breaks the line formatting
|
||||
maxDetailsLen := terminalWidth - textLen - statusPadding - 15
|
||||
details := t.details
|
||||
// in some cases (debugging under VS Code), terminalWidth is set to zero by goterm.Width() ; ensuring we don't tweak strings with negative char index
|
||||
if maxDetailsLen > 0 && len(details) > maxDetailsLen {
|
||||
details = details[:maxDetailsLen] + "..."
|
||||
}
|
||||
text := fmt.Sprintf("%s %s%s %s %s%s %s",
|
||||
pad,
|
||||
spinner(t),
|
||||
prefix,
|
||||
txt,
|
||||
strings.Repeat(" ", padding),
|
||||
colorFn(t.status)(t.text),
|
||||
details,
|
||||
)
|
||||
timer := fmt.Sprintf("%.1fs ", elapsed)
|
||||
o := align(text, TimerColor(timer), terminalWidth)
|
||||
|
||||
return o
|
||||
return lineData{
|
||||
spinner: spinner(t),
|
||||
prefix: prefix,
|
||||
taskID: t.ID,
|
||||
progress: progress,
|
||||
status: t.text,
|
||||
statusColor: colorFn(t.status),
|
||||
details: t.details,
|
||||
timer: fmt.Sprintf("%.1fs", elapsed),
|
||||
}
|
||||
}
|
||||
|
||||
func lineText(l lineData) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString(" ")
|
||||
sb.WriteString(l.spinner)
|
||||
sb.WriteString(l.prefix)
|
||||
sb.WriteString(" ")
|
||||
sb.WriteString(l.taskID)
|
||||
sb.WriteString(l.progress)
|
||||
sb.WriteString(strings.Repeat(" ", l.statusPad))
|
||||
sb.WriteString(" ")
|
||||
sb.WriteString(l.statusColor(l.status))
|
||||
if l.details != "" {
|
||||
sb.WriteString(" ")
|
||||
sb.WriteString(l.details)
|
||||
}
|
||||
sb.WriteString(strings.Repeat(" ", l.timerPad))
|
||||
sb.WriteString(TimerColor(l.timer))
|
||||
sb.WriteString("\n")
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -443,17 +641,6 @@ func numDone(tasks map[string]*task) int {
|
||||
return i
|
||||
}
|
||||
|
||||
func align(l, r string, w int) string {
|
||||
ll := lenAnsi(l)
|
||||
lr := lenAnsi(r)
|
||||
pad := ""
|
||||
count := w - ll - lr
|
||||
if count > 0 {
|
||||
pad = strings.Repeat(" ", count)
|
||||
}
|
||||
return fmt.Sprintf("%s%s%s\n", l, pad, r)
|
||||
}
|
||||
|
||||
// lenAnsi count of user-perceived characters in ANSI string.
|
||||
func lenAnsi(s string) int {
|
||||
length := 0
|
||||
|
||||
424
cmd/display/tty_test.go
Normal file
424
cmd/display/tty_test.go
Normal file
@@ -0,0 +1,424 @@
|
||||
/*
|
||||
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 display
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
|
||||
"github.com/docker/compose/v5/pkg/api"
|
||||
)
|
||||
|
||||
func newTestWriter() (*ttyWriter, *bytes.Buffer) {
|
||||
var buf bytes.Buffer
|
||||
w := &ttyWriter{
|
||||
out: &buf,
|
||||
info: &buf,
|
||||
tasks: map[string]*task{},
|
||||
done: make(chan bool),
|
||||
mtx: &sync.Mutex{},
|
||||
operation: "pull",
|
||||
}
|
||||
return w, &buf
|
||||
}
|
||||
|
||||
func addTask(w *ttyWriter, id, text, details string, status api.EventStatus) {
|
||||
t := &task{
|
||||
ID: id,
|
||||
parents: make(map[string]struct{}),
|
||||
startTime: time.Now(),
|
||||
text: text,
|
||||
details: details,
|
||||
status: status,
|
||||
spinner: NewSpinner(),
|
||||
}
|
||||
w.tasks[id] = t
|
||||
w.ids = append(w.ids, id)
|
||||
}
|
||||
|
||||
// extractLines parses the output buffer and returns lines without ANSI control sequences
|
||||
func extractLines(buf *bytes.Buffer) []string {
|
||||
content := buf.String()
|
||||
// Split by newline
|
||||
rawLines := strings.Split(content, "\n")
|
||||
var lines []string
|
||||
for _, line := range rawLines {
|
||||
// Skip empty lines and lines that are just ANSI codes
|
||||
if lenAnsi(line) > 0 {
|
||||
lines = append(lines, line)
|
||||
}
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
func TestPrintWithDimensions_LinesFitTerminalWidth(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
taskID string
|
||||
status string
|
||||
details string
|
||||
terminalWidth int
|
||||
}{
|
||||
{
|
||||
name: "short task fits wide terminal",
|
||||
taskID: "Image foo",
|
||||
status: "Pulling",
|
||||
details: "layer abc123",
|
||||
terminalWidth: 100,
|
||||
},
|
||||
{
|
||||
name: "long details truncated to fit",
|
||||
taskID: "Image foo",
|
||||
status: "Pulling",
|
||||
details: "downloading layer sha256:abc123def456789xyz0123456789abcdef",
|
||||
terminalWidth: 50,
|
||||
},
|
||||
{
|
||||
name: "long taskID truncated to fit",
|
||||
taskID: "very-long-image-name-that-exceeds-terminal-width",
|
||||
status: "Pulling",
|
||||
details: "",
|
||||
terminalWidth: 40,
|
||||
},
|
||||
{
|
||||
name: "both long taskID and details",
|
||||
taskID: "my-very-long-service-name-here",
|
||||
status: "Downloading",
|
||||
details: "layer sha256:abc123def456789xyz0123456789",
|
||||
terminalWidth: 50,
|
||||
},
|
||||
{
|
||||
name: "narrow terminal",
|
||||
taskID: "service-name",
|
||||
status: "Pulling",
|
||||
details: "some details",
|
||||
terminalWidth: 35,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
w, buf := newTestWriter()
|
||||
addTask(w, tc.taskID, tc.status, tc.details, api.Working)
|
||||
|
||||
w.printWithDimensions(tc.terminalWidth, 24)
|
||||
|
||||
lines := extractLines(buf)
|
||||
for i, line := range lines {
|
||||
lineLen := lenAnsi(line)
|
||||
assert.Assert(t, lineLen <= tc.terminalWidth,
|
||||
"line %d has length %d which exceeds terminal width %d: %q",
|
||||
i, lineLen, tc.terminalWidth, line)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrintWithDimensions_MultipleTasksFitTerminalWidth(t *testing.T) {
|
||||
w, buf := newTestWriter()
|
||||
|
||||
// Add multiple tasks with varying lengths
|
||||
addTask(w, "Image nginx", "Pulling", "layer sha256:abc123", api.Working)
|
||||
addTask(w, "Image postgres-database", "Pulling", "downloading", api.Working)
|
||||
addTask(w, "Image redis", "Pulled", "", api.Done)
|
||||
|
||||
terminalWidth := 60
|
||||
w.printWithDimensions(terminalWidth, 24)
|
||||
|
||||
lines := extractLines(buf)
|
||||
for i, line := range lines {
|
||||
lineLen := lenAnsi(line)
|
||||
assert.Assert(t, lineLen <= terminalWidth,
|
||||
"line %d has length %d which exceeds terminal width %d: %q",
|
||||
i, lineLen, terminalWidth, line)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrintWithDimensions_VeryNarrowTerminal(t *testing.T) {
|
||||
w, buf := newTestWriter()
|
||||
addTask(w, "Image nginx", "Pulling", "details", api.Working)
|
||||
|
||||
terminalWidth := 30
|
||||
w.printWithDimensions(terminalWidth, 24)
|
||||
|
||||
lines := extractLines(buf)
|
||||
for i, line := range lines {
|
||||
lineLen := lenAnsi(line)
|
||||
assert.Assert(t, lineLen <= terminalWidth,
|
||||
"line %d has length %d which exceeds terminal width %d: %q",
|
||||
i, lineLen, terminalWidth, line)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrintWithDimensions_TaskWithProgress(t *testing.T) {
|
||||
w, buf := newTestWriter()
|
||||
|
||||
// Create parent task
|
||||
parent := &task{
|
||||
ID: "Image nginx",
|
||||
parents: make(map[string]struct{}),
|
||||
startTime: time.Now(),
|
||||
text: "Pulling",
|
||||
status: api.Working,
|
||||
spinner: NewSpinner(),
|
||||
}
|
||||
w.tasks["Image nginx"] = parent
|
||||
w.ids = append(w.ids, "Image nginx")
|
||||
|
||||
// Create child tasks to trigger progress display
|
||||
for i := 0; i < 3; i++ {
|
||||
child := &task{
|
||||
ID: "layer" + string(rune('a'+i)),
|
||||
parents: map[string]struct{}{"Image nginx": {}},
|
||||
startTime: time.Now(),
|
||||
text: "Downloading",
|
||||
status: api.Working,
|
||||
total: 1000,
|
||||
current: 500,
|
||||
percent: 50,
|
||||
spinner: NewSpinner(),
|
||||
}
|
||||
w.tasks[child.ID] = child
|
||||
w.ids = append(w.ids, child.ID)
|
||||
}
|
||||
|
||||
terminalWidth := 80
|
||||
w.printWithDimensions(terminalWidth, 24)
|
||||
|
||||
lines := extractLines(buf)
|
||||
for i, line := range lines {
|
||||
lineLen := lenAnsi(line)
|
||||
assert.Assert(t, lineLen <= terminalWidth,
|
||||
"line %d has length %d which exceeds terminal width %d: %q",
|
||||
i, lineLen, terminalWidth, line)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdjustLineWidth_DetailsCorrectlyTruncated(t *testing.T) {
|
||||
w := &ttyWriter{}
|
||||
lines := []lineData{
|
||||
{
|
||||
taskID: "Image foo",
|
||||
status: "Pulling",
|
||||
details: "downloading layer sha256:abc123def456789xyz",
|
||||
},
|
||||
}
|
||||
|
||||
terminalWidth := 50
|
||||
timerLen := 5
|
||||
w.adjustLineWidth(lines, timerLen, terminalWidth)
|
||||
|
||||
// Verify the line fits
|
||||
detailsLen := len(lines[0].details)
|
||||
if detailsLen > 0 {
|
||||
detailsLen++ // space before details
|
||||
}
|
||||
// widthWithoutDetails = 5 + prefix(0) + taskID(9) + progress(0) + status(7) + timer(5) = 26
|
||||
lineWidth := 5 + len(lines[0].taskID) + len(lines[0].status) + detailsLen + timerLen
|
||||
|
||||
assert.Assert(t, lineWidth <= terminalWidth,
|
||||
"line width %d should not exceed terminal width %d (taskID=%q, details=%q)",
|
||||
lineWidth, terminalWidth, lines[0].taskID, lines[0].details)
|
||||
|
||||
// Verify details were truncated (not removed entirely)
|
||||
assert.Assert(t, lines[0].details != "", "details should be truncated, not removed")
|
||||
assert.Assert(t, strings.HasSuffix(lines[0].details, "..."), "truncated details should end with ...")
|
||||
}
|
||||
|
||||
func TestAdjustLineWidth_TaskIDCorrectlyTruncated(t *testing.T) {
|
||||
w := &ttyWriter{}
|
||||
lines := []lineData{
|
||||
{
|
||||
taskID: "very-long-image-name-that-exceeds-minimum-length",
|
||||
status: "Pulling",
|
||||
details: "",
|
||||
},
|
||||
}
|
||||
|
||||
terminalWidth := 40
|
||||
timerLen := 5
|
||||
w.adjustLineWidth(lines, timerLen, terminalWidth)
|
||||
|
||||
lineWidth := 5 + len(lines[0].taskID) + 7 + timerLen
|
||||
|
||||
assert.Assert(t, lineWidth <= terminalWidth,
|
||||
"line width %d should not exceed terminal width %d (taskID=%q)",
|
||||
lineWidth, terminalWidth, lines[0].taskID)
|
||||
|
||||
assert.Assert(t, strings.HasSuffix(lines[0].taskID, "..."), "truncated taskID should end with ...")
|
||||
}
|
||||
|
||||
func TestAdjustLineWidth_NoTruncationNeeded(t *testing.T) {
|
||||
w := &ttyWriter{}
|
||||
originalDetails := "short"
|
||||
originalTaskID := "Image foo"
|
||||
lines := []lineData{
|
||||
{
|
||||
taskID: originalTaskID,
|
||||
status: "Pulling",
|
||||
details: originalDetails,
|
||||
},
|
||||
}
|
||||
|
||||
// Wide terminal, nothing should be truncated
|
||||
w.adjustLineWidth(lines, 5, 100)
|
||||
|
||||
assert.Equal(t, originalTaskID, lines[0].taskID, "taskID should not be modified")
|
||||
assert.Equal(t, originalDetails, lines[0].details, "details should not be modified")
|
||||
}
|
||||
|
||||
func TestAdjustLineWidth_DetailsRemovedWhenTooShort(t *testing.T) {
|
||||
w := &ttyWriter{}
|
||||
lines := []lineData{
|
||||
{
|
||||
taskID: "Image foo",
|
||||
status: "Pulling",
|
||||
details: "abc", // Very short, can't be meaningfully truncated
|
||||
},
|
||||
}
|
||||
|
||||
// Terminal so narrow that even minimal details + "..." wouldn't help
|
||||
w.adjustLineWidth(lines, 5, 28)
|
||||
|
||||
assert.Equal(t, "", lines[0].details, "details should be removed entirely when too short to truncate")
|
||||
}
|
||||
|
||||
// stripAnsi removes ANSI escape codes from a string
|
||||
func stripAnsi(s string) string {
|
||||
var result strings.Builder
|
||||
inAnsi := false
|
||||
for _, r := range s {
|
||||
if r == '\x1b' {
|
||||
inAnsi = true
|
||||
continue
|
||||
}
|
||||
if inAnsi {
|
||||
// ANSI sequences end with a letter (m, h, l, G, etc.)
|
||||
if (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') {
|
||||
inAnsi = false
|
||||
}
|
||||
continue
|
||||
}
|
||||
result.WriteRune(r)
|
||||
}
|
||||
return result.String()
|
||||
}
|
||||
|
||||
func TestPrintWithDimensions_PulledAndPullingWithLongIDs(t *testing.T) {
|
||||
w, buf := newTestWriter()
|
||||
|
||||
// Add a completed task with long ID
|
||||
completedTask := &task{
|
||||
ID: "Image docker.io/library/nginx-long-name",
|
||||
parents: make(map[string]struct{}),
|
||||
startTime: time.Now().Add(-2 * time.Second),
|
||||
endTime: time.Now(),
|
||||
text: "Pulled",
|
||||
status: api.Done,
|
||||
spinner: NewSpinner(),
|
||||
}
|
||||
completedTask.spinner.Stop()
|
||||
w.tasks[completedTask.ID] = completedTask
|
||||
w.ids = append(w.ids, completedTask.ID)
|
||||
|
||||
// Add a pending task with long ID
|
||||
pendingTask := &task{
|
||||
ID: "Image docker.io/library/postgres-database",
|
||||
parents: make(map[string]struct{}),
|
||||
startTime: time.Now(),
|
||||
text: "Pulling",
|
||||
status: api.Working,
|
||||
spinner: NewSpinner(),
|
||||
}
|
||||
w.tasks[pendingTask.ID] = pendingTask
|
||||
w.ids = append(w.ids, pendingTask.ID)
|
||||
|
||||
terminalWidth := 50
|
||||
w.printWithDimensions(terminalWidth, 24)
|
||||
|
||||
// Strip all ANSI codes from output and split by newline
|
||||
stripped := stripAnsi(buf.String())
|
||||
lines := strings.Split(stripped, "\n")
|
||||
|
||||
// Filter non-empty lines
|
||||
var nonEmptyLines []string
|
||||
for _, line := range lines {
|
||||
if strings.TrimSpace(line) != "" {
|
||||
nonEmptyLines = append(nonEmptyLines, line)
|
||||
}
|
||||
}
|
||||
|
||||
// Expected output format (50 runes per task line)
|
||||
expected := `[+] pull 1/2
|
||||
✔ Image docker.io/library/nginx-l... Pulled 2.0s
|
||||
⠋ Image docker.io/library/postgre... Pulling 0.0s`
|
||||
|
||||
expectedLines := strings.Split(expected, "\n")
|
||||
|
||||
// Debug output
|
||||
t.Logf("Actual output:\n")
|
||||
for i, line := range nonEmptyLines {
|
||||
t.Logf(" line %d (%2d runes): %q", i, utf8.RuneCountInString(line), line)
|
||||
}
|
||||
|
||||
// Verify number of lines
|
||||
assert.Equal(t, len(expectedLines), len(nonEmptyLines), "number of lines should match")
|
||||
|
||||
// Verify each line matches expected
|
||||
for i, line := range nonEmptyLines {
|
||||
if i < len(expectedLines) {
|
||||
assert.Equal(t, expectedLines[i], line,
|
||||
"line %d should match expected", i)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify task lines fit within terminal width (strict - no tolerance)
|
||||
for i, line := range nonEmptyLines {
|
||||
if i > 0 { // Skip header line
|
||||
runeCount := utf8.RuneCountInString(line)
|
||||
assert.Assert(t, runeCount <= terminalWidth,
|
||||
"line %d has %d runes which exceeds terminal width %d: %q",
|
||||
i, runeCount, terminalWidth, line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLenAnsi(t *testing.T) {
|
||||
testCases := []struct {
|
||||
input string
|
||||
expected int
|
||||
}{
|
||||
{"hello", 5},
|
||||
{"\x1b[32mhello\x1b[0m", 5},
|
||||
{"\x1b[1;32mgreen\x1b[0m text", 10},
|
||||
{"", 0},
|
||||
{"\x1b[0m", 0},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.input, func(t *testing.T) {
|
||||
result := lenAnsi(tc.input)
|
||||
assert.Equal(t, tc.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
22
docs/sdk.md
22
docs/sdk.md
@@ -28,8 +28,8 @@ import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/flags"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/flags"
|
||||
"github.com/docker/compose/v5/pkg/api"
|
||||
"github.com/docker/compose/v5/pkg/compose"
|
||||
)
|
||||
@@ -37,15 +37,15 @@ import (
|
||||
func main() {
|
||||
ctx := context.Background()
|
||||
|
||||
dockerCLI, err := command.NewDockerCli()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create docker CLI: %v", err)
|
||||
}
|
||||
err = dockerCLI.Initialize(&flags.ClientOptions{})
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to initialize docker CLI: %v", err)
|
||||
}
|
||||
|
||||
dockerCLI, err := command.NewDockerCli()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create docker CLI: %v", err)
|
||||
}
|
||||
err = dockerCLI.Initialize(&flags.ClientOptions{})
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to initialize docker CLI: %v", err)
|
||||
}
|
||||
|
||||
// Create a new Compose service instance
|
||||
service, err := compose.NewComposeService(dockerCLI)
|
||||
if err != nil {
|
||||
|
||||
31
go.mod
31
go.mod
@@ -8,9 +8,9 @@ require (
|
||||
github.com/Microsoft/go-winio v0.6.2
|
||||
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
|
||||
github.com/buger/goterm v1.0.4
|
||||
github.com/compose-spec/compose-go/v2 v2.10.0
|
||||
github.com/compose-spec/compose-go/v2 v2.10.1
|
||||
github.com/containerd/console v1.0.5
|
||||
github.com/containerd/containerd/v2 v2.2.1-0.20251115011841-efd86f2b0bc2
|
||||
github.com/containerd/containerd/v2 v2.2.1
|
||||
github.com/containerd/errdefs v1.0.0
|
||||
github.com/containerd/platforms v1.0.0-rc.2
|
||||
github.com/distribution/reference v0.6.0
|
||||
@@ -22,7 +22,7 @@ require (
|
||||
github.com/docker/go-units v0.5.0
|
||||
github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203
|
||||
github.com/fsnotify/fsevents v0.2.0
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/hashicorp/go-version v1.8.0
|
||||
@@ -33,12 +33,11 @@ require (
|
||||
github.com/moby/go-archive v0.1.0
|
||||
github.com/moby/patternmatcher v0.6.0
|
||||
github.com/moby/sys/atomicwriter v0.1.0
|
||||
github.com/moby/term v0.5.2
|
||||
github.com/morikuni/aec v1.1.0
|
||||
github.com/opencontainers/go-digest v1.0.0
|
||||
github.com/opencontainers/image-spec v1.1.1
|
||||
github.com/otiai10/copy v1.14.1
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/sirupsen/logrus v1.9.4
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/spf13/pflag v1.0.10
|
||||
@@ -53,10 +52,10 @@ require (
|
||||
go.opentelemetry.io/otel/trace v1.38.0
|
||||
go.uber.org/goleak v1.3.0
|
||||
go.uber.org/mock v0.6.0
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.3
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.4
|
||||
golang.org/x/sync v0.19.0
|
||||
golang.org/x/sys v0.39.0
|
||||
google.golang.org/grpc v1.77.0
|
||||
golang.org/x/sys v0.41.0
|
||||
google.golang.org/grpc v1.78.0
|
||||
gotest.tools/v3 v3.5.2
|
||||
tags.cncf.io/container-device-interface v1.1.0
|
||||
)
|
||||
@@ -95,7 +94,7 @@ require (
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/klauspost/compress v1.18.1 // indirect
|
||||
github.com/klauspost/compress v1.18.2 // indirect
|
||||
github.com/magiconair/properties v1.8.9 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
@@ -112,6 +111,7 @@ require (
|
||||
github.com/moby/sys/symlink v0.3.0 // indirect
|
||||
github.com/moby/sys/user v0.4.0 // indirect
|
||||
github.com/moby/sys/userns v0.1.0 // indirect
|
||||
github.com/moby/term v0.5.2 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/otiai10/mint v1.6.3 // indirect
|
||||
github.com/pelletier/go-toml v1.9.5 // indirect
|
||||
@@ -149,18 +149,9 @@ require (
|
||||
golang.org/x/term v0.37.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
exclude (
|
||||
// FIXME(thaJeztah): remove this once kubernetes updated their dependencies to no longer need this.
|
||||
//
|
||||
// For additional details, see this PR and links mentioned in that PR:
|
||||
// https://github.com/kubernetes-sigs/kustomize/pull/5830#issuecomment-2569960859
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2
|
||||
)
|
||||
|
||||
52
go.sum
52
go.sum
@@ -46,16 +46,16 @@ github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004 h1:lkAMpLVBDaj17e
|
||||
github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA=
|
||||
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE=
|
||||
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4=
|
||||
github.com/compose-spec/compose-go/v2 v2.10.0 h1:K2C5LQ3KXvkYpy5N/SG6kIYB90iiAirA9btoTh/gB0Y=
|
||||
github.com/compose-spec/compose-go/v2 v2.10.0/go.mod h1:Ohac1SzhO/4fXXrzWIztIVB6ckmKBv1Nt5Z5mGVESUg=
|
||||
github.com/containerd/cgroups/v3 v3.1.0 h1:azxYVj+91ZgSnIBp2eI3k9y2iYQSR/ZQIgh9vKO+HSY=
|
||||
github.com/containerd/cgroups/v3 v3.1.0/go.mod h1:SA5DLYnXO8pTGYiAHXz94qvLQTKfVM5GEVisn4jpins=
|
||||
github.com/compose-spec/compose-go/v2 v2.10.1 h1:mFbXobojGRFIVi1UknrvaDAZ+PkJfyjqkA1yseh+vAU=
|
||||
github.com/compose-spec/compose-go/v2 v2.10.1/go.mod h1:Ohac1SzhO/4fXXrzWIztIVB6ckmKBv1Nt5Z5mGVESUg=
|
||||
github.com/containerd/cgroups/v3 v3.1.2 h1:OSosXMtkhI6Qove637tg1XgK4q+DhR0mX8Wi8EhrHa4=
|
||||
github.com/containerd/cgroups/v3 v3.1.2/go.mod h1:PKZ2AcWmSBsY/tJUVhtS/rluX0b1uq1GmPO1ElCmbOw=
|
||||
github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc=
|
||||
github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
|
||||
github.com/containerd/containerd/api v1.10.0 h1:5n0oHYVBwN4VhoX9fFykCV9dF1/BvAXeg2F8W6UYq1o=
|
||||
github.com/containerd/containerd/api v1.10.0/go.mod h1:NBm1OAk8ZL+LG8R0ceObGxT5hbUYj7CzTmR3xh0DlMM=
|
||||
github.com/containerd/containerd/v2 v2.2.1-0.20251115011841-efd86f2b0bc2 h1:WcvXNS/OmpiitTVdzRAudKwvShKxcOP4Elf2FyxSoTg=
|
||||
github.com/containerd/containerd/v2 v2.2.1-0.20251115011841-efd86f2b0bc2/go.mod h1:YCMjKjA4ZA7egdHNi3/93bJR1+2oniYlnS+c0N62HdE=
|
||||
github.com/containerd/containerd/v2 v2.2.1 h1:TpyxcY4AL5A+07dxETevunVS5zxqzuq7ZqJXknM11yk=
|
||||
github.com/containerd/containerd/v2 v2.2.1/go.mod h1:NR70yW1iDxe84F2iFWbR9xfAN0N2F0NcjTi1OVth4nU=
|
||||
github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4=
|
||||
github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE=
|
||||
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||
@@ -86,6 +86,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
|
||||
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||
github.com/cyphar/filepath-securejoin v0.5.1 h1:eYgfMq5yryL4fbWfkLpFFy2ukSELzaJOTaUTuh+oF48=
|
||||
github.com/cyphar/filepath-securejoin v0.5.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -141,8 +143,8 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre
|
||||
github.com/go-sql-driver/mysql v1.3.0 h1:pgwjLi/dvffoP9aabwkT3AKpXQM93QARkjFhDDqC1UE=
|
||||
github.com/go-sql-driver/mysql v1.3.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
|
||||
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
|
||||
github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
@@ -209,8 +211,8 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNU
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
|
||||
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
||||
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
@@ -301,8 +303,8 @@ github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJw
|
||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||
github.com/opencontainers/runtime-spec v1.3.0 h1:YZupQUdctfhpZy3TM39nN9Ika5CBWT5diQ8ibYCRkxg=
|
||||
github.com/opencontainers/runtime-spec v1.3.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
|
||||
github.com/opencontainers/selinux v1.12.0 h1:6n5JV4Cf+4y0KNXW48TLj5DwfXpvWlxXplUkdTrmPb8=
|
||||
github.com/opencontainers/selinux v1.12.0/go.mod h1:BTPX+bjVbWGXw7ZZWUbdENt8w0htPSrlgOOysQaU62U=
|
||||
github.com/opencontainers/selinux v1.13.1 h1:A8nNeceYngH9Ow++M+VVEwJVpdFmrlxsN22F+ISDCJE=
|
||||
github.com/opencontainers/selinux v1.13.1/go.mod h1:S10WXZ/osk2kWOYKy1x2f/eXF5ZHJoUs8UU/2caNRbg=
|
||||
github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU=
|
||||
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
||||
github.com/otiai10/copy v1.14.1 h1:5/7E6qsUMBaH5AnQ0sSLzzTg1oTECmcCmT6lvF45Na8=
|
||||
@@ -356,8 +358,8 @@ github.com/shibumi/go-pathspec v1.3.0/go.mod h1:Xutfslp817l2I1cZvgcfeMQJG5QnU2lh
|
||||
github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
||||
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
|
||||
github.com/spdx/tools-golang v0.5.5 h1:61c0KLfAcNqAjlg6UNMdkwpMernhw3zVRwDZ2x9XOmk=
|
||||
@@ -383,7 +385,6 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/theupdateframework/notary v0.7.0 h1:QyagRZ7wlSpjT5N2qQAh/pN+DVqgekv4DzbAiAiEL3c=
|
||||
@@ -447,8 +448,8 @@ go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.3 h1:3h1fjsh1CTAPjW7q/EMe+C8shx5d8ctzZTrLcs/j8Go=
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.3/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.4 h1:UP4+v6fFrBIb1l934bDl//mmnoIZEDK0idg1+AIvX5U=
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.4/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
@@ -500,12 +501,11 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
@@ -530,13 +530,13 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda h1:+2XxjfsAu6vqFxwGBRcHiMaDCuZiqXGDUDVWVtrFAnE=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda h1:i/Q+bfisr7gq6feoJnS/DlpdwEL4ihp41fvRiM3Ork0=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/grpc v1.0.5/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
||||
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
package desktop
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -34,9 +33,6 @@ func TestClientPing(t *testing.T) {
|
||||
t.Skip("Skipping - COMPOSE_TEST_DESKTOP_ENDPOINT not defined")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
|
||||
client := NewClient(desktopEndpoint)
|
||||
t.Cleanup(func() {
|
||||
_ = client.Close()
|
||||
@@ -44,7 +40,7 @@ func TestClientPing(t *testing.T) {
|
||||
|
||||
now := time.Now()
|
||||
|
||||
ret, err := client.Ping(ctx)
|
||||
ret, err := client.Ping(t.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
serverTime := time.Unix(0, ret.ServerTime)
|
||||
|
||||
@@ -35,6 +35,7 @@ import (
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/pkg/jsonmessage"
|
||||
"github.com/docker/go-connections/nat"
|
||||
"github.com/sirupsen/logrus"
|
||||
"go.yaml.in/yaml/v4"
|
||||
|
||||
"github.com/docker/compose/v5/pkg/api"
|
||||
@@ -85,7 +86,17 @@ func convert(ctx context.Context, dockerCli command.Cli, model map[string]any, o
|
||||
return err
|
||||
}
|
||||
|
||||
dir := os.TempDir()
|
||||
dir, err := os.MkdirTemp("", "compose-convert-*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
err := os.RemoveAll(dir)
|
||||
if err != nil {
|
||||
logrus.Warnf("failed to remove temp dir %s: %v", dir, err)
|
||||
}
|
||||
}()
|
||||
|
||||
composeYaml := filepath.Join(dir, "compose.yaml")
|
||||
err = os.WriteFile(composeYaml, raw, 0o600)
|
||||
if err != nil {
|
||||
|
||||
73
pkg/compose/api_versions.go
Normal file
73
pkg/compose/api_versions.go
Normal file
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package compose
|
||||
|
||||
// Docker Engine API version constants.
|
||||
// These versions correspond to specific Docker Engine releases and their features.
|
||||
const (
|
||||
// APIVersion144 represents Docker Engine API version 1.44 (Engine v25.0).
|
||||
//
|
||||
// New features in this version:
|
||||
// - Endpoint-specific MAC address configuration
|
||||
// - Multiple networks can be connected during container creation
|
||||
// - healthcheck.start_interval parameter support
|
||||
//
|
||||
// Before this version:
|
||||
// - MAC address was container-wide only
|
||||
// - Extra networks required post-creation NetworkConnect calls
|
||||
// - healthcheck.start_interval was not available
|
||||
APIVersion144 = "1.44"
|
||||
|
||||
// APIVersion148 represents Docker Engine API version 1.48 (Engine v28.0).
|
||||
//
|
||||
// New features in this version:
|
||||
// - Volume mounts with type=image support
|
||||
//
|
||||
// Before this version:
|
||||
// - Only bind, volume, and tmpfs mount types were supported
|
||||
APIVersion148 = "1.48"
|
||||
|
||||
// APIVersion149 represents Docker Engine API version 1.49 (Engine v28.1).
|
||||
//
|
||||
// New features in this version:
|
||||
// - Network interface_name configuration
|
||||
// - Platform parameter in ImageList API
|
||||
//
|
||||
// Before this version:
|
||||
// - interface_name was not configurable
|
||||
// - ImageList didn't support platform filtering
|
||||
APIVersion149 = "1.49"
|
||||
)
|
||||
|
||||
// Docker Engine version strings for user-facing error messages.
|
||||
// These should be used in error messages to provide clear version requirements.
|
||||
const (
|
||||
// DockerEngineV25 is the major version string for Docker Engine 25.x
|
||||
DockerEngineV25 = "v25"
|
||||
|
||||
// DockerEngineV28 is the major version string for Docker Engine 28.x
|
||||
DockerEngineV28 = "v28"
|
||||
|
||||
// DockerEngineV28_1 is the specific version string for Docker Engine 28.1
|
||||
DockerEngineV28_1 = "v28.1"
|
||||
)
|
||||
|
||||
// Build tool version constants
|
||||
const (
|
||||
// BuildxMinVersion is the minimum required version of buildx for compose build
|
||||
BuildxMinVersion = "0.17.0"
|
||||
)
|
||||
@@ -24,10 +24,9 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
containerType "github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/pkg/stdcopy"
|
||||
"github.com/moby/term"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/docker/compose/v5/pkg/api"
|
||||
"github.com/docker/compose/v5/pkg/utils"
|
||||
@@ -49,7 +48,10 @@ func (s *composeService) attach(ctx context.Context, project *types.Project, lis
|
||||
names = append(names, getContainerNameWithoutProject(c))
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintf(s.stdout(), "Attaching to %s\n", strings.Join(names, ", "))
|
||||
_, err = fmt.Fprintf(s.stdout(), "Attaching to %s\n", strings.Join(names, ", "))
|
||||
if err != nil {
|
||||
logrus.Debugf("failed to write attach message: %v", err)
|
||||
}
|
||||
|
||||
for _, ctr := range containers {
|
||||
err := s.attachContainer(ctx, ctr, listener)
|
||||
@@ -57,7 +59,7 @@ func (s *composeService) attach(ctx context.Context, project *types.Project, lis
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return containers, err
|
||||
return containers, nil
|
||||
}
|
||||
|
||||
func (s *composeService) attachContainer(ctx context.Context, container containerType.Summary, listener api.ContainerEventListener) error {
|
||||
@@ -91,78 +93,59 @@ func (s *composeService) doAttachContainer(ctx context.Context, service, id, nam
|
||||
})
|
||||
})
|
||||
|
||||
_, _, err = s.attachContainerStreams(ctx, id, inspect.Config.Tty, nil, wOut, wErr)
|
||||
return err
|
||||
err = s.attachContainerStreams(ctx, id, inspect.Config.Tty, wOut, wErr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *composeService) attachContainerStreams(ctx context.Context, container string, tty bool, stdin io.ReadCloser, stdout, stderr io.WriteCloser) (func(), chan bool, error) {
|
||||
detached := make(chan bool)
|
||||
restore := func() { /* noop */ }
|
||||
if stdin != nil {
|
||||
in := streams.NewIn(stdin)
|
||||
if in.IsTerminal() {
|
||||
state, err := term.SetRawTerminal(in.FD())
|
||||
if err != nil {
|
||||
return restore, detached, err
|
||||
}
|
||||
restore = func() {
|
||||
term.RestoreTerminal(in.FD(), state) //nolint:errcheck
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
streamIn, streamOut, err := s.getContainerStreams(ctx, container)
|
||||
func (s *composeService) attachContainerStreams(ctx context.Context, container string, tty bool, stdout, stderr io.WriteCloser) error {
|
||||
streamOut, err := s.getContainerStreams(ctx, container)
|
||||
if err != nil {
|
||||
return restore, detached, err
|
||||
}
|
||||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
if stdin != nil {
|
||||
stdin.Close() //nolint:errcheck
|
||||
}
|
||||
}()
|
||||
|
||||
if streamIn != nil && stdin != nil {
|
||||
go func() {
|
||||
_, err := io.Copy(streamIn, stdin)
|
||||
var escapeErr term.EscapeError
|
||||
if errors.As(err, &escapeErr) {
|
||||
close(detached)
|
||||
}
|
||||
}()
|
||||
return err
|
||||
}
|
||||
|
||||
if stdout != nil {
|
||||
go func() {
|
||||
defer stdout.Close() //nolint:errcheck
|
||||
defer stderr.Close() //nolint:errcheck
|
||||
defer streamOut.Close() //nolint:errcheck
|
||||
defer func() {
|
||||
if err := stdout.Close(); err != nil {
|
||||
logrus.Debugf("failed to close stdout: %v", err)
|
||||
}
|
||||
if err := stderr.Close(); err != nil {
|
||||
logrus.Debugf("failed to close stderr: %v", err)
|
||||
}
|
||||
if err := streamOut.Close(); err != nil {
|
||||
logrus.Debugf("failed to close stream output: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
var err error
|
||||
if tty {
|
||||
io.Copy(stdout, streamOut) //nolint:errcheck
|
||||
_, err = io.Copy(stdout, streamOut)
|
||||
} else {
|
||||
stdcopy.StdCopy(stdout, stderr, streamOut) //nolint:errcheck
|
||||
_, err = stdcopy.StdCopy(stdout, stderr, streamOut)
|
||||
}
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
logrus.Debugf("stream copy error for container %s: %v", container, err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
return restore, detached, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *composeService) getContainerStreams(ctx context.Context, container string) (io.WriteCloser, io.ReadCloser, error) {
|
||||
var stdout io.ReadCloser
|
||||
var stdin io.WriteCloser
|
||||
func (s *composeService) getContainerStreams(ctx context.Context, container string) (io.ReadCloser, error) {
|
||||
cnx, err := s.apiClient().ContainerAttach(ctx, container, containerType.AttachOptions{
|
||||
Stream: true,
|
||||
Stdin: true,
|
||||
Stdout: true,
|
||||
Stderr: true,
|
||||
Logs: false,
|
||||
DetachKeys: s.configFile().DetachKeys,
|
||||
Stream: true,
|
||||
Stdin: false,
|
||||
Stdout: true,
|
||||
Stderr: true,
|
||||
Logs: false,
|
||||
})
|
||||
if err == nil {
|
||||
stdout = ContainerStdout{HijackedResponse: cnx}
|
||||
stdin = ContainerStdin{HijackedResponse: cnx}
|
||||
return stdin, stdout, nil
|
||||
stdout := ContainerStdout{HijackedResponse: cnx}
|
||||
return stdout, nil
|
||||
}
|
||||
|
||||
// Fallback to logs API
|
||||
@@ -172,7 +155,7 @@ func (s *composeService) getContainerStreams(ctx context.Context, container stri
|
||||
Follow: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
return stdin, logs, nil
|
||||
return logs, nil
|
||||
}
|
||||
|
||||
@@ -40,7 +40,10 @@ func (s *composeService) Build(ctx context.Context, project *types.Project, opti
|
||||
return Run(ctx, func(ctx context.Context) error {
|
||||
return tracing.SpanWrapFunc("project/build", tracing.ProjectOptions(ctx, project),
|
||||
func(ctx context.Context) error {
|
||||
_, err := s.build(ctx, project, options, nil)
|
||||
builtImages, err := s.build(ctx, project, options, nil)
|
||||
if err == nil && len(builtImages) == 0 {
|
||||
logrus.Warn("No services to build")
|
||||
}
|
||||
return err
|
||||
})(ctx)
|
||||
}, "build", s.events)
|
||||
@@ -91,7 +94,6 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti
|
||||
}
|
||||
|
||||
if len(serviceToBuild) == 0 {
|
||||
logrus.Warn("No services to build")
|
||||
return imageIDs, nil
|
||||
}
|
||||
|
||||
@@ -156,11 +158,37 @@ func (s *composeService) ensureImagesExists(ctx context.Context, project *types.
|
||||
if ok {
|
||||
service.CustomLabels.Add(api.ImageDigestLabel, img.ID)
|
||||
}
|
||||
|
||||
resolveImageVolumes(&service, images, project.Name)
|
||||
|
||||
project.Services[name] = service
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func resolveImageVolumes(service *types.ServiceConfig, images map[string]api.ImageSummary, projectName string) {
|
||||
for i, vol := range service.Volumes {
|
||||
if vol.Type == types.VolumeTypeImage {
|
||||
imgName := vol.Source
|
||||
if _, ok := images[vol.Source]; !ok {
|
||||
// check if source is another service in the project
|
||||
imgName = api.GetImageNameOrDefault(types.ServiceConfig{Name: vol.Source}, projectName)
|
||||
// If we still can't find it, it might be an external image that wasn't pulled yet or doesn't exist
|
||||
if _, ok := images[imgName]; !ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if img, ok := images[imgName]; ok {
|
||||
// Use Image ID directly as source.
|
||||
// Using name@digest format (via reference.WithDigest) fails for local-only images
|
||||
// that don't have RepoDigests (e.g. built locally in CI).
|
||||
// Image ID (sha256:...) is always valid and ensures ServiceHash changes on rebuild.
|
||||
service.Volumes[i].Source = img.ID
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *composeService) getLocalImagesDigests(ctx context.Context, project *types.Project) (map[string]api.ImageSummary, error) {
|
||||
imageNames := utils.Set[string]{}
|
||||
for _, s := range project.Services {
|
||||
|
||||
@@ -424,8 +424,8 @@ func (s *composeService) getBuildxPlugin() (*manager.Plugin, error) {
|
||||
return nil, fmt.Errorf("failed to get version of buildx")
|
||||
}
|
||||
|
||||
if versions.LessThan(buildx.Version[1:], "0.17.0") {
|
||||
return nil, fmt.Errorf("compose build requires buildx 0.17 or later")
|
||||
if versions.LessThan(buildx.Version[1:], BuildxMinVersion) {
|
||||
return nil, fmt.Errorf("compose build requires buildx %s or later", BuildxMinVersion)
|
||||
}
|
||||
|
||||
return buildx, nil
|
||||
|
||||
@@ -478,7 +478,7 @@ var swarmEnabled = struct {
|
||||
err error
|
||||
}{}
|
||||
|
||||
func (s *composeService) isSWarmEnabled(ctx context.Context) (bool, error) {
|
||||
func (s *composeService) isSwarmEnabled(ctx context.Context) (bool, error) {
|
||||
swarmEnabled.once.Do(func() {
|
||||
info, err := s.apiClient().Info(ctx)
|
||||
if err != nil {
|
||||
|
||||
@@ -121,7 +121,7 @@ func (c *convergence) ensureService(ctx context.Context, project *types.Project,
|
||||
actual := len(containers)
|
||||
updated := make(Containers, expected)
|
||||
|
||||
eg, _ := errgroup.WithContext(ctx)
|
||||
eg, ctx := errgroup.WithContext(ctx)
|
||||
|
||||
err = c.resolveServiceReferences(&service)
|
||||
if err != nil {
|
||||
@@ -451,7 +451,7 @@ func (s *composeService) waitDependencies(ctx context.Context, project *types.Pr
|
||||
defer cancelFunc()
|
||||
ctx = withTimeout
|
||||
}
|
||||
eg, _ := errgroup.WithContext(ctx)
|
||||
eg, ctx := errgroup.WithContext(ctx)
|
||||
for dep, config := range dependencies {
|
||||
if shouldWait, err := shouldWaitForDependency(dep, config, project); err != nil {
|
||||
return err
|
||||
@@ -743,7 +743,7 @@ func (s *composeService) createMobyContainer(ctx context.Context, project *types
|
||||
}
|
||||
// Starting API version 1.44, the ContainerCreate API call takes multiple networks
|
||||
// so we include all the configurations there and can skip the one-by-one calls here
|
||||
if versions.LessThan(apiVersion, "1.44") {
|
||||
if versions.LessThan(apiVersion, APIVersion144) {
|
||||
// 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)
|
||||
@@ -774,11 +774,10 @@ func (s *composeService) getLinks(ctx context.Context, projectName string, servi
|
||||
}
|
||||
|
||||
for _, rawLink := range service.Links {
|
||||
linkSplit := strings.Split(rawLink, ":")
|
||||
linkServiceName := linkSplit[0]
|
||||
linkName := linkServiceName
|
||||
if len(linkSplit) == 2 {
|
||||
linkName = linkSplit[1] // linkName if informed like in: "serviceName:linkName"
|
||||
// linkName if informed like in: "serviceName[:linkName]"
|
||||
linkServiceName, linkName, ok := strings.Cut(rawLink, ":")
|
||||
if !ok {
|
||||
linkName = linkServiceName
|
||||
}
|
||||
cnts, err := getServiceContainers(linkServiceName)
|
||||
if err != nil {
|
||||
@@ -810,11 +809,9 @@ func (s *composeService) getLinks(ctx context.Context, projectName string, servi
|
||||
}
|
||||
|
||||
for _, rawExtLink := range service.ExternalLinks {
|
||||
extLinkSplit := strings.Split(rawExtLink, ":")
|
||||
externalLink := extLinkSplit[0]
|
||||
linkName := externalLink
|
||||
if len(extLinkSplit) == 2 {
|
||||
linkName = extLinkSplit[1]
|
||||
externalLink, linkName, ok := strings.Cut(rawExtLink, ":")
|
||||
if !ok {
|
||||
linkName = externalLink
|
||||
}
|
||||
links = append(links, format(externalLink, linkName))
|
||||
}
|
||||
@@ -833,7 +830,8 @@ func (s *composeService) isServiceHealthy(ctx context.Context, containers Contai
|
||||
return false, fmt.Errorf("container %s exited (%d)", name, ctr.State.ExitCode)
|
||||
}
|
||||
|
||||
if ctr.Config.Healthcheck == nil && fallbackRunning {
|
||||
noHealthcheck := ctr.Config.Healthcheck == nil || (len(ctr.Config.Healthcheck.Test) > 0 && ctr.Config.Healthcheck.Test[0] == "NONE")
|
||||
if noHealthcheck && fallbackRunning {
|
||||
// Container does not define a health check, but we can fall back to "running" state
|
||||
return ctr.State != nil && ctr.State.Status == container.StateRunning, nil
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
package compose
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -95,7 +94,7 @@ func TestServiceLinks(t *testing.T) {
|
||||
c := testContainer("db", dbContainerName, false)
|
||||
apiClient.EXPECT().ContainerList(gomock.Any(), containerListOptions).Return([]container.Summary{c}, nil)
|
||||
|
||||
links, err := tested.(*composeService).getLinks(context.Background(), testProject, s, 1)
|
||||
links, err := tested.(*composeService).getLinks(t.Context(), testProject, s, 1)
|
||||
assert.NilError(t, err)
|
||||
|
||||
assert.Equal(t, len(links), 3)
|
||||
@@ -118,7 +117,7 @@ func TestServiceLinks(t *testing.T) {
|
||||
c := testContainer("db", dbContainerName, false)
|
||||
|
||||
apiClient.EXPECT().ContainerList(gomock.Any(), containerListOptions).Return([]container.Summary{c}, nil)
|
||||
links, err := tested.(*composeService).getLinks(context.Background(), testProject, s, 1)
|
||||
links, err := tested.(*composeService).getLinks(t.Context(), testProject, s, 1)
|
||||
assert.NilError(t, err)
|
||||
|
||||
assert.Equal(t, len(links), 3)
|
||||
@@ -141,7 +140,7 @@ func TestServiceLinks(t *testing.T) {
|
||||
c := testContainer("db", dbContainerName, false)
|
||||
apiClient.EXPECT().ContainerList(gomock.Any(), containerListOptions).Return([]container.Summary{c}, nil)
|
||||
|
||||
links, err := tested.(*composeService).getLinks(context.Background(), testProject, s, 1)
|
||||
links, err := tested.(*composeService).getLinks(t.Context(), testProject, s, 1)
|
||||
assert.NilError(t, err)
|
||||
|
||||
assert.Equal(t, len(links), 3)
|
||||
@@ -165,7 +164,7 @@ func TestServiceLinks(t *testing.T) {
|
||||
c := testContainer("db", dbContainerName, false)
|
||||
apiClient.EXPECT().ContainerList(gomock.Any(), containerListOptions).Return([]container.Summary{c}, nil)
|
||||
|
||||
links, err := tested.(*composeService).getLinks(context.Background(), testProject, s, 1)
|
||||
links, err := tested.(*composeService).getLinks(t.Context(), testProject, s, 1)
|
||||
assert.NilError(t, err)
|
||||
|
||||
assert.Equal(t, len(links), 4)
|
||||
@@ -202,7 +201,7 @@ func TestServiceLinks(t *testing.T) {
|
||||
}
|
||||
apiClient.EXPECT().ContainerList(gomock.Any(), containerListOptionsOneOff).Return([]container.Summary{c}, nil)
|
||||
|
||||
links, err := tested.(*composeService).getLinks(context.Background(), testProject, s, 1)
|
||||
links, err := tested.(*composeService).getLinks(t.Context(), testProject, s, 1)
|
||||
assert.NilError(t, err)
|
||||
|
||||
assert.Equal(t, len(links), 3)
|
||||
@@ -233,7 +232,7 @@ func TestWaitDependencies(t *testing.T) {
|
||||
"db": {Condition: ServiceConditionRunningOrHealthy},
|
||||
"redis": {Condition: ServiceConditionRunningOrHealthy},
|
||||
}
|
||||
assert.NilError(t, tested.(*composeService).waitDependencies(context.Background(), &project, "", dependencies, nil, 0))
|
||||
assert.NilError(t, tested.(*composeService).waitDependencies(t.Context(), &project, "", dependencies, nil, 0))
|
||||
})
|
||||
t.Run("should skip dependencies with condition service_started", func(t *testing.T) {
|
||||
dbService := types.ServiceConfig{Name: "db", Scale: intPtr(1)}
|
||||
@@ -246,7 +245,149 @@ func TestWaitDependencies(t *testing.T) {
|
||||
"db": {Condition: types.ServiceConditionStarted, Required: true},
|
||||
"redis": {Condition: types.ServiceConditionStarted, Required: true},
|
||||
}
|
||||
assert.NilError(t, tested.(*composeService).waitDependencies(context.Background(), &project, "", dependencies, nil, 0))
|
||||
assert.NilError(t, tested.(*composeService).waitDependencies(t.Context(), &project, "", dependencies, nil, 0))
|
||||
})
|
||||
}
|
||||
|
||||
func TestIsServiceHealthy(t *testing.T) {
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
|
||||
apiClient := mocks.NewMockAPIClient(mockCtrl)
|
||||
cli := mocks.NewMockCli(mockCtrl)
|
||||
tested, err := NewComposeService(cli)
|
||||
assert.NilError(t, err)
|
||||
cli.EXPECT().Client().Return(apiClient).AnyTimes()
|
||||
|
||||
ctx := t.Context()
|
||||
|
||||
t.Run("disabled healthcheck with fallback to running", func(t *testing.T) {
|
||||
containerID := "test-container-id"
|
||||
containers := Containers{
|
||||
{ID: containerID},
|
||||
}
|
||||
|
||||
// Container with disabled healthcheck (Test: ["NONE"])
|
||||
apiClient.EXPECT().ContainerInspect(ctx, containerID).Return(container.InspectResponse{
|
||||
ContainerJSONBase: &container.ContainerJSONBase{
|
||||
ID: containerID,
|
||||
Name: "test-container",
|
||||
State: &container.State{Status: "running"},
|
||||
},
|
||||
Config: &container.Config{
|
||||
Healthcheck: &container.HealthConfig{
|
||||
Test: []string{"NONE"},
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
|
||||
isHealthy, err := tested.(*composeService).isServiceHealthy(ctx, containers, true)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, true, isHealthy, "Container with disabled healthcheck should be considered healthy when running with fallbackRunning=true")
|
||||
})
|
||||
|
||||
t.Run("disabled healthcheck without fallback", func(t *testing.T) {
|
||||
containerID := "test-container-id"
|
||||
containers := Containers{
|
||||
{ID: containerID},
|
||||
}
|
||||
|
||||
// Container with disabled healthcheck (Test: ["NONE"]) but fallbackRunning=false
|
||||
apiClient.EXPECT().ContainerInspect(ctx, containerID).Return(container.InspectResponse{
|
||||
ContainerJSONBase: &container.ContainerJSONBase{
|
||||
ID: containerID,
|
||||
Name: "test-container",
|
||||
State: &container.State{Status: "running"},
|
||||
},
|
||||
Config: &container.Config{
|
||||
Healthcheck: &container.HealthConfig{
|
||||
Test: []string{"NONE"},
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
|
||||
_, err := tested.(*composeService).isServiceHealthy(ctx, containers, false)
|
||||
assert.ErrorContains(t, err, "has no healthcheck configured")
|
||||
})
|
||||
|
||||
t.Run("no healthcheck with fallback to running", func(t *testing.T) {
|
||||
containerID := "test-container-id"
|
||||
containers := Containers{
|
||||
{ID: containerID},
|
||||
}
|
||||
|
||||
// Container with no healthcheck at all
|
||||
apiClient.EXPECT().ContainerInspect(ctx, containerID).Return(container.InspectResponse{
|
||||
ContainerJSONBase: &container.ContainerJSONBase{
|
||||
ID: containerID,
|
||||
Name: "test-container",
|
||||
State: &container.State{Status: "running"},
|
||||
},
|
||||
Config: &container.Config{
|
||||
Healthcheck: nil,
|
||||
},
|
||||
}, nil)
|
||||
|
||||
isHealthy, err := tested.(*composeService).isServiceHealthy(ctx, containers, true)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, true, isHealthy, "Container with no healthcheck should be considered healthy when running with fallbackRunning=true")
|
||||
})
|
||||
|
||||
t.Run("exited container with disabled healthcheck", func(t *testing.T) {
|
||||
containerID := "test-container-id"
|
||||
containers := Containers{
|
||||
{ID: containerID},
|
||||
}
|
||||
|
||||
// Container with disabled healthcheck but exited
|
||||
apiClient.EXPECT().ContainerInspect(ctx, containerID).Return(container.InspectResponse{
|
||||
ContainerJSONBase: &container.ContainerJSONBase{
|
||||
ID: containerID,
|
||||
Name: "test-container",
|
||||
State: &container.State{
|
||||
Status: "exited",
|
||||
ExitCode: 1,
|
||||
},
|
||||
},
|
||||
Config: &container.Config{
|
||||
Healthcheck: &container.HealthConfig{
|
||||
Test: []string{"NONE"},
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
|
||||
_, err := tested.(*composeService).isServiceHealthy(ctx, containers, true)
|
||||
assert.ErrorContains(t, err, "exited")
|
||||
})
|
||||
|
||||
t.Run("healthy container with healthcheck", func(t *testing.T) {
|
||||
containerID := "test-container-id"
|
||||
containers := Containers{
|
||||
{ID: containerID},
|
||||
}
|
||||
|
||||
// Container with actual healthcheck that is healthy
|
||||
apiClient.EXPECT().ContainerInspect(ctx, containerID).Return(container.InspectResponse{
|
||||
ContainerJSONBase: &container.ContainerJSONBase{
|
||||
ID: containerID,
|
||||
Name: "test-container",
|
||||
State: &container.State{
|
||||
Status: "running",
|
||||
Health: &container.Health{
|
||||
Status: container.Healthy,
|
||||
},
|
||||
},
|
||||
},
|
||||
Config: &container.Config{
|
||||
Healthcheck: &container.HealthConfig{
|
||||
Test: []string{"CMD", "curl", "-f", "http://localhost"},
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
|
||||
isHealthy, err := tested.(*composeService).isServiceHealthy(ctx, containers, false)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, true, isHealthy, "Container with healthy status should be healthy")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -333,7 +474,7 @@ func TestCreateMobyContainer(t *testing.T) {
|
||||
Aliases: []string{"bork-test-0"},
|
||||
}))
|
||||
|
||||
_, err = tested.(*composeService).createMobyContainer(context.Background(), &project, service, "test", 0, nil, createOptions{
|
||||
_, err = tested.(*composeService).createMobyContainer(t.Context(), &project, service, "test", 0, nil, createOptions{
|
||||
Labels: make(types.Labels),
|
||||
})
|
||||
assert.NilError(t, err)
|
||||
@@ -353,7 +494,7 @@ func TestCreateMobyContainer(t *testing.T) {
|
||||
// force `RuntimeVersion` to fetch fresh version
|
||||
runtimeVersion = runtimeVersionCache{}
|
||||
apiClient.EXPECT().ServerVersion(gomock.Any()).Return(moby.Version{
|
||||
APIVersion: "1.44",
|
||||
APIVersion: APIVersion144,
|
||||
}, nil).AnyTimes()
|
||||
|
||||
service := types.ServiceConfig{
|
||||
@@ -419,7 +560,7 @@ func TestCreateMobyContainer(t *testing.T) {
|
||||
NetworkSettings: &container.NetworkSettings{},
|
||||
}, nil)
|
||||
|
||||
_, err = tested.(*composeService).createMobyContainer(context.Background(), &project, service, "test", 0, nil, createOptions{
|
||||
_, err = tested.(*composeService).createMobyContainer(t.Context(), &project, service, "test", 0, nil, createOptions{
|
||||
Labels: make(types.Labels),
|
||||
})
|
||||
assert.NilError(t, err)
|
||||
|
||||
@@ -73,8 +73,8 @@ func (s *composeService) ToMobyHealthCheck(ctx context.Context, check *compose.H
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if versions.LessThan(version, "1.44") {
|
||||
return nil, errors.New("can't set healthcheck.start_interval as feature require Docker Engine v25 or later")
|
||||
if versions.LessThan(version, APIVersion144) {
|
||||
return nil, fmt.Errorf("can't set healthcheck.start_interval as feature require Docker Engine %s or later", DockerEngineV25)
|
||||
} else {
|
||||
startInterval = time.Duration(*check.StartInterval)
|
||||
}
|
||||
|
||||
@@ -317,15 +317,15 @@ func splitCpArg(arg string) (ctr, path string) {
|
||||
return "", arg
|
||||
}
|
||||
|
||||
parts := strings.SplitN(arg, ":", 2)
|
||||
ctr, path, ok := strings.Cut(arg, ":")
|
||||
|
||||
if len(parts) == 1 || strings.HasPrefix(parts[0], ".") {
|
||||
if !ok || strings.HasPrefix(ctr, ".") {
|
||||
// Either there's no `:` in the arg
|
||||
// OR it's an explicit local relative path like `./file:name.txt`.
|
||||
return "", arg
|
||||
}
|
||||
|
||||
return parts[0], parts[1]
|
||||
return ctr, path
|
||||
}
|
||||
|
||||
func resolveLocalPath(localPath string) (absPath string, err error) {
|
||||
|
||||
@@ -241,11 +241,8 @@ func (s *composeService) getCreateConfigs(ctx context.Context,
|
||||
} // VOLUMES/MOUNTS/FILESYSTEMS
|
||||
tmpfs := map[string]string{}
|
||||
for _, t := range service.Tmpfs {
|
||||
if arr := strings.SplitN(t, ":", 2); len(arr) > 1 {
|
||||
tmpfs[arr[0]] = arr[1]
|
||||
} else {
|
||||
tmpfs[arr[0]] = ""
|
||||
}
|
||||
k, v, _ := strings.Cut(t, ":")
|
||||
tmpfs[k] = v
|
||||
}
|
||||
binds, mounts, err := s.buildContainerVolumes(ctx, *p, service, inherit)
|
||||
if err != nil {
|
||||
@@ -360,7 +357,7 @@ func (s *composeService) prepareContainerMACAddress(ctx context.Context, service
|
||||
if macAddress != "" && mainNw != nil && mainNw.MacAddress != "" && mainNw.MacAddress != macAddress {
|
||||
return "", fmt.Errorf("the service-level mac_address should have the same value as network %s", nwName)
|
||||
}
|
||||
if versions.GreaterThanOrEqualTo(version, "1.44") {
|
||||
if versions.GreaterThanOrEqualTo(version, APIVersion144) {
|
||||
if mainNw != nil && mainNw.MacAddress == "" {
|
||||
mainNw.MacAddress = macAddress
|
||||
}
|
||||
@@ -374,7 +371,7 @@ func (s *composeService) prepareContainerMACAddress(ctx context.Context, service
|
||||
}
|
||||
|
||||
if len(withMacAddress) > 1 {
|
||||
return "", fmt.Errorf("a MAC address is specified for multiple networks (%s), but this feature requires Docker Engine v25 or later", strings.Join(withMacAddress, ", "))
|
||||
return "", fmt.Errorf("a MAC address is specified for multiple networks (%s), but this feature requires Docker Engine %s or later", strings.Join(withMacAddress, ", "), DockerEngineV25)
|
||||
}
|
||||
|
||||
if mainNw != nil && mainNw.MacAddress != "" {
|
||||
@@ -527,7 +524,7 @@ func defaultNetworkSettings(project *types.Project,
|
||||
// so we can pass all the extra networks we want the container to be connected to
|
||||
// in the network configuration instead of connecting the container to each extra
|
||||
// network individually after creation.
|
||||
if versions.GreaterThanOrEqualTo(version, "1.44") {
|
||||
if versions.GreaterThanOrEqualTo(version, APIVersion144) {
|
||||
if len(service.Networks) > 1 {
|
||||
serviceNetworks := service.NetworksByPriority()
|
||||
for _, networkKey := range serviceNetworks[1:] {
|
||||
@@ -541,10 +538,10 @@ func defaultNetworkSettings(project *types.Project,
|
||||
}
|
||||
}
|
||||
|
||||
if versions.LessThan(version, "1.49") {
|
||||
if versions.LessThan(version, APIVersion149) {
|
||||
for _, config := range service.Networks {
|
||||
if config != nil && config.InterfaceName != "" {
|
||||
return "", nil, fmt.Errorf("interface_name requires Docker Engine v28.1 or later")
|
||||
return "", nil, fmt.Errorf("interface_name requires Docker Engine %s or later", DockerEngineV28_1)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -563,13 +560,13 @@ func defaultNetworkSettings(project *types.Project,
|
||||
func getRestartPolicy(service types.ServiceConfig) container.RestartPolicy {
|
||||
var restart container.RestartPolicy
|
||||
if service.Restart != "" {
|
||||
split := strings.Split(service.Restart, ":")
|
||||
name, num, ok := strings.Cut(service.Restart, ":")
|
||||
var attempts int
|
||||
if len(split) > 1 {
|
||||
attempts, _ = strconv.Atoi(split[1])
|
||||
if ok {
|
||||
attempts, _ = strconv.Atoi(num)
|
||||
}
|
||||
restart = container.RestartPolicy{
|
||||
Name: mapRestartPolicyCondition(split[0]),
|
||||
Name: mapRestartPolicyCondition(name),
|
||||
MaximumRetryCount: attempts,
|
||||
}
|
||||
}
|
||||
@@ -861,8 +858,8 @@ func (s *composeService) buildContainerVolumes(
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if versions.LessThan(version, "1.48") {
|
||||
return nil, nil, fmt.Errorf("volume with type=image require Docker Engine v28 or later")
|
||||
if versions.LessThan(version, APIVersion148) {
|
||||
return nil, nil, fmt.Errorf("volume with type=image require Docker Engine %s or later", DockerEngineV28)
|
||||
}
|
||||
}
|
||||
mounts = append(mounts, m)
|
||||
@@ -1524,7 +1521,7 @@ func (s *composeService) resolveExternalNetwork(ctx context.Context, n *types.Ne
|
||||
case 1:
|
||||
return networks[0].ID, nil
|
||||
case 0:
|
||||
enabled, err := s.isSWarmEnabled(ctx)
|
||||
enabled, err := s.isSwarmEnabled(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
package compose
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
@@ -164,7 +163,7 @@ func TestBuildContainerMountOptions(t *testing.T) {
|
||||
}
|
||||
mock.EXPECT().ImageInspect(gomock.Any(), "myProject-myService").AnyTimes().Return(image.InspectResponse{}, nil)
|
||||
|
||||
mounts, err := s.buildContainerMountOptions(context.TODO(), project, project.Services["myService"], inherit)
|
||||
mounts, err := s.buildContainerMountOptions(t.Context(), project, project.Services["myService"], inherit)
|
||||
sort.Slice(mounts, func(i, j int) bool {
|
||||
return mounts[i].Target < mounts[j].Target
|
||||
})
|
||||
@@ -176,7 +175,7 @@ func TestBuildContainerMountOptions(t *testing.T) {
|
||||
assert.Equal(t, mounts[2].VolumeOptions.Subpath, "etc")
|
||||
assert.Equal(t, mounts[3].Target, "\\\\.\\pipe\\docker_engine")
|
||||
|
||||
mounts, err = s.buildContainerMountOptions(context.TODO(), project, project.Services["myService"], inherit)
|
||||
mounts, err = s.buildContainerMountOptions(t.Context(), project, project.Services["myService"], inherit)
|
||||
sort.Slice(mounts, func(i, j int) bool {
|
||||
return mounts[i].Target < mounts[j].Target
|
||||
})
|
||||
@@ -435,7 +434,7 @@ volumes:
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
p, err := composeloader.LoadWithContext(context.TODO(), composetypes.ConfigDetails{
|
||||
p, err := composeloader.LoadWithContext(t.Context(), composetypes.ConfigDetails{
|
||||
ConfigFiles: []composetypes.ConfigFile{
|
||||
{
|
||||
Filename: "test",
|
||||
@@ -448,7 +447,7 @@ volumes:
|
||||
})
|
||||
assert.NilError(t, err)
|
||||
s := &composeService{}
|
||||
binds, mounts, err := s.buildContainerVolumes(context.TODO(), *p, p.Services["test"], nil)
|
||||
binds, mounts, err := s.buildContainerVolumes(t.Context(), *p, p.Services["test"], nil)
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, tt.binds, binds)
|
||||
assert.DeepEqual(t, tt.mounts, mounts)
|
||||
|
||||
@@ -71,9 +71,6 @@ func TestTraversalWithMultipleParents(t *testing.T) {
|
||||
project.Services[name] = svc
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
|
||||
svc := make(chan string, 10)
|
||||
seen := make(map[string]int)
|
||||
done := make(chan struct{})
|
||||
@@ -84,7 +81,7 @@ func TestTraversalWithMultipleParents(t *testing.T) {
|
||||
done <- struct{}{}
|
||||
}()
|
||||
|
||||
err := InDependencyOrder(ctx, &project, func(ctx context.Context, service string) error {
|
||||
err := InDependencyOrder(t.Context(), &project, func(ctx context.Context, service string) error {
|
||||
svc <- service
|
||||
return nil
|
||||
})
|
||||
@@ -99,11 +96,8 @@ func TestTraversalWithMultipleParents(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestInDependencyUpCommandOrder(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
|
||||
var order []string
|
||||
err := InDependencyOrder(ctx, createTestProject(), func(ctx context.Context, service string) error {
|
||||
err := InDependencyOrder(t.Context(), createTestProject(), func(ctx context.Context, service string) error {
|
||||
order = append(order, service)
|
||||
return nil
|
||||
})
|
||||
@@ -112,11 +106,8 @@ func TestInDependencyUpCommandOrder(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestInDependencyReverseDownCommandOrder(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
|
||||
var order []string
|
||||
err := InReverseDependencyOrder(ctx, createTestProject(), func(ctx context.Context, service string) error {
|
||||
err := InReverseDependencyOrder(t.Context(), createTestProject(), func(ctx context.Context, service string) error {
|
||||
order = append(order, service)
|
||||
return nil
|
||||
})
|
||||
@@ -429,7 +420,7 @@ func TestWith_RootNodesAndUp(t *testing.T) {
|
||||
return nil
|
||||
})
|
||||
WithRootNodesAndDown(tt.nodes)(gt)
|
||||
err := gt.visit(context.TODO(), graph)
|
||||
err := gt.visit(t.Context(), graph)
|
||||
assert.NilError(t, err)
|
||||
sort.Strings(visited)
|
||||
assert.DeepEqual(t, tt.want, visited)
|
||||
|
||||
@@ -119,7 +119,7 @@ func (s *composeService) down(ctx context.Context, projectName string, options a
|
||||
logrus.Warnf("Warning: No resource found to remove for project %q.", projectName)
|
||||
}
|
||||
|
||||
eg, _ := errgroup.WithContext(ctx)
|
||||
eg, ctx := errgroup.WithContext(ctx)
|
||||
for _, op := range ops {
|
||||
eg.Go(op)
|
||||
}
|
||||
@@ -335,7 +335,7 @@ func (s *composeService) stopContainers(ctx context.Context, serv *types.Service
|
||||
}
|
||||
|
||||
func (s *composeService) removeContainers(ctx context.Context, containers []containerType.Summary, service *types.ServiceConfig, timeout *time.Duration, volumes bool) error {
|
||||
eg, _ := errgroup.WithContext(ctx)
|
||||
eg, ctx := errgroup.WithContext(ctx)
|
||||
for _, ctr := range containers {
|
||||
eg.Go(func() error {
|
||||
return s.stopAndRemoveContainer(ctx, ctr, service, timeout, volumes)
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
package compose
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
@@ -90,7 +89,7 @@ func TestDown(t *testing.T) {
|
||||
api.EXPECT().NetworkRemove(gomock.Any(), "abc123").Return(nil)
|
||||
api.EXPECT().NetworkRemove(gomock.Any(), "def456").Return(nil)
|
||||
|
||||
err = tested.Down(context.Background(), strings.ToLower(testProject), compose.DownOptions{})
|
||||
err = tested.Down(t.Context(), strings.ToLower(testProject), compose.DownOptions{})
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
|
||||
@@ -139,7 +138,7 @@ func TestDownWithGivenServices(t *testing.T) {
|
||||
api.EXPECT().NetworkInspect(gomock.Any(), "abc123", gomock.Any()).Return(network.Inspect{ID: "abc123"}, nil)
|
||||
api.EXPECT().NetworkRemove(gomock.Any(), "abc123").Return(nil)
|
||||
|
||||
err = tested.Down(context.Background(), strings.ToLower(testProject), compose.DownOptions{
|
||||
err = tested.Down(t.Context(), strings.ToLower(testProject), compose.DownOptions{
|
||||
Services: []string{"service1", "not-running-service"},
|
||||
})
|
||||
assert.NilError(t, err)
|
||||
@@ -175,7 +174,7 @@ func TestDownWithSpecifiedServiceButTheServicesAreNotRunning(t *testing.T) {
|
||||
{ID: "def456", Name: "myProject_default", Labels: map[string]string{compose.NetworkLabel: "default"}},
|
||||
}, nil)
|
||||
|
||||
err = tested.Down(context.Background(), strings.ToLower(testProject), compose.DownOptions{
|
||||
err = tested.Down(t.Context(), strings.ToLower(testProject), compose.DownOptions{
|
||||
Services: []string{"not-running-service1", "not-running-service2"},
|
||||
})
|
||||
assert.NilError(t, err)
|
||||
@@ -227,7 +226,7 @@ func TestDownRemoveOrphans(t *testing.T) {
|
||||
api.EXPECT().NetworkInspect(gomock.Any(), "abc123", gomock.Any()).Return(network.Inspect{ID: "abc123"}, nil)
|
||||
api.EXPECT().NetworkRemove(gomock.Any(), "abc123").Return(nil)
|
||||
|
||||
err = tested.Down(context.Background(), strings.ToLower(testProject), compose.DownOptions{RemoveOrphans: true})
|
||||
err = tested.Down(t.Context(), strings.ToLower(testProject), compose.DownOptions{RemoveOrphans: true})
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
|
||||
@@ -259,7 +258,7 @@ func TestDownRemoveVolumes(t *testing.T) {
|
||||
|
||||
api.EXPECT().VolumeRemove(gomock.Any(), "myProject_volume", true).Return(nil)
|
||||
|
||||
err = tested.Down(context.Background(), strings.ToLower(testProject), compose.DownOptions{Volumes: true})
|
||||
err = tested.Down(t.Context(), strings.ToLower(testProject), compose.DownOptions{Volumes: true})
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
|
||||
@@ -346,7 +345,7 @@ func TestDownRemoveImages(t *testing.T) {
|
||||
|
||||
t.Log("-> docker compose down --rmi=local")
|
||||
opts.Images = "local"
|
||||
err = tested.Down(context.Background(), strings.ToLower(testProject), opts)
|
||||
err = tested.Down(t.Context(), strings.ToLower(testProject), opts)
|
||||
assert.NilError(t, err)
|
||||
|
||||
otherImagesToBeRemoved := []string{
|
||||
@@ -361,7 +360,7 @@ func TestDownRemoveImages(t *testing.T) {
|
||||
|
||||
t.Log("-> docker compose down --rmi=all")
|
||||
opts.Images = "all"
|
||||
err = tested.Down(context.Background(), strings.ToLower(testProject), opts)
|
||||
err = tested.Down(t.Context(), strings.ToLower(testProject), opts)
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
|
||||
@@ -406,7 +405,7 @@ func TestDownRemoveImages_NoLabel(t *testing.T) {
|
||||
|
||||
api.EXPECT().ImageRemove(gomock.Any(), "testproject-service1:latest", image.RemoveOptions{}).Return(nil, nil)
|
||||
|
||||
err = tested.Down(context.Background(), strings.ToLower(testProject), compose.DownOptions{Images: "local"})
|
||||
err = tested.Down(t.Context(), strings.ToLower(testProject), compose.DownOptions{Images: "local"})
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ func (s *composeService) Images(ctx context.Context, projectName string, options
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
withPlatform := versions.GreaterThanOrEqualTo(version, "1.49")
|
||||
withPlatform := versions.GreaterThanOrEqualTo(version, APIVersion149)
|
||||
|
||||
summary := map[string]api.ImageSummary{}
|
||||
var mux sync.Mutex
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
package compose
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -40,7 +39,6 @@ func TestImages(t *testing.T) {
|
||||
tested, err := NewComposeService(cli)
|
||||
assert.NilError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
args := filters.NewArgs(projectFilter(strings.ToLower(testProject)))
|
||||
listOpts := container.ListOptions{All: true, Filters: args}
|
||||
api.EXPECT().ServerVersion(gomock.Any()).Return(types.Version{APIVersion: "1.96"}, nil).AnyTimes()
|
||||
@@ -56,9 +54,9 @@ func TestImages(t *testing.T) {
|
||||
c2 := containerDetail("service1", "456", "running", "bar:2")
|
||||
c2.Ports = []container.Port{{PublicPort: 80, PrivatePort: 90, IP: "localhost"}}
|
||||
c3 := containerDetail("service2", "789", "exited", "foo:1")
|
||||
api.EXPECT().ContainerList(ctx, listOpts).Return([]container.Summary{c1, c2, c3}, nil)
|
||||
api.EXPECT().ContainerList(t.Context(), listOpts).Return([]container.Summary{c1, c2, c3}, nil)
|
||||
|
||||
images, err := tested.Images(ctx, strings.ToLower(testProject), compose.ImagesOptions{})
|
||||
images, err := tested.Images(t.Context(), strings.ToLower(testProject), compose.ImagesOptions{})
|
||||
|
||||
expected := map[string]compose.ImageSummary{
|
||||
"123": {
|
||||
|
||||
@@ -45,8 +45,7 @@ func TestKillAll(t *testing.T) {
|
||||
|
||||
name := strings.ToLower(testProject)
|
||||
|
||||
ctx := context.Background()
|
||||
api.EXPECT().ContainerList(ctx, container.ListOptions{
|
||||
api.EXPECT().ContainerList(t.Context(), container.ListOptions{
|
||||
Filters: filters.NewArgs(projectFilter(name), hasConfigHashLabel()),
|
||||
}).Return(
|
||||
[]container.Summary{testContainer("service1", "123", false), testContainer("service1", "456", false), testContainer("service2", "789", false)}, nil)
|
||||
@@ -64,7 +63,7 @@ func TestKillAll(t *testing.T) {
|
||||
api.EXPECT().ContainerKill(anyCancellableContext(), "456", "").Return(nil)
|
||||
api.EXPECT().ContainerKill(anyCancellableContext(), "789", "").Return(nil)
|
||||
|
||||
err = tested.Kill(ctx, name, compose.KillOptions{})
|
||||
err = tested.Kill(t.Context(), name, compose.KillOptions{})
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
|
||||
@@ -82,8 +81,7 @@ func TestKillSignal(t *testing.T) {
|
||||
Filters: filters.NewArgs(projectFilter(name), serviceFilter(serviceName), hasConfigHashLabel()),
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
api.EXPECT().ContainerList(ctx, listOptions).Return([]container.Summary{testContainer(serviceName, "123", false)}, nil)
|
||||
api.EXPECT().ContainerList(t.Context(), listOptions).Return([]container.Summary{testContainer(serviceName, "123", false)}, nil)
|
||||
api.EXPECT().VolumeList(
|
||||
gomock.Any(),
|
||||
volume.ListOptions{
|
||||
@@ -96,7 +94,7 @@ func TestKillSignal(t *testing.T) {
|
||||
}, nil)
|
||||
api.EXPECT().ContainerKill(anyCancellableContext(), "123", "SIGTERM").Return(nil)
|
||||
|
||||
err = tested.Kill(ctx, name, compose.KillOptions{Services: []string{serviceName}, Signal: "SIGTERM"})
|
||||
err = tested.Kill(t.Context(), name, compose.KillOptions{Services: []string{serviceName}, Signal: "SIGTERM"})
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
|
||||
@@ -129,6 +127,7 @@ func containerLabels(service string, oneOff bool) map[string]string {
|
||||
}
|
||||
|
||||
func anyCancellableContext() gomock.Matcher {
|
||||
//nolint:forbidigo // This creates a context type for gomock matching, not for actual test usage
|
||||
ctxWithCancel, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
return gomock.AssignableToTypeOf(ctxWithCancel)
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
package compose
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
@@ -48,13 +47,11 @@ services:
|
||||
err := os.WriteFile(composeFile, []byte(composeContent), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create compose service
|
||||
service, err := NewComposeService(nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Load the project
|
||||
ctx := context.Background()
|
||||
project, err := service.LoadProject(ctx, api.ProjectLoadOptions{
|
||||
project, err := service.LoadProject(t.Context(), api.ProjectLoadOptions{
|
||||
ConfigPaths: []string{composeFile},
|
||||
})
|
||||
|
||||
@@ -87,19 +84,14 @@ services:
|
||||
require.NoError(t, err)
|
||||
|
||||
// Set environment variable
|
||||
require.NoError(t, os.Setenv("TEST_VAR", "resolved_value"))
|
||||
t.Cleanup(func() {
|
||||
require.NoError(t, os.Unsetenv("TEST_VAR"))
|
||||
})
|
||||
t.Setenv("TEST_VAR", "resolved_value")
|
||||
|
||||
service, err := NewComposeService(nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Test with environment resolution (default)
|
||||
t.Run("WithResolution", func(t *testing.T) {
|
||||
project, err := service.LoadProject(ctx, api.ProjectLoadOptions{
|
||||
project, err := service.LoadProject(t.Context(), api.ProjectLoadOptions{
|
||||
ConfigPaths: []string{composeFile},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
@@ -114,7 +106,7 @@ services:
|
||||
|
||||
// Test without environment resolution
|
||||
t.Run("WithoutResolution", func(t *testing.T) {
|
||||
project, err := service.LoadProject(ctx, api.ProjectLoadOptions{
|
||||
project, err := service.LoadProject(t.Context(), api.ProjectLoadOptions{
|
||||
ConfigPaths: []string{composeFile},
|
||||
ProjectOptionsFns: []cli.ProjectOptionsFn{cli.WithoutEnvironmentResolution},
|
||||
})
|
||||
@@ -145,10 +137,8 @@ services:
|
||||
service, err := NewComposeService(nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Load only specific services
|
||||
project, err := service.LoadProject(ctx, api.ProjectLoadOptions{
|
||||
project, err := service.LoadProject(t.Context(), api.ProjectLoadOptions{
|
||||
ConfigPaths: []string{composeFile},
|
||||
Services: []string{"web", "db"},
|
||||
})
|
||||
@@ -177,11 +167,9 @@ services:
|
||||
service, err := NewComposeService(nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Without debug profile
|
||||
t.Run("WithoutProfile", func(t *testing.T) {
|
||||
project, err := service.LoadProject(ctx, api.ProjectLoadOptions{
|
||||
project, err := service.LoadProject(t.Context(), api.ProjectLoadOptions{
|
||||
ConfigPaths: []string{composeFile},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
@@ -191,7 +179,7 @@ services:
|
||||
|
||||
// With debug profile
|
||||
t.Run("WithProfile", func(t *testing.T) {
|
||||
project, err := service.LoadProject(ctx, api.ProjectLoadOptions{
|
||||
project, err := service.LoadProject(t.Context(), api.ProjectLoadOptions{
|
||||
ConfigPaths: []string{composeFile},
|
||||
Profiles: []string{"debug"},
|
||||
})
|
||||
@@ -216,15 +204,13 @@ services:
|
||||
service, err := NewComposeService(nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Track events received
|
||||
var events []string
|
||||
listener := func(event string, metadata map[string]any) {
|
||||
events = append(events, event)
|
||||
}
|
||||
|
||||
project, err := service.LoadProject(ctx, api.ProjectLoadOptions{
|
||||
project, err := service.LoadProject(t.Context(), api.ProjectLoadOptions{
|
||||
ConfigPaths: []string{composeFile},
|
||||
LoadListeners: []api.LoadListener{listener},
|
||||
})
|
||||
@@ -251,11 +237,9 @@ services:
|
||||
service, err := NewComposeService(nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Without explicit project name
|
||||
t.Run("InferredName", func(t *testing.T) {
|
||||
project, err := service.LoadProject(ctx, api.ProjectLoadOptions{
|
||||
project, err := service.LoadProject(t.Context(), api.ProjectLoadOptions{
|
||||
ConfigPaths: []string{composeFile},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
@@ -265,7 +249,7 @@ services:
|
||||
|
||||
// With explicit project name
|
||||
t.Run("ExplicitName", func(t *testing.T) {
|
||||
project, err := service.LoadProject(ctx, api.ProjectLoadOptions{
|
||||
project, err := service.LoadProject(t.Context(), api.ProjectLoadOptions{
|
||||
ConfigPaths: []string{composeFile},
|
||||
ProjectName: "my-custom-project",
|
||||
})
|
||||
@@ -288,10 +272,8 @@ services:
|
||||
service, err := NewComposeService(nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// With compatibility mode
|
||||
project, err := service.LoadProject(ctx, api.ProjectLoadOptions{
|
||||
project, err := service.LoadProject(t.Context(), api.ProjectLoadOptions{
|
||||
ConfigPaths: []string{composeFile},
|
||||
Compatibility: true,
|
||||
})
|
||||
@@ -317,10 +299,8 @@ this is not valid yaml: [[[
|
||||
service, err := NewComposeService(nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Should return an error for invalid YAML
|
||||
project, err := service.LoadProject(ctx, api.ProjectLoadOptions{
|
||||
project, err := service.LoadProject(t.Context(), api.ProjectLoadOptions{
|
||||
ConfigPaths: []string{composeFile},
|
||||
})
|
||||
|
||||
@@ -332,10 +312,8 @@ func TestLoadProject_MissingComposeFile(t *testing.T) {
|
||||
service, err := NewComposeService(nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Should return an error for missing file
|
||||
project, err := service.LoadProject(ctx, api.ProjectLoadOptions{
|
||||
project, err := service.LoadProject(t.Context(), api.ProjectLoadOptions{
|
||||
ConfigPaths: []string{"/nonexistent/compose.yaml"},
|
||||
})
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
package compose
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -44,8 +43,7 @@ func TestComposeService_Logs_Demux(t *testing.T) {
|
||||
|
||||
name := strings.ToLower(testProject)
|
||||
|
||||
ctx := context.Background()
|
||||
api.EXPECT().ContainerList(ctx, containerType.ListOptions{
|
||||
api.EXPECT().ContainerList(t.Context(), containerType.ListOptions{
|
||||
All: true,
|
||||
Filters: filters.NewArgs(oneOffFilter(false), projectFilter(name), hasConfigHashLabel()),
|
||||
}).Return(
|
||||
@@ -87,7 +85,7 @@ func TestComposeService_Logs_Demux(t *testing.T) {
|
||||
}
|
||||
|
||||
consumer := &testLogConsumer{}
|
||||
err = tested.Logs(ctx, name, consumer, opts)
|
||||
err = tested.Logs(t.Context(), name, consumer, opts)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(
|
||||
@@ -114,8 +112,7 @@ func TestComposeService_Logs_ServiceFiltering(t *testing.T) {
|
||||
|
||||
name := strings.ToLower(testProject)
|
||||
|
||||
ctx := context.Background()
|
||||
api.EXPECT().ContainerList(ctx, containerType.ListOptions{
|
||||
api.EXPECT().ContainerList(t.Context(), containerType.ListOptions{
|
||||
All: true,
|
||||
Filters: filters.NewArgs(oneOffFilter(false), projectFilter(name), hasConfigHashLabel()),
|
||||
}).Return(
|
||||
@@ -157,7 +154,7 @@ func TestComposeService_Logs_ServiceFiltering(t *testing.T) {
|
||||
opts := compose.LogOptions{
|
||||
Project: proj,
|
||||
}
|
||||
err = tested.Logs(ctx, name, consumer, opts)
|
||||
err = tested.Logs(t.Context(), name, consumer, opts)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, []string{"hello c1"}, consumer.LogsForContainer("c1"))
|
||||
|
||||
@@ -29,7 +29,7 @@ import (
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/containerd/errdefs"
|
||||
"github.com/docker/cli/cli-plugins/manager"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/docker/docker/api/types/versions"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
@@ -159,21 +159,22 @@ func (m *modelAPI) PullModel(ctx context.Context, model types.ModelConfig, quiet
|
||||
}
|
||||
|
||||
func (m *modelAPI) ConfigureModel(ctx context.Context, config types.ModelConfig, events api.EventProcessor) error {
|
||||
if len(config.RuntimeFlags) != 0 {
|
||||
logrus.Warnf("Runtime flags are not supported and will be ignored for model %s", config.Model)
|
||||
config.RuntimeFlags = nil
|
||||
}
|
||||
events.On(api.Resource{
|
||||
ID: config.Name,
|
||||
Status: api.Working,
|
||||
Text: api.StatusConfiguring,
|
||||
})
|
||||
// configure [--context-size=<n>] MODEL
|
||||
// configure [--context-size=<n>] MODEL [-- <runtime-flags...>]
|
||||
args := []string{"configure"}
|
||||
if config.ContextSize > 0 {
|
||||
args = append(args, "--context-size", strconv.Itoa(config.ContextSize))
|
||||
}
|
||||
args = append(args, config.Model)
|
||||
// Only append RuntimeFlags if docker model CLI version is >= v1.0.6
|
||||
if len(config.RuntimeFlags) != 0 && m.supportsRuntimeFlags() {
|
||||
args = append(args, "--")
|
||||
args = append(args, config.RuntimeFlags...)
|
||||
}
|
||||
cmd := exec.CommandContext(ctx, m.path, args...)
|
||||
err := m.prepare(ctx, cmd)
|
||||
if err != nil {
|
||||
@@ -278,3 +279,16 @@ func (m *modelAPI) ListModels(ctx context.Context) ([]string, error) {
|
||||
}
|
||||
return availableModels, nil
|
||||
}
|
||||
|
||||
// supportsRuntimeFlags checks if the docker model version supports runtime flags
|
||||
// Runtime flags are supported in version >= v1.0.6
|
||||
func (m *modelAPI) supportsRuntimeFlags() bool {
|
||||
// If version is not cached, don't append runtime flags to be safe
|
||||
if m.version == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// Strip 'v' prefix if present (e.g., "v1.0.6" -> "1.0.6")
|
||||
versionStr := strings.TrimPrefix(m.version, "v")
|
||||
return !versions.LessThan(versionStr, "1.0.6")
|
||||
}
|
||||
|
||||
@@ -147,7 +147,7 @@ func (c *monitor) Start(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if inspect.State != nil && inspect.State.Restarting || inspect.State.Running {
|
||||
if inspect.State != nil && (inspect.State.Restarting || inspect.State.Running) {
|
||||
// State.Restarting is set by engine when container is configured to restart on exit
|
||||
// on ContainerRestart it doesn't (see https://github.com/moby/moby/issues/45538)
|
||||
// container state still is reported as "running"
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
package compose
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -37,7 +36,6 @@ func TestPs(t *testing.T) {
|
||||
tested, err := NewComposeService(cli)
|
||||
assert.NilError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
args := filters.NewArgs(projectFilter(strings.ToLower(testProject)), hasConfigHashLabel())
|
||||
args.Add("label", "com.docker.compose.oneoff=False")
|
||||
listOpts := containerType.ListOptions{Filters: args, All: false}
|
||||
@@ -45,12 +43,12 @@ func TestPs(t *testing.T) {
|
||||
c2, inspect2 := containerDetails("service1", "456", containerType.StateRunning, "", 0)
|
||||
c2.Ports = []containerType.Port{{PublicPort: 80, PrivatePort: 90, IP: "localhost"}}
|
||||
c3, inspect3 := containerDetails("service2", "789", containerType.StateExited, "", 130)
|
||||
api.EXPECT().ContainerList(ctx, listOpts).Return([]containerType.Summary{c1, c2, c3}, nil)
|
||||
api.EXPECT().ContainerList(t.Context(), listOpts).Return([]containerType.Summary{c1, c2, c3}, nil)
|
||||
api.EXPECT().ContainerInspect(anyCancellableContext(), "123").Return(inspect1, nil)
|
||||
api.EXPECT().ContainerInspect(anyCancellableContext(), "456").Return(inspect2, nil)
|
||||
api.EXPECT().ContainerInspect(anyCancellableContext(), "789").Return(inspect3, nil)
|
||||
|
||||
containers, err := tested.Ps(ctx, strings.ToLower(testProject), compose.PsOptions{})
|
||||
containers, err := tested.Ps(t.Context(), strings.ToLower(testProject), compose.PsOptions{})
|
||||
|
||||
expected := []compose.ContainerSummary{
|
||||
{
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
package compose
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
@@ -32,7 +31,7 @@ import (
|
||||
)
|
||||
|
||||
func Test_createLayers(t *testing.T) {
|
||||
project, err := loader.LoadWithContext(context.TODO(), types.ConfigDetails{
|
||||
project, err := loader.LoadWithContext(t.Context(), types.ConfigDetails{
|
||||
WorkingDir: "testdata/publish/",
|
||||
Environment: types.Mapping{},
|
||||
ConfigFiles: []types.ConfigFile{
|
||||
@@ -45,7 +44,7 @@ func Test_createLayers(t *testing.T) {
|
||||
project.ComposeFiles = []string{"testdata/publish/compose.yaml"}
|
||||
|
||||
service := &composeService{}
|
||||
layers, err := service.createLayers(context.TODO(), project, api.PublishOptions{
|
||||
layers, err := service.createLayers(t.Context(), project, api.PublishOptions{
|
||||
WithEnvironment: true,
|
||||
})
|
||||
assert.NilError(t, err)
|
||||
|
||||
@@ -29,8 +29,8 @@ import (
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/buildx/driver"
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
clitypes "github.com/docker/cli/cli/config/types"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/docker/pkg/jsonmessage"
|
||||
@@ -260,7 +260,11 @@ func ImageDigestResolver(ctx context.Context, file *configfile.ConfigFile, apiCl
|
||||
}
|
||||
}
|
||||
|
||||
func encodedAuth(ref reference.Named, configFile driver.Auth) (string, error) {
|
||||
type authProvider interface {
|
||||
GetAuthConfig(registryHostname string) (clitypes.AuthConfig, error)
|
||||
}
|
||||
|
||||
func encodedAuth(ref reference.Named, configFile authProvider) (string, error) {
|
||||
authConfig, err := configFile.GetAuthConfig(registry.GetAuthConfigKey(reference.Domain(ref)))
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
package compose
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -41,7 +40,6 @@ func TestStopTimeout(t *testing.T) {
|
||||
tested, err := NewComposeService(cli)
|
||||
assert.NilError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
api.EXPECT().ContainerList(gomock.Any(), projectFilterListOpt(false)).Return(
|
||||
[]container.Summary{
|
||||
testContainer("service1", "123", false),
|
||||
@@ -63,7 +61,7 @@ func TestStopTimeout(t *testing.T) {
|
||||
api.EXPECT().ContainerStop(gomock.Any(), "456", stopConfig).Return(nil)
|
||||
api.EXPECT().ContainerStop(gomock.Any(), "789", stopConfig).Return(nil)
|
||||
|
||||
err = tested.Stop(ctx, strings.ToLower(testProject), compose.StopOptions{
|
||||
err = tested.Stop(t.Context(), strings.ToLower(testProject), compose.StopOptions{
|
||||
Timeout: &timeout,
|
||||
})
|
||||
assert.NilError(t, err)
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
package compose
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
@@ -119,10 +118,8 @@ func TestViz(t *testing.T) {
|
||||
tested, err := NewComposeService(cli)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("viz (no ports, networks or image)", func(t *testing.T) {
|
||||
graphStr, err := tested.Viz(ctx, &project, compose.VizOptions{
|
||||
graphStr, err := tested.Viz(t.Context(), &project, compose.VizOptions{
|
||||
Indentation: " ",
|
||||
IncludePorts: false,
|
||||
IncludeImageName: false,
|
||||
@@ -181,7 +178,7 @@ func TestViz(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("viz (with ports, networks and image)", func(t *testing.T) {
|
||||
graphStr, err := tested.Viz(ctx, &project, compose.VizOptions{
|
||||
graphStr, err := tested.Viz(t.Context(), &project, compose.VizOptions{
|
||||
Indentation: "\t",
|
||||
IncludePorts: true,
|
||||
IncludeImageName: true,
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
package compose
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
@@ -58,7 +57,6 @@ func TestVolumes(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
args := filters.NewArgs(projectFilter(testProject))
|
||||
listOpts := container.ListOptions{Filters: args}
|
||||
volumeListArgs := filters.NewArgs(projectFilter(testProject))
|
||||
@@ -68,20 +66,19 @@ func TestVolumes(t *testing.T) {
|
||||
}
|
||||
containerReturn := []container.Summary{c1, c2}
|
||||
|
||||
// Mock API calls
|
||||
mockApi.EXPECT().ContainerList(ctx, listOpts).Times(2).Return(containerReturn, nil)
|
||||
mockApi.EXPECT().VolumeList(ctx, volumeListOpts).Times(2).Return(volumeReturn, nil)
|
||||
mockApi.EXPECT().ContainerList(t.Context(), listOpts).Times(2).Return(containerReturn, nil)
|
||||
mockApi.EXPECT().VolumeList(t.Context(), volumeListOpts).Times(2).Return(volumeReturn, nil)
|
||||
|
||||
// Test without service filter - should return all project volumes
|
||||
volumeOptions := api.VolumesOptions{}
|
||||
volumes, err := tested.Volumes(ctx, testProject, volumeOptions)
|
||||
volumes, err := tested.Volumes(t.Context(), testProject, volumeOptions)
|
||||
expected := []api.VolumesSummary{vol1, vol2, vol3}
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, volumes, expected)
|
||||
|
||||
// Test with service filter - should only return volumes used by service1
|
||||
volumeOptions = api.VolumesOptions{Services: []string{"service1"}}
|
||||
volumes, err = tested.Volumes(ctx, testProject, volumeOptions)
|
||||
volumes, err = tested.Volumes(t.Context(), testProject, volumeOptions)
|
||||
expected = []api.VolumesSummary{vol1, vol2}
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, volumes, expected)
|
||||
|
||||
@@ -355,6 +355,8 @@ func (s *composeService) watchEvents(ctx context.Context, project *types.Project
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
options.LogTo.Log(api.WatchLogger, "Watch disabled")
|
||||
// Ensure watcher is closed to release resources
|
||||
_ = watcher.Close()
|
||||
return nil
|
||||
case err, open := <-watcher.Errors():
|
||||
if err != nil {
|
||||
@@ -363,13 +365,28 @@ func (s *composeService) watchEvents(ctx context.Context, project *types.Project
|
||||
if open {
|
||||
continue
|
||||
}
|
||||
_ = watcher.Close()
|
||||
return err
|
||||
case batch := <-batchEvents:
|
||||
case batch, ok := <-batchEvents:
|
||||
if !ok {
|
||||
options.LogTo.Log(api.WatchLogger, "Watch disabled")
|
||||
_ = watcher.Close()
|
||||
return nil
|
||||
}
|
||||
if len(batch) > 1000 {
|
||||
logrus.Warnf("Very large batch of file changes detected: %d files. This may impact performance.", len(batch))
|
||||
options.LogTo.Log(api.WatchLogger, "Large batch of file changes detected. If you just switched branches, this is expected.")
|
||||
}
|
||||
start := time.Now()
|
||||
logrus.Debugf("batch start: count[%d]", len(batch))
|
||||
err := s.handleWatchBatch(ctx, project, options, batch, rules, syncer)
|
||||
if err != nil {
|
||||
logrus.Warnf("Error handling changed files: %v", err)
|
||||
// If context was canceled, exit immediately
|
||||
if ctx.Err() != nil {
|
||||
_ = watcher.Close()
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
logrus.Debugf("batch complete: duration[%s] count[%d]", time.Since(start), len(batch))
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ func TestWatch_Sync(t *testing.T) {
|
||||
//
|
||||
cli.EXPECT().Client().Return(apiClient).AnyTimes()
|
||||
|
||||
ctx, cancelFunc := context.WithCancel(context.Background())
|
||||
ctx, cancelFunc := context.WithCancel(t.Context())
|
||||
t.Cleanup(cancelFunc)
|
||||
|
||||
proj := types.Project{
|
||||
|
||||
@@ -39,7 +39,7 @@ func TestComposeCancel(t *testing.T) {
|
||||
t.Run("metrics on cancel Compose build", func(t *testing.T) {
|
||||
const buildProjectPath = "fixtures/build-infinite/compose.yaml"
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
defer cancel()
|
||||
|
||||
// require a separate groupID from the process running tests, in order to simulate ctrl+C from a terminal.
|
||||
|
||||
21
pkg/e2e/fixtures/image-volume-recreate/Dockerfile
Normal file
21
pkg/e2e/fixtures/image-volume-recreate/Dockerfile
Normal file
@@ -0,0 +1,21 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
#
|
||||
# Copyright 2020 Docker Compose CLI authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
FROM alpine
|
||||
WORKDIR /app
|
||||
ARG CONTENT=initial
|
||||
RUN echo "$CONTENT" > /app/content.txt
|
||||
18
pkg/e2e/fixtures/image-volume-recreate/compose.yaml
Normal file
18
pkg/e2e/fixtures/image-volume-recreate/compose.yaml
Normal file
@@ -0,0 +1,18 @@
|
||||
services:
|
||||
source:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
image: image-volume-source
|
||||
|
||||
consumer:
|
||||
image: alpine
|
||||
depends_on:
|
||||
- source
|
||||
command: ["cat", "/data/content.txt"]
|
||||
volumes:
|
||||
- type: image
|
||||
source: image-volume-source
|
||||
target: /data
|
||||
image:
|
||||
subpath: app
|
||||
@@ -212,11 +212,9 @@ func TestNetworkRecreate(t *testing.T) {
|
||||
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/network-recreate/compose.yaml", "--project-name", projectName, "--progress=plain", "up", "-d")
|
||||
err := res.Stderr()
|
||||
fmt.Println(err)
|
||||
res.Assert(t, icmd.Expected{Err: `
|
||||
Container network_recreate-web-1 Stopped
|
||||
Network network_recreate_test Removed
|
||||
Network network_recreate_test Creating
|
||||
Network network_recreate_test Created
|
||||
Container network_recreate-web-1 Starting
|
||||
Container network_recreate-web-1 Started`})
|
||||
hasStopped := strings.Contains(err, "Stopped")
|
||||
hasResumed := strings.Contains(err, "Started") || strings.Contains(err, "Recreated")
|
||||
if !hasStopped || !hasResumed {
|
||||
t.Fatalf("unexpected output, missing expected events, stderr: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ func TestUpDependenciesNotStopped(t *testing.T) {
|
||||
"app",
|
||||
)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 15*time.Second)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
cmd, err := StartWithNewGroupID(ctx, testCmd, upOut, nil)
|
||||
|
||||
@@ -19,7 +19,6 @@ package e2e
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
@@ -104,9 +103,7 @@ func TestProjectVolumeBind(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("Running on Windows. Skipping...")
|
||||
}
|
||||
tmpDir, err := os.MkdirTemp("", projectName)
|
||||
assert.NilError(t, err)
|
||||
defer os.RemoveAll(tmpDir) //nolint
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
|
||||
|
||||
@@ -193,3 +190,47 @@ func TestImageVolume(t *testing.T) {
|
||||
out := res.Combined()
|
||||
assert.Check(t, strings.Contains(out, "index.html"))
|
||||
}
|
||||
|
||||
func TestImageVolumeRecreateOnRebuild(t *testing.T) {
|
||||
c := NewCLI(t)
|
||||
const projectName = "compose-e2e-image-volume-recreate"
|
||||
t.Cleanup(func() {
|
||||
c.cleanupWithDown(t, projectName)
|
||||
c.RunDockerOrExitError(t, "rmi", "-f", "image-volume-source")
|
||||
})
|
||||
|
||||
version := c.RunDockerCmd(t, "version", "-f", "{{.Server.Version}}")
|
||||
major, _, found := strings.Cut(version.Combined(), ".")
|
||||
assert.Assert(t, found)
|
||||
if major == "26" || major == "27" {
|
||||
t.Skip("Skipping test due to docker version < 28")
|
||||
}
|
||||
|
||||
// First build and run with initial content
|
||||
c.RunDockerComposeCmd(t, "-f", "./fixtures/image-volume-recreate/compose.yaml",
|
||||
"--project-name", projectName, "build", "--build-arg", "CONTENT=foo")
|
||||
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/image-volume-recreate/compose.yaml",
|
||||
"--project-name", projectName, "up", "-d")
|
||||
assert.Check(t, !strings.Contains(res.Combined(), "error"))
|
||||
|
||||
// Check initial content
|
||||
res = c.RunDockerComposeCmd(t, "-f", "./fixtures/image-volume-recreate/compose.yaml",
|
||||
"--project-name", projectName, "logs", "consumer")
|
||||
assert.Check(t, strings.Contains(res.Combined(), "foo"), "Expected 'foo' in output, got: %s", res.Combined())
|
||||
|
||||
// Rebuild source image with different content
|
||||
c.RunDockerComposeCmd(t, "-f", "./fixtures/image-volume-recreate/compose.yaml",
|
||||
"--project-name", projectName, "build", "--build-arg", "CONTENT=bar")
|
||||
|
||||
// Run up again - consumer should be recreated because source image changed
|
||||
res = c.RunDockerComposeCmd(t, "-f", "./fixtures/image-volume-recreate/compose.yaml",
|
||||
"--project-name", projectName, "up", "-d")
|
||||
// The consumer container should be recreated
|
||||
assert.Check(t, strings.Contains(res.Combined(), "Recreate") || strings.Contains(res.Combined(), "Created"),
|
||||
"Expected container to be recreated, got: %s", res.Combined())
|
||||
|
||||
// Check updated content
|
||||
res = c.RunDockerComposeCmd(t, "-f", "./fixtures/image-volume-recreate/compose.yaml",
|
||||
"--project-name", projectName, "logs", "consumer")
|
||||
assert.Check(t, strings.Contains(res.Combined(), "bar"), "Expected 'bar' in output after rebuild, got: %s", res.Combined())
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ import (
|
||||
func Test_BatchDebounceEvents(t *testing.T) {
|
||||
ch := make(chan FileEvent)
|
||||
clock := clockwork.NewFakeClock()
|
||||
ctx, stop := context.WithCancel(context.Background())
|
||||
ctx, stop := context.WithCancel(t.Context())
|
||||
t.Cleanup(stop)
|
||||
|
||||
eventBatchCh := BatchDebounceEvents(ctx, clock, ch)
|
||||
|
||||
@@ -35,20 +35,20 @@ import (
|
||||
// behavior.
|
||||
|
||||
func TestWindowsBufferSize(t *testing.T) {
|
||||
orig := os.Getenv(WindowsBufferSizeEnvVar)
|
||||
defer os.Setenv(WindowsBufferSizeEnvVar, orig) //nolint:errcheck
|
||||
t.Run("empty value", func(t *testing.T) {
|
||||
t.Setenv(WindowsBufferSizeEnvVar, "")
|
||||
assert.Equal(t, defaultBufferSize, DesiredWindowsBufferSize())
|
||||
})
|
||||
|
||||
err := os.Setenv(WindowsBufferSizeEnvVar, "")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, defaultBufferSize, DesiredWindowsBufferSize())
|
||||
t.Run("invalid value", func(t *testing.T) {
|
||||
t.Setenv(WindowsBufferSizeEnvVar, "a")
|
||||
assert.Equal(t, defaultBufferSize, DesiredWindowsBufferSize())
|
||||
})
|
||||
|
||||
err = os.Setenv(WindowsBufferSizeEnvVar, "a")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, defaultBufferSize, DesiredWindowsBufferSize())
|
||||
|
||||
err = os.Setenv(WindowsBufferSizeEnvVar, "10")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 10, DesiredWindowsBufferSize())
|
||||
t.Run("valid value", func(t *testing.T) {
|
||||
t.Setenv(WindowsBufferSizeEnvVar, "10")
|
||||
assert.Equal(t, 10, DesiredWindowsBufferSize())
|
||||
})
|
||||
}
|
||||
|
||||
func TestNoEvents(t *testing.T) {
|
||||
@@ -114,7 +114,7 @@ func TestGitBranchSwitch(t *testing.T) {
|
||||
f.events = nil
|
||||
|
||||
// consume all the events in the background
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
done := f.consumeEventsInBackground(ctx)
|
||||
|
||||
for i, dir := range dirs {
|
||||
@@ -501,7 +501,7 @@ type notifyFixture struct {
|
||||
|
||||
func newNotifyFixture(t *testing.T) *notifyFixture {
|
||||
out := bytes.NewBuffer(nil)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
nf := ¬ifyFixture{
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/fsnotify/fsevents"
|
||||
@@ -38,6 +39,7 @@ type fseventNotify struct {
|
||||
stop chan struct{}
|
||||
|
||||
pathsWereWatching map[string]any
|
||||
closeOnce sync.Once
|
||||
}
|
||||
|
||||
func (d *fseventNotify) loop() {
|
||||
@@ -81,6 +83,8 @@ func (d *fseventNotify) Start() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
d.closeOnce = sync.Once{}
|
||||
|
||||
numberOfWatches.Add(int64(len(d.stream.Paths)))
|
||||
|
||||
err := d.stream.Start()
|
||||
@@ -92,11 +96,13 @@ func (d *fseventNotify) Start() error {
|
||||
}
|
||||
|
||||
func (d *fseventNotify) Close() error {
|
||||
numberOfWatches.Add(int64(-len(d.stream.Paths)))
|
||||
d.closeOnce.Do(func() {
|
||||
numberOfWatches.Add(int64(-len(d.stream.Paths)))
|
||||
|
||||
d.stream.Stop()
|
||||
close(d.errors)
|
||||
close(d.stop)
|
||||
d.stream.Stop()
|
||||
close(d.errors)
|
||||
close(d.stop)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
48
pkg/watch/watcher_darwin_test.go
Normal file
48
pkg/watch/watcher_darwin_test.go
Normal file
@@ -0,0 +1,48 @@
|
||||
//go:build fsnotify
|
||||
|
||||
/*
|
||||
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 watch
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestFseventNotifyCloseIdempotent(t *testing.T) {
|
||||
// Create a watcher with a temporary directory
|
||||
tmpDir := t.TempDir()
|
||||
watcher, err := newWatcher([]string{tmpDir})
|
||||
assert.NilError(t, err)
|
||||
|
||||
// Start the watcher
|
||||
err = watcher.Start()
|
||||
assert.NilError(t, err)
|
||||
|
||||
// Close should work the first time
|
||||
err = watcher.Close()
|
||||
assert.NilError(t, err)
|
||||
|
||||
// Close should be idempotent - calling it again should not panic
|
||||
err = watcher.Close()
|
||||
assert.NilError(t, err)
|
||||
|
||||
// Even a third time should be safe
|
||||
err = watcher.Close()
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
Reference in New Issue
Block a user