Skip to content
Draft
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
120 changes: 111 additions & 9 deletions cmd/kubectl-datadog/autoscaling/cluster/common/clients/clients.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"errors"
"fmt"
"log"
"strings"

awssdk "github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/aws/arn"
Expand Down Expand Up @@ -56,6 +57,15 @@ func Build(ctx context.Context, configFlags *genericclioptions.ConfigFlags, k8sC
return nil, fmt.Errorf("failed to load AWS config: %w", err)
}

// Reconcile the AWS region with the target EKS cluster before building the
// service clients (so a missing AWS_REGION is derived from the kubeconfig or
// reported clearly rather than as an opaque STS failure, and a region
// pointing elsewhere than the cluster is rejected).
awsConfig, err = reconcileRegion(ctx, awsConfig, configFlags)
if err != nil {
return nil, err
}

sch := runtime.NewScheme()

if err = scheme.AddToScheme(sch); err != nil {
Expand Down Expand Up @@ -158,33 +168,108 @@ func ResolveClusterName(configFlags *genericclioptions.ConfigFlags, explicit str
return name, nil
}

// getAccountIDFromKubeconfig attempts to extract the AWS account ID from the
// kubeconfig context. Returns an empty string if the context is not an EKS ARN.
func getAccountIDFromKubeconfig(configFlags *genericclioptions.ConfigFlags) (string, error) {
// getClusterARNFromKubeconfig returns the EKS cluster ARN parsed from the
// kubeconfig context. ok is false when the context is absent or its cluster
// field is not an ARN (e.g. plain name, eksctl FQDN) — treated as a normal
// fallback, not an error. The parsed ARN carries both the AWS account ID and
// the region, which are independent of the AWS credentials and cannot be
// fooled by same-named clusters in other accounts or regions.
func getClusterARNFromKubeconfig(configFlags *genericclioptions.ConfigFlags) (arn.ARN, bool, error) {
kubeRawConfig, kubeContext, err := resolveKubeContext(configFlags)
if err != nil || kubeContext == "" {
return "", err
return arn.ARN{}, false, err
}

kubeCtx, exists := kubeRawConfig.Contexts[kubeContext]
if !exists {
return "", fmt.Errorf("kube context %q doesn’t exist", kubeContext)
return arn.ARN{}, false, fmt.Errorf("kube context %q doesn’t exist", kubeContext)
}

// The kubeconfig cluster field may not be an ARN (e.g. plain name,
// eksctl FQDN). Treat that as a normal fallback, not an error.
if !arn.IsARN(kubeCtx.Cluster) {
return "", nil
return arn.ARN{}, false, nil
}

parsed, err := arn.Parse(kubeCtx.Cluster)
if err != nil {
return "", fmt.Errorf("failed to parse EKS cluster ARN %q: %w", kubeCtx.Cluster, err)
return arn.ARN{}, false, fmt.Errorf("failed to parse EKS cluster ARN %q: %w", kubeCtx.Cluster, err)
}

// Only an EKS cluster ARN carries the account/region we rely on. Any other
// ARN that happens to sit in the cluster field is treated as a normal
// non-ARN fallback rather than a misleading source of identity.
if parsed.Service != "eks" || !strings.HasPrefix(parsed.Resource, "cluster/") {
return arn.ARN{}, false, nil
}

return parsed, true, nil
}

// getAccountIDFromKubeconfig attempts to extract the AWS account ID from the
// kubeconfig context. Returns an empty string if the context is not an EKS ARN.
func getAccountIDFromKubeconfig(configFlags *genericclioptions.ConfigFlags) (string, error) {
parsed, ok, err := getClusterARNFromKubeconfig(configFlags)
if err != nil || !ok {
return "", err
}
return parsed.AccountID, nil
}

// resolveRegion reconciles the AWS region from the default credential chain
// (empty when unset) with the cluster's region derived from the kubeconfig
// context ARN (empty when the context is not an ARN):
// - both empty: error — the region cannot be determined.
// - configured region empty: derive it from the kubeconfig.
// - both set but different: RegionMismatchError.
// - otherwise: keep the configured region.
func resolveRegion(configRegion, kubeRegion, clusterName string) (string, error) {
switch {
case configRegion == "" && kubeRegion == "":
return "", errors.New("AWS region is not configured and could not be derived from the kubeconfig context; set the AWS_REGION environment variable or configure a region in your AWS profile")
case configRegion == "":
return kubeRegion, nil
case kubeRegion != "" && kubeRegion != configRegion:
return "", &RegionMismatchError{
ConfigRegion: configRegion,
ClusterRegion: kubeRegion,
ClusterName: clusterName,
}
default:
return configRegion, nil
}
}

// reconcileRegion ensures awsConfig targets the same AWS region as the EKS
// cluster from the kubeconfig context. When no region is configured it is
// derived from the kubeconfig and the config is reloaded with it, so credential
// providers built during config load (e.g. assume-role / web-identity STS
// clients) also use it rather than only the service clients created afterward.
// A configured region that differs from the cluster's is rejected with a
// RegionMismatchError.
func reconcileRegion(ctx context.Context, awsConfig awssdk.Config, configFlags *genericclioptions.ConfigFlags) (awssdk.Config, error) {
var kubeRegion, clusterName string
if parsed, ok, err := getClusterARNFromKubeconfig(configFlags); err != nil {
log.Printf("Warning: failed to read AWS region from kubeconfig: %v", err)
} else if ok {
kubeRegion = parsed.Region
clusterName = strings.TrimPrefix(parsed.Resource, "cluster/")
}

region, err := resolveRegion(awsConfig.Region, kubeRegion, clusterName)
if err != nil {
return awssdk.Config{}, err
}
if region == awsConfig.Region {
return awsConfig, nil
}

log.Printf("AWS region not set; using %q from the kubeconfig context.", region)
awsConfig, err = config.LoadDefaultConfig(ctx, config.WithRegion(region))
if err != nil {
return awssdk.Config{}, fmt.Errorf("failed to load AWS config: %w", err)
}
return awsConfig, nil
}

// GetAWSAccountID returns the AWS account ID from the current credentials.
func GetAWSAccountID(ctx context.Context, cli *Clients) (string, error) {
callerIdentity, err := cli.STS.GetCallerIdentity(ctx, &sts.GetCallerIdentityInput{})
Expand Down Expand Up @@ -266,6 +351,23 @@ func (e *AccountMismatchError) Error() string {
)
}

// RegionMismatchError indicates that the configured AWS region and the EKS
// cluster's region (derived from the kubeconfig context ARN) differ.
type RegionMismatchError struct {
ConfigRegion string
ClusterRegion string
ClusterName string
}

func (e *RegionMismatchError) Error() string {
return fmt.Sprintf(
"AWS region mismatch: the configured AWS region is %s, "+
"but EKS cluster %q is in region %s; "+
"set AWS_REGION to %s (or select a kubeconfig context for a cluster in %s)",
e.ConfigRegion, e.ClusterName, e.ClusterRegion, e.ClusterRegion, e.ConfigRegion,
)
}

// ClusterLookupUnavailableError wraps EKS.DescribeCluster failures with a
// ResourceNotFoundException — the cluster does not exist (e.g. already
// deleted). Callers such as uninstall may choose to proceed on this error.
Expand Down
Loading
Loading