Add resource-groups. (#1953)
This commit is contained in:
parent
238d1c7c39
commit
d8ff67197b
@ -3415,19 +3415,19 @@
|
|||||||
- [ ] start_stream_processor
|
- [ ] start_stream_processor
|
||||||
- [ ] stop_stream_processor
|
- [ ] stop_stream_processor
|
||||||
|
|
||||||
## resource-groups - 0% implemented
|
## resource-groups - 62% implemented
|
||||||
- [ ] create_group
|
- [X] create_group
|
||||||
- [ ] delete_group
|
- [X] delete_group
|
||||||
- [ ] get_group
|
- [X] get_group
|
||||||
- [ ] get_group_query
|
- [X] get_group_query
|
||||||
- [ ] get_tags
|
- [ ] get_tags
|
||||||
- [ ] list_group_resources
|
- [ ] list_group_resources
|
||||||
- [ ] list_groups
|
- [X] list_groups
|
||||||
- [ ] search_resources
|
- [ ] search_resources
|
||||||
- [ ] tag
|
- [ ] tag
|
||||||
- [ ] untag
|
- [ ] untag
|
||||||
- [ ] update_group
|
- [X] update_group
|
||||||
- [ ] update_group_query
|
- [X] update_group_query
|
||||||
|
|
||||||
## resourcegroupstaggingapi - 60% implemented
|
## resourcegroupstaggingapi - 60% implemented
|
||||||
- [X] get_resources
|
- [X] get_resources
|
||||||
|
@ -36,6 +36,7 @@ from .polly import mock_polly # flake8: noqa
|
|||||||
from .rds import mock_rds, mock_rds_deprecated # flake8: noqa
|
from .rds import mock_rds, mock_rds_deprecated # flake8: noqa
|
||||||
from .rds2 import mock_rds2, mock_rds2_deprecated # flake8: noqa
|
from .rds2 import mock_rds2, mock_rds2_deprecated # flake8: noqa
|
||||||
from .redshift import mock_redshift, mock_redshift_deprecated # flake8: noqa
|
from .redshift import mock_redshift, mock_redshift_deprecated # flake8: noqa
|
||||||
|
from .resourcegroups import mock_resourcegroups # flake8: noqa
|
||||||
from .s3 import mock_s3, mock_s3_deprecated # flake8: noqa
|
from .s3 import mock_s3, mock_s3_deprecated # flake8: noqa
|
||||||
from .ses import mock_ses, mock_ses_deprecated # flake8: noqa
|
from .ses import mock_ses, mock_ses_deprecated # flake8: noqa
|
||||||
from .secretsmanager import mock_secretsmanager # flake8: noqa
|
from .secretsmanager import mock_secretsmanager # flake8: noqa
|
||||||
|
@ -32,6 +32,7 @@ from moto.organizations import organizations_backends
|
|||||||
from moto.polly import polly_backends
|
from moto.polly import polly_backends
|
||||||
from moto.rds2 import rds2_backends
|
from moto.rds2 import rds2_backends
|
||||||
from moto.redshift import redshift_backends
|
from moto.redshift import redshift_backends
|
||||||
|
from moto.resourcegroups import resourcegroups_backends
|
||||||
from moto.route53 import route53_backends
|
from moto.route53 import route53_backends
|
||||||
from moto.s3 import s3_backends
|
from moto.s3 import s3_backends
|
||||||
from moto.ses import ses_backends
|
from moto.ses import ses_backends
|
||||||
@ -81,6 +82,7 @@ BACKENDS = {
|
|||||||
'organizations': organizations_backends,
|
'organizations': organizations_backends,
|
||||||
'polly': polly_backends,
|
'polly': polly_backends,
|
||||||
'redshift': redshift_backends,
|
'redshift': redshift_backends,
|
||||||
|
'resource-groups': resourcegroups_backends,
|
||||||
'rds': rds2_backends,
|
'rds': rds2_backends,
|
||||||
's3': s3_backends,
|
's3': s3_backends,
|
||||||
's3bucket_path': s3_backends,
|
's3bucket_path': s3_backends,
|
||||||
|
6
moto/resourcegroups/__init__.py
Normal file
6
moto/resourcegroups/__init__.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
from .models import resourcegroups_backends
|
||||||
|
from ..core.models import base_decorator
|
||||||
|
|
||||||
|
resourcegroups_backend = resourcegroups_backends['us-east-1']
|
||||||
|
mock_resourcegroups = base_decorator(resourcegroups_backends)
|
13
moto/resourcegroups/exceptions.py
Normal file
13
moto/resourcegroups/exceptions.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
import json
|
||||||
|
|
||||||
|
from werkzeug.exceptions import HTTPException
|
||||||
|
|
||||||
|
|
||||||
|
class BadRequestException(HTTPException):
|
||||||
|
code = 400
|
||||||
|
|
||||||
|
def __init__(self, message, **kwargs):
|
||||||
|
super(BadRequestException, self).__init__(
|
||||||
|
description=json.dumps({"Message": message, "Code": "BadRequestException"}), **kwargs
|
||||||
|
)
|
338
moto/resourcegroups/models.py
Normal file
338
moto/resourcegroups/models.py
Normal file
@ -0,0 +1,338 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
from builtins import str
|
||||||
|
|
||||||
|
import boto3
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
|
||||||
|
from moto.core import BaseBackend, BaseModel
|
||||||
|
from .exceptions import BadRequestException
|
||||||
|
|
||||||
|
|
||||||
|
class FakeResourceGroup(BaseModel):
|
||||||
|
def __init__(self, name, resource_query, description=None, tags=None):
|
||||||
|
self.errors = []
|
||||||
|
description = description or ""
|
||||||
|
tags = tags or {}
|
||||||
|
if self._validate_description(value=description):
|
||||||
|
self._description = description
|
||||||
|
if self._validate_name(value=name):
|
||||||
|
self._name = name
|
||||||
|
if self._validate_resource_query(value=resource_query):
|
||||||
|
self._resource_query = resource_query
|
||||||
|
if self._validate_tags(value=tags):
|
||||||
|
self._tags = tags
|
||||||
|
self._raise_errors()
|
||||||
|
self.arn = "arn:aws:resource-groups:us-west-1:123456789012:{name}".format(name=name)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _format_error(key, value, constraint):
|
||||||
|
return "Value '{value}' at '{key}' failed to satisfy constraint: {constraint}".format(
|
||||||
|
constraint=constraint,
|
||||||
|
key=key,
|
||||||
|
value=value,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _raise_errors(self):
|
||||||
|
if self.errors:
|
||||||
|
errors_len = len(self.errors)
|
||||||
|
plural = "s" if len(self.errors) > 1 else ""
|
||||||
|
errors = "; ".join(self.errors)
|
||||||
|
raise BadRequestException("{errors_len} validation error{plural} detected: {errors}".format(
|
||||||
|
errors_len=errors_len, plural=plural, errors=errors,
|
||||||
|
))
|
||||||
|
|
||||||
|
def _validate_description(self, value):
|
||||||
|
errors = []
|
||||||
|
if len(value) > 511:
|
||||||
|
errors.append(self._format_error(
|
||||||
|
key="description",
|
||||||
|
value=value,
|
||||||
|
constraint="Member must have length less than or equal to 512",
|
||||||
|
))
|
||||||
|
if not re.match(r"^[\sa-zA-Z0-9_.-]*$", value):
|
||||||
|
errors.append(self._format_error(
|
||||||
|
key="name",
|
||||||
|
value=value,
|
||||||
|
constraint=r"Member must satisfy regular expression pattern: [\sa-zA-Z0-9_\.-]*",
|
||||||
|
))
|
||||||
|
if errors:
|
||||||
|
self.errors += errors
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _validate_name(self, value):
|
||||||
|
errors = []
|
||||||
|
if len(value) > 128:
|
||||||
|
errors.append(self._format_error(
|
||||||
|
key="name",
|
||||||
|
value=value,
|
||||||
|
constraint="Member must have length less than or equal to 128",
|
||||||
|
))
|
||||||
|
# Note \ is a character to match not an escape.
|
||||||
|
if not re.match(r"^[a-zA-Z0-9_\\.-]+$", value):
|
||||||
|
errors.append(self._format_error(
|
||||||
|
key="name",
|
||||||
|
value=value,
|
||||||
|
constraint=r"Member must satisfy regular expression pattern: [a-zA-Z0-9_\.-]+",
|
||||||
|
))
|
||||||
|
if errors:
|
||||||
|
self.errors += errors
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _validate_resource_query(self, value):
|
||||||
|
errors = []
|
||||||
|
if value["Type"] not in {"CLOUDFORMATION_STACK_1_0", "TAG_FILTERS_1_0"}:
|
||||||
|
errors.append(self._format_error(
|
||||||
|
key="resourceQuery.type",
|
||||||
|
value=value,
|
||||||
|
constraint="Member must satisfy enum value set: [CLOUDFORMATION_STACK_1_0, TAG_FILTERS_1_0]",
|
||||||
|
))
|
||||||
|
if len(value["Query"]) > 2048:
|
||||||
|
errors.append(self._format_error(
|
||||||
|
key="resourceQuery.query",
|
||||||
|
value=value,
|
||||||
|
constraint="Member must have length less than or equal to 2048",
|
||||||
|
))
|
||||||
|
if errors:
|
||||||
|
self.errors += errors
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _validate_tags(self, value):
|
||||||
|
errors = []
|
||||||
|
# AWS only outputs one error for all keys and one for all values.
|
||||||
|
error_keys = None
|
||||||
|
error_values = None
|
||||||
|
regex = re.compile(r"^([\\p{L}\\p{Z}\\p{N}_.:/=+\-@]*)$")
|
||||||
|
for tag_key, tag_value in value.items():
|
||||||
|
# Validation for len(tag_key) >= 1 is done by botocore.
|
||||||
|
if len(tag_key) > 128 or re.match(regex, tag_key):
|
||||||
|
error_keys = self._format_error(
|
||||||
|
key="tags",
|
||||||
|
value=value,
|
||||||
|
constraint=(
|
||||||
|
"Map value must satisfy constraint: ["
|
||||||
|
"Member must have length less than or equal to 128, "
|
||||||
|
"Member must have length greater than or equal to 1, "
|
||||||
|
r"Member must satisfy regular expression pattern: ^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$"
|
||||||
|
"]"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
# Validation for len(tag_value) >= 0 is nonsensical.
|
||||||
|
if len(tag_value) > 256 or re.match(regex, tag_key):
|
||||||
|
error_values = self._format_error(
|
||||||
|
key="tags",
|
||||||
|
value=value,
|
||||||
|
constraint=(
|
||||||
|
"Map value must satisfy constraint: ["
|
||||||
|
"Member must have length less than or equal to 256, "
|
||||||
|
"Member must have length greater than or equal to 0, "
|
||||||
|
r"Member must satisfy regular expression pattern: ^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$"
|
||||||
|
"]"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if error_keys:
|
||||||
|
errors.append(error_keys)
|
||||||
|
if error_values:
|
||||||
|
errors.append(error_values)
|
||||||
|
if errors:
|
||||||
|
self.errors += errors
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def description(self):
|
||||||
|
return self._description
|
||||||
|
|
||||||
|
@description.setter
|
||||||
|
def description(self, value):
|
||||||
|
if not self._validate_description(value=value):
|
||||||
|
self._raise_errors()
|
||||||
|
self._description = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@name.setter
|
||||||
|
def name(self, value):
|
||||||
|
if not self._validate_name(value=value):
|
||||||
|
self._raise_errors()
|
||||||
|
self._name = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def resource_query(self):
|
||||||
|
return self._resource_query
|
||||||
|
|
||||||
|
@resource_query.setter
|
||||||
|
def resource_query(self, value):
|
||||||
|
if not self._validate_resource_query(value=value):
|
||||||
|
self._raise_errors()
|
||||||
|
self._resource_query = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tags(self):
|
||||||
|
return self._tags
|
||||||
|
|
||||||
|
@tags.setter
|
||||||
|
def tags(self, value):
|
||||||
|
if not self._validate_tags(value=value):
|
||||||
|
self._raise_errors()
|
||||||
|
self._tags = value
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceGroups():
|
||||||
|
def __init__(self):
|
||||||
|
self.by_name = {}
|
||||||
|
self.by_arn = {}
|
||||||
|
|
||||||
|
def __contains__(self, item):
|
||||||
|
return item in self.by_name
|
||||||
|
|
||||||
|
def append(self, resource_group):
|
||||||
|
self.by_name[resource_group.name] = resource_group
|
||||||
|
self.by_arn[resource_group.arn] = resource_group
|
||||||
|
|
||||||
|
def delete(self, name):
|
||||||
|
group = self.by_name[name]
|
||||||
|
del self.by_name[name]
|
||||||
|
del self.by_arn[group.arn]
|
||||||
|
return group
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceGroupsBackend(BaseBackend):
|
||||||
|
def __init__(self, region_name=None):
|
||||||
|
super(ResourceGroupsBackend, self).__init__()
|
||||||
|
self.region_name = region_name
|
||||||
|
self.groups = ResourceGroups()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _validate_resource_query(resource_query):
|
||||||
|
type = resource_query["Type"]
|
||||||
|
query = json.loads(resource_query["Query"])
|
||||||
|
query_keys = set(query.keys())
|
||||||
|
invalid_json_exception = BadRequestException("Invalid query: Invalid query format: check JSON syntax")
|
||||||
|
if not isinstance(query["ResourceTypeFilters"], list):
|
||||||
|
raise invalid_json_exception
|
||||||
|
if type == "CLOUDFORMATION_STACK_1_0":
|
||||||
|
if query_keys != {"ResourceTypeFilters", "StackIdentifier"}:
|
||||||
|
raise invalid_json_exception
|
||||||
|
stack_identifier = query["StackIdentifier"]
|
||||||
|
if not isinstance(stack_identifier, str):
|
||||||
|
raise invalid_json_exception
|
||||||
|
if not re.match(
|
||||||
|
r"^arn:aws:cloudformation:[a-z]{2}-[a-z]+-[0-9]+:[0-9]+:stack/[-0-9A-z]+/[-0-9a-f]+$",
|
||||||
|
stack_identifier,
|
||||||
|
):
|
||||||
|
raise BadRequestException(
|
||||||
|
"Invalid query: Verify that the specified ARN is formatted correctly."
|
||||||
|
)
|
||||||
|
# Once checking other resources is implemented.
|
||||||
|
# if stack_identifier not in self.cloudformation_backend.stacks:
|
||||||
|
# raise BadRequestException("Invalid query: The specified CloudFormation stack doesn't exist.")
|
||||||
|
if type == "TAG_FILTERS_1_0":
|
||||||
|
if query_keys != {"ResourceTypeFilters", "TagFilters"}:
|
||||||
|
raise invalid_json_exception
|
||||||
|
tag_filters = query["TagFilters"]
|
||||||
|
if not isinstance(tag_filters, list):
|
||||||
|
raise invalid_json_exception
|
||||||
|
if not tag_filters or len(tag_filters) > 50:
|
||||||
|
raise BadRequestException(
|
||||||
|
"Invalid query: The TagFilters list must contain between 1 and 50 elements"
|
||||||
|
)
|
||||||
|
for tag_filter in tag_filters:
|
||||||
|
if not isinstance(tag_filter, dict):
|
||||||
|
raise invalid_json_exception
|
||||||
|
if set(tag_filter.keys()) != {"Key", "Values"}:
|
||||||
|
raise invalid_json_exception
|
||||||
|
key = tag_filter["Key"]
|
||||||
|
if not isinstance(key, str):
|
||||||
|
raise invalid_json_exception
|
||||||
|
if not key:
|
||||||
|
raise BadRequestException(
|
||||||
|
"Invalid query: The TagFilter element cannot have empty or null Key field"
|
||||||
|
)
|
||||||
|
if len(key) > 128:
|
||||||
|
raise BadRequestException("Invalid query: The maximum length for a tag Key is 128")
|
||||||
|
values = tag_filter["Values"]
|
||||||
|
if not isinstance(values, list):
|
||||||
|
raise invalid_json_exception
|
||||||
|
if len(values) > 20:
|
||||||
|
raise BadRequestException(
|
||||||
|
"Invalid query: The TagFilter Values list must contain between 0 and 20 elements"
|
||||||
|
)
|
||||||
|
for value in values:
|
||||||
|
if not isinstance(value, str):
|
||||||
|
raise invalid_json_exception
|
||||||
|
if len(value) > 256:
|
||||||
|
raise BadRequestException(
|
||||||
|
"Invalid query: The maximum length for a tag Value is 256"
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _validate_tags(tags):
|
||||||
|
for tag in tags:
|
||||||
|
if tag.lower().startswith('aws:'):
|
||||||
|
raise BadRequestException("Tag keys must not start with 'aws:'")
|
||||||
|
|
||||||
|
def create_group(self, name, resource_query, description=None, tags=None):
|
||||||
|
tags = tags or {}
|
||||||
|
group = FakeResourceGroup(
|
||||||
|
name=name,
|
||||||
|
resource_query=resource_query,
|
||||||
|
description=description,
|
||||||
|
tags=tags,
|
||||||
|
)
|
||||||
|
if name in self.groups:
|
||||||
|
raise BadRequestException("Cannot create group: group already exists")
|
||||||
|
if name.upper().startswith("AWS"):
|
||||||
|
raise BadRequestException("Group name must not start with 'AWS'")
|
||||||
|
self._validate_tags(tags)
|
||||||
|
self._validate_resource_query(resource_query)
|
||||||
|
self.groups.append(group)
|
||||||
|
return group
|
||||||
|
|
||||||
|
def delete_group(self, group_name):
|
||||||
|
return self.groups.delete(name=group_name)
|
||||||
|
|
||||||
|
def get_group(self, group_name):
|
||||||
|
return self.groups.by_name[group_name]
|
||||||
|
|
||||||
|
def get_tags(self, arn):
|
||||||
|
return self.groups.by_arn[arn].tags
|
||||||
|
|
||||||
|
# def list_group_resources(self):
|
||||||
|
# ...
|
||||||
|
|
||||||
|
def list_groups(self, filters=None, max_results=None, next_token=None):
|
||||||
|
return self.groups.by_name
|
||||||
|
|
||||||
|
# def search_resources(self):
|
||||||
|
# ...
|
||||||
|
|
||||||
|
def tag(self, arn, tags):
|
||||||
|
all_tags = self.groups.by_arn[arn].tags
|
||||||
|
all_tags.update(tags)
|
||||||
|
self._validate_tags(all_tags)
|
||||||
|
self.groups.by_arn[arn].tags = all_tags
|
||||||
|
|
||||||
|
def untag(self, arn, keys):
|
||||||
|
group = self.groups.by_arn[arn]
|
||||||
|
for key in keys:
|
||||||
|
del group.tags[key]
|
||||||
|
|
||||||
|
def update_group(self, group_name, description=None):
|
||||||
|
if description:
|
||||||
|
self.groups.by_name[group_name].description = description
|
||||||
|
return self.groups.by_name[group_name]
|
||||||
|
|
||||||
|
def update_group_query(self, group_name, resource_query):
|
||||||
|
self._validate_resource_query(resource_query)
|
||||||
|
self.groups.by_name[group_name].resource_query = resource_query
|
||||||
|
return self.groups.by_name[group_name]
|
||||||
|
|
||||||
|
|
||||||
|
available_regions = boto3.session.Session().get_available_regions("resource-groups")
|
||||||
|
resourcegroups_backends = {region: ResourceGroupsBackend(region_name=region) for region in available_regions}
|
162
moto/resourcegroups/responses.py
Normal file
162
moto/resourcegroups/responses.py
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
import json
|
||||||
|
|
||||||
|
try:
|
||||||
|
from urllib import unquote
|
||||||
|
except ImportError:
|
||||||
|
from urllib.parse import unquote
|
||||||
|
|
||||||
|
from moto.core.responses import BaseResponse
|
||||||
|
from .models import resourcegroups_backends
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceGroupsResponse(BaseResponse):
|
||||||
|
SERVICE_NAME = 'resource-groups'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def resourcegroups_backend(self):
|
||||||
|
return resourcegroups_backends[self.region]
|
||||||
|
|
||||||
|
def create_group(self):
|
||||||
|
name = self._get_param("Name")
|
||||||
|
description = self._get_param("Description")
|
||||||
|
resource_query = self._get_param("ResourceQuery")
|
||||||
|
tags = self._get_param("Tags")
|
||||||
|
group = self.resourcegroups_backend.create_group(
|
||||||
|
name=name,
|
||||||
|
description=description,
|
||||||
|
resource_query=resource_query,
|
||||||
|
tags=tags,
|
||||||
|
)
|
||||||
|
return json.dumps({
|
||||||
|
"Group": {
|
||||||
|
"GroupArn": group.arn,
|
||||||
|
"Name": group.name,
|
||||||
|
"Description": group.description
|
||||||
|
},
|
||||||
|
"ResourceQuery": group.resource_query,
|
||||||
|
"Tags": group.tags
|
||||||
|
})
|
||||||
|
|
||||||
|
def delete_group(self):
|
||||||
|
group_name = self._get_param("GroupName")
|
||||||
|
group = self.resourcegroups_backend.delete_group(group_name=group_name)
|
||||||
|
return json.dumps({
|
||||||
|
"Group": {
|
||||||
|
"GroupArn": group.arn,
|
||||||
|
"Name": group.name,
|
||||||
|
"Description": group.description
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
def get_group(self):
|
||||||
|
group_name = self._get_param("GroupName")
|
||||||
|
group = self.resourcegroups_backend.get_group(group_name=group_name)
|
||||||
|
return json.dumps({
|
||||||
|
"Group": {
|
||||||
|
"GroupArn": group.arn,
|
||||||
|
"Name": group.name,
|
||||||
|
"Description": group.description,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
def get_group_query(self):
|
||||||
|
group_name = self._get_param("GroupName")
|
||||||
|
group = self.resourcegroups_backend.get_group(group_name=group_name)
|
||||||
|
return json.dumps({
|
||||||
|
"GroupQuery": {
|
||||||
|
"GroupName": group.name,
|
||||||
|
"ResourceQuery": group.resource_query,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
def get_tags(self):
|
||||||
|
arn = unquote(self._get_param("Arn"))
|
||||||
|
return json.dumps({
|
||||||
|
"Arn": arn,
|
||||||
|
"Tags": self.resourcegroups_backend.get_tags(arn=arn)
|
||||||
|
})
|
||||||
|
|
||||||
|
def list_group_resources(self):
|
||||||
|
raise NotImplementedError('ResourceGroups.list_group_resources is not yet implemented')
|
||||||
|
|
||||||
|
def list_groups(self):
|
||||||
|
filters = self._get_param("Filters")
|
||||||
|
if filters:
|
||||||
|
raise NotImplementedError(
|
||||||
|
'ResourceGroups.list_groups with filter parameter is not yet implemented'
|
||||||
|
)
|
||||||
|
max_results = self._get_int_param("MaxResults", 50)
|
||||||
|
next_token = self._get_param("NextToken")
|
||||||
|
groups = self.resourcegroups_backend.list_groups(
|
||||||
|
filters=filters,
|
||||||
|
max_results=max_results,
|
||||||
|
next_token=next_token
|
||||||
|
)
|
||||||
|
return json.dumps({
|
||||||
|
"GroupIdentifiers": [{
|
||||||
|
"GroupName": group.name,
|
||||||
|
"GroupArn": group.arn,
|
||||||
|
} for group in groups.values()],
|
||||||
|
"Groups": [{
|
||||||
|
"GroupArn": group.arn,
|
||||||
|
"Name": group.name,
|
||||||
|
"Description": group.description,
|
||||||
|
} for group in groups.values()],
|
||||||
|
"NextToken": next_token,
|
||||||
|
})
|
||||||
|
|
||||||
|
def search_resources(self):
|
||||||
|
raise NotImplementedError('ResourceGroups.search_resources is not yet implemented')
|
||||||
|
|
||||||
|
def tag(self):
|
||||||
|
arn = unquote(self._get_param("Arn"))
|
||||||
|
tags = self._get_param("Tags")
|
||||||
|
if arn not in self.resourcegroups_backend.groups.by_arn:
|
||||||
|
raise NotImplementedError(
|
||||||
|
'ResourceGroups.tag with non-resource-group Arn parameter is not yet implemented'
|
||||||
|
)
|
||||||
|
self.resourcegroups_backend.tag(arn=arn, tags=tags)
|
||||||
|
return json.dumps({
|
||||||
|
"Arn": arn,
|
||||||
|
"Tags": tags
|
||||||
|
})
|
||||||
|
|
||||||
|
def untag(self):
|
||||||
|
arn = unquote(self._get_param("Arn"))
|
||||||
|
keys = self._get_param("Keys")
|
||||||
|
if arn not in self.resourcegroups_backend.groups.by_arn:
|
||||||
|
raise NotImplementedError(
|
||||||
|
'ResourceGroups.untag with non-resource-group Arn parameter is not yet implemented'
|
||||||
|
)
|
||||||
|
self.resourcegroups_backend.untag(arn=arn, keys=keys)
|
||||||
|
return json.dumps({
|
||||||
|
"Arn": arn,
|
||||||
|
"Keys": keys
|
||||||
|
})
|
||||||
|
|
||||||
|
def update_group(self):
|
||||||
|
group_name = self._get_param("GroupName")
|
||||||
|
description = self._get_param("Description", "")
|
||||||
|
group = self.resourcegroups_backend.update_group(group_name=group_name, description=description)
|
||||||
|
return json.dumps({
|
||||||
|
"Group": {
|
||||||
|
"GroupArn": group.arn,
|
||||||
|
"Name": group.name,
|
||||||
|
"Description": group.description
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
def update_group_query(self):
|
||||||
|
group_name = self._get_param("GroupName")
|
||||||
|
resource_query = self._get_param("ResourceQuery")
|
||||||
|
group = self.resourcegroups_backend.update_group_query(
|
||||||
|
group_name=group_name,
|
||||||
|
resource_query=resource_query
|
||||||
|
)
|
||||||
|
return json.dumps({
|
||||||
|
"GroupQuery": {
|
||||||
|
"GroupName": group.name,
|
||||||
|
"ResourceQuery": resource_query
|
||||||
|
}
|
||||||
|
})
|
14
moto/resourcegroups/urls.py
Normal file
14
moto/resourcegroups/urls.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
from .responses import ResourceGroupsResponse
|
||||||
|
|
||||||
|
url_bases = [
|
||||||
|
"https?://resource-groups(-fips)?.(.+).amazonaws.com",
|
||||||
|
]
|
||||||
|
|
||||||
|
url_paths = {
|
||||||
|
'{0}/groups$': ResourceGroupsResponse.dispatch,
|
||||||
|
'{0}/groups/(?P<resource_group_name>[^/]+)$': ResourceGroupsResponse.dispatch,
|
||||||
|
'{0}/groups/(?P<resource_group_name>[^/]+)/query$': ResourceGroupsResponse.dispatch,
|
||||||
|
'{0}/groups-list$': ResourceGroupsResponse.dispatch,
|
||||||
|
'{0}/resources/(?P<resource_arn>[^/]+)/tags$': ResourceGroupsResponse.dispatch,
|
||||||
|
}
|
0
tests/test_resourcegroups/__init__.py
Normal file
0
tests/test_resourcegroups/__init__.py
Normal file
165
tests/test_resourcegroups/test_resourcegroups.py
Normal file
165
tests/test_resourcegroups/test_resourcegroups.py
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import boto3
|
||||||
|
import json
|
||||||
|
import sure # noqa
|
||||||
|
|
||||||
|
from moto import mock_resourcegroups
|
||||||
|
|
||||||
|
|
||||||
|
@mock_resourcegroups
|
||||||
|
def test_create_group():
|
||||||
|
resource_groups = boto3.client("resource-groups", region_name="us-east-1")
|
||||||
|
|
||||||
|
response = resource_groups.create_group(
|
||||||
|
Name="test_resource_group",
|
||||||
|
Description="description",
|
||||||
|
ResourceQuery={
|
||||||
|
"Type": "TAG_FILTERS_1_0",
|
||||||
|
"Query": json.dumps(
|
||||||
|
{
|
||||||
|
"ResourceTypeFilters": ["AWS::AllSupported"],
|
||||||
|
"TagFilters": [
|
||||||
|
{"Key": "resources_tag_key", "Values": ["resources_tag_value"]}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
),
|
||||||
|
},
|
||||||
|
Tags={"resource_group_tag_key": "resource_group_tag_value"}
|
||||||
|
)
|
||||||
|
response["Group"]["Name"].should.contain("test_resource_group")
|
||||||
|
response["ResourceQuery"]["Type"].should.contain("TAG_FILTERS_1_0")
|
||||||
|
response["Tags"]["resource_group_tag_key"].should.contain("resource_group_tag_value")
|
||||||
|
|
||||||
|
|
||||||
|
@mock_resourcegroups
|
||||||
|
def test_delete_group():
|
||||||
|
resource_groups = boto3.client("resource-groups", region_name="us-east-1")
|
||||||
|
|
||||||
|
test_create_group()
|
||||||
|
|
||||||
|
response = resource_groups.delete_group(GroupName="test_resource_group")
|
||||||
|
response["Group"]["Name"].should.contain("test_resource_group")
|
||||||
|
|
||||||
|
response = resource_groups.list_groups()
|
||||||
|
response["GroupIdentifiers"].should.have.length_of(0)
|
||||||
|
response["Groups"].should.have.length_of(0)
|
||||||
|
|
||||||
|
|
||||||
|
@mock_resourcegroups
|
||||||
|
def test_get_group():
|
||||||
|
resource_groups = boto3.client("resource-groups", region_name="us-east-1")
|
||||||
|
|
||||||
|
test_create_group()
|
||||||
|
|
||||||
|
response = resource_groups.get_group(GroupName="test_resource_group")
|
||||||
|
response["Group"]["Description"].should.contain("description")
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@mock_resourcegroups
|
||||||
|
def test_get_group_query():
|
||||||
|
resource_groups = boto3.client("resource-groups", region_name="us-east-1")
|
||||||
|
|
||||||
|
test_create_group()
|
||||||
|
|
||||||
|
response = resource_groups.get_group_query(GroupName="test_resource_group")
|
||||||
|
response["GroupQuery"]["ResourceQuery"]["Type"].should.contain("TAG_FILTERS_1_0")
|
||||||
|
|
||||||
|
|
||||||
|
@mock_resourcegroups
|
||||||
|
def test_get_tags():
|
||||||
|
resource_groups = boto3.client("resource-groups", region_name="us-east-1")
|
||||||
|
|
||||||
|
response = test_get_group()
|
||||||
|
|
||||||
|
response = resource_groups.get_tags(Arn=response["Group"]["GroupArn"])
|
||||||
|
response["Tags"].should.have.length_of(1)
|
||||||
|
response["Tags"]["resource_group_tag_key"].should.contain("resource_group_tag_value")
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@mock_resourcegroups
|
||||||
|
def test_list_groups():
|
||||||
|
resource_groups = boto3.client("resource-groups", region_name="us-east-1")
|
||||||
|
|
||||||
|
test_create_group()
|
||||||
|
|
||||||
|
response = resource_groups.list_groups()
|
||||||
|
response["GroupIdentifiers"].should.have.length_of(1)
|
||||||
|
response["Groups"].should.have.length_of(1)
|
||||||
|
|
||||||
|
|
||||||
|
@mock_resourcegroups
|
||||||
|
def test_tag():
|
||||||
|
resource_groups = boto3.client("resource-groups", region_name="us-east-1")
|
||||||
|
|
||||||
|
response = test_get_tags()
|
||||||
|
|
||||||
|
response = resource_groups.tag(
|
||||||
|
Arn=response["Arn"],
|
||||||
|
Tags={"resource_group_tag_key_2": "resource_group_tag_value_2"}
|
||||||
|
)
|
||||||
|
response["Tags"]["resource_group_tag_key_2"].should.contain("resource_group_tag_value_2")
|
||||||
|
|
||||||
|
response = resource_groups.get_tags(Arn=response["Arn"])
|
||||||
|
response["Tags"].should.have.length_of(2)
|
||||||
|
response["Tags"]["resource_group_tag_key_2"].should.contain("resource_group_tag_value_2")
|
||||||
|
|
||||||
|
|
||||||
|
@mock_resourcegroups
|
||||||
|
def test_untag():
|
||||||
|
resource_groups = boto3.client("resource-groups", region_name="us-east-1")
|
||||||
|
|
||||||
|
response = test_get_tags()
|
||||||
|
|
||||||
|
response = resource_groups.untag(Arn=response["Arn"], Keys=["resource_group_tag_key"])
|
||||||
|
response["Keys"].should.contain("resource_group_tag_key")
|
||||||
|
|
||||||
|
response = resource_groups.get_tags(Arn=response["Arn"])
|
||||||
|
response["Tags"].should.have.length_of(0)
|
||||||
|
|
||||||
|
|
||||||
|
@mock_resourcegroups
|
||||||
|
def test_update_group():
|
||||||
|
resource_groups = boto3.client("resource-groups", region_name="us-east-1")
|
||||||
|
|
||||||
|
test_get_group()
|
||||||
|
|
||||||
|
response = resource_groups.update_group(
|
||||||
|
GroupName="test_resource_group",
|
||||||
|
Description="description_2",
|
||||||
|
)
|
||||||
|
response["Group"]["Description"].should.contain("description_2")
|
||||||
|
|
||||||
|
response = resource_groups.get_group(GroupName="test_resource_group")
|
||||||
|
response["Group"]["Description"].should.contain("description_2")
|
||||||
|
|
||||||
|
|
||||||
|
@mock_resourcegroups
|
||||||
|
def test_update_group_query():
|
||||||
|
resource_groups = boto3.client("resource-groups", region_name="us-east-1")
|
||||||
|
|
||||||
|
test_create_group()
|
||||||
|
|
||||||
|
response = resource_groups.update_group_query(
|
||||||
|
GroupName="test_resource_group",
|
||||||
|
ResourceQuery={
|
||||||
|
"Type": "CLOUDFORMATION_STACK_1_0",
|
||||||
|
"Query": json.dumps(
|
||||||
|
{
|
||||||
|
"ResourceTypeFilters": ["AWS::AllSupported"],
|
||||||
|
"StackIdentifier": (
|
||||||
|
"arn:aws:cloudformation:eu-west-1:012345678912:stack/"
|
||||||
|
"test_stack/c223eca0-e744-11e8-8910-500c41f59083"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
response["GroupQuery"]["ResourceQuery"]["Type"].should.contain("CLOUDFORMATION_STACK_1_0")
|
||||||
|
|
||||||
|
response = resource_groups.get_group_query(GroupName="test_resource_group")
|
||||||
|
response["GroupQuery"]["ResourceQuery"]["Type"].should.contain("CLOUDFORMATION_STACK_1_0")
|
Loading…
Reference in New Issue
Block a user