Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
213490e
feat: Add EKS kubeconfig authentication integration (ATMOS-157)
Benbentwo Mar 6, 2026
82f85d1
[autofix.ci] apply automated fixes
autofix-ci[bot] Mar 6, 2026
fdbc911
docs: Add EKS kubeconfig authentication documentation
Benbentwo Mar 9, 2026
064da05
fix: resolve EKS token generation and kubeconfig authentication issues
Benbentwo Mar 10, 2026
63f8b50
[autofix.ci] apply automated fixes
autofix-ci[bot] Mar 10, 2026
815fe91
fix: address CodeRabbit review comments on EKS kubeconfig auth
Benbentwo Mar 13, 2026
7bbeb5f
fix: address second round of CodeRabbit review comments
Benbentwo Mar 13, 2026
edc432e
fix: update auth invalid-command snapshot to include eks-token subcom…
Benbentwo Mar 13, 2026
a21fb59
fix: register integration and identity flags in EKS test helper
Benbentwo Mar 13, 2026
10a9f3c
fix: run go mod tidy to fix missing go.sum entries
Benbentwo Mar 16, 2026
590b139
[autofix.ci] apply automated fixes
autofix-ci[bot] Mar 16, 2026
4340965
test: add test coverage for EKS/ECR integrations and kube config
Benbentwo Mar 16, 2026
33dd9d3
[autofix.ci] apply automated fixes
autofix-ci[bot] Mar 16, 2026
d3b7826
refactor: move `atmos auth eks-token` to `atmos aws eks token`
Benbentwo Mar 17, 2026
071fa8d
test: improve cmd/aws/eks coverage from 46% to 87%
Benbentwo Mar 17, 2026
1f7d1f7
[autofix.ci] apply automated fixes
autofix-ci[bot] Mar 19, 2026
6b257a3
Merge branch 'main' into feature/atmos-157-create-atmos-auth-identity…
Benbentwo Mar 20, 2026
70880bb
Merge branch 'main' into feature/atmos-157-create-atmos-auth-identity…
Benbentwo Mar 20, 2026
3668e52
docs: fix EKS tutorial to use exec/shell instead of bare login+kubectl
Benbentwo Mar 21, 2026
15a993b
refactor: move EKS SDK functions out of internal/exec
Benbentwo Mar 21, 2026
baa6f95
[autofix.ci] apply automated fixes
autofix-ci[bot] Mar 21, 2026
6559fe0
Merge branch 'main' into feature/atmos-157-create-atmos-auth-identity…
aknysh Mar 21, 2026
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
52 changes: 50 additions & 2 deletions NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,10 @@ APACHE 2.0 LICENSED DEPENDENCIES
License: Apache-2.0
URL: https://github.com/aws/aws-sdk-go-v2/blob/service/ecr/v1.56.1/service/ecr/LICENSE.txt

- github.com/aws/aws-sdk-go-v2/service/eks
License: Apache-2.0
URL: https://github.com/aws/aws-sdk-go-v2/blob/service/eks/v1.80.2/service/eks/LICENSE.txt

- github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding
License: Apache-2.0
URL: https://github.com/aws/aws-sdk-go-v2/blob/service/internal/accept-encoding/v1.13.7/service/internal/accept-encoding/LICENSE.txt
Expand Down Expand Up @@ -565,18 +569,42 @@ APACHE 2.0 LICENSED DEPENDENCIES
License: Apache-2.0
URL: https://github.com/go-yaml/yaml/blob/v2.4.0/LICENSE

- k8s.io/client-go/util/jsonpath
- k8s.io/apimachinery/pkg
License: Apache-2.0
URL: https://github.com/kubernetes/apimachinery/blob/v0.35.2/LICENSE

- k8s.io/client-go
License: Apache-2.0
URL: https://github.com/kubernetes/client-go/blob/v0.35.2/LICENSE

- k8s.io/utils/strings/slices
- k8s.io/klog/v2
License: Apache-2.0
URL: https://github.com/kubernetes/klog/blob/v2.130.1/LICENSE

- k8s.io/kube-openapi/pkg/util
License: Apache-2.0
URL: https://github.com/kubernetes/kube-openapi/blob/589584f1c912/LICENSE

- k8s.io/utils
License: Apache-2.0
URL: https://github.com/kubernetes/utils/blob/b8788abfbbc2/LICENSE

