Skip to content

Commit

Permalink
add cf notification arns (#756)
Browse files Browse the repository at this point in the history
* add cf notification arns parameter

* fixed lint errors

* added NotificationARNs arg to test

* adjustments based on feedback, added test

	modified:   stacker/providers/aws/default.py
	modified:   stacker/stack.py
	modified:   stacker/tests/providers/aws/test_default.py

* docs

* pin more-itertools to version which supports py27
	modified:   setup.py

Co-authored-by: Tim Kukhmazov <[email protected]>
  • Loading branch information
russellballestrini and Tim Kukhmazov authored May 29, 2020
1 parent 0f8c233 commit 06a4bee
Show file tree
Hide file tree
Showing 8 changed files with 106 additions and 23 deletions.
3 changes: 3 additions & 0 deletions docs/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,9 @@ A stack has the following keys:
an exception if the stack is in an `IN_PROGRESS` state. You can set this
option to `wait` and stacker will wait for the previous update to complete
before attempting to update the stack.
**notification_arns**:
(optional): If provided, accepts a list of None or many AWS SNS Topic ARNs
which will be notified of this stack's CloudFormation state changes.

Stacks Example
~~~~~~~~~~~~~~
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"formic2",
"python-dateutil>=2.0,<3.0",
"MarkupSafe<2.0", # 2.0 dropped python 2.7, 3.5 support - temporary
"more-itertools<6.0.0", # 6.0.0 dropped python 2.7 support - temporary
]

setup_requires = ['pytest-runner']
Expand Down
4 changes: 3 additions & 1 deletion stacker/actions/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,8 @@ def _launch_stack(self, stack, **kwargs):
logger.debug("Creating new stack: %s", stack.fqn)
provider.create_stack(stack.fqn, template, parameters, tags,
force_change_set,
stack_policy=stack_policy)
stack_policy=stack_policy,
notification_arns=stack.notification_arns)
return SubmittedStatus("creating new stack")

try:
Expand All @@ -359,6 +360,7 @@ def _launch_stack(self, stack, **kwargs):
force_interactive=stack.protected,
force_change_set=force_change_set,
stack_policy=stack_policy,
notification_arns=stack.notification_arns
)

logger.debug("Updating existing stack: %s", stack.fqn)
Expand Down
3 changes: 3 additions & 0 deletions stacker/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,9 @@ class Stack(Model):

in_progress_behavior = StringType(serialize_when_none=False)

notification_arns = ListType(
StringType, serialize_when_none=False, default=[])

