Skip to content

Commit 57b7ef3

Browse files
feat(bb-file-bucket): add explicit autoDeleteObjects opt-in escape hatch
Layers on the sandbox-only autoDeleteObjects gating with an explicit per-bucket override. autoDeleteObjects now resolves to `destroy && (options?.autoDeleteObjects ?? isSandbox)`, so a genuinely ephemeral prod bucket can opt into auto-empty (true) while the safe sandbox-only default is preserved. The `destroy &&` guard keeps CDK's construct-time invariant that auto-delete requires DESTROY. - types.ts: add FileBucketOptions.autoDeleteObjects with footgun JSDoc; cross-reference from removalPolicy docs - index.cdk.test.ts: add opt-in/opt-out/invariant tests; strengthen the non-sandbox default assertion to a positive DeletionPolicy: Retain check - changeset: document the behavior change and the new opt-in
1 parent a381d1b commit 57b7ef3

4 files changed

Lines changed: 79 additions & 17 deletions

File tree

.changeset/bb-file-bucket-removal-policy.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,8 @@
22
"@aws-blocks/bb-file-bucket": patch
33
---
44

5-
Fix `FileBucket` silently dropping bucket contents in non-sandbox stacks. Previously an explicit `removalPolicy: 'destroy'` enabled `autoDeleteObjects` in any stack, which provisions a hidden `Custom::S3AutoDeleteObjects` Lambda whose delete behavior cannot be overridden by stack-level retention Aspects (`RemovalPolicies.of(stack).retain()`). `autoDeleteObjects` is now enabled only in sandbox mode, so prod buckets honor retention.
5+
Fix `FileBucket` silently dropping bucket contents in non-sandbox stacks. Previously an explicit `removalPolicy: 'destroy'` enabled `autoDeleteObjects` in any stack, which provisions a hidden `Custom::S3AutoDeleteObjects` Lambda whose delete behavior cannot be overridden by stack-level retention Aspects (`RemovalPolicies.of(stack).retain()`). `autoDeleteObjects` now defaults to sandbox-only, so prod buckets honor retention.
6+
7+
Behavior change: in a non-sandbox stack, `removalPolicy: 'destroy'` now sets `RemovalPolicy.DESTROY` without auto-empty. `cdk destroy` on a non-empty prod bucket will fail with `DELETE_FAILED` (S3 rejects deleting a non-empty bucket) instead of silently wiping it. Callers relying on `'destroy'` for full teardown of a populated prod bucket should either empty it first or opt in explicitly (below).
8+
9+
New `FileBucketOptions.autoDeleteObjects?: boolean` escape hatch: defaults to sandbox-only behavior, but can be set `true` to force auto-empty for a genuinely-ephemeral prod bucket, or `false` to disable it even in sandbox. Only takes effect when the bucket is being destroyed.

packages/bb-file-bucket/src/index.cdk.test.ts

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,41 @@ test('CDK: non-sandbox default omits DESTROY (Aspect-overridable RETAIN) and no
127127
new FileBucket(parent, 'uploads');
128128
const template = Template.fromStack(stack);
129129
// No explicit removalPolicy → CDK default RETAIN, driven by Aspects.
130-
const buckets = template.findResources('AWS::S3::Bucket', { DeletionPolicy: 'Delete' });
131-
assert.strictEqual(Object.keys(buckets).length, 0);
130+
// Assert the positive default (RETAIN) so the intent is load-bearing rather
131+
// than relying on the absence of a 'Delete' policy.
132+
template.hasResource('AWS::S3::Bucket', { DeletionPolicy: 'Retain' });
133+
template.resourceCountIs('Custom::S3AutoDeleteObjects', 0);
134+
});
135+
136+
// ── autoDeleteObjects explicit opt-in / opt-out (escape hatch) ───────────────
137+
//
138+
// The sandbox-only default can be overridden per-bucket. Opting in lets a
139+
// genuinely-ephemeral prod bucket auto-empty on destroy; opting out disables
140+
// auto-empty even in sandbox. The DESTROY invariant still holds — auto-delete
141+
// never appears without the bucket being destroyed.
142+
143+
test('CDK: non-sandbox destroy with autoDeleteObjects:true opts into the auto-delete Lambda', () => {
144+
const { stack, parent } = setup();
145+
new FileBucket(parent, 'uploads', { removalPolicy: 'destroy', autoDeleteObjects: true });
146+
const template = Template.fromStack(stack);
147+
template.hasResource('AWS::S3::Bucket', { DeletionPolicy: 'Delete' });
148+
template.resourceCountIs('Custom::S3AutoDeleteObjects', 1);
149+
});
150+
151+
test('CDK: sandbox default with autoDeleteObjects:false opts out of the auto-delete Lambda', () => {
152+
const { stack, parent } = setup({ sandbox: true });
153+
new FileBucket(parent, 'uploads', { autoDeleteObjects: false });
154+
const template = Template.fromStack(stack);
155+
// Sandbox still drops the bucket, but the caller disabled auto-empty.
156+
template.hasResource('AWS::S3::Bucket', { DeletionPolicy: 'Delete' });
157+
template.resourceCountIs('Custom::S3AutoDeleteObjects', 0);
158+
});
159+
160+
test('CDK: autoDeleteObjects:true without destroy does NOT provision the auto-delete Lambda', () => {
161+
const { stack, parent } = setup();
162+
// No destroy → auto-delete cannot apply (invariant: auto-delete requires DESTROY).
163+
new FileBucket(parent, 'uploads', { autoDeleteObjects: true });
164+
const template = Template.fromStack(stack);
165+
template.hasResource('AWS::S3::Bucket', { DeletionPolicy: 'Retain' });
132166
template.resourceCountIs('Custom::S3AutoDeleteObjects', 0);
133167
});

