8000 Fix deletion of AWS::IAM::Policy (#9092) · codeperl/localstack@982da5d · GitHub
[go: up one dir, main page]

Skip to content

Commit 982da5d

Browse files
Fix deletion of AWS::IAM::Policy (localstack#9092)
1 parent 5557e15 commit 982da5d

File tree

9 files changed

+512
-20
lines changed

9 files changed

+512
-20
lines changed

localstack/services/cloudformation/engine/template_deployer.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
)
2323
from localstack.services.cloudformation.resource_provider import (
2424
Credentials,
25+
OperationStatus,
2526
ResourceProviderExecutor,
2627
ResourceProviderPayload,
2728
get_resource_type,
@@ -1358,13 +1359,31 @@ def apply_change(self, change: ChangeConfig, stack: Stack) -> None:
13581359

13591360
progress_event = executor.deploy_loop(resource_provider_payload) # noqa
13601361

1362+
# TODO: clean up the surrounding loop (do_apply_changes_in_loop) so that the responsibilities are clearer
1363+
stack_action = get_action_name_for_resource_change(action)
1364+
match progress_event.status:
1365+
case OperationStatus.FAILED:
1366+
stack.set_resource_status(resource_id, f"{stack_action}_FAILED")
1367+
8000 # TODO: remove exception raising here?
1368+
# TODO: fix request token
1369+
raise Exception(
1370+
f'Resource handler returned message: "{progress_event.message}" (RequestToken: 10c10335-276a-33d3-5c07-018b684c3d26, HandlerErrorCode: InvalidRequest){progress_event.error_code}'
1371+
)
1372+
case OperationStatus.SUCCESS:
1373+
stack.set_resource_status(resource_id, f"{stack_action}_COMPLETE")
1374+
case OperationStatus.PENDING:
1375+
# this isn't really a state we use at the moment
1376+
raise Exception(
1377+
f"Usage of currently unsupported operation status detected: {OperationStatus.PENDING}"
1378+
)
1379+
case OperationStatus.IN_PROGRESS:
1380+
raise Exception("Resource deployment loop should not finish in this state")
1381+
case unknown_status:
1382+
raise Exception(f"Unknown operation status: {unknown_status}")
1383+
13611384
# TODO: this is probably already done in executor, try removing this
13621385
resource["Properties"] = progress_event.resource_model
13631386

1364-
# update resource status and physical resource id
1365-
stack_action = get_action_name_for_resource_change(action)
1366-
stack.set_resource_status(resource_id, f"{stack_action}_COMPLETE")
1367-
13681387
def create_resource_provider_executor(self) -> ResourceProviderExecutor:
13691388
return ResourceProviderExecutor(
13701389
stack_name=self.stack.stack_name,

localstack/services/cloudformation/resource_provider.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -641,6 +641,9 @@ def deploy_loop(
641641

642642
event = self.execute_action(resource_provider, payload)
643643

644+
if event.status == OperationStatus.FAILED:
645+
return event
646+
644647
if event.status == OperationStatus.SUCCESS:
645648

646649
if not isinstance(resource_provider, LegacyResourceProvider):

localstack/services/iam/resource_providers/aws_iam_policy.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,14 @@ def create(
5858
policy_doc = json.dumps(util.remove_none_values(model["PolicyDocument"]))
5959
policy_name = model["PolicyName"]
6060

61+
if not any([model.get("Roles"), model.get("Users"), model.get("Groups")]):
62+
return ProgressEvent(
63+
status=OperationStatus.FAILED,
64+
resource_model={},
65+
error_code="InvalidRequest",
66+
message="At least one of [Groups,Roles,Users] must be non-empty.",
67+
)
68+
6169
for role in model.get("Roles", []):
6270
iam_client.put_role_policy(
6371
RoleName=role, PolicyName=policy_name, PolicyDocument=policy_doc
@@ -83,8 +91,6 @@ def read(
8391
) -> ProgressEvent[IAMPolicyProperties]:
8492
"""
8593
Fetch resource information
86-
87-
8894
"""
8995
raise NotImplementedError
9096

@@ -96,7 +102,16 @@ def delete(
96102
Delete a resource
97103
"""
98104
iam = request.aws_client_factory.iam
99-
iam.delete_policy(PolicyArn=request.desired_state["Id"])
105+
106+
model = request.previous_state
107+
policy_name = request.previous_state["PolicyName"]
108+
for role in model.get("Roles", []):
109+
iam.delete_role_policy(RoleName=role, PolicyName=policy_name)
110+
for user in model.get("Users", []):
111+
iam.delete_user_policy(UserName=user, PolicyName=policy_name)
112+
for group in model.get("Groups", []):
113+
iam.delete_group_policy(GroupName=group, PolicyName=policy_name)
114+
100115
return ProgressEvent(status=OperationStatus.SUCCESS, resource_model={})
101116

102117
def update(

localstack/testing/snapshots/transformer_utility.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def key_value(
5757
:return: KeyValueBasedTransformer
5858
"""
5959
return KeyValueBasedTransformer(
60-
lambda k, v: v if k == key else None,
60+
lambda k, v: v if k == key and (v is not None and v != "") else None,
6161
replacement=value_replacement or _replace_camel_string_with_hyphen(key),
6262
replace_reference=reference_replacement,
6363
)

tests/aws/services/cloudformation/api/test_stacks.py

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@
44
import botocore.exceptions
55
import pytest
66
import yaml
7+
from botocore.exceptions import WaiterError
78

9+
from localstack.aws.api.cloudformation import Capability
810
from localstack.services.cloudformation.engine.yaml_parser import parse_yaml
11+
from localstack.testing.aws.util import is_aws_cloud
912
from localstack.testing.pytest import markers
1013
from localstack.testing.snapshots.transformer import SortingTransformer
1114
from localstack.utils.files import load_file
@@ -633,7 +636,7 @@ def test_events_resource_types(deploy_cfn_template, snapshot, aws_client):
633636

634637

635638
@markers.aws.validated
636-
def test_list_parameter_type(aws_client, deploy_cfn_template, cleanups, lambda_su_role):
639+
def test_list_parameter_type(aws_client, deploy_cfn_template, cleanups):
637640
stack_name = f"test-stack-{short_uid()}"
638641
cleanups.append(lambda: aws_client.cloudformation.delete_stack(StackName=stack_name))
639642
stack = deploy_cfn_template(
@@ -646,3 +649,56 @@ def test_list_parameter_type(aws_client, deploy_cfn_template, cleanups, lambda_s
646649
)
647650

648651
assert stack.outputs["ParamValue"] == "foo|bar"
652+
653+
654+
@markers.aws.validated
655+
@pytest.mark.skipif(condition=not is_aws_cloud(), reason="rollback not implemented")
656+
def test_blocked_stack_deletion(aws_client, cleanups, snapshot):
657+
"""
658+
uses AWS::IAM::Policy for demonstrating this behavior
659+
660+
1. create fails
661+
2. rollback fails even though create didn't even provision anything
662+
3. trying to delete the stack afterwards also doesn't work
663+
4. deleting the stack with retain resources works
664+
"""
665+
cfn = aws_client.cloudformation
666+
stack_name = f"test-stacks-blocked-{short_uid()}"
667+
policy_name = f"test-broken-policy-{short_uid()}"
668+
snapshot.add_transformer(snapshot.transform.cloudformation_api())
669+
snapshot.add_transformer(snapshot.transform.regex(policy_name, "<policy-name>"))
670+
template_body = load_file(
671+
os.path.join(os.path.dirname(__file__), "../../../templates/iam_policy_invalid.yaml")
672+
)
673+
waiter_config = {"Delay": 1, "MaxAttempts": 20}
674+
675+
snapshot.add_transformer(snapshot.transform.key_value("PhysicalResourceId"))
676+
snapshot.add_transformer(
677+
snapshot.transform.key_value("ResourceStatusReason", reference_replacement=False)
678+
)
679+
680+
stack = cfn.create_stack(
681+
StackName=stack_name,
682+
TemplateBody=template_body,
683+
Parameters=[{"ParameterKey": "Name", "ParameterValue": policy_name}],
684+
Capabilities=[Capability.CAPABILITY_NAMED_IAM],
685+
)
686+
stack_id = stack["StackId"]
687+
cleanups.append(lambda: cfn.delete_stack(StackName=stack_id, RetainResources=["BrokenPolicy"]))
688+
with pytest.raises(WaiterError):
689+
cfn.get_waiter("stack_create_complete").wait(StackName=stack_id, WaiterConfig=waiter_config)
690+
stack_post_create = cfn.describe_stacks(StackName=stack_id)
691+
snapshot.match("stack_post_create", stack_post_create)
692+
693+
cfn.delete_stack(StackName=stack_id)
694+
with pytest.raises(WaiterError):
695+
cfn.get_waiter("stack_delete_complete").wait(StackName< 6BB6 /span>=stack_id, WaiterConfig=waiter_config)
696+
stack_post_fail_delete = cfn.describe_stacks(StackName=stack_id)
697+
snapshot.match("stack_post_fail_delete", stack_post_fail_delete)
698+
699+
cfn.delete_stack(StackName=stack_id, RetainResources=["BrokenPolicy"])
700+
cfn.get_waiter("stack_delete_complete").wait(StackName=stack_id, WaiterConfig=waiter_config)
701+
stack_post_success_delete = cfn.describe_stacks(StackName=stack_id)
702+
snapshot.match("stack_post_success_delete", stack_post_success_delete)
703+
stack_events = cfn.describe_stack_events(StackName=stack_id)
704+
snapshot.match("stack_events", stack_events)

0 commit comments

Comments
 (0)
0