Repository: getmoto/moto

Issue:
Cloud formation "depends_on" 
When your cloud formation parser is lazily iterating through the provided resources, I don't think it's taking the depends_on variable into account, which is causing resources to be created in the wrong order, and ends up failing a cloud formation script. The example that I came up with was creating an autoscaling group before the launch configuration was created, even though the autoscaling group has a depends on flag for the launch configuration


Discussion:
@bblommers I actually wouldn't mind working on this because it's something pretty critical to my team's unit testing. Do you have any contribution guidelines? I didn't see any in the README
Your contribution would be great!

Nothing too specific beyond this: https://github.com/spulec/moto/blob/master/CONTRIBUTING.md
Ok great thanks! I’ll try to do this tomorrow then

Resolving patch:
diff --git a/moto/cloudformation/parsing.py b/moto/cloudformation/parsing.py
--- a/moto/cloudformation/parsing.py
+++ b/moto/cloudformation/parsing.py
@@ -98,20 +98,46 @@
     "AWS::Events::Rule": events_models.Rule,
 }
 
+UNDOCUMENTED_NAME_TYPE_MAP = {
+    "AWS::AutoScaling::AutoScalingGroup": "AutoScalingGroupName",
+    "AWS::AutoScaling::LaunchConfiguration": "LaunchConfigurationName",
+    "AWS::IAM::InstanceProfile": "InstanceProfileName",
+}
+
 # http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-name.html
 NAME_TYPE_MAP = {
-    "AWS::CloudWatch::Alarm": "Alarm",
+    "AWS::ApiGateway::ApiKey": "Name",
+    "AWS::ApiGateway::Model": "Name",
+    "AWS::CloudWatch::Alarm": "AlarmName",
     "AWS::DynamoDB::Table": "TableName",
-    "AWS::ElastiCache::CacheCluster": "ClusterName",
     "AWS::ElasticBeanstalk::Application": "ApplicationName",
     "AWS::ElasticBeanstalk::Environment": "EnvironmentName",
+    "AWS::CodeDeploy::Application": "ApplicationName",
+    "AWS::CodeDeploy::DeploymentConfig": "DeploymentConfigName",
+    "AWS::CodeDeploy::DeploymentGroup": "DeploymentGroupName",
+    "AWS::Config::ConfigRule": "ConfigRuleName",
+    "AWS::Config::DeliveryChannel": "Name",
+    "AWS::Config::ConfigurationRecorder": "Name",
     "AWS::ElasticLoadBalancing::LoadBalancer": "LoadBalancerName",
+    "AWS::ElasticLoadBalancingV2::LoadBalancer": "Name",
     "AWS::ElasticLoadBalancingV2::TargetGroup": "Name",
+    "AWS::EC2::SecurityGroup": "GroupName",
+    "AWS::ElastiCache::CacheCluster": "ClusterName",
+    "AWS::ECR::Repository": "RepositoryName",
+    "AWS::ECS::Cluster": "ClusterName",
+    "AWS::Elasticsearch::Domain": "DomainName",
+    "AWS::Events::Rule": "Name",
+    "AWS::IAM::Group": "GroupName",
+    "AWS::IAM::ManagedPolicy": "ManagedPolicyName",
+    "AWS::IAM::Role": "RoleName",
+    "AWS::IAM::User": "UserName",
+    "AWS::Lambda::Function": "FunctionName",
     "AWS::RDS::DBInstance": "DBInstanceIdentifier",
     "AWS::S3::Bucket": "BucketName",
     "AWS::SNS::Topic": "TopicName",
     "AWS::SQS::Queue": "QueueName",
 }
