commit
a3cd699af8
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -109,7 +109,7 @@ class ExpressionTokenizer(object):
|
||||
|
||||
@classmethod
|
||||
def is_expression_attribute(cls, input_string):
|
||||
return re.compile("^[a-zA-Z][a-zA-Z0-9_]*$").match(input_string) is not None
|
||||
return re.compile("^[a-zA-Z0-9][a-zA-Z0-9_]*$").match(input_string) is not None
|
||||
|
||||
@classmethod
|
||||
def is_expression_attribute_name(cls, input_string):
|
||||
|
@ -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
|
||||
@ -2434,6 +2488,7 @@ class Snapshot(TaggedEC2Resource):
|
||||
self.description = description
|
||||
self.start_time = utc_date_and_time()
|
||||
self.create_volume_permission_groups = set()
|
||||
self.create_volume_permission_userids = set()
|
||||
self.ec2_backend = ec2_backend
|
||||
self.status = "completed"
|
||||
self.encrypted = encrypted
|
||||
@ -2598,32 +2653,36 @@ class EBSBackend(object):
|
||||
snapshot = self.get_snapshot(snapshot_id)
|
||||
return snapshot.create_volume_permission_groups
|
||||
|
||||
def add_create_volume_permission(self, snapshot_id, user_id=None, group=None):
|
||||
if user_id:
|
||||
self.raise_not_implemented_error(
|
||||
"The UserId parameter for ModifySnapshotAttribute"
|
||||
)
|
||||
|
||||
if group != "all":
|
||||
raise InvalidAMIAttributeItemValueError("UserGroup", group)
|
||||
def get_create_volume_permission_userids(self, snapshot_id):
|
||||
snapshot = self.get_snapshot(snapshot_id)
|
||||
snapshot.create_volume_permission_groups.add(group)
|
||||
return snapshot.create_volume_permission_userids
|
||||
|
||||
def add_create_volume_permission(self, snapshot_id, user_ids=None, groups=None):
|
||||
snapshot = self.get_snapshot(snapshot_id)
|
||||
if user_ids:
|
||||
snapshot.create_volume_permission_userids.update(user_ids)
|
||||
|
||||
if groups and groups != ["all"]:
|
||||
raise InvalidAMIAttributeItemValueError("UserGroup", groups)
|
||||
else:
|
||||
snapshot.create_volume_permission_groups.update(groups)
|
||||
|
||||
return True
|
||||
|
||||
def remove_create_volume_permission(self, snapshot_id, user_id=None, group=None):
|
||||
if user_id:
|
||||
self.raise_not_implemented_error(
|
||||
"The UserId parameter for ModifySnapshotAttribute"
|
||||
)
|
||||
|
||||
if group != "all":
|
||||
raise InvalidAMIAttributeItemValueError("UserGroup", group)
|
||||
def remove_create_volume_permission(self, snapshot_id, user_ids=None, groups=None):
|
||||
snapshot = self.get_snapshot(snapshot_id)
|
||||
snapshot.create_volume_permission_groups.discard(group)
|
||||
if user_ids:
|
||||
snapshot.create_volume_permission_userids.difference_update(user_ids)
|
||||
|
||||
if groups and groups != ["all"]:
|
||||
raise InvalidAMIAttributeItemValueError("UserGroup", groups)
|
||||
else:
|
||||
snapshot.create_volume_permission_groups.difference_update(groups)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class VPC(TaggedEC2Resource):
|
||||
class VPC(TaggedEC2Resource, CloudFormationModel):
|
||||
def __init__(
|
||||
self,
|
||||
ec2_backend,
|
||||
@ -2656,6 +2715,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 +3090,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 +3191,7 @@ class VPCPeeringConnectionBackend(object):
|
||||
return vpc_pcx
|
||||
|
||||
|
||||
class Subnet(TaggedEC2Resource):
|
||||
class Subnet(TaggedEC2Resource, CloudFormationModel):
|
||||
def __init__(
|
||||
self,
|
||||
ec2_backend,
|
||||
@ -3150,6 +3227,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 +3463,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 +3506,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 +3515,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 +3659,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 +3685,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 +3861,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 +3948,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 +4182,7 @@ class SpotFleetLaunchSpec(object):
|
||||
self.weighted_capacity = float(weighted_capacity)
|
||||
|
||||
|
||||
class SpotFleetRequest(TaggedEC2Resource):
|
||||
class SpotFleetRequest(TaggedEC2Resource, CloudFormationModel):
|
||||
def __init__(
|
||||
self,
|
||||
ec2_backend,
|
||||
@ -4100,6 +4231,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 +4463,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 +4475,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 +5244,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 +5282,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
|
||||
|
@ -116,22 +116,23 @@ class ElasticBlockStore(BaseResponse):
|
||||
def describe_snapshot_attribute(self):
|
||||
snapshot_id = self._get_param("SnapshotId")
|
||||
groups = self.ec2_backend.get_create_volume_permission_groups(snapshot_id)
|
||||
user_ids = self.ec2_backend.get_create_volume_permission_userids(snapshot_id)
|
||||
template = self.response_template(DESCRIBE_SNAPSHOT_ATTRIBUTES_RESPONSE)
|
||||
return template.render(snapshot_id=snapshot_id, groups=groups)
|
||||
return template.render(snapshot_id=snapshot_id, groups=groups, userIds=user_ids)
|
||||
|
||||
def modify_snapshot_attribute(self):
|
||||
snapshot_id = self._get_param("SnapshotId")
|
||||
operation_type = self._get_param("OperationType")
|
||||
group = self._get_param("UserGroup.1")
|
||||
user_id = self._get_param("UserId.1")
|
||||
groups = self._get_multi_param("UserGroup")
|
||||
user_ids = self._get_multi_param("UserId")
|
||||
if self.is_not_dryrun("ModifySnapshotAttribute"):
|
||||
if operation_type == "add":
|
||||
self.ec2_backend.add_create_volume_permission(
|
||||
snapshot_id, user_id=user_id, group=group
|
||||
snapshot_id, user_ids=user_ids, groups=groups
|
||||
)
|
||||
elif operation_type == "remove":
|
||||
self.ec2_backend.remove_create_volume_permission(
|
||||
snapshot_id, user_id=user_id, group=group
|
||||
snapshot_id, user_ids=user_ids, groups=groups
|
||||
)
|
||||
return MODIFY_SNAPSHOT_ATTRIBUTE_RESPONSE
|
||||
|
||||
@ -311,18 +312,18 @@ DESCRIBE_SNAPSHOT_ATTRIBUTES_RESPONSE = """
|
||||
<DescribeSnapshotAttributeResponse xmlns="http://ec2.amazonaws.com/doc/2013-10-15/">
|
||||
<requestId>a9540c9f-161a-45d8-9cc1-1182b89ad69f</requestId>
|
||||
<snapshotId>snap-a0332ee0</snapshotId>
|
||||
{% if not groups %}
|
||||
<createVolumePermission/>
|
||||
{% endif %}
|
||||
{% if groups %}
|
||||
<createVolumePermission>
|
||||
{% for group in groups %}
|
||||
<item>
|
||||
<group>{{ group }}</group>
|
||||
</item>
|
||||
{% endfor %}
|
||||
</createVolumePermission>
|
||||
{% endif %}
|
||||
<createVolumePermission>
|
||||
{% for group in groups %}
|
||||
<item>
|
||||
<group>{{ group }}</group>
|
||||
</item>
|
||||
{% endfor %}
|
||||
{% for userId in userIds %}
|
||||
<item>
|
||||
<userId>{{ userId }}</userId>
|
||||
</item>
|
||||
{% endfor %}
|
||||
</createVolumePermission>
|
||||
</DescribeSnapshotAttributeResponse>
|
||||
"""
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -2,6 +2,54 @@ from __future__ import unicode_literals
|
||||
from moto.core.exceptions import JsonRESTError
|
||||
|
||||
|
||||
class AccountAlreadyRegisteredException(JsonRESTError):
|
||||
code = 400
|
||||
|
||||
def __init__(self):
|
||||
super(AccountAlreadyRegisteredException, self).__init__(
|
||||
"AccountAlreadyRegisteredException",
|
||||
"The provided account is already a delegated administrator for your organization.",
|
||||
)
|
||||
|
||||
|
||||
class AccountNotRegisteredException(JsonRESTError):
|
||||
code = 400
|
||||
|
||||
def __init__(self):
|
||||
super(AccountNotRegisteredException, self).__init__(
|
||||
"AccountNotRegisteredException",
|
||||
"The provided account is not a registered delegated administrator for your organization.",
|
||||
)
|
||||
|
||||
|
||||
class AccountNotFoundException(JsonRESTError):
|
||||
code = 400
|
||||
|
||||
def __init__(self):
|
||||
super(AccountNotFoundException, self).__init__(
|
||||
"AccountNotFoundException", "You specified an account that doesn't exist."
|
||||
)
|
||||
|
||||
|
||||
class AWSOrganizationsNotInUseException(JsonRESTError):
|
||||
code = 400
|
||||
|
||||
def __init__(self):
|
||||
super(AWSOrganizationsNotInUseException, self).__init__(
|
||||
"AWSOrganizationsNotInUseException",
|
||||
"Your account is not a member of an organization.",
|
||||
)
|
||||
|
||||
|
||||
class ConstraintViolationException(JsonRESTError):
|
||||
code = 400
|
||||
|
||||
def __init__(self, message):
|
||||
super(ConstraintViolationException, self).__init__(
|
||||
"ConstraintViolationException", message
|
||||
)
|
||||
|
||||
|
||||
class InvalidInputException(JsonRESTError):
|
||||
code = 400
|
||||
|
||||
|
@ -4,7 +4,7 @@ import datetime
|
||||
import re
|
||||
import json
|
||||
|
||||
from moto.core import BaseBackend, BaseModel
|
||||
from moto.core import BaseBackend, BaseModel, ACCOUNT_ID
|
||||
from moto.core.exceptions import RESTError
|
||||
from moto.core.utils import unix_time
|
||||
from moto.organizations import utils
|
||||
@ -12,6 +12,11 @@ from moto.organizations.exceptions import (
|
||||
InvalidInputException,
|
||||
DuplicateOrganizationalUnitException,
|
||||
DuplicatePolicyException,
|
||||
AccountNotFoundException,
|
||||
ConstraintViolationException,
|
||||
AccountAlreadyRegisteredException,
|
||||
AWSOrganizationsNotInUseException,
|
||||
AccountNotRegisteredException,
|
||||
)
|
||||
|
||||
|
||||
@ -85,15 +90,13 @@ class FakeAccount(BaseModel):
|
||||
|
||||
def describe(self):
|
||||
return {
|
||||
"Account": {
|
||||
"Id": self.id,
|
||||
"Arn": self.arn,
|
||||
"Email": self.email,
|
||||
"Name": self.name,
|
||||
"Status": self.status,
|
||||
"JoinedMethod": self.joined_method,
|
||||
"JoinedTimestamp": unix_time(self.create_time),
|
||||
}
|
||||
"Id": self.id,
|
||||
"Arn": self.arn,
|
||||
"Email": self.email,
|
||||
"Name": self.name,
|
||||
"Status": self.status,
|
||||
"JoinedMethod": self.joined_method,
|
||||
"JoinedTimestamp": unix_time(self.create_time),
|
||||
}
|
||||
|
||||
|
||||
@ -221,6 +224,56 @@ class FakeServiceAccess(BaseModel):
|
||||
return service_principal in FakeServiceAccess.TRUSTED_SERVICES
|
||||
|
||||
|
||||
class FakeDelegatedAdministrator(BaseModel):
|
||||
# List of services, which support a different Account to ba a delegated administrator
|
||||
# https://docs.aws.amazon.com/organizations/latest/userguide/orgs_integrated-services-list.html
|
||||
SUPPORTED_SERVICES = [
|
||||
"config-multiaccountsetup.amazonaws.com",
|
||||
"guardduty.amazonaws.com",
|
||||
"access-analyzer.amazonaws.com",
|
||||
"macie.amazonaws.com",
|
||||
"servicecatalog.amazonaws.com",
|
||||
"ssm.amazonaws.com",
|
||||
]
|
||||
|
||||
def __init__(self, account):
|
||||
self.account = account
|
||||
self.enabled_date = datetime.datetime.utcnow()
|
||||
self.services = {}
|
||||
|
||||
def add_service_principal(self, service_principal):
|
||||
if service_principal in self.services:
|
||||
raise AccountAlreadyRegisteredException
|
||||
|
||||
if not self.supported_service(service_principal):
|
||||
raise InvalidInputException(
|
||||
"You specified an unrecognized service principal."
|
||||
)
|
||||
|
||||
self.services[service_principal] = {
|
||||
"ServicePrincipal": service_principal,
|
||||
"DelegationEnabledDate": unix_time(datetime.datetime.utcnow()),
|
||||
}
|
||||
|
||||
def remove_service_principal(self, service_principal):
|
||||
if service_principal not in self.services:
|
||||
raise InvalidInputException(
|
||||
"You specified an unrecognized service principal."
|
||||
)
|
||||
|
||||
self.services.pop(service_principal)
|
||||
|
||||
def describe(self):
|
||||
admin = self.account.describe()
|
||||
admin["DelegationEnabledDate"] = unix_time(self.enabled_date)
|
||||
|
||||
return admin
|
||||
|
||||
@staticmethod
|
||||
def supported_service(service_principal):
|
||||
return service_principal in FakeDelegatedAdministrator.SUPPORTED_SERVICES
|
||||
|
||||
|
||||
class OrganizationsBackend(BaseBackend):
|
||||
def __init__(self):
|
||||
self.org = None
|
||||
@ -228,6 +281,7 @@ class OrganizationsBackend(BaseBackend):
|
||||
self.ou = []
|
||||
self.policies = []
|
||||
self.services = []
|
||||
self.admins = []
|
||||
|
||||
def create_organization(self, **kwargs):
|
||||
self.org = FakeOrganization(kwargs["FeatureSet"])
|
||||
@ -259,10 +313,7 @@ class OrganizationsBackend(BaseBackend):
|
||||
|
||||
def describe_organization(self):
|
||||
if not self.org:
|
||||
raise RESTError(
|
||||
"AWSOrganizationsNotInUseException",
|
||||
"Your account is not a member of an organization.",
|
||||
)
|
||||
raise AWSOrganizationsNotInUseException
|
||||
return self.org.describe()
|
||||
|
||||
def list_roots(self):
|
||||
@ -325,10 +376,7 @@ class OrganizationsBackend(BaseBackend):
|
||||
(account for account in self.accounts if account.id == account_id), None
|
||||
)
|
||||
if account is None:
|
||||
raise RESTError(
|
||||
"AccountNotFoundException",
|
||||
"You specified an account that doesn't exist.",
|
||||
)
|
||||
raise AccountNotFoundException
|
||||
return account
|
||||
|
||||
def get_account_by_attr(self, attr, value):
|
||||
@ -341,15 +389,12 @@ class OrganizationsBackend(BaseBackend):
|
||||
None,
|
||||
)
|
||||
if account is None:
|
||||
raise RESTError(
|
||||
"AccountNotFoundException",
|
||||
"You specified an account that doesn't exist.",
|
||||
)
|
||||
raise AccountNotFoundException
|
||||
return account
|
||||
|
||||
def describe_account(self, **kwargs):
|
||||
account = self.get_account_by_id(kwargs["AccountId"])
|
||||
return account.describe()
|
||||
return dict(Account=account.describe())
|
||||
|
||||
def describe_create_account_status(self, **kwargs):
|
||||
account = self.get_account_by_attr(
|
||||
@ -358,15 +403,13 @@ class OrganizationsBackend(BaseBackend):
|
||||
return account.create_account_status
|
||||
|
||||
def list_accounts(self):
|
||||
return dict(
|
||||
Accounts=[account.describe()["Account"] for account in self.accounts]
|
||||
)
|
||||
return dict(Accounts=[account.describe() for account in self.accounts])
|
||||
|
||||
def list_accounts_for_parent(self, **kwargs):
|
||||
parent_id = self.validate_parent_id(kwargs["ParentId"])
|
||||
return dict(
|
||||
Accounts=[
|
||||
account.describe()["Account"]
|
||||
account.describe()
|
||||
for account in self.accounts
|
||||
if account.parent_id == parent_id
|
||||
]
|
||||
@ -399,7 +442,7 @@ class OrganizationsBackend(BaseBackend):
|
||||
elif kwargs["ChildType"] == "ORGANIZATIONAL_UNIT":
|
||||
obj_list = self.ou
|
||||
else:
|
||||
raise RESTError("InvalidInputException", "You specified an invalid value.")
|
||||
raise InvalidInputException("You specified an invalid value.")
|
||||
return dict(
|
||||
Children=[
|
||||
{"Id": obj.id, "Type": kwargs["ChildType"]}
|
||||
@ -427,7 +470,7 @@ class OrganizationsBackend(BaseBackend):
|
||||
"You specified a policy that doesn't exist.",
|
||||
)
|
||||
else:
|
||||
raise RESTError("InvalidInputException", "You specified an invalid value.")
|
||||
raise InvalidInputException("You specified an invalid value.")
|
||||
return policy.describe()
|
||||
|
||||
def get_policy_by_id(self, policy_id):
|
||||
@ -472,12 +515,9 @@ class OrganizationsBackend(BaseBackend):
|
||||
account.attached_policies.append(policy)
|
||||
policy.attachments.append(account)
|
||||
else:
|
||||
raise RESTError(
|
||||
"AccountNotFoundException",
|
||||
"You specified an account that doesn't exist.",
|
||||
)
|
||||
raise AccountNotFoundException
|
||||
else:
|
||||
raise RESTError("InvalidInputException", "You specified an invalid value.")
|
||||
raise InvalidInputException("You specified an invalid value.")
|
||||
|
||||
def list_policies(self, **kwargs):
|
||||
return dict(
|
||||
@ -510,12 +550,9 @@ class OrganizationsBackend(BaseBackend):
|
||||
elif re.compile(utils.ACCOUNT_ID_REGEX).match(kwargs["TargetId"]):
|
||||
obj = next((a for a in self.accounts if a.id == kwargs["TargetId"]), None)
|
||||
if obj is None:
|
||||
raise RESTError(
|
||||
"AccountNotFoundException",
|
||||
"You specified an account that doesn't exist.",
|
||||
)
|
||||
raise AccountNotFoundException
|
||||
else:
|
||||
raise RESTError("InvalidInputException", "You specified an invalid value.")
|
||||
raise InvalidInputException("You specified an invalid value.")
|
||||
return dict(
|
||||
Policies=[
|
||||
p.describe()["Policy"]["PolicySummary"] for p in obj.attached_policies
|
||||
@ -533,7 +570,7 @@ class OrganizationsBackend(BaseBackend):
|
||||
"You specified a policy that doesn't exist.",
|
||||
)
|
||||
else:
|
||||
raise RESTError("InvalidInputException", "You specified an invalid value.")
|
||||
raise InvalidInputException("You specified an invalid value.")
|
||||
objects = [
|
||||
{"TargetId": obj.id, "Arn": obj.arn, "Name": obj.name, "Type": obj.type}
|
||||
for obj in policy.attachments
|
||||
@ -606,5 +643,95 @@ class OrganizationsBackend(BaseBackend):
|
||||
if service_principal:
|
||||
self.services.remove(service_principal)
|
||||
|
||||
def register_delegated_administrator(self, **kwargs):
|
||||
account_id = kwargs["AccountId"]
|
||||
|
||||
if account_id == ACCOUNT_ID:
|
||||
raise ConstraintViolationException(
|
||||
"You cannot register master account/yourself as delegated administrator for your organization."
|
||||
)
|
||||
|
||||
account = self.get_account_by_id(account_id)
|
||||
|
||||
admin = next(
|
||||
(admin for admin in self.admins if admin.account.id == account_id), None
|
||||
)
|
||||
if admin is None:
|
||||
admin = FakeDelegatedAdministrator(account)
|
||||
self.admins.append(admin)
|
||||
|
||||
admin.add_service_principal(kwargs["ServicePrincipal"])
|
||||
|
||||
def list_delegated_administrators(self, **kwargs):
|
||||
admins = self.admins
|
||||
service = kwargs.get("ServicePrincipal")
|
||||
|
||||
if service:
|
||||
if not FakeDelegatedAdministrator.supported_service(service):
|
||||
raise InvalidInputException(
|
||||
"You specified an unrecognized service principal."
|
||||
)
|
||||
|
||||
admins = [admin for admin in admins if service in admin.services]
|
||||
|
||||
delegated_admins = [admin.describe() for admin in admins]
|
||||
|
||||
return dict(DelegatedAdministrators=delegated_admins)
|
||||
|
||||
def list_delegated_services_for_account(self, **kwargs):
|
||||
admin = next(
|
||||
(admin for admin in self.admins if admin.account.id == kwargs["AccountId"]),
|
||||
None,
|
||||
)
|
||||
if admin is None:
|
||||
account = next(
|
||||
(
|
||||
account
|
||||
for account in self.accounts
|
||||
if account.id == kwargs["AccountId"]
|
||||
),
|
||||
None,
|
||||
)
|
||||
if account:
|
||||
raise AccountNotRegisteredException
|
||||
|
||||
raise AWSOrganizationsNotInUseException
|
||||
|
||||
services = [service for service in admin.services.values()]
|
||||
|
||||
return dict(DelegatedServices=services)
|
||||
|
||||
def deregister_delegated_administrator(self, **kwargs):
|
||||
account_id = kwargs["AccountId"]
|
||||
service = kwargs["ServicePrincipal"]
|
||||
|
||||
if account_id == ACCOUNT_ID:
|
||||
raise ConstraintViolationException(
|
||||
"You cannot register master account/yourself as delegated administrator for your organization."
|
||||
)
|
||||
|
||||
admin = next(
|
||||
(admin for admin in self.admins if admin.account.id == account_id), None,
|
||||
)
|
||||
if admin is None:
|
||||
account = next(
|
||||
(
|
||||
account
|
||||
for account in self.accounts
|
||||
if account.id == kwargs["AccountId"]
|
||||
),
|
||||
None,
|
||||
)
|
||||
if account:
|
||||
raise AccountNotRegisteredException
|
||||
|
||||
raise AccountNotFoundException
|
||||
|
||||
admin.remove_service_principal(service)
|
||||
|
||||
# remove account, when no services attached
|
||||
if not admin.services:
|
||||
self.admins.remove(admin)
|
||||
|
||||
|
||||
organizations_backend = OrganizationsBackend()
|
||||
|
@ -163,3 +163,31 @@ class OrganizationsResponse(BaseResponse):
|
||||
return json.dumps(
|
||||
self.organizations_backend.disable_aws_service_access(**self.request_params)
|
||||
)
|
||||
|
||||
def register_delegated_administrator(self):
|
||||
return json.dumps(
|
||||
self.organizations_backend.register_delegated_administrator(
|
||||
**self.request_params
|
||||
)
|
||||
)
|
||||
|
||||
def list_delegated_administrators(self):
|
||||
return json.dumps(
|
||||
self.organizations_backend.list_delegated_administrators(
|
||||
**self.request_params
|
||||
)
|
||||
)
|
||||
|
||||
def list_delegated_services_for_account(self):
|
||||
return json.dumps(
|
||||
self.organizations_backend.list_delegated_services_for_account(
|
||||
**self.request_params
|
||||
)
|
||||
)
|
||||
|
||||
def deregister_delegated_administrator(self):
|
||||
return json.dumps(
|
||||
self.organizations_backend.deregister_delegated_administrator(
|
||||
**self.request_params
|
||||
)
|
||||
)
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -57,3 +57,8 @@ class InvalidRequestException(SecretsManagerClientError):
|
||||
super(InvalidRequestException, self).__init__(
|
||||
"InvalidRequestException", message
|
||||
)
|
||||
|
||||
|
||||
class ValidationException(SecretsManagerClientError):
|
||||
def __init__(self, message):
|
||||
super(ValidationException, self).__init__("ValidationException", message)
|
||||
|
0
moto/secretsmanager/list_secrets/__init__.py
Normal file
0
moto/secretsmanager/list_secrets/__init__.py
Normal file
44
moto/secretsmanager/list_secrets/filters.py
Normal file
44
moto/secretsmanager/list_secrets/filters.py
Normal file
@ -0,0 +1,44 @@
|
||||
def _matcher(pattern, str):
|
||||
for word in pattern.split(" "):
|
||||
if word not in str:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def name(secret, names):
|
||||
for n in names:
|
||||
if _matcher(n, secret["name"]):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def description(secret, descriptions):
|
||||
for d in descriptions:
|
||||
if _matcher(d, secret["description"]):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def tag_key(secret, tag_keys):
|
||||
for k in tag_keys:
|
||||
for tag in secret["tags"]:
|
||||
if _matcher(k, tag["Key"]):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def tag_value(secret, tag_values):
|
||||
for v in tag_values:
|
||||
for tag in secret["tags"]:
|
||||
if _matcher(v, tag["Value"]):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def all(secret, values):
|
||||
return (
|
||||
name(secret, values)
|
||||
or description(secret, values)
|
||||
or tag_key(secret, values)
|
||||
or tag_value(secret, values)
|
||||
)
|
@ -18,6 +18,31 @@ from .exceptions import (
|
||||
ClientError,
|
||||
)
|
||||
from .utils import random_password, secret_arn, get_secret_name_from_arn
|
||||
from .list_secrets.filters import all, tag_key, tag_value, description, name
|
||||
|
||||
|
||||
_filter_functions = {
|
||||
"all": all,
|
||||
"name": name,
|
||||
"description": description,
|
||||
"tag-key": tag_key,
|
||||
"tag-value": tag_value,
|
||||
}
|
||||
|
||||
|
||||
def filter_keys():
|
||||
return list(_filter_functions.keys())
|
||||
|
||||
|
||||
def _matches(secret, filters):
|
||||
is_match = True
|
||||
|
||||
for f in filters:
|
||||
# Filter names are pre-validated in the resource layer
|
||||
filter_function = _filter_functions.get(f["Key"])
|
||||
is_match = is_match and filter_function(secret, f["Values"])
|
||||
|
||||
return is_match
|
||||
|
||||
|
||||
class SecretsManager(BaseModel):
|
||||
@ -442,35 +467,35 @@ class SecretsManagerBackend(BaseBackend):
|
||||
|
||||
return response
|
||||
|
||||
def list_secrets(self, max_results, next_token):
|
||||
def list_secrets(self, filters, max_results, next_token):
|
||||
# TODO implement pagination and limits
|
||||
|
||||
secret_list = []
|
||||
for secret in self.secrets.values():
|
||||
if _matches(secret, filters):
|
||||
versions_to_stages = {}
|
||||
for version_id, version in secret["versions"].items():
|
||||
versions_to_stages[version_id] = version["version_stages"]
|
||||
|
||||
versions_to_stages = {}
|
||||
for version_id, version in secret["versions"].items():
|
||||
versions_to_stages[version_id] = version["version_stages"]
|
||||
|
||||
secret_list.append(
|
||||
{
|
||||
"ARN": secret_arn(self.region, secret["secret_id"]),
|
||||
"DeletedDate": secret.get("deleted_date", None),
|
||||
"Description": secret.get("description", ""),
|
||||
"KmsKeyId": "",
|
||||
"LastAccessedDate": None,
|
||||
"LastChangedDate": None,
|
||||
"LastRotatedDate": None,
|
||||
"Name": secret["name"],
|
||||
"RotationEnabled": secret["rotation_enabled"],
|
||||
"RotationLambdaARN": secret["rotation_lambda_arn"],
|
||||
"RotationRules": {
|
||||
"AutomaticallyAfterDays": secret["auto_rotate_after_days"]
|
||||
},
|
||||
"SecretVersionsToStages": versions_to_stages,
|
||||
"Tags": secret["tags"],
|
||||
}
|
||||
)
|
||||
secret_list.append(
|
||||
{
|
||||
"ARN": secret_arn(self.region, secret["secret_id"]),
|
||||
"DeletedDate": secret.get("deleted_date", None),
|
||||
"Description": secret.get("description", ""),
|
||||
"KmsKeyId": "",
|
||||
"LastAccessedDate": None,
|
||||
"LastChangedDate": None,
|
||||
"LastRotatedDate": None,
|
||||
"Name": secret["name"],
|
||||
"RotationEnabled": secret["rotation_enabled"],
|
||||
"RotationLambdaARN": secret["rotation_lambda_arn"],
|
||||
"RotationRules": {
|
||||
"AutomaticallyAfterDays": secret["auto_rotate_after_days"]
|
||||
},
|
||||
"SecretVersionsToStages": versions_to_stages,
|
||||
"Tags": secret["tags"],
|
||||
}
|
||||
)
|
||||
|
||||
return secret_list, None
|
||||
|
||||
|
@ -1,13 +1,36 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from moto.core.responses import BaseResponse
|
||||
from moto.secretsmanager.exceptions import InvalidRequestException
|
||||
from moto.secretsmanager.exceptions import (
|
||||
InvalidRequestException,
|
||||
InvalidParameterException,
|
||||
ValidationException,
|
||||
)
|
||||
|
||||
from .models import secretsmanager_backends
|
||||
from .models import secretsmanager_backends, filter_keys
|
||||
|
||||
import json
|
||||
|
||||
|
||||
def _validate_filters(filters):
|
||||
for idx, f in enumerate(filters):
|
||||
filter_key = f.get("Key", None)
|
||||
filter_values = f.get("Values", None)
|
||||
if filter_key is None:
|
||||
raise InvalidParameterException("Invalid filter key")
|
||||
if filter_key not in filter_keys():
|
||||
raise ValidationException(
|
||||
"1 validation error detected: Value '{}' at 'filters.{}.member.key' failed to satisfy constraint: "
|
||||
"Member must satisfy enum value set: [all, name, tag-key, description, tag-value]".format(
|
||||
filter_key, idx + 1
|
||||
)
|
||||
)
|
||||
if filter_values is None:
|
||||
raise InvalidParameterException(
|
||||
"Invalid filter values for key: {}".format(filter_key)
|
||||
)
|
||||
|
||||
|
||||
class SecretsManagerResponse(BaseResponse):
|
||||
def get_secret_value(self):
|
||||
secret_id = self._get_param("SecretId")
|
||||
@ -102,10 +125,12 @@ class SecretsManagerResponse(BaseResponse):
|
||||
)
|
||||
|
||||
def list_secrets(self):
|
||||
filters = self._get_param("Filters", if_none=[])
|
||||
_validate_filters(filters)
|
||||
max_results = self._get_int_param("MaxResults")
|
||||
next_token = self._get_param("NextToken")
|
||||
secret_list, next_token = secretsmanager_backends[self.region].list_secrets(
|
||||
max_results=max_results, next_token=next_token
|
||||
filters=filters, max_results=max_results, next_token=next_token
|
||||
)
|
||||
return json.dumps(dict(SecretList=secret_list, NextToken=next_token))
|
||||
|
||||
|
@ -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"]
|
||||
|
@ -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
|
||||
|
@ -219,6 +219,18 @@ def test_expression_tokenizer_single_set_action_attribute_name_valid_key():
|
||||
]
|
||||
|
||||
|
||||
def test_expression_tokenizer_single_set_action_attribute_name_leading_number():
|
||||
set_action = "SET attr=#0"
|
||||
token_list = ExpressionTokenizer.make_list(set_action)
|
||||
assert token_list == [
|
||||
Token(Token.ATTRIBUTE, "SET"),
|
||||
Token(Token.WHITESPACE, " "),
|
||||
Token(Token.ATTRIBUTE, "attr"),
|
||||
Token(Token.EQUAL_SIGN, "="),
|
||||
Token(Token.ATTRIBUTE_NAME, "#0"),
|
||||
]
|
||||
|
||||
|
||||
def test_expression_tokenizer_just_a_pipe():
|
||||
set_action = "|"
|
||||
try:
|
||||
|
@ -783,6 +783,16 @@ def test_ami_registration():
|
||||
assert images[0]["State"] == "available", "State should be available."
|
||||
|
||||
|
||||
@mock_ec2
|
||||
def test_ami_registration():
|
||||
ec2 = boto3.client("ec2", region_name="us-east-1")
|
||||
image_id = ec2.register_image(Name="test-register-image").get("ImageId", "")
|
||||
images = ec2.describe_images(ImageIds=[image_id]).get("Images", [])
|
||||
assert images[0]["Name"] == "test-register-image", "No image was registered."
|
||||
assert images[0]["RootDeviceName"] == "/dev/sda1", "Wrong root device name."
|
||||
assert images[0]["State"] == "available", "State should be available."
|
||||
|
||||
|
||||
@mock_ec2
|
||||
def test_ami_filter_wildcard():
|
||||
ec2_resource = boto3.resource("ec2", region_name="us-west-1")
|
||||
|
@ -562,19 +562,176 @@ def test_snapshot_attribute():
|
||||
cm.exception.status.should.equal(400)
|
||||
cm.exception.request_id.should_not.be.none
|
||||
|
||||
# Error: Add or remove with user ID instead of group
|
||||
conn.modify_snapshot_attribute.when.called_with(
|
||||
snapshot.id,
|
||||
attribute="createVolumePermission",
|
||||
operation="add",
|
||||
user_ids=["user"],
|
||||
).should.throw(NotImplementedError)
|
||||
conn.modify_snapshot_attribute.when.called_with(
|
||||
snapshot.id,
|
||||
attribute="createVolumePermission",
|
||||
operation="remove",
|
||||
user_ids=["user"],
|
||||
).should.throw(NotImplementedError)
|
||||
|
||||
@mock_ec2
|
||||
def test_modify_snapshot_attribute():
|
||||
import copy
|
||||
|
||||
ec2_client = boto3.client("ec2", region_name="us-east-1")
|
||||
response = ec2_client.create_volume(Size=80, AvailabilityZone="us-east-1a")
|
||||
volume = boto3.resource("ec2", region_name="us-east-1").Volume(response["VolumeId"])
|
||||
snapshot = volume.create_snapshot()
|
||||
|
||||
# Baseline
|
||||
attributes = ec2_client.describe_snapshot_attribute(
|
||||
SnapshotId=snapshot.id, Attribute="createVolumePermission"
|
||||
)
|
||||
assert not attributes[
|
||||
"CreateVolumePermissions"
|
||||
], "Snapshot should have no permissions."
|
||||
|
||||
ADD_GROUP_ARGS = {
|
||||
"SnapshotId": snapshot.id,
|
||||
"Attribute": "createVolumePermission",
|
||||
"OperationType": "add",
|
||||
"GroupNames": ["all"],
|
||||
}
|
||||
|
||||
REMOVE_GROUP_ARGS = {
|
||||
"SnapshotId": snapshot.id,
|
||||
"Attribute": "createVolumePermission",
|
||||
"OperationType": "remove",
|
||||
"GroupNames": ["all"],
|
||||
}
|
||||
|
||||
# Add 'all' group and confirm
|
||||
with assert_raises(ClientError) as cm:
|
||||
ec2_client.modify_snapshot_attribute(**dict(ADD_GROUP_ARGS, **{"DryRun": True}))
|
||||
|
||||
cm.exception.response["Error"]["Code"].should.equal("DryRunOperation")
|
||||
cm.exception.response["ResponseMetadata"]["RequestId"].should_not.be.none
|
||||
cm.exception.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
|
||||
|
||||
ec2_client.modify_snapshot_attribute(**ADD_GROUP_ARGS)
|
||||
|
||||
attributes = ec2_client.describe_snapshot_attribute(
|
||||
SnapshotId=snapshot.id, Attribute="createVolumePermission"
|
||||
)
|
||||
assert attributes["CreateVolumePermissions"] == [
|
||||
{"Group": "all"}
|
||||
], "This snapshot should have public group permissions."
|
||||
|
||||
# Add is idempotent
|
||||
ec2_client.modify_snapshot_attribute.when.called_with(
|
||||
**ADD_GROUP_ARGS
|
||||
).should_not.throw(ClientError)
|
||||
assert attributes["CreateVolumePermissions"] == [
|
||||
{"Group": "all"}
|
||||
], "This snapshot should have public group permissions."
|
||||
|
||||
# Remove 'all' group and confirm
|
||||
with assert_raises(ClientError) as ex:
|
||||
ec2_client.modify_snapshot_attribute(
|
||||
**dict(REMOVE_GROUP_ARGS, **{"DryRun": True})
|
||||
)
|
||||
cm.exception.response["Error"]["Code"].should.equal("DryRunOperation")
|
||||
cm.exception.response["ResponseMetadata"]["RequestId"].should_not.be.none
|
||||
cm.exception.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
|
||||
|
||||
ec2_client.modify_snapshot_attribute(**REMOVE_GROUP_ARGS)
|
||||
|
||||
attributes = ec2_client.describe_snapshot_attribute(
|
||||
SnapshotId=snapshot.id, Attribute="createVolumePermission"
|
||||
)
|
||||
assert not attributes[
|
||||
"CreateVolumePermissions"
|
||||
], "This snapshot should have no permissions."
|
||||
|
||||
# Remove is idempotent
|
||||
ec2_client.modify_snapshot_attribute.when.called_with(
|
||||
**REMOVE_GROUP_ARGS
|
||||
).should_not.throw(ClientError)
|
||||
assert not attributes[
|
||||
"CreateVolumePermissions"
|
||||
], "This snapshot should have no permissions."
|
||||
|
||||
# Error: Add with group != 'all'
|
||||
with assert_raises(ClientError) as cm:
|
||||
ec2_client.modify_snapshot_attribute(
|
||||
SnapshotId=snapshot.id,
|
||||
Attribute="createVolumePermission",
|
||||
OperationType="add",
|
||||
GroupNames=["everyone"],
|
||||
)
|
||||
cm.exception.response["Error"]["Code"].should.equal("InvalidAMIAttributeItemValue")
|
||||
cm.exception.response["ResponseMetadata"]["RequestId"].should_not.be.none
|
||||
cm.exception.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
|
||||
|
||||
# Error: Add with invalid snapshot ID
|
||||
with assert_raises(ClientError) as cm:
|
||||
ec2_client.modify_snapshot_attribute(
|
||||
SnapshotId="snapshot-abcd1234",
|
||||
Attribute="createVolumePermission",
|
||||
OperationType="add",
|
||||
GroupNames=["all"],
|
||||
)
|
||||
cm.exception.response["Error"]["Code"].should.equal("InvalidSnapshot.NotFound")
|
||||
cm.exception.response["ResponseMetadata"]["RequestId"].should_not.be.none
|
||||
cm.exception.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
|
||||
|
||||
# Error: Remove with invalid snapshot ID
|
||||
with assert_raises(ClientError) as cm:
|
||||
ec2_client.modify_snapshot_attribute(
|
||||
SnapshotId="snapshot-abcd1234",
|
||||
Attribute="createVolumePermission",
|
||||
OperationType="remove",
|
||||
GroupNames=["all"],
|
||||
)
|
||||
cm.exception.response["Error"]["Code"].should.equal("InvalidSnapshot.NotFound")
|
||||
cm.exception.response["ResponseMetadata"]["RequestId"].should_not.be.none
|
||||
cm.exception.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
|
||||
|
||||
# Test adding user id
|
||||
ec2_client.modify_snapshot_attribute(
|
||||
SnapshotId=snapshot.id,
|
||||
Attribute="createVolumePermission",
|
||||
OperationType="add",
|
||||
UserIds=["1234567891"],
|
||||
)
|
||||
|
||||
attributes = ec2_client.describe_snapshot_attribute(
|
||||
SnapshotId=snapshot.id, Attribute="createVolumePermission"
|
||||
)
|
||||
assert len(attributes["CreateVolumePermissions"]) == 1
|
||||
|
||||
# Test adding user id again along with additional.
|
||||
ec2_client.modify_snapshot_attribute(
|
||||
SnapshotId=snapshot.id,
|
||||
Attribute="createVolumePermission",
|
||||
OperationType="add",
|
||||
UserIds=["1234567891", "2345678912"],
|
||||
)
|
||||
|
||||
attributes = ec2_client.describe_snapshot_attribute(
|
||||
SnapshotId=snapshot.id, Attribute="createVolumePermission"
|
||||
)
|
||||
assert len(attributes["CreateVolumePermissions"]) == 2
|
||||
|
||||
# Test removing both user IDs.
|
||||
ec2_client.modify_snapshot_attribute(
|
||||
SnapshotId=snapshot.id,
|
||||
Attribute="createVolumePermission",
|
||||
OperationType="remove",
|
||||
UserIds=["1234567891", "2345678912"],
|
||||
)
|
||||
|
||||
attributes = ec2_client.describe_snapshot_attribute(
|
||||
SnapshotId=snapshot.id, Attribute="createVolumePermission"
|
||||
)
|
||||
assert len(attributes["CreateVolumePermissions"]) == 0
|
||||
|
||||
# Idempotency when removing users.
|
||||
ec2_client.modify_snapshot_attribute(
|
||||
SnapshotId=snapshot.id,
|
||||
Attribute="createVolumePermission",
|
||||
OperationType="remove",
|
||||
UserIds=["1234567891"],
|
||||
)
|
||||
|
||||
attributes = ec2_client.describe_snapshot_attribute(
|
||||
SnapshotId=snapshot.id, Attribute="createVolumePermission"
|
||||
)
|
||||
assert len(attributes["CreateVolumePermissions"]) == 0
|
||||
|
||||
|
||||
@mock_ec2_deprecated
|
||||
|
@ -10,6 +10,7 @@ from botocore.exceptions import ClientError
|
||||
from nose.tools import assert_raises
|
||||
|
||||
from moto import mock_organizations
|
||||
from moto.core import ACCOUNT_ID
|
||||
from moto.organizations import utils
|
||||
from .organizations_test_utils import (
|
||||
validate_organization,
|
||||
@ -64,8 +65,11 @@ def test_describe_organization_exception():
|
||||
response = client.describe_organization()
|
||||
ex = e.exception
|
||||
ex.operation_name.should.equal("DescribeOrganization")
|
||||
ex.response["Error"]["Code"].should.equal("400")
|
||||
ex.response["Error"]["Message"].should.contain("AWSOrganizationsNotInUseException")
|
||||
ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
|
||||
ex.response["Error"]["Code"].should.contain("AWSOrganizationsNotInUseException")
|
||||
ex.response["Error"]["Message"].should.equal(
|
||||
"Your account is not a member of an organization."
|
||||
)
|
||||
|
||||
|
||||
# Organizational Units
|
||||
@ -193,8 +197,11 @@ def test_describe_account_exception():
|
||||
response = client.describe_account(AccountId=utils.make_random_account_id())
|
||||
ex = e.exception
|
||||
ex.operation_name.should.equal("DescribeAccount")
|
||||
ex.response["Error"]["Code"].should.equal("400")
|
||||
ex.response["Error"]["Message"].should.contain("AccountNotFoundException")
|
||||
ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
|
||||
ex.response["Error"]["Code"].should.contain("AccountNotFoundException")
|
||||
ex.response["Error"]["Message"].should.equal(
|
||||
"You specified an account that doesn't exist."
|
||||
)
|
||||
|
||||
|
||||
@mock_organizations
|
||||
@ -340,8 +347,9 @@ def test_list_children_exception():
|
||||
response = client.list_children(ParentId=root_id, ChildType="BLEE")
|
||||
ex = e.exception
|
||||
ex.operation_name.should.equal("ListChildren")
|
||||
ex.response["Error"]["Code"].should.equal("400")
|
||||
ex.response["Error"]["Message"].should.contain("InvalidInputException")
|
||||
ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
|
||||
ex.response["Error"]["Code"].should.contain("InvalidInputException")
|
||||
ex.response["Error"]["Message"].should.equal("You specified an invalid value.")
|
||||
|
||||
|
||||
# Service Control Policies
|
||||
@ -405,8 +413,9 @@ def test_describe_policy_exception():
|
||||
response = client.describe_policy(PolicyId="meaninglessstring")
|
||||
ex = e.exception
|
||||
ex.operation_name.should.equal("DescribePolicy")
|
||||
ex.response["Error"]["Code"].should.equal("400")
|
||||
ex.response["Error"]["Message"].should.contain("InvalidInputException")
|
||||
ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
|
||||
ex.response["Error"]["Code"].should.contain("InvalidInputException")
|
||||
ex.response["Error"]["Message"].should.equal("You specified an invalid value.")
|
||||
|
||||
|
||||
@mock_organizations
|
||||
@ -517,16 +526,20 @@ def test_attach_policy_exception():
|
||||
response = client.attach_policy(PolicyId=policy_id, TargetId=account_id)
|
||||
ex = e.exception
|
||||
ex.operation_name.should.equal("AttachPolicy")
|
||||
ex.response["Error"]["Code"].should.equal("400")
|
||||
ex.response["Error"]["Message"].should.contain("AccountNotFoundException")
|
||||
ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
|
||||
ex.response["Error"]["Code"].should.contain("AccountNotFoundException")
|
||||
ex.response["Error"]["Message"].should.equal(
|
||||
"You specified an account that doesn't exist."
|
||||
)
|
||||
with assert_raises(ClientError) as e:
|
||||
response = client.attach_policy(
|
||||
PolicyId=policy_id, TargetId="meaninglessstring"
|
||||
)
|
||||
ex = e.exception
|
||||
ex.operation_name.should.equal("AttachPolicy")
|
||||
ex.response["Error"]["Code"].should.equal("400")
|
||||
ex.response["Error"]["Message"].should.contain("InvalidInputException")
|
||||
ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
|
||||
ex.response["Error"]["Code"].should.contain("InvalidInputException")
|
||||
ex.response["Error"]["Message"].should.equal("You specified an invalid value.")
|
||||
|
||||
|
||||
@mock_organizations
|
||||
@ -636,16 +649,20 @@ def test_list_policies_for_target_exception():
|
||||
)
|
||||
ex = e.exception
|
||||
ex.operation_name.should.equal("ListPoliciesForTarget")
|
||||
ex.response["Error"]["Code"].should.equal("400")
|
||||
ex.response["Error"]["Message"].should.contain("AccountNotFoundException")
|
||||
ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
|
||||
ex.response["Error"]["Code"].should.contain("AccountNotFoundException")
|
||||
ex.response["Error"]["Message"].should.equal(
|
||||
"You specified an account that doesn't exist."
|
||||
)
|
||||
with assert_raises(ClientError) as e:
|
||||
response = client.list_policies_for_target(
|
||||
TargetId="meaninglessstring", Filter="SERVICE_CONTROL_POLICY"
|
||||
)
|
||||
ex = e.exception
|
||||
ex.operation_name.should.equal("ListPoliciesForTarget")
|
||||
ex.response["Error"]["Code"].should.equal("400")
|
||||
ex.response["Error"]["Message"].should.contain("InvalidInputException")
|
||||
ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
|
||||
ex.response["Error"]["Code"].should.contain("InvalidInputException")
|
||||
ex.response["Error"]["Message"].should.equal("You specified an invalid value.")
|
||||
|
||||
|
||||
@mock_organizations
|
||||
@ -694,8 +711,9 @@ def test_list_targets_for_policy_exception():
|
||||
response = client.list_targets_for_policy(PolicyId="meaninglessstring")
|
||||
ex = e.exception
|
||||
ex.operation_name.should.equal("ListTargetsForPolicy")
|
||||
ex.response["Error"]["Code"].should.equal("400")
|
||||
ex.response["Error"]["Message"].should.contain("InvalidInputException")
|
||||
ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
|
||||
ex.response["Error"]["Code"].should.contain("InvalidInputException")
|
||||
ex.response["Error"]["Message"].should.equal("You specified an invalid value.")
|
||||
|
||||
|
||||
@mock_organizations
|
||||
@ -947,3 +965,343 @@ def test_disable_aws_service_access_errors():
|
||||
ex.response["Error"]["Message"].should.equal(
|
||||
"You specified an unrecognized service principal."
|
||||
)
|
||||
|
||||
|
||||
@mock_organizations
|
||||
def test_register_delegated_administrator():
|
||||
# given
|
||||
client = boto3.client("organizations", region_name="us-east-1")
|
||||
org_id = client.create_organization(FeatureSet="ALL")["Organization"]["Id"]
|
||||
account_id = client.create_account(AccountName=mockname, Email=mockemail)[
|
||||
"CreateAccountStatus"
|
||||
]["AccountId"]
|
||||
|
||||
# when
|
||||
client.register_delegated_administrator(
|
||||
AccountId=account_id, ServicePrincipal="ssm.amazonaws.com"
|
||||
)
|
||||
|
||||
# then
|
||||
response = client.list_delegated_administrators()
|
||||
response["DelegatedAdministrators"].should.have.length_of(1)
|
||||
admin = response["DelegatedAdministrators"][0]
|
||||
admin["Id"].should.equal(account_id)
|
||||
admin["Arn"].should.equal(
|
||||
"arn:aws:organizations::{0}:account/{1}/{2}".format(
|
||||
ACCOUNT_ID, org_id, account_id
|
||||
)
|
||||
)
|
||||
admin["Email"].should.equal(mockemail)
|
||||
admin["Name"].should.equal(mockname)
|
||||
admin["Status"].should.equal("ACTIVE")
|
||||
admin["JoinedMethod"].should.equal("CREATED")
|
||||
admin["JoinedTimestamp"].should.be.a(datetime)
|
||||
admin["DelegationEnabledDate"].should.be.a(datetime)
|
||||
|
||||
|
||||
@mock_organizations
|
||||
def test_register_delegated_administrator_errors():
|
||||
# given
|
||||
client = boto3.client("organizations", region_name="us-east-1")
|
||||
client.create_organization(FeatureSet="ALL")
|
||||
account_id = client.create_account(AccountName=mockname, Email=mockemail)[
|
||||
"CreateAccountStatus"
|
||||
]["AccountId"]
|
||||
client.register_delegated_administrator(
|
||||
AccountId=account_id, ServicePrincipal="ssm.amazonaws.com"
|
||||
)
|
||||
|
||||
# register master Account
|
||||
# when
|
||||
with assert_raises(ClientError) as e:
|
||||
client.register_delegated_administrator(
|
||||
AccountId=ACCOUNT_ID, ServicePrincipal="ssm.amazonaws.com"
|
||||
)
|
||||
|
||||
# then
|
||||
ex = e.exception
|
||||
ex.operation_name.should.equal("RegisterDelegatedAdministrator")
|
||||
ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
|
||||
ex.response["Error"]["Code"].should.contain("ConstraintViolationException")
|
||||
ex.response["Error"]["Message"].should.equal(
|
||||
"You cannot register master account/yourself as delegated administrator for your organization."
|
||||
)
|
||||
|
||||
# register not existing Account
|
||||
# when
|
||||
with assert_raises(ClientError) as e:
|
||||
client.register_delegated_administrator(
|
||||
AccountId="000000000000", ServicePrincipal="ssm.amazonaws.com"
|
||||
)
|
||||
|
||||
# then
|
||||
ex = e.exception
|
||||
ex.operation_name.should.equal("RegisterDelegatedAdministrator")
|
||||
ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
|
||||
ex.response["Error"]["Code"].should.contain("AccountNotFoundException")
|
||||
ex.response["Error"]["Message"].should.equal(
|
||||
"You specified an account that doesn't exist."
|
||||
)
|
||||
|
||||
# register not supported service
|
||||
# when
|
||||
with assert_raises(ClientError) as e:
|
||||
client.register_delegated_administrator(
|
||||
AccountId=account_id, ServicePrincipal="moto.amazonaws.com"
|
||||
)
|
||||
|
||||
# then
|
||||
ex = e.exception
|
||||
ex.operation_name.should.equal("RegisterDelegatedAdministrator")
|
||||
ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
|
||||
ex.response["Error"]["Code"].should.contain("InvalidInputException")
|
||||
ex.response["Error"]["Message"].should.equal(
|
||||
"You specified an unrecognized service principal."
|
||||
)
|
||||
|
||||
# register service again
|
||||
# when
|
||||
with assert_raises(ClientError) as e:
|
||||
client.register_delegated_administrator(
|
||||
AccountId=account_id, ServicePrincipal="ssm.amazonaws.com"
|
||||
)
|
||||
|
||||
# then
|
||||
ex = e.exception
|
||||
ex.operation_name.should.equal("RegisterDelegatedAdministrator")
|
||||
ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
|
||||
ex.response["Error"]["Code"].should.contain("AccountAlreadyRegisteredException")
|
||||
ex.response["Error"]["Message"].should.equal(
|
||||
"The provided account is already a delegated administrator for your organization."
|
||||
)
|
||||
|
||||
|
||||
@mock_organizations
|
||||
def test_list_delegated_administrators():
|
||||
# given
|
||||
client = boto3.client("organizations", region_name="us-east-1")
|
||||
org_id = client.create_organization(FeatureSet="ALL")["Organization"]["Id"]
|
||||
account_id_1 = client.create_account(AccountName=mockname, Email=mockemail)[
|
||||
"CreateAccountStatus"
|
||||
]["AccountId"]
|
||||
account_id_2 = client.create_account(AccountName=mockname, Email=mockemail)[
|
||||
"CreateAccountStatus"
|
||||
]["AccountId"]
|
||||
client.register_delegated_administrator(
|
||||
AccountId=account_id_1, ServicePrincipal="ssm.amazonaws.com"
|
||||
)
|
||||
client.register_delegated_administrator(
|
||||
AccountId=account_id_2, ServicePrincipal="guardduty.amazonaws.com"
|
||||
)
|
||||
|
||||
# when
|
||||
response = client.list_delegated_administrators()
|
||||
|
||||
# then
|
||||
response["DelegatedAdministrators"].should.have.length_of(2)
|
||||
sorted([admin["Id"] for admin in response["DelegatedAdministrators"]]).should.equal(
|
||||
sorted([account_id_1, account_id_2])
|
||||
)
|
||||
|
||||
# when
|
||||
response = client.list_delegated_administrators(
|
||||
ServicePrincipal="ssm.amazonaws.com"
|
||||
)
|
||||
|
||||
# then
|
||||
response["DelegatedAdministrators"].should.have.length_of(1)
|
||||
admin = response["DelegatedAdministrators"][0]
|
||||
admin["Id"].should.equal(account_id_1)
|
||||
admin["Arn"].should.equal(
|
||||
"arn:aws:organizations::{0}:account/{1}/{2}".format(
|
||||
ACCOUNT_ID, org_id, account_id_1
|
||||
)
|
||||
)
|
||||
admin["Email"].should.equal(mockemail)
|
||||
admin["Name"].should.equal(mockname)
|
||||
admin["Status"].should.equal("ACTIVE")
|
||||
admin["JoinedMethod"].should.equal("CREATED")
|
||||
admin["JoinedTimestamp"].should.be.a(datetime)
|
||||
admin["DelegationEnabledDate"].should.be.a(datetime)
|
||||
|
||||
|
||||
@mock_organizations
|
||||
def test_list_delegated_administrators_erros():
|
||||
# given
|
||||
client = boto3.client("organizations", region_name="us-east-1")
|
||||
client.create_organization(FeatureSet="ALL")
|
||||
|
||||
# list not supported service
|
||||
# when
|
||||
with assert_raises(ClientError) as e:
|
||||
client.list_delegated_administrators(ServicePrincipal="moto.amazonaws.com")
|
||||
|
||||
# then
|
||||
ex = e.exception
|
||||
ex.operation_name.should.equal("ListDelegatedAdministrators")
|
||||
ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
|
||||
ex.response["Error"]["Code"].should.contain("InvalidInputException")
|
||||
ex.response["Error"]["Message"].should.equal(
|
||||
"You specified an unrecognized service principal."
|
||||
)
|
||||
|
||||
|
||||
@mock_organizations
|
||||
def test_list_delegated_services_for_account():
|
||||
# given
|
||||
client = boto3.client("organizations", region_name="us-east-1")
|
||||
client.create_organization(FeatureSet="ALL")
|
||||
account_id = client.create_account(AccountName=mockname, Email=mockemail)[
|
||||
"CreateAccountStatus"
|
||||
]["AccountId"]
|
||||
client.register_delegated_administrator(
|
||||
AccountId=account_id, ServicePrincipal="ssm.amazonaws.com"
|
||||
)
|
||||
client.register_delegated_administrator(
|
||||
AccountId=account_id, ServicePrincipal="guardduty.amazonaws.com"
|
||||
)
|
||||
|
||||
# when
|
||||
response = client.list_delegated_services_for_account(AccountId=account_id)
|
||||
|
||||
# then
|
||||
response["DelegatedServices"].should.have.length_of(2)
|
||||
sorted(
|
||||
[service["ServicePrincipal"] for service in response["DelegatedServices"]]
|
||||
).should.equal(["guardduty.amazonaws.com", "ssm.amazonaws.com"])
|
||||
|
||||
|
||||
@mock_organizations
|
||||
def test_list_delegated_services_for_account_erros():
|
||||
# given
|
||||
client = boto3.client("organizations", region_name="us-east-1")
|
||||
client.create_organization(FeatureSet="ALL")
|
||||
|
||||
# list services for not existing Account
|
||||
# when
|
||||
with assert_raises(ClientError) as e:
|
||||
client.list_delegated_services_for_account(AccountId="000000000000")
|
||||
|
||||
# then
|
||||
ex = e.exception
|
||||
ex.operation_name.should.equal("ListDelegatedServicesForAccount")
|
||||
ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
|
||||
ex.response["Error"]["Code"].should.contain("AWSOrganizationsNotInUseException")
|
||||
ex.response["Error"]["Message"].should.equal(
|
||||
"Your account is not a member of an organization."
|
||||
)
|
||||
|
||||
# list services for not registered Account
|
||||
# when
|
||||
with assert_raises(ClientError) as e:
|
||||
client.list_delegated_services_for_account(AccountId=ACCOUNT_ID)
|
||||
|
||||
# then
|
||||
ex = e.exception
|
||||
ex.operation_name.should.equal("ListDelegatedServicesForAccount")
|
||||
ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
|
||||
ex.response["Error"]["Code"].should.contain("AccountNotRegisteredException")
|
||||
ex.response["Error"]["Message"].should.equal(
|
||||
"The provided account is not a registered delegated administrator for your organization."
|
||||
)
|
||||
|
||||
|
||||
@mock_organizations
|
||||
def test_deregister_delegated_administrator():
|
||||
# given
|
||||
client = boto3.client("organizations", region_name="us-east-1")
|
||||
client.create_organization(FeatureSet="ALL")
|
||||
account_id = client.create_account(AccountName=mockname, Email=mockemail)[
|
||||
"CreateAccountStatus"
|
||||
]["AccountId"]
|
||||
client.register_delegated_administrator(
|
||||
AccountId=account_id, ServicePrincipal="ssm.amazonaws.com"
|
||||
)
|
||||
|
||||
# when
|
||||
client.deregister_delegated_administrator(
|
||||
AccountId=account_id, ServicePrincipal="ssm.amazonaws.com"
|
||||
)
|
||||
|
||||
# then
|
||||
response = client.list_delegated_administrators()
|
||||
response["DelegatedAdministrators"].should.have.length_of(0)
|
||||
|
||||
|
||||
@mock_organizations
|
||||
def test_deregister_delegated_administrator_erros():
|
||||
# given
|
||||
client = boto3.client("organizations", region_name="us-east-1")
|
||||
client.create_organization(FeatureSet="ALL")
|
||||
account_id = client.create_account(AccountName=mockname, Email=mockemail)[
|
||||
"CreateAccountStatus"
|
||||
]["AccountId"]
|
||||
|
||||
# deregister master Account
|
||||
# when
|
||||
with assert_raises(ClientError) as e:
|
||||
client.deregister_delegated_administrator(
|
||||
AccountId=ACCOUNT_ID, ServicePrincipal="ssm.amazonaws.com"
|
||||
)
|
||||
|
||||
# then
|
||||
ex = e.exception
|
||||
ex.operation_name.should.equal("DeregisterDelegatedAdministrator")
|
||||
ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
|
||||
ex.response["Error"]["Code"].should.contain("ConstraintViolationException")
|
||||
ex.response["Error"]["Message"].should.equal(
|
||||
"You cannot register master account/yourself as delegated administrator for your organization."
|
||||
)
|
||||
|
||||
# deregister not existing Account
|
||||
# when
|
||||
with assert_raises(ClientError) as e:
|
||||
client.deregister_delegated_administrator(
|
||||
AccountId="000000000000", ServicePrincipal="ssm.amazonaws.com"
|
||||
)
|
||||
|
||||
# then
|
||||
ex = e.exception
|
||||
ex.operation_name.should.equal("DeregisterDelegatedAdministrator")
|
||||
ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
|
||||
ex.response["Error"]["Code"].should.contain("AccountNotFoundException")
|
||||
ex.response["Error"]["Message"].should.equal(
|
||||
"You specified an account that doesn't exist."
|
||||
)
|
||||
|
||||
# deregister not registered Account
|
||||
# when
|
||||
with assert_raises(ClientError) as e:
|
||||
client.deregister_delegated_administrator(
|
||||
AccountId=account_id, ServicePrincipal="ssm.amazonaws.com"
|
||||
)
|
||||
|
||||
# then
|
||||
ex = e.exception
|
||||
ex.operation_name.should.equal("DeregisterDelegatedAdministrator")
|
||||
ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
|
||||
ex.response["Error"]["Code"].should.contain("AccountNotRegisteredException")
|
||||
ex.response["Error"]["Message"].should.equal(
|
||||
"The provided account is not a registered delegated administrator for your organization."
|
||||
)
|
||||
|
||||
# given
|
||||
client.register_delegated_administrator(
|
||||
AccountId=account_id, ServicePrincipal="ssm.amazonaws.com"
|
||||
)
|
||||
|
||||
# deregister not registered service
|
||||
# when
|
||||
with assert_raises(ClientError) as e:
|
||||
client.deregister_delegated_administrator(
|
||||
AccountId=account_id, ServicePrincipal="guardduty.amazonaws.com"
|
||||
)
|
||||
|
||||
# then
|
||||
ex = e.exception
|
||||
ex.operation_name.should.equal("DeregisterDelegatedAdministrator")
|
||||
ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
|
||||
ex.response["Error"]["Code"].should.contain("InvalidInputException")
|
||||
ex.response["Error"]["Message"].should.equal(
|
||||
"You specified an unrecognized service principal."
|
||||
)
|
||||
|
251
tests/test_secretsmanager/test_list_secrets.py
Normal file
251
tests/test_secretsmanager/test_list_secrets.py
Normal file
@ -0,0 +1,251 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import boto3
|
||||
|
||||
from moto import mock_secretsmanager
|
||||
from botocore.exceptions import ClientError
|
||||
import sure # noqa
|
||||
from nose.tools import assert_raises
|
||||
|
||||
try:
|
||||
from nose.tools import assert_items_equal
|
||||
except ImportError:
|
||||
from nose.tools import assert_count_equal as assert_items_equal
|
||||
|
||||
|
||||
def boto_client():
|
||||
return boto3.client("secretsmanager", region_name="us-west-2")
|
||||
|
||||
|
||||
@mock_secretsmanager
|
||||
def test_empty():
|
||||
conn = boto_client()
|
||||
|
||||
secrets = conn.list_secrets()
|
||||
|
||||
assert_items_equal(secrets["SecretList"], [])
|
||||
|
||||
|
||||
@mock_secretsmanager
|
||||
def test_list_secrets():
|
||||
conn = boto_client()
|
||||
|
||||
conn.create_secret(Name="test-secret", SecretString="foosecret")
|
||||
|
||||
conn.create_secret(
|
||||
Name="test-secret-2",
|
||||
SecretString="barsecret",
|
||||
Tags=[{"Key": "a", "Value": "1"}],
|
||||
)
|
||||
|
||||
secrets = conn.list_secrets()
|
||||
|
||||
assert secrets["SecretList"][0]["ARN"] is not None
|
||||
assert secrets["SecretList"][0]["Name"] == "test-secret"
|
||||
assert secrets["SecretList"][1]["ARN"] is not None
|
||||
assert secrets["SecretList"][1]["Name"] == "test-secret-2"
|
||||
assert secrets["SecretList"][1]["Tags"] == [{"Key": "a", "Value": "1"}]
|
||||
|
||||
|
||||
@mock_secretsmanager
|
||||
def test_with_name_filter():
|
||||
conn = boto_client()
|
||||
|
||||
conn.create_secret(Name="foo", SecretString="secret")
|
||||
conn.create_secret(Name="bar", SecretString="secret")
|
||||
|
||||
secrets = conn.list_secrets(Filters=[{"Key": "name", "Values": ["foo"]}])
|
||||
|
||||
secret_names = list(map(lambda s: s["Name"], secrets["SecretList"]))
|
||||
assert_items_equal(secret_names, ["foo"])
|
||||
|
||||
|
||||
@mock_secretsmanager
|
||||
def test_with_tag_key_filter():
|
||||
conn = boto_client()
|
||||
|
||||
conn.create_secret(
|
||||
Name="foo", SecretString="secret", Tags=[{"Key": "baz", "Value": "1"}]
|
||||
)
|
||||
conn.create_secret(Name="bar", SecretString="secret")
|
||||
|
||||
secrets = conn.list_secrets(Filters=[{"Key": "tag-key", "Values": ["baz"]}])
|
||||
|
||||
secret_names = list(map(lambda s: s["Name"], secrets["SecretList"]))
|
||||
assert_items_equal(secret_names, ["foo"])
|
||||
|
||||
|
||||
@mock_secretsmanager
|
||||
def test_with_tag_value_filter():
|
||||
conn = boto_client()
|
||||
|
||||
conn.create_secret(
|
||||
Name="foo", SecretString="secret", Tags=[{"Key": "1", "Value": "baz"}]
|
||||
)
|
||||
conn.create_secret(Name="bar", SecretString="secret")
|
||||
|
||||
secrets = conn.list_secrets(Filters=[{"Key": "tag-value", "Values": ["baz"]}])
|
||||
|
||||
secret_names = list(map(lambda s: s["Name"], secrets["SecretList"]))
|
||||
assert_items_equal(secret_names, ["foo"])
|
||||
|
||||
|
||||
@mock_secretsmanager
|
||||
def test_with_description_filter():
|
||||
conn = boto_client()
|
||||
|
||||
conn.create_secret(Name="foo", SecretString="secret", Description="baz qux")
|
||||
conn.create_secret(Name="bar", SecretString="secret")
|
||||
|
||||
secrets = conn.list_secrets(Filters=[{"Key": "description", "Values": ["baz"]}])
|
||||
|
||||
secret_names = list(map(lambda s: s["Name"], secrets["SecretList"]))
|
||||
assert_items_equal(secret_names, ["foo"])
|
||||
|
||||
|
||||
@mock_secretsmanager
|
||||
def test_with_all_filter():
|
||||
# The 'all' filter will match a secret that contains ANY field with the criteria. In other words an implicit OR.
|
||||
|
||||
conn = boto_client()
|
||||
|
||||
conn.create_secret(Name="foo", SecretString="secret")
|
||||
conn.create_secret(Name="bar", SecretString="secret", Description="foo")
|
||||
conn.create_secret(
|
||||
Name="baz", SecretString="secret", Tags=[{"Key": "foo", "Value": "1"}]
|
||||
)
|
||||
conn.create_secret(
|
||||
Name="qux", SecretString="secret", Tags=[{"Key": "1", "Value": "foo"}]
|
||||
)
|
||||
conn.create_secret(
|
||||
Name="multi", SecretString="secret", Tags=[{"Key": "foo", "Value": "foo"}]
|
||||
)
|
||||
conn.create_secret(Name="none", SecretString="secret")
|
||||
|
||||
secrets = conn.list_secrets(Filters=[{"Key": "all", "Values": ["foo"]}])
|
||||
|
||||
secret_names = list(map(lambda s: s["Name"], secrets["SecretList"]))
|
||||
assert_items_equal(secret_names, ["foo", "bar", "baz", "qux", "multi"])
|
||||
|
||||
|
||||
@mock_secretsmanager
|
||||
def test_with_no_filter_key():
|
||||
conn = boto_client()
|
||||
|
||||
with assert_raises(ClientError) as ire:
|
||||
conn.list_secrets(Filters=[{"Values": ["foo"]}])
|
||||
|
||||
ire.exception.response["Error"]["Code"].should.equal("InvalidParameterException")
|
||||
ire.exception.response["Error"]["Message"].should.equal("Invalid filter key")
|
||||
|
||||
|
||||
@mock_secretsmanager
|
||||
def test_with_no_filter_values():
|
||||
conn = boto_client()
|
||||
|
||||
conn.create_secret(Name="foo", SecretString="secret", Description="hello")
|
||||
|
||||
with assert_raises(ClientError) as ire:
|
||||
conn.list_secrets(Filters=[{"Key": "description"}])
|
||||
|
||||
ire.exception.response["Error"]["Code"].should.equal("InvalidParameterException")
|
||||
ire.exception.response["Error"]["Message"].should.equal(
|
||||
"Invalid filter values for key: description"
|
||||
)
|
||||
|
||||
|
||||
@mock_secretsmanager
|
||||
def test_with_invalid_filter_key():
|
||||
conn = boto_client()
|
||||
|
||||
with assert_raises(ClientError) as ire:
|
||||
conn.list_secrets(Filters=[{"Key": "invalid", "Values": ["foo"]}])
|
||||
|
||||
ire.exception.response["Error"]["Code"].should.equal("ValidationException")
|
||||
ire.exception.response["Error"]["Message"].should.equal(
|
||||
"1 validation error detected: Value 'invalid' at 'filters.1.member.key' failed to satisfy constraint: Member "
|
||||
"must satisfy enum value set: [all, name, tag-key, description, tag-value]"
|
||||
)
|
||||
|
||||
|
||||
@mock_secretsmanager
|
||||
def test_with_duplicate_filter_keys():
|
||||
# Multiple filters with the same key combine with an implicit AND operator
|
||||
|
||||
conn = boto_client()
|
||||
|
||||
conn.create_secret(Name="foo", SecretString="secret", Description="one two")
|
||||
conn.create_secret(Name="bar", SecretString="secret", Description="one")
|
||||
conn.create_secret(Name="baz", SecretString="secret", Description="two")
|
||||
conn.create_secret(Name="qux", SecretString="secret", Description="unrelated")
|
||||
|
||||
secrets = conn.list_secrets(
|
||||
Filters=[
|
||||
{"Key": "description", "Values": ["one"]},
|
||||
{"Key": "description", "Values": ["two"]},
|
||||
]
|
||||
)
|
||||
|
||||
secret_names = list(map(lambda s: s["Name"], secrets["SecretList"]))
|
||||
assert_items_equal(secret_names, ["foo"])
|
||||
|
||||
|
||||
@mock_secretsmanager
|
||||
def test_with_multiple_filters():
|
||||
# Multiple filters combine with an implicit AND operator
|
||||
|
||||
conn = boto_client()
|
||||
|
||||
conn.create_secret(
|
||||
Name="foo", SecretString="secret", Tags=[{"Key": "right", "Value": "right"}]
|
||||
)
|
||||
conn.create_secret(
|
||||
Name="bar", SecretString="secret", Tags=[{"Key": "right", "Value": "wrong"}]
|
||||
)
|
||||
conn.create_secret(
|
||||
Name="baz", SecretString="secret", Tags=[{"Key": "wrong", "Value": "right"}]
|
||||
)
|
||||
conn.create_secret(
|
||||
Name="qux", SecretString="secret", Tags=[{"Key": "wrong", "Value": "wrong"}]
|
||||
)
|
||||
|
||||
secrets = conn.list_secrets(
|
||||
Filters=[
|
||||
{"Key": "tag-key", "Values": ["right"]},
|
||||
{"Key": "tag-value", "Values": ["right"]},
|
||||
]
|
||||
)
|
||||
|
||||
secret_names = list(map(lambda s: s["Name"], secrets["SecretList"]))
|
||||
assert_items_equal(secret_names, ["foo"])
|
||||
|
||||
|
||||
@mock_secretsmanager
|
||||
def test_with_filter_with_multiple_values():
|
||||
conn = boto_client()
|
||||
|
||||
conn.create_secret(Name="foo", SecretString="secret")
|
||||
conn.create_secret(Name="bar", SecretString="secret")
|
||||
conn.create_secret(Name="baz", SecretString="secret")
|
||||
|
||||
secrets = conn.list_secrets(Filters=[{"Key": "name", "Values": ["foo", "bar"]}])
|
||||
|
||||
secret_names = list(map(lambda s: s["Name"], secrets["SecretList"]))
|
||||
assert_items_equal(secret_names, ["foo", "bar"])
|
||||
|
||||
|
||||
@mock_secretsmanager
|
||||
def test_with_filter_with_value_with_multiple_words():
|
||||
conn = boto_client()
|
||||
|
||||
conn.create_secret(Name="foo", SecretString="secret", Description="one two")
|
||||
conn.create_secret(Name="bar", SecretString="secret", Description="one and two")
|
||||
conn.create_secret(Name="baz", SecretString="secret", Description="one")
|
||||
conn.create_secret(Name="qux", SecretString="secret", Description="two")
|
||||
conn.create_secret(Name="none", SecretString="secret", Description="unrelated")
|
||||
|
||||
secrets = conn.list_secrets(Filters=[{"Key": "description", "Values": ["one two"]}])
|
||||
|
||||
secret_names = list(map(lambda s: s["Name"], secrets["SecretList"]))
|
||||
assert_items_equal(secret_names, ["foo", "bar"])
|
@ -459,36 +459,6 @@ def test_describe_secret_that_does_not_match():
|
||||
result = conn.get_secret_value(SecretId="i-dont-match")
|
||||
|
||||
|
||||
@mock_secretsmanager
|
||||
def test_list_secrets_empty():
|
||||
conn = boto3.client("secretsmanager", region_name="us-west-2")
|
||||
|
||||
secrets = conn.list_secrets()
|
||||
|
||||
assert secrets["SecretList"] == []
|
||||
|
||||
|
||||
@mock_secretsmanager
|
||||
def test_list_secrets():
|
||||
conn = boto3.client("secretsmanager", region_name="us-west-2")
|
||||
|
||||
conn.create_secret(Name="test-secret", SecretString="foosecret")
|
||||
|
||||
conn.create_secret(
|
||||
Name="test-secret-2",
|
||||
SecretString="barsecret",
|
||||
Tags=[{"Key": "a", "Value": "1"}],
|
||||
)
|
||||
|
||||
secrets = conn.list_secrets()
|
||||
|
||||
assert secrets["SecretList"][0]["ARN"] is not None
|
||||
assert secrets["SecretList"][0]["Name"] == "test-secret"
|
||||
assert secrets["SecretList"][1]["ARN"] is not None
|
||||
assert secrets["SecretList"][1]["Name"] == "test-secret-2"
|
||||
assert secrets["SecretList"][1]["Tags"] == [{"Key": "a", "Value": "1"}]
|
||||
|
||||
|
||||
@mock_secretsmanager
|
||||
def test_restore_secret():
|
||||
conn = boto3.client("secretsmanager", region_name="us-west-2")
|
||||
|
Loading…
Reference in New Issue
Block a user