From fdd70e30e8efac2817c869a77dd476cd5e5b943a Mon Sep 17 00:00:00 2001 From: David Hutchison Date: Mon, 23 Dec 2024 12:57:32 +0000 Subject: [PATCH] fix(unit-template): allow generating StackId value #384 --- README.md | 4 +- src/cloud_radar/cf/unit/_template.py | 19 ++++- tests/templates/test_stackid.yaml | 16 ++++ tests/test_cf/test_unit/test_stack_stackid.py | 73 +++++++++++++++++++ 4 files changed, 108 insertions(+), 4 deletions(-) create mode 100644 tests/templates/test_stackid.yaml create mode 100644 tests/test_cf/test_unit/test_stack_stackid.py diff --git a/README.md b/README.md index 02974d31..4c4f4cb4 100644 --- a/README.md +++ b/README.md @@ -167,8 +167,8 @@ The default values for pseudo parameters: | **NoValue** | "" | | **Partition** | "aws" | | Region | "us-east-1" | -| **StackId** | "" | -| **StackName** | "" | +| StackId | (generated based on other values) | +| StackName | "my-cloud-radar-stack" | | **URLSuffix** | "amazonaws.com" | _Note: Bold variables are not fully impletmented yet see the [Roadmap](#roadmap)_ diff --git a/src/cloud_radar/cf/unit/_template.py b/src/cloud_radar/cf/unit/_template.py index c4ccc712..c235316a 100644 --- a/src/cloud_radar/cf/unit/_template.py +++ b/src/cloud_radar/cf/unit/_template.py @@ -2,6 +2,7 @@ import json import re +import uuid from pathlib import Path from typing import Any, Callable, Dict, Generator, List, Optional, Tuple, Union @@ -24,8 +25,8 @@ class Template: NoValue: str = "" # Not yet implemented Partition: str = "aws" # Other regions not implemented Region: str = "us-east-1" - StackId: str = "" # Not yet implemented - StackName: str = "" # Not yet implemented + StackId: str = "" # If left black this will be generated + StackName: str = "my-cloud-radar-stack" URLSuffix: str = "amazonaws.com" # Other regions not implemented def __init__( @@ -304,6 +305,19 @@ def remove_condtional_resources(self, template: Dict[str, Any]) -> Dict[str, Any return template + # If the StackId variable is not set, generate a value for it + def _get_populated_stack_id(self) -> str: + if not Template.StackId: + # Not explicitly set, generate a value + unique_uuid = uuid.uuid4() + + return ( + f"arn:{Template.Partition}:cloudformation:{self.Region}:" + f"{Template.AccountId}:stack/{Template.StackName}/{unique_uuid}" + ) + + return Template.StackId + def create_stack( self, params: Optional[Dict[str, str]] = None, @@ -312,6 +326,7 @@ def create_stack( ): if region: self.Region = region + self.StackId = self._get_populated_stack_id() self.render(params, parameters_file=parameters_file) diff --git a/tests/templates/test_stackid.yaml b/tests/templates/test_stackid.yaml new file mode 100644 index 00000000..bb2364f6 --- /dev/null +++ b/tests/templates/test_stackid.yaml @@ -0,0 +1,16 @@ +AWSTemplateFormatVersion: 2010-09-09 +Description: "Creates an S3 bucket to store logs." + +Resources: + UniqueBucket: + Type: AWS::S3::Bucket + Properties: + BucketName: !Sub + - 'my-test-${stack_region}-${uniqifier}-bucket' + - # AWS::StackId has this format + # arn:aws:cloudformation:us-west-2:123456789012:stack/teststack/51af3dc0-da77-11e4-872e-1234567db123 + # Trying to capture the last piece after the '-' + # As stack name could contain "-"s, split on the "/"s first + uniqifier: !Select [ 4, !Split [ "-", !Select [ 2, !Split [ "/", !Ref AWS::StackId ] ] ] ] + # Usually you would refer to AWS:::Region, but trying to test StackId creation works as expected + stack_region: !Select [ 3, !Split [":", !Ref AWS::StackId]] diff --git a/tests/test_cf/test_unit/test_stack_stackid.py b/tests/test_cf/test_unit/test_stack_stackid.py new file mode 100644 index 00000000..f253cd46 --- /dev/null +++ b/tests/test_cf/test_unit/test_stack_stackid.py @@ -0,0 +1,73 @@ +# Test case that verifies that generation of the value for AWS::StackId works as expected + +from pathlib import Path + +import pytest + +from cloud_radar.cf.unit._template import Template + + +@pytest.fixture +def template(): + template_path = Path(__file__).parent / "../../templates/test_stackid.yaml" + + return Template.from_yaml(template_path.resolve(), {}) + + +def test_function_populated_var(template): + expected_value = "arn:aws:cloudformation:us-west-2:123456789012:stack/teststack/51af3dc0-da77-11e4-872e-1234567db123" + Template.StackId = expected_value + + actual_value = template._get_populated_stack_id() + assert actual_value == expected_value + + +def test_function_blank_var(template): + Template.StackId = "" + + actual_value = template._get_populated_stack_id() + # Check all except the UUID + assert actual_value.startswith( + f"arn:{Template.Partition}:cloudformation:{Template.Region}:{Template.AccountId}:stack/{Template.StackName}/" + ) + + # Check the UUID part looks UUID like + unique_uuid = actual_value.split("/")[2] + assert 5 == len(unique_uuid.split("-")) + + +def test_template_blank_var_stack_region(template): + Template.StackId = "" + + stack = template.create_stack({}, region="eu-west-1") + + bucket = stack.get_resource("UniqueBucket") + bucket_name = bucket.get_property_value("BucketName") + + assert len(bucket_name) == 37 + assert bucket_name[:18] == "my-test-eu-west-1-" + assert bucket_name[30:] == "-bucket" + + +def test_template_blank_var_global_region(template): + Template.StackId = "" + + stack = template.create_stack({}) + + bucket = stack.get_resource("UniqueBucket") + bucket_name = bucket.get_property_value("BucketName") + + assert len(bucket_name) == 37 + assert bucket_name[:18] == "my-test-us-east-1-" + assert bucket_name[30:] == "-bucket" + + +def test_template_populated_var(template): + Template.StackId = "arn:aws:cloudformation:us-west-2:123456789012:stack/teststack/51af3dc0-da77-11e4-872e-1234567db123" + + stack = template.create_stack({}) + + bucket = stack.get_resource("UniqueBucket") + bucket_name = bucket.get_property_value("BucketName") + + assert "my-test-us-west-2-1234567db123-bucket" == bucket_name