diff --git a/moto/autoscaling/models.py b/moto/autoscaling/models.py index f4185da6c..d82f15095 100644 --- a/moto/autoscaling/models.py +++ b/moto/autoscaling/models.py @@ -6,7 +6,7 @@ from boto.ec2.blockdevicemapping import BlockDeviceType, BlockDeviceMapping from moto.ec2.exceptions import InvalidInstanceIdError from moto.compat import OrderedDict -from moto.core import BaseBackend, BaseModel +from moto.core import BaseBackend, BaseModel, CloudFormationModel from moto.ec2 import ec2_backends from moto.elb import elb_backends from moto.elbv2 import elbv2_backends @@ -74,7 +74,7 @@ class FakeScalingPolicy(BaseModel): ) -class FakeLaunchConfiguration(BaseModel): +class FakeLaunchConfiguration(CloudFormationModel): def __init__( self, name, @@ -127,6 +127,15 @@ class FakeLaunchConfiguration(BaseModel): ) return config + @staticmethod + def cloudformation_name_type(): + return "LaunchConfigurationName" + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-autoscaling-launchconfiguration.html + return "AWS::AutoScaling::LaunchConfiguration" + @classmethod def create_from_cloudformation_json( cls, resource_name, cloudformation_json, region_name @@ -215,7 +224,7 @@ class FakeLaunchConfiguration(BaseModel): return block_device_map -class FakeAutoScalingGroup(BaseModel): +class FakeAutoScalingGroup(CloudFormationModel): def __init__( self, name, @@ -309,6 +318,15 @@ class FakeAutoScalingGroup(BaseModel): tag["PropagateAtLaunch"] = bool_to_string[tag["PropagateAtLaunch"]] return tags + @staticmethod + def cloudformation_name_type(): + return "AutoScalingGroupName" + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-autoscaling-autoscalinggroup.html + return "AWS::AutoScaling::AutoScalingGroup" + @classmethod def create_from_cloudformation_json( cls, resource_name, cloudformation_json, region_name diff --git a/moto/awslambda/models.py b/moto/awslambda/models.py index afbe9775a..a234fbe01 100644 --- a/moto/awslambda/models.py +++ b/moto/awslambda/models.py @@ -28,7 +28,7 @@ import requests.adapters from boto3 import Session from moto.awslambda.policy import Policy -from moto.core import BaseBackend, BaseModel +from moto.core import BaseBackend, CloudFormationModel from moto.core.exceptions import RESTError from moto.iam.models import iam_backend from moto.iam.exceptions import IAMNotFoundException @@ -151,7 +151,7 @@ class _DockerDataVolumeContext: raise # multiple processes trying to use same volume? -class LambdaFunction(BaseModel): +class LambdaFunction(CloudFormationModel): def __init__(self, spec, region, validate_s3=True, version=1): # required self.region = region @@ -492,6 +492,15 @@ class LambdaFunction(BaseModel): return result + @staticmethod + def cloudformation_name_type(): + return "FunctionName" + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-function.html + return "AWS::Lambda::Function" + @classmethod def create_from_cloudformation_json( cls, resource_name, cloudformation_json, region_name @@ -556,7 +565,7 @@ class LambdaFunction(BaseModel): lambda_backends[region].delete_function(self.function_name) -class EventSourceMapping(BaseModel): +class EventSourceMapping(CloudFormationModel): def __init__(self, spec): # required self.function_name = spec["FunctionName"] @@ -633,6 +642,15 @@ class EventSourceMapping(BaseModel): lambda_backend = lambda_backends[region_name] lambda_backend.delete_event_source_mapping(self.uuid) + @staticmethod + def cloudformation_name_type(): + return None + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-eventsourcemapping.html + return "AWS::Lambda::EventSourceMapping" + @classmethod def create_from_cloudformation_json( cls, resource_name, cloudformation_json, region_name @@ -667,13 +685,22 @@ class EventSourceMapping(BaseModel): esm.delete(region_name) -class LambdaVersion(BaseModel): +class LambdaVersion(CloudFormationModel): def __init__(self, spec): self.version = spec["Version"] def __repr__(self): return str(self.logical_resource_id) + @staticmethod + def cloudformation_name_type(): + return None + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-version.html + return "AWS::Lambda::Version" + @classmethod def create_from_cloudformation_json( cls, resource_name, cloudformation_json, region_name diff --git a/moto/batch/models.py b/moto/batch/models.py index fde744911..c4bc81a73 100644 --- a/moto/batch/models.py +++ b/moto/batch/models.py @@ -13,7 +13,7 @@ import threading import dateutil.parser from boto3 import Session -from moto.core import BaseBackend, BaseModel +from moto.core import BaseBackend, BaseModel, CloudFormationModel from moto.iam import iam_backends from moto.ec2 import ec2_backends from moto.ecs import ecs_backends @@ -42,7 +42,7 @@ def datetime2int(date): return int(time.mktime(date.timetuple())) -class ComputeEnvironment(BaseModel): +class ComputeEnvironment(CloudFormationModel): def __init__( self, compute_environment_name, @@ -76,6 +76,15 @@ class ComputeEnvironment(BaseModel): def physical_resource_id(self): return self.arn + @staticmethod + def cloudformation_name_type(): + return "ComputeEnvironmentName" + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-batch-computeenvironment.html + return "AWS::Batch::ComputeEnvironment" + @classmethod def create_from_cloudformation_json( cls, resource_name, cloudformation_json, region_name @@ -95,7 +104,7 @@ class ComputeEnvironment(BaseModel): return backend.get_compute_environment_by_arn(arn) -class JobQueue(BaseModel): +class JobQueue(CloudFormationModel): def __init__( self, name, priority, state, environments, env_order_json, region_name ): @@ -139,6 +148,15 @@ class JobQueue(BaseModel): def physical_resource_id(self): return self.arn + @staticmethod + def cloudformation_name_type(): + return "JobQueueName" + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-batch-jobqueue.html + return "AWS::Batch::JobQueue" + @classmethod def create_from_cloudformation_json( cls, resource_name, cloudformation_json, region_name @@ -164,7 +182,7 @@ class JobQueue(BaseModel): return backend.get_job_queue_by_arn(arn) -class JobDefinition(BaseModel): +class JobDefinition(CloudFormationModel): def __init__( self, name, @@ -264,6 +282,15 @@ class JobDefinition(BaseModel): def physical_resource_id(self): return self.arn + @staticmethod + def cloudformation_name_type(): + return "JobDefinitionName" + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-batch-jobdefinition.html + return "AWS::Batch::JobDefinition" + @classmethod def create_from_cloudformation_json( cls, resource_name, cloudformation_json, region_name diff --git a/moto/cloudformation/parsing.py b/moto/cloudformation/parsing.py index 58409901d..2c212a148 100644 --- a/moto/cloudformation/parsing.py +++ b/moto/cloudformation/parsing.py @@ -6,31 +6,43 @@ import copy import warnings import re -from moto.autoscaling import models as autoscaling_models -from moto.awslambda import models as lambda_models -from moto.batch import models as batch_models -from moto.cloudwatch import models as cloudwatch_models -from moto.cognitoidentity import models as cognitoidentity_models from moto.compat import collections_abc -from moto.datapipeline import models as datapipeline_models -from moto.dynamodb2 import models as dynamodb2_models + +# This ugly section of imports is necessary because we +# build the list of CloudFormationModel subclasses using +# CloudFormationModel.__subclasses__(). However, if the class +# definition of a subclass hasn't been executed yet - for example, if +# the subclass's module hasn't been imported yet - then that subclass +# doesn't exist yet, and __subclasses__ won't find it. +# So we import here to populate the list of subclasses. +from moto.autoscaling import models as autoscaling_models # noqa +from moto.awslambda import models as awslambda_models # noqa +from moto.batch import models as batch_models # noqa +from moto.cloudwatch import models as cloudwatch_models # noqa +from moto.datapipeline import models as datapipeline_models # noqa +from moto.dynamodb2 import models as dynamodb2_models # noqa +from moto.ecr import models as ecr_models # noqa +from moto.ecs import models as ecs_models # noqa +from moto.elb import models as elb_models # noqa +from moto.elbv2 import models as elbv2_models # noqa +from moto.events import models as events_models # noqa +from moto.iam import models as iam_models # noqa +from moto.kinesis import models as kinesis_models # noqa +from moto.kms import models as kms_models # noqa +from moto.rds import models as rds_models # noqa +from moto.rds2 import models as rds2_models # noqa +from moto.redshift import models as redshift_models # noqa +from moto.route53 import models as route53_models # noqa +from moto.s3 import models as s3_models # noqa +from moto.sns import models as sns_models # noqa +from moto.sqs import models as sqs_models # noqa + +# End ugly list of imports + from moto.ec2 import models as ec2_models -from moto.ecs import models as ecs_models -from moto.elb import models as elb_models -from moto.elbv2 import models as elbv2_models -from moto.events import models as events_models -from moto.iam import models as iam_models -from moto.kinesis import models as kinesis_models -from moto.kms import models as kms_models -from moto.rds import models as rds_models -from moto.rds2 import models as rds2_models -from moto.redshift import models as redshift_models -from moto.route53 import models as route53_models -from moto.s3 import models as s3_models, s3_backend +from moto.s3 import models as _, s3_backend # noqa from moto.s3.utils import bucket_and_name_from_url -from moto.sns import models as sns_models -from moto.sqs import models as sqs_models -from moto.core import ACCOUNT_ID +from moto.core import ACCOUNT_ID, CloudFormationModel from .utils import random_suffix from .exceptions import ( ExportNotFound, @@ -40,105 +52,13 @@ from .exceptions import ( ) from boto.cloudformation.stack import Output -MODEL_MAP = { - "AWS::AutoScaling::AutoScalingGroup": autoscaling_models.FakeAutoScalingGroup, - "AWS::AutoScaling::LaunchConfiguration": autoscaling_models.FakeLaunchConfiguration, - "AWS::Batch::JobDefinition": batch_models.JobDefinition, - "AWS::Batch::JobQueue": batch_models.JobQueue, - "AWS::Batch::ComputeEnvironment": batch_models.ComputeEnvironment, - "AWS::DynamoDB::Table": dynamodb2_models.Table, - "AWS::Kinesis::Stream": kinesis_models.Stream, - "AWS::Lambda::EventSourceMapping": lambda_models.EventSourceMapping, - "AWS::Lambda::Function": lambda_models.LambdaFunction, - "AWS::Lambda::Version": lambda_models.LambdaVersion, - "AWS::EC2::EIP": ec2_models.ElasticAddress, - "AWS::EC2::Instance": ec2_models.Instance, - "AWS::EC2::InternetGateway": ec2_models.InternetGateway, - "AWS::EC2::NatGateway": ec2_models.NatGateway, - "AWS::EC2::NetworkInterface": ec2_models.NetworkInterface, - "AWS::EC2::Route": ec2_models.Route, - "AWS::EC2::RouteTable": ec2_models.RouteTable, - "AWS::EC2::SecurityGroup": ec2_models.SecurityGroup, - "AWS::EC2::SecurityGroupIngress": ec2_models.SecurityGroupIngress, - "AWS::EC2::SpotFleet": ec2_models.SpotFleetRequest, - "AWS::EC2::Subnet": ec2_models.Subnet, - "AWS::EC2::SubnetRouteTableAssociation": ec2_models.SubnetRouteTableAssociation, - "AWS::EC2::Volume": ec2_models.Volume, - "AWS::EC2::VolumeAttachment": ec2_models.VolumeAttachment, - "AWS::EC2::VPC": ec2_models.VPC, - "AWS::EC2::VPCGatewayAttachment": ec2_models.VPCGatewayAttachment, - "AWS::EC2::VPCPeeringConnection": ec2_models.VPCPeeringConnection, - "AWS::ECS::Cluster": ecs_models.Cluster, - "AWS::ECS::TaskDefinition": ecs_models.TaskDefinition, - "AWS::ECS::Service": ecs_models.Service, - "AWS::ElasticLoadBalancing::LoadBalancer": elb_models.FakeLoadBalancer, - "AWS::ElasticLoadBalancingV2::LoadBalancer": elbv2_models.FakeLoadBalancer, - "AWS::ElasticLoadBalancingV2::TargetGroup": elbv2_models.FakeTargetGroup, - "AWS::ElasticLoadBalancingV2::Listener": elbv2_models.FakeListener, - "AWS::Cognito::IdentityPool": cognitoidentity_models.CognitoIdentity, - "AWS::DataPipeline::Pipeline": datapipeline_models.Pipeline, - "AWS::IAM::InstanceProfile": iam_models.InstanceProfile, - "AWS::IAM::Role": iam_models.Role, - "AWS::KMS::Key": kms_models.Key, - "AWS::Logs::LogGroup": cloudwatch_models.LogGroup, - "AWS::RDS::DBInstance": rds_models.Database, - "AWS::RDS::DBSecurityGroup": rds_models.SecurityGroup, - "AWS::RDS::DBSubnetGroup": rds_models.SubnetGroup, - "AWS::RDS::DBParameterGroup": rds2_models.DBParameterGroup, - "AWS::Redshift::Cluster": redshift_models.Cluster, - "AWS::Redshift::ClusterParameterGroup": redshift_models.ParameterGroup, - "AWS::Redshift::ClusterSubnetGroup": redshift_models.SubnetGroup, - "AWS::Route53::HealthCheck": route53_models.HealthCheck, - "AWS::Route53::HostedZone": route53_models.FakeZone, - "AWS::Route53::RecordSet": route53_models.RecordSet, - "AWS::Route53::RecordSetGroup": route53_models.RecordSetGroup, - "AWS::SNS::Topic": sns_models.Topic, - "AWS::S3::Bucket": s3_models.FakeBucket, - "AWS::SQS::Queue": sqs_models.Queue, - "AWS::Events::Rule": events_models.Rule, - "AWS::Events::EventBus": events_models.EventBus, -} - -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 +# List of supported CloudFormation models +MODEL_LIST = CloudFormationModel.__subclasses__() +MODEL_MAP = {model.cloudformation_type(): model for model in MODEL_LIST} NAME_TYPE_MAP = { - "AWS::ApiGateway::ApiKey": "Name", - "AWS::ApiGateway::Model": "Name", - "AWS::CloudWatch::Alarm": "AlarmName", - "AWS::DynamoDB::Table": "TableName", - "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", + model.cloudformation_type(): model.cloudformation_name_type() + for model in MODEL_LIST } -NAME_TYPE_MAP.update(UNDOCUMENTED_NAME_TYPE_MAP) # Just ignore these models types for now NULL_MODELS = [ @@ -292,9 +212,11 @@ def clean_json(resource_json, resources_map): def resource_class_from_type(resource_type): if resource_type in NULL_MODELS: return None + if resource_type not in MODEL_MAP: logger.warning("No Moto CloudFormation support for %s", resource_type) return None + return MODEL_MAP.get(resource_type) diff --git a/moto/cloudwatch/models.py b/moto/cloudwatch/models.py index f089acb14..d8b28bc97 100644 --- a/moto/cloudwatch/models.py +++ b/moto/cloudwatch/models.py @@ -3,7 +3,7 @@ import json from boto3 import Session from moto.core.utils import iso_8601_datetime_without_milliseconds -from moto.core import BaseBackend, BaseModel +from moto.core import BaseBackend, BaseModel, CloudFormationModel from moto.core.exceptions import RESTError from moto.logs import logs_backends from datetime import datetime, timedelta @@ -490,13 +490,22 @@ class CloudWatchBackend(BaseBackend): return None, metrics -class LogGroup(BaseModel): +class LogGroup(CloudFormationModel): def __init__(self, spec): # required self.name = spec["LogGroupName"] # optional self.tags = spec.get("Tags", []) + @staticmethod + def cloudformation_name_type(): + return "LogGroupName" + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-logs-loggroup.html + return "AWS::Logs::LogGroup" + @classmethod def create_from_cloudformation_json( cls, resource_name, cloudformation_json, region_name diff --git a/moto/core/__init__.py b/moto/core/__init__.py index 045124fab..09f5b1e16 100644 --- a/moto/core/__init__.py +++ b/moto/core/__init__.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals from .models import BaseModel, BaseBackend, moto_api_backend, ACCOUNT_ID # noqa +from .models import CloudFormationModel # noqa from .responses import ActionAuthenticatorMixin moto_api_backends = {"global": moto_api_backend} diff --git a/moto/core/models.py b/moto/core/models.py index 26ee1a1f5..ded6a4fc1 100644 --- a/moto/core/models.py +++ b/moto/core/models.py @@ -8,6 +8,7 @@ import os import re import six import types +from abc import abstractmethod from io import BytesIO from collections import defaultdict from botocore.config import Config @@ -534,6 +535,47 @@ class BaseModel(object): return instance +# Parent class for every Model that can be instantiated by CloudFormation +# On subclasses, implement the two methods as @staticmethod to ensure correct behaviour of the CF parser +class CloudFormationModel(BaseModel): + @abstractmethod + def cloudformation_name_type(self): + # This must be implemented as a staticmethod with no parameters + # Return None for resources that do not have a name property + pass + + @abstractmethod + def cloudformation_type(self): + # This must be implemented as a staticmethod with no parameters + # See for example https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-dynamodb-table.html + return "AWS::SERVICE::RESOURCE" + + @abstractmethod + def create_from_cloudformation_json(self): + # This must be implemented as a classmethod with parameters: + # cls, resource_name, cloudformation_json, region_name + # Extract the resource parameters from the cloudformation json + # and return an instance of the resource class + pass + + @abstractmethod + def update_from_cloudformation_json(self): + # This must be implemented as a classmethod with parameters: + # cls, original_resource, new_resource_name, cloudformation_json, region_name + # Extract the resource parameters from the cloudformation json, + # delete the old resource and return the new one. Optionally inspect + # the change in parameters and no-op when nothing has changed. + pass + + @abstractmethod + def delete_from_cloudformation_json(self): + # This must be implemented as a classmethod with parameters: + # cls, resource_name, cloudformation_json, region_name + # Extract the resource parameters from the cloudformation json + # and delete the resource. Do not include a return statement. + pass + + class BaseBackend(object): def _reset_model_refs(self): # Remove all references to the models stored diff --git a/moto/datapipeline/models.py b/moto/datapipeline/models.py index d93deea61..b17da1f09 100644 --- a/moto/datapipeline/models.py +++ b/moto/datapipeline/models.py @@ -4,7 +4,7 @@ import datetime from boto3 import Session from moto.compat import OrderedDict -from moto.core import BaseBackend, BaseModel +from moto.core import BaseBackend, BaseModel, CloudFormationModel from .utils import get_random_pipeline_id, remove_capitalization_of_dict_keys @@ -18,7 +18,7 @@ class PipelineObject(BaseModel): return {"fields": self.fields, "id": self.object_id, "name": self.name} -class Pipeline(BaseModel): +class Pipeline(CloudFormationModel): def __init__(self, name, unique_id, **kwargs): self.name = name self.unique_id = unique_id @@ -74,6 +74,15 @@ class Pipeline(BaseModel): def activate(self): self.status = "SCHEDULED" + @staticmethod + def cloudformation_name_type(): + return "Name" + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-datapipeline-pipeline.html + return "AWS::DataPipeline::Pipeline" + @classmethod def create_from_cloudformation_json( cls, resource_name, cloudformation_json, region_name diff --git a/moto/dynamodb/models.py b/moto/dynamodb/models.py index f5771ec6e..1a3b4afce 100644 --- a/moto/dynamodb/models.py +++ b/moto/dynamodb/models.py @@ -4,7 +4,7 @@ import datetime import json from moto.compat import OrderedDict -from moto.core import BaseBackend, BaseModel +from moto.core import BaseBackend, BaseModel, CloudFormationModel from moto.core.utils import unix_time from moto.core import ACCOUNT_ID from .comparisons import get_comparison_func @@ -82,7 +82,7 @@ class Item(BaseModel): return {"Item": included} -class Table(BaseModel): +class Table(CloudFormationModel): def __init__( self, name, @@ -135,6 +135,15 @@ class Table(BaseModel): } return results + @staticmethod + def cloudformation_name_type(): + return "TableName" + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-dynamodb-table.html + return "AWS::DynamoDB::Table" + @classmethod def create_from_cloudformation_json( cls, resource_name, cloudformation_json, region_name diff --git a/moto/dynamodb2/models/__init__.py b/moto/dynamodb2/models/__init__.py index 70fcc5d09..175ed64f8 100644 --- a/moto/dynamodb2/models/__init__.py +++ b/moto/dynamodb2/models/__init__.py @@ -9,7 +9,7 @@ import uuid from boto3 import Session from moto.compat import OrderedDict -from moto.core import BaseBackend, BaseModel +from moto.core import BaseBackend, BaseModel, CloudFormationModel from moto.core.utils import unix_time from moto.core.exceptions import JsonRESTError from moto.dynamodb2.comparisons import get_filter_expression @@ -359,7 +359,7 @@ class GlobalSecondaryIndex(SecondaryIndex): self.throughput = u.get("ProvisionedThroughput", self.throughput) -class Table(BaseModel): +class Table(CloudFormationModel): def __init__( self, table_name, @@ -431,6 +431,15 @@ class Table(BaseModel): def physical_resource_id(self): return self.name + @staticmethod + def cloudformation_name_type(): + return "TableName" + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-dynamodb-table.html + return "AWS::DynamoDB::Table" + @classmethod def create_from_cloudformation_json( cls, resource_name, cloudformation_json, region_name diff --git a/moto/ec2/models.py b/moto/ec2/models.py index ad9ae3b1b..3d60654a9 100644 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -22,7 +22,7 @@ from boto.ec2.launchspecification import LaunchSpecification from moto.compat import OrderedDict from moto.core import BaseBackend -from moto.core.models import Model, BaseModel +from moto.core.models import Model, BaseModel, CloudFormationModel from moto.core.utils import ( iso_8601_datetime_with_milliseconds, camelcase_to_underscores, @@ -219,7 +219,7 @@ class TaggedEC2Resource(BaseModel): raise FilterNotImplementedError(filter_name, method_name) -class NetworkInterface(TaggedEC2Resource): +class NetworkInterface(TaggedEC2Resource, CloudFormationModel): def __init__( self, ec2_backend, @@ -268,6 +268,15 @@ class NetworkInterface(TaggedEC2Resource): if group: self._group_set.append(group) + @staticmethod + def cloudformation_name_type(): + return None + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-networkinterface.html + return "AWS::EC2::NetworkInterface" + @classmethod def create_from_cloudformation_json( cls, resource_name, cloudformation_json, region_name @@ -454,7 +463,7 @@ class NetworkInterfaceBackend(object): return generic_filter(filters, enis) -class Instance(TaggedEC2Resource, BotoInstance): +class Instance(TaggedEC2Resource, BotoInstance, CloudFormationModel): VALID_ATTRIBUTES = { "instanceType", "kernel", @@ -621,6 +630,15 @@ class Instance(TaggedEC2Resource, BotoInstance): formatted_ip, self.region_name ) + @staticmethod + def cloudformation_name_type(): + return None + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-instance.html + return "AWS::EC2::Instance" + @classmethod def create_from_cloudformation_json( cls, resource_name, cloudformation_json, region_name @@ -1843,7 +1861,7 @@ class SecurityRule(object): return True -class SecurityGroup(TaggedEC2Resource): +class SecurityGroup(TaggedEC2Resource, CloudFormationModel): def __init__(self, ec2_backend, group_id, name, description, vpc_id=None): self.ec2_backend = ec2_backend self.id = group_id @@ -1861,6 +1879,15 @@ class SecurityGroup(TaggedEC2Resource): if vpc and len(vpc.get_cidr_block_association_set(ipv6=True)) > 0: self.egress_rules.append(SecurityRule("-1", None, None, [], [])) + @staticmethod + def cloudformation_name_type(): + return "GroupName" + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-securitygroup.html + return "AWS::EC2::SecurityGroup" + @classmethod def create_from_cloudformation_json( cls, resource_name, cloudformation_json, region_name @@ -2260,11 +2287,20 @@ class SecurityGroupBackend(object): raise RulesPerSecurityGroupLimitExceededError -class SecurityGroupIngress(object): +class SecurityGroupIngress(CloudFormationModel): def __init__(self, security_group, properties): self.security_group = security_group self.properties = properties + @staticmethod + def cloudformation_name_type(): + return None + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-securitygroupingress.html + return "AWS::EC2::SecurityGroupIngress" + @classmethod def create_from_cloudformation_json( cls, resource_name, cloudformation_json, region_name @@ -2328,7 +2364,7 @@ class SecurityGroupIngress(object): return cls(security_group, properties) -class VolumeAttachment(object): +class VolumeAttachment(CloudFormationModel): def __init__(self, volume, instance, device, status): self.volume = volume self.attach_time = utc_date_and_time() @@ -2336,6 +2372,15 @@ class VolumeAttachment(object): self.device = device self.status = status + @staticmethod + def cloudformation_name_type(): + return None + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-volumeattachment.html + return "AWS::EC2::VolumeAttachment" + @classmethod def create_from_cloudformation_json( cls, resource_name, cloudformation_json, region_name @@ -2354,7 +2399,7 @@ class VolumeAttachment(object): return attachment -class Volume(TaggedEC2Resource): +class Volume(TaggedEC2Resource, CloudFormationModel): def __init__( self, ec2_backend, volume_id, size, zone, snapshot_id=None, encrypted=False ): @@ -2367,6 +2412,15 @@ class Volume(TaggedEC2Resource): self.ec2_backend = ec2_backend self.encrypted = encrypted + @staticmethod + def cloudformation_name_type(): + return None + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-volume.html + return "AWS::EC2::Volume" + @classmethod def create_from_cloudformation_json( cls, resource_name, cloudformation_json, region_name @@ -2623,7 +2677,7 @@ class EBSBackend(object): return True -class VPC(TaggedEC2Resource): +class VPC(TaggedEC2Resource, CloudFormationModel): def __init__( self, ec2_backend, @@ -2656,6 +2710,15 @@ class VPC(TaggedEC2Resource): amazon_provided_ipv6_cidr_block=amazon_provided_ipv6_cidr_block, ) + @staticmethod + def cloudformation_name_type(): + return None + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-vpc.html + return "AWS::EC2::VPC" + @classmethod def create_from_cloudformation_json( cls, resource_name, cloudformation_json, region_name @@ -3022,13 +3085,22 @@ class VPCPeeringConnectionStatus(object): self.message = "Inactive" -class VPCPeeringConnection(TaggedEC2Resource): +class VPCPeeringConnection(TaggedEC2Resource, CloudFormationModel): def __init__(self, vpc_pcx_id, vpc, peer_vpc): self.id = vpc_pcx_id self.vpc = vpc self.peer_vpc = peer_vpc self._status = VPCPeeringConnectionStatus() + @staticmethod + def cloudformation_name_type(): + return None + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-vpcpeeringconnection.html + return "AWS::EC2::VPCPeeringConnection" + @classmethod def create_from_cloudformation_json( cls, resource_name, cloudformation_json, region_name @@ -3114,7 +3186,7 @@ class VPCPeeringConnectionBackend(object): return vpc_pcx -class Subnet(TaggedEC2Resource): +class Subnet(TaggedEC2Resource, CloudFormationModel): def __init__( self, ec2_backend, @@ -3150,6 +3222,15 @@ class Subnet(TaggedEC2Resource): self._unused_ips = set() # if instance is destroyed hold IP here for reuse self._subnet_ips = {} # has IP: instance + @staticmethod + def cloudformation_name_type(): + return None + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-subnet.html + return "AWS::EC2::Subnet" + @classmethod def create_from_cloudformation_json( cls, resource_name, cloudformation_json, region_name @@ -3377,11 +3458,20 @@ class SubnetBackend(object): raise InvalidParameterValueError(attr_name) -class SubnetRouteTableAssociation(object): +class SubnetRouteTableAssociation(CloudFormationModel): def __init__(self, route_table_id, subnet_id): self.route_table_id = route_table_id self.subnet_id = subnet_id + @staticmethod + def cloudformation_name_type(): + return None + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-subnetroutetableassociation.html + return "AWS::EC2::SubnetRouteTableAssociation" + @classmethod def create_from_cloudformation_json( cls, resource_name, cloudformation_json, region_name @@ -3411,7 +3501,7 @@ class SubnetRouteTableAssociationBackend(object): return subnet_association -class RouteTable(TaggedEC2Resource): +class RouteTable(TaggedEC2Resource, CloudFormationModel): def __init__(self, ec2_backend, route_table_id, vpc_id, main=False): self.ec2_backend = ec2_backend self.id = route_table_id @@ -3420,6 +3510,15 @@ class RouteTable(TaggedEC2Resource): self.associations = {} self.routes = {} + @staticmethod + def cloudformation_name_type(): + return None + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-routetable.html + return "AWS::EC2::RouteTable" + @classmethod def create_from_cloudformation_json( cls, resource_name, cloudformation_json, region_name @@ -3555,7 +3654,7 @@ class RouteTableBackend(object): return self.associate_route_table(route_table_id, subnet_id) -class Route(object): +class Route(CloudFormationModel): def __init__( self, route_table, @@ -3581,6 +3680,15 @@ class Route(object): self.interface = interface self.vpc_pcx = vpc_pcx + @staticmethod + def cloudformation_name_type(): + return None + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-route.html + return "AWS::EC2::Route" + @classmethod def create_from_cloudformation_json( cls, resource_name, cloudformation_json, region_name @@ -3748,12 +3856,21 @@ class RouteBackend(object): return deleted -class InternetGateway(TaggedEC2Resource): +class InternetGateway(TaggedEC2Resource, CloudFormationModel): def __init__(self, ec2_backend): self.ec2_backend = ec2_backend self.id = random_internet_gateway_id() self.vpc = None + @staticmethod + def cloudformation_name_type(): + return None + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-internetgateway.html + return "AWS::EC2::InternetGateway" + @classmethod def create_from_cloudformation_json( cls, resource_name, cloudformation_json, region_name @@ -3826,11 +3943,20 @@ class InternetGatewayBackend(object): return self.describe_internet_gateways(internet_gateway_ids=igw_ids)[0] -class VPCGatewayAttachment(BaseModel): +class VPCGatewayAttachment(CloudFormationModel): def __init__(self, gateway_id, vpc_id): self.gateway_id = gateway_id self.vpc_id = vpc_id + @staticmethod + def cloudformation_name_type(): + return None + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-vpcgatewayattachment.html + return "AWS::EC2::VPCGatewayAttachment" + @classmethod def create_from_cloudformation_json( cls, resource_name, cloudformation_json, region_name @@ -4051,7 +4177,7 @@ class SpotFleetLaunchSpec(object): self.weighted_capacity = float(weighted_capacity) -class SpotFleetRequest(TaggedEC2Resource): +class SpotFleetRequest(TaggedEC2Resource, CloudFormationModel): def __init__( self, ec2_backend, @@ -4100,6 +4226,15 @@ class SpotFleetRequest(TaggedEC2Resource): def physical_resource_id(self): return self.id + @staticmethod + def cloudformation_name_type(): + return None + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-spotfleet.html + return "AWS::EC2::SpotFleet" + @classmethod def create_from_cloudformation_json( cls, resource_name, cloudformation_json, region_name @@ -4323,7 +4458,7 @@ class SpotFleetBackend(object): return True -class ElasticAddress(object): +class ElasticAddress(CloudFormationModel): def __init__(self, domain, address=None): if address: self.public_ip = address @@ -4335,6 +4470,15 @@ class ElasticAddress(object): self.eni = None self.association_id = None + @staticmethod + def cloudformation_name_type(): + return None + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-eip.html + return "AWS::EC2::EIP" + @classmethod def create_from_cloudformation_json( cls, resource_name, cloudformation_json, region_name @@ -5095,7 +5239,7 @@ class CustomerGatewayBackend(object): return deleted -class NatGateway(object): +class NatGateway(CloudFormationModel): def __init__(self, backend, subnet_id, allocation_id): # public properties self.id = random_nat_gateway_id() @@ -5133,6 +5277,15 @@ class NatGateway(object): eips = self._backend.address_by_allocation([self.allocation_id]) return eips[0].public_ip + @staticmethod + def cloudformation_name_type(): + return None + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-natgateway.html + return "AWS::EC2::NatGateway" + @classmethod def create_from_cloudformation_json( cls, resource_name, cloudformation_json, region_name diff --git a/moto/ecr/models.py b/moto/ecr/models.py index 88b058e1e..a1d5aa6e5 100644 --- a/moto/ecr/models.py +++ b/moto/ecr/models.py @@ -7,7 +7,7 @@ from random import random from botocore.exceptions import ParamValidationError -from moto.core import BaseBackend, BaseModel +from moto.core import BaseBackend, BaseModel, CloudFormationModel from moto.ec2 import ec2_backends from moto.ecr.exceptions import ImageNotFoundException, RepositoryNotFoundException @@ -38,7 +38,7 @@ class BaseObject(BaseModel): return self.gen_response_object() -class Repository(BaseObject): +class Repository(BaseObject, CloudFormationModel): def __init__(self, repository_name): self.registry_id = DEFAULT_REGISTRY_ID self.arn = "arn:aws:ecr:us-east-1:{0}:repository/{1}".format( @@ -67,6 +67,15 @@ class Repository(BaseObject): del response_object["arn"], response_object["name"], response_object["images"] return response_object + @staticmethod + def cloudformation_name_type(): + return "RepositoryName" + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ecr-repository.html + return "AWS::ECR::Repository" + @classmethod def create_from_cloudformation_json( cls, resource_name, cloudformation_json, region_name diff --git a/moto/ecs/models.py b/moto/ecs/models.py index a78614cc5..bf20c2245 100644 --- a/moto/ecs/models.py +++ b/moto/ecs/models.py @@ -8,7 +8,7 @@ import pytz from boto3 import Session from moto.core.exceptions import JsonRESTError -from moto.core import BaseBackend, BaseModel +from moto.core import BaseBackend, BaseModel, CloudFormationModel from moto.core.utils import unix_time from moto.ec2 import ec2_backends from copy import copy @@ -44,7 +44,7 @@ class BaseObject(BaseModel): return self.gen_response_object() -class Cluster(BaseObject): +class Cluster(BaseObject, CloudFormationModel): def __init__(self, cluster_name, region_name): self.active_services_count = 0 self.arn = "arn:aws:ecs:{0}:012345678910:cluster/{1}".format( @@ -69,6 +69,15 @@ class Cluster(BaseObject): del response_object["arn"], response_object["name"] return response_object + @staticmethod + def cloudformation_name_type(): + return "ClusterName" + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ecs-cluster.html + return "AWS::ECS::Cluster" + @classmethod def create_from_cloudformation_json( cls, resource_name, cloudformation_json, region_name @@ -116,7 +125,7 @@ class Cluster(BaseObject): raise UnformattedGetAttTemplateException() -class TaskDefinition(BaseObject): +class TaskDefinition(BaseObject, CloudFormationModel): def __init__( self, family, @@ -159,6 +168,15 @@ class TaskDefinition(BaseObject): def physical_resource_id(self): return self.arn + @staticmethod + def cloudformation_name_type(): + return None + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ecs-taskdefinition.html + return "AWS::ECS::TaskDefinition" + @classmethod def create_from_cloudformation_json( cls, resource_name, cloudformation_json, region_name @@ -235,7 +253,7 @@ class Task(BaseObject): return response_object -class Service(BaseObject): +class Service(BaseObject, CloudFormationModel): def __init__( self, cluster, @@ -315,6 +333,15 @@ class Service(BaseObject): return response_object + @staticmethod + def cloudformation_name_type(): + return "ServiceName" + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ecs-service.html + return "AWS::ECS::Service" + @classmethod def create_from_cloudformation_json( cls, resource_name, cloudformation_json, region_name diff --git a/moto/elb/models.py b/moto/elb/models.py index 4991b0754..715758090 100644 --- a/moto/elb/models.py +++ b/moto/elb/models.py @@ -13,7 +13,7 @@ from boto.ec2.elb.attributes import ( ) from boto.ec2.elb.policies import Policies, OtherPolicy from moto.compat import OrderedDict -from moto.core import BaseBackend, BaseModel +from moto.core import BaseBackend, BaseModel, CloudFormationModel from moto.ec2.models import ec2_backends from .exceptions import ( BadHealthCheckDefinition, @@ -69,7 +69,7 @@ class FakeBackend(BaseModel): ) -class FakeLoadBalancer(BaseModel): +class FakeLoadBalancer(CloudFormationModel): def __init__( self, name, @@ -119,6 +119,15 @@ class FakeLoadBalancer(BaseModel): ) self.backends.append(backend) + @staticmethod + def cloudformation_name_type(): + return "LoadBalancerName" + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-elasticloadbalancing-loadbalancer.html + return "AWS::ElasticLoadBalancing::LoadBalancer" + @classmethod def create_from_cloudformation_json( cls, resource_name, cloudformation_json, region_name diff --git a/moto/elbv2/models.py b/moto/elbv2/models.py index a6da0d01c..1deaac9c4 100644 --- a/moto/elbv2/models.py +++ b/moto/elbv2/models.py @@ -6,7 +6,7 @@ from jinja2 import Template from botocore.exceptions import ParamValidationError from moto.compat import OrderedDict from moto.core.exceptions import RESTError -from moto.core import BaseBackend, BaseModel +from moto.core import BaseBackend, BaseModel, CloudFormationModel from moto.core.utils import camelcase_to_underscores, underscores_to_camelcase from moto.ec2.models import ec2_backends from moto.acm.models import acm_backends @@ -50,7 +50,7 @@ class FakeHealthStatus(BaseModel): self.description = description -class FakeTargetGroup(BaseModel): +class FakeTargetGroup(CloudFormationModel): HTTP_CODE_REGEX = re.compile(r"(?:(?:\d+-\d+|\d+),?)+") def __init__( @@ -143,6 +143,15 @@ class FakeTargetGroup(BaseModel): ) return FakeHealthStatus(t["id"], t["port"], self.healthcheck_port, "healthy") + @staticmethod + def cloudformation_name_type(): + return "Name" + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-elasticloadbalancingv2-targetgroup.html + return "AWS::ElasticLoadBalancingV2::TargetGroup" + @classmethod def create_from_cloudformation_json( cls, resource_name, cloudformation_json, region_name @@ -183,7 +192,7 @@ class FakeTargetGroup(BaseModel): return target_group -class FakeListener(BaseModel): +class FakeListener(CloudFormationModel): def __init__( self, load_balancer_arn, @@ -228,6 +237,15 @@ class FakeListener(BaseModel): self._non_default_rules, key=lambda x: x.priority ) + @staticmethod + def cloudformation_name_type(): + return None + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-elasticloadbalancingv2-listener.html + return "AWS::ElasticLoadBalancingV2::Listener" + @classmethod def create_from_cloudformation_json( cls, resource_name, cloudformation_json, region_name @@ -343,7 +361,7 @@ class FakeBackend(BaseModel): ) -class FakeLoadBalancer(BaseModel): +class FakeLoadBalancer(CloudFormationModel): VALID_ATTRS = { "access_logs.s3.enabled", "access_logs.s3.bucket", @@ -402,6 +420,15 @@ class FakeLoadBalancer(BaseModel): """ Not exposed as part of the ELB API - used for CloudFormation. """ elbv2_backends[region].delete_load_balancer(self.arn) + @staticmethod + def cloudformation_name_type(): + return "Name" + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-elasticloadbalancingv2-loadbalancer.html + return "AWS::ElasticLoadBalancingV2::LoadBalancer" + @classmethod def create_from_cloudformation_json( cls, resource_name, cloudformation_json, region_name diff --git a/moto/events/models.py b/moto/events/models.py index d70898198..7fa7d225f 100644 --- a/moto/events/models.py +++ b/moto/events/models.py @@ -4,14 +4,14 @@ import json from boto3 import Session from moto.core.exceptions import JsonRESTError -from moto.core import BaseBackend, BaseModel +from moto.core import BaseBackend, CloudFormationModel from moto.sts.models import ACCOUNT_ID from moto.utilities.tagging_service import TaggingService from uuid import uuid4 -class Rule(BaseModel): +class Rule(CloudFormationModel): def _generate_arn(self, name): return "arn:aws:events:{region_name}:111111111111:rule/{name}".format( region_name=self.region_name, name=name @@ -73,6 +73,15 @@ class Rule(BaseModel): raise UnformattedGetAttTemplateException() + @staticmethod + def cloudformation_name_type(): + return "Name" + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-events-rule.html + return "AWS::Events::Rule" + @classmethod def create_from_cloudformation_json( cls, resource_name, cloudformation_json, region_name @@ -101,7 +110,7 @@ class Rule(BaseModel): event_backend.delete_rule(name=event_name) -class EventBus(BaseModel): +class EventBus(CloudFormationModel): def __init__(self, region_name, name): self.region = region_name self.name = name @@ -152,6 +161,15 @@ class EventBus(BaseModel): raise UnformattedGetAttTemplateException() + @staticmethod + def cloudformation_name_type(): + return "Name" + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-events-eventbus.html + return "AWS::Events::EventBus" + @classmethod def create_from_cloudformation_json( cls, resource_name, cloudformation_json, region_name diff --git a/moto/iam/models.py b/moto/iam/models.py index 49755e57a..16b3ac0ab 100755 --- a/moto/iam/models.py +++ b/moto/iam/models.py @@ -15,7 +15,7 @@ from six.moves.urllib.parse import urlparse from uuid import uuid4 from moto.core.exceptions import RESTError -from moto.core import BaseBackend, BaseModel, ACCOUNT_ID +from moto.core import BaseBackend, BaseModel, ACCOUNT_ID, CloudFormationModel from moto.core.utils import ( iso_8601_datetime_without_milliseconds, iso_8601_datetime_with_milliseconds, @@ -299,7 +299,7 @@ class InlinePolicy(Policy): """TODO: is this needed?""" -class Role(BaseModel): +class Role(CloudFormationModel): def __init__( self, role_id, @@ -327,6 +327,15 @@ class Role(BaseModel): def created_iso_8601(self): return iso_8601_datetime_with_milliseconds(self.create_date) + @staticmethod + def cloudformation_name_type(): + return "RoleName" + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-role.html + return "AWS::IAM::Role" + @classmethod def create_from_cloudformation_json( cls, resource_name, cloudformation_json, region_name @@ -384,7 +393,7 @@ class Role(BaseModel): return [self.tags[tag] for tag in self.tags] -class InstanceProfile(BaseModel): +class InstanceProfile(CloudFormationModel): def __init__(self, instance_profile_id, name, path, roles): self.id = instance_profile_id self.name = name @@ -396,6 +405,15 @@ class InstanceProfile(BaseModel): def created_iso_8601(self): return iso_8601_datetime_with_milliseconds(self.create_date) + @staticmethod + def cloudformation_name_type(): + return "InstanceProfileName" + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-instanceprofile.html + return "AWS::IAM::InstanceProfile" + @classmethod def create_from_cloudformation_json( cls, resource_name, cloudformation_json, region_name diff --git a/moto/kinesis/models.py b/moto/kinesis/models.py index ec9655bfa..c4b04d924 100644 --- a/moto/kinesis/models.py +++ b/moto/kinesis/models.py @@ -12,7 +12,7 @@ from hashlib import md5 from boto3 import Session from moto.compat import OrderedDict -from moto.core import BaseBackend, BaseModel +from moto.core import BaseBackend, BaseModel, CloudFormationModel from moto.core.utils import unix_time from moto.core import ACCOUNT_ID from .exceptions import ( @@ -129,7 +129,7 @@ class Shard(BaseModel): } -class Stream(BaseModel): +class Stream(CloudFormationModel): def __init__(self, stream_name, shard_count, region): self.stream_name = stream_name self.shard_count = shard_count @@ -216,6 +216,15 @@ class Stream(BaseModel): } } + @staticmethod + def cloudformation_name_type(): + return "Name" + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-kinesis-stream.html + return "AWS::Kinesis::Stream" + @classmethod def create_from_cloudformation_json( cls, resource_name, cloudformation_json, region_name diff --git a/moto/kms/models.py b/moto/kms/models.py index 36f72e6de..2eb7cb771 100644 --- a/moto/kms/models.py +++ b/moto/kms/models.py @@ -6,7 +6,7 @@ from datetime import datetime, timedelta from boto3 import Session -from moto.core import BaseBackend, BaseModel +from moto.core import BaseBackend, CloudFormationModel from moto.core.utils import unix_time from moto.utilities.tagging_service import TaggingService from moto.core.exceptions import JsonRESTError @@ -15,7 +15,7 @@ from moto.iam.models import ACCOUNT_ID from .utils import decrypt, encrypt, generate_key_id, generate_master_key -class Key(BaseModel): +class Key(CloudFormationModel): def __init__( self, policy, key_usage, customer_master_key_spec, description, region ): @@ -99,6 +99,15 @@ class Key(BaseModel): def delete(self, region_name): kms_backends[region_name].delete_key(self.id) + @staticmethod + def cloudformation_name_type(): + return None + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-kms-key.html + return "AWS::KMS::Key" + @classmethod def create_from_cloudformation_json( self, resource_name, cloudformation_json, region_name diff --git a/moto/rds/models.py b/moto/rds/models.py index 40b1197b6..440da34d2 100644 --- a/moto/rds/models.py +++ b/moto/rds/models.py @@ -3,14 +3,14 @@ from __future__ import unicode_literals import boto.rds from jinja2 import Template -from moto.core import BaseBackend, BaseModel +from moto.core import BaseBackend, CloudFormationModel from moto.core.utils import get_random_hex from moto.ec2.models import ec2_backends from moto.rds.exceptions import UnformattedGetAttTemplateException from moto.rds2.models import rds2_backends -class Database(BaseModel): +class Database(CloudFormationModel): def get_cfn_attribute(self, attribute_name): if attribute_name == "Endpoint.Address": return self.address @@ -18,13 +18,22 @@ class Database(BaseModel): return self.port raise UnformattedGetAttTemplateException() + @staticmethod + def cloudformation_name_type(): + return "DBInstanceIdentifier" + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rds-dbinstance.html + return "AWS::RDS::DBInstance" + @classmethod def create_from_cloudformation_json( cls, resource_name, cloudformation_json, region_name ): properties = cloudformation_json["Properties"] - db_instance_identifier = properties.get("DBInstanceIdentifier") + db_instance_identifier = properties.get(cls.cloudformation_name_type()) if not db_instance_identifier: db_instance_identifier = resource_name.lower() + get_random_hex(12) db_security_groups = properties.get("DBSecurityGroups") @@ -163,7 +172,7 @@ class Database(BaseModel): backend.delete_database(self.db_instance_identifier) -class SecurityGroup(BaseModel): +class SecurityGroup(CloudFormationModel): def __init__(self, group_name, description): self.group_name = group_name self.description = description @@ -206,6 +215,15 @@ class SecurityGroup(BaseModel): def authorize_security_group(self, security_group): self.ec2_security_groups.append(security_group) + @staticmethod + def cloudformation_name_type(): + return None + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rds-dbsecuritygroup.html + return "AWS::RDS::DBSecurityGroup" + @classmethod def create_from_cloudformation_json( cls, resource_name, cloudformation_json, region_name @@ -239,7 +257,7 @@ class SecurityGroup(BaseModel): backend.delete_security_group(self.group_name) -class SubnetGroup(BaseModel): +class SubnetGroup(CloudFormationModel): def __init__(self, subnet_name, description, subnets): self.subnet_name = subnet_name self.description = description @@ -271,13 +289,23 @@ class SubnetGroup(BaseModel): ) return template.render(subnet_group=self) + @staticmethod + def cloudformation_name_type(): + return "DBSubnetGroupName" + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rds-dbsubnetgroup.html + return "AWS::RDS::DBSubnetGroup" + @classmethod def create_from_cloudformation_json( cls, resource_name, cloudformation_json, region_name ): properties = cloudformation_json["Properties"] - - subnet_name = resource_name.lower() + get_random_hex(12) + subnet_name = properties.get(cls.cloudformation_name_type()) + if not subnet_name: + subnet_name = resource_name.lower() + get_random_hex(12) description = properties["DBSubnetGroupDescription"] subnet_ids = properties["SubnetIds"] tags = properties.get("Tags") diff --git a/moto/rds2/models.py b/moto/rds2/models.py index 7fa4f3316..5f46311ec 100644 --- a/moto/rds2/models.py +++ b/moto/rds2/models.py @@ -9,7 +9,7 @@ from boto3 import Session from jinja2 import Template from re import compile as re_compile from moto.compat import OrderedDict -from moto.core import BaseBackend, BaseModel +from moto.core import BaseBackend, BaseModel, CloudFormationModel from moto.core.utils import get_random_hex from moto.core.utils import iso_8601_datetime_with_milliseconds from moto.ec2.models import ec2_backends @@ -28,7 +28,7 @@ from .exceptions import ( ) -class Database(BaseModel): +class Database(CloudFormationModel): def __init__(self, **kwargs): self.status = "available" self.is_replica = False @@ -356,13 +356,22 @@ class Database(BaseModel): "sqlserver-web": {"gp2": 20, "io1": 100, "standard": 20}, }[engine][storage_type] + @staticmethod + def cloudformation_name_type(): + return "DBInstanceIdentifier" + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rds-dbinstance.html + return "AWS::RDS::DBInstance" + @classmethod def create_from_cloudformation_json( cls, resource_name, cloudformation_json, region_name ): properties = cloudformation_json["Properties"] - db_instance_identifier = properties.get("DBInstanceIdentifier") + db_instance_identifier = properties.get(cls.cloudformation_name_type()) if not db_instance_identifier: db_instance_identifier = resource_name.lower() + get_random_hex(12) db_security_groups = properties.get("DBSecurityGroups") @@ -564,7 +573,7 @@ class Snapshot(BaseModel): self.tags = [tag_set for tag_set in self.tags if tag_set["Key"] not in tag_keys] -class SecurityGroup(BaseModel): +class SecurityGroup(CloudFormationModel): def __init__(self, group_name, description, tags): self.group_name = group_name self.description = description @@ -627,6 +636,15 @@ class SecurityGroup(BaseModel): def authorize_security_group(self, security_group): self.ec2_security_groups.append(security_group) + @staticmethod + def cloudformation_name_type(): + return None + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rds-dbsecuritygroup.html + return "AWS::RDS::DBSecurityGroup" + @classmethod def create_from_cloudformation_json( cls, resource_name, cloudformation_json, region_name @@ -671,7 +689,7 @@ class SecurityGroup(BaseModel): backend.delete_security_group(self.group_name) -class SubnetGroup(BaseModel): +class SubnetGroup(CloudFormationModel): def __init__(self, subnet_name, description, subnets, tags): self.subnet_name = subnet_name self.description = description @@ -726,13 +744,24 @@ class SubnetGroup(BaseModel): ) return template.render(subnet_group=self) + @staticmethod + def cloudformation_name_type(): + return "DBSubnetGroupName" + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rds-dbsubnetgroup.html + return "AWS::RDS::DBSubnetGroup" + @classmethod def create_from_cloudformation_json( cls, resource_name, cloudformation_json, region_name ): properties = cloudformation_json["Properties"] - subnet_name = resource_name.lower() + get_random_hex(12) + subnet_name = properties.get(cls.cloudformation_name_type()) + if not subnet_name: + subnet_name = resource_name.lower() + get_random_hex(12) description = properties["DBSubnetGroupDescription"] subnet_ids = properties["SubnetIds"] tags = properties.get("Tags") @@ -1441,7 +1470,7 @@ class OptionGroupOptionSetting(object): return template.render(option_group_option_setting=self) -class DBParameterGroup(object): +class DBParameterGroup(CloudFormationModel): def __init__(self, name, description, family, tags): self.name = name self.description = description @@ -1480,6 +1509,15 @@ class DBParameterGroup(object): backend = rds2_backends[region_name] backend.delete_db_parameter_group(self.name) + @staticmethod + def cloudformation_name_type(): + return None + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rds-dbparametergroup.html + return "AWS::RDS::DBParameterGroup" + @classmethod def create_from_cloudformation_json( cls, resource_name, cloudformation_json, region_name diff --git a/moto/redshift/models.py b/moto/redshift/models.py index 07baf18c0..0bdb14edc 100644 --- a/moto/redshift/models.py +++ b/moto/redshift/models.py @@ -6,7 +6,7 @@ import datetime from boto3 import Session from botocore.exceptions import ClientError from moto.compat import OrderedDict -from moto.core import BaseBackend, BaseModel +from moto.core import BaseBackend, BaseModel, CloudFormationModel from moto.core.utils import iso_8601_datetime_with_milliseconds from moto.ec2 import ec2_backends from .exceptions import ( @@ -63,7 +63,7 @@ class TaggableResourceMixin(object): return self.tags -class Cluster(TaggableResourceMixin, BaseModel): +class Cluster(TaggableResourceMixin, CloudFormationModel): resource_type = "cluster" @@ -157,6 +157,15 @@ class Cluster(TaggableResourceMixin, BaseModel): self.iam_roles_arn = iam_roles_arn or [] self.restored_from_snapshot = restored_from_snapshot + @staticmethod + def cloudformation_name_type(): + return None + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-redshift-cluster.html + return "AWS::Redshift::Cluster" + @classmethod def create_from_cloudformation_json( cls, resource_name, cloudformation_json, region_name @@ -170,6 +179,7 @@ class Cluster(TaggableResourceMixin, BaseModel): ].cluster_subnet_group_name else: subnet_group_name = None + cluster = redshift_backend.create_cluster( cluster_identifier=resource_name, node_type=properties.get("NodeType"), @@ -321,7 +331,7 @@ class SnapshotCopyGrant(TaggableResourceMixin, BaseModel): } -class SubnetGroup(TaggableResourceMixin, BaseModel): +class SubnetGroup(TaggableResourceMixin, CloudFormationModel): resource_type = "subnetgroup" @@ -342,6 +352,15 @@ class SubnetGroup(TaggableResourceMixin, BaseModel): if not self.subnets: raise InvalidSubnetError(subnet_ids) + @staticmethod + def cloudformation_name_type(): + return None + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-redshift-clustersubnetgroup.html + return "AWS::Redshift::ClusterSubnetGroup" + @classmethod def create_from_cloudformation_json( cls, resource_name, cloudformation_json, region_name @@ -412,7 +431,7 @@ class SecurityGroup(TaggableResourceMixin, BaseModel): } -class ParameterGroup(TaggableResourceMixin, BaseModel): +class ParameterGroup(TaggableResourceMixin, CloudFormationModel): resource_type = "parametergroup" @@ -429,6 +448,15 @@ class ParameterGroup(TaggableResourceMixin, BaseModel): self.group_family = group_family self.description = description + @staticmethod + def cloudformation_name_type(): + return None + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-redshift-clusterparametergroup.html + return "AWS::Redshift::ClusterParameterGroup" + @classmethod def create_from_cloudformation_json( cls, resource_name, cloudformation_json, region_name diff --git a/moto/route53/models.py b/moto/route53/models.py index 0bdefd25b..52f60d971 100644 --- a/moto/route53/models.py +++ b/moto/route53/models.py @@ -7,7 +7,7 @@ import random import uuid from jinja2 import Template -from moto.core import BaseBackend, BaseModel +from moto.core import BaseBackend, CloudFormationModel ROUTE53_ID_CHOICE = string.ascii_uppercase + string.digits @@ -18,7 +18,7 @@ def create_route53_zone_id(): return "".join([random.choice(ROUTE53_ID_CHOICE) for _ in range(0, 15)]) -class HealthCheck(BaseModel): +class HealthCheck(CloudFormationModel): def __init__(self, health_check_id, health_check_args): self.id = health_check_id self.ip_address = health_check_args.get("ip_address") @@ -34,6 +34,15 @@ class HealthCheck(BaseModel): def physical_resource_id(self): return self.id + @staticmethod + def cloudformation_name_type(): + return None + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-route53-healthcheck.html + return "AWS::Route53::HealthCheck" + @classmethod def create_from_cloudformation_json( cls, resource_name, cloudformation_json, region_name @@ -75,7 +84,7 @@ class HealthCheck(BaseModel): return template.render(health_check=self) -class RecordSet(BaseModel): +class RecordSet(CloudFormationModel): def __init__(self, kwargs): self.name = kwargs.get("Name") self.type_ = kwargs.get("Type") @@ -91,6 +100,15 @@ class RecordSet(BaseModel): self.failover = kwargs.get("Failover") self.geo_location = kwargs.get("GeoLocation") + @staticmethod + def cloudformation_name_type(): + return "Name" + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-route53-recordset.html + return "AWS::Route53::RecordSet" + @classmethod def create_from_cloudformation_json( cls, resource_name, cloudformation_json, region_name @@ -202,7 +220,7 @@ def reverse_domain_name(domain_name): return ".".join(reversed(domain_name.split("."))) -class FakeZone(BaseModel): +class FakeZone(CloudFormationModel): def __init__(self, name, id_, private_zone, comment=None): self.name = name self.id = id_ @@ -267,6 +285,15 @@ class FakeZone(BaseModel): def physical_resource_id(self): return self.id + @staticmethod + def cloudformation_name_type(): + return "Name" + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-route53-hostedzone.html + return "AWS::Route53::HostedZone" + @classmethod def create_from_cloudformation_json( cls, resource_name, cloudformation_json, region_name @@ -278,7 +305,7 @@ class FakeZone(BaseModel): return hosted_zone -class RecordSetGroup(BaseModel): +class RecordSetGroup(CloudFormationModel): def __init__(self, hosted_zone_id, record_sets): self.hosted_zone_id = hosted_zone_id self.record_sets = record_sets @@ -287,6 +314,15 @@ class RecordSetGroup(BaseModel): def physical_resource_id(self): return "arn:aws:route53:::hostedzone/{0}".format(self.hosted_zone_id) + @staticmethod + def cloudformation_name_type(): + return None + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-route53-recordsetgroup.html + return "AWS::Route53::RecordSetGroup" + @classmethod def create_from_cloudformation_json( cls, resource_name, cloudformation_json, region_name diff --git a/moto/s3/models.py b/moto/s3/models.py index e5237168e..800601690 100644 --- a/moto/s3/models.py +++ b/moto/s3/models.py @@ -21,7 +21,7 @@ import uuid import six from bisect import insort -from moto.core import ACCOUNT_ID, BaseBackend, BaseModel +from moto.core import ACCOUNT_ID, BaseBackend, BaseModel, CloudFormationModel from moto.core.utils import iso_8601_datetime_without_milliseconds_s3, rfc_1123_datetime from moto.cloudwatch.models import MetricDatum from moto.utilities.tagging_service import TaggingService @@ -763,7 +763,7 @@ class PublicAccessBlock(BaseModel): } -class FakeBucket(BaseModel): +class FakeBucket(CloudFormationModel): def __init__(self, name, region_name): self.name = name self.region_name = region_name @@ -1070,6 +1070,15 @@ class FakeBucket(BaseModel): def physical_resource_id(self): return self.name + @staticmethod + def cloudformation_name_type(): + return "BucketName" + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-s3-bucket.html + return "AWS::S3::Bucket" + @classmethod def create_from_cloudformation_json( cls, resource_name, cloudformation_json, region_name diff --git a/moto/sns/models.py b/moto/sns/models.py index 76376e58f..8a4771a37 100644 --- a/moto/sns/models.py +++ b/moto/sns/models.py @@ -11,7 +11,7 @@ import re from boto3 import Session from moto.compat import OrderedDict -from moto.core import BaseBackend, BaseModel +from moto.core import BaseBackend, BaseModel, CloudFormationModel from moto.core.utils import ( iso_8601_datetime_with_milliseconds, camelcase_to_underscores, @@ -37,7 +37,7 @@ DEFAULT_PAGE_SIZE = 100 MAXIMUM_MESSAGE_LENGTH = 262144 # 256 KiB -class Topic(BaseModel): +class Topic(CloudFormationModel): def __init__(self, name, sns_backend): self.name = name self.sns_backend = sns_backend @@ -87,6 +87,15 @@ class Topic(BaseModel): def policy(self, policy): self._policy_json = json.loads(policy) + @staticmethod + def cloudformation_name_type(): + return "TopicName" + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sns-topic.html + return "AWS::SNS::Topic" + @classmethod def create_from_cloudformation_json( cls, resource_name, cloudformation_json, region_name @@ -94,7 +103,7 @@ class Topic(BaseModel): sns_backend = sns_backends[region_name] properties = cloudformation_json["Properties"] - topic = sns_backend.create_topic(properties.get("TopicName")) + topic = sns_backend.create_topic(properties.get(cls.cloudformation_name_type())) for subscription in properties.get("Subscription", []): sns_backend.subscribe( topic.arn, subscription["Endpoint"], subscription["Protocol"] diff --git a/moto/sqs/models.py b/moto/sqs/models.py index 4befbb50a..a3642c17e 100644 --- a/moto/sqs/models.py +++ b/moto/sqs/models.py @@ -12,7 +12,7 @@ from xml.sax.saxutils import escape from boto3 import Session from moto.core.exceptions import RESTError -from moto.core import BaseBackend, BaseModel +from moto.core import BaseBackend, BaseModel, CloudFormationModel from moto.core.utils import ( camelcase_to_underscores, get_random_message_id, @@ -188,7 +188,7 @@ class Message(BaseModel): return False -class Queue(BaseModel): +class Queue(CloudFormationModel): BASE_ATTRIBUTES = [ "ApproximateNumberOfMessages", "ApproximateNumberOfMessagesDelayed", @@ -354,6 +354,15 @@ class Queue(BaseModel): ), ) + @staticmethod + def cloudformation_name_type(): + return "QueueName" + + @staticmethod + def cloudformation_type(): + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sqs-queue.html + return "AWS::SQS::Queue" + @classmethod def create_from_cloudformation_json( cls, resource_name, cloudformation_json, region_name