2014-08-27 11:17:06 -04:00
|
|
|
from __future__ import unicode_literals
|
2016-06-29 21:56:39 +00:00
|
|
|
from datetime import datetime
|
2014-03-27 19:12:53 -04:00
|
|
|
import json
|
2017-05-01 11:28:35 -07:00
|
|
|
import yaml
|
2017-03-04 20:12:55 -08:00
|
|
|
import uuid
|
2014-03-27 19:12:53 -04:00
|
|
|
|
2014-11-15 13:34:52 -05:00
|
|
|
import boto.cloudformation
|
2017-05-10 21:58:42 -04:00
|
|
|
from moto.compat import OrderedDict
|
2017-03-11 23:41:12 -05:00
|
|
|
from moto.core import BaseBackend, BaseModel
|
2014-03-27 19:12:53 -04:00
|
|
|
|
2014-10-21 12:45:03 -04:00
|
|
|
from .parsing import ResourceMap, OutputMap
|
2017-12-11 01:23:35 -08:00
|
|
|
from .utils import (
|
|
|
|
generate_changeset_id,
|
|
|
|
generate_stack_id,
|
|
|
|
yaml_tag_constructor,
|
2018-11-02 16:04:17 -07:00
|
|
|
validate_template_cfn_lint,
|
2017-12-11 01:23:35 -08:00
|
|
|
)
|
2014-10-23 14:39:15 -04:00
|
|
|
from .exceptions import ValidationError
|
2014-03-27 19:12:53 -04:00
|
|
|
|
|
|
|
|
2017-03-11 23:41:12 -05:00
|
|
|
class FakeStack(BaseModel):
|
2017-02-23 21:37:43 -05:00
|
|
|
|
2017-12-11 01:23:35 -08:00
|
|
|
def __init__(self, stack_id, name, template, parameters, region_name, notification_arns=None, tags=None, role_arn=None, cross_stack_resources=None, create_change_set=False):
|
2014-03-27 19:12:53 -04:00
|
|
|
self.stack_id = stack_id
|
|
|
|
self.name = name
|
2014-12-31 14:21:47 -05:00
|
|
|
self.template = template
|
2017-05-01 11:28:35 -07:00
|
|
|
self._parse_template()
|
2014-12-31 14:21:47 -05:00
|
|
|
self.parameters = parameters
|
2014-11-15 13:34:52 -05:00
|
|
|
self.region_name = region_name
|
2014-10-29 11:59:41 -04:00
|
|
|
self.notification_arns = notification_arns if notification_arns else []
|
2017-01-18 19:59:47 -08:00
|
|
|
self.role_arn = role_arn
|
2015-08-31 16:48:36 -04:00
|
|
|
self.tags = tags if tags else {}
|
2016-06-29 21:56:39 +00:00
|
|
|
self.events = []
|
2017-12-11 01:23:35 -08:00
|
|
|
if create_change_set:
|
|
|
|
self._add_stack_event("REVIEW_IN_PROGRESS",
|
|
|
|
resource_status_reason="User Initiated")
|
|
|
|
else:
|
|
|
|
self._add_stack_event("CREATE_IN_PROGRESS",
|
|
|
|
resource_status_reason="User Initiated")
|
2014-03-27 19:12:53 -04:00
|
|
|
|
2015-07-13 13:56:46 -04:00
|
|
|
self.description = self.template_dict.get('Description')
|
2018-05-30 11:59:25 -07:00
|
|
|
self.cross_stack_resources = cross_stack_resources or {}
|
2015-07-13 13:56:46 -04:00
|
|
|
self.resource_map = self._create_resource_map()
|
|
|
|
self.output_map = self._create_output_map()
|
2016-06-29 21:56:39 +00:00
|
|
|
self._add_stack_event("CREATE_COMPLETE")
|
|
|
|
self.status = 'CREATE_COMPLETE'
|
2014-03-27 19:12:53 -04:00
|
|
|
|
2015-07-13 13:56:46 -04:00
|
|
|
def _create_resource_map(self):
|
2017-02-23 21:37:43 -05:00
|
|
|
resource_map = ResourceMap(
|
2017-06-08 15:33:28 -04:00
|
|
|
self.stack_id, self.name, self.parameters, self.tags, self.region_name, self.template_dict, self.cross_stack_resources)
|
2015-07-13 13:56:46 -04:00
|
|
|
resource_map.create()
|
|
|
|
return resource_map
|
2014-03-27 19:12:53 -04:00
|
|
|
|
2015-07-13 13:56:46 -04:00
|
|
|
def _create_output_map(self):
|
2017-06-02 16:18:52 -04:00
|
|
|
output_map = OutputMap(self.resource_map, self.template_dict, self.stack_id)
|
2015-07-13 13:56:46 -04:00
|
|
|
output_map.create()
|
|
|
|
return output_map
|
2014-10-21 12:45:03 -04:00
|
|
|
|
2016-06-29 21:56:39 +00:00
|
|
|
def _add_stack_event(self, resource_status, resource_status_reason=None, resource_properties=None):
|
|
|
|
self.events.append(FakeEvent(
|
|
|
|
stack_id=self.stack_id,
|
|
|
|
stack_name=self.name,
|
|
|
|
logical_resource_id=self.name,
|
|
|
|
physical_resource_id=self.stack_id,
|
|
|
|
resource_type="AWS::CloudFormation::Stack",
|
|
|
|
resource_status=resource_status,
|
|
|
|
resource_status_reason=resource_status_reason,
|
|
|
|
resource_properties=resource_properties,
|
|
|
|
))
|
|
|
|
|
|
|
|
def _add_resource_event(self, logical_resource_id, resource_status, resource_status_reason=None, resource_properties=None):
|
|
|
|
# not used yet... feel free to help yourself
|
|
|
|
resource = self.resource_map[logical_resource_id]
|
|
|
|
self.events.append(FakeEvent(
|
|
|
|
stack_id=self.stack_id,
|
|
|
|
stack_name=self.name,
|
|
|
|
logical_resource_id=logical_resource_id,
|
|
|
|
physical_resource_id=resource.physical_resource_id,
|
|
|
|
resource_type=resource.type,
|
|
|
|
resource_status=resource_status,
|
|
|
|
resource_status_reason=resource_status_reason,
|
|
|
|
resource_properties=resource_properties,
|
|
|
|
))
|
|
|
|
|
2017-05-01 11:28:35 -07:00
|
|
|
def _parse_template(self):
|
2017-09-08 03:28:15 +09:00
|
|
|
yaml.add_multi_constructor('', yaml_tag_constructor)
|
2017-05-01 11:28:35 -07:00
|
|
|
try:
|
|
|
|
self.template_dict = yaml.load(self.template)
|
2017-05-11 07:15:07 -07:00
|
|
|
except yaml.parser.ParserError:
|
|
|
|
self.template_dict = json.loads(self.template)
|
2017-05-01 11:28:35 -07:00
|
|
|
|
2014-12-31 14:21:47 -05:00
|
|
|
@property
|
|
|
|
def stack_parameters(self):
|
|
|
|
return self.resource_map.resolved_parameters
|
|
|
|
|
2014-03-27 19:12:53 -04:00
|
|
|
@property
|
|
|
|
def stack_resources(self):
|
|
|
|
return self.resource_map.values()
|
|
|
|
|
2014-10-21 12:45:03 -04:00
|
|
|
@property
|
|
|
|
def stack_outputs(self):
|
|
|
|
return self.output_map.values()
|
|
|
|
|
2017-06-02 16:18:52 -04:00
|
|
|
@property
|
|
|
|
def exports(self):
|
|
|
|
return self.output_map.exports
|
|
|
|
|
2017-03-17 23:57:57 +00:00
|
|
|
def update(self, template, role_arn=None, parameters=None, tags=None):
|
2016-06-29 21:56:39 +00:00
|
|
|
self._add_stack_event("UPDATE_IN_PROGRESS", resource_status_reason="User Initiated")
|
2015-07-13 13:56:46 -04:00
|
|
|
self.template = template
|
2017-12-21 12:10:27 -08:00
|
|
|
self._parse_template()
|
2017-12-18 20:44:04 -08:00
|
|
|
self.resource_map.update(self.template_dict, parameters)
|
2015-07-13 13:56:46 -04:00
|
|
|
self.output_map = self._create_output_map()
|
2016-06-29 21:56:39 +00:00
|
|
|
self._add_stack_event("UPDATE_COMPLETE")
|
2016-04-28 09:21:54 -04:00
|
|
|
self.status = "UPDATE_COMPLETE"
|
2017-01-18 19:59:47 -08:00
|
|
|
self.role_arn = role_arn
|
2017-03-17 23:57:57 +00:00
|
|
|
# only overwrite tags if passed
|
|
|
|
if tags is not None:
|
|
|
|
self.tags = tags
|
|
|
|
# TODO: update tags in the resource map
|
2015-07-13 13:56:46 -04:00
|
|
|
|
2016-02-29 19:50:29 +00:00
|
|
|
def delete(self):
|
2017-02-23 21:37:43 -05:00
|
|
|
self._add_stack_event("DELETE_IN_PROGRESS",
|
|
|
|
resource_status_reason="User Initiated")
|
2016-02-29 19:50:29 +00:00
|
|
|
self.resource_map.delete()
|
2016-06-29 21:56:39 +00:00
|
|
|
self._add_stack_event("DELETE_COMPLETE")
|
2016-04-28 09:21:54 -04:00
|
|
|
self.status = "DELETE_COMPLETE"
|
2016-02-29 19:50:29 +00:00
|
|
|
|
2014-03-27 19:12:53 -04:00
|
|
|
|
2018-10-30 11:09:35 +00:00
|
|
|
class FakeChange(BaseModel):
|
|
|
|
|
|
|
|
def __init__(self, action, logical_resource_id, resource_type):
|
|
|
|
self.action = action
|
|
|
|
self.logical_resource_id = logical_resource_id
|
|
|
|
self.resource_type = resource_type
|
|
|
|
|
|
|
|
|
|
|
|
class FakeChangeSet(FakeStack):
|
|
|
|
|
|
|
|
def __init__(self, stack_id, stack_name, stack_template, change_set_id, change_set_name, template, parameters, region_name, notification_arns=None, tags=None, role_arn=None, cross_stack_resources=None):
|
|
|
|
super(FakeChangeSet, self).__init__(
|
|
|
|
stack_id,
|
|
|
|
stack_name,
|
|
|
|
stack_template,
|
|
|
|
parameters,
|
|
|
|
region_name,
|
|
|
|
notification_arns=notification_arns,
|
|
|
|
tags=tags,
|
|
|
|
role_arn=role_arn,
|
|
|
|
cross_stack_resources=cross_stack_resources,
|
|
|
|
create_change_set=True,
|
|
|
|
)
|
|
|
|
self.stack_name = stack_name
|
|
|
|
self.change_set_id = change_set_id
|
|
|
|
self.change_set_name = change_set_name
|
|
|
|
self.changes = self.diff(template=template, parameters=parameters)
|
|
|
|
|
|
|
|
def diff(self, template, parameters=None):
|
|
|
|
self.template = template
|
|
|
|
self._parse_template()
|
|
|
|
changes = []
|
|
|
|
resources_by_action = self.resource_map.diff(self.template_dict, parameters)
|
|
|
|
for action, resources in resources_by_action.items():
|
|
|
|
for resource_name, resource in resources.items():
|
|
|
|
changes.append(FakeChange(
|
|
|
|
action=action,
|
|
|
|
logical_resource_id=resource_name,
|
|
|
|
resource_type=resource['ResourceType'],
|
|
|
|
))
|
|
|
|
return changes
|
|
|
|
|
|
|
|
|
2017-03-11 23:41:12 -05:00
|
|
|
class FakeEvent(BaseModel):
|
2017-02-23 21:37:43 -05:00
|
|
|
|
2016-06-29 21:56:39 +00:00
|
|
|
def __init__(self, stack_id, stack_name, logical_resource_id, physical_resource_id, resource_type, resource_status, resource_status_reason=None, resource_properties=None):
|
|
|
|
self.stack_id = stack_id
|
|
|
|
self.stack_name = stack_name
|
|
|
|
self.logical_resource_id = logical_resource_id
|
|
|
|
self.physical_resource_id = physical_resource_id
|
|
|
|
self.resource_type = resource_type
|
|
|
|
self.resource_status = resource_status
|
|
|
|
self.resource_status_reason = resource_status_reason
|
|
|
|
self.resource_properties = resource_properties
|
|
|
|
self.timestamp = datetime.utcnow()
|
2017-03-04 20:12:55 -08:00
|
|
|
self.event_id = uuid.uuid4()
|
2016-06-29 21:56:39 +00:00
|
|
|
|
|
|
|
|
2014-03-27 19:12:53 -04:00
|
|
|
class CloudFormationBackend(BaseBackend):
|
|
|
|
|
|
|
|
def __init__(self):
|
2017-05-10 21:58:42 -04:00
|
|
|
self.stacks = OrderedDict()
|
2014-10-22 23:58:42 -04:00
|
|
|
self.deleted_stacks = {}
|
2017-06-02 16:18:52 -04:00
|
|
|
self.exports = OrderedDict()
|
2017-12-11 01:23:35 -08:00
|
|
|
self.change_sets = OrderedDict()
|
2014-03-27 19:12:53 -04:00
|
|
|
|
2017-12-11 01:23:35 -08:00
|
|
|
def create_stack(self, name, template, parameters, region_name, notification_arns=None, tags=None, role_arn=None, create_change_set=False):
|
2014-03-27 19:12:53 -04:00
|
|
|
stack_id = generate_stack_id(name)
|
2014-11-15 13:34:52 -05:00
|
|
|
new_stack = FakeStack(
|
|
|
|
stack_id=stack_id,
|
|
|
|
name=name,
|
|
|
|
template=template,
|
2014-12-31 14:21:47 -05:00
|
|
|
parameters=parameters,
|
2014-11-15 13:34:52 -05:00
|
|
|
region_name=region_name,
|
|
|
|
notification_arns=notification_arns,
|
2015-08-31 16:48:36 -04:00
|
|
|
tags=tags,
|
2017-01-18 19:59:47 -08:00
|
|
|
role_arn=role_arn,
|
2017-06-08 15:33:28 -04:00
|
|
|
cross_stack_resources=self.exports,
|
2017-12-11 01:23:35 -08:00
|
|
|
create_change_set=create_change_set,
|
2014-11-15 13:34:52 -05:00
|
|
|
)
|
2014-03-27 19:12:53 -04:00
|
|
|
self.stacks[stack_id] = new_stack
|
2017-06-02 16:23:42 -04:00
|
|
|
self._validate_export_uniqueness(new_stack)
|
2017-06-02 16:18:52 -04:00
|
|
|
for export in new_stack.exports:
|
|
|
|
self.exports[export.name] = export
|
2014-03-27 19:12:53 -04:00
|
|
|
return new_stack
|
|
|
|
|
2017-12-11 01:23:35 -08:00
|
|
|
def create_change_set(self, stack_name, change_set_name, template, parameters, region_name, change_set_type, notification_arns=None, tags=None, role_arn=None):
|
2018-10-30 11:09:35 +00:00
|
|
|
stack_id = None
|
|
|
|
stack_template = None
|
2017-12-11 01:23:35 -08:00
|
|
|
if change_set_type == 'UPDATE':
|
|
|
|
stacks = self.stacks.values()
|
|
|
|
stack = None
|
|
|
|
for s in stacks:
|
|
|
|
if s.name == stack_name:
|
|
|
|
stack = s
|
2018-10-30 11:09:35 +00:00
|
|
|
stack_id = stack.stack_id
|
|
|
|
stack_template = stack.template
|
2017-12-11 01:23:35 -08:00
|
|
|
if stack is None:
|
|
|
|
raise ValidationError(stack_name)
|
|
|
|
else:
|
2018-10-30 11:09:35 +00:00
|
|
|
stack_id = generate_stack_id(stack_name)
|
|
|
|
stack_template = template
|
|
|
|
|
2017-12-11 01:23:35 -08:00
|
|
|
change_set_id = generate_changeset_id(change_set_name, region_name)
|
2018-10-30 11:09:35 +00:00
|
|
|
new_change_set = FakeChangeSet(
|
|
|
|
stack_id=stack_id,
|
|
|
|
stack_name=stack_name,
|
|
|
|
stack_template=stack_template,
|
|
|
|
change_set_id=change_set_id,
|
|
|
|
change_set_name=change_set_name,
|
|
|
|
template=template,
|
|
|
|
parameters=parameters,
|
|
|
|
region_name=region_name,
|
|
|
|
notification_arns=notification_arns,
|
|
|
|
tags=tags,
|
|
|
|
role_arn=role_arn,
|
|
|
|
cross_stack_resources=self.exports
|
|
|
|
)
|
|
|
|
self.change_sets[change_set_id] = new_change_set
|
|
|
|
self.stacks[stack_id] = new_change_set
|
|
|
|
return change_set_id, stack_id
|
|
|
|
|
|
|
|
def delete_change_set(self, change_set_name, stack_name=None):
|
|
|
|
if change_set_name in self.change_sets:
|
|
|
|
# This means arn was passed in
|
|
|
|
del self.change_sets[change_set_name]
|
|
|
|
else:
|
|
|
|
for cs in self.change_sets:
|
|
|
|
if self.change_sets[cs].change_set_name == change_set_name:
|
|
|
|
del self.change_sets[cs]
|
|
|
|
|
|
|
|
def describe_change_set(self, change_set_name, stack_name=None):
|
|
|
|
change_set = None
|
|
|
|
if change_set_name in self.change_sets:
|
|
|
|
# This means arn was passed in
|
|
|
|
change_set = self.change_sets[change_set_name]
|
|
|
|
else:
|
|
|
|
for cs in self.change_sets:
|
|
|
|
if self.change_sets[cs].change_set_name == change_set_name:
|
|
|
|
change_set = self.change_sets[cs]
|
|
|
|
if change_set is None:
|
|
|
|
raise ValidationError(change_set_name)
|
|
|
|
return change_set
|
2017-12-11 01:23:35 -08:00
|
|
|
|
2017-12-14 04:07:23 -08:00
|
|
|
def execute_change_set(self, change_set_name, stack_name=None):
|
|
|
|
stack = None
|
|
|
|
if change_set_name in self.change_sets:
|
|
|
|
# This means arn was passed in
|
|
|
|
stack = self.change_sets[change_set_name]
|
|
|
|
else:
|
|
|
|
for cs in self.change_sets:
|
2018-10-30 11:09:35 +00:00
|
|
|
if self.change_sets[cs].change_set_name == change_set_name:
|
2017-12-14 04:07:23 -08:00
|
|
|
stack = self.change_sets[cs]
|
|
|
|
if stack is None:
|
|
|
|
raise ValidationError(stack_name)
|
|
|
|
if stack.events[-1].resource_status == 'REVIEW_IN_PROGRESS':
|
|
|
|
stack._add_stack_event('CREATE_COMPLETE')
|
|
|
|
else:
|
|
|
|
stack._add_stack_event('UPDATE_IN_PROGRESS')
|
|
|
|
stack._add_stack_event('UPDATE_COMPLETE')
|
|
|
|
return True
|
|
|
|
|
2014-10-22 23:58:42 -04:00
|
|
|
def describe_stacks(self, name_or_stack_id):
|
2014-03-27 19:12:53 -04:00
|
|
|
stacks = self.stacks.values()
|
2014-10-22 23:58:42 -04:00
|
|
|
if name_or_stack_id:
|
|
|
|
for stack in stacks:
|
|
|
|
if stack.name == name_or_stack_id or stack.stack_id == name_or_stack_id:
|
|
|
|
return [stack]
|
2014-10-23 14:39:15 -04:00
|
|
|
if self.deleted_stacks:
|
|
|
|
deleted_stacks = self.deleted_stacks.values()
|
|
|
|
for stack in deleted_stacks:
|
|
|
|
if stack.stack_id == name_or_stack_id:
|
|
|
|
return [stack]
|
|
|
|
raise ValidationError(name_or_stack_id)
|
2014-03-27 19:12:53 -04:00
|
|
|
else:
|
2017-05-10 21:58:42 -04:00
|
|
|
return list(stacks)
|
2014-03-27 19:12:53 -04:00
|
|
|
|
2018-10-30 11:09:35 +00:00
|
|
|
def list_change_sets(self):
|
|
|
|
return self.change_sets.values()
|
|
|
|
|
2014-03-27 19:12:53 -04:00
|
|
|
def list_stacks(self):
|
2018-11-26 23:58:41 +00:00
|
|
|
return [
|
|
|
|
v for v in self.stacks.values()
|
|
|
|
] + [
|
|
|
|
v for v in self.deleted_stacks.values()
|
|
|
|
]
|
2014-03-27 19:12:53 -04:00
|
|
|
|
|
|
|
def get_stack(self, name_or_stack_id):
|
2016-06-29 21:56:39 +00:00
|
|
|
all_stacks = dict(self.deleted_stacks, **self.stacks)
|
|
|
|
if name_or_stack_id in all_stacks:
|
|
|
|
# Lookup by stack id - deleted stacks incldued
|
|
|
|
return all_stacks[name_or_stack_id]
|
2014-03-27 19:12:53 -04:00
|
|
|
else:
|
2016-06-29 21:56:39 +00:00
|
|
|
# Lookup by stack name - undeleted stacks only
|
|
|
|
for stack in self.stacks.values():
|
|
|
|
if stack.name == name_or_stack_id:
|
|
|
|
return stack
|
2014-03-27 19:12:53 -04:00
|
|
|
|
2017-03-17 23:57:57 +00:00
|
|
|
def update_stack(self, name, template, role_arn=None, parameters=None, tags=None):
|
2015-07-13 13:56:46 -04:00
|
|
|
stack = self.get_stack(name)
|
2017-03-17 23:57:57 +00:00
|
|
|
stack.update(template, role_arn, parameters=parameters, tags=tags)
|
2015-07-13 13:56:46 -04:00
|
|
|
return stack
|
2014-03-27 19:12:53 -04:00
|
|
|
|
2015-07-13 11:05:36 -04:00
|
|
|
def list_stack_resources(self, stack_name_or_id):
|
|
|
|
stack = self.get_stack(stack_name_or_id)
|
|
|
|
return stack.stack_resources
|
|
|
|
|
2014-03-27 19:12:53 -04:00
|
|
|
def delete_stack(self, name_or_stack_id):
|
|
|
|
if name_or_stack_id in self.stacks:
|
|
|
|
# Delete by stack id
|
2014-10-22 23:58:42 -04:00
|
|
|
stack = self.stacks.pop(name_or_stack_id, None)
|
2016-02-29 19:50:29 +00:00
|
|
|
stack.delete()
|
2014-10-22 23:58:42 -04:00
|
|
|
self.deleted_stacks[stack.stack_id] = stack
|
2017-06-02 16:18:52 -04:00
|
|
|
[self.exports.pop(export.name) for export in stack.exports]
|
2014-03-27 19:12:53 -04:00
|
|
|
return self.stacks.pop(name_or_stack_id, None)
|
|
|
|
else:
|
|
|
|
# Delete by stack name
|
2016-02-29 19:50:29 +00:00
|
|
|
for stack in list(self.stacks.values()):
|
|
|
|
if stack.name == name_or_stack_id:
|
|
|
|
self.delete_stack(stack.stack_id)
|
2014-03-27 19:12:53 -04:00
|
|
|
|
2017-06-02 16:22:48 -04:00
|
|
|
def list_exports(self, token):
|
2017-06-02 17:16:25 -04:00
|
|
|
all_exports = list(self.exports.values())
|
2017-06-02 16:22:48 -04:00
|
|
|
if token is None:
|
|
|
|
exports = all_exports[0:100]
|
|
|
|
next_token = '100' if len(all_exports) > 100 else None
|
|
|
|
else:
|
|
|
|
token = int(token)
|
|
|
|
exports = all_exports[token:token + 100]
|
|
|
|
next_token = str(token + 100) if len(all_exports) > token + 100 else None
|
|
|
|
return exports, next_token
|
|
|
|
|
2018-11-02 16:04:17 -07:00
|
|
|
def validate_template(self, template):
|
|
|
|
return validate_template_cfn_lint(template)
|
|
|
|
|
2017-06-02 16:23:42 -04:00
|
|
|
def _validate_export_uniqueness(self, stack):
|
|
|
|
new_stack_export_names = [x.name for x in stack.exports]
|
|
|
|
export_names = self.exports.keys()
|
|
|
|
if not set(export_names).isdisjoint(new_stack_export_names):
|
|
|
|
raise ValidationError(stack.stack_id, message='Export names must be unique across a given region')
|
|
|
|
|
2014-03-27 19:12:53 -04:00
|
|
|
|
2014-11-15 13:34:52 -05:00
|
|
|
cloudformation_backends = {}
|
|
|
|
for region in boto.cloudformation.regions():
|
|
|
|
cloudformation_backends[region.name] = CloudFormationBackend()
|