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/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 diff --git a/cmd/formatter/shortcut.go b/cmd/formatter/shortcut.go index d1c7363196f..079cd1a015d 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: + 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 diff --git a/go.mod b/go.mod index 94af1e3a539..1386dcc4626 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 @@ -14,21 +14,22 @@ 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/cli v28.5.0+incompatible + 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.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 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 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 @@ -53,7 +54,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 @@ -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 @@ -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 50d0566bfc1..807b1e602e8 100644 --- a/go.sum +++ b/go.sum @@ -126,17 +126,17 @@ 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/cli v28.5.0+incompatible h1:crVqLrtKsrhC9c00ythRx435H8LiQnUKRtJLRR+Auxk= -github.com/docker/cli v28.5.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +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= 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= 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= @@ -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= @@ -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= @@ -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= 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) } diff --git a/pkg/compose/build_bake.go b/pkg/compose/build_bake.go index 71735dcb06a..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" @@ -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/google/uuid" "github.com/moby/buildkit/client" gitutil "github.com/moby/buildkit/frontend/dockerfile/dfgitutil" "github.com/moby/buildkit/util/progress/progressui" @@ -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,22 +277,26 @@ 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 } 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() { @@ -308,15 +309,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) @@ -371,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 @@ -511,7 +512,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{ diff --git a/pkg/compose/publish.go b/pkg/compose/publish.go index 8f3fcb8197d..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 @@ -221,6 +225,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 @@ -250,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 } 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..42ccd4e76b8 100644 --- a/pkg/compose/secrets.go +++ b/pkg/compose/secrets.go @@ -22,88 +22,146 @@ import ( "context" "fmt" "strconv" + "strings" "time" "github.com/compose-spec/compose-go/v2/types" "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 { - for _, config := range service.Secrets { - file := project.Secrets[config.Source] - if file.Environment == "" { + return s.injectFileReferences(ctx, project, service, id, secretMount) +} + +func (s *composeService) injectConfigs(ctx context.Context, project *types.Project, service types.ServiceConfig, id string) error { + return s.injectFileReferences(ctx, project, service, id, configMount) +} + +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 + + for _, mount := range mounts { + content, err := s.resolveFileContent(project, sources[mount.Source], mountType) + if err != nil { + return err + } + if content == "" { continue } 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) + 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) } - if config.Target == "" { - config.Target = "/run/secrets/" + config.Source - } else if !isAbsTarget(config.Target) { - config.Target = "/run/secrets/" + config.Target - } + s.setDefaultTarget(&mount, mountType) - content := file.Content - 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 - } - b, err := createTar(content, types.FileReferenceConfig(config)) + 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 { - 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) } + fileMap = make(map[string]types.FileObjectConfig) + for k, v := range project.Configs { + fileMap[k] = types.FileObjectConfig(v) + } + } + return files, fileMap +} - if config.Target == "" { - config.Target = "/" + config.Source +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 + } +} + +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 + } - err = s.apiClient().CopyToContainer(ctx, id, "/", &b, container.CopyToContainerOptions{ - CopyUIDGID: config.UID != "" || config.GID != "", - }) + 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/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() { 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") 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} 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..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" @@ -40,6 +41,28 @@ 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 := 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 := 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) {