Added AWS Config batching capabilities
- Support for aggregated and non-aggregated batching - Currently only implemented for S3
This commit is contained in:
parent
893f0d4f83
commit
96f0666df9
@ -18,7 +18,7 @@ from moto.config.exceptions import InvalidResourceTypeException, InvalidDelivery
|
|||||||
NoSuchDeliveryChannelException, LastDeliveryChannelDeleteFailedException, TagKeyTooBig, \
|
NoSuchDeliveryChannelException, LastDeliveryChannelDeleteFailedException, TagKeyTooBig, \
|
||||||
TooManyTags, TagValueTooBig, TooManyAccountSources, InvalidParameterValueException, InvalidNextTokenException, \
|
TooManyTags, TagValueTooBig, TooManyAccountSources, InvalidParameterValueException, InvalidNextTokenException, \
|
||||||
NoSuchConfigurationAggregatorException, InvalidTagCharacters, DuplicateTags, InvalidLimit, InvalidResourceParameters, \
|
NoSuchConfigurationAggregatorException, InvalidTagCharacters, DuplicateTags, InvalidLimit, InvalidResourceParameters, \
|
||||||
TooManyResourceIds, ResourceNotDiscoveredException
|
TooManyResourceIds, ResourceNotDiscoveredException, TooManyResourceKeys
|
||||||
|
|
||||||
from moto.core import BaseBackend, BaseModel
|
from moto.core import BaseBackend, BaseModel
|
||||||
from moto.s3.config import s3_config_query
|
from moto.s3.config import s3_config_query
|
||||||
@ -791,7 +791,7 @@ class ConfigBackend(BaseBackend):
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def get_resource_config_history(self, type, id, backend_region):
|
def get_resource_config_history(self, resource_type, id, backend_region):
|
||||||
"""Returns the configuration of an item in the AWS Config format of the resource for the current regional backend.
|
"""Returns the configuration of an item in the AWS Config format of the resource for the current regional backend.
|
||||||
|
|
||||||
NOTE: This is --NOT-- returning history as it is not supported in moto at this time. (PR's welcome!)
|
NOTE: This is --NOT-- returning history as it is not supported in moto at this time. (PR's welcome!)
|
||||||
@ -799,30 +799,102 @@ class ConfigBackend(BaseBackend):
|
|||||||
return 1 item. (If no items, it raises an exception)
|
return 1 item. (If no items, it raises an exception)
|
||||||
"""
|
"""
|
||||||
# If the type isn't implemented then we won't find the item:
|
# If the type isn't implemented then we won't find the item:
|
||||||
if type not in RESOURCE_MAP:
|
if resource_type not in RESOURCE_MAP:
|
||||||
raise ResourceNotDiscoveredException(type, id)
|
raise ResourceNotDiscoveredException(resource_type, id)
|
||||||
|
|
||||||
# Is the resource type global?
|
# Is the resource type global?
|
||||||
if RESOURCE_MAP[type].backends.get('global'):
|
if RESOURCE_MAP[resource_type].backends.get('global'):
|
||||||
backend_region = 'global'
|
backend_region = 'global'
|
||||||
|
|
||||||
# If the backend region isn't implemented then we won't find the item:
|
# If the backend region isn't implemented then we won't find the item:
|
||||||
if not RESOURCE_MAP[type].backends.get(backend_region):
|
if not RESOURCE_MAP[resource_type].backends.get(backend_region):
|
||||||
raise ResourceNotDiscoveredException(type, id)
|
raise ResourceNotDiscoveredException(resource_type, id)
|
||||||
|
|
||||||
# Get the item:
|
# Get the item:
|
||||||
item = RESOURCE_MAP[type].get_config_resource(id, backend_region=backend_region)
|
item = RESOURCE_MAP[resource_type].get_config_resource(id, backend_region=backend_region)
|
||||||
if not item:
|
if not item:
|
||||||
raise ResourceNotDiscoveredException(type, id)
|
raise ResourceNotDiscoveredException(resource_type, id)
|
||||||
|
|
||||||
item['accountId'] = DEFAULT_ACCOUNT_ID
|
item['accountId'] = DEFAULT_ACCOUNT_ID
|
||||||
|
|
||||||
return {'configurationItems': [item]}
|
return {'configurationItems': [item]}
|
||||||
|
|
||||||
def batch_get_resource_config(self, resource_keys, backend_region):
|
def batch_get_resource_config(self, resource_keys, backend_region):
|
||||||
"""Returns the configuration of an item in the AWS Config format of the resource for the current regional backend."""
|
"""Returns the configuration of an item in the AWS Config format of the resource for the current regional backend.
|
||||||
|
|
||||||
|
:param resource_keys:
|
||||||
|
:param backend_region:
|
||||||
|
"""
|
||||||
# Can't have more than 100 items
|
# Can't have more than 100 items
|
||||||
pass
|
if len(resource_keys) > 100:
|
||||||
|
raise TooManyResourceKeys(['com.amazonaws.starling.dove.ResourceKey@12345'] * len(resource_keys))
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for resource in resource_keys:
|
||||||
|
# Does the resource type exist?
|
||||||
|
if not RESOURCE_MAP.get(resource['resourceType']):
|
||||||
|
# Not found so skip.
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Is the resource type global?
|
||||||
|
if RESOURCE_MAP[resource['resourceType']].backends.get('global'):
|
||||||
|
backend_region = 'global'
|
||||||
|
|
||||||
|
# If the backend region isn't implemented then we won't find the item:
|
||||||
|
if not RESOURCE_MAP[resource['resourceType']].backends.get(backend_region):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Get the item:
|
||||||
|
item = RESOURCE_MAP[resource['resourceType']].get_config_resource(resource['resourceId'], backend_region=backend_region)
|
||||||
|
if not item:
|
||||||
|
continue
|
||||||
|
|
||||||
|
item['accountId'] = DEFAULT_ACCOUNT_ID
|
||||||
|
|
||||||
|
results.append(item)
|
||||||
|
|
||||||
|
return {'baseConfigurationItems': results, 'unprocessedResourceKeys': []} # At this time, moto is not adding unprocessed items.
|
||||||
|
|
||||||
|
def batch_get_aggregate_resource_config(self, aggregator_name, resource_identifiers):
|
||||||
|
"""Returns the configuration of an item in the AWS Config format of the resource for the current regional backend.
|
||||||
|
|
||||||
|
As far a moto goes -- the only real difference between this function and the `batch_get_resource_config` function is that
|
||||||
|
this will require a Config Aggregator be set up a priori and can search based on resource regions.
|
||||||
|
|
||||||
|
Note: moto will IGNORE the resource account ID in the search query.
|
||||||
|
"""
|
||||||
|
if not self.config_aggregators.get(aggregator_name):
|
||||||
|
raise NoSuchConfigurationAggregatorException()
|
||||||
|
|
||||||
|
# Can't have more than 100 items
|
||||||
|
if len(resource_identifiers) > 100:
|
||||||
|
raise TooManyResourceKeys(['com.amazonaws.starling.dove.AggregateResourceIdentifier@12345'] * len(resource_identifiers))
|
||||||
|
|
||||||
|
found = []
|
||||||
|
not_found = []
|
||||||
|
for identifier in resource_identifiers:
|
||||||
|
resource_type = identifier['ResourceType']
|
||||||
|
resource_region = identifier['SourceRegion']
|
||||||
|
resource_id = identifier['ResourceId']
|
||||||
|
resource_name = identifier.get('ResourceName', None)
|
||||||
|
|
||||||
|
# Does the resource type exist?
|
||||||
|
if not RESOURCE_MAP.get(resource_type):
|
||||||
|
not_found.append(identifier)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Get the item:
|
||||||
|
item = RESOURCE_MAP[resource_type].get_config_resource(resource_id, resource_name=resource_name,
|
||||||
|
resource_region=resource_region)
|
||||||
|
if not item:
|
||||||
|
not_found.append(identifier)
|
||||||
|
continue
|
||||||
|
|
||||||
|
item['accountId'] = DEFAULT_ACCOUNT_ID
|
||||||
|
|
||||||
|
found.append(item)
|
||||||
|
|
||||||
|
return {'BaseConfigurationItems': found, 'UnprocessedResourceIdentifiers': not_found}
|
||||||
|
|
||||||
|
|
||||||
config_backends = {}
|
config_backends = {}
|
||||||
|
@ -108,12 +108,12 @@ class ConfigResponse(BaseResponse):
|
|||||||
self.region)
|
self.region)
|
||||||
return json.dumps(schema)
|
return json.dumps(schema)
|
||||||
|
|
||||||
"""
|
|
||||||
def batch_get_resource_config(self):
|
def batch_get_resource_config(self):
|
||||||
# TODO implement me!
|
schema = self.config_backend.batch_get_resource_config(self._get_param('resourceKeys'),
|
||||||
return ""
|
self.region)
|
||||||
|
return json.dumps(schema)
|
||||||
|
|
||||||
def batch_get_aggregate_resource_config(self):
|
def batch_get_aggregate_resource_config(self):
|
||||||
# TODO implement me!
|
schema = self.config_backend.batch_get_aggregate_resource_config(self._get_param('ConfigurationAggregatorName'),
|
||||||
return ""
|
self._get_param('ResourceIdentifiers'))
|
||||||
"""
|
return json.dumps(schema)
|
||||||
|
@ -1216,3 +1216,127 @@ def test_get_resource_config_history():
|
|||||||
assert len(result) == 1
|
assert len(result) == 1
|
||||||
assert result[0]['resourceName'] == result[0]['resourceId'] == 'bucket1'
|
assert result[0]['resourceName'] == result[0]['resourceId'] == 'bucket1'
|
||||||
assert result[0]['arn'] == 'arn:aws:s3:::bucket1'
|
assert result[0]['arn'] == 'arn:aws:s3:::bucket1'
|
||||||
|
|
||||||
|
|
||||||
|
@mock_config
|
||||||
|
@mock_s3
|
||||||
|
def test_batch_get_resource_config():
|
||||||
|
"""NOTE: We are only really testing the Config part. For each individual service, please add tests
|
||||||
|
for that individual service's "get_config_resource" function.
|
||||||
|
"""
|
||||||
|
client = boto3.client('config', region_name='us-west-2')
|
||||||
|
|
||||||
|
# With more than 100 resourceKeys:
|
||||||
|
with assert_raises(ClientError) as ce:
|
||||||
|
client.batch_get_resource_config(resourceKeys=[{'resourceType': 'AWS::S3::Bucket', 'resourceId': 'someBucket'}] * 101)
|
||||||
|
assert 'Member must have length less than or equal to 100' in ce.exception.response['Error']['Message']
|
||||||
|
|
||||||
|
# With invalid resource types and resources that don't exist:
|
||||||
|
result = client.batch_get_resource_config(resourceKeys=[
|
||||||
|
{'resourceType': 'NOT::A::RESOURCE', 'resourceId': 'NotAThing'}, {'resourceType': 'AWS::S3::Bucket', 'resourceId': 'NotAThing'},
|
||||||
|
])
|
||||||
|
|
||||||
|
assert not result['baseConfigurationItems']
|
||||||
|
assert not result['unprocessedResourceKeys']
|
||||||
|
|
||||||
|
# Create some S3 buckets:
|
||||||
|
s3_client = boto3.client('s3', region_name='us-west-2')
|
||||||
|
for x in range(0, 10):
|
||||||
|
s3_client.create_bucket(Bucket='bucket{}'.format(x), CreateBucketConfiguration={'LocationConstraint': 'us-west-2'})
|
||||||
|
|
||||||
|
# Get them all:
|
||||||
|
keys = [{'resourceType': 'AWS::S3::Bucket', 'resourceId': 'bucket{}'.format(x)} for x in range(0, 10)]
|
||||||
|
result = client.batch_get_resource_config(resourceKeys=keys)
|
||||||
|
assert len(result['baseConfigurationItems']) == 10
|
||||||
|
buckets_missing = ['bucket{}'.format(x) for x in range(0, 10)]
|
||||||
|
for r in result['baseConfigurationItems']:
|
||||||
|
buckets_missing.remove(r['resourceName'])
|
||||||
|
|
||||||
|
assert not buckets_missing
|
||||||
|
|
||||||
|
|
||||||
|
@mock_config
|
||||||
|
@mock_s3
|
||||||
|
def test_batch_get_aggregate_resource_config():
|
||||||
|
"""NOTE: We are only really testing the Config part. For each individual service, please add tests
|
||||||
|
for that individual service's "get_config_resource" function.
|
||||||
|
"""
|
||||||
|
from moto.config.models import DEFAULT_ACCOUNT_ID
|
||||||
|
client = boto3.client('config', region_name='us-west-2')
|
||||||
|
|
||||||
|
# Without an aggregator:
|
||||||
|
bad_ri = {'SourceAccountId': '000000000000', 'SourceRegion': 'not-a-region', 'ResourceType': 'NOT::A::RESOURCE', 'ResourceId': 'nope'}
|
||||||
|
with assert_raises(ClientError) as ce:
|
||||||
|
client.batch_get_aggregate_resource_config(ConfigurationAggregatorName='lolno', ResourceIdentifiers=[bad_ri])
|
||||||
|
assert 'The configuration aggregator does not exist' in ce.exception.response['Error']['Message']
|
||||||
|
|
||||||
|
# Create the aggregator:
|
||||||
|
account_aggregation_source = {
|
||||||
|
'AccountIds': [
|
||||||
|
'012345678910',
|
||||||
|
'111111111111',
|
||||||
|
'222222222222'
|
||||||
|
],
|
||||||
|
'AllAwsRegions': True
|
||||||
|
}
|
||||||
|
client.put_configuration_aggregator(
|
||||||
|
ConfigurationAggregatorName='testing',
|
||||||
|
AccountAggregationSources=[account_aggregation_source]
|
||||||
|
)
|
||||||
|
|
||||||
|
# With more than 100 items:
|
||||||
|
with assert_raises(ClientError) as ce:
|
||||||
|
client.batch_get_aggregate_resource_config(ConfigurationAggregatorName='testing', ResourceIdentifiers=[bad_ri] * 101)
|
||||||
|
assert 'Member must have length less than or equal to 100' in ce.exception.response['Error']['Message']
|
||||||
|
|
||||||
|
# Create some S3 buckets:
|
||||||
|
s3_client = boto3.client('s3', region_name='us-west-2')
|
||||||
|
for x in range(0, 10):
|
||||||
|
s3_client.create_bucket(Bucket='bucket{}'.format(x), CreateBucketConfiguration={'LocationConstraint': 'us-west-2'})
|
||||||
|
|
||||||
|
s3_client_eu = boto3.client('s3', region_name='eu-west-1')
|
||||||
|
for x in range(10, 12):
|
||||||
|
s3_client_eu.create_bucket(Bucket='eu-bucket{}'.format(x), CreateBucketConfiguration={'LocationConstraint': 'eu-west-1'})
|
||||||
|
|
||||||
|
# Now try with resources that exist and ones that don't:
|
||||||
|
identifiers = [{'SourceAccountId': DEFAULT_ACCOUNT_ID, 'SourceRegion': 'us-west-2', 'ResourceType': 'AWS::S3::Bucket',
|
||||||
|
'ResourceId': 'bucket{}'.format(x)} for x in range(0, 10)]
|
||||||
|
identifiers += [{'SourceAccountId': DEFAULT_ACCOUNT_ID, 'SourceRegion': 'eu-west-1', 'ResourceType': 'AWS::S3::Bucket',
|
||||||
|
'ResourceId': 'eu-bucket{}'.format(x)} for x in range(10, 12)]
|
||||||
|
identifiers += [bad_ri]
|
||||||
|
|
||||||
|
result = client.batch_get_aggregate_resource_config(ConfigurationAggregatorName='testing', ResourceIdentifiers=identifiers)
|
||||||
|
assert len(result['UnprocessedResourceIdentifiers']) == 1
|
||||||
|
assert result['UnprocessedResourceIdentifiers'][0] == bad_ri
|
||||||
|
|
||||||
|
# Verify all the buckets are there:
|
||||||
|
assert len(result['BaseConfigurationItems']) == 12
|
||||||
|
missing_buckets = ['bucket{}'.format(x) for x in range(0, 10)] + ['eu-bucket{}'.format(x) for x in range(10, 12)]
|
||||||
|
|
||||||
|
for r in result['BaseConfigurationItems']:
|
||||||
|
missing_buckets.remove(r['resourceName'])
|
||||||
|
|
||||||
|
assert not missing_buckets
|
||||||
|
|
||||||
|
# Verify that if the resource name and ID are correct that things are good:
|
||||||
|
identifiers = [{'SourceAccountId': DEFAULT_ACCOUNT_ID, 'SourceRegion': 'us-west-2', 'ResourceType': 'AWS::S3::Bucket',
|
||||||
|
'ResourceId': 'bucket1', 'ResourceName': 'bucket1'}]
|
||||||
|
result = client.batch_get_aggregate_resource_config(ConfigurationAggregatorName='testing', ResourceIdentifiers=identifiers)
|
||||||
|
assert not result['UnprocessedResourceIdentifiers']
|
||||||
|
assert len(result['BaseConfigurationItems']) == 1 and result['BaseConfigurationItems'][0]['resourceName'] == 'bucket1'
|
||||||
|
|
||||||
|
# Verify that if the resource name and ID mismatch that we don't get a result:
|
||||||
|
identifiers = [{'SourceAccountId': DEFAULT_ACCOUNT_ID, 'SourceRegion': 'us-west-2', 'ResourceType': 'AWS::S3::Bucket',
|
||||||
|
'ResourceId': 'bucket1', 'ResourceName': 'bucket2'}]
|
||||||
|
result = client.batch_get_aggregate_resource_config(ConfigurationAggregatorName='testing', ResourceIdentifiers=identifiers)
|
||||||
|
assert not result['BaseConfigurationItems']
|
||||||
|
assert len(result['UnprocessedResourceIdentifiers']) == 1
|
||||||
|
assert len(result['UnprocessedResourceIdentifiers']) == 1 and result['UnprocessedResourceIdentifiers'][0]['ResourceName'] == 'bucket2'
|
||||||
|
|
||||||
|
# Verify that if the region is incorrect that we don't get a result:
|
||||||
|
identifiers = [{'SourceAccountId': DEFAULT_ACCOUNT_ID, 'SourceRegion': 'eu-west-1', 'ResourceType': 'AWS::S3::Bucket',
|
||||||
|
'ResourceId': 'bucket1'}]
|
||||||
|
result = client.batch_get_aggregate_resource_config(ConfigurationAggregatorName='testing', ResourceIdentifiers=identifiers)
|
||||||
|
assert not result['BaseConfigurationItems']
|
||||||
|
assert len(result['UnprocessedResourceIdentifiers']) == 1
|
||||||
|
assert len(result['UnprocessedResourceIdentifiers']) == 1 and result['UnprocessedResourceIdentifiers'][0]['SourceRegion'] == 'eu-west-1'
|
||||||
|
Loading…
Reference in New Issue
Block a user