CloudFormation: Allow Conditions in Outputs (#7470)
This commit is contained in:
parent
18a392bd0a
commit
e4ac65f7cb
2
.github/workflows/tests_real_aws.yml
vendored
2
.github/workflows/tests_real_aws.yml
vendored
@ -42,4 +42,4 @@ jobs:
|
||||
env:
|
||||
MOTO_TEST_ALLOW_AWS_REQUEST: ${{ true }}
|
||||
run: |
|
||||
pytest -sv tests/test_dynamodb/ tests/test_ec2/ tests/test_iam/ tests/test_lakeformation/ tests/test_logs/ tests/test_sqs/ tests/test_ses/ tests/test_s3* tests/test_sns/ -m aws_verified
|
||||
pytest -sv tests/test_cloudformation/ tests/test_dynamodb/ tests/test_ec2/ tests/test_iam/ tests/test_lakeformation/ tests/test_logs/ tests/test_sqs/ tests/test_ses/ tests/test_s3* tests/test_sns/ -m aws_verified
|
||||
|
@ -475,6 +475,11 @@ def parse_condition(condition: Union[Dict[str, Any], bool], resources_map: "Reso
|
||||
def parse_output(
|
||||
output_logical_id: str, output_json: Any, resources_map: "ResourceMap"
|
||||
) -> Optional[Output]:
|
||||
if "Condition" in output_json and not resources_map.lazy_condition_map.get(
|
||||
output_json["Condition"]
|
||||
):
|
||||
# This Resource is not initialized - impossible to show Output
|
||||
return None
|
||||
output_json = clean_json(output_json, resources_map)
|
||||
if "Value" not in output_json:
|
||||
return None
|
||||
@ -684,11 +689,22 @@ class ResourceMap(collections_abc.Mapping): # type: ignore[type-arg]
|
||||
def validate_outputs(self) -> None:
|
||||
outputs = self._template.get("Outputs") or {}
|
||||
for value in outputs.values():
|
||||
if "Condition" in value:
|
||||
if not self.lazy_condition_map[value["Condition"]]:
|
||||
# This Output is not shown - no point in validating it
|
||||
continue
|
||||
value = value.get("Value", {})
|
||||
if "Fn::GetAtt" in value:
|
||||
resource_type = self._resource_json_map.get(value["Fn::GetAtt"][0])[ # type: ignore[index]
|
||||
"Type"
|
||||
]
|
||||
resource_name = value["Fn::GetAtt"][0]
|
||||
resource = self._resource_json_map.get(resource_name)
|
||||
# validate resource will be created
|
||||
if "Condition" in resource: # type: ignore
|
||||
if not self.lazy_condition_map[resource["Condition"]]: # type: ignore[index]
|
||||
raise ValidationError(
|
||||
message=f"Unresolved resource dependencies [{resource_name}] in the Outputs block of the template"
|
||||
)
|
||||
# Validate attribute exists on this Type
|
||||
resource_type = resource["Type"] # type: ignore[index]
|
||||
attr = value["Fn::GetAtt"][1]
|
||||
resource_class = resource_class_from_type(resource_type)
|
||||
if not resource_class.has_cfn_attr(attr):
|
||||
|
@ -812,6 +812,7 @@ DESCRIBE_STACKS_TEMPLATE = """<DescribeStacksResponse>
|
||||
<NotificationARNs/>
|
||||
{% endif %}
|
||||
<DisableRollback>false</DisableRollback>
|
||||
{%if stack.stack_outputs %}
|
||||
<Outputs>
|
||||
{% for output in stack.stack_outputs %}
|
||||
<member>
|
||||
@ -820,6 +821,7 @@ DESCRIBE_STACKS_TEMPLATE = """<DescribeStacksResponse>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</Outputs>
|
||||
{% endif %}
|
||||
<Parameters>
|
||||
{% for param_name, param_value in stack.stack_parameters.items() %}
|
||||
<member>
|
||||
|
@ -0,0 +1,31 @@
|
||||
import os
|
||||
from functools import wraps
|
||||
|
||||
from moto import mock_aws
|
||||
|
||||
|
||||
def cloudformation_aws_verified():
|
||||
"""
|
||||
Function that is verified to work against AWS.
|
||||
Can be run against AWS at any time by setting:
|
||||
MOTO_TEST_ALLOW_AWS_REQUEST=true
|
||||
|
||||
If this environment variable is not set, the function runs in a `mock_aws` context.
|
||||
"""
|
||||
|
||||
def inner(func):
|
||||
@wraps(func)
|
||||
def pagination_wrapper():
|
||||
allow_aws_request = (
|
||||
os.environ.get("MOTO_TEST_ALLOW_AWS_REQUEST", "false").lower() == "true"
|
||||
)
|
||||
|
||||
if allow_aws_request:
|
||||
return func()
|
||||
else:
|
||||
with mock_aws():
|
||||
return func()
|
||||
|
||||
return pagination_wrapper
|
||||
|
||||
return inner
|
@ -150,7 +150,7 @@ def test_create_custom_lambda_resource__verify_manual_request():
|
||||
)
|
||||
stack_id = stack["StackId"]
|
||||
stack = cf.describe_stacks(StackName=stack_id)["Stacks"][0]
|
||||
assert stack["Outputs"] == []
|
||||
assert "Outputs" not in stack
|
||||
assert stack["StackStatus"] == "CREATE_IN_PROGRESS"
|
||||
|
||||
callback_url = f"http://cloudformation.{region_name}.amazonaws.com/cloudformation_{region_name}/cfnresponse?stack={stack_id}"
|
||||
|
212
tests/test_cloudformation/test_conditions.py
Normal file
212
tests/test_cloudformation/test_conditions.py
Normal file
@ -0,0 +1,212 @@
|
||||
from uuid import uuid4
|
||||
|
||||
import boto3
|
||||
import pytest
|
||||
from botocore.exceptions import ClientError
|
||||
|
||||
from . import cloudformation_aws_verified
|
||||
|
||||
conditional_output_conditional_value = """
|
||||
Outputs:
|
||||
roleArn:
|
||||
Condition: RoleCondition
|
||||
Value: !GetAtt role.Arn
|
||||
|
||||
Parameters:
|
||||
roleName:
|
||||
Type: String
|
||||
Description: Predefined role name
|
||||
|
||||
Conditions:
|
||||
RoleCondition: !Equals [ !Ref roleName, "myrole" ]
|
||||
|
||||
Resources:
|
||||
role:
|
||||
Condition: RoleCondition
|
||||
Type: 'AWS::IAM::Role'
|
||||
Properties:
|
||||
AssumeRolePolicyDocument:
|
||||
Version: 2012-10-17
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Principal:
|
||||
Service:
|
||||
- ec2.amazonaws.com
|
||||
Action:
|
||||
- 'sts:AssumeRole'
|
||||
"""
|
||||
|
||||
|
||||
@pytest.mark.aws_verified
|
||||
@cloudformation_aws_verified()
|
||||
def test_conditional_output_conditional_value():
|
||||
cf = boto3.client("cloudformation", region_name="us-east-1")
|
||||
param = [{"ParameterKey": "roleName", "ParameterValue": "myrole"}]
|
||||
name = f"stack{str(uuid4())[0:6]}"
|
||||
cf.create_stack(
|
||||
StackName=name,
|
||||
TemplateBody=conditional_output_conditional_value,
|
||||
Parameters=param,
|
||||
Capabilities=["CAPABILITY_IAM"],
|
||||
)
|
||||
waiter = cf.get_waiter("stack_create_complete")
|
||||
waiter.wait(StackName=name)
|
||||
|
||||
stack = cf.describe_stacks(StackName=name)["Stacks"][0]
|
||||
assert stack["Outputs"][0]["OutputKey"] == "roleArn"
|
||||
|
||||
cf.delete_stack(StackName=name)
|
||||
|
||||
# verify role does not exist with different name
|
||||
param = [{"ParameterKey": "roleName", "ParameterValue": "diffname"}]
|
||||
cf.create_stack(
|
||||
StackName=f"{name}2",
|
||||
TemplateBody=conditional_output_conditional_value,
|
||||
Parameters=param,
|
||||
Capabilities=["CAPABILITY_IAM"],
|
||||
)
|
||||
waiter = cf.get_waiter("stack_create_complete")
|
||||
waiter.wait(StackName=f"{name}2")
|
||||
stack = cf.describe_stacks()["Stacks"][0]
|
||||
assert "Outputs" not in stack
|
||||
|
||||
cf.delete_stack(StackName=f"{name}2")
|
||||
|
||||
|
||||
permanent_output_conditional_value = """
|
||||
Outputs:
|
||||
roleArn:
|
||||
Value: !GetAtt role.Arn
|
||||
|
||||
Parameters:
|
||||
roleName:
|
||||
Type: String
|
||||
Description: Predefined role name
|
||||
|
||||
Conditions:
|
||||
RoleCondition: !Equals [ !Ref roleName, "myrole" ]
|
||||
|
||||
Resources:
|
||||
role:
|
||||
Condition: RoleCondition
|
||||
Type: 'AWS::IAM::Role'
|
||||
Properties:
|
||||
AssumeRolePolicyDocument:
|
||||
Version: 2012-10-17
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Principal:
|
||||
Service:
|
||||
- ec2.amazonaws.com
|
||||
Action:
|
||||
- 'sts:AssumeRole'
|
||||
"""
|
||||
|
||||
|
||||
@pytest.mark.aws_verified
|
||||
@cloudformation_aws_verified()
|
||||
def test_permanent_output_conditional_value():
|
||||
cf = boto3.client("cloudformation", region_name="us-east-1")
|
||||
param = [{"ParameterKey": "roleName", "ParameterValue": "myrole"}]
|
||||
|
||||
name = f"stack{str(uuid4())[0:6]}"
|
||||
cf.create_stack(
|
||||
StackName=name,
|
||||
TemplateBody=permanent_output_conditional_value,
|
||||
Parameters=param,
|
||||
Capabilities=["CAPABILITY_IAM"],
|
||||
)
|
||||
waiter = cf.get_waiter("stack_create_complete")
|
||||
waiter.wait(StackName=name)
|
||||
|
||||
# Works - because role exists
|
||||
stack = cf.describe_stacks(StackName=name)["Stacks"][0]
|
||||
assert stack["Outputs"][0]["OutputKey"] == "roleArn"
|
||||
|
||||
cf.delete_stack(StackName=name)
|
||||
|
||||
# Role does not exist with different name
|
||||
# So we can't get the Output either
|
||||
param = [{"ParameterKey": "roleName", "ParameterValue": "diffname"}]
|
||||
with pytest.raises(ClientError) as exc:
|
||||
cf.create_stack(
|
||||
StackName=f"{name}2",
|
||||
TemplateBody=permanent_output_conditional_value,
|
||||
Parameters=param,
|
||||
Capabilities=["CAPABILITY_IAM"],
|
||||
)
|
||||
err = exc.value.response["Error"]
|
||||
assert err["Code"] == "ValidationError"
|
||||
assert (
|
||||
err["Message"]
|
||||
== "Unresolved resource dependencies [role] in the Outputs block of the template"
|
||||
)
|
||||
|
||||
|
||||
conditional_output_of_permanent_value = """
|
||||
Outputs:
|
||||
roleArn:
|
||||
Condition: RoleCondition
|
||||
Value: !GetAtt role.Arn
|
||||
|
||||
Parameters:
|
||||
roleName:
|
||||
Type: String
|
||||
Description: Predefined role name
|
||||
|
||||
Conditions:
|
||||
RoleCondition: !Equals [ !Ref roleName, "myrole" ]
|
||||
|
||||
Resources:
|
||||
role:
|
||||
Type: 'AWS::IAM::Role'
|
||||
Properties:
|
||||
AssumeRolePolicyDocument:
|
||||
Version: 2012-10-17
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Principal:
|
||||
Service:
|
||||
- ec2.amazonaws.com
|
||||
Action:
|
||||
- 'sts:AssumeRole'
|
||||
"""
|
||||
|
||||
|
||||
@pytest.mark.aws_verified
|
||||
@cloudformation_aws_verified()
|
||||
def test_conditional_output_of_permanent_value():
|
||||
cf = boto3.client("cloudformation", region_name="us-east-1")
|
||||
param = [{"ParameterKey": "roleName", "ParameterValue": "myrole"}]
|
||||
|
||||
name = f"stack{str(uuid4())[0:6]}"
|
||||
cf.create_stack(
|
||||
StackName=name,
|
||||
TemplateBody=conditional_output_of_permanent_value,
|
||||
Parameters=param,
|
||||
Capabilities=["CAPABILITY_IAM"],
|
||||
)
|
||||
waiter = cf.get_waiter("stack_create_complete")
|
||||
waiter.wait(StackName=name)
|
||||
|
||||
stack = cf.describe_stacks(StackName=name)["Stacks"][0]
|
||||
assert stack["Outputs"][0]["OutputKey"] == "roleArn"
|
||||
|
||||
cf.delete_stack(StackName=name)
|
||||
|
||||
# verify role does not exist with different name
|
||||
param = [{"ParameterKey": "roleName", "ParameterValue": "diffname"}]
|
||||
cf.create_stack(
|
||||
StackName=f"{name}2",
|
||||
TemplateBody=conditional_output_of_permanent_value,
|
||||
Parameters=param,
|
||||
Capabilities=["CAPABILITY_IAM"],
|
||||
)
|
||||
waiter = cf.get_waiter("stack_create_complete")
|
||||
waiter.wait(StackName=f"{name}2")
|
||||
stack = cf.describe_stacks(StackName=f"{name}2")["Stacks"][0]
|
||||
# The Role does exist
|
||||
# But Output is conditional, and just doesn't return anything
|
||||
assert "Outputs" not in stack
|
||||
|
||||
cf.delete_stack(StackName=f"{name}2")
|
Loading…
Reference in New Issue
Block a user