sigs.k8s.io/cluster-api-provider-aws@v1.5.5/pkg/cloud/services/iamauth/reconcile.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 iamauth 18 19 import ( 20 "context" 21 "fmt" 22 23 "github.com/aws/aws-sdk-go/aws" 24 "github.com/aws/aws-sdk-go/service/iam" 25 "github.com/pkg/errors" 26 corev1 "k8s.io/api/core/v1" 27 "sigs.k8s.io/controller-runtime/pkg/client" 28 29 infrav1 "sigs.k8s.io/cluster-api-provider-aws/api/v1beta1" 30 ekscontrolplanev1 "sigs.k8s.io/cluster-api-provider-aws/controlplane/eks/api/v1beta1" 31 expinfrav1 "sigs.k8s.io/cluster-api-provider-aws/exp/api/v1beta1" 32 iamv1 "sigs.k8s.io/cluster-api-provider-aws/iam/api/v1beta1" 33 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" 34 expclusterv1 "sigs.k8s.io/cluster-api/exp/api/v1beta1" 35 ) 36 37 // ReconcileIAMAuthenticator is used to create the aws-iam-authenticator in a cluster. 38 func (s *Service) ReconcileIAMAuthenticator(ctx context.Context) error { 39 s.scope.Info("Reconciling aws-iam-authenticator configuration", "cluster-name", s.scope.Name()) 40 41 remoteClient, err := s.scope.RemoteClient() 42 if err != nil { 43 s.scope.Error(err, "getting client for remote cluster") 44 return fmt.Errorf("getting client for remote cluster: %w", err) 45 } 46 47 authBackend, err := NewBackend(s.backend, remoteClient) 48 if err != nil { 49 return fmt.Errorf("getting aws-iam-authenticator backend: %w", err) 50 } 51 nodeRoles, err := s.getRolesForWorkers(ctx) 52 if err != nil { 53 s.scope.Error(err, "getting roles for remote workers") 54 return fmt.Errorf("getting roles for remote workers: %w", err) 55 } 56 for roleName := range nodeRoles { 57 roleARN, err := s.getARNForRole(roleName) 58 if err != nil { 59 return fmt.Errorf("failed to get ARN for role %s: %w", roleARN, err) 60 } 61 nodesRoleMapping := ekscontrolplanev1.RoleMapping{ 62 RoleARN: roleARN, 63 KubernetesMapping: ekscontrolplanev1.KubernetesMapping{ 64 UserName: EC2NodeUserName, 65 Groups: NodeGroups, 66 }, 67 } 68 s.scope.V(2).Info("Mapping node IAM role", "iam-role", nodesRoleMapping.RoleARN, "user", nodesRoleMapping.UserName) 69 if err := authBackend.MapRole(nodesRoleMapping); err != nil { 70 return fmt.Errorf("mapping iam node role: %w", err) 71 } 72 } 73 74 s.scope.V(2).Info("Mapping additional IAM roles and users") 75 iamCfg := s.scope.IAMAuthConfig() 76 for _, roleMapping := range iamCfg.RoleMappings { 77 s.scope.V(2).Info("Mapping IAM role", "iam-role", roleMapping.RoleARN, "user", roleMapping.UserName) 78 if err := authBackend.MapRole(roleMapping); err != nil { 79 return fmt.Errorf("mapping iam role: %w", err) 80 } 81 } 82 83 for _, userMapping := range iamCfg.UserMappings { 84 s.scope.V(2).Info("Mapping IAM user", "iam-user", userMapping.UserARN, "user", userMapping.UserName) 85 if err := authBackend.MapUser(userMapping); err != nil { 86 return fmt.Errorf("mapping iam user: %w", err) 87 } 88 } 89 90 s.scope.Info("Reconciled aws-iam-authenticator configuration", "cluster-name", s.scope.KubernetesClusterName()) 91 92 return nil 93 } 94 95 func (s *Service) getARNForRole(role string) (string, error) { 96 input := &iam.GetRoleInput{ 97 RoleName: aws.String(role), 98 } 99 out, err := s.IAMClient.GetRole(input) 100 if err != nil { 101 return "", errors.Wrap(err, "unable to get role") 102 } 103 return aws.StringValue(out.Role.Arn), nil 104 } 105 106 func (s *Service) getRolesForWorkers(ctx context.Context) (map[string]struct{}, error) { 107 // previously this was the default role always added to the IAM authenticator config 108 // we'll keep this to not break existing behavior for users 109 allRoles := map[string]struct{}{ 110 fmt.Sprintf("nodes%s", iamv1.DefaultNameSuffix): {}, 111 } 112 if err := s.getRolesForMachineDeployments(ctx, allRoles); err != nil { 113 return nil, fmt.Errorf("failed to get roles from machine deployments %w", err) 114 } 115 if err := s.getRolesForMachinePools(ctx, allRoles); err != nil { 116 return nil, fmt.Errorf("failed to get roles from machine pools %w", err) 117 } 118 return allRoles, nil 119 } 120 121 func (s *Service) getRolesForMachineDeployments(ctx context.Context, allRoles map[string]struct{}) error { 122 deploymentList := &clusterv1.MachineDeploymentList{} 123 selectors := []client.ListOption{ 124 client.InNamespace(s.scope.Namespace()), 125 client.MatchingLabels{ 126 clusterv1.ClusterLabelName: s.scope.Name(), 127 }, 128 } 129 err := s.client.List(ctx, deploymentList, selectors...) 130 if err != nil { 131 return fmt.Errorf("failed to list machine deployments for cluster %s/%s: %w", s.scope.Namespace(), s.scope.Name(), err) 132 } 133 134 for _, deployment := range deploymentList.Items { 135 ref := deployment.Spec.Template.Spec.InfrastructureRef 136 if ref.Kind != "AWSMachineTemplate" { 137 continue 138 } 139 awsMachineTemplate := &infrav1.AWSMachineTemplate{} 140 err := s.client.Get(ctx, client.ObjectKey{ 141 Name: ref.Name, 142 Namespace: s.scope.Namespace(), 143 }, awsMachineTemplate) 144 if err != nil { 145 return fmt.Errorf("failed to get AWSMachine %s/%s: %w", ref.Namespace, ref.Name, err) 146 } 147 instanceProfile := awsMachineTemplate.Spec.Template.Spec.IAMInstanceProfile 148 if _, ok := allRoles[instanceProfile]; !ok && instanceProfile != "" { 149 allRoles[instanceProfile] = struct{}{} 150 } 151 } 152 return nil 153 } 154 155 func (s *Service) getRolesForMachinePools(ctx context.Context, allRoles map[string]struct{}) error { 156 machinePoolList := &expclusterv1.MachinePoolList{} 157 selectors := []client.ListOption{ 158 client.InNamespace(s.scope.Namespace()), 159 client.MatchingLabels{ 160 clusterv1.ClusterLabelName: s.scope.Name(), 161 }, 162 } 163 err := s.client.List(ctx, machinePoolList, selectors...) 164 if err != nil { 165 return fmt.Errorf("failed to list machine pools for cluster %s/%s: %w", s.scope.Namespace(), s.scope.Name(), err) 166 } 167 for _, pool := range machinePoolList.Items { 168 ref := pool.Spec.Template.Spec.InfrastructureRef 169 switch ref.Kind { 170 case "AWSMachinePool": 171 if err := s.getRolesForAWSMachinePool(ctx, ref, allRoles); err != nil { 172 return err 173 } 174 case "AWSManagedMachinePool": 175 if err := s.getRolesForAWSManagedMachinePool(ctx, ref, allRoles); err != nil { 176 return err 177 } 178 default: 179 } 180 } 181 return nil 182 } 183 184 func (s *Service) getRolesForAWSMachinePool(ctx context.Context, ref corev1.ObjectReference, allRoles map[string]struct{}) error { 185 awsMachinePool := &expinfrav1.AWSMachinePool{} 186 err := s.client.Get(ctx, client.ObjectKey{ 187 Name: ref.Name, 188 Namespace: s.scope.Namespace(), 189 }, awsMachinePool) 190 if err != nil { 191 return fmt.Errorf("failed to get AWSMachine %s/%s: %w", ref.Namespace, ref.Name, err) 192 } 193 instanceProfile := awsMachinePool.Spec.AWSLaunchTemplate.IamInstanceProfile 194 if _, ok := allRoles[instanceProfile]; !ok && instanceProfile != "" { 195 allRoles[instanceProfile] = struct{}{} 196 } 197 return nil 198 } 199 200 func (s *Service) getRolesForAWSManagedMachinePool(ctx context.Context, ref corev1.ObjectReference, allRoles map[string]struct{}) error { 201 awsManagedMachinePool := &expinfrav1.AWSManagedMachinePool{} 202 err := s.client.Get(ctx, client.ObjectKey{ 203 Name: ref.Name, 204 Namespace: s.scope.Namespace(), 205 }, awsManagedMachinePool) 206 if err != nil { 207 return fmt.Errorf("failed to get AWSMachine %s/%s: %w", ref.Namespace, ref.Name, err) 208 } 209 instanceProfile := awsManagedMachinePool.Spec.RoleName 210 if _, ok := allRoles[instanceProfile]; !ok && instanceProfile != "" { 211 allRoles[instanceProfile] = struct{}{} 212 } 213 return nil 214 }