From 8fd54f1594c60749b6234f793fca0aa8ae230975 Mon Sep 17 00:00:00 2001 From: Guillaume Lours Date: Mon, 27 Jul 2026 10:37:58 +0200 Subject: [PATCH] fix: tolerate missing env file on scale, watch and shell completion Follow-up to #13603: scale, watch and shell completion loaded the project without any tolerance option, so a missing env_file on a service not involved in the operation aborted the command, while up/exec/ps already tolerate this since #13156 and #13603. Mirror the WithServices pattern: load with WithoutEnvironmentResolution and resolve the environment once the project has been reduced to the selected services, so targeted services still get their env_file validated. Completion only needs names and never resolves. This also aligns the config hash of scale-created containers with up-created ones. Signed-off-by: Guillaume Lours --- cmd/compose/completion.go | 13 +++++++++---- cmd/compose/scale.go | 10 +++++++++- cmd/compose/watch.go | 10 +++++++++- pkg/e2e/env_file_test.go | 19 +++++++++++++++++++ 4 files changed, 46 insertions(+), 6 deletions(-) diff --git a/cmd/compose/completion.go b/cmd/compose/completion.go index 0d6c9fe4d40..7a472937c55 100644 --- a/cmd/compose/completion.go +++ b/cmd/compose/completion.go @@ -20,6 +20,7 @@ import ( "sort" "strings" + "github.com/compose-spec/compose-go/v2/cli" "github.com/docker/cli/cli/command" "github.com/spf13/cobra" @@ -44,7 +45,9 @@ func completeServiceNames(dockerCli command.Cli, p *ProjectOptions) validArgsFn return nil, cobra.ShellCompDirectiveNoFileComp } - project, _, err := p.ToProject(cmd.Context(), dockerCli, backend, nil) + // only service names are needed, so skip environment resolution: a missing + // env_file must not prevent completion + project, _, err := p.ToProject(cmd.Context(), dockerCli, backend, nil, cli.WithoutEnvironmentResolution) if err != nil { return nil, cobra.ShellCompDirectiveNoFileComp } @@ -90,7 +93,9 @@ func completeProfileNames(dockerCli command.Cli, p *ProjectOptions) validArgsFn return nil, cobra.ShellCompDirectiveNoFileComp } - project, _, err := p.ToProject(cmd.Context(), dockerCli, backend, nil) + // only profile names are needed, so skip environment resolution: a missing + // env_file must not prevent completion + project, _, err := p.ToProject(cmd.Context(), dockerCli, backend, nil, cli.WithoutEnvironmentResolution) if err != nil { return nil, cobra.ShellCompDirectiveNoFileComp } @@ -108,9 +113,9 @@ func completeProfileNames(dockerCli command.Cli, p *ProjectOptions) validArgsFn } } -func completeScaleArgs(cli command.Cli, p *ProjectOptions) cobra.CompletionFunc { +func completeScaleArgs(dockerCli command.Cli, p *ProjectOptions) cobra.CompletionFunc { return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - completions, directive := completeServiceNames(cli, p)(cmd, args, toComplete) + completions, directive := completeServiceNames(dockerCli, p)(cmd, args, toComplete) for i, completion := range completions { completions[i] = completion + "=" } diff --git a/cmd/compose/scale.go b/cmd/compose/scale.go index 8070858e9a1..ac1e25469ba 100644 --- a/cmd/compose/scale.go +++ b/cmd/compose/scale.go @@ -24,6 +24,7 @@ import ( "strconv" "strings" + "github.com/compose-spec/compose-go/v2/cli" "github.com/compose-spec/compose-go/v2/types" "github.com/docker/cli/cli/command" "github.com/spf13/cobra" @@ -67,7 +68,14 @@ func runScale(ctx context.Context, dockerCli command.Cli, backendOptions *Backen } services := slices.Sorted(maps.Keys(serviceReplicaTuples)) - project, _, err := opts.ToProject(ctx, dockerCli, backend, services) + project, _, err := opts.ToProject(ctx, dockerCli, backend, services, cli.WithoutEnvironmentResolution) + if err != nil { + return err + } + + // resolve environment after the project has been reduced to selected services, + // so env_file declared by unrelated services doesn't need to exist + project, err = project.WithServicesEnvironmentResolved(true) if err != nil { return err } diff --git a/cmd/compose/watch.go b/cmd/compose/watch.go index c60b243860e..039971ce23f 100644 --- a/cmd/compose/watch.go +++ b/cmd/compose/watch.go @@ -20,6 +20,7 @@ import ( "context" "fmt" + "github.com/compose-spec/compose-go/v2/cli" "github.com/compose-spec/compose-go/v2/types" "github.com/docker/cli/cli/command" "github.com/sirupsen/logrus" @@ -71,7 +72,14 @@ func runWatch(ctx context.Context, dockerCli command.Cli, backendOptions *Backen return err } - project, _, err := watchOpts.ToProject(ctx, dockerCli, backend, services) + project, _, err := watchOpts.ToProject(ctx, dockerCli, backend, services, cli.WithoutEnvironmentResolution) + if err != nil { + return err + } + + // resolve environment after the project has been reduced to selected services, + // so env_file declared by unrelated services doesn't need to exist + project, err = project.WithServicesEnvironmentResolved(true) if err != nil { return err } diff --git a/pkg/e2e/env_file_test.go b/pkg/e2e/env_file_test.go index 9fb3e0ac31d..865d7bfa124 100644 --- a/pkg/e2e/env_file_test.go +++ b/pkg/e2e/env_file_test.go @@ -42,6 +42,25 @@ func TestUnusedMissingEnvFile(t *testing.T) { c.RunDockerComposeCmd(t, "-f", "./fixtures/env_file/compose.yaml", "ps") c.RunDockerComposeCmd(t, "-f", "./fixtures/env_file/compose.yaml", "logs") c.RunDockerComposeCmd(t, "-f", "./fixtures/env_file/compose.yaml", "exec", "serviceA", "echo", "hello") + + // scale should work even with missing env file on a service not being scaled + c.RunDockerComposeCmd(t, "-f", "./fixtures/env_file/compose.yaml", "scale", "serviceA=2") + + // but scaling the service with the missing env file must still fail + res := c.RunDockerComposeCmdNoCheck(t, "-f", "./fixtures/env_file/compose.yaml", "scale", "serviceB=1") + res.Assert(t, icmd.Expected{ExitCode: 1, Err: "env file /doesnotexist/.env not found"}) + + // shell completion should list services even with missing env file. + // ComposeStandalonePath fails the test outside standalone mode, so only the + // plugin form can be the default here. + completeCmd := []string{DockerExecutableName, "__complete", "compose"} + if composeStandaloneMode { + completeCmd = []string{ComposeStandalonePath(t), "__complete"} + } + res = c.RunCmd(t, append(completeCmd, "-f", "./fixtures/env_file/compose.yaml", "exec", "")...) + res.Assert(t, icmd.Expected{Out: "serviceA"}) + res.Assert(t, icmd.Expected{Out: "serviceB"}) + c.RunDockerComposeCmd(t, "-f", "./fixtures/env_file/compose.yaml", "down") }