From dcd09c43dbaf4ac830e3b3524a8585131118686f Mon Sep 17 00:00:00 2001 From: Daneyon Hansen Date: Tue, 12 Nov 2024 10:51:34 -0800 Subject: [PATCH] Adds Darwin Support for Vault and Consul e2e Deps (#10156) Signed-off-by: Daneyon Hansen --- changelog/v1.18.0-beta34/issue_10152.yaml | 7 + test/services/consul.go | 9 +- test/services/utils/download.go | 179 ++++++++++++++++++---- test/services/utils/download_test.go | 156 +++++++++++++++++++ test/services/vault.go | 7 +- test/testutils/services.go | 21 +++ 6 files changed, 338 insertions(+), 41 deletions(-) create mode 100644 changelog/v1.18.0-beta34/issue_10152.yaml create mode 100644 test/services/utils/download_test.go create mode 100644 test/testutils/services.go diff --git a/changelog/v1.18.0-beta34/issue_10152.yaml b/changelog/v1.18.0-beta34/issue_10152.yaml new file mode 100644 index 00000000000..2dbd98f4866 --- /dev/null +++ b/changelog/v1.18.0-beta34/issue_10152.yaml @@ -0,0 +1,7 @@ +changelog: + - type: NON_USER_FACING + issueLink: https://github.com/solo-io/gloo/issues/10152 + resolvesIssue: true + description: >- + Adds `darwin` operating system support for the Consul and Vault dependencies + used by Kubernetes e2e testing. diff --git a/test/services/consul.go b/test/services/consul.go index 4e8eea4bb70..d1baed5faae 100644 --- a/test/services/consul.go +++ b/test/services/consul.go @@ -20,11 +20,6 @@ import ( "github.com/solo-io/gloo/test/testutils" ) -const ( - consulDockerImage = "hashicorp/consul:1.15.3" - consulBinaryName = "consul" -) - type ConsulFactory struct { consulPath string tmpdir string @@ -49,8 +44,8 @@ func NewConsulFactory() (*ConsulFactory, error) { return nil, err } binaryPath, err := utils.GetBinary(utils.GetBinaryParams{ - Filename: consulBinaryName, - DockerImage: consulDockerImage, + Filename: testutils.ConsulBinaryName, + DockerImage: testutils.ConsulDockerImage, DockerPath: "/bin/consul", EnvKey: testutils.ConsulBinary, TmpDir: tmpdir, diff --git a/test/services/utils/download.go b/test/services/utils/download.go index c65abb32381..8144e1401f8 100644 --- a/test/services/utils/download.go +++ b/test/services/utils/download.go @@ -1,51 +1,85 @@ package utils import ( + "archive/zip" "fmt" "log" + "net/http" "os" "os/exec" "path/filepath" + "runtime" "github.com/onsi/ginkgo/v2" + + "github.com/solo-io/gloo/test/testutils" ) -type GetBinaryParams struct { - Filename string // the name of the binary on the $PATH or in the docker container - DockerImage string // the docker image to use if Env or Local are not present - DockerPath string // the location of the binary in the docker container, including the filename - EnvKey string // the environment var to look at for a user-specified service binary - TmpDir string // the temp directory to store a downloaded binary if needed -} +// ExecLookPathWrapper is a wrapper around exec.LookPath so it can be mocked in tests. +var ExecLookPathWrapper = exec.LookPath -// GetBinary uses the passed params structure to get a binary for the service in the first found of 3 locations: -// -// 1. specified via environment variable -// -// 2. matching binary already on path -// -// 3. download the hard-coded version via docker and extract the binary from that -func GetBinary(params GetBinaryParams) (string, error) { - // first check if an environment variable was specified for the binary location - envPath := os.Getenv(params.EnvKey) - if envPath != "" { - log.Printf("Using %s specified in environment variable %s: %s", params.Filename, params.EnvKey, envPath) - return envPath, nil +// DownloadAndExtractBinary downloads and extracts the Vault or Consul binary for darwin OS +var DownloadAndExtractBinary = func(tmpDir, filename, version string) (string, error) { + goos := runtime.GOOS + goarch := runtime.GOARCH + + url := fmt.Sprintf("https://releases.hashicorp.com/%s/%s/%s_%s_%s_%s.zip", filename, version, filename, version, goos, goarch) + log.Printf("Downloading %s binary from: %s", filename, url) + + // Create temp zip file + zipPath := filepath.Join(tmpDir, fmt.Sprintf("%s.zip", filename)) + out, err := os.Create(zipPath) + if err != nil { + return "", fmt.Errorf("failed to create temp zip file: %w", err) } + defer out.Close() - // next check if we have a matching binary on $PATH - localPath, err := exec.LookPath(params.Filename) - if err == nil { - log.Printf("Using %s from PATH: %s", params.Filename, localPath) - return localPath, nil + // Download the zip file + resp, err := http.Get(url) + if err != nil { + return "", fmt.Errorf("failed to download %s binary: %w", filename, err) } + defer resp.Body.Close() - // finally, try to grab one from docker - return dockerDownload(params.TmpDir, params) + _, err = out.ReadFrom(resp.Body) + if err != nil { + return "", fmt.Errorf("failed to download %s binary: %w", filename, err) + } + // Extract the zip file + err = Unzip(zipPath, tmpDir) + if err != nil { + return "", fmt.Errorf("failed to unzip %s binary: %w", filename, err) + } + + // Add extracted binary to PATH + binaryPath := filepath.Join(tmpDir, filename) + if err := os.Chmod(binaryPath, 0755); err != nil { + return "", fmt.Errorf("failed to make %s binary executable: %w", filename, err) + } + + log.Printf("%s binary extracted to: %s", filename, binaryPath) + return binaryPath, nil } -func dockerDownload(tmpdir string, params GetBinaryParams) (string, error) { +// DockerDownload extracts a binary from a Docker image by running a temporary +// Docker container, copying the binary from the container's filesystem, and +// saving it to a local temporary directory. This function is primarily used +// when a binary is not available via environment variables or on the system's PATH. +// +// tmpdir: The temporary directory where the binary will be saved. +// params: A struct containing parameters such as the filename, Docker image, +// +// and Docker path to locate the binary in the container. +// +// Returns the path to the saved binary on success or an error if the operation fails. +var DockerDownload = func(tmpdir string, params GetBinaryParams) (string, error) { + goos := runtime.GOOS + + if goos == "darwin" { + return "", fmt.Errorf("unsupported operating system: %s", goos) + } + log.Printf("Using %s from docker image: %s", params.Filename, params.DockerImage) // use bash to run a docker container and extract the binary file from the running container @@ -53,7 +87,7 @@ func dockerDownload(tmpdir string, params GetBinaryParams) (string, error) { set -ex CID=$(docker run -d %s /bin/sh -c exit) -# just print the image sha for repoducibility +# just print the image sha for reproducibility echo "Using %s Image:" docker inspect %s -f "{{.RepoDigests}}" @@ -78,5 +112,92 @@ docker rm -f $CID } return filepath.Join(tmpdir, params.Filename), nil +} + +type GetBinaryParams struct { + Filename string // the name of the binary on the $PATH or in the docker container + DockerImage string // the docker image to use if Env or Local are not present + DockerPath string // the location of the binary in the docker container, including the filename + EnvKey string // the environment var to look at for a user-specified service binary + TmpDir string // the temp directory to store a downloaded binary if needed +} + +// GetBinary checks for a binary in the following order: +// 1. From the environment variable if specified +// 2. Locally available on the $PATH +// 3. If `darwin` is the OS, download the Vault or Consul binary from the HashiCorp release page +// 4. As a last resort, pull the binary from a Docker image +func GetBinary(params GetBinaryParams) (string, error) { + // first check if an environment variable was specified for the binary location + envPath := os.Getenv(params.EnvKey) + if envPath != "" { + log.Printf("Using %s specified in environment variable %s: %s", params.Filename, params.EnvKey, envPath) + return envPath, nil + } + + // next check if we have a matching binary on $PATH + localPath, err := ExecLookPathWrapper(params.Filename) + if err == nil { + log.Printf("Using %s from PATH: %s", params.Filename, localPath) + return localPath, nil + } + + // if GOOS is darwin and the Filename is either vault or consul, download from HashiCorp releases + if runtime.GOOS == "darwin" { + switch params.Filename { + case "vault": + log.Printf("Downloading %s for darwin", params.Filename) + return DownloadAndExtractBinary(params.TmpDir, testutils.VaultBinaryName, testutils.VaultBinaryVersion) + case "consul": + log.Printf("Downloading %s for darwin", params.Filename) + return DownloadAndExtractBinary(params.TmpDir, testutils.ConsulBinaryName, testutils.ConsulBinaryVersion) + } + } + // finally, try to grab one from docker + return DockerDownload(params.TmpDir, params) +} + +// Unzip unzips the given archive to the specified destination +func Unzip(src, dest string) error { + r, err := zip.OpenReader(src) + if err != nil { + return err + } + defer r.Close() + + for _, f := range r.File { + fpath := filepath.Join(dest, f.Name) + + // Create directories or files + if f.FileInfo().IsDir() { + os.MkdirAll(fpath, os.ModePerm) + continue + } + + // Ensure the directory exists + if err := os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil { + return err + } + + // Extract the file + outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) + if err != nil { + return err + } + + rc, err := f.Open() + if err != nil { + return err + } + + _, err = outFile.ReadFrom(rc) + outFile.Close() + rc.Close() + + if err != nil { + return err + } + } + return nil } diff --git a/test/services/utils/download_test.go b/test/services/utils/download_test.go new file mode 100644 index 00000000000..0d938a48c10 --- /dev/null +++ b/test/services/utils/download_test.go @@ -0,0 +1,156 @@ +package utils_test + +import ( + "archive/zip" + "errors" + "os" + "os/exec" + "path/filepath" + "runtime" + "testing" + + "github.com/solo-io/gloo/test/services/utils" + "github.com/solo-io/gloo/test/testutils" + "github.com/stretchr/testify/assert" +) + +func TestGetBinaryFromEnv(t *testing.T) { + // Set up a temporary environment variable + os.Setenv(testutils.VaultBinary, "/usr/local/bin/vault") + defer os.Unsetenv(testutils.VaultBinary) + + params := utils.GetBinaryParams{ + Filename: testutils.VaultBinaryName, + EnvKey: testutils.VaultBinary, + TmpDir: t.TempDir(), + DockerImage: testutils.VaultDockerImage, + } + + // Test that GetBinary reads the binary from the environment variable + binaryPath, err := utils.GetBinary(params) + assert.NoError(t, err) + assert.Equal(t, "/usr/local/bin/vault", binaryPath) +} + +func TestGetBinaryFromPath(t *testing.T) { + // Make sure there is no environment variable set + os.Unsetenv(testutils.VaultBinary) + + // Mock ExecLookPathWrapper to always return a path + utils.ExecLookPathWrapper = func(file string) (string, error) { + if file == testutils.VaultBinaryName { + return "/usr/bin/vault", nil + } + return "", errors.New("not found") + } + // Reset after test + defer func() { utils.ExecLookPathWrapper = exec.LookPath }() + + params := utils.GetBinaryParams{ + Filename: testutils.VaultBinaryName, + TmpDir: t.TempDir(), + DockerImage: testutils.VaultDockerImage, + } + + // Test that GetBinary finds the binary in $PATH + binaryPath, err := utils.GetBinary(params) + assert.NoError(t, err) + assert.Equal(t, "/usr/bin/vault", binaryPath) +} + +func TestGetBinaryDownloadDarwin(t *testing.T) { + if runtime.GOOS != "darwin" { + t.Skip("This test is only for Darwin systems") + } + + // Mock downloading and extracting binary + utils.DownloadAndExtractBinary = func(tmpDir, filename, version string) (string, error) { + return filepath.Join(tmpDir, filename), nil + } + + params := utils.GetBinaryParams{ + Filename: testutils.VaultBinaryName, + TmpDir: t.TempDir(), + DockerImage: testutils.VaultDockerImage, + } + + // Test that GetBinary downloads and extracts the binary on Darwin systems + binaryPath, err := utils.GetBinary(params) + assert.NoError(t, err) + assert.Contains(t, binaryPath, testutils.VaultBinaryName) +} + +func TestGetBinaryDockerDownload(t *testing.T) { + // Mock exec.LookPath to simulate missing binary on $PATH + utils.ExecLookPathWrapper = func(file string) (string, error) { + return "", errors.New("not found") + } + // Reset after test + defer func() { utils.ExecLookPathWrapper = exec.LookPath }() + + params := utils.GetBinaryParams{ + Filename: testutils.VaultBinaryName, + TmpDir: t.TempDir(), + DockerImage: testutils.VaultDockerImage, + } + + // Mock the dockerDownload function + utils.DockerDownload = func(tmpdir string, params utils.GetBinaryParams) (string, error) { + return filepath.Join(tmpdir, testutils.VaultBinaryName), nil + } + + // Test that GetBinary falls back to downloading from Docker + binaryPath, err := utils.GetBinary(params) + assert.NoError(t, err) + assert.Contains(t, binaryPath, testutils.VaultBinaryName) +} + +func TestDownloadAndExtractBinary(t *testing.T) { + // Use a temporary directory for the test + tmpDir := t.TempDir() + + // Replace with real implementation to test + binaryPath, err := utils.DownloadAndExtractBinary(tmpDir, testutils.VaultBinaryName, testutils.VaultBinaryVersion) + assert.NoError(t, err) + assert.Contains(t, binaryPath, "vault") +} + +func TestUnzip(t *testing.T) { + // Use a temporary directory for the test + tmpDir := t.TempDir() + + // Create a zip file in the temporary directory + zipFilePath := filepath.Join(tmpDir, "test.zip") + zipFile, err := os.Create(zipFilePath) + assert.NoError(t, err) + defer zipFile.Close() + + // Write a valid zip file + zipWriter := zip.NewWriter(zipFile) + fileInZip, err := zipWriter.Create("test.txt") + assert.NoError(t, err) + + _, err = fileInZip.Write([]byte("This is a test file inside the zip archive")) + assert.NoError(t, err) + err = zipWriter.Close() + assert.NoError(t, err) + + // Create another directory to unzip into + unzipDir := filepath.Join(tmpDir, "unzipped") + err = os.Mkdir(unzipDir, 0755) + assert.NoError(t, err) + + // Test unzipping the file + err = utils.Unzip(zipFilePath, unzipDir) + assert.NoError(t, err) + + // Verify that the file was unzipped correctly + unzippedFilePath := filepath.Join(unzipDir, "test.txt") + _, err = os.Stat(unzippedFilePath) + assert.NoError(t, err, "unzipped file not found") + + // Read the contents of the unzipped file + unzippedFileContent, err := os.ReadFile(unzippedFilePath) + assert.NoError(t, err) + assert.Equal(t, "This is a test file inside the zip archive", string(unzippedFileContent)) +} diff --git a/test/services/vault.go b/test/services/vault.go index 94aa7c907e5..770d8e69f7d 100644 --- a/test/services/vault.go +++ b/test/services/vault.go @@ -28,9 +28,6 @@ const ( DefaultHost = "127.0.0.1" DefaultPort = 8200 DefaultVaultToken = "root" - - vaultDockerImage = "hashicorp/vault:1.13.3" - vaultBinaryName = "vault" ) type VaultFactory struct { @@ -47,8 +44,8 @@ func NewVaultFactory() (*VaultFactory, error) { return nil, err } binaryPath, err := utils.GetBinary(utils.GetBinaryParams{ - Filename: vaultBinaryName, - DockerImage: vaultDockerImage, + Filename: testutils.VaultBinaryName, + DockerImage: testutils.VaultDockerImage, DockerPath: "/bin/vault", EnvKey: testutils.VaultBinary, TmpDir: tmpdir, diff --git a/test/testutils/services.go b/test/testutils/services.go new file mode 100644 index 00000000000..7df2ecb0bd9 --- /dev/null +++ b/test/testutils/services.go @@ -0,0 +1,21 @@ +package testutils + +import ( + "fmt" +) + +const ( + // ConsulBinaryVersion defines the version of the Consul binary + ConsulBinaryVersion = "1.13.3" + // ConsulBinaryName defines the name of the Consul binary + ConsulBinaryName = "consul" + // VaultBinaryVersion defines the version of the Vault binary + VaultBinaryVersion = "1.13.3" + // VaultBinaryName defines the name of the Vault binary + VaultBinaryName = "vault" +) + +var ( + ConsulDockerImage = fmt.Sprintf("hashicorp/%s:%s", ConsulBinaryName, ConsulBinaryVersion) + VaultDockerImage = fmt.Sprintf("hashicorp/%s:%s", VaultBinaryName, VaultBinaryVersion) +)