Skip to content

Commit fbb218f

Browse files
authored
Merge branch 'main' into fix/describe-affected-deleted-crash
2 parents 4f07e4a + fe8ebf3 commit fbb218f

File tree

91 files changed

+13045
-2299
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

91 files changed

+13045
-2299
lines changed

CLAUDE.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,14 @@ Always ask first: "This will discard uncommitted changes. Proceed? [y/N]"
362362
### Test Coverage (MANDATORY)
363363
80% minimum (CodeCov enforced). All features need tests. `make testacc-coverage` for reports.
364364

365+
### Cyclomatic Complexity (MANDATORY)
366+
golangci-lint enforces `cyclop: max-complexity: 15` and `funlen: lines: 60, statements: 40`.
367+
When refactoring high-complexity functions:
368+
1. Extract blocks with clear single responsibilities into named helper functions.
369+
2. Use the pattern: `buildXSubcommandArgs`, `resolveX`, `checkX`, `assembleX`, `handleX`.
370+
3. Keep the orchestrator function as a flat linear pipeline of named steps (see `ExecuteTerraform`).
371+
4. Previously high-complexity functions: `ExecuteTerraform` (160→26, see `internal/exec/terraform.go`), `ExecuteDescribeStacks` (247→10), `processArgsAndFlags`.
372+
365373
### Environment Variables (MANDATORY)
366374
Use `viper.BindEnv("ATMOS_VAR", "ATMOS_VAR", "FALLBACK")` - ATMOS_ prefix required.
367375

NOTICE

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,10 @@ APACHE 2.0 LICENSED DEPENDENCIES
145145
License: Apache-2.0
146146
URL: https://github.com/aws/aws-sdk-go-v2/blob/service/ecr/v1.56.1/service/ecr/LICENSE.txt
147147

148+
- github.com/aws/aws-sdk-go-v2/service/eks
149+
License: Apache-2.0
150+
URL: https://github.com/aws/aws-sdk-go-v2/blob/service/eks/v1.80.2/service/eks/LICENSE.txt
151+
148152
- github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding
149153
License: Apache-2.0
150154
URL: https://github.com/aws/aws-sdk-go-v2/blob/service/internal/accept-encoding/v1.13.7/service/internal/accept-encoding/LICENSE.txt
@@ -565,18 +569,42 @@ APACHE 2.0 LICENSED DEPENDENCIES
565569
License: Apache-2.0
566570
URL: https://github.com/go-yaml/yaml/blob/v2.4.0/LICENSE
567571

568-
- k8s.io/client-go/util/jsonpath
572+
- k8s.io/apimachinery/pkg
573+
License: Apache-2.0
574+
URL: https://github.com/kubernetes/apimachinery/blob/v0.35.2/LICENSE
575+
576+
- k8s.io/client-go
569577
License: Apache-2.0
570578
URL: https://github.com/kubernetes/client-go/blob/v0.35.2/LICENSE
571579

572-
- k8s.io/utils/strings/slices
580+
- k8s.io/klog/v2
581+
License: Apache-2.0
582+
URL: https://github.com/kubernetes/klog/blob/v2.130.1/LICENSE
583+
584+
- k8s.io/kube-openapi/pkg/util
585+
License: Apache-2.0
586+
URL: https://github.com/kubernetes/kube-openapi/blob/589584f1c912/LICENSE
587+
588+
- k8s.io/utils
573589
License: Apache-2.0
574590
URL: https://github.com/kubernetes/utils/blob/b8788abfbbc2/LICENSE
575591

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

596+
- sigs.k8s.io/json
597+
License: Apache-2.0
598+
URL: https://github.com/kubernetes-sigs/json/blob/2d320260d730/LICENSE
599+
600+
- sigs.k8s.io/randfill
601+
License: Apache-2.0
602+
URL: https://github.com/kubernetes-sigs/randfill/blob/v1.0.0/LICENSE
603+
604+
- sigs.k8s.io/structured-merge-diff/v6/value
605+
License: Apache-2.0
606+
URL: https://github.com/kubernetes-sigs/structured-merge-diff/blob/v6.3.0/LICENSE
607+
580608
- sigs.k8s.io/yaml
581609
License: Apache-2.0
582610
URL: https://github.com/kubernetes-sigs/yaml/blob/v1.6.0/LICENSE
@@ -850,6 +878,10 @@ BSD LICENSED DEPENDENCIES
850878
License: BSD-3-Clause
851879
URL: https://github.com/protocolbuffers/protobuf-go/blob/v1.36.11/LICENSE
852880

