From 1660c948d6984d7195fd72c6ef95e3e7817742ca Mon Sep 17 00:00:00 2001 From: Garison Draper Date: Mon, 23 Oct 2017 14:02:36 -0700 Subject: [PATCH] added TLK for bucket_tags w/ failback method to tags function if not set --- docs/config.rst | 43 +++++++++++++++++++++++++----- stacker/config/__init__.py | 2 ++ stacker/context.py | 16 +++++++++++ stacker/tests/actions/test_base.py | 33 +++++++++++++++++++++++ stacker/util.py | 23 ++++++++++++---- 5 files changed, 105 insertions(+), 12 deletions(-) diff --git a/docs/config.rst b/docs/config.rst index 35f8f7cea..19756b6ac 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -90,6 +90,33 @@ See the `CloudFormation Limits Reference`_. .. _`CloudFormation Limits Reference`: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cloudformation-limits.html + +S3 Bucket tags +---- + +Various resources in AWS support arbitrary key-value pair tags. You can set +the `bucket_tags` Top Level Keyword to populate tags on all S3 buckets Staker +attempts to create for CloudFormation template uploads, inclduing the S3 bucket +created by the aws_lambda pre-hook. + +If bucket_tags is not set in your Configuration, stacker will fallback to the +method used to determine tags in your config by the `tags` top level keyword. +The `bucket_tags` keyword takes precedence over `tags` when applying. Example:: + + bucket_tags: + "hello": world + "my_tag:with_colons_in_key": ${dynamic_tag_value_from_my_env} + simple_tag: simple value + +If you prefer to have no tags applied to your stacks (versus the default tags +that stacker applies), specify an empty map for the top-level keyword:: + + bucket_tags: {} + +Tags updates get applied on every stacker run + +.. _`AWS CloudFormation Resource Tags Type`: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-resource-tags.html + Module Paths ------------ When setting the ``classpath`` for blueprints/hooks, it is sometimes desirable to @@ -220,29 +247,31 @@ Tags ---- Various resources in AWS support arbitrary key-value pair tags. You can set -the `tags` Top Level Keyword to populate tags on all Resources that support -that feature. The S3 bucket created by stacker for CloudFormation uploads and -all CloudFormation stack-level resources, including automatically created tags, -are propagated to resources that AWS CloudFormation supports. See -`AWS CloudFormation Resource Tags Type`_ for more details. +the `tags` Top Level Keyword to populate tags on all Resources that Staker +attempts to create via CloudFormation. All CloudFormation stack-level resources, +including automatically created tags, are propagated to resources that AWS +CloudFormation supports. See `AWS CloudFormation Resource Tags Type`_ for +more details. If no tags are specified, the `stacker_namespace` tag is applied to your stack with the value of `namespace` as the tag value. If you prefer to apply a custom set of tags, specify the top-level keyword -`tags` as a map. Example:: +`tags` as a map. The `stacker_namespace` tag will be automaticly added as well +to help identify resources created by Stacker. Example:: tags: "hello": world "my_tag:with_colons_in_key": ${dynamic_tag_value_from_my_env} simple_tag: simple value + If you prefer to have no tags applied to your stacks (versus the default tags that stacker applies), specify an empty map for the top-level keyword:: tags: {} -Tags are updated on every stacker run +Tags updates get applied on every stacker run .. _`AWS CloudFormation Resource Tags Type`: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-resource-tags.html diff --git a/stacker/config/__init__.py b/stacker/config/__init__.py index 89c104ae0..4ca84c381 100644 --- a/stacker/config/__init__.py +++ b/stacker/config/__init__.py @@ -339,6 +339,8 @@ class Config(Model): tags = DictType(StringType, serialize_when_none=False) + bucket_tags = DictType(StringType, serialize_when_none=False) + mappings = DictType( DictType(DictType(StringType)), serialize_when_none=False) diff --git a/stacker/context.py b/stacker/context.py index 3bcc31762..5bc0ad1ce 100644 --- a/stacker/context.py +++ b/stacker/context.py @@ -97,11 +97,27 @@ def upload_templates_to_s3(self): def tags(self): tags = self.config.tags if tags is not None: + if "stacker_namespace" in tags: + return tags + tags["stacker_namespace"] = self.namespace return tags if self.namespace: return {"stacker_namespace": self.namespace} return {} + @property + def s3_bucket_tags(self): + s3_bucket_tags = self.config.bucket_tags + if s3_bucket_tags is not None: + return s3_bucket_tags + else: + s3_bucket_tags = self.config.tags + if s3_bucket_tags is not None: + return s3_bucket_tags + if self.namespace: + return {"stacker_namespace": self.namespace} + return {} + @property def _base_fqn(self): return self.namespace.replace(".", "-").lower() diff --git a/stacker/tests/actions/test_base.py b/stacker/tests/actions/test_base.py index 446b07b2e..9c86b1245 100644 --- a/stacker/tests/actions/test_base.py +++ b/stacker/tests/actions/test_base.py @@ -42,6 +42,17 @@ def test_ensure_cfn_bucket_exists(self): "Bucket": ANY, } ) + stubber.add_response( + "put_bucket_tagging", + service_response={}, + expected_params={ + "Bucket": ANY, + "Tagging": { + "TagSet": [ + {"Key": "stacker_namespace", + "Value": u"mynamespace"}]} + } + ) with stubber: action.ensure_cfn_bucket() @@ -65,6 +76,17 @@ def test_ensure_cfn_bucket_doesnt_exist_us_east(self): "Bucket": ANY, } ) + stubber.add_response( + "put_bucket_tagging", + service_response={}, + expected_params={ + "Bucket": ANY, + "Tagging": { + "TagSet": [ + {"Key": "stacker_namespace", + "Value": u"mynamespace"}]} + } + ) with stubber: action.ensure_cfn_bucket() @@ -91,6 +113,17 @@ def test_ensure_cfn_bucket_doesnt_exist_us_west(self): } } ) + stubber.add_response( + "put_bucket_tagging", + service_response={}, + expected_params={ + "Bucket": ANY, + "Tagging": { + "TagSet": [ + {"Key": "stacker_namespace", + "Value": u"mynamespace"}]} + } + ) with stubber: action.ensure_cfn_bucket() diff --git a/stacker/util.py b/stacker/util.py index 4b1e54c7d..67ee6a9ad 100644 --- a/stacker/util.py +++ b/stacker/util.py @@ -13,7 +13,6 @@ import yaml from collections import OrderedDict from git import Repo -import stacker.actions.build import botocore.exceptions logger = logging.getLogger(__name__) @@ -481,8 +480,8 @@ def ensure_s3_bucket(s3_client, bucket_name, bucket_region, context): try: # Checking is bucket exists s3_client.head_bucket(Bucket=bucket_name) - # pulling tags from context - tagset = stacker.actions.build.build_stack_tags(context) + # pulling tags from s3_bucket_tags function + tagset = _s3_bucket_tags(context) # setting tags on every run - must have permission to perform # the s3:PutBucketTagging action s3_client.put_bucket_tagging(Bucket=bucket_name, @@ -498,8 +497,8 @@ def ensure_s3_bucket(s3_client, bucket_name, bucket_region, context): create_args["CreateBucketConfiguration"] = { "LocationConstraint": location_constraint } - # pulling tags from context - tagset = stacker.actions.build.build_stack_tags(context) + # pulling tags from s3_bucket_tags function + tagset = _s3_bucket_tags(context) s3_client.create_bucket(**create_args) # setting tags on every run - must have permission to perform # the s3:PutBucketTagging action @@ -516,6 +515,20 @@ def ensure_s3_bucket(s3_client, bucket_name, bucket_region, context): raise +def _s3_bucket_tags(context): + """Returns the tags to be applied for a S3 bucket. + + Args: + context (:class:`stacker.context.Context`): The stacker context, used + set the S3 bucket tags from the stacker config + + Returns: + List of dictionaries containing tags to apply to that bucket. + """ + return [ + {'Key': t[0], 'Value': t[1]} for t in context.s3_bucket_tags.items()] + + class SourceProcessor(): """Makes remote python package sources available in the running python environment."""