- oras.land/oras-go/v2
License: Apache-2.0
URL: https://github.com/oras-project/oras-go/blob/v2.6.0/LICENSE

- sigs.k8s.io/json
License: Apache-2.0
URL: https://github.com/kubernetes-sigs/json/blob/2d320260d730/LICENSE

- sigs.k8s.io/randfill
License: Apache-2.0
URL: https://github.com/kubernetes-sigs/randfill/blob/v1.0.0/LICENSE

- sigs.k8s.io/structured-merge-diff/v6/value
License: Apache-2.0
URL: https://github.com/kubernetes-sigs/structured-merge-diff/blob/v6.3.0/LICENSE

- sigs.k8s.io/yaml
License: Apache-2.0
URL: https://github.com/kubernetes-sigs/yaml/blob/v1.6.0/LICENSE
Expand Down Expand Up @@ -850,6 +878,10 @@ BSD LICENSED DEPENDENCIES
License: BSD-3-Clause
URL: https://github.com/protocolbuffers/protobuf-go/blob/v1.36.11/LICENSE

- gopkg.in/inf.v0
License: BSD-3-Clause
URL: https://github.com/go-inf/inf/blob/v0.9.1/LICENSE

- gopkg.in/op/go-logging.v1
License: BSD-3-Clause
URL: https://github.com/op/go-logging/blob/b2cb9fa56473/LICENSE
Expand All @@ -862,10 +894,18 @@ BSD LICENSED DEPENDENCIES
License: BSD-3-Clause
URL: Unknown

- k8s.io/apimachinery/third_party/forked/golang/reflect
License: BSD-3-Clause
URL: https://github.com/kubernetes/apimachinery/blob/v0.35.2/third_party/forked/golang/LICENSE

- k8s.io/client-go/third_party/forked/golang/template
License: BSD-3-Clause
URL: https://github.com/kubernetes/client-go/blob/v0.35.2/third_party/forked/golang/LICENSE

- k8s.io/utils/internal/third_party/forked/golang/net
License: BSD-3-Clause
URL: https://github.com/kubernetes/utils/blob/b8788abfbbc2/internal/third_party/forked/golang/LICENSE

- modernc.org/memory
License: BSD-3-Clause
URL: https://gitlab.com/cznic/memory/blob/v1.11.0/LICENSE-GO
Expand Down Expand Up @@ -1300,6 +1340,10 @@ MIT LICENSED DEPENDENCIES
License: MIT
URL: https://github.com/forPelevin/gomoji/blob/v1.4.1/LICENSE

- github.com/fxamacker/cbor/v2
License: MIT
URL: https://github.com/fxamacker/cbor/blob/v2.9.0/LICENSE

- github.com/gabriel-vasile/mimetype
License: MIT
URL: https://github.com/gabriel-vasile/mimetype/blob/v1.4.13/LICENSE
Expand Down Expand Up @@ -1696,6 +1740,10 @@ MIT LICENSED DEPENDENCIES
License: MIT
URL: https://github.com/wlynxg/chardet/blob/v1.0.4/LICENSE

- github.com/x448/float16
License: MIT
URL: https://github.com/x448/float16/blob/v0.8.4/LICENSE

- github.com/xo/terminfo
License: MIT
URL: https://github.com/xo/terminfo/blob/abceb7e1c41e/LICENSE
Expand Down
256 changes: 256 additions & 0 deletions cmd/aws/eks/token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
package eks

import (
"context"
"encoding/json"
"fmt"
"os"
"time"

"github.com/spf13/cobra"

errUtils "github.com/cloudposse/atmos/errors"
"github.com/cloudposse/atmos/pkg/auth"
awsCloud "github.com/cloudposse/atmos/pkg/auth/cloud/aws"
"github.com/cloudposse/atmos/pkg/auth/credentials"
"github.com/cloudposse/atmos/pkg/auth/types"
"github.com/cloudposse/atmos/pkg/auth/validation"
cfg "github.com/cloudposse/atmos/pkg/config"
"github.com/cloudposse/atmos/pkg/data"
log "github.com/cloudposse/atmos/pkg/logger"
"github.com/cloudposse/atmos/pkg/perf"
"github.com/cloudposse/atmos/pkg/schema"
)

// execCredentialAPIVersion is the Kubernetes exec credential plugin API version.
const execCredentialAPIVersion = "client.authentication.k8s.io/v1beta1"

