Skip to content

Commit 301a43f

Browse files
authored
Merge pull request #569 from pc-alves/rollback-cross-stack-policy
Rolling back changes introduced for the cross stack policy
2 parents 5397371 + 1b9dcf3 commit 301a43f

File tree

7 files changed

+4
-450
lines changed

7 files changed

+4
-450
lines changed

senza/components/elastigroup.py

+1-85
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,8 @@
1313
from senza.components.auto_scaling_group import normalize_network_threshold
1414
from senza.components.taupage_auto_scaling_group import check_application_id, check_application_version, \
1515
check_docker_image_exists, generate_user_data
16-
from senza.utils import ensure_keys, CROSS_STACK_POLICY_NAME
16+
from senza.utils import ensure_keys
1717
from senza.spotinst import MissingSpotinstAccount
18-
import senza.manaus.iam
1918

2019
ELASTIGROUP_RESOURCE_TYPE = 'Custom::elastigroup'
2120
SPOTINST_LAMBDA_FORMATION_ARN = 'arn:aws:lambda:{}:178579023202:function:spotinst-cloudformation'
@@ -29,88 +28,6 @@
2928
ELASTIGROUP_DEFAULT_PRODUCT = "Linux/UNIX"
3029

3130

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-
11431
def component_elastigroup(definition, configuration, args, info, force, account_info):
11532
"""
11633
This component creates a Spotinst Elastigroup CloudFormation custom resource template.
@@ -145,7 +62,6 @@ def component_elastigroup(definition, configuration, args, info, force, account_
14562
extract_auto_scaling_rules(configuration, elastigroup_config)
14663
extract_block_mappings(configuration, elastigroup_config)
14764
extract_instance_profile(args, definition, configuration, elastigroup_config)
148-
patch_cross_stack_policy(definition, elastigroup_config)
14965
# cfn definition
15066
access_token = _extract_spotinst_access_token(definition)
15167
config_name = configuration["Name"]

senza/manaus/iam.py

-40
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
from typing import Any, Dict, Iterator, Optional, Union
1515

1616
import boto3
17-
import json
1817
from botocore.exceptions import ClientError
1918

2019
from .boto_proxy import BotoClientProxy
@@ -162,42 +161,3 @@ def get_certificates(
162161
continue
163162

164163
yield certificate
165-
166-
167-
def _get_policy_by_name(policy_name, iam_client):
168-
"""
169-
This function goes through all the policies in the AWS account and return the first one matching the policy_name
170-
input parameter
171-
"""
172-
paginator = iam_client.get_paginator("list_policies")
173-
174-
page_iterator = paginator.paginate()
175-
176-
for page in page_iterator:
177-
if "Policies" in page:
178-
for policy in page["Policies"]:
179-
if policy["PolicyName"] == policy_name:
180-
return policy
181-
182-
return None
183-
184-
185-
def find_or_create_policy(policy_name, policy_document, description):
186-
"""
187-
This function will look for a policy name with `policy_name`.
188-
If not found, it will create the policy using the provided `policy_name` and `policy_document`.
189-
190-
:return: Policy object
191-
"""
192-
iam_client = boto3.client("iam")
193-
194-
policy = _get_policy_by_name(policy_name, iam_client)
195-
if policy is None:
196-
response = iam_client.create_policy(
197-
PolicyName=policy_name,
198-
PolicyDocument=json.dumps(policy_document),
199-
Description=description
200-
)
201-
policy = response["Policy"]
202-
203-
return policy

senza/templates/_helper.py

-56
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@
88
from click import confirm
99
from clickclick import Action
1010
from senza.aws import get_account_alias, get_account_id, get_security_group
11-
from senza.utils import CROSS_STACK_POLICY_NAME
12-
import senza.manaus.iam
1311

1412
from ..manaus.boto_proxy import BotoClientProxy
1513

@@ -148,33 +146,6 @@ def create_mint_read_policy_document(application_id: str, bucket_name: str, regi
148146
}
149147

150148

151-
def create_cross_stack_policy_document():
152-
return {
153-
"Version": "2012-10-17",
154-
"Statement": [
155-
{
156-
"Effect": "Allow",
157-
"Action": [
158-
"cloudformation:SignalResource",
159-
"cloudformation:DescribeStackResource"
160-
],
161-
"Resource": "*"
162-
}
163-
]
164-
}
165-
166-
167-
def check_cross_stack_policy(iam, role_name: str):
168-
try:
169-
iam.get_role_policy(
170-
RoleName=role_name,
171-
PolicyName=CROSS_STACK_POLICY_NAME
172-
)
173-
return True
174-
except botocore.exceptions.ClientError:
175-
return False
176-
177-
178149
def check_iam_role(application_id: str, bucket_name: str, region: str):
179150
role_name = "app-{}".format(application_id)
180151
with Action("Checking IAM role {}..".format(role_name)):
@@ -229,33 +200,6 @@ def check_iam_role(application_id: str, bucket_name: str, region: str):
229200
PolicyDocument=json.dumps(mint_read_policy),
230201
)
231202

232-
attach_cross_stack_policy(exists, create, role_name, iam)
233-
234-
235-
def find_or_create_cross_stack_policy():
236-
return senza.manaus.iam.find_or_create_policy(policy_name=CROSS_STACK_POLICY_NAME,
237-
policy_document=create_cross_stack_policy_document(),
238-
description="Required permissions for EC2 instances created by "
239-
"Spotinst to signal CloudFormation")
240-
241-
242-
def attach_cross_stack_policy(pre_existing_role, role_created, role_name, iam_client):
243-
if not pre_existing_role and not role_created:
244-
return
245-
246-
cross_stack_policy_exists = False
247-
if pre_existing_role:
248-
cross_stack_policy_exists = check_cross_stack_policy(iam_client, role_name)
249-
250-
if role_created or not cross_stack_policy_exists:
251-
with Action("Updating IAM role policy of {}..".format(role_name)):
252-
policy = find_or_create_cross_stack_policy()
253-
254-
iam_client.attach_role_policy(
255-
RoleName=role_name,
256-
PolicyArn=policy["Arn"],
257-
)
258-
259203

260204
def check_s3_bucket(bucket_name: str, region: str):
261205
s3 = boto3.resource("s3", region)

senza/utils.py

-2
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@
66
import re
77
import pystache
88

9-
CROSS_STACK_POLICY_NAME = "system-cf-notifications"
10-
119

1210
def named_value(dictionary):
1311
"""

