diff --git a/cmd/agent/container/daemon.go b/cmd/agent/container/daemon.go index 1c47f9fd2..7a39ad67b 100644 --- a/cmd/agent/container/daemon.go +++ b/cmd/agent/container/daemon.go @@ -16,6 +16,7 @@ import ( "github.com/devsy-org/devsy/pkg/agent" config2 "github.com/devsy-org/devsy/pkg/config" agentd "github.com/devsy-org/devsy/pkg/daemon/agent" + "github.com/devsy-org/devsy/pkg/devcontainer/config" "github.com/devsy-org/devsy/pkg/log" "github.com/devsy-org/devsy/pkg/platform/client" "github.com/devsy-org/devsy/pkg/ts" @@ -46,7 +47,10 @@ func NewDaemonCmd() *cobra.Command { daemonCmd.Flags(). StringVar(&cmd.Config.Timeout, "timeout", "", "The timeout to stop the container after") daemonCmd.Flags(). - StringVar(&cmd.Config.ShutdownAction, "shutdown-action", "", "The shutdown action (none or stopContainer)") + StringVar( + &cmd.Config.ShutdownAction, "shutdown-action", "", + "The shutdown action (none, stopContainer, or stopCompose)", + ) return daemonCmd } @@ -99,7 +103,7 @@ func (cmd *DaemonCmd) Run(c *cobra.Command, args []string) error { } // Start timeout monitor unless shutdownAction is "none". - if timeoutDuration > 0 && cmd.Config.ShutdownAction != "none" { + if timeoutDuration > 0 && cmd.Config.ShutdownAction != config.ShutdownActionNone { tasksStarted = true g.Go(func() error { return runTimeoutMonitor(ctx, timeoutDuration) diff --git a/cmd/agent/daemon.go b/cmd/agent/daemon.go index b62114a9f..e1888d05e 100644 --- a/cmd/agent/daemon.go +++ b/cmd/agent/daemon.go @@ -11,6 +11,7 @@ import ( "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/pkg/agent" "github.com/devsy-org/devsy/pkg/client/clientimplementation" + "github.com/devsy-org/devsy/pkg/devcontainer/config" "github.com/devsy-org/devsy/pkg/driver/custom" "github.com/devsy-org/devsy/pkg/log" provider2 "github.com/devsy-org/devsy/pkg/provider" @@ -41,7 +42,10 @@ func NewDaemonCmd(flags *flags.GlobalFlags) *cobra.Command { daemonCmd.Flags(). StringVar(&cmd.Interval, "interval", "", "The interval how to poll workspaces") daemonCmd.Flags(). - StringVar(&cmd.ShutdownAction, "shutdown-action", "", "The shutdown action (none or stopContainer)") + StringVar( + &cmd.ShutdownAction, "shutdown-action", "", + "The shutdown action (none, stopContainer, or stopCompose)", + ) return daemonCmd } @@ -129,7 +133,7 @@ func (cmd *DaemonCmd) checkAndShutdown( latestActivity *time.Time, workspace *provider2.AgentWorkspaceInfo, ) { - if cmd.ShutdownAction == "none" { + if cmd.ShutdownAction == config.ShutdownActionNone { return } diff --git a/e2e/tests/up-docker-compose/helper.go b/e2e/tests/up-docker-compose/helper.go index 75fb13086..82b88db1c 100644 --- a/e2e/tests/up-docker-compose/helper.go +++ b/e2e/tests/up-docker-compose/helper.go @@ -109,6 +109,40 @@ func (tc *testContext) getAppContainer( return ids, detail, err } +func (tc *testContext) findAppAndSidecar( + ctx context.Context, + workspaceUID string, +) (appIDs, sidecarIDs []string) { + var err error + appIDs, err = findComposeContainer( + ctx, tc.dockerHelper, tc.composeHelper, workspaceUID, "app", + ) + framework.ExpectNoError(err) + gomega.Expect(appIDs).To(gomega.HaveLen(1)) + + sidecarIDs, err = findComposeContainer( + ctx, tc.dockerHelper, tc.composeHelper, workspaceUID, "sidecar", + ) + framework.ExpectNoError(err) + gomega.Expect(sidecarIDs).To(gomega.HaveLen(1)) + return appIDs, sidecarIDs +} + +func (tc *testContext) inspectRunningState( + ctx context.Context, + appIDs, sidecarIDs []string, +) (appRunning, sidecarRunning bool) { + var appDetails []container.InspectResponse + err := tc.dockerHelper.Inspect(ctx, appIDs, "container", &appDetails) + framework.ExpectNoError(err) + + var sidecarDetails []container.InspectResponse + err = tc.dockerHelper.Inspect(ctx, sidecarIDs, "container", &sidecarDetails) + framework.ExpectNoError(err) + + return appDetails[0].State.Running, sidecarDetails[0].State.Running +} + func (tc *testContext) verifyWorkspaceMount( ctx context.Context, workspace *provider2.Workspace, diff --git a/e2e/tests/up-docker-compose/testdata/docker-compose-shutdown-action-container/.devcontainer/devcontainer.json b/e2e/tests/up-docker-compose/testdata/docker-compose-shutdown-action-container/.devcontainer/devcontainer.json new file mode 100644 index 000000000..3de0278b7 --- /dev/null +++ b/e2e/tests/up-docker-compose/testdata/docker-compose-shutdown-action-container/.devcontainer/devcontainer.json @@ -0,0 +1,7 @@ +{ + "name": "shutdown-action-container-test", + "dockerComposeFile": "../docker-compose.yaml", + "service": "app", + "workspaceFolder": "/workspaces", + "shutdownAction": "stopContainer" +} diff --git a/e2e/tests/up-docker-compose/testdata/docker-compose-shutdown-action-container/docker-compose.yaml b/e2e/tests/up-docker-compose/testdata/docker-compose-shutdown-action-container/docker-compose.yaml new file mode 100644 index 000000000..f833a964e --- /dev/null +++ b/e2e/tests/up-docker-compose/testdata/docker-compose-shutdown-action-container/docker-compose.yaml @@ -0,0 +1,9 @@ +services: + app: + image: ghcr.io/devsy-org/test-images/go:1 + command: sleep infinity + volumes: + - .:/workspaces:cached + sidecar: + image: alpine:latest + command: sleep infinity diff --git a/e2e/tests/up-docker-compose/testdata/docker-compose-shutdown-action/.devcontainer/devcontainer.json b/e2e/tests/up-docker-compose/testdata/docker-compose-shutdown-action/.devcontainer/devcontainer.json new file mode 100644 index 000000000..f20bbcf71 --- /dev/null +++ b/e2e/tests/up-docker-compose/testdata/docker-compose-shutdown-action/.devcontainer/devcontainer.json @@ -0,0 +1,7 @@ +{ + "name": "shutdown-action-test", + "dockerComposeFile": "../docker-compose.yaml", + "service": "app", + "workspaceFolder": "/workspaces", + "shutdownAction": "stopCompose" +} diff --git a/e2e/tests/up-docker-compose/testdata/docker-compose-shutdown-action/docker-compose.yaml b/e2e/tests/up-docker-compose/testdata/docker-compose-shutdown-action/docker-compose.yaml new file mode 100644 index 000000000..f833a964e --- /dev/null +++ b/e2e/tests/up-docker-compose/testdata/docker-compose-shutdown-action/docker-compose.yaml @@ -0,0 +1,9 @@ +services: + app: + image: ghcr.io/devsy-org/test-images/go:1 + command: sleep infinity + volumes: + - .:/workspaces:cached + sidecar: + image: alpine:latest + command: sleep infinity diff --git a/e2e/tests/up-docker-compose/up_docker_compose.go b/e2e/tests/up-docker-compose/up_docker_compose.go index 364ae17bb..b762db989 100644 --- a/e2e/tests/up-docker-compose/up_docker_compose.go +++ b/e2e/tests/up-docker-compose/up_docker_compose.go @@ -557,5 +557,55 @@ var _ = ginkgo.Describe( gomega.Expect(strings.TrimSpace(buildArgs)). To(gomega.Equal("ghcr.io/devsy-org/test-images/go:1")) }, ginkgo.SpecTimeout(framework.TimeoutShort())) + + ginkgo.It( + "shutdownAction stopCompose stops all services", + func(ctx context.Context) { + tempDir, workspace, err := tc.setupAndStartWorkspace( + ctx, + "tests/up-docker-compose/testdata/docker-compose-shutdown-action", + ) + framework.ExpectNoError(err) + + appIDs, sidecarIDs := tc.findAppAndSidecar(ctx, workspace.UID) + + err = tc.f.DevsyStop(ctx, tempDir) + framework.ExpectNoError(err) + + appRunning, sidecarRunning := tc.inspectRunningState( + ctx, appIDs, sidecarIDs, + ) + gomega.Expect(appRunning). + To(gomega.BeFalse(), "app container should be stopped") + gomega.Expect(sidecarRunning). + To(gomega.BeFalse(), "sidecar container should be stopped") + }, + ginkgo.SpecTimeout(framework.TimeoutShort()), + ) + + ginkgo.It( + "shutdownAction stopContainer only stops main service", + func(ctx context.Context) { + tempDir, workspace, err := tc.setupAndStartWorkspace( + ctx, + "tests/up-docker-compose/testdata/docker-compose-shutdown-action-container", + ) + framework.ExpectNoError(err) + + appIDs, sidecarIDs := tc.findAppAndSidecar(ctx, workspace.UID) + + err = tc.f.DevsyStop(ctx, tempDir) + framework.ExpectNoError(err) + + appRunning, sidecarRunning := tc.inspectRunningState( + ctx, appIDs, sidecarIDs, + ) + gomega.Expect(appRunning). + To(gomega.BeFalse(), "app container should be stopped") + gomega.Expect(sidecarRunning). + To(gomega.BeTrue(), "sidecar container should still be running") + }, + ginkgo.SpecTimeout(framework.TimeoutShort()), + ) }, ) diff --git a/pkg/daemon/agent/daemon.go b/pkg/daemon/agent/daemon.go index 045e8f61c..83d367481 100644 --- a/pkg/daemon/agent/daemon.go +++ b/pkg/daemon/agent/daemon.go @@ -61,7 +61,7 @@ func BuildWorkspaceDaemonConfig( // build info isn't required in the workspace and can be omitted platformOptions.Build = nil - shutdownAction := "stopContainer" + shutdownAction := config.ShutdownActionStopContainer if mergedConfig.ShutdownAction != "" { shutdownAction = mergedConfig.ShutdownAction } diff --git a/pkg/daemon/agent/daemon_test.go b/pkg/daemon/agent/daemon_test.go index ac23cadc8..c80202cf4 100644 --- a/pkg/daemon/agent/daemon_test.go +++ b/pkg/daemon/agent/daemon_test.go @@ -17,17 +17,22 @@ func TestBuildWorkspaceDaemonConfig_ShutdownAction(t *testing.T) { { name: "defaults to stopContainer when empty", shutdownAction: "", - want: "stopContainer", + want: config.ShutdownActionStopContainer, }, { name: "preserves none", - shutdownAction: "none", - want: "none", + shutdownAction: config.ShutdownActionNone, + want: config.ShutdownActionNone, }, { name: "preserves stopContainer", - shutdownAction: "stopContainer", - want: "stopContainer", + shutdownAction: config.ShutdownActionStopContainer, + want: config.ShutdownActionStopContainer, + }, + { + name: "preserves stopCompose", + shutdownAction: config.ShutdownActionStopCompose, + want: config.ShutdownActionStopCompose, }, } diff --git a/pkg/devcontainer/config/config.go b/pkg/devcontainer/config/config.go index 66f8fac36..fe662bec5 100644 --- a/pkg/devcontainer/config/config.go +++ b/pkg/devcontainer/config/config.go @@ -419,6 +419,12 @@ type PortAttribute struct { Protocol string `json:"protocol,omitempty"` } +const ( + ShutdownActionNone = "none" + ShutdownActionStopContainer = "stopContainer" + ShutdownActionStopCompose = "stopCompose" +) + const ( AutoForwardIgnore = "ignore" AutoForwardNotify = "notify" diff --git a/pkg/devcontainer/delete.go b/pkg/devcontainer/delete.go index ffd521ddb..b14534aaa 100644 --- a/pkg/devcontainer/delete.go +++ b/pkg/devcontainer/delete.go @@ -48,23 +48,37 @@ func (r *runner) Stop(ctx context.Context) error { return nil } - if strings.ToLower(containerDetails.State.Status) == "running" { - if isDockerCompose, projectName := getDockerComposeProject( - containerDetails, - ); isDockerCompose { - err = r.stopDockerCompose(ctx, projectName) - if err != nil { - return err - } - } else { - err = r.Driver.StopDevContainer(ctx, r.ID) - if err != nil { - return err - } + if strings.ToLower(containerDetails.State.Status) != "running" { + return nil + } + + isCompose, projectName := getDockerComposeProject(containerDetails) + action := r.getShutdownAction(isCompose) + + switch action { + case config.ShutdownActionNone: + return nil + case config.ShutdownActionStopCompose: + if isCompose { + return r.stopDockerCompose(ctx, projectName) } + return r.Driver.StopDevContainer(ctx, r.ID) + default: + return r.Driver.StopDevContainer(ctx, r.ID) } +} - return nil +func (r *runner) getShutdownAction(isCompose bool) string { + if r.WorkspaceConfig != nil && + r.WorkspaceConfig.LastDevContainerConfig != nil && + r.WorkspaceConfig.LastDevContainerConfig.Config != nil && + r.WorkspaceConfig.LastDevContainerConfig.Config.ShutdownAction != "" { + return r.WorkspaceConfig.LastDevContainerConfig.Config.ShutdownAction + } + if isCompose { + return config.ShutdownActionStopCompose + } + return config.ShutdownActionStopContainer } func getDockerComposeProject(containerDetails *config.ContainerDetails) (bool, string) {