Repository: cloud-custodian/cloud-custodian

Issue:
Tagging CloudFormation - Error: Parameters must have value
Initially the stack is created with an input parameter.

**c7n policy**
```
policies:
  - name: add-cfn-tag
    resource: cfn
    filters:
      - "tag:testcfn": present
    actions:
      - type: tag
        value: abc
        key: BusinessUnit
```
**Error**
An error occurred (ValidationError) when calling the UpdateStack operation: Parameters: [input_param] must have values


Discussion:
how are you creating a cft with a required parameter? afaics the tooling is a bit opaque around this
Initially when the stack is created it is provided with a list of parameters, example below. Using c7n we need to tag the stack, that is stack update operation. When I am try to do so c7n gives an error
because 'UsePreviousValue': True  is not set while update.

  "Parameters": {
    "CISUserName": {
      "Type": "String",
      "Description": "The access key used to run CIS report utility using Jenkin's target",
      "MinLength": "1",
      "ConstraintDescription": "This parameter is required."
    }
  }
Adding to the previous comment, I tried different ways like using periodic lambda, event based lambda on stack creation ,and using local setup. All give the same error- "Parameters must have value". Almost all of our stacks have input parameters and we need to update tagging only.
thanks, i was able to reproduce with that snippet, fix in pull request #3464
Thanks Kapil for the quick fix.
With above fix it is no more showing parameter issue but it is not working as expected.

1) Previously existing tags are removed, only newly added tags are reflected.

2) If we provide multiple tags to update it only reflect last tag in API sequence  request with an error mentioned below.

policies:
  - name: add-cfn-tag
    resource: cfn
    filters:
      - "tag:BusinessUnit": abc
    actions:
      - type: tag
        value: abc2
        key: BusinessUnit2
      - type: tag
        value: abc1
        key: BusinessUnit1

An error occurred (ValidationError) when calling the UpdateStack operation: Stack:arn:aws:cloudformation:us-west-2:594523226318:stack/test-jsingh-c7n-tagging/b9fb10b0-2416-11e9-95ca-0ae06257a2d8 is in UPDATE_IN_PROGRESS state and can not be updated

3)   If we are updating a stack with CAPABILITY_IAM, below error is being observed.

2019-01-29 14:28:24,262: custodian.actions:ERROR Exception with tags: [{u'Value': 'abc', u'Key': 'BusinessUnit'}] on resources: c7n-custodian-user-policy-and-ops 
 An error occurred (InsufficientCapabilitiesException) when calling the UpdateStack operation: Requires capabilities : [CAPABILITY_IAM]
Kapil, I am not sure does the fix incorporate the changes to avoid previous tags replacement and iam capability in stack. If not, do I need to open an issue to track the same separately. 
hmm.. noted, sorry i'll have to do a followup, its very unfortunate syntax that requires providing all the existing state to trigger a tag change.
wrt to tagging with multiple values use syntax

````
type: tag
tags:
    BusinessUnit1: abc2
    BusinessUnit2: abc1
