@@ -3,6 +3,7 @@ package objectstorage
33import (
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.
5561type 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.
7388type 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.
79101type 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 ),
0 commit comments