Repository: aws-ia/taskcat
Issue: Tags parameter in taskcat.yml config file is not honoring for taskcat tests

The issue has been reported to the AWS support team . They suggested us to report the issue in Github.

Issue reported :
We have a requirement to test our cloudformation in all the regions. For testing , we use taskcat tool.
Taskcat version installed was 0.9.8. The .taskcat.yml has below contents :
```
general:
  parameters:
    KeyName: 'keyname'
    InstanceType: 't2.small'
    SSHLocation: '0.0.0.0/32'
project:
  name:  test1040
  regions:
    - us-east-2
tests:
  dei:
    regions:
      - us-east-2
    tags:
      APPLICATIONENV: 'somevalue'
      APPLICATIONROLE: 'somevalue'
      BUSINESSUNIT: 'somevalue'
      NAME: 'somevalue'
      OWNEREMAIL: 'somevalue'
      RUNNINGSCHEDULE: 'somevalue'
    template: ./EC2InstanceWithSecurityGroupSample.template
```

We have specified tags in the config file but its not getting applying to cloudformation . Our company has strict policy on associating tags with every aws resource. Due to the above bug , we are unable to test our cloudformation solution using taskcat  .
Please let me know if you have any way to impose tags on the cloudformation.

Confirmation email from the AWS Support team :+1: 
From the correspondence, I understand you reached out to us regarding an issue you are currently facing with the taskcat tool, where the passed tags doesn't get applied to the CloudFormation stacks that are created using the tool.

To begin with, I tested this issue by running a sample project with a CloudFormation template using the taskcat tool, and I was successful in replicating your issue as the tags that were given in the taskcat config file didn't get applied to the CloudFormation stack. And the below given default tags were the only ones that were applied to the stack.


------------------------- CloudFormation Tags-------------------------------
Tags :

Key 					Value
taskcat-id		       aa3e32f7bed2427389de9ab9d987f3fb
taskcat-project-name	my-cfn-project-2
taskcat-test-name		my-test

-------------------------------------------------------------------------------------

Clearly, it looks like a bug that should be reported to the taskcat team, and since taskcat is an open source tool and is available to customers via github, I would suggest you to create an issue via the github page so that the developers working on this can address your issue directly, also this would help other people who are using the tool and encounter the same kind of problems. 

Also, I have escalated this issue internally so it can be looked over, unfortunately I don't have any ETA for this and any update should be directly communicated via the github issue.

_@andrew-glenn edit: Wrapping the provided config file in a code block_

Changed files:
tests/test_cfn_stack.py, taskcat/_config.py, taskcat/_cfn/stack.py, taskcat/_cli_modules/deploy.py, taskcat/_cfn/threaded.py, taskcat/_dataclasses.py

diff --git a/taskcat/_cfn/stack.py b/taskcat/_cfn/stack.py
index b19e307..a6ffc40 100644
--- a/taskcat/_cfn/stack.py
+++ b/taskcat/_cfn/stack.py
@@ -15,7 +15,7 @@ import yaml
 
 from taskcat._cfn.template import Template
 from taskcat._common_utils import ordered_dump, pascal_to_snake, s3_url_maker
-from taskcat._dataclasses import TestRegion
+from taskcat._dataclasses import Tag, TestRegion
 
 LOG = logging.getLogger(__name__)
 
@@ -155,18 +155,6 @@ class Output:
             self.export_name = output_dict["ExportName"]
 
 
-class Tag:
-    def __init__(self, tag_dict: dict):
-        if isinstance(tag_dict, Tag):
-            tag_dict = {"Key": tag_dict.key, "Value": tag_dict.value}
-        self.key: str = tag_dict["Key"]
-        self.value: str = tag_dict["Value"]
-
-    def dump(self):
-        tag_dict = {"Key": self.key, "Value": self.value}
-        return tag_dict
-
-
 class FilterableList(list):
     def filter(self, criteria: Optional[dict] = None, **kwargs):
         if not criteria and not kwargs:
diff --git a/taskcat/_cfn/threaded.py b/taskcat/_cfn/threaded.py
index 82e583c..b52b137 100644
--- a/taskcat/_cfn/threaded.py
+++ b/taskcat/_cfn/threaded.py
@@ -6,10 +6,10 @@ from typing import Dict, List
 
 import boto3
 
-from taskcat._cfn.stack import Stack, Stacks, StackStatus, Tag
+from taskcat._cfn.stack import Stack, Stacks, StackStatus
 from taskcat._client_factory import Boto3Cache
 from taskcat._common_utils import merge_dicts
-from taskcat._dataclasses import TestObj, TestRegion
+from taskcat._dataclasses import Tag, TestObj, TestRegion
 from taskcat.exceptions import TaskCatException
 
 LOG = logging.getLogger(__name__)