+NAME_TYPE_MAP.update(UNDOCUMENTED_NAME_TYPE_MAP)
 
 # Just ignore these models types for now
 NULL_MODELS = [
@@ -455,6 +481,7 @@ def __getitem__(self, key):
             return self._parsed_resources[resource_logical_id]
         else:
             resource_json = self._resource_json_map.get(resource_logical_id)
+
             if not resource_json:
                 raise KeyError(resource_logical_id)
             new_resource = parse_and_create_resource(
@@ -470,6 +497,34 @@ def __iter__(self):
     def __len__(self):
         return len(self._resource_json_map)
 
+    def __get_resources_in_dependency_order(self):
+        resource_map = copy.deepcopy(self._resource_json_map)
+        resources_in_dependency_order = []
+
+        def recursively_get_dependencies(resource):
+            resource_info = resource_map[resource]
+
+            if "DependsOn" not in resource_info:
+                resources_in_dependency_order.append(resource)
+                del resource_map[resource]
+                return
+
+            dependencies = resource_info["DependsOn"]
+            if isinstance(dependencies, str):  # Dependencies may be a string or list
+                dependencies = [dependencies]
+
+            for dependency in dependencies:
+                if dependency in resource_map:
+                    recursively_get_dependencies(dependency)
+
+            resources_in_dependency_order.append(resource)
+            del resource_map[resource]
+
+        while resource_map:
+            recursively_get_dependencies(list(resource_map.keys())[0])
+
+        return resources_in_dependency_order
+
     @property
     def resources(self):
         return self._resource_json_map.keys()
@@ -547,7 +602,7 @@ def create(self):
                 "aws:cloudformation:stack-id": self.get("AWS::StackId"),
             }
         )