881+
- gopkg.in/inf.v0
882+
License: BSD-3-Clause
883+
URL: https://github.com/go-inf/inf/blob/v0.9.1/LICENSE
884+
853885
- gopkg.in/op/go-logging.v1
854886
License: BSD-3-Clause
855887
URL: https://github.com/op/go-logging/blob/b2cb9fa56473/LICENSE
@@ -862,10 +894,18 @@ BSD LICENSED DEPENDENCIES
862894
License: BSD-3-Clause
863895
URL: Unknown
864896

897+
- k8s.io/apimachinery/third_party/forked/golang/reflect
898+
License: BSD-3-Clause
899+
URL: https://github.com/kubernetes/apimachinery/blob/v0.35.2/third_party/forked/golang/LICENSE
900+
865901
- k8s.io/client-go/third_party/forked/golang/template
866902
License: BSD-3-Clause
867903
URL: https://github.com/kubernetes/client-go/blob/v0.35.2/third_party/forked/golang/LICENSE
868904

905+
- k8s.io/utils/internal/third_party/forked/golang/net
906+
License: BSD-3-Clause
907+
URL: https://github.com/kubernetes/utils/blob/b8788abfbbc2/internal/third_party/forked/golang/LICENSE
908+
869909
- modernc.org/memory
870910
License: BSD-3-Clause
871911
URL: https://gitlab.com/cznic/memory/blob/v1.11.0/LICENSE-GO
@@ -1300,6 +1340,10 @@ MIT LICENSED DEPENDENCIES
13001340
License: MIT
13011341
URL: https://github.com/forPelevin/gomoji/blob/v1.4.1/LICENSE
13021342

1343+
- github.com/fxamacker/cbor/v2
1344+
License: MIT
1345+
URL: https://github.com/fxamacker/cbor/blob/v2.9.0/LICENSE
1346+
13031347
- github.com/gabriel-vasile/mimetype
13041348
License: MIT
13051349
URL: https://github.com/gabriel-vasile/mimetype/blob/v1.4.13/LICENSE
@@ -1696,6 +1740,10 @@ MIT LICENSED DEPENDENCIES
16961740
License: MIT
16971741
URL: https://github.com/wlynxg/chardet/blob/v1.0.4/LICENSE
16981742

1743+
- github.com/x448/float16
1744+
License: MIT
1745+
URL: https://github.com/x448/float16/blob/v0.8.4/LICENSE
1746+
16991747
- github.com/xo/terminfo
17001748
License: MIT
17011749
URL: https://github.com/xo/terminfo/blob/abceb7e1c41e/LICENSE

