Skip to content
Open
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
42 changes: 41 additions & 1 deletion internal/k8s/cluster/eks.go
Original file line number Diff line number Diff line change
Expand Up @@ -1065,8 +1065,48 @@ func (p *EKSProvider) runAWS(ctx context.Context, args ...string) (string, error

err := cmd.Run()
if err != nil {
return "", fmt.Errorf("aws command failed: %w, stderr: %s", err, stderr.String())
stderrStr := stderr.String()
return "", fmt.Errorf("aws command failed: %w, stderr: %s%s", err, stderrStr, p.errorHint(stderrStr))
}

return stdout.String(), nil
}

// errorHint returns helpful hints for common AWS CLI errors
func (p *EKSProvider) errorHint(stderr string) string {
lower := strings.ToLower(stderr)
switch {
case strings.Contains(lower, "accessdenied") || strings.Contains(lower, "access denied"):
return " (hint: check IAM permissions for EKS operations)"
case strings.Contains(lower, "authorizationerror") || strings.Contains(lower, "not authorized"):
return " (hint: IAM user/role lacks required permissions)"
case strings.Contains(lower, "resourcenotfoundexception"):
return " (hint: cluster or resource does not exist)"
case strings.Contains(lower, "invalidparameterexception"):
return " (hint: check parameter values, e.g., region, cluster name)"
case strings.Contains(lower, "resourceinuseexception"):
return " (hint: resource is currently in use or being modified)"
case strings.Contains(lower, "clusteralreadyexists"):
return " (hint: cluster with this name already exists)"
case strings.Contains(lower, "limitexceeded") || strings.Contains(lower, "service quota"):
return " (hint: AWS service quota exceeded, request increase)"
case strings.Contains(lower, "unable to locate credentials") || strings.Contains(lower, "no credentials"):
return " (hint: run 'aws configure' or set AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY)"
case strings.Contains(lower, "expired token") || strings.Contains(lower, "security token"):
return " (hint: AWS session token expired, refresh credentials)"
case strings.Contains(lower, "invalid region"):
return " (hint: check region name, e.g., us-west-2, eu-west-1)"
case strings.Contains(lower, "vpc") && strings.Contains(lower, "not found"):
return " (hint: VPC does not exist in this region)"
case strings.Contains(lower, "subnet") && strings.Contains(lower, "not found"):
return " (hint: subnet does not exist or is in a different VPC)"
case strings.Contains(lower, "security group") && strings.Contains(lower, "not found"):
return " (hint: security group does not exist or is in a different VPC)"
case strings.Contains(lower, "role") && (strings.Contains(lower, "not found") || strings.Contains(lower, "invalid")):
return " (hint: IAM role ARN is invalid or does not exist)"
case strings.Contains(lower, "throttling") || strings.Contains(lower, "rate exceeded"):
return " (hint: API rate limit exceeded, retry after a moment)"
default:
return ""
}
}
124 changes: 124 additions & 0 deletions internal/k8s/cluster/eks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,3 +263,127 @@ func TestProviderManagerIntegration(t *testing.T) {
t.Error("EKS not found in provider list")
}
}

func TestEKSProviderErrorHints(t *testing.T) {
provider := NewEKSProvider(EKSProviderOptions{
Region: "us-east-1",
})

tests := []struct {
name string
stderr string
contains string
}{
{
name: "access denied",
stderr: "AccessDenied: User is not authorized",
contains: "IAM permissions",
},
{
name: "authorization error",
stderr: "AuthorizationError: not authorized to perform operation",
contains: "IAM user/role lacks required permissions",
},
{
name: "resource not found",
stderr: "ResourceNotFoundException: cluster not found",
contains: "does not exist",
},
{
name: "invalid parameter",
stderr: "InvalidParameterException: invalid value for region",
contains: "check parameter values",
},
{
name: "resource in use",
stderr: "ResourceInUseException: cluster is being updated",
contains: "currently in use",
},
{
name: "cluster already exists",
stderr: "ClusterAlreadyExists: cluster test-cluster already exists",
contains: "already exists",
},
{
name: "limit exceeded",
stderr: "LimitExceeded: maximum number of clusters reached",
contains: "quota exceeded",
},
{
name: "no credentials",
stderr: "Unable to locate credentials",
contains: "aws configure",
},
{
name: "expired token",
stderr: "ExpiredToken: security token has expired",
contains: "session token expired",
},
{
name: "invalid region",
stderr: "Invalid region: us-invalid-1",
contains: "check region name",
},
{
name: "vpc not found",
stderr: "VPC vpc-123 not found",
contains: "VPC does not exist",
},
{
name: "subnet not found",
stderr: "Subnet subnet-123 not found",
contains: "subnet does not exist",
},
{
name: "security group not found",
stderr: "Security group sg-123 not found",
contains: "security group does not exist",
},
{
name: "role not found",
stderr: "Role arn:aws:iam::123:role/invalid not found",
contains: "IAM role ARN is invalid",
},
{
name: "throttling",
stderr: "Throttling: Rate exceeded",
contains: "rate limit exceeded",
},
{
name: "unknown error",
stderr: "Some unknown error occurred",
contains: "", // No hint expected
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
hint := provider.errorHint(tt.stderr)
if tt.contains == "" {
if hint != "" {
t.Errorf("expected no hint, got %q", hint)
}
} else {
if hint == "" {
t.Errorf("expected hint containing %q, got empty", tt.contains)
} else if !containsSubstring(hint, tt.contains) {
t.Errorf("expected hint containing %q, got %q", tt.contains, hint)
}
}
})
}
}

// containsSubstring checks if s contains substr (case-insensitive)
func containsSubstring(s, substr string) bool {
return len(s) >= len(substr) && (s == substr || len(substr) == 0 || findInString(s, substr))
}

func findInString(s, substr string) bool {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}