Skip to content

Commit 26b9508

Browse files
authored
feat(storage): implement lifecycle transitions (#148)
* feat(storage): implement lifecycle transitions * feat(storage): add noncurrent object transitions * style: fix typo
1 parent 2406539 commit 26b9508

File tree

7 files changed

+187
-9
lines changed

7 files changed

+187
-9
lines changed

coreweave/object_storage/resource_bucket_lifecycle_configuration.go

Lines changed: 112 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package objectstorage
33
import (
44
"bytes"
55
"context"
6+
"encoding/json"
67
"errors"
78
"fmt"
89
"slices"
@@ -16,12 +17,17 @@ import (
1617

1718
"github.com/hashicorp/hcl/v2/hclsyntax"
1819
"github.com/hashicorp/hcl/v2/hclwrite"
20+
"github.com/hashicorp/terraform-plugin-framework-validators/int32validator"
21+
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
1922
"github.com/hashicorp/terraform-plugin-framework/attr"
23+
"github.com/hashicorp/terraform-plugin-framework/path"
2024
"github.com/hashicorp/terraform-plugin-framework/resource"
2125
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
2226
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
2327
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
28+
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
2429
"github.com/hashicorp/terraform-plugin-framework/types"
30+
"github.com/hashicorp/terraform-plugin-log/tflog"
2531
"github.com/zclconf/go-cty/cty"
2632
)
2733

@@ -53,13 +59,15 @@ type BucketLifecycleResourceModel struct {
5359

5460
// LifecycleRuleModel maps a single lifecycle rule block.
5561
type LifecycleRuleModel struct {
56-
ID types.String `tfsdk:"id"`
57-
Prefix types.String `tfsdk:"prefix"`
58-
Status types.String `tfsdk:"status"`
59-
Expiration *ExpirationModel `tfsdk:"expiration"`
60-
NoncurrentVersionExpiration *NoncurrentVersionExpirationModel `tfsdk:"noncurrent_version_expiration"`
61-
AbortIncompleteMultipart *AbortIncompleteMultipartModel `tfsdk:"abort_incomplete_multipart_upload"`
62-
Filter *FilterModel `tfsdk:"filter"`
62+
ID types.String `tfsdk:"id"`
63+
Prefix types.String `tfsdk:"prefix"`
64+
Status types.String `tfsdk:"status"`
65+
Expiration *ExpirationModel `tfsdk:"expiration"`
66+
Transitions []*TransitionModel `tfsdk:"transition"`
67+
NoncurrentVersionExpiration *NoncurrentVersionExpirationModel `tfsdk:"noncurrent_version_expiration"`
68+
NoncurrentVersionTransitions []*NoncurrentVersionTransitionModel `tfsdk:"noncurrent_version_transition"`
69+
AbortIncompleteMultipart *AbortIncompleteMultipartModel `tfsdk:"abort_incomplete_multipart_upload"`
70+
Filter *FilterModel `tfsdk:"filter"`
6371
}
6472

6573
// ExpirationModel maps the expiration sub-block.
@@ -69,12 +77,26 @@ type ExpirationModel struct {
6977
ExpiredObjectDeleteMarker types.Bool `tfsdk:"expired_object_delete_marker"`
7078
}
7179

80+
// TransitionModel maps the transition sub-block.
81+
type TransitionModel struct {
82+
Date types.String `tfsdk:"date"`
83+
Days types.Int32 `tfsdk:"days"`
84+
StorageClass types.String `tfsdk:"storage_class"`
85+
}
86+
7287
// NoncurrentVersionExpirationModel maps the noncurrent_version_expiration sub-block.
7388
type NoncurrentVersionExpirationModel struct {
7489
NoncurrentDays types.Int32 `tfsdk:"noncurrent_days"`
7590
NewerNoncurrentVersions types.Int32 `tfsdk:"newer_noncurrent_versions"`
7691
}
7792

93+
// NoncurrentVersionTransitionModel maps the noncurrent_version_transition sub-block.
94+
type NoncurrentVersionTransitionModel struct {
95+
NoncurrentDays types.Int32 `tfsdk:"noncurrent_days"`
96+
NewerNoncurrentVersions types.Int32 `tfsdk:"newer_noncurrent_versions"`
97+
StorageClass types.String `tfsdk:"storage_class"`
98+
}
99+
78100
// AbortIncompleteMultipartModel maps the abort_incomplete_multipart_upload sub-block.
79101
type AbortIncompleteMultipartModel struct {
80102
DaysAfterInitiation types.Int32 `tfsdk:"days_after_initiation"`
@@ -157,6 +179,7 @@ func (r *BucketLifecycleResource) Schema(ctx context.Context, req resource.Schem
157179
},
158180
"days": schema.Int32Attribute{
159181
Optional: true,
182+
Validators: []validator.Int32{int32validator.AtLeast(0)},
160183
MarkdownDescription: "Number of days after object creation for expiration",
161184
},
162185
"expired_object_delete_marker": schema.BoolAttribute{
@@ -229,6 +252,51 @@ func (r *BucketLifecycleResource) Schema(ctx context.Context, req resource.Schem
229252
},
230253
},
231254
},
255+
"noncurrent_version_transition": schema.SetNestedBlock{
256+
NestedObject: schema.NestedBlockObject{
257+
Attributes: map[string]schema.Attribute{
258+
"newer_noncurrent_versions": schema.Int32Attribute{
259+
Optional: true,
260+
Validators: []validator.Int32{int32validator.AtLeast(1)},
261+
MarkdownDescription: "Number of noncurrent versions to retain",
262+
},
263+
"noncurrent_days": schema.Int32Attribute{
264+
Required: true,
265+
Validators: []validator.Int32{int32validator.AtLeast(0)},
266+
MarkdownDescription: "Number of days after object becomes noncurrent before the transition may occur",
267+
},
268+
"storage_class": schema.StringAttribute{
269+
Required: true,
270+
MarkdownDescription: "Storage class to transition noncurrent objects to",
271+
},
272+
},
273+
},
274+
},
275+
"transition": schema.SetNestedBlock{
276+
NestedObject: schema.NestedBlockObject{
277+
Attributes: map[string]schema.Attribute{
278+
"date": schema.StringAttribute{
279+
Optional: true,
280+
Validators: []validator.String{
281+
stringvalidator.ConflictsWith(path.MatchRelative().AtParent().AtName("days")),
282+
},
283+
MarkdownDescription: "ISO8601 date when objects transition",
284+
},
285+
"days": schema.Int32Attribute{
286+
Optional: true,
287+
Validators: []validator.Int32{
288+
int32validator.ConflictsWith(path.MatchRelative().AtParent().AtName("date")),
289+
int32validator.AtLeast(0),
290+
},
291+
MarkdownDescription: "Number of days after object creation for transition",
292+
},
293+
"storage_class": schema.StringAttribute{
294+
Required: true,
295+
MarkdownDescription: "Storage class to transition objects to",
296+
},
297+
},
298+
},
299+
},
232300
},
233301
},
234302
},
@@ -279,6 +347,16 @@ func expandRules(ctx context.Context, in []LifecycleRuleModel) []s3types.Lifecyc
279347
}
280348
rule.Expiration = &exp
281349
}
350+
for _, transition := range r.Transitions {
351+
t := s3types.Transition{
352+
StorageClass: s3types.TransitionStorageClass(transition.StorageClass.ValueString()),
353+
Days: transition.Days.ValueInt32Pointer(),
354+
}
355+
if !transition.Date.IsNull() {
356+
t.Date = aws.Time(parseISO8601(transition.Date.ValueString()))
357+
}
358+
rule.Transitions = append(rule.Transitions, t)
359+
}
282360
if r.NoncurrentVersionExpiration != nil {
283361
nc := s3types.NoncurrentVersionExpiration{}
284362
if !r.NoncurrentVersionExpiration.NoncurrentDays.IsNull() {
@@ -515,6 +593,12 @@ func (r *BucketLifecycleResource) Create(ctx context.Context, req resource.Creat
515593
}
516594

517595
rules := expandRules(ctx, data.Rule)
596+
rulesJSON, err := json.Marshal(rules)
597+
if err != nil {
598+
resp.Diagnostics.AddError("Failed to marshal lifecycle rules to JSON", err.Error())
599+
return
600+
}
601+
tflog.Debug(ctx, "creating lifecycle rules for bucket", map[string]any{"rules": string(rulesJSON), "bucket": data.Bucket.ValueString()})
518602
lifecycleConfig := &s3types.BucketLifecycleConfiguration{
519603
Rules: rules,
520604
}
@@ -567,13 +651,34 @@ func flattenLifecycleRules(in []s3types.LifecycleRule) []LifecycleRuleModel {
567651
mdl.Expiration = expiration
568652
}
569653

654+
for _, t := range r.Transitions {
655+
transition := &TransitionModel{
656+
Date: types.StringNull(),
657+
Days: types.Int32PointerValue(t.Days),
658+
StorageClass: types.StringValue(string(t.StorageClass)),
659+
}
660+
if t.Date != nil {
661+
transition.Date = types.StringValue(t.Date.Format(time.RFC3339))
662+
}
663+
mdl.Transitions = append(mdl.Transitions, transition)
664+
}
665+
570666
if r.NoncurrentVersionExpiration != nil {
571667
mdl.NoncurrentVersionExpiration = &NoncurrentVersionExpirationModel{
572668
NoncurrentDays: types.Int32PointerValue(r.NoncurrentVersionExpiration.NoncurrentDays),
573669
NewerNoncurrentVersions: types.Int32PointerValue(r.NoncurrentVersionExpiration.NewerNoncurrentVersions),
574670
}
575671
}
576672

673+
for _, nct := range r.NoncurrentVersionTransitions {
674+
ncTransition := &NoncurrentVersionTransitionModel{
675+
NoncurrentDays: types.Int32PointerValue(nct.NoncurrentDays),
676+
StorageClass: types.StringValue(string(nct.StorageClass)),
677+
NewerNoncurrentVersions: types.Int32PointerValue(nct.NewerNoncurrentVersions),
678+
}
679+
mdl.NoncurrentVersionTransitions = append(mdl.NoncurrentVersionTransitions, ncTransition)
680+
}
681+
577682
if r.AbortIncompleteMultipartUpload != nil {
578683
mdl.AbortIncompleteMultipart = &AbortIncompleteMultipartModel{
579684
DaysAfterInitiation: types.Int32PointerValue(r.AbortIncompleteMultipartUpload.DaysAfterInitiation),

coreweave/object_storage/resource_bucket_lifecycle_configuration_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,26 @@ func TestBucketLifecycleConfiguration(t *testing.T) {
264264
NewerNoncurrentVersions: types.Int32Value(2),
265265
},
266266
}
267+
transitionOnly := objectstorage.LifecycleRuleModel{
268+
ID: types.StringValue("transition-only"),
269+
Status: types.StringValue("Enabled"),
270+
Transitions: []*objectstorage.TransitionModel{
271+
{
272+
Days: types.Int32Value(30),
273+
StorageClass: types.StringValue("STANDARD_IA"),
274+
},
275+
},
276+
}
277+
noncurrentTransitionOnly := objectstorage.LifecycleRuleModel{
278+
ID: types.StringValue("noncurrent-transition-only"),
279+
Status: types.StringValue("Enabled"),
280+
NoncurrentVersionTransitions: []*objectstorage.NoncurrentVersionTransitionModel{
281+
{
282+
NoncurrentDays: types.Int32Value(30),
283+
StorageClass: types.StringValue("STANDARD_IA"),
284+
},
285+
},
286+
}
267287
abortOnly := objectstorage.LifecycleRuleModel{
268288
ID: types.StringValue("abort-only"),
269289
Status: types.StringValue("Enabled"),
@@ -340,6 +360,30 @@ func TestBucketLifecycleConfiguration(t *testing.T) {
340360
},
341361
},
342362
}),
363+
createLifecycleTestStep(ctx, t, lifecycleTestConfig{
364+
name: "transition only",
365+
resourceName: resourceName,
366+
bucket: bucket,
367+
bucketVersioning: versioning,
368+
rules: []objectstorage.LifecycleRuleModel{transitionOnly},
369+
configPlanChecks: resource.ConfigPlanChecks{
370+
PreApply: []plancheck.PlanCheck{
371+
plancheck.ExpectResourceAction(fmt.Sprintf("coreweave_object_storage_bucket_lifecycle_configuration.%s", resourceName), plancheck.ResourceActionUpdate),
372+
},
373+
},
374+
}),
375+
createLifecycleTestStep(ctx, t, lifecycleTestConfig{
376+
name: "noncurrent transition only",
377+
resourceName: resourceName,
378+
bucket: bucket,
379+
bucketVersioning: versioning,
380+
rules: []objectstorage.LifecycleRuleModel{noncurrentTransitionOnly},
381+
configPlanChecks: resource.ConfigPlanChecks{
382+
PreApply: []plancheck.PlanCheck{
383+
plancheck.ExpectResourceAction(fmt.Sprintf("coreweave_object_storage_bucket_lifecycle_configuration.%s", resourceName), plancheck.ResourceActionUpdate),
384+
},
385+
},
386+
}),
343387
createLifecycleTestStep(ctx, t, lifecycleTestConfig{
344388
name: "abort only",
345389
resourceName: resourceName,

docs/resources/object_storage_bucket_lifecycle_configuration.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,9 @@ Optional:
100100
- `filter` (Block, Optional) (see [below for nested schema](#nestedblock--rule--filter))
101101
- `id` (String) Unique identifier for the rule
102102
- `noncurrent_version_expiration` (Block, Optional) (see [below for nested schema](#nestedblock--rule--noncurrent_version_expiration))
103+
- `noncurrent_version_transition` (Block Set) (see [below for nested schema](#nestedblock--rule--noncurrent_version_transition))
103104
- `prefix` (String) Object key prefix to which the rule applies
105+
- `transition` (Block Set) (see [below for nested schema](#nestedblock--rule--transition))
104106

105107
<a id="nestedblock--rule--abort_incomplete_multipart_upload"></a>
106108
### Nested Schema for `rule.abort_incomplete_multipart_upload`
@@ -160,6 +162,32 @@ Optional:
160162
- `newer_noncurrent_versions` (Number) Number of noncurrent versions to retain
161163
- `noncurrent_days` (Number) Days after becoming noncurrent before deletion
162164

165+
166+
<a id="nestedblock--rule--noncurrent_version_transition"></a>
167+
### Nested Schema for `rule.noncurrent_version_transition`
168+
169+
Required:
170+
171+
- `noncurrent_days` (Number) Number of days after object becomes noncurrent before the transition may occur
172+
- `storage_class` (String) Storage class to transition noncurrent objects to
173+
174+
Optional:
175+
176+
- `newer_noncurrent_versions` (Number) Number of noncurrent versions to retain
177+
178+
179+
<a id="nestedblock--rule--transition"></a>
180+
### Nested Schema for `rule.transition`
181+
182+
Required:
183+
184+
- `storage_class` (String) Storage class to transition objects to
185+
186+
Optional:
187+
188+
- `date` (String) ISO8601 date when objects transition
189+
- `days` (Number) Number of days after object creation for transition
190+
163191
## Import
164192

165193
Import is supported using the following syntax:

docs/resources/object_storage_bucket_versioning.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ resource "coreweave_object_storage_bucket_versioning" "default" {
2424
versioning_configuration {
2525
status = "Enabled"
2626
}
27-
2827
}
2928
```
3029

examples/resources/coreweave_object_storage_bucket_versioning/resource.tf

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,4 @@ resource "coreweave_object_storage_bucket_versioning" "default" {
99
versioning_configuration {
1010
status = "Enabled"
1111
}
12-
1312
}

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ require (
2020
github.com/hashicorp/go-uuid v1.0.3
2121
github.com/hashicorp/hcl/v2 v2.23.0
2222
github.com/hashicorp/terraform-plugin-framework v1.15.1
23+
github.com/hashicorp/terraform-plugin-framework-validators v0.18.0
2324
github.com/hashicorp/terraform-plugin-go v0.27.0
2425
github.com/hashicorp/terraform-plugin-log v0.9.0
2526
github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.0

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,8 @@ github.com/hashicorp/terraform-json v0.23.0 h1:sniCkExU4iKtTADReHzACkk8fnpQXrdD2
138138
github.com/hashicorp/terraform-json v0.23.0/go.mod h1:MHdXbBAbSg0GvzuWazEGKAn/cyNfIB7mN6y7KJN6y2c=
139139
github.com/hashicorp/terraform-plugin-framework v1.15.1 h1:2mKDkwb8rlx/tvJTlIcpw0ykcmvdWv+4gY3SIgk8Pq8=
140140
github.com/hashicorp/terraform-plugin-framework v1.15.1/go.mod h1:hxrNI/GY32KPISpWqlCoTLM9JZsGH3CyYlir09bD/fI=
141+
github.com/hashicorp/terraform-plugin-framework-validators v0.18.0 h1:OQnlOt98ua//rCw+QhBbSqfW3QbwtVrcdWeQN5gI3Hw=
142+
github.com/hashicorp/terraform-plugin-framework-validators v0.18.0/go.mod h1:lZvZvagw5hsJwuY7mAY6KUz45/U6fiDR0CzQAwWD0CA=
141143
github.com/hashicorp/terraform-plugin-go v0.27.0 h1:ujykws/fWIdsi6oTUT5Or4ukvEan4aN9lY+LOxVP8EE=
142144
github.com/hashicorp/terraform-plugin-go v0.27.0/go.mod h1:FDa2Bb3uumkTGSkTFpWSOwWJDwA7bf3vdP3ltLDTH6o=
143145
github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0=

0 commit comments

Comments
 (0)