Add lambda_bucket_event module (#58059)
This commit is contained in:
parent
1c1da3c11d
commit
42073b6331
8 changed files with 879 additions and 1 deletions
|
@ -13,6 +13,7 @@
|
||||||
"s3:GetBucketVersioning",
|
"s3:GetBucketVersioning",
|
||||||
"s3:GetEncryptionConfiguration",
|
"s3:GetEncryptionConfiguration",
|
||||||
"s3:GetObject",
|
"s3:GetObject",
|
||||||
|
"s3:GetBucketNotification",
|
||||||
"s3:HeadBucket",
|
"s3:HeadBucket",
|
||||||
"s3:ListBucket",
|
"s3:ListBucket",
|
||||||
"s3:PutBucketAcl",
|
"s3:PutBucketAcl",
|
||||||
|
@ -22,7 +23,8 @@
|
||||||
"s3:PutBucketVersioning",
|
"s3:PutBucketVersioning",
|
||||||
"s3:PutEncryptionConfiguration",
|
"s3:PutEncryptionConfiguration",
|
||||||
"s3:PutObject",
|
"s3:PutObject",
|
||||||
"s3:PutObjectAcl"
|
"s3:PutObjectAcl",
|
||||||
|
"s3:PutBucketNotification"
|
||||||
],
|
],
|
||||||
"Effect": "Allow",
|
"Effect": "Allow",
|
||||||
"Resource": [
|
"Resource": [
|
||||||
|
|
267
lib/ansible/modules/cloud/amazon/s3_bucket_notification.py
Normal file
267
lib/ansible/modules/cloud/amazon/s3_bucket_notification.py
Normal file
|
@ -0,0 +1,267 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
# (c) 2019, XLAB d.o.o <www.xlab.si>
|
||||||
|
# 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()
|
2
test/integration/targets/s3_bucket_notification/aliases
Normal file
2
test/integration/targets/s3_bucket_notification/aliases
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
cloud/aws
|
||||||
|
shippable/aws/group2
|
|
@ -0,0 +1,3 @@
|
||||||
|
---
|
||||||
|
# defaults file for aws_lambda test
|
||||||
|
lambda_function_name: '{{resource_prefix}}'
|
|
@ -0,0 +1,8 @@
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
def lambda_handler(event, context):
|
||||||
|
return {
|
||||||
|
'statusCode': 200,
|
||||||
|
'body': json.dumps('Hello from Lambda!')
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
dependencies:
|
||||||
|
- prepare_tests
|
||||||
|
- setup_ec2
|
335
test/integration/targets/s3_bucket_notification/tasks/main.yml
Normal file
335
test/integration/targets/s3_bucket_notification/tasks/main.yml
Normal file
|
@ -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")'
|
258
test/units/modules/cloud/amazon/test_s3_bucket_notification.py
Normal file
258
test/units/modules/cloud/amazon/test_s3_bucket_notification.py
Normal file
|
@ -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'
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
})
|
Loading…
Reference in a new issue