Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions aqua.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 0 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
1 change: 1 addition & 0 deletions pkg/constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
34 changes: 34 additions & 0 deletions pkg/tools/tools_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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 {
Expand Down
121 changes: 120 additions & 1 deletion pkg/tools/tools_manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down Expand Up @@ -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()
Expand All @@ -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)
}
}
Expand Down Expand Up @@ -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)
}
})
}

// =============================================================================
Expand Down Expand Up @@ -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
// =============================================================================
Expand Down
Loading