diff --git a/hacking/aws_config/testing_policies/storage-policy.json b/hacking/aws_config/testing_policies/storage-policy.json index 06fbf0069e7..e48aad64c83 100644 --- a/hacking/aws_config/testing_policies/storage-policy.json +++ b/hacking/aws_config/testing_policies/storage-policy.json @@ -13,6 +13,7 @@ "s3:GetBucketVersioning", "s3:GetEncryptionConfiguration", "s3:GetObject", + "s3:GetBucketNotification", "s3:HeadBucket", "s3:ListBucket", "s3:PutBucketAcl", @@ -22,7 +23,8 @@ "s3:PutBucketVersioning", "s3:PutEncryptionConfiguration", "s3:PutObject", - "s3:PutObjectAcl" + "s3:PutObjectAcl", + "s3:PutBucketNotification" ], "Effect": "Allow", "Resource": [ diff --git a/lib/ansible/modules/cloud/amazon/s3_bucket_notification.py b/lib/ansible/modules/cloud/amazon/s3_bucket_notification.py new file mode 100644 index 00000000000..66e2f993405 --- /dev/null +++ b/lib/ansible/modules/cloud/amazon/s3_bucket_notification.py @@ -0,0 +1,267 @@ +#!/usr/bin/python +# (c) 2019, XLAB d.o.o +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = ''' +--- +module: s3_bucket_notification +short_description: Creates, updates or deletes S3 Bucket notification for lambda +description: + - This module allows the management of AWS Lambda function bucket event mappings via the + Ansible framework. Use module M(lambda) to manage the lambda function itself, M(lambda_alias) + to manage function aliases and M(lambda_policy) to modify lambda permissions. +notes: + - This module heavily depends on M(lambda_policy) as you need to allow C(lambda:InvokeFunction) + permission for your lambda function. +version_added: "2.9" + +author: + - XLAB d.o.o. (@xlab-si) + - Aljaz Kosir (@aljazkosir) + - Miha Plesko (@miha-plesko) +options: + event_name: + description: + - unique name for event notification on bucket + required: True + type: str + lambda_function_arn: + description: + - The ARN of the lambda function. + aliases: ['function_arn'] + type: str + bucket_name: + description: + - S3 bucket name + required: True + type: str + state: + description: + - Describes the desired state. + required: true + default: "present" + choices: ["present", "absent"] + type: str + lambda_alias: + description: + - Name of the Lambda function alias. Mutually exclusive with C(lambda_version). + required: false + type: str + lambda_version: + description: + - Version of the Lambda function. Mutually exclusive with C(lambda_alias). + required: false + type: int + events: + description: + - Events that you want to be triggering notifications. You can select multiple events to send + to the same destination, you can set up different events to send to different destinations, + and you can set up a prefix or suffix for an event. However, for each bucket, + individual events cannot have multiple configurations with overlapping prefixes or + suffixes that could match the same object key. + required: True + choices: ['s3:ObjectCreated:*', 's3:ObjectCreated:Put', 's3:ObjectCreated:Post', + 's3:ObjectCreated:Copy', 's3:ObjectCreated:CompleteMultipartUpload', + 's3:ObjectRemoved:*', 's3:ObjectRemoved:Delete', + 's3:ObjectRemoved:DeleteMarkerCreated', 's3:ObjectRestore:Post', + 's3:ObjectRestore:Completed', 's3:ReducedRedundancyLostObject'] + type: list + prefix: + description: + - Optional prefix to limit the notifications to objects with keys that start with matching + characters. + required: false + type: str + suffix: + description: + - Optional suffix to limit the notifications to objects with keys that end with matching + characters. + required: false + type: str +requirements: + - boto3 +extends_documentation_fragment: + - aws + - ec2 +''' + +EXAMPLES = ''' +--- +# Example that creates a lambda event notification for a bucket +- hosts: localhost + gather_facts: no + tasks: + - name: Process jpg image + s3_bucket_notification: + state: present + event_name: on_file_add_or_remove + bucket_name: test-bucket + function_name: arn:aws:lambda:us-east-2:526810320200:function:test-lambda + events: ["s3:ObjectCreated:*", "s3:ObjectRemoved:*"] + prefix: images/ + suffix: .jpg +''' + +RETURN = ''' +notification_configuration: + description: list of currently applied notifications + returned: success + type: list +''' + +from ansible.module_utils.aws.core import AnsibleAWSModule +from ansible.module_utils.ec2 import camel_dict_to_snake_dict + +try: + from botocore.exceptions import ClientError, BotoCoreError +except ImportError: + pass # will be protected by AnsibleAWSModule + + +class AmazonBucket: + def __init__(self, client, bucket_name): + self.client = client + self.bucket_name = bucket_name + self._full_config_cache = None + + def full_config(self): + if self._full_config_cache is None: + self._full_config_cache = [Config.from_api(cfg) for cfg in + self.client.get_bucket_notification_configuration( + Bucket=self.bucket_name).get( + 'LambdaFunctionConfigurations', list())] + return self._full_config_cache + + def current_config(self, config_name): + for config in self.full_config(): + if config.raw['Id'] == config_name: + return config + + def apply_config(self, desired): + configs = [cfg.raw for cfg in self.full_config() if cfg.name != desired.raw['Id']] + configs.append(desired.raw) + self._upload_bucket_config(configs) + return configs + + def delete_config(self, desired): + configs = [cfg.raw for cfg in self.full_config() if cfg.name != desired.raw['Id']] + self._upload_bucket_config(configs) + return configs + + def _upload_bucket_config(self, config): + self.client.put_bucket_notification_configuration( + Bucket=self.bucket_name, + NotificationConfiguration={ + 'LambdaFunctionConfigurations': config + }) + + +class Config: + def __init__(self, content): + self._content = content + self.name = content['Id'] + + @property + def raw(self): + return self._content + + def __eq__(self, other): + if other: + return self.raw == other.raw + return False + + @classmethod + def from_params(cls, **params): + function_arn = params['lambda_function_arn'] + + qualifier = None + if params['lambda_version'] > 0: + qualifier = str(params['lambda_version']) + elif params['lambda_alias']: + qualifier = str(params['lambda_alias']) + if qualifier: + params['lambda_function_arn'] = '{0}:{1}'.format(function_arn, qualifier) + + return cls({ + 'Id': params['event_name'], + 'LambdaFunctionArn': params['lambda_function_arn'], + 'Events': sorted(params['events']), + 'Filter': { + 'Key': { + 'FilterRules': [{ + 'Name': 'Prefix', + 'Value': params['prefix'] + }, { + 'Name': 'Suffix', + 'Value': params['suffix'] + }] + } + } + }) + + @classmethod + def from_api(cls, config): + return cls(config) + + +def main(): + event_types = ['s3:ObjectCreated:*', 's3:ObjectCreated:Put', 's3:ObjectCreated:Post', + 's3:ObjectCreated:Copy', 's3:ObjectCreated:CompleteMultipartUpload', + 's3:ObjectRemoved:*', 's3:ObjectRemoved:Delete', + 's3:ObjectRemoved:DeleteMarkerCreated', 's3:ObjectRestore:Post', + 's3:ObjectRestore:Completed', 's3:ReducedRedundancyLostObject'] + argument_spec = dict( + state=dict(default='present', choices=['present', 'absent']), + event_name=dict(required=True), + lambda_function_arn=dict(aliases=['function_arn']), + bucket_name=dict(required=True), + events=dict(type='list', default=[], choices=event_types), + prefix=dict(default=''), + suffix=dict(default=''), + lambda_alias=dict(), + lambda_version=dict(type='int', default=0), + ) + + module = AnsibleAWSModule( + argument_spec=argument_spec, + supports_check_mode=True, + mutually_exclusive=[['lambda_alias', 'lambda_version']], + required_if=[['state', 'present', ['events']]] + ) + + bucket = AmazonBucket(module.client('s3'), module.params['bucket_name']) + current = bucket.current_config(module.params['event_name']) + desired = Config.from_params(**module.params) + notification_configuration = [cfg.raw for cfg in bucket.full_config()] + + state = module.params['state'] + try: + if (state == 'present' and current == desired) or (state == 'absent' and not current): + changed = False + elif module.check_mode: + changed = True + elif state == 'present': + changed = True + notification_configuration = bucket.apply_config(desired) + elif state == 'absent': + changed = True + notification_configuration = bucket.delete_config(desired) + except (ClientError, BotoCoreError) as e: + module.fail_json(msg='{0}'.format(e)) + + module.exit_json(**dict(changed=changed, + notification_configuration=[camel_dict_to_snake_dict(cfg) for cfg in + notification_configuration])) + + +if __name__ == '__main__': + main() diff --git a/test/integration/targets/s3_bucket_notification/aliases b/test/integration/targets/s3_bucket_notification/aliases new file mode 100644 index 00000000000..6e3860bee23 --- /dev/null +++ b/test/integration/targets/s3_bucket_notification/aliases @@ -0,0 +1,2 @@ +cloud/aws +shippable/aws/group2 diff --git a/test/integration/targets/s3_bucket_notification/defaults/main.yml b/test/integration/targets/s3_bucket_notification/defaults/main.yml new file mode 100644 index 00000000000..d227210344f --- /dev/null +++ b/test/integration/targets/s3_bucket_notification/defaults/main.yml @@ -0,0 +1,3 @@ +--- +# defaults file for aws_lambda test +lambda_function_name: '{{resource_prefix}}' diff --git a/test/integration/targets/s3_bucket_notification/files/mini_lambda.py b/test/integration/targets/s3_bucket_notification/files/mini_lambda.py new file mode 100644 index 00000000000..0ba9e0d3009 --- /dev/null +++ b/test/integration/targets/s3_bucket_notification/files/mini_lambda.py @@ -0,0 +1,8 @@ +import json + + +def lambda_handler(event, context): + return { + 'statusCode': 200, + 'body': json.dumps('Hello from Lambda!') + } diff --git a/test/integration/targets/s3_bucket_notification/meta/main.yml b/test/integration/targets/s3_bucket_notification/meta/main.yml new file mode 100644 index 00000000000..1f64f1169a9 --- /dev/null +++ b/test/integration/targets/s3_bucket_notification/meta/main.yml @@ -0,0 +1,3 @@ +dependencies: + - prepare_tests + - setup_ec2 diff --git a/test/integration/targets/s3_bucket_notification/tasks/main.yml b/test/integration/targets/s3_bucket_notification/tasks/main.yml new file mode 100644 index 00000000000..873c80d184d --- /dev/null +++ b/test/integration/targets/s3_bucket_notification/tasks/main.yml @@ -0,0 +1,335 @@ +--- +# ============================================================ +- name: set up aws connection info + set_fact: + aws_connection_info: &aws_connection_info + aws_access_key: "{{ aws_access_key }}" + aws_secret_key: "{{ aws_secret_key }}" + security_token: "{{ security_token }}" + region: "{{ aws_region }}" + no_log: yes +# ============================================================ +- name: test add s3 bucket notification + block: + - name: move lambda into place for archive module + copy: + src: "mini_lambda.py" + dest: "{{output_dir}}/mini_lambda.py" + + - name: bundle lambda into a zip + archive: + format: zip + path: "{{output_dir}}/mini_lambda.py" + dest: "{{output_dir}}/mini_lambda.zip" + register: function_res + + - name: register bucket + s3_bucket: + name: "{{resource_prefix}}-bucket" + state: present + <<: *aws_connection_info + register: bucket_info + + - name: register lambda + lambda: + name: "{{resource_prefix}}-lambda" + state: present + role: "ansible_lambda_role" + runtime: "python3.7" + zip_file: "{{function_res.dest}}" + handler: "lambda_function.lambda_handler" + memory_size: "128" + timeout: "30" + <<: *aws_connection_info + register: lambda_info + + - name: register notification without invoke permissions + s3_bucket_notification: + state: present + event_name: "{{resource_prefix}}-on_file_add_or_remove" + bucket_name: "{{resource_prefix}}-bucket" + lambda_function_arn: "{{ lambda_info.configuration.function_arn }}" + events: ["s3:ObjectCreated:*", "s3:ObjectRemoved:*"] + prefix: images/ + suffix: .jpg + <<: *aws_connection_info + register: result + ignore_errors: true + - name: assert nice message returned + assert: + that: + - result is failed + - result.msg != 'MODULE FAILURE' + + - name: Add invocation permission of Lambda function on AWS + lambda_policy: + function_name: "{{ lambda_info.configuration.function_arn }}" + statement_id: allow_lambda_invoke + action: lambda:InvokeFunction + principal: "s3.amazonaws.com" + source_arn: "arn:aws:s3:::{{bucket_info.name}}" + <<: *aws_connection_info + + - name: register s3 bucket notification + s3_bucket_notification: + state: present + event_name: "{{resource_prefix}}-on_file_add_or_remove" + bucket_name: "{{resource_prefix}}-bucket" + lambda_function_arn: "{{ lambda_info.configuration.function_arn }}" + events: ["s3:ObjectCreated:*", "s3:ObjectRemoved:*"] + prefix: images/ + suffix: .jpg + <<: *aws_connection_info + register: result + - name: assert result.changed == True + assert: + that: + - result.changed == True + + # ============================================================ + - name: test check_mode without change + s3_bucket_notification: + state: present + event_name: "{{resource_prefix}}-on_file_add_or_remove" + bucket_name: "{{resource_prefix}}-bucket" + lambda_function_arn: "{{ lambda_info.configuration.function_arn }}" + events: ["s3:ObjectCreated:*", "s3:ObjectRemoved:*"] + prefix: images/ + suffix: .jpg + <<: *aws_connection_info + register: result + check_mode: yes + - name: assert result.changed == False + assert: + that: + - result.changed == False + + - name: test check_mode change events + s3_bucket_notification: + state: present + event_name: "{{resource_prefix}}-on_file_add_or_remove" + bucket_name: "{{resource_prefix}}-bucket" + lambda_function_arn: "{{ lambda_info.configuration.function_arn }}" + events: ["s3:ObjectCreated:*"] + prefix: images/ + suffix: .jpg + <<: *aws_connection_info + register: result + check_mode: yes + - name: assert result.changed == True + assert: + that: + - result.changed == True + + - name: test that check_mode didn't change events + s3_bucket_notification: + state: present + event_name: "{{resource_prefix}}-on_file_add_or_remove" + bucket_name: "{{resource_prefix}}-bucket" + lambda_function_arn: "{{ lambda_info.configuration.function_arn }}" + events: ["s3:ObjectCreated:*", "s3:ObjectRemoved:*"] + prefix: images/ + suffix: .jpg + <<: *aws_connection_info + register: result + - name: assert result.changed == False + assert: + that: + - result.changed == False + + # ============================================================ + - name: test mutually exclusive parameters + s3_bucket_notification: + state: present + event_name: "{{resource_prefix}}-on_file_add_or_remove" + bucket_name: "{{resource_prefix}}-bucket" + lambda_function_arn: "{{ lambda_info.configuration.function_arn }}" + events: ["s3:ObjectCreated:Post"] + prefix: photos/ + suffix: .gif + lambda_version: 0 + lambda_alias: 0 + <<: *aws_connection_info + register: result + ignore_errors: true + - name: assert task failed + assert: + that: + - result is failed + - "result.msg == 'parameters are mutually exclusive: lambda_alias|lambda_version'" + + # ============================================================ + # Test configuration changes + - name: test configuration change on suffix + s3_bucket_notification: + state: present + event_name: "{{resource_prefix}}-on_file_add_or_remove" + bucket_name: "{{resource_prefix}}-bucket" + lambda_function_arn: "{{ lambda_info.configuration.function_arn }}" + events: ["s3:ObjectCreated:*", "s3:ObjectRemoved:*"] + prefix: images/ + suffix: .gif + <<: *aws_connection_info + register: result + - name: assert result.changed == True + assert: + that: + - result.changed == True + + - name: test configuration change on prefix + s3_bucket_notification: + state: present + event_name: "{{resource_prefix}}-on_file_add_or_remove" + bucket_name: "{{resource_prefix}}-bucket" + lambda_function_arn: "{{ lambda_info.configuration.function_arn }}" + events: ["s3:ObjectCreated:*", "s3:ObjectRemoved:*"] + prefix: photos/ + suffix: .gif + <<: *aws_connection_info + register: result + - name: assert result.changed == True + assert: + that: + - result.changed == True + + - name: test configuration change on new events added + s3_bucket_notification: + state: present + event_name: "{{resource_prefix}}-on_file_add_or_remove" + bucket_name: "{{resource_prefix}}-bucket" + lambda_function_arn: "{{ lambda_info.configuration.function_arn }}" + events: ["s3:ObjectCreated:*", "s3:ObjectRemoved:*", "s3:ObjectRestore:Post"] + prefix: photos/ + suffix: .gif + <<: *aws_connection_info + register: result + - name: assert result.changed == True + assert: + that: + - result.changed == True + + - name: test configuration change on events removed + s3_bucket_notification: + state: present + event_name: "{{resource_prefix}}-on_file_add_or_remove" + bucket_name: "{{resource_prefix}}-bucket" + lambda_function_arn: "{{ lambda_info.configuration.function_arn }}" + events: ["s3:ObjectCreated:Post"] + prefix: photos/ + suffix: .gif + <<: *aws_connection_info + register: result + - name: assert result.changed == True + assert: + that: + - result.changed == True + + # ============================================================ + # Test idempotency of CRUD + + - name: change events + s3_bucket_notification: + state: present + event_name: "{{resource_prefix}}-on_file_add_or_remove" + bucket_name: "{{resource_prefix}}-bucket" + lambda_function_arn: "{{ lambda_info.configuration.function_arn }}" + events: ["s3:ObjectCreated:*", "s3:ObjectRemoved:*", "s3:ObjectRestore:Post"] + prefix: photos/ + suffix: .gif + <<: *aws_connection_info + register: result + + - name: test that event order does not matter + s3_bucket_notification: + state: present + event_name: "{{resource_prefix}}-on_file_add_or_remove" + bucket_name: "{{resource_prefix}}-bucket" + lambda_function_arn: "{{ lambda_info.configuration.function_arn }}" + events: ["s3:ObjectRestore:Post", "s3:ObjectRemoved:*", "s3:ObjectCreated:*"] + prefix: photos/ + suffix: .gif + <<: *aws_connection_info + register: result + - name: assert result.changed == False + assert: + that: + - result.changed == False + + - name: test that configuration is the same as previous task + s3_bucket_notification: + state: present + event_name: "{{resource_prefix}}-on_file_add_or_remove" + bucket_name: "{{resource_prefix}}-bucket" + lambda_function_arn: "{{ lambda_info.configuration.function_arn }}" + events: ["s3:ObjectCreated:*", "s3:ObjectRemoved:*", "s3:ObjectRestore:Post"] + prefix: photos/ + suffix: .gif + <<: *aws_connection_info + register: result + - name: assert result.changed == False + assert: + that: + - result.changed == False + + - name: test remove notification + s3_bucket_notification: + state: absent + event_name: "{{resource_prefix}}-on_file_add_or_remove" + bucket_name: "{{resource_prefix}}-bucket" + <<: *aws_connection_info + register: result + - name: assert result.changed == True + assert: + that: + - result.changed == True + + - name: test that events is already removed + s3_bucket_notification: + state: absent + event_name: "{{resource_prefix}}-on_file_add_or_remove" + bucket_name: "{{resource_prefix}}-bucket" + <<: *aws_connection_info + register: result + - name: assert result.changed == False + assert: + that: + - result.changed == False + + always: + - name: clean-up bucket + s3_bucket: + name: "{{resource_prefix}}-bucket" + state: absent + <<: *aws_connection_info + + - name: clean-up lambda + lambda: + name: "{{resource_prefix}}-lambda" + state: absent + <<: *aws_connection_info +# ============================================================ +- +- block: + # ============================================================ + - name: test with no parameters except state absent + s3_bucket_notification: + state=absent + register: result + ignore_errors: true + - name: assert failure when called with no parameters + assert: + that: + - 'result.failed' + - 'result.msg.startswith("missing required arguments: event_name, bucket_name")' + + # ============================================================ + - name: test abesnt + s3_bucket_notification: + state=absent + register: result + ignore_errors: true + - name: assert failure when called with no parameters + assert: + that: + - 'result.failed' + - 'result.msg.startswith("missing required arguments: event_name, bucket_name")' \ No newline at end of file diff --git a/test/units/modules/cloud/amazon/test_s3_bucket_notification.py b/test/units/modules/cloud/amazon/test_s3_bucket_notification.py new file mode 100644 index 00000000000..0e386e5702c --- /dev/null +++ b/test/units/modules/cloud/amazon/test_s3_bucket_notification.py @@ -0,0 +1,258 @@ +import pytest + +from units.compat.mock import MagicMock, patch +from units.modules.utils import AnsibleExitJson, AnsibleFailJson, ModuleTestCase, set_module_args + +from ansible.modules.cloud.amazon.s3_bucket_notification import AmazonBucket, Config +from ansible.modules.cloud.amazon import s3_bucket_notification +try: + from botocore.exceptions import ClientError, ParamValidationError, BotoCoreError +except ImportError: + pass + + +class TestAmazonBucketOperations: + def test_current_config(self): + api_config = { + 'Id': 'test-id', + 'LambdaFunctionArn': 'test-arn', + 'Events': [], + 'Filter': { + 'Key': { + 'FilterRules': [{ + 'Name': 'Prefix', + 'Value': '' + }, { + 'Name': 'Suffix', + 'Value': '' + }] + } + } + } + client = MagicMock() + client.get_bucket_notification_configuration.return_value = { + 'LambdaFunctionConfigurations': [api_config] + } + bucket = AmazonBucket(client, 'test-bucket') + current = bucket.current_config('test-id') + assert current.raw == api_config + assert client.get_bucket_notification_configuration.call_count == 1 + + def test_current_config_empty(self): + client = MagicMock() + client.get_bucket_notification_configuration.return_value = { + 'LambdaFunctionConfigurations': [] + } + bucket = AmazonBucket(client, 'test-bucket') + current = bucket.current_config('test-id') + assert current is None + assert client.get_bucket_notification_configuration.call_count == 1 + + def test_apply_invalid_config(self): + client = MagicMock() + client.get_bucket_notification_configuration.return_value = { + 'LambdaFunctionConfigurations': [] + } + client.put_bucket_notification_configuration.side_effect = ClientError({}, '') + bucket = AmazonBucket(client, 'test-bucket') + config = Config.from_params(**{ + 'event_name': 'test_event', + 'lambda_function_arn': 'lambda_arn', + 'lambda_version': 1, + 'events': ['s3:ObjectRemoved:*', 's3:ObjectCreated:*'], + 'prefix': '', + 'suffix': '' + }) + with pytest.raises(ClientError): + bucket.apply_config(config) + + def test_apply_config(self): + client = MagicMock() + client.get_bucket_notification_configuration.return_value = { + 'LambdaFunctionConfigurations': [] + } + + bucket = AmazonBucket(client, 'test-bucket') + config = Config.from_params(**{ + 'event_name': 'test_event', + 'lambda_function_arn': 'lambda_arn', + 'lambda_version': 1, + 'events': ['s3:ObjectRemoved:*', 's3:ObjectCreated:*'], + 'prefix': '', + 'suffix': '' + }) + bucket.apply_config(config) + assert client.get_bucket_notification_configuration.call_count == 1 + assert client.put_bucket_notification_configuration.call_count == 1 + + def test_apply_config_add_event(self): + api_config = { + 'Id': 'test-id', + 'LambdaFunctionArn': 'test-arn', + 'Events': ['s3:ObjectRemoved:*'], + 'Filter': { + 'Key': { + 'FilterRules': [{ + 'Name': 'Prefix', + 'Value': '' + }, { + 'Name': 'Suffix', + 'Value': '' + }] + } + } + } + client = MagicMock() + client.get_bucket_notification_configuration.return_value = { + 'LambdaFunctionConfigurations': [api_config] + } + + bucket = AmazonBucket(client, 'test-bucket') + config = Config.from_params(**{ + 'event_name': 'test-id', + 'lambda_function_arn': 'test-arn', + 'lambda_version': 1, + 'events': ['s3:ObjectRemoved:*', 's3:ObjectCreated:*'], + 'prefix': '', + 'suffix': '' + }) + bucket.apply_config(config) + assert client.get_bucket_notification_configuration.call_count == 1 + assert client.put_bucket_notification_configuration.call_count == 1 + client.put_bucket_notification_configuration.assert_called_with( + Bucket='test-bucket', + NotificationConfiguration={ + 'LambdaFunctionConfigurations': [{ + 'Id': 'test-id', + 'LambdaFunctionArn': 'test-arn:1', + 'Events': ['s3:ObjectCreated:*', 's3:ObjectRemoved:*'], + 'Filter': { + 'Key': { + 'FilterRules': [{ + 'Name': 'Prefix', + 'Value': '' + }, { + 'Name': 'Suffix', + 'Value': '' + }] + } + } + }] + } + ) + + def test_delete_config(self): + api_config = { + 'Id': 'test-id', + 'LambdaFunctionArn': 'test-arn', + 'Events': [], + 'Filter': { + 'Key': { + 'FilterRules': [{ + 'Name': 'Prefix', + 'Value': '' + }, { + 'Name': 'Suffix', + 'Value': '' + }] + } + } + } + client = MagicMock() + client.get_bucket_notification_configuration.return_value = { + 'LambdaFunctionConfigurations': [api_config] + } + bucket = AmazonBucket(client, 'test-bucket') + config = Config.from_params(**{ + 'event_name': 'test-id', + 'lambda_function_arn': 'lambda_arn', + 'lambda_version': 1, + 'events': [], + 'prefix': '', + 'suffix': '' + }) + bucket.delete_config(config) + assert client.get_bucket_notification_configuration.call_count == 1 + assert client.put_bucket_notification_configuration.call_count == 1 + client.put_bucket_notification_configuration.assert_called_with( + Bucket='test-bucket', + NotificationConfiguration={'LambdaFunctionConfigurations': []} + ) + + +class TestConfig: + def test_config_from_params(self): + config = Config({ + 'Id': 'test-id', + 'LambdaFunctionArn': 'test-arn:10', + 'Events': [], + 'Filter': { + 'Key': { + 'FilterRules': [{ + 'Name': 'Prefix', + 'Value': '' + }, { + 'Name': 'Suffix', + 'Value': '' + }] + } + } + }) + config_from_params = Config.from_params(**{ + 'event_name': 'test-id', + 'lambda_function_arn': 'test-arn', + 'lambda_version': 10, + 'events': [], + 'prefix': '', + 'suffix': '' + }) + assert config.raw == config_from_params.raw + assert config == config_from_params + + +class TestModule(ModuleTestCase): + def test_module_fail_when_required_args_missing(self): + with pytest.raises(AnsibleFailJson): + set_module_args({}) + s3_bucket_notification.main() + + @patch('ansible.modules.cloud.amazon.s3_bucket_notification.AnsibleAWSModule.client') + def test_add_s3_bucket_notification(self, aws_client): + aws_client.return_value.get_bucket_notification_configuration.return_value = { + 'LambdaFunctionConfigurations': [] + } + set_module_args({ + 'region': 'us-east-2', + 'lambda_function_arn': 'test-lambda-arn', + 'bucket_name': 'test-lambda', + 'event_name': 'test-id', + 'events': ['s3:ObjectCreated:*', 's3:ObjectRemoved:*'], + 'state': 'present', + 'prefix': '/images', + 'suffix': '.jpg' + }) + with pytest.raises(AnsibleExitJson) as context: + s3_bucket_notification.main() + result = context.value.args[0] + assert result['changed'] is True + assert aws_client.return_value.get_bucket_notification_configuration.call_count == 1 + aws_client.return_value.put_bucket_notification_configuration.assert_called_with( + Bucket='test-lambda', + NotificationConfiguration={ + 'LambdaFunctionConfigurations': [{ + 'Id': 'test-id', + 'LambdaFunctionArn': 'test-lambda-arn', + 'Events': ['s3:ObjectCreated:*', 's3:ObjectRemoved:*'], + 'Filter': { + 'Key': { + 'FilterRules': [{ + 'Name': 'Prefix', + 'Value': '/images' + }, { + 'Name': 'Suffix', + 'Value': '.jpg' + }] + } + } + }] + })