From b830fe74d9f99e646f7322bc09349014712a13d0 Mon Sep 17 00:00:00 2001 From: Ryan VanGundy Date: Sun, 25 May 2025 19:33:11 -0400 Subject: [PATCH 1/3] feat(tools): add AWS CLI version check when aws.enabled is true - Add MINIMUM_VERSION_AWS_CLI constant - Implement checkAwsCli function with version validation - Add comprehensive test coverage for AWS CLI checks - Maintain 97.8% overall test coverage --- pkg/constants/constants.go | 1 + pkg/tools/tools_manager.go | 34 +++++++++ pkg/tools/tools_manager_test.go | 121 +++++++++++++++++++++++++++++++- 3 files changed, 155 insertions(+), 1 deletion(-) diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index 1b4a6d66e..3701b5648 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -87,4 +87,5 @@ const ( MINIMUM_VERSION_TALOSCTL = "1.7.0" MINIMUM_VERSION_TERRAFORM = "1.7.0" MINIMUM_VERSION_1PASSWORD = "2.15.0" + MINIMUM_VERSION_AWS_CLI = "2.15.0" ) diff --git a/pkg/tools/tools_manager.go b/pkg/tools/tools_manager.go index 41467cd71..c680da699 100644 --- a/pkg/tools/tools_manager.go +++ b/pkg/tools/tools_manager.go @@ -96,6 +96,14 @@ func (t *BaseToolsManager) Check() error { } } + if t.configHandler.GetBool("aws.enabled") { + if err := t.checkAwsCli(); err != nil { + spin.Stop() + fmt.Fprintf(os.Stderr, "\033[31m✗ %s - Failed\033[0m\n", message) + return fmt.Errorf("aws cli check failed: %v", err) + } + } + if t.configHandler.GetBool("cluster.enabled") { if err := t.checkKubectl(); err != nil { spin.Stop() @@ -296,6 +304,32 @@ func (t *BaseToolsManager) checkOnePassword() error { return nil } +// checkAwsCli ensures AWS CLI is available in the system's PATH using execLookPath. +// It checks for 'aws' in the system's PATH and verifies its version. +// Returns nil if found and meets the minimum version requirement, else an error indicating it is not available or outdated. +func (t *BaseToolsManager) checkAwsCli() error { + if _, err := execLookPath("aws"); err != nil { + return fmt.Errorf("aws cli is not available in the PATH") + } + output, _ := t.shell.ExecSilent("aws", "--version") + re := regexp.MustCompile(`aws-cli/(\d+\.\d+\.\d+)`) + match := re.FindStringSubmatch(output) + var awsVersion string + if len(match) > 1 { + awsVersion = match[1] + } else { + awsVersion = extractVersion(output) + } + if awsVersion == "" { + return fmt.Errorf("failed to extract aws cli version") + } + if compareVersion(awsVersion, constants.MINIMUM_VERSION_AWS_CLI) < 0 { + return fmt.Errorf("aws cli version %s is below the minimum required version %s", awsVersion, constants.MINIMUM_VERSION_AWS_CLI) + } + + return nil +} + // compareVersion is a helper function to compare two version strings. // It returns -1 if version1 < version2, 0 if version1 == version2, and 1 if version1 > version2. func compareVersion(version1, version2 string) int { diff --git a/pkg/tools/tools_manager_test.go b/pkg/tools/tools_manager_test.go index 5d4ae8614..0e48516c6 100644 --- a/pkg/tools/tools_manager_test.go +++ b/pkg/tools/tools_manager_test.go @@ -99,6 +99,8 @@ func setupMocks(t *testing.T, opts ...*SetupOptions) *Mocks { return fmt.Sprintf("Terraform v%s", constants.MINIMUM_VERSION_TERRAFORM), nil case name == "op" && args[0] == "--version": return fmt.Sprintf("1Password CLI %s", constants.MINIMUM_VERSION_1PASSWORD), nil + case name == "aws" && args[0] == "--version": + return fmt.Sprintf("aws-cli/%s Python/3.13.3 Darwin/24.0.0 exe/x86_64", constants.MINIMUM_VERSION_AWS_CLI), nil } return "", fmt.Errorf("command not found") } @@ -262,6 +264,7 @@ func TestToolsManager_Check(t *testing.T) { "talosctl": {"version", "--client", "--short"}, "terraform": {"version"}, "op": {"--version"}, + "aws": {"--version"}, } // When checking tool versions err := toolsManager.Check() @@ -283,7 +286,8 @@ func TestToolsManager_Check(t *testing.T) { !strings.Contains(output, constants.MINIMUM_VERSION_KUBECTL) && !strings.Contains(output, constants.MINIMUM_VERSION_TALOSCTL) && !strings.Contains(output, constants.MINIMUM_VERSION_TERRAFORM) && - !strings.Contains(output, constants.MINIMUM_VERSION_1PASSWORD) { + !strings.Contains(output, constants.MINIMUM_VERSION_1PASSWORD) && + !strings.Contains(output, constants.MINIMUM_VERSION_AWS_CLI) { t.Errorf("Expected %s version check to pass, got output: %s", tool, output) } } @@ -454,6 +458,33 @@ contexts: t.Errorf("Expected error to contain '1password check failed: 1Password CLI is not available in the PATH', got: %v", err) } }) + + t.Run("MultipleToolFailures", func(t *testing.T) { + // Given multiple tools are enabled but fail checks + mocks, toolsManager := setup(t, defaultConfig) + mocks.ConfigHandler.SetContextValue("docker.enabled", true) + mocks.ConfigHandler.SetContextValue("aws.enabled", true) + mocks.ConfigHandler.SetContextValue("cluster.enabled", true) + + // Mock failures for multiple tools + originalExecLookPath := execLookPath + execLookPath = func(name string) (string, error) { + if name == "docker" || name == "aws" || name == "kubectl" { + return "", fmt.Errorf("%s is not available in the PATH", name) + } + return originalExecLookPath(name) + } + + // When checking tool versions + err := toolsManager.Check() + + // Then an error should be returned for the first failing tool + if err == nil { + t.Error("Expected error when multiple tools fail checks") + } else if !strings.Contains(err.Error(), "docker check failed") { + t.Errorf("Expected error to contain 'docker check failed', got: %v", err) + } + }) } // ============================================================================= @@ -953,6 +984,94 @@ func TestToolsManager_checkOnePassword(t *testing.T) { }) } +// Tests for AWS CLI version validation +func TestToolsManager_checkAwsCli(t *testing.T) { + setup := func(t *testing.T) (*Mocks, *BaseToolsManager) { + t.Helper() + mocks := setupMocks(t) + toolsManager := NewToolsManager(mocks.Injector) + toolsManager.Initialize() + return mocks, toolsManager + } + + t.Run("Success", func(t *testing.T) { + // Given AWS CLI is available with correct version + mocks, toolsManager := setup(t) + execLookPath = func(name string) (string, error) { + return "/usr/bin/" + name, nil + } + mocks.Shell.ExecSilentFunc = func(name string, args ...string) (string, error) { + if name == "aws" && args[0] == "--version" { + return "aws-cli/2.15.0", nil + } + return "", fmt.Errorf("command not found") + } + // When checking AWS CLI version + err := toolsManager.checkAwsCli() + // Then no error should be returned + if err != nil { + t.Errorf("Expected checkAwsCli to succeed, but got error: %v", err) + } + }) + + t.Run("AwsCliNotAvailable", func(t *testing.T) { + // Given AWS CLI is not found in PATH + _, toolsManager := setup(t) + execLookPath = func(name string) (string, error) { + if name == "aws" { + return "", fmt.Errorf("aws cli is not available in the PATH") + } + return "/usr/bin/" + name, nil + } + // When checking AWS CLI version + err := toolsManager.checkAwsCli() + // Then an error indicating AWS CLI is not available should be returned + if err == nil || !strings.Contains(err.Error(), "aws cli is not available in the PATH") { + t.Errorf("Expected aws cli not available error, got %v", err) + } + }) + + t.Run("AwsCliVersionInvalidResponse", func(t *testing.T) { + // Given AWS CLI version response is invalid + mocks, toolsManager := setup(t) + execLookPath = func(name string) (string, error) { + return "/usr/bin/" + name, nil + } + mocks.Shell.ExecSilentFunc = func(name string, args ...string) (string, error) { + if name == "aws" && args[0] == "--version" { + return "Invalid version response", nil + } + return "", fmt.Errorf("command not found") + } + // When checking AWS CLI version + err := toolsManager.checkAwsCli() + // Then an error indicating version extraction failed should be returned + if err == nil || !strings.Contains(err.Error(), "failed to extract aws cli version") { + t.Errorf("Expected failed to extract aws cli version error, got %v", err) + } + }) + + t.Run("AwsCliVersionTooLow", func(t *testing.T) { + // Given AWS CLI version is below minimum required version + mocks, toolsManager := setup(t) + execLookPath = func(name string) (string, error) { + return "/usr/bin/" + name, nil + } + mocks.Shell.ExecSilentFunc = func(name string, args ...string) (string, error) { + if name == "aws" && args[0] == "--version" { + return "aws-cli/1.0.0", nil + } + return "", fmt.Errorf("command not found") + } + // When checking AWS CLI version + err := toolsManager.checkAwsCli() + // Then an error indicating version is too low should be returned + if err == nil || !strings.Contains(err.Error(), "aws cli version 1.0.0 is below the minimum required version") { + t.Errorf("Expected aws cli version too low error, got %v", err) + } + }) +} + // ============================================================================= // Test Public Helpers // ============================================================================= From cbbbfa04b0a00848950761dda0458e9837bad214 Mon Sep 17 00:00:00 2001 From: Ryan VanGundy Date: Sun, 25 May 2025 19:33:31 -0400 Subject: [PATCH 2/3] go mod tidy --- go.mod | 1 - go.sum | 1 - 2 files changed, 2 deletions(-) diff --git a/go.mod b/go.mod index b88c0fcd2..f38fd58e5 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,6 @@ require ( github.com/aws/smithy-go v1.22.3 github.com/briandowns/spinner v1.23.2 github.com/compose-spec/compose-go v1.20.2 - github.com/compose-spec/compose-go/v2 v2.6.4 github.com/fluxcd/kustomize-controller/api v1.5.1 github.com/fluxcd/pkg/apis/kustomize v1.9.0 github.com/fluxcd/pkg/apis/meta v1.10.0 diff --git a/go.sum b/go.sum index dde286bfc..88e751276 100644 --- a/go.sum +++ b/go.sum @@ -125,7 +125,6 @@ github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/compose-spec/compose-go v1.20.2 h1:u/yfZHn4EaHGdidrZycWpxXgFffjYULlTbRfJ51ykjQ= github.com/compose-spec/compose-go v1.20.2/go.mod h1:+MdqXV4RA7wdFsahh/Kb8U0pAJqkg7mr4PM9tFKU8RM= -github.com/compose-spec/compose-go/v2 v2.6.4/go.mod h1:vPlkN0i+0LjLf9rv52lodNMUTJF5YHVfHVGLLIP67NA= github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4= github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= From 41d234972a9fc9f8f07d99b99a6f2eeea2c46aec Mon Sep 17 00:00:00 2001 From: Ryan VanGundy Date: Sun, 25 May 2025 19:33:48 -0400 Subject: [PATCH 3/3] Add awscli to aqua --- aqua.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/aqua.yaml b/aqua.yaml index 04147be46..45b6b362c 100644 --- a/aqua.yaml +++ b/aqua.yaml @@ -30,3 +30,4 @@ packages: - name: helm/helm@v3.18.0 - name: 1password/cli@v2.30.3 - name: fluxcd/flux2@v2.5.1 +- name: aws/aws-cli@2.27.22