def validate_class_path(self, data, value):
if value and data["template_path"]:
raise ValidationError(
Expand Down
1 change: 1 addition & 0 deletions stacker/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ def get_stacks(self):
locked=stack_def.locked,
enabled=stack_def.enabled,
protected=stack_def.protected,
notification_arns=stack_def.notification_arns
)
stacks.append(stack)
self._stacks = stacks
Expand Down
57 changes: 43 additions & 14 deletions stacker/providers/aws/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,17 +334,26 @@ def wait_till_change_set_complete(cfn_client, change_set_id, try_count=25,
return response


def create_change_set(cfn_client, fqn, template, parameters, tags,
change_set_type='UPDATE', replacements_only=False,
service_role=None):
def create_change_set(
cfn_client,
fqn,
template,
parameters,
tags,
change_set_type='UPDATE',
replacements_only=False,
service_role=None,
notification_arns=None
):
logger.debug("Attempting to create change set of type %s for stack: %s.",
change_set_type,
fqn)
args = generate_cloudformation_args(
fqn, parameters, tags, template,
change_set_type=change_set_type,
service_role=service_role,
change_set_name=get_change_set_name()
change_set_name=get_change_set_name(),
notification_arns=notification_arns
)
try:
response = cfn_client.create_change_set(**args)
Expand Down Expand Up @@ -414,12 +423,18 @@ def check_tags_contain(actual, expected):
return actual_set >= expected_set


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):
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,
notification_arns=None,
):
"""Used to generate the args for common cloudformation API interactions.
This is used for create_stack/update_stack/create_change_set calls in
Expand All @@ -443,6 +458,8 @@ def generate_cloudformation_args(stack_name, parameters, tags, template,
object representing a stack policy.
change_set_name (str, optional): An optional change set name to use
with create_change_set.
notification_arns (list, optional): An optional list of SNS topic ARNs
to send CloudFormation Events to.
Returns:
dict: A dictionary of arguments to be used in the Cloudformation API
Expand All @@ -461,6 +478,9 @@ def generate_cloudformation_args(stack_name, parameters, tags, template,
if change_set_name:
args["ChangeSetName"] = change_set_name

if notification_arns:
args["NotificationARNs"] = notification_arns

if change_set_type:
args["ChangeSetType"] = change_set_type

Expand Down Expand Up @@ -738,9 +758,13 @@ def destroy_stack(self, stack, **kwargs):
self.cloudformation.delete_stack(**args)
return True

def create_stack(self, fqn, template, parameters, tags,
force_change_set=False, stack_policy=None,
**kwargs):
def create_stack(
self, fqn, template, parameters, tags,
force_change_set=False,
stack_policy=None,
notification_arns=None,
**kwargs
):
"""Create a new Cloudformation stack.
Args:
Expand All @@ -754,6 +778,8 @@ def create_stack(self, fqn, template, parameters, tags,
force_change_set (bool): Whether or not to force change set use.
stack_policy (:class:`stacker.providers.base.Template`): A template
object representing a stack policy.
notification_arns (list, optional): An optional list of SNS topic
ARNs to send CloudFormation Events to.
"""

logger.debug("Attempting to create stack %s:.", fqn)
Expand All @@ -780,6 +806,7 @@ def create_stack(self, fqn, template, parameters, tags,
fqn, parameters, tags, template,
service_role=self.service_role,
stack_policy=stack_policy,
notification_arns=notification_arns
)

try:
Expand Down Expand Up @@ -1029,7 +1056,8 @@ def noninteractive_changeset_update(self, fqn, template, old_parameters,
)

def default_update_stack(self, fqn, template, old_parameters, parameters,
tags, stack_policy=None, **kwargs):
tags, stack_policy=None,
notification_arns=[], **kwargs):
"""Update a Cloudformation stack in default mode.
Args:
Expand All @@ -1051,6 +1079,7 @@ def default_update_stack(self, fqn, template, old_parameters, parameters,
fqn, parameters, tags, template,
service_role=self.service_role,
stack_policy=stack_policy,
notification_arns=notification_arns
)

try:
Expand Down
18 changes: 15 additions & 3 deletions stacker/stack.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,23 @@ class Stack(object):
blueprint.
locked (bool, optional): Whether or not the stack is locked.
force (bool, optional): Whether to force updates on this stack.
enabled (bool, optional): Whether this stack is enabled
enabled (bool, optional): Whether this stack is enabled.
protected (boot, optional): Whether this stack is protected.
notification_arns (list, optional): An optional list of SNS topic ARNs
to send CloudFormation Events to.
"""

def __init__(self, definition, context, variables=None, mappings=None,
locked=False, force=False, enabled=True, protected=False):
def __init__(
self, definition, context,
variables=None,
mappings=None,
locked=False,
force=False,
enabled=True,
protected=False,
notification_arns=None,
):
self.logging = True
self.name = definition.name
self.fqn = context.get_fqn(definition.stack_name or self.name)
Expand All @@ -75,6 +86,7 @@ def __init__(self, definition, context, variables=None, mappings=None,
self.context = context
self.outputs = None
self.in_progress_behavior = definition.in_progress_behavior
self.notification_arns = notification_arns

def __repr__(self):
return self.fqn
Expand Down
42 changes: 37 additions & 5 deletions stacker/tests/providers/aws/test_default.py
Original file line number Diff line number Diff line change
Expand Up @@ -403,11 +403,18 @@ def test_generate_cloudformation_args(self):
template_url = "http://fake.s3url.com/blah.json"
template_body = '{"fake_body": "woot"}'
std_args = {
"stack_name": stack_name, "parameters": [], "tags": [],
"template": Template(url=template_url)}
std_return = {"StackName": stack_name, "Parameters": [], "Tags": [],
"Capabilities": DEFAULT_CAPABILITIES,
"TemplateURL": template_url}
"stack_name": stack_name,
"parameters": [],
"tags": [],
"template": Template(url=template_url)
}
std_return = {
"StackName": stack_name,
"Parameters": [],
"Tags": [],
"Capabilities": DEFAULT_CAPABILITIES,
"TemplateURL": template_url,
}
result = generate_cloudformation_args(**std_args)
self.assertEqual(result, std_return)

Expand Down Expand Up @@ -439,6 +446,31 @@ def test_generate_cloudformation_args(self):
result = generate_cloudformation_args(**std_args)
self.assertEqual(result, template_body_result)

def test_generate_cloudformation_args_with_notification_arns(self):
stack_name = "mystack"
template_url = "http://fake.s3url.com/blah.json"
std_args = {
"stack_name": stack_name,
"parameters": [],
"tags": [],
"template": Template(url=template_url),
"notification_arns": [
"arn:aws:sns:us-east-1:1234567890:test-cf-deploy-notify-sns-topic-CfDeployNotify" # noqa
]
}
std_return = {
"StackName": stack_name,
"Parameters": [],
"Tags": [],
"Capabilities": DEFAULT_CAPABILITIES,
"TemplateURL": template_url,
"NotificationARNs": [
"arn:aws:sns:us-east-1:1234567890:test-cf-deploy-notify-sns-topic-CfDeployNotify" # noqa
]
}
result = generate_cloudformation_args(**std_args)
self.assertEqual(result, std_return)


class TestProviderDefaultMode(unittest.TestCase):
def setUp(self):
Expand Down

0 comments on commit 06a4bee

Please sign in to comment.