github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/provider/ec2/iam.go (about) 1 // Copyright 2021 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package ec2 5 6 import ( 7 stdcontext "context" 8 stderrors "errors" 9 "fmt" 10 "net/http" 11 "time" 12 13 "github.com/aws/aws-sdk-go-v2/aws" 14 awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http" 15 "github.com/aws/aws-sdk-go-v2/service/ec2" 16 ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types" 17 "github.com/aws/aws-sdk-go-v2/service/iam" 18 iamtypes "github.com/aws/aws-sdk-go-v2/service/iam/types" 19 "github.com/juju/clock" 20 "github.com/juju/errors" 21 "github.com/juju/retry" 22 23 "github.com/juju/juju/core/instance" 24 "github.com/juju/juju/core/status" 25 "github.com/juju/juju/environs" 26 "github.com/juju/juju/environs/cloudspec" 27 "github.com/juju/juju/environs/context" 28 "github.com/juju/juju/environs/instances" 29 "github.com/juju/juju/environs/tags" 30 ) 31 32 // instanceProfileClient is a subset interface of the ec2 client for attaching 33 // Instance Profiles to ec2 machines. 34 type instanceProfileClient interface { 35 AssociateIamInstanceProfile(stdcontext.Context, *ec2.AssociateIamInstanceProfileInput, ...func(*ec2.Options)) (*ec2.AssociateIamInstanceProfileOutput, error) 36 DescribeIamInstanceProfileAssociations(stdcontext.Context, *ec2.DescribeIamInstanceProfileAssociationsInput, ...func(*ec2.Options)) (*ec2.DescribeIamInstanceProfileAssociationsOutput, error) 37 } 38 39 // IAMClient is a subset interface of the AWS IAM client. This interface aims 40 // to define the small set of what Juju's needs from the larger client. 41 type IAMClient interface { 42 // STOP!! 43 // Are you about to add a new function to this interface? 44 // If so please make sure you update Juju permission policy on discourse 45 // here https://discourse.charmhub.io/t/juju-aws-permissions/5307 46 // We must keep this policy inline with our usage for operators that are 47 // using very strict permissions for Juju. 48 // 49 // You must also update the controllerRolePolicy document found in 50 // iam_docs.go. 51 AddRoleToInstanceProfile(stdcontext.Context, *iam.AddRoleToInstanceProfileInput, ...func(*iam.Options)) (*iam.AddRoleToInstanceProfileOutput, error) 52 CreateInstanceProfile(stdcontext.Context, *iam.CreateInstanceProfileInput, ...func(*iam.Options)) (*iam.CreateInstanceProfileOutput, error) 53 CreateRole(stdcontext.Context, *iam.CreateRoleInput, ...func(*iam.Options)) (*iam.CreateRoleOutput, error) 54 DeleteInstanceProfile(stdcontext.Context, *iam.DeleteInstanceProfileInput, ...func(*iam.Options)) (*iam.DeleteInstanceProfileOutput, error) 55 DeleteRole(stdcontext.Context, *iam.DeleteRoleInput, ...func(*iam.Options)) (*iam.DeleteRoleOutput, error) 56 DeleteRolePolicy(stdcontext.Context, *iam.DeleteRolePolicyInput, ...func(*iam.Options)) (*iam.DeleteRolePolicyOutput, error) 57 GetInstanceProfile(stdcontext.Context, *iam.GetInstanceProfileInput, ...func(*iam.Options)) (*iam.GetInstanceProfileOutput, error) 58 GetRole(stdcontext.Context, *iam.GetRoleInput, ...func(*iam.Options)) (*iam.GetRoleOutput, error) 59 ListInstanceProfiles(stdcontext.Context, *iam.ListInstanceProfilesInput, ...func(*iam.Options)) (*iam.ListInstanceProfilesOutput, error) 60 ListRolePolicies(stdcontext.Context, *iam.ListRolePoliciesInput, ...func(*iam.Options)) (*iam.ListRolePoliciesOutput, error) 61 ListRoles(stdcontext.Context, *iam.ListRolesInput, ...func(*iam.Options)) (*iam.ListRolesOutput, error) 62 PutRolePolicy(stdcontext.Context, *iam.PutRolePolicyInput, ...func(*iam.Options)) (*iam.PutRolePolicyOutput, error) 63 RemoveRoleFromInstanceProfile(stdcontext.Context, *iam.RemoveRoleFromInstanceProfileInput, ...func(*iam.Options)) (*iam.RemoveRoleFromInstanceProfileOutput, error) 64 } 65 66 // IAMClientFunc defines a type that can generate an AWS IAMClient from a 67 // provided cloudspec. 68 type IAMClientFunc = func(stdcontext.Context, cloudspec.CloudSpec, ...ClientOption) (IAMClient, error) 69 70 const ( 71 // setProfileAssociationDelay is the delay between retry attempts when. 72 setProfileAssociationDelay = time.Second * 15 73 74 // setProfileAssociationMaxAttempt is the maxium number of attempts before 75 // giving up on iam profile association. 76 setProfileAssociationMaxAttempt = 5 77 78 // setProfileDelay is the delay between retry attempts when setting an 79 // instances iam profile. 80 setProfileDelay = time.Second * 5 81 82 // setProfileMaxAttempt is the maxium number of attempts before giving up 83 // on setting an instances iam profile. 84 setProfileMaxAttempt = 5 85 ) 86 87 // iamClientFunc implements the IAMClientFunc type and is used internally by 88 // Juju for creating an IAM client. 89 func iamClientFunc( 90 ctx stdcontext.Context, 91 spec cloudspec.CloudSpec, 92 clientOptions ...ClientOption, 93 ) (IAMClient, error) { 94 cfg, err := configFromCloudSpec(ctx, spec, clientOptions...) 95 if err != nil { 96 return nil, errors.Annotate(err, "building aws config from cloudspec") 97 } 98 return iam.NewFromConfig(cfg), nil 99 } 100 101 // controllerPath returns an AWS path to use for IAM resources based on the 102 // controller UUID 103 func controllerPath(controllerUUID string) string { 104 return fmt.Sprintf("/juju/controller/%s/", controllerUUID) 105 } 106 107 // deleteInstanceProfile is a convience method for removing instance profile by 108 // first detaching all roles from the profile then deleting. 109 func deleteInstanceProfile( 110 ctx stdcontext.Context, 111 client IAMClient, 112 instanceProfile iamtypes.InstanceProfile, 113 ) error { 114 for _, role := range instanceProfile.Roles { 115 _, err := client.RemoveRoleFromInstanceProfile(ctx, &iam.RemoveRoleFromInstanceProfileInput{ 116 InstanceProfileName: instanceProfile.InstanceProfileName, 117 RoleName: role.RoleName, 118 }) 119 120 if err != nil { 121 return errors.Annotatef(err, "removing role %q from instance profile", *role.RoleName) 122 } 123 } 124 125 _, err := client.DeleteInstanceProfile(ctx, &iam.DeleteInstanceProfileInput{ 126 InstanceProfileName: instanceProfile.InstanceProfileName, 127 }) 128 129 return errors.Trace(err) 130 } 131 132 // deleteRole is a convience method for delete a role and it's associated 133 // inline policies. 134 func deleteRole( 135 ctx stdcontext.Context, 136 client IAMClient, 137 roleName string, 138 ) error { 139 140 var ( 141 inlinePolicyNames = []string{} 142 marker *string 143 ) 144 for { 145 res, err := client.ListRolePolicies(ctx, &iam.ListRolePoliciesInput{ 146 Marker: marker, 147 RoleName: aws.String(roleName), 148 }) 149 150 if err != nil { 151 return errors.Annotatef(err, "listing inline policies for role %q", roleName) 152 } 153 154 inlinePolicyNames = append(inlinePolicyNames, res.PolicyNames...) 155 if !res.IsTruncated { 156 break 157 } 158 marker = res.Marker 159 } 160 161 for _, policyName := range inlinePolicyNames { 162 _, err := client.DeleteRolePolicy(ctx, &iam.DeleteRolePolicyInput{ 163 PolicyName: aws.String(policyName), 164 RoleName: aws.String(roleName), 165 }) 166 if err != nil { 167 return errors.Annotatef(err, "delete inline policy %q", policyName) 168 } 169 } 170 171 _, err := client.DeleteRole(ctx, &iam.DeleteRoleInput{ 172 RoleName: aws.String(roleName), 173 }) 174 175 return errors.Trace(err) 176 } 177 178 // ensureControllerInstanceProfile ensures that a controller Instance Profile 179 // has been created for the supplied controller name in the specified AWS cloud. 180 func ensureControllerInstanceProfile( 181 ctx stdcontext.Context, 182 client IAMClient, 183 controllerName, 184 controllerUUID string, 185 ) (*iamtypes.InstanceProfile, []func(), error) { 186 role, cleanups, err := ensureControllerInstanceRole(ctx, client, controllerName, controllerUUID) 187 if err != nil { 188 return nil, cleanups, err 189 } 190 191 profileName := fmt.Sprintf("juju-controller-%s", controllerName) 192 res, err := client.CreateInstanceProfile(ctx, &iam.CreateInstanceProfileInput{ 193 InstanceProfileName: aws.String(profileName), 194 Tags: []iamtypes.Tag{ 195 { 196 Key: aws.String(tags.JujuController), 197 Value: aws.String(controllerUUID), 198 }, 199 }, 200 Path: aws.String(controllerPath(controllerUUID)), 201 }) 202 if err != nil { 203 var alreadyExistsErr *iamtypes.EntityAlreadyExistsException 204 if stderrors.As(err, &alreadyExistsErr) { 205 // Instance Profile already exists so we don't need todo anything. Let just find it 206 ip, err := findInstanceProfileFromName(ctx, client, profileName) 207 return ip, cleanups, err 208 } 209 // Some other error that we can't recover from. 210 return nil, cleanups, errors.Annotate(err, "creating controller instance profile") 211 } 212 213 cleanups = append([]func(){func() { 214 _, err := client.DeleteInstanceProfile(ctx, &iam.DeleteInstanceProfileInput{ 215 InstanceProfileName: res.InstanceProfile.InstanceProfileName, 216 }) 217 if err != nil { 218 logger.Errorf("cleanup delete instance profile %q: %v", 219 *res.InstanceProfile.InstanceProfileName, 220 err) 221 } 222 }}, cleanups...) 223 224 _, err = client.AddRoleToInstanceProfile(ctx, &iam.AddRoleToInstanceProfileInput{ 225 InstanceProfileName: res.InstanceProfile.InstanceProfileName, 226 RoleName: role.RoleName, 227 }) 228 229 if err != nil { 230 return nil, cleanups, errors.Annotatef( 231 err, 232 "attaching role %s to instance profile %s", 233 *role.RoleName, 234 *res.InstanceProfile.InstanceProfileName, 235 ) 236 } 237 238 cleanups = append([]func(){func() { 239 _, err := client.RemoveRoleFromInstanceProfile(ctx, &iam.RemoveRoleFromInstanceProfileInput{ 240 InstanceProfileName: res.InstanceProfile.InstanceProfileName, 241 RoleName: role.RoleName, 242 }) 243 if err != nil { 244 logger.Errorf("cleanup remove role %q from instance profile %q: %v", 245 *role.RoleName, 246 *res.InstanceProfile.InstanceProfileName, 247 err) 248 } 249 }}, cleanups...) 250 251 return res.InstanceProfile, cleanups, nil 252 } 253 254 func ensureControllerInstanceRole( 255 ctx stdcontext.Context, 256 client IAMClient, 257 controllerName, 258 controllerUUID string, 259 ) (*iamtypes.Role, []func(), error) { 260 roleName := fmt.Sprintf("juju-controller-%s", controllerName) 261 cleanups := []func(){} 262 res, err := client.CreateRole(ctx, &iam.CreateRoleInput{ 263 AssumeRolePolicyDocument: aws.String(controllerRoleAssumePolicy), 264 RoleName: aws.String(roleName), 265 Description: aws.String(fmt.Sprintf("juju role for controller %s", controllerName)), 266 Path: aws.String(controllerPath(controllerUUID)), 267 Tags: []iamtypes.Tag{ 268 { 269 Key: aws.String(tags.JujuController), 270 Value: aws.String(controllerUUID), 271 }, 272 }, 273 }) 274 275 if err != nil { 276 var alreadyExistsErr *iamtypes.EntityAlreadyExistsException 277 if stderrors.As(err, &alreadyExistsErr) { 278 // Role already exists so we don't need todo anything. Let just find it 279 r, err := findRoleFromName(ctx, client, roleName) 280 return r, cleanups, err 281 } 282 // Some other error that we can't recover from. 283 return nil, cleanups, errors.Annotate(err, "creating controller instance role") 284 } 285 286 cleanups = append(cleanups, func() { 287 _, err := client.DeleteRole(ctx, &iam.DeleteRoleInput{ 288 RoleName: res.Role.RoleName, 289 }) 290 if err != nil { 291 logger.Errorf("cleanup delete role %q: %v", 292 *res.Role.RoleName, 293 err) 294 } 295 }) 296 297 _, err = client.PutRolePolicy(ctx, &iam.PutRolePolicyInput{ 298 PolicyDocument: aws.String(controllerRolePolicy), 299 PolicyName: aws.String(roleName), 300 RoleName: res.Role.RoleName, 301 }) 302 303 if err != nil { 304 return nil, cleanups, errors.Annotatef(err, "attaching role %s policy", *res.Role.RoleName) 305 } 306 307 cleanups = append([]func(){func() { 308 _, err := client.DeleteRolePolicy(ctx, &iam.DeleteRolePolicyInput{ 309 PolicyName: aws.String(roleName), 310 RoleName: res.Role.RoleName, 311 }) 312 if err != nil { 313 logger.Errorf("cleanup delete role %q policy %q: %v", 314 *res.Role.RoleName, 315 roleName, 316 err) 317 } 318 }}, cleanups...) 319 320 return res.Role, cleanups, nil 321 } 322 323 // findInstanceProfileForName is responsible for finding the concrete instance 324 // profile for a supplied name. This is used to subsequently fetch the ARN of 325 // the InstanceProfile. 326 func findInstanceProfileFromName( 327 ctx stdcontext.Context, 328 client IAMClient, 329 name string, 330 ) (*iamtypes.InstanceProfile, error) { 331 res, err := client.GetInstanceProfile(ctx, &iam.GetInstanceProfileInput{ 332 InstanceProfileName: &name, 333 }) 334 335 if err != nil { 336 if isAWSHTTPErrorCode(err, http.StatusNotFound) { 337 return nil, errors.NotFoundf("instance profile %q not found", name) 338 } 339 return nil, errors.Annotatef(err, "finding instance profile for name %s", name) 340 } 341 342 return res.InstanceProfile, nil 343 } 344 345 func listInstanceProfilesForController( 346 ctx stdcontext.Context, 347 client IAMClient, 348 controllerUUID string, 349 ) ([]iamtypes.InstanceProfile, error) { 350 var ( 351 marker *string 352 rval = []iamtypes.InstanceProfile{} 353 ) 354 for { 355 res, err := client.ListInstanceProfiles(ctx, &iam.ListInstanceProfilesInput{ 356 Marker: marker, 357 PathPrefix: aws.String(controllerPath(controllerUUID)), 358 }) 359 360 if err != nil { 361 if isAWSHTTPErrorCode(err, http.StatusForbidden) { 362 return rval, errors.Unauthorizedf("listing instance profiles for controllerUUID %s", controllerUUID) 363 } 364 return rval, errors.Annotatef( 365 err, 366 "listing roles with controller prefix %q", 367 controllerPath(controllerUUID)) 368 } 369 370 rval = append(rval, res.InstanceProfiles...) 371 if !res.IsTruncated { 372 break 373 } 374 marker = res.Marker 375 } 376 return rval, nil 377 } 378 379 func listRolesForController( 380 ctx stdcontext.Context, 381 client IAMClient, 382 controllerUUID string, 383 ) ([]iamtypes.Role, error) { 384 var ( 385 marker *string 386 rval = []iamtypes.Role{} 387 ) 388 for { 389 res, err := client.ListRoles(ctx, &iam.ListRolesInput{ 390 Marker: marker, 391 PathPrefix: aws.String(controllerPath(controllerUUID)), 392 }) 393 394 if err != nil { 395 if isAWSHTTPErrorCode(err, http.StatusForbidden) { 396 return rval, errors.Unauthorizedf( 397 "listing roles for controllerUUID %s", 398 controllerUUID) 399 } 400 return rval, errors.Annotatef( 401 err, 402 "listing roles with controller prefix %q", 403 controllerPath(controllerUUID)) 404 } 405 406 rval = append(rval, res.Roles...) 407 if !res.IsTruncated { 408 break 409 } 410 marker = res.Marker 411 } 412 return rval, nil 413 } 414 415 func isAWSHTTPErrorCode(err error, statusCode int) bool { 416 var opHTTPErr *awshttp.ResponseError 417 if stderrors.As(err, &opHTTPErr) && opHTTPErr.HTTPStatusCode() == statusCode { 418 return true 419 } 420 return false 421 } 422 423 func findRoleFromName( 424 ctx stdcontext.Context, 425 client IAMClient, 426 name string, 427 ) (*iamtypes.Role, error) { 428 res, err := client.GetRole(ctx, &iam.GetRoleInput{ 429 RoleName: aws.String(name), 430 }) 431 432 if err != nil { 433 if isAWSHTTPErrorCode(err, http.StatusNotFound) { 434 return nil, errors.NotFoundf("role %q not found", name) 435 } 436 return nil, errors.Annotatef(err, "finding role for name %s", name) 437 } 438 439 return res.Role, nil 440 } 441 442 // setInstanceProfileWithWait sets the instnace profile for a given instance 443 // blocking until the instance is in a running state where the profile can be 444 // applied. This function also waits for the instance profile to be associated 445 // with the instance. 446 func setInstanceProfileWithWait( 447 ctx context.ProviderCallContext, 448 client instanceProfileClient, 449 profile *iamtypes.InstanceProfile, 450 inst instances.Instance, 451 instLister environs.InstanceLister, 452 ) error { 453 var association *ec2.AssociateIamInstanceProfileOutput 454 455 err := retry.Call(retry.CallArgs{ 456 Attempts: setProfileMaxAttempt, 457 Delay: setProfileDelay, 458 Func: func() (err error) { 459 association, err = setInstanceProfile(ctx, client, profile, inst, instLister) 460 return err 461 }, 462 IsFatalError: func(err error) bool { 463 return !errors.IsNotProvisioned(err) 464 }, 465 BackoffFunc: retry.DoubleDelay, 466 Clock: clock.WallClock, 467 Stop: ctx.Done(), 468 }) 469 470 if err != nil { 471 return errors.Annotatef( 472 err, 473 "setting instance profile %s for instance %s", 474 *profile.InstanceProfileName, 475 inst.Id(), 476 ) 477 } 478 479 // We need to wait here till the instance profile is associated to the 480 // instance. 481 return retry.Call(retry.CallArgs{ 482 Attempts: setProfileAssociationMaxAttempt, 483 Delay: setProfileAssociationDelay, 484 Func: func() error { 485 return IsInstanceProfileAssociated( 486 ctx, 487 client, 488 *association.IamInstanceProfileAssociation.AssociationId, 489 *association.IamInstanceProfileAssociation.InstanceId, 490 ) 491 }, 492 IsFatalError: func(err error) bool { 493 return !errors.IsNotProvisioned(err) 494 }, 495 BackoffFunc: retry.DoubleDelay, 496 Clock: clock.WallClock, 497 Stop: ctx.Done(), 498 }) 499 } 500 501 func IsInstanceProfileAssociated( 502 ctx context.ProviderCallContext, 503 client instanceProfileClient, 504 associationId, 505 instanceId string, 506 ) error { 507 rval, err := client.DescribeIamInstanceProfileAssociations( 508 ctx, 509 &ec2.DescribeIamInstanceProfileAssociationsInput{ 510 AssociationIds: []string{ 511 associationId, 512 }, 513 Filters: []ec2types.Filter{ 514 { 515 Name: aws.String("instance-id"), 516 Values: []string{ 517 instanceId, 518 }, 519 }, 520 }, 521 }, 522 ) 523 524 if err != nil { 525 return errors.Annotatef( 526 err, 527 "describing Instance Profile association %s", 528 associationId, 529 ) 530 } 531 532 // We have only asked for one association from aws so getting back 533 // more then one result doesn't make sense here so lets error. This 534 // condition should never be hit. 535 if len(rval.IamInstanceProfileAssociations) != 1 { 536 return errors.Errorf("expected 1 IAM Instance Profile association, got %d", len(rval.IamInstanceProfileAssociations)) 537 } 538 539 switch rval.IamInstanceProfileAssociations[0].State { 540 case ec2types.IamInstanceProfileAssociationStateAssociated: 541 return nil 542 case ec2types.IamInstanceProfileAssociationStateAssociating: 543 return errors.NotProvisionedf("IAM Instance Profile association %s", associationId) 544 // This should only ever be hit if the association is being 545 // Disassociated. This should never happen. 546 default: 547 return errors.NotSupportedf(" IAM Instance Profile association %s state %s", 548 associationId, 549 rval.IamInstanceProfileAssociations[0].State, 550 ) 551 } 552 } 553 554 // setInstanceProfile sets the instance profile for a given instance. This 555 // function first checks to see that the supplied instance is in a running 556 // state first otherwise a Juju NotProvisioned error returned. Use 557 // setInstanceProfileWithWait to block on the instance status being running. 558 func setInstanceProfile( 559 ctx context.ProviderCallContext, 560 client instanceProfileClient, 561 profile *iamtypes.InstanceProfile, 562 inst instances.Instance, 563 instLister environs.InstanceLister, 564 ) (*ec2.AssociateIamInstanceProfileOutput, error) { 565 rInst, err := instLister.Instances(ctx, []instance.Id{inst.Id()}) 566 if err != nil { 567 return nil, errors.Annotatef(err, "listing instance with id %s", inst.Id()) 568 } 569 if len(rInst) != 1 { 570 return nil, errors.Errorf("expected 1 instance for id %s got %d", inst.Id(), len(rInst)) 571 } 572 573 if rInst[0].Status(ctx).Status != status.Running { 574 return nil, errors.NotProvisionedf("instance %s is not running", inst.Id()) 575 } 576 577 instanceProfileInput := ec2.AssociateIamInstanceProfileInput{ 578 IamInstanceProfile: &ec2types.IamInstanceProfileSpecification{ 579 Arn: profile.Arn, 580 Name: profile.InstanceProfileName, 581 }, 582 InstanceId: aws.String(string(inst.Id())), 583 } 584 585 rval, err := client.AssociateIamInstanceProfile(ctx, &instanceProfileInput) 586 if err != nil { 587 return nil, errors.Annotatef( 588 err, 589 "attaching instance profile %s to instance %s", 590 *profile.InstanceProfileName, 591 inst.Id()) 592 } 593 594 return rval, nil 595 }