Repository: cloud-custodian/cloud-custodian

Issue:
Add support for CodeDeploy
**Is your feature request related to a problem? Please describe.**
Yes, we would like to make sure all our deployments using CodeDeploy are going through whitelisted set of S3 buckets

**Describe the solution you'd like**
Add support of CodeDeploy and use ValueFilter to enforce usage of whitelisted buckets

**Describe alternatives you've considered**
NA

**Additional context**
NA


Discussion:
PratMis: Thanks for the feedback @thisisshi. Currently we're trying to only restrict to using AppSec in conjunction with CodeDeploy but yes, you're right re: whitelisting of S3 buckets we would need a Related Resource filter. Happy to add that in the next PR. I have addressed your comments.
PratMis: @kapilt I went ahead and added a `codedeploy-deploymentgroup` resource. Interestingly, deployment group inherits `targetRevision` from the most recent deployment. I was able to trigger two back to back deployments with different targetRevisions and found only the latest revision to be stored. I think we may still need a `codedeploy-deployment` resource to monitor deployments in cloudtrail mode. Please let me know your thoughts!! Thanks
PratMis: I went ahead and removed `stop` action on deployments. Kept the resource itself since it would be beneficial to get to know about a deployment that didn't come from a whitelisted sources. Deployment Groups inherit `targetRevision` from deployments but only the latest revision. Having a CloudTrail mode policy on deployments would instantly notify about any non-compliance. 
kapilt: last consideration was just on resource naming, if we want to try and simplify application -> app  .. and deploymentgroup -> group.. thoughts?
PratMis: > last consideration was just on resource naming, if we want to try and simplify application -> app .. and deploymentgroup -> group.. thoughts?

Yep, I can make that change

Patch:
diff --git a/c7n/resources/code.py b/c7n/resources/code.py
index 02330c40b..a7086d170 100644
--- a/c7n/resources/code.py
+++ b/c7n/resources/code.py
@@ -6,9 +6,11 @@ import jmespath
 from c7n.actions import BaseAction
 from c7n.filters.vpc import SubnetFilter, SecurityGroupFilter, VpcFilter
 from c7n.manager import resources
-from c7n.query import QueryResourceManager, DescribeSource, ConfigSource, TypeInfo
+from c7n.query import (
+    QueryResourceManager, DescribeSource, ConfigSource, TypeInfo, ChildResourceManager)
 from c7n.tags import universal_augment
 from c7n.utils import local_session, type_schema
+from c7n import query
 
 from .securityhub import OtherResourcePostFinding
 
@@ -265,3 +267,132 @@ class DeletePipeline(BaseAction):
                 self.manager.retry(client.delete_pipeline, name=r['name'])
             except client.exceptions.PipelineNotFoundException:
                 continue
