Design#
Goals#
- Converge an S3 bucket to a desired spec for the supported features:
- tags
- versioning
- public access block
- Stay declarative: Desired → Current → Plan → Apply → Result
- Keep the module primitive and composable: no “SDK wrapper” surface
Future scope (not implemented yet): encryption, lifecycle rules, bucket policy templates (e.g., TLS-only), CloudFront-origin/OAC policy convergence.
Architecture#
The purpose of this module is to provide a convergent management interface to Amazon S3 buckets.
We do not expose the full boto3 surface area. Instead, we converge a pragmatic subset of bucket features that are commonly needed by dxaws primitives. This keeps the public contract small, testable, and stable.
Declarative Convergence#
This module follows a declarative convergence model.
Instead of issuing imperative AWS calls directly, callers describe the desired state. The module then:
- Plans what needs to change to reach that state
- Applies the plan using provider-backed AWS operations
This separation makes behavior predictable, testable, and idempotent, while keeping AWS-specific logic isolated in providers and making plans easy to unit test.
Key Concepts#
Providers#
Providers encapsulate AWS SDK (boto3) calls. The rest of the module treats the provider as an interface so that planning/execution can be unit tested without AWS.
Manager (public facade)#
manager.py is the public entrypoint. It:
- Validates the desired state
- Reads the current state from the provider
- Calls the planner to compute a plan
- Applies the plan via the executor
- Returns a result object
Planner / Executor#
- planner.py computes a deterministic plan (a list of actions) by comparing desired vs current state.
- executor.py applies the plan using provider-backed operations.
planner.py#
Planner rules are deterministic and include purpose-driven invariants:
- If bucket doesn’t exist →
CREATE_BUCKET - Tags drift →
PUT_TAGS - Public access block drift →
PUT_PUBLIC_ACCESS_BLOCK - Versioning drift →
PUT_VERSIONING
These four rules define the current contract that higher-level modules can rely on.
Future planner rules (not implemented yet)#
- Encryption drift →
PUT_ENCRYPTION - Bucket policy convergence:
- TLS-only deny statement
- CloudFront OAC allow-read statement
- Lifecycle rules / intelligent-tiering / replication templates
executor.py#
The executor applies actions in a safe order:
- Create bucket (if needed)
- Public access block
- Versioning
- Tags
Returns S3BucketResult from a provider read-back.
providers/base.py (interface)#
boto3 methods that will be encapsulated:
get_current(name) -> S3BucketCurrentcreate_bucket(desired)put_tags(name, tags)put_public_access_block(name, config)put_versioning(name, enabled: bool)destroy_bucket(name)get_outputs(name) -> S3BucketResult
AWS implementation in providers/aws.py uses boto3 S3.
constants.py / exceptions.py#
Defaults:
DEFAULT_ENCRYPTION = "SSE-S3"DEFAULT_BLOCK_PUBLIC_ACCESS = TrueDEFAULT_ENFORCE_TLS = True
Exceptions:
InvalidDesiredStateErrorBucketNameErrorMissingCloudFrontInfoError(only if strict mode is enabled)
CLI Surface#
The CLI orchestration for S3 lives in dxaws-cli.
This module remains Python-first and focuses on providing a stable manager/provider contract that higher-level tooling can call.
Tradeoffs#
Eventual Consistency#
Some S3 operations (especially create/delete and feature reads immediately after) can be eventually consistent.
Acceptance tests intentionally include:
- a visibility wait after create
- drift + converge checks
- a delete waiter with small grace delays
This keeps the contract stable while acknowledging AWS propagation behavior.
CloudFront / Policy Convergence#
CloudFront-origin and policy convergence (TLS-only, OAC statements, etc.) are intentionally deferred. The current module stays focused on the primitive features we already rely on.