// initCliConfigFn loads Atmos CLI configuration. Overridable in tests.
var initCliConfigFn = func(info schema.ConfigAndStacksInfo, processStacks bool) (schema.AtmosConfiguration, error) {
return cfg.InitCliConfig(info, processStacks)
}

// authenticateForTokenFn authenticates an identity and returns credentials. Overridable in tests.
var authenticateForTokenFn = authenticateForToken

// getEKSTokenFn generates an EKS bearer token. Overridable in tests.
var getEKSTokenFn = awsCloud.GetToken

// tokenCmd generates a short-lived EKS bearer token for kubectl.
var tokenCmd = &cobra.Command{
Use: "token",
Short: "Generate an EKS bearer token for kubectl",
Long: `Generate a short-lived EKS bearer token using STS pre-signed GetCallerIdentity URL.

This command is designed to be used as a kubectl exec credential plugin.
It authenticates using the specified identity and outputs an ExecCredential
JSON object to stdout.

The kubeconfig generated by 'atmos auth login' automatically configures
kubectl to call this command for token generation.

Examples:
# Generate token for a cluster (typically called by kubectl)
atmos aws eks token --cluster-name my-cluster --region us-east-2

# Generate token using a specific identity
atmos aws eks token --cluster-name my-cluster --region us-east-2 --identity dev-admin`,

FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false},
Args: cobra.NoArgs,
RunE: executeTokenCommand,
// Suppress usage on errors since kubectl invokes this automatically.
SilenceUsage: true,
}

// execCredential represents the Kubernetes ExecCredential response.
type execCredential struct {
APIVersion string `json:"apiVersion"`
Kind string `json:"kind"`
Status execCredentialStatus `json:"status"`
}

// execCredentialStatus contains the token and expiration.
type execCredentialStatus struct {
ExpirationTimestamp string `json:"expirationTimestamp"`
Token string `json:"token"`
}

func executeTokenCommand(cmd *cobra.Command, args []string) error {
// Load atmos config.
atmosConfig, err := initCliConfigFn(schema.ConfigAndStacksInfo{}, false)
if err != nil {
return fmt.Errorf(errUtils.ErrWrapFormat, errUtils.ErrFailedToInitConfig, err)
}
defer perf.Track(&atmosConfig, "eks.executeTokenCommand")()

ctx := context.Background()

// Get flag values.
clusterName, _ := cmd.Flags().GetString("cluster-name")
region, _ := cmd.Flags().GetString("region")

if clusterName == "" {
return fmt.Errorf("%w: --cluster-name is required", errUtils.ErrEKSTokenGeneration)
}

if region == "" {
return fmt.Errorf("%w: --region is required", errUtils.ErrEKSTokenGeneration)
}

// Resolve identity: flag > env var > default.
identityName := resolveIdentity(cmd)

log.Debug("Generating EKS token", "cluster", clusterName, "region", region, "identity", identityName)

// Authenticate to get credentials.
// Skip integrations to avoid rewriting the kubeconfig during token generation.
ctx = auth.ContextWithSkipIntegrations(ctx)
creds, err := authenticateForTokenFn(ctx, &atmosConfig.Auth, atmosConfig.CliConfigPath, identityName)
if err != nil {
return fmt.Errorf("%w: %w", errUtils.ErrEKSTokenGeneration, err)
}

// Export AWS credentials to process environment so the AWS SDK can use them
// for the STS presign call. This ensures credentials are available regardless
// of how the exec plugin is invoked (e.g., by kubectl).
if err := exportAWSCredsToEnv(creds); err != nil {
log.Warn("eks token: failed to export AWS credentials to environment", "error", err)
}

// Generate token.
token, expiresAt, err := getEKSTokenFn(ctx, creds, clusterName, region)
if err != nil {
return fmt.Errorf("%w: %w", errUtils.ErrEKSTokenGeneration, err)
}

// Output ExecCredential JSON to stdout.
credential := execCredential{
APIVersion: execCredentialAPIVersion,
Kind: "ExecCredential",
Status: execCredentialStatus{
ExpirationTimestamp: expiresAt.UTC().Format(time.RFC3339),
Token: token,
},
}

output, err := json.Marshal(credential)
if err != nil {
return fmt.Errorf("%w: failed to marshal ExecCredential: %w", errUtils.ErrEKSTokenGeneration, err)
}

return data.Write(string(output))
}

