github.com/argoproj/argo-cd/v3@v3.2.1/cmd/argocd-k8s-auth/commands/aws.go (about) 1 package commands 2 3 import ( 4 "context" 5 "encoding/base64" 6 "encoding/json" 7 "fmt" 8 "os" 9 "time" 10 11 "github.com/aws/aws-sdk-go/aws" 12 "github.com/aws/aws-sdk-go/aws/credentials/stscreds" 13 "github.com/aws/aws-sdk-go/aws/session" 14 "github.com/aws/aws-sdk-go/service/sts" 15 "github.com/spf13/cobra" 16 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 17 clientauthv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1" 18 19 "github.com/argoproj/argo-cd/v3/util/errors" 20 ) 21 22 const ( 23 clusterIDHeader = "x-k8s-aws-id" 24 // The sts GetCallerIdentity request is valid for 15 minutes regardless of this parameters value after it has been 25 // signed, but we set this unused parameter to 60 for legacy reasons (we check for a value between 0 and 60 on the 26 // server side in 0.3.0 or earlier). IT IS IGNORED. If we can get STS to support x-amz-expires, then we should 27 // set this parameter to the actual expiration, and make it configurable. 28 requestPresignParam = 60 29 // The actual token expiration (presigned STS urls are valid for 15 minutes after timestamp in x-amz-date). 30 presignedURLExpiration = 15 * time.Minute 31 v1Prefix = "k8s-aws-v1." 32 ) 33 34 // newAWSCommand returns a new instance of an aws command that generates k8s auth token 35 // implementation is "inspired" by https://github.com/kubernetes-sigs/aws-iam-authenticator/blob/e61f537662b64092ed83cb76e600e023f627f628/pkg/token/token.go#L316 36 func newAWSCommand() *cobra.Command { 37 var ( 38 clusterName string 39 roleARN string 40 profile string 41 ) 42 command := &cobra.Command{ 43 Use: "aws", 44 Run: func(c *cobra.Command, _ []string) { 45 ctx := c.Context() 46 47 presignedURLString, err := getSignedRequestWithRetry(ctx, time.Minute, 5*time.Second, clusterName, roleARN, profile, getSignedRequest) 48 errors.CheckError(err) 49 token := v1Prefix + base64.RawURLEncoding.EncodeToString([]byte(presignedURLString)) 50 // Set token expiration to 1 minute before the presigned URL expires for some cushion 51 tokenExpiration := time.Now().Local().Add(presignedURLExpiration - 1*time.Minute) 52 _, _ = fmt.Fprint(os.Stdout, formatJSON(token, tokenExpiration)) 53 }, 54 } 55 command.Flags().StringVar(&clusterName, "cluster-name", "", "AWS Cluster name") 56 command.Flags().StringVar(&roleARN, "role-arn", "", "AWS Role ARN") 57 command.Flags().StringVar(&profile, "profile", "", "AWS Profile") 58 return command 59 } 60 61 type getSignedRequestFunc func(clusterName, roleARN string, profile string) (string, error) 62 63 func getSignedRequestWithRetry(ctx context.Context, timeout, interval time.Duration, clusterName, roleARN string, profile string, fn getSignedRequestFunc) (string, error) { 64 ctx, cancel := context.WithTimeout(ctx, timeout) 65 defer cancel() 66 for { 67 signed, err := fn(clusterName, roleARN, profile) 68 if err == nil { 69 return signed, nil 70 } 71 select { 72 case <-ctx.Done(): 73 return "", fmt.Errorf("timeout while trying to get signed aws request: last error: %w", err) 74 case <-time.After(interval): 75 } 76 } 77 } 78 79 func getSignedRequest(clusterName, roleARN string, profile string) (string, error) { 80 sess, err := session.NewSessionWithOptions(session.Options{ 81 Profile: profile, 82 }) 83 if err != nil { 84 return "", fmt.Errorf("error creating new AWS session: %w", err) 85 } 86 stsAPI := sts.New(sess) 87 if roleARN != "" { 88 creds := stscreds.NewCredentials(sess, roleARN) 89 stsAPI = sts.New(sess, &aws.Config{Credentials: creds}) 90 } 91 request, _ := stsAPI.GetCallerIdentityRequest(&sts.GetCallerIdentityInput{}) 92 request.HTTPRequest.Header.Add(clusterIDHeader, clusterName) 93 signed, err := request.Presign(requestPresignParam) 94 if err != nil { 95 return "", fmt.Errorf("error presigning AWS request: %w", err) 96 } 97 return signed, nil 98 } 99 100 func formatJSON(token string, expiration time.Time) string { 101 expirationTimestamp := metav1.NewTime(expiration) 102 execInput := &clientauthv1beta1.ExecCredential{ 103 TypeMeta: metav1.TypeMeta{ 104 APIVersion: "client.authentication.k8s.io/v1beta1", 105 Kind: "ExecCredential", 106 }, 107 Status: &clientauthv1beta1.ExecCredentialStatus{ 108 ExpirationTimestamp: &expirationTimestamp, 109 Token: token, 110 }, 111 } 112 enc, _ := json.Marshal(execInput) 113 return string(enc) 114 }