Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for stack policies #570

Merged
merged 1 commit into from
Mar 21, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,11 @@ A stack has the following keys:
(optional): If provided, specifies the name of a AWS profile to use when
performing AWS API calls for this stack. This can be used to provision stacks
in multiple accounts or regions.
**stack_policy_path**:
(optional): If provided, specifies the path to a JSON formatted stack policy
that will be applied when the CloudFormation stack is created and updated.
You can use stack policies to prevent CloudFormation from making updates to
protected resources (e.g. databases). See: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/protect-stack-resources.html

Here's an example from stacker_blueprints_, used to create a VPC::

Expand Down
15 changes: 12 additions & 3 deletions stacker/actions/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,19 +280,21 @@ def _launch_stack(self, stack, **kwargs):

logger.debug("Launching stack %s now.", stack.fqn)
template = self._template(stack.blueprint)
stack_policy = self._stack_policy(stack)
tags = build_stack_tags(stack)
parameters = self.build_parameters(stack, provider_stack)
force_change_set = stack.blueprint.requires_change_set

if recreate:
logger.debug("Re-creating stack: %s", stack.fqn)
provider.create_stack(stack.fqn, template, parameters,
tags)
tags, stack_policy=stack_policy)
return SubmittedStatus("re-creating stack")
elif not provider_stack:
logger.debug("Creating new stack: %s", stack.fqn)
provider.create_stack(stack.fqn, template, parameters, tags,
force_change_set)
force_change_set,
stack_policy=stack_policy)
return SubmittedStatus("creating new stack")

try:
Expand All @@ -305,7 +307,8 @@ def _launch_stack(self, stack, **kwargs):
parameters,
tags,
force_interactive=stack.protected,
force_change_set=force_change_set
force_change_set=force_change_set,
stack_policy=stack_policy,
)

logger.debug("Updating existing stack: %s", stack.fqn)
Expand All @@ -332,6 +335,12 @@ def _template(self, blueprint):
else:
return Template(body=blueprint.rendered)

def _stack_policy(self, stack):
"""Returns a Template object for the stacks stack policy, or None if
the stack doesn't have a stack policy."""
if stack.stack_policy:
return Template(body=stack.stack_policy)