+
+
+@resources.register('codedeploy-app')
+class CodeDeployApplication(QueryResourceManager):
+
+    class resource_type(TypeInfo):
+        service = 'codedeploy'
+        enum_spec = ('list_applications', 'applications', None)
+        batch_detail_spec = (
+            'batch_get_applications', 'applicationNames',
+            None, 'applicationsInfo', None)
+        id = 'applicationId'
+        name = 'applicationName'
+        date = 'createTime'
+        arn_type = "application"
+        arn_separator = ":"
+        cfn_type = "AWS::CodeDeploy::Application"
+        universal_taggable = True
+
+    def augment(self, resources):
+        resources = super().augment(resources)
+        client = local_session(self.session_factory).client('codedeploy')
+        for r, arn in zip(resources, self.get_arns(resources)):
+            r['Tags'] = client.list_tags_for_resource(
+                ResourceArn=arn).get('Tags', [])
+        return resources
+
+    def get_arns(self, resources):
+        return [self.generate_arn(r['applicationName']) for r in resources]
+
+
+@CodeDeployApplication.action_registry.register('delete')
+class DeleteApplication(BaseAction):
+
+    schema = type_schema('delete')
+    permissions = ('codedeploy:DeleteApplication',)
+
+    def process(self, resources):
+        client = local_session(self.manager.session_factory).client('codedeploy')
+        for r in resources:
+            try:
+                self.manager.retry(client.delete_application, applicationName=r['applicationName'])
+            except (client.exceptions.InvalidApplicationNameException,
+            client.exceptions.ApplicationDoesNotExistException):
+                continue
+
+
+@resources.register('codedeploy-deployment')
+class CodeDeployDeployment(QueryResourceManager):
+
+    class resource_type(TypeInfo):
+        service = 'codedeploy'
+        enum_spec = ('list_deployments', 'deployments', {'includeOnlyStatuses': [
+            'Created', 'Queued', 'InProgress', 'Baking', 'Ready']})
+        batch_detail_spec = (
+            'batch_get_deployments', 'deploymentIds',
+            None, 'deploymentsInfo', None)
+        name = id = 'deploymentId'
+        # couldn't find a real cloudformation type
+        cfn_type = None
+        arn_type = "deploymentgroup"
+        date = 'createTime'
+
+
+class DescribeDeploymentGroup(query.ChildDescribeSource):
+
+    def get_query(self):
+        query = super().get_query()
+        query.capture_parent_id = True
+        return query
+
+    def augment(self, resources):
+        client = local_session(self.manager.session_factory).client('codedeploy')
+        results = []
+        for parent_id, group_name in resources:
+            dg = self.manager.retry(
+                client.get_deployment_group, applicationName=parent_id,
+                deploymentGroupName=group_name).get('deploymentGroupInfo')
+            results.append(dg)
+        for r in results:
+            rarn = self.manager.generate_arn(r['applicationName'] + '/' + r['deploymentGroupName'])
+            r['Tags'] = self.manager.retry(
+                client.list_tags_for_resource, ResourceArn=rarn).get('Tags')
+        return results
+
+
+@resources.register('codedeploy-group')
+class CodeDeployDeploymentGroup(ChildResourceManager):
+
+    class resource_type(TypeInfo):
+        service = 'codedeploy'
+        parent_spec = ('codedeploy-app', 'applicationName', None)
+        enum_spec = ('list_deployment_groups', 'deploymentGroups', None)
+        id = 'deploymentGroupId'
+        name = 'deploymentGroupName'
+        arn_type = "deploymentgroup"
+        cfn_type = 'AWS::CodeDeploy::DeploymentGroup'
+        arn_separator = ':'
+        permission_prefix = 'codedeploy'
+        universal_taggable = True
+
+    source_mapping = {
+        'describe-child': DescribeDeploymentGroup
+    }
+
+    def get_arns(self, resources):
+        arns = []
+        for r in resources:
+            arns.append(self.generate_arn(r['applicationName'] + '/' + r['deploymentGroupName']))
+        return arns
+
+
+@CodeDeployDeploymentGroup.action_registry.register('delete')
+class DeleteDeploymentGroup(BaseAction):
+    """Delete a deployment group tied to an application.
+    """
+
+    schema = type_schema('delete')
+    permissions = ('codedeploy:DeleteDeploymentGroup',)
+
+    def process(self, resources):
+        client = local_session(self.manager.session_factory).client('codedeploy')
+        for r in resources:
+            try:
+                self.manager.retry(client.delete_deployment_group,
+                      applicationName=r['applicationName'],
+                      deploymentGroupName=r['deploymentGroupName'])
+            except client.exceptions.InvalidDeploymentGroupNameException:
+                continue
diff --git a/c7n/resources/resource_map.py b/c7n/resources/resource_map.py
index 1a26f574c..f87358ee2 100644
--- a/c7n/resources/resource_map.py
+++ b/c7n/resources/resource_map.py
@@ -28,6 +28,9 @@ ResourceMap = {
     "aws.artifact-repo": "c7n.resources.artifact.ArtifactRepo",
     "aws.codebuild": "c7n.resources.code.CodeBuildProject",
     "aws.codecommit": "c7n.resources.code.CodeRepository",
+    "aws.codedeploy-app": "c7n.resources.code.CodeDeployApplication",
+    "aws.codedeploy-deployment": "c7n.resources.code.CodeDeployDeployment",
+    "aws.codedeploy-group": "c7n.resources.code.CodeDeployDeploymentGroup",
     "aws.codepipeline": "c7n.resources.code.CodeDeployPipeline",
     "aws.config-recorder": "c7n.resources.config.ConfigRecorder",
     "aws.config-rule": "c7n.resources.config.ConfigRule",


Test patch:
diff --git a/tests/data/placebo/test_codedeploy_deploymentgroup_delete/codedeploy.DeleteDeploymentGroup_1.json b/tests/data/placebo/test_codedeploy_deploymentgroup_delete/codedeploy.DeleteDeploymentGroup_1.json
new file mode 100644
index 000000000..9a55dbd99
--- /dev/null
+++ b/tests/data/placebo/test_codedeploy_deploymentgroup_delete/codedeploy.DeleteDeploymentGroup_1.json
@@ -0,0 +1,7 @@
+{
+    "status_code": 200,
+    "data": {
+        "hooksNotCleanedUp": [],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_codedeploy_deploymentgroup_delete/codedeploy.GetDeploymentGroup_1.json b/tests/data/placebo/test_codedeploy_deploymentgroup_delete/codedeploy.GetDeploymentGroup_1.json
new file mode 100644
index 000000000..ee439cb18
--- /dev/null
+++ b/tests/data/placebo/test_codedeploy_deploymentgroup_delete/codedeploy.GetDeploymentGroup_1.json
@@ -0,0 +1,84 @@
+{
+    "status_code": 200,
+    "data": {
+        "deploymentGroupInfo": {
+            "applicationName": "c7n-abc",
+            "deploymentGroupId": "d247ae32-3a87-4bbb-8aef-1986aca70a1e",
+            "deploymentGroupName": "World",
+            "deploymentConfigName": "CodeDeployDefault.LambdaAllAtOnce",
+            "ec2TagFilters": [],
+            "onPremisesInstanceTagFilters": [],
+            "autoScalingGroups": [],
+            "serviceRoleArn": "arn:aws:iam::644160558196:role/custodian-codebuild",
+            "targetRevision": {
+                "revisionType": "S3",
+                "s3Location": {
+                    "bucket": "reboot-s3-bad-01",
+                    "key": "appspec.yml",
+                    "bundleType": "YAML"
+                }
+            },
+            "triggerConfigurations": [],
+            "alarmConfiguration": {
+                "enabled": false,
+                "ignorePollAlarmFailure": false,
+                "alarms": []
+            },
+            "deploymentStyle": {
+                "deploymentType": "BLUE_GREEN",
+                "deploymentOption": "WITH_TRAFFIC_CONTROL"
+            },
+            "outdatedInstancesStrategy": "UPDATE",
+            "lastSuccessfulDeployment": {
+                "deploymentId": "d-Y7J3BCY5C",
+                "status": "Succeeded",
+                "endTime": {
+                    "__class__": "datetime",
+                    "year": 2021,
+                    "month": 7,
+                    "day": 22,
+                    "hour": 1,
+                    "minute": 41,
+                    "second": 3,
+                    "microsecond": 69000
+                },
+                "createTime": {
+                    "__class__": "datetime",
+                    "year": 2021,
+                    "month": 7,
+                    "day": 22,
+                    "hour": 1,
+                    "minute": 40,
+                    "second": 59,
+                    "microsecond": 647000
+                }
+            },
+            "lastAttemptedDeployment": {
+                "deploymentId": "d-Y7J3BCY5C",
+                "status": "Succeeded",
+                "endTime": {
+                    "__class__": "datetime",
+                    "year": 2021,
+                    "month": 7,
+                    "day": 22,
+                    "hour": 1,
+                    "minute": 41,
+                    "second": 3,
+                    "microsecond": 69000
+                },
+                "createTime": {
+                    "__class__": "datetime",
+                    "year": 2021,
+                    "month": 7,
+                    "day": 22,
+                    "hour": 1,
+                    "minute": 40,
+                    "second": 59,
+                    "microsecond": 647000
+                }
+            },
+            "computePlatform": "Lambda"
+        },
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_codedeploy_deploymentgroup_delete/codedeploy.GetDeploymentGroup_2.json b/tests/data/placebo/test_codedeploy_deploymentgroup_delete/codedeploy.GetDeploymentGroup_2.json
new file mode 100644
index 000000000..a8dabc1b3
--- /dev/null
+++ b/tests/data/placebo/test_codedeploy_deploymentgroup_delete/codedeploy.GetDeploymentGroup_2.json
@@ -0,0 +1,28 @@
+{
+    "status_code": 200,
+    "data": {
+        "deploymentGroupInfo": {
+            "applicationName": "test",
+            "deploymentGroupId": "653b64f1-6194-4dfa-96b7-58634abeff3c",
+            "deploymentGroupName": "test",
+            "deploymentConfigName": "CodeDeployDefault.LambdaAllAtOnce",
+            "ec2TagFilters": [],
+            "onPremisesInstanceTagFilters": [],
+            "autoScalingGroups": [],
+            "serviceRoleArn": "arn:aws:iam::644160558196:role/custodian-codebuild",
+            "triggerConfigurations": [],
+            "alarmConfiguration": {
+                "enabled": false,
+                "ignorePollAlarmFailure": false,
+                "alarms": []
+            },
+            "deploymentStyle": {
+                "deploymentType": "BLUE_GREEN",
+                "deploymentOption": "WITH_TRAFFIC_CONTROL"
+            },
+            "outdatedInstancesStrategy": "UPDATE",
+            "computePlatform": "Lambda"
+        },
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_codedeploy_deploymentgroup_delete/codedeploy.GetDeploymentGroup_3.json b/tests/data/placebo/test_codedeploy_deploymentgroup_delete/codedeploy.GetDeploymentGroup_3.json
new file mode 100644
index 000000000..c89dda394
--- /dev/null
+++ b/tests/data/placebo/test_codedeploy_deploymentgroup_delete/codedeploy.GetDeploymentGroup_3.json
@@ -0,0 +1,10 @@
+{
+    "status_code": 400,
+    "data": {
+        "Error": {
+            "Message": "No Deployment Group found for name: World",
+            "Code": "DeploymentGroupDoesNotExistException"
+        },
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_codedeploy_deploymentgroup_delete/codedeploy.ListApplications_1.json b/tests/data/placebo/test_codedeploy_deploymentgroup_delete/codedeploy.ListApplications_1.json
new file mode 100644
index 000000000..724d15410
--- /dev/null
+++ b/tests/data/placebo/test_codedeploy_deploymentgroup_delete/codedeploy.ListApplications_1.json
@@ -0,0 +1,10 @@
+{
+    "status_code": 200,
+    "data": {
+        "applications": [
+            "c7n-abc",
+            "test"
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_codedeploy_deploymentgroup_delete/codedeploy.ListDeploymentGroups_1.json b/tests/data/placebo/test_codedeploy_deploymentgroup_delete/codedeploy.ListDeploymentGroups_1.json
new file mode 100644
index 000000000..8380b6f5b
--- /dev/null
+++ b/tests/data/placebo/test_codedeploy_deploymentgroup_delete/codedeploy.ListDeploymentGroups_1.json
@@ -0,0 +1,10 @@
+{
+    "status_code": 200,
+    "data": {
+        "applicationName": "c7n-abc",
+        "deploymentGroups": [
+            "World"
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_codedeploy_deploymentgroup_delete/codedeploy.ListDeploymentGroups_2.json b/tests/data/placebo/test_codedeploy_deploymentgroup_delete/codedeploy.ListDeploymentGroups_2.json
new file mode 100644
index 000000000..aacb26791
--- /dev/null
+++ b/tests/data/placebo/test_codedeploy_deploymentgroup_delete/codedeploy.ListDeploymentGroups_2.json
@@ -0,0 +1,10 @@
+{
+    "status_code": 200,
+    "data": {
+        "applicationName": "test",
+        "deploymentGroups": [
+            "test"
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_codedeploy_deploymentgroup_delete/codedeploy.ListTagsForResource_1.json b/tests/data/placebo/test_codedeploy_deploymentgroup_delete/codedeploy.ListTagsForResource_1.json
new file mode 100644
index 000000000..e41b16b3e
--- /dev/null
+++ b/tests/data/placebo/test_codedeploy_deploymentgroup_delete/codedeploy.ListTagsForResource_1.json
@@ -0,0 +1,7 @@
+{
+    "status_code": 200,
+    "data": {
+        "Tags": [],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_codedeploy_deploymentgroup_delete/codedeploy.ListTagsForResource_2.json b/tests/data/placebo/test_codedeploy_deploymentgroup_delete/codedeploy.ListTagsForResource_2.json
new file mode 100644
index 000000000..e41b16b3e
--- /dev/null
+++ b/tests/data/placebo/test_codedeploy_deploymentgroup_delete/codedeploy.ListTagsForResource_2.json
@@ -0,0 +1,7 @@
+{
+    "status_code": 200,
+    "data": {
+        "Tags": [],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_delete_codedeploy_application/codedeploy.BatchGetApplications_1.json b/tests/data/placebo/test_delete_codedeploy_application/codedeploy.BatchGetApplications_1.json
new file mode 100644
index 000000000..70f527c2f
--- /dev/null
+++ b/tests/data/placebo/test_delete_codedeploy_application/codedeploy.BatchGetApplications_1.json
@@ -0,0 +1,24 @@
+{
+    "status_code": 200,
+    "data": {
+        "applicationsInfo": [
+            {
+                "applicationId": "38b7c7b7-2fbd-46a9-a7ef-8233d81a462f",
+                "applicationName": "HelloWorld",
+                "createTime": {
+                    "__class__": "datetime",
+                    "year": 2021,
+                    "month": 7,
+                    "day": 12,
+                    "hour": 16,
+                    "minute": 37,
+                    "second": 50,
+                    "microsecond": 963000
+                },
+                "linkedToGitHub": false,
+                "computePlatform": "Lambda"
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_delete_codedeploy_application/codedeploy.DeleteApplication_1.json b/tests/data/placebo/test_delete_codedeploy_application/codedeploy.DeleteApplication_1.json
new file mode 100644
index 000000000..5b2170a07
--- /dev/null
+++ b/tests/data/placebo/test_delete_codedeploy_application/codedeploy.DeleteApplication_1.json
@@ -0,0 +1,6 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_delete_codedeploy_application/codedeploy.ListApplications_1.json b/tests/data/placebo/test_delete_codedeploy_application/codedeploy.ListApplications_1.json
new file mode 100644
index 000000000..d6b1eb409
--- /dev/null
+++ b/tests/data/placebo/test_delete_codedeploy_application/codedeploy.ListApplications_1.json
@@ -0,0 +1,9 @@
+{
+    "status_code": 200,
+    "data": {
+        "applications": [
+            "HelloWorld"
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_delete_codedeploy_application/codedeploy.ListApplications_2.json b/tests/data/placebo/test_delete_codedeploy_application/codedeploy.ListApplications_2.json
new file mode 100644
index 000000000..90211ee78
--- /dev/null
+++ b/tests/data/placebo/test_delete_codedeploy_application/codedeploy.ListApplications_2.json
@@ -0,0 +1,7 @@
+{
+    "status_code": 200,
+    "data": {
+        "applications": [],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_delete_codedeploy_application/codedeploy.ListTagsForResource_1.json b/tests/data/placebo/test_delete_codedeploy_application/codedeploy.ListTagsForResource_1.json
new file mode 100644
index 000000000..4b22ba616
--- /dev/null
+++ b/tests/data/placebo/test_delete_codedeploy_application/codedeploy.ListTagsForResource_1.json
@@ -0,0 +1,12 @@
+{
+    "status_code": 200,
+    "data": {
+        "Tags": [
+            {
+                "Key": "c7n",
+                "Value": "test"
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_tag_untag_codedeploy_application/codedeploy.BatchGetApplications_1.json b/tests/data/placebo/test_tag_untag_codedeploy_application/codedeploy.BatchGetApplications_1.json
new file mode 100644
index 000000000..0f3fd63bd
--- /dev/null
+++ b/tests/data/placebo/test_tag_untag_codedeploy_application/codedeploy.BatchGetApplications_1.json
@@ -0,0 +1,40 @@
+{
+    "status_code": 200,
+    "data": {
+        "applicationsInfo": [
+            {
+                "applicationId": "38b7c7b7-2fbd-46a9-a7ef-8233d81a462f",
+                "applicationName": "HelloWorld",
+                "createTime": {
+                    "__class__": "datetime",
+                    "year": 2021,
+                    "month": 7,
+                    "day": 12,
+                    "hour": 16,
+                    "minute": 37,
+                    "second": 50,
+                    "microsecond": 963000
+                },
+                "linkedToGitHub": false,
+                "computePlatform": "Lambda"
+            },
+            {
+                "applicationId": "be5890f2-5437-409b-9633-4fc105fcb922",
+                "applicationName": "cloud-custodian",
+                "createTime": {
+                    "__class__": "datetime",
+                    "year": 2021,
+                    "month": 7,
+                    "day": 19,
+                    "hour": 10,
+                    "minute": 42,
+                    "second": 29,
+                    "microsecond": 520000
+                },
+                "linkedToGitHub": false,
+                "computePlatform": "Lambda"
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_tag_untag_codedeploy_application/codedeploy.ListApplications_1.json b/tests/data/placebo/test_tag_untag_codedeploy_application/codedeploy.ListApplications_1.json
new file mode 100644
index 000000000..2c6ff70fb
--- /dev/null
+++ b/tests/data/placebo/test_tag_untag_codedeploy_application/codedeploy.ListApplications_1.json
@@ -0,0 +1,10 @@
+{
+    "status_code": 200,
+    "data": {
+        "applications": [
+            "HelloWorld",
+            "cloud-custodian"
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_tag_untag_codedeploy_application/codedeploy.ListTagsForResource_1.json b/tests/data/placebo/test_tag_untag_codedeploy_application/codedeploy.ListTagsForResource_1.json
new file mode 100644
index 000000000..4b22ba616
--- /dev/null
+++ b/tests/data/placebo/test_tag_untag_codedeploy_application/codedeploy.ListTagsForResource_1.json
@@ -0,0 +1,12 @@
+{
+    "status_code": 200,
+    "data": {
+        "Tags": [
+            {
+                "Key": "c7n",
+                "Value": "test"
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_tag_untag_codedeploy_application/codedeploy.ListTagsForResource_2.json b/tests/data/placebo/test_tag_untag_codedeploy_application/codedeploy.ListTagsForResource_2.json
new file mode 100644
index 000000000..e41b16b3e
--- /dev/null
+++ b/tests/data/placebo/test_tag_untag_codedeploy_application/codedeploy.ListTagsForResource_2.json
@@ -0,0 +1,7 @@
+{
+    "status_code": 200,
+    "data": {
+        "Tags": [],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_tag_untag_codedeploy_application/codedeploy.ListTagsForResource_3.json b/tests/data/placebo/test_tag_untag_codedeploy_application/codedeploy.ListTagsForResource_3.json
new file mode 100644
index 000000000..e41b16b3e
--- /dev/null
+++ b/tests/data/placebo/test_tag_untag_codedeploy_application/codedeploy.ListTagsForResource_3.json
@@ -0,0 +1,7 @@
+{
+    "status_code": 200,
+    "data": {
+        "Tags": [],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_tag_untag_codedeploy_application/tagging.UntagResources_1.json b/tests/data/placebo/test_tag_untag_codedeploy_application/tagging.UntagResources_1.json
new file mode 100644
index 000000000..bd2dead85
--- /dev/null
+++ b/tests/data/placebo/test_tag_untag_codedeploy_application/tagging.UntagResources_1.json
@@ -0,0 +1,7 @@
+{
+    "status_code": 200,
+    "data": {
+        "FailedResourcesMap": {},
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/test_code.py b/tests/test_code.py
index f1820e0d4..863fca2ed 100644
--- a/tests/test_code.py
+++ b/tests/test_code.py
@@ -4,6 +4,7 @@ import time
 from .common import BaseTest, event_data
 
 from c7n.resources.aws import shape_validate
+from botocore.exceptions import ClientError
 
 
 class CodeArtifact(BaseTest):
@@ -215,3 +216,64 @@ class CodePipeline(BaseTest):
         if self.recording:
             time.sleep(2)
         self.assertFalse(client.list_pipelines().get('pipelines'))
+
+
+class CodeDeploy(BaseTest):
+
+    def test_delete_codedeploy_application(self):
+        factory = self.replay_flight_data('test_delete_codedeploy_application')
+        p = self.load_policy(
+            {
+                "name": "codedeploy-delete-application",
+                "resource": "codedeploy-app",
+                "filters": [{"linkedToGitHub": False}],
+                "actions": ["delete"],
+            },
+            session_factory=factory)
+        resources = p.run()
+        self.assertEqual(len(resources), 1)
+        client = factory().client('codedeploy')
+        if self.recording:
+            time.sleep(2)
+        self.assertFalse(client.list_applications().get('applications'))
+
+    def test_tag_untag_codedeploy_application(self):
+        factory = self.replay_flight_data('test_tag_untag_codedeploy_application')
+        p = self.load_policy(
+            {
+                "name": "codedeploy-tag-application",
+                "resource": "codedeploy-app",
+                "filters": [{"tag:c7n": "test"}],
+                "actions": [{"type": "remove-tag", "tags": ["c7n"]}],
+            },
+            session_factory=factory)
+        resources = p.run()
+        self.assertEqual(len(resources), 1)
+        client = factory().client('codedeploy')
+        arn = p.resource_manager.generate_arn(resources[0]["applicationName"])
+        self.assertEqual(len(client.list_tags_for_resource(ResourceArn=arn).get('Tags')), 0)
+
+    def test_codedeploy_deploymentgroup_delete(self):
+        factory = self.replay_flight_data('test_codedeploy_deploymentgroup_delete')
+        p = self.load_policy(
+            {
+                "name": "codedeploy-delete-deploymentgroup",
+                "resource": "codedeploy-group",
+                "filters": [
+                    {
+                        "type": "value",
+                        "key": "targetRevision.revisionType",
+                        "value": "S3"
+                    }
+                ],
+                "actions": [{"type": "delete"}],
+            },
+            session_factory=factory)
+        resources = p.run()
+        self.assertEqual(len(resources), 1)
+        client = factory().client('codedeploy')
+        with self.assertRaises(ClientError) as e:
+            client.get_deployment_group(applicationName=resources[0]['applicationName'],
+            deploymentGroupName=resources[0]['deploymentGroupName'])
+        self.assertEqual(
+            e.exception.response['Error']['Code'], 'DeploymentGroupDoesNotExistException')
diff --git a/tests/test_policy.py b/tests/test_policy.py
index 6119d4c70..b65dc6faa 100644
--- a/tests/test_policy.py
+++ b/tests/test_policy.py
@@ -188,7 +188,7 @@ class PolicyMetaLint(BaseTest):
 
         overrides = overrides.difference(
             {'account', 's3', 'hostedzone', 'log-group', 'rest-api', 'redshift-snapshot',
-             'rest-stage'})
+             'rest-stage', 'codedeploy-app', 'codedeploy-group'})
         if overrides:
             raise ValueError("unknown arn overrides in %s" % (", ".join(overrides)))
 
@@ -378,7 +378,7 @@ class PolicyMetaLint(BaseTest):
     def test_resource_type_empty_metadata(self):
         empty = set()
         for k, v in manager.resources.items():
-            if k in ('rest-account', 'account'):
+            if k in ('rest-account', 'account', 'codedeploy-deployment'):
                 continue
             for rk, rv in v.resource_type.__dict__.items():
                 if rk[0].isalnum() and rv is None: