Repository: cloud-custodian/cloud-custodian

Issue:
aws.ecs-task subnet filter
**Is your feature request related to a problem? Please describe.**
Want to lock all singleton tasks to a private subnet. 

**Describe the solution you'd like**
Would be nice if I could add a subnet filter for ecs-task. Although, it not very clearly mentioned in the docs, but the describe_tasks api returns back ENI details that also includes subnet id in it.
```
  "attachments": [
                {
                    "id": "a1b2c3d4-5678-90ab-cdef-33333EXAMPLE",
                    "type": "ElasticNetworkInterface",
                    "status": "ATTACHED",
                    "details": [
                        {
                            "name": "subnetId",
                            "value": "subnet-12344321"
                        },
                        {
                            "name": "networkInterfaceId",
                            "value": "eni-12344321"
                        },
                        {
                            "name": "macAddress",
                            "value": "0a:90:09:84:f9:14"
                        },
                        {
                            "name": "privateIPv4Address",
                            "value": "10.0.0.4"
                        }
                    ]
                }
            ],
```
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ecs.html#ECS.Client.describe_tasks

**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.

**Additional context**
Add any other context or screenshots about the feature request here.



Discussion:
sounds good

Resolving patch:
diff --git a/c7n/resources/ecs.py b/c7n/resources/ecs.py
--- a/c7n/resources/ecs.py
+++ b/c7n/resources/ecs.py
@@ -418,6 +418,12 @@ def get_resources(self, ids, cache=True, augment=True):
         return super(Task, self).get_resources(ids, cache, augment=False)
 
 
