Skip to content

Commit

Permalink
s3_bucket: object lock enabled (ansible-collections#1372) (ansible-co…
Browse files Browse the repository at this point in the history
…llections#1407)

[PR ansible-collections#1372/8aec8f1b backport][stable-5] s3_bucket: object lock enabled

SUMMARY
Fixes ansible-collections#1347
ISSUE TYPE

Feature Pull Request

COMPONENT NAME
s3_bucket
ADDITIONAL INFORMATION
Reviewed-by: Mark Chappell
Reviewed-by: Alina Buzachis

Reviewed-by: Alina Buzachis
  • Loading branch information
tremble authored Mar 3, 2023
1 parent e274f54 commit 67bc04d
Show file tree
Hide file tree
Showing 4 changed files with 187 additions and 6 deletions.
2 changes: 2 additions & 0 deletions changelogs/fragments/1347-s3-object-lock-enabled.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
minor_changes:
- s3_bucket - add option to support creation of buckets with object lock enabled (https://github.com/ansible-collections/amazon.aws/pull/1372).
59 changes: 53 additions & 6 deletions plugins/modules/s3_bucket.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,12 @@
choices: [ 'BucketOwnerEnforced', 'BucketOwnerPreferred', 'ObjectWriter' ]
type: str
version_added: 2.0.0
object_lock_enabled:
description:
- Whether S3 Object Lock to be enabled.
- Defaults to C(False) when creating a new bucket.
type: bool
version_added: 5.3.0
delete_object_ownership:
description:
- Delete bucket's ownership controls.
Expand Down Expand Up @@ -383,6 +389,7 @@ def create_or_update_bucket(s3_client, module, location):
delete_public_access = module.params.get("delete_public_access")
delete_object_ownership = module.params.get("delete_object_ownership")
object_ownership = module.params.get("object_ownership")
object_lock_enabled = module.params.get("object_lock_enabled")
acl = module.params.get("acl")
changed = False
result = {}
Expand All @@ -396,7 +403,7 @@ def create_or_update_bucket(s3_client, module, location):

if not bucket_is_present:
try:
bucket_changed = create_bucket(s3_client, name, location)
bucket_changed = create_bucket(s3_client, name, location, object_lock_enabled)
s3_client.get_waiter('bucket_exists').wait(Bucket=name)
changed = changed or bucket_changed
except botocore.exceptions.WaiterError as e:
Expand Down Expand Up @@ -644,6 +651,32 @@ def create_or_update_bucket(s3_client, module, location):
except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: # pylint: disable=duplicate-except
module.fail_json_aws(e, msg="Failed to update bucket ACL")

# -- Object Lock
try:
object_lock_status = get_bucket_object_lock_enabled(s3_client, name)
result["object_lock_enabled"] = object_lock_status
except is_boto3_error_code(["NotImplemented", "XNotImplemented"]) as e:
if object_lock_enabled is not None:
module.fail_json(msg="Fetching bucket object lock state is not supported")
except is_boto3_error_code("ObjectLockConfigurationNotFoundError"): # pylint: disable=duplicate-except
if object_lock_enabled:
module.fail_json(msg="Enabling object lock for existing buckets is not supported")
result["object_lock_enabled"] = False
except is_boto3_error_code("AccessDenied") as e: # pylint: disable=duplicate-except
if object_lock_enabled is not None:
module.fail_json(msg="Permission denied fetching object lock state for bucket")
except (
botocore.exceptions.BotoCoreError,
botocore.exceptions.ClientError,
) as e: # pylint: disable=duplicate-except
module.fail_json_aws(e, msg="Failed to fetch bucket object lock state")
else:
if object_lock_status is not None:
if not object_lock_enabled and object_lock_status:
module.fail_json(msg="Disabling object lock for existing buckets is not supported")
if object_lock_enabled and not object_lock_status:
module.fail_json(msg="Enabling object lock for existing buckets is not supported")

# Module exit
module.exit_json(changed=changed, name=name, **result)

Expand All @@ -658,15 +691,22 @@ def bucket_exists(s3_client, bucket_name):


@AWSRetry.exponential_backoff(max_delay=120)
def create_bucket(s3_client, bucket_name, location):
def create_bucket(s3_client, bucket_name, location, object_lock_enabled=False):
try:
params = {"Bucket": bucket_name}

configuration = {}
if location not in ('us-east-1', None):
configuration['LocationConstraint'] = location
if len(configuration) > 0:
s3_client.create_bucket(Bucket=bucket_name, CreateBucketConfiguration=configuration)
else:
s3_client.create_bucket(Bucket=bucket_name)

if configuration:
params["CreateBucketConfiguration"] = configuration

if object_lock_enabled is not None:
params["ObjectLockEnabledForBucket"] = object_lock_enabled

s3_client.create_bucket(**params)

return True
except is_boto3_error_code('BucketAlreadyOwnedByYou'):
# We should never get here since we check the bucket presence before calling the create_or_update_bucket
Expand Down Expand Up @@ -722,6 +762,12 @@ def put_bucket_versioning(s3_client, bucket_name, required_versioning):
s3_client.put_bucket_versioning(Bucket=bucket_name, VersioningConfiguration={'Status': required_versioning})


@AWSRetry.exponential_backoff(max_delay=120, catch_extra_error_codes=["NoSuchBucket", "OperationAborted"])
def get_bucket_object_lock_enabled(s3_client, bucket_name):
object_lock_configuration = s3_client.get_object_lock_configuration(Bucket=bucket_name)
return object_lock_configuration["ObjectLockConfiguration"]["ObjectLockEnabled"] == "Enabled"


@AWSRetry.exponential_backoff(max_delay=120, catch_extra_error_codes=['NoSuchBucket', 'OperationAborted'])
def get_bucket_encryption(s3_client, bucket_name):
try:
Expand Down Expand Up @@ -1111,6 +1157,7 @@ def main():
delete_object_ownership=dict(type='bool', default=False),
acl=dict(type='str', choices=['private', 'public-read', 'public-read-write', 'authenticated-read']),
validate_bucket_name=dict(type='bool', default=True),
object_lock_enabled=dict(type="bool"),
)

required_by = dict(
Expand Down
1 change: 1 addition & 0 deletions tests/integration/targets/s3_bucket/inventory
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ encryption_bucket_key
encryption_sse
public_access
acl
object_lock

[all:vars]
ansible_connection=local
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
---
- module_defaults:
group/aws:
aws_access_key: "{{ aws_access_key }}"
aws_secret_key: "{{ aws_secret_key }}"
security_token: "{{ security_token | default(omit) }}"
region: "{{ aws_region }}"
block:
- set_fact:
local_bucket_name: "{{ bucket_name | hash('md5')}}-objectlock"

# ============================================================

- name: 'Create a simple bucket'
s3_bucket:
name: '{{ local_bucket_name }}'
state: present
register: output

- assert:
that:
- output.changed
- not output.object_lock_enabled

- name: 'Re-disable object lock (idempotency)'
s3_bucket:
name: '{{ local_bucket_name }}'
state: present
object_lock_enabled: false
register: output

- assert:
that:
- not output.changed
- not output.object_lock_enabled

- name: 'Enable object lock'
s3_bucket:
name: '{{ local_bucket_name }}'
state: present
object_lock_enabled: true
register: output
ignore_errors: true

- assert:
that:
- output is failed

- name: Delete test s3 bucket
s3_bucket:
name: '{{ local_bucket_name }}'
state: absent
register: output

- assert:
that:
- output.changed

# ============================================================

- name: 'Create a bucket with object lock enabled'
s3_bucket:
name: '{{ local_bucket_name }}-2'
state: present
object_lock_enabled: true
register: output

- assert:
that:
- output.changed
- output.object_lock_enabled

- name: 'Disable object lock'
s3_bucket:
name: '{{ local_bucket_name }}-2'
state: present
object_lock_enabled: false
register: output
ignore_errors: true

- assert:
that:
- output is failed

- name: 'Re-Enable object lock (idempotency)'
s3_bucket:
name: '{{ local_bucket_name }}-2'
state: present
object_lock_enabled: true
register: output

- assert:
that:
- not output.changed
- output.object_lock_enabled

- name: 'Touch bucket with object lock enabled (idempotency)'
s3_bucket:
name: '{{ local_bucket_name }}-2'
state: present
object_lock_enabled: true
register: output

- assert:
that:
- not output.changed
- output.object_lock_enabled

- name: Delete test s3 bucket
s3_bucket:
name: '{{ local_bucket_name }}-2'
state: absent
register: output

- assert:
that:
- output.changed

# ============================================================
always:
- name: Ensure all buckets are deleted
s3_bucket:
name: '{{ local_bucket_name }}'
state: absent
ignore_errors: yes

- name: Ensure all buckets are deleted
s3_bucket:
name: '{{ local_bucket_name }}-2'
state: absent
ignore_errors: yes

0 comments on commit 67bc04d

Please sign in to comment.