From 1cb12f3a3a449556a66770c284974d52c572784c Mon Sep 17 00:00:00 2001 From: Ramon Date: Fri, 24 Nov 2023 14:56:01 +0100 Subject: [PATCH] add globals to model and able to resolve it --- pycfmodel/model/cf_model.py | 25 +++++++++++++-- tests/test_cf_model.py | 62 +++++++++++++++++++++++++++++++++++++ tests/test_resolver.py | 22 +++++++++++++ 3 files changed, 107 insertions(+), 2 deletions(-) diff --git a/pycfmodel/model/cf_model.py b/pycfmodel/model/cf_model.py index 19c59512..734aca05 100644 --- a/pycfmodel/model/cf_model.py +++ b/pycfmodel/model/cf_model.py @@ -18,9 +18,10 @@ class CFModel(CustomModel): Properties: - - AWSTemplateFormatVersion + - AWSTemplateFormatVersion: The AWS CloudFormation template version that the template conforms to. - Conditions: Conditions that control behaviour of the template. - Description: Description for the template. + - Globals: TODO - Mappings: A 3 level mapping of keys and associated values. - Metadata: Additional information about the template. - Outputs: Output values of the template. @@ -30,11 +31,14 @@ class CFModel(CustomModel): - Transform: For serverless applications, specifies the version of the AWS Serverless Application Model (AWS SAM) to use. More info at [AWS Docs](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-anatomy.html) + + More info for Globals at [AWS Globals Docs](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-specification-template-anatomy-globals.html) """ AWSTemplateFormatVersion: Optional[date] Conditions: Optional[Dict] = {} Description: Optional[str] = None + Globals: Optional[Dict] = {} Mappings: Optional[Dict[str, Dict[str, Dict[str, Any]]]] = {} Metadata: Optional[Dict[str, Any]] = None Outputs: Optional[Dict[str, Dict[str, Union[str, Dict]]]] = {} @@ -84,6 +88,12 @@ def resolve(self, extra_params=None) -> "CFModel": {key: _extended_bool(resolve(value, extended_parameters, self.Mappings, resolved_conditions))} ) + globals_cf = dict_value.pop("Globals", {}) + resolved_globals = { + key: resolve(value, extended_parameters, self.Mappings, resolved_conditions) + for key, value in globals_cf.items() + } + resources = dict_value.pop("Resources") resolved_resources = { key: resolve(value, extended_parameters, self.Mappings, resolved_conditions) @@ -91,7 +101,10 @@ def resolve(self, extra_params=None) -> "CFModel": if value.get("Condition") is None or (value.get("Condition") is not None and resolved_conditions.get(value["Condition"], True)) } - return CFModel(**dict_value, Conditions=resolved_conditions, Resources=resolved_resources) + + return CFModel( + **dict_value, Conditions=resolved_conditions, Resources=resolved_resources, Globals=resolved_globals + ) def expand_actions(self) -> "CFModel": """ @@ -134,3 +147,11 @@ def resources_filtered_by_type( if isinstance(resource, allowed_resource_classes) or resource.Type in allowed_types: result[resource_name] = resource return result + + def is_sam_model(self) -> bool: + transform = self.Transform + if self.Transform is None: + return False + if isinstance(transform, str): + transform = [transform] + return any(macro.startswith("AWS::Serverless") for macro in transform) diff --git a/tests/test_cf_model.py b/tests/test_cf_model.py index acccc85f..32a11f45 100644 --- a/tests/test_cf_model.py +++ b/tests/test_cf_model.py @@ -18,6 +18,55 @@ def model(): "Resources": {"Logical ID": {"Type": "Resource type", "Properties": {"foo": "bar"}}}, "Rules": {}, "Outputs": {}, + "Globals": {}, + } + ) + + +@pytest.fixture() +def model_single_transform(): + return CFModel( + **{ + "AWSTemplateFormatVersion": "2012-12-12", + "Description": "", + "Metadata": {}, + "Parameters": {}, + "Mappings": {}, + "Conditions": {}, + "Transform": "AWS::Serverless-2016-10-31", + "Resources": {"Logical ID": {"Type": "Resource type", "Properties": {"foo": "bar"}}}, + "Rules": {}, + "Outputs": {}, + } + ) + + +@pytest.fixture() +def model_no_transform(): + return CFModel( + **{ + "AWSTemplateFormatVersion": "2012-12-12", + "Description": "", + "Metadata": {}, + "Parameters": {}, + "Mappings": {}, + "Conditions": {}, + "Resources": {"Logical ID": {"Type": "Resource type", "Properties": {"foo": "bar"}}}, + "Rules": {}, + "Outputs": {}, + } + ) + + +@pytest.fixture() +def model_with_empty_globals(): + return CFModel( + **{ + "AWSTemplateFormatVersion": "2012-12-12", + "Globals": {}, + "Parameters": {}, + "Transform": "AWS::Serverless-2016-10-31", + "Resources": {"Logical ID": {"Type": "AWS::Dummy::Dummy", "Properties": {"foo": "bar"}}}, } ) @@ -26,6 +75,7 @@ def test_basic_json(model: CFModel): assert type(model).__name__ == "CFModel" assert len(model.Resources) == 1 assert model.Transform == ["MyMacro", "AWS::Serverless"] + assert model.Globals == {} def test_resources_filtered_by_type(): @@ -61,3 +111,15 @@ def test_transform_handles_string(): def test_resolve_model(model): assert model.resolve() == model + + +@pytest.mark.parametrize( + "model_fixture,is_sam_model", + [("model", True), ("model_single_transform", True), ("model_no_transform", False)], +) +def test_transform_is_of_type_sam_model(model_fixture, is_sam_model, request): + assert request.getfixturevalue(model_fixture).is_sam_model() is is_sam_model + + +def test_model_with_empty_globals_is_able_to_resolve_to_empty_dict(model_with_empty_globals): + assert model_with_empty_globals.Globals == {} diff --git a/tests/test_resolver.py b/tests/test_resolver.py index 34f4be2f..db6ad50d 100644 --- a/tests/test_resolver.py +++ b/tests/test_resolver.py @@ -815,3 +815,25 @@ def test_resolve_find_in_map_for_bool_values_in_map(params, expected_resolved_va result = resolve_find_in_map(function_body=function_body, params=params, mappings=mappings, conditions={}) assert result == expected_resolved_value + + +def test_resolve_globals_with_values_and_referencing_parameters(): + """ + This test aims to be able to solve these type of templates: + https://github.com/Skyscanner/cfripper/issues/259#issuecomment-1824485673 + """ + template = { + "AWSTemplateFormatVersion": "2012-12-12", + "Transform": "AWS::Serverless-2016-10-31", + "Parameters": { + "ProjectName": {"Type": "String", "Default": "my-project"}, + "Environment": {"Type": "String", "Default": "development"}, + }, + "Globals": {"Function": {"Tags": {"Env": {"Ref": "Environment"}, "Project": {"Ref": "ProjectName"}}}}, + "Resources": {"MySNSTopic": {"Type": "AWS::SNS::Topic"}}, + } + + model = parse(template).resolve() + + assert model.Globals.get("Function").get("Tags").get("Env") == "development" + assert model.Globals.get("Function").get("Tags").get("Project") == "my-project"