+@Task.filter_registry.register('subnet')
+class TaskSubnetFilter(net_filters.SubnetFilter):
+
+    RelatedIdsExpression = "attachments[].details[?name == 'subnetId'].value[]"
+
+
 @Task.filter_registry.register('task-definition')
 class TaskTaskDefinitionFilter(RelatedTaskDefinitionFilter):
     """Filter tasks by their task definition.


Test patch:
diff --git a/tests/data/placebo/test_ecs_task_subnet/ec2.DescribeSubnets_1.json b/tests/data/placebo/test_ecs_task_subnet/ec2.DescribeSubnets_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_ecs_task_subnet/ec2.DescribeSubnets_1.json
@@ -0,0 +1,56 @@
+{
+    "status_code": 200,
+    "data": {
+        "Subnets": [
+            {
+                "AvailabilityZone": "us-east-1b",
+                "AvailabilityZoneId": "use1-az6",
+                "AvailableIpAddressCount": 250,
+                "CidrBlock": "10.0.0.0/24",
+                "DefaultForAz": false,
+                "MapPublicIpOnLaunch": false,
+                "MapCustomerOwnedIpOnLaunch": false,
+                "State": "available",
+                "SubnetId": "subnet-05b58b4afe5124322",
+                "VpcId": "vpc-0a59f94e3fca9fbc0",
+                "OwnerId": "644160558196",
+                "AssignIpv6AddressOnCreation": false,
+                "Ipv6CidrBlockAssociationSet": [],
+                "Tags": [
+                    {
+                        "Key": "Name",
+                        "Value": "implied"
+                    }
+                ],
+                "SubnetArn": "arn:aws:ec2:us-east-1:644160558196:subnet/subnet-05b58b4afe5124322"
+            },
+            {
+                "AvailabilityZone": "us-east-1a",
+                "AvailabilityZoneId": "use1-az4",
+                "AvailableIpAddressCount": 4081,
+                "CidrBlock": "10.205.0.0/20",
+                "DefaultForAz": false,
+                "MapPublicIpOnLaunch": true,
+                "MapCustomerOwnedIpOnLaunch": false,
+                "State": "available",
+                "SubnetId": "subnet-07c118e47bb84cee7",
+                "VpcId": "vpc-03005fb9b8740263d",
+                "OwnerId": "644160558196",
+                "AssignIpv6AddressOnCreation": false,
+                "Ipv6CidrBlockAssociationSet": [],
+                "Tags": [
+                    {
+                        "Key": "NetworkLocation",
+                        "Value": "Public"
+                    },
+                    {
+                        "Key": "Name",
+                        "Value": "testing-subnet"
+                    }
+                ],
+                "SubnetArn": "arn:aws:ec2:us-east-1:644160558196:subnet/subnet-07c118e47bb84cee7"
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_ecs_task_subnet/ecs.DescribeTasks_1.json b/tests/data/placebo/test_ecs_task_subnet/ecs.DescribeTasks_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_ecs_task_subnet/ecs.DescribeTasks_1.json
@@ -0,0 +1,173 @@
+{
+    "status_code": 200,
+    "data": {
+        "tasks": [
+            {
+                "attachments": [
+                    {
+                        "id": "8808ac1b-278b-4763-8c5f-c6aaf3f10ebe",
+                        "type": "ElasticNetworkInterface",
+                        "status": "ATTACHING",
+                        "details": [
+                            {
+                                "name": "subnetId",
+                                "value": "subnet-07c118e47bb84cee7"
+                            },
+                            {
+                                "name": "networkInterfaceId",
+                                "value": "eni-0d77e756c403bc543"
+                            },
+                            {
+                                "name": "macAddress",
+                                "value": "0a:9e:9b:5e:43:8b"
+                            },
+                            {
+                                "name": "privateIPv4Address",
+                                "value": "10.205.2.28"
+                            }
+                        ]
+                    }
+                ],
+                "availabilityZone": "us-east-1a",
+                "capacityProviderName": "FARGATE_SPOT",
+                "clusterArn": "arn:aws:ecs:us-east-1:644160558196:cluster/c7n-fargate-test",
+                "containers": [
+                    {
+                        "containerArn": "arn:aws:ecs:us-east-1:644160558196:container/9868b138-b800-4c9a-89f9-bc35a80e041d",
+                        "taskArn": "arn:aws:ecs:us-east-1:644160558196:task/c7n-fargate-test/575e66d2c7814af6aab4d3ec8ab58dab",
+                        "name": "fargate-app-2",
+                        "image": "httpd:2.4",
+                        "lastStatus": "PENDING",
+                        "networkInterfaces": [
+                            {
+                                "attachmentId": "8808ac1b-278b-4763-8c5f-c6aaf3f10ebe",
+                                "privateIpv4Address": "10.205.2.28"
+                            }
+                        ],
+                        "healthStatus": "UNKNOWN",
+                        "cpu": "0"
+                    }
+                ],
+                "cpu": "256",
+                "createdAt": {
+                    "__class__": "datetime",
+                    "year": 2021,
+                    "month": 2,
+                    "day": 23,
+                    "hour": 3,
+                    "minute": 27,
+                    "second": 17,
+                    "microsecond": 901000
+                },
+                "desiredStatus": "RUNNING",
+                "group": "family:app-fargate-task",
+                "healthStatus": "UNKNOWN",
+                "lastStatus": "PROVISIONING",
+                "launchType": "FARGATE",
+                "memory": "512",
+                "overrides": {
+                    "containerOverrides": [
+                        {
+                            "name": "fargate-app-2"
+                        }
+                    ],
+                    "inferenceAcceleratorOverrides": []
+                },
+                "platformVersion": "1.3.0",
+                "tags": [
+                    {
+                        "key": "aws:ecs:clusterName",
+                        "value": "c7n-fargate-test"
+                    }
+                ],
+                "taskArn": "arn:aws:ecs:us-east-1:644160558196:task/c7n-fargate-test/575e66d2c7814af6aab4d3ec8ab58dab",
+                "taskDefinitionArn": "arn:aws:ecs:us-east-1:644160558196:task-definition/app-fargate-task:1",
+                "version": 1
+            },
+            {
+                "attachments": [
+                    {
+                        "id": "78c1846d-e6a5-4cd4-9149-89f56c35821f",
+                        "type": "ElasticNetworkInterface",
+                        "status": "ATTACHED",
+                        "details": [
+                            {
+                                "name": "subnetId",
+                                "value": "subnet-05b58b4afe5124322"
+                            },
+                            {
+                                "name": "networkInterfaceId",
+                                "value": "eni-0c3042130d331bd71"
+                            },
+                            {
+                                "name": "macAddress",
+                                "value": "0e:2e:e1:cc:ab:91"
+                            },
+                            {
+                                "name": "privateIPv4Address",
+                                "value": "10.0.0.57"
+                            }
+                        ]
+                    }
+                ],
+                "availabilityZone": "us-east-1b",
+                "capacityProviderName": "FARGATE_SPOT",
+                "clusterArn": "arn:aws:ecs:us-east-1:644160558196:cluster/c7n-fargate-test",
+                "containers": [
+                    {
+                        "containerArn": "arn:aws:ecs:us-east-1:644160558196:container/58f80ee5-f881-4cca-9f15-79f666ad1836",
+                        "taskArn": "arn:aws:ecs:us-east-1:644160558196:task/c7n-fargate-test/ab3791544fcc4bd2a501edb673f037f6",
+                        "name": "fargate-app",
+                        "image": "httpd:2.4",
+                        "lastStatus": "PENDING",
+                        "networkInterfaces": [
+                            {
+                                "attachmentId": "78c1846d-e6a5-4cd4-9149-89f56c35821f",
+                                "privateIpv4Address": "10.0.0.57"
+                            }
+                        ],
+                        "healthStatus": "UNKNOWN",
+                        "cpu": "0"
+                    }
+                ],
+                "cpu": "256",
+                "createdAt": {
+                    "__class__": "datetime",
+                    "year": 2021,
+                    "month": 2,
+                    "day": 23,
+                    "hour": 3,
+                    "minute": 27,
+                    "second": 3,
+                    "microsecond": 213000
+                },
+                "desiredStatus": "RUNNING",
+                "group": "family:webapp-fargate-task",
+                "healthStatus": "UNKNOWN",
+                "lastStatus": "PENDING",
+                "launchType": "FARGATE",
+                "memory": "512",
+                "overrides": {
+                    "containerOverrides": [
+                        {
+                            "name": "fargate-app"
+                        }
+                    ],
+                    "inferenceAcceleratorOverrides": []
+                },
+                "platformVersion": "1.3.0",
+                "tags": [
+                    {
+                        "key": "aws:ecs:clusterName",
+                        "value": "c7n-fargate-test"
+                    }
+                ],
+                "taskArn": "arn:aws:ecs:us-east-1:644160558196:task/c7n-fargate-test/ab3791544fcc4bd2a501edb673f037f6",
+                "taskDefinitionArn": "arn:aws:ecs:us-east-1:644160558196:task-definition/webapp-fargate-task:1",
+                "version": 2
+            }
+        ],
+        "failures": [],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_ecs_task_subnet/ecs.ListClusters_1.json b/tests/data/placebo/test_ecs_task_subnet/ecs.ListClusters_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_ecs_task_subnet/ecs.ListClusters_1.json
@@ -0,0 +1,12 @@
+{
+    "status_code": 200,
+    "data": {
+        "clusterArns": [
+            "arn:aws:ecs:us-east-1:644160558196:cluster/Default",
+            "arn:aws:ecs:us-east-1:644160558196:cluster/default-cluster",
+            "arn:aws:ecs:us-east-1:644160558196:cluster/c7n-fargate-test",
+            "arn:aws:ecs:us-east-1:644160558196:cluster/c7n-fargate-test-2"
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_ecs_task_subnet/ecs.ListTasks_1.json b/tests/data/placebo/test_ecs_task_subnet/ecs.ListTasks_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_ecs_task_subnet/ecs.ListTasks_1.json
@@ -0,0 +1,7 @@
+{
+    "status_code": 200,
+    "data": {
+        "taskArns": [],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_ecs_task_subnet/ecs.ListTasks_2.json b/tests/data/placebo/test_ecs_task_subnet/ecs.ListTasks_2.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_ecs_task_subnet/ecs.ListTasks_2.json
@@ -0,0 +1,7 @@
+{
+    "status_code": 200,
+    "data": {
+        "taskArns": [],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_ecs_task_subnet/ecs.ListTasks_3.json b/tests/data/placebo/test_ecs_task_subnet/ecs.ListTasks_3.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_ecs_task_subnet/ecs.ListTasks_3.json
@@ -0,0 +1,10 @@
+{
+    "status_code": 200,
+    "data": {
+        "taskArns": [
+            "arn:aws:ecs:us-east-1:644160558196:task/c7n-fargate-test/575e66d2c7814af6aab4d3ec8ab58dab",
+            "arn:aws:ecs:us-east-1:644160558196:task/c7n-fargate-test/ab3791544fcc4bd2a501edb673f037f6"
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_ecs_task_subnet/ecs.ListTasks_4.json b/tests/data/placebo/test_ecs_task_subnet/ecs.ListTasks_4.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_ecs_task_subnet/ecs.ListTasks_4.json
@@ -0,0 +1,7 @@
+{
+    "status_code": 200,
+    "data": {
+        "taskArns": [],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_ecs_task_subnet/ecs.ListTasks_5.json b/tests/data/placebo/test_ecs_task_subnet/ecs.ListTasks_5.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_ecs_task_subnet/ecs.ListTasks_5.json
@@ -0,0 +1,7 @@
+{
+    "status_code": 200,
+    "data": {
+        "taskArns": [],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_ecs_task_subnet/ecs.ListTasks_6.json b/tests/data/placebo/test_ecs_task_subnet/ecs.ListTasks_6.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_ecs_task_subnet/ecs.ListTasks_6.json
@@ -0,0 +1,7 @@
+{
+    "status_code": 200,
+    "data": {
+        "taskArns": [],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/test_ecs.py b/tests/test_ecs.py
--- a/tests/test_ecs.py
+++ b/tests/test_ecs.py
@@ -333,6 +333,27 @@ def test_task_resource(self):
         resources = p.run()
         self.assertEqual(len(resources), 4)
 
+    def test_ecs_task_subnet(self):
+        session_factory = self.replay_flight_data("test_ecs_task_subnet")
+        p = self.load_policy(
+            {
+                "name": "ecs-task-fargate-subnets",
+                "resource": "ecs-task",
+                "filters": [
+                    {
+                        "type": "subnet",
+                        "key": "tag:Name",
+                        "value": "implied"
+                    }
+                ],
+            },
+            session_factory=session_factory,
+        )
+        resources = p.run()
+        self.assertEqual(len(resources), 1)
+        self.assertEqual(resources[0].get('attachments')[0].get(
+            'details')[0].get('value'), "subnet-05b58b4afe5124322")
+
     def test_task_delete(self):
         session_factory = self.replay_flight_data("test_ecs_task_delete")
         p = self.load_policy(