Add CloudFormation support to EC2 Launch Templates (#5477)
This commit is contained in:
parent
837ad2cbb7
commit
c648c0f957
@ -718,6 +718,14 @@ class InvalidLaunchTemplateNameNotFoundError(EC2ClientError):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidLaunchTemplateNameNotFoundWithNameError(EC2ClientError):
|
||||||
|
def __init__(self, name):
|
||||||
|
super().__init__(
|
||||||
|
"InvalidLaunchTemplateName.NotFoundException",
|
||||||
|
f"The specified launch template, with template name {name}, does not exist",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class InvalidParameterDependency(EC2ClientError):
|
class InvalidParameterDependency(EC2ClientError):
|
||||||
def __init__(self, param, param_needed):
|
def __init__(self, param, param_needed):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
|
@ -1,9 +1,17 @@
|
|||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
from moto.core import CloudFormationModel
|
||||||
from .core import TaggedEC2Resource
|
from .core import TaggedEC2Resource
|
||||||
from ..utils import generic_filter, random_launch_template_id, utc_date_and_time
|
from ..utils import (
|
||||||
|
generic_filter,
|
||||||
|
random_launch_template_id,
|
||||||
|
utc_date_and_time,
|
||||||
|
convert_tag_spec,
|
||||||
|
)
|
||||||
from ..exceptions import (
|
from ..exceptions import (
|
||||||
InvalidLaunchTemplateNameAlreadyExistsError,
|
InvalidLaunchTemplateNameAlreadyExistsError,
|
||||||
InvalidLaunchTemplateNameNotFoundError,
|
InvalidLaunchTemplateNameNotFoundError,
|
||||||
|
InvalidLaunchTemplateNameNotFoundWithNameError,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -32,7 +40,7 @@ class LaunchTemplateVersion(object):
|
|||||||
return self.data.get("UserData", "")
|
return self.data.get("UserData", "")
|
||||||
|
|
||||||
|
|
||||||
class LaunchTemplate(TaggedEC2Resource):
|
class LaunchTemplate(TaggedEC2Resource, CloudFormationModel):
|
||||||
def __init__(self, backend, name, template_data, version_description, tag_spec):
|
def __init__(self, backend, name, template_data, version_description, tag_spec):
|
||||||
self.ec2_backend = backend
|
self.ec2_backend = backend
|
||||||
self.name = name
|
self.name = name
|
||||||
@ -72,12 +80,89 @@ class LaunchTemplate(TaggedEC2Resource):
|
|||||||
def latest_version_number(self):
|
def latest_version_number(self):
|
||||||
return self.latest_version().number
|
return self.latest_version().number
|
||||||
|
|
||||||
|
@property
|
||||||
|
def physical_resource_id(self):
|
||||||
|
return self.id
|
||||||
|
|
||||||
def get_filter_value(self, filter_name):
|
def get_filter_value(self, filter_name):
|
||||||
if filter_name == "launch-template-name":
|
if filter_name == "launch-template-name":
|
||||||
return self.name
|
return self.name
|
||||||
else:
|
else:
|
||||||
return super().get_filter_value(filter_name, "DescribeLaunchTemplates")
|
return super().get_filter_value(filter_name, "DescribeLaunchTemplates")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def cloudformation_name_type():
|
||||||
|
return "LaunchTemplateName"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def cloudformation_type():
|
||||||
|
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-launchtemplate.html
|
||||||
|
return "AWS::EC2::LaunchTemplate"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_from_cloudformation_json(
|
||||||
|
cls, resource_name, cloudformation_json, account_id, region_name, **kwargs
|
||||||
|
):
|
||||||
|
|
||||||
|
from ..models import ec2_backends
|
||||||
|
|
||||||
|
backend = ec2_backends[account_id][region_name]
|
||||||
|
|
||||||
|
properties = cloudformation_json["Properties"]
|
||||||
|
name = properties.get("LaunchTemplateName")
|
||||||
|
data = properties.get("LaunchTemplateData")
|
||||||
|
description = properties.get("VersionDescription")
|
||||||
|
tag_spec = convert_tag_spec(
|
||||||
|
properties.get("TagSpecifications", {}), tag_key="Tags"
|
||||||
|
)
|
||||||
|
|
||||||
|
launch_template = backend.create_launch_template(
|
||||||
|
name, description, data, tag_spec
|
||||||
|
)
|
||||||
|
|
||||||
|
return launch_template
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def update_from_cloudformation_json(
|
||||||
|
cls,
|
||||||
|
original_resource,
|
||||||
|
new_resource_name,
|
||||||
|
cloudformation_json,
|
||||||
|
account_id,
|
||||||
|
region_name,
|
||||||
|
):
|
||||||
|
|
||||||
|
from ..models import ec2_backends
|
||||||
|
|
||||||
|
backend = ec2_backends[account_id][region_name]
|
||||||
|
|
||||||
|
properties = cloudformation_json["Properties"]
|
||||||
|
|
||||||
|
name = properties.get("LaunchTemplateName")
|
||||||
|
data = properties.get("LaunchTemplateData")
|
||||||
|
description = properties.get("VersionDescription")
|
||||||
|
|
||||||
|
launch_template = backend.get_launch_template_by_name(name)
|
||||||
|
|
||||||
|
launch_template.create_version(data, description)
|
||||||
|
|
||||||
|
return launch_template
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def delete_from_cloudformation_json(
|
||||||
|
cls, resource_name, cloudformation_json, account_id, region_name
|
||||||
|
):
|
||||||
|
|
||||||
|
from ..models import ec2_backends
|
||||||
|
|
||||||
|
backend = ec2_backends[account_id][region_name]
|
||||||
|
|
||||||
|
properties = cloudformation_json["Properties"]
|
||||||
|
|
||||||
|
name = properties.get("LaunchTemplateName")
|
||||||
|
|
||||||
|
backend.delete_launch_template(name, None)
|
||||||
|
|
||||||
|
|
||||||
class LaunchTemplateBackend:
|
class LaunchTemplateBackend:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@ -98,6 +183,8 @@ class LaunchTemplateBackend:
|
|||||||
return self.launch_templates[template_id]
|
return self.launch_templates[template_id]
|
||||||
|
|
||||||
def get_launch_template_by_name(self, name):
|
def get_launch_template_by_name(self, name):
|
||||||
|
if name not in self.launch_template_name_to_ids:
|
||||||
|
raise InvalidLaunchTemplateNameNotFoundWithNameError(name)
|
||||||
return self.get_launch_template(self.launch_template_name_to_ids[name])
|
return self.get_launch_template(self.launch_template_name_to_ids[name])
|
||||||
|
|
||||||
def delete_launch_template(self, name, tid):
|
def delete_launch_template(self, name, tid):
|
||||||
|
@ -787,14 +787,15 @@ def gen_moto_amis(described_images, drop_images_missing_keys=True):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def convert_tag_spec(tag_spec_set):
|
def convert_tag_spec(tag_spec_set, tag_key="Tag"):
|
||||||
# IN: [{"ResourceType": _type, "Tag": [{"Key": k, "Value": v}, ..]}]
|
# IN: [{"ResourceType": _type, "Tag": [{"Key": k, "Value": v}, ..]}]
|
||||||
# OUT: {_type: {k: v, ..}}
|
# (or) [{"ResourceType": _type, "Tags": [{"Key": k, "Value": v}, ..]}] <-- special cfn case
|
||||||
|
# OUT: {_type: {k: v, ..}}
|
||||||
tags = {}
|
tags = {}
|
||||||
for tag_spec in tag_spec_set:
|
for tag_spec in tag_spec_set:
|
||||||
if tag_spec["ResourceType"] not in tags:
|
if tag_spec["ResourceType"] not in tags:
|
||||||
tags[tag_spec["ResourceType"]] = {}
|
tags[tag_spec["ResourceType"]] = {}
|
||||||
tags[tag_spec["ResourceType"]].update(
|
tags[tag_spec["ResourceType"]].update(
|
||||||
{tag["Key"]: tag["Value"] for tag in tag_spec["Tag"]}
|
{tag["Key"]: tag["Value"] for tag in tag_spec[tag_key]}
|
||||||
)
|
)
|
||||||
return tags
|
return tags
|
||||||
|
@ -760,3 +760,184 @@ def test_vpc_endpoint_creation():
|
|||||||
endpoint.should.have.key("State").equals("available")
|
endpoint.should.have.key("State").equals("available")
|
||||||
endpoint.should.have.key("SubnetIds").equals([subnet1.id])
|
endpoint.should.have.key("SubnetIds").equals([subnet1.id])
|
||||||
endpoint.should.have.key("VpcEndpointType").equals("GatewayLoadBalancer")
|
endpoint.should.have.key("VpcEndpointType").equals("GatewayLoadBalancer")
|
||||||
|
|
||||||
|
|
||||||
|
@mock_cloudformation
|
||||||
|
@mock_ec2
|
||||||
|
def test_launch_template_create():
|
||||||
|
|
||||||
|
cf = boto3.client("cloudformation", region_name="us-west-1")
|
||||||
|
ec2 = boto3.client("ec2", region_name="us-west-1")
|
||||||
|
|
||||||
|
launch_template_name = str(uuid4())[0:6]
|
||||||
|
logical_id = str(uuid4())[0:6]
|
||||||
|
stack_name = str(uuid4())[0:6]
|
||||||
|
|
||||||
|
lt_tags = [{"Key": "lt-tag-key", "Value": "lt-tag-value"}]
|
||||||
|
|
||||||
|
template_json = json.dumps(
|
||||||
|
{
|
||||||
|
"AWSTemplateFormatVersion": "2010-09-09",
|
||||||
|
"Resources": {
|
||||||
|
logical_id: {
|
||||||
|
"Type": "AWS::EC2::LaunchTemplate",
|
||||||
|
"Properties": {
|
||||||
|
"LaunchTemplateName": launch_template_name,
|
||||||
|
"VersionDescription": "some template",
|
||||||
|
"LaunchTemplateData": {
|
||||||
|
"TagSpecifications": [
|
||||||
|
{
|
||||||
|
"ResourceType": "instance",
|
||||||
|
"Tags": [
|
||||||
|
{"Key": "i-tag-key", "Value": "i-tag-value"}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"TagSpecifications": [
|
||||||
|
{
|
||||||
|
"ResourceType": "launch-template",
|
||||||
|
"Tags": lt_tags,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Outputs": {
|
||||||
|
"LaunchTemplateId": {
|
||||||
|
"Description": "The ID of the created launch template",
|
||||||
|
"Value": {"Ref": logical_id},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
cf.create_stack(StackName=stack_name, TemplateBody=template_json)
|
||||||
|
|
||||||
|
resources = cf.list_stack_resources(StackName=stack_name)["StackResourceSummaries"]
|
||||||
|
resources.should.have.length_of(1)
|
||||||
|
resources[0].should.have.key("LogicalResourceId").equals(logical_id)
|
||||||
|
launch_template_id = resources[0]["PhysicalResourceId"]
|
||||||
|
|
||||||
|
outputs = cf.describe_stacks(StackName=stack_name)["Stacks"][0]["Outputs"]
|
||||||
|
outputs.should.have.length_of(1)
|
||||||
|
outputs[0].should.equal(
|
||||||
|
{"OutputKey": "LaunchTemplateId", "OutputValue": launch_template_id}
|
||||||
|
)
|
||||||
|
|
||||||
|
launch_template = ec2.describe_launch_templates(
|
||||||
|
LaunchTemplateNames=[launch_template_name]
|
||||||
|
)["LaunchTemplates"][0]
|
||||||
|
launch_template.should.have.key("LaunchTemplateName").equals(launch_template_name)
|
||||||
|
launch_template.should.have.key("LaunchTemplateId").equals(launch_template_id)
|
||||||
|
launch_template.should.have.key("Tags").equals(lt_tags)
|
||||||
|
|
||||||
|
launch_template_version = ec2.describe_launch_template_versions(
|
||||||
|
LaunchTemplateName=launch_template_name
|
||||||
|
)["LaunchTemplateVersions"][0]
|
||||||
|
launch_template_version.should.have.key("LaunchTemplateName").equals(
|
||||||
|
launch_template_name
|
||||||
|
)
|
||||||
|
launch_template_version.should.have.key("LaunchTemplateId").equals(
|
||||||
|
launch_template_id
|
||||||
|
)
|
||||||
|
launch_template_version["LaunchTemplateData"]["TagSpecifications"][
|
||||||
|
0
|
||||||
|
].should.have.key("ResourceType").equals("instance")
|
||||||
|
|
||||||
|
|
||||||
|
@mock_cloudformation
|
||||||
|
@mock_ec2
|
||||||
|
def test_launch_template_update():
|
||||||
|
|
||||||
|
cf = boto3.client("cloudformation", region_name="us-west-1")
|
||||||
|
ec2 = boto3.client("ec2", region_name="us-west-1")
|
||||||
|
|
||||||
|
launch_template_name = str(uuid4())[0:6]
|
||||||
|
logical_id = str(uuid4())[0:6]
|
||||||
|
stack_name = str(uuid4())[0:6]
|
||||||
|
|
||||||
|
template_json = json.dumps(
|
||||||
|
{
|
||||||
|
"AWSTemplateFormatVersion": "2010-09-09",
|
||||||
|
"Resources": {
|
||||||
|
logical_id: {
|
||||||
|
"Type": "AWS::EC2::LaunchTemplate",
|
||||||
|
"Properties": {
|
||||||
|
"LaunchTemplateName": launch_template_name,
|
||||||
|
"VersionDescription": "some template",
|
||||||
|
"LaunchTemplateData": {"UserData": ""},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
cf.create_stack(StackName=stack_name, TemplateBody=template_json)
|
||||||
|
|
||||||
|
template_json = json.dumps(
|
||||||
|
{
|
||||||
|
"AWSTemplateFormatVersion": "2010-09-09",
|
||||||
|
"Resources": {
|
||||||
|
logical_id: {
|
||||||
|
"Type": "AWS::EC2::LaunchTemplate",
|
||||||
|
"Properties": {
|
||||||
|
"LaunchTemplateName": launch_template_name,
|
||||||
|
"VersionDescription": "a better template",
|
||||||
|
"LaunchTemplateData": {"UserData": ""},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
cf.update_stack(StackName=stack_name, TemplateBody=template_json)
|
||||||
|
|
||||||
|
resources = cf.list_stack_resources(StackName=stack_name)["StackResourceSummaries"]
|
||||||
|
resources.should.have.length_of(1)
|
||||||
|
|
||||||
|
launch_template_versions = ec2.describe_launch_template_versions(
|
||||||
|
LaunchTemplateName=launch_template_name
|
||||||
|
)["LaunchTemplateVersions"]
|
||||||
|
launch_template_versions.should.have.length_of(2)
|
||||||
|
launch_template_versions[0].should.have.key("VersionDescription").equals(
|
||||||
|
"some template"
|
||||||
|
)
|
||||||
|
launch_template_versions[1].should.have.key("VersionDescription").equals(
|
||||||
|
"a better template"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@mock_cloudformation
|
||||||
|
@mock_ec2
|
||||||
|
def test_launch_template_delete():
|
||||||
|
|
||||||
|
cf = boto3.client("cloudformation", region_name="us-west-1")
|
||||||
|
ec2 = boto3.client("ec2", region_name="us-west-1")
|
||||||
|
|
||||||
|
launch_template_name = str(uuid4())[0:6]
|
||||||
|
logical_id = str(uuid4())[0:6]
|
||||||
|
stack_name = str(uuid4())[0:6]
|
||||||
|
|
||||||
|
template_json = json.dumps(
|
||||||
|
{
|
||||||
|
"AWSTemplateFormatVersion": "2010-09-09",
|
||||||
|
"Resources": {
|
||||||
|
logical_id: {
|
||||||
|
"Type": "AWS::EC2::LaunchTemplate",
|
||||||
|
"Properties": {
|
||||||
|
"LaunchTemplateName": launch_template_name,
|
||||||
|
"VersionDescription": "some template",
|
||||||
|
"LaunchTemplateData": {"UserData": ""},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
cf.create_stack(StackName=stack_name, TemplateBody=template_json)
|
||||||
|
|
||||||
|
cf.delete_stack(StackName=stack_name)
|
||||||
|
|
||||||
|
ec2.describe_launch_templates(LaunchTemplateNames=[launch_template_name])[
|
||||||
|
"LaunchTemplates"
|
||||||
|
].should.have.length_of(0)
|
||||||
|
@ -86,6 +86,21 @@ def test_describe_launch_template_versions():
|
|||||||
templ.should.equal(template_data)
|
templ.should.equal(template_data)
|
||||||
|
|
||||||
|
|
||||||
|
@mock_ec2
|
||||||
|
def test_describe_launch_template_versions_by_name_when_absent():
|
||||||
|
cli = boto3.client("ec2", region_name="us-east-1")
|
||||||
|
|
||||||
|
template_name = "foo"
|
||||||
|
|
||||||
|
# test using name
|
||||||
|
with pytest.raises(
|
||||||
|
ClientError,
|
||||||
|
match=f"The specified launch template, with template name {template_name}, does not exist",
|
||||||
|
):
|
||||||
|
|
||||||
|
cli.describe_launch_template_versions(LaunchTemplateName=template_name)
|
||||||
|
|
||||||
|
|
||||||
@mock_ec2
|
@mock_ec2
|
||||||
def test_create_launch_template_version():
|
def test_create_launch_template_version():
|
||||||
cli = boto3.client("ec2", region_name="us-east-1")
|
cli = boto3.client("ec2", region_name="us-east-1")
|
||||||
|
Loading…
Reference in New Issue
Block a user