Compare commits

..

37 Commits

Author SHA1 Message Date
Nicolas De Loof
c428a77111 set fsnotify build tag when building for OSX
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2026-01-20 13:57:53 +01:00
David Gageot
04b4a832dc chore(lint): add forbidigo rules to enforce t.Context() in tests
Add linter rules to prevent usage of context.Background() and
context.TODO() in test files - t.Context() should be used instead.

The rules only apply to *_test.go files, not production code.

Note: os.Setenv is not covered by forbidigo due to a limitation where
it only catches calls when the return value is assigned. However,
errcheck will flag unchecked os.Setenv calls.

Assisted-By: cagent
Signed-off-by: David Gageot <david.gageot@docker.com>
2026-01-20 11:34:11 +01:00
David Gageot
27faa3b84e test: replace os.MkdirTemp with t.TempDir()
Use t.TempDir() which automatically cleans up the temporary directory
when the test completes, eliminating the need for manual cleanup.

Go 1.14 modernization pattern.

Assisted-By: cagent
Signed-off-by: David Gageot <david.gageot@docker.com>
2026-01-20 11:34:11 +01:00
David Gageot
bcc0401e0e test: replace os.Setenv with t.Setenv()
Use t.Setenv() which automatically restores the original value when
the test completes, eliminating the need for manual cleanup.

Go 1.18 modernization pattern.

Assisted-By: cagent
Signed-off-by: David Gageot <david.gageot@docker.com>
2026-01-20 11:34:11 +01:00
David Gageot
093205121c test: replace context.Background()/context.TODO() with t.Context()
Replace manual context creation with t.Context() which is automatically
cancelled when the test completes.

Go 1.24 modernization pattern.

Assisted-By: cagent
Signed-off-by: David Gageot <david.gageot@docker.com>
2026-01-20 11:34:11 +01:00
Amol Yadav
b92b87dd9c fix: robustly handle large file change batches in watch mode
Ensured all watcher and sync goroutines and channels are robustly closed on context cancellation or error.
Added explicit logging for large batches and context cancellation to prevent stuck processes and ensure graceful shutdown on Ctrl-C.

Signed-off-by: Amol Yadav <amyssnipet@yahoo.com>
2026-01-20 08:34:15 +01:00
hiroto.toyoda
06e1287483 fix: update github.com/moby/term to indirect dependency
Signed-off-by: hiroto.toyoda <hiroto.toyoda@dena.com>
2026-01-19 17:46:55 +01:00
hiroto.toyoda
d7bdb34ff5 refactor(attach): remove unused stdin from getContainerStream
Signed-off-by: hiroto.toyoda <hiroto.toyoda@dena.com>
2026-01-19 17:46:55 +01:00
hiroto.toyoda
79d7a8acd6 refactor(attach): simplify attachContainerStreams signature
Signed-off-by: hiroto.toyoda <hiroto.toyoda@dena.com>
2026-01-19 17:46:55 +01:00
hiroto.toyoda
abd99be4fd refactor(attach): remove unused detach watcher and keep attach behavior
Signed-off-by: hiroto.toyoda <hiroto.toyoda@dena.com>
2026-01-19 17:46:55 +01:00
hiroto.toyoda
2672d34217 Improve error handling in attach.go
Signed-off-by: hiroto.toyoda <hiroto.toyoda@dena.com>
2026-01-19 17:46:55 +01:00
Nicolas De Loof
27bf40357a Bump compose to v2.10.1
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2026-01-19 16:46:17 +01:00
Nicolas De Loof
c8d687599a Fixed progress UI to adapt to terminal width
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2026-01-19 11:14:23 +01:00
Stavros Kois
2f108ffaa8 handle healthcheck.disable true in isServiceHealthy
Signed-off-by: Stavros Kois <s.kois@outlook.com>
2026-01-19 10:18:34 +01:00
Sebastiaan van Stijn
0a07df0e5b build(deps): bump github.com/sirupsen/logrus v1.9.4
full diff: https://github.com/sirupsen/logrus/compare/v1.9.3...v1.9.4

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-01-15 19:45:49 +01:00
tensorworker
02b606ef8e use go-compose instead Signed-off-by: tensorworker <tensorworker@proton.me>
Signed-off-by: tensorworker <tensorworker@proton.me>
2026-01-15 08:24:20 +01:00
tensorworker
9856802945 fix: expand tilde in --env-file paths to user home directory
When using --env-file=~/.env, the tilde was not expanded to the user's
home directory. Instead, it was treated as a literal character and
resolved relative to the current working directory, resulting in errors
like "couldn't find env file: /current/dir/~/.env".

