Repository: cloud-custodian/cloud-custodian

Issue:
AWS RDS Snapshot - Add Filter that associates snapshots with active instances
### Describe the feature

Users want to be able to delete only the RDS Snapshots that are associated with an active RDS Instance. If there are snapshots for a deleted RDS instance, they want those snapshots to stay around as they are the final backups of that database. A new RDS Snapshot filter would be needed to check the database's attribute such as DBInstanceStatus status.

Here is what I imagine the policy looking like:

```
- name: aws-rds-snapshot-delete-active
  resource: aws.rds-snapshot
  filters:
     - type: instance-attributes
        key: DBInstanceStatus
        value: "available"
     - type: value
       key: SnapshotCreateTime
       value_type: age
       op: greater-than
       value: 30
  actions:
    - delete
```

### Extra information or context

_No response_


Discussion:

Resolving patch:
diff --git a/c7n/filters/related.py b/c7n/filters/related.py
--- a/c7n/filters/related.py
+++ b/c7n/filters/related.py
@@ -75,17 +75,18 @@ def process_resource(self, resource, related):
         for rid in related_ids:
             robj = related.get(rid, None)
             if robj is None:
-                self.log.warning(
-                    "Resource %s:%s references non existant %s: %s",
-                    self.manager.type,
-                    resource[model.id],
-                    self.RelatedResource.rsplit('.', 1)[-1],
-                    rid)
                 # in the event that the filter is looking specifically for absent values, we can
                 # safely assume that the non-existant related resource will have an absent value at
                 # any given key
                 if self.data['value'] == 'absent':
                     found.append(rid)
+                else:
+                    self.log.warning(
+                        "Resource %s:%s references non existant %s: %s",
+                        self.manager.type,
+                        resource[model.id],
+                        self.RelatedResource.rsplit('.', 1)[-1],
+                        rid)
                 continue
             if self.match(robj):
                 found.append(rid)
diff --git a/c7n/resources/rds.py b/c7n/resources/rds.py
--- a/c7n/resources/rds.py
+++ b/c7n/resources/rds.py
@@ -53,6 +53,7 @@
 from c7n.filters import (
     CrossAccountAccessFilter, FilterRegistry, Filter, ValueFilter, AgeFilter)
 from c7n.filters.offhours import OffHour, OnHour
