From 18fc92c49fe3aa5ee31c74bf1236422a179efeac Mon Sep 17 00:00:00 2001 From: David Hutchison Date: Sat, 28 Dec 2024 12:07:23 +0000 Subject: [PATCH 1/3] feat(unit-functions): allow GetAtt to use values defined in resource metadata #394 --- src/cloud_radar/cf/unit/functions.py | 13 ++++++- tests/templates/test_media_getatt.yaml | 36 +++++++++++++++++++ .../test_unit/test_functions_get_att.py | 29 +++++++++++++++ 3 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 tests/templates/test_media_getatt.yaml create mode 100644 tests/test_cf/test_unit/test_functions_get_att.py diff --git a/src/cloud_radar/cf/unit/functions.py b/src/cloud_radar/cf/unit/functions.py index 2bb97fb7..e8c06458 100644 --- a/src/cloud_radar/cf/unit/functions.py +++ b/src/cloud_radar/cf/unit/functions.py @@ -455,7 +455,18 @@ def get_att(template: "Template", values: Any) -> str: if resource_name not in template.template["Resources"]: raise KeyError(f"Fn::GetAtt - Resource {resource_name} not found in template.") - return f"{resource_name}.{att_name}" + # Get the resource definition + resource = template.template["Resources"][resource_name] + + # Check if there is a value in the resource Metadata for this attribute. + # If the attribute requested is in the metadata, return it. + # Otherwise use the string value of "{resource_name}.{att_name}" + + metadata = resource.get("Metadata", {}) + cloud_radar_metadata = metadata.get("Cloud-Radar", {}) + attribute_values = cloud_radar_metadata.get("attribute-values", {}) + + return attribute_values.get(att_name, f"{resource_name}.{att_name}") def get_azs(_t: "Template", region: Any) -> List[str]: diff --git a/tests/templates/test_media_getatt.yaml b/tests/templates/test_media_getatt.yaml new file mode 100644 index 00000000..d28d2c71 --- /dev/null +++ b/tests/templates/test_media_getatt.yaml @@ -0,0 +1,36 @@ +AWSTemplateFormatVersion: 2010-09-09 +Description: "Basic template to check GetAtt behaviours" + +Resources: + MediaPackageV2Channel: + Type: AWS::MediaPackageV2::Channel + Metadata: + Cloud-Radar: + attribute-values: + # When unit testing there are no real AWS resources created, and cloud-radar + # does not attempt to realistically generate attribute values - a string is always + # returned. This works good enough most of the time, but there are some cases where + # if you are attempting to apply intrinsic functions against the attribute value + # it needs to be more correct. + # + # In this case, the attribute value is expected to be a List, not a string. + IngestEndpointUrls: + - http://one.example.com + - http://two.example.com + Properties: + ChannelGroupName: dev_video_1 + ChannelName: !Sub ${AWS::StackName}-MediaPackageChannel + +Outputs: + ChannelArn: + Description: The ARN of the MediaPackageV2 Channel. + Value: !GetAtt MediaPackageV2Channel.Arn + ChannelCreatedAt: + Description: The creation timestamp of the MediaPackageV2 Channel. + Value: !GetAtt MediaPackageV2Channel.CreatedAt + ChannelIngestEndpointUrl1: + Description: The first IngestEndpointUrl of the MediaPackageV2 Channel. + Value: !Select [0, !GetAtt MediaPackageV2Channel.IngestEndpointUrls] + ChannelIngestEndpointUrl2: + Description: The second IngestEndpointUrl of the MediaPackageV2 Channel. + Value: !Select [1, !GetAtt MediaPackageV2Channel.IngestEndpointUrls] diff --git a/tests/test_cf/test_unit/test_functions_get_att.py b/tests/test_cf/test_unit/test_functions_get_att.py new file mode 100644 index 00000000..751215e9 --- /dev/null +++ b/tests/test_cf/test_unit/test_functions_get_att.py @@ -0,0 +1,29 @@ +from pathlib import Path + +import pytest + +from cloud_radar.cf.unit._template import Template + +"""Tests that the GetAtt function can use attribute values defined in a template.""" + + +@pytest.fixture +def template(): + template_path = Path(__file__).parent / "../../templates/test_media_getatt.yaml" + + return Template.from_yaml(template_path.resolve(), {}) + + +def test_outputs(template: Template): + stack = template.create_stack() + + # These two outputs are expected to use values which came from the metadata override + stack.get_output("ChannelIngestEndpointUrl1").assert_value_is( + "http://one.example.com" + ) + stack.get_output("ChannelIngestEndpointUrl2").assert_value_is( + "http://two.example.com" + ) + + # This attribute will use the default format + stack.get_output("ChannelArn").assert_value_is("MediaPackageV2Channel.Arn") From b32a966b41908ffe72a2ed5b78d6b5041f191198 Mon Sep 17 00:00:00 2001 From: David Hutchison Date: Sat, 28 Dec 2024 12:09:29 +0000 Subject: [PATCH 2/3] feat(tests): fix unrelated test failure #394 --- tests/test_cf/test_e2e/test_stack.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_cf/test_e2e/test_stack.py b/tests/test_cf/test_e2e/test_stack.py index 6d1b9fc6..6fbab42b 100644 --- a/tests/test_cf/test_e2e/test_stack.py +++ b/tests/test_cf/test_e2e/test_stack.py @@ -27,7 +27,8 @@ def test_constructor(template_dir, default_params): assert stack.config.config.project.regions[0] == "us-east-1" - assert stack.config.config.project.parameters == {} + # Assert either empty or None at the start + assert not stack.config.config.project.parameters stack = Stack(str(template)) From ca0ac457dc109982ce9330eac5400a6313694d83 Mon Sep 17 00:00:00 2001 From: David Hutchison Date: Sat, 28 Dec 2024 12:36:52 +0000 Subject: [PATCH 3/3] feat(docs): add detail on GetAtt using metadata #394 --- README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/README.md b/README.md index cd112158..95715878 100644 --- a/README.md +++ b/README.md @@ -211,6 +211,24 @@ dynamic_references = { template = Template(template_content, dynamic_references=dynamic_references) ``` +There are cases where the default behaviour of our `GetAtt` implementation may not be sufficient and you need a more accurate returned value. When unit testing there are no real AWS resources created, and cloud-radar does not attempt to realistically generate attribute values - a string is always returned. This works good enough most of the time, but there are some cases where if you are attempting to apply intrinsic functions against the attribute value it needs to be more correct. When this occurs, you can add Metadata to the template to provide test values to use. + +``` +Resources: + MediaPackageV2Channel: + Type: AWS::MediaPackageV2::Channel + Metadata: + Cloud-Radar: + attribute-values: + # Default behaviour of a string is not good enough here, the attribute value is expected to be a List. + IngestEndpointUrls: + - http://one.example.com + - http://two.example.com + Properties: + ChannelGroupName: dev_video_1 + ChannelName: !Sub ${AWS::StackName}-MediaPackageChannel +``` + A real unit testing example using Pytest can be seen [here](./tests/test_cf/test_examples/test_unit.py)