This adds an ExpandUser function that expands ~ to the home directory
before converting relative paths to absolute paths.

Fixes #13508

Signed-off-by: tensorworker <tensorworker@proton.me>
2026-01-15 08:24:20 +01:00
Adam Sven Johnson
63ae7eb0fa Replace tabbed indentation in sdk.md
Tabs and spaces were mixed in the example code which didn't indent cleanly in the github preview.

Signed-off-by: Adam Sven Johnson <adam@pkqk.net>
2026-01-14 07:56:25 +01:00
dependabot[bot]
f17d0dfc61 build(deps): bump github.com/go-viper/mapstructure/v2
Bumps [github.com/go-viper/mapstructure/v2](https://github.com/go-viper/mapstructure) from 2.4.0 to 2.5.0.
- [Release notes](https://github.com/go-viper/mapstructure/releases)
- [Changelog](https://github.com/go-viper/mapstructure/blob/main/CHANGELOG.md)
- [Commits](https://github.com/go-viper/mapstructure/compare/v2.4.0...v2.5.0)

---
updated-dependencies:
- dependency-name: github.com/go-viper/mapstructure/v2
  dependency-version: 2.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-13 10:21:23 +01:00
dependabot[bot]
ef14cfcfea build(deps): bump google.golang.org/grpc from 1.77.0 to 1.78.0
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.77.0 to 1.78.0.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.77.0...v1.78.0)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-version: 1.78.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-12 17:50:14 +01:00
hiroto.toyoda
b760afaf9f refactor: extract API version constants to dedicated file
Signed-off-by: hiroto.toyoda <hiroto.toyoda@dena.com>
2026-01-11 17:04:40 +01:00
dependabot[bot]
a2a5c86f53 build(deps): bump golang.org/x/sys from 0.39.0 to 0.40.0
Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.39.0 to 0.40.0.
- [Commits](https://github.com/golang/sys/compare/v0.39.0...v0.40.0)

---
updated-dependencies:
- dependency-name: golang.org/x/sys
  dependency-version: 0.40.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-09 10:22:48 +01:00
Sebastiaan van Stijn
98e82127b3 build(deps): bump github.com/containerd/containerd/v2 to v2.2.1
The pull request that was needed has been released now as part of v2.2.1;
full diff: https://github.com/containerd/containerd/compare/efd86f2b0bc2...v2.2.1

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-01-08 11:33:06 +01:00
Sebastiaan van Stijn
03e19e4a84 go.mod: remove exclude rules
Commit 640c7deae0 added these exclude
rules as a temporary workaround until these transitive dependency
versions would be gone;

> downgrade go-difflib and go-spew to tagged releases
>
> These dependencies were updated to "master" in some modules we depend on,
> but have no code-changes since their last release. Unfortunately, this also
> causes a ripple effect, forcing all users of the containerd module to also
> update these dependencies to an unrelease / un-tagged version.
>
> Both these dependencies will unlikely do a new release in the near future,
> so exclude these versions so that we can downgrade to the current release.

Kubernetes, and other dependencies have reverted those bumps, so these
exclude rules are no longer needed.

This reverts commit 640c7deae0.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-01-08 07:07:57 +01:00
Sebastiaan van Stijn
b2c17ff118 build(deps): bump github.com/klauspost/compress to v1.18.2
Fixes a regression in v1.18.1 that resulted in invalid flate/zip/gzip encoding.
The v1.18.1 tag has been retracted.

full diff: https://github.com/klauspost/compress/compare/v1.18.1...v1.18.2

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-01-07 14:03:12 +01:00
Nicolas De Loof
ec88588cd8 Removed build warning when no explicit build has been requested.
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2026-01-05 13:17:20 +01:00
Jan-Robin Aumann-O'Keefe
7d5913403a add service name completion to down command
Signed-off-by: Jan-Robin Aumann-O'Keefe <jan-robin@aumann.org>
2026-01-05 09:32:39 +01:00
hiroto.toyoda
d95aa57f01 fix: avoid setting timeout when waitTimeout is not positive
Signed-off-by: hiroto.toyoda <hiroto.toyoda@dena.com>
2026-01-05 09:31:14 +01:00
hiroto.toyoda
ee4c01b66b fix: correctly use errgroup.WithContext
Signed-off-by: hiroto.toyoda <hiroto.toyoda@dena.com>
2026-01-05 09:27:46 +01:00
hiroto.toyoda
d7a65f53f8 fix: correct typo in isSwarmEnabled method name
Signed-off-by: hiroto.toyoda <hiroto.toyoda@dena.com>
2026-01-05 09:11:58 +01:00
hiroto.toyoda
4520bcbaf6 fix: clean up temporary compose files after conversion
Signed-off-by: hiroto.toyoda <hiroto.toyoda@dena.com>
2026-01-05 09:01:56 +01:00
hiroto.toyoda
327be1fcd5 add unit test
Signed-off-by: hiroto.toyoda <hiroto.toyoda@dena.com>
2026-01-05 08:15:02 +01:00
Ignacio López Luna
59f04b85af remove duplicated version field
Signed-off-by: Ignacio López Luna <ignasi.lopez.luna@gmail.com>
2025-12-18 15:24:06 +01:00
Ignacio López Luna
b4574c8bd6 do not strip build metadata
Signed-off-by: Ignacio López Luna <ignasi.lopez.luna@gmail.com>
2025-12-18 15:24:06 +01:00
Ignacio López Luna
29d6c918c4 use github.com/docker/docker/api/types/versions for comparing versions and store plugin version obtained by pluginManager in newModelAPI
Signed-off-by: Ignacio López Luna <ignasi.lopez.luna@gmail.com>
2025-12-18 15:24:06 +01:00
Ignacio López Luna
58403169f3 Only append RuntimeFlags if docker model CLI version is >= v1.0.6
Signed-off-by: Ignacio López Luna <ignasi.lopez.luna@gmail.com>
2025-12-18 15:24:06 +01:00
Ignacio López Luna
6aee7f8370 gets back runtime flags when configuring models
Signed-off-by: Ignacio López Luna <ignasi.lopez.luna@gmail.com>
2025-12-18 15:24:06 +01:00
46 changed files with 1219 additions and 376 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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"
@@ -550,12 +551,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
}
}

View File

@@ -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))

