From 4ad9bc96372251fcba5f51f6242fb4d97a11f3e4 Mon Sep 17 00:00:00 2001 From: Aditya Manthramurthy Date: Wed, 14 Sep 2016 17:52:05 -0700 Subject: [PATCH] Implement Bucket Notifications APIs (#404) --- docs/API.md | 195 ++++++++++- minio/api.py | 72 +++- minio/helpers.py | 140 ++++++++ minio/parsers.py | 71 ++++ minio/xml_marshal.py | 137 +++++++- tests/unit/set_bucket_notification_test.py | 377 +++++++++++++++++++++ 6 files changed, 984 insertions(+), 8 deletions(-) create mode 100644 tests/unit/set_bucket_notification_test.py diff --git a/docs/API.md b/docs/API.md index 94017a1d0..9da0e22e0 100644 --- a/docs/API.md +++ b/docs/API.md @@ -40,6 +40,9 @@ s3Client = Minio('s3.amazonaws.com', |[`list_incomplete_uploads`](#list_incomplete_uploads) | [`fput_object`](#fput_object) | | | [`get_bucket_policy`](#get_bucket_policy) |[`fget_object`](#fget_object) | | | [`set_bucket_policy`](#set_bucket_policy) | [`get_partial_object`](#get_partial_object) | | +| [`get_bucket_notification`](#get_bucket_notification) | | +| [`set_bucket_notification`](#set_bucket_notification) | | +| [`remove_all_bucket_notifications`](#remove_all_bucket_notifications) | | ## 1. Constructor @@ -232,7 +235,7 @@ __Parameters__ |Param |Type |Description | |:---|:---|:---| -|``bucketname`` | _string_|Name of the bucket.| +|``bucket_name`` | _string_|Name of the bucket.| |``prefix`` |_string_ |The prefix of the incomplete objects uploaded should be listed. | |``recursive`` |_bool_ |``True`` indicates recursive style listing and ``False`` indicates directory style listing delimited by '/'. Optional default is ``False``. | @@ -270,7 +273,7 @@ __Parameters__ |Param |Type |Description | |:---|:---|:---| -|``bucketname`` | _string_ |Name of the bucket.| +|``bucket_name`` | _string_ |Name of the bucket.| |``prefix`` |_string_ |The prefix of objects to get current policy. | __Return Value__ @@ -302,7 +305,7 @@ __Parameters__ |Param |Type |Description | |:---|:---|:---| -|``bucketname`` | _string_ |Name of the bucket.| +|``bucket_name`` | _string_ |Name of the bucket.| |``prefix`` |_string_ |The prefix of objects to get current policy. | |``Policy`` | _minio.policy.Policy_ |Policy enum. Policy.READ_ONLY,Policy.WRITE_ONLY,Policy.READ_WRITE or Policy.NONE. | @@ -319,6 +322,192 @@ minioClient.set_bucket_policy('mybucket', ``` + +### get_bucket_notification(bucket_name) + +Fetch the notifications configuration on a bucket. + +__Parameters__ + +|Param |Type |Description | +|:---|:---|:---| +|``bucket_name`` | _string_ |Name of the bucket.| + +__Return Value__ + +|Param |Type |Description | +|:---|:---|:---| +|``notification`` | _dict_ | If there is no notification configuration, an empty dictionary is returned. Otherwise it has the same structure as the argument to set_bucket_notification | + +__Example__ + + +```py + +# Get the notifications configuration for a bucket. +notification = minioClient.get_bucket_notification('mybucket') +# If no notification is present on the bucket: +# notification == {} + +``` + + +### set_bucket_notification(bucket_name, notification) + +Set notification configuration on a bucket. + +__Parameters__ + +|Param |Type |Description | +|:---|:---|:---| +|``bucket_name`` | _string_ |Name of the bucket.| +|``notification`` | _dict_ |Non-empty dictionary with the structure specified below.| + +The `notification` argument has the following structure: + +* (dict) -- + * __TopicConfigurations__ (list) -- Optional list of service + configuration items specifying AWS SNS Topics as the target of the + notification. + * __QueueConfigurations__ (list) -- Optional list of service + configuration items specifying AWS SQS Queues as the target of the + notification. + * __CloudFunctionconfigurations__ (list) -- Optional list of service + configuration items specifying AWS Lambda Cloud functions as the + target of the notification. + +At least one of the above items needs to be specified in the +`notification` argument. + +The "service configuration item" alluded to above has the following structure: + +* (dict) -- + * __Id__ (string) -- Optional Id for the configuration item. If not + specified, it is auto-generated by the server. + * __Arn__ (string) -- Specifies the particular Topic/Queue/Cloud + Function identifier. + * __Events__ (list) -- A non-empty list of event-type strings from: + _'s3:ReducedRedundancyLostObject'_, + _'s3:ObjectCreated:*'_, + _'s3:ObjectCreated:Put'_, + _'s3:ObjectCreated:Post'_, + _'s3:ObjectCreated:Copy'_, + _'s3:ObjectCreated:CompleteMultipartUpload'_, + _'s3:ObjectRemoved:*'_, + _'s3:ObjectRemoved:Delete'_, + _'s3:ObjectRemoved:DeleteMarkerCreated'_ + * __Filter__ (dict) -- An optional dictionary container of object + key name based filter rules. + * __Key__ (dict) -- Dictionary container of object key name prefix + and suffix filtering rules. + * __FilterRules__ (list) -- A list of containers that specify + the criteria for the filter rule. + * (dict) -- A dictionary container of key value pairs that + specify a single filter rule. + * __Name__ (string) -- Object key name with value 'prefix' + or 'suffix'. + * __Value__ (string) -- Specify the value of the + prefix/suffix to which the rule applies. + + +There is no return value. If there are errors from the target +server/service, a `ResponseError` is thrown. If there are validation +errors, `InvalidArgumentError` or `TypeError` may be thrown. The input +configuration cannot be empty - to delete the notification +configuration on a bucket, use the `remove_all_bucket_notifications()` +API. + +__Example__ + + +```py + +notification = { + 'QueueConfigurations': [ + { + 'Id': '1', + 'Arn': 'arn1', + 'Events': ['s3:ObjectCreated:PutObject'], + 'Filter': { + 'Key': { + 'FilterRules': [ + { + 'Name': 'prefix', + 'Value': 'abc' + } + ] + } + } + } + ], + 'TopicConfigurations': [ + { + 'Arn': 'arn2', + 'Events': ['s3:ObjectCreated:PostObject'], + 'Filter': { + 'Key': { + 'FilterRules': [ + { + 'Name': 'suffix', + 'Value': '.jpg' + } + ] + } + } + } + ], + 'CloudFunctionConfigurations': [ + { + 'Arn': 'arn3', + 'Events': ['s3:ObjectCreated:DeleteObject'], + 'Filter': { + 'Key': { + 'FilterRules': [ + { + 'Name': 'suffix', + 'Value': '.jpg' + } + ] + } + } + } + ] +} + +try: + minioClient.set_bucket_notification('mybucket', notification) +except ResponseError: + # handle error response from service. + pass +except (ArgumentError, TypeError): + # should happen only during development. Fix the notification argument + pass +``` + + +### remove_all_bucket_notifications(bucket_name) + +Remove all notifications configured on the bucket. + +__Parameters__ + +|Param |Type |Description | +|:---|:---|:---| +|``bucket_name`` | _string_ |Name of the bucket.| + +There is no returned value. A `ResponseError` exception is thrown if +the operation did not complete successfully. + +__Example__ + + +```py + +# Get the notifications configuration for a bucket. +minioClient.remove_all_bucket_notifications('mybucket') + +``` + ## 3. Object operations ### get_object(bucket_name, object_name) diff --git a/minio/api.py b/minio/api.py index 51c96430e..ba78a43be 100644 --- a/minio/api.py +++ b/minio/api.py @@ -53,12 +53,14 @@ parse_list_multipart_uploads, parse_new_multipart_upload, parse_location_constraint, - parse_multipart_upload_result) + parse_multipart_upload_result, + parse_get_bucket_notification) from .helpers import (get_target_url, is_non_empty_string, is_valid_endpoint, get_sha256, encode_to_base64, get_md5, optimal_part_info, encode_to_hex, is_valid_bucket_name, parts_manager, + is_valid_bucket_notification_config, mkdir_p, dump_http) from .helpers import (MAX_MULTIPART_OBJECT_SIZE, MIN_OBJECT_SIZE) @@ -66,7 +68,8 @@ generate_credential_string, post_presign_signature, _SIGN_V4_ALGORITHM) from .xml_marshal import (xml_marshal_bucket_constraint, - xml_marshal_complete_multipart_upload) + xml_marshal_complete_multipart_upload, + xml_marshal_bucket_notifications) from .limited_reader import LimitedReader from . import policy @@ -381,6 +384,71 @@ def set_bucket_policy(self, bucket_name, prefix, policy_access): body=content, content_sha256=content_sha256_hex) + def get_bucket_notification(self, bucket_name): + """ + Get notifications configured for the given bucket. + + :param bucket_name: Bucket name. + """ + is_valid_bucket_name(bucket_name) + + response = self._url_open( + "GET", + bucket_name=bucket_name, + query={"notification": ""}, + headers={} + ) + data = response.read().decode('utf-8') + return parse_get_bucket_notification(data) + + def set_bucket_notification(self, bucket_name, notifications): + """ + Set the given notifications on the bucket. + + :param bucket_name: Bucket name. + :param notifications: Notifications structure + """ + is_valid_bucket_name(bucket_name) + is_valid_bucket_notification_config(notifications) + + content = xml_marshal_bucket_notifications(notifications) + headers = { + 'Content-Length': str(len(content)), + 'Content-MD5': encode_to_base64(get_md5(content)) + } + content_sha256_hex = encode_to_hex(get_sha256(content)) + self._url_open( + 'PUT', + bucket_name=bucket_name, + query={"notification": ""}, + headers=headers, + body=content, + content_sha256=content_sha256_hex + ) + + def remove_all_bucket_notifications(self, bucket_name): + """ + Remove all notifications configured on the bucket.' + + :param bucket_name: Bucket name. + """ + is_valid_bucket_name(bucket_name) + + content_bytes = xml_marshal_bucket_notifications({}) + headers = { + 'Content-Length': str(len(content_bytes)), + 'Content-MD5': encode_to_base64(get_md5(content_bytes)) + } + content_sha256_hex = encode_to_hex(get_sha256(content_bytes)) + self._url_open( + 'PUT', + bucket_name=bucket_name, + query={"notification": ""}, + headers=headers, + body=content_bytes, + content_sha256=content_sha256_hex + ) + def _get_upload_id(self, bucket_name, object_name, content_type): """ Get previously uploaded upload id for object name or initiate a request to diff --git a/minio/helpers.py b/minio/helpers.py index 1faac84b5..cab3bd232 100644 --- a/minio/helpers.py +++ b/minio/helpers.py @@ -379,6 +379,146 @@ def is_non_empty_string(input_string): return True +def is_valid_bucket_notification_config(notifications): + """ + Validate the notifications config structure + + :param notifications: Dictionary with specific structure. + :return: True if input is a valid bucket notifications structure. + Raise :exc:`InvalidArgumentError` otherwise. + """ + # check if notifications is a dict. + if not isinstance(notifications, dict): + raise TypeError('notifications configuration must be a dictionary') + + if len(notifications) == 0: + raise InvalidArgumentError( + 'notifications configuration may not be empty' + ) + + VALID_NOTIFICATION_KEYS = set([ + "TopicConfigurations", + "QueueConfigurations", + "CloudFunctionConfigurations", + ]) + + VALID_SERVICE_CONFIG_KEYS = set([ + 'Id', + 'Arn', + 'Events', + 'Filter', + ]) + + NOTIFICATION_EVENTS = set([ + 's3:ReducedRedundancyLostObject', + 's3:ObjectCreated:*', + 's3:ObjectCreated:Put', + 's3:ObjectCreated:Post', + 's3:ObjectCreated:Copy', + 's3:ObjectCreated:CompleteMultipartUpload', + 's3:ObjectRemoved:*', + 's3:ObjectRemoved:Delete', + 's3:ObjectRemoved:DeleteMarkerCreated', + ]) + + for key, value in notifications.items(): + # check if key names are valid + if key not in VALID_NOTIFICATION_KEYS: + raise InvalidArgumentError( + ('{} is an invalid key ' + 'for notifications configuration').format( + key + ) + ) + + # check if config values conform + # first check if value is a list + if not isinstance(value, list): + raise InvalidArgumentError( + ('The value for key "{}" in the notifications ' + 'configuration must be a list.').format(key) + ) + for service_config in value: + # check type matches + if not isinstance(service_config, dict): + raise InvalidArgumentError( + ('Each service configuration item for "{}" must be a ' + 'dictionary').format(key) + ) + # check keys are valid + for skey in service_config.keys(): + if skey not in VALID_SERVICE_CONFIG_KEYS: + raise InvalidArgumentError( + ('{} is an invalid key for a service ' + 'configuration item').format( + skey + ) + ) + # check for required keys + arn = service_config.get('Arn', '') + if arn == '': + raise InvalidArgumentError( + 'Arn key in service config must be present and has to be ' + 'non-empty string' + ) + events = service_config.get('Events', []) + if len(events) < 1: + raise InvalidArgumentError( + 'At least one event must be specified in a service config' + ) + if not isinstance(events, list): + raise InvalidArgumentError( + '"Events" must be a list of strings in a service ' + 'configuration' + ) + # check if 'Id' key is present, it should be a string + if not isinstance(service_config.get('Id', ''), str): + raise InvalidArgumentError( + '"Id" key must be a string' + ) + for event in events: + if event not in NOTIFICATION_EVENTS: + raise InvalidArgumentError( + '{} is not a valid event. Valid events are: {}'.format( + event, NOTIFICATION_EVENTS + ) + ) + if 'Filter' in service_config: + exception_msg = ( + '{} - If a Filter key is given, it must be a ' + 'dictionary, the dictionary must have the ' + 'key "Key", and its value must be an object, with ' + 'a key named "FilterRules" which must be a non-empty list.' + ).format( + service_config['Filter'] + ) + try: + filter_rules = service_config.get('Filter', {}).get( + 'Key', {}).get('FilterRules', []) + if not isinstance(filter_rules, list): + raise InvalidArgumentError(exception_msg) + if len(filter_rules) < 1: + raise InvalidArgumentError(exception_msg) + except AttributeError: + raise InvalidArgumentError(exception_msg) + for filter_rule in filter_rules: + try: + name = filter_rule['Name'] + value = filter_rule['Value'] + except KeyError: + raise InvalidArgumentError( + ('{} - a FilterRule dictionary must have "Name" ' + 'and "Value" keys').format(filter_rule) + ) + if name not in ['prefix', 'suffix']: + raise InvalidArgumentError( + ('{} - The "Name" key in a filter rule must be ' + 'either "prefix" or "suffix"').format(name) + ) + + return True + + def encode_object_name(object_name): """ URL encode input object name. diff --git a/minio/parsers.py b/minio/parsers.py index 76ee27a6f..317125820 100644 --- a/minio/parsers.py +++ b/minio/parsers.py @@ -38,6 +38,8 @@ from .compat import urldecode from .definitions import (Object, Bucket, IncompleteUpload, UploadPart, MultipartUploadResult) +from .xml_marshal import (NOTIFICATIONS_ARN_FIELDNAME_MAP) + if hasattr(cElementTree, 'ParseError'): _ETREE_EXCEPTIONS = (ParseError, AttributeError, ValueError, TypeError) @@ -339,3 +341,72 @@ def _iso8601_to_localized_time(date_string): parsed_date = datetime.strptime(date_string, '%Y-%m-%dT%H:%M:%S.%fZ') localized_time = pytz.utc.localize(parsed_date) return localized_time + +def parse_get_bucket_notification(data): + """ + Parser for a get_bucket_notification response from S3. + + :param data: Body of response from get_bucket_notification. + :return: Returns bucket notification configuration + """ + try: + root = cElementTree.fromstring(data) + except _ETREE_EXCEPTIONS as error: + raise InvalidXMLError('"GetBucketNotificationResult" XML is not parsable. ' + 'Message: {0}'.format(error.message)) + + # perhaps we could ignore this condition? + if root.tag != '{{{s3}}}NotificationConfiguration'.format(**_S3_NS): + raise InvalidXMLError('"GetBucketNotificationresult" XML root is ' + 'invalid.') + + notifications = _parse_add_notifying_service_config( + root, {}, + 'TopicConfigurations', 'TopicConfiguration' + ) + notifications = _parse_add_notifying_service_config( + root, notifications, + 'QueueConfigurations', 'QueueConfiguration' + ) + notifications = _parse_add_notifying_service_config( + root, notifications, + 'CloudFunctionConfigurations', 'CloudFunctionConfiguration' + ) + + return notifications + +def _parse_add_notifying_service_config(data, notifications, service_key, + service_xml_tag): + config = [] + stag = 's3:{}'.format(service_xml_tag) + for service in data.findall(stag, _S3_NS): + service_config = {} + service_config['Id'] = service.find('s3:Id', _S3_NS).text + arn_tag = 's3:{}'.format( + NOTIFICATIONS_ARN_FIELDNAME_MAP[service_xml_tag] + ) + service_config['Arn'] = service.find(arn_tag, _S3_NS).text + service_config['Events'] = [] + for event in service.findall('s3:Event', _S3_NS): + service_config['Events'].append(event.text) + xml_filter_rule = service.find('s3:Filter', _S3_NS) + if xml_filter_rule: + xml_filter_rules = xml_filter_rule.find( + 's3:S3Key', _S3_NS).findall('s3:FilterRule', _S3_NS) + filter_rules = [] + for xml_filter_rule in xml_filter_rules: + filter_rules.append( + { + 'Name': xml_filter_rule.find('s3:Name', _S3_NS), + 'Value': xml_filter_rule.find('s3:Value', _S3_NS), + } + ) + service_config['Filter'] = { + 'Key': { + 'FilterRules': filter_rules + } + } + config.append(service_config) + if config: + notifications[service_key] = config + return notifications diff --git a/minio/xml_marshal.py b/minio/xml_marshal.py index 2d7dbd96b..45495979e 100644 --- a/minio/xml_marshal.py +++ b/minio/xml_marshal.py @@ -34,8 +34,8 @@ def xml_marshal_bucket_constraint(region): """ - Marshal's bucket constraint based on *region* -. + Marshal's bucket constraint based on *region*. + :param region: Region name of a given bucket. :return: Marshalled XML data. """ @@ -50,7 +50,7 @@ def xml_marshal_bucket_constraint(region): def xml_marshal_complete_multipart_upload(uploaded_parts): """ Marshal's complete multipart upload request based on *uploaded_parts*. -. + :param uploaded_parts: List of all uploaded parts ordered in the way they were uploaded. :return: Marshalled XML data. @@ -66,3 +66,134 @@ def xml_marshal_complete_multipart_upload(uploaded_parts): s3_xml.ElementTree(root).write(data, encoding=None, xml_declaration=False) return data.getvalue() + +def xml_marshal_bucket_notifications(notifications): + """ + Marshals the notifications structure for sending to S3 compatible storage + + :param notifications: Dictionary with following structure: + + { + 'TopicConfigurations': [ + { + 'Id': 'string', + 'Arn': 'string', + 'Events': [ + 's3:ReducedRedundancyLostObject'|'s3:ObjectCreated:*'|'s3:ObjectCreated:Put'|'s3:ObjectCreated:Post'|'s3:ObjectCreated:Copy'|'s3:ObjectCreated:CompleteMultipartUpload'|'s3:ObjectRemoved:*'|'s3:ObjectRemoved:Delete'|'s3:ObjectRemoved:DeleteMarkerCreated', + ], + 'Filter': { + 'Key': { + 'FilterRules': [ + { + 'Name': 'prefix'|'suffix', + 'Value': 'string' + }, + ] + } + } + }, + ], + 'QueueConfigurations': [ + { + 'Id': 'string', + 'Arn': 'string', + 'Events': [ + 's3:ReducedRedundancyLostObject'|'s3:ObjectCreated:*'|'s3:ObjectCreated:Put'|'s3:ObjectCreated:Post'|'s3:ObjectCreated:Copy'|'s3:ObjectCreated:CompleteMultipartUpload'|'s3:ObjectRemoved:*'|'s3:ObjectRemoved:Delete'|'s3:ObjectRemoved:DeleteMarkerCreated', + ], + 'Filter': { + 'Key': { + 'FilterRules': [ + { + 'Name': 'prefix'|'suffix', + 'Value': 'string' + }, + ] + } + } + }, + ], + 'CloudFunctionConfigurations': [ + { + 'Id': 'string', + 'Arn': 'string', + 'Events': [ + 's3:ReducedRedundancyLostObject'|'s3:ObjectCreated:*'|'s3:ObjectCreated:Put'|'s3:ObjectCreated:Post'|'s3:ObjectCreated:Copy'|'s3:ObjectCreated:CompleteMultipartUpload'|'s3:ObjectRemoved:*'|'s3:ObjectRemoved:Delete'|'s3:ObjectRemoved:DeleteMarkerCreated', + ], + 'Filter': { + 'Key': { + 'FilterRules': [ + { + 'Name': 'prefix'|'suffix', + 'Value': 'string' + }, + ] + } + } + }, + ] + } + + :return: Marshalled XML data + """ + root = s3_xml.Element('NotificationConfiguration', {'xmlns': _S3_NAMESPACE}) + _add_notification_config_to_xml( + root, + 'TopicConfiguration', + notifications.get('TopicConfigurations', []) + ) + _add_notification_config_to_xml( + root, + 'QueueConfiguration', + notifications.get('QueueConfigurations', []) + ) + _add_notification_config_to_xml( + root, + 'CloudFunctionConfiguration', + notifications.get('CloudFunctionConfigurations', []) + ) + + data = io.BytesIO() + s3_xml.ElementTree(root).write(data, encoding=None, xml_declaration=False) + return data.getvalue() + +NOTIFICATIONS_ARN_FIELDNAME_MAP = { + 'TopicConfiguration': 'Topic', + 'QueueConfiguration': 'Queue', + 'CloudFunctionConfiguration': 'CloudFunction', +} + +def _add_notification_config_to_xml(node, element_name, configs): + """ + Internal function that builds the XML sub-structure for a given + kind of notification configuration. + + """ + for config in configs: + config_node = s3_xml.SubElement(node, element_name) + + if 'Id' in config: + id_node = s3_xml.SubElement(config_node, 'Id') + id_node.text = config['Id'] + + arn_node = s3_xml.SubElement( + config_node, + NOTIFICATIONS_ARN_FIELDNAME_MAP[element_name] + ) + arn_node.text = config['Arn'] + + for event in config['Events']: + event_node = s3_xml.SubElement(config_node, 'Event') + event_node.text = event + + filter_rules = config_node.get('Filter', {}).get( + 'Key', {}).get('FilterRules', []) + if filter_rules: + filter_node = s3_xml.SubElement(config_node, 'Filter') + s3key_node = s3_xml.SubElement(filter_node, 'S3Key') + for filter_rule in filter_rules: + filter_rule_node = s3_xml.SubElement(s3key_node, 'FilterRule') + name_node = s3_xml.SubElement(filter_rule_node, 'Name') + name_node.text = filter_rule['Name'] + value_node = s3_xml.SubElement(filter_rule_node, 'Value') + value_node.text = filter_rule['Value'] + return node diff --git a/tests/unit/set_bucket_notification_test.py b/tests/unit/set_bucket_notification_test.py new file mode 100644 index 000000000..3af451852 --- /dev/null +++ b/tests/unit/set_bucket_notification_test.py @@ -0,0 +1,377 @@ +# -*- coding: utf-8 -*- +# Minio Python Library for Amazon S3 Compatible Cloud Storage, (C) 2015 Minio, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import mock + +from unittest import TestCase +from nose.tools import raises + +from minio import Minio +from minio.api import _DEFAULT_USER_AGENT +from minio.error import InvalidArgumentError + +from .minio_mocks import MockResponse, MockConnection + +class SetBucketNotificationTest(TestCase): + @raises(TypeError) + def test_notification_is_dict_1(self): + client = Minio('localhost:9000') + client.set_bucket_notification('my-test-bucket', 'abc') + + @raises(TypeError) + def test_notification_is_dict_2(self): + client = Minio('localhost:9000') + client.set_bucket_notification('my-test-bucket', ['myconfig1']) + + @raises(InvalidArgumentError) + def test_notification_config_is_nonempty(self): + client = Minio('localhost:9000') + client.set_bucket_notification( + 'my-test-bucket', + {} + ) + + @raises(InvalidArgumentError) + def test_notification_config_has_valid_keys(self): + client = Minio('localhost:9000') + client.set_bucket_notification( + 'my-test-bucket', + { + 'QueueConfiguration': [ + { + 'Id': '1', + 'Arn': 'arn1', + 'Events': ['s3:ObjectCreated:*'], + } + ] + } + ) + + @raises(InvalidArgumentError) + def test_notification_config_arn_key_is_present(self): + client = Minio('localhost:9000') + client.set_bucket_notification( + 'my-test-bucket', + { + 'QueueConfigurations': [ + { + 'Id': '1', + 'Events': ['s3:ObjectCreated:*'], + } + ] + } + ) + + @raises(InvalidArgumentError) + def test_notification_config_id_key_is_string(self): + client = Minio('localhost:9000') + client.set_bucket_notification( + 'my-test-bucket', + { + 'QueueConfigurations': [ + { + 'Id': 1, + 'Arn': 'abc', + 'Events': ['s3:ObjectCreated:*'], + } + ] + } + ) + + @raises(InvalidArgumentError) + def test_notification_config_events_key_is_present(self): + client = Minio('localhost:9000') + client.set_bucket_notification( + 'my-test-bucket', + { + 'QueueConfigurations': [ + { + 'Id': '1', + 'Arn': 'arn1', + } + ] + } + ) + + @raises(InvalidArgumentError) + def test_notification_config_event_values_are_valid(self): + client = Minio('localhost:9000') + client.set_bucket_notification( + 'my-test-bucket', + { + 'QueueConfigurations': [ + { + 'Id': '1', + 'Arn': 'arn1', + 'Events': ['object_created'] + } + ] + } + ) + + @mock.patch('urllib3.PoolManager') + def test_notification_config_id_key_is_optional(self, mock_connection): + mock_server = MockConnection() + mock_connection.return_value = mock_server + mock_server.mock_add_request( + MockResponse( + 'PUT', + 'https://localhost:9000/my-test-bucket/?notification=', + { + 'Content-MD5': 'f+TfVp/A4pNnI7S4S+MkFg==', + 'Content-Length': '196', + 'User-Agent': _DEFAULT_USER_AGENT, + }, + 200, content="" + ) + ) + client = Minio('localhost:9000') + client.set_bucket_notification( + 'my-test-bucket', + { + 'QueueConfigurations': [ + { + 'Arn': 'arn1', + 'Events': ['s3:ObjectCreated:*'], + } + ] + } + ) + + @raises(InvalidArgumentError) + def test_notification_config_has_valid_event_names(self): + client = Minio('localhost:9000') + client.set_bucket_notification( + 'my-test-bucket', + { + 'QueueConfigurations': [ + { + 'Id': '1', + 'Arn': 'arn1', + 'Events': ['object_created'], + } + ] + } + ) + + @raises(InvalidArgumentError) + def test_notification_config_filterspec_is_valid_1(self): + client = Minio('localhost:9000') + client.set_bucket_notification( + 'my-test-bucket', + { + 'QueueConfigurations': [ + { + 'Id': '1', + 'Arn': 'arn1', + 'Events': ['s3:ObjectCreated:*'], + 'Filter': [] + } + ] + } + ) + + @raises(InvalidArgumentError) + def test_notification_config_filterspec_is_valid_2(self): + client = Minio('localhost:9000') + client.set_bucket_notification( + 'my-test-bucket', + { + 'QueueConfigurations': [ + { + 'Id': '1', + 'Arn': 'arn1', + 'Events': ['s3:ObjectCreated:*'], + 'Filter': { + 'S3Key': { + } + } + } + ] + } + ) + + @raises(InvalidArgumentError) + def test_notification_config_filterspec_is_valid_3(self): + client = Minio('localhost:9000') + client.set_bucket_notification( + 'my-test-bucket', + { + 'QueueConfigurations': [ + { + 'Id': '1', + 'Arn': 'arn1', + 'Events': ['s3:ObjectCreated:*'], + 'Filter': { + 'Key': { + } + } + } + ] + } + ) + + @raises(InvalidArgumentError) + def test_notification_config_filterspec_is_valid_4(self): + client = Minio('localhost:9000') + client.set_bucket_notification( + 'my-test-bucket', + { + 'QueueConfigurations': [ + { + 'Id': '1', + 'Arn': 'arn1', + 'Events': ['s3:ObjectCreated:*'], + 'Filter': { + 'Key': { + 'FilterRules': [] + } + } + } + ] + } + ) + + @raises(InvalidArgumentError) + def test_notification_config_filterspec_is_valid_5(self): + client = Minio('localhost:9000') + client.set_bucket_notification( + 'my-test-bucket', + { + 'QueueConfigurations': [ + { + 'Id': '1', + 'Arn': 'arn1', + 'Events': ['s3:ObjectCreated:*'], + 'Filter': { + 'Key': { + 'FilterRules': [ + { + 'rule1': 'ab', + 'val1': 'abc' + } + ] + } + } + } + ] + } + ) + + @raises(InvalidArgumentError) + def test_notification_config_filterspec_is_valid_6(self): + client = Minio('localhost:9000') + client.set_bucket_notification( + 'my-test-bucket', + { + 'QueueConfigurations': [ + { + 'Id': '1', + 'Arn': 'arn1', + 'Events': ['s3:ObjectCreated:*'], + 'Filter': { + 'Key': { + 'FilterRules': [ + { + 'Name': 'ab', + 'Value': 'abc' + } + ] + } + } + } + ] + } + ) + + @mock.patch('urllib3.PoolManager') + def test_notification_config_filterspec_is_valid_7(self, mock_connection): + mock_server = MockConnection() + mock_connection.return_value = mock_server + mock_server.mock_add_request( + MockResponse( + 'PUT', + 'https://localhost:9000/my-test-bucket/?notification=', + { + 'Content-MD5': 'AGCNfbD5OuiyIJFd+r67MA==', + 'User-Agent': _DEFAULT_USER_AGENT, + 'Content-Length': '206', + }, + 200, content="" + ) + ) + client = Minio('localhost:9000') + client.set_bucket_notification( + 'my-test-bucket', + { + 'QueueConfigurations': [ + { + 'Id': '1', + 'Arn': 'arn1', + 'Events': ['s3:ObjectCreated:*'], + 'Filter': { + 'Key': { + 'FilterRules': [ + { + 'Name': 'prefix', + 'Value': 'abc' + } + ] + } + } + } + ] + } + ) + + @mock.patch('urllib3.PoolManager') + def test_notification_config_filterspec_is_valid_8(self, mock_connection): + mock_server = MockConnection() + mock_connection.return_value = mock_server + mock_server.mock_add_request( + MockResponse( + 'PUT', + 'https://localhost:9000/my-test-bucket/?notification=', + { + 'Content-Length': '206', + 'Content-MD5': 'AGCNfbD5OuiyIJFd+r67MA==', + 'User-Agent': _DEFAULT_USER_AGENT, + }, + 200, content="" + ) + ) + client = Minio('localhost:9000') + client.set_bucket_notification( + 'my-test-bucket', + { + 'QueueConfigurations': [ + { + 'Id': '1', + 'Arn': 'arn1', + 'Events': ['s3:ObjectCreated:*'], + 'Filter': { + 'Key': { + 'FilterRules': [ + { + 'Name': 'suffix', + 'Value': 'abc' + } + ] + } + } + } + ] + } + )