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 }