Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions changelogs/fragments/s3-bucket-refactor.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
minor_changes:
- s3_bucket_cors - The s3_cors module has been renamed to s3_bucket_cors for consistency with other S3 bucket configuration modules. The usage of the module has not changed and the s3_cors alias will continue to work (https://github.com/ansible-collections/community.aws/pull/2472).
- s3_bucket_lifecycle - The s3_lifecycle module has been renamed to s3_bucket_lifecycle for consistency with other S3 bucket configuration modules. The usage of the module has not changed and the s3_lifecycle alias will continue to work (https://github.com/ansible-collections/community.aws/pull/2472).
- s3_bucket_metrics_configuration - The s3_metrics_configuration module has been renamed to s3_bucket_metrics_configuration for consistency with other S3 bucket configuration modules. The usage of the module has not changed and the s3_metrics_configuration alias will continue to work (https://github.com/ansible-collections/community.aws/pull/2472).
- s3_bucket_website - The s3_website module has been renamed to s3_bucket_website for consistency with other S3 bucket configuration modules. The usage of the module has not changed and the s3_website alias will continue to work (https://github.com/ansible-collections/community.aws/pull/2472).
17 changes: 17 additions & 0 deletions meta/runtime.yml
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,16 @@ action_groups:
- redshift_info
- redshift_subnet_group
- route53_wait
- s3_bucket_cors
- s3_bucket_cors_info
- s3_bucket_lifecycle
- s3_bucket_lifecycle_info
- s3_bucket_metrics_configuration
- s3_bucket_metrics_configuration_info
- s3_bucket_notification
- s3_bucket_notification_info
- s3_bucket_website
- s3_bucket_website_info
- s3_cors
- s3_lifecycle
- s3_logging
Expand Down Expand Up @@ -606,6 +615,14 @@ plugin_routing:
redirect: amazon.aws.route53_zone
s3_bucket_info:
redirect: amazon.aws.s3_bucket_info
s3_cors:
redirect: community.aws.s3_bucket_cors
s3_lifecycle:
redirect: community.aws.s3_bucket_lifecycle
s3_metrics_configuration:
redirect: community.aws.s3_bucket_metrics_configuration
s3_website:
redirect: community.aws.s3_bucket_website
sts_assume_role:
redirect: amazon.aws.sts_assume_role
ec2_vpc_egress_igw:
Expand Down
Empty file.
68 changes: 68 additions & 0 deletions plugins/module_utils/_s3/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# -*- coding: utf-8 -*-

# Copyright: Contributors to the Ansible project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

"""
S3-specific error handling extensions for community.aws modules.

Extends amazon.aws S3ErrorHandler to include additional error codes
specific to S3 bucket configuration operations.
"""

from __future__ import annotations

import typing

if typing.TYPE_CHECKING:
from typing import Callable

from ansible_collections.amazon.aws.plugins.module_utils.botocore import is_boto3_error_code
from ansible_collections.amazon.aws.plugins.module_utils.s3 import S3ErrorHandler as BaseS3ErrorHandler


class S3ErrorHandler(BaseS3ErrorHandler):
"""
Extended S3 error handler with support for additional configuration error codes.

Extends amazon.aws.S3ErrorHandler to include error codes specific to
S3 bucket configuration operations (CORS, lifecycle, metrics, website, etc.).
"""

@classmethod
def _is_missing(cls) -> Callable:
"""
Check if a boto3 exception indicates a missing/not found resource.

Extends the base implementation to include additional S3 configuration
error codes:
- NoSuchConfiguration: Metrics configuration not found
- NoSuchWebsiteConfiguration: Website configuration not found
- NoSuchLifecycleConfiguration: Lifecycle configuration not found
- NoSuchCORSConfiguration: CORS configuration not found

Returns:
A matcher function for boto3 error codes indicating missing resources.
"""
# Get the base error codes by calling the parent implementation
# We need to extract the codes and add our additional ones
return is_boto3_error_code(
[
# Base error codes from amazon.aws.S3ErrorHandler
"404",
"NoSuchTagSet",
"NoSuchTagSetError",
"ObjectLockConfigurationNotFoundError",
"NoSuchBucketPolicy",
"ServerSideEncryptionConfigurationNotFoundError",
"NoSuchBucket",
"NoSuchPublicAccessBlockConfiguration",
"OwnershipControlsNotFoundError",
"NoSuchOwnershipControls",
# Additional configuration error codes for community.aws
"NoSuchConfiguration",
"NoSuchWebsiteConfiguration",
"NoSuchLifecycleConfiguration",
"NoSuchCORSConfiguration",
]
)
217 changes: 217 additions & 0 deletions plugins/module_utils/_s3/transformations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
# -*- coding: utf-8 -*-

# Copyright: Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

"""
S3 transformation and normalization functions.

This module contains functions for transforming S3 API responses into
Ansible-friendly formats and vice versa.
"""

from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict

from ansible_collections.amazon.aws.plugins.module_utils.tagging import ansible_dict_to_boto3_tag_list
from ansible_collections.amazon.aws.plugins.module_utils.transformation import boto3_resource_list_to_ansible_dict
from ansible_collections.amazon.aws.plugins.module_utils.transformation import boto3_resource_to_ansible_dict


def build_notification_configuration(bucket_config):
"""
Build notification configuration dict for return value.

Args:
bucket_config: Dictionary with QueueConfigurations, TopicConfigurations,
LambdaFunctionConfigurations keys containing Config objects

Returns:
Dictionary with snake_case keys suitable for module return
"""
notification_configs = dict(QueueConfigurations=[], TopicConfigurations=[], LambdaFunctionConfigurations=[])
for target_configs in bucket_config:
for cfg in bucket_config[target_configs]:
notification_configs[target_configs].append(camel_dict_to_snake_dict(cfg.raw))
return camel_dict_to_snake_dict(notification_configs)


def create_website_configuration(suffix, error_key, redirect_all_requests):
"""
Create website configuration payload for S3 API.

Args:
suffix: Index document suffix (e.g., 'index.html')
error_key: Error document key
redirect_all_requests: Redirect URL (format: 'protocol://hostname' or 'hostname')

Returns:
Dictionary suitable for put_bucket_website API call

Raises:
ValueError: If redirect_all_requests URL format is invalid
"""
website_configuration = {}

if error_key is not None:
website_configuration["ErrorDocument"] = {"Key": error_key}

if suffix is not None:
website_configuration["IndexDocument"] = {"Suffix": suffix}

if redirect_all_requests is not None:
website_configuration["RedirectAllRequestsTo"] = _create_redirect_dict(redirect_all_requests)

return website_configuration


def _create_redirect_dict(url):
"""
Parse redirect URL into protocol and hostname components.

Args:
url: Redirect URL (format: 'protocol://hostname' or 'hostname')

Returns:
Dictionary with Protocol and/or HostName keys

Raises:
ValueError: If URL format is invalid
"""
redirect_dict = {}
url_split = url.split(":")

# Did we split anything?
if len(url_split) == 2:
redirect_dict["Protocol"] = url_split[0]
redirect_dict["HostName"] = url_split[1].replace("//", "")
elif len(url_split) == 1:
redirect_dict["HostName"] = url_split[0]
else:
raise ValueError("Redirect URL appears invalid")

return redirect_dict


def normalize_cors_rules(cors_rules):
"""
Normalize CORS rules to snake_case.

Args:
cors_rules: List of CORS rule dictionaries from AWS API (CamelCase)

Returns:
List of normalized CORS rule dictionaries with snake_case keys
"""
return boto3_resource_list_to_ansible_dict(cors_rules, transform_tags=False)


def normalize_lifecycle_rules(lifecycle_config):
"""
Normalize lifecycle configuration to snake_case.

Args:
lifecycle_config: Lifecycle configuration dictionary from AWS API (CamelCase)

Returns:
List of normalized lifecycle rule dictionaries with snake_case keys
"""
rules = lifecycle_config.get("Rules", [])
return boto3_resource_list_to_ansible_dict(rules, transform_tags=False)


def normalize_notification_configuration(notification_config):
"""
Normalize notification configuration to snake_case.

Args:
notification_config: Notification configuration dictionary from AWS API (CamelCase)

Returns:
Normalized notification configuration dictionary with snake_case keys
"""
return boto3_resource_to_ansible_dict(notification_config, transform_tags=False)


def normalize_website_configuration(website_config):
"""
Normalize website configuration to snake_case.

Args:
website_config: Website configuration dictionary from AWS API (CamelCase)

Returns:
Normalized website configuration dictionary with snake_case keys,
with ResponseMetadata stripped
"""
if not website_config:
return website_config
normalized = boto3_resource_to_ansible_dict(website_config, transform_tags=False)
# Strip response_metadata after normalization (it's now snake_case)
normalized.pop("response_metadata", None)
return normalized


def normalize_metrics_configuration(config):
"""
Normalize a metrics configuration to snake_case.

Args:
config: Raw metrics configuration from AWS API (CamelCase)

Returns:
Normalized configuration dictionary with snake_case keys
"""
if not config:
return None

normalized = {"id": config.get("Id")}

if "Filter" in config:
filter_config = config["Filter"]
normalized_filter = {}

if "Prefix" in filter_config:
normalized_filter["prefix"] = filter_config["Prefix"]

if "Tag" in filter_config:
tag = filter_config["Tag"]
normalized_filter["tags"] = {tag["Key"]: tag["Value"]}

if "And" in filter_config:
and_filter = filter_config["And"]
if "Prefix" in and_filter:
normalized_filter["prefix"] = and_filter["Prefix"]
if "Tags" in and_filter:
normalized_filter["tags"] = {tag["Key"]: tag["Value"] for tag in and_filter["Tags"]}

if normalized_filter:
normalized["filter"] = normalized_filter

return normalized


def create_metrics_configuration(mc_id, filter_prefix, filter_tags):
"""
Create metrics configuration payload for S3 API.

Args:
mc_id: Metrics configuration ID
filter_prefix: Prefix filter for metrics
filter_tags: Tag filters as Ansible dictionary

Returns:
Dictionary suitable for put_bucket_metrics_configuration API call
"""
payload = {"Id": mc_id}
# Just a filter_prefix or just a single tag filter is a special case
if filter_prefix and not filter_tags:
payload["Filter"] = {"Prefix": filter_prefix}
elif not filter_prefix and len(filter_tags) == 1:
payload["Filter"] = {"Tag": ansible_dict_to_boto3_tag_list(filter_tags)[0]}
# Otherwise we need to use 'And'
elif filter_tags:
payload["Filter"] = {"And": {"Tags": ansible_dict_to_boto3_tag_list(filter_tags)}}
if filter_prefix:
payload["Filter"]["And"]["Prefix"] = filter_prefix

return payload
Loading
Loading