packages/bb-file-bucket/src/index.cdk.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,16 +49,18 @@ export class FileBucket<O extends FileBucketOptions = FileBucketOptions> extends
4949
const isSandbox = cdk.Stack.of(this).node.tryGetContext('sandboxMode') === 'true';
5050
const destroy = options?.removalPolicy === 'destroy' || (isSandbox && options?.removalPolicy === undefined);
5151

52-
// `autoDeleteObjects` is enabled ONLY in sandbox mode — never in a prod
53-
// stack, even with an explicit `removalPolicy: 'destroy'`. CDK's L2
52+
// `autoDeleteObjects` defaults to sandbox-only — never in a prod stack,
53+
// even with an explicit `removalPolicy: 'destroy'`, unless the caller
54+
// deliberately opts in via `options.autoDeleteObjects: true`. CDK's L2
5455
// `s3.Bucket` implements auto-delete via a hidden
5556
// `Custom::S3AutoDeleteObjects` Lambda with a hardcoded delete behavior
5657
// that stack-level retention Aspects (`RemovalPolicies.of(stack).retain()`)
5758
// cannot override. Attaching it to a prod bucket would silently empty the
58-
// bucket on `cdk destroy` even when the stack intends to retain it. In
59-
// prod, `'destroy'` sets DESTROY without auto-empty; S3 rejects deleting a
60-
// non-empty bucket, which is the correct, safe behavior.
61-
const autoDeleteObjects = isSandbox && destroy;
59+
// bucket on `cdk destroy` even when the stack intends to retain it, so the
60+
// safe default is sandbox-only. It only ever applies when the bucket is
61+
// actually being destroyed (`destroy`), preserving CDK's construct-time
62+
// invariant that auto-delete requires DESTROY.
63+
const autoDeleteObjects = destroy && (options?.autoDeleteObjects ?? isSandbox);
6264

6365
// Bucket name is derived from the scope chain. Validate against S3's
6466
// naming rules at synth so an invalid name fails here rather than at

packages/bb-file-bucket/src/types.ts

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,18 @@ export interface FileBucketOptions {
2424
* preserved on `cdk destroy`).
2525
*
2626
* Pass `'destroy'` to drop the bucket on teardown (`RemovalPolicy.DESTROY`).
27-
* `autoDeleteObjects` (which empties the bucket before deletion) is enabled
28-
* **only in sandbox mode**. In a prod stack, `'destroy'` sets DESTROY but
29-
* does NOT auto-empty — S3 rejects deleting a non-empty bucket, so a
30-
* populated prod bucket is preserved rather than silently wiped.
27+
* By default `autoDeleteObjects` (which empties the bucket before deletion)
28+
* is enabled **only in sandbox mode**. In a prod stack, `'destroy'` sets
29+
* DESTROY but does NOT auto-empty — S3 rejects deleting a non-empty bucket,
30+
* so a populated prod bucket is preserved rather than silently wiped. To
31+
* opt a genuinely-ephemeral prod bucket into auto-empty behavior, set
32+
* {@link autoDeleteObjects} explicitly.
3133
*
32-
* Why sandbox-only: CDK implements `autoDeleteObjects` via a hidden
33-
* `Custom::S3AutoDeleteObjects` Lambda whose delete behavior stack-level
34-
* retention Aspects (`RemovalPolicies.of(stack).retain()`) cannot override.
35-
* Keeping it out of prod stacks ensures those Aspects remain effective.
34+
* Why sandbox-only by default: CDK implements `autoDeleteObjects` via a
35+
* hidden `Custom::S3AutoDeleteObjects` Lambda whose delete behavior
36+
* stack-level retention Aspects (`RemovalPolicies.of(stack).retain()`)
37+
* cannot override. Keeping it out of prod stacks by default ensures those
38+
* Aspects remain effective unless a caller deliberately opts out.
3639
*
3740
* Pass `'retain'` to set the policy explicitly (identical to omitting
3841
* it today, but robust against stack-layer policy overrides).
@@ -43,6 +46,25 @@ export interface FileBucketOptions {
4346
* Ignored by the mock and browser runtimes (no AWS resource to retain).
4447
*/
4548
removalPolicy?: 'destroy' | 'retain';
49+
/**
50+
* Explicitly control whether the bucket is emptied before deletion via
51+
* CDK's `autoDeleteObjects`. Only takes effect when the bucket is being
52+
* destroyed (`removalPolicy: 'destroy'`, or sandbox default).
53+
*
54+
* When omitted, defaults to sandbox-only: auto-empty is enabled in sandbox
55+
* mode and disabled in prod. Set `true` to force auto-empty for a
56+
* deliberately-ephemeral prod bucket, or `false` to disable it even in
57+
* sandbox.
58+
*
59+
* ⚠️ Enabling this provisions a hidden `Custom::S3AutoDeleteObjects` Lambda
60+
* whose delete behavior stack-level retention Aspects
61+
* (`RemovalPolicies.of(stack).retain()`) cannot override. Only opt in for a
62+
* bucket you genuinely intend to be throwaway — this is a deliberate,
63+
* visible footgun rather than the default.
64+
*
65+
* Ignored by the mock and browser runtimes.
66+
*/
67+
autoDeleteObjects?: boolean;
4668
/** Optional logger for internal operations. When omitted, a default Logger at error level is created. */
4769
logger?: ChildLogger;
4870
}

0 commit comments

Comments
 (0)