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 <glours@users.noreply.github.com>
This commit is contained in:
Guillaume Lours 2026-07-27 10:37:58 +02:00 committed by Guillaume Lours
parent 7bdc31c5de
commit d8370536f0
4 changed files with 46 additions and 6 deletions

View file

@ -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 + "="
}

View file

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

View file

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

View file

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