Support CloudFormation and ECS ResourceTypeFilters in ResourceGroupsTaggingAPI get_resources() (#6023)
This commit is contained in:
parent
d022b404d3
commit
1934d24ad5
@ -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():
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user