sigs.k8s.io/cluster-api-provider-aws@v1.5.5/pkg/cloud/services/eks/config.go (about)

     1  /*
     2  Copyright 2020 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8  	http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package eks
    18  
    19  import (
    20  	"context"
    21  	"encoding/base64"
    22  	"fmt"
    23  	"time"
    24  
    25  	"github.com/aws/aws-sdk-go/service/eks"
    26  	"github.com/aws/aws-sdk-go/service/sts"
    27  	"github.com/pkg/errors"
    28  	corev1 "k8s.io/api/core/v1"
    29  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    30  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    31  	"k8s.io/apimachinery/pkg/types"
    32  	"k8s.io/client-go/tools/clientcmd"
    33  	"k8s.io/client-go/tools/clientcmd/api"
    34  
    35  	ekscontrolplanev1 "sigs.k8s.io/cluster-api-provider-aws/controlplane/eks/api/v1beta1"
    36  	"sigs.k8s.io/cluster-api-provider-aws/pkg/record"
    37  	"sigs.k8s.io/cluster-api/util/kubeconfig"
    38  	"sigs.k8s.io/cluster-api/util/secret"
    39  )
    40  
    41  const (
    42  	tokenPrefix       = "k8s-aws-v1." //nolint:gosec
    43  	clusterNameHeader = "x-k8s-aws-id"
    44  	tokenAgeMins      = 15
    45  )
    46  
    47  func (s *Service) reconcileKubeconfig(ctx context.Context, cluster *eks.Cluster) error {
    48  	s.scope.V(2).Info("Reconciling EKS kubeconfigs for cluster", "cluster-name", s.scope.KubernetesClusterName())
    49  
    50  	clusterRef := types.NamespacedName{
    51  		Name:      s.scope.Cluster.Name,
    52  		Namespace: s.scope.Cluster.Namespace,
    53  	}
    54  
    55  	// Create the kubeconfig used by CAPI
    56  	configSecret, err := secret.GetFromNamespacedName(ctx, s.scope.Client, clusterRef, secret.Kubeconfig)
    57  	if err != nil {
    58  		if !apierrors.IsNotFound(err) {
    59  			return errors.Wrap(err, "failed to get kubeconfig secret")
    60  		}
    61  
    62  		if createErr := s.createCAPIKubeconfigSecret(
    63  			ctx,
    64  			cluster,
    65  			&clusterRef,
    66  		); createErr != nil {
    67  			return fmt.Errorf("creating kubeconfig secret: %w", err)
    68  		}
    69  	} else if updateErr := s.updateCAPIKubeconfigSecret(ctx, configSecret, cluster); updateErr != nil {
    70  		return fmt.Errorf("updating kubeconfig secret: %w", err)
    71  	}
    72  
    73  	// Set initialized to true to indicate the kubconfig has been created
    74  	s.scope.ControlPlane.Status.Initialized = true
    75  
    76  	return nil
    77  }
    78  
    79  func (s *Service) reconcileAdditionalKubeconfigs(ctx context.Context, cluster *eks.Cluster) error {
    80  	s.scope.V(2).Info("Reconciling additional EKS kubeconfigs for cluster", "cluster-name", s.scope.KubernetesClusterName())
    81  
    82  	clusterRef := types.NamespacedName{
    83  		Name:      s.scope.Cluster.Name + "-user",
    84  		Namespace: s.scope.Cluster.Namespace,
    85  	}
    86  
    87  	// Create the additional kubeconfig for users. This doesn't need updating on every sync
    88  	_, err := secret.GetFromNamespacedName(ctx, s.scope.Client, clusterRef, secret.Kubeconfig)
    89  	if err != nil {
    90  		if !apierrors.IsNotFound(err) {
    91  			return errors.Wrap(err, "failed to get kubeconfig (user) secret")
    92  		}
    93  
    94  		createErr := s.createUserKubeconfigSecret(
    95  			ctx,
    96  			cluster,
    97  			&clusterRef,
    98  		)
    99  		if createErr != nil {
   100  			return err
   101  		}
   102  	}
   103  
   104  	return nil
   105  }
   106  
   107  func (s *Service) createCAPIKubeconfigSecret(ctx context.Context, cluster *eks.Cluster, clusterRef *types.NamespacedName) error {
   108  	controllerOwnerRef := *metav1.NewControllerRef(s.scope.ControlPlane, ekscontrolplanev1.GroupVersion.WithKind("AWSManagedControlPlane"))
   109  
   110  	clusterName := s.scope.KubernetesClusterName()
   111  	userName := s.getKubeConfigUserName(clusterName, false)
   112  
   113  	cfg, err := s.createBaseKubeConfig(cluster, userName)
   114  	if err != nil {
   115  		return fmt.Errorf("creating base kubeconfig: %w", err)
   116  	}
   117  
   118  	token, err := s.generateToken()
   119  	if err != nil {
   120  		return fmt.Errorf("generating presigned token: %w", err)
   121  	}
   122  
   123  	cfg.AuthInfos = map[string]*api.AuthInfo{
   124  		userName: {
   125  			Token: token,
   126  		},
   127  	}
   128  
   129  	out, err := clientcmd.Write(*cfg)
   130  	if err != nil {
   131  		return errors.Wrap(err, "failed to serialize config to yaml")
   132  	}
   133  
   134  	kubeconfigSecret := kubeconfig.GenerateSecretWithOwner(*clusterRef, out, controllerOwnerRef)
   135  	if err := s.scope.Client.Create(ctx, kubeconfigSecret); err != nil {
   136  		return errors.Wrap(err, "failed to create kubeconfig secret")
   137  	}
   138  
   139  	record.Eventf(s.scope.ControlPlane, "SucessfulCreateKubeconfig", "Created kubeconfig for cluster %q", s.scope.Name())
   140  	return nil
   141  }
   142  
   143  func (s *Service) updateCAPIKubeconfigSecret(ctx context.Context, configSecret *corev1.Secret, cluster *eks.Cluster) error {
   144  	s.scope.V(2).Info("Updating EKS kubeconfigs for cluster", "cluster-name", s.scope.KubernetesClusterName())
   145  
   146  	data, ok := configSecret.Data[secret.KubeconfigDataName]
   147  	if !ok {
   148  		return errors.Errorf("missing key %q in secret data", secret.KubeconfigDataName)
   149  	}
   150  
   151  	config, err := clientcmd.Load(data)
   152  	if err != nil {
   153  		return errors.Wrap(err, "failed to convert kubeconfig Secret into a clientcmdapi.Config")
   154  	}
   155  
   156  	token, err := s.generateToken()
   157  	if err != nil {
   158  		return fmt.Errorf("generating presigned token: %w", err)
   159  	}
   160  
   161  	userName := s.getKubeConfigUserName(*cluster.Name, false)
   162  	config.AuthInfos[userName].Token = token
   163  
   164  	out, err := clientcmd.Write(*config)
   165  	if err != nil {
   166  		return errors.Wrap(err, "failed to serialize config to yaml")
   167  	}
   168  
   169  	configSecret.Data[secret.KubeconfigDataName] = out
   170  
   171  	err = s.scope.Client.Update(ctx, configSecret)
   172  	if err != nil {
   173  		return fmt.Errorf("updating kubeconfig secret: %w", err)
   174  	}
   175  
   176  	return nil
   177  }
   178  
   179  func (s *Service) createUserKubeconfigSecret(ctx context.Context, cluster *eks.Cluster, clusterRef *types.NamespacedName) error {
   180  	controllerOwnerRef := *metav1.NewControllerRef(s.scope.ControlPlane, ekscontrolplanev1.GroupVersion.WithKind("AWSManagedControlPlane"))
   181  
   182  	clusterName := s.scope.KubernetesClusterName()
   183  	userName := s.getKubeConfigUserName(clusterName, true)
   184  
   185  	cfg, err := s.createBaseKubeConfig(cluster, userName)
   186  	if err != nil {
   187  		return fmt.Errorf("creating base kubeconfig: %w", err)
   188  	}
   189  
   190  	// Version v1alpha1 was removed in Kubernetes v1.23.
   191  	// Version v1 was released in Kubernetes v1.23.
   192  	// Version v1beta1 was selected as it has the widest range of support
   193  	// This should be changed to v1 once EKS no longer supports Kubernetes <v1.23
   194  	execConfig := &api.ExecConfig{APIVersion: "client.authentication.k8s.io/v1beta1"}
   195  	switch s.scope.TokenMethod() {
   196  	case ekscontrolplanev1.EKSTokenMethodIAMAuthenticator:
   197  		execConfig.Command = "aws-iam-authenticator"
   198  		execConfig.Args = []string{
   199  			"token",
   200  			"-i",
   201  			clusterName,
   202  		}
   203  	case ekscontrolplanev1.EKSTokenMethodAWSCli:
   204  		execConfig.Command = "aws"
   205  		execConfig.Args = []string{
   206  			"eks",
   207  			"get-token",
   208  			"--cluster-name",
   209  			clusterName,
   210  		}
   211  	default:
   212  		return fmt.Errorf("using token method %s: %w", s.scope.TokenMethod(), ErrUnknownTokenMethod)
   213  	}
   214  	cfg.AuthInfos = map[string]*api.AuthInfo{
   215  		userName: {
   216  			Exec: execConfig,
   217  		},
   218  	}
   219  
   220  	out, err := clientcmd.Write(*cfg)
   221  	if err != nil {
   222  		return errors.Wrap(err, "failed to serialize config to yaml")
   223  	}
   224  
   225  	kubeconfigSecret := kubeconfig.GenerateSecretWithOwner(*clusterRef, out, controllerOwnerRef)
   226  	if err := s.scope.Client.Create(ctx, kubeconfigSecret); err != nil {
   227  		return errors.Wrap(err, "failed to create kubeconfig secret")
   228  	}
   229  
   230  	record.Eventf(s.scope.ControlPlane, "SucessfulCreateUserKubeconfig", "Created user kubeconfig for cluster %q", s.scope.Name())
   231  	return nil
   232  }
   233  
   234  func (s *Service) createBaseKubeConfig(cluster *eks.Cluster, userName string) (*api.Config, error) {
   235  	clusterName := s.scope.KubernetesClusterName()
   236  	contextName := fmt.Sprintf("%s@%s", userName, clusterName)
   237  
   238  	certData, err := base64.StdEncoding.DecodeString(*cluster.CertificateAuthority.Data)
   239  	if err != nil {
   240  		return nil, fmt.Errorf("decoding cluster CA cert: %w", err)
   241  	}
   242  
   243  	cfg := &api.Config{
   244  		APIVersion: api.SchemeGroupVersion.Version,
   245  		Clusters: map[string]*api.Cluster{
   246  			clusterName: {
   247  				Server:                   *cluster.Endpoint,
   248  				CertificateAuthorityData: certData,
   249  			},
   250  		},
   251  		Contexts: map[string]*api.Context{
   252  			contextName: {
   253  				Cluster:  clusterName,
   254  				AuthInfo: userName,
   255  			},
   256  		},
   257  		CurrentContext: contextName,
   258  	}
   259  
   260  	return cfg, nil
   261  }
   262  
   263  func (s *Service) generateToken() (string, error) {
   264  	eksClusterName := s.scope.KubernetesClusterName()
   265  
   266  	req, output := s.STSClient.GetCallerIdentityRequest(&sts.GetCallerIdentityInput{})
   267  	req.HTTPRequest.Header.Add(clusterNameHeader, eksClusterName)
   268  	s.Logger.V(4).Info("generating token for AWS identity", "user", output.UserId, "account", output.Account, "arn", output.Arn)
   269  
   270  	presignedURL, err := req.Presign(tokenAgeMins * time.Minute)
   271  	if err != nil {
   272  		return "", fmt.Errorf("presigning AWS get caller identity: %w", err)
   273  	}
   274  
   275  	encodedURL := base64.RawURLEncoding.EncodeToString([]byte(presignedURL))
   276  	return fmt.Sprintf("%s%s", tokenPrefix, encodedURL), nil
   277  }
   278  
   279  func (s *Service) getKubeConfigUserName(clusterName string, isUser bool) string {
   280  	if isUser {
   281  		return fmt.Sprintf("%s-user", clusterName)
   282  	}
   283  
   284  	return fmt.Sprintf("%s-capi-admin", clusterName)
   285  }