Support CloudFormation and ECS ResourceTypeFilters in ResourceGroupsTaggingAPI get_resources() (#6023)

This commit is contained in:
Steph Manning 2023-03-11 08:19:22 -05:00 committed by GitHub
parent d022b404d3
commit 1934d24ad5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 283 additions and 73 deletions

View File

@ -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():

View File

@ -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