View File

@@ -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)

View File

@@ -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,

View File

@@ -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{

View File

@@ -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)
})
}
}

View File

@@ -21,9 +21,11 @@ import (
"fmt"
"io"
"iter"
"slices"
"strings"
"sync"
"time"
"unicode/utf8"
"github.com/buger/goterm"
"github.com/docker/go-units"
@@ -258,13 +260,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 +311,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 +520,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 +536,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 +552,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 +639,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
View 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)
})
}
}

View File

@@ -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 {

29
go.mod
View File

@@ -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
@@ -55,8 +54,8 @@ require (
go.uber.org/mock v0.6.0
go.yaml.in/yaml/v4 v4.0.0-rc.3
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.40.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
)

48
go.sum
View File

@@ -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=
@@ -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.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.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=

View File

@@ -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)

View File

@@ -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 {

View File

@@ -0,0 +1,73 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
// 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"
)

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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 {

View File

@@ -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)
@@ -833,7 +833,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
}

View File

@@ -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)

View File

@@ -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)
}

View File

@@ -360,7 +360,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 +374,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 +527,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 +541,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)
}
}
}
@@ -861,8 +861,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 +1524,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
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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": {

View File

@@ -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)

View File

@@ -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"},
})

View File

@@ -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"))

View File

@@ -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")
}

View File

@@ -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{
{

View File

@@ -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)

View File

@@ -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)

View File

@@ -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,

View File

@@ -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)

View File

@@ -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))
}

View File

@@ -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{

View File

@@ -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.

View File

@@ -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)

View File

@@ -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")

View File

@@ -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)

View File

@@ -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 := &notifyFixture{
ctx: ctx,
cancel: cancel,