diff --git a/internal/k8s/cluster/eks.go b/internal/k8s/cluster/eks.go index 9e90e98..d2a0b63 100644 --- a/internal/k8s/cluster/eks.go +++ b/internal/k8s/cluster/eks.go @@ -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 "" + } +} diff --git a/internal/k8s/cluster/eks_test.go b/internal/k8s/cluster/eks_test.go index 43f5933..f87e1fd 100644 --- a/internal/k8s/cluster/eks_test.go +++ b/internal/k8s/cluster/eks_test.go @@ -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 +}