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  }