diff --git a/e2e/tests/readconfiguration/readconfiguration.go b/e2e/tests/readconfiguration/readconfiguration.go index 0070be1ec..db64eafe4 100644 --- a/e2e/tests/readconfiguration/readconfiguration.go +++ b/e2e/tests/readconfiguration/readconfiguration.go @@ -470,4 +470,46 @@ var _ = ginkgo.Describe("read-configuration command", ginkgo.Label("read-configu }) framework.ExpectError(err) }, ginkgo.SpecTimeout(framework.TimeoutShort())) + + ginkgo.DescribeTable("resolves shutdownAction per devcontainer spec", + ginkgo.Label("read-configuration"), + func(ctx context.Context, testdataDir, expected string) { + f := framework.NewDefaultFramework(initialDir + "/bin") + tempDir, err := framework.CopyToTempDirWithoutChdir(testdataDir) + framework.ExpectNoError(err) + ginkgo.DeferCleanup(func() { _ = os.RemoveAll(tempDir) }) + + stdout, _, err := f.ExecCommandCapture(ctx, []string{ + "read-configuration", + "--workspace-folder", tempDir, + "--include-merged-configuration", + }) + framework.ExpectNoError(err) + + var result map[string]any + err = json.Unmarshal([]byte(stdout), &result) + framework.ExpectNoError(err) + + merged, ok := result["mergedConfiguration"].(map[string]any) + gomega.Expect(ok).To(gomega.BeTrue(), "mergedConfiguration should be an object") + gomega.Expect(merged).To( + gomega.HaveKeyWithValue("shutdownAction", expected), + ) + }, + ginkgo.Entry("defaults to stopContainer for image-based config", + "tests/readconfiguration/testdata-shutdown-action", + "stopContainer", + ginkgo.SpecTimeout(framework.TimeoutShort()), + ), + ginkgo.Entry("defaults to stopCompose for docker-compose config", + "tests/readconfiguration/testdata-shutdown-action-compose", + "stopCompose", + ginkgo.SpecTimeout(framework.TimeoutShort()), + ), + ginkgo.Entry("preserves explicit none", + "tests/readconfiguration/testdata-shutdown-action-explicit", + "none", + ginkgo.SpecTimeout(framework.TimeoutShort()), + ), + ) }) diff --git a/e2e/tests/readconfiguration/testdata-shutdown-action-compose/.devcontainer.json b/e2e/tests/readconfiguration/testdata-shutdown-action-compose/.devcontainer.json new file mode 100644 index 000000000..e8ca08b2d --- /dev/null +++ b/e2e/tests/readconfiguration/testdata-shutdown-action-compose/.devcontainer.json @@ -0,0 +1,4 @@ +{ + "dockerComposeFile": "docker-compose.yml", + "service": "app" +} diff --git a/e2e/tests/readconfiguration/testdata-shutdown-action-compose/docker-compose.yml b/e2e/tests/readconfiguration/testdata-shutdown-action-compose/docker-compose.yml new file mode 100644 index 000000000..8fd583d54 --- /dev/null +++ b/e2e/tests/readconfiguration/testdata-shutdown-action-compose/docker-compose.yml @@ -0,0 +1,4 @@ +version: "3" +services: + app: + image: ghcr.io/devsy-org/test-images/base:ubuntu diff --git a/e2e/tests/readconfiguration/testdata-shutdown-action-explicit/.devcontainer.json b/e2e/tests/readconfiguration/testdata-shutdown-action-explicit/.devcontainer.json new file mode 100644 index 000000000..8d230561b --- /dev/null +++ b/e2e/tests/readconfiguration/testdata-shutdown-action-explicit/.devcontainer.json @@ -0,0 +1,4 @@ +{ + "image": "ghcr.io/devsy-org/test-images/base:ubuntu", + "shutdownAction": "none" +} diff --git a/e2e/tests/readconfiguration/testdata-shutdown-action/.devcontainer.json b/e2e/tests/readconfiguration/testdata-shutdown-action/.devcontainer.json new file mode 100644 index 000000000..351e8dcfe --- /dev/null +++ b/e2e/tests/readconfiguration/testdata-shutdown-action/.devcontainer.json @@ -0,0 +1,3 @@ +{ + "image": "ghcr.io/devsy-org/test-images/base:ubuntu" +} diff --git a/pkg/devcontainer/config/merge.go b/pkg/devcontainer/config/merge.go index 1bc50d9dd..b64d38e2a 100644 --- a/pkg/devcontainer/config/merge.go +++ b/pkg/devcontainer/config/merge.go @@ -140,6 +140,17 @@ func MergeConfiguration( ) mergedConfig.HostRequirements = mergeHostRequirements(reversed) + if mergedConfig.ShutdownAction == "" { + switch { + case copiedConfig.ShutdownAction != "": + mergedConfig.ShutdownAction = copiedConfig.ShutdownAction + case len(copiedConfig.DockerComposeFile) > 0: + mergedConfig.ShutdownAction = ShutdownActionStopCompose + default: + mergedConfig.ShutdownAction = ShutdownActionStopContainer + } + } + return mergedConfig, nil } diff --git a/pkg/devcontainer/config/merge_test.go b/pkg/devcontainer/config/merge_test.go index 3c45355d8..671034eed 100644 --- a/pkg/devcontainer/config/merge_test.go +++ b/pkg/devcontainer/config/merge_test.go @@ -516,3 +516,51 @@ func TestMergeLifestyleHooks_MultipleFeatures(t *testing.T) { t.Errorf("expected image hook last, got %v", got[2]) } } + +func TestMergeConfiguration_ShutdownActionDefault_ImageConfig(t *testing.T) { + cfg := &DevContainerConfig{ + ImageContainer: ImageContainer{Image: "ubuntu:latest"}, + } + merged, err := MergeConfiguration(cfg, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if merged.ShutdownAction != ShutdownActionStopContainer { + t.Errorf("ShutdownAction = %q, want %q", merged.ShutdownAction, ShutdownActionStopContainer) + } +} + +func TestMergeConfiguration_ShutdownActionDefault_ComposeConfig(t *testing.T) { + cfg := &DevContainerConfig{ + ComposeContainer: ComposeContainer{ + DockerComposeFile: []string{"docker-compose.yml"}, + Service: "app", + }, + } + merged, err := MergeConfiguration(cfg, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if merged.ShutdownAction != ShutdownActionStopCompose { + t.Errorf("ShutdownAction = %q, want %q", merged.ShutdownAction, ShutdownActionStopCompose) + } +} + +func TestMergeConfiguration_ShutdownActionExplicit_NotOverridden(t *testing.T) { + cfg := &DevContainerConfig{ + DevContainerConfigBase: DevContainerConfigBase{ + ShutdownAction: ShutdownActionNone, + }, + ComposeContainer: ComposeContainer{ + DockerComposeFile: []string{"docker-compose.yml"}, + Service: "app", + }, + } + merged, err := MergeConfiguration(cfg, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if merged.ShutdownAction != ShutdownActionNone { + t.Errorf("ShutdownAction = %q, want %q", merged.ShutdownAction, ShutdownActionNone) + } +}