From 14acc35814afcb9bd4f78c119c76a3ea9aad6789 Mon Sep 17 00:00:00 2001 From: maks2134 Date: Tue, 17 Mar 2026 00:16:00 +0300 Subject: [PATCH 1/3] Fix deadlock in ttyWriter.Done() Resolves race condition between main thread calling Done() and UI thread calling printWithDimensions(). The issue was that Done() held the mutex while sending to the done channel, but the UI thread needed the same mutex to process the done signal. Fixed by sending the done signal before acquiring the mutex, allowing the UI thread to receive the signal and release any held locks. Fixes #13639 --- .idea/.gitignore | 8 ++++++++ .idea/compose.iml | 9 +++++++++ .idea/golinter.xml | 7 +++++++ .idea/material_theme_project_new.xml | 10 ++++++++++ .idea/modules.xml | 8 ++++++++ .idea/vcs.xml | 6 ++++++ cmd/display/tty.go | 2 +- cmd/display/tty_test.go | 21 +++++++++++++++++++++ 8 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/compose.iml create mode 100644 .idea/golinter.xml create mode 100644 .idea/material_theme_project_new.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 00000000000..13566b81b01 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/compose.iml b/.idea/compose.iml new file mode 100644 index 00000000000..5e764c4f0b9 --- /dev/null +++ b/.idea/compose.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/golinter.xml b/.idea/golinter.xml new file mode 100644 index 00000000000..1ccf3ec6d93 --- /dev/null +++ b/.idea/golinter.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/.idea/material_theme_project_new.xml b/.idea/material_theme_project_new.xml new file mode 100644 index 00000000000..b1a83ad3544 --- /dev/null +++ b/.idea/material_theme_project_new.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 00000000000..2efec7ef046 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 00000000000..35eb1ddfbbc --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/cmd/display/tty.go b/cmd/display/tty.go index 0726ce27049..39e5fc946b2 100644 --- a/cmd/display/tty.go +++ b/cmd/display/tty.go @@ -176,13 +176,13 @@ func (w *ttyWriter) Start(ctx context.Context, operation string) { func (w *ttyWriter) Done(operation string, success bool) { w.print() + w.done <- true w.mtx.Lock() defer w.mtx.Unlock() if w.ticker != nil { w.ticker.Stop() } w.operation = "" - w.done <- true } func (w *ttyWriter) On(events ...api.Resource) { diff --git a/cmd/display/tty_test.go b/cmd/display/tty_test.go index 0bbd35f2a29..f6a1a66a98e 100644 --- a/cmd/display/tty_test.go +++ b/cmd/display/tty_test.go @@ -18,6 +18,7 @@ package display import ( "bytes" + "context" "strings" "sync" "testing" @@ -422,3 +423,23 @@ func TestLenAnsi(t *testing.T) { }) } } + +func TestDoneDeadlockFix(t *testing.T) { + w, _ := newTestWriter() + addTask(w, "test-task", "Working", "details", api.Working) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + w.Start(ctx, "test") + done := make(chan bool) + go func() { + w.Done("test", true) + done <- true + }() + + select { + case <-done: + case <-time.After(5 * time.Second): + t.Fatal("Deadlock detected: Done() did not complete within 5 seconds") + } +} From c5cc6e647eb01c59630852ee2fb0be04f8604c2c Mon Sep 17 00:00:00 2001 From: maks2134 Date: Wed, 18 Mar 2026 18:56:33 +0300 Subject: [PATCH 2/3] Fix docker compose ps to honor psFormat setting in .docker/config.json - Change format flag default from "table" to empty string to allow config override - Add logic to use PsFormat from config when no format argument provided - Fall back to "table" format when neither PsFormat nor format arg is set - Add warning when both --format and --quiet flags are used - Add test to verify format flag default value Fixes #13643 Signed-off-by: maks2134 --- .idea/material_theme_project_new.xml | 10 ------ cmd/compose/ps.go | 9 ++++-- cmd/compose/ps_format_test.go | 47 ++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 12 deletions(-) delete mode 100644 .idea/material_theme_project_new.xml create mode 100644 cmd/compose/ps_format_test.go diff --git a/.idea/material_theme_project_new.xml b/.idea/material_theme_project_new.xml deleted file mode 100644 index b1a83ad3544..00000000000 --- a/.idea/material_theme_project_new.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/cmd/compose/ps.go b/cmd/compose/ps.go index 2528fccacfb..e79a8bcef89 100644 --- a/cmd/compose/ps.go +++ b/cmd/compose/ps.go @@ -81,7 +81,7 @@ func psCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *Backend ValidArgsFunction: completeServiceNames(dockerCli, p), } flags := psCmd.Flags() - flags.StringVar(&opts.Format, "format", "table", cliflags.FormatHelp) + flags.StringVar(&opts.Format, "format", "", cliflags.FormatHelp) flags.StringVar(&opts.Filter, "filter", "", "Filter services by a property (supported filters: status)") flags.StringArrayVar(&opts.Status, "status", []string{}, "Filter services by status. Values: [paused | restarting | removing | running | dead | created | exited]") flags.BoolVarP(&opts.Quiet, "quiet", "q", false, "Only display IDs") @@ -152,9 +152,14 @@ func runPs(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOp return nil } - if opts.Format == "" { + if len(opts.Format) == 0 { opts.Format = dockerCli.ConfigFile().PsFormat } + if len(opts.Format) == 0 { + opts.Format = "table" + } else if opts.Quiet { + _, _ = dockerCli.Err().Write([]byte("WARNING: Ignoring custom format, because both --format and --quiet are set.\n")) + } containerCtx := cliformatter.Context{ Output: dockerCli.Out(), diff --git a/cmd/compose/ps_format_test.go b/cmd/compose/ps_format_test.go new file mode 100644 index 00000000000..dd5f8b68b19 --- /dev/null +++ b/cmd/compose/ps_format_test.go @@ -0,0 +1,47 @@ +/* + Copyright 2020 Docker Compose CLI authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package compose + +import ( + "testing" + + "github.com/docker/cli/cli/config/configfile" + "github.com/docker/cli/cli/streams" + "go.uber.org/mock/gomock" + "gotest.tools/v3/assert" + + "github.com/docker/compose/v5/pkg/mocks" +) + +func TestPsCommandDefaultFormat(t *testing.T) { + // Test that the format flag has empty string as default + projectOpts := &ProjectOptions{} + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + cli := mocks.NewMockCli(mockCtrl) + cli.EXPECT().ConfigFile().Return(configfile.New("test")).AnyTimes() + cli.EXPECT().Out().Return(&streams.Out{}).AnyTimes() + cli.EXPECT().Err().Return(&streams.Out{}).AnyTimes() + + backendOptions := &BackendOptions{} + cmd := psCommand(projectOpts, cli, backendOptions) + + // Check default value of format flag + formatFlag := cmd.Flags().Lookup("format") + assert.Equal(t, formatFlag.DefValue, "") +} From 5b111bbc706b20b315172716a45f72039181850a Mon Sep 17 00:00:00 2001 From: maks2134 Date: Fri, 20 Mar 2026 23:47:58 +0300 Subject: [PATCH 3/3] Fix: respect required: false for env_file in publish command Fixes #13648 The docker compose publish command was ignoring required: false setting on env_file entries, causing failures when optional env files were missing. Changes made: - Modified checkForSensitiveData() to skip env files with required: false - Updated processFile() to only process required env files into layers - Fixed checkEnvironmentVariables() to only consider required env files - Added comprehensive tests to verify the fix Signed-off-by: Maks Kozlov Signed-off-by: maks2134 --- pkg/compose/publish.go | 13 +++- pkg/compose/publish_test.go | 67 +++++++++++++++++++ .../publish/compose-required-false.yaml | 9 +++ pkg/compose/testdata/publish/existing.env | 1 + 4 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 pkg/compose/testdata/publish/compose-required-false.yaml create mode 100644 pkg/compose/testdata/publish/existing.env diff --git a/pkg/compose/publish.go b/pkg/compose/publish.go index fb466607559..4ec67a63263 100644 --- a/pkg/compose/publish.go +++ b/pkg/compose/publish.go @@ -251,6 +251,9 @@ func processFile(ctx context.Context, file string, project *types.Project, extFi } for name, service := range base.Services { for i, envFile := range service.EnvFiles { + if !envFile.Required { + continue + } hash := fmt.Sprintf("%x.env", sha256.Sum256([]byte(envFile.Path))) envFiles[envFile.Path] = hash f, err = transform.ReplaceEnvFile(f, name, i, hash) @@ -351,8 +354,11 @@ func (s *composeService) checkEnvironmentVariables(project *types.Project, optio errorList := map[string][]string{} for _, service := range project.Services { - if len(service.EnvFiles) > 0 { - errorList[service.Name] = append(errorList[service.Name], fmt.Sprintf("service %q has env_file declared.", service.Name)) + for _, envFile := range service.EnvFiles { + if envFile.Required { + errorList[service.Name] = append(errorList[service.Name], fmt.Sprintf("service %q has env_file declared.", service.Name)) + break + } } } @@ -438,6 +444,9 @@ func (s *composeService) checkForSensitiveData(project *types.Project) ([]secret for _, service := range project.Services { // Check env files for _, envFile := range service.EnvFiles { + if !envFile.Required { + continue + } findings, err := scan.ScanFile(envFile.Path) if err != nil { return nil, fmt.Errorf("failed to scan env file %s: %w", envFile.Path, err) diff --git a/pkg/compose/publish_test.go b/pkg/compose/publish_test.go index 8f91f663e69..b3ba8c77875 100644 --- a/pkg/compose/publish_test.go +++ b/pkg/compose/publish_test.go @@ -18,6 +18,7 @@ package compose import ( "slices" + "strings" "testing" "github.com/compose-spec/compose-go/v2/loader" @@ -100,3 +101,69 @@ services: return !slices.Contains([]string{".Data", ".Digest", ".Size"}, path.String()) }, cmp.Ignore())) } + +func Test_createLayers_withRequiredFalse(t *testing.T) { + project, err := loader.LoadWithContext(t.Context(), types.ConfigDetails{ + WorkingDir: "testdata/publish/", + Environment: types.Mapping{}, + ConfigFiles: []types.ConfigFile{ + { + Filename: "testdata/publish/compose-required-false.yaml", + }, + }, + }) + assert.NilError(t, err) + project.ComposeFiles = []string{"testdata/publish/compose-required-false.yaml"} + + service := &composeService{} + layers, err := service.createLayers(t.Context(), project, api.PublishOptions{ + WithEnvironment: true, + }) + assert.NilError(t, err) + + assert.Equal(t, len(layers), 2) + + assert.Equal(t, layers[0].Annotations["com.docker.compose.file"], "compose-required-false.yaml") + + assert.Equal(t, layers[1].MediaType, "application/vnd.docker.compose.envfile") + + envFileHash := layers[1].Annotations["com.docker.compose.envfile"] + assert.Assert(t, len(envFileHash) > 0) + assert.Assert(t, envFileHash != "missing.env") +} + +func Test_checkEnvironmentVariables_withRequiredFalse(t *testing.T) { + project := &types.Project{ + Services: types.Services{ + "test": { + Name: "test", + EnvFiles: []types.EnvFile{ + { + Path: "missing.env", + Required: false, + }, + { + Path: "existing.env", + Required: true, + }, + }, + }, + "test2": { + Name: "test2", + EnvFiles: []types.EnvFile{ + { + Path: "optional.env", + Required: false, + }, + }, + }, + }, + } + + service := &composeService{} + + err := service.checkEnvironmentVariables(project, api.PublishOptions{}) + assert.Assert(t, err != nil) + assert.Assert(t, strings.Contains(err.Error(), `service "test" has env_file declared.`)) + assert.Assert(t, !strings.Contains(err.Error(), `service "test2"`)) +} diff --git a/pkg/compose/testdata/publish/compose-required-false.yaml b/pkg/compose/testdata/publish/compose-required-false.yaml new file mode 100644 index 00000000000..64b94e38942 --- /dev/null +++ b/pkg/compose/testdata/publish/compose-required-false.yaml @@ -0,0 +1,9 @@ +name: test-required-false +services: + test: + image: test + env_file: + - path: missing.env + required: false + - path: existing.env + required: true diff --git a/pkg/compose/testdata/publish/existing.env b/pkg/compose/testdata/publish/existing.env new file mode 100644 index 00000000000..bda5e6e581f --- /dev/null +++ b/pkg/compose/testdata/publish/existing.env @@ -0,0 +1 @@ +EXISTING_VAR=value