diff --git a/tests/test_autoscaling/test_autoscaling_cloudformation.py b/tests/test_autoscaling/test_autoscaling_cloudformation.py index 558b583ef..ecea6d6fe 100644 --- a/tests/test_autoscaling/test_autoscaling_cloudformation.py +++ b/tests/test_autoscaling/test_autoscaling_cloudformation.py @@ -1,11 +1,8 @@ import boto3 +import json import sure # noqa -from moto import ( - mock_autoscaling, - mock_cloudformation, - mock_ec2, -) +from moto import mock_autoscaling, mock_cloudformation, mock_ec2, mock_elb from .utils import setup_networking from tests import EXAMPLE_AMI_ID @@ -276,3 +273,194 @@ Outputs: lt["LaunchTemplateId"].should.be.equal(launch_template_id) lt["LaunchTemplateName"].should.be.equal("test_launch_template_new") lt["Version"].should.be.equal("1") + + +@mock_autoscaling +@mock_elb +@mock_cloudformation +@mock_ec2 +def test_autoscaling_group_with_elb(): + web_setup_template = { + "AWSTemplateFormatVersion": "2010-09-09", + "Resources": { + "my-as-group": { + "Type": "AWS::AutoScaling::AutoScalingGroup", + "Properties": { + "AvailabilityZones": ["us-east-1a"], + "LaunchConfigurationName": {"Ref": "my-launch-config"}, + "MinSize": "2", + "MaxSize": "2", + "DesiredCapacity": "2", + "LoadBalancerNames": [{"Ref": "my-elb"}], + "Tags": [ + { + "Key": "propagated-test-tag", + "Value": "propagated-test-tag-value", + "PropagateAtLaunch": True, + }, + { + "Key": "not-propagated-test-tag", + "Value": "not-propagated-test-tag-value", + "PropagateAtLaunch": False, + }, + ], + }, + }, + "my-launch-config": { + "Type": "AWS::AutoScaling::LaunchConfiguration", + "Properties": { + "ImageId": EXAMPLE_AMI_ID, + "InstanceType": "t2.medium", + "UserData": "some user data", + }, + }, + "my-elb": { + "Type": "AWS::ElasticLoadBalancing::LoadBalancer", + "Properties": { + "AvailabilityZones": ["us-east-1a"], + "Listeners": [ + { + "LoadBalancerPort": "80", + "InstancePort": "80", + "Protocol": "HTTP", + } + ], + "LoadBalancerName": "my-elb", + "HealthCheck": { + "Target": "HTTP:80", + "HealthyThreshold": "3", + "UnhealthyThreshold": "5", + "Interval": "30", + "Timeout": "5", + }, + }, + }, + }, + } + + web_setup_template_json = json.dumps(web_setup_template) + + cf = boto3.client("cloudformation", region_name="us-east-1") + ec2 = boto3.client("ec2", region_name="us-east-1") + elb = boto3.client("elb", region_name="us-east-1") + client = boto3.client("autoscaling", region_name="us-east-1") + + cf.create_stack(StackName="web_stack", TemplateBody=web_setup_template_json) + + autoscale_group = client.describe_auto_scaling_groups()["AutoScalingGroups"][0] + autoscale_group["LaunchConfigurationName"].should.contain("my-launch-config") + autoscale_group["LoadBalancerNames"].should.equal(["my-elb"]) + + # Confirm the Launch config was actually created + client.describe_launch_configurations()[ + "LaunchConfigurations" + ].should.have.length_of(1) + + # Confirm the ELB was actually created + elb.describe_load_balancers()["LoadBalancerDescriptions"].should.have.length_of(1) + + resources = cf.list_stack_resources(StackName="web_stack")["StackResourceSummaries"] + as_group_resource = [ + resource + for resource in resources + if resource["ResourceType"] == "AWS::AutoScaling::AutoScalingGroup" + ][0] + as_group_resource["PhysicalResourceId"].should.contain("my-as-group") + + launch_config_resource = [ + resource + for resource in resources + if resource["ResourceType"] == "AWS::AutoScaling::LaunchConfiguration" + ][0] + launch_config_resource["PhysicalResourceId"].should.contain("my-launch-config") + + elb_resource = [ + resource + for resource in resources + if resource["ResourceType"] == "AWS::ElasticLoadBalancing::LoadBalancer" + ][0] + elb_resource["PhysicalResourceId"].should.contain("my-elb") + + # confirm the instances were created with the right tags + reservations = ec2.describe_instances()["Reservations"] + + reservations.should.have.length_of(1) + reservations[0]["Instances"].should.have.length_of(2) + for instance in reservations[0]["Instances"]: + tag_keys = [t["Key"] for t in instance["Tags"]] + tag_keys.should.contain("propagated-test-tag") + tag_keys.should_not.contain("not-propagated-test-tag") + + +@mock_autoscaling +@mock_cloudformation +@mock_ec2 +def test_autoscaling_group_update(): + asg_template = { + "AWSTemplateFormatVersion": "2010-09-09", + "Resources": { + "my-as-group": { + "Type": "AWS::AutoScaling::AutoScalingGroup", + "Properties": { + "AvailabilityZones": ["us-west-1a"], + "LaunchConfigurationName": {"Ref": "my-launch-config"}, + "MinSize": "2", + "MaxSize": "2", + "DesiredCapacity": "2", + }, + }, + "my-launch-config": { + "Type": "AWS::AutoScaling::LaunchConfiguration", + "Properties": { + "ImageId": EXAMPLE_AMI_ID, + "InstanceType": "t2.medium", + "UserData": "some user data", + }, + }, + }, + } + asg_template_json = json.dumps(asg_template) + + cf = boto3.client("cloudformation", region_name="us-west-1") + ec2 = boto3.client("ec2", region_name="us-west-1") + client = boto3.client("autoscaling", region_name="us-west-1") + cf.create_stack(StackName="asg_stack", TemplateBody=asg_template_json) + + asg = client.describe_auto_scaling_groups()["AutoScalingGroups"][0] + asg["MinSize"].should.equal(2) + asg["MaxSize"].should.equal(2) + asg["DesiredCapacity"].should.equal(2) + + asg_template["Resources"]["my-as-group"]["Properties"]["MaxSize"] = 3 + asg_template["Resources"]["my-as-group"]["Properties"]["Tags"] = [ + { + "Key": "propagated-test-tag", + "Value": "propagated-test-tag-value", + "PropagateAtLaunch": True, + }, + { + "Key": "not-propagated-test-tag", + "Value": "not-propagated-test-tag-value", + "PropagateAtLaunch": False, + }, + ] + asg_template_json = json.dumps(asg_template) + cf.update_stack(StackName="asg_stack", TemplateBody=asg_template_json) + asg = client.describe_auto_scaling_groups()["AutoScalingGroups"][0] + asg["MinSize"].should.equal(2) + asg["MaxSize"].should.equal(3) + asg["DesiredCapacity"].should.equal(2) + + # confirm the instances were created with the right tags + reservations = ec2.describe_instances()["Reservations"] + running_instance_count = 0 + for res in reservations: + for instance in res["Instances"]: + if instance["State"]["Name"] == "running": + running_instance_count += 1 + instance["Tags"].should.contain( + {"Key": "propagated-test-tag", "Value": "propagated-test-tag-value"} + ) + tag_keys = [t["Key"] for t in instance["Tags"]] + tag_keys.should_not.contain("not-propagated-test-tag") + running_instance_count.should.equal(2) diff --git a/tests/test_cloudformation/test_cloudformation_stack_crud.py b/tests/test_cloudformation/test_cloudformation_stack_crud.py index 05a1ffeed..7d2428c5c 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_crud.py +++ b/tests/test_cloudformation/test_cloudformation_stack_crud.py @@ -4,7 +4,6 @@ import os import json import boto -import boto3 import boto.dynamodb2 import boto.iam import boto.s3 @@ -15,7 +14,6 @@ from freezegun import freeze_time import sure # noqa import pytest -from moto.core import ACCOUNT_ID from moto import ( mock_cloudformation_deprecated, @@ -79,6 +77,7 @@ dummy_template_json3 = json.dumps(dummy_template3) dummy_template_json4 = json.dumps(dummy_template4) +# Has boto3 equivalent @mock_cloudformation_deprecated def test_create_stack(): conn = boto.connect_cloudformation() @@ -127,6 +126,7 @@ def test_create_stack_with_other_region(): ) +# Has boto3 equivalent @mock_cloudformation_deprecated @mock_route53_deprecated def test_create_stack_hosted_zone_by_id(): @@ -168,6 +168,7 @@ def test_create_stack_hosted_zone_by_id(): assert stack.list_resources() +# Has boto3 equivalent @mock_cloudformation_deprecated def test_creating_stacks_across_regions(): west1_conn = boto.cloudformation.connect_to_region("us-west-1") @@ -180,6 +181,7 @@ def test_creating_stacks_across_regions(): list(west2_conn.describe_stacks()).should.have.length_of(1) +# Has boto3 equivalent @mock_cloudformation_deprecated @mock_sns_deprecated @mock_sqs_deprecated @@ -246,6 +248,7 @@ def test_create_stack_with_notification_arn(): msg.should.have.key("UnsubscribeURL") +# Has boto3 equivalent @mock_cloudformation_deprecated @mock_s3_deprecated def test_create_stack_from_s3_url(): @@ -275,6 +278,7 @@ def test_create_stack_from_s3_url(): ) +# Has boto3 equivalent @mock_cloudformation_deprecated def test_describe_stack_by_name(): conn = boto.connect_cloudformation() @@ -284,6 +288,7 @@ def test_describe_stack_by_name(): stack.stack_name.should.equal("test_stack") +# Has boto3 equivalent @mock_cloudformation_deprecated def test_describe_stack_by_stack_id(): conn = boto.connect_cloudformation() @@ -296,6 +301,7 @@ def test_describe_stack_by_stack_id(): @mock_dynamodb2_deprecated +# Has boto3 equivalent @mock_cloudformation_deprecated def test_delete_stack_dynamo_template(): conn = boto.connect_cloudformation() @@ -308,6 +314,7 @@ def test_delete_stack_dynamo_template(): db_conn.list_tables()["TableNames"].should.have.length_of(0) +# Has boto3 equivalent @mock_cloudformation_deprecated def test_describe_deleted_stack(): conn = boto.connect_cloudformation() @@ -322,6 +329,7 @@ def test_describe_deleted_stack(): stack_by_id.stack_status.should.equal("DELETE_COMPLETE") +# Has boto3 equivalent @mock_cloudformation_deprecated def test_get_template_by_name(): conn = boto.connect_cloudformation() @@ -342,6 +350,7 @@ def test_get_template_by_name(): ) +# Has boto3 equivalent @mock_cloudformation_deprecated def test_list_stacks(): conn = boto.connect_cloudformation() @@ -353,6 +362,7 @@ def test_list_stacks(): stacks[0].template_description.should.equal("Stack 1") +# Has boto3 equivalent @mock_cloudformation_deprecated def test_list_stacks_with_filter(): conn = boto.connect_cloudformation() @@ -366,6 +376,7 @@ def test_list_stacks_with_filter(): stacks.should.have.length_of(1) +# Has boto3 equivalent @mock_cloudformation_deprecated def test_delete_stack_by_name(): conn = boto.connect_cloudformation() @@ -376,6 +387,7 @@ def test_delete_stack_by_name(): conn.describe_stacks().should.have.length_of(0) +# Has boto3 equivalent @mock_cloudformation_deprecated def test_delete_stack_by_id(): conn = boto.connect_cloudformation() @@ -390,6 +402,7 @@ def test_delete_stack_by_id(): conn.describe_stacks(stack_id).should.have.length_of(1) +# Has boto3 equivalent @mock_cloudformation_deprecated def test_delete_stack_with_resource_missing_delete_attr(): conn = boto.connect_cloudformation() @@ -400,6 +413,7 @@ def test_delete_stack_with_resource_missing_delete_attr(): conn.describe_stacks().should.have.length_of(0) +# Has boto3 equivalent @mock_cloudformation_deprecated def test_bad_describe_stack(): conn = boto.connect_cloudformation() @@ -407,6 +421,7 @@ def test_bad_describe_stack(): conn.describe_stacks("bad_stack") +# Has boto3 equivalent @mock_cloudformation_deprecated() def test_cloudformation_params(): dummy_template = { @@ -435,6 +450,7 @@ def test_cloudformation_params(): param.value.should.equal("testing123") +# Has boto3 equivalent @mock_cloudformation_deprecated def test_cloudformation_params_conditions_and_resources_are_distinct(): dummy_template = { @@ -471,6 +487,7 @@ def test_cloudformation_params_conditions_and_resources_are_distinct(): ] +# Has boto3 equivalent @mock_cloudformation_deprecated def test_stack_tags(): conn = boto.connect_cloudformation() @@ -484,6 +501,7 @@ def test_stack_tags(): dict(stack.tags).should.equal({"foo": "bar", "baz": "bleh"}) +# Has boto3 equivalent @mock_cloudformation_deprecated def test_update_stack(): conn = boto.connect_cloudformation() @@ -507,6 +525,7 @@ def test_update_stack(): ) +# Has boto3 equivalent @mock_cloudformation_deprecated def test_update_stack_with_previous_template(): conn = boto.connect_cloudformation() @@ -529,6 +548,7 @@ def test_update_stack_with_previous_template(): ) +# Has boto3 equivalent @mock_cloudformation_deprecated def test_update_stack_with_parameters(): dummy_template = { @@ -559,6 +579,7 @@ def test_update_stack_with_parameters(): assert stack.parameters[0].value == "192.168.0.1/16" +# Has boto3 equivalent @mock_cloudformation_deprecated def test_update_stack_replace_tags(): conn = boto.connect_cloudformation() @@ -575,6 +596,7 @@ def test_update_stack_replace_tags(): dict(stack.tags).should.equal({"foo": "baz"}) +# Has boto3 equivalent @mock_cloudformation_deprecated def test_update_stack_when_rolled_back(): conn = boto.connect_cloudformation() @@ -594,6 +616,7 @@ def test_update_stack_when_rolled_back(): ex.status.should.equal(400) +# Has boto3 equivalent @mock_cloudformation_deprecated def test_describe_stack_events_shows_create_update_and_delete(): conn = boto.connect_cloudformation() @@ -647,6 +670,7 @@ def test_describe_stack_events_shows_create_update_and_delete(): err.status.should.equal(400) +# Has boto3 equivalent @mock_cloudformation_deprecated def test_create_stack_lambda_and_dynamodb(): conn = boto.connect_cloudformation() @@ -714,6 +738,7 @@ def test_create_stack_lambda_and_dynamodb(): assert len(resources) == 4 +# Has boto3 equivalent @mock_cloudformation_deprecated def test_create_stack_kinesis(): conn = boto.connect_cloudformation() diff --git a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py index 685405813..248f7a3f9 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py +++ b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py @@ -3,12 +3,14 @@ from __future__ import unicode_literals import json from collections import OrderedDict from datetime import datetime, timedelta +import os import pytz import boto3 from botocore.exceptions import ClientError import pytest +from unittest import SkipTest from moto import ( mock_cloudformation, @@ -17,9 +19,14 @@ from moto import ( mock_sns, mock_sqs, mock_ec2, + mock_iam, + mock_lambda, ) +from moto import settings from moto.core import ACCOUNT_ID +from moto.cloudformation import cloudformation_backends from .test_cloudformation_stack_crud import dummy_template_json2, dummy_template_json4 + from tests import EXAMPLE_AMI_ID dummy_template = { @@ -223,6 +230,20 @@ dummy_redrive_template_json = json.dumps(dummy_redrive_template) @mock_cloudformation +@mock_ec2 +def test_create_stack(): + cf_conn = boto3.client("cloudformation", region_name="us-east-1") + cf_conn.create_stack(StackName="test_stack", TemplateBody=dummy_template_json) + + stack = cf_conn.describe_stacks()["Stacks"][0] + stack.should.have.key("StackName").equal("test_stack") + + template = cf_conn.get_template(StackName="test_stack")["TemplateBody"] + template.should.equal(dummy_template) + + +@mock_cloudformation +@mock_ec2 def test_boto3_describe_stack_instances(): cf_conn = boto3.client("cloudformation", region_name="us-east-1") cf_conn.create_stack_set( @@ -1560,15 +1581,39 @@ def test_describe_updated_stack(): stack_by_id["RoleARN"].should.equal("arn:aws:iam::{}:role/moto".format(ACCOUNT_ID)) stack_by_id["Tags"].should.equal([{"Key": "foo", "Value": "baz"}]) + # Verify the updated template is persisted + template = cf_conn.get_template(StackName="test_stack")["TemplateBody"] + template.should.equal(dummy_update_template) + + +@mock_cloudformation +def test_update_stack_with_previous_template(): + cf_conn = boto3.client("cloudformation", region_name="us-east-1") + cf_conn.create_stack(StackName="test_stack", TemplateBody=dummy_template_json) + cf_conn.update_stack(StackName="test_stack", UsePreviousTemplate=True) + + stack = cf_conn.describe_stacks(StackName="test_stack")["Stacks"][0] + stack["StackName"].should.equal("test_stack") + stack["StackStatus"].should.equal("UPDATE_COMPLETE") + + # Verify the original template is persisted + template = cf_conn.get_template(StackName="test_stack")["TemplateBody"] + template.should.equal(dummy_template) + @mock_cloudformation def test_bad_describe_stack(): cf_conn = boto3.client("cloudformation", region_name="us-east-1") - with pytest.raises(ClientError): + with pytest.raises(ClientError) as exc: cf_conn.describe_stacks(StackName="non_existent_stack") + err = exc.value.response["Error"] + err.should.have.key("Code").being.equal("ValidationError") + err.should.have.key("Message").being.equal( + "Stack with id non_existent_stack does not exist" + ) -@mock_cloudformation() +@mock_cloudformation def test_cloudformation_params(): dummy_template_with_params = { "AWSTemplateFormatVersion": "2010-09-09", @@ -1597,6 +1642,119 @@ def test_cloudformation_params(): param["ParameterValue"].should.equal("testing123") +@mock_cloudformation +@mock_ec2 +def test_update_stack_with_parameters(): + template = { + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "Stack", + "Resources": { + "VPC": { + "Properties": {"CidrBlock": {"Ref": "Bar"}}, + "Type": "AWS::EC2::VPC", + } + }, + "Parameters": {"Bar": {"Type": "String"}}, + } + template_json = json.dumps(template) + cf = boto3.client("cloudformation", region_name="us-east-1") + cf.create_stack( + StackName="test_stack", + TemplateBody=template_json, + Parameters=[{"ParameterKey": "Bar", "ParameterValue": "192.168.0.0/16"}], + ) + cf.update_stack( + StackName="test_stack", + TemplateBody=template_json, + Parameters=[{"ParameterKey": "Bar", "ParameterValue": "192.168.0.1/16"}], + ) + + stack = cf.describe_stacks(StackName="test_stack")["Stacks"][0] + stack["Parameters"].should.have.length_of(1) + stack["Parameters"][0].should.equal( + {"ParameterKey": "Bar", "ParameterValue": "192.168.0.1/16"} + ) + + +@mock_cloudformation +@mock_ec2 +def test_update_stack_replace_tags(): + cf = boto3.client("cloudformation", region_name="us-east-1") + cf.create_stack( + StackName="test_stack", + TemplateBody=dummy_template_json, + Tags=[{"Key": "foo", "Value": "bar"}], + ) + cf.update_stack( + StackName="test_stack", + TemplateBody=dummy_template_json, + Tags=[{"Key": "foo", "Value": "baz"}], + ) + + stack = cf.describe_stacks(StackName="test_stack")["Stacks"][0] + stack["StackStatus"].should.equal("UPDATE_COMPLETE") + stack["Tags"].should.equal([{"Key": "foo", "Value": "baz"}]) + + +@mock_cloudformation +def test_update_stack_when_rolled_back(): + if settings.TEST_SERVER_MODE: + raise SkipTest("Cant manipulate backend in server mode") + cf = boto3.client("cloudformation", region_name="us-east-1") + stack = cf.create_stack(StackName="test_stack", TemplateBody=dummy_template_json) + stack_id = stack["StackId"] + + cloudformation_backends["us-east-1"].stacks[stack_id].status = "ROLLBACK_COMPLETE" + + with pytest.raises(ClientError) as ex: + cf.update_stack(StackName="test_stack", TemplateBody=dummy_template_json) + + err = ex.value.response["Error"] + err.should.have.key("Code").being.equal("ValidationError") + err.should.have.key("Message").match( + r"Stack:arn:aws:cloudformation:us-east-1:123456789:stack/test_stack/[a-z0-9-]+ is in ROLLBACK_COMPLETE state and can not be updated." + ) + + +@mock_cloudformation +@mock_ec2 +def test_cloudformation_params_conditions_and_resources_are_distinct(): + template_with_conditions = { + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "Stack 1", + "Conditions": { + "FooEnabled": {"Fn::Equals": [{"Ref": "FooEnabled"}, "true"]}, + "FooDisabled": { + "Fn::Not": [{"Fn::Equals": [{"Ref": "FooEnabled"}, "true"]}] + }, + }, + "Parameters": { + "FooEnabled": {"Type": "String", "AllowedValues": ["true", "false"]} + }, + "Resources": { + "Bar": { + "Properties": {"CidrBlock": "192.168.0.0/16"}, + "Condition": "FooDisabled", + "Type": "AWS::EC2::VPC", + } + }, + } + template_with_conditions = json.dumps(template_with_conditions) + cf = boto3.client("cloudformation", region_name="us-east-1") + cf.create_stack( + StackName="test_stack1", + TemplateBody=template_with_conditions, + Parameters=[{"ParameterKey": "FooEnabled", "ParameterValue": "true"}], + ) + stack = cf.describe_stacks(StackName="test_stack1")["Stacks"][0] + resources = cf.list_stack_resources(StackName="test_stack1")[ + "StackResourceSummaries" + ] + assert not [ + resource for resource in resources if resource["LogicalResourceId"] == "Bar" + ] + + @mock_cloudformation def test_stack_tags(): tags = [{"Key": "foo", "Value": "bar"}, {"Key": "baz", "Value": "bleh"}] @@ -1791,3 +1949,91 @@ def test_delete_stack_dynamo_template(): table_desc = dynamodb_client.list_tables() len(table_desc.get("TableNames")).should.equal(0) conn.create_stack(StackName="test_stack", TemplateBody=dummy_template_json4) + + +@mock_dynamodb2 +@mock_cloudformation +@mock_lambda +def test_create_stack_lambda_and_dynamodb(): + if settings.TEST_SERVER_MODE: + raise SkipTest("Cant set environment variables in server mode") + cf = boto3.client("cloudformation", region_name="us-east-1") + template = { + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "Stack Lambda Test 1", + "Parameters": {}, + "Resources": { + "func1": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": {"S3Bucket": "bucket_123", "S3Key": "key_123"}, + "FunctionName": "func1", + "Handler": "handler.handler", + "Role": get_role_name(), + "Runtime": "python2.7", + "Description": "descr", + "MemorySize": 12345, + }, + }, + "func1version": { + "Type": "AWS::Lambda::Version", + "Properties": {"FunctionName": {"Ref": "func1"}}, + }, + "tab1": { + "Type": "AWS::DynamoDB::Table", + "Properties": { + "TableName": "tab1", + "KeySchema": [{"AttributeName": "attr1", "KeyType": "HASH"}], + "AttributeDefinitions": [ + {"AttributeName": "attr1", "AttributeType": "string"} + ], + "ProvisionedThroughput": { + "ReadCapacityUnits": 10, + "WriteCapacityUnits": 10, + }, + "StreamSpecification": {"StreamViewType": "KEYS_ONLY"}, + }, + }, + "func1mapping": { + "Type": "AWS::Lambda::EventSourceMapping", + "Properties": { + "FunctionName": {"Ref": "func1"}, + "EventSourceArn": {"Fn::GetAtt": ["tab1", "StreamArn"]}, + "StartingPosition": "0", + "BatchSize": 100, + "Enabled": True, + }, + }, + }, + } + validate_s3_before = os.environ.get("VALIDATE_LAMBDA_S3", "") + try: + os.environ["VALIDATE_LAMBDA_S3"] = "false" + cf.create_stack( + StackName="test_stack_lambda", TemplateBody=json.dumps(template), + ) + finally: + os.environ["VALIDATE_LAMBDA_S3"] = validate_s3_before + + resources = cf.list_stack_resources(StackName="test_stack_lambda")[ + "StackResourceSummaries" + ] + resources.should.have.length_of(4) + resource_types = [r["ResourceType"] for r in resources] + resource_types.should.contain("AWS::Lambda::Function") + resource_types.should.contain("AWS::Lambda::Version") + resource_types.should.contain("AWS::DynamoDB::Table") + resource_types.should.contain("AWS::Lambda::EventSourceMapping") + + +def get_role_name(): + with mock_iam(): + iam = boto3.client("iam", region_name="us-east-1") + try: + return iam.get_role(RoleName="my-role")["Role"]["Arn"] + except ClientError: + return iam.create_role( + RoleName="my-role", + AssumeRolePolicyDocument="some policy", + Path="/my-path/", + )["Role"]["Arn"] diff --git a/tests/test_cloudformation/test_cloudformation_stack_integration.py b/tests/test_cloudformation/test_cloudformation_stack_integration.py index bd0983b41..24706508e 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_integration.py +++ b/tests/test_cloudformation/test_cloudformation_stack_integration.py @@ -21,6 +21,8 @@ import boto.sqs import boto.vpc import boto3 import sure # noqa +import pytest +from copy import deepcopy from string import Template from moto import ( @@ -44,6 +46,7 @@ from moto import ( mock_route53_deprecated, mock_s3, mock_sns_deprecated, + mock_sqs, mock_sqs_deprecated, mock_elbv2, ) @@ -65,6 +68,7 @@ from tests.test_cloudformation.fixtures import ( ) +# Has boto3 equivalent @mock_cloudformation_deprecated() def test_stack_sqs_integration(): sqs_template = { @@ -88,6 +92,7 @@ def test_stack_sqs_integration(): queue.physical_resource_id.should.equal("my-queue") +# Has boto3 equivalent @mock_cloudformation_deprecated() def test_stack_list_resources(): sqs_template = { @@ -112,6 +117,7 @@ def test_stack_list_resources(): queue.physical_resource_id.should.equal("my-queue") +# Has boto3 equivalent @mock_cloudformation_deprecated() @mock_sqs_deprecated() def test_update_stack(): @@ -147,6 +153,7 @@ def test_update_stack(): ) +# Has boto3 equivalent @mock_cloudformation_deprecated() @mock_sqs_deprecated() def test_update_stack_and_remove_resource(): @@ -176,6 +183,7 @@ def test_update_stack_and_remove_resource(): queues.should.have.length_of(0) +# Has boto3 equivalent @mock_cloudformation_deprecated() @mock_sqs_deprecated() def test_update_stack_and_add_resource(): @@ -205,6 +213,7 @@ def test_update_stack_and_add_resource(): queues.should.have.length_of(1) +# Has boto3 equivalent @mock_ec2_deprecated() @mock_cloudformation_deprecated() def test_stack_ec2_integration(): @@ -233,6 +242,7 @@ def test_stack_ec2_integration(): instance.physical_resource_id.should.equal(ec2_instance.id) +# Has boto3 equivalent @mock_ec2_deprecated() @mock_elb_deprecated() @mock_cloudformation_deprecated() @@ -277,6 +287,7 @@ def test_stack_elb_integration_with_attached_ec2_instances(): list(load_balancer.availability_zones).should.equal(["us-east-1"]) +# Has boto3 equivalent @mock_elb_deprecated() @mock_cloudformation_deprecated() def test_stack_elb_integration_with_health_check(): @@ -322,6 +333,7 @@ def test_stack_elb_integration_with_health_check(): health_check.unhealthy_threshold.should.equal(2) +# Has boto3 equivalent @mock_elb_deprecated() @mock_cloudformation_deprecated() def test_stack_elb_integration_with_update(): @@ -363,6 +375,7 @@ def test_stack_elb_integration_with_update(): load_balancer.availability_zones[0].should.equal("us-west-1b") +# Has boto3 equivalent @mock_ec2_deprecated() @mock_redshift_deprecated() @mock_cloudformation_deprecated() @@ -409,6 +422,7 @@ def test_redshift_stack(): group.rules[0].grants[0].cidr_ip.should.equal("10.0.0.1/16") +# Has boto3 equivalent @mock_ec2_deprecated() @mock_cloudformation_deprecated() def test_stack_security_groups(): @@ -485,6 +499,7 @@ def test_stack_security_groups(): rule2.grants[0].group_id.should.equal(other_group.id) +# Has boto3 equivalent @mock_autoscaling_deprecated() @mock_elb_deprecated() @mock_cloudformation_deprecated() @@ -599,6 +614,7 @@ def test_autoscaling_group_with_elb(): instance.tags.keys().should_not.contain("not-propagated-test-tag") +# Has boto3 equivalent @mock_autoscaling_deprecated() @mock_cloudformation_deprecated() @mock_ec2_deprecated() @@ -672,6 +688,7 @@ def test_autoscaling_group_update(): running_instance_count.should.equal(2) +# Has boto3 equivalent @mock_ec2_deprecated() @mock_cloudformation_deprecated() def test_vpc_single_instance_in_subnet(): @@ -727,56 +744,7 @@ def test_vpc_single_instance_in_subnet(): eip_resource.physical_resource_id.should.equal(eip.public_ip) -@mock_cloudformation() -@mock_ec2() -@mock_rds2() -def test_rds_db_parameter_groups(): - ec2_conn = boto3.client("ec2", region_name="us-west-1") - ec2_conn.create_security_group( - GroupName="application", Description="Our Application Group" - ) - - template_json = json.dumps(rds_mysql_with_db_parameter_group.template) - cf_conn = boto3.client("cloudformation", "us-west-1") - cf_conn.create_stack( - StackName="test_stack", - TemplateBody=template_json, - Parameters=[ - {"ParameterKey": key, "ParameterValue": value} - for key, value in [ - ("DBInstanceIdentifier", "master_db"), - ("DBName", "my_db"), - ("DBUser", "my_user"), - ("DBPassword", "my_password"), - ("DBAllocatedStorage", "20"), - ("DBInstanceClass", "db.m1.medium"), - ("EC2SecurityGroup", "application"), - ("MultiAZ", "true"), - ] - ], - ) - - rds_conn = boto3.client("rds", region_name="us-west-1") - - db_parameter_groups = rds_conn.describe_db_parameter_groups() - len(db_parameter_groups["DBParameterGroups"]).should.equal(1) - db_parameter_group_name = db_parameter_groups["DBParameterGroups"][0][ - "DBParameterGroupName" - ] - - found_cloudformation_set_parameter = False - for db_parameter in rds_conn.describe_db_parameters( - DBParameterGroupName=db_parameter_group_name - )["Parameters"]: - if ( - db_parameter["ParameterName"] == "BACKLOG_QUEUE_LIMIT" - and db_parameter["ParameterValue"] == "2048" - ): - found_cloudformation_set_parameter = True - - found_cloudformation_set_parameter.should.equal(True) - - +# Has boto3 equivalent @mock_cloudformation_deprecated() @mock_ec2_deprecated() @mock_rds_deprecated() @@ -819,6 +787,7 @@ def test_rds_mysql_with_read_replica(): security_group.ec2_groups[0].name.should.equal("application") +# Has boto3 equivalent @mock_cloudformation_deprecated() @mock_ec2_deprecated() @mock_rds_deprecated() @@ -847,6 +816,7 @@ def test_rds_mysql_with_read_replica_in_vpc(): subnet_group.description.should.equal("my db subnet group") +# Has boto3 equivalent @mock_autoscaling_deprecated() @mock_iam_deprecated() @mock_cloudformation_deprecated() @@ -1002,6 +972,7 @@ def test_iam_roles(): {r.physical_resource_id for r in role_resources}.should.equal(set(role_names)) +# Has boto3 equivalent @mock_ec2_deprecated() @mock_cloudformation_deprecated() def test_single_instance_with_ebs_volume(): @@ -1033,6 +1004,7 @@ def test_single_instance_with_ebs_volume(): ebs_volumes[0].physical_resource_id.should.equal(volume.id) +# Has boto3 equivalent @mock_cloudformation_deprecated() def test_create_template_without_required_param(): template_json = json.dumps(single_instance_with_ebs_volume.template) @@ -1042,6 +1014,18 @@ def test_create_template_without_required_param(): ).should.throw(BotoServerError) +@mock_cloudformation +def test_create_template_without_required_param_boto3(): + template_json = json.dumps(single_instance_with_ebs_volume.template) + cf = boto3.client("cloudformation", region_name="us-west-1") + with pytest.raises(ClientError) as ex: + cf.create_stack(StackName="test_stack", TemplateBody=template_json) + err = ex.value.response["Error"] + err.should.have.key("Code").equal("Missing Parameter") + err.should.have.key("Message").equal("Missing parameter KeyName") + + +# Has boto3 equivalent @mock_ec2_deprecated() @mock_cloudformation_deprecated() def test_classic_eip(): @@ -1059,6 +1043,7 @@ def test_classic_eip(): cfn_eip.physical_resource_id.should.equal(eip.public_ip) +# Has boto3 equivalent @mock_ec2_deprecated() @mock_cloudformation_deprecated() def test_vpc_eip(): @@ -1076,6 +1061,7 @@ def test_vpc_eip(): cfn_eip.physical_resource_id.should.equal(eip.public_ip) +# Has boto3 equivalent @mock_ec2_deprecated() @mock_cloudformation_deprecated() def test_fn_join(): @@ -1090,6 +1076,21 @@ def test_fn_join(): fn_join_output.value.should.equal("test eip:{0}".format(eip.public_ip)) +@mock_ec2 +@mock_cloudformation +def test_fn_join_boto3(): + template_json = json.dumps(fn_join.template) + cf = boto3.client("cloudformation", region_name="us-west-1") + cf.create_stack(StackName="test_stack", TemplateBody=template_json) + ec2 = boto3.client("ec2", region_name="us-west-1") + eip = ec2.describe_addresses()["Addresses"][0] + + stack = cf.describe_stacks()["Stacks"][0] + fn_join_output = stack["Outputs"][0] + fn_join_output["OutputValue"].should.equal("test eip:{0}".format(eip["PublicIp"])) + + +# Has boto3 equivalent @mock_cloudformation_deprecated() @mock_sqs_deprecated() def test_conditional_resources(): @@ -1128,6 +1129,43 @@ def test_conditional_resources(): list(sqs_conn.get_all_queues()).should.have.length_of(1) +@mock_cloudformation +@mock_sqs +def test_conditional_resources_boto3(): + sqs_template = { + "AWSTemplateFormatVersion": "2010-09-09", + "Parameters": { + "EnvType": {"Description": "Environment type.", "Type": "String"} + }, + "Conditions": {"CreateQueue": {"Fn::Equals": [{"Ref": "EnvType"}, "prod"]}}, + "Resources": { + "QueueGroup": { + "Condition": "CreateQueue", + "Type": "AWS::SQS::Queue", + "Properties": {"QueueName": "my-queue", "VisibilityTimeout": 60}, + } + }, + } + sqs_template_json = json.dumps(sqs_template) + + cf = boto3.client("cloudformation", region_name="us-west-1") + cf.create_stack( + StackName="test_stack_without_queue", + TemplateBody=sqs_template_json, + Parameters=[{"ParameterKey": "EnvType", "ParameterValue": "staging"}], + ) + sqs = boto3.client("sqs", region_name="us-west-1") + sqs.list_queues().shouldnt.have.key("QueueUrls") + + cf.create_stack( + StackName="test_stack_with_queue", + TemplateBody=sqs_template_json, + Parameters=[{"ParameterKey": "EnvType", "ParameterValue": "prod"}], + ) + sqs.list_queues()["QueueUrls"].should.have.length_of(1) + + +# Has boto3 equivalent @mock_cloudformation_deprecated() @mock_ec2_deprecated() def test_conditional_if_handling(): @@ -1173,6 +1211,51 @@ def test_conditional_if_handling(): ec2_instance.image_id.should.equal(EXAMPLE_AMI_ID) +@mock_cloudformation +@mock_ec2 +def test_conditional_if_handling_boto3(): + dummy_template = { + "AWSTemplateFormatVersion": "2010-09-09", + "Conditions": {"EnvEqualsPrd": {"Fn::Equals": [{"Ref": "ENV"}, "prd"]}}, + "Parameters": { + "ENV": { + "Default": "dev", + "Description": "Deployment environment for the stack (dev/prd)", + "Type": "String", + } + }, + "Description": "Stack 1", + "Resources": { + "App1": { + "Properties": { + "ImageId": { + "Fn::If": ["EnvEqualsPrd", EXAMPLE_AMI_ID, EXAMPLE_AMI_ID2] + } + }, + "Type": "AWS::EC2::Instance", + } + }, + } + dummy_template_json = json.dumps(dummy_template) + + cf = boto3.client("cloudformation", region_name="us-west-1") + cf.create_stack(StackName="test_stack", TemplateBody=dummy_template_json) + ec2 = boto3.client("ec2", region_name="us-west-1") + ec2_instance = ec2.describe_instances()["Reservations"][0]["Instances"][0] + ec2_instance["ImageId"].should.equal(EXAMPLE_AMI_ID2) + + cf = boto3.client("cloudformation", region_name="us-west-2") + cf.create_stack( + StackName="test_stack", + TemplateBody=dummy_template_json, + Parameters=[{"ParameterKey": "ENV", "ParameterValue": "prd"}], + ) + ec2 = boto3.client("ec2", region_name="us-west-2") + ec2_instance = ec2.describe_instances()["Reservations"][0]["Instances"][0] + ec2_instance["ImageId"].should.equal(EXAMPLE_AMI_ID) + + +# Has boto3 equivalent @mock_cloudformation_deprecated() @mock_ec2_deprecated() def test_cloudformation_mapping(): @@ -1217,6 +1300,49 @@ def test_cloudformation_mapping(): ec2_instance.image_id.should.equal(EXAMPLE_AMI_ID) +@mock_cloudformation +@mock_ec2 +def test_cloudformation_mapping_boto3(): + dummy_template = { + "AWSTemplateFormatVersion": "2010-09-09", + "Mappings": { + "RegionMap": { + "us-east-1": {"32": EXAMPLE_AMI_ID, "64": "n/a"}, + "us-west-1": {"32": EXAMPLE_AMI_ID2, "64": "n/a"}, + "eu-west-1": {"32": "n/a", "64": "n/a"}, + "ap-southeast-1": {"32": "n/a", "64": "n/a"}, + "ap-northeast-1": {"32": "n/a", "64": "n/a"}, + } + }, + "Resources": { + "WebServer": { + "Type": "AWS::EC2::Instance", + "Properties": { + "ImageId": { + "Fn::FindInMap": ["RegionMap", {"Ref": "AWS::Region"}, "32"] + }, + "InstanceType": "m1.small", + }, + } + }, + } + + dummy_template_json = json.dumps(dummy_template) + + cf = boto3.client("cloudformation", region_name="us-east-1") + cf.create_stack(StackName="test_stack1", TemplateBody=dummy_template_json) + ec2 = boto3.client("ec2", region_name="us-east-1") + ec2_instance = ec2.describe_instances()["Reservations"][0]["Instances"][0] + ec2_instance["ImageId"].should.equal(EXAMPLE_AMI_ID) + + cf = boto3.client("cloudformation", region_name="us-west-1") + cf.create_stack(StackName="test_stack1", TemplateBody=dummy_template_json) + ec2 = boto3.client("ec2", region_name="us-west-1") + ec2_instance = ec2.describe_instances()["Reservations"][0]["Instances"][0] + ec2_instance["ImageId"].should.equal(EXAMPLE_AMI_ID2) + + +# Has boto3 equivalent @mock_cloudformation_deprecated() @mock_route53_deprecated() def test_route53_roundrobin(): @@ -1259,6 +1385,7 @@ def test_route53_roundrobin(): output.value.should.equal("arn:aws:route53:::hostedzone/{0}".format(zone_id)) +# Has boto3 equivalent @mock_cloudformation_deprecated() @mock_ec2_deprecated() @mock_route53_deprecated() @@ -1292,6 +1419,7 @@ def test_route53_ec2_instance_with_public_ip(): record_set1.resource_records[0].should.equal("10.0.0.25") +# Has boto3 equivalent @mock_cloudformation_deprecated() @mock_route53_deprecated() def test_route53_associate_health_check(): @@ -1330,6 +1458,7 @@ def test_route53_associate_health_check(): record_set.health_check.should.equal(health_check_id) +# Has boto3 equivalent @mock_cloudformation_deprecated() @mock_route53_deprecated() def test_route53_with_update(): @@ -1353,10 +1482,11 @@ def test_route53_with_update(): record_set = rrsets[0] record_set.resource_records.should.equal(["my.example.com"]) - route53_health_check.template["Resources"]["myDNSRecord"]["Properties"][ - "ResourceRecords" - ] = ["my_other.example.com"] - template_json = json.dumps(route53_health_check.template) + template = deepcopy(route53_health_check.template) + template["Resources"]["myDNSRecord"]["Properties"]["ResourceRecords"] = [ + "my_other.example.com" + ] + template_json = json.dumps(template) cf_conn.update_stack("test_stack", template_body=template_json) zones = route53_conn.get_all_hosted_zones()["ListHostedZonesResponse"][ @@ -1374,6 +1504,7 @@ def test_route53_with_update(): record_set.resource_records.should.equal(["my_other.example.com"]) +# Has boto3 equivalent @mock_cloudformation_deprecated() @mock_sns_deprecated() def test_sns_topic(): @@ -1424,6 +1555,7 @@ def test_sns_topic(): topic_arn_output.value.should.equal(topic_arn) +# Has boto3 equivalent @mock_cloudformation_deprecated @mock_ec2_deprecated def test_vpc_gateway_attachment_creation_should_attach_itself_to_vpc(): @@ -1461,6 +1593,7 @@ def test_vpc_gateway_attachment_creation_should_attach_itself_to_vpc(): igws.should.have.length_of(1) +# Has boto3 equivalent @mock_cloudformation_deprecated @mock_ec2_deprecated def test_vpc_peering_creation(): @@ -1485,6 +1618,7 @@ def test_vpc_peering_creation(): peering_connections.should.have.length_of(1) +# Has boto3 equivalent @mock_cloudformation_deprecated @mock_ec2_deprecated def test_multiple_security_group_ingress_separate_from_security_group_by_id(): @@ -1538,6 +1672,7 @@ def test_multiple_security_group_ingress_separate_from_security_group_by_id(): security_group1.rules[0].to_port.should.equal("8080") +# Has boto3 equivalent @mock_cloudformation_deprecated @mock_ec2_deprecated def test_security_group_ingress_separate_from_security_group_by_id(): @@ -1585,6 +1720,7 @@ def test_security_group_ingress_separate_from_security_group_by_id(): security_group1.rules[0].to_port.should.equal("8080") +# Has boto3 equivalent @mock_cloudformation_deprecated @mock_ec2_deprecated def test_security_group_ingress_separate_from_security_group_by_id_using_vpc(): @@ -1642,6 +1778,7 @@ def test_security_group_ingress_separate_from_security_group_by_id_using_vpc(): security_group1.rules[0].to_port.should.equal("8080") +# Has boto3 equivalent @mock_cloudformation_deprecated @mock_ec2_deprecated def test_security_group_with_update(): @@ -1676,6 +1813,7 @@ def test_security_group_with_update(): security_group.vpc_id.should.equal(vpc2.id) +# Has boto3 equivalent @mock_cloudformation_deprecated @mock_ec2_deprecated def test_subnets_should_be_created_with_availability_zone(): @@ -1702,6 +1840,7 @@ def test_subnets_should_be_created_with_availability_zone(): subnet.availability_zone.should.equal("us-west-1b") +# Has boto3 equivalent @mock_cloudformation_deprecated @mock_datapipeline_deprecated def test_datapipeline(): diff --git a/tests/test_datapipeline/test_datapipeline_cloudformation.py b/tests/test_datapipeline/test_datapipeline_cloudformation.py new file mode 100644 index 000000000..68f8e7d3c --- /dev/null +++ b/tests/test_datapipeline/test_datapipeline_cloudformation.py @@ -0,0 +1,70 @@ +import boto3 +import json +import sure # noqa + + +from moto import mock_cloudformation, mock_datapipeline + + +@mock_cloudformation +@mock_datapipeline +def test_datapipeline(): + dp_template = { + "AWSTemplateFormatVersion": "2010-09-09", + "Resources": { + "dataPipeline": { + "Properties": { + "Activate": "true", + "Name": "testDataPipeline", + "PipelineObjects": [ + { + "Fields": [ + { + "Key": "failureAndRerunMode", + "StringValue": "CASCADE", + }, + {"Key": "scheduleType", "StringValue": "cron"}, + {"Key": "schedule", "RefValue": "DefaultSchedule"}, + { + "Key": "pipelineLogUri", + "StringValue": "s3://bucket/logs", + }, + {"Key": "type", "StringValue": "Default"}, + ], + "Id": "Default", + "Name": "Default", + }, + { + "Fields": [ + { + "Key": "startDateTime", + "StringValue": "1970-01-01T01:00:00", + }, + {"Key": "period", "StringValue": "1 Day"}, + {"Key": "type", "StringValue": "Schedule"}, + ], + "Id": "DefaultSchedule", + "Name": "RunOnce", + }, + ], + "PipelineTags": [], + }, + "Type": "AWS::DataPipeline::Pipeline", + } + }, + } + cf = boto3.client("cloudformation", region_name="us-east-1") + template_json = json.dumps(dp_template) + cf.create_stack(StackName="test_stack", TemplateBody=template_json) + + dp = boto3.client("datapipeline", region_name="us-east-1") + data_pipelines = dp.list_pipelines()["pipelineIdList"] + + data_pipelines.should.have.length_of(1) + data_pipelines[0]["name"].should.equal("testDataPipeline") + + stack_resources = cf.list_stack_resources(StackName="test_stack")[ + "StackResourceSummaries" + ] + stack_resources.should.have.length_of(1) + stack_resources[0]["PhysicalResourceId"].should.equal(data_pipelines[0]["id"]) diff --git a/tests/test_dynamodb2/test_dynamodb_cloudformation.py b/tests/test_dynamodb2/test_dynamodb_cloudformation.py new file mode 100644 index 000000000..bed5005c7 --- /dev/null +++ b/tests/test_dynamodb2/test_dynamodb_cloudformation.py @@ -0,0 +1,51 @@ +import boto3 +import json +import sure # noqa + +from moto import mock_cloudformation, mock_dynamodb2 + + +template_create_table = { + "AWSTemplateFormatVersion": "2010-09-09", + "Resources": { + "myDynamoDBTable": { + "Type": "AWS::DynamoDB::Table", + "Properties": { + "AttributeDefinitions": [ + {"AttributeName": "Name", "AttributeType": "S"}, + {"AttributeName": "Age", "AttributeType": "S"}, + ], + "KeySchema": [ + {"AttributeName": "Name", "KeyType": "HASH"}, + {"AttributeName": "Age", "KeyType": "RANGE"}, + ], + "ProvisionedThroughput": { + "ReadCapacityUnits": 5, + "WriteCapacityUnits": 5, + }, + "TableName": "Person", + }, + } + }, +} + + +@mock_dynamodb2 +@mock_cloudformation +def test_delete_stack_dynamo_template_boto3(): + conn = boto3.client("cloudformation", region_name="us-east-1") + dynamodb_client = boto3.client("dynamodb", region_name="us-east-1") + + conn.create_stack( + StackName="test_stack", TemplateBody=json.dumps(template_create_table) + ) + table_desc = dynamodb_client.list_tables() + len(table_desc.get("TableNames")).should.equal(1) + + conn.delete_stack(StackName="test_stack") + table_desc = dynamodb_client.list_tables() + len(table_desc.get("TableNames")).should.equal(0) + + conn.create_stack( + StackName="test_stack", TemplateBody=json.dumps(template_create_table) + ) diff --git a/tests/test_ec2/test_ec2_cloudformation.py b/tests/test_ec2/test_ec2_cloudformation.py index 5a7ac0d8f..df862d5cb 100644 --- a/tests/test_ec2/test_ec2_cloudformation.py +++ b/tests/test_ec2/test_ec2_cloudformation.py @@ -1,16 +1,124 @@ +from botocore.exceptions import ClientError from moto import mock_cloudformation_deprecated, mock_ec2_deprecated from moto import mock_cloudformation, mock_ec2 from tests import EXAMPLE_AMI_ID +from tests.test_cloudformation.fixtures import ec2_classic_eip +from tests.test_cloudformation.fixtures import single_instance_with_ebs_volume +from tests.test_cloudformation.fixtures import vpc_eip from tests.test_cloudformation.fixtures import vpc_eni +from tests.test_cloudformation.fixtures import vpc_single_instance_in_subnet import boto import boto.ec2 import boto.cloudformation import boto.vpc import boto3 import json +import pytest import sure # noqa +template_vpc = { + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "Create VPC", + "Resources": { + "VPC": {"Properties": {"CidrBlock": "192.168.0.0/16"}, "Type": "AWS::EC2::VPC"} + }, +} + + +@mock_ec2 +@mock_cloudformation +def test_vpc_single_instance_in_subnet(): + template_json = json.dumps(vpc_single_instance_in_subnet.template) + cf = boto3.client("cloudformation", region_name="us-west-1") + cf.create_stack( + StackName="test_stack", + TemplateBody=template_json, + Parameters=[{"ParameterKey": "KeyName", "ParameterValue": "my_key"}], + ) + + ec2 = boto3.client("ec2", region_name="us-west-1") + + vpc = ec2.describe_vpcs(Filters=[{"Name": "cidrBlock", "Values": ["10.0.0.0/16"]}])[ + "Vpcs" + ][0] + vpc["CidrBlock"].should.equal("10.0.0.0/16") + + ec2.describe_internet_gateways()["InternetGateways"].should.have.length_of(1) + + subnet = ec2.describe_subnets( + Filters=[{"Name": "vpcId", "Values": [vpc["VpcId"]]}] + )["Subnets"][0] + subnet["VpcId"].should.equal(vpc["VpcId"]) + + ec2 = boto3.client("ec2", region_name="us-west-1") + reservation = ec2.describe_instances()["Reservations"][0] + instance = reservation["Instances"][0] + instance["Tags"].should.contain({"Key": "Foo", "Value": "Bar"}) + # Check that the EIP is attached the the EC2 instance + eip = ec2.describe_addresses()["Addresses"][0] + eip["Domain"].should.equal("vpc") + eip["InstanceId"].should.equal(instance["InstanceId"]) + + security_group = ec2.describe_security_groups( + Filters=[{"Name": "vpc-id", "Values": [vpc["VpcId"]]}] + )["SecurityGroups"][0] + security_group["VpcId"].should.equal(vpc["VpcId"]) + + stack = cf.describe_stacks(StackName="test_stack")["Stacks"][0] + + vpc["Tags"].should.contain({"Key": "Application", "Value": stack["StackId"]}) + + resources = cf.list_stack_resources(StackName="test_stack")[ + "StackResourceSummaries" + ] + vpc_resource = [ + resource + for resource in resources + if resource["ResourceType"] == "AWS::EC2::VPC" + ][0] + vpc_resource["PhysicalResourceId"].should.equal(vpc["VpcId"]) + + subnet_resource = [ + resource + for resource in resources + if resource["ResourceType"] == "AWS::EC2::Subnet" + ][0] + subnet_resource["PhysicalResourceId"].should.equal(subnet["SubnetId"]) + + eip_resource = [ + resource + for resource in resources + if resource["ResourceType"] == "AWS::EC2::EIP" + ][0] + eip_resource["PhysicalResourceId"].should.equal(eip["PublicIp"]) + + +@mock_cloudformation +@mock_ec2 +def test_delete_stack_with_resource_missing_delete_attr(): + cf = boto3.client("cloudformation", region_name="us-east-1") + ec2 = boto3.client("ec2", region_name="us-east-1") + name = "test_stack" + + cf.create_stack(StackName=name, TemplateBody=json.dumps(template_vpc)) + cf.describe_stacks(StackName=name)["Stacks"].should.have.length_of(1) + ec2.describe_vpcs()["Vpcs"].should.have.length_of(2) + + cf.delete_stack( + StackName=name + ) # should succeed, despite the fact that the resource itself cannot be deleted + with pytest.raises(ClientError) as exc: + cf.describe_stacks(StackName=name) + err = exc.value.response["Error"] + err.should.have.key("Code").equals("ValidationError") + err.should.have.key("Message").equals("Stack with id test_stack does not exist") + + # We still have two VPCs, as the VPC-object does not have a delete-method yet + ec2.describe_vpcs()["Vpcs"].should.have.length_of(2) + + +# Has boto3 equivalent @mock_ec2_deprecated @mock_cloudformation_deprecated def test_elastic_network_interfaces_cloudformation(): @@ -35,6 +143,34 @@ def test_elastic_network_interfaces_cloudformation(): outputs["ENIIpAddress"].should.equal(eni.private_ip_addresses[0].private_ip_address) +@mock_ec2 +@mock_cloudformation +def test_elastic_network_interfaces_cloudformation_boto3(): + template = vpc_eni.template + template_json = json.dumps(template) + cf = boto3.client("cloudformation", region_name="us-west-1") + cf.create_stack(StackName="test_stack", TemplateBody=template_json) + ec2 = boto3.client("ec2", region_name="us-west-1") + eni = ec2.describe_network_interfaces()["NetworkInterfaces"][0] + eni["PrivateIpAddresses"].should.have.length_of(1) + private_ip_address = eni["PrivateIpAddresses"][0]["PrivateIpAddress"] + + resources = cf.list_stack_resources(StackName="test_stack")[ + "StackResourceSummaries" + ] + cfn_eni = [ + resource + for resource in resources + if resource["ResourceType"] == "AWS::EC2::NetworkInterface" + ][0] + cfn_eni["PhysicalResourceId"].should.equal(eni["NetworkInterfaceId"]) + + outputs = cf.describe_stacks(StackName="test_stack")["Stacks"][0]["Outputs"] + outputs.should.contain( + {"OutputKey": "ENIIpAddress", "OutputValue": private_ip_address} + ) + + @mock_ec2 @mock_cloudformation def test_volume_size_through_cloudformation(): @@ -63,7 +199,15 @@ def test_volume_size_through_cloudformation(): } template_json = json.dumps(volume_template) cf.create_stack(StackName="test_stack", TemplateBody=template_json) - instances = ec2.describe_instances() + + resource = cf.list_stack_resources(StackName="test_stack")[ + "StackResourceSummaries" + ][0] + resource.should.have.key("LogicalResourceId").being.equal("testInstance") + resource.should.have.key("PhysicalResourceId").shouldnt.be.none + resource.should.have.key("ResourceType").being.equal("AWS::EC2::Instance") + + instances = ec2.describe_instances(InstanceIds=[resource["PhysicalResourceId"]]) volume = instances["Reservations"][0]["Instances"][0]["BlockDeviceMappings"][0][ "Ebs" ] @@ -72,6 +216,7 @@ def test_volume_size_through_cloudformation(): volumes["Volumes"][0]["Size"].should.equal(50) +# Has boto3 equivalent @mock_ec2_deprecated @mock_cloudformation_deprecated def test_subnet_tags_through_cloudformation(): @@ -102,3 +247,405 @@ def test_subnet_tags_through_cloudformation(): subnet = vpc_conn.get_all_subnets(filters={"cidrBlock": "10.0.0.0/24"})[0] subnet.tags["foo"].should.equal("bar") subnet.tags["blah"].should.equal("baz") + + +@mock_ec2 +@mock_cloudformation +def test_subnet_tags_through_cloudformation_boto3(): + ec2 = boto3.client("ec2", region_name="us-west-1") + ec2_res = boto3.resource("ec2", region_name="us-west-1") + vpc = ec2_res.create_vpc(CidrBlock="10.0.0.0/16") + + subnet_template = { + "AWSTemplateFormatVersion": "2010-09-09", + "Resources": { + "testSubnet": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": vpc.id, + "CidrBlock": "10.0.0.0/24", + "AvailabilityZone": "us-west-1b", + "Tags": [ + {"Key": "foo", "Value": "bar"}, + {"Key": "blah", "Value": "baz"}, + ], + }, + } + }, + } + template_json = json.dumps(subnet_template) + cf = boto3.client("cloudformation", region_name="us-west-1") + cf.create_stack(StackName="test_stack", TemplateBody=template_json) + + subnet = ec2.describe_subnets( + Filters=[{"Name": "cidrBlock", "Values": ["10.0.0.0/24"]}] + )["Subnets"][0] + subnet["Tags"].should.contain({"Key": "foo", "Value": "bar"}) + subnet["Tags"].should.contain({"Key": "blah", "Value": "baz"}) + + +@mock_ec2 +@mock_cloudformation +def test_single_instance_with_ebs_volume(): + template_json = json.dumps(single_instance_with_ebs_volume.template) + cf = boto3.client("cloudformation", region_name="us-west-1") + cf.create_stack( + StackName="test_stack", + TemplateBody=template_json, + Parameters=[{"ParameterKey": "KeyName", "ParameterValue": "key_name"}], + ) + + ec2 = boto3.client("ec2", region_name="us-west-1") + ec2_instance = ec2.describe_instances()["Reservations"][0]["Instances"][0] + + volumes = ec2.describe_volumes()["Volumes"] + # Grab the mounted drive + volume = [ + volume for volume in volumes if volume["Attachments"][0]["Device"] == "/dev/sdh" + ][0] + volume["State"].should.equal("in-use") + volume["Attachments"][0]["InstanceId"].should.equal(ec2_instance["InstanceId"]) + + resources = cf.list_stack_resources(StackName="test_stack")[ + "StackResourceSummaries" + ] + ebs_volumes = [ + resource + for resource in resources + if resource["ResourceType"] == "AWS::EC2::Volume" + ] + ebs_volumes[0]["PhysicalResourceId"].should.equal(volume["VolumeId"]) + + +@mock_ec2 +@mock_cloudformation +def test_classic_eip(): + template_json = json.dumps(ec2_classic_eip.template) + cf = boto3.client("cloudformation", region_name="us-west-1") + cf.create_stack(StackName="test_stack", TemplateBody=template_json) + ec2 = boto3.client("ec2", region_name="us-west-1") + eip = ec2.describe_addresses()["Addresses"][0] + + resources = cf.list_stack_resources(StackName="test_stack")[ + "StackResourceSummaries" + ] + cfn_eip = [ + resource + for resource in resources + if resource["ResourceType"] == "AWS::EC2::EIP" + ][0] + cfn_eip["PhysicalResourceId"].should.equal(eip["PublicIp"]) + + +@mock_ec2 +@mock_cloudformation +def test_vpc_eip(): + template_json = json.dumps(vpc_eip.template) + cf = boto3.client("cloudformation", region_name="us-west-1") + cf.create_stack(StackName="test_stack", TemplateBody=template_json) + ec2 = boto3.client("ec2", region_name="us-west-1") + eip = ec2.describe_addresses()["Addresses"][0] + + resources = cf.list_stack_resources(StackName="test_stack")[ + "StackResourceSummaries" + ] + cfn_eip = [ + resource + for resource in resources + if resource["ResourceType"] == "AWS::EC2::EIP" + ][0] + cfn_eip["PhysicalResourceId"].should.equal(eip["PublicIp"]) + + +@mock_cloudformation +@mock_ec2 +def test_vpc_gateway_attachment_creation_should_attach_itself_to_vpc(): + template = { + "AWSTemplateFormatVersion": "2010-09-09", + "Resources": { + "internetgateway": {"Type": "AWS::EC2::InternetGateway"}, + "testvpc": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": "true", + "EnableDnsSupport": "true", + "InstanceTenancy": "default", + }, + }, + "vpcgatewayattachment": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "InternetGatewayId": {"Ref": "internetgateway"}, + "VpcId": {"Ref": "testvpc"}, + }, + }, + }, + } + + template_json = json.dumps(template) + cf = boto3.client("cloudformation", region_name="us-west-1") + cf.create_stack(StackName="test_stack", TemplateBody=template_json) + + ec2 = boto3.client("ec2", region_name="us-west-1") + vpc = ec2.describe_vpcs(Filters=[{"Name": "cidrBlock", "Values": ["10.0.0.0/16"]}])[ + "Vpcs" + ][0] + + igws = ec2.describe_internet_gateways( + Filters=[{"Name": "attachment.vpc-id", "Values": [vpc["VpcId"]]}] + )["InternetGateways"] + igws.should.have.length_of(1) + + +@mock_cloudformation +@mock_ec2 +def test_vpc_peering_creation(): + ec2 = boto3.resource("ec2", region_name="us-west-1") + ec2_client = boto3.client("ec2", region_name="us-west-1") + vpc_source = ec2.create_vpc(CidrBlock="10.0.0.0/16") + peer_vpc = ec2.create_vpc(CidrBlock="10.1.0.0/16") + template = { + "AWSTemplateFormatVersion": "2010-09-09", + "Resources": { + "vpcpeeringconnection": { + "Type": "AWS::EC2::VPCPeeringConnection", + "Properties": {"PeerVpcId": peer_vpc.id, "VpcId": vpc_source.id}, + } + }, + } + + template_json = json.dumps(template) + cf = boto3.client("cloudformation", region_name="us-west-1") + cf.create_stack(StackName="test_stack", TemplateBody=template_json) + + peering_connections = ec2_client.describe_vpc_peering_connections()[ + "VpcPeeringConnections" + ] + peering_connections.should.have.length_of(1) + + +@mock_cloudformation +@mock_ec2 +def test_multiple_security_group_ingress_separate_from_security_group_by_id(): + template = { + "AWSTemplateFormatVersion": "2010-09-09", + "Resources": { + "test-security-group1": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "test security group", + "Tags": [{"Key": "sg-name", "Value": "sg1"}], + }, + }, + "test-security-group2": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "test security group", + "Tags": [{"Key": "sg-name", "Value": "sg2"}], + }, + }, + "test-sg-ingress": { + "Type": "AWS::EC2::SecurityGroupIngress", + "Properties": { + "GroupId": {"Ref": "test-security-group1"}, + "IpProtocol": "tcp", + "FromPort": "80", + "ToPort": "8080", + "SourceSecurityGroupId": {"Ref": "test-security-group2"}, + }, + }, + }, + } + + template_json = json.dumps(template) + cf = boto3.client("cloudformation", region_name="us-west-1") + cf.create_stack(StackName="test_stack", TemplateBody=template_json) + ec2 = boto3.client("ec2", region_name="us-west-1") + + security_group1 = get_secgroup_by_tag(ec2, "sg1") + security_group2 = get_secgroup_by_tag(ec2, "sg2") + + security_group1["IpPermissions"].should.have.length_of(1) + security_group1["IpPermissions"][0]["UserIdGroupPairs"].should.have.length_of(1) + security_group1["IpPermissions"][0]["UserIdGroupPairs"][0]["GroupId"].should.equal( + security_group2["GroupId"] + ) + security_group1["IpPermissions"][0]["IpProtocol"].should.equal("tcp") + security_group1["IpPermissions"][0]["FromPort"].should.equal(80) + security_group1["IpPermissions"][0]["ToPort"].should.equal(8080) + + +@mock_cloudformation +@mock_ec2 +def test_security_group_ingress_separate_from_security_group_by_id(): + ec2 = boto3.client("ec2", region_name="us-west-1") + ec2.create_security_group( + GroupName="test-security-group1", Description="test security group" + ) + + template = { + "AWSTemplateFormatVersion": "2010-09-09", + "Resources": { + "test-security-group2": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "test security group", + "Tags": [{"Key": "sg-name", "Value": "sg2"}], + }, + }, + "test-sg-ingress": { + "Type": "AWS::EC2::SecurityGroupIngress", + "Properties": { + "GroupName": "test-security-group1", + "IpProtocol": "tcp", + "FromPort": "80", + "ToPort": "8080", + "SourceSecurityGroupId": {"Ref": "test-security-group2"}, + }, + }, + }, + } + + template_json = json.dumps(template) + cf = boto3.client("cloudformation", region_name="us-west-1") + cf.create_stack(StackName="test_stack", TemplateBody=template_json) + security_group1 = ec2.describe_security_groups(GroupNames=["test-security-group1"])[ + "SecurityGroups" + ][0] + security_group2 = get_secgroup_by_tag(ec2, "sg2") + + security_group1["IpPermissions"].should.have.length_of(1) + security_group1["IpPermissions"][0]["UserIdGroupPairs"].should.have.length_of(1) + security_group1["IpPermissions"][0]["UserIdGroupPairs"][0]["GroupId"].should.equal( + security_group2["GroupId"] + ) + security_group1["IpPermissions"][0]["IpProtocol"].should.equal("tcp") + security_group1["IpPermissions"][0]["FromPort"].should.equal(80) + security_group1["IpPermissions"][0]["ToPort"].should.equal(8080) + + +@mock_cloudformation +@mock_ec2 +def test_security_group_ingress_separate_from_security_group_by_id_using_vpc(): + ec2 = boto3.resource("ec2", region_name="us-west-1") + ec2_client = boto3.client("ec2", region_name="us-west-1") + vpc = ec2.create_vpc(CidrBlock="10.0.0.0/16") + + template = { + "AWSTemplateFormatVersion": "2010-09-09", + "Resources": { + "test-security-group1": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "test security group", + "VpcId": vpc.id, + "Tags": [{"Key": "sg-name", "Value": "sg1"}], + }, + }, + "test-security-group2": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "test security group", + "VpcId": vpc.id, + "Tags": [{"Key": "sg-name", "Value": "sg2"}], + }, + }, + "test-sg-ingress": { + "Type": "AWS::EC2::SecurityGroupIngress", + "Properties": { + "GroupId": {"Ref": "test-security-group1"}, + "VpcId": vpc.id, + "IpProtocol": "tcp", + "FromPort": "80", + "ToPort": "8080", + "SourceSecurityGroupId": {"Ref": "test-security-group2"}, + }, + }, + }, + } + + template_json = json.dumps(template) + cf = boto3.client("cloudformation", region_name="us-west-1") + cf.create_stack(StackName="test_stack", TemplateBody=template_json) + security_group1 = get_secgroup_by_tag(ec2_client, "sg1") + security_group2 = get_secgroup_by_tag(ec2_client, "sg2") + + security_group1["IpPermissions"].should.have.length_of(1) + security_group1["IpPermissions"][0]["UserIdGroupPairs"].should.have.length_of(1) + security_group1["IpPermissions"][0]["UserIdGroupPairs"][0]["GroupId"].should.equal( + security_group2["GroupId"] + ) + security_group1["IpPermissions"][0]["IpProtocol"].should.equal("tcp") + security_group1["IpPermissions"][0]["FromPort"].should.equal(80) + security_group1["IpPermissions"][0]["ToPort"].should.equal(8080) + + +@mock_cloudformation +@mock_ec2 +def test_security_group_with_update(): + ec2 = boto3.resource("ec2", region_name="us-west-1") + ec2_client = boto3.client("ec2", region_name="us-west-1") + vpc1 = ec2.create_vpc(CidrBlock="10.0.0.0/16") + vpc2 = ec2.create_vpc(CidrBlock="10.1.0.0/16") + + template = { + "AWSTemplateFormatVersion": "2010-09-09", + "Resources": { + "test-security-group": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "test security group", + "VpcId": vpc1.id, + "Tags": [{"Key": "sg-name", "Value": "sg"}], + }, + } + }, + } + + template_json = json.dumps(template) + cf = boto3.client("cloudformation", region_name="us-west-1") + cf.create_stack(StackName="test_stack", TemplateBody=template_json) + security_group = get_secgroup_by_tag(ec2_client, "sg") + security_group["VpcId"].should.equal(vpc1.id) + + template["Resources"]["test-security-group"]["Properties"]["VpcId"] = vpc2.id + template_json = json.dumps(template) + cf.update_stack(StackName="test_stack", TemplateBody=template_json) + security_group = get_secgroup_by_tag(ec2_client, "sg") + security_group["VpcId"].should.equal(vpc2.id) + + +@mock_cloudformation +@mock_ec2 +def test_subnets_should_be_created_with_availability_zone(): + ec2 = boto3.resource("ec2", region_name="us-west-1") + ec2_client = boto3.client("ec2", region_name="us-west-1") + vpc = ec2.create_vpc(CidrBlock="10.0.0.0/16") + + subnet_template = { + "AWSTemplateFormatVersion": "2010-09-09", + "Resources": { + "testSubnet": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": vpc.id, + "CidrBlock": "10.0.0.0/24", + "AvailabilityZone": "us-west-1b", + }, + } + }, + } + cf = boto3.client("cloudformation", region_name="us-west-1") + template_json = json.dumps(subnet_template) + cf.create_stack(StackName="test_stack", TemplateBody=template_json) + subnet = ec2_client.describe_subnets( + Filters=[{"Name": "cidrBlock", "Values": ["10.0.0.0/24"]}] + )["Subnets"][0] + subnet["AvailabilityZone"].should.equal("us-west-1b") + + +def get_secgroup_by_tag(ec2, sg_): + return ec2.describe_security_groups( + Filters=[{"Name": "tag:sg-name", "Values": [sg_]}] + )["SecurityGroups"][0] diff --git a/tests/test_ec2/test_security_groups_cloudformation.py b/tests/test_ec2/test_security_groups_cloudformation.py index 56c93dff3..e3cdafc07 100644 --- a/tests/test_ec2/test_security_groups_cloudformation.py +++ b/tests/test_ec2/test_security_groups_cloudformation.py @@ -1,6 +1,8 @@ import boto3 +import json import sure # noqa from moto import mock_cloudformation, mock_ec2 +from tests import EXAMPLE_AMI_ID SEC_GROUP_INGRESS = """{ @@ -85,6 +87,44 @@ SEC_GROUP_INGRESS_WITHOUT_DESC = """{ } """ +SEC_GROUP_SOURCE = { + "AWSTemplateFormatVersion": "2010-09-09", + "Resources": { + "my-security-group": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": {"GroupDescription": "My other group"}, + }, + "Ec2Instance2": { + "Type": "AWS::EC2::Instance", + "Properties": { + "SecurityGroups": [{"Ref": "InstanceSecurityGroup"}], + "ImageId": EXAMPLE_AMI_ID, + }, + }, + "InstanceSecurityGroup": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "My security group", + "Tags": [{"Key": "bar", "Value": "baz"}], + "SecurityGroupIngress": [ + { + "IpProtocol": "tcp", + "FromPort": "22", + "ToPort": "22", + "CidrIp": "123.123.123.123/32", + }, + { + "IpProtocol": "tcp", + "FromPort": "80", + "ToPort": "8000", + "SourceSecurityGroupId": {"Ref": "my-security-group"}, + }, + ], + }, + }, + }, +} + @mock_cloudformation @mock_ec2 @@ -137,3 +177,46 @@ def test_security_group_ingress_without_description(): len(group["IpPermissions"]).should.be(1) ingress = group["IpPermissions"][0] ingress["IpRanges"].should.equal([{"CidrIp": "10.0.0.0/8"}]) + + +@mock_ec2 +@mock_cloudformation +def test_stack_security_groups(): + + template = json.dumps(SEC_GROUP_SOURCE) + + cf = boto3.client("cloudformation", region_name="us-west-1") + cf.create_stack( + StackName="security_group_stack", + TemplateBody=template, + Tags=[{"Key": "foo", "Value": "bar"}], + ) + + ec2 = boto3.client("ec2", region_name="us-west-1") + instance_group = ec2.describe_security_groups( + Filters=[{"Name": "description", "Values": ["My security group"]}] + )["SecurityGroups"][0] + instance_group.should.have.key("Description").equal("My security group") + instance_group.should.have.key("Tags") + instance_group["Tags"].should.contain({"Key": "bar", "Value": "baz"}) + instance_group["Tags"].should.contain({"Key": "foo", "Value": "bar"}) + other_group = ec2.describe_security_groups( + Filters=[{"Name": "description", "Values": ["My other group"]}] + )["SecurityGroups"][0] + + ec2_instance = ec2.describe_instances()["Reservations"][0]["Instances"][0] + + ec2_instance["NetworkInterfaces"][0]["Groups"][0]["GroupId"].should.equal( + instance_group["GroupId"] + ) + + rule1, rule2 = instance_group["IpPermissions"] + int(rule1["ToPort"]).should.equal(22) + int(rule1["FromPort"]).should.equal(22) + rule1["IpRanges"][0]["CidrIp"].should.equal("123.123.123.123/32") + rule1["IpProtocol"].should.equal("tcp") + + int(rule2["ToPort"]).should.equal(8000) + int(rule2["FromPort"]).should.equal(80) + rule2["IpProtocol"].should.equal("tcp") + rule2["UserIdGroupPairs"][0]["GroupId"].should.equal(other_group["GroupId"]) diff --git a/tests/test_elb/test_elb_cloudformation.py b/tests/test_elb/test_elb_cloudformation.py new file mode 100644 index 000000000..938810206 --- /dev/null +++ b/tests/test_elb/test_elb_cloudformation.py @@ -0,0 +1,141 @@ +import boto3 +import json +import sure # noqa + +from moto import mock_cloudformation, mock_ec2, mock_elb +from tests import EXAMPLE_AMI_ID + + +@mock_ec2 +@mock_elb +@mock_cloudformation +def test_stack_elb_integration_with_attached_ec2_instances(): + elb_template = { + "AWSTemplateFormatVersion": "2010-09-09", + "Resources": { + "MyELB": { + "Type": "AWS::ElasticLoadBalancing::LoadBalancer", + "Properties": { + "Instances": [{"Ref": "Ec2Instance1"}], + "LoadBalancerName": "test-elb", + "AvailabilityZones": ["us-east-1"], + "Listeners": [ + { + "InstancePort": "80", + "LoadBalancerPort": "80", + "Protocol": "HTTP", + } + ], + }, + }, + "Ec2Instance1": { + "Type": "AWS::EC2::Instance", + "Properties": {"ImageId": EXAMPLE_AMI_ID, "UserData": "some user data"}, + }, + }, + } + elb_template_json = json.dumps(elb_template) + + cf = boto3.client("cloudformation", region_name="us-west-1") + cf.create_stack(StackName="elb_stack", TemplateBody=elb_template_json) + + elb = boto3.client("elb", region_name="us-west-1") + load_balancer = elb.describe_load_balancers()["LoadBalancerDescriptions"][0] + + ec2 = boto3.client("ec2", region_name="us-west-1") + reservations = ec2.describe_instances()["Reservations"][0] + ec2_instance = reservations["Instances"][0] + + load_balancer["Instances"][0]["InstanceId"].should.equal(ec2_instance["InstanceId"]) + load_balancer["AvailabilityZones"].should.equal(["us-east-1"]) + + +@mock_elb +@mock_cloudformation +def test_stack_elb_integration_with_health_check(): + elb_template = { + "AWSTemplateFormatVersion": "2010-09-09", + "Resources": { + "MyELB": { + "Type": "AWS::ElasticLoadBalancing::LoadBalancer", + "Properties": { + "LoadBalancerName": "test-elb", + "AvailabilityZones": ["us-west-1"], + "HealthCheck": { + "HealthyThreshold": "3", + "Interval": "5", + "Target": "HTTP:80/healthcheck", + "Timeout": "4", + "UnhealthyThreshold": "2", + }, + "Listeners": [ + { + "InstancePort": "80", + "LoadBalancerPort": "80", + "Protocol": "HTTP", + } + ], + }, + } + }, + } + elb_template_json = json.dumps(elb_template) + + cf = boto3.client("cloudformation", region_name="us-west-1") + cf.create_stack(StackName="elb_stack", TemplateBody=elb_template_json) + + elb = boto3.client("elb", region_name="us-west-1") + load_balancer = elb.describe_load_balancers()["LoadBalancerDescriptions"][0] + health_check = load_balancer["HealthCheck"] + + health_check.should.have.key("HealthyThreshold").equal(3) + health_check.should.have.key("Interval").equal(5) + health_check.should.have.key("Target").equal("HTTP:80/healthcheck") + health_check.should.have.key("Timeout").equal(4) + health_check.should.have.key("UnhealthyThreshold").equal(2) + + +@mock_elb +@mock_cloudformation +def test_stack_elb_integration_with_update(): + elb_template = { + "AWSTemplateFormatVersion": "2010-09-09", + "Resources": { + "MyELB": { + "Type": "AWS::ElasticLoadBalancing::LoadBalancer", + "Properties": { + "LoadBalancerName": "test-elb", + "AvailabilityZones": ["us-west-1a"], + "Listeners": [ + { + "InstancePort": "80", + "LoadBalancerPort": "80", + "Protocol": "HTTP", + } + ], + "Policies": {"Ref": "AWS::NoValue"}, + }, + } + }, + } + elb_template_json = json.dumps(elb_template) + + # when + cf = boto3.client("cloudformation", region_name="us-west-1") + cf.create_stack(StackName="elb_stack", TemplateBody=elb_template_json) + + # then + elb = boto3.client("elb", region_name="us-west-1") + load_balancer = elb.describe_load_balancers()["LoadBalancerDescriptions"][0] + load_balancer["AvailabilityZones"].should.equal(["us-west-1a"]) + + # when + elb_template["Resources"]["MyELB"]["Properties"]["AvailabilityZones"] = [ + "us-west-1b" + ] + elb_template_json = json.dumps(elb_template) + cf.update_stack(StackName="elb_stack", TemplateBody=elb_template_json) + + # then + load_balancer = elb.describe_load_balancers()["LoadBalancerDescriptions"][0] + load_balancer["AvailabilityZones"].should.equal(["us-west-1b"]) diff --git a/tests/test_iam/test_iam_cloudformation.py b/tests/test_iam/test_iam_cloudformation.py index 268a55585..e8fe5f3f7 100644 --- a/tests/test_iam/test_iam_cloudformation.py +++ b/tests/test_iam/test_iam_cloudformation.py @@ -1,12 +1,14 @@ import boto3 +import json import yaml import sure # noqa import pytest from botocore.exceptions import ClientError -from moto import mock_iam, mock_cloudformation, mock_s3, mock_sts from moto.core import ACCOUNT_ID +from moto import mock_autoscaling, mock_iam, mock_cloudformation, mock_s3, mock_sts +from tests import EXAMPLE_AMI_ID TEMPLATE_MINIMAL_ROLE = """ @@ -60,6 +62,7 @@ Resources: - !Ref RootRole """ + # AWS::IAM::User Tests @mock_iam @mock_cloudformation @@ -1495,3 +1498,165 @@ def test_iam_cloudformation_create_role_and_instance_profile(): cf_client.delete_stack(StackName=stack_name) iam_client.list_roles()["Roles"].should.have.length_of(0) + + +@mock_autoscaling +@mock_iam +@mock_cloudformation +def test_iam_roles(): + iam_template = { + "AWSTemplateFormatVersion": "2010-09-09", + "Resources": { + "my-launch-config": { + "Properties": { + "IamInstanceProfile": {"Ref": "my-instance-profile-with-path"}, + "ImageId": EXAMPLE_AMI_ID, + "InstanceType": "t2.medium", + }, + "Type": "AWS::AutoScaling::LaunchConfiguration", + }, + "my-instance-profile-with-path": { + "Properties": { + "Path": "my-path", + "Roles": [{"Ref": "my-role-with-path"}], + }, + "Type": "AWS::IAM::InstanceProfile", + }, + "my-instance-profile-no-path": { + "Properties": {"Roles": [{"Ref": "my-role-no-path"}]}, + "Type": "AWS::IAM::InstanceProfile", + }, + "my-role-with-path": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": ["sts:AssumeRole"], + "Effect": "Allow", + "Principal": {"Service": ["ec2.amazonaws.com"]}, + } + ] + }, + "Path": "/my-path/", + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ec2:CreateTags", + "ec2:DescribeInstances", + "ec2:DescribeTags", + ], + "Effect": "Allow", + "Resource": ["*"], + } + ], + "Version": "2012-10-17", + }, + "PolicyName": "EC2_Tags", + }, + { + "PolicyDocument": { + "Statement": [ + { + "Action": ["sqs:*"], + "Effect": "Allow", + "Resource": ["*"], + } + ], + "Version": "2012-10-17", + }, + "PolicyName": "SQS", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "my-role-no-path": { + "Properties": { + "RoleName": "my-role-no-path-name", + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": ["sts:AssumeRole"], + "Effect": "Allow", + "Principal": {"Service": ["ec2.amazonaws.com"]}, + } + ] + }, + }, + "Type": "AWS::IAM::Role", + }, + }, + } + + iam_template_json = json.dumps(iam_template) + cf = boto3.client("cloudformation", region_name="us-west-1") + cf.create_stack(StackName="test_stack", TemplateBody=iam_template_json) + + iam = boto3.client("iam", region_name="us-west-1") + + role_results = iam.list_roles()["Roles"] + role_name_to_id = {} + role_names = [] + for role_result in role_results: + role = iam.get_role(RoleName=role_result["RoleName"])["Role"] + role_names.append(role["RoleName"]) + # Role name is not specified, so randomly generated - can't check exact name + if "with-path" in role["RoleName"]: + role_name_to_id["with-path"] = role["RoleId"] + role["Path"].should.equal("/my-path/") + else: + role_name_to_id["no-path"] = role["RoleId"] + role["RoleName"].should.equal("my-role-no-path-name") + role["Path"].should.equal("/") + + instance_profile_responses = iam.list_instance_profiles()["InstanceProfiles"] + instance_profile_responses.should.have.length_of(2) + instance_profile_names = [] + + for instance_profile_response in instance_profile_responses: + instance_profile = iam.get_instance_profile( + InstanceProfileName=instance_profile_response["InstanceProfileName"] + )["InstanceProfile"] + instance_profile_names.append(instance_profile["InstanceProfileName"]) + instance_profile["InstanceProfileName"].should.contain("my-instance-profile") + if "with-path" in instance_profile["InstanceProfileName"]: + instance_profile["Path"].should.equal("my-path") + instance_profile["Roles"][0]["RoleId"].should.equal( + role_name_to_id["with-path"] + ) + else: + instance_profile["InstanceProfileName"].should.contain("no-path") + instance_profile["Roles"][0]["RoleId"].should.equal( + role_name_to_id["no-path"] + ) + instance_profile["Path"].should.equal("/") + + autoscale = boto3.client("autoscaling", region_name="us-west-1") + launch_config = autoscale.describe_launch_configurations()["LaunchConfigurations"][ + 0 + ] + launch_config.should.have.key("IamInstanceProfile").should.contain( + "my-instance-profile-with-path" + ) + + resources = cf.list_stack_resources(StackName="test_stack")[ + "StackResourceSummaries" + ] + instance_profile_resources = [ + resource + for resource in resources + if resource["ResourceType"] == "AWS::IAM::InstanceProfile" + ] + {ip["PhysicalResourceId"] for ip in instance_profile_resources}.should.equal( + set(instance_profile_names) + ) + + role_resources = [ + resource + for resource in resources + if resource["ResourceType"] == "AWS::IAM::Role" + ] + {r["PhysicalResourceId"] for r in role_resources}.should.equal(set(role_names)) diff --git a/tests/test_rds2/test_rds2_cloudformation.py b/tests/test_rds2/test_rds2_cloudformation.py index a3554aa43..e1492363a 100644 --- a/tests/test_rds2/test_rds2_cloudformation.py +++ b/tests/test_rds2/test_rds2_cloudformation.py @@ -2,6 +2,8 @@ import boto3 import json import sure # noqa from moto import mock_cloudformation, mock_ec2, mock_rds2 +from tests.test_cloudformation.fixtures import rds_mysql_with_db_parameter_group +from tests.test_cloudformation.fixtures import rds_mysql_with_read_replica @mock_ec2 @@ -112,3 +114,137 @@ def test_create_dbsecuritygroup_via_cf(): created = result[0] created["DBSecurityGroupDescription"].should.equal("my sec group") + + +@mock_cloudformation +@mock_ec2 +@mock_rds2 +def test_rds_db_parameter_groups(): + ec2_conn = boto3.client("ec2", region_name="us-west-1") + ec2_conn.create_security_group( + GroupName="application", Description="Our Application Group" + ) + + template_json = json.dumps(rds_mysql_with_db_parameter_group.template) + cf_conn = boto3.client("cloudformation", "us-west-1") + cf_conn.create_stack( + StackName="test_stack", + TemplateBody=template_json, + Parameters=[ + {"ParameterKey": key, "ParameterValue": value} + for key, value in [ + ("DBInstanceIdentifier", "master_db"), + ("DBName", "my_db"), + ("DBUser", "my_user"), + ("DBPassword", "my_password"), + ("DBAllocatedStorage", "20"), + ("DBInstanceClass", "db.m1.medium"), + ("EC2SecurityGroup", "application"), + ("MultiAZ", "true"), + ] + ], + ) + + rds_conn = boto3.client("rds", region_name="us-west-1") + + db_parameter_groups = rds_conn.describe_db_parameter_groups() + db_parameter_groups["DBParameterGroups"].should.have.length_of(1) + db_parameter_group_name = db_parameter_groups["DBParameterGroups"][0][ + "DBParameterGroupName" + ] + + found_cloudformation_set_parameter = False + for db_parameter in rds_conn.describe_db_parameters( + DBParameterGroupName=db_parameter_group_name + )["Parameters"]: + if ( + db_parameter["ParameterName"] == "BACKLOG_QUEUE_LIMIT" + and db_parameter["ParameterValue"] == "2048" + ): + found_cloudformation_set_parameter = True + + found_cloudformation_set_parameter.should.equal(True) + + +@mock_cloudformation +@mock_ec2 +@mock_rds2 +def test_rds_mysql_with_read_replica(): + ec2_conn = boto3.client("ec2", region_name="us-west-1") + ec2_conn.create_security_group( + GroupName="application", Description="Our Application Group" + ) + + template_json = json.dumps(rds_mysql_with_read_replica.template) + cf = boto3.client("cloudformation", "us-west-1") + cf.create_stack( + StackName="test_stack", + TemplateBody=template_json, + Parameters=[ + {"ParameterKey": "DBInstanceIdentifier", "ParameterValue": "master_db"}, + {"ParameterKey": "DBName", "ParameterValue": "my_db"}, + {"ParameterKey": "DBUser", "ParameterValue": "my_user"}, + {"ParameterKey": "DBPassword", "ParameterValue": "my_password"}, + {"ParameterKey": "DBAllocatedStorage", "ParameterValue": "20"}, + {"ParameterKey": "DBInstanceClass", "ParameterValue": "db.m1.medium"}, + {"ParameterKey": "EC2SecurityGroup", "ParameterValue": "application"}, + {"ParameterKey": "MultiAZ", "ParameterValue": "true"}, + ], + ) + + rds = boto3.client("rds", region_name="us-west-1") + + primary = rds.describe_db_instances(DBInstanceIdentifier="master_db")[ + "DBInstances" + ][0] + primary.should.have.key("MasterUsername").equal("my_user") + primary.should.have.key("AllocatedStorage").equal(20) + primary.should.have.key("DBInstanceClass").equal("db.m1.medium") + primary.should.have.key("MultiAZ").equal(True) + primary.should.have.key("ReadReplicaDBInstanceIdentifiers").being.length_of(1) + replica_id = primary["ReadReplicaDBInstanceIdentifiers"][0] + + replica = rds.describe_db_instances(DBInstanceIdentifier=replica_id)["DBInstances"][ + 0 + ] + replica.should.have.key("DBInstanceClass").equal("db.m1.medium") + + security_group_name = primary["DBSecurityGroups"][0]["DBSecurityGroupName"] + security_group = rds.describe_db_security_groups( + DBSecurityGroupName=security_group_name + )["DBSecurityGroups"][0] + security_group["EC2SecurityGroups"][0]["EC2SecurityGroupName"].should.equal( + "application" + ) + + +@mock_cloudformation +@mock_ec2 +@mock_rds2 +def test_rds_mysql_with_read_replica_in_vpc(): + template_json = json.dumps(rds_mysql_with_read_replica.template) + cf = boto3.client("cloudformation", "eu-central-1") + cf.create_stack( + StackName="test_stack", + TemplateBody=template_json, + Parameters=[ + {"ParameterKey": "DBInstanceIdentifier", "ParameterValue": "master_db"}, + {"ParameterKey": "DBName", "ParameterValue": "my_db"}, + {"ParameterKey": "DBUser", "ParameterValue": "my_user"}, + {"ParameterKey": "DBPassword", "ParameterValue": "my_password"}, + {"ParameterKey": "DBAllocatedStorage", "ParameterValue": "20"}, + {"ParameterKey": "DBInstanceClass", "ParameterValue": "db.m1.medium"}, + {"ParameterKey": "MultiAZ", "ParameterValue": "true"}, + ], + ) + + rds = boto3.client("rds", region_name="eu-central-1") + primary = rds.describe_db_instances(DBInstanceIdentifier="master_db")[ + "DBInstances" + ][0] + + subnet_group_name = primary["DBSubnetGroup"]["DBSubnetGroupName"] + subnet_group = rds.describe_db_subnet_groups(DBSubnetGroupName=subnet_group_name)[ + "DBSubnetGroups" + ][0] + subnet_group.should.have.key("DBSubnetGroupDescription").equal("my db subnet group") diff --git a/tests/test_redshift/test_redshift_cloudformation.py b/tests/test_redshift/test_redshift_cloudformation.py new file mode 100644 index 000000000..ff369ec61 --- /dev/null +++ b/tests/test_redshift/test_redshift_cloudformation.py @@ -0,0 +1,52 @@ +import boto3 +import json +import sure # noqa + +from moto import mock_cloudformation, mock_ec2, mock_redshift +from tests.test_cloudformation.fixtures import redshift + + +@mock_ec2 +@mock_redshift +@mock_cloudformation +def test_redshift_stack(): + redshift_template_json = json.dumps(redshift.template) + + ec2 = boto3.client("ec2", region_name="us-west-2") + cf = boto3.client("cloudformation", region_name="us-west-2") + cf.create_stack( + StackName="redshift_stack", + TemplateBody=redshift_template_json, + Parameters=[ + {"ParameterKey": "DatabaseName", "ParameterValue": "mydb"}, + {"ParameterKey": "ClusterType", "ParameterValue": "multi-node"}, + {"ParameterKey": "NumberOfNodes", "ParameterValue": "2"}, + {"ParameterKey": "NodeType", "ParameterValue": "dw1.xlarge"}, + {"ParameterKey": "MasterUsername", "ParameterValue": "myuser"}, + {"ParameterKey": "MasterUserPassword", "ParameterValue": "mypass"}, + {"ParameterKey": "InboundTraffic", "ParameterValue": "10.0.0.1/16"}, + {"ParameterKey": "PortNumber", "ParameterValue": "5439"}, + ], + ) + + redshift_conn = boto3.client("redshift", region_name="us-west-2") + + cluster_res = redshift_conn.describe_clusters() + clusters = cluster_res["Clusters"] + clusters.should.have.length_of(1) + cluster = clusters[0] + cluster["DBName"].should.equal("mydb") + cluster["NumberOfNodes"].should.equal(2) + cluster["NodeType"].should.equal("dw1.xlarge") + cluster["MasterUsername"].should.equal("myuser") + cluster["Endpoint"]["Port"].should.equal(5439) + cluster["VpcSecurityGroups"].should.have.length_of(1) + security_group_id = cluster["VpcSecurityGroups"][0]["VpcSecurityGroupId"] + + groups = ec2.describe_security_groups(GroupIds=[security_group_id])[ + "SecurityGroups" + ] + groups.should.have.length_of(1) + group = groups[0] + group["IpPermissions"].should.have.length_of(1) + group["IpPermissions"][0]["IpRanges"][0]["CidrIp"].should.equal("10.0.0.1/16") diff --git a/tests/test_resourcegroupstaggingapi/test_resourcegroupstaggingapi.py b/tests/test_resourcegroupstaggingapi/test_resourcegroupstaggingapi.py index 24a977896..ae478dc97 100644 --- a/tests/test_resourcegroupstaggingapi/test_resourcegroupstaggingapi.py +++ b/tests/test_resourcegroupstaggingapi/test_resourcegroupstaggingapi.py @@ -11,6 +11,7 @@ from moto import mock_s3 from tests import EXAMPLE_AMI_ID, EXAMPLE_AMI_ID2 +@mock_rds2 @mock_ec2 @mock_resourcegroupstaggingapi def test_get_resources_ec2(): diff --git a/tests/test_route53/test_route53_cloudformation.py b/tests/test_route53/test_route53_cloudformation.py new file mode 100644 index 000000000..40793f2bd --- /dev/null +++ b/tests/test_route53/test_route53_cloudformation.py @@ -0,0 +1,226 @@ +import boto3 +import json +import sure # noqa + +from copy import deepcopy +from moto import mock_cloudformation, mock_ec2, mock_route53 +from tests.test_cloudformation.fixtures import route53_ec2_instance_with_public_ip +from tests.test_cloudformation.fixtures import route53_health_check +from tests.test_cloudformation.fixtures import route53_roundrobin + +template_hosted_zone = { + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "Stack 1", + "Parameters": {}, + "Resources": { + "Bar": { + "Type": "AWS::Route53::HostedZone", + "Properties": {"Name": "foo.bar.baz"}, + } + }, +} +template_record_set = { + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "Stack 2", + "Parameters": {"ZoneId": {"Type": "String"}}, + "Resources": { + "Foo": { + "Properties": { + "HostedZoneId": {"Ref": "ZoneId"}, + "RecordSets": [ + { + "Name": "test.vpc.internal", + "Type": "A", + "SetIdentifier": "test1", + "Weight": 50, + } + ], + }, + "Type": "AWS::Route53::RecordSetGroup", + } + }, +} + + +@mock_cloudformation +@mock_route53 +def test_create_stack_hosted_zone_by_id(): + cf_conn = boto3.client("cloudformation", region_name="us-east-1") + conn = boto3.client("route53", region_name="us-east-1") + + # when creating a hosted zone via CF + cf_conn.create_stack( + StackName="test_stack1", TemplateBody=json.dumps(template_hosted_zone) + ) + + # then a hosted zone should exist + zone = conn.list_hosted_zones()["HostedZones"][0] + zone.should.have.key("Name").equal("foo.bar.baz") + zone.should.have.key("ResourceRecordSetCount").equal(0) + + # when adding a record set to this zone + cf_conn.create_stack( + StackName="test_stack2", + TemplateBody=json.dumps(template_record_set), + Parameters=[{"ParameterKey": "ZoneId", "ParameterValue": zone["Id"]}], + ) + + # then the hosted zone should have a record + updated_zone = conn.list_hosted_zones()["HostedZones"][0] + updated_zone.should.have.key("Id").equal(zone["Id"]) + updated_zone.should.have.key("Name").equal("foo.bar.baz") + updated_zone.should.have.key("ResourceRecordSetCount").equal(1) + + +@mock_cloudformation +@mock_route53 +def test_route53_roundrobin(): + cf = boto3.client("cloudformation", region_name="us-west-1") + route53 = boto3.client("route53", region_name="us-west-1") + + template_json = json.dumps(route53_roundrobin.template) + cf.create_stack(StackName="test_stack", TemplateBody=template_json) + + zones = route53.list_hosted_zones()["HostedZones"] + zones.should.have.length_of(1) + zone_id = zones[0]["Id"].split("/")[2] + + rrsets = route53.list_resource_record_sets(HostedZoneId=zone_id)[ + "ResourceRecordSets" + ] + rrsets.should.have.length_of(2) + record_set1 = rrsets[0] + record_set1["Name"].should.equal("test_stack.us-west-1.my_zone.") + record_set1["SetIdentifier"].should.equal("test_stack AWS") + record_set1["Type"].should.equal("CNAME") + record_set1["TTL"].should.equal(900) + record_set1["Weight"].should.equal(3) + record_set1["ResourceRecords"][0]["Value"].should.equal("aws.amazon.com") + + record_set2 = rrsets[1] + record_set2["Name"].should.equal("test_stack.us-west-1.my_zone.") + record_set2["SetIdentifier"].should.equal("test_stack Amazon") + record_set2["Type"].should.equal("CNAME") + record_set2["TTL"].should.equal(900) + record_set2["Weight"].should.equal(1) + record_set2["ResourceRecords"][0]["Value"].should.equal("www.amazon.com") + + stack = cf.describe_stacks(StackName="test_stack")["Stacks"][0] + output = stack["Outputs"][0] + output["OutputKey"].should.equal("DomainName") + output["OutputValue"].should.equal( + "arn:aws:route53:::hostedzone/{0}".format(zone_id) + ) + + +@mock_cloudformation +@mock_ec2 +@mock_route53 +def test_route53_ec2_instance_with_public_ip(): + route53 = boto3.client("route53", region_name="us-west-1") + ec2 = boto3.client("ec2", region_name="us-west-1") + + template_json = json.dumps(route53_ec2_instance_with_public_ip.template) + cf = boto3.client("cloudformation", region_name="us-west-1") + cf.create_stack(StackName="test_stack", TemplateBody=template_json) + + instance_id = ec2.describe_instances()["Reservations"][0]["Instances"][0][ + "InstanceId" + ] + + zones = route53.list_hosted_zones()["HostedZones"] + zones.should.have.length_of(1) + zone_id = zones[0]["Id"].split("/")[2] + + rrsets = route53.list_resource_record_sets(HostedZoneId=zone_id)[ + "ResourceRecordSets" + ] + rrsets.should.have.length_of(1) + + record_set = rrsets[0] + record_set["Name"].should.equal("{0}.us-west-1.my_zone.".format(instance_id)) + record_set.shouldnt.have.key("SetIdentifier") + record_set["Type"].should.equal("A") + record_set["TTL"].should.equal(900) + record_set.shouldnt.have.key("Weight") + record_set["ResourceRecords"][0]["Value"].should.equal("10.0.0.25") + + +@mock_cloudformation +@mock_route53 +def test_route53_associate_health_check(): + route53 = boto3.client("route53", region_name="us-west-1") + + template_json = json.dumps(route53_health_check.template) + cf = boto3.client("cloudformation", region_name="us-west-1") + cf.create_stack(StackName="test_stack", TemplateBody=template_json) + + checks = route53.list_health_checks()["HealthChecks"] + checks.should.have.length_of(1) + check = checks[0] + health_check_id = check["Id"] + config = check["HealthCheckConfig"] + config["FailureThreshold"].should.equal(3) + config["IPAddress"].should.equal("10.0.0.4") + config["Port"].should.equal(80) + config["RequestInterval"].should.equal(10) + config["ResourcePath"].should.equal("/") + config["Type"].should.equal("HTTP") + + zones = route53.list_hosted_zones()["HostedZones"] + zones.should.have.length_of(1) + zone_id = zones[0]["Id"].split("/")[2] + + rrsets = route53.list_resource_record_sets(HostedZoneId=zone_id)[ + "ResourceRecordSets" + ] + rrsets.should.have.length_of(1) + record_set = rrsets[0] + record_set["HealthCheckId"].should.equal(health_check_id) + + +@mock_cloudformation +@mock_route53 +def test_route53_with_update(): + route53 = boto3.client("route53", region_name="us-west-1") + + template_json = json.dumps(route53_health_check.template) + cf = boto3.client("cloudformation", region_name="us-west-1") + cf.create_stack(StackName="test_stack", TemplateBody=template_json) + + zones = route53.list_hosted_zones()["HostedZones"] + zones.should.have.length_of(1) + zone_id = zones[0]["Id"] + zone_id = zone_id.split("/") + zone_id = zone_id[2] + + rrsets = route53.list_resource_record_sets(HostedZoneId=zone_id)[ + "ResourceRecordSets" + ] + rrsets.should.have.length_of(1) + + record_set = rrsets[0] + record_set["ResourceRecords"][0]["Value"].should.equal("my.example.com") + + # given + template = deepcopy(route53_health_check.template) + template["Resources"]["myDNSRecord"]["Properties"]["ResourceRecords"] = [ + "my_other.example.com" + ] + template_json = json.dumps(template) + + # when + cf.update_stack(StackName="test_stack", TemplateBody=template_json) + + # then + zones = route53.list_hosted_zones()["HostedZones"] + zones.should.have.length_of(1) + zone_id = zones[0]["Id"].split("/")[2] + + rrsets = route53.list_resource_record_sets(HostedZoneId=zone_id)[ + "ResourceRecordSets" + ] + rrsets.should.have.length_of(1) + + record_set = rrsets[0] + record_set["ResourceRecords"][0]["Value"].should.equal("my_other.example.com") diff --git a/tests/test_sns/test_sns_cloudformation.py b/tests/test_sns/test_sns_cloudformation.py new file mode 100644 index 000000000..ab249d20d --- /dev/null +++ b/tests/test_sns/test_sns_cloudformation.py @@ -0,0 +1,53 @@ +import boto3 +import json +import sure # noqa + +from moto import mock_cloudformation, mock_sns + + +@mock_cloudformation +@mock_sns +def test_sns_topic(): + dummy_template = { + "AWSTemplateFormatVersion": "2010-09-09", + "Resources": { + "MySNSTopic": { + "Type": "AWS::SNS::Topic", + "Properties": { + "Subscription": [ + {"Endpoint": "https://example.com", "Protocol": "https"} + ], + "TopicName": "my_topics", + }, + } + }, + "Outputs": { + "topic_name": {"Value": {"Fn::GetAtt": ["MySNSTopic", "TopicName"]}}, + "topic_arn": {"Value": {"Ref": "MySNSTopic"}}, + }, + } + template_json = json.dumps(dummy_template) + cf = boto3.client("cloudformation", region_name="us-west-1") + cf.create_stack(StackName="test_stack", TemplateBody=template_json) + + sns = boto3.client("sns", region_name="us-west-1") + topics = sns.list_topics()["Topics"] + topics.should.have.length_of(1) + topic_arn = topics[0]["TopicArn"] + topic_arn.should.contain("my_topics") + + subscriptions = sns.list_subscriptions()["Subscriptions"] + subscriptions.should.have.length_of(1) + subscription = subscriptions[0] + subscription["TopicArn"].should.equal(topic_arn) + subscription["Protocol"].should.equal("https") + subscription["SubscriptionArn"].should.contain(topic_arn) + subscription["Endpoint"].should.equal("https://example.com") + + stack = cf.describe_stacks(StackName="test_stack")["Stacks"][0] + topic_name_output = [x for x in stack["Outputs"] if x["OutputKey"] == "topic_name"][ + 0 + ] + topic_name_output["OutputValue"].should.equal("my_topics") + topic_arn_output = [x for x in stack["Outputs"] if x["OutputKey"] == "topic_arn"][0] + topic_arn_output["OutputValue"].should.equal(topic_arn) diff --git a/tests/test_sqs/test_sqs_cloudformation.py b/tests/test_sqs/test_sqs_cloudformation.py index 73f76c8f6..fd4af186d 100644 --- a/tests/test_sqs/test_sqs_cloudformation.py +++ b/tests/test_sqs/test_sqs_cloudformation.py @@ -1,6 +1,21 @@ import boto3 -from moto import mock_sqs, mock_cloudformation +import json +import sure # noqa +from moto import mock_sqs, mock_cloudformation +from moto.core import ACCOUNT_ID + + +simple_queue = { + "AWSTemplateFormatVersion": "2010-09-09", + "Resources": { + "QueueGroup": { + "Type": "AWS::SQS::Queue", + "Properties": {"QueueName": "my-queue", "VisibilityTimeout": 60}, + } + }, +} +simple_queue_json = json.dumps(simple_queue) sqs_template_with_tags = """ { "AWSTemplateFormatVersion": "2010-09-09", @@ -24,6 +39,43 @@ sqs_template_with_tags = """ }""" +@mock_sqs +@mock_cloudformation +def test_describe_stack_subresources(): + res = boto3.resource("cloudformation", region_name="us-east-1") + cf = boto3.client("cloudformation", region_name="us-east-1") + client = boto3.client("sqs", region_name="us-east-1") + + cf.create_stack(StackName="test_sqs", TemplateBody=simple_queue_json) + + queue_url = client.list_queues()["QueueUrls"][0] + queue_url.should.contain("{}/{}".format(ACCOUNT_ID, "my-queue")) + + stack = res.Stack("test_sqs") + for s in stack.resource_summaries.all(): + s.resource_type.should.equal("AWS::SQS::Queue") + s.logical_id.should.equal("QueueGroup") + s.physical_resource_id.should.equal("my-queue") + + +@mock_sqs +@mock_cloudformation +def test_list_stack_resources(): + cf = boto3.client("cloudformation", region_name="us-east-1") + client = boto3.client("sqs", region_name="us-east-1") + + cf.create_stack(StackName="test_sqs", TemplateBody=simple_queue_json) + + queue_url = client.list_queues()["QueueUrls"][0] + queue_url.should.contain("{}/{}".format(ACCOUNT_ID, "my-queue")) + + queue = cf.list_stack_resources(StackName="test_sqs")["StackResourceSummaries"][0] + + queue.should.have.key("ResourceType").equal("AWS::SQS::Queue") + queue.should.have.key("LogicalResourceId").should.equal("QueueGroup") + queue.should.have.key("PhysicalResourceId").should.equal("my-queue") + + @mock_sqs @mock_cloudformation def test_create_from_cloudformation_json_with_tags(): @@ -36,3 +88,98 @@ def test_create_from_cloudformation_json_with_tags(): queue_tags = client.list_queue_tags(QueueUrl=queue_url)["Tags"] queue_tags.should.equal({"keyname1": "value1", "keyname2": "value2"}) + + +@mock_cloudformation +@mock_sqs +def test_update_stack(): + sqs_template = { + "AWSTemplateFormatVersion": "2010-09-09", + "Resources": { + "QueueGroup": { + "Type": "AWS::SQS::Queue", + "Properties": {"QueueName": "my-queue", "VisibilityTimeout": 60}, + } + }, + } + sqs_template_json = json.dumps(sqs_template) + + cf = boto3.client("cloudformation", region_name="us-west-1") + cf.create_stack(StackName="test_stack", TemplateBody=sqs_template_json) + + client = boto3.client("sqs", region_name="us-west-1") + queues = client.list_queues()["QueueUrls"] + queues.should.have.length_of(1) + attrs = client.get_queue_attributes(QueueUrl=queues[0], AttributeNames=["All"])[ + "Attributes" + ] + attrs["VisibilityTimeout"].should.equal("60") + + # when updating + sqs_template["Resources"]["QueueGroup"]["Properties"]["VisibilityTimeout"] = 100 + sqs_template_json = json.dumps(sqs_template) + cf.update_stack(StackName="test_stack", TemplateBody=sqs_template_json) + + # then the attribute should be updated + queues = client.list_queues()["QueueUrls"] + queues.should.have.length_of(1) + attrs = client.get_queue_attributes(QueueUrl=queues[0], AttributeNames=["All"])[ + "Attributes" + ] + attrs["VisibilityTimeout"].should.equal("100") + + +@mock_cloudformation +@mock_sqs +def test_update_stack_and_remove_resource(): + sqs_template = { + "AWSTemplateFormatVersion": "2010-09-09", + "Resources": { + "QueueGroup": { + "Type": "AWS::SQS::Queue", + "Properties": {"QueueName": "my-queue", "VisibilityTimeout": 60}, + } + }, + } + sqs_template_json = json.dumps(sqs_template) + + cf = boto3.client("cloudformation", region_name="us-west-1") + cf.create_stack(StackName="test_stack", TemplateBody=sqs_template_json) + + client = boto3.client("sqs", region_name="us-west-1") + client.list_queues()["QueueUrls"].should.have.length_of(1) + + sqs_template["Resources"].pop("QueueGroup") + sqs_template_json = json.dumps(sqs_template) + cf.update_stack(StackName="test_stack", TemplateBody=sqs_template_json) + + client.list_queues().shouldnt.have.key( + "QueueUrls" + ) # No queues exist, so the key is not passed through + + +@mock_cloudformation +@mock_sqs +def test_update_stack_and_add_resource(): + sqs_template = {"AWSTemplateFormatVersion": "2010-09-09", "Resources": {}} + sqs_template_json = json.dumps(sqs_template) + + cf = boto3.client("cloudformation", region_name="us-west-1") + cf.create_stack(StackName="test_stack", TemplateBody=sqs_template_json) + + client = boto3.client("sqs", region_name="us-west-1") + client.list_queues().shouldnt.have.key("QueueUrls") + + sqs_template = { + "AWSTemplateFormatVersion": "2010-09-09", + "Resources": { + "QueueGroup": { + "Type": "AWS::SQS::Queue", + "Properties": {"QueueName": "my-queue", "VisibilityTimeout": 60}, + } + }, + } + sqs_template_json = json.dumps(sqs_template) + cf.update_stack(StackName="test_stack", TemplateBody=sqs_template_json) + + client.list_queues()["QueueUrls"].should.have.length_of(1)