diff --git a/cmd/compose/compose.go b/cmd/compose/compose.go index 42416ff70a..17008a1751 100644 --- a/cmd/compose/compose.go +++ b/cmd/compose/compose.go @@ -29,8 +29,10 @@ import ( "syscall" "github.com/compose-spec/compose-go/v2/cli" + "github.com/compose-spec/compose-go/v2/dotenv" "github.com/compose-spec/compose-go/v2/loader" "github.com/compose-spec/compose-go/v2/types" + composegoutils "github.com/compose-spec/compose-go/v2/utils" "github.com/docker/buildx/util/logutil" dockercli "github.com/docker/cli/cli" "github.com/docker/cli/cli-plugins/manager" @@ -450,6 +452,11 @@ func RootCommand(dockerCli command.Cli, backend Backend) *cobra.Command { //noli if verbose { logrus.SetLevel(logrus.TraceLevel) } + + err := setEnvWithDotEnv(opts) + if err != nil { + return err + } if noAnsi { if ansi != "auto" { return errors.New(`cannot specify DEPRECATED "--no-ansi" and "--ansi". Please use only "--ansi"`) @@ -541,7 +548,7 @@ func RootCommand(dockerCli command.Cli, backend Backend) *cobra.Command { //noli } // dry run detection - ctx, err := backend.DryRunMode(ctx, dryRun) + ctx, err = backend.DryRunMode(ctx, dryRun) if err != nil { return err } @@ -641,6 +648,30 @@ func RootCommand(dockerCli command.Cli, backend Backend) *cobra.Command { //noli return c } +func setEnvWithDotEnv(opts ProjectOptions) error { + options, err := cli.NewProjectOptions(opts.ConfigPaths, + cli.WithWorkingDirectory(opts.ProjectDir), + cli.WithOsEnv, + cli.WithEnvFiles(opts.EnvFiles...), + cli.WithDotEnv, + ) + if err != nil { + return nil + } + envFromFile, err := dotenv.GetEnvFromFile(composegoutils.GetAsEqualsMap(os.Environ()), options.EnvFiles) + if err != nil { + return nil + } + for k, v := range envFromFile { + if _, ok := os.LookupEnv(k); !ok { + if err = os.Setenv(k, v); err != nil { + return nil + } + } + } + return err +} + var printerModes = []string{ ui.ModeAuto, ui.ModeTTY, diff --git a/pkg/compose/containers.go b/pkg/compose/containers.go index 3eb0270dff..e898ce848c 100644 --- a/pkg/compose/containers.go +++ b/pkg/compose/containers.go @@ -118,17 +118,16 @@ func isRunning() containerPredicate { } } -func isNotService(services ...string) containerPredicate { - return func(c moby.Container) bool { - service := c.Labels[api.ServiceLabel] - return !utils.StringContains(services, service) - } -} - // isOrphaned is a predicate to select containers without a matching service definition in compose project func isOrphaned(project *types.Project) containerPredicate { services := append(project.ServiceNames(), project.DisabledServiceNames()...) return func(c moby.Container) bool { + // One-off container + v, ok := c.Labels[api.OneoffLabel] + if ok && v == "True" { + return c.State == ContainerExited || c.State == ContainerDead + } + // Service that is not defined in the compose model service := c.Labels[api.ServiceLabel] return !utils.StringContains(services, service) } diff --git a/pkg/compose/create.go b/pkg/compose/create.go index b48df9d03b..4c3e1099b6 100644 --- a/pkg/compose/create.go +++ b/pkg/compose/create.go @@ -101,8 +101,7 @@ func (s *composeService) create(ctx context.Context, project *types.Project, opt return err } - allServiceNames := append(project.ServiceNames(), project.DisabledServiceNames()...) - orphans := observedState.filter(isNotService(allServiceNames...)) + orphans := observedState.filter(isOrphaned(project)) if len(orphans) > 0 && !options.IgnoreOrphans { if options.RemoveOrphans { err := s.removeContainers(ctx, orphans, nil, false) diff --git a/pkg/compose/down.go b/pkg/compose/down.go index baaff789b9..cecb0583a8 100644 --- a/pkg/compose/down.go +++ b/pkg/compose/down.go @@ -327,6 +327,10 @@ func (s *composeService) stopAndRemoveContainer(ctx context.Context, container m w := progress.ContextWriter(ctx) eventName := getContainerProgressName(container) err := s.stopContainer(ctx, w, container, timeout) + if errdefs.IsNotFound(err) { + w.Event(progress.RemovedEvent(eventName)) + return nil + } if err != nil { return err } diff --git a/pkg/compose/kill_test.go b/pkg/compose/kill_test.go index 2d56027066..c51b603ebd 100644 --- a/pkg/compose/kill_test.go +++ b/pkg/compose/kill_test.go @@ -112,6 +112,7 @@ func testContainer(service string, id string, oneOff bool) moby.Container { ID: id, Names: []string{name}, Labels: containerLabels(service, oneOff), + State: ContainerExited, } } diff --git a/pkg/e2e/compose_run_test.go b/pkg/e2e/compose_run_test.go index 69b20ab2f4..8a19f3d318 100644 --- a/pkg/e2e/compose_run_test.go +++ b/pkg/e2e/compose_run_test.go @@ -37,7 +37,7 @@ func TestLocalComposeRun(t *testing.T) { "Hello one more time") lines = Lines(res.Stdout()) assert.Equal(t, lines[len(lines)-1], "Hello one more time", res.Stdout()) - assert.Assert(t, !strings.Contains(res.Combined(), "orphan")) + assert.Assert(t, strings.Contains(res.Combined(), "orphan")) }) t.Run("check run container exited", func(t *testing.T) { diff --git a/pkg/e2e/fixtures/orphans/.env b/pkg/e2e/fixtures/orphans/.env new file mode 100644 index 0000000000..717e3306ba --- /dev/null +++ b/pkg/e2e/fixtures/orphans/.env @@ -0,0 +1 @@ +COMPOSE_REMOVE_ORPHANS=true diff --git a/pkg/e2e/fixtures/orphans/compose.yaml b/pkg/e2e/fixtures/orphans/compose.yaml new file mode 100644 index 0000000000..33dbac0d26 --- /dev/null +++ b/pkg/e2e/fixtures/orphans/compose.yaml @@ -0,0 +1,7 @@ +services: + orphan: + profiles: [run] + image: alpine + command: echo hello + test: + image: nginx:alpine diff --git a/pkg/e2e/orphans_test.go b/pkg/e2e/orphans_test.go new file mode 100644 index 0000000000..e5ec2aba0a --- /dev/null +++ b/pkg/e2e/orphans_test.go @@ -0,0 +1,39 @@ +/* + 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 e2e + +import ( + "strings" + "testing" + + "gotest.tools/v3/assert" +) + +func TestRemoveOrphans(t *testing.T) { + c := NewCLI(t) + + const projectName = "compose-e2e-orphans" + + c.RunDockerComposeCmd(t, "-f", "./fixtures/orphans/compose.yaml", "-p", projectName, "run", "orphan") + res := c.RunDockerComposeCmd(t, "-p", projectName, "ps", "--all") + assert.Check(t, strings.Contains(res.Combined(), "compose-e2e-orphans-orphan-run-")) + + c.RunDockerComposeCmd(t, "-f", "./fixtures/orphans/compose.yaml", "-p", projectName, "up", "-d") + + res = c.RunDockerComposeCmd(t, "-p", projectName, "ps", "--all") + assert.Check(t, !strings.Contains(res.Combined(), "compose-e2e-orphans-orphan-run-")) +}