@@ -86,6 +86,34 @@ func (r *credentialResource) ModifyPlan(ctx context.Context, req resource.Modify
8686 }
8787}
8888
89+ // ModifyPlan implements resource.ResourceWithModifyPlan.
90+ func (r * credentialResource ) ModifyPlan (ctx context.Context , req resource.ModifyPlanRequest , resp * resource.ModifyPlanResponse ) { // nolint:gocritic // function signature required by Terraform
91+ p := path .Root ("expiration_timestamp" )
92+ var (
93+ stateDate time.Time
94+ planDate time.Time
95+ )
96+
97+ resp .Diagnostics .Append (utils .GetTimeFromStringAttribute (ctx , p , req .State , time .RFC3339 , & stateDate )... )
98+ if resp .Diagnostics .HasError () {
99+ return
100+ }
101+ resp .Diagnostics .Append (utils .GetTimeFromStringAttribute (ctx , p , resp .Plan , time .RFC3339 , & planDate )... )
102+ if resp .Diagnostics .HasError () {
103+ return
104+ }
105+
106+ // replace the planned expiration time with the current state date, iff they represent
107+ // the same point in time (but perhaps with different textual representation)
108+ // this will prevent no-op updates
109+ if stateDate .Equal (planDate ) {
110+ resp .Diagnostics .Append (resp .Plan .SetAttribute (ctx , p , types .StringValue (stateDate .Format (time .RFC3339 )))... )
111+ if resp .Diagnostics .HasError () {
112+ return
113+ }
114+ }
115+ }
116+
89117// Metadata returns the resource type name.
90118func (r * credentialResource ) Metadata (_ context.Context , req resource.MetadataRequest , resp * resource.MetadataResponse ) {
91119 resp .TypeName = req .ProviderTypeName + "_objectstorage_credential"
@@ -202,6 +230,7 @@ func (r *credentialResource) Schema(_ context.Context, _ resource.SchemaRequest,
202230 },
203231 PlanModifiers : []planmodifier.String {
204232 stringplanmodifier .UseStateForUnknown (),
233+ stringplanmodifier .RequiresReplace (),
205234 },
206235 },
207236 "region" : schema.StringAttribute {
@@ -265,6 +294,25 @@ func (r *credentialResource) Create(ctx context.Context, req resource.CreateRequ
265294 core .LogAndAddError (ctx , & resp .Diagnostics , "Error creating credential" , fmt .Sprintf ("Processing API payload: %v" , err ))
266295 return
267296 }
297+
298+ var (
299+ actualDate time.Time
300+ planDate time.Time
301+ )
302+ resp .Diagnostics .Append (utils .ToTime (ctx , time .RFC3339 , model .ExpirationTimestamp , & actualDate )... )
303+ if resp .Diagnostics .HasError () {
304+ return
305+ }
306+ resp .Diagnostics .Append (utils .GetTimeFromStringAttribute (ctx , path .Root ("expiration_timestamp" ), req .Plan , time .RFC3339 , & planDate )... )
307+ if resp .Diagnostics .HasError () {
308+ return
309+ }
310+ // replace the planned expiration date with the original date, iff
311+ // they represent the same point in time, (perhaps with different textual representations)
312+ if actualDate .Equal (planDate ) {
313+ model .ExpirationTimestamp = types .StringValue (planDate .Format (time .RFC3339 ))
314+ }
315+
268316 diags = resp .State .Set (ctx , model )
269317 resp .Diagnostics .Append (diags ... )
270318 if resp .Diagnostics .HasError () {
@@ -301,6 +349,26 @@ func (r *credentialResource) Read(ctx context.Context, req resource.ReadRequest,
301349 resp .State .RemoveResource (ctx )
302350 return
303351 }
352+ var (
353+ currentApiDate time.Time
354+ stateDate time.Time
355+ )
356+
357+ resp .Diagnostics .Append (utils .ToTime (ctx , time .RFC3339 , model .ExpirationTimestamp , & currentApiDate )... )
358+ if resp .Diagnostics .HasError () {
359+ return
360+ }
361+
362+ resp .Diagnostics .Append (utils .GetTimeFromStringAttribute (ctx , path .Root ("expiration_timestamp" ), req .State , time .RFC3339 , & stateDate )... )
363+ if resp .Diagnostics .HasError () {
364+ return
365+ }
366+
367+ // replace the resulting expiration date with the original date, iff
368+ // they represent the same point in time, (perhaps with different textual representations)
369+ if currentApiDate .Equal (stateDate ) {
370+ model .ExpirationTimestamp = types .StringValue (stateDate .Format (time .RFC3339 ))
371+ }
304372
305373 // Set refreshed state
306374 diags = resp .State .Set (ctx , model )
@@ -312,9 +380,16 @@ func (r *credentialResource) Read(ctx context.Context, req resource.ReadRequest,
312380}
313381
314382// Update updates the resource and sets the updated Terraform state on success.
315- func (r * credentialResource ) Update (ctx context.Context , _ resource.UpdateRequest , resp * resource.UpdateResponse ) { // nolint:gocritic // function signature required by Terraform
316- // Update shouldn't be called
317- core .LogAndAddError (ctx , & resp .Diagnostics , "Error updating credential" , "Credential can't be updated" )
383+ func (r * credentialResource ) Update (_ context.Context , _ resource.UpdateRequest , _ * resource.UpdateResponse ) { // nolint:gocritic // function signature required by Terraform
384+ /*
385+ While a credential cannot be updated, the Update call must not be prevented with an error:
386+ When the expiration timestamp has been updated to the same point in time, but e.g. with a different timezone,
387+ terraform will still trigger an Update due to the computed attributes. These will not change,
388+ but terraform has no way of knowing this without calling this function. So it is
389+ still updated as a no-op.
390+ A possible enhancement would be to emit an error, if it is attempted to change one of the not computed attributes
391+ and abort with an error in this case.
392+ */
318393}
319394
320395// Delete deletes the resource and removes the Terraform state on success.
0 commit comments