import io import json import sys import zipfile from decimal import Decimal from string import Template import boto3 import pytest from botocore.exceptions import ClientError from moto import mock_aws from moto.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID from moto.utilities.distutils_version import LooseVersion from tests import EXAMPLE_AMI_ID, EXAMPLE_AMI_ID2 from tests.markers import requires_docker from tests.test_cloudformation.fixtures import fn_join, single_instance_with_ebs_volume boto3_version = sys.modules["botocore"].__version__ @mock_aws 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"] assert err["Code"] == "Missing Parameter" assert err["Message"] == "Missing parameter KeyName" @mock_aws 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] assert fn_join_output["OutputValue"] == f"test eip:{eip['PublicIp']}" @mock_aws 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") assert "QueueUrls" not in sqs.list_queues() cf.create_stack( StackName="test_stack_with_queue", TemplateBody=sqs_template_json, Parameters=[{"ParameterKey": "EnvType", "ParameterValue": "prod"}], ) assert len(sqs.list_queues()["QueueUrls"]) == 1 @mock_aws 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] assert ec2_instance["ImageId"] == 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] assert ec2_instance["ImageId"] == EXAMPLE_AMI_ID @mock_aws 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] assert ec2_instance["ImageId"] == 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] assert ec2_instance["ImageId"] == EXAMPLE_AMI_ID2 @mock_aws @requires_docker def test_lambda_function(): # switch this to python as backend lambda only supports python execution. lambda_code = """ def lambda_handler(event, context): return {"event": event} """ template = { "AWSTemplateFormatVersion": "2010-09-09", "Resources": { "lambdaTest": { "Type": "AWS::Lambda::Function", "Properties": { "Code": { # CloudFormation expects a string as ZipFile, not a ZIP file base64-encoded "ZipFile": {"Fn::Join": ["\n", lambda_code.splitlines()]} }, "Handler": "index.lambda_handler", "Description": "Test function", "MemorySize": 128, "Role": {"Fn::GetAtt": ["MyRole", "Arn"]}, "Runtime": "python3.11", "Environment": {"Variables": {"TEST_ENV_KEY": "test-env-val"}}, "ReservedConcurrentExecutions": 10, }, }, "MyRole": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { "Statement": [ { "Action": ["sts:AssumeRole"], "Effect": "Allow", "Principal": {"Service": ["ec2.amazonaws.com"]}, } ] } }, }, }, } template_json = json.dumps(template) cf_conn = boto3.client("cloudformation", "us-east-1") cf_conn.create_stack(StackName="test_stack", TemplateBody=template_json) conn = boto3.client("lambda", "us-east-1") result = conn.list_functions() assert len(result["Functions"]) == 1 assert result["Functions"][0]["Description"] == "Test function" assert result["Functions"][0]["Handler"] == "index.lambda_handler" assert result["Functions"][0]["MemorySize"] == 128 assert result["Functions"][0]["Runtime"] == "python3.11" assert result["Functions"][0]["Environment"] == { "Variables": {"TEST_ENV_KEY": "test-env-val"} } function_name = result["Functions"][0]["FunctionName"] result = conn.get_function(FunctionName=function_name) assert result["Concurrency"]["ReservedConcurrentExecutions"] == 10 response = conn.invoke(FunctionName=function_name) result = json.loads(response["Payload"].read()) assert result == {"event": "{}"} def _make_zipfile(func_str): zip_output = io.BytesIO() zip_file = zipfile.ZipFile(zip_output, "w", zipfile.ZIP_DEFLATED) zip_file.writestr("lambda_function.py", func_str) zip_file.close() zip_output.seek(0) return zip_output.read() @mock_aws def test_lambda_layer(): # switch this to python as backend lambda only supports python execution. layer_code = """ def lambda_handler(event, context): return (event, context) """ region = "us-east-1" bucket_name = "test_bucket" s3_conn = boto3.client("s3", region) s3_conn.create_bucket(Bucket=bucket_name) zip_content = _make_zipfile(layer_code) s3_conn.put_object(Bucket=bucket_name, Key="test.zip", Body=zip_content) template = { "AWSTemplateFormatVersion": "2010-09-09", "Resources": { "lambdaTest": { "Type": "AWS::Lambda::LayerVersion", "Properties": { "Content": {"S3Bucket": bucket_name, "S3Key": "test.zip"}, "LayerName": "testLayer", "Description": "Test Layer", "CompatibleRuntimes": ["python2.7", "python3.6"], "LicenseInfo": "MIT", "CompatibleArchitectures": [], }, }, }, } template_json = json.dumps(template) cf_conn = boto3.client("cloudformation", region) cf_conn.create_stack(StackName="test_stack", TemplateBody=template_json) lambda_conn = boto3.client("lambda", region) result = lambda_conn.list_layers() layer_name = result["Layers"][0]["LayerName"] lv = lambda_conn.list_layer_versions(LayerName=layer_name)["LayerVersions"][0] assert lv["Version"] == 1 assert ( lv["LayerVersionArn"] == f"arn:aws:lambda:{region}:{ACCOUNT_ID}:layer:{layer_name}:1" ) assert lv["CompatibleRuntimes"] == ["python2.7", "python3.6"] assert lv["Description"] == "Test Layer" assert lv["LicenseInfo"] == "MIT" if LooseVersion(boto3_version) > LooseVersion("1.29.0"): # "Parameters only available in newer versions" assert lv["CompatibleArchitectures"] == [] @mock_aws def test_nat_gateway(): ec2_conn = boto3.client("ec2", "us-east-1") vpc_id = ec2_conn.create_vpc(CidrBlock="10.0.0.0/16")["Vpc"]["VpcId"] subnet_id = ec2_conn.create_subnet(CidrBlock="10.0.1.0/24", VpcId=vpc_id)["Subnet"][ "SubnetId" ] route_table_id = ec2_conn.create_route_table(VpcId=vpc_id)["RouteTable"][ "RouteTableId" ] template = { "AWSTemplateFormatVersion": "2010-09-09", "Resources": { "NAT": { "DependsOn": "vpcgatewayattachment", "Type": "AWS::EC2::NatGateway", "Properties": { "AllocationId": {"Fn::GetAtt": ["EIP", "AllocationId"]}, "SubnetId": subnet_id, }, }, "EIP": {"Type": "AWS::EC2::EIP", "Properties": {"Domain": "vpc"}}, "Route": { "Type": "AWS::EC2::Route", "Properties": { "RouteTableId": route_table_id, "DestinationCidrBlock": "0.0.0.0/0", "NatGatewayId": {"Ref": "NAT"}, }, }, "internetgateway": {"Type": "AWS::EC2::InternetGateway"}, "vpcgatewayattachment": { "Type": "AWS::EC2::VPCGatewayAttachment", "Properties": { "InternetGatewayId": {"Ref": "internetgateway"}, "VpcId": vpc_id, }, }, }, } cf_conn = boto3.client("cloudformation", "us-east-1") cf_conn.create_stack(StackName="test_stack", TemplateBody=json.dumps(template)) stack_resources = cf_conn.list_stack_resources(StackName="test_stack") nat_gateway_resource = stack_resources.get("StackResourceSummaries")[0] for resource in stack_resources["StackResourceSummaries"]: if resource["ResourceType"] == "AWS::EC2::NatGateway": nat_gateway_resource = resource elif resource["ResourceType"] == "AWS::EC2::Route": route_resource = resource result = ec2_conn.describe_nat_gateways() assert len(result["NatGateways"]) == 1 assert result["NatGateways"][0]["VpcId"] == vpc_id assert result["NatGateways"][0]["SubnetId"] == subnet_id assert result["NatGateways"][0]["State"] == "available" physical_id = nat_gateway_resource.get("PhysicalResourceId") assert result["NatGateways"][0]["NatGatewayId"] == physical_id assert "rtb-" in route_resource["PhysicalResourceId"] @mock_aws def test_stack_kms(): kms_key_template = { "Resources": { "kmskey": { "Properties": { "Description": "A kms key", "EnableKeyRotation": True, "Enabled": True, "KeyPolicy": "a policy", }, "Type": "AWS::KMS::Key", } } } kms_key_template_json = json.dumps(kms_key_template) cf_conn = boto3.client("cloudformation", "us-east-1") cf_conn.create_stack(StackName="test_stack", TemplateBody=kms_key_template_json) kms_conn = boto3.client("kms", "us-east-1") keys = kms_conn.list_keys()["Keys"] assert len(keys) == 1 result = kms_conn.describe_key(KeyId=keys[0]["KeyId"]) assert result["KeyMetadata"]["Enabled"] is True assert result["KeyMetadata"]["KeyUsage"] == "ENCRYPT_DECRYPT" @mock_aws def test_stack_spot_fleet(): conn = boto3.client("ec2", "us-east-1") vpc = conn.create_vpc(CidrBlock="10.0.0.0/16")["Vpc"] subnet = conn.create_subnet( VpcId=vpc["VpcId"], CidrBlock="10.0.0.0/16", AvailabilityZone="us-east-1a" )["Subnet"] subnet_id = subnet["SubnetId"] spot_fleet_template = { "Resources": { "SpotFleet": { "Type": "AWS::EC2::SpotFleet", "Properties": { "SpotFleetRequestConfigData": { "IamFleetRole": f"arn:aws:iam::{ACCOUNT_ID}:role/fleet", "SpotPrice": "0.12", "TargetCapacity": 6, "AllocationStrategy": "diversified", "LaunchSpecifications": [ { "EbsOptimized": "false", "InstanceType": "t2.small", "ImageId": EXAMPLE_AMI_ID, "SubnetId": subnet_id, "WeightedCapacity": "2", "SpotPrice": "0.13", }, { "EbsOptimized": "true", "InstanceType": "t2.large", "ImageId": EXAMPLE_AMI_ID, "Monitoring": {"Enabled": "true"}, "SecurityGroups": [{"GroupId": "sg-123"}], "SubnetId": subnet_id, "IamInstanceProfile": { "Arn": f"arn:aws:iam::{ACCOUNT_ID}:role/fleet" }, "WeightedCapacity": "4", "SpotPrice": "10.00", }, ], } }, } } } spot_fleet_template_json = json.dumps(spot_fleet_template) cf_conn = boto3.client("cloudformation", "us-east-1") stack_id = cf_conn.create_stack( StackName="test_stack", TemplateBody=spot_fleet_template_json )["StackId"] stack_resources = cf_conn.list_stack_resources(StackName=stack_id) assert len(stack_resources["StackResourceSummaries"]) == 1 spot_fleet_id = stack_resources["StackResourceSummaries"][0]["PhysicalResourceId"] spot_fleet_requests = conn.describe_spot_fleet_requests( SpotFleetRequestIds=[spot_fleet_id] )["SpotFleetRequestConfigs"] assert len(spot_fleet_requests) == 1 spot_fleet_request = spot_fleet_requests[0] assert spot_fleet_request["SpotFleetRequestState"] == "active" spot_fleet_config = spot_fleet_request["SpotFleetRequestConfig"] assert spot_fleet_config["SpotPrice"] == "0.12" assert spot_fleet_config["TargetCapacity"] == 6 assert spot_fleet_config["IamFleetRole"] == f"arn:aws:iam::{ACCOUNT_ID}:role/fleet" assert spot_fleet_config["AllocationStrategy"] == "diversified" assert spot_fleet_config["FulfilledCapacity"] == 6.0 assert len(spot_fleet_config["LaunchSpecifications"]) == 2 launch_spec = spot_fleet_config["LaunchSpecifications"][0] assert launch_spec["EbsOptimized"] is False assert launch_spec["ImageId"] == EXAMPLE_AMI_ID assert launch_spec["InstanceType"] == "t2.small" assert launch_spec["SubnetId"] == subnet_id assert launch_spec["SpotPrice"] == "0.13" assert launch_spec["WeightedCapacity"] == 2.0 @mock_aws def test_stack_spot_fleet_should_figure_out_default_price(): conn = boto3.client("ec2", "us-east-1") vpc = conn.create_vpc(CidrBlock="10.0.0.0/16")["Vpc"] subnet = conn.create_subnet( VpcId=vpc["VpcId"], CidrBlock="10.0.0.0/16", AvailabilityZone="us-east-1a" )["Subnet"] subnet_id = subnet["SubnetId"] spot_fleet_template = { "Resources": { "SpotFleet1": { "Type": "AWS::EC2::SpotFleet", "Properties": { "SpotFleetRequestConfigData": { "IamFleetRole": f"arn:aws:iam::{ACCOUNT_ID}:role/fleet", "TargetCapacity": 6, "AllocationStrategy": "diversified", "LaunchSpecifications": [ { "EbsOptimized": "false", "InstanceType": "t2.small", "ImageId": EXAMPLE_AMI_ID, "SubnetId": subnet_id, "WeightedCapacity": "2", }, { "EbsOptimized": "true", "InstanceType": "t2.large", "ImageId": EXAMPLE_AMI_ID, "Monitoring": {"Enabled": "true"}, "SecurityGroups": [{"GroupId": "sg-123"}], "SubnetId": subnet_id, "IamInstanceProfile": { "Arn": f"arn:aws:iam::{ACCOUNT_ID}:role/fleet" }, "WeightedCapacity": "4", }, ], } }, } } } spot_fleet_template_json = json.dumps(spot_fleet_template) cf_conn = boto3.client("cloudformation", "us-east-1") stack_id = cf_conn.create_stack( StackName="test_stack", TemplateBody=spot_fleet_template_json )["StackId"] stack_resources = cf_conn.list_stack_resources(StackName=stack_id) assert len(stack_resources["StackResourceSummaries"]) == 1 spot_fleet_id = stack_resources["StackResourceSummaries"][0]["PhysicalResourceId"] spot_fleet_requests = conn.describe_spot_fleet_requests( SpotFleetRequestIds=[spot_fleet_id] )["SpotFleetRequestConfigs"] assert len(spot_fleet_requests) == 1 spot_fleet_request = spot_fleet_requests[0] assert spot_fleet_request["SpotFleetRequestState"] == "active" spot_fleet_config = spot_fleet_request["SpotFleetRequestConfig"] assert "SpotPrice" not in spot_fleet_config assert len(spot_fleet_config["LaunchSpecifications"]) == 2 launch_spec1 = spot_fleet_config["LaunchSpecifications"][0] launch_spec2 = spot_fleet_config["LaunchSpecifications"][1] assert "SpotPrice" not in launch_spec1 assert "SpotPrice" not in launch_spec2 @mock_aws def test_invalid_action_type_listener_rule(): invalid_listener_template = { "AWSTemplateFormatVersion": "2010-09-09", "Resources": { "alb": { "Type": "AWS::ElasticLoadBalancingV2::LoadBalancer", "Properties": { "Name": "myelbv2", "Scheme": "internet-facing", "Subnets": [{"Ref": "mysubnet"}], }, }, "mytargetgroup1": { "Type": "AWS::ElasticLoadBalancingV2::TargetGroup", "Properties": {"Name": "mytargetgroup1"}, }, "mytargetgroup2": { "Type": "AWS::ElasticLoadBalancingV2::TargetGroup", "Properties": {"Name": "mytargetgroup2"}, }, "listener": { "Type": "AWS::ElasticLoadBalancingV2::Listener", "Properties": { "DefaultActions": [ {"Type": "forward", "TargetGroupArn": {"Ref": "mytargetgroup1"}} ], "LoadBalancerArn": {"Ref": "alb"}, "Port": "80", "Protocol": "HTTP", }, }, "rule": { "Type": "AWS::ElasticLoadBalancingV2::ListenerRule", "Properties": { "Actions": [ { "Type": "forward2", "TargetGroupArn": {"Ref": "mytargetgroup2"}, } ], "Conditions": [{"field": "path-pattern", "values": ["/*"]}], "ListenerArn": {"Ref": "listener"}, "Priority": 2, }, }, "myvpc": { "Type": "AWS::EC2::VPC", "Properties": {"CidrBlock": "10.0.0.0/16"}, }, "mysubnet": { "Type": "AWS::EC2::Subnet", "Properties": {"CidrBlock": "10.0.0.0/27", "VpcId": {"Ref": "myvpc"}}, }, }, } listener_template_json = json.dumps(invalid_listener_template) cfn_conn = boto3.client("cloudformation", "us-west-1") with pytest.raises(ClientError) as exc: cfn_conn.create_stack(StackName="s", TemplateBody=listener_template_json) err = exc.value.response["Error"] assert err["Code"] == "ValidationError" @mock_aws def test_update_stack_listener_and_rule(): initial_template = { "AWSTemplateFormatVersion": "2010-09-09", "Resources": { "alb": { "Type": "AWS::ElasticLoadBalancingV2::LoadBalancer", "Properties": { "Name": "myelbv2", "Scheme": "internet-facing", "Subnets": [{"Ref": "mysubnet"}], "SecurityGroups": [{"Ref": "mysg"}], "Type": "application", "IpAddressType": "ipv4", }, }, "mytargetgroup1": { "Type": "AWS::ElasticLoadBalancingV2::TargetGroup", "Properties": {"Name": "mytargetgroup1"}, }, "mytargetgroup2": { "Type": "AWS::ElasticLoadBalancingV2::TargetGroup", "Properties": {"Name": "mytargetgroup2"}, }, "listener": { "Type": "AWS::ElasticLoadBalancingV2::Listener", "Properties": { "DefaultActions": [ {"Type": "forward", "TargetGroupArn": {"Ref": "mytargetgroup1"}} ], "LoadBalancerArn": {"Ref": "alb"}, "Port": "80", "Protocol": "HTTP", }, }, "rule": { "Type": "AWS::ElasticLoadBalancingV2::ListenerRule", "Properties": { "Actions": [ { "Type": "forward", "TargetGroupArn": {"Ref": "mytargetgroup2"}, } ], "Conditions": [{"Field": "path-pattern", "Values": ["/*"]}], "ListenerArn": {"Ref": "listener"}, "Priority": 2, }, }, "myvpc": { "Type": "AWS::EC2::VPC", "Properties": {"CidrBlock": "10.0.0.0/16"}, }, "mysubnet": { "Type": "AWS::EC2::Subnet", "Properties": {"CidrBlock": "10.0.0.0/27", "VpcId": {"Ref": "myvpc"}}, }, "mysg": { "Type": "AWS::EC2::SecurityGroup", "Properties": { "GroupName": "mysg", "GroupDescription": "test security group", "VpcId": {"Ref": "myvpc"}, }, }, }, } initial_template_json = json.dumps(initial_template) cfn_conn = boto3.client("cloudformation", "us-west-1") cfn_conn.create_stack(StackName="initial_stack", TemplateBody=initial_template_json) elbv2_conn = boto3.client("elbv2", "us-west-1") initial_template["Resources"]["rule"]["Properties"]["Conditions"][0][ "Field" ] = "host-header" initial_template["Resources"]["rule"]["Properties"]["Conditions"][0]["Values"] = "*" initial_template["Resources"]["listener"]["Properties"]["Port"] = 90 initial_template_json = json.dumps(initial_template) cfn_conn.update_stack(StackName="initial_stack", TemplateBody=initial_template_json) load_balancers = elbv2_conn.describe_load_balancers()["LoadBalancers"] listeners = elbv2_conn.describe_listeners( LoadBalancerArn=load_balancers[0]["LoadBalancerArn"] )["Listeners"] assert listeners[0]["Port"] == 90 l_rule = elbv2_conn.describe_rules(ListenerArn=listeners[0]["ListenerArn"])["Rules"] assert l_rule[0]["Conditions"] == [{"Field": "host-header", "Values": ["*"]}] @mock_aws def test_stack_elbv2_resources_integration(): alb_template = { "AWSTemplateFormatVersion": "2010-09-09", "Outputs": { "albdns": { "Description": "Load balanacer DNS", "Value": {"Fn::GetAtt": ["alb", "DNSName"]}, }, "albname": { "Description": "Load balancer name", "Value": {"Fn::GetAtt": ["alb", "LoadBalancerName"]}, }, "canonicalhostedzoneid": { "Description": "Load balancer canonical hosted zone ID", "Value": {"Fn::GetAtt": ["alb", "CanonicalHostedZoneID"]}, }, }, "Resources": { "alb": { "Type": "AWS::ElasticLoadBalancingV2::LoadBalancer", "Properties": { "Name": "myelbv2", "Scheme": "internet-facing", "Subnets": [{"Ref": "mysubnet"}], "SecurityGroups": [{"Ref": "mysg"}], "Type": "application", "IpAddressType": "ipv4", }, }, "mytargetgroup1": { "Type": "AWS::ElasticLoadBalancingV2::TargetGroup", "Properties": { "HealthCheckIntervalSeconds": 30, "HealthCheckPath": "/status", "HealthCheckPort": 80, "HealthCheckProtocol": "HTTP", "HealthCheckTimeoutSeconds": 5, "HealthyThresholdCount": 30, "UnhealthyThresholdCount": 5, "Matcher": {"HttpCode": "200,201"}, "Name": "mytargetgroup1", "Port": 80, "Protocol": "HTTP", "TargetType": "instance", "Targets": [{"Id": {"Ref": "ec2instance", "Port": 80}}], "VpcId": {"Ref": "myvpc"}, }, }, "mytargetgroup2": { "Type": "AWS::ElasticLoadBalancingV2::TargetGroup", "Properties": { "HealthCheckIntervalSeconds": 30, "HealthCheckPath": "/status", "HealthCheckPort": 8080, "HealthCheckProtocol": "HTTP", "HealthCheckTimeoutSeconds": 5, "HealthyThresholdCount": 30, "UnhealthyThresholdCount": 5, "Name": "mytargetgroup2", "Port": 8080, "Protocol": "HTTP", "TargetType": "instance", "Targets": [{"Id": {"Ref": "ec2instance", "Port": 8080}}], "VpcId": {"Ref": "myvpc"}, }, }, "listener": { "Type": "AWS::ElasticLoadBalancingV2::Listener", "Properties": { "DefaultActions": [ {"Type": "forward", "TargetGroupArn": {"Ref": "mytargetgroup1"}} ], "LoadBalancerArn": {"Ref": "alb"}, "Port": "80", "Protocol": "HTTP", }, }, "rule": { "Type": "AWS::ElasticLoadBalancingV2::ListenerRule", "Properties": { "Actions": [ { "Type": "forward", "ForwardConfig": { "TargetGroups": [ { "TargetGroupArn": {"Ref": "mytargetgroup2"}, "Weight": 1, }, { "TargetGroupArn": {"Ref": "mytargetgroup1"}, "Weight": 2, }, ] }, } ], "Conditions": [{"Field": "path-pattern", "Values": ["/*"]}], "ListenerArn": {"Ref": "listener"}, "Priority": 2, }, }, "rule2": { "Type": "AWS::ElasticLoadBalancingV2::ListenerRule", "Properties": { "Actions": [ {"Type": "forward", "TargetGroupArn": {"Ref": "mytargetgroup2"}} ], "Conditions": [{"Field": "host-header", "Values": ["example.com"]}], "ListenerArn": {"Ref": "listener"}, "Priority": 30, }, }, "myvpc": { "Type": "AWS::EC2::VPC", "Properties": {"CidrBlock": "10.0.0.0/16"}, }, "mysubnet": { "Type": "AWS::EC2::Subnet", "Properties": {"CidrBlock": "10.0.0.0/27", "VpcId": {"Ref": "myvpc"}}, }, "mysg": { "Type": "AWS::EC2::SecurityGroup", "Properties": { "GroupName": "mysg", "GroupDescription": "test security group", "VpcId": {"Ref": "myvpc"}, }, }, "ec2instance": { "Type": "AWS::EC2::Instance", "Properties": {"ImageId": EXAMPLE_AMI_ID, "UserData": "some user data"}, }, }, } alb_template_json = json.dumps(alb_template) cfn_conn = boto3.client("cloudformation", "us-west-1") cfn_conn.create_stack(StackName="elb_stack", TemplateBody=alb_template_json) elbv2_conn = boto3.client("elbv2", "us-west-1") lbs = elbv2_conn.describe_load_balancers()["LoadBalancers"] assert len(lbs) == 1 assert lbs[0]["LoadBalancerName"] == "myelbv2" assert lbs[0]["Scheme"] == "internet-facing" assert lbs[0]["Type"] == "application" assert lbs[0]["IpAddressType"] == "ipv4" target_groups = elbv2_conn.describe_target_groups()["TargetGroups"] # sort to do comparison with indexes target_groups = sorted(target_groups, key=lambda tg: tg["TargetGroupName"]) assert len(target_groups) == 2 assert target_groups[0]["HealthCheckIntervalSeconds"] == 30 assert target_groups[0]["HealthCheckPath"] == "/status" assert target_groups[0]["HealthCheckPort"] == "80" assert target_groups[0]["HealthCheckProtocol"] == "HTTP" assert target_groups[0]["HealthCheckTimeoutSeconds"] == 5 assert target_groups[0]["HealthyThresholdCount"] == 30 assert target_groups[0]["UnhealthyThresholdCount"] == 5 assert target_groups[0]["Matcher"] == {"HttpCode": "200,201"} assert target_groups[0]["TargetGroupName"] == "mytargetgroup1" assert target_groups[0]["Port"] == 80 assert target_groups[0]["Protocol"] == "HTTP" assert target_groups[0]["TargetType"] == "instance" assert target_groups[1]["HealthCheckIntervalSeconds"] == 30 assert target_groups[1]["HealthCheckPath"] == "/status" assert target_groups[1]["HealthCheckPort"] == "8080" assert target_groups[1]["HealthCheckProtocol"] == "HTTP" assert target_groups[1]["HealthCheckTimeoutSeconds"] == 5 assert target_groups[1]["HealthyThresholdCount"] == 30 assert target_groups[1]["UnhealthyThresholdCount"] == 5 assert target_groups[1]["Matcher"] == {"HttpCode": "200"} assert target_groups[1]["TargetGroupName"] == "mytargetgroup2" assert target_groups[1]["Port"] == 8080 assert target_groups[1]["Protocol"] == "HTTP" assert target_groups[1]["TargetType"] == "instance" lstnrs = elbv2_conn.describe_listeners(LoadBalancerArn=lbs[0]["LoadBalancerArn"])[ "Listeners" ] assert len(lstnrs) == 1 assert lstnrs[0]["LoadBalancerArn"] == lbs[0]["LoadBalancerArn"] assert lstnrs[0]["Port"] == 80 assert lstnrs[0]["Protocol"] == "HTTP" assert lstnrs[0]["DefaultActions"] == [ {"Type": "forward", "TargetGroupArn": target_groups[0]["TargetGroupArn"]} ] rule = elbv2_conn.describe_rules(ListenerArn=lstnrs[0]["ListenerArn"])["Rules"] assert len(rule) == 3 assert rule[0]["Priority"] == "2" assert rule[0]["Actions"] == [ { "Type": "forward", "ForwardConfig": { "TargetGroups": [ { "TargetGroupArn": target_groups[1]["TargetGroupArn"], "Weight": 1, }, { "TargetGroupArn": target_groups[0]["TargetGroupArn"], "Weight": 2, }, ], "TargetGroupStickinessConfig": {"Enabled": False}, }, } ] assert rule[0]["Conditions"] == [{"Field": "path-pattern", "Values": ["/*"]}] assert rule[1]["Priority"] == "30" assert rule[1]["Actions"] == [ {"Type": "forward", "TargetGroupArn": target_groups[1]["TargetGroupArn"]} ] assert rule[1]["Conditions"] == [ {"Field": "host-header", "Values": ["example.com"]} ] # test outputs stacks = cfn_conn.describe_stacks(StackName="elb_stack")["Stacks"] assert len(stacks) == 1 dns = list( filter(lambda item: item["OutputKey"] == "albdns", stacks[0]["Outputs"]) )[0] name = list( filter(lambda item: item["OutputKey"] == "albname", stacks[0]["Outputs"]) )[0] assert dns["OutputValue"] == lbs[0]["DNSName"] assert name["OutputValue"] == lbs[0]["LoadBalancerName"] @mock_aws def test_stack_dynamodb_resources_integration(): dynamodb_template = { "AWSTemplateFormatVersion": "2010-09-09", "Resources": { "myDynamoDBTable": { "Type": "AWS::DynamoDB::Table", "Properties": { "AttributeDefinitions": [ {"AttributeName": "Album", "AttributeType": "S"}, {"AttributeName": "Artist", "AttributeType": "S"}, {"AttributeName": "Sales", "AttributeType": "N"}, {"AttributeName": "NumberOfSongs", "AttributeType": "N"}, ], "KeySchema": [ {"AttributeName": "Album", "KeyType": "HASH"}, {"AttributeName": "Artist", "KeyType": "RANGE"}, ], "ProvisionedThroughput": { "ReadCapacityUnits": "5", "WriteCapacityUnits": "5", }, "TableName": "myTableName", "GlobalSecondaryIndexes": [ { "IndexName": "myGSI", "KeySchema": [ {"AttributeName": "Sales", "KeyType": "HASH"}, {"AttributeName": "Artist", "KeyType": "RANGE"}, ], "Projection": { "NonKeyAttributes": ["Album", "NumberOfSongs"], "ProjectionType": "INCLUDE", }, "ProvisionedThroughput": { "ReadCapacityUnits": "5", "WriteCapacityUnits": "5", }, }, { "IndexName": "myGSI2", "KeySchema": [ {"AttributeName": "NumberOfSongs", "KeyType": "HASH"}, {"AttributeName": "Sales", "KeyType": "RANGE"}, ], "Projection": { "NonKeyAttributes": ["Album", "Artist"], "ProjectionType": "INCLUDE", }, "ProvisionedThroughput": { "ReadCapacityUnits": "5", "WriteCapacityUnits": "5", }, }, ], "LocalSecondaryIndexes": [ { "IndexName": "myLSI", "KeySchema": [ {"AttributeName": "Album", "KeyType": "HASH"}, {"AttributeName": "Sales", "KeyType": "RANGE"}, ], "Projection": { "NonKeyAttributes": ["Artist", "NumberOfSongs"], "ProjectionType": "INCLUDE", }, } ], "StreamSpecification": {"StreamViewType": "KEYS_ONLY"}, }, } }, } dynamodb_template_json = json.dumps(dynamodb_template) cfn_conn = boto3.client("cloudformation", "us-east-1") cfn_conn.create_stack( StackName="dynamodb_stack", TemplateBody=dynamodb_template_json ) dynamodb_client = boto3.client("dynamodb", region_name="us-east-1") table_desc = dynamodb_client.describe_table(TableName="myTableName")["Table"] assert table_desc["StreamSpecification"] == { "StreamEnabled": True, "StreamViewType": "KEYS_ONLY", } dynamodb_conn = boto3.resource("dynamodb", region_name="us-east-1") table = dynamodb_conn.Table("myTableName") assert table.name == "myTableName" table.put_item( Item={"Album": "myAlbum", "Artist": "myArtist", "Sales": 10, "NumberOfSongs": 5} ) response = table.get_item(Key={"Album": "myAlbum", "Artist": "myArtist"}) assert response["Item"]["Album"] == "myAlbum" assert response["Item"]["Sales"] == Decimal("10") assert response["Item"]["NumberOfSongs"] == Decimal("5") assert response["Item"]["Album"] == "myAlbum" @mock_aws def test_create_log_group_using_fntransform(): s3_resource = boto3.resource("s3") s3_resource.create_bucket( Bucket="owi-common-cf", CreateBucketConfiguration={"LocationConstraint": "us-west-2"}, ) s3_resource.Object("owi-common-cf", "snippets/test.json").put( Body=json.dumps({"lgname": {"name": "some-log-group"}}) ) template = { "AWSTemplateFormatVersion": "2010-09-09", "Mappings": { "EnvironmentMapping": { "Fn::Transform": { "Name": "AWS::Include", "Parameters": {"Location": "s3://owi-common-cf/snippets/test.json"}, } } }, "Resources": { "LogGroup": { "Properties": { "LogGroupName": { "Fn::FindInMap": ["EnvironmentMapping", "lgname", "name"] }, "RetentionInDays": 90, }, "Type": "AWS::Logs::LogGroup", } }, } cf_conn = boto3.client("cloudformation", "us-west-2") cf_conn.create_stack(StackName="test_stack", TemplateBody=json.dumps(template)) logs_conn = boto3.client("logs", region_name="us-west-2") log_group = logs_conn.describe_log_groups()["logGroups"][0] assert log_group["logGroupName"] == "some-log-group" @mock_aws def test_create_cloudwatch_logs_resource_policy(): policy_document = json.dumps( { "Statement": [ { "Action": ["logs:CreateLogStream", "logs:PutLogEvents"], "Effect": "Allow", "Principal": {"Service": "es.amazonaws.com"}, "Resource": "*", } ] } ) template1 = { "AWSTemplateFormatVersion": "2010-09-09", "Resources": { "LogGroupPolicy1": { "Type": "AWS::Logs::ResourcePolicy", "Properties": { "PolicyDocument": policy_document, "PolicyName": "TestPolicyA", }, } }, } template2 = { "AWSTemplateFormatVersion": "2010-09-09", "Resources": { "LogGroupPolicy1": { "Type": "AWS::Logs::ResourcePolicy", "Properties": { "PolicyDocument": policy_document, "PolicyName": "TestPolicyB", }, }, "LogGroupPolicy2": { "Type": "AWS::Logs::ResourcePolicy", "Properties": { "PolicyDocument": policy_document, "PolicyName": "TestPolicyC", }, }, }, } cf_conn = boto3.client("cloudformation", "us-east-1") cf_conn.create_stack(StackName="test_stack", TemplateBody=json.dumps(template1)) logs_conn = boto3.client("logs", region_name="us-east-1") policies = logs_conn.describe_resource_policies()["resourcePolicies"] assert len(policies) == 1 assert policies[0]["policyName"] == "TestPolicyA" assert policies[0]["policyDocument"] == policy_document cf_conn.update_stack(StackName="test_stack", TemplateBody=json.dumps(template2)) policies = logs_conn.describe_resource_policies()["resourcePolicies"] assert len(policies) == 2 policy_b = [pol for pol in policies if pol["policyName"] == "TestPolicyB"][0][ "policyDocument" ] assert policy_b == policy_document policy_c = [pol for pol in policies if pol["policyName"] == "TestPolicyC"][0][ "policyDocument" ] assert policy_c == policy_document cf_conn.update_stack(StackName="test_stack", TemplateBody=json.dumps(template1)) policies = logs_conn.describe_resource_policies()["resourcePolicies"] assert len(policies) == 1 assert policies[0]["policyName"] == "TestPolicyA" assert policies[0]["policyDocument"] == policy_document @mock_aws def test_delete_stack_containing_cloudwatch_logs_resource_policy(): template1 = { "AWSTemplateFormatVersion": "2010-09-09", "Resources": { "LogGroupPolicy1": { "Type": "AWS::Logs::ResourcePolicy", "Properties": { "PolicyDocument": '{"Statement":[{"Action":"logs:*","Effect":"Allow","Principal":"*","Resource":"*"}]}', "PolicyName": "TestPolicyA", }, } }, } cf_conn = boto3.client("cloudformation", "us-east-1") cf_conn.create_stack(StackName="test_stack", TemplateBody=json.dumps(template1)) logs_conn = boto3.client("logs", region_name="us-east-1") policies = logs_conn.describe_resource_policies()["resourcePolicies"] assert len(policies) == 1 cf_conn.delete_stack(StackName="test_stack") policies = logs_conn.describe_resource_policies()["resourcePolicies"] assert len(policies) == 0 @mock_aws def test_delete_stack_with_deletion_policy_boto3(): sqs_template = { "AWSTemplateFormatVersion": "2010-09-09", "Resources": { "QueueGroup": { "DeletionPolicy": "Retain", "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, ) sqs = boto3.client("sqs", region_name="us-west-1") assert len(sqs.list_queues()["QueueUrls"]) == 1 cf.delete_stack(StackName="test_stack") assert len(sqs.list_queues()["QueueUrls"]) == 1 @mock_aws def test_stack_events_create_rule_integration(): events_template = { "AWSTemplateFormatVersion": "2010-09-09", "Resources": { "Event": { "Type": "AWS::Events::Rule", "Properties": { "Name": "quick-fox", "State": "ENABLED", "ScheduleExpression": "rate(5 minutes)", }, } }, } cf_conn = boto3.client("cloudformation", "us-west-2") cf_conn.create_stack( StackName="test_stack", TemplateBody=json.dumps(events_template) ) rules = boto3.client("events", "us-west-2").list_rules() assert len(rules["Rules"]) == 1 assert rules["Rules"][0]["Name"] == "quick-fox" assert rules["Rules"][0]["State"] == "ENABLED" assert rules["Rules"][0]["ScheduleExpression"] == "rate(5 minutes)" @mock_aws def test_stack_events_delete_rule_integration(): events_template = { "AWSTemplateFormatVersion": "2010-09-09", "Resources": { "Event": { "Type": "AWS::Events::Rule", "Properties": { "Name": "quick-fox", "State": "ENABLED", "ScheduleExpression": "rate(5 minutes)", }, } }, } cf_conn = boto3.client("cloudformation", "us-west-2") cf_conn.create_stack( StackName="test_stack", TemplateBody=json.dumps(events_template) ) rules = boto3.client("events", "us-west-2").list_rules() assert len(rules["Rules"]) == 1 cf_conn.delete_stack(StackName="test_stack") rules = boto3.client("events", "us-west-2").list_rules() assert len(rules["Rules"]) == 0 @mock_aws def test_stack_events_create_rule_without_name_integration(): events_template = { "AWSTemplateFormatVersion": "2010-09-09", "Resources": { "Event": { "Type": "AWS::Events::Rule", "Properties": { "State": "ENABLED", "ScheduleExpression": "rate(5 minutes)", }, } }, } cf_conn = boto3.client("cloudformation", "us-west-2") cf_conn.create_stack( StackName="test_stack", TemplateBody=json.dumps(events_template) ) rules = boto3.client("events", "us-west-2").list_rules() assert "test_stack-Event-" in rules["Rules"][0]["Name"] @mock_aws def test_stack_events_create_rule_as_target(): events_template = { "AWSTemplateFormatVersion": "2010-09-09", "Resources": { "SecurityGroup": { "Type": "AWS::Logs::LogGroup", "Properties": { "LogGroupName": {"Fn::GetAtt": ["Event", "Arn"]}, "RetentionInDays": 3, }, }, "Event": { "Type": "AWS::Events::Rule", "Properties": { "State": "ENABLED", "ScheduleExpression": "rate(5 minutes)", }, }, }, } cf_conn = boto3.client("cloudformation", "us-west-2") cf_conn.create_stack( StackName="test_stack", TemplateBody=json.dumps(events_template) ) rules = boto3.client("events", "us-west-2").list_rules() log_groups = boto3.client("logs", "us-west-2").describe_log_groups() assert "test_stack-Event-" in rules["Rules"][0]["Name"] assert log_groups["logGroups"][0]["logGroupName"] == rules["Rules"][0]["Arn"] assert log_groups["logGroups"][0]["retentionInDays"] == 3 @mock_aws def test_stack_events_update_rule_integration(): events_template = Template( """{ "AWSTemplateFormatVersion": "2010-09-09", "Resources": { "Event": { "Type": "AWS::Events::Rule", "Properties": { "Name": "$Name", "State": "$State", "ScheduleExpression": "rate(5 minutes)", }, } }, } """ ) cf_conn = boto3.client("cloudformation", "us-west-2") original_template = events_template.substitute(Name="Foo", State="ENABLED") cf_conn.create_stack(StackName="test_stack", TemplateBody=original_template) rules = boto3.client("events", "us-west-2").list_rules() assert len(rules["Rules"]) == 1 assert rules["Rules"][0]["Name"] == "Foo" assert rules["Rules"][0]["State"] == "ENABLED" update_template = events_template.substitute(Name="Bar", State="DISABLED") cf_conn.update_stack(StackName="test_stack", TemplateBody=update_template) rules = boto3.client("events", "us-west-2").list_rules() assert len(rules["Rules"]) == 1 assert rules["Rules"][0]["Name"] == "Bar" assert rules["Rules"][0]["State"] == "DISABLED" @mock_aws def test_autoscaling_propagate_tags(): autoscaling_group_with_tags = { "AWSTemplateFormatVersion": "2010-09-09", "Resources": { "AutoScalingGroup": { "Type": "AWS::AutoScaling::AutoScalingGroup", "Properties": { "AutoScalingGroupName": "test-scaling-group", "DesiredCapacity": 1, "MinSize": 1, "MaxSize": 50, "LaunchConfigurationName": "test-launch-config", "AvailabilityZones": ["us-east-1a"], "Tags": [ { "Key": "test-key-propagate", "Value": "test", "PropagateAtLaunch": True, }, { "Key": "test-key-no-propagate", "Value": "test", "PropagateAtLaunch": False, }, ], }, "DependsOn": "LaunchConfig", }, "LaunchConfig": { "Type": "AWS::AutoScaling::LaunchConfiguration", "Properties": { "LaunchConfigurationName": "test-launch-config", "ImageId": EXAMPLE_AMI_ID, "InstanceType": "t2.medium", }, }, "ScheduledAction": { "Type": "AWS::AutoScaling::ScheduledAction", "Properties": { "AutoScalingGroupName": "test-scaling-group", "DesiredCapacity": 10, "EndTime": "2022-08-01T00:00:00Z", "MaxSize": 15, "MinSize": 5, "Recurrence": "* * * * *", "StartTime": "2022-07-01T00:00:00Z", }, }, }, } boto3.client("cloudformation", "us-east-1").create_stack( StackName="propagate_tags_test", TemplateBody=json.dumps(autoscaling_group_with_tags), ) autoscaling = boto3.client("autoscaling", "us-east-1") autoscaling_group_tags = autoscaling.describe_auto_scaling_groups()[ "AutoScalingGroups" ][0]["Tags"] propagation_dict = { tag["Key"]: tag["PropagateAtLaunch"] for tag in autoscaling_group_tags } assert propagation_dict["test-key-propagate"] assert not propagation_dict["test-key-no-propagate"] @mock_aws def test_stack_eventbus_create_from_cfn_integration(): eventbus_template = """{ "AWSTemplateFormatVersion": "2010-09-09", "Resources": { "EventBus": { "Type": "AWS::Events::EventBus", "Properties": { "Name": "MyCustomEventBus" }, } }, }""" cf_conn = boto3.client("cloudformation", "us-west-2") cf_conn.create_stack(StackName="test_stack", TemplateBody=eventbus_template) event_buses = boto3.client("events", "us-west-2").list_event_buses( NamePrefix="MyCustom" ) assert len(event_buses["EventBuses"]) == 1 assert event_buses["EventBuses"][0]["Name"] == "MyCustomEventBus" @mock_aws def test_stack_events_delete_eventbus_integration(): eventbus_template = """{ "AWSTemplateFormatVersion": "2010-09-09", "Resources": { "EventBus": { "Type": "AWS::Events::EventBus", "Properties": { "Name": "MyCustomEventBus" }, } }, }""" cf_conn = boto3.client("cloudformation", "us-west-2") cf_conn.create_stack(StackName="test_stack", TemplateBody=eventbus_template) event_buses = boto3.client("events", "us-west-2").list_event_buses( NamePrefix="MyCustom" ) assert len(event_buses["EventBuses"]) == 1 cf_conn.delete_stack(StackName="test_stack") event_buses = boto3.client("events", "us-west-2").list_event_buses( NamePrefix="MyCustom" ) assert len(event_buses["EventBuses"]) == 0 @mock_aws def test_stack_events_delete_from_cfn_integration(): eventbus_template = Template( """{ "AWSTemplateFormatVersion": "2010-09-09", "Resources": { "$resource_name": { "Type": "AWS::Events::EventBus", "Properties": { "Name": "$name" }, } }, }""" ) cf_conn = boto3.client("cloudformation", "us-west-2") original_template = eventbus_template.substitute( {"resource_name": "original", "name": "MyCustomEventBus"} ) cf_conn.create_stack(StackName="test_stack", TemplateBody=original_template) original_event_buses = boto3.client("events", "us-west-2").list_event_buses( NamePrefix="MyCustom" ) assert len(original_event_buses["EventBuses"]) == 1 original_eventbus = original_event_buses["EventBuses"][0] updated_template = eventbus_template.substitute( {"resource_name": "updated", "name": "AnotherEventBus"} ) cf_conn.update_stack(StackName="test_stack", TemplateBody=updated_template) update_event_buses = boto3.client("events", "us-west-2").list_event_buses( NamePrefix="AnotherEventBus" ) assert len(update_event_buses["EventBuses"]) == 1 assert update_event_buses["EventBuses"][0]["Arn"] != original_eventbus["Arn"] @mock_aws def test_stack_events_update_from_cfn_integration(): eventbus_template = Template( """{ "AWSTemplateFormatVersion": "2010-09-09", "Resources": { "EventBus": { "Type": "AWS::Events::EventBus", "Properties": { "Name": "$name" }, } }, }""" ) cf_conn = boto3.client("cloudformation", "us-west-2") original_template = eventbus_template.substitute({"name": "MyCustomEventBus"}) cf_conn.create_stack(StackName="test_stack", TemplateBody=original_template) original_event_buses = boto3.client("events", "us-west-2").list_event_buses( NamePrefix="MyCustom" ) assert len(original_event_buses["EventBuses"]) == 1 original_eventbus = original_event_buses["EventBuses"][0] updated_template = eventbus_template.substitute({"name": "NewEventBus"}) cf_conn.update_stack(StackName="test_stack", TemplateBody=updated_template) update_event_buses = boto3.client("events", "us-west-2").list_event_buses( NamePrefix="NewEventBus" ) assert len(update_event_buses["EventBuses"]) == 1 assert update_event_buses["EventBuses"][0]["Name"] == "NewEventBus" assert update_event_buses["EventBuses"][0]["Arn"] != original_eventbus["Arn"] @mock_aws def test_stack_events_get_attribute_integration(): eventbus_template = """{ "AWSTemplateFormatVersion": "2010-09-09", "Resources": { "EventBus": { "Type": "AWS::Events::EventBus", "Properties": { "Name": "MyEventBus" }, } }, "Outputs": { "bus_arn": {"Value": {"Fn::GetAtt": ["EventBus", "Arn"]}}, "bus_name": {"Value": {"Fn::GetAtt": ["EventBus", "Name"]}}, } }""" cf = boto3.client("cloudformation", "us-west-2") events = boto3.client("events", "us-west-2") cf.create_stack(StackName="test_stack", TemplateBody=eventbus_template) stack = cf.describe_stacks(StackName="test_stack")["Stacks"][0] outputs = stack["Outputs"] output_arn = list(filter(lambda item: item["OutputKey"] == "bus_arn", outputs))[0] output_name = list(filter(lambda item: item["OutputKey"] == "bus_name", outputs))[0] event_bus = events.list_event_buses(NamePrefix="MyEventBus")["EventBuses"][0] assert output_arn["OutputValue"] == event_bus["Arn"] assert output_name["OutputValue"] == event_bus["Name"] @mock_aws def test_dynamodb_table_creation(): CFN_TEMPLATE = { "Outputs": {"MyTableName": {"Value": {"Ref": "MyTable"}}}, "Resources": { "MyTable": { "Type": "AWS::DynamoDB::Table", "Properties": { "KeySchema": [{"AttributeName": "id", "KeyType": "HASH"}], "AttributeDefinitions": [ {"AttributeName": "id", "AttributeType": "S"} ], "BillingMode": "PAY_PER_REQUEST", }, }, }, } stack_name = "foobar" cfn = boto3.client("cloudformation", "us-west-2") cfn.create_stack(StackName=stack_name, TemplateBody=json.dumps(CFN_TEMPLATE)) # Wait until moto creates the stack waiter = cfn.get_waiter("stack_create_complete") waiter.wait(StackName=stack_name) # Verify the TableName is part of the outputs stack = cfn.describe_stacks(StackName=stack_name)["Stacks"][0] outputs = stack["Outputs"] assert len(outputs) == 1 assert outputs[0]["OutputKey"] == "MyTableName" assert "foobar" in outputs[0]["OutputValue"] # Assert the table is created ddb = boto3.client("dynamodb", "us-west-2") table_names = ddb.list_tables()["TableNames"] assert table_names == [outputs[0]["OutputValue"]] @mock_aws def test_ssm_parameter(): parameter_template = { "AWSTemplateFormatVersion": "2010-09-09", "Resources": { "BasicParameter": { "Type": "AWS::SSM::Parameter", "Properties": { "Name": "test_ssm", "Type": "String", "Value": "Test SSM Parameter", "Description": "Test SSM Description", "AllowedPattern": "^[a-zA-Z]{1,10}$", }, } }, } stack_name = "test_stack" cfn = boto3.client("cloudformation", "us-west-2") cfn.create_stack(StackName=stack_name, TemplateBody=json.dumps(parameter_template)) # Wait until moto creates the stack waiter = cfn.get_waiter("stack_create_complete") waiter.wait(StackName=stack_name) stack_resources = cfn.list_stack_resources(StackName=stack_name) ssm_resource = stack_resources.get("StackResourceSummaries")[0] assert ssm_resource.get("PhysicalResourceId") == "test_ssm" ssm_client = boto3.client("ssm", region_name="us-west-2") parameters = ssm_client.get_parameters(Names=["test_ssm"], WithDecryption=False)[ "Parameters" ] assert len(parameters) == 1 assert parameters[0]["Name"] == "test_ssm" assert parameters[0]["Value"] == "Test SSM Parameter" @mock_aws def test_ssm_parameter_update_stack(): parameter_template = { "AWSTemplateFormatVersion": "2010-09-09", "Resources": { "BasicParameter": { "Type": "AWS::SSM::Parameter", "Properties": { "Name": "test_ssm", "Type": "String", "Value": "Test SSM Parameter", "Description": "Test SSM Description", "AllowedPattern": "^[a-zA-Z]{1,10}$", }, } }, } stack_name = "test_stack" cfn = boto3.client("cloudformation", "us-west-2") cfn.create_stack(StackName=stack_name, TemplateBody=json.dumps(parameter_template)) # Wait until moto creates the stack waiter = cfn.get_waiter("stack_create_complete") waiter.wait(StackName=stack_name) ssm_client = boto3.client("ssm", region_name="us-west-2") parameters = ssm_client.get_parameters(Names=["test_ssm"], WithDecryption=False)[ "Parameters" ] assert len(parameters) == 1 assert parameters[0]["Name"] == "test_ssm" assert parameters[0]["Value"] == "Test SSM Parameter" parameter_template["Resources"]["BasicParameter"]["Properties"][ "Value" ] = "Test SSM Parameter Updated" cfn.update_stack(StackName=stack_name, TemplateBody=json.dumps(parameter_template)) ssm_client = boto3.client("ssm", region_name="us-west-2") parameters = ssm_client.get_parameters(Names=["test_ssm"], WithDecryption=False)[ "Parameters" ] assert len(parameters) == 1 assert parameters[0]["Name"] == "test_ssm" assert parameters[0]["Value"] == "Test SSM Parameter Updated" @mock_aws def test_ssm_parameter_update_stack_and_remove_resource(): parameter_template = { "AWSTemplateFormatVersion": "2010-09-09", "Resources": { "BasicParameter": { "Type": "AWS::SSM::Parameter", "Properties": { "Name": "test_ssm", "Type": "String", "Value": "Test SSM Parameter", "Description": "Test SSM Description", "AllowedPattern": "^[a-zA-Z]{1,10}$", }, } }, } stack_name = "test_stack" cfn = boto3.client("cloudformation", "us-west-2") cfn.create_stack(StackName=stack_name, TemplateBody=json.dumps(parameter_template)) # Wait until moto creates the stack waiter = cfn.get_waiter("stack_create_complete") waiter.wait(StackName=stack_name) ssm_client = boto3.client("ssm", region_name="us-west-2") parameters = ssm_client.get_parameters(Names=["test_ssm"], WithDecryption=False)[ "Parameters" ] assert len(parameters) == 1 assert parameters[0]["Name"] == "test_ssm" assert parameters[0]["Value"] == "Test SSM Parameter" parameter_template["Resources"].pop("BasicParameter") cfn.update_stack(StackName=stack_name, TemplateBody=json.dumps(parameter_template)) ssm_client = boto3.client("ssm", region_name="us-west-1") parameters = ssm_client.get_parameters(Names=["test_ssm"], WithDecryption=False)[ "Parameters" ] assert len(parameters) == 0 @mock_aws def test_ssm_parameter_update_stack_and_add_resource(): parameter_template = {"AWSTemplateFormatVersion": "2010-09-09", "Resources": {}} stack_name = "test_stack" cfn = boto3.client("cloudformation", "us-west-2") cfn.create_stack(StackName=stack_name, TemplateBody=json.dumps(parameter_template)) # Wait until moto creates the stack waiter = cfn.get_waiter("stack_create_complete") waiter.wait(StackName=stack_name) ssm_client = boto3.client("ssm", region_name="us-west-2") parameters = ssm_client.get_parameters(Names=["test_ssm"], WithDecryption=False)[ "Parameters" ] assert len(parameters) == 0 parameter_template = { "AWSTemplateFormatVersion": "2010-09-09", "Resources": { "BasicParameter": { "Type": "AWS::SSM::Parameter", "Properties": { "Name": "test_ssm", "Type": "String", "Value": "Test SSM Parameter", "Description": "Test SSM Description", "AllowedPattern": "^[a-zA-Z]{1,10}$", }, } }, } cfn.update_stack(StackName=stack_name, TemplateBody=json.dumps(parameter_template)) ssm_client = boto3.client("ssm", region_name="us-west-2") parameters = ssm_client.get_parameters(Names=["test_ssm"], WithDecryption=False)[ "Parameters" ] assert len(parameters) == 1 assert parameters[0]["Name"] == "test_ssm" assert parameters[0]["Value"] == "Test SSM Parameter"