@@ -67,6 +67,7 @@ class Stacker:
         )
         tags.append(Tag({"Key": "taskcat-project-name", "Value": self.project_name}))
         tags.append(Tag({"Key": "taskcat-test-name", "Value": test.name}))
+        tags += test.tags
         partial_kwargs = {
             "stack_name": stack_name,
             "template": test.template,
diff --git a/taskcat/_cli_modules/deploy.py b/taskcat/_cli_modules/deploy.py
index a8cf60f..cd86ca3 100644
--- a/taskcat/_cli_modules/deploy.py
+++ b/taskcat/_cli_modules/deploy.py
@@ -6,10 +6,10 @@ from time import sleep
 
 from dulwich import porcelain
 from dulwich.config import ConfigFile, parse_submodules
-from taskcat._cfn.stack import Tag
 from taskcat._cfn.threaded import Stacker
 from taskcat._client_factory import Boto3Cache
 from taskcat._config import Config
+from taskcat._dataclasses import Tag
 from taskcat._name_generator import generate_name
 from taskcat._s3_stage import stage_in_s3
 from taskcat.exceptions import TaskCatException
diff --git a/taskcat/_config.py b/taskcat/_config.py
index 73d646a..6e46f05 100644
--- a/taskcat/_config.py
+++ b/taskcat/_config.py
@@ -9,7 +9,14 @@ import yaml
 from taskcat._cfn.template import Template
 from taskcat._client_factory import Boto3Cache
 from taskcat._common_utils import generate_bucket_name
-from taskcat._dataclasses import BaseConfig, RegionObj, S3BucketObj, TestObj, TestRegion
+from taskcat._dataclasses import (
+    BaseConfig,
+    RegionObj,
+    S3BucketObj,
+    Tag,
+    TestObj,
+    TestRegion,
+)
 from taskcat._legacy_config import legacy_overrides, parse_legacy_config
 from taskcat._template_params import ParamGen
 from taskcat.exceptions import TaskCatException
@@ -317,6 +324,10 @@ class Config:
         tests = {}
         for test_name, test in self.config.tests.items():
             region_list = []
+            tag_list = []
+            if test.tags:
+                for tag_key, tag_value in test.tags.items():
+                    tag_list.append(Tag({"Key": tag_key, "Value": tag_value}))
             for region_obj in regions[test_name].values():
                 region_list.append(
                     TestRegion.from_region_obj(
@@ -331,5 +342,6 @@ class Config:
                 template=templates[test_name],
                 project_root=project_root,
                 regions=region_list,
+                tags=tag_list,
             )
         return tests
diff --git a/taskcat/_dataclasses.py b/taskcat/_dataclasses.py
index 6fcd041..b85cb9e 100644
--- a/taskcat/_dataclasses.py
+++ b/taskcat/_dataclasses.py
@@ -288,6 +288,18 @@ class S3BucketObj:
         return False
 
 
+class Tag:
+    def __init__(self, tag_dict: dict):
+        if isinstance(tag_dict, Tag):
+            tag_dict = {"Key": tag_dict.key, "Value": tag_dict.value}
+        self.key: str = tag_dict["Key"]
+        self.value: str = tag_dict["Value"]
+
+    def dump(self):
+        tag_dict = {"Key": self.key, "Value": self.value}
+        return tag_dict
+
+
 @dataclass
 class TestRegion(RegionObj):
     s3_bucket: S3BucketObj
@@ -305,6 +317,7 @@ class TestObj:
     project_root: Path
     name: TestName
     regions: List[TestRegion]
+    tags: List[Tag]
 
 
 @dataclass
diff --git a/tests/test_cfn_stack.py b/tests/test_cfn_stack.py
index a0dc9d5..ba8f14f 100644
--- a/tests/test_cfn_stack.py
+++ b/tests/test_cfn_stack.py
@@ -14,11 +14,11 @@ from taskcat._cfn.stack import (
     Resource,
     Resources,
     Stack,
-    Tag,
     Tags,
     TestRegion,
     criteria_matches,
 )
+from taskcat._dataclasses import Tag
 
 event_template = {
     "EventId": "test_event_id",
@@ -41,7 +41,7 @@ class TestCriteriaMatcher(unittest.TestCase):
             tag = Tag({"Key": "my_key", "Value": "my_value"})
             criteria_matches({"invalid": "blah"}, tag)
         self.assertEqual(
-            "invalid is not a valid property of <class 'taskcat._cfn.stack.Tag'>",
+            "invalid is not a valid property of <class 'taskcat._dataclasses.Tag'>",
             str(cm.exception),
         )