tests/test_elastigroup.py

+1-137
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
ensure_default_product, fill_standard_tags, extract_subnets,
1313
extract_load_balancer_name, extract_public_ips,
1414
extract_image_id, extract_security_group_ids, extract_instance_types,
15-
extract_instance_profile, patch_cross_stack_policy)
15+
extract_instance_profile)
1616

1717

1818
def test_component_elastigroup_defaults(monkeypatch):
@@ -799,142 +799,6 @@ def test_extract_instance_profile(monkeypatch):
799799
assert test_case["expected_config"] == got
800800

801801

802-
def test_patch_cross_stack_policy(monkeypatch):
803-
test_cases = [
804-
{ # No instance profile in definition
805-
"elastigroup_config": {"compute": {"launchSpecification": {}}},
806-
"definition": {},
807-
"expected_output": {}
808-
},
809-
{ # Instance profile definition references a managed instance profile
810-
"elastigroup_config": {"compute": {"launchSpecification": {"iamRole": {
811-
"arn": "arn:aws:iam::12345667:instance-profile/foo"}}}},
812-
"definition": {},
813-
"expected_output": {}
814-
},
815-
{ # Instance profile Role definition references a managed role
816-
"elastigroup_config": {"compute": {"launchSpecification": {"iamRole": {
817-
"name": {"Ref": "my-instance-profile"}}}}},
818-
"definition": {"Resources": {"my-instance-profile": {
819-
"Type": "AWS::IAM::InstanceProfile",
820-
"Properties": {"Path": "/", "Roles": ['a-managed-role']}}}},
821-
"expected_output": {"Resources": {"my-instance-profile": {
822-
"Type": "AWS::IAM::InstanceProfile",
823-
"Properties": {"Path": "/", "Roles": ['a-managed-role']}}}}
824-
},
825-
{ # Policy not in policies list of role
826-
"elastigroup_config": {"compute": {"launchSpecification": {"iamRole": {
827-
"name": {"Ref": "my-instance-profile1"}}}}},
828-
"definition": {"Resources": {
829-
"my-instance-profile1": {
830-
"Type": "AWS::IAM::InstanceProfile",
831-
"Properties": {"Path": "/", "Roles": [{"Ref": "my-role1"}]}
832-
},
833-
"my-role1": {
834-
"Type": "AWS::IAM::Role",
835-
"Properties": {}
836-
}
837-
}},
838-
"expected_output": {"Resources": {
839-
"my-instance-profile1": {
840-
"Type": "AWS::IAM::InstanceProfile",
841-
"Properties": {"Path": "/", "Roles": [{"Ref": "my-role1"}]}
842-
},
843-
"my-role1": {
844-
"Type": "AWS::IAM::Role",
845-
"Properties": {"ManagedPolicyArns": ['arn:aws:iam::aws:policy/zed']}
846-
}
847-
}}
848-
},
849-
{ # Policy already in policies list of role
850-
"elastigroup_config": {"compute": {"launchSpecification": {"iamRole": {
851-
"name": {"Ref": "my-instance-profile2"}}}}},
852-
"definition": {"Resources": {
853-
"my-instance-profile2": {
854-
"Type": "AWS::IAM::InstanceProfile",
855-
"Properties": {"Path": "/", "Roles": [{"Ref": "my-role2"}]}
856-
},
857-
"my-role2": {
858-
"Type": "AWS::IAM::Role",
859-
"Properties": {"ManagedPolicyArns": ['arn:aws:iam::aws:policy/zed']}
860-
}
861-
}},
862-
"expected_output": {"Resources": {
863-
"my-instance-profile2": {
864-
"Type": "AWS::IAM::InstanceProfile",
865-
"Properties": {"Path": "/", "Roles": [{"Ref": "my-role2"}]}
866-
},
867-
"my-role2": {
868-
"Type": "AWS::IAM::Role",
869-
"Properties": {"ManagedPolicyArns": ['arn:aws:iam::aws:policy/zed']}
870-
}
871-
}}
872-
}
873-
]
874-
875-
cross_stack_policy_mock = MagicMock()
876-
cross_stack_policy_mock.return_value = {"PolicyName": "zed", "Arn": "arn:aws:iam::aws:policy/zed"}
877-
monkeypatch.setattr("senza.manaus.iam.find_or_create_policy", cross_stack_policy_mock)
878-
879-
for test_case in test_cases:
880-
definition = test_case["definition"]
881-
patch_cross_stack_policy(definition, test_case["elastigroup_config"])
882-
883-
assert definition == test_case["expected_output"]
884-
885-
886-
def test_patch_cross_stack_policy_errors():
887-
# Error case 1 :: Instance profile not in Resources
888-
with pytest.raises(click.UsageError):
889-
elastigroup_config = {"compute": {"launchSpecification": {"iamRole": {
890-
"name": {"Ref": "my-instance-profile"}}}}}
891-
definition = {"Resources": {}}
892-
893-
patch_cross_stack_policy(definition, elastigroup_config)
894-
895-
# Error case 2 :: Instance profile not of type AWS::IAM::InstanceProfile
896-
with pytest.raises(click.UsageError):
897-
elastigroup_config = {"compute": {"launchSpecification": {"iamRole": {
898-
"name": {"Ref": "my-instance-profile"}}}}}
899-
definition = {"Resources": {
900-
"my-instance-profile": {
901-
"Type": "AWS::IAM::SomeOtherResource",
902-
"Properties": {"Path": "/", "Roles": [{"Ref": "my-role"}]}
903-
}}}
904-
905-
patch_cross_stack_policy(definition, elastigroup_config)
906-
907-
# Error case 3 :: Instance profile Role not in Resources
908-
with pytest.raises(click.UsageError):
909-
elastigroup_config = {"compute": {"launchSpecification": {"iamRole": {
910-
"name": {"Ref": "my-instance-profile"}}}}}
911-
definition = {"Resources": {
912-
"my-instance-profile": {
913-
"Type": "AWS::IAM::InstanceProfile",
914-
"Properties": {"Path": "/", "Roles": [{"Ref": "my-role"}]}
915-
}
916-
}}
917-
918-
patch_cross_stack_policy(definition, elastigroup_config)
919-
920-
# Error case 4 :: Instance profile Role not of type AWS::IAM::Role
921-
with pytest.raises(click.UsageError):
922-
elastigroup_config = {"compute": {"launchSpecification": {"iamRole": {
923-
"name": {"Ref": "my-instance-profile"}}}}}
924-
definition = {"Resources": {
925-
"my-instance-profile": {
926-
"Type": "AWS::IAM::InstanceProfile",
927-
"Properties": {"Path": "/", "Roles": [{"Ref": "my-role"}]}
928-
},
929-
"my-role": {
930-
"Type": "AWS::IAM::SomeOtherResource",
931-
"Properties": {"ManagedPolicyArns": ['arn:aws:iam::aws:policy/zed']}
932-
}
933-
}}
934-
935-
patch_cross_stack_policy(definition, elastigroup_config)
936-
937-
938802
def test_multiple_elastigroups(monkeypatch):
939803
config1 = {
940804
"Name": "eg1",

0 commit comments

Comments
 (0)