Repository: cloud-custodian/cloud-custodian

Issue:
aws.eni copy-related-tag action tags incorrect instance.
The **copy-related-tag** action for **aws.eni** associates the incorrect interface to an instance.

```
policies:
    - name: copy-tags-from-ec2-to-eni
      resource: aws.eni
      actions:
        - type: copy-related-tag
          resource: ec2
          skip_missing: True
          key: Attachment.InstanceId
          tags: '*'
```

This is the current setup (2 VPCs)
```
vpc-0d73...
	eni-091f...	Interface for NAT Gateway nat-071539e026431a0df
vpc-06b3...
	eni-0228...	Interface for NAT Gateway nat-099a190bd144be827
	eni-0d17...	Primary network interface (instance i-089...)
	eni-0e01...	RDSNetworkInterface
```
The one that gets tagged was tagged **eni-091f**
The one that should get tagged: **eni-0d17**


Discussion:

Resolving patch:
diff --git a/c7n/tags.py b/c7n/tags.py
--- a/c7n/tags.py
+++ b/c7n/tags.py
@@ -1017,18 +1017,23 @@ def validate(self):
         return self
 
     def process(self, resources):
-        related_resources = list(
-            zip(jmespath.search('[].[%s || "c7n:NotFound"]|[]' % self.data['key'], resources),
-                resources))
+        related_resources = []
+        for rrid, r in zip(jmespath.search('[].[%s]' % self.data['key'], resources),
+                           resources):
+            related_resources.append((rrid[0], r))
         related_ids = set([r[0] for r in related_resources])
-        related_ids.discard('c7n:NotFound')
+        missing = False
+        if None in related_ids:
+            missing = True
+            related_ids.discard(None)
         related_tag_map = self.get_resource_tag_map(self.data['resource'], related_ids)
 
         missing_related_tags = related_ids.difference(related_tag_map.keys())
-        if not self.data.get('skip_missing', True) and missing_related_tags:
+        if not self.data.get('skip_missing', True) and (missing_related_tags or missing):
             raise PolicyExecutionError(
                 "Unable to find all %d %s related resources tags %d missing" % (
-                    len(related_ids), self.data['resource'], len(missing_related_tags)))
+                    len(related_ids), self.data['resource'],
+                    len(missing_related_tags) + int(missing)))
 
         # rely on resource manager tag action implementation as it can differ between resources
         tag_action = self.manager.action_registry.get('tag')({}, self.manager)
@@ -1038,7 +1043,9 @@ def process(self, resources):
         stats = Counter()
 
         for related, r in related_resources:
-            if related in missing_related_tags or not related_tag_map[related]:
+            if (related is None or
+                related in missing_related_tags or
+                    not related_tag_map[related]):
                 stats['missing'] += 1
             elif self.process_resource(
                     client, r, related_tag_map[related], self.data['tags'], tag_action):
