Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions pycfmodel/model/cf_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,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.
Expand All @@ -35,11 +36,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] = None
Conditions: Optional[Dict] = {}
Description: Optional[str] = None
Globals: Optional[Dict] = {}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd create a subclass for SAM. Normal CF does not support Globals

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was one of the ideas I had in mind. I'd try to give this PR some development time this week and do a subclass instead for SAM 👍

Mappings: Optional[Dict[str, Dict[str, Dict[str, Any]]]] = {}
Metadata: Optional[Dict[str, Any]] = None
Outputs: Optional[Dict[str, Dict[str, Union[str, Dict]]]] = {}
Expand Down Expand Up @@ -89,14 +93,23 @@ 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)
for key, value in resources.items()
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":
"""
Expand Down Expand Up @@ -139,3 +152,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)
62 changes: 62 additions & 0 deletions tests/test_cf_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,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"}}},
}
)

Expand All @@ -28,6 +77,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():
Expand Down Expand Up @@ -98,3 +148,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 == {}
22 changes: 22 additions & 0 deletions tests/test_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -850,3 +850,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"
Loading