Repository: cloud-custodian/cloud-custodian

Issue:
Delete an ECS task definition
### Describe the feature

AWS added a new [api](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ecs/client/delete_task_definitions.html) that allows deletion of an ECS task definition. As per the [docs](https://aws.amazon.com/about-aws/whats-new/2023/02/amazon-ecs-deletion-inactive-task-definition-revisions/), the task definitions that are in INACTIVE state are the ones that can be deleted. 

By default the list_task_definitions api call only returns task definitions in the ACTIVE status.
```
status (string) -- The task definition status to filter the ListTaskDefinitions results with. By default, only ACTIVE task definitions are listed. By setting this parameter to INACTIVE , you can view task definitions that are INACTIVE as long as an active task or service still references them. If you paginate the resulting output, be sure to keep the status value constant in each subsequent request.
```

Design proposal: We can potentially write a delete action that can only work in conjunction with the deregister action. That way we get to delete task definitions the moment they get deregistered. There’s some risk with this approach: Currently a task definition in INACTIVE state can be switched back to ACTIVE state if there’s a need. When the delete action follows the deregister action, we won’t have the capability to get a td back. Also, this approach won’t help with the cleanup of task definitions currently sitting in INACTIVE state since they won’t be visible to custodian

### Extra information or context

_No response_


Discussion:

Resolving patch:
diff --git a/c7n/resources/ecs.py b/c7n/resources/ecs.py
--- a/c7n/resources/ecs.py
+++ b/c7n/resources/ecs.py
@@ -594,25 +594,56 @@ class DeleteTaskDefinition(BaseAction):
     The definition will be marked as InActive. Currently running
     services and task can still reference, new services & tasks
     can't.
+    
+    force is False by default. When given as True, the task definition will 
+    be permanently deleted.
+    
+    .. code-block:: yaml
+
+       policies:
+         - name: deregister-task-definition
+           resource: ecs-task-definition
+           filters:
+             - family: test-task-def
+           actions:
+             - type: delete
+
+         - name: delete-task-definition
+           resource: ecs-task-definition
+           filters:
+             - family: test-task-def
+           actions:
+             - type: delete
+               force: True
     """
 
-    schema = type_schema('delete')
-    permissions = ('ecs:DeregisterTaskDefinition',)
+    schema = type_schema('delete', force={'type': 'boolean'})
+    permissions = ('ecs:DeregisterTaskDefinition','ecs:DeleteTaskDefinitions',)
 
     def process(self, resources):
         client = local_session(self.manager.session_factory).client('ecs')
         retry = get_retry(('Throttling',))
+        force = self.data.get('force', False)
 
         for r in resources:
+            if r['status'] == 'INACTIVE':
+                continue
             try:
                 retry(client.deregister_task_definition,
                       taskDefinition=r['taskDefinitionArn'])
             except ClientError as e:
-                # No error code for not found.
                 if e.response['Error'][
-                        'Message'] != 'The specified task definition does not exist.':
+                    'Message'] != 'The specified task definition does not exist.':
                     raise
 
+        if force:
+            task_definitions_arns = [
+                r['taskDefinitionArn']
+                for r in resources
+            ]
+            for chunk in chunks(task_definitions_arns, size=10):
+                retry(client.delete_task_definitions, taskDefinitions=chunk)
+
 
 @resources.register('ecs-container-instance')
 class ContainerInstance(query.ChildResourceManager):


Test patch:
diff --git a/tests/data/iam-actions.json b/tests/data/iam-actions.json
--- a/tests/data/iam-actions.json
+++ b/tests/data/iam-actions.json
@@ -3773,6 +3773,7 @@
     "DeleteCapacityProvider",
     "DeleteCluster",
     "DeleteService",
+    "DeleteTaskDefinitions",
     "DeleteTaskSet",
     "DeregisterContainerInstance",
     "DeregisterTaskDefinition",
@@ -3785,11 +3786,13 @@
     "DescribeTasks",
     "DiscoverPollEndpoint",
     "ExecuteCommand",
+    "GetTaskProtection",
     "ListAccountSettings",
     "ListAttributes",
     "ListClusters",
     "ListContainerInstances",
     "ListServices",
+    "ListServicesByNamespace",
     "ListTagsForResource",
     "ListTaskDefinitionFamilies",
     "ListTaskDefinitions",
@@ -3817,6 +3820,7 @@
     "UpdateContainerInstancesState",
     "UpdateService",
     "UpdateServicePrimaryTaskSet",
+    "UpdateTaskProtection",
     "UpdateTaskSet"
   ],
   "eks": [
diff --git a/tests/data/placebo/test_ecs_task_def_delete_permanently/ecs.DeleteTaskDefinitions_1.json b/tests/data/placebo/test_ecs_task_def_delete_permanently/ecs.DeleteTaskDefinitions_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_ecs_task_def_delete_permanently/ecs.DeleteTaskDefinitions_1.json
@@ -0,0 +1,109 @@
+{
+    "status_code": 200,
+    "data": {
+        "taskDefinitions": [
+            {
+                "taskDefinitionArn": "arn:aws:ecs:us-east-1:644160558196:task-definition/test-delete-definition:2",
+                "containerDefinitions": [
+                    {
+                        "name": "testing",
+                        "image": "644160558196.dkr.ecr.us-east-1.amazonaws.com/docker-on-aws/nodejs:latest",
+                        "cpu": 0,
+                        "portMappings": [
+                            {
+                                "containerPort": 80,
+                                "hostPort": 80,
+                                "protocol": "tcp",
+                                "name": "testing-80-tcp",
+                                "appProtocol": "http"
+                            }
+                        ],
+                        "essential": true,
+                        "environment": [],
+                        "environmentFiles": [],
+                        "mountPoints": [],
+                        "volumesFrom": [],
+                        "logConfiguration": {
+                            "logDriver": "awslogs",
+                            "options": {
+                                "awslogs-create-group": "true",
+                                "awslogs-group": "/ecs/test-delete-definition",
+                                "awslogs-region": "us-east-1",
+                                "awslogs-stream-prefix": "ecs"
+                            }
+                        }
+                    }
+                ],
+                "family": "test-delete-definition",
+                "executionRoleArn": "arn:aws:iam::644160558196:role/ecsTaskExecutionRole",
+                "networkMode": "awsvpc",
+                "revision": 2,
+                "volumes": [],
+                "status": "DELETE_IN_PROGRESS",
+                "requiresAttributes": [
+                    {
+                        "name": "com.amazonaws.ecs.capability.logging-driver.awslogs"
+                    },
+                    {
+                        "name": "ecs.capability.execution-role-awslogs"
+                    },
+                    {
+                        "name": "com.amazonaws.ecs.capability.ecr-auth"
+                    },
+                    {
+                        "name": "com.amazonaws.ecs.capability.docker-remote-api.1.19"
+                    },
+                    {
+                        "name": "ecs.capability.execution-role-ecr-pull"
+                    },
+                    {
+                        "name": "com.amazonaws.ecs.capability.docker-remote-api.1.18"
+                    },
+                    {
+                        "name": "ecs.capability.task-eni"
+                    },
+                    {
+                        "name": "com.amazonaws.ecs.capability.docker-remote-api.1.29"
+                    }
+                ],
+                "placementConstraints": [],
+                "compatibilities": [
+                    "EC2",
+                    "FARGATE"
+                ],
+                "runtimePlatform": {
+                    "cpuArchitecture": "X86_64",
+                    "operatingSystemFamily": "LINUX"
+                },
+                "requiresCompatibilities": [
+                    "FARGATE"
+                ],
+                "cpu": "1024",
+                "memory": "3072",
+                "registeredAt": {
+                    "__class__": "datetime",
+                    "year": 2023,
+                    "month": 3,
+                    "day": 27,
+                    "hour": 15,
+                    "minute": 30,
+                    "second": 9,
+                    "microsecond": 622000
+                },
+                "deregisteredAt": {
+                    "__class__": "datetime",
+                    "year": 2023,
+                    "month": 3,
+                    "day": 27,
+                    "hour": 15,
+                    "minute": 32,
+                    "second": 11,
+                    "microsecond": 769000
+                },
+                "registeredBy": "arn:aws:iam::644160558196:user/timmy"
+            }
+        ],
+        "failures": [],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_ecs_task_def_delete_permanently/ecs.DeregisterTaskDefinition_1.json b/tests/data/placebo/test_ecs_task_def_delete_permanently/ecs.DeregisterTaskDefinition_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_ecs_task_def_delete_permanently/ecs.DeregisterTaskDefinition_1.json
@@ -0,0 +1,96 @@
+{
+    "status_code": 200,
+    "data": {
+        "taskDefinition": {
+            "taskDefinitionArn": "arn:aws:ecs:us-east-1:644160558196:task-definition/test-delete-definition:2",
+            "containerDefinitions": [
+                {
+                    "name": "testing",
+                    "image": "644160558196.dkr.ecr.us-east-1.amazonaws.com/docker-on-aws/nodejs:latest",
+                    "cpu": 0,
+                    "portMappings": [
+                        {
+                            "containerPort": 80,
+                            "hostPort": 80,
+                            "protocol": "tcp",
+                            "name": "testing-80-tcp",
+                            "appProtocol": "http"
+                        }
+                    ],
+                    "essential": true,
+                    "environment": [],
+                    "environmentFiles": [],
+                    "mountPoints": [],
+                    "volumesFrom": [],
+                    "logConfiguration": {
+                        "logDriver": "awslogs",
+                        "options": {
+                            "awslogs-create-group": "true",
+                            "awslogs-group": "/ecs/test-delete-definition",
+                            "awslogs-region": "us-east-1",
+                            "awslogs-stream-prefix": "ecs"
+                        }
+                    }
+                }
+            ],
+            "family": "test-delete-definition",
+            "executionRoleArn": "arn:aws:iam::644160558196:role/ecsTaskExecutionRole",
+            "networkMode": "awsvpc",
+            "revision": 2,
+            "volumes": [],
+            "status": "INACTIVE",
+            "requiresAttributes": [
+                {
+                    "name": "com.amazonaws.ecs.capability.logging-driver.awslogs"
+                },
+                {
+                    "name": "ecs.capability.execution-role-awslogs"
+                },
+                {
+                    "name": "com.amazonaws.ecs.capability.ecr-auth"
+                },
+                {
+                    "name": "com.amazonaws.ecs.capability.docker-remote-api.1.19"
+                },
+                {
+                    "name": "ecs.capability.execution-role-ecr-pull"
+                },
+                {
+                    "name": "com.amazonaws.ecs.capability.docker-remote-api.1.18"
+                },
+                {
+                    "name": "ecs.capability.task-eni"
+                },
+                {
+                    "name": "com.amazonaws.ecs.capability.docker-remote-api.1.29"
+                }
+            ],
+            "placementConstraints": [],
+            "compatibilities": [
+                "EC2",
+                "FARGATE"
+            ],
+            "runtimePlatform": {
+                "cpuArchitecture": "X86_64",
+                "operatingSystemFamily": "LINUX"
+            },
+            "requiresCompatibilities": [
+                "FARGATE"
+            ],
+            "cpu": "1024",
+            "memory": "3072",
+            "registeredAt": {
+                "__class__": "datetime",
+                "year": 2023,
+                "month": 3,
+                "day": 27,
+                "hour": 15,
+                "minute": 30,
+                "second": 9,
+                "microsecond": 622000
+            },
+            "registeredBy": "arn:aws:iam::644160558196:user/timmy"
+        },
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_ecs_task_def_delete_permanently/ecs.DescribeTaskDefinition_1.json b/tests/data/placebo/test_ecs_task_def_delete_permanently/ecs.DescribeTaskDefinition_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_ecs_task_def_delete_permanently/ecs.DescribeTaskDefinition_1.json
@@ -0,0 +1,97 @@
+{
+    "status_code": 200,
+    "data": {
+        "taskDefinition": {
+            "taskDefinitionArn": "arn:aws:ecs:us-east-1:644160558196:task-definition/test-delete-definition:2",
+            "containerDefinitions": [
+                {
+                    "name": "testing",
+                    "image": "644160558196.dkr.ecr.us-east-1.amazonaws.com/docker-on-aws/nodejs:latest",
+                    "cpu": 0,
+                    "portMappings": [
+                        {
+                            "containerPort": 80,
+                            "hostPort": 80,
+                            "protocol": "tcp",
+                            "name": "testing-80-tcp",
+                            "appProtocol": "http"
+                        }
+                    ],
+                    "essential": true,
+                    "environment": [],
+                    "environmentFiles": [],
+                    "mountPoints": [],
+                    "volumesFrom": [],
+                    "logConfiguration": {
+                        "logDriver": "awslogs",
+                        "options": {
+                            "awslogs-create-group": "true",
+                            "awslogs-group": "/ecs/test-delete-definition",
+                            "awslogs-region": "us-east-1",
+                            "awslogs-stream-prefix": "ecs"
+                        }
+                    }
+                }
+            ],
+            "family": "test-delete-definition",
+            "executionRoleArn": "arn:aws:iam::644160558196:role/ecsTaskExecutionRole",
+            "networkMode": "awsvpc",
+            "revision": 2,
+            "volumes": [],
+            "status": "ACTIVE",
+            "requiresAttributes": [
+                {
+                    "name": "com.amazonaws.ecs.capability.logging-driver.awslogs"
+                },
+                {
+                    "name": "ecs.capability.execution-role-awslogs"
+                },
+                {
+                    "name": "com.amazonaws.ecs.capability.ecr-auth"
+                },
+                {
+                    "name": "com.amazonaws.ecs.capability.docker-remote-api.1.19"
+                },
+                {
+                    "name": "ecs.capability.execution-role-ecr-pull"
+                },
+                {
+                    "name": "com.amazonaws.ecs.capability.docker-remote-api.1.18"
+                },
+                {
+                    "name": "ecs.capability.task-eni"
+                },
+                {
+                    "name": "com.amazonaws.ecs.capability.docker-remote-api.1.29"
+                }
+            ],
+            "placementConstraints": [],
+            "compatibilities": [
+                "EC2",
+                "FARGATE"
+            ],
+            "runtimePlatform": {
+                "cpuArchitecture": "X86_64",
+                "operatingSystemFamily": "LINUX"
+            },
+            "requiresCompatibilities": [
+                "FARGATE"
+            ],
+            "cpu": "1024",
+            "memory": "3072",
+            "registeredAt": {
+                "__class__": "datetime",
+                "year": 2023,
+                "month": 3,
+                "day": 27,
+                "hour": 15,
+                "minute": 30,
+                "second": 9,
+                "microsecond": 622000
+            },
+            "registeredBy": "arn:aws:iam::644160558196:user/timmy"
+        },
+        "tags": [],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_ecs_task_def_delete_permanently/ecs.ListTaskDefinitions_1.json b/tests/data/placebo/test_ecs_task_def_delete_permanently/ecs.ListTaskDefinitions_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_ecs_task_def_delete_permanently/ecs.ListTaskDefinitions_1.json
@@ -0,0 +1,9 @@
+{
+    "status_code": 200,
+    "data": {
+        "taskDefinitionArns": [
+            "arn:aws:ecs:us-east-1:644160558196:task-definition/test-delete-definition:2"
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_ecs_task_def_delete_permanently/ecs.ListTaskDefinitions_2.json b/tests/data/placebo/test_ecs_task_def_delete_permanently/ecs.ListTaskDefinitions_2.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_ecs_task_def_delete_permanently/ecs.ListTaskDefinitions_2.json
@@ -0,0 +1,9 @@
+{
+    "status_code": 200,
+    "data": {
+        "taskDefinitionArns": [
+            "arn:aws:ecs:us-east-1:644160558196:task-definition/test-delete-definition:2"
+        ],
+        "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
@@ -372,6 +372,28 @@ def test_task_definition_delete(self):
             "taskDefinitionArns"
         )
         self.assertEqual(arns, [])
+        
+    def test_task_definition_delete_permanently(self):
+        session_factory = self.replay_flight_data("test_ecs_task_def_delete_permanently")
+        p = self.load_policy(
+            {
+                "name": "task-defs",
+                "resource": "ecs-task-definition",
+                "filters": [{"family": "test-delete-definition"}],
+                "actions": [{"type": "delete", "force": True}],
+            },
+            session_factory=session_factory,
+        )
+        resources = p.run()
+        self.assertEqual(len(resources), 1)
+        arns = session_factory().client("ecs").list_task_definitions(
+            familyPrefix="test-delete-definition", status="DELETE_IN_PROGRESS"
+        ).get(
+            "taskDefinitionArns"
+        )
+        self.assertEqual(arns, 
+                         ["arn:aws:ecs:us-east-1:644160558196:task-definition/test-delete-definition:2"])
+        
 
     def test_task_definition_get_resources(self):
         session_factory = self.replay_flight_data("test_ecs_task_def_query")