github.com/openshift/installer@v1.4.17/pkg/infrastructure/aws/clusterapi/iam.go (about) 1 package clusterapi 2 3 import ( 4 "context" 5 "encoding/json" 6 "errors" 7 "fmt" 8 9 "github.com/aws/aws-sdk-go/aws" 10 "github.com/aws/aws-sdk-go/aws/awserr" 11 "github.com/aws/aws-sdk-go/aws/endpoints" 12 "github.com/aws/aws-sdk-go/service/iam" 13 "github.com/sirupsen/logrus" 14 iamv1 "sigs.k8s.io/cluster-api-provider-aws/v2/iam/api/v1beta1" 15 16 "github.com/openshift/installer/pkg/asset/installconfig" 17 ) 18 19 const ( 20 master = "master" 21 worker = "worker" 22 ) 23 24 var ( 25 policies = map[string]*iamv1.PolicyDocument{ 26 master: { 27 Version: "2012-10-17", 28 Statement: []iamv1.StatementEntry{ 29 { 30 Effect: "Allow", 31 Action: []string{ 32 "ec2:AttachVolume", 33 "ec2:AuthorizeSecurityGroupIngress", 34 "ec2:CreateSecurityGroup", 35 "ec2:CreateTags", 36 "ec2:CreateVolume", 37 "ec2:DeleteSecurityGroup", 38 "ec2:DeleteVolume", 39 "ec2:Describe*", 40 "ec2:DetachVolume", 41 "ec2:ModifyInstanceAttribute", 42 "ec2:ModifyVolume", 43 "ec2:RevokeSecurityGroupIngress", 44 "elasticloadbalancing:AddTags", 45 "elasticloadbalancing:AttachLoadBalancerToSubnets", 46 "elasticloadbalancing:ApplySecurityGroupsToLoadBalancer", 47 "elasticloadbalancing:CreateListener", 48 "elasticloadbalancing:CreateLoadBalancer", 49 "elasticloadbalancing:CreateLoadBalancerPolicy", 50 "elasticloadbalancing:CreateLoadBalancerListeners", 51 "elasticloadbalancing:CreateTargetGroup", 52 "elasticloadbalancing:ConfigureHealthCheck", 53 "elasticloadbalancing:DeleteListener", 54 "elasticloadbalancing:DeleteLoadBalancer", 55 "elasticloadbalancing:DeleteLoadBalancerListeners", 56 "elasticloadbalancing:DeleteTargetGroup", 57 "elasticloadbalancing:DeregisterInstancesFromLoadBalancer", 58 "elasticloadbalancing:DeregisterTargets", 59 "elasticloadbalancing:Describe*", 60 "elasticloadbalancing:DetachLoadBalancerFromSubnets", 61 "elasticloadbalancing:ModifyListener", 62 "elasticloadbalancing:ModifyLoadBalancerAttributes", 63 "elasticloadbalancing:ModifyTargetGroup", 64 "elasticloadbalancing:ModifyTargetGroupAttributes", 65 "elasticloadbalancing:RegisterInstancesWithLoadBalancer", 66 "elasticloadbalancing:RegisterTargets", 67 "elasticloadbalancing:SetLoadBalancerPoliciesForBackendServer", 68 "elasticloadbalancing:SetLoadBalancerPoliciesOfListener", 69 "kms:DescribeKey", 70 }, 71 Resource: iamv1.Resources{ 72 "*", 73 }, 74 }, 75 }, 76 }, 77 worker: { 78 Version: "2012-10-17", 79 Statement: []iamv1.StatementEntry{ 80 { 81 Effect: "Allow", 82 Action: iamv1.Actions{ 83 "ec2:DescribeInstances", 84 "ec2:DescribeRegions", 85 }, 86 Resource: iamv1.Resources{"*"}, 87 }, 88 }, 89 }, 90 } 91 ) 92 93 // createIAMRoles creates the roles used by control-plane and compute nodes. 94 func createIAMRoles(ctx context.Context, infraID string, ic *installconfig.InstallConfig) error { 95 logrus.Infoln("Reconciling IAM roles for control-plane and compute nodes") 96 // Create the IAM Role with the aws sdk. 97 // https://docs.aws.amazon.com/sdk-for-go/api/service/iam/#IAM.CreateRole 98 session, err := ic.AWS.Session(ctx) 99 if err != nil { 100 return fmt.Errorf("failed to load AWS session: %w", err) 101 } 102 svc := iam.New(session) 103 104 // Create the IAM Roles for master and workers. 105 tags := []*iam.Tag{ 106 { 107 Key: aws.String(fmt.Sprintf("kubernetes.io/cluster/%s", infraID)), 108 Value: aws.String("owned"), 109 }, 110 } 111 112 for k, v := range ic.Config.AWS.UserTags { 113 tags = append(tags, &iam.Tag{ 114 Key: aws.String(k), 115 Value: aws.String(v), 116 }) 117 } 118 119 assumePolicy := &iamv1.PolicyDocument{ 120 Version: "2012-10-17", 121 Statement: iamv1.Statements{ 122 { 123 Effect: "Allow", 124 Principal: iamv1.Principals{ 125 iamv1.PrincipalService: []string{ 126 getPartitionService(ic.AWS.Region), 127 }, 128 }, 129 Action: iamv1.Actions{ 130 "sts:AssumeRole", 131 }, 132 }, 133 }, 134 } 135 assumePolicyBytes, err := json.Marshal(assumePolicy) 136 if err != nil { 137 return fmt.Errorf("failed to marshal assume policy: %w", err) 138 } 139 140 var defaultProfile string 141 if dmp := ic.Config.AWS.DefaultMachinePlatform; dmp != nil && len(dmp.IAMProfile) > 0 { 142 defaultProfile = dmp.IAMProfile 143 } 144 145 for _, role := range []string{master, worker} { 146 instanceProfile := defaultProfile 147 switch role { 148 case master: 149 if cp := ic.Config.ControlPlane; cp != nil && cp.Platform.AWS != nil && len(cp.Platform.AWS.IAMProfile) > 0 { 150 instanceProfile = cp.Platform.AWS.IAMProfile 151 } 152 case worker: 153 if w := ic.Config.Compute; len(w) > 0 && w[0].Platform.AWS != nil && len(w[0].Platform.AWS.IAMProfile) > 0 { 154 instanceProfile = w[0].Platform.AWS.IAMProfile 155 } 156 } 157 158 // A user-provided instance profile already has a role attached to it, so there is nothing else for the 159 // Installer to do. 160 if len(instanceProfile) > 0 { 161 logrus.Debugf("Using existing %s instance profile %q", role, instanceProfile) 162 continue 163 } 164 165 roleName, err := getOrCreateIAMRole(ctx, role, infraID, string(assumePolicyBytes), *ic, tags, svc) 166 if err != nil { 167 return fmt.Errorf("failed to create IAM %s role: %w", role, err) 168 } 169 170 profileName := aws.String(fmt.Sprintf("%s-%s-profile", infraID, role)) 171 if _, err := svc.GetInstanceProfileWithContext(ctx, &iam.GetInstanceProfileInput{InstanceProfileName: profileName}); err != nil { 172 var awsErr awserr.Error 173 if errors.As(err, &awsErr) && awsErr.Code() != iam.ErrCodeNoSuchEntityException { 174 return fmt.Errorf("failed to get %s instance profile: %w", role, err) 175 } 176 // If the profile does not exist, create it. 177 if _, err := svc.CreateInstanceProfileWithContext(ctx, &iam.CreateInstanceProfileInput{ 178 InstanceProfileName: profileName, 179 Tags: tags, 180 }); err != nil { 181 return fmt.Errorf("failed to create %s instance profile: %w", role, err) 182 } 183 if err := svc.WaitUntilInstanceProfileExistsWithContext(ctx, &iam.GetInstanceProfileInput{InstanceProfileName: profileName}); err != nil { 184 return fmt.Errorf("failed to wait for %s instance profile to exist: %w", role, err) 185 } 186 187 // Finally, attach the role to the profile. 188 if _, err := svc.AddRoleToInstanceProfileWithContext(ctx, &iam.AddRoleToInstanceProfileInput{ 189 InstanceProfileName: profileName, 190 RoleName: aws.String(roleName), 191 }); err != nil { 192 return fmt.Errorf("failed to add %s role to instance profile: %w", role, err) 193 } 194 } 195 } 196 197 return nil 198 } 199 200 // getOrCreateRole returns the name of the IAM role to be used, 201 // creating it when not specified by the user in the install config. 202 func getOrCreateIAMRole(ctx context.Context, nodeRole, infraID, assumePolicy string, ic installconfig.InstallConfig, tags []*iam.Tag, svc *iam.IAM) (string, error) { 203 roleName := aws.String(fmt.Sprintf("%s-%s-role", infraID, nodeRole)) 204 205 var defaultRole string 206 if dmp := ic.Config.AWS.DefaultMachinePlatform; dmp != nil && len(dmp.IAMRole) > 0 { 207 defaultRole = dmp.IAMRole 208 } 209 210 masterRole := defaultRole 211 if cp := ic.Config.ControlPlane; cp != nil && cp.Platform.AWS != nil && len(cp.Platform.AWS.IAMRole) > 0 { 212 masterRole = cp.Platform.AWS.IAMRole 213 } 214 215 workerRole := defaultRole 216 if w := ic.Config.Compute; len(w) > 0 && w[0].Platform.AWS != nil && len(w[0].Platform.AWS.IAMRole) > 0 { 217 workerRole = w[0].Platform.AWS.IAMRole 218 } 219 220 switch { 221 case nodeRole == master && len(masterRole) > 0: 222 return masterRole, nil 223 case nodeRole == worker && len(workerRole) > 0: 224 return workerRole, nil 225 } 226 227 if _, err := svc.GetRoleWithContext(ctx, &iam.GetRoleInput{RoleName: roleName}); err != nil { 228 var awsErr awserr.Error 229 if errors.As(err, &awsErr) && awsErr.Code() != iam.ErrCodeNoSuchEntityException { 230 return "", fmt.Errorf("failed to get %s role: %w", nodeRole, err) 231 } 232 // If the role does not exist, create it. 233 logrus.Infof("Creating IAM role for %s", nodeRole) 234 createRoleInput := &iam.CreateRoleInput{ 235 RoleName: roleName, 236 AssumeRolePolicyDocument: aws.String(assumePolicy), 237 Tags: tags, 238 } 239 if _, err := svc.CreateRoleWithContext(ctx, createRoleInput); err != nil { 240 return "", fmt.Errorf("failed to create %s role: %w", nodeRole, err) 241 } 242 243 if err := svc.WaitUntilRoleExistsWithContext(ctx, &iam.GetRoleInput{RoleName: roleName}); err != nil { 244 return "", fmt.Errorf("failed to wait for %s role to exist: %w", nodeRole, err) 245 } 246 } 247 248 // Put the policy inline. 249 policyName := aws.String(fmt.Sprintf("%s-%s-policy", infraID, nodeRole)) 250 b, err := json.Marshal(policies[nodeRole]) 251 if err != nil { 252 return "", fmt.Errorf("failed to marshal %s policy: %w", nodeRole, err) 253 } 254 if _, err := svc.PutRolePolicyWithContext(ctx, &iam.PutRolePolicyInput{ 255 PolicyDocument: aws.String(string(b)), 256 PolicyName: policyName, 257 RoleName: roleName, 258 }); err != nil { 259 return "", fmt.Errorf("failed to create inline policy for role %s: %w", nodeRole, err) 260 } 261 262 return *roleName, nil 263 } 264 265 func getPartitionService(region string) string { 266 partitionDNSSuffix := "amazonaws.com" 267 if ps, found := endpoints.PartitionForRegion(endpoints.DefaultPartitions(), region); found { 268 partitionDNSSuffix = ps.DNSSuffix() 269 } 270 return fmt.Sprintf("ec2.%s", partitionDNSSuffix) 271 }