```
Kapil, issue to prevent deletion of previously existing tag is of great importance. Previously existing tags must be preserved on stack update. 

Resolving patch:
diff --git a/c7n/resources/cfn.py b/c7n/resources/cfn.py
--- a/c7n/resources/cfn.py
+++ b/c7n/resources/cfn.py
@@ -135,8 +135,8 @@ class CloudFormationAddTag(Tag):
               - 'tag:DesiredTag': absent
             actions:
               - type: tag
-                key: DesiredTag
-                value: DesiredValue
+                tags:
+                  DesiredTag: DesiredValue
     """
     permissions = ('cloudformation:UpdateStack',)
 
@@ -144,19 +144,40 @@ def process_resource_set(self, stacks, tags):
         client = local_session(
             self.manager.session_factory).client('cloudformation')
 
-        def _tag_stacks(s):
-            params = []
-            for p in s.get('Parameters', []):
-                params.append({'ParameterKey': p['ParameterKey'],
-                               'UsePreviousValue': True})
-            client.update_stack(
-                StackName=s['StackName'],
-                UsePreviousTemplate=True,
-                Parameters=params,
-                Tags=tags)
+        for s in stacks:
+            _tag_stack(client, s, add=tags)
 
-        with self.executor_factory(max_workers=2) as w:
-            list(w.map(_tag_stacks, stacks))
+
+def _tag_stack(client, s, add=(), remove=()):
+
+    tags = {t['Key']: t['Value'] for t in s.get('Tags')}
+    for t in remove:
+        tags.pop(t, None)
+
+    for t in add:
+        tags[t['Key']] = t['Value']
+
+    params = []
+    for p in s.get('Parameters', []):
+        params.append(
+            {'ParameterKey': p['ParameterKey'],
+             'UsePreviousValue': True})
+
+    capabilities = []
+    for c in s.get('Capabilities', []):
+        capabilities.append(c)
+
+    notifications = []
+    for n in s.get('NotificationArns', []):
+        notifications.append(n)
+
+    client.update_stack(
+        StackName=s['StackName'],
+        UsePreviousTemplate=True,
+        Capabilities=capabilities,
+        Parameters=params,
+        NotificationARNs=notifications,
+        Tags=[{'Key': k, 'Value': v} for k, v in tags.items()])
 
 
 @CloudFormation.action_registry.register('remove-tag')
@@ -181,12 +202,5 @@ def process_resource_set(self, stacks, keys):
         client = local_session(
             self.manager.session_factory).client('cloudformation')
 
-        def _remove_tag(s):
-            tags = [t for t in s['Tags'] if t['Key'] not in keys]
-            client.update_stack(
-                StackName=s['StackName'],
-                UsePreviousTemplate=True,
-                Tags=tags)
-
-        with self.executor_factory(max_workers=2) as w:
-            list(w.map(_remove_tag, stacks))
+        for s in stacks:
+            _tag_stack(client, s, remove=keys)


Test patch:
diff --git a/tests/data/placebo/test_cfn_add_tag_w_params/cloudformation.DescribeStacks_1.json b/tests/data/placebo/test_cfn_add_tag_w_params/cloudformation.DescribeStacks_1.json
--- a/tests/data/placebo/test_cfn_add_tag_w_params/cloudformation.DescribeStacks_1.json
+++ b/tests/data/placebo/test_cfn_add_tag_w_params/cloudformation.DescribeStacks_1.json
@@ -28,9 +28,9 @@
                     "month": 1,
                     "day": 29,
                     "hour": 18,
-                    "minute": 48,
-                    "second": 41,
-                    "microsecond": 771000
+                    "minute": 51,
+                    "second": 40,
+                    "microsecond": 115000
                 },
                 "RollbackConfiguration": {},
                 "StackStatus": "UPDATE_COMPLETE",
@@ -38,8 +38,8 @@
                 "NotificationARNs": [],
                 "Tags": [
                     {
-                        "Key": "Author",
-                        "Value": "Kapil"
+                        "Key": "Env",
+                        "Value": "Dev"
                     }
                 ],
                 "DriftInformation": {
@@ -48,14 +48,14 @@
             }
         ],
         "ResponseMetadata": {
-            "RequestId": "e9940725-23f6-11e9-a2ce-4597e04b9a84",
+            "RequestId": "f2131b08-24cb-11e9-a640-e3442d72b9ca",
             "HTTPStatusCode": 200,
             "HTTPHeaders": {
-                "x-amzn-requestid": "e9940725-23f6-11e9-a2ce-4597e04b9a84",
+                "x-amzn-requestid": "f2131b08-24cb-11e9-a640-e3442d72b9ca",
                 "content-type": "text/xml",
-                "content-length": "16472",
+                "content-length": "16467",
                 "vary": "Accept-Encoding",
-                "date": "Tue, 29 Jan 2019 18:51:39 GMT"
+                "date": "Wed, 30 Jan 2019 20:16:36 GMT"
             },
             "RetryAttempts": 0
         }
diff --git a/tests/data/placebo/test_cfn_add_tag_w_params/cloudformation.DescribeStacks_2.json b/tests/data/placebo/test_cfn_add_tag_w_params/cloudformation.DescribeStacks_2.json
--- a/tests/data/placebo/test_cfn_add_tag_w_params/cloudformation.DescribeStacks_2.json
+++ b/tests/data/placebo/test_cfn_add_tag_w_params/cloudformation.DescribeStacks_2.json
@@ -26,11 +26,11 @@
                     "__class__": "datetime",
                     "year": 2019,
                     "month": 1,
-                    "day": 29,
-                    "hour": 18,
-                    "minute": 51,
-                    "second": 40,
-                    "microsecond": 115000
+                    "day": 30,
+                    "hour": 20,
+                    "minute": 16,
+                    "second": 37,
+                    "microsecond": 763000
                 },
                 "RollbackConfiguration": {},
                 "StackStatus": "UPDATE_IN_PROGRESS",
@@ -38,6 +38,10 @@
                 "DisableRollback": false,
                 "NotificationARNs": [],
                 "Tags": [
+                    {
+                        "Key": "App",
+                        "Value": "Ftw"
+                    },
                     {
                         "Key": "Env",
                         "Value": "Dev"
@@ -50,13 +54,13 @@
             }
         ],
         "ResponseMetadata": {
-            "RequestId": "e9c5291a-23f6-11e9-8a67-cbe15c822cf9",
+            "RequestId": "f29d5977-24cb-11e9-877a-c398f938be5e",
             "HTTPStatusCode": 200,
             "HTTPHeaders": {
-                "x-amzn-requestid": "e9c5291a-23f6-11e9-8a67-cbe15c822cf9",
+                "x-amzn-requestid": "f29d5977-24cb-11e9-877a-c398f938be5e",
                 "content-type": "text/xml",
-                "content-length": "1400",
-                "date": "Tue, 29 Jan 2019 18:51:39 GMT"
+                "content-length": "1497",
+                "date": "Wed, 30 Jan 2019 20:16:37 GMT"
             },
             "RetryAttempts": 0
         }
diff --git a/tests/data/placebo/test_cfn_add_tag_w_params/cloudformation.UpdateStack_1.json b/tests/data/placebo/test_cfn_add_tag_w_params/cloudformation.UpdateStack_1.json
--- a/tests/data/placebo/test_cfn_add_tag_w_params/cloudformation.UpdateStack_1.json
+++ b/tests/data/placebo/test_cfn_add_tag_w_params/cloudformation.UpdateStack_1.json
@@ -3,13 +3,13 @@
     "data": {
         "StackId": "arn:aws:cloudformation:us-east-1:644160558196:stack/mosdef2/387d82a0-23f5-11e9-98e5-128a65397f5a",
         "ResponseMetadata": {
-            "RequestId": "e9a62eec-23f6-11e9-9333-3feb7299e7ae",
+            "RequestId": "f27e8701-24cb-11e9-b642-754098a9759f",
             "HTTPStatusCode": 200,
             "HTTPHeaders": {
-                "x-amzn-requestid": "e9a62eec-23f6-11e9-9333-3feb7299e7ae",
+                "x-amzn-requestid": "f27e8701-24cb-11e9-b642-754098a9759f",
                 "content-type": "text/xml",
                 "content-length": "377",
-                "date": "Tue, 29 Jan 2019 18:51:39 GMT"
+                "date": "Wed, 30 Jan 2019 20:16:37 GMT"
             },
             "RetryAttempts": 0
         }
diff --git a/tests/test_cfn.py b/tests/test_cfn.py
--- a/tests/test_cfn.py
+++ b/tests/test_cfn.py
@@ -74,7 +74,7 @@ def test_cfn_add_tag_with_params(self):
                 "resource": "cfn",
                 "filters": [{"StackName": "mosdef2"}],
                 "actions": [
-                    {"type": "tag", "tags": {'Env': 'Dev'}}
+                    {"type": "tag", "tags": {'App': 'Ftw'}}
                 ],
             },
             session_factory=session_factory,
@@ -82,9 +82,11 @@ def test_cfn_add_tag_with_params(self):
         resources = p.run()
         self.assertEqual(len(resources), 1)
         client = session_factory(region="us-east-1").client("cloudformation")
+        rtags = {t['Key']: t['Value'] for t in resources[0]['Tags']}
+        self.assertEqual(rtags, {'Env': 'Dev'})
         tags = {t['Key']: t['Value'] for t in client.describe_stacks(
             StackName=resources[0]["StackName"])["Stacks"][0]["Tags"]}
-        self.assertEqual(tags, {'Env': 'Dev'})
+        self.assertEqual(tags, {'Env': 'Dev', 'App': 'Ftw'})
 
     def test_cfn_add_tag(self):
         session_factory = self.replay_flight_data("test_cfn_add_tag")