From fa7e85ed839ae5e38aeafa9ef6ea81894d5f8dc0 Mon Sep 17 00:00:00 2001 From: Benedikt Radtke Date: Sat, 4 Oct 2025 01:20:59 +0200 Subject: [PATCH 01/19] Write error to watcher error channel if Start() fails Up's loop will notice globalCtx is done, and invoke watcher.Stop(). Stop() reads from the watcher error channel. If Start() does not write an error, Stop() will never finish. Fixes https://github.com/docker/compose/issues/13262 Signed-off-by: Benedikt Radtke --- pkg/compose/watch.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/compose/watch.go b/pkg/compose/watch.go index 192e8c911cf..644a4dced98 100644 --- a/pkg/compose/watch.go +++ b/pkg/compose/watch.go @@ -89,6 +89,9 @@ func (w *Watcher) Start(ctx context.Context) error { w.stopFn = cancelFunc wait, err := w.watchFn(ctx, w.project, w.options) if err != nil { + go func() { + w.errCh <- err + }() return err } go func() { From ce463d50b2327c09a22d487454b55dfecfff90f8 Mon Sep 17 00:00:00 2001 From: Kian Eliasi Date: Wed, 8 Oct 2025 10:23:30 +0330 Subject: [PATCH 02/19] Fix: set PWD only if not set Signed-off-by: Kian Eliasi --- cmd/compose/compose.go | 54 +++++++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/cmd/compose/compose.go b/cmd/compose/compose.go index da3c7def26b..5a1772080f9 100644 --- a/cmd/compose/compose.go +++ b/cmd/compose/compose.go @@ -383,32 +383,38 @@ func (o *ProjectOptions) remoteLoaders(dockerCli command.Cli) []loader.ResourceL } func (o *ProjectOptions) toProjectOptions(po ...cli.ProjectOptionsFn) (*cli.ProjectOptions, error) { - pwd, err := os.Getwd() - if err != nil { - return nil, err + opts := []cli.ProjectOptionsFn{ + cli.WithWorkingDirectory(o.ProjectDir), + // First apply os.Environment, always win + cli.WithOsEnv, + } + + if _, present := os.LookupEnv("PWD"); !present { + if pwd, err := os.Getwd(); err != nil { + return nil, err + } else { + opts = append(opts, cli.WithEnv([]string{"PWD=" + pwd})) + } } - return cli.NewProjectOptions(o.ConfigPaths, - append(po, - cli.WithWorkingDirectory(o.ProjectDir), - // First apply os.Environment, always win - cli.WithOsEnv, - // set PWD as this variable is not consistently supported on Windows - cli.WithEnv([]string{"PWD=" + pwd}), - // Load PWD/.env if present and no explicit --env-file has been set - cli.WithEnvFiles(o.EnvFiles...), - // read dot env file to populate project environment - cli.WithDotEnv, - // get compose file path set by COMPOSE_FILE - cli.WithConfigFileEnv, - // if none was selected, get default compose.yaml file from current dir or parent folder - cli.WithDefaultConfigPath, - // .. and then, a project directory != PWD maybe has been set so let's load .env file - cli.WithEnvFiles(o.EnvFiles...), - cli.WithDotEnv, - // eventually COMPOSE_PROFILES should have been set - cli.WithDefaultProfiles(o.Profiles...), - cli.WithName(o.ProjectName))...) + opts = append(opts, + // Load PWD/.env if present and no explicit --env-file has been set + cli.WithEnvFiles(o.EnvFiles...), + // read dot env file to populate project environment + cli.WithDotEnv, + // get compose file path set by COMPOSE_FILE + cli.WithConfigFileEnv, + // if none was selected, get default compose.yaml file from current dir or parent folder + cli.WithDefaultConfigPath, + // .. and then, a project directory != PWD maybe has been set so let's load .env file + cli.WithEnvFiles(o.EnvFiles...), + cli.WithDotEnv, + // eventually COMPOSE_PROFILES should have been set + cli.WithDefaultProfiles(o.Profiles...), + cli.WithName(o.ProjectName), + ) + + return cli.NewProjectOptions(o.ConfigPaths, append(po, opts...)...) } // PluginName is the name of the plugin From 7fec70b6c750e91f07a1215bbde3f1748a631b6c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 9 Oct 2025 09:13:55 +0000 Subject: [PATCH 03/19] build(deps): bump github.com/docker/docker Bumps [github.com/docker/docker](https://github.com/docker/docker) from 28.5.0+incompatible to 28.5.1+incompatible. - [Release notes](https://github.com/docker/docker/releases) - [Commits](https://github.com/docker/docker/compare/v28.5.0...v28.5.1) --- updated-dependencies: - dependency-name: github.com/docker/docker dependency-version: 28.5.1+incompatible dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 94af1e3a539..1559c273b82 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/docker/buildx v0.28.0 github.com/docker/cli v28.5.0+incompatible github.com/docker/cli-docs-tool v0.10.0 - github.com/docker/docker v28.5.0+incompatible + github.com/docker/docker v28.5.1+incompatible github.com/docker/go-connections v0.6.0 github.com/docker/go-units v0.5.0 github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 diff --git a/go.sum b/go.sum index 50d0566bfc1..cae9c6fffd2 100644 --- a/go.sum +++ b/go.sum @@ -135,8 +135,8 @@ github.com/docker/cli-docs-tool v0.10.0/go.mod h1:5EM5zPnT2E7yCLERZmrDA234Vwn09f github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v28.5.0+incompatible h1:ZdSQoRUE9XxhFI/B8YLvhnEFMmYN9Pp8Egd2qcaFk1E= -github.com/docker/docker v28.5.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM= +github.com/docker/docker v28.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0= From f45a3ebcfd319e832debcb7831a0e38ad8790169 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 9 Oct 2025 09:13:48 +0000 Subject: [PATCH 04/19] build(deps): bump github.com/docker/cli Bumps [github.com/docker/cli](https://github.com/docker/cli) from 28.5.0+incompatible to 28.5.1+incompatible. - [Commits](https://github.com/docker/cli/compare/v28.5.0...v28.5.1) --- updated-dependencies: - dependency-name: github.com/docker/cli dependency-version: 28.5.1+incompatible dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 1559c273b82..ae2254533c4 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/davecgh/go-spew v1.1.1 github.com/distribution/reference v0.6.0 github.com/docker/buildx v0.28.0 - github.com/docker/cli v28.5.0+incompatible + github.com/docker/cli v28.5.1+incompatible github.com/docker/cli-docs-tool v0.10.0 github.com/docker/docker v28.5.1+incompatible github.com/docker/go-connections v0.6.0 diff --git a/go.sum b/go.sum index cae9c6fffd2..d3b6a1bd79b 100644 --- a/go.sum +++ b/go.sum @@ -128,8 +128,8 @@ github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxK github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/docker/buildx v0.28.0 h1:ZnrVsZ/qQwSOQ4Fx3IgXjiurAwvocaF1YUaPbIXD89E= github.com/docker/buildx v0.28.0/go.mod h1:nLwx58w7xrQbLVSXiWiHpkVhY4ou4ci/hYomc139Vjk= -github.com/docker/cli v28.5.0+incompatible h1:crVqLrtKsrhC9c00ythRx435H8LiQnUKRtJLRR+Auxk= -github.com/docker/cli v28.5.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v28.5.1+incompatible h1:ESutzBALAD6qyCLqbQSEf1a/U8Ybms5agw59yGVc+yY= +github.com/docker/cli v28.5.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli-docs-tool v0.10.0 h1:bOD6mKynPQgojQi3s2jgcUWGp/Ebqy1SeCr9VfKQLLU= github.com/docker/cli-docs-tool v0.10.0/go.mod h1:5EM5zPnT2E7yCLERZmrDA234Vwn09fzRHP4aX1qwp1U= github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= From a07f2b8ded4a25bfe83522d3421294db5e1ec8dc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 9 Oct 2025 09:13:43 +0000 Subject: [PATCH 05/19] build(deps): bump golang.org/x/sys from 0.36.0 to 0.37.0 Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.36.0 to 0.37.0. - [Commits](https://github.com/golang/sys/compare/v0.36.0...v0.37.0) --- updated-dependencies: - dependency-name: golang.org/x/sys dependency-version: 0.37.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index ae2254533c4..fa851f90347 100644 --- a/go.mod +++ b/go.mod @@ -53,7 +53,7 @@ require ( go.uber.org/goleak v1.3.0 go.uber.org/mock v0.6.0 golang.org/x/sync v0.17.0 - golang.org/x/sys v0.36.0 + golang.org/x/sys v0.37.0 google.golang.org/grpc v1.74.2 gopkg.in/yaml.v3 v3.0.1 gotest.tools/v3 v3.5.2 diff --git a/go.sum b/go.sum index d3b6a1bd79b..8828943b690 100644 --- a/go.sum +++ b/go.sum @@ -591,8 +591,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/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.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= -golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.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= From a03f2562df7ed6c8be19d8a6ecd2d7ddbcd0cf50 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Wed, 8 Oct 2025 12:20:34 +0200 Subject: [PATCH 06/19] bake only interpolates ${*} Signed-off-by: Nicolas De Loof --- pkg/compose/build_bake.go | 12 +++--------- pkg/e2e/build_test.go | 11 ++++++++--- .../fixtures/build-test/escaped/compose.yaml | 17 +++++++++++++++++ 3 files changed, 28 insertions(+), 12 deletions(-) diff --git a/pkg/compose/build_bake.go b/pkg/compose/build_bake.go index 71735dcb06a..777e69942a6 100644 --- a/pkg/compose/build_bake.go +++ b/pkg/compose/build_bake.go @@ -184,12 +184,9 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project build := *service.Build labels := getImageBuildLabels(project, service) - args := types.Mapping{} - for k, v := range resolveAndMergeBuildArgs(s.dockerCli, project, service, options) { - if v == nil { - continue - } - args[k] = *v + args := resolveAndMergeBuildArgs(s.dockerCli, project, service, options).ToMapping() + for k, v := range args { + args[k] = strings.ReplaceAll(v, "${", "$${") } entitlements := build.Entitlements @@ -280,9 +277,6 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project return nil, err } - // escape all occurrences of '$' as we interpolated everything that has to - b = bytes.ReplaceAll(b, []byte("$"), []byte("$$")) - if options.Print { _, err = fmt.Fprintln(s.stdout(), string(b)) return nil, err diff --git a/pkg/e2e/build_test.go b/pkg/e2e/build_test.go index e0533261652..2acabe51dd6 100644 --- a/pkg/e2e/build_test.go +++ b/pkg/e2e/build_test.go @@ -648,8 +648,13 @@ func TestBuildTLS(t *testing.T) { func TestBuildEscaped(t *testing.T) { c := NewParallelCLI(t) - // ensure local test run does not reuse previously build image - c.RunDockerOrExitError(t, "rmi", "build-test-tags") - res := c.RunDockerComposeCmd(t, "--project-directory", "./fixtures/build-test/escaped", "build", "--no-cache") + + res := c.RunDockerComposeCmd(t, "--project-directory", "./fixtures/build-test/escaped", "build", "--no-cache", "foo") res.Assert(t, icmd.Expected{Out: "foo is ${bar}"}) + + res = c.RunDockerComposeCmd(t, "--project-directory", "./fixtures/build-test/escaped", "build", "--no-cache", "echo") + res.Assert(t, icmd.Success) + + res = c.RunDockerComposeCmd(t, "--project-directory", "./fixtures/build-test/escaped", "build", "--no-cache", "arg") + res.Assert(t, icmd.Success) } diff --git a/pkg/e2e/fixtures/build-test/escaped/compose.yaml b/pkg/e2e/fixtures/build-test/escaped/compose.yaml index 997af4e9209..2d0077b9e63 100644 --- a/pkg/e2e/fixtures/build-test/escaped/compose.yaml +++ b/pkg/e2e/fixtures/build-test/escaped/compose.yaml @@ -4,3 +4,20 @@ services: context: . args: foo: $${bar} + + echo: + build: + dockerfile_inline: | + FROM bash + RUN <<'EOF' + echo $(seq 10) + EOF + + arg: + build: + args: + BOOL: "true" + dockerfile_inline: | + FROM alpine:latest + ARG BOOL + RUN /bin/$${BOOL} From 63920c4cc022289c1e1238c38e750cc8581c5ace Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Mon, 13 Oct 2025 15:04:27 +0200 Subject: [PATCH 07/19] pkg/compose: align classic builder implementation with docker/cli This aligns the implementation closer to the implementation in docker/cli, with the refactor done in [cli@260f1db]; this removes some direct uses of the github.com/docker/docker/builder/remotecontext/urlutil package, which won't be included in the new Moby modules. There's still some remaining uses in the `dockerFilePath` utility (which may need to be updated to also account for remote contexts that are not "git"), so possibly we can remove the use in that utility as well. [cli@260f1db]: https://github.com/docker/cli/commit/260f1dbebb9f3d70d2bdda67e511a6bee9fe365b Signed-off-by: Sebastiaan van Stijn --- pkg/compose/build_bake.go | 4 +- pkg/compose/build_classic.go | 76 +++++++++++++++++++----------------- 2 files changed, 42 insertions(+), 38 deletions(-) diff --git a/pkg/compose/build_bake.go b/pkg/compose/build_bake.go index 777e69942a6..a2a8a59cc44 100644 --- a/pkg/compose/build_bake.go +++ b/pkg/compose/build_bake.go @@ -37,10 +37,10 @@ import ( "github.com/containerd/errdefs" "github.com/docker/cli/cli-plugins/manager" "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/command/image/build" "github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/progress" "github.com/docker/docker/api/types/versions" - "github.com/docker/docker/builder/remotecontext/urlutil" "github.com/moby/buildkit/client" gitutil "github.com/moby/buildkit/frontend/dockerfile/dfgitutil" "github.com/moby/buildkit/util/progress/progressui" @@ -505,7 +505,7 @@ func dockerFilePath(ctxName string, dockerfile string) string { if dockerfile == "" { return "" } - if urlutil.IsGitURL(ctxName) { + if contextType, _ := build.DetectContextType(ctxName); contextType == build.ContextTypeGit { return dockerfile } if !filepath.IsAbs(dockerfile) { diff --git a/pkg/compose/build_classic.go b/pkg/compose/build_classic.go index 7e85b5bf201..a84929193e5 100644 --- a/pkg/compose/build_classic.go +++ b/pkg/compose/build_classic.go @@ -34,7 +34,6 @@ import ( buildtypes "github.com/docker/docker/api/types/build" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/registry" - "github.com/docker/docker/builder/remotecontext/urlutil" "github.com/docker/docker/pkg/jsonmessage" "github.com/docker/docker/pkg/progress" "github.com/docker/docker/pkg/streamformatter" @@ -48,17 +47,9 @@ func (s *composeService) doBuildClassic(ctx context.Context, project *types.Proj buildCtx io.ReadCloser dockerfileCtx io.ReadCloser contextDir string - tempDir string relDockerfile string - - err error ) - dockerfileName := dockerFilePath(service.Build.Context, service.Build.Dockerfile) - specifiedContext := service.Build.Context - progBuff := s.stdout() - buildBuff := s.stdout() - if len(service.Build.Platforms) > 1 { return "", fmt.Errorf("the classic builder doesn't support multi-arch build, set DOCKER_BUILDKIT=1 to use BuildKit") } @@ -80,34 +71,51 @@ func (s *composeService) doBuildClassic(ctx context.Context, project *types.Proj } service.Build.Labels[api.ImageBuilderLabel] = "classic" - switch { - case isLocalDir(specifiedContext): + dockerfileName := dockerFilePath(service.Build.Context, service.Build.Dockerfile) + specifiedContext := service.Build.Context + progBuff := s.stdout() + buildBuff := s.stdout() + + contextType, err := build.DetectContextType(specifiedContext) + if err != nil { + return "", err + } + + switch contextType { + case build.ContextTypeStdin: + return "", fmt.Errorf("building from STDIN is not supported") + case build.ContextTypeLocal: contextDir, relDockerfile, err = build.GetContextFromLocalDir(specifiedContext, dockerfileName) - if err == nil && strings.HasPrefix(relDockerfile, ".."+string(filepath.Separator)) { - // Dockerfile is outside of build-context; read the Dockerfile and pass it as dockerfileCtx + if err != nil { + return "", fmt.Errorf("unable to prepare context: %w", err) + } + if strings.HasPrefix(relDockerfile, ".."+string(filepath.Separator)) { + // Dockerfile is outside build-context; read the Dockerfile and pass it as dockerfileCtx dockerfileCtx, err = os.Open(dockerfileName) if err != nil { return "", fmt.Errorf("unable to open Dockerfile: %w", err) } defer dockerfileCtx.Close() //nolint:errcheck } - case urlutil.IsGitURL(specifiedContext): + case build.ContextTypeGit: + var tempDir string tempDir, relDockerfile, err = build.GetContextFromGitURL(specifiedContext, dockerfileName) - case urlutil.IsURL(specifiedContext): + if err != nil { + return "", fmt.Errorf("unable to prepare context: %w", err) + } + defer func() { + _ = os.RemoveAll(tempDir) + }() + contextDir = tempDir + case build.ContextTypeRemote: buildCtx, relDockerfile, err = build.GetContextFromURL(progBuff, specifiedContext, dockerfileName) + if err != nil { + return "", fmt.Errorf("unable to prepare context: %w", err) + } default: return "", fmt.Errorf("unable to prepare context: path %q not found", specifiedContext) } - if err != nil { - return "", fmt.Errorf("unable to prepare context: %w", err) - } - - if tempDir != "" { - defer os.RemoveAll(tempDir) //nolint:errcheck - contextDir = tempDir - } - // read from a directory into tar archive if buildCtx == nil { excludes, err := build.ReadDockerignore(contextDir) @@ -125,7 +133,7 @@ func (s *composeService) doBuildClassic(ctx context.Context, project *types.Proj excludes = build.TrimBuildFilesFromExcludes(excludes, relDockerfile, false) buildCtx, err = archive.TarWithOptions(contextDir, &archive.TarOptions{ ExcludePatterns: excludes, - ChownOpts: &archive.ChownOpts{}, + ChownOpts: &archive.ChownOpts{UID: 0, GID: 0}, }) if err != nil { return "", err @@ -145,6 +153,7 @@ func (s *composeService) doBuildClassic(ctx context.Context, project *types.Proj return "", err } + // Setup an upload progress bar progressOutput := streamformatter.NewProgressOutput(progBuff) body := progress.NewProgressReader(buildCtx, progressOutput, 0, "", "Sending build context to Docker daemon") @@ -166,16 +175,16 @@ func (s *composeService) doBuildClassic(ctx context.Context, project *types.Proj RegistryToken: authConfig.RegistryToken, } } - buildOptions := imageBuildOptions(s.dockerCli, project, service, options) + buildOpts := imageBuildOptions(s.dockerCli, project, service, options) imageName := api.GetImageNameOrDefault(service, project.Name) - buildOptions.Tags = append(buildOptions.Tags, imageName) - buildOptions.Dockerfile = relDockerfile - buildOptions.AuthConfigs = authConfigs - buildOptions.Memory = options.Memory + buildOpts.Tags = append(buildOpts.Tags, imageName) + buildOpts.Dockerfile = relDockerfile + buildOpts.AuthConfigs = authConfigs + buildOpts.Memory = options.Memory ctx, cancel := context.WithCancel(ctx) defer cancel() - response, err := s.apiClient().ImageBuild(ctx, body, buildOptions) + response, err := s.apiClient().ImageBuild(ctx, body, buildOpts) if err != nil { return "", err } @@ -206,11 +215,6 @@ func (s *composeService) doBuildClassic(ctx context.Context, project *types.Proj return imageID, nil } -func isLocalDir(c string) bool { - _, err := os.Stat(c) - return err == nil -} - func imageBuildOptions(dockerCli command.Cli, project *types.Project, service types.ServiceConfig, options api.BuildOptions) buildtypes.ImageBuildOptions { config := service.Build return buildtypes.ImageBuildOptions{ From 0b5fb36eb551ed21d6c2155f912a144fde6da1d4 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Mon, 13 Oct 2025 17:23:40 +0200 Subject: [PATCH 08/19] build(deps): bump docker/buildx v0.29.1, moby/buildkit v0.25.1 full diff: - https://github.com/docker/buildx/compare/v0.28.0...v0.29.1 - https://github.com/moby/buildkit/compare/v0.24.0...v0.25.1 Signed-off-by: Sebastiaan van Stijn --- go.mod | 8 ++++---- go.sum | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index fa851f90347..141f3355261 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/containerd/platforms v1.0.0-rc.1 github.com/davecgh/go-spew v1.1.1 github.com/distribution/reference v0.6.0 - github.com/docker/buildx v0.28.0 + github.com/docker/buildx v0.29.1 github.com/docker/cli v28.5.1+incompatible github.com/docker/cli-docs-tool v0.10.0 github.com/docker/docker v28.5.1+incompatible @@ -28,7 +28,7 @@ require ( github.com/jonboulle/clockwork v0.5.0 github.com/mattn/go-shellwords v1.0.12 github.com/mitchellh/go-ps v1.0.0 - github.com/moby/buildkit v0.24.0 + github.com/moby/buildkit v0.25.1 github.com/moby/go-archive v0.1.0 github.com/moby/patternmatcher v0.6.0 github.com/moby/sys/atomicwriter v0.1.0 @@ -168,7 +168,7 @@ require ( github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xhit/go-str2duration/v2 v2.1.0 // indirect - github.com/zclconf/go-cty v1.16.2 // indirect + github.com/zclconf/go-cty v1.17.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0 // indirect @@ -186,7 +186,7 @@ require ( golang.org/x/time v0.11.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect - google.golang.org/protobuf v1.36.6 // indirect + google.golang.org/protobuf v1.36.9 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/go.sum b/go.sum index 8828943b690..807b1e602e8 100644 --- a/go.sum +++ b/go.sum @@ -126,8 +126,8 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/docker/buildx v0.28.0 h1:ZnrVsZ/qQwSOQ4Fx3IgXjiurAwvocaF1YUaPbIXD89E= -github.com/docker/buildx v0.28.0/go.mod h1:nLwx58w7xrQbLVSXiWiHpkVhY4ou4ci/hYomc139Vjk= +github.com/docker/buildx v0.29.1 h1:58hxM5Z4mnNje3G5NKfULT9xCr8ooM8XFtlfUK9bKaA= +github.com/docker/buildx v0.29.1/go.mod h1:J4EFv6oxlPiV1MjO0VyJx2u5tLM7ImDEl9zyB8d4wPI= github.com/docker/cli v28.5.1+incompatible h1:ESutzBALAD6qyCLqbQSEf1a/U8Ybms5agw59yGVc+yY= github.com/docker/cli v28.5.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli-docs-tool v0.10.0 h1:bOD6mKynPQgojQi3s2jgcUWGp/Ebqy1SeCr9VfKQLLU= @@ -316,8 +316,8 @@ github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/z github.com/mitchellh/mapstructure v0.0.0-20150613213606-2caf8efc9366/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/moby/buildkit v0.24.0 h1:qYfTl7W1SIJzWDIDCcPT8FboHIZCYfi++wvySi3eyFE= -github.com/moby/buildkit v0.24.0/go.mod h1:4qovICAdR2H4C7+EGMRva5zgHW1gyhT4/flHI7F5F9k= +github.com/moby/buildkit v0.25.1 h1:j7IlVkeNbEo+ZLoxdudYCHpmTsbwKvhgc/6UJ/mY/o8= +github.com/moby/buildkit v0.25.1/go.mod h1:phM8sdqnvgK2y1dPDnbwI6veUCXHOZ6KFSl6E164tkc= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ= @@ -493,8 +493,8 @@ github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtX github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/zclconf/go-cty v1.16.2 h1:LAJSwc3v81IRBZyUVQDUdZ7hs3SYs9jv0eZJDWHD/70= -github.com/zclconf/go-cty v1.16.2/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= +github.com/zclconf/go-cty v1.17.0 h1:seZvECve6XX4tmnvRzWtJNHdscMtYEx5R7bnnVyd/d0= +github.com/zclconf/go-cty v1.17.0/go.mod h1:wqFzcImaLTI6A5HfsRwB0nj5n0MRZFwmey8YoFPPs3U= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= @@ -624,8 +624,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go. google.golang.org/grpc v1.0.5/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4= google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM= -google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= -google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= +google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/cenkalti/backoff.v2 v2.2.1 h1:eJ9UAg01/HIHG987TwxvnzK2MgxXq97YY6rYDpY9aII= From ae3309afabf82c95e816fcd9da4daae917fcb582 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Mon, 13 Oct 2025 17:43:35 +0200 Subject: [PATCH 09/19] pkg/compose: build with bake: drop support for buildx v0.16 and lower [buildx v0.17][1] was released a Year ago, so any version this conditional code was accounting for would be versions before that; the latest of which being [buildx v0.16.2][2] (July 2024). Given that those versions are long EOL and no longer supported, we can probably remove the conditional code. [1]: https://github.com/docker/buildx/releases/tag/v0.17.0 [2]: https://github.com/docker/buildx/releases/tag/v0.16.2 Signed-off-by: Sebastiaan van Stijn --- pkg/compose/build_bake.go | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/pkg/compose/build_bake.go b/pkg/compose/build_bake.go index a2a8a59cc44..299c6f2f431 100644 --- a/pkg/compose/build_bake.go +++ b/pkg/compose/build_bake.go @@ -40,7 +40,6 @@ import ( "github.com/docker/cli/cli/command/image/build" "github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/progress" - "github.com/docker/docker/api/types/versions" "github.com/moby/buildkit/client" gitutil "github.com/moby/buildkit/frontend/dockerfile/dfgitutil" "github.com/moby/buildkit/util/progress/progressui" @@ -302,15 +301,12 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project } args := []string{"bake", "--file", "-", "--progress", "rawjson", "--metadata-file", metadataFile} - mustAllow := buildx.Version != "" && versions.GreaterThanOrEqualTo(buildx.Version[1:], "0.17.0") - if mustAllow { - // FIXME we should prompt user about this, but this is a breaking change in UX - for _, path := range read { - args = append(args, "--allow", "fs.read="+path) - } - if privileged { - args = append(args, "--allow", "security.insecure") - } + // FIXME we should prompt user about this, but this is a breaking change in UX + for _, path := range read { + args = append(args, "--allow", "fs.read="+path) + } + if privileged { + args = append(args, "--allow", "security.insecure") } if options.SBOM != "" { args = append(args, "--sbom="+options.SBOM) From e7aa484b781f1a86df8d462065f3df2de1a43bfb Mon Sep 17 00:00:00 2001 From: Olivier Goulpeau Date: Fri, 10 Oct 2025 15:08:27 +0200 Subject: [PATCH 10/19] fix(publish): in `processFile()`, load the compose file passing the `project.Profiles` to the `loader.Options`. Signed-off-by: Olivier Goulpeau --- pkg/compose/publish.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/compose/publish.go b/pkg/compose/publish.go index 8f3fcb8197d..b149debc9c1 100644 --- a/pkg/compose/publish.go +++ b/pkg/compose/publish.go @@ -221,6 +221,7 @@ func processFile(ctx context.Context, file string, project *types.Project, extFi options.SkipExtends = true options.SkipConsistencyCheck = true options.ResolvePaths = true + options.Profiles = project.Profiles }) if err != nil { return nil, err From 289faae5faebcda61cd8b2bd9d810e57a7bf483c Mon Sep 17 00:00:00 2001 From: Olivier Goulpeau Date: Fri, 10 Oct 2025 18:42:59 +0200 Subject: [PATCH 11/19] fix(publish): in `publish()`, select all profiles in the `project` to publish. This code is moved from `generateImageDigestsOverride()` as no more needed at that point. Signed-off-by: Olivier Goulpeau --- pkg/compose/publish.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/compose/publish.go b/pkg/compose/publish.go index b149debc9c1..a504dc15115 100644 --- a/pkg/compose/publish.go +++ b/pkg/compose/publish.go @@ -50,6 +50,10 @@ func (s *composeService) Publish(ctx context.Context, project *types.Project, re //nolint:gocyclo func (s *composeService) publish(ctx context.Context, project *types.Project, repository string, options api.PublishOptions) error { + project, err := project.WithProfiles([]string{"*"}) + if err != nil { + return err + } accept, err := s.preChecks(project, options) if err != nil { return err @@ -251,11 +255,7 @@ func processFile(ctx context.Context, file string, project *types.Project, extFi } func (s *composeService) generateImageDigestsOverride(ctx context.Context, project *types.Project) ([]byte, error) { - project, err := project.WithProfiles([]string{"*"}) - if err != nil { - return nil, err - } - project, err = project.WithImagesResolved(ImageDigestResolver(ctx, s.configFile(), s.apiClient())) + project, err := project.WithImagesResolved(ImageDigestResolver(ctx, s.configFile(), s.apiClient())) if err != nil { return nil, err } From 147923c44c00110a23a62863d312ec8c228db243 Mon Sep 17 00:00:00 2001 From: Guillaume Lours <705411+glours@users.noreply.github.com> Date: Tue, 14 Oct 2025 08:51:30 +0200 Subject: [PATCH 12/19] bump golang to version 1.24.9 Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com> --- Dockerfile | 2 +- go.mod | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index b8901a3b122..9b47ed8594d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,7 +15,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -ARG GO_VERSION=1.24.7 +ARG GO_VERSION=1.24.9 ARG XX_VERSION=1.6.1 ARG GOLANGCI_LINT_VERSION=v2.0.2 ARG ADDLICENSE_VERSION=v1.0.0 diff --git a/go.mod b/go.mod index 141f3355261..0cd8f7a7b6b 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/docker/compose/v2 -go 1.24.7 +go 1.24.9 require ( github.com/AlecAivazis/survey/v2 v2.3.7 From 7755302348dbfb216feeab8c9421fd48b1a25c74 Mon Sep 17 00:00:00 2001 From: Guillaume Lours <705411+glours@users.noreply.github.com> Date: Tue, 14 Oct 2025 08:57:31 +0200 Subject: [PATCH 13/19] use fixed version of compose bridge transformer images to avoid CI issue on Compose when a new version is released and change the outputs Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com> --- pkg/e2e/bridge_test.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/e2e/bridge_test.go b/pkg/e2e/bridge_test.go index 6024f456d97..c4c99b8d292 100644 --- a/pkg/e2e/bridge_test.go +++ b/pkg/e2e/bridge_test.go @@ -17,6 +17,7 @@ package e2e import ( + "fmt" "path/filepath" "strings" "testing" @@ -28,12 +29,13 @@ func TestConvertAndTransformList(t *testing.T) { c := NewParallelCLI(t) const projectName = "bridge" + const bridgeImageVersion = "v0.0.3" tmpDir := t.TempDir() t.Run("kubernetes manifests", func(t *testing.T) { kubedir := filepath.Join(tmpDir, "kubernetes") res := c.RunDockerComposeCmd(t, "-f", "./fixtures/bridge/compose.yaml", "--project-name", projectName, "bridge", "convert", - "--output", kubedir) + "--output", kubedir, "--transformation", fmt.Sprintf("docker/compose-bridge-kubernetes:%s", bridgeImageVersion)) assert.NilError(t, res.Error) assert.Equal(t, res.ExitCode, 0) res = c.RunCmd(t, "diff", "-r", kubedir, "./fixtures/bridge/expected-kubernetes") @@ -43,7 +45,7 @@ func TestConvertAndTransformList(t *testing.T) { t.Run("helm charts", func(t *testing.T) { helmDir := filepath.Join(tmpDir, "helm") res := c.RunDockerComposeCmd(t, "-f", "./fixtures/bridge/compose.yaml", "--project-name", projectName, "bridge", "convert", - "--output", helmDir, "--transformation", "docker/compose-bridge-helm") + "--output", helmDir, "--transformation", fmt.Sprintf("docker/compose-bridge-helm:%s", bridgeImageVersion)) assert.NilError(t, res.Error) assert.Equal(t, res.ExitCode, 0) res = c.RunCmd(t, "diff", "-r", helmDir, "./fixtures/bridge/expected-helm") From 88aae9c46e7e2ebb507f6637f52ffd1f292a62c9 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Wed, 15 Oct 2025 16:48:16 +0200 Subject: [PATCH 14/19] support Ctrl+Z to run compose in background Signed-off-by: Nicolas De Loof --- cmd/formatter/shortcut.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cmd/formatter/shortcut.go b/cmd/formatter/shortcut.go index d1c7363196f..4a5e774737e 100644 --- a/cmd/formatter/shortcut.go +++ b/cmd/formatter/shortcut.go @@ -321,6 +321,8 @@ func (lk *LogKeyboard) HandleKeyEvents(ctx context.Context, event keyboard.KeyEv lk.logLevel = NONE // will notify main thread to kill and will handle gracefully lk.signalChannel <- syscall.SIGINT + case keyboard.KeyCtrlZ: + _ = syscall.Kill(0, syscall.SIGSTOP) case keyboard.KeyEnter: newLine() lk.printNavigationMenu() From 157617480a987e82e803cff133a00272ee5be105 Mon Sep 17 00:00:00 2001 From: Paul Thiele Date: Thu, 16 Oct 2025 08:55:54 +0200 Subject: [PATCH 15/19] fix race-condition bug in publish command Signed-off-by: Paul Thiele --- internal/oci/resolver.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/internal/oci/resolver.go b/internal/oci/resolver.go index a71d335ba44..0f44f947006 100644 --- a/internal/oci/resolver.go +++ b/internal/oci/resolver.go @@ -125,10 +125,13 @@ func Push(ctx context.Context, resolver remotes.Resolver, ref reference.Named, d if err != nil { return err } - defer func() { - _ = push.Close() - }() _, err = push.Write(descriptor.Data) - return err + if err != nil { + // Close the writer on error since Commit won't be called + _ = push.Close() + return err + } + // Commit will close the writer + return push.Commit(ctx, int64(len(descriptor.Data)), descriptor.Digest) } From ee75be342b8d22258fc722dd9ae263a0c3a1b289 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Wed, 15 Oct 2025 11:10:27 +0200 Subject: [PATCH 16/19] Set secret/config uid:gid to match container's USER Signed-off-by: Nicolas De Loof --- pkg/compose/run.go | 9 ++++-- pkg/compose/secrets.go | 40 ++++++++++++++++++++++++ pkg/e2e/fixtures/env-secret/compose.yaml | 17 ++++++++++ pkg/e2e/secrets_test.go | 15 +++++++++ 4 files changed, 79 insertions(+), 2 deletions(-) diff --git a/pkg/compose/run.go b/pkg/compose/run.go index 242f2fc1cce..0e454e6a2f3 100644 --- a/pkg/compose/run.go +++ b/pkg/compose/run.go @@ -133,12 +133,17 @@ func (s *composeService) prepareRun(ctx context.Context, project *types.Project, return "", err } - err = s.injectSecrets(ctx, project, service, created.ID) + ctr, err := s.apiClient().ContainerInspect(ctx, created.ID) + if err != nil { + return "", err + } + + err = s.injectSecrets(ctx, project, service, ctr.ID) if err != nil { return created.ID, err } - err = s.injectConfigs(ctx, project, service, created.ID) + err = s.injectConfigs(ctx, project, service, ctr.ID) return created.ID, err } diff --git a/pkg/compose/secrets.go b/pkg/compose/secrets.go index e8064cca8b2..bd5ca1a90a7 100644 --- a/pkg/compose/secrets.go +++ b/pkg/compose/secrets.go @@ -22,6 +22,7 @@ import ( "context" "fmt" "strconv" + "strings" "time" "github.com/compose-spec/compose-go/v2/types" @@ -29,6 +30,7 @@ import ( ) func (s *composeService) injectSecrets(ctx context.Context, project *types.Project, service types.ServiceConfig, id string) error { + var ctrConfig *container.Config for _, config := range service.Secrets { file := project.Secrets[config.Source] if file.Environment == "" { @@ -53,6 +55,25 @@ func (s *composeService) injectSecrets(ctx context.Context, project *types.Proje } content = env } + + if config.UID == "" && config.GID == "" { + if ctrConfig == nil { + ctr, err := s.apiClient().ContainerInspect(ctx, id) + if err != nil { + return err + } + ctrConfig = ctr.Config + } + + parts := strings.Split(ctrConfig.User, ":") + if len(parts) > 0 { + config.UID = parts[0] + } + if len(parts) > 1 { + config.GID = parts[1] + } + } + b, err := createTar(content, types.FileReferenceConfig(config)) if err != nil { return err @@ -69,6 +90,7 @@ func (s *composeService) injectSecrets(ctx context.Context, project *types.Proje } func (s *composeService) injectConfigs(ctx context.Context, project *types.Project, service types.ServiceConfig, id string) error { + var ctrConfig *container.Config for _, config := range service.Configs { file := project.Configs[config.Source] content := file.Content @@ -91,6 +113,24 @@ func (s *composeService) injectConfigs(ctx context.Context, project *types.Proje config.Target = "/" + config.Source } + if config.UID == "" && config.GID == "" { + if ctrConfig == nil { + ctr, err := s.apiClient().ContainerInspect(ctx, id) + if err != nil { + return err + } + ctrConfig = ctr.Config + } + + parts := strings.Split(ctrConfig.User, ":") + if len(parts) > 0 { + config.UID = parts[0] + } + if len(parts) > 1 { + config.GID = parts[1] + } + } + b, err := createTar(content, types.FileReferenceConfig(config)) if err != nil { return err diff --git a/pkg/e2e/fixtures/env-secret/compose.yaml b/pkg/e2e/fixtures/env-secret/compose.yaml index 51052d36d21..ef272419a40 100644 --- a/pkg/e2e/fixtures/env-secret/compose.yaml +++ b/pkg/e2e/fixtures/env-secret/compose.yaml @@ -14,6 +14,23 @@ services: mode: 0440 command: cat /run/secrets/bar + bar: + image: alpine + user: "1005" + secrets: + - source: secret + target: bar + command: cat /run/secrets/bar + + zot: + image: alpine + user: "1005:1005" + secrets: + - source: secret + target: bar + command: cat /run/secrets/bar + + secrets: secret: environment: SECRET diff --git a/pkg/e2e/secrets_test.go b/pkg/e2e/secrets_test.go index 3e3895112a3..febfdd2b7ab 100644 --- a/pkg/e2e/secrets_test.go +++ b/pkg/e2e/secrets_test.go @@ -40,6 +40,21 @@ func TestSecretFromEnv(t *testing.T) { }) res.Assert(t, icmd.Expected{Out: "-r--r----- 1 1005 1005"}) }) + t.Run("secret uid from user", func(t *testing.T) { + res := icmd.RunCmd(c.NewDockerComposeCmd(t, "-f", "./fixtures/env-secret/compose.yaml", "run", "bar", "ls", "-al", "/var/run/secrets/bar"), + func(cmd *icmd.Cmd) { + cmd.Env = append(cmd.Env, "SECRET=BAR") + }) + res.Assert(t, icmd.Expected{Out: "-r--r--r-- 1 1005 root"}) + }) + t.Run("secret uid:gid from user", func(t *testing.T) { + res := icmd.RunCmd(c.NewDockerComposeCmd(t, "-f", "./fixtures/env-secret/compose.yaml", "run", "zot", "ls", "-al", "/var/run/secrets/bar"), + func(cmd *icmd.Cmd) { + cmd.Env = append(cmd.Env, "SECRET=BAR") + }) + res.Assert(t, icmd.Expected{Out: "-r--r--r-- 1 1005 1005"}) + }) + } func TestSecretFromInclude(t *testing.T) { From 2681ed17a74b11867f4fe8d0a2275eaa9e5e39e4 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Wed, 15 Oct 2025 11:41:59 +0200 Subject: [PATCH 17/19] mutualize code from injectSecrets / injectConfigs Signed-off-by: Nicolas De Loof --- pkg/compose/secrets.go | 186 ++++++++++++++++++++++------------------ pkg/e2e/secrets_test.go | 14 ++- 2 files changed, 113 insertions(+), 87 deletions(-) diff --git a/pkg/compose/secrets.go b/pkg/compose/secrets.go index bd5ca1a90a7..42ccd4e76b8 100644 --- a/pkg/compose/secrets.go +++ b/pkg/compose/secrets.go @@ -29,121 +29,139 @@ import ( "github.com/docker/docker/api/types/container" ) +type mountType string + +const ( + secretMount mountType = "secret" + configMount mountType = "config" +) + func (s *composeService) injectSecrets(ctx context.Context, project *types.Project, service types.ServiceConfig, id string) error { - var ctrConfig *container.Config - for _, config := range service.Secrets { - file := project.Secrets[config.Source] - if file.Environment == "" { - continue - } + return s.injectFileReferences(ctx, project, service, id, secretMount) +} - if service.ReadOnly { - return fmt.Errorf("cannot create secret %q in read-only service %s: `file` is the sole supported option", file.Name, service.Name) - } +func (s *composeService) injectConfigs(ctx context.Context, project *types.Project, service types.ServiceConfig, id string) error { + return s.injectFileReferences(ctx, project, service, id, configMount) +} - if config.Target == "" { - config.Target = "/run/secrets/" + config.Source - } else if !isAbsTarget(config.Target) { - config.Target = "/run/secrets/" + config.Target - } +func (s *composeService) injectFileReferences(ctx context.Context, project *types.Project, service types.ServiceConfig, id string, mountType mountType) error { + mounts, sources := s.getFilesAndMap(project, service, mountType) + var ctrConfig *container.Config - content := file.Content + for _, mount := range mounts { + content, err := s.resolveFileContent(project, sources[mount.Source], mountType) + if err != nil { + return err + } if content == "" { - env, ok := project.Environment[file.Environment] - if !ok { - return fmt.Errorf("environment variable %q required by secret %q is not set", file.Environment, file.Name) - } - content = env + continue } - if config.UID == "" && config.GID == "" { - if ctrConfig == nil { - ctr, err := s.apiClient().ContainerInspect(ctx, id) - if err != nil { - return err - } - ctrConfig = ctr.Config - } - - parts := strings.Split(ctrConfig.User, ":") - if len(parts) > 0 { - config.UID = parts[0] - } - if len(parts) > 1 { - config.GID = parts[1] - } + if service.ReadOnly { + return fmt.Errorf("cannot create %s %q in read-only service %s: `file` is the sole supported option", mountType, sources[mount.Source].Name, service.Name) } - b, err := createTar(content, types.FileReferenceConfig(config)) + s.setDefaultTarget(&mount, mountType) + + ctrConfig, err = s.setFileOwnership(ctx, id, &mount, ctrConfig) if err != nil { return err } - err = s.apiClient().CopyToContainer(ctx, id, "/", &b, container.CopyToContainerOptions{ - CopyUIDGID: config.UID != "" || config.GID != "", - }) - if err != nil { + if err := s.copyFileToContainer(ctx, id, content, mount); err != nil { return err } } return nil } -func (s *composeService) injectConfigs(ctx context.Context, project *types.Project, service types.ServiceConfig, id string) error { - var ctrConfig *container.Config - for _, config := range service.Configs { - file := project.Configs[config.Source] - content := file.Content - if file.Environment != "" { - env, ok := project.Environment[file.Environment] - if !ok { - return fmt.Errorf("environment variable %q required by config %q is not set", file.Environment, file.Name) - } - content = env +func (s *composeService) getFilesAndMap(project *types.Project, service types.ServiceConfig, mountType mountType) ([]types.FileReferenceConfig, map[string]types.FileObjectConfig) { + var files []types.FileReferenceConfig + var fileMap map[string]types.FileObjectConfig + + switch mountType { + case secretMount: + files = make([]types.FileReferenceConfig, len(service.Secrets)) + for i, config := range service.Secrets { + files[i] = types.FileReferenceConfig(config) } - if content == "" { - continue + fileMap = make(map[string]types.FileObjectConfig) + for k, v := range project.Secrets { + fileMap[k] = types.FileObjectConfig(v) } - - if service.ReadOnly { - return fmt.Errorf("cannot create config %q in read-only service %s: `file` is the sole supported option", file.Name, service.Name) + case configMount: + files = make([]types.FileReferenceConfig, len(service.Configs)) + for i, config := range service.Configs { + files[i] = types.FileReferenceConfig(config) } - - if config.Target == "" { - config.Target = "/" + config.Source + fileMap = make(map[string]types.FileObjectConfig) + for k, v := range project.Configs { + fileMap[k] = types.FileObjectConfig(v) } + } + return files, fileMap +} - if config.UID == "" && config.GID == "" { - if ctrConfig == nil { - ctr, err := s.apiClient().ContainerInspect(ctx, id) - if err != nil { - return err - } - ctrConfig = ctr.Config - } - - parts := strings.Split(ctrConfig.User, ":") - if len(parts) > 0 { - config.UID = parts[0] - } - if len(parts) > 1 { - config.GID = parts[1] - } +func (s *composeService) resolveFileContent(project *types.Project, source types.FileObjectConfig, mountType mountType) (string, error) { + if source.Content != "" { + // inlined, or already resolved by include + return source.Content, nil + } + if source.Environment != "" { + env, ok := project.Environment[source.Environment] + if !ok { + return "", fmt.Errorf("environment variable %q required by %s %q is not set", source.Environment, mountType, source.Name) } + return env, nil + } + return "", nil +} - b, err := createTar(content, types.FileReferenceConfig(config)) - if err != nil { - return err +func (s *composeService) setDefaultTarget(file *types.FileReferenceConfig, mountType mountType) { + if file.Target == "" { + if mountType == secretMount { + file.Target = "/run/secrets/" + file.Source + } else { + file.Target = "/" + file.Source } + } else if mountType == secretMount && !isAbsTarget(file.Target) { + file.Target = "/run/secrets/" + file.Target + } +} - err = s.apiClient().CopyToContainer(ctx, id, "/", &b, container.CopyToContainerOptions{ - CopyUIDGID: config.UID != "" || config.GID != "", - }) +func (s *composeService) setFileOwnership(ctx context.Context, id string, file *types.FileReferenceConfig, ctrConfig *container.Config) (*container.Config, error) { + if file.UID != "" || file.GID != "" { + return ctrConfig, nil + } + + if ctrConfig == nil { + ctr, err := s.apiClient().ContainerInspect(ctx, id) if err != nil { - return err + return nil, err } + ctrConfig = ctr.Config } - return nil + + parts := strings.Split(ctrConfig.User, ":") + if len(parts) > 0 { + file.UID = parts[0] + } + if len(parts) > 1 { + file.GID = parts[1] + } + + return ctrConfig, nil +} + +func (s *composeService) copyFileToContainer(ctx context.Context, id, content string, file types.FileReferenceConfig) error { + b, err := createTar(content, file) + if err != nil { + return err + } + + return s.apiClient().CopyToContainer(ctx, id, "/", &b, container.CopyToContainerOptions{ + CopyUIDGID: true, + }) } func createTar(env string, config types.FileReferenceConfig) (bytes.Buffer, error) { diff --git a/pkg/e2e/secrets_test.go b/pkg/e2e/secrets_test.go index febfdd2b7ab..dde21061b36 100644 --- a/pkg/e2e/secrets_test.go +++ b/pkg/e2e/secrets_test.go @@ -17,6 +17,7 @@ package e2e import ( + "strings" "testing" "gotest.tools/v3/icmd" @@ -41,20 +42,27 @@ func TestSecretFromEnv(t *testing.T) { res.Assert(t, icmd.Expected{Out: "-r--r----- 1 1005 1005"}) }) t.Run("secret uid from user", func(t *testing.T) { - res := icmd.RunCmd(c.NewDockerComposeCmd(t, "-f", "./fixtures/env-secret/compose.yaml", "run", "bar", "ls", "-al", "/var/run/secrets/bar"), + res := c.RunDockerCmd(t, "version", "--format", "{{ .Server.Version }}") + if strings.HasPrefix(res.Stdout(), "27.") { + t.Skip("USER uid:gid is not supported") + } + res = icmd.RunCmd(c.NewDockerComposeCmd(t, "-f", "./fixtures/env-secret/compose.yaml", "run", "bar", "ls", "-al", "/var/run/secrets/bar"), func(cmd *icmd.Cmd) { cmd.Env = append(cmd.Env, "SECRET=BAR") }) res.Assert(t, icmd.Expected{Out: "-r--r--r-- 1 1005 root"}) }) t.Run("secret uid:gid from user", func(t *testing.T) { - res := icmd.RunCmd(c.NewDockerComposeCmd(t, "-f", "./fixtures/env-secret/compose.yaml", "run", "zot", "ls", "-al", "/var/run/secrets/bar"), + res := c.RunDockerCmd(t, "version", "--format", "{{ .Server.Version }}") + if strings.HasPrefix(res.Stdout(), "27.") { + t.Skip("USER uid:gid is not supported") + } + res = icmd.RunCmd(c.NewDockerComposeCmd(t, "-f", "./fixtures/env-secret/compose.yaml", "run", "zot", "ls", "-al", "/var/run/secrets/bar"), func(cmd *icmd.Cmd) { cmd.Env = append(cmd.Env, "SECRET=BAR") }) res.Assert(t, icmd.Expected{Out: "-r--r--r-- 1 1005 1005"}) }) - } func TestSecretFromInclude(t *testing.T) { From 27f59d7f42b60a680b85e63b63a40a6130e53f99 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Fri, 10 Oct 2025 13:46:51 +0200 Subject: [PATCH 18/19] Detect failure to access os.TempDir Signed-off-by: Nicolas De Loof --- go.mod | 2 +- pkg/compose/build_bake.go | 23 +++++++++++++++++------ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index 0cd8f7a7b6b..1386dcc4626 100644 --- a/go.mod +++ b/go.mod @@ -24,6 +24,7 @@ require ( github.com/fsnotify/fsevents v0.2.0 github.com/go-viper/mapstructure/v2 v2.4.0 github.com/google/go-cmp v0.7.0 + github.com/google/uuid v1.6.0 github.com/hashicorp/go-version v1.7.0 github.com/jonboulle/clockwork v0.5.0 github.com/mattn/go-shellwords v1.0.12 @@ -108,7 +109,6 @@ require ( github.com/google/gnostic-models v0.6.8 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect diff --git a/pkg/compose/build_bake.go b/pkg/compose/build_bake.go index 299c6f2f431..5a84a81ef8d 100644 --- a/pkg/compose/build_bake.go +++ b/pkg/compose/build_bake.go @@ -25,7 +25,7 @@ import ( "errors" "fmt" "io" - "math/rand" + "io/fs" "os" "os/exec" "path/filepath" @@ -40,6 +40,7 @@ import ( "github.com/docker/cli/cli/command/image/build" "github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/progress" + "github.com/google/uuid" "github.com/moby/buildkit/client" gitutil "github.com/moby/buildkit/frontend/dockerfile/dfgitutil" "github.com/moby/buildkit/util/progress/progressui" @@ -282,13 +283,20 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project } logrus.Debugf("bake build config:\n%s", string(b)) + tmpdir := os.TempDir() var metadataFile string for { // we don't use os.CreateTemp here as we need a temporary file name, but don't want it actually created // as bake relies on atomicwriter and this creates conflict during rename - metadataFile = filepath.Join(os.TempDir(), fmt.Sprintf("compose-build-metadataFile-%d.json", rand.Int31())) - if _, err = os.Stat(metadataFile); os.IsNotExist(err) { - break + metadataFile = filepath.Join(tmpdir, fmt.Sprintf("compose-build-metadataFile-%s.json", uuid.New().String())) + if _, err = os.Stat(metadataFile); err != nil { + if os.IsNotExist(err) { + break + } + var pathError *fs.PathError + if errors.As(err, &pathError) { + return nil, fmt.Errorf("can't acces os.tempDir %s: %w", tmpdir, pathError.Err) + } } } defer func() { @@ -361,9 +369,12 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project if readErr != nil { if readErr == io.EOF { break - } else { - return nil, fmt.Errorf("failed to execute bake: %w", readErr) } + if errors.Is(readErr, os.ErrClosed) { + logrus.Debugf("bake stopped") + break + } + return nil, fmt.Errorf("failed to execute bake: %w", readErr) } decoder := json.NewDecoder(strings.NewReader(line)) var status client.SolveStatus From be8c7e6c60c6dad95527fa4a338a1490ea5f3f97 Mon Sep 17 00:00:00 2001 From: Guillaume Lours <705411+glours@users.noreply.github.com> Date: Fri, 17 Oct 2025 14:51:31 +0200 Subject: [PATCH 19/19] make CTRL+Z a no-op operation on Windows Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com> --- cmd/formatter/shortcut.go | 2 +- cmd/formatter/shortcut_unix.go | 25 +++++++++++++++++++++++++ cmd/formatter/shortcut_windows.go | 25 +++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 cmd/formatter/shortcut_unix.go create mode 100644 cmd/formatter/shortcut_windows.go diff --git a/cmd/formatter/shortcut.go b/cmd/formatter/shortcut.go index 4a5e774737e..079cd1a015d 100644 --- a/cmd/formatter/shortcut.go +++ b/cmd/formatter/shortcut.go @@ -322,7 +322,7 @@ func (lk *LogKeyboard) HandleKeyEvents(ctx context.Context, event keyboard.KeyEv // will notify main thread to kill and will handle gracefully lk.signalChannel <- syscall.SIGINT case keyboard.KeyCtrlZ: - _ = syscall.Kill(0, syscall.SIGSTOP) + handleCtrlZ() case keyboard.KeyEnter: newLine() lk.printNavigationMenu() diff --git a/cmd/formatter/shortcut_unix.go b/cmd/formatter/shortcut_unix.go new file mode 100644 index 00000000000..0baa3a949cc --- /dev/null +++ b/cmd/formatter/shortcut_unix.go @@ -0,0 +1,25 @@ +//go:build !windows + +/* + Copyright 2024 Docker Compose CLI authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package formatter + +import "syscall" + +func handleCtrlZ() { + _ = syscall.Kill(0, syscall.SIGSTOP) +} diff --git a/cmd/formatter/shortcut_windows.go b/cmd/formatter/shortcut_windows.go new file mode 100644 index 00000000000..c642d069a4a --- /dev/null +++ b/cmd/formatter/shortcut_windows.go @@ -0,0 +1,25 @@ +//go:build windows + +/* + Copyright 2024 Docker Compose CLI authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package formatter + +// handleCtrlZ is a no-op on Windows as SIGSTOP is not supported +func handleCtrlZ() { + // Windows doesn't support SIGSTOP/SIGCONT signals + // Ctrl+Z behavior is handled differently by the Windows terminal +} \ No newline at end of file