Skip to content

Commit 5ca2088

Browse files
authored
fix: s3 lifecycle noncurrent version transitions are not applied (#161)
* fix: detect and fail when s3 lifecycle rules fail to apply * fix: save noncurrent version transitions when copying models
1 parent 5c65e0d commit 5ca2088

File tree

1 file changed

+28
-10
lines changed

1 file changed

+28
-10
lines changed

coreweave/object_storage/resource_bucket_lifecycle_configuration.go

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,7 @@ func parseISO8601(s string) time.Time {
324324
return t
325325
}
326326

327-
func expandRules(ctx context.Context, in []LifecycleRuleModel) []s3types.LifecycleRule {
327+
func expandRules(ctx context.Context, in []LifecycleRuleModel) []s3types.LifecycleRule { //nolint:gocyclo
328328
out := make([]s3types.LifecycleRule, 0, len(in))
329329
for _, r := range in {
330330
rule := s3types.LifecycleRule{
@@ -367,6 +367,16 @@ func expandRules(ctx context.Context, in []LifecycleRuleModel) []s3types.Lifecyc
367367
}
368368
rule.NoncurrentVersionExpiration = &nc
369369
}
370+
if r.NoncurrentVersionTransitions != nil {
371+
for _, noncurrentTransition := range r.NoncurrentVersionTransitions {
372+
nct := s3types.NoncurrentVersionTransition{
373+
StorageClass: s3types.TransitionStorageClass(noncurrentTransition.StorageClass.ValueString()),
374+
NoncurrentDays: noncurrentTransition.NoncurrentDays.ValueInt32Pointer(),
375+
NewerNoncurrentVersions: noncurrentTransition.NewerNoncurrentVersions.ValueInt32Pointer(),
376+
}
377+
rule.NoncurrentVersionTransitions = append(rule.NoncurrentVersionTransitions, nct)
378+
}
379+
}
370380
if r.AbortIncompleteMultipart != nil {
371381
ai := s3types.AbortIncompleteMultipartUpload{
372382
DaysAfterInitiation: aws.Int32(r.AbortIncompleteMultipart.DaysAfterInitiation.ValueInt32()),
@@ -559,24 +569,27 @@ func eqLifecycleRule(a, b s3types.LifecycleRule) bool { //nolint:gocyclo
559569
return true
560570
}
561571

562-
func waitForLifecycleConfig(parentCtx context.Context, client *s3.Client, bucket string, expected s3types.BucketLifecycleConfiguration) error {
572+
func waitForLifecycleConfig(parentCtx context.Context, client *s3.Client, bucket string, expected s3types.BucketLifecycleConfiguration) (*s3.GetBucketLifecycleConfigurationOutput, error) {
563573
// make a sorted copy of expected rules
564-
exp := append([]s3types.LifecycleRule(nil), expected.Rules...)
565-
slices.SortFunc(exp, cmpLifecycleRule)
574+
exp := slices.SortedFunc(slices.Values(expected.Rules), cmpLifecycleRule)
566575

567-
return coreweave.PollUntil("bucket lifecycle configuration", parentCtx, 5*time.Second, 5*time.Minute, func(ctx context.Context) (bool, error) {
568-
out, err := client.GetBucketLifecycleConfiguration(ctx, &s3.GetBucketLifecycleConfigurationInput{Bucket: aws.String(bucket)})
576+
var out *s3.GetBucketLifecycleConfigurationOutput
577+
err := coreweave.PollUntil("bucket lifecycle configuration", parentCtx, 5*time.Second, 5*time.Minute, func(ctx context.Context) (bool, error) {
578+
result, err := client.GetBucketLifecycleConfiguration(ctx, &s3.GetBucketLifecycleConfigurationInput{Bucket: aws.String(bucket)})
569579
if err != nil {
580+
out = nil
570581
if isTransientS3Error(err) {
571582
return false, nil
572583
}
573584
return false, err
574585
}
586+
out = result
575587

576-
rules := out.Rules
577-
slices.SortFunc(rules, cmpLifecycleRule)
588+
// Make sorted a copy of the slice to sort for comparison, to avoid mutating the returned slice by reference.
589+
rules := slices.SortedFunc(slices.Values(result.Rules), cmpLifecycleRule)
578590
return slices.EqualFunc(exp, rules, eqLifecycleRule), nil
579591
})
592+
return out, err
580593
}
581594

582595
func (r *BucketLifecycleResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
@@ -618,9 +631,11 @@ func (r *BucketLifecycleResource) Create(ctx context.Context, req resource.Creat
618631
}
619632

620633
// wait for lifecycle config to be read back from s3 API since it is not guaranteed to propagate immediately
621-
if err := waitForLifecycleConfig(ctx, s3c, data.Bucket.ValueString(), *lifecycleConfig); err != nil {
634+
if result, err := waitForLifecycleConfig(ctx, s3c, data.Bucket.ValueString(), *lifecycleConfig); err != nil {
622635
handleS3Error(err, &resp.Diagnostics, data.Bucket.ValueString())
623636
return
637+
} else {
638+
data.Rule = flattenLifecycleRules(result.Rules)
624639
}
625640

626641
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
@@ -787,9 +802,12 @@ func (r *BucketLifecycleResource) Update(ctx context.Context, req resource.Updat
787802
}
788803

789804
// wait for lifecycle config to be read back from s3 API since it is not guaranteed to propagate immediately
790-
if err := waitForLifecycleConfig(ctx, s3c, data.Bucket.ValueString(), *lifecycleConfig); err != nil {
805+
if result, err := waitForLifecycleConfig(ctx, s3c, data.Bucket.ValueString(), *lifecycleConfig); err != nil {
791806
handleS3Error(err, &resp.Diagnostics, data.Bucket.ValueString())
792807
return
808+
} else {
809+
// Read the result back into state. Terraform will detect and fail if the state does not match the plan.
810+
data.Rule = flattenLifecycleRules(result.Rules)
793811
}
794812

795813
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)

0 commit comments

Comments
 (0)