@@ -1075,10 +1082,10 @@ def get_resource_tag_map(self, r_type, ids):
         """
         manager = self.manager.get_resource_manager(r_type)
         r_id = manager.resource_type.id
-        # TODO only fetch resource with the given ids.
+
         return {
             r[r_id]: {t['Key']: t['Value'] for t in r.get('Tags', [])}
-            for r in manager.resources() if r[r_id] in ids
+            for r in manager.get_resources(list(ids))
         }
 
     @classmethod


Test patch:
diff --git a/tests/data/placebo/test_copy_related_resource_tag_empty/ec2.CreateTags_1.json b/tests/data/placebo/test_copy_related_resource_tag_empty/ec2.CreateTags_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_copy_related_resource_tag_empty/ec2.CreateTags_1.json
@@ -0,0 +1,16 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {
+            "RequestId": "f4b336aa-d331-4029-a67e-49daea8d5a4d",
+            "HTTPStatusCode": 200,
+            "HTTPHeaders": {
+                "content-type": "text/xml;charset=UTF-8",
+                "content-length": "221",
+                "date": "Mon, 23 Sep 2019 14:28:40 GMT",
+                "server": "AmazonEC2"
+            },
+            "RetryAttempts": 0
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_copy_related_resource_tag_empty/ec2.DescribeInstances_1.json b/tests/data/placebo/test_copy_related_resource_tag_empty/ec2.DescribeInstances_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_copy_related_resource_tag_empty/ec2.DescribeInstances_1.json
@@ -0,0 +1,59 @@
+{
+    "status_code": 200,
+    "data": {
+        "Reservations": [
+            {
+                "Groups": [],
+                "Instances": [
+                    {
+                        "InstanceId": "i-0f94b4d7343149de3",
+                        "NetworkInterfaces": [
+                            {
+                                "Attachment": {
+                                    "AttachTime": {
+                                        "__class__": "datetime",
+                                        "year": 2019,
+                                        "month": 6,
+                                        "day": 21,
+                                        "hour": 13,
+                                        "minute": 40,
+                                        "second": 19,
+                                        "microsecond": 0
+                                    },
+                                    "AttachmentId": "eni-attach-085d0d695460607ca",
+                                    "DeleteOnTermination": true,
+                                    "DeviceIndex": 0,
+                                    "Status": "attached"
+                                },
+                                "NetworkInterfaceId": "eni-0e1324ba169ed7b2f",
+                                "InterfaceType": "interface"
+                            }
+                        ],
+                        "Tags": [
+                            {
+                                "Key": "Env",
+                                "Value": "Dev"
+                            },
+                            {
+                                "Key": "Origin",
+                                "Value": "Home"
+                            }
+                        ]
+                    }
+                ]
+            }
+        ],
+        "ResponseMetadata": {
+            "RequestId": "3906195a-9d57-4cad-9ed9-e313f853d0d8",
+            "HTTPStatusCode": 200,
+            "HTTPHeaders": {
+                "content-type": "text/xml;charset=UTF-8",
+                "content-length": "7132",
+                "vary": "accept-encoding",
+                "date": "Mon, 23 Sep 2019 14:28:40 GMT",
+                "server": "AmazonEC2"
+            },
+            "RetryAttempts": 0
+        }
+    }
+}
diff --git a/tests/data/placebo/test_copy_related_resource_tag_empty/ec2.DescribeNetworkInterfaces_1.json b/tests/data/placebo/test_copy_related_resource_tag_empty/ec2.DescribeNetworkInterfaces_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_copy_related_resource_tag_empty/ec2.DescribeNetworkInterfaces_1.json
@@ -0,0 +1,44 @@
+{
+    "status_code": 200,
+    "data": {
+        "NetworkInterfaces": [
+            {
+                "NetworkInterfaceId": "eni-0e78f79b7b27d86ec",
+                "TagSet": []
+            },	    
+            {
+                "Attachment": {
+                    "AttachTime": {
+                        "__class__": "datetime",
+                        "year": 2019,
+                        "month": 6,
+                        "day": 21,
+                        "hour": 13,
+                        "minute": 40,
+                        "second": 19,
+                        "microsecond": 0
+                    },
+                    "AttachmentId": "eni-attach-085d0d695460607ca",
+                    "DeleteOnTermination": true,
+                    "DeviceIndex": 0,
+                    "InstanceId": "i-0f94b4d7343149de3",
+                    "InstanceOwnerId": "644160558196",
+                    "Status": "attached"
+                },
+                "NetworkInterfaceId": "eni-0e1324ba169ed7b2f"
+            }
+        ],
+        "ResponseMetadata": {
+            "RequestId": "d4d51c9e-32df-4f1f-a5ee-fe293a9ffcb8",
+            "HTTPStatusCode": 200,
+            "HTTPHeaders": {
+                "content-type": "text/xml;charset=UTF-8",
+                "content-length": "4359",
+                "vary": "accept-encoding",
+                "date": "Mon, 23 Sep 2019 14:28:40 GMT",
+                "server": "AmazonEC2"
+            },
+            "RetryAttempts": 0
+        }
+    }
+}
diff --git a/tests/data/placebo/test_copy_related_resource_tag_empty/ec2.DescribeNetworkInterfaces_2.json b/tests/data/placebo/test_copy_related_resource_tag_empty/ec2.DescribeNetworkInterfaces_2.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_copy_related_resource_tag_empty/ec2.DescribeNetworkInterfaces_2.json
@@ -0,0 +1,32 @@
+{
+    "status_code": 200,
+    "data": {
+        "NetworkInterfaces": [
+            {
+                "NetworkInterfaceId": "eni-0e1324ba169ed7b2f",
+                "TagSet": [
+                    {
+                        "Key": "Env",
+                        "Value": "Dev"
+                    },
+                    {
+                        "Key": "Origin",
+                        "Value": "Home"
+                    }
+                ]
+            }
+        ],
+        "ResponseMetadata": {
+            "RequestId": "07a97919-0661-404d-9c15-3edf26170ee9",
+            "HTTPStatusCode": 200,
+            "HTTPHeaders": {
+                "content-type": "text/xml;charset=UTF-8",
+                "content-length": "3249",
+                "vary": "accept-encoding",
+                "date": "Mon, 23 Sep 2019 14:28:44 GMT",
+                "server": "AmazonEC2"
+            },
+            "RetryAttempts": 0
+        }
+    }
+}
diff --git a/tests/test_tags.py b/tests/test_tags.py
--- a/tests/test_tags.py
+++ b/tests/test_tags.py
@@ -318,6 +318,36 @@ def test_copy_related_resource_tag_validate(self):
         }
         self.assertRaises(PolicyValidationError, self.load_policy, policy)
 
+    def test_copy_related_tag_empty(self):
+        # check the case where the related expression doesn't return
+        # value.
+        output = self.capture_logging('custodian.actions')
+        session_factory = self.replay_flight_data(
+            'test_copy_related_resource_tag_empty')
+        client = session_factory().client('ec2')
+        p = self.load_policy({
+            'name': 'copy-related-ec2',
+            'resource': 'aws.eni',
+            'actions': [{
+                'type': 'copy-related-tag',
+                'resource': 'ec2',
+                'skip_missing': True,
+                'key': 'Attachment.InstanceId',
+                'tags': '*'}]},
+            session_factory=session_factory)
+        p.run()
+        if self.recording:
+            time.sleep(3)
+        nics = client.describe_network_interfaces(
+            NetworkInterfaceIds=['eni-0e1324ba169ed7b2f'])['NetworkInterfaces']
+        self.assertEqual(
+            nics[0]['TagSet'],
+            [{'Key': 'Env', 'Value': 'Dev'},
+             {'Key': 'Origin', 'Value': 'Home'}])
+        self.assertEqual(
+            output.getvalue().strip(),
+            'Tagged 1 resources from related, missing-skipped 1 unchanged 0')
+
     def test_copy_related_resource_tag_multi_ref(self):
         session_factory = self.replay_flight_data('test_copy_related_resource_tag_multi_ref')
         client = session_factory().client('ec2')