From d1ce5a36b9c6c51a2fcfa803b6ed475fb98e6292 Mon Sep 17 00:00:00 2001 From: Crypt Keeper <64215+codefromthecrypt@users.noreply.github.com> Date: Thu, 1 Apr 2021 16:56:35 +0800 Subject: [PATCH] Rewrites trickiest cmd tests to help clarify what's happening (#151) Signed-off-by: Adrian Cole --- pkg/cmd/extension/build/build_suite_test.go | 27 - pkg/cmd/extension/build/cmd_test.go | 314 +++-- pkg/cmd/extension/clean/cmd_test.go | 314 +++-- pkg/cmd/extension/clean/test_suite_test.go | 27 - .../extension/clean/testdata/.licenserignore | 1 + .../.getenvoy/extension/extension.yaml | 14 + .../extension/toolchains/default.yaml | 1 + pkg/cmd/extension/push/cmd_test.go | 186 ++- pkg/cmd/extension/push/push_suite_test.go | 27 - pkg/cmd/extension/run/cmd_test.go | 1154 ++++++----------- pkg/cmd/extension/run/run_suite_test.go | 27 - pkg/cmd/extension/test/cmd_test.go | 314 +++-- pkg/cmd/extension/test/test_suite_test.go | 27 - .../.getenvoy/extension/extension.yaml | 28 + .../extension/toolchains/default.yaml | 15 + .../runtime/getenvoy/getenvoy_suite_test.go | 27 - .../example/runtime/getenvoy/runtime_test.go | 257 ++-- .../runtime/getenvoy/testdata/.licenserignore | 4 +- .../runtime/getenvoy/testdata/envoy/bin/envoy | 51 - .../examples/default/envoy.tmpl.yaml | 86 ++ .../extension/examples/default/example.yaml | 18 + .../extension/examples/default/extension.json | 1 + .../.getenvoy/extension/extension.yaml | 28 + .../examples/default/envoy.tmpl.yaml | 72 + .../extension/examples/default/example.yaml | 4 + .../extension/examples/default/extension.json | 1 + .../.getenvoy/extension/extension.yaml | 14 + .../getenvoy_extension_run/envoy.tmpl.yaml | 75 ++ .../getenvoy_extension_run/example.yaml | 4 + .../getenvoy_extension_run/extension.json | 1 + .../builtin/testdata/toolchain/docker | 15 +- .../toolchain/builtin/toolchain_test.go | 18 +- pkg/test/cmd/command.go | 234 ++++ pkg/test/cmd/{extension => }/context.go | 2 +- 34 files changed, 1661 insertions(+), 1727 deletions(-) delete mode 100644 pkg/cmd/extension/build/build_suite_test.go delete mode 100644 pkg/cmd/extension/clean/test_suite_test.go create mode 100644 pkg/cmd/extension/clean/testdata/.licenserignore create mode 100644 pkg/cmd/extension/clean/testdata/workspace/.getenvoy/extension/extension.yaml create mode 100644 pkg/cmd/extension/clean/testdata/workspace/.getenvoy/extension/toolchains/default.yaml delete mode 100644 pkg/cmd/extension/push/push_suite_test.go delete mode 100644 pkg/cmd/extension/run/run_suite_test.go delete mode 100644 pkg/cmd/extension/test/test_suite_test.go create mode 100644 pkg/cmd/extension/test/testdata/workspace/.getenvoy/extension/extension.yaml create mode 100644 pkg/cmd/extension/test/testdata/workspace/.getenvoy/extension/toolchains/default.yaml delete mode 100644 pkg/extension/workspace/example/runtime/getenvoy/getenvoy_suite_test.go delete mode 100755 pkg/extension/workspace/example/runtime/getenvoy/testdata/envoy/bin/envoy create mode 100644 pkg/extension/workspace/example/runtime/getenvoy/testdata/invalidWorkspace/.getenvoy/extension/examples/default/envoy.tmpl.yaml create mode 100644 pkg/extension/workspace/example/runtime/getenvoy/testdata/invalidWorkspace/.getenvoy/extension/examples/default/example.yaml create mode 100644 pkg/extension/workspace/example/runtime/getenvoy/testdata/invalidWorkspace/.getenvoy/extension/examples/default/extension.json create mode 100644 pkg/extension/workspace/example/runtime/getenvoy/testdata/invalidWorkspace/.getenvoy/extension/extension.yaml create mode 100644 pkg/extension/workspace/example/runtime/getenvoy/testdata/workspace/.getenvoy/extension/examples/default/envoy.tmpl.yaml create mode 100644 pkg/extension/workspace/example/runtime/getenvoy/testdata/workspace/.getenvoy/extension/examples/default/example.yaml create mode 100644 pkg/extension/workspace/example/runtime/getenvoy/testdata/workspace/.getenvoy/extension/examples/default/extension.json create mode 100644 pkg/extension/workspace/example/runtime/getenvoy/testdata/workspace/.getenvoy/extension/extension.yaml create mode 100644 pkg/extension/workspace/example/runtime/getenvoy/testdata/workspace/expected/getenvoy_extension_run/envoy.tmpl.yaml create mode 100644 pkg/extension/workspace/example/runtime/getenvoy/testdata/workspace/expected/getenvoy_extension_run/example.yaml create mode 100644 pkg/extension/workspace/example/runtime/getenvoy/testdata/workspace/expected/getenvoy_extension_run/extension.json create mode 100644 pkg/test/cmd/command.go rename pkg/test/cmd/{extension => }/context.go (98%) diff --git a/pkg/cmd/extension/build/build_suite_test.go b/pkg/cmd/extension/build/build_suite_test.go deleted file mode 100644 index d128c254..00000000 --- a/pkg/cmd/extension/build/build_suite_test.go +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2020 Tetrate -// -// 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 build_test - -import ( - "testing" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -func TestBuild(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Build Suite") -} diff --git a/pkg/cmd/extension/build/cmd_test.go b/pkg/cmd/extension/build/cmd_test.go index 01a5ede4..30cc6fd3 100644 --- a/pkg/cmd/extension/build/cmd_test.go +++ b/pkg/cmd/extension/build/cmd_test.go @@ -15,186 +15,162 @@ package build_test import ( - "bytes" "fmt" - "os" - "path/filepath" - "strings" + "os/user" + "testing" - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - "github.com/spf13/cobra" + "github.com/stretchr/testify/require" - "github.com/tetratelabs/getenvoy/pkg/cmd" - testcontext "github.com/tetratelabs/getenvoy/pkg/test/cmd/extension" + cmd2 "github.com/tetratelabs/getenvoy/pkg/test/cmd" cmdutil "github.com/tetratelabs/getenvoy/pkg/util/cmd" ) -var _ = Describe("getenvoy extension build", func() { +// relativeWorkspaceDir points to a usable pre-initialized workspace +const relativeWorkspaceDir = "testdata/workspace" - var dockerDir string - - BeforeEach(func() { - dir, err := filepath.Abs("../../../extension/workspace/toolchain/builtin/testdata/toolchain") - Expect(err).ToNot(HaveOccurred()) - dockerDir = dir - }) - - var pathBackup string - - BeforeEach(func() { - pathBackup = os.Getenv("PATH") - - // override PATH to overshadow `docker` executable during the test - path := strings.Join([]string{dockerDir, pathBackup}, string(filepath.ListSeparator)) - os.Setenv("PATH", path) - }) - - AfterEach(func() { - os.Setenv("PATH", pathBackup) - }) - - var cwdBackup string - - BeforeEach(func() { - cwd, err := os.Getwd() - Expect(err).ToNot(HaveOccurred()) - cwdBackup = cwd - }) - - AfterEach(func() { - if cwdBackup != "" { - Expect(os.Chdir(cwdBackup)).To(Succeed()) - } - }) - - testcontext.SetDefaultUser() // UID:GID == 1001:1002 - - var stdout *bytes.Buffer - var stderr *bytes.Buffer - - BeforeEach(func() { - stdout = new(bytes.Buffer) - stderr = new(bytes.Buffer) - }) - - var c *cobra.Command - - BeforeEach(func() { - c = cmd.NewRoot() - c.SetOut(stdout) - c.SetErr(stderr) - }) - - It("should validate value of --toolchain-container-image flag", func() { - By("running command") - c.SetArgs([]string{"extension", "build", "--toolchain-container-image", "?invalid value?"}) - err := cmdutil.Execute(c) - Expect(err).To(HaveOccurred()) - - By("verifying command output") - Expect(stdout.String()).To(BeEmpty()) - Expect(stderr.String()).To(Equal(`Error: "?invalid value?" is not a valid image name: invalid reference format - -Run 'getenvoy extension build --help' for usage. -`)) - }) - - It("should validate value of --toolchain-container-options flag", func() { - By("running command") - c.SetArgs([]string{"extension", "build", "--toolchain-container-options", "imbalanced ' quotes"}) - err := cmdutil.Execute(c) - Expect(err).To(HaveOccurred()) - - By("verifying command output") - Expect(stdout.String()).To(BeEmpty()) - Expect(stderr.String()).To(Equal(`Error: "imbalanced ' quotes" is not a valid command line string - -Run 'getenvoy extension build --help' for usage. -`)) - }) - - chdir := func(path string) string { - dir, err := filepath.Abs(path) - Expect(err).ToNot(HaveOccurred()) - - err = os.Chdir(dir) - Expect(err).ToNot(HaveOccurred()) - - return dir +func TestGetEnvoyExtensionBuildValidateFlag(t *testing.T) { + type testCase struct { + flag string + flagValue string + expectedErr string } - //nolint:lll - Context("inside a workspace directory", func() { - It("should succeed", func() { - By("changing to a workspace dir") - workspaceDir := chdir("testdata/workspace") - - By("running command") - c.SetArgs([]string{"extension", "build"}) - err := cmdutil.Execute(c) - Expect(err).ToNot(HaveOccurred()) - - By("verifying command output") - Expect(stdout.String()).To(Equal(fmt.Sprintf("%s/docker run -u 1001:1002 --rm -t -v %s:/source -w /source --init getenvoy/extension-rust-builder:latest build --output-file target/getenvoy/extension.wasm\n", dockerDir, workspaceDir))) - Expect(stderr.String()).To(Equal("docker stderr\n")) - }) - - It("should allow to override build image and add Docker cli options", func() { - By("changing to a workspace dir") - workspaceDir := chdir("testdata/workspace") - - By("running command") - c.SetArgs([]string{"extension", "build", - "--toolchain-container-image", "build/image", - "--toolchain-container-options", `-e 'VAR=VALUE' -v "/host:/container"`, - }) - err := cmdutil.Execute(c) - Expect(err).ToNot(HaveOccurred()) - - By("verifying command output") - Expect(stdout.String()).To(Equal(fmt.Sprintf("%s/docker run -u 1001:1002 --rm -t -v %s:/source -w /source --init -e VAR=VALUE -v /host:/container build/image build --output-file target/getenvoy/extension.wasm\n", dockerDir, workspaceDir))) - Expect(stderr.String()).To(Equal("docker stderr\n")) - }) - - It("should properly handle Docker build failing", func() { - By("changing to a workspace dir") - workspaceDir := chdir("testdata/workspace") - - By("running command") - c.SetArgs([]string{"extension", "build", - "--toolchain-container-image", "build/image", - "--toolchain-container-options", `-e EXIT_CODE=3`, - }) - err := cmdutil.Execute(c) - Expect(err).To(HaveOccurred()) - - By("verifying command output") - Expect(stdout.String()).To(Equal(fmt.Sprintf("%s/docker run -u 1001:1002 --rm -t -v %s:/source -w /source --init -e EXIT_CODE=3 build/image build --output-file target/getenvoy/extension.wasm\n", dockerDir, workspaceDir))) - Expect(stderr.String()).To(Equal(fmt.Sprintf(`docker stderr -Error: failed to build Envoy extension using "default" toolchain: failed to execute an external command "%s/docker run -u 1001:1002 --rm -t -v %s:/source -w /source --init -e EXIT_CODE=3 build/image build --output-file target/getenvoy/extension.wasm": exit status 3 - -Run 'getenvoy extension build --help' for usage. -`, dockerDir, workspaceDir))) - }) - }) - - Context("outside of a workspace directory", func() { - It("should fail", func() { - By("changing to a non-workspace dir") - dir := chdir("testdata") + tests := []testCase{ + { + flag: "--toolchain-container-image", + flagValue: "?invalid value?", + expectedErr: `"?invalid value?" is not a valid image name: invalid reference format`, + }, + { + flag: "--toolchain-container-options", + flagValue: "imbalanced ' quotes", + expectedErr: `"imbalanced ' quotes" is not a valid command line string`, + }, + } - By("running command") - c.SetArgs([]string{"extension", "build"}) - err := cmdutil.Execute(c) - Expect(err).To(HaveOccurred()) + for _, test := range tests { + test := test // pin! see https://github.com/kyoh86/scopelint for why - By("verifying command output") - Expect(stdout.String()).To(BeEmpty()) - Expect(stderr.String()).To(Equal(fmt.Sprintf(`Error: there is no extension directory at or above: %s + t.Run(test.flag+"="+test.flagValue, func(t *testing.T) { + // Run "getenvoy extension build" with the flags we are testing + cmd, stdout, stderr := cmd2.NewRootCommand() + cmd.SetArgs([]string{"extension", "build", test.flag, test.flagValue}) + err := cmdutil.Execute(cmd) + require.EqualError(t, err, test.expectedErr, `expected an error running [%v]`, cmd) -Run 'getenvoy extension build --help' for usage. -`, dir))) + // Verify the command failed with the expected error + require.Empty(t, stdout.String(), `expected no stdout running [%v]`, cmd) + expectedStderr := fmt.Sprintf("Error: %s\n\nRun 'getenvoy extension build --help' for usage.\n", test.expectedErr) + require.Equal(t, expectedStderr, stderr.String(), `expected stderr running [%v]`, cmd) }) + } +} + +func TestGetEnvoyExtensionBuildFailsOutsideWorkspaceDirectory(t *testing.T) { + // Change to a non-workspace dir + dir, revertWd := cmd2.RequireChDir(t, relativeWorkspaceDir+"/..") + defer revertWd() + + // Run "getenvoy extension build" + cmd, stdout, stderr := cmd2.NewRootCommand() + cmd.SetArgs([]string{"extension", "build"}) + err := cmdutil.Execute(cmd) + + // Verify the command failed with the expected error + expectedErr := "there is no extension directory at or above: " + dir + require.EqualError(t, err, expectedErr, `expected an error running [%v]`, cmd) + require.Empty(t, stdout.String(), `expected no stdout running [%v]`, cmd) + expectedStderr := fmt.Sprintf("Error: %s\n\nRun 'getenvoy extension build --help' for usage.\n", expectedErr) + require.Equal(t, expectedStderr, stderr.String(), `expected stderr running [%v]`, cmd) +} + +func TestGetEnvoyExtensionBuild(t *testing.T) { + // We use a fake docker command to capture the commandline that would be invoked + dockerDir, revertPath := cmd2.RequireOverridePath(t, cmd2.FakeDockerDir) + defer revertPath() + + // "getenvoy extension build" must be in a valid workspace directory + workspaceDir, revertWd := cmd2.RequireChDir(t, relativeWorkspaceDir) + defer revertWd() + + // Fake the current user so we can test it is used in the docker args + expectedUser := user.User{Uid: "1001", Gid: "1002"} + revertGetCurrentUser := cmd2.OverrideGetCurrentUser(&expectedUser) + defer revertGetCurrentUser() + + // Run "getenvoy extension build" + cmd, stdout, stderr := cmd2.NewRootCommand() + cmd.SetArgs([]string{"extension", "build"}) + err := cmdutil.Execute(cmd) + + // We expect docker to run from the correct path, as the current user and mount a volume for the correct workspace. + expectedDockerExec := fmt.Sprintf("%s/docker run -u %s:%s --rm -t -v %s:/source -w /source --init getenvoy/extension-rust-builder:latest build --output-file target/getenvoy/extension.wasm", + dockerDir, expectedUser.Uid, expectedUser.Gid, workspaceDir) + + // Verify the command invoked, passing the correct default commandline + require.NoError(t, err, `expected no error running [%v]`, cmd) + require.Equal(t, expectedDockerExec+"\n", stdout.String(), `expected stdout running [%v]`, cmd) + require.Equal(t, "docker stderr\n", stderr.String(), `expected stderr running [%v]`, cmd) +} + +// This tests --toolchain-container flags become docker command options +func TestGetEnvoyExtensionBuildWithDockerOptions(t *testing.T) { + // We use a fake docker command to capture the commandline that would be invoked + _, revertPath := cmd2.RequireOverridePath(t, cmd2.FakeDockerDir) + defer revertPath() + + // "getenvoy extension build" must be in a valid workspace directory + _, revertWd := cmd2.RequireChDir(t, relativeWorkspaceDir) + defer revertWd() + + // Run "getenvoy extension build" + cmd, stdout, stderr := cmd2.NewRootCommand() + cmd.SetArgs([]string{"extension", "build", + "--toolchain-container-image", "build/image", + "--toolchain-container-options", `-e 'VAR=VALUE' -v "/host:/container"`, }) -}) + err := cmdutil.Execute(cmd) + + // Verify the command's stdout includes the init args. TestGetEnvoyExtensionBuild tests the rest of stdout. + require.NoError(t, err, `expected no error running [%v]`, cmd) + require.Regexp(t, ".*--init -e VAR=VALUE -v /host:/container build/image build.*", stdout.String(), `expected stdout running [%v]`, cmd) + require.Equal(t, "docker stderr\n", stderr.String(), `expected stderr running [%v]`, cmd) +} + +// TestGetEnvoyExtensionBuildFail ensures build failures show useful information in stderr +func TestGetEnvoyExtensionBuildFail(t *testing.T) { + // We use a fake docker command to capture the commandline that would be invoked, and force a failure. + dockerDir, revertPath := cmd2.RequireOverridePath(t, cmd2.FakeDockerDir) + defer revertPath() + + // "getenvoy extension build" must be in a valid workspace directory + workspaceDir, revertWd := cmd2.RequireChDir(t, relativeWorkspaceDir) + defer revertWd() + + // Fake the current user so we can test it is used in the docker args + expectedUser := user.User{Uid: "1001", Gid: "1002"} + revertGetCurrentUser := cmd2.OverrideGetCurrentUser(&expectedUser) + defer revertGetCurrentUser() + + // "-e DOCKER_EXIT_CODE=3" is a special instruction handled in the fake docker script + toolchainOptions := "-e DOCKER_EXIT_CODE=3" + // Run "getenvoy extension build" + cmd, stdout, stderr := cmd2.NewRootCommand() + cmd.SetArgs([]string{"extension", "build", "--toolchain-container-options", toolchainOptions}) + err := cmdutil.Execute(cmd) + + // We expect the exit instruction to have gotten to the fake docker script, along with the default options. + expectedDockerExec := fmt.Sprintf("%s/docker run -u %s:%s --rm -t -v %s:/source -w /source --init %s getenvoy/extension-rust-builder:latest build --output-file target/getenvoy/extension.wasm", + dockerDir, expectedUser.Uid, expectedUser.Gid, workspaceDir, toolchainOptions) + + // Verify the command failed with the expected error. + expectedErr := fmt.Sprintf(`failed to build Envoy extension using "default" toolchain: failed to execute an external command "%s": exit status 3`, expectedDockerExec) + require.EqualError(t, err, expectedErr, `expected an error running [%v]`, cmd) + + // We should see stdout because the docker script was invoked. + require.Equal(t, expectedDockerExec+"\n", stdout.String(), `expected stdout running [%v]`, cmd) + + // We also expect "docker stderr" in the output for the same reason. + expectedStderr := fmt.Sprintf("docker stderr\nError: %s\n\nRun 'getenvoy extension build --help' for usage.\n", expectedErr) + require.Equal(t, expectedStderr, stderr.String(), `expected stderr running [%v]`, cmd) +} diff --git a/pkg/cmd/extension/clean/cmd_test.go b/pkg/cmd/extension/clean/cmd_test.go index 50389ab4..d6683674 100644 --- a/pkg/cmd/extension/clean/cmd_test.go +++ b/pkg/cmd/extension/clean/cmd_test.go @@ -15,186 +15,162 @@ package clean_test import ( - "bytes" "fmt" - "os" - "path/filepath" - "strings" + "os/user" + "testing" - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - "github.com/spf13/cobra" + "github.com/stretchr/testify/require" - "github.com/tetratelabs/getenvoy/pkg/cmd" - testcontext "github.com/tetratelabs/getenvoy/pkg/test/cmd/extension" + cmd2 "github.com/tetratelabs/getenvoy/pkg/test/cmd" cmdutil "github.com/tetratelabs/getenvoy/pkg/util/cmd" ) -var _ = Describe("getenvoy extension clean", func() { +// relativeWorkspaceDir points to a usable pre-initialized workspace +const relativeWorkspaceDir = "testdata/workspace" - var dockerDir string - - BeforeEach(func() { - dir, err := filepath.Abs("../../../extension/workspace/toolchain/builtin/testdata/toolchain") - Expect(err).ToNot(HaveOccurred()) - dockerDir = dir - }) - - var pathBackup string - - BeforeEach(func() { - pathBackup = os.Getenv("PATH") - - // override PATH to overshadow `docker` executable during the test - path := strings.Join([]string{dockerDir, pathBackup}, string(filepath.ListSeparator)) - os.Setenv("PATH", path) - }) - - AfterEach(func() { - os.Setenv("PATH", pathBackup) - }) - - var cwdBackup string - - BeforeEach(func() { - cwd, err := os.Getwd() - Expect(err).ToNot(HaveOccurred()) - cwdBackup = cwd - }) - - AfterEach(func() { - if cwdBackup != "" { - Expect(os.Chdir(cwdBackup)).To(Succeed()) - } - }) - - testcontext.SetDefaultUser() // UID:GID == 1001:1002 - - var stdout *bytes.Buffer - var stderr *bytes.Buffer - - BeforeEach(func() { - stdout = new(bytes.Buffer) - stderr = new(bytes.Buffer) - }) - - var c *cobra.Command - - BeforeEach(func() { - c = cmd.NewRoot() - c.SetOut(stdout) - c.SetErr(stderr) - }) - - It("should validate value of --toolchain-container-image flag", func() { - By("running command") - c.SetArgs([]string{"extension", "clean", "--toolchain-container-image", "?invalid value?"}) - err := cmdutil.Execute(c) - Expect(err).To(HaveOccurred()) - - By("verifying command output") - Expect(stdout.String()).To(BeEmpty()) - Expect(stderr.String()).To(Equal(`Error: "?invalid value?" is not a valid image name: invalid reference format - -Run 'getenvoy extension clean --help' for usage. -`)) - }) - - It("should validate value of --toolchain-container-options flag", func() { - By("running command") - c.SetArgs([]string{"extension", "clean", "--toolchain-container-options", "imbalanced ' quotes"}) - err := cmdutil.Execute(c) - Expect(err).To(HaveOccurred()) - - By("verifying command output") - Expect(stdout.String()).To(BeEmpty()) - Expect(stderr.String()).To(Equal(`Error: "imbalanced ' quotes" is not a valid command line string - -Run 'getenvoy extension clean --help' for usage. -`)) - }) - - chdir := func(path string) string { - dir, err := filepath.Abs(path) - Expect(err).ToNot(HaveOccurred()) - - err = os.Chdir(dir) - Expect(err).ToNot(HaveOccurred()) - - return dir +func TestGetEnvoyExtensionCleanValidateFlag(t *testing.T) { + type testCase struct { + flag string + flagValue string + expectedErr string } - //nolint:lll - Context("inside a workspace directory", func() { - It("should succeed", func() { - By("changing to a workspace dir") - workspaceDir := chdir("../build/testdata/workspace") - - By("running command") - c.SetArgs([]string{"extension", "clean"}) - err := cmdutil.Execute(c) - Expect(err).ToNot(HaveOccurred()) - - By("verifying command output") - Expect(stdout.String()).To(Equal(fmt.Sprintf("%s/docker run -u 1001:1002 --rm -t -v %s:/source -w /source --init getenvoy/extension-rust-builder:latest clean\n", dockerDir, workspaceDir))) - Expect(stderr.String()).To(Equal("docker stderr\n")) - }) - - It("should allow to override build image and add Docker cli options", func() { - By("changing to a workspace dir") - workspaceDir := chdir("../build/testdata/workspace") - - By("running command") - c.SetArgs([]string{"extension", "clean", - "--toolchain-container-image", "clean/image", - "--toolchain-container-options", `-e 'VAR=VALUE' -v "/host:/container"`, - }) - err := cmdutil.Execute(c) - Expect(err).ToNot(HaveOccurred()) - - By("verifying command output") - Expect(stdout.String()).To(Equal(fmt.Sprintf("%s/docker run -u 1001:1002 --rm -t -v %s:/source -w /source --init -e VAR=VALUE -v /host:/container clean/image clean\n", dockerDir, workspaceDir))) - Expect(stderr.String()).To(Equal("docker stderr\n")) - }) - - It("should properly handle Docker build failing", func() { - By("changing to a workspace dir") - workspaceDir := chdir("../build/testdata/workspace") - - By("running command") - c.SetArgs([]string{"extension", "clean", - "--toolchain-container-image", "clean/image", - "--toolchain-container-options", `-e EXIT_CODE=3`, - }) - err := cmdutil.Execute(c) - Expect(err).To(HaveOccurred()) - - By("verifying command output") - Expect(stdout.String()).To(Equal(fmt.Sprintf("%s/docker run -u 1001:1002 --rm -t -v %s:/source -w /source --init -e EXIT_CODE=3 clean/image clean\n", dockerDir, workspaceDir))) - Expect(stderr.String()).To(Equal(fmt.Sprintf(`docker stderr -Error: failed to clean build directory of Envoy extension using "default" toolchain: failed to execute an external command "%s/docker run -u 1001:1002 --rm -t -v %s:/source -w /source --init -e EXIT_CODE=3 clean/image clean": exit status 3 - -Run 'getenvoy extension clean --help' for usage. -`, dockerDir, workspaceDir))) - }) - }) - - Context("outside of a workspace directory", func() { - It("should fail", func() { - By("changing to a non-workspace dir") - dir := chdir("../build/testdata") + tests := []testCase{ + { + flag: "--toolchain-container-image", + flagValue: "?invalid value?", + expectedErr: `"?invalid value?" is not a valid image name: invalid reference format`, + }, + { + flag: "--toolchain-container-options", + flagValue: "imbalanced ' quotes", + expectedErr: `"imbalanced ' quotes" is not a valid command line string`, + }, + } - By("running command") - c.SetArgs([]string{"extension", "clean"}) - err := cmdutil.Execute(c) - Expect(err).To(HaveOccurred()) + for _, test := range tests { + test := test // pin! see https://github.com/kyoh86/scopelint for why - By("verifying command output") - Expect(stdout.String()).To(BeEmpty()) - Expect(stderr.String()).To(Equal(fmt.Sprintf(`Error: there is no extension directory at or above: %s + t.Run(test.flag+"="+test.flagValue, func(t *testing.T) { + // Run "getenvoy extension clean" with the flags we are testing + cmd, stdout, stderr := cmd2.NewRootCommand() + cmd.SetArgs([]string{"extension", "clean", test.flag, test.flagValue}) + err := cmdutil.Execute(cmd) + require.EqualError(t, err, test.expectedErr, `expected an error running [%v]`, cmd) -Run 'getenvoy extension clean --help' for usage. -`, dir))) + // Verify the command failed with the expected error + require.Empty(t, stdout.String(), `expected no stdout running [%v]`, cmd) + expectedStderr := fmt.Sprintf("Error: %s\n\nRun 'getenvoy extension clean --help' for usage.\n", test.expectedErr) + require.Equal(t, expectedStderr, stderr.String(), `expected stderr running [%v]`, cmd) }) + } +} + +func TestGetEnvoyExtensionCleanFailsOutsideWorkspaceDirectory(t *testing.T) { + // Change to a non-workspace dir + dir, revertWd := cmd2.RequireChDir(t, relativeWorkspaceDir+"/..") + defer revertWd() + + // Run "getenvoy extension clean" + cmd, stdout, stderr := cmd2.NewRootCommand() + cmd.SetArgs([]string{"extension", "clean"}) + err := cmdutil.Execute(cmd) + + // Verify the command failed with the expected error + expectedErr := "there is no extension directory at or above: " + dir + require.EqualError(t, err, expectedErr, `expected an error running [%v]`, cmd) + require.Empty(t, stdout.String(), `expected no stdout running [%v]`, cmd) + expectedStderr := fmt.Sprintf("Error: %s\n\nRun 'getenvoy extension clean --help' for usage.\n", expectedErr) + require.Equal(t, expectedStderr, stderr.String(), `expected stderr running [%v]`, cmd) +} + +func TestGetEnvoyExtensionClean(t *testing.T) { + // We use a fake docker command to capture the commandline that would be invoked + dockerDir, revertPath := cmd2.RequireOverridePath(t, cmd2.FakeDockerDir) + defer revertPath() + + // "getenvoy extension clean" must be in a valid workspace directory + workspaceDir, revertWd := cmd2.RequireChDir(t, relativeWorkspaceDir) + defer revertWd() + + // Fake the current user so we can test it is used in the docker args + expectedUser := user.User{Uid: "1001", Gid: "1002"} + revertGetCurrentUser := cmd2.OverrideGetCurrentUser(&expectedUser) + defer revertGetCurrentUser() + + // Run "getenvoy extension clean" + cmd, stdout, stderr := cmd2.NewRootCommand() + cmd.SetArgs([]string{"extension", "clean"}) + err := cmdutil.Execute(cmd) + + // We expect docker to run from the correct path, as the current user and mount a volume for the correct workspace. + expectedDockerExec := fmt.Sprintf("%s/docker run -u %s:%s --rm -t -v %s:/source -w /source --init getenvoy/extension-rust-builder:latest clean", + dockerDir, expectedUser.Uid, expectedUser.Gid, workspaceDir) + + // Verify the command invoked, passing the correct default commandline + require.NoError(t, err, `expected no error running [%v]`, cmd) + require.Equal(t, expectedDockerExec+"\n", stdout.String(), `expected stdout running [%v]`, cmd) + require.Equal(t, "docker stderr\n", stderr.String(), `expected stderr running [%v]`, cmd) +} + +// This tests --toolchain-container flags become docker command options +func TestGetEnvoyExtensionCleanWithDockerOptions(t *testing.T) { + // We use a fake docker command to capture the commandline that would be invoked + _, revertPath := cmd2.RequireOverridePath(t, cmd2.FakeDockerDir) + defer revertPath() + + // "getenvoy extension clean" must be in a valid workspace directory + _, revertWd := cmd2.RequireChDir(t, relativeWorkspaceDir) + defer revertWd() + + // Run "getenvoy extension clean" + cmd, stdout, stderr := cmd2.NewRootCommand() + cmd.SetArgs([]string{"extension", "clean", + "--toolchain-container-image", "clean/image", + "--toolchain-container-options", `-e 'VAR=VALUE' -v "/host:/container"`, }) -}) + err := cmdutil.Execute(cmd) + + // Verify the command's stdout includes the init args. TestGetEnvoyExtensionClean tests the rest of stdout. + require.NoError(t, err, `expected no error running [%v]`, cmd) + require.Regexp(t, ".*--init -e VAR=VALUE -v /host:/container clean/image clean.*", stdout.String(), `expected stdout running [%v]`, cmd) + require.Equal(t, "docker stderr\n", stderr.String(), `expected stderr running [%v]`, cmd) +} + +// TestGetEnvoyExtensionCleanFail ensures clean failures show useful information in stderr +func TestGetEnvoyExtensionCleanFail(t *testing.T) { + // We use a fake docker command to capture the commandline that would be invoked, and force a failure. + dockerDir, revertPath := cmd2.RequireOverridePath(t, cmd2.FakeDockerDir) + defer revertPath() + + // "getenvoy extension clean" must be in a valid workspace directory + workspaceDir, revertWd := cmd2.RequireChDir(t, relativeWorkspaceDir) + defer revertWd() + + // Fake the current user so we can test it is used in the docker args + expectedUser := user.User{Uid: "1001", Gid: "1002"} + revertGetCurrentUser := cmd2.OverrideGetCurrentUser(&expectedUser) + defer revertGetCurrentUser() + + // "-e DOCKER_EXIT_CODE=3" is a special instruction handled in the fake docker script + toolchainOptions := "-e DOCKER_EXIT_CODE=3" + // Run "getenvoy extension clean" + cmd, stdout, stderr := cmd2.NewRootCommand() + cmd.SetArgs([]string{"extension", "clean", "--toolchain-container-options", toolchainOptions}) + err := cmdutil.Execute(cmd) + + // We expect the exit instruction to have gotten to the fake docker script, along with the default options. + expectedDockerExec := fmt.Sprintf("%s/docker run -u %s:%s --rm -t -v %s:/source -w /source --init %s getenvoy/extension-rust-builder:latest clean", + dockerDir, expectedUser.Uid, expectedUser.Gid, workspaceDir, toolchainOptions) + + // Verify the command failed with the expected error. + expectedErr := fmt.Sprintf(`failed to clean build directory of Envoy extension using "default" toolchain: failed to execute an external command "%s": exit status 3`, expectedDockerExec) + require.EqualError(t, err, expectedErr, `expected an error running [%v]`, cmd) + + // We should see stdout because the docker script was invoked. + require.Equal(t, expectedDockerExec+"\n", stdout.String(), `expected stdout running [%v]`, cmd) + + // We also expect "docker stderr" in the output for the same reason. + expectedStderr := fmt.Sprintf("docker stderr\nError: %s\n\nRun 'getenvoy extension clean --help' for usage.\n", expectedErr) + require.Equal(t, expectedStderr, stderr.String(), `expected stderr running [%v]`, cmd) +} diff --git a/pkg/cmd/extension/clean/test_suite_test.go b/pkg/cmd/extension/clean/test_suite_test.go deleted file mode 100644 index b72c07c0..00000000 --- a/pkg/cmd/extension/clean/test_suite_test.go +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2020 Tetrate -// -// 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 clean_test - -import ( - "testing" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -func TestTest(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Clean Suite") -} diff --git a/pkg/cmd/extension/clean/testdata/.licenserignore b/pkg/cmd/extension/clean/testdata/.licenserignore new file mode 100644 index 00000000..9e440c00 --- /dev/null +++ b/pkg/cmd/extension/clean/testdata/.licenserignore @@ -0,0 +1 @@ +/workspace/ diff --git a/pkg/cmd/extension/clean/testdata/workspace/.getenvoy/extension/extension.yaml b/pkg/cmd/extension/clean/testdata/workspace/.getenvoy/extension/extension.yaml new file mode 100644 index 00000000..c03cb2ad --- /dev/null +++ b/pkg/cmd/extension/clean/testdata/workspace/.getenvoy/extension/extension.yaml @@ -0,0 +1,14 @@ +# +# Envoy Wasm extension created with getenvoy toolkit. +# +kind: Extension + +name: mycompany.filters.http.custom_metrics + +category: envoy.filters.http +language: rust + +# Runtime the extension is being developed against. +runtime: + envoy: + version: standard:1.17.0 diff --git a/pkg/cmd/extension/clean/testdata/workspace/.getenvoy/extension/toolchains/default.yaml b/pkg/cmd/extension/clean/testdata/workspace/.getenvoy/extension/toolchains/default.yaml new file mode 100644 index 00000000..df16b59a --- /dev/null +++ b/pkg/cmd/extension/clean/testdata/workspace/.getenvoy/extension/toolchains/default.yaml @@ -0,0 +1 @@ +kind: BuiltinToolchain diff --git a/pkg/cmd/extension/push/cmd_test.go b/pkg/cmd/extension/push/cmd_test.go index 929215fa..d2f21c1b 100644 --- a/pkg/cmd/extension/push/cmd_test.go +++ b/pkg/cmd/extension/push/cmd_test.go @@ -15,122 +15,84 @@ package push_test import ( - "bytes" "fmt" - "os" "path/filepath" + "testing" - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - "github.com/spf13/cobra" + "github.com/stretchr/testify/require" - "github.com/tetratelabs/getenvoy/pkg/cmd" - testcontext "github.com/tetratelabs/getenvoy/pkg/test/cmd/extension" + cmd2 "github.com/tetratelabs/getenvoy/pkg/test/cmd" cmdutil "github.com/tetratelabs/getenvoy/pkg/util/cmd" ) -const ( - localRegistryWasmImageRef = "localhost:5000/getenvoy/sample" -) - -var _ = Describe("getenvoy extension push", func() { - - var cwdBackup string - - BeforeEach(func() { - cwd, err := os.Getwd() - Expect(err).ToNot(HaveOccurred()) - cwdBackup = cwd - }) - - AfterEach(func() { - if cwdBackup != "" { - Expect(os.Chdir(cwdBackup)).To(Succeed()) - } - }) - - testcontext.SetDefaultUser() // UID:GID == 1001:1002 - - var stdout *bytes.Buffer - var stderr *bytes.Buffer - - BeforeEach(func() { - stdout = new(bytes.Buffer) - stderr = new(bytes.Buffer) - }) - - var c *cobra.Command - - BeforeEach(func() { - c = cmd.NewRoot() - c.SetOut(stdout) - c.SetErr(stderr) - }) - - chdir := func(path string) string { - dir, err := filepath.Abs(path) - Expect(err).ToNot(HaveOccurred()) - - err = os.Chdir(dir) - Expect(err).ToNot(HaveOccurred()) - - return dir - } - - // TODO(musaprg): write teardown process for local registries if it's needed - - //nolint:lll - Context("inside a workspace directory", func() { - When("if the image ref is valid", func() { - It("should succeed", func() { - By("changing to a workspace dir") - _ = chdir("testdata/workspace") - - By("push to local registry") - c.SetArgs([]string{"extension", "push", localRegistryWasmImageRef}) - err := cmdutil.Execute(c) - Expect(err).ToNot(HaveOccurred()) - - By("verifying command output") - Expect(stdout.String()).NotTo(BeEmpty()) - Expect(stderr.String()).To(BeEmpty()) - }) - }) - }) - - Context("outside of a workspace directory", func() { - When("if the target wasm binary is specified", func() { - It("should succeed", func() { - By("changing to a non-workspace dir") - dir := chdir("testdata") - - By("running command") - c.SetArgs([]string{"extension", "push", localRegistryWasmImageRef, "--extension-file", filepath.Join(dir, "workspace", "extension.wasm")}) - err := cmdutil.Execute(c) - Expect(err).NotTo(HaveOccurred()) - - By("verifying command output") - Expect(stdout.String()).NotTo(BeEmpty()) - Expect(stderr.String()).To(BeEmpty()) - }) - }) - When("if no wasm binary specified", func() { - It("should fail", func() { - By("changing to a non-workspace dir") - dir := chdir("testdata") - - By("running command") - c.SetArgs([]string{"extension", "push", localRegistryWasmImageRef}) - err := cmdutil.Execute(c) - Expect(err).To(HaveOccurred()) - - By("verifying command output") - Expect(stdout.String()).To(BeEmpty()) - Expect(stderr.String()).To(Equal(fmt.Sprintf(`Error: there is no extension directory at or above: %s - -Run 'getenvoy extension push --help' for usage. -`, dir))) - }) - }) - }) -}) +// relativeWorkspaceDir points to a usable pre-initialized workspace +const relativeWorkspaceDir = "testdata/workspace" + +// localRegistryWasmImageRef corresponds to a Docker container running the image "registry:2" +// As this is not intended to be an end-to-end test, this could be improved to use a mock/fake HTTP registry instead. +const localRegistryWasmImageRef = "localhost:5000/getenvoy/sample" + +// When unspecified, we default the tag to Docker's default "latest". Note: recent tools enforce qualifying this! +const defaultTag = "latest" + +// TestGetEnvoyExtensionPush shows current directory is usable, provided it is a valid workspace. +func TestGetEnvoyExtensionPush(t *testing.T) { + _, revertWd := cmd2.RequireChDir(t, relativeWorkspaceDir) + defer revertWd() + + // Run "getenvoy extension push localhost:5000/getenvoy/sample" + cmd, stdout, stderr := cmd2.NewRootCommand() + cmd.SetArgs([]string{"extension", "push", localRegistryWasmImageRef}) + err := cmdutil.Execute(cmd) + + // A fully qualified image ref includes the tag + imageRef := localRegistryWasmImageRef + ":" + defaultTag + + // Verify stdout shows the latest tag and the correct image ref + require.NoError(t, err, `expected no error running [%v]`, cmd) + + require.Contains(t, stdout.String(), fmt.Sprintf(`Using default tag: %s +Pushed %s +digest: sha256`, defaultTag, imageRef), `unexpected stderr after running [%v]`, cmd) + require.Empty(t, stderr.String(), `expected no stderr running [%v]`, cmd) +} + +func TestGetEnvoyExtensionPushFailsOutsideWorkspaceDirectory(t *testing.T) { + // Change to a non-workspace dir + dir, revertWd := cmd2.RequireChDir(t, relativeWorkspaceDir+"/..") + defer revertWd() + + // Run "getenvoy extension push localhost:5000/getenvoy/sample" + cmd, stdout, stderr := cmd2.NewRootCommand() + cmd.SetArgs([]string{"extension", "push", localRegistryWasmImageRef}) + err := cmdutil.Execute(cmd) + + // Verify the command failed with the expected error + expectedErr := "there is no extension directory at or above: " + dir + require.EqualError(t, err, expectedErr, `expected an error running [%v]`, cmd) + require.Empty(t, stdout.String(), `expected no stdout running [%v]`, cmd) + expectedStderr := fmt.Sprintf("Error: %s\n\nRun 'getenvoy extension push --help' for usage.\n", expectedErr) + require.Equal(t, expectedStderr, stderr.String(), `expected stderr running [%v]`, cmd) +} + +// TestGetEnvoyExtensionPushWithExplicitFileOption shows we don't need to be in a workspace directory to push a wasm. +func TestGetEnvoyExtensionPushWithExplicitFileOption(t *testing.T) { + // Change to a non-workspace dir + dir, revertWd := cmd2.RequireChDir(t, relativeWorkspaceDir+"/..") + defer revertWd() + + // Point to a wasm file explicitly + wasm := filepath.Join(dir, "workspace", "extension.wasm") + + // Run "getenvoy extension push localhost:5000/getenvoy/sample --extension-file testdata/workspace/extension.wasm" + cmd, stdout, stderr := cmd2.NewRootCommand() + cmd.SetArgs([]string{"extension", "push", localRegistryWasmImageRef, "--extension-file", wasm}) + err := cmdutil.Execute(cmd) + + // Verify the pushed a latest tag to the correct registry + require.NoError(t, err, `expected no error running [%v]`, cmd) + require.Contains(t, stdout.String(), fmt.Sprintf(`Using default tag: latest +Pushed %s:latest +digest: sha256`, localRegistryWasmImageRef)) + require.Empty(t, stderr.String(), `expected no stderr running [%v]`, cmd) +} diff --git a/pkg/cmd/extension/push/push_suite_test.go b/pkg/cmd/extension/push/push_suite_test.go deleted file mode 100644 index 8233ff5c..00000000 --- a/pkg/cmd/extension/push/push_suite_test.go +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2021 Tetrate -// -// 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 push_test - -import ( - "testing" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -func TestPush(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Push Suite") -} diff --git a/pkg/cmd/extension/run/cmd_test.go b/pkg/cmd/extension/run/cmd_test.go index bf89769d..c99c363a 100644 --- a/pkg/cmd/extension/run/cmd_test.go +++ b/pkg/cmd/extension/run/cmd_test.go @@ -15,684 +15,365 @@ package run_test import ( - "bytes" - "encoding/json" "fmt" "io/ioutil" "os" + "os/user" "path/filepath" - "strings" + "testing" - "github.com/Masterminds/semver" - "github.com/ghodss/yaml" - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" "github.com/otiai10/copy" - "github.com/pkg/errors" - "github.com/spf13/cobra" - - "github.com/tetratelabs/getenvoy/pkg/cmd" - "github.com/tetratelabs/getenvoy/pkg/manifest" - testcontext "github.com/tetratelabs/getenvoy/pkg/test/cmd/extension" - manifesttest "github.com/tetratelabs/getenvoy/pkg/test/manifest" - "github.com/tetratelabs/getenvoy/pkg/types" + "github.com/stretchr/testify/require" + + cmd2 "github.com/tetratelabs/getenvoy/pkg/test/cmd" cmdutil "github.com/tetratelabs/getenvoy/pkg/util/cmd" ) -//nolint:lll -var _ = Describe("getenvoy extension run", func() { - - var cwdBackup string - - BeforeEach(func() { - cwd, err := os.Getwd() - Expect(err).ToNot(HaveOccurred()) - cwdBackup = cwd - }) - - AfterEach(func() { - if cwdBackup != "" { - Expect(os.Chdir(cwdBackup)).To(Succeed()) - } - }) - - var dockerDir string - - BeforeEach(func() { - dir, err := filepath.Abs("../../../extension/workspace/toolchain/builtin/testdata/toolchain") - Expect(err).ToNot(HaveOccurred()) - dockerDir = dir - }) - - var pathBackup string - - BeforeEach(func() { - pathBackup = os.Getenv("PATH") - }) - - AfterEach(func() { - os.Setenv("PATH", pathBackup) - }) - - BeforeEach(func() { - // override PATH to overshadow `docker` executable during the test - path := strings.Join([]string{dockerDir, pathBackup}, string(filepath.ListSeparator)) - os.Setenv("PATH", path) - }) - - var getenvoyHomeBackup string - - BeforeEach(func() { - getenvoyHomeBackup = os.Getenv("GETENVOY_HOME") - }) - - AfterEach(func() { - os.Setenv("GETENVOY_HOME", getenvoyHomeBackup) - }) - - var getenvoyHomeDir string - - BeforeEach(func() { - tempDir, err := ioutil.TempDir("", "") - Expect(err).NotTo(HaveOccurred()) - getenvoyHomeDir = tempDir - - // override GETENVOY_HOME directory during the test - os.Setenv("GETENVOY_HOME", getenvoyHomeDir) - }) - - AfterEach(func() { - if getenvoyHomeDir != "" { - Expect(os.RemoveAll(getenvoyHomeDir)).To(Succeed()) - } - }) - - var envoySubstituteArchiveDir string - - BeforeEach(func() { - envoySubstituteArchiveDir = filepath.Join(cwdBackup, "../../../extension/workspace/example/runtime/getenvoy/testdata/envoy") - }) - - var manifestURLBackup string - - BeforeEach(func() { - manifestURLBackup = manifest.GetURL() - }) - - AfterEach(func() { - Expect(manifest.SetURL(manifestURLBackup)).To(Succeed()) - }) - - var manifestServer manifesttest.Server - - BeforeEach(func() { - testManifest, err := manifesttest.NewSimpleManifest("standard:1.17.0", "wasm:1.15", "wasm:stable") - Expect(err).NotTo(HaveOccurred()) - - manifestServer = manifesttest.NewServer(&manifesttest.ServerOpts{ - Manifest: testManifest, - GetArtifactDir: func(uri string) (string, error) { - ref, e := types.ParseReference(uri) - if e != nil { - return "", e - } - if ref.Flavor == "wasm" { - return envoySubstituteArchiveDir, nil - } - if ref.Flavor == "standard" { - ver, e := semver.NewVersion(ref.Version) - if e == nil && ver.Major() >= 1 && ver.Minor() >= 17 { - return envoySubstituteArchiveDir, nil - } - } - return "", errors.Errorf("unexpected version of Envoy %q", uri) - }, - OnError: func(err error) { - Expect(err).NotTo(HaveOccurred()) - }, - }) - - // override location of the GetEnvoy manifest - err = manifest.SetURL(manifestServer.GetManifestURL()) - Expect(err).NotTo(HaveOccurred()) - }) +// relativeWorkspaceDir points to a usable pre-initialized workspace +const relativeWorkspaceDir = "testdata/workspace" - AfterEach(func() { - if manifestServer != nil { - manifestServer.Close() - } - }) - - var platform string - - BeforeEach(func() { - key, err := manifest.NewKey("standard:1.17.0") - Expect(err).NotTo(HaveOccurred()) - platform = strings.ToLower(key.Platform) - }) +func TestGetEnvoyExtensionRunValidateFlag(t *testing.T) { + type testCase struct { + name string + flags []string + flagValues []string + expectedErr string + } - // envoyCaptureDir represents a directory used by the Envoy substitute script - // to store captured info. - var envoyCaptureDir string + tempDir, closer := cmd2.RequireNewTempDir(t) + defer closer() - BeforeEach(func() { - dir, err := ioutil.TempDir("", "") - Expect(err).NotTo(HaveOccurred()) - envoyCaptureDir = dir - }) + // Create a fake envoy script so that we can verify execute bit is required. + notExecutable := filepath.Join(tempDir, "envoy") + err := ioutil.WriteFile(notExecutable, []byte(`#!/bin/sh`), 0600) + require.NoError(t, err, `couldn't create fake envoy script'`) - AfterEach(func() { - if envoyCaptureDir != "" { - Expect(os.RemoveAll(envoyCaptureDir)).To(Succeed()) - } - }) - - BeforeEach(func() { - // set environment variables to give `envoy` substitute script a hint - // where to put captured info - os.Setenv("TEST_ENVOY_CAPTURE_CMDLINE_FILE", filepath.Join(envoyCaptureDir, "cmdline")) - os.Setenv("TEST_ENVOY_CAPTURE_CWD_FILE", filepath.Join(envoyCaptureDir, "cwd")) - os.Setenv("TEST_ENVOY_CAPTURE_CWD_DIR", filepath.Join(envoyCaptureDir, "cwd.d")) - }) - - envoyCaptured := struct { - cmdline func() string - cwd func() string - readFile func(string) []byte - readFileToJSON func(string) map[string]interface{} - }{ - cmdline: func() string { - data, err := ioutil.ReadFile(os.Getenv("TEST_ENVOY_CAPTURE_CMDLINE_FILE")) - Expect(err).NotTo(HaveOccurred()) - return string(data) + tests := []testCase{ + { + name: "--envoy-options with imbalanced quotes", + flags: []string{"--envoy-options"}, + flagValues: []string{"imbalanced ' quotes"}, + expectedErr: `"imbalanced ' quotes" is not a valid command line string`, }, - cwd: func() string { - data, err := ioutil.ReadFile(os.Getenv("TEST_ENVOY_CAPTURE_CWD_FILE")) - Expect(err).NotTo(HaveOccurred()) - return strings.TrimSpace(string(data)) + { + name: "--envoy-path file doesn't exist", + flags: []string{"--envoy-path"}, + flagValues: []string{"non-existing-file"}, + expectedErr: `unable to find custom Envoy binary at "non-existing-file": stat non-existing-file: no such file or directory`, }, - readFile: func(name string) []byte { - data, err := ioutil.ReadFile(filepath.Join(os.Getenv("TEST_ENVOY_CAPTURE_CWD_DIR"), name)) - Expect(err).NotTo(HaveOccurred()) - return data + { + name: "--envoy-path is a directory", + flags: []string{"--envoy-path"}, + flagValues: []string{"."}, + expectedErr: `unable to find custom Envoy binary at ".": there is a directory at a given path instead of a regular file`, }, - readFileToJSON: func(name string) map[string]interface{} { - data, err := ioutil.ReadFile(filepath.Join(os.Getenv("TEST_ENVOY_CAPTURE_CWD_DIR"), name)) - Expect(err).NotTo(HaveOccurred()) - data, err = yaml.YAMLToJSON(data) - Expect(err).NotTo(HaveOccurred()) - obj := make(map[string]interface{}) - err = json.Unmarshal(data, &obj) - Expect(err).ToNot(HaveOccurred()) - return obj + { + name: "--envoy-path not executable", + flags: []string{"--envoy-path"}, + flagValues: []string{notExecutable}, + expectedErr: fmt.Sprintf(`unable to find custom Envoy binary at "%s": file is not executable`, notExecutable), + }, + { + name: "--envoy-version with invalid value", + flags: []string{"--envoy-version"}, + flagValues: []string{"???"}, + expectedErr: `Envoy version is not valid: "???" is not a valid GetEnvoy reference. Expected format: :[/]`, + }, + { + name: "--envoy-version and --envoy-path flags at the same time", + flags: []string{"--envoy-version", "--envoy-path"}, + flagValues: []string{"standard:1.17.0", "envoy"}, + expectedErr: `only one of flags '--envoy-version' and '--envoy-path' can be used at a time`, + }, + { + name: "--extension-config-file file doesn't exist", + flags: []string{"--extension-config-file"}, + flagValues: []string{"non-existing-file"}, + expectedErr: `failed to read custom extension config from file "non-existing-file": open non-existing-file: no such file or directory`, + }, + { + name: "--extension-config-file is a directory", + flags: []string{"--extension-config-file"}, + flagValues: []string{"."}, + expectedErr: `failed to read custom extension config from file ".": read .: is a directory`, + }, + { + name: "--extension-file file doesn't exist", + flags: []string{"--extension-file"}, + flagValues: []string{"non-existing-file"}, + expectedErr: `unable to find a pre-built *.wasm file at "non-existing-file": stat non-existing-file: no such file or directory`, + }, + { + name: "--extension-file is a directory", + flags: []string{"--extension-file"}, + flagValues: []string{"."}, + expectedErr: `unable to find a pre-built *.wasm file at ".": there is a directory at a given path instead of a regular file`, + }, + { + name: "--toolchain-container-options with invalid value", + flags: []string{"--toolchain-container-image"}, + flagValues: []string{"?invalid value?"}, + expectedErr: `"?invalid value?" is not a valid image name: invalid reference format`, + }, + { + name: "--toolchain-container-options with imbalanced quotes", + flags: []string{"--toolchain-container-options"}, + flagValues: []string{"imbalanced ' quotes"}, + expectedErr: `"imbalanced ' quotes" is not a valid command line string`, }, } - testcontext.SetDefaultUser() // UID:GID == 1001:1002 - - var stdout *bytes.Buffer - var stderr *bytes.Buffer - - BeforeEach(func() { - stdout = new(bytes.Buffer) - stderr = new(bytes.Buffer) - }) - - var c *cobra.Command - - BeforeEach(func() { - c = cmd.NewRoot() - c.SetOut(stdout) - c.SetErr(stderr) - }) - - It("should validate value of --toolchain-container-image flag", func() { - By("running command") - c.SetArgs([]string{"extension", "run", "--toolchain-container-image", "?invalid value?"}) - err := cmdutil.Execute(c) - Expect(err).To(HaveOccurred()) - - By("verifying command output") - Expect(stdout.String()).To(BeEmpty()) - Expect(stderr.String()).To(Equal(`Error: "?invalid value?" is not a valid image name: invalid reference format - -Run 'getenvoy extension run --help' for usage. -`)) - }) - - It("should validate value of --toolchain-container-options flag", func() { - By("running command") - c.SetArgs([]string{"extension", "run", "--toolchain-container-options", "imbalanced ' quotes"}) - err := cmdutil.Execute(c) - Expect(err).To(HaveOccurred()) - - By("verifying command output") - Expect(stdout.String()).To(BeEmpty()) - Expect(stderr.String()).To(Equal(`Error: "imbalanced ' quotes" is not a valid command line string - -Run 'getenvoy extension run --help' for usage. -`)) - }) - - It("should validate value of --envoy-version flag", func() { - By("running command") - c.SetArgs([]string{"extension", "run", "--envoy-version", "???"}) - err := cmdutil.Execute(c) - Expect(err).To(HaveOccurred()) - - By("verifying command output") - Expect(stdout.String()).To(BeEmpty()) - Expect(stderr.String()).To(Equal(`Error: Envoy version is not valid: "???" is not a valid GetEnvoy reference. Expected format: :[/] - -Run 'getenvoy extension run --help' for usage. -`)) - }) - - It("should not allow --envoy-version and --envoy-path flags at the same time", func() { - By("running command") - c.SetArgs([]string{"extension", "run", "--envoy-version", "standard:1.17.0", "--envoy-path", "envoy"}) - err := cmdutil.Execute(c) - Expect(err).To(HaveOccurred()) - - By("verifying command output") - Expect(stdout.String()).To(BeEmpty()) - Expect(stderr.String()).To(Equal(`Error: only one of flags '--envoy-version' and '--envoy-path' can be used at a time - -Run 'getenvoy extension run --help' for usage. -`)) - }) - - It("should validate value of --envoy-path flag (path doesn't exist)", func() { - By("creating a path for test") - tempDir, err := ioutil.TempDir("", "") - Expect(err).NotTo(HaveOccurred()) - defer func() { - Expect(os.RemoveAll(tempDir)).To(Succeed()) - }() - filePath := filepath.Join(tempDir, "non-existing-dir", "non-existing-file") - - By("running command") - c.SetArgs([]string{"extension", "run", "--envoy-path", filePath}) - err = cmdutil.Execute(c) - Expect(err).To(HaveOccurred()) - - By("verifying command output") - Expect(stdout.String()).To(BeEmpty()) - Expect(stderr.String()).To(Equal(fmt.Sprintf(`Error: unable to find custom Envoy binary at %[1]q: stat %[1]s: no such file or directory - -Run 'getenvoy extension run --help' for usage. -`, filePath))) - }) - - It("should validate value of --envoy-path flag (path is a dir)", func() { - By("creating a path for test") - tempDir, err := ioutil.TempDir("", "") - Expect(err).NotTo(HaveOccurred()) - defer func() { - Expect(os.RemoveAll(tempDir)).To(Succeed()) - }() - dirPath := tempDir - - By("running command") - c.SetArgs([]string{"extension", "run", "--envoy-path", dirPath}) - err = cmdutil.Execute(c) - Expect(err).To(HaveOccurred()) - - By("verifying command output") - Expect(stdout.String()).To(BeEmpty()) - Expect(stderr.String()).To(Equal(fmt.Sprintf(`Error: unable to find custom Envoy binary at %q: there is a directory at a given path instead of a regular file - -Run 'getenvoy extension run --help' for usage. -`, dirPath))) - }) - - It("should validate value of --envoy-path flag (file is not executable)", func() { - By("creating a path for test") - tempDir, err := ioutil.TempDir("", "") - Expect(err).NotTo(HaveOccurred()) - defer func() { - Expect(os.RemoveAll(tempDir)).To(Succeed()) - }() - - By("creating a non-executable file") - filePath := filepath.Join(tempDir, "envoy") - err = ioutil.WriteFile(filePath, []byte(`#!/bin/sh`), 0600) - Expect(err).NotTo(HaveOccurred()) - - By("running command") - c.SetArgs([]string{"extension", "run", "--envoy-path", filePath}) - err = cmdutil.Execute(c) - Expect(err).To(HaveOccurred()) - - By("verifying command output") - Expect(stdout.String()).To(BeEmpty()) - Expect(stderr.String()).To(Equal(fmt.Sprintf(`Error: unable to find custom Envoy binary at %q: file is not executable - -Run 'getenvoy extension run --help' for usage. -`, filePath))) - }) - - It("should validate value of --envoy-options flag", func() { - By("running command") - c.SetArgs([]string{"extension", "run", "--envoy-options", "imbalanced ' quotes"}) - err := cmdutil.Execute(c) - Expect(err).To(HaveOccurred()) - - By("verifying command output") - Expect(stdout.String()).To(BeEmpty()) - Expect(stderr.String()).To(Equal(`Error: "imbalanced ' quotes" is not a valid command line string - -Run 'getenvoy extension run --help' for usage. -`)) - }) - - It("should validate value of --extension-file flag (path doesn't exist)", func() { - By("creating a path for test") - tempDir, err := ioutil.TempDir("", "") - Expect(err).NotTo(HaveOccurred()) - defer func() { - Expect(os.RemoveAll(tempDir)).To(Succeed()) - }() - filePath := filepath.Join(tempDir, "non-existing-dir", "non-existing-file") - - By("running command") - c.SetArgs([]string{"extension", "run", "--extension-file", filePath}) - err = cmdutil.Execute(c) - Expect(err).To(HaveOccurred()) - - By("verifying command output") - Expect(stdout.String()).To(BeEmpty()) - Expect(stderr.String()).To(Equal(fmt.Sprintf(`Error: unable to find a pre-built *.wasm file at %[1]q: stat %[1]s: no such file or directory - -Run 'getenvoy extension run --help' for usage. -`, filePath))) - }) - - It("should validate value of --extension-file flag (path is a dir)", func() { - By("creating a path for test") - tempDir, err := ioutil.TempDir("", "") - Expect(err).NotTo(HaveOccurred()) - defer func() { - Expect(os.RemoveAll(tempDir)).To(Succeed()) - }() - dirPath := tempDir - - By("running command") - c.SetArgs([]string{"extension", "run", "--extension-file", dirPath}) - err = cmdutil.Execute(c) - Expect(err).To(HaveOccurred()) - - By("verifying command output") - Expect(stdout.String()).To(BeEmpty()) - Expect(stderr.String()).To(Equal(fmt.Sprintf(`Error: unable to find a pre-built *.wasm file at %q: there is a directory at a given path instead of a regular file - -Run 'getenvoy extension run --help' for usage. -`, dirPath))) - }) - - It("should validate value of --extension-config-file flag (path doesn't exist)", func() { - By("creating a path for test") - tempDir, err := ioutil.TempDir("", "") - Expect(err).NotTo(HaveOccurred()) - defer func() { - Expect(os.RemoveAll(tempDir)).To(Succeed()) - }() - filePath := filepath.Join(tempDir, "non-existing-dir", "non-existing-file") - - By("running command") - c.SetArgs([]string{"extension", "run", "--extension-config-file", filePath}) - err = cmdutil.Execute(c) - Expect(err).To(HaveOccurred()) - - By("verifying command output") - Expect(stdout.String()).To(BeEmpty()) - Expect(stderr.String()).To(Equal(fmt.Sprintf(`Error: failed to read custom extension config from file %[1]q: open %[1]s: no such file or directory - -Run 'getenvoy extension run --help' for usage. -`, filePath))) - }) - - chdir := func(path string) string { - dir, err := filepath.Abs(path) - Expect(err).ToNot(HaveOccurred()) - - dir, err = filepath.EvalSymlinks(dir) - Expect(err).ToNot(HaveOccurred()) - - err = os.Chdir(dir) - Expect(err).ToNot(HaveOccurred()) - - return dir - } - - //nolint:lll - Context("inside a workspace directory", func() { - It("should succeed", func() { - By("changing to a workspace dir") - workspaceDir := chdir("testdata/workspace") - - By("running command") - c.SetArgs([]string{"extension", "run"}) - err := cmdutil.Execute(c) - Expect(err).ToNot(HaveOccurred()) - - By("verifying command output") - Expect(stdout.String()).To(Equal(fmt.Sprintf(`%s/docker run -u 1001:1002 --rm -t -v %s:/source -w /source --init getenvoy/extension-rust-builder:latest build --output-file target/getenvoy/extension.wasm -%s/builds/standard/1.17.0/%s/bin/envoy -c %s/envoy.tmpl.yaml -`, dockerDir, workspaceDir, getenvoyHomeDir, platform, envoyCaptured.cwd()))) - Expect(stderr.String()).To(Equal("docker stderr\nenvoy stderr\n")) - - By("verifying Envoy config") - placeholders := envoyCaptured.readFileToJSON("placeholders.tmpl.yaml") - Expect(placeholders["extension.name"]).To(Equal(`mycompany.filters.http.custom_metrics`)) - Expect(placeholders["extension.code"]).To(Equal(map[string]interface{}{ - "local": map[string]interface{}{ - "filename": filepath.Join(workspaceDir, "target/getenvoy/extension.wasm"), - }, - })) - Expect(placeholders["extension.config"]).To(Equal(map[string]interface{}{ - "@type": "type.googleapis.com/google.protobuf.StringValue", - "value": `{"key":"value"}`, - })) - }) - - It("should allow to override build image and add Docker cli options", func() { - By("changing to a workspace dir") - workspaceDir := chdir("testdata/workspace") - - By("running command") - c.SetArgs([]string{"extension", "run", - "--toolchain-container-image", "build/image", - "--toolchain-container-options", `-e 'VAR=VALUE' -v "/host:/container"`, - }) - err := cmdutil.Execute(c) - Expect(err).ToNot(HaveOccurred()) - - By("verifying command output") - Expect(stdout.String()).To(Equal(fmt.Sprintf(`%s/docker run -u 1001:1002 --rm -t -v %s:/source -w /source --init -e VAR=VALUE -v /host:/container build/image build --output-file target/getenvoy/extension.wasm -%s/builds/standard/1.17.0/%s/bin/envoy -c %s/envoy.tmpl.yaml -`, dockerDir, workspaceDir, getenvoyHomeDir, platform, envoyCaptured.cwd()))) - Expect(stderr.String()).To(Equal("docker stderr\nenvoy stderr\n")) - }) - - It("should properly handle Docker build failing", func() { - By("changing to a workspace dir") - workspaceDir := chdir("testdata/workspace") - - By("running command") - c.SetArgs([]string{"extension", "run", - "--toolchain-container-image", "build/image", - "--toolchain-container-options", `-e EXIT_CODE=3`, - }) - err := cmdutil.Execute(c) - Expect(err).To(HaveOccurred()) - - By("verifying command output") - Expect(stdout.String()).To(Equal(fmt.Sprintf("%s/docker run -u 1001:1002 --rm -t -v %s:/source -w /source --init -e EXIT_CODE=3 build/image build --output-file target/getenvoy/extension.wasm\n", dockerDir, workspaceDir))) - Expect(stderr.String()).To(Equal(fmt.Sprintf(`docker stderr -Error: failed to build Envoy extension using "default" toolchain: failed to execute an external command "%s/docker run -u 1001:1002 --rm -t -v %s:/source -w /source --init -e EXIT_CODE=3 build/image build --output-file target/getenvoy/extension.wasm": exit status 3 - -Run 'getenvoy extension run --help' for usage. -`, dockerDir, workspaceDir))) - }) - - It("should allow to override Envoy version via --envoy-version flag", func() { - By("changing to a workspace dir") - workspaceDir := chdir("testdata/workspace") - - By("running command") - c.SetArgs([]string{"extension", "run", "--envoy-version", "wasm:stable"}) - err := cmdutil.Execute(c) - Expect(err).NotTo(HaveOccurred()) - - By("verifying command output") - Expect(stdout.String()).To(Equal(fmt.Sprintf(`%s/docker run -u 1001:1002 --rm -t -v %s:/source -w /source --init getenvoy/extension-rust-builder:latest build --output-file target/getenvoy/extension.wasm -%s/builds/wasm/stable/%s/bin/envoy -c %s/envoy.tmpl.yaml -`, dockerDir, workspaceDir, getenvoyHomeDir, platform, envoyCaptured.cwd()))) - Expect(stderr.String()).To(Equal("docker stderr\nenvoy stderr\n")) - }) - - It("should properly handle unknown Envoy version", func() { - By("changing to a workspace dir") - workspaceDir := chdir("testdata/workspace") - - By("running command") - c.SetArgs([]string{"extension", "run", "--envoy-version", "wasm:unknown"}) - err := cmdutil.Execute(c) - Expect(err).To(HaveOccurred()) - - key, err := manifest.NewKey("wasm:unknown") - Expect(err).NotTo(HaveOccurred()) - - By("verifying command output") - Expect(stdout.String()).To(Equal(fmt.Sprintf("%s/docker run -u 1001:1002 --rm -t -v %s:/source -w /source --init getenvoy/extension-rust-builder:latest build --output-file target/getenvoy/extension.wasm\n", dockerDir, workspaceDir))) - Expect(stderr.String()).To(Equal(fmt.Sprintf(`docker stderr -Error: failed to run "default" example: unable to find matching GetEnvoy build for reference %q - -Run 'getenvoy extension run --help' for usage. -`, key))) - }) - - It("should allow to provide a custom Envoy binary via --envoy-path flag", func() { - By("changing to a workspace dir") - workspaceDir := chdir("testdata/workspace") - - By("running command") - c.SetArgs([]string{"extension", "run", "--envoy-path", filepath.Join(envoySubstituteArchiveDir, "bin/envoy")}) - err := cmdutil.Execute(c) - Expect(err).NotTo(HaveOccurred()) - - By("verifying command output") - Expect(stdout.String()).To(Equal(fmt.Sprintf(`%s/docker run -u 1001:1002 --rm -t -v %s:/source -w /source --init getenvoy/extension-rust-builder:latest build --output-file target/getenvoy/extension.wasm -%s -c %s/envoy.tmpl.yaml -`, dockerDir, workspaceDir, filepath.Join(envoySubstituteArchiveDir, "bin/envoy"), envoyCaptured.cwd()))) - Expect(stderr.String()).To(Equal("docker stderr\nenvoy stderr\n")) - }) - - It("should allow to provide extra options for Envoy via --envoy-options flag", func() { - By("changing to a workspace dir") - workspaceDir := chdir("testdata/workspace") - - By("running command") - c.SetArgs([]string{"extension", "run", "--envoy-options", "'--concurrency 2 --component-log-level wasm:debug,config:trace'"}) - err := cmdutil.Execute(c) - Expect(err).NotTo(HaveOccurred()) - - By("verifying command output") - Expect(stdout.String()).To(Equal(fmt.Sprintf(`%s/docker run -u 1001:1002 --rm -t -v %s:/source -w /source --init getenvoy/extension-rust-builder:latest build --output-file target/getenvoy/extension.wasm -%s/builds/standard/1.17.0/%s/bin/envoy -c %s/envoy.tmpl.yaml --concurrency 2 --component-log-level wasm:debug,config:trace -`, dockerDir, workspaceDir, getenvoyHomeDir, platform, envoyCaptured.cwd()))) - Expect(stderr.String()).To(Equal("docker stderr\nenvoy stderr\n")) - }) - - It("should allow to provide a pre-build *.wasm files via --extension-file flag", func() { - By("changing to a workspace dir") - _ = chdir("testdata/workspace") - - By("simulating a pre-built *.wasm file") - tempDir, err := ioutil.TempDir("", "") - Expect(err).NotTo(HaveOccurred()) - defer func() { - Expect(os.RemoveAll(tempDir)).To(Succeed()) - }() - wasmFile := filepath.Join(tempDir, "extension.wasm") - err = ioutil.WriteFile(wasmFile, []byte{}, 0600) - Expect(err).NotTo(HaveOccurred()) - - By("running command") - c.SetArgs([]string{"extension", "run", "--extension-file", wasmFile}) - err = cmdutil.Execute(c) - Expect(err).NotTo(HaveOccurred()) - - By("verifying command output") - Expect(stdout.String()).To(Equal(fmt.Sprintf("%s/builds/standard/1.17.0/%s/bin/envoy -c %s/envoy.tmpl.yaml\n", getenvoyHomeDir, platform, envoyCaptured.cwd()))) - Expect(stderr.String()).To(Equal("envoy stderr\n")) - - By("verifying Envoy config") - placeholders := envoyCaptured.readFileToJSON("placeholders.tmpl.yaml") - Expect(placeholders["extension.code"]).To(Equal(map[string]interface{}{ - "local": map[string]interface{}{ - "filename": wasmFile, - }, - })) - }) - - It("should allow to provide a custom extension config via --extension-config-file flag", func() { - By("changing to a workspace dir") - workspaceDir := chdir("testdata/workspace") - - By("simulating a custom extension config") - tempDir, err := ioutil.TempDir("", "") - Expect(err).NotTo(HaveOccurred()) - defer func() { - Expect(os.RemoveAll(tempDir)).To(Succeed()) - }() - configFile := filepath.Join(tempDir, "config.json") - err = ioutil.WriteFile(configFile, []byte(`{"key2":"value2"}`), 0600) - Expect(err).NotTo(HaveOccurred()) - - By("running command") - c.SetArgs([]string{"extension", "run", "--extension-config-file", configFile}) - err = cmdutil.Execute(c) - Expect(err).NotTo(HaveOccurred()) - - By("verifying command output") - Expect(stdout.String()).To(Equal(fmt.Sprintf(`%s/docker run -u 1001:1002 --rm -t -v %s:/source -w /source --init getenvoy/extension-rust-builder:latest build --output-file target/getenvoy/extension.wasm -%s/builds/standard/1.17.0/%s/bin/envoy -c %s/envoy.tmpl.yaml -`, dockerDir, workspaceDir, getenvoyHomeDir, platform, envoyCaptured.cwd()))) - Expect(stderr.String()).To(Equal("docker stderr\nenvoy stderr\n")) - - By("verifying Envoy config") - placeholders := envoyCaptured.readFileToJSON("placeholders.tmpl.yaml") - Expect(placeholders["extension.config"]).To(Equal(map[string]interface{}{ - "@type": "type.googleapis.com/google.protobuf.StringValue", - "value": `{"key2":"value2"}`, - })) + for _, test := range tests { + test := test // pin! see https://github.com/kyoh86/scopelint for why + + t.Run(test.name, func(t *testing.T) { + // Run "getenvoy extension run" with the flags we are testing + cmd, stdout, stderr := cmd2.NewRootCommand() + args := []string{"extension", "run"} + for i := range test.flags { + args = append(args, test.flags[i], test.flagValues[i]) + } + cmd.SetArgs(args) + err := cmdutil.Execute(cmd) + require.EqualError(t, err, test.expectedErr, `expected an error running [%v]`, cmd) + + // Verify the command failed with the expected error + require.Empty(t, stdout.String(), `expected no stdout running [%v]`, cmd) + expectedStderr := fmt.Sprintf("Error: %s\n\nRun 'getenvoy extension run --help' for usage.\n", test.expectedErr) + require.Equal(t, expectedStderr, stderr.String(), `expected stderr running [%v]`, cmd) }) - - It("should create default example if missing", func() { - By("simulating a workspace without 'default' example") - tempDir, err := ioutil.TempDir("", "") - Expect(err).NotTo(HaveOccurred()) - defer func() { - Expect(os.RemoveAll(tempDir)).To(Succeed()) - }() - err = copy.Copy("../build/testdata/workspace", tempDir) - Expect(err).NotTo(HaveOccurred()) - - By("changing to a workspace dir") - workspaceDir := chdir(tempDir) - - By("running command") - c.SetArgs([]string{"extension", "run"}) - err = cmdutil.Execute(c) - Expect(err).ToNot(HaveOccurred()) - - By("verifying command output") - Expect(stdout.String()).To(Equal(fmt.Sprintf(`%s/docker run -u 1001:1002 --rm -t -v %s:/source -w /source --init getenvoy/extension-rust-builder:latest build --output-file target/getenvoy/extension.wasm -%s/builds/standard/1.17.0/%s/bin/envoy -c %s/envoy.tmpl.yaml -`, dockerDir, workspaceDir, getenvoyHomeDir, platform, envoyCaptured.cwd()))) - Expect(stderr.String()).To(Equal(`Scaffolding a new example setup: + } +} + +func TestGetEnvoyExtensionRunFailsOutsideWorkspaceDirectory(t *testing.T) { + // Change to a non-workspace dir + config, cleanup := setupTest(t, relativeWorkspaceDir+"/..") + defer cleanup() + + // Run "getenvoy extension run" + cmd, stdout, stderr := cmd2.NewRootCommand() + cmd.SetArgs([]string{"extension", "run"}) + err := cmdutil.Execute(cmd) + + // Verify the command failed with the expected error + expectedErr := "there is no extension directory at or above: " + config.workspaceDir + require.EqualError(t, err, expectedErr, `expected an error running [%v]`, cmd) + require.Empty(t, stdout.String(), `expected no stdout running [%v]`, cmd) + expectedStderr := fmt.Sprintf("Error: %s\n\nRun 'getenvoy extension run --help' for usage.\n", expectedErr) + require.Equal(t, expectedStderr, stderr.String(), `expected stderr running [%v]`, cmd) +} + +func TestGetEnvoyExtensionRun(t *testing.T) { + config, cleanup := setupTest(t, relativeWorkspaceDir) + defer cleanup() + + // Run "getenvoy extension run" + cmd, stdout, stderr := cmd2.NewRootCommand() + cmd.SetArgs([]string{"extension", "run", "--home-dir", config.envoyHome}) + err := cmdutil.Execute(cmd) + + // Verify the command invoked, passing the correct default commandline + require.NoError(t, err, `expected no error running [%v]`, cmd) + + envoyBin := filepath.Join(config.envoyHome, "builds/standard/1.17.0", config.platform, "/bin/envoy") + // The working directory of envoy isn't the same as docker or the workspace + envoyWd := cmd2.ParseEnvoyWorkDirectory(stdout) + + // We expect docker to build from the correct path, as the current user and mount a volume for the correct workspace. + expectedStdout := fmt.Sprintf(`%s/docker run -u %s --rm -t -v %s:/source -w /source --init getenvoy/extension-rust-builder:latest build --output-file target/getenvoy/extension.wasm +envoy pwd: %s +envoy bin: %s +envoy args: -c %s/envoy.tmpl.yaml`, + config.dockerDir, config.expectedUidGid, config.workspaceDir, envoyWd, envoyBin, envoyWd) + require.Equal(t, expectedStdout+"\n", stdout.String(), `expected stdout running [%v]`, cmd) + require.Equal(t, "docker stderr\nenvoy stderr\n", stderr.String(), `expected stderr running [%v]`, cmd) + + // Verify the placeholders envoy would have ran substituted, notably including the generated extension.wasm + expectedYaml := fmt.Sprintf(`'extension.name': "mycompany.filters.http.custom_metrics" +'extension.code': {"local":{"filename":"%s/target/getenvoy/extension.wasm"}} +'extension.config': {"@type":"type.googleapis.com/google.protobuf.StringValue","value":"{\"key\":\"value\"}"} +`, config.workspaceDir) + yaml := requirePlaceholdersYaml(t, config.envoyHome) + require.Equal(t, expectedYaml, yaml, `unexpected placeholders yaml after running [%v]`, cmd) +} + +// TestGetEnvoyExtensionRunDockerFail ensures docker failures show useful information in stderr +func TestGetEnvoyExtensionRunDockerFail(t *testing.T) { + config, cleanup := setupTest(t, relativeWorkspaceDir) + defer cleanup() + + // "-e DOCKER_EXIT_CODE=3" is a special instruction handled in the fake docker script + toolchainOptions := "-e DOCKER_EXIT_CODE=3" + // Run "getenvoy extension run" + cmd, _, stderr := cmd2.NewRootCommand() + cmd.SetArgs([]string{"extension", "run", "--toolchain-container-options", toolchainOptions}) + err := cmdutil.Execute(cmd) + + // We expect the exit instruction to have gotten to the fake docker script, along with the default options. + expectedDockerExec := fmt.Sprintf("%s/docker run -u %s --rm -t -v %s:/source -w /source --init %s getenvoy/extension-rust-builder:latest build --output-file target/getenvoy/extension.wasm", + config.dockerDir, config.expectedUidGid, config.workspaceDir, toolchainOptions) + + // Verify the command failed with the expected error. + expectedErr := fmt.Sprintf(`failed to build Envoy extension using "default" toolchain: failed to execute an external command "%s": exit status 3`, expectedDockerExec) + require.EqualError(t, err, expectedErr, `expected an error running [%v]`, cmd) + + // We also expect "docker stderr" in the output for the same reason. + expectedStderr := fmt.Sprintf("docker stderr\nError: %s\n\nRun 'getenvoy extension run --help' for usage.\n", expectedErr) + require.Equal(t, expectedStderr, stderr.String(), `expected stderr running [%v]`, cmd) +} + +// TestGetEnvoyExtensionRunWithExplicitVersion only tests that a version override is used. It doesn't test things that +// aren't different between here and TestGetEnvoyExtensionRun. +func TestGetEnvoyExtensionRunWithExplicitVersion(t *testing.T) { + config, cleanup := setupTest(t, relativeWorkspaceDir) + defer cleanup() + + // Run "getenvoy extension run --envoy-version wasm:stable" + cmd, stdout, _ := cmd2.NewRootCommand() + cmd.SetArgs([]string{"extension", "run", "--home-dir", config.envoyHome, "--envoy-version", "wasm:stable"}) + err := cmdutil.Execute(cmd) + + // Verify the command invoked, passing the correct default commandline + require.NoError(t, err, `expected no error running [%v]`, cmd) + + // verify the expected binary is in the command output + envoyBin := filepath.Join(config.envoyHome, "builds/wasm/stable", config.platform, "/bin/envoy") + require.Contains(t, stdout.String(), "envoy bin: "+envoyBin, `expected stdout running [%v]`, cmd) +} + +func TestGetEnvoyExtensionRunFailWithUnknownVersion(t *testing.T) { + config, cleanup := setupTest(t, relativeWorkspaceDir) + defer cleanup() + + version := "wasm:unknown" + // Run "getenvoy extension run --envoy-version wasm:unknown" + cmd, _, stderr := cmd2.NewRootCommand() + cmd.SetArgs([]string{"extension", "run", "--home-dir", config.envoyHome, "--envoy-version", version}) + err := cmdutil.Execute(cmd) + + // Verify the command failed with the expected error. + reference := version + "/" + config.platform + expectedErr := fmt.Sprintf(`failed to run "default" example: unable to find matching GetEnvoy build for reference "%s"`, reference) + require.EqualError(t, err, expectedErr, `expected an error running [%v]`, cmd) + + // We also expect "docker stderr" in the output for the same reason. + expectedStderr := fmt.Sprintf("docker stderr\nError: %s\n\nRun 'getenvoy extension run --help' for usage.\n", expectedErr) + require.Equal(t, expectedStderr, stderr.String(), `expected stderr running [%v]`, cmd) +} + +func TestGetEnvoyExtensionRunWithCustomBinary(t *testing.T) { + config, cleanup := setupTest(t, relativeWorkspaceDir) + defer cleanup() + + // Run "getenvoy extension run --envoy-path $ENVOY_HOME/bin/envoy" + envoyBin := filepath.Join(config.envoyHome, "bin/envoy") + cmd, stdout, _ := cmd2.NewRootCommand() + cmd.SetArgs([]string{"extension", "run", "--envoy-path", envoyBin}) + err := cmdutil.Execute(cmd) + + // Verify the command invoked, passing the correct default commandline + require.NoError(t, err, `expected no error running [%v]`, cmd) + + // The only way we can see "envoy bin: ..." in stdout, is if our fake envoy script was read + require.Contains(t, stdout.String(), "envoy bin: "+envoyBin, `expected stdout running [%v]`, cmd) +} + +func TestGetEnvoyExtensionRunWithOptions(t *testing.T) { + config, cleanup := setupTest(t, relativeWorkspaceDir) + defer cleanup() + + // Run "getenvoy extension run ..." + cmd, stdout, _ := cmd2.NewRootCommand() + cmd.SetArgs([]string{"extension", "run", "--home-dir", config.envoyHome, + "--envoy-options", "'--concurrency 2 --component-log-level wasm:debug,config:trace'"}) + err := cmdutil.Execute(cmd) + + // Verify the command invoked, passing the correct default commandline + require.NoError(t, err, `expected no error running [%v]`, cmd) + + // The working directory of envoy is a temp directory not controlled by this test, so we have to parse it. + envoyWd := cmd2.ParseEnvoyWorkDirectory(stdout) + + envoyArgs := fmt.Sprintf(`-c %s/envoy.tmpl.yaml --concurrency 2 --component-log-level wasm:debug,config:trace`, envoyWd) + require.Contains(t, stdout.String(), "envoy args: "+envoyArgs, `expected stdout running [%v]`, cmd) +} + +// TestGetEnvoyExtensionRunWithWasm shows docker isn't run when the user supplies an "--extension-file" +func TestGetEnvoyExtensionRunWithWasm(t *testing.T) { + config, cleanup := setupTest(t, relativeWorkspaceDir) + defer cleanup() + + // As all scripts invoked are fakes, we only need to touch a file as it isn't read + wasmFile := filepath.Join(config.tempDir, "extension.wasm") + err := ioutil.WriteFile(wasmFile, []byte{}, 0600) + require.NoError(t, err, `expected no error creating extension.wasm: %s`, wasmFile) + + // Run "getenvoy extension run --extension-file /path/to/extension.wasm" + cmd, stdout, stderr := cmd2.NewRootCommand() + cmd.SetArgs([]string{"extension", "run", "--home-dir", config.envoyHome, "--extension-file", wasmFile}) + err = cmdutil.Execute(cmd) + + // Verify the command invoked, passing the correct default commandline + require.NoError(t, err, `expected no error running [%v]`, cmd) + + envoyBin := filepath.Join(config.envoyHome, "builds/standard/1.17.0", config.platform, "/bin/envoy") + + // The working directory of envoy is a temp directory not controlled by this test, so we have to parse it. + envoyWd := cmd2.ParseEnvoyWorkDirectory(stdout) + + // We expect docker to not have ran, since we supplied a pre-existing wasm. However, envoy should have. + expectedStdout := fmt.Sprintf(`envoy pwd: %s +envoy bin: %s +envoy args: -c %s/envoy.tmpl.yaml`, + envoyWd, envoyBin, envoyWd) + require.Equal(t, expectedStdout+"\n", stdout.String(), `expected stdout running [%v]`, cmd) + require.Equal(t, "envoy stderr\n", stderr.String(), `expected stderr running [%v]`, cmd) + + // Verify the placeholders envoy would have ran substituted, notably including the specified extension.wasm + yamlExtensionCode := fmt.Sprintf(`'extension.code': {"local":{"filename":"%s"}}`, wasmFile) + yaml := requirePlaceholdersYaml(t, config.envoyHome) + require.Contains(t, yaml, yamlExtensionCode, `unexpected placeholders yaml after running [%v]`, cmd) +} + +// TestGetEnvoyExtensionRunWithConfig shows extension config passed as an argument ends up readable by envoy. +func TestGetEnvoyExtensionRunWithConfig(t *testing.T) { + config, cleanup := setupTest(t, relativeWorkspaceDir) + defer cleanup() + + // As all scripts invoked are fakes, we only need to touch a file as it isn't read + configFile := filepath.Join(config.tempDir, "config.json") + err := ioutil.WriteFile(configFile, []byte(`{"key2":"value2"}`), 0600) + require.NoError(t, err, `expected no error creating extension.wasm: %s`, configFile) + + // Run "getenvoy extension run --extension-config-file /path/to/config.json" + cmd, _, _ := cmd2.NewRootCommand() + cmd.SetArgs([]string{"extension", "run", "--home-dir", config.envoyHome, "--extension-config-file", configFile}) + err = cmdutil.Execute(cmd) + + // Verify the command invoked, passing the correct default commandline + require.NoError(t, err, `expected no error running [%v]`, cmd) + + // Verify the placeholders envoy would have ran substituted, notably including the escaped config + yamlExtensionConfig := `'extension.config': {"@type":"type.googleapis.com/google.protobuf.StringValue","value":"{\"key2\":\"value2\"}"}` + yaml := requirePlaceholdersYaml(t, config.envoyHome) + require.Contains(t, yaml, yamlExtensionConfig, `unexpected placeholders yaml after running [%v]`, cmd) +} + +func TestGetEnvoyExtensionRunCreatesExampleWhenMissing(t *testing.T) { + // Use the workspace from the "extension build" test as it doesn't include examples. + config, cleanup := setupTest(t, "../build/testdata/workspace") + defer cleanup() + + // Run "getenvoy extension run" + cmd, _, stderr := cmd2.NewRootCommand() + cmd.SetArgs([]string{"extension", "run", "--home-dir", config.envoyHome}) + err := cmdutil.Execute(cmd) + + // Verify the command invoked, passing the correct default commandline + require.NoError(t, err, `expected no error running [%v]`, cmd) + + // Verify a new example was scaffolded prior to running docker and envoy + require.Equal(t, `Scaffolding a new example setup: * .getenvoy/extension/examples/default/README.md * .getenvoy/extension/examples/default/envoy.tmpl.yaml * .getenvoy/extension/examples/default/example.yaml @@ -700,67 +381,96 @@ Run 'getenvoy extension run --help' for usage. Done! docker stderr envoy stderr -`)) - - By("verifying Envoy config") - bootstrap := envoyCaptured.readFileToJSON("envoy.tmpl.yaml") - Expect(bootstrap).NotTo(BeEmpty()) - }) - - It("should create default example if missing for TinyGo", func() { - By("simulating a workspace without 'default' example") - tempDir, err := ioutil.TempDir("", "") - Expect(err).NotTo(HaveOccurred()) - defer func() { - Expect(os.RemoveAll(tempDir)).To(Succeed()) - }() - err = copy.Copy("testdata/workspace_tinygo", tempDir) - Expect(err).NotTo(HaveOccurred()) - - By("changing to a workspace dir") - workspaceDir := chdir(tempDir) - - By("running command") - c.SetArgs([]string{"extension", "run"}) - err = cmdutil.Execute(c) - Expect(err).ToNot(HaveOccurred()) - - By("verifying command output") - Expect(stdout.String()).To(Equal(fmt.Sprintf(`%s/docker run -u 1001:1002 --rm -t -v %s:/source -w /source --init getenvoy/extension-tinygo-builder:latest build --output-file build/extension.wasm -%s/builds/standard/1.17.0/%s/bin/envoy -c %s/envoy.tmpl.yaml -`, dockerDir, workspaceDir, getenvoyHomeDir, platform, envoyCaptured.cwd()))) - Expect(stderr.String()).To(Equal(`Scaffolding a new example setup: -* .getenvoy/extension/examples/default/README.md -* .getenvoy/extension/examples/default/envoy.tmpl.yaml -* .getenvoy/extension/examples/default/example.yaml -* .getenvoy/extension/examples/default/extension.txt -Done! -docker stderr -envoy stderr -`)) - - By("verifying Envoy config") - bootstrap := envoyCaptured.readFileToJSON("envoy.tmpl.yaml") - Expect(bootstrap).NotTo(BeEmpty()) - }) - }) - - Context("outside of a workspace directory", func() { - It("should fail", func() { - By("changing to a non-workspace dir") - dir := chdir("testdata") - - By("running command") - c.SetArgs([]string{"extension", "run"}) - err := cmdutil.Execute(c) - Expect(err).To(HaveOccurred()) - - By("verifying command output") - Expect(stdout.String()).To(BeEmpty()) - Expect(stderr.String()).To(Equal(fmt.Sprintf(`Error: there is no extension directory at or above: %s - -Run 'getenvoy extension run --help' for usage. -`, dir))) - }) - }) -}) +`, stderr.String(), `expected stderr running [%v]`, cmd) +} + +// TestGetEnvoyExtensionRunTinyGo ensures the docker command isn't pinned to rust projects +func TestGetEnvoyExtensionRunTinyGo(t *testing.T) { + config, cleanup := setupTest(t, "testdata/workspace_tinygo") + defer cleanup() + + // Run "getenvoy extension run" + cmd, stdout, _ := cmd2.NewRootCommand() + cmd.SetArgs([]string{"extension", "run", "--home-dir", config.envoyHome}) + err := cmdutil.Execute(cmd) + + // Verify the command invoked, passing the correct default commandline + require.NoError(t, err, `expected no error running [%v]`, cmd) + + // Verify the docker command used the tinygo instead of the rust builder image + require.Contains(t, stdout.String(), `--init getenvoy/extension-tinygo-builder:latest build`, `expected stdout running [%v]`, cmd) +} + +type testEnvoyExtensionConfig struct { + // tempDir is deleted on exit and contains many of the other directories + tempDir string + // dockerDir is the absolute location of extension.FakeDockerDir + dockerDir string + // workspaceDir will be the CWD of "getenvoy" + workspaceDir string + // envoyHome is a fake $tempDir/envoy_home, initialized with initFakeEnvoyHome + envoyHome string + // platform is the types.Reference.Platform used in manifest commands + platform string + // expectedUidGid corresponds to a fake user.User ex 1001:1002 the builtin toolchain will see. + expectedUidGid string //nolint +} + +// setupTest returns testEnvoyExtensionConfig and a tear-down function. +// The tear-down functions reverts side-effects such as temp directories and a fake manifest server. +// relativeWorkspaceTemplate is relative to the test file and will be copied into the resulting config.workspaceDir. +func setupTest(t *testing.T, relativeWorkspaceTemplate string) (*testEnvoyExtensionConfig, func()) { + result := testEnvoyExtensionConfig{} + var tearDown []func() + + tempDir, deleteTempDir := cmd2.RequireNewTempDir(t) + tearDown = append(tearDown, deleteTempDir) + result.tempDir = tempDir + + // We use a fake docker command to capture the commandline that would be invoked + dockerDir, revertPath := cmd2.RequireOverridePath(t, cmd2.FakeDockerDir) + tearDown = append(tearDown, revertPath) + result.dockerDir = dockerDir + + envoyHome := filepath.Join(tempDir, "envoy_home") + cmd2.InitFakeEnvoyHome(t, envoyHome) + result.envoyHome = envoyHome + + // create a new workspaceDir under tempDir + workspaceDir := filepath.Join(tempDir, "workspace") + err := os.Mkdir(workspaceDir, 0700) + require.NoError(t, err, `error creating directory: %s`, workspaceDir) + + // Copy the template into the new workspaceDir to avoid tainting the source tree + err = copy.Copy(cmd2.RequireAbsDir(t, relativeWorkspaceTemplate), workspaceDir) + require.NoError(t, err, `expected no error copying the directory: %s`, relativeWorkspaceTemplate) + result.workspaceDir = workspaceDir + + // "getenvoy extension run" must be executed inside a valid workspace directory + _, revertWd := cmd2.RequireChDir(t, workspaceDir) + tearDown = append(tearDown, revertWd) + + platform := cmd2.RequireManifestPlatform(t) + shutdownTestServer := cmd2.RequireManifestTestServer(t, envoyHome) + tearDown = append(tearDown, shutdownTestServer) + result.platform = platform + + // Fake the current user so we can test it is used in the docker args + expectedUser := user.User{Uid: "1001", Gid: "1002"} + revertGetCurrentUser := cmd2.OverrideGetCurrentUser(&expectedUser) + tearDown = append(tearDown, revertGetCurrentUser) + result.expectedUidGid = expectedUser.Uid + ":" + expectedUser.Gid + + return &result, func() { + for i := len(tearDown) - 1; i >= 0; i-- { + tearDown[i]() + } + } +} + +func requirePlaceholdersYaml(t *testing.T, envoyHome string) string { + placeholders := filepath.Join(envoyHome, "capture", "placeholders.tmpl.yaml") + b, err := ioutil.ReadFile(placeholders) + require.NoError(t, err, `expected no error reading placeholders: %s`, placeholders) + return string(b) +} diff --git a/pkg/cmd/extension/run/run_suite_test.go b/pkg/cmd/extension/run/run_suite_test.go deleted file mode 100644 index 894b6418..00000000 --- a/pkg/cmd/extension/run/run_suite_test.go +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2020 Tetrate -// -// 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 run_test - -import ( - "testing" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -func TestRun(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Run Suite") -} diff --git a/pkg/cmd/extension/test/cmd_test.go b/pkg/cmd/extension/test/cmd_test.go index f4204edb..092e82c3 100644 --- a/pkg/cmd/extension/test/cmd_test.go +++ b/pkg/cmd/extension/test/cmd_test.go @@ -15,186 +15,162 @@ package test_test import ( - "bytes" "fmt" - "os" - "path/filepath" - "strings" + "os/user" + "testing" - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - "github.com/spf13/cobra" + "github.com/stretchr/testify/require" - "github.com/tetratelabs/getenvoy/pkg/cmd" - testcontext "github.com/tetratelabs/getenvoy/pkg/test/cmd/extension" + cmd2 "github.com/tetratelabs/getenvoy/pkg/test/cmd" cmdutil "github.com/tetratelabs/getenvoy/pkg/util/cmd" ) -var _ = Describe("getenvoy extension test", func() { +// relativeWorkspaceDir points to a usable pre-initialized workspace +const relativeWorkspaceDir = "testdata/workspace" - var dockerDir string - - BeforeEach(func() { - dir, err := filepath.Abs("../../../extension/workspace/toolchain/builtin/testdata/toolchain") - Expect(err).ToNot(HaveOccurred()) - dockerDir = dir - }) - - var pathBackup string - - BeforeEach(func() { - pathBackup = os.Getenv("PATH") - - // override PATH to overshadow `docker` executable during the test - path := strings.Join([]string{dockerDir, pathBackup}, string(filepath.ListSeparator)) - os.Setenv("PATH", path) - }) - - AfterEach(func() { - os.Setenv("PATH", pathBackup) - }) - - var cwdBackup string - - BeforeEach(func() { - cwd, err := os.Getwd() - Expect(err).ToNot(HaveOccurred()) - cwdBackup = cwd - }) - - AfterEach(func() { - if cwdBackup != "" { - Expect(os.Chdir(cwdBackup)).To(Succeed()) - } - }) - - testcontext.SetDefaultUser() // UID:GID == 1001:1002 - - var stdout *bytes.Buffer - var stderr *bytes.Buffer - - BeforeEach(func() { - stdout = new(bytes.Buffer) - stderr = new(bytes.Buffer) - }) - - var c *cobra.Command - - BeforeEach(func() { - c = cmd.NewRoot() - c.SetOut(stdout) - c.SetErr(stderr) - }) - - It("should validate value of --toolchain-container-image flag", func() { - By("running command") - c.SetArgs([]string{"extension", "test", "--toolchain-container-image", "?invalid value?"}) - err := cmdutil.Execute(c) - Expect(err).To(HaveOccurred()) - - By("verifying command output") - Expect(stdout.String()).To(BeEmpty()) - Expect(stderr.String()).To(Equal(`Error: "?invalid value?" is not a valid image name: invalid reference format - -Run 'getenvoy extension test --help' for usage. -`)) - }) - - It("should validate value of --toolchain-container-options flag", func() { - By("running command") - c.SetArgs([]string{"extension", "test", "--toolchain-container-options", "imbalanced ' quotes"}) - err := cmdutil.Execute(c) - Expect(err).To(HaveOccurred()) - - By("verifying command output") - Expect(stdout.String()).To(BeEmpty()) - Expect(stderr.String()).To(Equal(`Error: "imbalanced ' quotes" is not a valid command line string - -Run 'getenvoy extension test --help' for usage. -`)) - }) - - chdir := func(path string) string { - dir, err := filepath.Abs(path) - Expect(err).ToNot(HaveOccurred()) - - err = os.Chdir(dir) - Expect(err).ToNot(HaveOccurred()) - - return dir +func TestGetEnvoyExtensionTestValidateFlag(t *testing.T) { + type testCase struct { + flag string + flagValue string + expectedErr string } - //nolint:lll - Context("inside a workspace directory", func() { - It("should succeed", func() { - By("changing to a workspace dir") - workspaceDir := chdir("../build/testdata/workspace") - - By("running command") - c.SetArgs([]string{"extension", "test"}) - err := cmdutil.Execute(c) - Expect(err).ToNot(HaveOccurred()) - - By("verifying command output") - Expect(stdout.String()).To(Equal(fmt.Sprintf("%s/docker run -u 1001:1002 --rm -t -v %s:/source -w /source --init getenvoy/extension-rust-builder:latest test\n", dockerDir, workspaceDir))) - Expect(stderr.String()).To(Equal("docker stderr\n")) - }) - - It("should allow to override build image and add Docker cli options", func() { - By("changing to a workspace dir") - workspaceDir := chdir("../build/testdata/workspace") - - By("running command") - c.SetArgs([]string{"extension", "test", - "--toolchain-container-image", "build/image", - "--toolchain-container-options", `-e 'VAR=VALUE' -v "/host:/container"`, - }) - err := cmdutil.Execute(c) - Expect(err).ToNot(HaveOccurred()) - - By("verifying command output") - Expect(stdout.String()).To(Equal(fmt.Sprintf("%s/docker run -u 1001:1002 --rm -t -v %s:/source -w /source --init -e VAR=VALUE -v /host:/container build/image test\n", dockerDir, workspaceDir))) - Expect(stderr.String()).To(Equal("docker stderr\n")) - }) - - It("should properly handle Docker build failing", func() { - By("changing to a workspace dir") - workspaceDir := chdir("../build/testdata/workspace") - - By("running command") - c.SetArgs([]string{"extension", "test", - "--toolchain-container-image", "build/image", - "--toolchain-container-options", `-e EXIT_CODE=3`, - }) - err := cmdutil.Execute(c) - Expect(err).To(HaveOccurred()) - - By("verifying command output") - Expect(stdout.String()).To(Equal(fmt.Sprintf("%s/docker run -u 1001:1002 --rm -t -v %s:/source -w /source --init -e EXIT_CODE=3 build/image test\n", dockerDir, workspaceDir))) - Expect(stderr.String()).To(Equal(fmt.Sprintf(`docker stderr -Error: failed to unit test Envoy extension using "default" toolchain: failed to execute an external command "%s/docker run -u 1001:1002 --rm -t -v %s:/source -w /source --init -e EXIT_CODE=3 build/image test": exit status 3 - -Run 'getenvoy extension test --help' for usage. -`, dockerDir, workspaceDir))) - }) - }) - - Context("outside of a workspace directory", func() { - It("should fail", func() { - By("changing to a non-workspace dir") - dir := chdir("../build/testdata") + tests := []testCase{ + { + flag: "--toolchain-container-image", + flagValue: "?invalid value?", + expectedErr: `"?invalid value?" is not a valid image name: invalid reference format`, + }, + { + flag: "--toolchain-container-options", + flagValue: "imbalanced ' quotes", + expectedErr: `"imbalanced ' quotes" is not a valid command line string`, + }, + } - By("running command") - c.SetArgs([]string{"extension", "test"}) - err := cmdutil.Execute(c) - Expect(err).To(HaveOccurred()) + for _, test := range tests { + test := test // pin! see https://github.com/kyoh86/scopelint for why - By("verifying command output") - Expect(stdout.String()).To(BeEmpty()) - Expect(stderr.String()).To(Equal(fmt.Sprintf(`Error: there is no extension directory at or above: %s + t.Run(test.flag+"="+test.flagValue, func(t *testing.T) { + // Run "getenvoy extension test" with the flags we are testing + cmd, stdout, stderr := cmd2.NewRootCommand() + cmd.SetArgs([]string{"extension", "test", test.flag, test.flagValue}) + err := cmdutil.Execute(cmd) + require.EqualError(t, err, test.expectedErr, `expected an error running [%v]`, cmd) -Run 'getenvoy extension test --help' for usage. -`, dir))) + // Verify the command failed with the expected error + require.Empty(t, stdout.String(), `expected no stdout running [%v]`, cmd) + expectedStderr := fmt.Sprintf("Error: %s\n\nRun 'getenvoy extension test --help' for usage.\n", test.expectedErr) + require.Equal(t, expectedStderr, stderr.String(), `expected stderr running [%v]`, cmd) }) + } +} + +func TestGetEnvoyExtensionTestFailsOutsideWorkspaceDirectory(t *testing.T) { + // Change to a non-workspace dir + dir, revertWd := cmd2.RequireChDir(t, relativeWorkspaceDir+"/..") + defer revertWd() + + // Run "getenvoy extension test" + cmd, stdout, stderr := cmd2.NewRootCommand() + cmd.SetArgs([]string{"extension", "test"}) + err := cmdutil.Execute(cmd) + + // Verify the command failed with the expected error + expectedErr := "there is no extension directory at or above: " + dir + require.EqualError(t, err, expectedErr, `expected an error running [%v]`, cmd) + require.Empty(t, stdout.String(), `expected no stdout running [%v]`, cmd) + expectedStderr := fmt.Sprintf("Error: %s\n\nRun 'getenvoy extension test --help' for usage.\n", expectedErr) + require.Equal(t, expectedStderr, stderr.String(), `expected stderr running [%v]`, cmd) +} + +func TestGetEnvoyExtensionTest(t *testing.T) { + // We use a fake docker command to capture the commandline that would be invoked + dockerDir, revertPath := cmd2.RequireOverridePath(t, cmd2.FakeDockerDir) + defer revertPath() + + // "getenvoy extension test" must be in a valid workspace directory + workspaceDir, revertWd := cmd2.RequireChDir(t, relativeWorkspaceDir) + defer revertWd() + + // Fake the current user so we can test it is used in the docker args + expectedUser := user.User{Uid: "1001", Gid: "1002"} + revertGetCurrentUser := cmd2.OverrideGetCurrentUser(&expectedUser) + defer revertGetCurrentUser() + + // Run "getenvoy extension test" + cmd, stdout, stderr := cmd2.NewRootCommand() + cmd.SetArgs([]string{"extension", "build"}) + err := cmdutil.Execute(cmd) + + // We expect docker to run from the correct path, as the current user and mount a volume for the correct workspace. + expectedDockerExec := fmt.Sprintf("%s/docker run -u %s:%s --rm -t -v %s:/source -w /source --init getenvoy/extension-rust-builder:latest build --output-file target/getenvoy/extension.wasm", + dockerDir, expectedUser.Uid, expectedUser.Gid, workspaceDir) + + // Verify the command invoked, passing the correct default commandline + require.NoError(t, err, `expected no error running [%v]`, cmd) + require.Equal(t, expectedDockerExec+"\n", stdout.String(), `expected stdout running [%v]`, cmd) + require.Equal(t, "docker stderr\n", stderr.String(), `expected stderr running [%v]`, cmd) +} + +// This tests --toolchain-container flags become docker command options +func TestGetEnvoyExtensionBuildWithDockerOptions(t *testing.T) { + // We use a fake docker command to capture the commandline that would be invoked + _, revertPath := cmd2.RequireOverridePath(t, cmd2.FakeDockerDir) + defer revertPath() + + // "getenvoy extension build" must be in a valid workspace directory + _, revertWd := cmd2.RequireChDir(t, relativeWorkspaceDir) + defer revertWd() + + // Run "getenvoy extension build" + cmd, stdout, stderr := cmd2.NewRootCommand() + cmd.SetArgs([]string{"extension", "build", + "--toolchain-container-image", "build/image", + "--toolchain-container-options", `-e 'VAR=VALUE' -v "/host:/container"`, }) -}) + err := cmdutil.Execute(cmd) + + // Verify the command's stdout includes the init args. TestGetEnvoyExtensionBuild tests the rest of stdout. + require.NoError(t, err, `expected no error running [%v]`, cmd) + require.Regexp(t, ".*--init -e VAR=VALUE -v /host:/container build/image build.*", stdout.String(), `expected stdout running [%v]`, cmd) + require.Equal(t, "docker stderr\n", stderr.String(), `expected stderr running [%v]`, cmd) +} + +// TestGetEnvoyExtensionTestFail ensures test failures show useful information in stderr +func TestGetEnvoyExtensionTestFail(t *testing.T) { + // We use a fake docker command to capture the commandline that would be invoked, and force a failure. + dockerDir, revertPath := cmd2.RequireOverridePath(t, cmd2.FakeDockerDir) + defer revertPath() + + // "getenvoy extension test" must be in a valid workspace directory + workspaceDir, revertWd := cmd2.RequireChDir(t, relativeWorkspaceDir) + defer revertWd() + + // Fake the current user so we can test it is used in the docker args + expectedUser := user.User{Uid: "1001", Gid: "1002"} + revertGetCurrentUser := cmd2.OverrideGetCurrentUser(&expectedUser) + defer revertGetCurrentUser() + + // "-e DOCKER_EXIT_CODE=3" is a special instruction handled in the fake docker script + toolchainOptions := "-e DOCKER_EXIT_CODE=3" + // Run "getenvoy extension test" + cmd, stdout, stderr := cmd2.NewRootCommand() + cmd.SetArgs([]string{"extension", "test", "--toolchain-container-options", toolchainOptions}) + err := cmdutil.Execute(cmd) + + // We expect the exit instruction to have gotten to the fake docker script, along with the default options. + expectedDockerExec := fmt.Sprintf("%s/docker run -u %s:%s --rm -t -v %s:/source -w /source --init %s getenvoy/extension-rust-builder:latest test", + dockerDir, expectedUser.Uid, expectedUser.Gid, workspaceDir, toolchainOptions) + + // Verify the command failed with the expected error. + expectedErr := fmt.Sprintf(`failed to unit test Envoy extension using "default" toolchain: failed to execute an external command "%s": exit status 3`, expectedDockerExec) + require.EqualError(t, err, expectedErr, `expected an error running [%v]`, cmd) + + // We should see stdout because the docker script was invoked. + require.Equal(t, expectedDockerExec+"\n", stdout.String(), `expected stdout running [%v]`, cmd) + + // We also expect "docker stderr" in the output for the same reason. + expectedStderr := fmt.Sprintf("docker stderr\nError: %s\n\nRun 'getenvoy extension test --help' for usage.\n", expectedErr) + require.Equal(t, expectedStderr, stderr.String(), `expected stderr running [%v]`, cmd) +} diff --git a/pkg/cmd/extension/test/test_suite_test.go b/pkg/cmd/extension/test/test_suite_test.go deleted file mode 100644 index 13147a14..00000000 --- a/pkg/cmd/extension/test/test_suite_test.go +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2020 Tetrate -// -// 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 test_test - -import ( - "testing" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -func TestTest(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Test Suite") -} diff --git a/pkg/cmd/extension/test/testdata/workspace/.getenvoy/extension/extension.yaml b/pkg/cmd/extension/test/testdata/workspace/.getenvoy/extension/extension.yaml new file mode 100644 index 00000000..dae80a2d --- /dev/null +++ b/pkg/cmd/extension/test/testdata/workspace/.getenvoy/extension/extension.yaml @@ -0,0 +1,28 @@ +# Copyright 2021 Tetrate +# +# 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. + +# +# Envoy Wasm extension created with getenvoy toolkit. +# +kind: Extension + +name: mycompany.filters.http.custom_metrics + +category: envoy.filters.http +language: rust + +# Runtime the extension is being developed against. +runtime: + envoy: + version: standard:1.17.0 diff --git a/pkg/cmd/extension/test/testdata/workspace/.getenvoy/extension/toolchains/default.yaml b/pkg/cmd/extension/test/testdata/workspace/.getenvoy/extension/toolchains/default.yaml new file mode 100644 index 00000000..4b9d971b --- /dev/null +++ b/pkg/cmd/extension/test/testdata/workspace/.getenvoy/extension/toolchains/default.yaml @@ -0,0 +1,15 @@ +# Copyright 2021 Tetrate +# +# 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. + +kind: BuiltinToolchain diff --git a/pkg/extension/workspace/example/runtime/getenvoy/getenvoy_suite_test.go b/pkg/extension/workspace/example/runtime/getenvoy/getenvoy_suite_test.go deleted file mode 100644 index 1e5993f3..00000000 --- a/pkg/extension/workspace/example/runtime/getenvoy/getenvoy_suite_test.go +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2020 Tetrate -// -// 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 getenvoy_test - -import ( - "testing" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -func TestGetenvoy(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "GetEnvoy Suite") -} diff --git a/pkg/extension/workspace/example/runtime/getenvoy/runtime_test.go b/pkg/extension/workspace/example/runtime/getenvoy/runtime_test.go index 61cae555..90e4122b 100644 --- a/pkg/extension/workspace/example/runtime/getenvoy/runtime_test.go +++ b/pkg/extension/workspace/example/runtime/getenvoy/runtime_test.go @@ -17,166 +17,123 @@ package getenvoy_test import ( "bytes" "fmt" - stdioutil "io/ioutil" - "os" "path/filepath" + "testing" - . "github.com/onsi/ginkgo" - . "github.com/onsi/ginkgo/extensions/table" - . "github.com/onsi/gomega" + "github.com/stretchr/testify/require" - "github.com/tetratelabs/getenvoy/pkg/common" workspaces "github.com/tetratelabs/getenvoy/pkg/extension/workspace" "github.com/tetratelabs/getenvoy/pkg/extension/workspace/example/runtime" . "github.com/tetratelabs/getenvoy/pkg/extension/workspace/example/runtime/getenvoy" "github.com/tetratelabs/getenvoy/pkg/extension/workspace/model" - argutil "github.com/tetratelabs/getenvoy/pkg/util/args" + "github.com/tetratelabs/getenvoy/pkg/test/cmd" ioutil "github.com/tetratelabs/getenvoy/pkg/util/io" ) -var _ = Describe("runtime", func() { - Describe("Run()", func() { - - var backupHomeDir string - - BeforeEach(func() { - backupHomeDir = common.HomeDir - }) - - AfterEach(func() { - common.HomeDir = backupHomeDir - }) - - var tempHomeDir string - - BeforeEach(func() { - dir, err := stdioutil.TempDir("", "getenvoy-home") - Expect(err).NotTo(HaveOccurred()) - tempHomeDir = dir - }) - - AfterEach(func() { - if tempHomeDir != "" { - Expect(os.RemoveAll(tempHomeDir)).To(Succeed()) - } - }) - - BeforeEach(func() { - common.HomeDir = tempHomeDir - }) - - envoyPath := func() string { - path, err := filepath.Abs("testdata/envoy/bin/envoy") - Expect(err).ToNot(HaveOccurred()) - return path - } +const ( + relativeWorkspaceDir = "testdata/workspace" + invalidWorkspaceDir = "testdata/invalidWorkspace" +) - var stdout *bytes.Buffer - var stderr *bytes.Buffer - - BeforeEach(func() { - stdout = new(bytes.Buffer) - stderr = new(bytes.Buffer) - }) - - runContext := func(workspace model.Workspace, example model.Example) *runtime.RunContext { - return &runtime.RunContext{ - Opts: runtime.RunOpts{ - Workspace: workspace, - Example: runtime.ExampleOpts{ - Name: "default", - Example: example, - }, - Extension: runtime.ExtensionOpts{ - WasmFile: `/path/to/extension.wasm`, - Config: model.File{ - Source: "/path/to/config", - Content: []byte(`{"key2":"value2"}`), - }, - }, - Envoy: runtime.EnvoyOpts{ - Path: envoyPath(), - }, - }, - IO: ioutil.StdStreams{ - Out: stdout, - Err: stderr, +func TestRuntimeRun(t *testing.T) { + workspace, err := workspaces.GetWorkspaceAt(relativeWorkspaceDir) + require.NoError(t, err, `expected no error getting workspace from directory %s`, relativeWorkspaceDir) + + example, err := workspace.GetExample("default") + require.NoError(t, err, `expected no error getting example from workspace %s`, workspace) + + fakeEnvoyPath, tearDown := setupFakeEnvoy(t) + defer tearDown() + + // Create and run a new context that will invoke a fake envoy script + ctx, stdout, stderr := runContext(workspace, example, fakeEnvoyPath) + err = NewRuntime().Run(ctx) + require.NoError(t, err, `expected no error running running [%v]`, ctx) + + // The working directory of envoy is a temp directory not controlled by this test, so we have to parse it. + envoyWd := cmd.ParseEnvoyWorkDirectory(stdout) + + // Verify we executed the indicated envoy binary, and it captured the arguments we expected + expectedStdout := fmt.Sprintf(`envoy pwd: %s +envoy bin: %s +envoy args: -c %s/envoy.tmpl.yaml +`, envoyWd, ctx.Opts.Envoy.Path, envoyWd) + require.Equal(t, expectedStdout, stdout.String(), `expected stdout running [%v]`, ctx) + + // Verify we didn't accidentally combine the stderr of envoy into stdout, or otherwise dropped it. + require.Equal(t, "envoy stderr\n", stderr.String(), `expected stderr running [%v]`, ctx) +} + +func TestRuntimeRunFailsOnInvalidWorkspace(t *testing.T) { + invalidWorkspaceDir := cmd.RequireAbsDir(t, invalidWorkspaceDir) + workspace, err := workspaces.GetWorkspaceAt(invalidWorkspaceDir) + require.NoError(t, err, `expected no error getting workspace from directory %s`, invalidWorkspaceDir) + + example, err := workspace.GetExample("default") + require.NoError(t, err, `expected no error getting example from workspace %s`, workspace) + + fakeEnvoyPath, tearDown := setupFakeEnvoy(t) + defer tearDown() + + // Create and run a new context that will invoke a fake envoy script + ctx, stdout, stderr := runContext(workspace, example, fakeEnvoyPath) + err = NewRuntime().Run(ctx) + + // Verify the error raised parsing the template from the input directory, before running envoy. + invalidTemplate := invalidWorkspaceDir + "/.getenvoy/extension/examples/default/envoy.tmpl.yaml" + expectedErr := fmt.Sprintf(`failed to process Envoy config template coming from "%s": failed to render Envoy config template: template: :18:19: executing "" at <.GetEnvoy.DefaultValue>: error calling DefaultValue: unknown property "???"`, invalidTemplate) + require.EqualError(t, err, expectedErr, `expected an error running [%v]`, ctx) + + // Verify there was no stdout or stderr because envoy shouldn't have run, yet. + require.Empty(t, stdout.String(), `expected no stdout running [%v]`, ctx) + require.Empty(t, stderr.String(), `expected no stderr running [%v]`, ctx) +} + +func runContext(workspace model.Workspace, example model.Example, envoyPath string) (ctx *runtime.RunContext, stdout, stderr *bytes.Buffer) { + stdout = new(bytes.Buffer) + stderr = new(bytes.Buffer) + ctx = &runtime.RunContext{ + Opts: runtime.RunOpts{ + Workspace: workspace, + Example: runtime.ExampleOpts{ + Name: "default", + Example: example, + }, + Extension: runtime.ExtensionOpts{ + WasmFile: `/path/to/extension.wasm`, + Config: model.File{ + Source: "/path/to/config", + Content: []byte(`{"key2":"value2"}`), }, - } + }, + Envoy: runtime.EnvoyOpts{ + Path: envoyPath, + }, + }, + IO: ioutil.StdStreams{ + Out: stdout, + Err: stderr, + }, + } + return +} + +// setupFakeEnvoy creates a fake envoy home and returns the path to the binary. +// Side effects are reversed in the returned tear-down function. +func setupFakeEnvoy(t *testing.T) (string, func()) { + var tearDown []func() + + tempDir, deleteTempDir := cmd.RequireNewTempDir(t) + tearDown = append(tearDown, deleteTempDir) + + envoyHome := filepath.Join(tempDir, "envoy_home") + fakeEnvoyPath := cmd.InitFakeEnvoyHome(t, envoyHome) + revertHomeDir := cmd.OverrideHomeDir(envoyHome) + tearDown = append(tearDown, revertHomeDir) + + return fakeEnvoyPath, func() { + for i := len(tearDown) - 1; i >= 0; i-- { + tearDown[i]() } - - Describe("in case of valid input", func() { - type testCase struct { - workspaceDir string - isEnvoyTemplate func(string) bool - } - DescribeTable("should run Envoy with a proper config", - func(given testCase) { - workspace, err := workspaces.GetWorkspaceAt(given.workspaceDir) - Expect(err).ToNot(HaveOccurred()) - - example, err := workspace.GetExample("default") - Expect(err).ToNot(HaveOccurred()) - - ctx := runContext(workspace, example) - - By("running Envoy") - err = NewRuntime().Run(ctx) - Expect(err).ToNot(HaveOccurred()) - - By("verifying Envoy output") - Expect(stdout.String()).NotTo(BeEmpty()) - Expect(stderr.String()).To(Equal("envoy stderr\n")) - - By("verifying Envoy arguments") - args, err := argutil.SplitCommandLine(stdout.String()) - Expect(err).ToNot(HaveOccurred()) - Expect(args).To(HaveLen(3)) - Expect(args[0]).To(Equal(ctx.Opts.Envoy.Path)) - Expect(args[1]).To(Equal("-c")) - }, - Entry("envoy.tmpl.yaml", testCase{ - workspaceDir: "../configdir/testdata/workspace1", - isEnvoyTemplate: func(name string) bool { - return name == "envoy.tmpl.yaml" - }, - }), - ) - }) - - Describe("in case of invalid input", func() { - abs := func(path string) string { - path, err := filepath.Abs(path) - if err != nil { - panic(err) - } - return path - } - - type testCase struct { - workspaceDir string - expectedErr string - } - //nolint:lll - DescribeTable("should fail with a proper error", - func(given testCase) { - workspace, err := workspaces.GetWorkspaceAt(given.workspaceDir) - Expect(err).ToNot(HaveOccurred()) - - example, err := workspace.GetExample("default") - Expect(err).ToNot(HaveOccurred()) - - ctx := runContext(workspace, example) - - By("running Envoy") - err = NewRuntime().Run(ctx) - Expect(err).To(MatchError(given.expectedErr)) - }, - Entry("envoy.tmpl.yaml: invalid placeholder", testCase{ - workspaceDir: "../configdir/testdata/workspace5", - expectedErr: fmt.Sprintf(`failed to process Envoy config template coming from %q: failed to render Envoy config template: template: :4:19: executing "" at <.GetEnvoy.DefaultValue>: error calling DefaultValue: unknown property "???"`, abs("../configdir/testdata/workspace5/.getenvoy/extension/examples/default/envoy.tmpl.yaml")), - }), - ) - }) - }) -}) + } +} diff --git a/pkg/extension/workspace/example/runtime/getenvoy/testdata/.licenserignore b/pkg/extension/workspace/example/runtime/getenvoy/testdata/.licenserignore index 0f2cfc01..1db62e1a 100644 --- a/pkg/extension/workspace/example/runtime/getenvoy/testdata/.licenserignore +++ b/pkg/extension/workspace/example/runtime/getenvoy/testdata/.licenserignore @@ -1,2 +1,2 @@ -# shell script -/envoy/bin/envoy +/workspace/ +/invalidWorkspace/ diff --git a/pkg/extension/workspace/example/runtime/getenvoy/testdata/envoy/bin/envoy b/pkg/extension/workspace/example/runtime/getenvoy/testdata/envoy/bin/envoy deleted file mode 100755 index 2aae9144..00000000 --- a/pkg/extension/workspace/example/runtime/getenvoy/testdata/envoy/bin/envoy +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env bash - -# Copyright 2020 Tetrate -# -# 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. - -set -e - -echo "$0" "$@" - -if [[ -n "${TEST_ENVOY_CAPTURE_CMDLINE_FILE}" ]]; then - mkdir -p "$(dirname "${TEST_ENVOY_CAPTURE_CMDLINE_FILE}")" - echo "$0" "$@" >"${TEST_ENVOY_CAPTURE_CMDLINE_FILE}" -fi - -if [[ -n "${TEST_ENVOY_CAPTURE_CWD_FILE}" ]]; then - mkdir -p "$(dirname "${TEST_ENVOY_CAPTURE_CWD_FILE}")" - echo "$PWD" >"${TEST_ENVOY_CAPTURE_CWD_FILE}" -fi - -if [[ -n "${TEST_ENVOY_CAPTURE_CWD_DIR}" ]]; then - mkdir -p "${TEST_ENVOY_CAPTURE_CWD_DIR}" - cp -R "$PWD"/* "${TEST_ENVOY_CAPTURE_CWD_DIR}" -fi - -printf >&2 '%s\n' "envoy stderr" - -# -# To simulate exit with an error, pass an argument in the form EXIT_CODE=NN, -# e.g. EXIT_CODE=3 -# -exit_code="0" -while test $# -gt 0; do - case "$1" in - EXIT_CODE=*) - exit_code="${1:10}" - ;; - esac - shift -done -exit "$exit_code" diff --git a/pkg/extension/workspace/example/runtime/getenvoy/testdata/invalidWorkspace/.getenvoy/extension/examples/default/envoy.tmpl.yaml b/pkg/extension/workspace/example/runtime/getenvoy/testdata/invalidWorkspace/.getenvoy/extension/examples/default/envoy.tmpl.yaml new file mode 100644 index 00000000..bec22c96 --- /dev/null +++ b/pkg/extension/workspace/example/runtime/getenvoy/testdata/invalidWorkspace/.getenvoy/extension/examples/default/envoy.tmpl.yaml @@ -0,0 +1,86 @@ +# Copyright 2021 Tetrate +# +# 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. + +# +# Example Envoy configuration. +# +admin: {{ .GetEnvoy.DefaultValue "???" }} + +static_resources: + listeners: + - name: ingress + address: + socket_address: + address: 0.0.0.0 + port_value: 10000 + filter_chains: + - filters: + - name: envoy.filters.network.wasm + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.wasm.v3.Wasm + config: + configuration: {{ .GetEnvoy.Extension.Config }} + name: {{ .GetEnvoy.Extension.Name }} + root_id: {{ .GetEnvoy.Extension.Name }} + vm_config: + vm_id: {{ .GetEnvoy.Extension.Name }} + runtime: envoy.wasm.runtime.v8 + code: {{ .GetEnvoy.Extension.Code }} + - name: envoy.filters.network.tcp_proxy + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy + stat_prefix: ingress + cluster: mock_service + + - name: mock + address: + socket_address: + address: 127.0.0.1 + port_value: 10001 + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: mock + route_config: + name: local_route + virtual_hosts: + - name: mock + domains: + - "*" + routes: + - match: + prefix: "/" + direct_response: + status: 200 + body: + inline_string: "Hi from mock service!\n" + http_filters: + - name: envoy.filters.http.router + + clusters: + - name: mock_service + connect_timeout: 0.25s + type: STATIC + lb_policy: ROUND_ROBIN + load_assignment: + cluster_name: mock_service + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: 127.0.0.1 + port_value: 10001 diff --git a/pkg/extension/workspace/example/runtime/getenvoy/testdata/invalidWorkspace/.getenvoy/extension/examples/default/example.yaml b/pkg/extension/workspace/example/runtime/getenvoy/testdata/invalidWorkspace/.getenvoy/extension/examples/default/example.yaml new file mode 100644 index 00000000..c2f49598 --- /dev/null +++ b/pkg/extension/workspace/example/runtime/getenvoy/testdata/invalidWorkspace/.getenvoy/extension/examples/default/example.yaml @@ -0,0 +1,18 @@ +# Copyright 2021 Tetrate +# +# 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. + +# +# Example of a Wasm Http Filter. +# +kind: Example diff --git a/pkg/extension/workspace/example/runtime/getenvoy/testdata/invalidWorkspace/.getenvoy/extension/examples/default/extension.json b/pkg/extension/workspace/example/runtime/getenvoy/testdata/invalidWorkspace/.getenvoy/extension/examples/default/extension.json new file mode 100644 index 00000000..29f05f6b --- /dev/null +++ b/pkg/extension/workspace/example/runtime/getenvoy/testdata/invalidWorkspace/.getenvoy/extension/examples/default/extension.json @@ -0,0 +1 @@ +{"key":"value"} diff --git a/pkg/extension/workspace/example/runtime/getenvoy/testdata/invalidWorkspace/.getenvoy/extension/extension.yaml b/pkg/extension/workspace/example/runtime/getenvoy/testdata/invalidWorkspace/.getenvoy/extension/extension.yaml new file mode 100644 index 00000000..dae80a2d --- /dev/null +++ b/pkg/extension/workspace/example/runtime/getenvoy/testdata/invalidWorkspace/.getenvoy/extension/extension.yaml @@ -0,0 +1,28 @@ +# Copyright 2021 Tetrate +# +# 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. + +# +# Envoy Wasm extension created with getenvoy toolkit. +# +kind: Extension + +name: mycompany.filters.http.custom_metrics + +category: envoy.filters.http +language: rust + +# Runtime the extension is being developed against. +runtime: + envoy: + version: standard:1.17.0 diff --git a/pkg/extension/workspace/example/runtime/getenvoy/testdata/workspace/.getenvoy/extension/examples/default/envoy.tmpl.yaml b/pkg/extension/workspace/example/runtime/getenvoy/testdata/workspace/.getenvoy/extension/examples/default/envoy.tmpl.yaml new file mode 100644 index 00000000..9f531e69 --- /dev/null +++ b/pkg/extension/workspace/example/runtime/getenvoy/testdata/workspace/.getenvoy/extension/examples/default/envoy.tmpl.yaml @@ -0,0 +1,72 @@ +# +# Example Envoy configuration. +# +admin: {{ .GetEnvoy.DefaultValue "admin" }} + +static_resources: + listeners: + - name: ingress + address: + socket_address: + address: 0.0.0.0 + port_value: 10000 + filter_chains: + - filters: + - name: envoy.filters.network.wasm + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.wasm.v3.Wasm + config: + configuration: {{ .GetEnvoy.Extension.Config }} + name: {{ .GetEnvoy.Extension.Name }} + root_id: {{ .GetEnvoy.Extension.Name }} + vm_config: + vm_id: {{ .GetEnvoy.Extension.Name }} + runtime: envoy.wasm.runtime.v8 + code: {{ .GetEnvoy.Extension.Code }} + - name: envoy.filters.network.tcp_proxy + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy + stat_prefix: ingress + cluster: mock_service + + - name: mock + address: + socket_address: + address: 127.0.0.1 + port_value: 10001 + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: mock + route_config: + name: local_route + virtual_hosts: + - name: mock + domains: + - "*" + routes: + - match: + prefix: "/" + direct_response: + status: 200 + body: + inline_string: "Hi from mock service!\n" + http_filters: + - name: envoy.filters.http.router + + clusters: + - name: mock_service + connect_timeout: 0.25s + type: STATIC + lb_policy: ROUND_ROBIN + load_assignment: + cluster_name: mock_service + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: 127.0.0.1 + port_value: 10001 diff --git a/pkg/extension/workspace/example/runtime/getenvoy/testdata/workspace/.getenvoy/extension/examples/default/example.yaml b/pkg/extension/workspace/example/runtime/getenvoy/testdata/workspace/.getenvoy/extension/examples/default/example.yaml new file mode 100644 index 00000000..c45b1262 --- /dev/null +++ b/pkg/extension/workspace/example/runtime/getenvoy/testdata/workspace/.getenvoy/extension/examples/default/example.yaml @@ -0,0 +1,4 @@ +# +# Example of a Wasm Http Filter. +# +kind: Example diff --git a/pkg/extension/workspace/example/runtime/getenvoy/testdata/workspace/.getenvoy/extension/examples/default/extension.json b/pkg/extension/workspace/example/runtime/getenvoy/testdata/workspace/.getenvoy/extension/examples/default/extension.json new file mode 100644 index 00000000..29f05f6b --- /dev/null +++ b/pkg/extension/workspace/example/runtime/getenvoy/testdata/workspace/.getenvoy/extension/examples/default/extension.json @@ -0,0 +1 @@ +{"key":"value"} diff --git a/pkg/extension/workspace/example/runtime/getenvoy/testdata/workspace/.getenvoy/extension/extension.yaml b/pkg/extension/workspace/example/runtime/getenvoy/testdata/workspace/.getenvoy/extension/extension.yaml new file mode 100644 index 00000000..c03cb2ad --- /dev/null +++ b/pkg/extension/workspace/example/runtime/getenvoy/testdata/workspace/.getenvoy/extension/extension.yaml @@ -0,0 +1,14 @@ +# +# Envoy Wasm extension created with getenvoy toolkit. +# +kind: Extension + +name: mycompany.filters.http.custom_metrics + +category: envoy.filters.http +language: rust + +# Runtime the extension is being developed against. +runtime: + envoy: + version: standard:1.17.0 diff --git a/pkg/extension/workspace/example/runtime/getenvoy/testdata/workspace/expected/getenvoy_extension_run/envoy.tmpl.yaml b/pkg/extension/workspace/example/runtime/getenvoy/testdata/workspace/expected/getenvoy_extension_run/envoy.tmpl.yaml new file mode 100644 index 00000000..5ad3c038 --- /dev/null +++ b/pkg/extension/workspace/example/runtime/getenvoy/testdata/workspace/expected/getenvoy_extension_run/envoy.tmpl.yaml @@ -0,0 +1,75 @@ +# +# Example Envoy configuration. +# +admin: {"accessLogPath":"/dev/null","address":{"socketAddress":{"address":"127.0.0.1","portValue":9901}}} + +static_resources: + listeners: + - name: ingress + address: + socket_address: + address: 0.0.0.0 + port_value: 10000 + filter_chains: + - filters: + - name: envoy.filters.network.wasm + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.wasm.v3.Wasm + config: + configuration: + '@type': type.googleapis.com/google.protobuf.StringValue + value: | + {"key":"value"} + name: mycompany.filters.http.custom_metrics + root_id: mycompany.filters.http.custom_metrics + vm_config: + vm_id: mycompany.filters.http.custom_metrics + runtime: envoy.wasm.runtime.v8 + code: {"local":{"filename":"/path/to/extension.wasm"}} + - name: envoy.filters.network.tcp_proxy + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy + stat_prefix: ingress + cluster: mock_service + + - name: mock + address: + socket_address: + address: 127.0.0.1 + port_value: 10001 + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: mock + route_config: + name: local_route + virtual_hosts: + - name: mock + domains: + - "*" + routes: + - match: + prefix: "/" + direct_response: + status: 200 + body: + inline_string: "Hi from mock service!\n" + http_filters: + - name: envoy.filters.http.router + + clusters: + - name: mock_service + connect_timeout: 0.25s + type: STATIC + lb_policy: ROUND_ROBIN + load_assignment: + cluster_name: mock_service + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: 127.0.0.1 + port_value: 10001 diff --git a/pkg/extension/workspace/example/runtime/getenvoy/testdata/workspace/expected/getenvoy_extension_run/example.yaml b/pkg/extension/workspace/example/runtime/getenvoy/testdata/workspace/expected/getenvoy_extension_run/example.yaml new file mode 100644 index 00000000..c45b1262 --- /dev/null +++ b/pkg/extension/workspace/example/runtime/getenvoy/testdata/workspace/expected/getenvoy_extension_run/example.yaml @@ -0,0 +1,4 @@ +# +# Example of a Wasm Http Filter. +# +kind: Example diff --git a/pkg/extension/workspace/example/runtime/getenvoy/testdata/workspace/expected/getenvoy_extension_run/extension.json b/pkg/extension/workspace/example/runtime/getenvoy/testdata/workspace/expected/getenvoy_extension_run/extension.json new file mode 100644 index 00000000..29f05f6b --- /dev/null +++ b/pkg/extension/workspace/example/runtime/getenvoy/testdata/workspace/expected/getenvoy_extension_run/extension.json @@ -0,0 +1 @@ +{"key":"value"} diff --git a/pkg/extension/workspace/toolchain/builtin/testdata/toolchain/docker b/pkg/extension/workspace/toolchain/builtin/testdata/toolchain/docker index 09e3412b..ab1894c5 100755 --- a/pkg/extension/workspace/toolchain/builtin/testdata/toolchain/docker +++ b/pkg/extension/workspace/toolchain/builtin/testdata/toolchain/docker @@ -15,20 +15,13 @@ # limitations under the License. echo "$0" "$@" +echo >&2 docker stderr -printf >&2 '%s\n' "docker stderr" - -# -# To simulate exit with an error, pass an argument in the form EXIT_CODE=NN, -# e.g. EXIT_CODE=3 -# -exit_code="0" +# An arg DOCKER_EXIT_CODE=N exits with that code number while test $# -gt 0; do case "$1" in - EXIT_CODE=*) - exit_code="${1:10}" - ;; + DOCKER_EXIT_CODE=*) exit "${1:17}" ;; esac shift done -exit "$exit_code" +exit 0 diff --git a/pkg/extension/workspace/toolchain/builtin/toolchain_test.go b/pkg/extension/workspace/toolchain/builtin/toolchain_test.go index 78cf78b4..95438108 100644 --- a/pkg/extension/workspace/toolchain/builtin/toolchain_test.go +++ b/pkg/extension/workspace/toolchain/builtin/toolchain_test.go @@ -234,13 +234,13 @@ var _ = Describe("built-in toolchain", func() { image: build/image options: - -e - - EXIT_CODE=3 + - DOCKER_EXIT_CODE=3 output: wasmFile: output/file.wasm `, tool: build, - expectedStdOut: fmt.Sprintf("testdata/toolchain/docker run -u 1001:1002 --rm -t -v %s:/source -w /source --init -e EXIT_CODE=3 build/image build --output-file output/file.wasm\n", workspace.GetDir().GetRootDir()), - expectedErr: fmt.Sprintf("failed to execute an external command \"testdata/toolchain/docker run -u 1001:1002 --rm -t -v %s:/source -w /source --init -e EXIT_CODE=3 build/image build --output-file output/file.wasm\": exit status 3", workspace.GetDir().GetRootDir()), + expectedStdOut: fmt.Sprintf("testdata/toolchain/docker run -u 1001:1002 --rm -t -v %s:/source -w /source --init -e DOCKER_EXIT_CODE=3 build/image build --output-file output/file.wasm\n", workspace.GetDir().GetRootDir()), + expectedErr: fmt.Sprintf("failed to execute an external command \"testdata/toolchain/docker run -u 1001:1002 --rm -t -v %s:/source -w /source --init -e DOCKER_EXIT_CODE=3 build/image build --output-file output/file.wasm\": exit status 3", workspace.GetDir().GetRootDir()), } }), Entry("test using given container image", func() testCase { @@ -300,11 +300,11 @@ var _ = Describe("built-in toolchain", func() { image: test/image options: - -e - - EXIT_CODE=3 + - DOCKER_EXIT_CODE=3 `, tool: test, - expectedStdOut: fmt.Sprintf("testdata/toolchain/docker run -u 1001:1002 --rm -t -v %s:/source -w /source --init -e EXIT_CODE=3 test/image test\n", workspace.GetDir().GetRootDir()), - expectedErr: fmt.Sprintf("failed to execute an external command \"testdata/toolchain/docker run -u 1001:1002 --rm -t -v %s:/source -w /source --init -e EXIT_CODE=3 test/image test\": exit status 3", workspace.GetDir().GetRootDir()), + expectedStdOut: fmt.Sprintf("testdata/toolchain/docker run -u 1001:1002 --rm -t -v %s:/source -w /source --init -e DOCKER_EXIT_CODE=3 test/image test\n", workspace.GetDir().GetRootDir()), + expectedErr: fmt.Sprintf("failed to execute an external command \"testdata/toolchain/docker run -u 1001:1002 --rm -t -v %s:/source -w /source --init -e DOCKER_EXIT_CODE=3 test/image test\": exit status 3", workspace.GetDir().GetRootDir()), } }), Entry("clean using given container image", func() testCase { @@ -364,11 +364,11 @@ var _ = Describe("built-in toolchain", func() { image: clean/image options: - -e - - EXIT_CODE=3 + - DOCKER_EXIT_CODE=3 `, tool: clean, - expectedStdOut: fmt.Sprintf("testdata/toolchain/docker run -u 1001:1002 --rm -t -v %s:/source -w /source --init -e EXIT_CODE=3 clean/image clean\n", workspace.GetDir().GetRootDir()), - expectedErr: fmt.Sprintf("failed to execute an external command \"testdata/toolchain/docker run -u 1001:1002 --rm -t -v %s:/source -w /source --init -e EXIT_CODE=3 clean/image clean\": exit status 3", workspace.GetDir().GetRootDir()), + expectedStdOut: fmt.Sprintf("testdata/toolchain/docker run -u 1001:1002 --rm -t -v %s:/source -w /source --init -e DOCKER_EXIT_CODE=3 clean/image clean\n", workspace.GetDir().GetRootDir()), + expectedErr: fmt.Sprintf("failed to execute an external command \"testdata/toolchain/docker run -u 1001:1002 --rm -t -v %s:/source -w /source --init -e DOCKER_EXIT_CODE=3 clean/image clean\": exit status 3", workspace.GetDir().GetRootDir()), } }), ) diff --git a/pkg/test/cmd/command.go b/pkg/test/cmd/command.go new file mode 100644 index 00000000..2202716f --- /dev/null +++ b/pkg/test/cmd/command.go @@ -0,0 +1,234 @@ +// Copyright 2021 Tetrate +// +// 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 cmd + +import ( + "bytes" + "fmt" + "io/fs" + "io/ioutil" + "os" + "os/user" + "path/filepath" + "regexp" + "strings" + "testing" + + "github.com/Masterminds/semver" + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/stretchr/testify/require" + + "github.com/tetratelabs/getenvoy/pkg/cmd" + "github.com/tetratelabs/getenvoy/pkg/common" + builtintoolchain "github.com/tetratelabs/getenvoy/pkg/extension/workspace/toolchain/builtin" + "github.com/tetratelabs/getenvoy/pkg/manifest" + manifesttest "github.com/tetratelabs/getenvoy/pkg/test/manifest" + "github.com/tetratelabs/getenvoy/pkg/types" +) + +// FakeDockerDir includes "docker" which only executes the output. This means it doesn't really invoke docker. +// +// TODO: fake via exec.Run in unit tests because it is less complicated and error-prone than faking via shell scripts. +const FakeDockerDir = "../../../extension/workspace/toolchain/builtin/testdata/toolchain" + +// NewRootCommand initializes a command with buffers for stdout and stderr. +func NewRootCommand() (c *cobra.Command, stdout, stderr *bytes.Buffer) { + stdout = new(bytes.Buffer) + stderr = new(bytes.Buffer) + c = cmd.NewRoot() + c.SetOut(stdout) + c.SetErr(stderr) + return c, stdout, stderr +} + +// RequireNewTempDir creates a new directory. The function returned cleans it up. +func RequireNewTempDir(t *testing.T) (string, func()) { + d, err := ioutil.TempDir("", "") + if err != nil { + require.NoError(t, err, `ioutil.TempDir("", "") erred`) + } + d, err = filepath.EvalSymlinks(d) + require.NoError(t, err, `filepath.EvalSymlinks(%s) erred`, d) + require.NotEmpty(t, d, `filepath.EvalSymlinks(%s) returned ""`) + return d, func() { + e := os.RemoveAll(d) + require.NoError(t, e, `error removing directory: %v`, d) + } +} + +// RequireChDir will os.Chdir into the indicated dir, panicing on any problem. +// The string returned is the absolute path corresponding to the input. The function returned reverts to the original. +func RequireChDir(t *testing.T, d string) (string, func()) { + dir := RequireAbsDir(t, d) + + // Save previous working directory to that it can be reverted later. + previous, err := os.Getwd() + require.NoError(t, err, `error determining current directory`) + + // Now, actually change to the directory. + err = os.Chdir(d) + require.NoError(t, err, `error changing to directory: %v`, d) + return dir, func() { + e := os.Chdir(previous) + require.NoError(t, e, `error changing to directory: %v`, previous) + } +} + +// RequireAbsDir runs filepath.Abs and ensures there are no errors and the input is a directory. +func RequireAbsDir(t *testing.T, d string) string { + dir, err := filepath.Abs(d) + require.NoError(t, err, `error determining absolute directory: %v`, d) + require.DirExists(t, dir, `directory doesn't exist': %v`, dir) + return dir +} + +// RequireOverridePath will prefix os.Setenv with the indicated dir, panicing on any problem. +// The string returned is the absolute path corresponding to the input. The function returned reverts to the original. +func RequireOverridePath(t *testing.T, d string) (string, func()) { + dir := RequireAbsDir(t, d) + + // Save previous path to that it can be reverted later. + previous := os.Getenv("PATH") + + // Place the resolved directory in from of the previous path + path := strings.Join([]string{dir, previous}, string(filepath.ListSeparator)) + + // Now, actually change the PATH env + err := os.Setenv("PATH", path) + require.NoError(t, err, `error setting PATH to: %v`, path) + return dir, func() { + e := os.Setenv("PATH", previous) + require.NoError(t, e, `error reverting to PATH: %v`, previous) + } +} + +// OverrideGetCurrentUser sets builtin.GetCurrentUser to return the indicated user. +// The function returned reverts to the original. +func OverrideGetCurrentUser(u *user.User) func() { + previous := builtintoolchain.GetCurrentUser + builtintoolchain.GetCurrentUser = func() (*user.User, error) { + return u, nil + } + return func() { + builtintoolchain.GetCurrentUser = previous + } +} + +// OverrideHomeDir sets common.HomeDir to return the indicated path. The function returned reverts to the original. +func OverrideHomeDir(homeDir string) func() { + previous := common.HomeDir + common.HomeDir = homeDir + return func() { + common.HomeDir = previous + } +} + +// RequireManifestPlatform returns the current platform as used in manifests. +func RequireManifestPlatform(t *testing.T) string { + key, err := manifest.NewKey("standard:1.17.0") + require.NoError(t, err, `error resolving manifest for key: %s`, key) + return key.Platform +} + +// RequireManifestTestServer calls manifest.SetURL to a test new tests server. +// The function returned stops that server and calls manifest.SetURL with the original URL. +func RequireManifestTestServer(t *testing.T, envoySubstituteArchiveDir string) func() { + testManifest, err := manifesttest.NewSimpleManifest("standard:1.17.0", "wasm:1.15", "wasm:stable") + + require.NoError(t, err, `error creating test manifest`) + + manifestServer := manifesttest.NewServer(&manifesttest.ServerOpts{ + Manifest: testManifest, + GetArtifactDir: func(uri string) (string, error) { + ref, e := types.ParseReference(uri) + if e != nil { + return "", e + } + if ref.Flavor == "wasm" { + return envoySubstituteArchiveDir, nil + } + if ref.Flavor == "standard" { + ver, e := semver.NewVersion(ref.Version) + if e == nil && ver.Major() >= 1 && ver.Minor() >= 17 { + return envoySubstituteArchiveDir, nil + } + } + return "", errors.Errorf("unexpected version of Envoy %q", uri) + }, + OnError: func(err error) { + require.NoError(t, err, `unexpected error from test manifest server`) + }, + }) + + // override location of the GetEnvoy manifest + previous := manifest.GetURL() + u := manifestServer.GetManifestURL() + err = manifest.SetURL(u) + require.NoError(t, err, `error manifest URL to: %s`, u) + + return func() { + e := manifest.SetURL(previous) + manifestServer.Close() // before require to ensure this occurs + require.NoError(t, e, `error reverting manifest URL to: %s`, previous) + } +} + +// InitFakeEnvoyHome creates "$envoyHome/bin/envoy", which echos the commandline, output and stderr. It returns the +// path to the fake envoy script. +// +// "$envoyHome/bin/envoy" also copies any contents in current working directory to "$envoyHome/capture" when invoked. +// +// The capture is necessary because "$envoyHome/bin/envoy" is executed from a getenvoy-managed temp directory, deleted +// on exit. This directory defines how envoy would have run, so we need to save off contents in order to verify them. +// +// TODO: fake via exec.Run in unit tests because it is less complicated and error-prone than faking via shell scripts. +func InitFakeEnvoyHome(t *testing.T, envoyHome string) string { + // Setup $envoyHome/bin and $envoyHome/capture + _ = os.Mkdir(envoyHome, fs.ModePerm) + envoyBin := filepath.Join(envoyHome, "bin") + envoyCapture := filepath.Join(envoyHome, "capture") + for _, dir := range []string{envoyBin, envoyCapture} { + err := os.Mkdir(dir, fs.ModePerm) + require.NoError(t, err, `couldn't create directory: %s`, dir) + } + + // Create script literal of $envoyHome/bin/envoy which copies the current directory to $envoyCapture when invoked. + // stdout and stderr are prefixed "envoy " to differentiate them from other command output, namely docker. + fakeEnvoyScript := fmt.Sprintf(`#!/bin/sh +set -ue +# Copy all files in the cwd to the capture directory. +cp -r . "%s" + +# Echo invocation context to stdout and fake stderr to ensure it is not combined into stdout. +echo envoy pwd: $PWD +echo envoy bin: $0 +echo envoy args: $@ +echo >&2 envoy stderr +`, envoyCapture) + + // Write $envoyHome/bin/envoy and ensure it is executable + fakeEnvoyPath := filepath.Join(envoyBin, "envoy") + err := ioutil.WriteFile(fakeEnvoyPath, []byte(fakeEnvoyScript), 0700) // nolint:gosec + require.NoError(t, err, `couldn't create fake envoy script: %s`, fakeEnvoyPath) + return fakeEnvoyPath +} + +// ParseEnvoyWorkDirectory returns the CWD captured by the script generated by InitFakeEnvoyHome. +func ParseEnvoyWorkDirectory(stdout *bytes.Buffer) string { + re := regexp.MustCompile(`.*envoy pwd: (.*)\n.*`) + envoyWd := re.FindStringSubmatch(stdout.String())[1] + return envoyWd +} diff --git a/pkg/test/cmd/extension/context.go b/pkg/test/cmd/context.go similarity index 98% rename from pkg/test/cmd/extension/context.go rename to pkg/test/cmd/context.go index d882c05f..57b7fe02 100644 --- a/pkg/test/cmd/extension/context.go +++ b/pkg/test/cmd/context.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package extension +package cmd import ( "os/user"