diff --git a/moto/cloudformation/parsing.py b/moto/cloudformation/parsing.py index a798a33ef..0c55ad418 100644 --- a/moto/cloudformation/parsing.py +++ b/moto/cloudformation/parsing.py @@ -740,11 +740,9 @@ class ResourceMap(collections_abc.Mapping): # type: ignore[type-arg] new = other_template["Resources"] resource_names_by_action = { - "Add": set(new) - set(old), - "Modify": set( - name for name in new if name in old and new[name] != old[name] - ), - "Remove": set(old) - set(new), + "Add": [name for name in new if name not in old], + "Modify": [name for name in new if name in old and new[name] != old[name]], + "Remove": [name for name in old if name not in new], } return resource_names_by_action diff --git a/moto/ec2/models/launch_templates.py b/moto/ec2/models/launch_templates.py index 3e329d866..1e4a63f34 100644 --- a/moto/ec2/models/launch_templates.py +++ b/moto/ec2/models/launch_templates.py @@ -192,6 +192,25 @@ class LaunchTemplate(TaggedEC2Resource, CloudFormationModel): backend.delete_launch_template(name, None) + @classmethod + def has_cfn_attr(cls, attr: str) -> bool: + return attr in [ + "DefaultVersionNumber", + "LaunchTemplateId", + "LatestVersionNumber", + ] + + def get_cfn_attribute(self, attribute_name: str) -> str: + from moto.cloudformation.exceptions import UnformattedGetAttTemplateException + + if attribute_name == "DefaultVersionNumber": + return str(self.default_version_number) + if attribute_name == "LaunchTemplateId": + return self.id + if attribute_name == "LatestVersionNumber": + return str(self.latest_version_number) + raise UnformattedGetAttTemplateException() + class LaunchTemplateBackend: def __init__(self) -> None: diff --git a/tests/test_ec2/test_launch_templates_cloudformation.py b/tests/test_ec2/test_launch_templates_cloudformation.py new file mode 100644 index 000000000..71ce94a98 --- /dev/null +++ b/tests/test_ec2/test_launch_templates_cloudformation.py @@ -0,0 +1,247 @@ +import json +from uuid import uuid4 + +import boto3 + +from moto import mock_autoscaling, mock_cloudformation, mock_ec2 +from tests import EXAMPLE_AMI_ID, EXAMPLE_AMI_ID2 + + +@mock_autoscaling +@mock_cloudformation +@mock_ec2 +def test_asg_with_latest_launch_template_version(): + cf_client = boto3.client("cloudformation", region_name="us-west-1") + ec2 = boto3.resource("ec2", region_name="us-west-1") + vpc = ec2.create_vpc(CidrBlock="10.0.0.0/16") + autoscaling_client = boto3.client("autoscaling", region_name="us-west-1") + + subnet1 = ec2.create_subnet( + CidrBlock="10.0.1.0/24", VpcId=vpc.id, AvailabilityZone="us-west-1a" + ) + + subnet2 = ec2.create_subnet( + CidrBlock="10.0.2.0/24", VpcId=vpc.id, AvailabilityZone="us-west-1b" + ) + + autoscaling_group_name = str(uuid4()) + + stack_name = str(uuid4()) + launch_template_name = str(uuid4())[0:6] + + version_attribute = "LatestVersionNumber" + + template_json = json.dumps( + { + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "AWS CloudFormation Template to create an ASG group with LaunchTemplate", + "Resources": { + "LaunchTemplate": { + "Type": "AWS::EC2::LaunchTemplate", + "Properties": { + "LaunchTemplateName": launch_template_name, + "LaunchTemplateData": { + "ImageId": EXAMPLE_AMI_ID, + "InstanceType": "t3.small", + "UserData": "", + }, + }, + }, + "AutoScalingGroup": { + "Type": "AWS::AutoScaling::AutoScalingGroup", + "Properties": { + "AutoScalingGroupName": autoscaling_group_name, + "VPCZoneIdentifier": [subnet1.id], + "LaunchTemplate": { + "LaunchTemplateId": {"Ref": "LaunchTemplate"}, + "Version": { + "Fn::GetAtt": ["LaunchTemplate", version_attribute] + }, + }, + "MinSize": 1, + "MaxSize": 1, + }, + }, + }, + } + ) + + cf_client.create_stack( + StackName=stack_name, + TemplateBody=template_json, + Capabilities=["CAPABILITY_NAMED_IAM"], + OnFailure="DELETE", + ) + + template_json = json.dumps( + { + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "AWS CloudFormation Template to create an ASG group with LaunchTemplate", + "Resources": { + "LaunchTemplate": { + "Type": "AWS::EC2::LaunchTemplate", + "Properties": { + "LaunchTemplateName": launch_template_name, + "LaunchTemplateData": { + "ImageId": EXAMPLE_AMI_ID2, + "InstanceType": "t3.medium", + "UserData": "", + }, + }, + }, + "AutoScalingGroup": { + "Type": "AWS::AutoScaling::AutoScalingGroup", + "Properties": { + "AutoScalingGroupName": autoscaling_group_name, + "VPCZoneIdentifier": [subnet2.id], + "LaunchTemplate": { + "LaunchTemplateId": {"Ref": "LaunchTemplate"}, + "Version": { + "Fn::GetAtt": ["LaunchTemplate", version_attribute] + }, + }, + "MinSize": 1, + "MaxSize": 2, + }, + }, + }, + } + ) + + cf_client.update_stack( + StackName=stack_name, + TemplateBody=template_json, + Capabilities=["CAPABILITY_NAMED_IAM"], + ) + + autoscaling_group = autoscaling_client.describe_auto_scaling_groups( + AutoScalingGroupNames=[ + autoscaling_group_name, + ] + )["AutoScalingGroups"][0] + + assert ( + autoscaling_group["LaunchTemplate"]["LaunchTemplateName"] + == launch_template_name + ) + assert autoscaling_group["LaunchTemplate"]["Version"] == "2" + + +@mock_autoscaling +@mock_cloudformation +@mock_ec2 +def test_asg_with_default_launch_template_version(): + cf_client = boto3.client("cloudformation", region_name="us-west-1") + ec2 = boto3.resource("ec2", region_name="us-west-1") + vpc = ec2.create_vpc(CidrBlock="10.0.0.0/16") + autoscaling_client = boto3.client("autoscaling", region_name="us-west-1") + + subnet1 = ec2.create_subnet( + CidrBlock="10.0.1.0/24", VpcId=vpc.id, AvailabilityZone="us-west-1a" + ) + + subnet2 = ec2.create_subnet( + CidrBlock="10.0.2.0/24", VpcId=vpc.id, AvailabilityZone="us-west-1b" + ) + + autoscaling_group_name = str(uuid4()) + + stack_name = str(uuid4()) + launch_template_name = str(uuid4())[0:6] + + version_attribute = "DefaultVersionNumber" + + template_json = json.dumps( + { + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "AWS CloudFormation Template to create an ASG group with LaunchTemplate", + "Resources": { + "LaunchTemplate": { + "Type": "AWS::EC2::LaunchTemplate", + "Properties": { + "LaunchTemplateName": launch_template_name, + "LaunchTemplateData": { + "ImageId": EXAMPLE_AMI_ID, + "InstanceType": "t3.small", + "UserData": "", + }, + }, + }, + "AutoScalingGroup": { + "Type": "AWS::AutoScaling::AutoScalingGroup", + "Properties": { + "AutoScalingGroupName": autoscaling_group_name, + "VPCZoneIdentifier": [subnet1.id], + "LaunchTemplate": { + "LaunchTemplateId": {"Ref": "LaunchTemplate"}, + "Version": { + "Fn::GetAtt": ["LaunchTemplate", version_attribute] + }, + }, + "MinSize": 1, + "MaxSize": 1, + }, + }, + }, + } + ) + + cf_client.create_stack( + StackName=stack_name, + TemplateBody=template_json, + Capabilities=["CAPABILITY_NAMED_IAM"], + OnFailure="DELETE", + ) + + template_json = json.dumps( + { + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "AWS CloudFormation Template to create an ASG group with LaunchTemplate", + "Resources": { + "LaunchTemplate": { + "Type": "AWS::EC2::LaunchTemplate", + "Properties": { + "LaunchTemplateName": launch_template_name, + "LaunchTemplateData": { + "ImageId": EXAMPLE_AMI_ID2, + "InstanceType": "t3.medium", + "UserData": "", + }, + }, + }, + "AutoScalingGroup": { + "Type": "AWS::AutoScaling::AutoScalingGroup", + "Properties": { + "AutoScalingGroupName": autoscaling_group_name, + "VPCZoneIdentifier": [subnet2.id], + "LaunchTemplate": { + "LaunchTemplateId": {"Ref": "LaunchTemplate"}, + "Version": { + "Fn::GetAtt": ["LaunchTemplate", version_attribute] + }, + }, + "MinSize": 1, + "MaxSize": 2, + }, + }, + }, + } + ) + + cf_client.update_stack( + StackName=stack_name, + TemplateBody=template_json, + Capabilities=["CAPABILITY_NAMED_IAM"], + ) + + autoscaling_group = autoscaling_client.describe_auto_scaling_groups( + AutoScalingGroupNames=[ + autoscaling_group_name, + ] + )["AutoScalingGroups"][0] + + assert ( + autoscaling_group["LaunchTemplate"]["LaunchTemplateName"] + == launch_template_name + ) + assert autoscaling_group["LaunchTemplate"]["Version"] == "1"