def _generate_plan(self, tail=False):
return plan(
description="Create/Update stacks",
Expand Down
2 changes: 2 additions & 0 deletions stacker/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,8 @@ class Stack(Model):

tags = DictType(StringType, serialize_when_none=False)

stack_policy_path = StringType(serialize_when_none=False)

def validate_class_path(self, data, value):
if value and data["template_path"]:
raise ValidationError(
Expand Down
44 changes: 39 additions & 5 deletions stacker/providers/aws/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,7 @@ def generate_cloudformation_args(stack_name, parameters, tags, template,
capabilities=DEFAULT_CAPABILITIES,
change_set_type=None,
service_role=None,
stack_policy=None,
change_set_name=None):
"""Used to generate the args for common cloudformation API interactions.

Expand Down Expand Up @@ -414,6 +415,28 @@ def generate_cloudformation_args(stack_name, parameters, tags, template,
else:
args["TemplateBody"] = template.body

# When creating args for CreateChangeSet, don't include the stack policy,
# since ChangeSets don't support it.
if not change_set_name:
args.update(generate_stack_policy_args(stack_policy))

return args


def generate_stack_policy_args(stack_policy=None):
args = {}
if stack_policy:
logger.debug("Stack has a stack policy")
if stack_policy.url:
# stacker currently does not support uploading stack policies to
# S3, so this will never get hit (unless your implementing S3
# uploads, and then you're probably reading this comment about why
# the exception below was raised :))
#
# args["StackPolicyURL"] = stack_policy.url
raise NotImplementedError
else:
args["StackPolicyBody"] = stack_policy.body
return args


Expand Down Expand Up @@ -600,7 +623,8 @@ def destroy_stack(self, stack, **kwargs):
return True

def create_stack(self, fqn, template, parameters, tags,
force_change_set=False, **kwargs):
force_change_set=False, stack_policy=None,
**kwargs):
"""Create a new Cloudformation stack.

Args:
Expand Down Expand Up @@ -637,6 +661,7 @@ def create_stack(self, fqn, template, parameters, tags,
args = generate_cloudformation_args(
fqn, parameters, tags, template,
service_role=self.service_role,
stack_policy=stack_policy,
)

try:
Expand Down Expand Up @@ -739,7 +764,7 @@ def prepare_stack_for_update(self, stack, tags):

def update_stack(self, fqn, template, old_parameters, parameters, tags,
force_interactive=False, force_change_set=False,
**kwargs):
stack_policy=None, **kwargs):
"""Update a Cloudformation stack.

Args:
Expand Down Expand Up @@ -770,10 +795,11 @@ def update_stack(self, fqn, template, old_parameters, parameters, tags,
force_change_set)

return update_method(fqn, template, old_parameters, parameters, tags,
**kwargs)
stack_policy=stack_policy, **kwargs)

def interactive_update_stack(self, fqn, template, old_parameters,
parameters, tags, **kwargs):
parameters, tags, stack_policy=None,
**kwargs):
"""Update a Cloudformation stack in interactive mode.

Args:
Expand Down Expand Up @@ -814,6 +840,13 @@ def interactive_update_stack(self, fqn, template, old_parameters,
finally:
ui.unlock()

# ChangeSets don't support specifying a stack policy inline, like
# CreateStack/UpdateStack, so we just SetStackPolicy if there is one.
if stack_policy:
args = generate_stack_policy_args(stack_policy)
args["StackName"] = fqn
self.cloudformation.set_stack_policy(args)

self.cloudformation.execute_change_set(
ChangeSetName=change_set_id,
)
Expand Down Expand Up @@ -848,7 +881,7 @@ def noninteractive_changeset_update(self, fqn, template, old_parameters,
)

def default_update_stack(self, fqn, template, old_parameters, parameters,
tags, **kwargs):
tags, stack_policy=None, **kwargs):
"""Update a Cloudformation stack in default mode.

Args:
Expand All @@ -867,6 +900,7 @@ def default_update_stack(self, fqn, template, old_parameters, parameters,
args = generate_cloudformation_args(
fqn, parameters, tags, template,
service_role=self.service_role,
stack_policy=stack_policy,
)

try:
Expand Down
10 changes: 10 additions & 0 deletions stacker/stack.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,16 @@ def requires(self):

return requires

@property
def stack_policy(self):
if not hasattr(self, "_stack_policy"):
self._stack_policy = None
if self.definition.stack_policy_path:
with open(self.definition.stack_policy_path) as f:
self._stack_policy = f.read()

return self._stack_policy

@property
def blueprint(self):
if not hasattr(self, "_blueprint"):
Expand Down
1 change: 1 addition & 0 deletions stacker/tests/fixtures/mock_blueprints.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ def create_template(self):
awacs.cloudformation.DeleteStack,
awacs.cloudformation.CreateStack,
awacs.cloudformation.UpdateStack,
awacs.cloudformation.SetStackPolicy,
awacs.cloudformation.DescribeStacks,
awacs.cloudformation.DescribeStackEvents])]))

Expand Down
8 changes: 8 additions & 0 deletions stacker/tests/providers/aws/test_default.py
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,14 @@ def test_generate_cloudformation_args(self):
change_set_result["ChangeSetName"] = "MyChanges"
self.assertEqual(result, change_set_result)

# Check stack policy
stack_policy = Template(body="{}")
result = generate_cloudformation_args(stack_policy=stack_policy,
**std_args)
stack_policy_result = copy.deepcopy(std_return)
stack_policy_result["StackPolicyBody"] = "{}"
self.assertEqual(result, stack_policy_result)

# If not TemplateURL is provided, use TemplateBody
std_args["template"] = Template(body=template_body)
template_body_result = copy.deepcopy(std_return)
Expand Down
10 changes: 10 additions & 0 deletions tests/fixtures/stack_policies/default.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"Statement" : [
{
"Effect" : "Allow",
"Action" : "Update:*",
"Principal": "*",
"Resource" : "*"
}
]
}
10 changes: 10 additions & 0 deletions tests/fixtures/stack_policies/none.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"Statement" : [
{
"Effect" : "Deny",
"Action" : "Update:*",
"Principal": "*",
"Resource" : "*"
}
]
}
1 change: 1 addition & 0 deletions tests/suite.bats
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ namespace: ${STACKER_NAMESPACE}
stacks:
- name: vpc
class_path: stacker.tests.fixtures.mock_blueprints.VPC
stack_policy: tests/fixtures/stack_policies/default.json
variables:
PublicSubnets: 10.128.0.0/24,10.128.1.0/24,10.128.2.0/24,10.128.3.0/24
PrivateSubnets: 10.128.8.0/22,10.128.12.0/22,10.128.16.0/22,10.128.20.0/22
Expand Down