Skip to content

Commit 37973c2

Browse files
klaemovimtor
andauthored
feat(bucket): support custom bucket lifecycle rule ids (#6312)
* feat: support custom bucket lifecycle rule ids * Update examples/aws-bucket-lifecycle-rules/sst.config.ts Co-authored-by: Victor Navarro <vn4varro@gmail.com> * Update examples/aws-bucket-lifecycle-rules/sst.config.ts Co-authored-by: Victor Navarro <vn4varro@gmail.com> --------- Co-authored-by: Victor Navarro <vn4varro@gmail.com>
1 parent 5d4c36e commit 37973c2

File tree

3 files changed

+117
-1
lines changed

3 files changed

+117
-1
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/* tslint:disable */
2+
/* eslint-disable */
3+
import "sst"
4+
declare module "sst" {
5+
export interface Resource {
6+
MyBucket: {
7+
name: string
8+
type: "sst.aws.Bucket"
9+
}
10+
}
11+
}
12+
export {}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/// <reference path="./.sst/platform/config.d.ts" />
2+
3+
/**
4+
* ## Bucket lifecycle policies
5+
*
6+
* Configure S3 bucket lifecycle policies to expire objects automatically.
7+
*/
8+
export default $config({
9+
app(input) {
10+
return {
11+
name: "aws-bucket-lifecycle-rules",
12+
home: "aws",
13+
removal: input?.stage === "production" ? "retain" : "remove",
14+
};
15+
},
16+
async run() {
17+
const bucket = new sst.aws.Bucket("MyBucket", {
18+
lifecycle: [
19+
{
20+
expiresIn: "60 days",
21+
},
22+
{
23+
id: "expire-tmp-files",
24+
prefix: "tmp/",
25+
expiresIn: "30 days",
26+
},
27+
{
28+
prefix: "data/",
29+
expiresAt: "2028-12-31",
30+
},
31+
],
32+
});
33+
34+
return {
35+
bucket: bucket.name,
36+
};
37+
},
38+
});

platform/src/components/aws/bucket.ts

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,25 @@ interface BucketCorsArgs {
100100
}
101101

102102
interface BucketLifecycleArgs {
103+
/**
104+
* The unique identifier for the lifecycle rule.
105+
*
106+
* This ID must be unique across all lifecycle rules in the bucket and cannot exceed 255 characters.
107+
* Whitespace-only values are not allowed.
108+
*
109+
* If not provided, SST will generate a unique ID based on the bucket component name and rule index.
110+
*
111+
* @example
112+
* Use stable IDs to ensure rule identity is preserved when reordering rules.
113+
* ```js
114+
* {
115+
* id: "expire-tmp-files",
116+
* prefix: "/tmp",
117+
* expiresIn: "7 days"
118+
* }
119+
* ```
120+
*/
121+
id?: Input<string>;
103122
/**
104123
* An S3 object key prefix that the lifecycle rule applies to.
105124
* @example
@@ -443,6 +462,24 @@ export interface BucketArgs {
443462
* ]
444463
* }
445464
* ```
465+
*
466+
* Use stable IDs to preserve rule identity when reordering.
467+
* ```js
468+
* {
469+
* lifecycle: [
470+
* {
471+
* id: "expire-tmp-files",
472+
* prefix: "/tmp",
473+
* expiresIn: "7 days"
474+
* },
475+
* {
476+
* id: "archive-old-logs",
477+
* prefix: "/logs",
478+
* expiresIn: "90 days"
479+
* }
480+
* ]
481+
* }
482+
* ```
446483
*/
447484
lifecycle?: Input<Input<Prettify<BucketLifecycleArgs>>[]>;
448485
/**
@@ -924,14 +961,43 @@ export class Bucket extends Component implements Link.Linkable {
924961
return output(args.lifecycle).apply((lifecycleRules) => {
925962
if (!lifecycleRules || lifecycleRules.length === 0) return;
926963

964+
const seenIds = new Map<string, number>();
965+
966+
const resolvedIds = lifecycleRules.map((rule, index) => {
967+
const rawId = rule.id ?? `${name}LifecycleRule${index}`;
968+
const resolvedId = rawId.trim();
969+
970+
if (resolvedId.length === 0) {
971+
throw new VisibleError(
972+
`Lifecycle rule at index ${index} has an empty or whitespace-only "id". Please provide a valid id or omit it to use the auto-generated id.`,
973+
);
974+
}
975+
976+
if (resolvedId.length > 255) {
977+
throw new VisibleError(
978+
`Lifecycle rule at index ${index} has an "id" that is ${resolvedId.length} characters long. AWS S3 lifecycle rule IDs cannot exceed 255 characters.`,
979+
);
980+
}
981+
982+
const existingIndex = seenIds.get(resolvedId);
983+
if (existingIndex !== undefined) {
984+
throw new VisibleError(
985+
`Lifecycle rule "id" values must be unique. The id "${resolvedId}" is used by rules at indexes ${existingIndex} and ${index}.`,
986+
);
987+
}
988+
seenIds.set(resolvedId, index);
989+
990+
return resolvedId;
991+
});
992+
927993
return new s3.BucketLifecycleConfigurationV2(
928994
...transform(
929995
args.transform?.lifecycle,
930996
`${name}Lifecycle`,
931997
{
932998
bucket: bucket.bucket,
933999
rules: lifecycleRules.map((rule, index) => ({
934-
id: `${name}LifecycleRule${index}`,
1000+
id: resolvedIds[index],
9351001
status: rule.enabled !== false ? "Enabled" : "Disabled",
9361002
expiration:
9371003
rule.expiresIn || rule.expiresAt

0 commit comments

Comments
 (0)