diff --git a/e2e/framework/env.go b/e2e/framework/env.go new file mode 100644 index 0000000000..902bfbb167 --- /dev/null +++ b/e2e/framework/env.go @@ -0,0 +1,45 @@ +package framework + +import ( + "os" + + "github.com/onsi/ginkgo/v2" +) + +// SetEnvForSpec sets name to value for the current spec. The prior state is +// restored in DeferCleanup; unset and set-to-empty are kept distinct. +func SetEnvForSpec(name, value string) { + restore, err := overrideEnv(name, &value) + ExpectNoError(err) + ginkgo.DeferCleanup(restore) +} + +// UnsetEnvForSpec unsets name for the current spec and restores the prior state +// in DeferCleanup. +func UnsetEnvForSpec(name string) { + restore, err := overrideEnv(name, nil) + ExpectNoError(err) + ginkgo.DeferCleanup(restore) +} + +// overrideEnv applies value (nil means unset) and returns a restore func that +// returns name to exactly its prior state. +func overrideEnv(name string, value *string) (func(), error) { + previous, hadPrevious := os.LookupEnv(name) + var err error + if value == nil { + err = os.Unsetenv(name) + } else { + err = os.Setenv(name, *value) + } + if err != nil { + return nil, err + } + return func() { + if hadPrevious { + _ = os.Setenv(name, previous) + return + } + _ = os.Unsetenv(name) + }, nil +} diff --git a/e2e/framework/env_test.go b/e2e/framework/env_test.go new file mode 100644 index 0000000000..193647cb62 --- /dev/null +++ b/e2e/framework/env_test.go @@ -0,0 +1,104 @@ +package framework + +import ( + "os" + "testing" + + "github.com/onsi/ginkgo/v2" + "github.com/onsi/gomega" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestOverrideEnvRestoresUnsetAfterSet(t *testing.T) { + const name = "DEVSY_E2E_TEST_ENV_UNSET_THEN_SET" + require.NoError(t, os.Unsetenv(name)) + + restore, err := overrideEnv(name, new("value")) + require.NoError(t, err) + assert.Equal(t, "value", os.Getenv(name)) + + restore() + _, present := os.LookupEnv(name) + assert.False(t, present, "unset prior state must come back as unset, not empty") +} + +func TestOverrideEnvRestoresEmptyAfterSet(t *testing.T) { + const name = "DEVSY_E2E_TEST_ENV_EMPTY_THEN_SET" + t.Setenv(name, "") + + restore, err := overrideEnv(name, new("value")) + require.NoError(t, err) + assert.Equal(t, "value", os.Getenv(name)) + + restore() + got, present := os.LookupEnv(name) + assert.True(t, present, "set-to-empty prior state must stay set") + assert.Empty(t, got) +} + +func TestOverrideEnvRestoresPreviousValue(t *testing.T) { + const name = "DEVSY_E2E_TEST_ENV_VALUE_THEN_SET" + t.Setenv(name, "original") + + restore, err := overrideEnv(name, new("replacement")) + require.NoError(t, err) + assert.Equal(t, "replacement", os.Getenv(name)) + + restore() + assert.Equal(t, "original", os.Getenv(name)) +} + +func TestOverrideEnvUnsetRestoresPreviousValue(t *testing.T) { + const name = "DEVSY_E2E_TEST_ENV_VALUE_THEN_UNSET" + t.Setenv(name, "original") + + restore, err := overrideEnv(name, nil) + require.NoError(t, err) + _, present := os.LookupEnv(name) + assert.False(t, present, "nil value must unset the variable") + + restore() + assert.Equal(t, "original", os.Getenv(name)) +} + +func TestEnvSpecHelpers(t *testing.T) { + gomega.RegisterFailHandler(ginkgo.Fail) + ginkgo.RunSpecs(t, "framework env helpers") +} + +var _ = ginkgo.Describe("scoped environment helpers", ginkgo.Ordered, func() { + const ( + setName = "DEVSY_E2E_TEST_SPEC_ENV_SET_THEN_RESTORE" + unsetName = "DEVSY_E2E_TEST_SPEC_ENV_VALUE_THEN_RESTORE" + ) + + ginkgo.BeforeAll(func() { + ginkgo.DeferCleanup(os.Unsetenv, setName) + ginkgo.DeferCleanup(os.Unsetenv, unsetName) + gomega.Expect(os.Unsetenv(setName)).To(gomega.Succeed()) + gomega.Expect(os.Setenv(unsetName, "original")).To(gomega.Succeed()) + }) + + ginkgo.It("sets the variable for the spec", func() { + SetEnvForSpec(setName, "scoped") + gomega.Expect(os.Getenv(setName)).To(gomega.Equal("scoped")) + }) + + ginkgo.It("restores the unset prior state after the previous spec", func() { + _, present := os.LookupEnv(setName) + gomega.Expect(present).To(gomega.BeFalse()) + }) + + ginkgo.It("unsets the variable for the spec", func() { + UnsetEnvForSpec(unsetName) + _, present := os.LookupEnv(unsetName) + gomega.Expect(present).To(gomega.BeFalse()) + }) + + ginkgo.It("restores the prior value after the previous spec", func() { + got, present := os.LookupEnv(unsetName) + gomega.Expect(present).To(gomega.BeTrue()) + gomega.Expect(got).To(gomega.Equal("original")) + }) +}) diff --git a/e2e/framework/secrets.go b/e2e/framework/secrets.go index fbd3cde234..f7e000bb36 100644 --- a/e2e/framework/secrets.go +++ b/e2e/framework/secrets.go @@ -1,26 +1,8 @@ package framework -import ( - "os" - - "github.com/onsi/ginkgo/v2" -) - // UseFileSecretsBackend forces the file backend so tests do not depend on an OS // keyring (unavailable in CI); env is restored on cleanup. func UseFileSecretsBackend() { - for k, v := range map[string]string{ - "DEVSY_SECRETS_BACKEND": "file", - "DEVSY_SECRETS_PASSPHRASE": "e2e-passphrase", - } { - prev, had := os.LookupEnv(k) - ExpectNoError(os.Setenv(k, v)) - ginkgo.DeferCleanup(func() { - if had { - _ = os.Setenv(k, prev) - } else { - _ = os.Unsetenv(k) - } - }) - } + SetEnvForSpec("DEVSY_SECRETS_BACKEND", "file") + SetEnvForSpec("DEVSY_SECRETS_PASSPHRASE", "e2e-passphrase") } diff --git a/e2e/tests/dockerbuildkit/docker_internal_buildkit.go b/e2e/tests/dockerbuildkit/docker_internal_buildkit.go index df70bddcaa..2205a281c3 100644 --- a/e2e/tests/dockerbuildkit/docker_internal_buildkit.go +++ b/e2e/tests/dockerbuildkit/docker_internal_buildkit.go @@ -118,19 +118,6 @@ func uniqueTag() string { return fmt.Sprintf("devsy-internal-buildkit:%d", time.Now().UnixNano()) } -// setProcessEnv overrides a process variable for one spec only. -func setProcessEnv(name, value string) { - previous, had := os.LookupEnv(name) - framework.ExpectNoError(os.Setenv(name, value)) - ginkgo.DeferCleanup(func() { - if had { - _ = os.Setenv(name, previous) - } else { - _ = os.Unsetenv(name) - } - }) -} - var _ = ginkgo.Describe( "docker internal buildkit transport", ginkgo.Label("docker-internal-buildkit"), @@ -260,8 +247,8 @@ var _ = ginkgo.Describe( func(ctx context.Context) { // Make the process daemon reachable (the same WSL daemon) so a // silent fallback would succeed. - setProcessEnv("DOCKER_HOST", sshHost) - setProcessEnv(dockerContextEnv, "") + framework.SetEnvForSpec("DOCKER_HOST", sshHost) + framework.SetEnvForSpec(dockerContextEnv, "") tag := uniqueTag() ginkgo.DeferCleanup(func(ctx context.Context) { @@ -347,8 +334,8 @@ var _ = ginkgo.Describe( "BuildDevContainer builds on the named Docker context's daemon despite a dead process DOCKER_HOST", ginkgo.SpecTimeout(framework.TimeoutLong()), func(ctx context.Context) { - setProcessEnv("DOCKER_HOST", deadProcessHost) - setProcessEnv(dockerContextEnv, "") + framework.SetEnvForSpec("DOCKER_HOST", deadProcessHost) + framework.SetEnvForSpec(dockerContextEnv, "") _, err := dockerRun(ctx, nil, "info") gomega.Expect(err).To(gomega.HaveOccurred(), "process-env daemon must be unreachable for this regression to mean anything") @@ -422,8 +409,8 @@ var _ = ginkgo.Describe( ginkgo.SpecTimeout(framework.TimeoutLong()), func(ctx context.Context) { // Make the process daemon reachable so a silent fallback would succeed. - setProcessEnv("DOCKER_HOST", sshHost) - setProcessEnv(dockerContextEnv, "") + framework.SetEnvForSpec("DOCKER_HOST", sshHost) + framework.SetEnvForSpec(dockerContextEnv, "") gomega.Expect(dockerOutput(ctx, nil, "info", "--format", "{{.ID}}")). To(gomega.Equal(wslID), "process daemon must be reachable for this negative control") diff --git a/e2e/tests/up-docker-compose/up_docker_compose.go b/e2e/tests/up-docker-compose/up_docker_compose.go index 8daac00df9..240aa91756 100644 --- a/e2e/tests/up-docker-compose/up_docker_compose.go +++ b/e2e/tests/up-docker-compose/up_docker_compose.go @@ -33,23 +33,6 @@ const ( composeFileSecretsDir = "/run/secrets" ) -func useComposeFileSecretsBackend() { - for name, value := range map[string]string{ - "DEVSY_SECRETS_BACKEND": "file", - "DEVSY_SECRETS_PASSPHRASE": "e2e-passphrase", - } { - previous, hadPrevious := os.LookupEnv(name) - framework.ExpectNoError(os.Setenv(name, value)) - ginkgo.DeferCleanup(func() { - if hadPrevious { - _ = os.Setenv(name, previous) - } else { - _ = os.Unsetenv(name) - } - }) - } -} - var _ = ginkgo.Describe( "devsy up docker compose test suite", ginkgo.Label("up-docker-compose"), @@ -73,7 +56,7 @@ var _ = ginkgo.Describe( ginkgo.It( "migrates an existing Compose workspace when a terminal secret is attached later", func(ctx context.Context) { - useComposeFileSecretsBackend() + framework.UseFileSecretsBackend() contextName := fmt.Sprintf("compose-late-secret-%d", time.Now().UnixNano()) framework.ExpectNoError(tc.f.DevsyContextCreate(ctx, contextName)) ginkgo.DeferCleanup(func(cleanupCtx context.Context) { @@ -164,7 +147,7 @@ var _ = ginkgo.Describe( ginkgo.It( "refreshes saved Compose files for a new file secret after the app container is removed", func(ctx context.Context) { - useComposeFileSecretsBackend() + framework.UseFileSecretsBackend() tempDir, workspace, err := tc.setupAndStartWorkspace( ctx, "tests/up-docker-compose/testdata/docker-compose-shutdown-action-container", @@ -231,7 +214,7 @@ var _ = ginkgo.Describe( ginkgo.It( "injects attached terminal secrets through a tmpfs mount", func(ctx context.Context) { - useComposeFileSecretsBackend() + framework.UseFileSecretsBackend() contextName := fmt.Sprintf("compose-secret-%d", time.Now().UnixNano()) framework.ExpectNoError(tc.f.DevsyContextCreate(ctx, contextName)) ginkgo.DeferCleanup(func(cleanupCtx context.Context) { diff --git a/e2e/tests/up/sops.go b/e2e/tests/up/sops.go index 6b2244c55d..fa801d0da4 100644 --- a/e2e/tests/up/sops.go +++ b/e2e/tests/up/sops.go @@ -147,9 +147,10 @@ var _ = ginkgo.Describe( "sops-failure", filepath.Join(tempDir, "secrets.enc.yaml"), ) - framework.ExpectNoError(os.Setenv("SOPS_AGE_KEY", "")) - framework.ExpectNoError( - os.Setenv("SOPS_AGE_KEY_FILE", filepath.Join(tempDir, "missing-age-key")), + framework.SetEnvForSpec("SOPS_AGE_KEY", "") + framework.SetEnvForSpec( + "SOPS_AGE_KEY_FILE", + filepath.Join(tempDir, "missing-age-key"), ) stdout, stderr, upErr := dtc.f.DevsyUpStreams( @@ -170,33 +171,9 @@ var _ = ginkgo.Describe( ) func useSOPSAgeIdentity() { - setSOPSEnv("SOPS_AGE_KEY", sopsE2EAgeIdentity) - unsetSOPSEnv("SOPS_AGE_KEY_FILE") - unsetSOPSEnv("SOPS_AGE_KEY_CMD") -} - -func setSOPSEnv(name, value string) { - previous, had := os.LookupEnv(name) - framework.ExpectNoError(os.Setenv(name, value)) - ginkgo.DeferCleanup(func() { - if had { - _ = os.Setenv(name, previous) - return - } - _ = os.Unsetenv(name) - }) -} - -func unsetSOPSEnv(name string) { - previous, had := os.LookupEnv(name) - framework.ExpectNoError(os.Unsetenv(name)) - ginkgo.DeferCleanup(func() { - if had { - _ = os.Setenv(name, previous) - return - } - _ = os.Unsetenv(name) - }) + framework.SetEnvForSpec("SOPS_AGE_KEY", sopsE2EAgeIdentity) + framework.UnsetEnvForSpec("SOPS_AGE_KEY_FILE") + framework.UnsetEnvForSpec("SOPS_AGE_KEY_CMD") } func registerSOPSSource(ctx context.Context, dtc *dockerTestContext, name, filePath string) {