-        for resource in self.resources:
+        for resource in self.__get_resources_in_dependency_order():
             if isinstance(self[resource], ec2_models.TaggedEC2Resource):
                 self.tags["aws:cloudformation:logical-id"] = resource
                 ec2_models.ec2_backends[self._region_name].create_tags(


Test patch:
diff --git a/tests/test_cloudformation/test_cloudformation_depends_on.py b/tests/test_cloudformation/test_cloudformation_depends_on.py
new file mode 100644
--- /dev/null
+++ b/tests/test_cloudformation/test_cloudformation_depends_on.py
@@ -0,0 +1,143 @@
+import boto3
+from moto import mock_cloudformation, mock_ecs, mock_autoscaling, mock_s3
+import json
+
+depends_on_template_list = {
+    "AWSTemplateFormatVersion": "2010-09-09",
+    "Resources": {
+        "ECSCluster": {
+            "Type": "AWS::ECS::Cluster",
+            "Properties": {"ClusterName": "test-cluster"},
+        },
+        "AutoScalingGroup": {
+            "Type": "AWS::AutoScaling::AutoScalingGroup",
+            "Properties": {
+                "AutoScalingGroupName": "test-scaling-group",
+                "DesiredCapacity": 1,
+                "MinSize": 1,
+                "MaxSize": 50,
+                "LaunchConfigurationName": "test-launch-config",
+                "AvailabilityZones": ["us-east-1a"],
+            },
+            "DependsOn": ["ECSCluster", "LaunchConfig"],
+        },
+        "LaunchConfig": {
+            "Type": "AWS::AutoScaling::LaunchConfiguration",
+            "Properties": {"LaunchConfigurationName": "test-launch-config",},
+        },
+    },
+}
+
+depends_on_template_string = {
+    "AWSTemplateFormatVersion": "2010-09-09",
+    "Resources": {
+        "AutoScalingGroup": {
+            "Type": "AWS::AutoScaling::AutoScalingGroup",
+            "Properties": {
+                "AutoScalingGroupName": "test-scaling-group",
+                "DesiredCapacity": 1,
+                "MinSize": 1,
+                "MaxSize": 50,
+                "LaunchConfigurationName": "test-launch-config",
+                "AvailabilityZones": ["us-east-1a"],
+            },
+            "DependsOn": "LaunchConfig",
+        },
+        "LaunchConfig": {
+            "Type": "AWS::AutoScaling::LaunchConfiguration",
+            "Properties": {"LaunchConfigurationName": "test-launch-config",},
+        },
+    },
+}
+
+
+def make_chained_depends_on_template():
+    depends_on_template_linked_dependencies = {
+        "AWSTemplateFormatVersion": "2010-09-09",
+        "Resources": {
+            "Bucket1": {
+                "Type": "AWS::S3::Bucket",
+                "Properties": {"BucketName": "test-bucket-0-us-east-1"},
+            },
+        },
+    }
+
+    for i in range(1, 10):
+        depends_on_template_linked_dependencies["Resources"]["Bucket" + str(i)] = {
+            "Type": "AWS::S3::Bucket",
+            "Properties": {"BucketName": "test-bucket-" + str(i) + "-us-east-1"},
+            "DependsOn": ["Bucket" + str(i - 1)],
+        }
+
+    return json.dumps(depends_on_template_linked_dependencies)
+
+
+depends_on_template_list_json = json.dumps(depends_on_template_list)
+depends_on_template_string_json = json.dumps(depends_on_template_string)
+
+
+@mock_cloudformation
+@mock_autoscaling
+@mock_ecs
+def test_create_stack_with_depends_on():
+    boto3.client("cloudformation", region_name="us-east-1").create_stack(
+        StackName="depends_on_test", TemplateBody=depends_on_template_list_json
+    )
+
+    autoscaling = boto3.client("autoscaling", region_name="us-east-1")
+    autoscaling_group = autoscaling.describe_auto_scaling_groups()["AutoScalingGroups"][
+        0
+    ]
+    assert autoscaling_group["AutoScalingGroupName"] == "test-scaling-group"
+    assert autoscaling_group["DesiredCapacity"] == 1
+    assert autoscaling_group["MinSize"] == 1
+    assert autoscaling_group["MaxSize"] == 50
+    assert autoscaling_group["AvailabilityZones"] == ["us-east-1a"]
+
+    launch_configuration = autoscaling.describe_launch_configurations()[
+        "LaunchConfigurations"
+    ][0]
+    assert launch_configuration["LaunchConfigurationName"] == "test-launch-config"
+
+    ecs = boto3.client("ecs", region_name="us-east-1")
+    cluster_arn = ecs.list_clusters()["clusterArns"][0]
+    assert cluster_arn == "arn:aws:ecs:us-east-1:012345678910:cluster/test-cluster"
+
+
+@mock_cloudformation
+@mock_autoscaling
+def test_create_stack_with_depends_on_string():
+    boto3.client("cloudformation", region_name="us-east-1").create_stack(
+        StackName="depends_on_string_test", TemplateBody=depends_on_template_string_json
+    )
+
+    autoscaling = boto3.client("autoscaling", region_name="us-east-1")
+    autoscaling_group = autoscaling.describe_auto_scaling_groups()["AutoScalingGroups"][
+        0
+    ]
+    assert autoscaling_group["AutoScalingGroupName"] == "test-scaling-group"
+    assert autoscaling_group["DesiredCapacity"] == 1
+    assert autoscaling_group["MinSize"] == 1
+    assert autoscaling_group["MaxSize"] == 50
+    assert autoscaling_group["AvailabilityZones"] == ["us-east-1a"]
+
+    launch_configuration = autoscaling.describe_launch_configurations()[
+        "LaunchConfigurations"
+    ][0]
+    assert launch_configuration["LaunchConfigurationName"] == "test-launch-config"
+
+
+@mock_cloudformation
+@mock_s3
+def test_create_chained_depends_on_stack():
+    boto3.client("cloudformation", region_name="us-east-1").create_stack(
+        StackName="linked_depends_on_test",
+        TemplateBody=make_chained_depends_on_template(),
+    )
+
+    s3 = boto3.client("s3", region_name="us-east-1")
+    bucket_response = s3.list_buckets()["Buckets"]
+
+    assert sorted([bucket["Name"] for bucket in bucket_response]) == [
+        "test-bucket-" + str(i) + "-us-east-1" for i in range(1, 10)
+    ]
diff --git a/tests/test_cloudformation/test_cloudformation_stack_integration.py b/tests/test_cloudformation/test_cloudformation_stack_integration.py
--- a/tests/test_cloudformation/test_cloudformation_stack_integration.py
+++ b/tests/test_cloudformation/test_cloudformation_stack_integration.py
@@ -49,7 +49,7 @@
 from moto.core import ACCOUNT_ID
 from moto.dynamodb2.models import Table
 
-from .fixtures import (
+from tests.test_cloudformation.fixtures import (
     ec2_classic_eip,
     fn_join,
     rds_mysql_with_db_parameter_group,
@@ -940,12 +940,10 @@ def test_iam_roles():
     role_name_to_id = {}
     for role_result in role_results:
         role = iam_conn.get_role(role_result.role_name)
-        if "my-role" not in role.role_name:
+        # Role name is not specified, so randomly generated - can't check exact name
+        if "with-path" in role.role_name:
             role_name_to_id["with-path"] = role.role_id
             role.path.should.equal("my-path")
-            len(role.role_name).should.equal(
-                5
-            )  # Role name is not specified, so randomly generated - can't check exact name
         else:
             role_name_to_id["no-path"] = role.role_id
             role.role_name.should.equal("my-role-no-path-name")