Skip to content

Commit

Permalink
Merge pull request #570 from remind101/stack-policies
Browse files Browse the repository at this point in the history
Add support for stack policies
  • Loading branch information
ejholmes committed Mar 21, 2018
2 parents ec235fb + 99e6b99 commit a76abe6
Show file tree
Hide file tree
Showing 10 changed files with 98 additions and 8 deletions.
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

0 comments on commit a76abe6

Please sign in to comment.