+from c7n.filters import related
 import c7n.filters.vpc as net_filters
 from c7n.manager import resources
 from c7n.query import (
@@ -1061,6 +1062,33 @@ class RDSSnapshotOnHour(OnHour):
     """Scheduled action on rds snapshot."""
 
 
+@RDSSnapshot.filter_registry.register('instance')
+class SnapshotInstance(related.RelatedResourceFilter):
+    """Filter snapshots by their database attributes.
+
+    :example:
+
+      Find snapshots without an extant database
+
+    .. code-block:: yaml
+
+       policies:
+         - name: rds-snapshot-orphan
+           resource: aws.rds-snapshot
+           filters:
+            - type: instance
+              value: 0
+              value_type: resource_count
+    """
+    schema = type_schema(
+        'instance', rinherit=ValueFilter.schema
+    )
+
+    RelatedResource = "c7n.resources.rds.RDS"
+    RelatedIdsExpression = "DBInstanceIdentifier"
+    FetchThreshold = 5
+
+
 @RDSSnapshot.filter_registry.register('latest')
 class LatestSnapshot(Filter):
     """Return the latest snapshot for each database.


Test patch:
diff --git a/tests/data/placebo/test_rds_snapshot_instance/rds.DescribeDBInstances_1.json b/tests/data/placebo/test_rds_snapshot_instance/rds.DescribeDBInstances_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_rds_snapshot_instance/rds.DescribeDBInstances_1.json
@@ -0,0 +1,137 @@
+{
+    "status_code": 200,
+    "data": {
+        "DBInstances": [
+            {
+                "DBInstanceIdentifier": "kapilt-testx-testx",
+                "DBInstanceClass": "db.m6g.large",
+                "Engine": "postgres",
+                "DBInstanceStatus": "available",
+                "MasterUsername": "testx",
+                "DBName": "testx",
+                "Endpoint": {
+                    "Address": "kapilt-testx-testx.cnj6v9152dmz.us-east-1.rds.amazonaws.com",
+                    "Port": 5432,
+                    "HostedZoneId": "Z2R2ITUGPM61AM"
+                },
+                "AllocatedStorage": 30,
+                "InstanceCreateTime": {
+                    "__class__": "datetime",
+                    "year": 2023,
+                    "month": 5,
+                    "day": 17,
+                    "hour": 18,
+                    "minute": 26,
+                    "second": 31,
+                    "microsecond": 598000
+                },
+                "PreferredBackupWindow": "09:52-10:22",
+                "BackupRetentionPeriod": 0,
+                "DBSecurityGroups": [],
+                "VpcSecurityGroups": [
+                    {
+                        "VpcSecurityGroupId": "sg-05943f46e0435babd",
+                        "Status": "active"
+                    }
+                ],
+                "DBParameterGroups": [
+                    {
+                        "DBParameterGroupName": "default.postgres14",
+                        "ParameterApplyStatus": "in-sync"
+                    }
+                ],
+                "AvailabilityZone": "us-east-1d",
+                "DBSubnetGroup": {
+                    "DBSubnetGroupName": "kapilt-testx-db",
+                    "DBSubnetGroupDescription": "Managed by Terraform",
+                    "VpcId": "vpc-029654b93830acd5a",
+                    "SubnetGroupStatus": "Complete",
+                    "Subnets": [
+                        {
+                            "SubnetIdentifier": "subnet-0867db2dcb1462488",
+                            "SubnetAvailabilityZone": {
+                                "Name": "us-east-1f"
+                            },
+                            "SubnetOutpost": {},
+                            "SubnetStatus": "Active"
+                        },
+                        {
+                            "SubnetIdentifier": "subnet-0395f575be6823738",
+                            "SubnetAvailabilityZone": {
+                                "Name": "us-east-1e"
+                            },
+                            "SubnetOutpost": {},
+                            "SubnetStatus": "Active"
+                        },
+                        {
+                            "SubnetIdentifier": "subnet-0ba582f1e2af5d98a",
+                            "SubnetAvailabilityZone": {
+                                "Name": "us-east-1d"
+                            },
+                            "SubnetOutpost": {},
+                            "SubnetStatus": "Active"
+                        }
+                    ]
+                },
+                "PreferredMaintenanceWindow": "tue:07:32-tue:08:02",
+                "PendingModifiedValues": {},
+                "MultiAZ": false,
+                "EngineVersion": "14.7",
+                "AutoMinorVersionUpgrade": true,
+                "ReadReplicaDBInstanceIdentifiers": [],
+                "LicenseModel": "postgresql-license",
+                "OptionGroupMemberships": [
+                    {
+                        "OptionGroupName": "default:postgres-14",
+                        "Status": "in-sync"
+                    }
+                ],
+                "PubliclyAccessible": true,
+                "StorageType": "gp2",
+                "DbInstancePort": 0,
+                "StorageEncrypted": true,
+                "KmsKeyId": "arn:aws:kms:us-east-1:644160558196:key/e426740f-3b07-4d07-ad22-3cb504d65620",
+                "DbiResourceId": "db-CR3ECGHYRFFMW4ACZACV4Y7FUU",
+                "CACertificateIdentifier": "rds-ca-2019",
+                "DomainMemberships": [],
+                "CopyTagsToSnapshot": true,
+                "MonitoringInterval": 0,
+                "DBInstanceArn": "arn:aws:rds:us-east-1:644160558196:db:kapilt-testx-testx",
+                "IAMDatabaseAuthenticationEnabled": true,
+                "PerformanceInsightsEnabled": false,
+                "DeletionProtection": false,
+                "AssociatedRoles": [],
+                "MaxAllocatedStorage": 100,
+                "TagList": [
+                    {
+                        "Key": "App",
+                        "Value": "Testx"
+                    },
+                    {
+                        "Key": "Env",
+                        "Value": "Dev"
+                    }
+                ],
+                "CustomerOwnedIpEnabled": false,
+                "ActivityStreamStatus": "stopped",
+                "BackupTarget": "region",
+                "NetworkType": "IPV4",
+                "StorageThroughput": 0,
+                "CertificateDetails": {
+                    "CAIdentifier": "rds-ca-2019",
+                    "ValidTill": {
+                        "__class__": "datetime",
+                        "year": 2024,
+                        "month": 8,
+                        "day": 22,
+                        "hour": 17,
+                        "minute": 8,
+                        "second": 50,
+                        "microsecond": 0
+                    }
+                }
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
diff --git a/tests/data/placebo/test_rds_snapshot_instance/rds.DescribeDBSnapshots_1.json b/tests/data/placebo/test_rds_snapshot_instance/rds.DescribeDBSnapshots_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_rds_snapshot_instance/rds.DescribeDBSnapshots_1.json
@@ -0,0 +1,73 @@
+{
+    "status_code": 200,
+    "data": {
+        "DBSnapshots": [
+            {
+                "DBSnapshotIdentifier": "manual-testx",
+                "DBInstanceIdentifier": "kapilt-testx-testx",
+                "SnapshotCreateTime": {
+                    "__class__": "datetime",
+                    "year": 2023,
+                    "month": 7,
+                    "day": 25,
+                    "hour": 13,
+                    "minute": 22,
+                    "second": 6,
+                    "microsecond": 244000
+                },
+                "Engine": "postgres",
+                "AllocatedStorage": 30,
+                "Status": "available",
+                "Port": 5432,
+                "AvailabilityZone": "us-east-1d",
+                "VpcId": "vpc-029654b93830acd5a",
+                "InstanceCreateTime": {
+                    "__class__": "datetime",
+                    "year": 2023,
+                    "month": 5,
+                    "day": 17,
+                    "hour": 18,
+                    "minute": 26,
+                    "second": 31,
+                    "microsecond": 598000
+                },
+                "MasterUsername": "testx",
+                "EngineVersion": "14.7",
+                "LicenseModel": "postgresql-license",
+                "SnapshotType": "manual",
+                "OptionGroupName": "default:postgres-14",
+                "PercentProgress": 100,
+                "StorageType": "gp2",
+                "Encrypted": true,
+                "KmsKeyId": "arn:aws:kms:us-east-1:644160558196:key/e426740f-3b07-4d07-ad22-3cb504d65620",
+                "DBSnapshotArn": "arn:aws:rds:us-east-1:644160558196:snapshot:manual-testx",
+                "IAMDatabaseAuthenticationEnabled": true,
+                "ProcessorFeatures": [],
+                "DbiResourceId": "db-CR3ECGHYRFFMW4ACZACV4Y7FUU",
+                "TagList": [
+                    {
+                        "Key": "App",
+                        "Value": "Testx"
+                    },
+                    {
+                        "Key": "Env",
+                        "Value": "Dev"
+                    }
+                ],
+                "OriginalSnapshotCreateTime": {
+                    "__class__": "datetime",
+                    "year": 2023,
+                    "month": 7,
+                    "day": 25,
+                    "hour": 13,
+                    "minute": 22,
+                    "second": 6,
+                    "microsecond": 244000
+                },
+                "SnapshotTarget": "region",
+                "StorageThroughput": 0
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
diff --git a/tests/test_rds.py b/tests/test_rds.py
--- a/tests/test_rds.py
+++ b/tests/test_rds.py
@@ -924,6 +924,21 @@ def test_rds_db_option_groups(self):
         self.assertEqual(resources[0]["DBInstanceIdentifier"], "database-2")
 
 
+def test_rds_snapshot_instance(test):
+    factory = test.replay_flight_data('test_rds_snapshot_instance')
+    p = test.load_policy(
+        {'name': 'check-instance',
+         'resource': 'aws.rds-snapshot',
+         'filters': [
+             {'type': 'instance',
+              'key': 'DeletionProtection',
+              'value': False}]},
+        session_factory=factory)
+    resources = p.run()
+    assert len(resources) == 1
+    resources[0]['DBSnapshotIdentifier'] == 'manual-testx'
+
+
 class RDSSnapshotTest(BaseTest):
 
     def test_rds_snapshot_copy_tags_enable(self):