diff --git a/plugins/aws/resoto_plugin_aws/resource/cloudfront.py b/plugins/aws/resoto_plugin_aws/resource/cloudfront.py index 65ff16b8da..7b5c514b8e 100644 --- a/plugins/aws/resoto_plugin_aws/resource/cloudfront.py +++ b/plugins/aws/resoto_plugin_aws/resource/cloudfront.py @@ -666,6 +666,13 @@ def fetch_distribution(did: str) -> None: builder.core_feedback.info(msg, log) raise + @classmethod + def called_collect_apis(cls) -> List[AwsApiSpec]: + return super().called_mutator_apis() + [ + AwsApiSpec(service_name, "get-distribution"), + AwsApiSpec(service_name, "list-distributions"), + ] + @classmethod def called_mutator_apis(cls) -> List[AwsApiSpec]: return super().called_mutator_apis() + [ diff --git a/plugins/aws/resoto_plugin_aws/resource/kms.py b/plugins/aws/resoto_plugin_aws/resource/kms.py index 44ec973df5..e9d589dbb6 100644 --- a/plugins/aws/resoto_plugin_aws/resource/kms.py +++ b/plugins/aws/resoto_plugin_aws/resource/kms.py @@ -1,3 +1,4 @@ +import json from typing import ClassVar, Dict, List, Optional, Type from attrs import define, field from resoto_plugin_aws.aws_client import AwsClient @@ -115,13 +116,20 @@ class AwsKmsKey(AwsResource, BaseAccessKey): kms_pending_deletion_window_in_days: Optional[int] = field(default=None) kms_mac_algorithms: List[str] = field(factory=list) kms_key_rotation_enabled: Optional[bool] = field(default=None) + kms_key_policy: Optional[Json] = field(default=None) @classmethod def called_collect_apis(cls) -> List[AwsApiSpec]: - return [cls.api_spec, AwsApiSpec(service_name, "describe-key"), AwsApiSpec(service_name, "list-resource-tags")] + return [ + cls.api_spec, + AwsApiSpec(service_name, "describe-key"), + AwsApiSpec(service_name, "list-resource-tags"), + AwsApiSpec(service_name, "get-key-policy"), + AwsApiSpec(service_name, "get-key-rotation-status"), + ] @classmethod - def collect(cls: Type[AwsResource], json: List[Json], builder: GraphBuilder) -> None: + def collect(cls: Type[AwsResource], js_list: List[Json], builder: GraphBuilder) -> None: def add_instance(key: Dict[str, str]) -> None: key_metadata = builder.client.get( service_name, "describe-key", result_name="KeyMetadata", KeyId=key["KeyId"] @@ -130,9 +138,23 @@ def add_instance(key: Dict[str, str]) -> None: if instance := AwsKmsKey.from_api(key_metadata, builder): builder.add_node(instance) builder.submit_work(service_name, add_tags, instance) + builder.submit_work(service_name, fetch_key_policy, instance) if instance.kms_key_manager == "CUSTOMER" and instance.access_key_status == "Enabled": builder.submit_work(service_name, add_rotation_status, instance) + def fetch_key_policy(key: AwsKmsKey) -> None: + with builder.suppress(f"{service_name}.get-key-policy"): + key_policy: Optional[str] = builder.client.get( # type: ignore + service_name, + "get-key-policy", + result_name="Policy", + KeyId=key.id, + PolicyName="default", + expected_errors=["NotFoundException"], + ) + if key_policy is not None: + key.kms_key_policy = json.loads(key_policy) + def add_rotation_status(key: AwsKmsKey) -> None: with builder.suppress(f"{service_name}.get-key-rotation-status"): key.kms_key_rotation_enabled = builder.client.get( # type: ignore @@ -150,8 +172,8 @@ def add_tags(key: AwsKmsKey) -> None: if tags: key.tags = bend(ToDict(key="TagKey", value="TagValue"), tags) - for js in json: - add_instance(js) + for js in js_list: + builder.submit_work(service_name, add_instance, js) def update_resource_tag(self, client: AwsClient, key: str, value: str) -> bool: client.call( diff --git a/plugins/aws/resoto_plugin_aws/resource/rds.py b/plugins/aws/resoto_plugin_aws/resource/rds.py index 639aebde13..c850d98e87 100644 --- a/plugins/aws/resoto_plugin_aws/resource/rds.py +++ b/plugins/aws/resoto_plugin_aws/resource/rds.py @@ -6,8 +6,8 @@ from resoto_plugin_aws.resource.ec2 import AwsEc2SecurityGroup, AwsEc2Subnet, AwsEc2Vpc from resoto_plugin_aws.resource.kinesis import AwsKinesisStream from resoto_plugin_aws.resource.kms import AwsKmsKey -from resoto_plugin_aws.utils import ToDict -from resotolib.baseresources import BaseDatabase, ModelReference +from resoto_plugin_aws.utils import ToDict, TagsValue +from resotolib.baseresources import BaseDatabase, ModelReference, BaseSnapshot from resotolib.graph import Graph from resotolib.json_bender import F, K, S, Bend, Bender, ForallBend, bend from resotolib.types import Json @@ -316,19 +316,6 @@ class AwsRdsDBRole: status: Optional[str] = field(default=None) -@define(eq=False, slots=False) -class AwsRdsTag: - kind: ClassVar[str] = "aws_rds_tag" - kind_display: ClassVar[str] = "AWS RDS Tag" - kind_description: ClassVar[str] = ( - "Tags for Amazon RDS instances and resources, which are key-value pairs to" - " help manage and organize resources." - ) - mapping: ClassVar[Dict[str, Bender]] = {"key": S("Key"), "value": S("Value")} - key: Optional[str] = field(default=None) - value: Optional[str] = field(default=None) - - @define(eq=False, slots=False) class AwsRdsInstance(RdsTaggable, AwsResource, BaseDatabase): kind: ClassVar[str] = "aws_rds_instance" @@ -347,7 +334,7 @@ class AwsRdsInstance(RdsTaggable, AwsResource, BaseDatabase): } mapping: ClassVar[Dict[str, Bender]] = { "id": S("DBInstanceIdentifier"), - "tags": S("TagList", default=[]) >> ForallBend(AwsRdsTag.mapping) >> ToDict(), + "tags": S("TagList", default=[]) >> ToDict(), "name": S("DBName"), "ctime": S("InstanceCreateTime"), "arn": S("DBInstanceArn"), @@ -895,4 +882,86 @@ def called_mutator_apis(cls) -> List[AwsApiSpec]: return super().called_mutator_apis() + [AwsApiSpec(service_name, "delete-db-cluster")] -resources: List[Type[AwsResource]] = [AwsRdsCluster, AwsRdsInstance] +@define(eq=False, slots=False) +class AwsRdsDBSnapshot(RdsTaggable, AwsResource, BaseSnapshot): + kind: ClassVar[str] = "aws_rds_db_snapshot" + api_spec: ClassVar[AwsApiSpec] = AwsApiSpec("rds", "describe-db-snapshots", "DBSnapshots") + mapping: ClassVar[Dict[str, Bender]] = { + "id": S("DBSnapshotIdentifier"), + "tags": S("TagList", default=[]) >> ToDict(), + "name": S("Tags", default=[]) >> TagsValue("Name"), + "ctime": S("SnapshotCreateTime"), + "arn": S("DBSnapshotArn"), + "rds_db_instance_identifier": S("DBInstanceIdentifier"), + "rds_engine": S("Engine"), + "rds_allocated_storage": S("AllocatedStorage"), + "snapshot_status": S("Status"), + "rds_port": S("Port"), + "rds_availability_zone": S("AvailabilityZone"), + "rds_vpc_id": S("VpcId"), + "rds_instance_create_time": S("InstanceCreateTime"), + "rds_master_username": S("MasterUsername"), + "rds_engine_version": S("EngineVersion"), + "rds_license_model": S("LicenseModel"), + "rds_snapshot_type": S("SnapshotType"), + "rds_iops": S("Iops"), + "rds_option_group_name": S("OptionGroupName"), + "rds_percent_progress": S("PercentProgress"), + "rds_source_region": S("SourceRegion"), + "rds_source_db_snapshot_identifier": S("SourceDBSnapshotIdentifier"), + "rds_storage_type": S("StorageType"), + "rds_tde_credential_arn": S("TdeCredentialArn"), + "rds_encrypted": S("Encrypted"), + "rds_kms_key_id": S("KmsKeyId"), + "rds_timezone": S("Timezone"), + "rds_iam_database_authentication_enabled": S("IAMDatabaseAuthenticationEnabled"), + "rds_processor_features": S("ProcessorFeatures", default=[]) >> ToDict(key="Name", value="Value"), + "rds_dbi_resource_id": S("DbiResourceId"), + "rds_original_snapshot_create_time": S("OriginalSnapshotCreateTime"), + "rds_snapshot_database_time": S("SnapshotDatabaseTime"), + "rds_snapshot_target": S("SnapshotTarget"), + "rds_storage_throughput": S("StorageThroughput"), + "rds_db_system_id": S("DBSystemId"), + "rds_dedicated_log_volume": S("DedicatedLogVolume"), + "rds_multi_tenant": S("MultiTenant"), + } + rds_db_instance_identifier: Optional[str] = field(default=None, metadata={"description": "Specifies the DB instance identifier of the DB instance this DB snapshot was created from."}) # fmt: skip + rds_engine: Optional[str] = field(default=None, metadata={"description": "Specifies the name of the database engine."}) # fmt: skip + rds_allocated_storage: Optional[int] = field(default=None, metadata={"description": "Specifies the allocated storage size in gibibytes (GiB)."}) # fmt: skip + rds_port: Optional[int] = field(default=None, metadata={"description": "Specifies the port that the database engine was listening on at the time of the snapshot."}) # fmt: skip + rds_availability_zone: Optional[str] = field(default=None, metadata={"description": "Specifies the name of the Availability Zone the DB instance was located in at the time of the DB snapshot."}) # fmt: skip + rds_vpc_id: Optional[str] = field(default=None, metadata={"description": "Provides the VPC ID associated with the DB snapshot."}) # fmt: skip + rds_instance_create_time: Optional[datetime] = field(default=None, metadata={"description": "Specifies the time in Coordinated Universal Time (UTC) when the DB instance, from which the snapshot was taken, was created."}) # fmt: skip + rds_master_username: Optional[str] = field(default=None, metadata={"description": "Provides the master username for the DB snapshot."}) # fmt: skip + rds_engine_version: Optional[str] = field(default=None, metadata={"description": "Specifies the version of the database engine."}) # fmt: skip + rds_license_model: Optional[str] = field(default=None, metadata={"description": "License model information for the restored DB instance."}) # fmt: skip + rds_snapshot_type: Optional[str] = field(default=None, metadata={"description": "Provides the type of the DB snapshot."}) # fmt: skip + rds_iops: Optional[int] = field(default=None, metadata={"description": "Specifies the Provisioned IOPS (I/O operations per second) value of the DB instance at the time of the snapshot."}) # fmt: skip + rds_option_group_name: Optional[str] = field(default=None, metadata={"description": "Provides the option group name for the DB snapshot."}) # fmt: skip + rds_percent_progress: Optional[int] = field(default=None, metadata={"description": "The percentage of the estimated data that has been transferred."}) # fmt: skip + rds_source_region: Optional[str] = field(default=None, metadata={"description": "The Amazon Web Services Region that the DB snapshot was created in or copied from."}) # fmt: skip + rds_source_db_snapshot_identifier: Optional[str] = field(default=None, metadata={"description": "The DB snapshot Amazon Resource Name (ARN) that the DB snapshot was copied from. It only has a value in the case of a cross-account or cross-Region copy."}) # fmt: skip + rds_storage_type: Optional[str] = field(default=None, metadata={"description": "Specifies the storage type associated with DB snapshot."}) # fmt: skip + rds_tde_credential_arn: Optional[str] = field(default=None, metadata={"description": "The ARN from the key store with which to associate the instance for TDE encryption."}) # fmt: skip + rds_encrypted: Optional[bool] = field(default=None, metadata={"description": "Indicates whether the DB snapshot is encrypted."}) # fmt: skip + rds_kms_key_id: Optional[str] = field(default=None, metadata={"description": "If Encrypted is true, the Amazon Web Services KMS key identifier for the encrypted DB snapshot. The Amazon Web Services KMS key identifier is the key ARN, key ID, alias ARN, or alias name for the KMS key."}) # fmt: skip + rds_timezone: Optional[str] = field(default=None, metadata={"description": "The time zone of the DB snapshot. In most cases, the Timezone element is empty. Timezone content appears only for snapshots taken from Microsoft SQL Server DB instances that were created with a time zone specified."}) # fmt: skip + rds_iam_database_authentication_enabled: Optional[bool] = field(default=None, metadata={"description": "Indicates whether mapping of Amazon Web Services Identity and Access Management (IAM) accounts to database accounts is enabled."}) # fmt: skip + rds_processor_features: Optional[Dict[str, str]] = field(factory=list, metadata={"description": "The number of CPU cores and the number of threads per core for the DB instance class of the DB instance when the DB snapshot was created."}) # fmt: skip + rds_dbi_resource_id: Optional[str] = field(default=None, metadata={"description": "The identifier for the source DB instance, which can't be changed and which is unique to an Amazon Web Services Region."}) # fmt: skip + rds_original_snapshot_create_time: Optional[datetime] = field(default=None, metadata={"description": "Specifies the time of the CreateDBSnapshot operation in Coordinated Universal Time (UTC). Doesn't change when the snapshot is copied."}) # fmt: skip + rds_snapshot_database_time: Optional[datetime] = field(default=None, metadata={"description": "The timestamp of the most recent transaction applied to the database that you're backing up. Thus, if you restore a snapshot, SnapshotDatabaseTime is the most recent transaction in the restored DB instance. In contrast, originalSnapshotCreateTime specifies the system time that the snapshot completed. If you back up a read replica, you can determine the replica lag by comparing SnapshotDatabaseTime with originalSnapshotCreateTime. For example, if originalSnapshotCreateTime is two hours later than SnapshotDatabaseTime, then the replica lag is two hours."}) # fmt: skip + rds_snapshot_target: Optional[str] = field(default=None, metadata={"description": "Specifies where manual snapshots are stored: Amazon Web Services Outposts or the Amazon Web Services Region."}) # fmt: skip + rds_storage_throughput: Optional[int] = field(default=None, metadata={"description": "Specifies the storage throughput for the DB snapshot."}) # fmt: skip + rds_db_system_id: Optional[str] = field(default=None, metadata={"description": "The Oracle system identifier (SID), which is the name of the Oracle database instance that manages your database files. The Oracle SID is also the name of your CDB."}) # fmt: skip + rds_dedicated_log_volume: Optional[bool] = field(default=None, metadata={"description": "Indicates whether the DB instance has a dedicated log volume (DLV) enabled."}) # fmt: skip + rds_multi_tenant: Optional[bool] = field(default=None, metadata={"description": "Indicates whether the snapshot is of a DB instance using the multi-tenant configuration (TRUE) or the single-tenant configuration (FALSE)."}) # fmt: skip + + def connect_in_graph(self, builder: GraphBuilder, source: Json) -> None: + if dbi := self.rds_db_instance_identifier: + builder.add_edge(self, reverse=True, clazz=AwsRdsInstance, id=dbi) + if vpc_id := self.rds_vpc_id: + builder.add_edge(self, reverse=True, clazz=AwsEc2Vpc, id=vpc_id) + + +resources: List[Type[AwsResource]] = [AwsRdsCluster, AwsRdsInstance, AwsRdsDBSnapshot] diff --git a/plugins/aws/test/resources/files/rds/describe-db-snapshots.json b/plugins/aws/test/resources/files/rds/describe-db-snapshots.json new file mode 100644 index 0000000000..812a9153bd --- /dev/null +++ b/plugins/aws/test/resources/files/rds/describe-db-snapshots.json @@ -0,0 +1,69 @@ +{ + "Marker": "foo", + "DBSnapshots": [ + { + "DBSnapshotIdentifier": "foo", + "DBInstanceIdentifier": "foo", + "SnapshotCreateTime": "2024-01-03T13:45:34Z", + "Engine": "foo", + "AllocatedStorage": 123, + "Status": "foo", + "Port": 123, + "AvailabilityZone": "foo", + "VpcId": "foo", + "InstanceCreateTime": "2024-01-03T13:45:34Z", + "MasterUsername": "foo", + "EngineVersion": "foo", + "LicenseModel": "foo", + "SnapshotType": "foo", + "Iops": 123, + "OptionGroupName": "foo", + "PercentProgress": 123, + "SourceRegion": "foo", + "SourceDBSnapshotIdentifier": "foo", + "StorageType": "foo", + "TdeCredentialArn": "foo", + "Encrypted": true, + "KmsKeyId": "foo", + "DBSnapshotArn": "foo", + "Timezone": "foo", + "IAMDatabaseAuthenticationEnabled": true, + "ProcessorFeatures": [ + { + "Name": "foo", + "Value": "foo" + }, + { + "Name": "foo", + "Value": "foo" + }, + { + "Name": "foo", + "Value": "foo" + } + ], + "DbiResourceId": "foo", + "TagList": [ + { + "Key": "foo", + "Value": "foo" + }, + { + "Key": "foo", + "Value": "foo" + }, + { + "Key": "foo", + "Value": "foo" + } + ], + "OriginalSnapshotCreateTime": "2024-01-03T13:45:34Z", + "SnapshotDatabaseTime": "2024-01-03T13:45:34Z", + "SnapshotTarget": "foo", + "StorageThroughput": 123, + "DBSystemId": "foo", + "DedicatedLogVolume": true, + "MultiTenant": true + } + ] +} diff --git a/plugins/aws/test/resources/rds_test.py b/plugins/aws/test/resources/rds_test.py index 1d4cdcd0ff..f672f20677 100644 --- a/plugins/aws/test/resources/rds_test.py +++ b/plugins/aws/test/resources/rds_test.py @@ -1,9 +1,10 @@ -from resotolib.graph import Graph -from test.resources import round_trip_for from types import SimpleNamespace from typing import cast, Any + from resoto_plugin_aws.aws_client import AwsClient -from resoto_plugin_aws.resource.rds import AwsRdsInstance, AwsRdsCluster +from resoto_plugin_aws.resource.rds import AwsRdsInstance, AwsRdsCluster, AwsRdsDBSnapshot +from resotolib.graph import Graph +from test.resources import round_trip_for def test_rds_instances() -> None: @@ -16,6 +17,10 @@ def test_rds_cluster() -> None: round_trip_for(AwsRdsCluster) +def test_rds_snapshots() -> None: + round_trip_for(AwsRdsDBSnapshot, "description", "volume_id", "owner_id", "owner_alias") + + def test_tagging() -> None: instance, _ = round_trip_for(AwsRdsInstance) diff --git a/plugins/aws/tools/aws_model_gen.py b/plugins/aws/tools/aws_model_gen.py index 9f930577e7..785b88d55e 100644 --- a/plugins/aws/tools/aws_model_gen.py +++ b/plugins/aws/tools/aws_model_gen.py @@ -262,9 +262,9 @@ def all_models() -> List[AwsModel]: return result -def create_test_response(service: str, function: str) -> JsonElement: +def create_test_response(service: str, function: str, is_pascal: bool = False) -> JsonElement: sm = service_model(service) - op = sm.operation_model(pascalcase(function)) + op = sm.operation_model(function if is_pascal else pascalcase(function)) def sample(shape: Shape) -> JsonElement: if isinstance(shape, StringShape) and shape.enum: @@ -847,6 +847,7 @@ def default_imports() -> str: "rds": [ # AwsResotoModel("describe-db-instances", "Instances", "DBInstance", prefix="Rds", prop_prefix="rds_") # AwsResotoModel("describe-db-clusters", "Clusters", "DBCluster", prefix="Rds", prop_prefix="rds_") + # AwsResotoModel("describe-db-snapshots", "DBSnapshots", "DBSnapshot", prefix="Rds", prop_prefix="rds_") ], "route53": [ # AwsResotoModel("list_hosted_zones", "HostedZones", "HostedZone", prefix="Route53", prop_prefix="zone_"),