diff --git a/moto/resourcegroupstaggingapi/models.py b/moto/resourcegroupstaggingapi/models.py index 0514901c6..4abb7893f 100644 --- a/moto/resourcegroupstaggingapi/models.py +++ b/moto/resourcegroupstaggingapi/models.py @@ -13,6 +13,7 @@ from moto.glacier import glacier_backends from moto.redshift import redshift_backends from moto.emr import emr_backends from moto.awslambda import lambda_backends +from moto.ecs import ecs_backends # Left: EC2 ElastiCache RDS ELB CloudFront WorkSpaces Lambda EMR Glacier Kinesis Redshift Route53 # StorageGateway DynamoDB MachineLearning ACM DirectConnect DirectoryService CloudHSM @@ -106,6 +107,13 @@ class ResourceGroupsTaggingAPIBackend(BaseBackend): """ return lambda_backends[self.account_id][self.region_name] + @property + def ecs_backend(self): + """ + :rtype: moto.ecs.models.EcsnBackend + """ + return ecs_backends[self.account_id][self.region_name] + def _get_resources_generator(self, tag_filters=None, resource_type_filters=None): # Look at # https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html @@ -143,7 +151,19 @@ class ResourceGroupsTaggingAPIBackend(BaseBackend): else: return True - # Do S3, resource type s3 + def format_tags(tags): + result = [] + for key, value in tags.items(): + result.append({"Key": key, "Value": value}) + return result + + def format_tag_keys(tags, keys): + result = [] + for tag in tags: + result.append({"Key": tag[keys[0]], "Value": tag[keys[1]]}) + return result + + # S3 if not resource_type_filters or "s3" in resource_type_filters: for bucket in self.s3_backend.buckets.values(): tags = self.s3_backend.tagger.list_tags_for_resource(bucket.arn)["Tags"] @@ -153,12 +173,45 @@ class ResourceGroupsTaggingAPIBackend(BaseBackend): continue yield {"ResourceARN": "arn:aws:s3:::" + bucket.name, "Tags": tags} - # EC2 tags - def get_ec2_tags(res_id): - result = [] - for key, value in self.ec2_backend.tags.get(res_id, {}).items(): - result.append({"Key": key, "Value": value}) - return result + # CloudFormation + if not resource_type_filters or "cloudformation:stack" in resource_type_filters: + + try: + from moto.cloudformation import cloudformation_backends + + backend = cloudformation_backends[self.account_id][self.region_name] + + for stack in backend.stacks.values(): + tags = format_tags(stack.tags) + if not tag_filter(tags): + continue + yield {"ResourceARN": f"{stack.stack_id}", "Tags": tags} + + except Exception: + pass + + # ECS + if not resource_type_filters or "ecs:service" in resource_type_filters: + for service in self.ecs_backend.services.values(): + tags = format_tag_keys(service.tags, ["key", "value"]) + if not tag_filter(tags): + continue + yield {"ResourceARN": f"{service.physical_resource_id}", "Tags": tags} + + if not resource_type_filters or "ecs:cluster" in resource_type_filters: + for cluster in self.ecs_backend.clusters.values(): + tags = format_tag_keys(cluster.tags, ["key", "value"]) + if not tag_filter(tags): + continue + yield {"ResourceARN": f"{cluster.arn}", "Tags": tags} + + if not resource_type_filters or "ecs:task" in resource_type_filters: + for task_dict in self.ecs_backend.tasks.values(): + for task in task_dict.values(): + tags = format_tag_keys(task.tags, ["key", "value"]) + if not tag_filter(tags): + continue + yield {"ResourceARN": f"{task.task_arn}", "Tags": tags} # EC2 AMI, resource type ec2:image if ( @@ -167,7 +220,7 @@ class ResourceGroupsTaggingAPIBackend(BaseBackend): or "ec2:image" in resource_type_filters ): for ami in self.ec2_backend.amis.values(): - tags = get_ec2_tags(ami.id) + tags = format_tags(self.ec2_backend.tags.get(ami.id, {})) if not tags or not tag_filter( tags @@ -186,7 +239,7 @@ class ResourceGroupsTaggingAPIBackend(BaseBackend): ): for reservation in self.ec2_backend.reservations.values(): for instance in reservation.instances: - tags = get_ec2_tags(instance.id) + tags = format_tags(self.ec2_backend.tags.get(instance.id, {})) if not tags or not tag_filter( tags @@ -204,7 +257,7 @@ class ResourceGroupsTaggingAPIBackend(BaseBackend): or "ec2:network-interface" in resource_type_filters ): for eni in self.ec2_backend.enis.values(): - tags = get_ec2_tags(eni.id) + tags = format_tags(self.ec2_backend.tags.get(eni.id, {})) if not tags or not tag_filter( tags @@ -225,7 +278,7 @@ class ResourceGroupsTaggingAPIBackend(BaseBackend): ): for vpc in self.ec2_backend.groups.values(): for sg in vpc.values(): - tags = get_ec2_tags(sg.id) + tags = format_tags(self.ec2_backend.tags.get(sg.id, {})) if not tags or not tag_filter( tags @@ -243,7 +296,7 @@ class ResourceGroupsTaggingAPIBackend(BaseBackend): or "ec2:snapshot" in resource_type_filters ): for snapshot in self.ec2_backend.snapshots.values(): - tags = get_ec2_tags(snapshot.id) + tags = format_tags(self.ec2_backend.tags.get(snapshot.id, {})) if not tags or not tag_filter( tags @@ -263,7 +316,7 @@ class ResourceGroupsTaggingAPIBackend(BaseBackend): or "ec2:volume" in resource_type_filters ): for volume in self.ec2_backend.volumes.values(): - tags = get_ec2_tags(volume.id) + tags = format_tags(self.ec2_backend.tags.get(volume.id, {})) if not tags or not tag_filter( tags @@ -312,15 +365,12 @@ class ResourceGroupsTaggingAPIBackend(BaseBackend): # Kinesis # KMS - def get_kms_tags(kms_key_id): - result = [] - for tag in self.kms_backend.list_resource_tags(kms_key_id).get("Tags", []): - result.append({"Key": tag["TagKey"], "Value": tag["TagValue"]}) - return result - if not resource_type_filters or "kms" in resource_type_filters: for kms_key in self.kms_backend.list_keys(): - tags = get_kms_tags(kms_key.id) + tags = format_tag_keys( + self.kms_backend.list_resource_tags(kms_key.id).get("Tags", []), + ["TagKey", "TagValue"], + ) if not tag_filter(tags): # Skip if no tags, or invalid filter continue @@ -408,7 +458,7 @@ class ResourceGroupsTaggingAPIBackend(BaseBackend): or "ec2:vpc" in resource_type_filters ): for vpc in self.ec2_backend.vpcs.values(): - tags = get_ec2_tags(vpc.id) + tags = format_tags(self.ec2_backend.tags.get(vpc.id, {})) if not tags or not tag_filter( tags ): # Skip if no tags, or invalid filter @@ -427,15 +477,9 @@ class ResourceGroupsTaggingAPIBackend(BaseBackend): # VPC VPN Connection # Lambda Instance - def transform_lambda_tags(dictTags): - result = [] - for key, value in dictTags.items(): - result.append({"Key": key, "Value": value}) - return result - if not resource_type_filters or "lambda" in resource_type_filters: for f in self.lambda_backend.list_functions(): - tags = transform_lambda_tags(f.tags) + tags = format_tags(f.tags) if not tags or not tag_filter(tags): continue yield { @@ -447,7 +491,7 @@ class ResourceGroupsTaggingAPIBackend(BaseBackend): # Look at # https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html - # Do S3, resource type s3 + # S3 for bucket in self.s3_backend.buckets.values(): tags = self.s3_backend.tagger.get_tag_dict_for_resource(bucket.arn) for key, _ in tags.items(): diff --git a/tests/test_resourcegroupstaggingapi/test_resourcegroupstaggingapi.py b/tests/test_resourcegroupstaggingapi/test_resourcegroupstaggingapi.py index 072092ad3..cba164cfb 100644 --- a/tests/test_resourcegroupstaggingapi/test_resourcegroupstaggingapi.py +++ b/tests/test_resourcegroupstaggingapi/test_resourcegroupstaggingapi.py @@ -1,4 +1,5 @@ import boto3 +import json import sure # noqa # pylint: disable=unused-import from moto import mock_ec2 from moto import mock_elbv2 @@ -8,10 +9,219 @@ from moto import mock_resourcegroupstaggingapi from moto import mock_s3 from moto import mock_lambda from moto import mock_iam +from moto import mock_cloudformation +from moto import mock_ecs from botocore.client import ClientError from tests import EXAMPLE_AMI_ID, EXAMPLE_AMI_ID2 +@mock_kms +@mock_cloudformation +@mock_resourcegroupstaggingapi +def test_get_resources_cloudformation(): + + template = { + "AWSTemplateFormatVersion": "2010-09-09", + "Resources": {"test": {"Type": "AWS::S3::Bucket"}}, + } + template_json = json.dumps(template) + + cf_client = boto3.client("cloudformation", region_name="us-east-1") + + stack_one = cf_client.create_stack( + StackName="stack-1", + TemplateBody=template_json, + Tags=[{"Key": "tag", "Value": "one"}], + ).get("StackId") + stack_two = cf_client.create_stack( + StackName="stack-2", + TemplateBody=template_json, + Tags=[{"Key": "tag", "Value": "two"}], + ).get("StackId") + stack_three = cf_client.create_stack( + StackName="stack-3", + TemplateBody=template_json, + Tags=[{"Key": "tag", "Value": "three"}], + ).get("StackId") + + rgta_client = boto3.client("resourcegroupstaggingapi", region_name="us-east-1") + + resp = rgta_client.get_resources(TagFilters=[{"Key": "tag", "Values": ["one"]}]) + resp["ResourceTagMappingList"].should.have.length_of(1) + resp["ResourceTagMappingList"][0]["ResourceARN"].should.contain(stack_one) + + resp = rgta_client.get_resources( + TagFilters=[{"Key": "tag", "Values": ["one", "three"]}] + ) + resp["ResourceTagMappingList"].should.have.length_of(2) + resp["ResourceTagMappingList"][0]["ResourceARN"].should.contain(stack_one) + resp["ResourceTagMappingList"][1]["ResourceARN"].should.contain(stack_three) + + kms_client = boto3.client("kms", region_name="us-east-1") + kms_client.create_key( + KeyUsage="ENCRYPT_DECRYPT", Tags=[{"TagKey": "tag", "TagValue": "two"}] + ) + + resp = rgta_client.get_resources(TagFilters=[{"Key": "tag", "Values": ["two"]}]) + resp["ResourceTagMappingList"].should.have.length_of(2) + resp["ResourceTagMappingList"][0]["ResourceARN"].should.contain(stack_two) + resp["ResourceTagMappingList"][1]["ResourceARN"].should.contain("kms") + + resp = rgta_client.get_resources( + ResourceTypeFilters=["cloudformation:stack"], + TagFilters=[{"Key": "tag", "Values": ["two"]}], + ) + resp["ResourceTagMappingList"].should.have.length_of(1) + resp["ResourceTagMappingList"][0]["ResourceARN"].should.contain(stack_two) + + +@mock_ecs +@mock_ec2 +@mock_resourcegroupstaggingapi +def test_get_resources_ecs(): + + # ecs:cluster + client = boto3.client("ecs", region_name="us-east-1") + cluster_one = ( + client.create_cluster( + clusterName="cluster-a", tags=[{"key": "tag", "value": "a tag"}] + ) + .get("cluster") + .get("clusterArn") + ) + cluster_two = ( + client.create_cluster( + clusterName="cluster-b", tags=[{"key": "tag", "value": "b tag"}] + ) + .get("cluster") + .get("clusterArn") + ) + + rgta_client = boto3.client("resourcegroupstaggingapi", region_name="us-east-1") + resp = rgta_client.get_resources(TagFilters=[{"Key": "tag", "Values": ["a tag"]}]) + resp["ResourceTagMappingList"].should.have.length_of(1) + resp["ResourceTagMappingList"][0]["ResourceARN"].should.contain(cluster_one) + + # ecs:service + service_one = ( + client.create_service( + cluster=cluster_one, + serviceName="service-a", + tags=[{"key": "tag", "value": "a tag"}], + ) + .get("service") + .get("serviceArn") + ) + + service_two = ( + client.create_service( + cluster=cluster_two, + serviceName="service-b", + tags=[{"key": "tag", "value": "b tag"}], + ) + .get("service") + .get("serviceArn") + ) + + resp = rgta_client.get_resources(TagFilters=[{"Key": "tag", "Values": ["a tag"]}]) + resp["ResourceTagMappingList"].should.have.length_of(2) + resp["ResourceTagMappingList"][0]["ResourceARN"].should.contain(service_one) + resp["ResourceTagMappingList"][1]["ResourceARN"].should.contain(cluster_one) + + resp = rgta_client.get_resources( + ResourceTypeFilters=["ecs:cluster"], + TagFilters=[{"Key": "tag", "Values": ["b tag"]}], + ) + resp["ResourceTagMappingList"].should.have.length_of(1) + resp["ResourceTagMappingList"][0]["ResourceARN"].should_not.contain(service_two) + resp["ResourceTagMappingList"][0]["ResourceARN"].should.contain(cluster_two) + + resp = rgta_client.get_resources( + ResourceTypeFilters=["ecs:service"], + TagFilters=[{"Key": "tag", "Values": ["b tag"]}], + ) + resp["ResourceTagMappingList"].should.have.length_of(1) + resp["ResourceTagMappingList"][0]["ResourceARN"].should.contain(service_two) + resp["ResourceTagMappingList"][0]["ResourceARN"].should_not.contain(cluster_two) + + # ecs:task + resp = client.register_task_definition( + family="test_ecs_task", + containerDefinitions=[ + { + "name": "hello_world", + "image": "docker/hello-world:latest", + "cpu": 1024, + "memory": 400, + "essential": True, + "environment": [ + {"name": "AWS_ACCESS_KEY_ID", "value": "SOME_ACCESS_KEY"} + ], + "logConfiguration": {"logDriver": "json-file"}, + } + ], + ) + + ec2_client = boto3.client("ec2", region_name="us-east-1") + vpc = ec2_client.create_vpc(CidrBlock="10.0.0.0/16").get("Vpc").get("VpcId") + subnet = ( + ec2_client.create_subnet(VpcId=vpc, CidrBlock="10.0.0.0/18") + .get("Subnet") + .get("SubnetId") + ) + sg = ec2_client.create_security_group( + VpcId=vpc, GroupName="test-ecs", Description="moto ecs" + ).get("GroupId") + + task_one = ( + client.run_task( + cluster="cluster-a", + taskDefinition="test_ecs_task", + launchType="FARGATE", + networkConfiguration={ + "awsvpcConfiguration": { + "subnets": [subnet], + "securityGroups": [sg], + } + }, + tags=[{"key": "tag", "value": "a tag"}], + ) + .get("tasks")[0] + .get("taskArn") + ) + + task_two = ( + client.run_task( + cluster="cluster-b", + taskDefinition="test_ecs_task", + launchType="FARGATE", + networkConfiguration={ + "awsvpcConfiguration": { + "subnets": [subnet], + "securityGroups": [sg], + } + }, + tags=[{"key": "tag", "value": "b tag"}], + ) + .get("tasks")[0] + .get("taskArn") + ) + + resp = rgta_client.get_resources(TagFilters=[{"Key": "tag", "Values": ["b tag"]}]) + resp["ResourceTagMappingList"].should.have.length_of(3) + resp["ResourceTagMappingList"][0]["ResourceARN"].should.contain(service_two) + resp["ResourceTagMappingList"][1]["ResourceARN"].should.contain(cluster_two) + resp["ResourceTagMappingList"][2]["ResourceARN"].should.contain(task_two) + + resp = rgta_client.get_resources( + ResourceTypeFilters=["ecs:task"], + TagFilters=[{"Key": "tag", "Values": ["a tag"]}], + ) + resp["ResourceTagMappingList"].should.have.length_of(1) + resp["ResourceTagMappingList"][0]["ResourceARN"].should.contain(task_one) + resp["ResourceTagMappingList"][0]["ResourceARN"].should_not.contain(task_two) + + @mock_rds @mock_ec2 @mock_resourcegroupstaggingapi @@ -422,50 +632,6 @@ def test_get_resources_rds(): assert_response(resp, 2) -@mock_rds -@mock_resourcegroupstaggingapi -def test_get_resources_rds_clusters(): - client = boto3.client("rds", region_name="us-west-2") - resources_tagged = [] - resources_untagged = [] - for i in range(3): - cluster = client.create_db_cluster( - DBClusterIdentifier=f"db-cluster-{i}", - Engine="postgres", - MasterUsername="admin", - MasterUserPassword="P@ssw0rd!", - CopyTagsToSnapshot=True if i else False, - Tags=[{"Key": "test", "Value": f"value-{i}"}] if i else [], - ).get("DBCluster") - snapshot = client.create_db_cluster_snapshot( - DBClusterIdentifier=cluster["DBClusterIdentifier"], - DBClusterSnapshotIdentifier=f"snapshot-{i}", - ).get("DBClusterSnapshot") - group = resources_tagged if i else resources_untagged - group.append(cluster["DBClusterArn"]) - group.append(snapshot["DBClusterSnapshotArn"]) - - def assert_response(response, expected_count, resource_type=None): - results = response.get("ResourceTagMappingList", []) - results.should.have.length_of(expected_count) - for item in results: - arn = item["ResourceARN"] - arn.should.be.within(resources_tagged) - arn.should_not.be.within(resources_untagged) - if resource_type: - sure.this(f":{resource_type}:").should.be.within(arn) - - rtapi = boto3.client("resourcegroupstaggingapi", region_name="us-west-2") - resp = rtapi.get_resources(ResourceTypeFilters=["rds"]) - assert_response(resp, 4) - resp = rtapi.get_resources(ResourceTypeFilters=["rds:cluster"]) - assert_response(resp, 2, resource_type="cluster") - resp = rtapi.get_resources(ResourceTypeFilters=["rds:cluster-snapshot"]) - assert_response(resp, 2, resource_type="cluster-snapshot") - resp = rtapi.get_resources(TagFilters=[{"Key": "test", "Values": ["value-1"]}]) - assert_response(resp, 2) - - @mock_lambda @mock_resourcegroupstaggingapi @mock_iam