cmd/aws/eks/token.go

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
package eks
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"os"
8+
"time"
9+
10+
"github.com/spf13/cobra"
11+
12+
errUtils "github.com/cloudposse/atmos/errors"
13+
"github.com/cloudposse/atmos/pkg/auth"
14+
awsCloud "github.com/cloudposse/atmos/pkg/auth/cloud/aws"
15+
"github.com/cloudposse/atmos/pkg/auth/credentials"
16+
"github.com/cloudposse/atmos/pkg/auth/types"
17+
"github.com/cloudposse/atmos/pkg/auth/validation"
18+
cfg "github.com/cloudposse/atmos/pkg/config"
19+
"github.com/cloudposse/atmos/pkg/data"
20+
log "github.com/cloudposse/atmos/pkg/logger"
21+
"github.com/cloudposse/atmos/pkg/perf"
22+
"github.com/cloudposse/atmos/pkg/schema"
23+
)
24+
25+
// execCredentialAPIVersion is the Kubernetes exec credential plugin API version.
26+
const execCredentialAPIVersion = "client.authentication.k8s.io/v1beta1"
27+
28+
// initCliConfigFn loads Atmos CLI configuration. Overridable in tests.
29+
var initCliConfigFn = func(info schema.ConfigAndStacksInfo, processStacks bool) (schema.AtmosConfiguration, error) {
30+
return cfg.InitCliConfig(info, processStacks)
31+
}
32+
33+
// authenticateForTokenFn authenticates an identity and returns credentials. Overridable in tests.
34+
var authenticateForTokenFn = authenticateForToken
35+
36+
// getEKSTokenFn generates an EKS bearer token. Overridable in tests.
37+
var getEKSTokenFn = awsCloud.GetToken
38+
39+
// tokenCmd generates a short-lived EKS bearer token for kubectl.
40+
var tokenCmd = &cobra.Command{
41+
Use: "token",
42+
Short: "Generate an EKS bearer token for kubectl",
43+
Long: `Generate a short-lived EKS bearer token using STS pre-signed GetCallerIdentity URL.
44+
45+
This command is designed to be used as a kubectl exec credential plugin.
46+
It authenticates using the specified identity and outputs an ExecCredential
47+
JSON object to stdout.
48+
49+
The kubeconfig generated by 'atmos auth login' automatically configures
50+
kubectl to call this command for token generation.
51+
52+
Examples:
53+
# Generate token for a cluster (typically called by kubectl)
54+
atmos aws eks token --cluster-name my-cluster --region us-east-2
55+
56+
# Generate token using a specific identity
57+
atmos aws eks token --cluster-name my-cluster --region us-east-2 --identity dev-admin`,
58+
59+
FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false},
60+
Args: cobra.NoArgs,
61+
RunE: executeTokenCommand,
62+
// Suppress usage on errors since kubectl invokes this automatically.
63+
SilenceUsage: true,
64+
}
65+
66+
// execCredential represents the Kubernetes ExecCredential response.
67+
type execCredential struct {
68+
APIVersion string `json:"apiVersion"`
69+
Kind string `json:"kind"`
70+
Status execCredentialStatus `json:"status"`
71+
}
72+
73+
// execCredentialStatus contains the token and expiration.
74+
type execCredentialStatus struct {
75+
ExpirationTimestamp string `json:"expirationTimestamp"`
76+
Token string `json:"token"`
77+
}
78+
79+
func executeTokenCommand(cmd *cobra.Command, args []string) error {
80+
// Load atmos config.
81+
atmosConfig, err := initCliConfigFn(schema.ConfigAndStacksInfo{}, false)
82+
if err != nil {
83+
return fmt.Errorf(errUtils.ErrWrapFormat, errUtils.ErrFailedToInitConfig, err)
84+
}
85+
defer perf.Track(&atmosConfig, "eks.executeTokenCommand")()
86+
87+
ctx := context.Background()
88+
89+
// Get flag values.
90+
clusterName, _ := cmd.Flags().GetString("cluster-name")
91+
region, _ := cmd.Flags().GetString("region")
92+
93+
if clusterName == "" {
94+
return fmt.Errorf("%w: --cluster-name is required", errUtils.ErrEKSTokenGeneration)
95+
}
96+
97+
if region == "" {
98+
return fmt.Errorf("%w: --region is required", errUtils.ErrEKSTokenGeneration)
99+
}
100+
101+
// Resolve identity: flag > env var > default.
102+
identityName := resolveIdentity(cmd)
103+
104+
log.Debug("Generating EKS token", "cluster", clusterName, "region", region, "identity", identityName)
105+
106+
// Authenticate to get credentials.
107+
// Skip integrations to avoid rewriting the kubeconfig during token generation.
108+
ctx = auth.ContextWithSkipIntegrations(ctx)
109+
creds, err := authenticateForTokenFn(ctx, &atmosConfig.Auth, atmosConfig.CliConfigPath, identityName)
110+
if err != nil {
111+
return fmt.Errorf("%w: %w", errUtils.ErrEKSTokenGeneration, err)
112+
}
113+
114+
// Export AWS credentials to process environment so the AWS SDK can use them
115+
// for the STS presign call. This ensures credentials are available regardless
116+
// of how the exec plugin is invoked (e.g., by kubectl).
117+
if err := exportAWSCredsToEnv(creds); err != nil {
118+
log.Warn("eks token: failed to export AWS credentials to environment", "error", err)
119+
}
120+
121+
// Generate token.
122+
token, expiresAt, err := getEKSTokenFn(ctx, creds, clusterName, region)
123+
if err != nil {
124+
return fmt.Errorf("%w: %w", errUtils.ErrEKSTokenGeneration, err)
125+
}
126+
127+
// Output ExecCredential JSON to stdout.
128+
credential := execCredential{
129+
APIVersion: execCredentialAPIVersion,
130+
Kind: "ExecCredential",
131+
Status: execCredentialStatus{
132+
ExpirationTimestamp: expiresAt.UTC().Format(time.RFC3339),
133+
Token: token,
134+
},
135+
}
136+
137+
output, err := json.Marshal(credential)
138+
if err != nil {
139+
return fmt.Errorf("%w: failed to marshal ExecCredential: %w", errUtils.ErrEKSTokenGeneration, err)
140+
}
141+
142+
return data.Write(string(output))
143+
}
144+
145+
// resolveIdentity resolves the identity name from flag, env var, or returns empty.
146+
func resolveIdentity(cmd *cobra.Command) string {
147+
// Check flag first.
148+
identity, _ := cmd.Flags().GetString("identity")
149+
if identity != "" {
150+
return identity
151+
}
152+
153+
// Fall back to environment variable.
154+
if envIdentity := os.Getenv("ATMOS_IDENTITY"); envIdentity != "" {
155+
return envIdentity
156+
}
157+
158+
return ""
159+
}
160+
161+
// authenticateForToken authenticates an identity and returns credentials.
162+
func authenticateForToken(ctx context.Context, authConfig *schema.AuthConfig, cliConfigPath, identityName string) (types.ICredentials, error) {
163+
defer perf.Track(nil, "eks.authenticateForToken")()
164+
165+
authStackInfo := &schema.ConfigAndStacksInfo{
166+
AuthContext: &schema.AuthContext{},
167+
}
168+
169+
credStore := credentials.NewCredentialStore()
170+
validator := validation.NewValidator()
171+
172+
mgr, err := auth.NewAuthManager(authConfig, credStore, validator, authStackInfo, cliConfigPath)
173+
if err != nil {
174+
return nil, fmt.Errorf(errUtils.ErrWrapFormat, errUtils.ErrFailedToInitializeAuthManager, err)
175+
}
176+
177+
// If no identity specified, try to resolve default.
178+
if identityName == "" {
179+
identityName = resolveDefaultIdentity(authConfig)
180+
if identityName == "" {
181+
return nil, fmt.Errorf("%w: no identity specified and no default identity found", errUtils.ErrEKSTokenGeneration)
182+
}
183+
}
184+
185+
whoami, err := mgr.Authenticate(ctx, identityName)
186+
if err != nil {
187+
return nil, fmt.Errorf(errUtils.ErrWrapWithNameAndCauseFormat, errUtils.ErrIdentityAuthFailed, identityName, err)
188+
}
189+
190+
if whoami.Credentials == nil {
191+
return nil, fmt.Errorf(errUtils.ErrWrapWithNameAndCauseFormat, errUtils.ErrIdentityAuthFailed, identityName, errUtils.ErrIdentityCredentialsNone)
192+
}
193+
194+
return whoami.Credentials, nil
195+
}
196+
197+
// resolveDefaultIdentity finds a default identity from the auth config.
198+
func resolveDefaultIdentity(authConfig *schema.AuthConfig) string {
199+
if authConfig == nil || len(authConfig.Identities) == 0 {
200+
return ""
201+
}
202+
203+
// If there's only one identity, use it.
204+
if len(authConfig.Identities) == 1 {
205+
for name := range authConfig.Identities {
206+
return name
207+
}
208+
}
209+
210+
return ""
211+
}
212+
213+
// exportAWSCredsToEnv sets AWS credential environment variables in the current process.
214+
// This ensures the AWS SDK can authenticate for the STS presign call used in token generation.
215+
func exportAWSCredsToEnv(creds types.ICredentials) error {
216+
defer perf.Track(nil, "eks.exportAWSCredsToEnv")()
217+
218+
awsCreds, ok := creds.(*types.AWSCredentials)
219+
if !ok {
220+
return fmt.Errorf("%w: expected AWS credentials for environment export", errUtils.ErrEKSTokenGeneration)
221+
}
222+
223+
if awsCreds.AccessKeyID != "" {
224+
os.Setenv("AWS_ACCESS_KEY_ID", awsCreds.AccessKeyID)
225+
}
226+
if awsCreds.SecretAccessKey != "" {
227+
os.Setenv("AWS_SECRET_ACCESS_KEY", awsCreds.SecretAccessKey)
228+
}
229+
if awsCreds.SessionToken != "" {
230+
os.Setenv("AWS_SESSION_TOKEN", awsCreds.SessionToken)
231+
}
232+
if awsCreds.Region != "" {
233+
os.Setenv("AWS_REGION", awsCreds.Region)
234+
os.Setenv("AWS_DEFAULT_REGION", awsCreds.Region)
235+
}
236+
237+
// Clear AWS_PROFILE to prevent the SDK from using a named profile
238+
// that might conflict with the explicit credentials.
239+
os.Unsetenv("AWS_PROFILE")
240+
241+
log.Debug("Exported AWS credentials to environment",
242+
"hasAccessKey", awsCreds.AccessKeyID != "",
243+
"hasSecretKey", awsCreds.SecretAccessKey != "",
244+
"hasSessionToken", awsCreds.SessionToken != "",
245+
"region", awsCreds.Region,
246+
)
247+
248+
return nil
249+
}
250+
251+
func init() {
252+
tokenCmd.Flags().String("cluster-name", "", "EKS cluster name (required)")
253+
tokenCmd.Flags().String("region", "", "AWS region (required)")
254+
tokenCmd.Flags().StringP("identity", "i", "", "Atmos identity to authenticate with")
255+
EksCmd.AddCommand(tokenCmd)
256+
}

0 commit comments

Comments
 (0)