From 2a472ffe93cb8391409b9a8d557a3400952f691f Mon Sep 17 00:00:00 2001 From: nash Date: Sat, 14 Feb 2026 09:27:49 +0000 Subject: [PATCH] feat(k8s/cluster): add errorHint method to EKS provider Add errorHint method that provides helpful guidance for common AWS CLI errors. This matches the pattern already implemented in the GKE provider. Error categories covered: - Access denied and authorization errors - Resource not found errors - Invalid parameter errors - Resource in use errors - Cluster already exists errors - Limit/quota exceeded errors - Credential errors (missing or expired) - Invalid region errors - VPC/subnet/security group not found errors - IAM role not found errors - Throttling errors Also adds comprehensive tests for all error hint scenarios. Refs #70 Co-Authored-By: Claude Opus 4.6 --- internal/k8s/cluster/eks.go | 42 ++++++++++- internal/k8s/cluster/eks_test.go | 124 +++++++++++++++++++++++++++++++ 2 files changed, 165 insertions(+), 1 deletion(-) 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 +}