diff --git a/pkg/e2e/compose_build_test.go b/pkg/e2e/compose_build_test.go index 9483a10e..68df43f8 100644 --- a/pkg/e2e/compose_build_test.go +++ b/pkg/e2e/compose_build_test.go @@ -18,7 +18,6 @@ package e2e import ( "net/http" - "os" "strings" "testing" "time" @@ -81,11 +80,6 @@ func TestLocalComposeBuild(t *testing.T) { }) t.Run("build failed with ssh default value", func(t *testing.T) { - //unset SSH_AUTH_SOCK to be sure we don't have a default value for the SSH Agent - defaultSSHAUTHSOCK := os.Getenv("SSH_AUTH_SOCK") - os.Unsetenv("SSH_AUTH_SOCK") //nolint:errcheck - defer os.Setenv("SSH_AUTH_SOCK", defaultSSHAUTHSOCK) //nolint:errcheck - res := c.RunDockerComposeCmdNoCheck(t, "--project-directory", "fixtures/build-test", "build", "--ssh", "") res.Assert(t, icmd.Expected{ ExitCode: 1, diff --git a/pkg/e2e/compose_environment_test.go b/pkg/e2e/compose_environment_test.go index e70a2536..3314617c 100644 --- a/pkg/e2e/compose_environment_test.go +++ b/pkg/e2e/compose_environment_test.go @@ -17,7 +17,6 @@ package e2e import ( - "os" "strings" "testing" @@ -43,13 +42,11 @@ func TestEnvPriority(t *testing.T) { // 4. Dockerfile // 5. Variable is not defined t.Run("compose file priority", func(t *testing.T) { - os.Setenv("WHEREAMI", "shell") //nolint:errcheck - defer os.Unsetenv("WHEREAMI") //nolint:errcheck - - res := c.RunDockerComposeCmd(t, "-f", "./fixtures/environment/env-priority/compose-with-env.yaml", - "--project-directory", projectDir, "--env-file", "./fixtures/environment/env-priority/.env.override", - "run", "--rm", "-e", "WHEREAMI", "env-compose-priority") - + cmd := c.NewDockerComposeCmd(t, "-f", "./fixtures/environment/env-priority/compose-with-env.yaml", + "--project-directory", projectDir, "--env-file", "./fixtures/environment/env-priority/.env.override", "run", + "--rm", "-e", "WHEREAMI", "env-compose-priority") + cmd.Env = append(cmd.Env, "WHEREAMI=shell") + res := icmd.RunCmd(cmd) assert.Equal(t, strings.TrimSpace(res.Stdout()), "Compose File") }) @@ -60,12 +57,11 @@ func TestEnvPriority(t *testing.T) { // 4. Dockerfile // 5. Variable is not defined t.Run("shell priority", func(t *testing.T) { - os.Setenv("WHEREAMI", "shell") //nolint:errcheck - defer os.Unsetenv("WHEREAMI") //nolint:errcheck - - res := c.RunDockerComposeCmd(t, "-f", "./fixtures/environment/env-priority/compose.yaml", - "--project-directory", projectDir, "--env-file", "./fixtures/environment/env-priority/.env.override", - "run", "--rm", "-e", "WHEREAMI", "env-compose-priority") + cmd := c.NewDockerComposeCmd(t, "-f", "./fixtures/environment/env-priority/compose.yaml", "--project-directory", + projectDir, "--env-file", "./fixtures/environment/env-priority/.env.override", "run", "--rm", "-e", + "WHEREAMI", "env-compose-priority") + cmd.Env = append(cmd.Env, "WHEREAMI=shell") + res := icmd.RunCmd(cmd) assert.Equal(t, strings.TrimSpace(res.Stdout()), "shell") }) @@ -137,11 +133,10 @@ func TestEnvInterpolation(t *testing.T) { // 4. Dockerfile // 5. Variable is not defined t.Run("shell priority from run command", func(t *testing.T) { - os.Setenv("WHEREAMI", "shell") //nolint:errcheck - defer os.Unsetenv("WHEREAMI") //nolint:errcheck - res := c.RunDockerComposeCmd(t, "-f", "./fixtures/environment/env-interpolation/compose.yaml", + cmd := c.NewDockerComposeCmd(t, "-f", "./fixtures/environment/env-interpolation/compose.yaml", "--project-directory", projectDir, "config") - + cmd.Env = append(cmd.Env, "WHEREAMI=shell") + res := icmd.RunCmd(cmd) res.Assert(t, icmd.Expected{Out: `IMAGE: default_env:shell`}) }) } diff --git a/pkg/e2e/ddev_test.go b/pkg/e2e/ddev_test.go index 37293afb..00b890d1 100644 --- a/pkg/e2e/ddev_test.go +++ b/pkg/e2e/ddev_test.go @@ -19,11 +19,13 @@ package e2e import ( "fmt" "os" + "os/exec" "path/filepath" "runtime" "strings" "testing" + "github.com/stretchr/testify/require" "gotest.tools/v3/assert" ) @@ -31,26 +33,38 @@ const ddevVersion = "v1.19.1" func TestComposeRunDdev(t *testing.T) { if !composeStandaloneMode { - t.Skip("Not running on standalone mode.") + t.Skip("Not running in plugin mode - ddev only supports invoking standalone `docker-compose`") } if runtime.GOOS == "windows" { t.Skip("Running on Windows. Skipping...") } - _ = os.Setenv("DDEV_DEBUG", "true") - c := NewParallelCLI(t) - dir, err := os.MkdirTemp("", t.Name()+"-") - assert.NilError(t, err) + // ddev shells out to `docker` and `docker-compose` (standalone), so a + // temporary directory is created with symlinks to system Docker and the + // locally-built standalone Compose binary to use as PATH + requiredTools := []string{ + findToolInPath(t, DockerExecutableName), + ComposeStandalonePath(t), + findToolInPath(t, "tar"), + findToolInPath(t, "gzip"), + } + pathDir := t.TempDir() + for _, tool := range requiredTools { + require.NoError(t, os.Symlink(tool, filepath.Join(pathDir, filepath.Base(tool))), + "Could not create symlink for %q", tool) + } - // ddev needs to be able to find mkcert to figure out where certs are. - _ = os.Setenv("PATH", fmt.Sprintf("%s:%s", os.Getenv("PATH"), dir)) + c := NewCLI(t, WithEnv( + "DDEV_DEBUG=true", + fmt.Sprintf("PATH=%s", pathDir), + )) - siteName := filepath.Base(dir) + ddevDir := t.TempDir() + siteName := filepath.Base(ddevDir) t.Cleanup(func() { - _ = c.RunCmdInDir(t, dir, "./ddev", "delete", "-Oy") - _ = c.RunCmdInDir(t, dir, "./ddev", "poweroff") - _ = os.RemoveAll(dir) + _ = c.RunCmdInDir(t, ddevDir, "./ddev", "delete", "-Oy") + _ = c.RunCmdInDir(t, ddevDir, "./ddev", "poweroff") }) osName := "linux" @@ -59,28 +73,34 @@ func TestComposeRunDdev(t *testing.T) { } compressedFilename := fmt.Sprintf("ddev_%s-%s.%s.tar.gz", osName, runtime.GOARCH, ddevVersion) - c.RunCmdInDir(t, dir, "curl", "-LO", - fmt.Sprintf("https://github.com/drud/ddev/releases/download/%s/%s", - ddevVersion, - compressedFilename)) + c.RunCmdInDir(t, ddevDir, "curl", "-LO", fmt.Sprintf("https://github.com/drud/ddev/releases/download/%s/%s", + ddevVersion, + compressedFilename)) - c.RunCmdInDir(t, dir, "tar", "-xzf", compressedFilename) + c.RunCmdInDir(t, ddevDir, "tar", "-xzf", compressedFilename) // Create a simple index.php we can test against. - c.RunCmdInDir(t, dir, "sh", "-c", "echo 'index.php") + c.RunCmdInDir(t, ddevDir, "sh", "-c", "echo 'index.php") - c.RunCmdInDir(t, dir, "./ddev", "config", "--auto") - c.RunCmdInDir(t, dir, "./ddev", "config", "global", "--use-docker-compose-from-path") - vRes := c.RunCmdInDir(t, dir, "./ddev", "version") + c.RunCmdInDir(t, ddevDir, "./ddev", "config", "--auto") + c.RunCmdInDir(t, ddevDir, "./ddev", "config", "global", "--use-docker-compose-from-path") + vRes := c.RunCmdInDir(t, ddevDir, "./ddev", "version") out := vRes.Stdout() fmt.Printf("ddev version: %s\n", out) - c.RunCmdInDir(t, dir, "./ddev", "poweroff") + c.RunCmdInDir(t, ddevDir, "./ddev", "poweroff") - c.RunCmdInDir(t, dir, "./ddev", "start", "-y") + c.RunCmdInDir(t, ddevDir, "./ddev", "start", "-y") - curlRes := c.RunCmdInDir(t, dir, "curl", "-sSL", fmt.Sprintf("http://%s.ddev.site", siteName)) + curlRes := c.RunCmdInDir(t, ddevDir, "curl", "-sSL", fmt.Sprintf("http://%s.ddev.site", siteName)) out = curlRes.Stdout() fmt.Println(out) assert.Assert(t, strings.Contains(out, "ddev is working"), "Could not start project") } + +func findToolInPath(t testing.TB, name string) string { + t.Helper() + binPath, err := exec.LookPath(name) + require.NoError(t, err, "Could not find %q in path", name) + return binPath +} diff --git a/pkg/e2e/framework.go b/pkg/e2e/framework.go index fabf90cd..2a5880ad 100644 --- a/pkg/e2e/framework.go +++ b/pkg/e2e/framework.go @@ -30,8 +30,8 @@ import ( "time" "github.com/pkg/errors" + "github.com/stretchr/testify/require" "gotest.tools/v3/assert" - is "gotest.tools/v3/assert/cmp" "gotest.tools/v3/icmd" "gotest.tools/v3/poll" @@ -59,19 +59,59 @@ func init() { // CLI is used to wrap the CLI for end to end testing type CLI struct { + // ConfigDir for Docker configuration (set as DOCKER_CONFIG) ConfigDir string + + // HomeDir for tools that look for user files (set as HOME) + HomeDir string + + // env overrides to apply to every invoked command + // + // To populate, use WithEnv when creating a CLI instance. + env []string } -// NewParallelCLI returns a configured CLI with t.Parallel() set -func NewParallelCLI(t *testing.T) *CLI { +// CLIOption to customize behavior for all commands for a CLI instance. +type CLIOption func(c *CLI) + +// NewParallelCLI marks the parent test as parallel and returns a CLI instance +// suitable for usage across child tests. +func NewParallelCLI(t *testing.T, opts ...CLIOption) *CLI { + t.Helper() t.Parallel() - return NewCLI(t) + return NewCLI(t, opts...) } -// NewCLI returns a CLI to use for E2E tests -func NewCLI(t testing.TB) *CLI { - d, err := ioutil.TempDir("", "") - assert.Check(t, is.Nil(err)) +// NewCLI creates a CLI instance for running E2E tests. +func NewCLI(t testing.TB, opts ...CLIOption) *CLI { + t.Helper() + + configDir := t.TempDir() + initializePlugins(t, configDir) + + c := &CLI{ + ConfigDir: configDir, + HomeDir: t.TempDir(), + } + + for _, opt := range opts { + opt(c) + } + + return c +} + +// WithEnv sets environment variables that will be passed to commands. +func WithEnv(env ...string) CLIOption { + return func(c *CLI) { + c.env = append(c.env, env...) + } +} + +// initializePlugins copies the necessary plugin files to the temporary config +// directory for the test. +func initializePlugins(t testing.TB, d string) { + t.Helper() t.Cleanup(func() { if t.Failed() { @@ -101,8 +141,6 @@ func NewCLI(t testing.TB) *CLI { panic(err) } } - - return &CLI{ConfigDir: d} } func dirContents(dir string) []string { @@ -154,28 +192,33 @@ func CopyFile(sourceFile string, destinationFile string) error { return err } +// BaseEnvironment provides the minimal environment variables used across all +// Docker / Compose commands. +func (c *CLI) BaseEnvironment() []string { + return []string{ + "HOME=" + c.HomeDir, + "USER=" + os.Getenv("USER"), + "DOCKER_CONFIG=" + c.ConfigDir, + "KUBECONFIG=invalid", + } +} + // NewCmd creates a cmd object configured with the test environment set func (c *CLI) NewCmd(command string, args ...string) icmd.Cmd { - env := append(os.Environ(), - "DOCKER_CONFIG="+c.ConfigDir, - "KUBECONFIG=invalid", - ) return icmd.Cmd{ Command: append([]string{command}, args...), - Env: env, + Env: append(c.BaseEnvironment(), c.env...), } } // NewCmdWithEnv creates a cmd object configured with the test environment set with additional env vars func (c *CLI) NewCmdWithEnv(envvars []string, command string, args ...string) icmd.Cmd { - env := append(os.Environ(), - append(envvars, - "DOCKER_CONFIG="+c.ConfigDir, - "KUBECONFIG=invalid")..., - ) + // base env -> CLI overrides -> cmd overrides + cmdEnv := append(c.BaseEnvironment(), c.env...) + cmdEnv = append(cmdEnv, envvars...) return icmd.Cmd{ Command: append([]string{command}, args...), - Env: env, + Env: cmdEnv, } } @@ -234,13 +277,34 @@ func (c *CLI) RunDockerComposeCmd(t testing.TB, args ...string) *icmd.Result { // RunDockerComposeCmdNoCheck runs a docker compose command, don't presume of any expectation and returns a result func (c *CLI) RunDockerComposeCmdNoCheck(t testing.TB, args ...string) *icmd.Result { + return icmd.RunCmd(c.NewDockerComposeCmd(t, args...)) +} + +// NewDockerComposeCmd creates a command object for Compose, either in plugin +// or standalone mode (based on build tags). +func (c *CLI) NewDockerComposeCmd(t testing.TB, args ...string) icmd.Cmd { + t.Helper() if composeStandaloneMode { - composeBinary, err := findExecutable(DockerComposeExecutableName, []string{"../../bin", "../../../bin"}) - assert.NilError(t, err) - return icmd.RunCmd(c.NewCmd(composeBinary, args...)) + return c.NewCmd(ComposeStandalonePath(t), args...) } args = append([]string{"compose"}, args...) - return icmd.RunCmd(c.NewCmd(DockerExecutableName, args...)) + return c.NewCmd(DockerExecutableName, args...) +} + +// ComposeStandalonePath returns the path to the locally-built Compose +// standalone binary from the repo. +// +// This function will fail the test immediately if invoked when not running +// in standalone test mode. +func ComposeStandalonePath(t testing.TB) string { + t.Helper() + if !composeStandaloneMode { + require.Fail(t, "Not running in standalone mode") + } + composeBinary, err := findExecutable(DockerComposeExecutableName, []string{"../../bin", "../../../bin"}) + require.NoError(t, err, "Could not find standalone Compose binary (%q)", + DockerComposeExecutableName) + return composeBinary } // StdoutContains returns a predicate on command result expecting a string in stdout