Repository: cloud-custodian/cloud-custodian

Issue:
Expand aws.ecr to provide image tag filtering, lifecycle policy actions
Feature Request

ECR policy runs only return the following items:
```
  {
    "registryId": "ACCOUNT_ID", 
    "repositoryName": "MY_REPO_NAME", 
    "repositoryArn": "arn:aws:ecr:us-west-2:xxxxxxxxxxx:repository/MY_REPO_NAME", 
    "createdAt": "2017-11-22T12:14:58-07:00", 
    "repositoryUri": "xxxxxxxxxxx.dkr.ecr.us-west-2.amazonaws.com/MY_REPO_NAME"
  }
```

Currently it appears that one can only invoke a lambda function and delete lifecycle policy statements. For cleaning up ECRs in development accounts, it would be helpful to be able to do the following:
* filter on ECR tags
* Put lifecycle policies that expire images without tags


Discussion:
Prior to announcement yesterday https://aws.amazon.com/about-aws/whats-new/2018/12/amazon-ecr-now-allows-repository-tagging/ ECR repo's could not be tagged, but now that AWS supports it, support can be added to custodian.

Resolving patch:
diff --git a/c7n/resources/ecr.py b/c7n/resources/ecr.py
--- a/c7n/resources/ecr.py
+++ b/c7n/resources/ecr.py
@@ -13,14 +13,14 @@
 # limitations under the License.
 from __future__ import absolute_import, division, print_function, unicode_literals
 
-from botocore.exceptions import ClientError
-
 import json
+
+from c7n.actions import RemovePolicyBase, Action
 from c7n.exceptions import PolicyValidationError
 from c7n.filters import CrossAccountAccessFilter, Filter, ValueFilter
 from c7n.manager import resources
 from c7n.query import QueryResourceManager
-from c7n.actions import RemovePolicyBase, Action
+from c7n import tags
 from c7n.utils import local_session, type_schema
 
 
@@ -36,8 +36,59 @@ class resource_type(object):
         filter_name = 'repositoryNames'
         filter_type = 'list'
 
+    def augment(self, resources):
+        client = local_session(self.session_factory).client('ecr')
+        results = []
+        for r in resources:
+            try:
+                r['Tags'] = client.list_tags_for_resource(
+                    resourceArn=r['repositoryArn']).get('tags')
+                results.append(r)
+            except client.exceptions.RepositoryNotFoundException:
+                continue
+
+        return results
+
 
-ErrPolicyNotFound = 'RepositoryPolicyNotFoundException'
+@ECR.action_registry.register('tag')
+class ECRTag(tags.Tag):
+
+    permissions = ('ecr:TagResource',)
+
+    def process_resource_set(self, resources, tags):
+        client = local_session(
+            self.manager.session_factory).client('ecr')
+        for r in resources:
+            try:
+                client.tag_resource(resourceArn=r['repositoryArn'], tags=tags)
+            except client.exceptions.RepositoryNotFoundException:
+                pass
+
+
+@ECR.action_registry.register('remove-tag')
+class ECRRemoveTags(tags.RemoveTag):
+
+    permissions = ('ecr:UntagResource',)
+
+    def process_resource_set(self, resources, tags):
+        client = local_session(
+            self.manager.session_factory).client('ecr')
+        for r in resources:
+            try:
+                client.untag_resource(resourceArn=r['repositoryArn'], tagKeys=tags)
+            except client.exceptions.RepositoryNotFoundException:
+                pass
+
+
+@ECR.action_registry.register('mark-for-op')
+class ECRMarkForOp(tags.TagDelayedAction):
+
+    def process_resource_set(self, resources, tags):
+        tagger = self.manager.action_registry.get('tag')({}, self.manager)
+        tagger.process_resource_set(resources, tags)
+
+
+ECR.filter_registry.register('marked-for-op', tags.TagActionFilter)
 
 
 @ECR.filter_registry.register('cross-account')
@@ -61,19 +112,18 @@ class ECRCrossAccountAccessFilter(CrossAccountAccessFilter):
 
     def process(self, resources, event=None):
 
+        client = local_session(self.manager.session_factory).client('ecr')
+
         def _augment(r):
