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

[Partially Resolve #1193] Automatically adding Sceptre tags to all stacks #1204

Closed
wants to merge 3 commits into from
Closed
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
37 changes: 22 additions & 15 deletions integration-tests/features/create-stack.feature
Original file line number Diff line number Diff line change
Expand Up @@ -2,60 +2,67 @@ Feature: Create stack

Scenario: create new stack
Given stack "1/A" does not exist
and the template for stack "1/A" is "valid_template.json"
And the template for stack "1/A" is "valid_template.json"
When the user creates stack "1/A"
Then stack "1/A" exists in "CREATE_COMPLETE" state

Scenario: create a stack that already exists
Given stack "1/A" exists in "CREATE_COMPLETE" state
and the template for stack "1/A" is "valid_template.json"
And the template for stack "1/A" is "valid_template.json"
When the user creates stack "1/A"
Then stack "1/A" exists in "CREATE_COMPLETE" state

Scenario: create new stack that has previously failed
Given stack "1/A" exists in "CREATE_FAILED" state
and the template for stack "1/A" is "valid_template.json"
And the template for stack "1/A" is "valid_template.json"
When the user creates stack "1/A"
Then stack "1/A" exists in "CREATE_FAILED" state

Scenario: create new stack that is rolled back on failure
Given stack "8/A" does not exist
and the template for stack "8/A" is "invalid_template.json"
And the template for stack "8/A" is "invalid_template.json"
When the user creates stack "8/A"
Then stack "8/A" exists in "ROLLBACK_COMPLETE" state

Scenario: create new stack that is retained on failure
Given stack "8/B" does not exist
and the template for stack "8/B" is "invalid_template.json"
And the template for stack "8/B" is "invalid_template.json"
When the user creates stack "8/B"
Then stack "8/B" exists in "CREATE_FAILED" state

Scenario: create new stack that is rolled back after timeout
Given stack "8/C" does not exist
and the template for stack "8/C" is "valid_template_wait_300.json"
and the stack_timeout for stack "8/C" is "1"
And the template for stack "8/C" is "valid_template_wait_300.json"
And the stack_timeout for stack "8/C" is "1"
When the user creates stack "8/C"
Then stack "8/C" exists in "ROLLBACK_COMPLETE" state

Scenario: create new stack that ignores dependencies
Given stack "1/A" does not exist
and the template for stack "1/A" is "valid_template.json"
And the template for stack "1/A" is "valid_template.json"
When the user creates stack "1/A" with ignore dependencies
Then stack "1/A" exists in "CREATE_COMPLETE" state

Scenario: create new stack containing a SAM template transform
Given stack "10/A" does not exist
and the template for stack "10/A" is "sam_template.yaml"
And the template for stack "10/A" is "sam_template.yaml"
When the user creates stack "10/A"
Then stack "10/A" exists in "CREATE_COMPLETE" state

Scenario: create new stack with nested config jinja resolver
Given stack_group "12/1" does not exist
When the user launches stack_group "12/1"
Then all the stacks in stack_group "12/1" are in "CREATE_COMPLETE"
and stack "12/1/A" has "Project" tag with "A" value
and stack "12/1/A" has "Key" tag with "A" value
and stack "12/1/2/B" has "Project" tag with "B" value
and stack "12/1/2/B" has "Key" tag with "A-B" value
and stack "12/1/2/3/C" has "Project" tag with "C" value
and stack "12/1/2/3/C" has "Key" tag with "A-B-C" value
And stack "12/1/A" has "Project" tag with "A" value
And stack "12/1/A" has "Key" tag with "A" value
And stack "12/1/2/B" has "Project" tag with "B" value
And stack "12/1/2/B" has "Key" tag with "A-B" value
And stack "12/1/2/3/C" has "Project" tag with "C" value
And stack "12/1/2/3/C" has "Key" tag with "A-B-C" value

Scenario: Created stacks have sceptre tags
Given stack "1/A" does not exist
And the template for stack "1/A" is "valid_template.json"
When the user creates stack "1/A"
Then stack "1/A" has "sceptre_name" tag with "1/A" value
And stack "1/A" has "sceptre_project_code" tag with "${context.project_code}" value
6 changes: 6 additions & 0 deletions integration-tests/steps/stacks.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import re
from ast import literal_eval
from copy import deepcopy
from io import StringIO
Expand Down Expand Up @@ -367,6 +368,11 @@ def step_impl(context, stack_name, desired_status):

@then('stack "{stack_name}" has "{tag_name}" tag with "{desired_tag_value}" value')
def step_impl(context, stack_name, tag_name, desired_tag_value):
# If you can reference context attributes like ${context.attribute_name}
match = re.match(r'\$\{context\.([a-z_]{1,})}', desired_tag_value)
if match:
desired_tag_value = str(getattr(context, match.group(1)))

full_name = get_cloudformation_stack_name(context, stack_name)

tags = get_stack_tags(context, full_name)
Expand Down
26 changes: 20 additions & 6 deletions sceptre/config/reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import fnmatch
import logging
from os import environ, path, walk
from typing import Set
from typing import Set, Tuple, Dict

from pkg_resources import iter_entry_points
from pathlib import Path
Expand Down Expand Up @@ -188,7 +188,7 @@ def resolve_node_tag(self, loader, node):
node.tag = loader.resolve(type(node), node.value, (True, False))
return node

def construct_stacks(self) -> Set[Stack]:
def construct_stacks(self) -> Tuple[Set[Stack], Set[Stack]]:
"""
Traverses the files under the command path.
For each file encountered, a Stack is constructed
Expand Down Expand Up @@ -524,9 +524,8 @@ def _construct_stack(self, rel_path, stack_group_config=None):
)
)

s3_details = self._collect_s3_details(
stack_name, config
)
s3_details = self._collect_s3_details(stack_name, config)
stack_tags = self._collect_stack_tags(stack_name, config)
stack = Stack(
name=stack_name,
project_code=config["project_code"],
Expand All @@ -545,7 +544,7 @@ def _construct_stack(self, rel_path, stack_group_config=None):
dependencies=config.get("dependencies", []),
role_arn=config.get("role_arn"),
protected=config.get("protect", False),
tags=config.get("stack_tags", {}),
tags=stack_tags,
external_name=config.get("stack_name"),
notifications=config.get("notifications"),
on_failure=config.get("on_failure"),
Expand All @@ -569,3 +568,18 @@ def _parsed_stack_group_config(self, stack_group_config):
}
parsed_config.pop("stack_group_path")
return parsed_config

def _collect_stack_tags(self, stack_name: str, config: dict) -> Dict[str, str]:
"""
Assembles the stack tags dictionary, including default stack tags.

:param stack_name:
:param config:
:return:
"""
stack_tags = {
'sceptre_name': stack_name,
'sceptre_project_code': config['project_code'],
Comment on lines +581 to +582
Copy link
Contributor

@zaro0508 zaro0508 Feb 20, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we follow the cloudformation convention of using a <prefix>: ? https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-resource-tags.html

so something like sceptre:name and sceptre:project-code?

also, aws tags have limitations so maybe we should check for them? https://docs.aws.amazon.com/general/latest/gr/aws_tagging.html (Tag naming limits and requirements)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we follow the cloudformation convention of using a : ? https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-resource-tags.html

Yeah, that sounds good to me, actually. That's an easy change.

also, aws tags have limitations so maybe we should check for them? https://docs.aws.amazon.com/general/latest/gr/aws_tagging.html (Tag naming limits and requirements)

That's a good point. While I don't think it's very likely that we'd hit the 256 character limit on values or infract upon the allowed character limitations, we could indeed validate and even coerce those values to conform to those limits.

**config.get('stack_tags', {})
}
return stack_tags
5 changes: 4 additions & 1 deletion tests/test_config_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,10 @@ def test_construct_stacks_constructs_stack(
iam_role=None,
role_arn=None,
protected=False,
tags={},
tags={
'sceptre_name': 'account/stack-group/region/vpc',
'sceptre_project_code': 'account_project_code'
},
external_name=None,
notifications=None,
on_failure=None,
Expand Down