From ea19466c381184a2bbfab946deaf982e33fc850c Mon Sep 17 00:00:00 2001 From: Brian Pandola Date: Mon, 12 Oct 2020 12:53:30 -0700 Subject: [PATCH] Fix missing properties when ecs:TaskDefinition created via CloudFormation (#3378) There's a larger problem here that needs a more generalized solution, but this solves the immediate issue with a minimum amount of code. Closes #3171 --- moto/core/utils.py | 32 +++++++++++++++++++++++ moto/ecs/models.py | 8 +++--- tests/test_ecs/test_ecs_cloudformation.py | 22 +++++++++++++++- 3 files changed, 58 insertions(+), 4 deletions(-) diff --git a/moto/core/utils.py b/moto/core/utils.py index 235b895e..5f35538d 100644 --- a/moto/core/utils.py +++ b/moto/core/utils.py @@ -57,6 +57,11 @@ def underscores_to_camelcase(argument): return result +def pascal_to_camelcase(argument): + """Converts a PascalCase param to the camelCase equivalent""" + return argument[0].lower() + argument[1:] + + def method_names_from_class(clazz): # On Python 2, methods are different from functions, and the `inspect` # predicates distinguish between them. On Python 3, methods are just @@ -367,3 +372,30 @@ def tags_from_cloudformation_tags_list(tags_list): tags[key] = value return tags + + +def remap_nested_keys(root, key_transform): + """This remap ("recursive map") function is used to traverse and + transform the dictionary keys of arbitrarily nested structures. + List comprehensions do not recurse, making it tedious to apply + transforms to all keys in a tree-like structure. + + A common issue for `moto` is changing the casing of dict keys: + + >>> remap_nested_keys({'KeyName': 'Value'}, camelcase_to_underscores) + {'key_name': 'Value'} + + Args: + root: The target data to traverse. Supports iterables like + :class:`list`, :class:`tuple`, and :class:`dict`. + key_transform (callable): This function is called on every + dictionary key found in *root*. + """ + if isinstance(root, (list, tuple)): + return [remap_nested_keys(item, key_transform) for item in root] + if isinstance(root, dict): + return { + key_transform(k): remap_nested_keys(v, key_transform) + for k, v in six.iteritems(root) + } + return root diff --git a/moto/ecs/models.py b/moto/ecs/models.py index 69ed51cb..a4522660 100644 --- a/moto/ecs/models.py +++ b/moto/ecs/models.py @@ -11,7 +11,7 @@ from boto3 import Session from moto.core import BaseBackend, BaseModel, CloudFormationModel from moto.core.exceptions import JsonRESTError -from moto.core.utils import unix_time +from moto.core.utils import unix_time, pascal_to_camelcase, remap_nested_keys from moto.ec2 import ec2_backends from .exceptions import ( ServiceNotFoundException, @@ -174,8 +174,10 @@ class TaskDefinition(BaseObject, CloudFormationModel): family = properties.get( "Family", "task-definition-{0}".format(int(random() * 10 ** 6)) ) - container_definitions = properties["ContainerDefinitions"] - volumes = properties.get("Volumes") + container_definitions = remap_nested_keys( + properties.get("ContainerDefinitions", []), pascal_to_camelcase + ) + volumes = remap_nested_keys(properties.get("Volumes", []), pascal_to_camelcase) ecs_backend = ecs_backends[region_name] return ecs_backend.register_task_definition( diff --git a/tests/test_ecs/test_ecs_cloudformation.py b/tests/test_ecs/test_ecs_cloudformation.py index a34c89aa..fcb1beec 100644 --- a/tests/test_ecs/test_ecs_cloudformation.py +++ b/tests/test_ecs/test_ecs_cloudformation.py @@ -2,6 +2,8 @@ import boto3 import json from copy import deepcopy from moto import mock_cloudformation, mock_ecs +from moto.core.utils import pascal_to_camelcase, remap_nested_keys +import sure # noqa @mock_ecs @@ -231,9 +233,16 @@ def test_create_task_definition_through_cloudformation(): "Cpu": "200", "Memory": "500", "Essential": "true", + "PortMappings": [ + { + "ContainerPort": 123, + "HostPort": 123, + "Protocol": "tcp", + }, + ], } ], - "Volumes": [], + "Volumes": [{"Name": "ecs-vol"}], }, } }, @@ -252,3 +261,14 @@ def test_create_task_definition_through_cloudformation(): StackName=stack_name, LogicalResourceId="testTaskDefinition" )["StackResourceDetail"] task_definition_details["PhysicalResourceId"].should.equal(task_definition_arn) + + task_definition = ecs_conn.describe_task_definition( + taskDefinition=task_definition_arn + ).get("taskDefinition") + expected_properties = remap_nested_keys( + template["Resources"]["testTaskDefinition"]["Properties"], pascal_to_camelcase + ) + task_definition["volumes"].should.equal(expected_properties["volumes"]) + task_definition["containerDefinitions"].should.equal( + expected_properties["containerDefinitions"] + )