-            client = local_session(self.manager.session_factory).client('ecr')
             try:
                 r['Policy'] = client.get_repository_policy(
                     repositoryName=r['repositoryName'])['policyText']
-            except ClientError as e:
-                if e.response['Error']['Code'] == ErrPolicyNotFound:
-                    return None
-                raise
+            except client.exceptions.RepositoryPolicyNotFoundException:
+                return None
             return r
 
         self.log.debug("fetching policy for %d repos" % len(resources))
-        with self.executor_factory(max_workers=3) as w:
+        with self.executor_factory(max_workers=2) as w:
             resources = list(filter(None, w.map(_augment, resources)))
 
         return super(ECRCrossAccountAccessFilter, self).process(
@@ -234,7 +284,7 @@ def process(self, resources):
                         registryId=r['registryId'],
                         repositoryName=r['repositoryName'])
                     continue
-                except client.exceptions.RepositoryPolicyNotFoundException:
+                except client.exceptions.LifecyclePolicyNotFoundException:
                     pass
             client.put_lifecycle_policy(
                 registryId=r['registryId'],
@@ -278,13 +328,8 @@ def process_resource(self, client, resource):
             try:
                 resource['Policy'] = client.get_repository_policy(
                     repositoryName=resource['repositoryName'])['policyText']
-            except ClientError as e:
-                if e.response['Error']['Code'] != ErrPolicyNotFound:
-                    raise
-                resource['Policy'] = None
-
-        if not resource['Policy']:
-            return
+            except client.exceptions.RepositoryPolicyNotFoundException:
+                return
 
         p = json.loads(resource['Policy'])
         statements, found = self.process_policy(


Test patch:
diff --git a/tests/data/placebo/test_cross_account_ecr/api.ecr.ListTagsForResource_1.json b/tests/data/placebo/test_cross_account_ecr/api.ecr.ListTagsForResource_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_cross_account_ecr/api.ecr.ListTagsForResource_1.json
@@ -0,0 +1,16 @@
+{
+    "status_code": 200,
+    "data": {
+        "tags": [],
+        "ResponseMetadata": {
+            "RequestId": "a6f8e2dc-294d-11e9-ad8d-89f33dbd3d1f",
+            "HTTPStatusCode": 200,
+            "HTTPHeaders": {
+                "x-amzn-requestid": "a6f8e2dc-294d-11e9-ad8d-89f33dbd3d1f",
+                "content-type": "application/x-amz-json-1.1",
+                "content-length": "11"
+            },
+            "RetryAttempts": 0
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_ecr_lifecycle_delete/api.ecr.ListTagsForResource_1.json b/tests/data/placebo/test_ecr_lifecycle_delete/api.ecr.ListTagsForResource_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_ecr_lifecycle_delete/api.ecr.ListTagsForResource_1.json
@@ -0,0 +1,16 @@
+{
+    "status_code": 200,
+    "data": {
+        "tags": [],
+        "ResponseMetadata": {
+            "RequestId": "a6f8e2dc-294d-11e9-ad8d-89f33dbd3d1f",
+            "HTTPStatusCode": 200,
+            "HTTPHeaders": {
+                "x-amzn-requestid": "a6f8e2dc-294d-11e9-ad8d-89f33dbd3d1f",
+                "content-type": "application/x-amz-json-1.1",
+                "content-length": "11"
+            },
+            "RetryAttempts": 0
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_ecr_lifecycle_update/api.ecr.ListTagsForResource_1.json b/tests/data/placebo/test_ecr_lifecycle_update/api.ecr.ListTagsForResource_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_ecr_lifecycle_update/api.ecr.ListTagsForResource_1.json
@@ -0,0 +1,16 @@
+{
+    "status_code": 200,
+    "data": {
+        "tags": [],
+        "ResponseMetadata": {
+            "RequestId": "a6f8e2dc-294d-11e9-ad8d-89f33dbd3d1f",
+            "HTTPStatusCode": 200,
+            "HTTPHeaders": {
+                "x-amzn-requestid": "a6f8e2dc-294d-11e9-ad8d-89f33dbd3d1f",
+                "content-type": "application/x-amz-json-1.1",
+                "content-length": "11"
+            },
+            "RetryAttempts": 0
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_ecr_no_policy/api.ecr.CreateRepository_1.json b/tests/data/placebo/test_ecr_no_policy/api.ecr.CreateRepository_1.json
--- a/tests/data/placebo/test_ecr_no_policy/api.ecr.CreateRepository_1.json
+++ b/tests/data/placebo/test_ecr_no_policy/api.ecr.CreateRepository_1.json
@@ -1,34 +1,31 @@
 {
-    "status_code": 200, 
+    "status_code": 200,
     "data": {
-        "ResponseMetadata": {
-            "RetryAttempts": 0, 
-            "HTTPStatusCode": 200, 
-            "RequestId": "39335f1a-92ea-11e7-b0d7-07a0454bb90a", 
-            "HTTPHeaders": {
-                "x-amzn-requestid": "39335f1a-92ea-11e7-b0d7-07a0454bb90a", 
-                "content-length": "273", 
-                "server": "Server", 
-                "connection": "keep-alive", 
-                "date": "Wed, 06 Sep 2017 10:00:33 GMT", 
-                "content-type": "application/x-amz-json-1.1"
-            }
-        }, 
         "repository": {
-            "registryId": "644160558196", 
-            "repositoryName": "test-ecr-no-policy", 
-            "repositoryArn": "arn:aws:ecr:us-east-1:644160558196:repository/test-ecr-no-policy", 
+            "repositoryArn": "arn:aws:ecr:us-east-2:644160558196:repository/test-ecr-no-policy",
+            "registryId": "644160558196",
+            "repositoryName": "test-ecr-no-policy",
+            "repositoryUri": "644160558196.dkr.ecr.us-east-2.amazonaws.com/test-ecr-no-policy",
             "createdAt": {
-                "hour": 6, 
-                "__class__": "datetime", 
-                "month": 9, 
-                "second": 34, 
-                "microsecond": 0, 
-                "year": 2017, 
-                "day": 6, 
-                "minute": 0
-            }, 
-            "repositoryUri": "644160558196.dkr.ecr.us-east-1.amazonaws.com/test-ecr-no-policy"
+                "__class__": "datetime",
+                "year": 2019,
+                "month": 2,
+                "day": 5,
+                "hour": 8,
+                "minute": 55,
+                "second": 10,
+                "microsecond": 0
+            }
+        },
+        "ResponseMetadata": {
+            "RequestId": "a6d7c5ea-294d-11e9-8ed9-ff55339fda5b",
+            "HTTPStatusCode": 200,
+            "HTTPHeaders": {
+                "x-amzn-requestid": "a6d7c5ea-294d-11e9-8ed9-ff55339fda5b",
+                "content-type": "application/x-amz-json-1.1",
+                "content-length": "272"
+            },
+            "RetryAttempts": 0
         }
     }
 }
\ No newline at end of file
diff --git a/tests/data/placebo/test_ecr_no_policy/api.ecr.DeleteRepository_1.json b/tests/data/placebo/test_ecr_no_policy/api.ecr.DeleteRepository_1.json
--- a/tests/data/placebo/test_ecr_no_policy/api.ecr.DeleteRepository_1.json
+++ b/tests/data/placebo/test_ecr_no_policy/api.ecr.DeleteRepository_1.json
@@ -1,34 +1,31 @@
 {
-    "status_code": 200, 
+    "status_code": 200,
     "data": {
-        "ResponseMetadata": {
-            "RetryAttempts": 0, 
-            "HTTPStatusCode": 200, 
-            "RequestId": "399862ee-92ea-11e7-b0d7-07a0454bb90a", 
-            "HTTPHeaders": {
-                "x-amzn-requestid": "399862ee-92ea-11e7-b0d7-07a0454bb90a", 
-                "content-length": "273", 
-                "server": "Server", 
-                "connection": "keep-alive", 
-                "date": "Wed, 06 Sep 2017 10:00:34 GMT", 
-                "content-type": "application/x-amz-json-1.1"
-            }
-        }, 
         "repository": {
-            "registryId": "644160558196", 
-            "repositoryName": "test-ecr-no-policy", 
-            "repositoryArn": "arn:aws:ecr:us-east-1:644160558196:repository/test-ecr-no-policy", 
+            "repositoryArn": "arn:aws:ecr:us-east-2:644160558196:repository/test-ecr-no-policy",
+            "registryId": "644160558196",
+            "repositoryName": "test-ecr-no-policy",
+            "repositoryUri": "644160558196.dkr.ecr.us-east-2.amazonaws.com/test-ecr-no-policy",
             "createdAt": {
-                "hour": 6, 
-                "__class__": "datetime", 
-                "month": 9, 
-                "second": 34, 
-                "microsecond": 0, 
-                "year": 2017, 
-                "day": 6, 
-                "minute": 0
-            }, 
-            "repositoryUri": "644160558196.dkr.ecr.us-east-1.amazonaws.com/test-ecr-no-policy"
+                "__class__": "datetime",
+                "year": 2019,
+                "month": 2,
+                "day": 5,
+                "hour": 8,
+                "minute": 55,
+                "second": 10,
+                "microsecond": 0
+            }
+        },
+        "ResponseMetadata": {
+            "RequestId": "a717670e-294d-11e9-8ed9-ff55339fda5b",
+            "HTTPStatusCode": 200,
+            "HTTPHeaders": {
+                "x-amzn-requestid": "a717670e-294d-11e9-8ed9-ff55339fda5b",
+                "content-type": "application/x-amz-json-1.1",
+                "content-length": "272"
+            },
+            "RetryAttempts": 0
         }
     }
 }
\ No newline at end of file
diff --git a/tests/data/placebo/test_ecr_no_policy/api.ecr.DescribeRepositories_1.json b/tests/data/placebo/test_ecr_no_policy/api.ecr.DescribeRepositories_1.json
--- a/tests/data/placebo/test_ecr_no_policy/api.ecr.DescribeRepositories_1.json
+++ b/tests/data/placebo/test_ecr_no_policy/api.ecr.DescribeRepositories_1.json
@@ -1,36 +1,16 @@
 {
-    "status_code": 200, 
+    "status_code": 200,
     "data": {
-        "repositories": [
-            {
-                "registryId": "644160558196", 
-                "repositoryName": "test-ecr-no-policy", 
-                "repositoryArn": "arn:aws:ecr:us-east-1:644160558196:repository/test-ecr-no-policy", 
-                "createdAt": {
-                    "hour": 6, 
-                    "__class__": "datetime", 
-                    "month": 9, 
-                    "second": 34, 
-                    "microsecond": 0, 
-                    "year": 2017, 
-                    "day": 6, 
-                    "minute": 0
-                }, 
-                "repositoryUri": "644160558196.dkr.ecr.us-east-1.amazonaws.com/test-ecr-no-policy"
-            }
-        ], 
+        "repositories": [],
         "ResponseMetadata": {
-            "RetryAttempts": 0, 
-            "HTTPStatusCode": 200, 
-            "RequestId": "395eb4eb-92ea-11e7-b23d-9f149cf136f1", 
+            "RequestId": "a6d1d276-294d-11e9-8ed9-ff55339fda5b",
+            "HTTPStatusCode": 200,
             "HTTPHeaders": {
-                "x-amzn-requestid": "395eb4eb-92ea-11e7-b23d-9f149cf136f1", 
-                "content-length": "277", 
-                "server": "Server", 
-                "connection": "keep-alive", 
-                "date": "Wed, 06 Sep 2017 10:00:34 GMT", 
-                "content-type": "application/x-amz-json-1.1"
-            }
+                "x-amzn-requestid": "a6d1d276-294d-11e9-8ed9-ff55339fda5b",
+                "content-type": "application/x-amz-json-1.1",
+                "content-length": "19"
+            },
+            "RetryAttempts": 0
         }
     }
 }
\ No newline at end of file
diff --git a/tests/data/placebo/test_ecr_no_policy/api.ecr.DescribeRepositories_2.json b/tests/data/placebo/test_ecr_no_policy/api.ecr.DescribeRepositories_2.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_ecr_no_policy/api.ecr.DescribeRepositories_2.json
@@ -0,0 +1,33 @@
+{
+    "status_code": 200,
+    "data": {
+        "repositories": [
+            {
+                "repositoryArn": "arn:aws:ecr:us-east-2:644160558196:repository/test-ecr-no-policy",
+                "registryId": "644160558196",
+                "repositoryName": "test-ecr-no-policy",
+                "repositoryUri": "644160558196.dkr.ecr.us-east-2.amazonaws.com/test-ecr-no-policy",
+                "createdAt": {
+                    "__class__": "datetime",
+                    "year": 2019,
+                    "month": 2,
+                    "day": 5,
+                    "hour": 8,
+                    "minute": 55,
+                    "second": 10,
+                    "microsecond": 0
+                }
+            }
+        ],
+        "ResponseMetadata": {
+            "RequestId": "a6e9a099-294d-11e9-88be-91bb45a60940",
+            "HTTPStatusCode": 200,
+            "HTTPHeaders": {
+                "x-amzn-requestid": "a6e9a099-294d-11e9-88be-91bb45a60940",
+                "content-type": "application/x-amz-json-1.1",
+                "content-length": "276"
+            },
+            "RetryAttempts": 0
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_ecr_no_policy/api.ecr.GetRepositoryPolicy_1.json b/tests/data/placebo/test_ecr_no_policy/api.ecr.GetRepositoryPolicy_1.json
--- a/tests/data/placebo/test_ecr_no_policy/api.ecr.GetRepositoryPolicy_1.json
+++ b/tests/data/placebo/test_ecr_no_policy/api.ecr.GetRepositoryPolicy_1.json
@@ -1,22 +1,19 @@
 {
-    "status_code": 400, 
+    "status_code": 400,
     "data": {
-        "ResponseMetadata": {
-            "RetryAttempts": 0, 
-            "HTTPStatusCode": 400, 
-            "RequestId": "398d66c3-92ea-11e7-b052-2fc633e9ba9a", 
-            "HTTPHeaders": {
-                "x-amzn-requestid": "398d66c3-92ea-11e7-b052-2fc633e9ba9a", 
-                "content-length": "180", 
-                "server": "Server", 
-                "connection": "keep-alive", 
-                "date": "Wed, 06 Sep 2017 10:00:34 GMT", 
-                "content-type": "application/x-amz-json-1.1"
-            }
-        }, 
         "Error": {
-            "Message": "Repository policy does not exist for the repository with name 'test-ecr-no-policy' in the registry with id '644160558196'", 
+            "Message": "Repository policy does not exist for the repository with name 'test-ecr-no-policy' in the registry with id '644160558196'",
             "Code": "RepositoryPolicyNotFoundException"
+        },
+        "ResponseMetadata": {
+            "RequestId": "a710b08c-294d-11e9-8d46-2793fca2ff40",
+            "HTTPStatusCode": 400,
+            "HTTPHeaders": {
+                "x-amzn-requestid": "a710b08c-294d-11e9-8d46-2793fca2ff40",
+                "content-type": "application/x-amz-json-1.1",
+                "content-length": "180"
+            },
+            "RetryAttempts": 0
         }
     }
 }
\ No newline at end of file
diff --git a/tests/data/placebo/test_ecr_no_policy/api.ecr.ListTagsForResource_1.json b/tests/data/placebo/test_ecr_no_policy/api.ecr.ListTagsForResource_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_ecr_no_policy/api.ecr.ListTagsForResource_1.json
@@ -0,0 +1,16 @@
+{
+    "status_code": 200,
+    "data": {
+        "tags": [],
+        "ResponseMetadata": {
+            "RequestId": "a6f8e2dc-294d-11e9-ad8d-89f33dbd3d1f",
+            "HTTPStatusCode": 200,
+            "HTTPHeaders": {
+                "x-amzn-requestid": "a6f8e2dc-294d-11e9-ad8d-89f33dbd3d1f",
+                "content-type": "application/x-amz-json-1.1",
+                "content-length": "11"
+            },
+            "RetryAttempts": 0
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_ecr_remove_matched/api.ecr.ListTagsForResource_1.json b/tests/data/placebo/test_ecr_remove_matched/api.ecr.ListTagsForResource_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_ecr_remove_matched/api.ecr.ListTagsForResource_1.json
@@ -0,0 +1,16 @@
+{
+    "status_code": 200,
+    "data": {
+        "tags": [],
+        "ResponseMetadata": {
+            "RequestId": "a6f8e2dc-294d-11e9-ad8d-89f33dbd3d1f",
+            "HTTPStatusCode": 200,
+            "HTTPHeaders": {
+                "x-amzn-requestid": "a6f8e2dc-294d-11e9-ad8d-89f33dbd3d1f",
+                "content-type": "application/x-amz-json-1.1",
+                "content-length": "11"
+            },
+            "RetryAttempts": 0
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_ecr_remove_named/api.ecr.ListTagsForResource_1.json b/tests/data/placebo/test_ecr_remove_named/api.ecr.ListTagsForResource_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_ecr_remove_named/api.ecr.ListTagsForResource_1.json
@@ -0,0 +1,16 @@
+{
+    "status_code": 200,
+    "data": {
+        "tags": [],
+        "ResponseMetadata": {
+            "RequestId": "a6f8e2dc-294d-11e9-ad8d-89f33dbd3d1f",
+            "HTTPStatusCode": 200,
+            "HTTPHeaders": {
+                "x-amzn-requestid": "a6f8e2dc-294d-11e9-ad8d-89f33dbd3d1f",
+                "content-type": "application/x-amz-json-1.1",
+                "content-length": "11"
+            },
+            "RetryAttempts": 0
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_ecr_tags/api.ecr.DescribeRepositories_1.json b/tests/data/placebo/test_ecr_tags/api.ecr.DescribeRepositories_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_ecr_tags/api.ecr.DescribeRepositories_1.json
@@ -0,0 +1,33 @@
+{
+    "status_code": 200,
+    "data": {
+        "repositories": [
+            {
+                "repositoryArn": "arn:aws:ecr:us-east-2:644160558196:repository/devx",
+                "registryId": "644160558196",
+                "repositoryName": "devx",
+                "repositoryUri": "644160558196.dkr.ecr.us-east-2.amazonaws.com/devx",
+                "createdAt": {
+                    "__class__": "datetime",
+                    "year": 2019,
+                    "month": 2,
+                    "day": 5,
+                    "hour": 9,
+                    "minute": 24,
+                    "second": 17,
+                    "microsecond": 0
+                }
+            }
+        ],
+        "ResponseMetadata": {
+            "RequestId": "7b7e6a5a-2952-11e9-9ee0-a7e2f85d3534",
+            "HTTPStatusCode": 200,
+            "HTTPHeaders": {
+                "x-amzn-requestid": "7b7e6a5a-2952-11e9-9ee0-a7e2f85d3534",
+                "content-type": "application/x-amz-json-1.1",
+                "content-length": "235"
+            },
+            "RetryAttempts": 0
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_ecr_tags/api.ecr.ListTagsForResource_1.json b/tests/data/placebo/test_ecr_tags/api.ecr.ListTagsForResource_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_ecr_tags/api.ecr.ListTagsForResource_1.json
@@ -0,0 +1,21 @@
+{
+    "status_code": 200,
+    "data": {
+        "tags": [
+            {
+                "Key": "Role",
+                "Value": "Dev"
+            }
+        ],
+        "ResponseMetadata": {
+            "RequestId": "7b8d8533-2952-11e9-a30d-13279ea71d7d",
+            "HTTPStatusCode": 200,
+            "HTTPHeaders": {
+                "x-amzn-requestid": "7b8d8533-2952-11e9-a30d-13279ea71d7d",
+                "content-type": "application/x-amz-json-1.1",
+                "content-length": "39"
+            },
+            "RetryAttempts": 0
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_ecr_tags/api.ecr.ListTagsForResource_2.json b/tests/data/placebo/test_ecr_tags/api.ecr.ListTagsForResource_2.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_ecr_tags/api.ecr.ListTagsForResource_2.json
@@ -0,0 +1,25 @@
+{
+    "status_code": 200,
+    "data": {
+        "tags": [
+            {
+                "Key": "Env",
+                "Value": "Dev"
+            },
+            {
+                "Key": "maid_status",
+                "Value": "Resource does not meet policy: post-finding@2019/02/07"
+            }
+        ],
+        "ResponseMetadata": {
+            "RequestId": "7f9a8238-2952-11e9-a478-83077bbee338",
+            "HTTPStatusCode": 200,
+            "HTTPHeaders": {
+                "x-amzn-requestid": "7f9a8238-2952-11e9-a478-83077bbee338",
+                "content-type": "application/x-amz-json-1.1",
+                "content-length": "125"
+            },
+            "RetryAttempts": 0
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_ecr_tags/api.ecr.TagResource_1.json b/tests/data/placebo/test_ecr_tags/api.ecr.TagResource_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_ecr_tags/api.ecr.TagResource_1.json
@@ -0,0 +1,15 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {
+            "RequestId": "7b9dd8ed-2952-11e9-a30d-13279ea71d7d",
+            "HTTPStatusCode": 200,
+            "HTTPHeaders": {
+                "x-amzn-requestid": "7b9dd8ed-2952-11e9-a30d-13279ea71d7d",
+                "content-type": "application/x-amz-json-1.1",
+                "content-length": "2"
+            },
+            "RetryAttempts": 0
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_ecr_tags/api.ecr.TagResource_2.json b/tests/data/placebo/test_ecr_tags/api.ecr.TagResource_2.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_ecr_tags/api.ecr.TagResource_2.json
@@ -0,0 +1,15 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {
+            "RequestId": "7e893961-2952-11e9-acf7-e3e4c3b27a3e",
+            "HTTPStatusCode": 200,
+            "HTTPHeaders": {
+                "x-amzn-requestid": "7e893961-2952-11e9-acf7-e3e4c3b27a3e",
+                "content-type": "application/x-amz-json-1.1",
+                "content-length": "2"
+            },
+            "RetryAttempts": 0
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_ecr_tags/api.ecr.UntagResource_1.json b/tests/data/placebo/test_ecr_tags/api.ecr.UntagResource_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_ecr_tags/api.ecr.UntagResource_1.json
@@ -0,0 +1,15 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {
+            "RequestId": "7bacf3e1-2952-11e9-acf7-e3e4c3b27a3e",
+            "HTTPStatusCode": 200,
+            "HTTPHeaders": {
+                "x-amzn-requestid": "7bacf3e1-2952-11e9-acf7-e3e4c3b27a3e",
+                "content-type": "application/x-amz-json-1.1",
+                "content-length": "2"
+            },
+            "RetryAttempts": 0
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/test_ecr.py b/tests/test_ecr.py
--- a/tests/test_ecr.py
+++ b/tests/test_ecr.py
@@ -91,6 +91,33 @@ def test_ecr_lifecycle_delete(self):
             client.get_lifecycle_policy,
             repositoryName='c7n')
 
+    def test_ecr_tags(self):
+        factory = self.replay_flight_data('test_ecr_tags')
+        p = self.load_policy({
+            'name': 'ecr-tag',
+            'resource': 'ecr',
+            'filters': [{'tag:Role': 'Dev'}],
+            'actions': [
+                {'type': 'tag',
+                 'tags': {'Env': 'Dev'}},
+                {'type': 'remove-tag',
+                 'tags': ['Role']},
+                {'type': 'mark-for-op',
+                 'op': 'post-finding',
+                 'days': 2}]},
+            session_factory=factory)
+        resources = p.run()
+        self.assertEqual(len(resources), 1)
+
+        client = factory().client('ecr')
+        tags = {t['Key']: t['Value'] for t in
+                client.list_tags_for_resource(
+                    resourceArn=resources[0]['repositoryArn']).get('tags')}
+        self.assertEqual(
+            tags,
+            {'Env': 'Dev',
+             'maid_status': 'Resource does not meet policy: post-finding@2019/02/07'})
+
     @functional
     def test_ecr_no_policy(self):
         # running against a registry with no policy causes no issues.