Add stacksets (#3)

Added most stack set responses
This commit is contained in:
John Corrales 2019-01-10 21:33:15 -08:00 committed by GitHub
commit 4207a8e182
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 872 additions and 14 deletions

View file

@ -1,5 +1,5 @@
from __future__ import unicode_literals
from datetime import datetime
from datetime import datetime, timedelta
import json
import yaml
import uuid
@ -12,12 +12,137 @@ from .parsing import ResourceMap, OutputMap
from .utils import (
generate_changeset_id,
generate_stack_id,
generate_stackset_arn,
generate_stackset_id,
yaml_tag_constructor,
validate_template_cfn_lint,
)
from .exceptions import ValidationError
class FakeStackSet(BaseModel):
def __init__(self, stackset_id, name, template, region='us-east-1', status='ACTIVE', description=None, parameters=None, tags=None, admin_role=None, execution_role=None):
self.id = stackset_id
self.arn = generate_stackset_arn(stackset_id, region)
self.name = name
self.template = template
self.description = description
self.parameters = parameters
self.tags = tags
self.admin_role = admin_role
self.execution_role = execution_role
self.status = status
self.instances = FakeStackInstances(parameters, self.id, self.name)
self.operations = []
@property
def stack_instances(self):
return self.instances.stack_instances
def _create_operation(self, operation_id, action, status):
operation = {
'OperationId': str(operation_id),
'Action': action,
'Status': status,
'CreationTimestamp': datetime.now(),
'EndTimestamp': datetime.now() + timedelta(minutes=2),
}
self.operations += [operation]
return operation
def delete(self):
self.status = 'DELETED'
def update(self, template, description, parameters, tags, admin_role,
execution_role, accounts, regions, operation_id=None):
if not operation_id:
operation_id = uuid.uuid4()
self.template = template if template else self.template
self.description = description if description is not None else self.description
self.parameters = parameters if parameters else self.parameters
self.tags = tags if tags else self.tags
self.admin_role = admin_role if admin_role else self.admin_role
self.execution_role = execution_role if execution_role else self.execution_role
if accounts and regions:
self.update_instances(accounts, regions, self.parameters)
operation = self._create_operation(operation_id=operation_id, action='UPDATE', status='SUCCEEDED')
return operation
def create_stack_instances(self, accounts, regions, parameters, operation_id=None):
if not operation_id:
operation_id = uuid.uuid4()
if not parameters:
parameters = self.parameters
self.instances.create_instances(accounts, regions, parameters, operation_id)
self._create_operation(operation_id=operation_id, action='CREATE', status='SUCCEEDED')
def delete_stack_instances(self, accounts, regions, operation_id=None):
if not operation_id:
operation_id = uuid.uuid4()
self.instances.delete(accounts, regions)
self._create_operation(operation_id=operation_id, action='DELETE', status='SUCCEEDED')
def update_instances(self, accounts, regions, parameters, operation_id=None):
if not operation_id:
operation_id = uuid.uuid4()
self.instances.update(accounts, regions, parameters)
operation = self._create_operation(operation_id=operation_id, action='UPDATE', status='SUCCEEDED')
return operation
class FakeStackInstances(BaseModel):
def __init__(self, parameters, stackset_id, stackset_name):
self.parameters = parameters if parameters else {}
self.stackset_id = stackset_id
self.stack_name = "StackSet-{}".format(stackset_id)
self.stackset_name = stackset_name
self.stack_instances = []
def create_instances(self, accounts, regions, parameters, operation_id):
new_instances = []
for region in regions:
for account in accounts:
instance = {
'StackId': generate_stack_id(self.stack_name, region, account),
'StackSetId': self.stackset_id,
'Region': region,
'Account': account,
'Status': "CURRENT",
'ParameterOverrides': parameters if parameters else [],
}
new_instances.append(instance)
self.stack_instances += new_instances
return new_instances
def update(self, accounts, regions, parameters):
for account in accounts:
for region in regions:
instance = self.get_instance(account, region)
if parameters:
instance['ParameterOverrides'] = parameters
else:
instance['ParameterOverrides'] = []
def delete(self, accounts, regions):
for i, instance in enumerate(self.stack_instances):
if instance['Region'] in regions and instance['Account'] in accounts:
self.stack_instances.pop(i)
def get_instance(self, account, region):
for i, instance in enumerate(self.stack_instances):
if instance['Region'] == region and instance['Account'] == account:
return self.stack_instances[i]
class FakeStack(BaseModel):
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):
@ -146,10 +271,72 @@ class CloudFormationBackend(BaseBackend):
def __init__(self):
self.stacks = OrderedDict()
self.stacksets = OrderedDict()
self.deleted_stacks = {}
self.exports = OrderedDict()
self.change_sets = OrderedDict()
def create_stack_set(self, name, template, parameters, tags=None, description=None, region='us-east-1', admin_role=None, execution_role=None):
stackset_id = generate_stackset_id(name)
new_stackset = FakeStackSet(
stackset_id=stackset_id,
name=name,
template=template,
parameters=parameters,
description=description,
tags=tags,
admin_role=admin_role,
execution_role=execution_role,
)
self.stacksets[stackset_id] = new_stackset
return new_stackset
def get_stack_set(self, name):
stacksets = self.stacksets.keys()
for stackset in stacksets:
if self.stacksets[stackset].name == name:
return self.stacksets[stackset]
raise ValidationError(name)
def delete_stack_set(self, name):
stacksets = self.stacksets.keys()
for stackset in stacksets:
if self.stacksets[stackset].name == name:
self.stacksets[stackset].delete()
def create_stack_instances(self, stackset_name, accounts, regions, parameters, operation_id=None):
stackset = self.get_stack_set(stackset_name)
stackset.create_stack_instances(
accounts=accounts,
regions=regions,
parameters=parameters,
operation_id=operation_id,
)
return stackset
def update_stack_set(self, stackset_name, template=None, description=None,
parameters=None, tags=None, admin_role=None, execution_role=None,
accounts=None, regions=None, operation_id=None):
stackset = self.get_stack_set(stackset_name)
update = stackset.update(
template=template,
description=description,
parameters=parameters,
tags=tags,
admin_role=admin_role,
execution_role=execution_role,
accounts=accounts,
regions=regions,
operation_id=operation_id
)
return update
def delete_stack_instances(self, stackset_name, accounts, regions, operation_id=None):
stackset = self.get_stack_set(stackset_name)
stackset.delete_stack_instances(accounts, regions, operation_id)
return stackset
def create_stack(self, name, template, parameters, region_name, notification_arns=None, tags=None, role_arn=None, create_change_set=False):
stack_id = generate_stack_id(name)
new_stack = FakeStack(

View file

@ -312,6 +312,151 @@ class CloudFormationResponse(BaseResponse):
template = self.response_template(VALIDATE_STACK_RESPONSE_TEMPLATE)
return template.render(description=description)
def create_stack_set(self):
stackset_name = self._get_param('StackSetName')
stack_body = self._get_param('TemplateBody')
template_url = self._get_param('TemplateURL')
# role_arn = self._get_param('RoleARN')
parameters_list = self._get_list_prefix("Parameters.member")
tags = dict((item['key'], item['value'])
for item in self._get_list_prefix("Tags.member"))
# Copy-Pasta - Hack dict-comprehension
parameters = dict([
(parameter['parameter_key'], parameter['parameter_value'])
for parameter
in parameters_list
])
if template_url:
stack_body = self._get_stack_from_s3_url(template_url)
stackset = self.cloudformation_backend.create_stack_set(
name=stackset_name,
template=stack_body,
parameters=parameters,
tags=tags,
# role_arn=role_arn,
)
if self.request_json:
return json.dumps({
'CreateStackSetResponse': {
'CreateStackSetResult': {
'StackSetId': stackset.stackset_id,
}
}
})
else:
template = self.response_template(CREATE_STACK_SET_RESPONSE_TEMPLATE)
return template.render(stackset=stackset)
def create_stack_instances(self):
stackset_name = self._get_param('StackSetName')
accounts = self._get_multi_param('Accounts.member')
regions = self._get_multi_param('Regions.member')
parameters = self._get_multi_param('ParameterOverrides.member')
self.cloudformation_backend.create_stack_instances(stackset_name, accounts, regions, parameters)
template = self.response_template(CREATE_STACK_INSTANCES_TEMPLATE)
return template.render()
def delete_stack_set(self):
stackset_name = self._get_param('StackSetName')
self.cloudformation_backend.delete_stack_set(stackset_name)
template = self.response_template(DELETE_STACK_SET_RESPONSE_TEMPLATE)
return template.render()
def delete_stack_instances(self):
stackset_name = self._get_param('StackSetName')
accounts = self._get_multi_param('Accounts.member')
regions = self._get_multi_param('Regions.member')
self.cloudformation_backend.delete_stack_instances(stackset_name, accounts, regions)
template = self.response_template(DELETE_STACK_INSTANCES_TEMPLATE)
return template.render()
def describe_stack_set(self):
stackset_name = self._get_param('StackSetName')
stackset = self.cloudformation_backend.get_stack_set(stackset_name)
if not stackset.admin_role:
stackset.admin_role = 'arn:aws:iam::123456789012:role/AWSCloudFormationStackSetAdministrationRole'
if not stackset.execution_role:
stackset.execution_role = 'AWSCloudFormationStackSetExecutionRole'
template = self.response_template(DESCRIBE_STACK_SET_RESPONSE_TEMPLATE)
return template.render(stackset=stackset)
def describe_stack_instance(self):
stackset_name = self._get_param('StackSetName')
account = self._get_param('StackInstanceAccount')
region = self._get_param('StackInstanceRegion')
instance = self.cloudformation_backend.get_stack_set(stackset_name).instances.get_instance(account, region)
template = self.response_template(DESCRIBE_STACK_INSTANCE_TEMPLATE)
rendered = template.render(instance=instance)
return rendered
def list_stack_sets(self):
stacksets = self.cloudformation_backend.stacksets
template = self.response_template(LIST_STACK_SETS_TEMPLATE)
return template.render(stacksets=stacksets)
def list_stack_instances(self):
stackset_name = self._get_param('StackSetName')
stackset = self.cloudformation_backend.get_stack_set(stackset_name)
template = self.response_template(LIST_STACK_INSTANCES_TEMPLATE)
return template.render(stackset=stackset)
def list_stack_set_operations(self):
stackset_name = self._get_param('StackSetName')
stackset = self.cloudformation_backend.get_stack_set(stackset_name)
template = self.response_template(LIST_STACK_SET_OPERATIONS_RESPONSE_TEMPLATE)
return template.render(stackset=stackset)
def update_stack_set(self):
stackset_name = self._get_param('StackSetName')
operation_id = self._get_param('OperationId')
description = self._get_param('Description')
execution_role = self._get_param('ExecutionRoleName')
admin_role = self._get_param('AdministrationRoleARN')
accounts = self._get_multi_param('Accounts.member')
regions = self._get_multi_param('Regions.member')
template_body = self._get_param('TemplateBody')
template_url = self._get_param('TemplateURL')
if template_url:
template_body = self._get_stack_from_s3_url(template_url)
tags = dict((item['key'], item['value'])
for item in self._get_list_prefix("Tags.member"))
parameters_list = self._get_list_prefix("Parameters.member")
parameters = dict([
(parameter['parameter_key'], parameter['parameter_value'])
for parameter
in parameters_list
])
operation = self.cloudformation_backend.update_stack_set(
stackset_name=stackset_name,
template=template_body,
description=description,
parameters=parameters,
tags=tags,
admin_role=admin_role,
execution_role=execution_role,
accounts=accounts,
regions=regions,
operation_id=operation_id
)
template = self.response_template(UPDATE_STACK_SET_RESPONSE_TEMPLATE)
return template.render(operation=operation)
def update_stack_instances(self):
stackset_name = self._get_param('StackSetName')
accounts = self._get_multi_param('Accounts.member')
regions = self._get_multi_param('Regions.member')
parameters = self._get_multi_param('ParameterOverrides.member')
operation = self.cloudformation_backend.get_stack_set(stackset_name).update_instances(accounts, regions, parameters)
template = self.response_template(UPDATE_STACK_INSTANCES_RESPONSE_TEMPLATE)
return template.render(operation=operation)
VALIDATE_STACK_RESPONSE_TEMPLATE = """<ValidateTemplateResponse>
<ValidateTemplateResult>
@ -553,3 +698,183 @@ LIST_EXPORTS_RESPONSE = """<ListExportsResponse xmlns="http://cloudformation.ama
<RequestId>5ccc7dcd-744c-11e5-be70-example</RequestId>
</ResponseMetadata>
</ListExportsResponse>"""
CREATE_STACK_SET_RESPONSE_TEMPLATE = """<CreateStackSetResponse xmlns="http://internal.amazon.com/coral/com.amazonaws.maestro.service.v20160713/">
<CreateStackSetResult>
<StackSetId>{{ stackset.stackset_id }}</StackSetId>
</CreateStackSetResult>
<ResponseMetadata>
<RequestId>f457258c-391d-41d1-861f-example</RequestId>
</ResponseMetadata>
</CreateStackSetResponse>
"""
DESCRIBE_STACK_SET_RESPONSE_TEMPLATE = """<DescribeStackSetResponse xmlns="http://internal.amazon.com/coral/com.amazonaws.maestro.service.v20160713/">
<DescribeStackSetResult>
<StackSet>
<Capabilities/>
<StackSetARN>{{ stackset.arn }}</StackSetARN>
<ExecutionRoleName>{{ stackset.execution_role }}</ExecutionRoleName>
<AdministrationRoleARN>{{ stackset.admin_role }}</AdministrationRoleARN>
<StackSetId>{{ stackset.id }}</StackSetId>
<TemplateBody>{{ stackset.template }}</TemplateBody>
<StackSetName>{{ stackset.name }}</StackSetName>
<Parameters>
{% for param_name, param_value in stackset.parameters.items() %}
<member>
<ParameterKey>{{ param_name }}</ParameterKey>
<ParameterValue>{{ param_value }}</ParameterValue>
</member>
{% endfor %}
</Parameters>
<Tags>
{% for tag_key, tag_value in stackset.tags.items() %}
<member>
<Key>{{ tag_key }}</Key>
<Value>{{ tag_value }}</Value>
</member>
{% endfor %}
</Tags>
<Status>{{ stackset.status }}</Status>
</StackSet>
</DescribeStackSetResult>
<ResponseMetadata>
<RequestId>d8b64e11-5332-46e1-9603-example</RequestId>
</ResponseMetadata>
</DescribeStackSetResponse>"""
DELETE_STACK_SET_RESPONSE_TEMPLATE = """<DeleteStackSetResponse xmlns="http://internal.amazon.com/coral/com.amazonaws.maestro.service.v20160713/">
<DeleteStackSetResult/>
<ResponseMetadata>
<RequestId>c35ec2d0-d69f-4c4d-9bd7-example</RequestId>
</ResponseMetadata>
</DeleteStackSetResponse>"""
CREATE_STACK_INSTANCES_TEMPLATE = """<CreateStackInstancesResponse xmlns="http://internal.amazon.com/coral/com.amazonaws.maestro.service.v20160713/">
<CreateStackInstancesResult>
<OperationId>1459ad6d-63cc-4c96-a73e-example</OperationId>
</CreateStackInstancesResult>
<ResponseMetadata>
<RequestId>6b29f7e3-69be-4d32-b374-example</RequestId>
</ResponseMetadata>
</CreateStackInstancesResponse>
"""
LIST_STACK_INSTANCES_TEMPLATE = """<ListStackInstancesResponse xmlns="http://internal.amazon.com/coral/com.amazonaws.maestro.service.v20160713/">
<ListStackInstancesResult>
<Summaries>
{% for instance in stackset.stack_instances %}
<member>
<StackId>{{ instance.StackId }}</StackId>
<StackSetId>{{ instance.StackSetId }}</StackSetId>
<Region>{{ instance.Region }}</Region>
<Account>{{ instance.Account }}</Account>
<Status>{{ instance.Status }}</Status>
</member>
{% endfor %}
</Summaries>
</ListStackInstancesResult>
<ResponseMetadata>
<RequestId>83c27e73-b498-410f-993c-example</RequestId>
</ResponseMetadata>
</ListStackInstancesResponse>
"""
DELETE_STACK_INSTANCES_TEMPLATE = """<DeleteStackInstancesResponse xmlns="http://internal.amazon.com/coral/com.amazonaws.maestro.service.v20160713/">
<DeleteStackInstancesResult>
<OperationId>d76a070d-279a-45f3-b9b4-example</OperationId>
</DeleteStackInstancesResult>
<ResponseMetadata>
<RequestId>e5325090-66f6-4ecd-a531-example</RequestId>
</ResponseMetadata>
</DeleteStackInstancesResponse>
"""
DESCRIBE_STACK_INSTANCE_TEMPLATE = """<DescribeStackInstanceResponse xmlns="http://internal.amazon.com/coral/com.amazonaws.maestro.service.v20160713/">
<DescribeStackInstanceResult>
<StackInstance>
<StackId>{{ instance.StackId }}</StackId>
<StackSetId>{{ instance.StackSetId }}</StackSetId>
{% if instance.ParameterOverrides %}
<ParameterOverrides>
{% for override in instance.ParameterOverrides %}
{% if override['ParameterKey'] or override['ParameterValue'] %}
<member>
<ParameterKey>{{ override.ParameterKey }}</ParameterKey>
<UsePreviousValue>false</UsePreviousValue>
<ParameterValue>{{ override.ParameterValue }}</ParameterValue>
</member>
{% endif %}
{% endfor %}
</ParameterOverrides>
{% else %}
<ParameterOverrides/>
{% endif %}
<Region>{{ instance.Region }}</Region>
<Account>{{ instance.Account }}</Account>
<Status>{{ instance.Status }}</Status>
</StackInstance>
</DescribeStackInstanceResult>
<ResponseMetadata>
<RequestId>c6c7be10-0343-4319-8a25-example</RequestId>
</ResponseMetadata>
</DescribeStackInstanceResponse>
"""
LIST_STACK_SETS_TEMPLATE = """<ListStackSetsResponse xmlns="http://internal.amazon.com/coral/com.amazonaws.maestro.service.v20160713/">
<ListStackSetsResult>
<Summaries>
{% for key, value in stacksets.items() %}
<member>
<StackSetName>{{ value.name }}</StackSetName>
<StackSetId>{{ value.id }}</StackSetId>
<Status>{{ value.status }}</Status>
</member>
{% endfor %}
</Summaries>
</ListStackSetsResult>
<ResponseMetadata>
<RequestId>4dcacb73-841e-4ed8-b335-example</RequestId>
</ResponseMetadata>
</ListStackSetsResponse>
"""
UPDATE_STACK_INSTANCES_RESPONSE_TEMPLATE = """<UpdateStackInstancesResponse xmlns="http://internal.amazon.com/coral/com.amazonaws.maestro.service.v20160713/">
<UpdateStackInstancesResult>
<OperationId>{{ operation }}</OperationId>
</UpdateStackInstancesResult>
<ResponseMetadata>
<RequestId>bdbf8e94-19b6-4ce4-af85-example</RequestId>
</ResponseMetadata>
</UpdateStackInstancesResponse>
"""
UPDATE_STACK_SET_RESPONSE_TEMPLATE = """<UpdateStackSetResponse xmlns="http://internal.amazon.com/coral/com.amazonaws.maestro.service.v20160713/">
<UpdateStackSetResult>
<OperationId>{{ operation.OperationId }}</OperationId>
</UpdateStackSetResult>
<ResponseMetadata>
<RequestId>adac907b-17e3-43e6-a254-example</RequestId>
</ResponseMetadata>
</UpdateStackSetResponse>
"""
LIST_STACK_SET_OPERATIONS_RESPONSE_TEMPLATE = """<ListStackSetOperationsResponse xmlns="http://internal.amazon.com/coral/com.amazonaws.maestro.service.v20160713/">
<ListStackSetOperationsResult>
<Summaries>
{% for operation in stackset.operations %}
<member>
<CreationTimestamp>{{ operation.CreationTimestamp }}</CreationTimestamp>
<OperationId>{{ operation.OperationId }}</OperationId>
<Action>{{ operation.Action }}</Action>
<EndTimestamp>{{ operation.EndTimestamp }}</EndTimestamp>
<Status>{{ operation.Status }}</Status>
</member>
{% endfor %}
</Summaries>
</ListStackSetOperationsResult>
<ResponseMetadata>
<RequestId>65b9d9be-08bb-4a43-9a21-example</RequestId>
</ResponseMetadata>
</ListStackSetOperationsResponse>
"""

View file

@ -8,9 +8,9 @@ import os
from cfnlint import decode, core
def generate_stack_id(stack_name):
def generate_stack_id(stack_name, region="us-east-1", account="123456789"):
random_id = uuid.uuid4()
return "arn:aws:cloudformation:us-east-1:123456789:stack/{0}/{1}".format(stack_name, random_id)
return "arn:aws:cloudformation:{}:{}:stack/{}/{}".format(region, account, stack_name, random_id)
def generate_changeset_id(changeset_name, region_name):
@ -18,6 +18,15 @@ def generate_changeset_id(changeset_name, region_name):
return 'arn:aws:cloudformation:{0}:123456789:changeSet/{1}/{2}'.format(region_name, changeset_name, random_id)
def generate_stackset_id(stackset_name):
random_id = uuid.uuid4()
return '{}:{}'.format(stackset_name, random_id)
def generate_stackset_arn(stackset_id, region_name):
return 'arn:aws:cloudformation:{}:123456789012:stackset/{}'.format(region_name, stackset_id)
def random_suffix():
size = 12
chars = list(range(10)) + ['A-Z']