|
13 | 13 | from senza.components.auto_scaling_group import normalize_network_threshold
|
14 | 14 | from senza.components.taupage_auto_scaling_group import check_application_id, check_application_version, \
|
15 | 15 | check_docker_image_exists, generate_user_data
|
16 |
| -from senza.utils import ensure_keys |
| 16 | +from senza.utils import ensure_keys, CROSS_STACK_POLICY_NAME |
17 | 17 | from senza.spotinst import MissingSpotinstAccount
|
| 18 | +import senza.manaus.iam |
18 | 19 |
|
19 | 20 | ELASTIGROUP_RESOURCE_TYPE = 'Custom::elastigroup'
|
20 | 21 | SPOTINST_LAMBDA_FORMATION_ARN = 'arn:aws:lambda:{}:178579023202:function:spotinst-cloudformation'
|
|
28 | 29 | ELASTIGROUP_DEFAULT_PRODUCT = "Linux/UNIX"
|
29 | 30 |
|
30 | 31 |
|
| 32 | +def get_instance_profile_from_definition(definition, elastigroup_config): |
| 33 | + launch_spec = elastigroup_config["compute"]["launchSpecification"] |
| 34 | + |
| 35 | + if "iamRole" not in launch_spec: |
| 36 | + return None |
| 37 | + |
| 38 | + if "name" in launch_spec["iamRole"]: |
| 39 | + if isinstance(launch_spec["iamRole"]["name"], dict): |
| 40 | + instance_profile_id = launch_spec["iamRole"]["name"]["Ref"] |
| 41 | + instance_profile = definition["Resources"].get(instance_profile_id, None) |
| 42 | + if instance_profile is None: |
| 43 | + raise click.UsageError("Instance Profile referenced is not present in Resources") |
| 44 | + |
| 45 | + if instance_profile["Type"] != "AWS::IAM::InstanceProfile": |
| 46 | + raise click.UsageError( |
| 47 | + "Instance Profile references a Resource that is not of type 'AWS::IAM::InstanceProfile'") |
| 48 | + |
| 49 | + return instance_profile |
| 50 | + |
| 51 | + return None |
| 52 | + |
| 53 | + |
| 54 | +def get_instance_profile_role(instance_profile, definition): |
| 55 | + roles = instance_profile["Properties"]["Roles"] |
| 56 | + if isinstance(roles[0], dict): |
| 57 | + role_id = roles[0]["Ref"] |
| 58 | + role = definition["Resources"].get(role_id, None) |
| 59 | + if role is None: |
| 60 | + raise click.UsageError("Instance Profile references a Role that is not present in Resources") |
| 61 | + |
| 62 | + if role["Type"] != "AWS::IAM::Role": |
| 63 | + raise click.UsageError("Instance Profile Role references a Resource that is not of type 'AWS::IAM::Role'") |
| 64 | + |
| 65 | + return role |
| 66 | + |
| 67 | + return None |
| 68 | + |
| 69 | + |
| 70 | +def create_cross_stack_policy_document(): |
| 71 | + return { |
| 72 | + "Version": "2012-10-17", |
| 73 | + "Statement": [ |
| 74 | + { |
| 75 | + "Effect": "Allow", |
| 76 | + "Action": [ |
| 77 | + "cloudformation:SignalResource", |
| 78 | + "cloudformation:DescribeStackResource" |
| 79 | + ], |
| 80 | + "Resource": "*" |
| 81 | + } |
| 82 | + ] |
| 83 | + } |
| 84 | + |
| 85 | + |
| 86 | +def find_or_create_cross_stack_policy(): |
| 87 | + return senza.manaus.iam.find_or_create_policy(policy_name=CROSS_STACK_POLICY_NAME, |
| 88 | + policy_document=create_cross_stack_policy_document(), |
| 89 | + description="Required permissions for EC2 instances created by " |
| 90 | + "Spotinst to signal CloudFormation") |
| 91 | + |
| 92 | + |
| 93 | +def patch_cross_stack_policy(definition, elastigroup_config): |
| 94 | + """ |
| 95 | + This function will make sure that the role used in the Instance Profile includes the Cross Stack API |
| 96 | + requests policy, needed for Elastigroups to run as expected. |
| 97 | + """ |
| 98 | + instance_profile = get_instance_profile_from_definition(definition, elastigroup_config) |
| 99 | + if instance_profile is None: |
| 100 | + return |
| 101 | + |
| 102 | + instance_profile_role = get_instance_profile_role(instance_profile, definition) |
| 103 | + if instance_profile_role is None: |
| 104 | + return |
| 105 | + |
| 106 | + cross_stack_policy = find_or_create_cross_stack_policy() |
| 107 | + |
| 108 | + role_properties = instance_profile_role["Properties"] |
| 109 | + managed_policies_set = set(role_properties.get("ManagedPolicyArns", [])) |
| 110 | + managed_policies_set.add(cross_stack_policy["Arn"]) |
| 111 | + role_properties["ManagedPolicyArns"] = list(managed_policies_set) |
| 112 | + |
| 113 | + |
31 | 114 | def component_elastigroup(definition, configuration, args, info, force, account_info):
|
32 | 115 | """
|
33 | 116 | This component creates a Spotinst Elastigroup CloudFormation custom resource template.
|
@@ -62,6 +145,7 @@ def component_elastigroup(definition, configuration, args, info, force, account_
|
62 | 145 | extract_auto_scaling_rules(configuration, elastigroup_config)
|
63 | 146 | extract_block_mappings(configuration, elastigroup_config)
|
64 | 147 | extract_instance_profile(args, definition, configuration, elastigroup_config)
|
| 148 | + patch_cross_stack_policy(definition, elastigroup_config) |
65 | 149 | # cfn definition
|
66 | 150 | access_token = _extract_spotinst_access_token(definition)
|
67 | 151 | config_name = configuration["Name"]
|
|
0 commit comments