// resolveIdentity resolves the identity name from flag, env var, or returns empty.
func resolveIdentity(cmd *cobra.Command) string {
// Check flag first.
identity, _ := cmd.Flags().GetString("identity")
if identity != "" {
return identity
}

// Fall back to environment variable.
if envIdentity := os.Getenv("ATMOS_IDENTITY"); envIdentity != "" {
return envIdentity
}

return ""
}

// authenticateForToken authenticates an identity and returns credentials.
func authenticateForToken(ctx context.Context, authConfig *schema.AuthConfig, cliConfigPath, identityName string) (types.ICredentials, error) {
defer perf.Track(nil, "eks.authenticateForToken")()

authStackInfo := &schema.ConfigAndStacksInfo{
AuthContext: &schema.AuthContext{},
}

credStore := credentials.NewCredentialStore()
validator := validation.NewValidator()

mgr, err := auth.NewAuthManager(authConfig, credStore, validator, authStackInfo, cliConfigPath)
if err != nil {
return nil, fmt.Errorf(errUtils.ErrWrapFormat, errUtils.ErrFailedToInitializeAuthManager, err)
}

// If no identity specified, try to resolve default.
if identityName == "" {
identityName = resolveDefaultIdentity(authConfig)
if identityName == "" {
return nil, fmt.Errorf("%w: no identity specified and no default identity found", errUtils.ErrEKSTokenGeneration)
}
}

whoami, err := mgr.Authenticate(ctx, identityName)
if err != nil {
return nil, fmt.Errorf(errUtils.ErrWrapWithNameAndCauseFormat, errUtils.ErrIdentityAuthFailed, identityName, err)
}

if whoami.Credentials == nil {
return nil, fmt.Errorf(errUtils.ErrWrapWithNameAndCauseFormat, errUtils.ErrIdentityAuthFailed, identityName, errUtils.ErrIdentityCredentialsNone)
}

return whoami.Credentials, nil
}

// resolveDefaultIdentity finds a default identity from the auth config.
func resolveDefaultIdentity(authConfig *schema.AuthConfig) string {
if authConfig == nil || len(authConfig.Identities) == 0 {
return ""
}

// If there's only one identity, use it.
if len(authConfig.Identities) == 1 {
for name := range authConfig.Identities {
return name
}
}

return ""
}

// exportAWSCredsToEnv sets AWS credential environment variables in the current process.
// This ensures the AWS SDK can authenticate for the STS presign call used in token generation.
func exportAWSCredsToEnv(creds types.ICredentials) error {
defer perf.Track(nil, "eks.exportAWSCredsToEnv")()

awsCreds, ok := creds.(*types.AWSCredentials)
if !ok {
return fmt.Errorf("%w: expected AWS credentials for environment export", errUtils.ErrEKSTokenGeneration)
}

if awsCreds.AccessKeyID != "" {
os.Setenv("AWS_ACCESS_KEY_ID", awsCreds.AccessKeyID)
}
if awsCreds.SecretAccessKey != "" {
os.Setenv("AWS_SECRET_ACCESS_KEY", awsCreds.SecretAccessKey)
}
if awsCreds.SessionToken != "" {
os.Setenv("AWS_SESSION_TOKEN", awsCreds.SessionToken)
}
if awsCreds.Region != "" {
os.Setenv("AWS_REGION", awsCreds.Region)
os.Setenv("AWS_DEFAULT_REGION", awsCreds.Region)
}

// Clear AWS_PROFILE to prevent the SDK from using a named profile
// that might conflict with the explicit credentials.
os.Unsetenv("AWS_PROFILE")

log.Debug("Exported AWS credentials to environment",
"hasAccessKey", awsCreds.AccessKeyID != "",
"hasSecretKey", awsCreds.SecretAccessKey != "",
"hasSessionToken", awsCreds.SessionToken != "",
"region", awsCreds.Region,
)

return nil
}

func init() {
tokenCmd.Flags().String("cluster-name", "", "EKS cluster name (required)")
tokenCmd.Flags().String("region", "", "AWS region (required)")
tokenCmd.Flags().StringP("identity", "i", "", "Atmos identity to authenticate with")
EksCmd.AddCommand(tokenCmd)
}
Loading
Loading