diff --git a/cloudsupport/v1/ekssupport.go b/cloudsupport/v1/ekssupport.go index 80db1d4..05ee85b 100644 --- a/cloudsupport/v1/ekssupport.go +++ b/cloudsupport/v1/ekssupport.go @@ -6,6 +6,7 @@ import ( "fmt" "net/url" "os" + "regexp" "strings" "github.com/aws/aws-sdk-go-v2/aws" @@ -35,6 +36,12 @@ type IEKSSupport interface { type EKSSupport struct { } +var ( + awsRegionPattern = regexp.MustCompile(`^[a-z]{2}(?:-[a-z]+)?-[a-z]+-\d+$`) + eksArnRegionPattern = regexp.MustCompile(`^arn:[^:]+:eks:([a-z]{2}(?:-[a-z]+)?-[a-z]+-\d+):`) + eksDashedArnRegionPattern = regexp.MustCompile(`^arn-(?:[a-z]+-)+eks-([a-z]{2}(?:-[a-z]+)?-[a-z]+-\d+)-`) +) + const ( awsauthconfigmap = "aws-auth" ) @@ -114,28 +121,34 @@ func (eksSupport *EKSSupport) GetName(describe *eks.DescribeClusterOutput) strin // GetRegion returns the region in which eks cluster is running. func (eksSupport *EKSSupport) GetRegion(cluster string) (string, error) { region, present := os.LookupEnv(KS_CLOUD_REGION_ENV_VAR) - if present { + if present && region != "" { return region, nil } + + if matches := eksArnRegionPattern.FindStringSubmatch(cluster); len(matches) == 2 { + return matches[1], nil + } + + if matches := eksDashedArnRegionPattern.FindStringSubmatch(cluster); len(matches) == 2 { + return matches[1], nil + } + splittedClusterContext := strings.Split(cluster, ".") + if len(splittedClusterContext) >= 2 && awsRegionPattern.MatchString(splittedClusterContext[1]) { + return splittedClusterContext[1], nil + } - if len(splittedClusterContext) < 2 { - splittedClusterContext := strings.Split(cluster, ":") - if len(splittedClusterContext) < 4 { - splittedClusterContext := strings.Split(cluster, "-") - if len(splittedClusterContext) < 4 { - return "", fmt.Errorf("failed to get region") - } else if len(splittedClusterContext) >= 6 { - return strings.Join(splittedClusterContext[3:6], "-"), nil - } else { - return "", fmt.Errorf("failed to get region") - } - } - region = splittedClusterContext[3] - } else { - region = splittedClusterContext[1] + region, present = os.LookupEnv("AWS_REGION") + if present && region != "" { + return region, nil } - return region, nil + + awsConfig, err := config.LoadDefaultConfig(context.TODO()) + if err == nil && awsConfig.Region != "" { + return awsConfig.Region, nil + } + + return "", fmt.Errorf("failed to get region: tried KS_CLOUD_REGION, cluster name parsing, AWS_REGION, and AWS config") } // Context can be in one of 3 ways: diff --git a/cloudsupport/v1/ekssupport_test.go b/cloudsupport/v1/ekssupport_test.go index 7eb6b2b..cec3261 100644 --- a/cloudsupport/v1/ekssupport_test.go +++ b/cloudsupport/v1/ekssupport_test.go @@ -2,6 +2,7 @@ package v1 import ( "os" + "path/filepath" "strings" "testing" @@ -10,7 +11,59 @@ import ( clientcmdapi "k8s.io/client-go/tools/clientcmd/api" ) +func setEnv(t *testing.T, key, value string) { + t.Helper() + + previousValue, hadPreviousValue := os.LookupEnv(key) + if value == "" { + if err := os.Unsetenv(key); err != nil { + t.Fatalf("unset %s: %v", key, err) + } + } else { + if err := os.Setenv(key, value); err != nil { + t.Fatalf("set %s: %v", key, err) + } + } + + t.Cleanup(func() { + var err error + if hadPreviousValue { + err = os.Setenv(key, previousValue) + } else { + err = os.Unsetenv(key) + } + + if err != nil { + t.Fatalf("restore %s: %v", key, err) + } + }) +} + +func isolateAWSRegionSources(t *testing.T) { + t.Helper() + + configPath := filepath.Join(t.TempDir(), "config") + credentialsPath := filepath.Join(t.TempDir(), "credentials") + + if err := os.WriteFile(configPath, nil, 0o600); err != nil { + t.Fatalf("write %s: %v", configPath, err) + } + if err := os.WriteFile(credentialsPath, nil, 0o600); err != nil { + t.Fatalf("write %s: %v", credentialsPath, err) + } + + setEnv(t, KS_CLOUD_REGION_ENV_VAR, "") + setEnv(t, "AWS_REGION", "") + setEnv(t, "AWS_DEFAULT_REGION", "") + setEnv(t, "AWS_PROFILE", "") + setEnv(t, "AWS_EC2_METADATA_DISABLED", "true") + setEnv(t, "AWS_CONFIG_FILE", configPath) + setEnv(t, "AWS_SHARED_CREDENTIALS_FILE", credentialsPath) +} + func TestGetContextName(t *testing.T) { + isolateAWSRegionSources(t) + defer tearDown() // Test ARN context names @@ -134,9 +187,10 @@ func TestGetRegion(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + isolateAWSRegionSources(t) + if tt.envRegion != "" { - os.Setenv(KS_CLOUD_REGION_ENV_VAR, tt.envRegion) - defer os.Unsetenv(KS_CLOUD_REGION_ENV_VAR) + setEnv(t, KS_CLOUD_REGION_ENV_VAR, tt.envRegion) } eksSupport := &EKSSupport{} @@ -150,4 +204,153 @@ func TestGetRegion(t *testing.T) { } }) } + + t.Run("AWS_REGION environment variable is checked", func(t *testing.T) { + isolateAWSRegionSources(t) + + awsRegion := "ap-southeast-2" + setEnv(t, "AWS_REGION", awsRegion) + + eksSupport := &EKSSupport{} + region, err := eksSupport.GetRegion("my-cluster") + + assert.NoError(t, err) + assert.Equal(t, awsRegion, region) + }) + + t.Run("KS_CLOUD_REGION takes precedence over AWS_REGION", func(t *testing.T) { + isolateAWSRegionSources(t) + + ksRegion := "us-west-2" + awsRegion := "eu-west-1" + setEnv(t, KS_CLOUD_REGION_ENV_VAR, ksRegion) + setEnv(t, "AWS_REGION", awsRegion) + + eksSupport := &EKSSupport{} + region, err := eksSupport.GetRegion("my-cluster") + + assert.NoError(t, err) + assert.Equal(t, ksRegion, region) + }) + + t.Run("KS_CLOUD_REGION takes precedence over cluster name parsing", func(t *testing.T) { + isolateAWSRegionSources(t) + setEnv(t, KS_CLOUD_REGION_ENV_VAR, "us-west-2") + + eksSupport := &EKSSupport{} + region, err := eksSupport.GetRegion("arn:aws:eks:eu-north-1:123456789:cluster/test-cluster") + + assert.NoError(t, err) + assert.Equal(t, "us-west-2", region) + }) + + t.Run("Cluster name parsing takes precedence over AWS default config", func(t *testing.T) { + isolateAWSRegionSources(t) + setEnv(t, "AWS_DEFAULT_REGION", "us-east-1") + + eksSupport := &EKSSupport{} + region, err := eksSupport.GetRegion("arn:aws:eks:eu-north-1:123456789:cluster/test-cluster") + + assert.NoError(t, err) + assert.Equal(t, "eu-north-1", region) + }) + + t.Run("Cluster name parsing takes precedence over AWS_REGION", func(t *testing.T) { + isolateAWSRegionSources(t) + setEnv(t, "AWS_REGION", "us-east-1") + + eksSupport := &EKSSupport{} + region, err := eksSupport.GetRegion("arn:aws:eks:eu-north-1:123456789:cluster/test-cluster") + + assert.NoError(t, err) + assert.Equal(t, "eu-north-1", region) + }) + + t.Run("Partitioned ARN parsing takes precedence over AWS_REGION", func(t *testing.T) { + tests := []struct { + name string + cluster string + region string + }{ + { + name: "GovCloud standard ARN", + cluster: "arn:aws-us-gov:eks:us-gov-west-1:123456789012:cluster/test-cluster", + region: "us-gov-west-1", + }, + { + name: "China standard ARN", + cluster: "arn:aws-cn:eks:cn-north-1:123456789012:cluster/test-cluster", + region: "cn-north-1", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + isolateAWSRegionSources(t) + setEnv(t, "AWS_REGION", "us-east-1") + + eksSupport := &EKSSupport{} + region, err := eksSupport.GetRegion(tt.cluster) + + assert.NoError(t, err) + assert.Equal(t, tt.region, region) + }) + } + }) + + t.Run("Dashed ARN parsing supports GovCloud regions", func(t *testing.T) { + isolateAWSRegionSources(t) + setEnv(t, "AWS_REGION", "us-east-1") + + eksSupport := &EKSSupport{} + region, err := eksSupport.GetRegion("arn-aws-us-gov-eks-us-gov-west-1-123456789012-cluster-test-cluster") + + assert.NoError(t, err) + assert.Equal(t, "us-gov-west-1", region) + }) + + t.Run("Valid dotted cluster name takes precedence over AWS fallbacks", func(t *testing.T) { + isolateAWSRegionSources(t) + setEnv(t, "AWS_REGION", "us-east-1") + setEnv(t, "AWS_DEFAULT_REGION", "ap-southeast-1") + + eksSupport := &EKSSupport{} + region, err := eksSupport.GetRegion("cluster.us-west-2.eksctl.io") + + assert.NoError(t, err) + assert.Equal(t, "us-west-2", region) + }) + + t.Run("Invalid dotted cluster name falls back to AWS_REGION", func(t *testing.T) { + isolateAWSRegionSources(t) + setEnv(t, "AWS_REGION", "us-west-2") + + eksSupport := &EKSSupport{} + region, err := eksSupport.GetRegion("foo.bar") + + assert.NoError(t, err) + assert.Equal(t, "us-west-2", region) + }) + + t.Run("Invalid parsed region falls back to AWS default config", func(t *testing.T) { + isolateAWSRegionSources(t) + setEnv(t, "AWS_DEFAULT_REGION", "ap-southeast-1") + + eksSupport := &EKSSupport{} + region, err := eksSupport.GetRegion("arn:aws:eks:not-a-region:123456789:cluster/test-cluster") + + assert.NoError(t, err) + assert.Equal(t, "ap-southeast-1", region) + }) + + t.Run("AWS default config is used after cluster parsing fails", func(t *testing.T) { + isolateAWSRegionSources(t) + setEnv(t, "AWS_DEFAULT_REGION", "ap-southeast-1") + + eksSupport := &EKSSupport{} + region, err := eksSupport.GetRegion("my-cluster") + + assert.NoError(t, err) + assert.Equal(t, "ap-southeast-1", region) + }) }