@@ -5,7 +5,6 @@ package provider
5
5
6
6
import (
7
7
"context"
8
- "encoding/json"
9
8
"errors"
10
9
"fmt"
11
10
"log"
@@ -23,6 +22,8 @@ import (
23
22
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
24
23
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
25
24
"github.com/hashicorp/terraform-plugin-framework/types"
25
+ "github.com/hashicorp/terraform-provider-tfe/internal/provider/helpers"
26
+ "github.com/hashicorp/terraform-provider-tfe/internal/provider/planmodifiers"
26
27
)
27
28
28
29
// resourceTFEVariable implements the tfe_variable resource type. Note: Much of
@@ -180,7 +181,7 @@ func (r *resourceTFEVariable) Schema(ctx context.Context, req resource.SchemaReq
180
181
stringvalidator .ConflictsWith (path .MatchRoot ("value" )),
181
182
},
182
183
PlanModifiers : []planmodifier.String {
183
- & replaceValueWOPlanModifier {} ,
184
+ planmodifiers . NewReplaceForWriteOnlyStringValue ( "value_wo" ) ,
184
185
},
185
186
},
186
187
"category" : schema.StringAttribute {
@@ -316,6 +317,7 @@ func (r *resourceTFEVariable) createWithWorkspace(ctx context.Context, req resou
316
317
Description : data .Description .ValueStringPointer (),
317
318
}
318
319
320
+ // Set Value from `value_wo` if set, otherwise use the normal value
319
321
if ! config .ValueWO .IsNull () {
320
322
options .Value = config .ValueWO .ValueStringPointer ()
321
323
} else {
@@ -334,15 +336,11 @@ func (r *resourceTFEVariable) createWithWorkspace(ctx context.Context, req resou
334
336
// Got a variable back, so set state to new values
335
337
result := modelFromTFEVariable (* variable , data .Value , ! config .ValueWO .IsNull ())
336
338
337
- if ! config .ValueWO .IsNull () {
338
- // Use the resource's private state to store secure hashes of write-only argument values, the provider during planmodify will use the hash to determine if a write-only argument value has changed in later Terraform runs.
339
- hashedValue := generateSHA256Hash (config .ValueWO .ValueString ())
340
- diags := resp .Private .SetKey (ctx , ValueWOHashedPrivateKey , fmt .Appendf (nil , `"%s"` , hashedValue ))
341
- resp .Diagnostics .Append (diags ... )
342
- } else {
343
- // if the value is not configured as write-only, then remove valueWO key from private state. Setting a key with an empty byte slice is interpreted by the framework as a request to remove the key from the ProviderData map.
344
- diags := resp .Private .SetKey (ctx , ValueWOHashedPrivateKey , []byte ("" ))
345
- resp .Diagnostics .Append (diags ... )
339
+ // Store the hashed write-only value in the private state
340
+ store := r .writeOnlyValueStore (resp .Private )
341
+ resp .Diagnostics .Append (store .SetPriorValue (ctx , config .ValueWO )... )
342
+ if resp .Diagnostics .HasError () {
343
+ return
346
344
}
347
345
348
346
diags = resp .State .Set (ctx , & result )
@@ -422,12 +420,14 @@ func (r *resourceTFEVariable) readWithWorkspace(ctx context.Context, req resourc
422
420
return
423
421
}
424
422
425
- isWriteOnlyValue := isWriteOnlyValueInPrivateState (req , resp ) // to avoid reading from written-only values
426
- if resp .Diagnostics .HasError () {
423
+ // Check if the parameter is write-only
424
+ isWriteOnly , diags := r .writeOnlyValueStore (resp .Private ).PriorValueExists (ctx )
425
+ resp .Diagnostics .Append (diags ... )
426
+ if diags .HasError () {
427
427
return
428
428
}
429
429
// update state
430
- result := modelFromTFEVariable (* variable , data .Value , isWriteOnlyValue )
430
+ result := modelFromTFEVariable (* variable , data .Value , isWriteOnly )
431
431
diags = resp .State .Set (ctx , & result )
432
432
resp .Diagnostics .Append (diags ... )
433
433
}
@@ -528,30 +528,18 @@ func (r *resourceTFEVariable) updateWithWorkspace(ctx context.Context, req resou
528
528
)
529
529
return
530
530
}
531
- // Update state
532
- result := modelFromTFEVariable ( * variable , plan . Value , ! config . ValueWO . IsNull () )
533
- r . updatePrivateState ( ctx , resp , config .ValueWO )
531
+ // Store the hashed write-only value in the private state
532
+ store := r . writeOnlyValueStore ( resp . Private )
533
+ resp . Diagnostics . Append ( store . SetPriorValue ( ctx , config .ValueWO ) ... )
534
534
if resp .Diagnostics .HasError () {
535
535
return
536
536
}
537
-
537
+ // Update state
538
+ result := modelFromTFEVariable (* variable , plan .Value , ! config .ValueWO .IsNull ())
538
539
diags = resp .State .Set (ctx , & result )
539
540
resp .Diagnostics .Append (diags ... )
540
541
}
541
542
542
- func (r * resourceTFEVariable ) updatePrivateState (ctx context.Context , resp * resource.UpdateResponse , configValueWO types.String ) {
543
- if ! configValueWO .IsNull () {
544
- // Use the resource's private state to store secure hashes of write-only argument values, planModify will use the hash to determine if a write-only argument value has changed in later Terraform runs.
545
- hashedValue := generateSHA256Hash (configValueWO .ValueString ())
546
- diags := resp .Private .SetKey (ctx , ValueWOHashedPrivateKey , fmt .Appendf (nil , `"%s"` , hashedValue ))
547
- resp .Diagnostics .Append (diags ... )
548
- } else {
549
- // if value is not configured as write-only, remove valueWO key from private state
550
- diags := resp .Private .SetKey (ctx , ValueWOHashedPrivateKey , []byte ("" ))
551
- resp .Diagnostics .Append (diags ... )
552
- }
553
- }
554
-
555
543
// updateWithVariableSet is the variable set version of Update.
556
544
func (r * resourceTFEVariable ) updateWithVariableSet (ctx context.Context , req resource.UpdateRequest , resp * resource.UpdateResponse ) {
557
545
// Get both plan and state; must compare them to handle sensitive values safely.
@@ -789,65 +777,6 @@ func (r *resourceTFEVariable) ImportState(ctx context.Context, req resource.Impo
789
777
resp .Diagnostics .Append (diags ... )
790
778
}
791
779
792
- type replaceValueWOPlanModifier struct {}
793
-
794
- func (v * replaceValueWOPlanModifier ) Description (ctx context.Context ) string {
795
- return "The resource will be replaced when the value of value_wo has changed"
796
- }
797
-
798
- func (v * replaceValueWOPlanModifier ) MarkdownDescription (ctx context.Context ) string {
799
- return v .Description (ctx )
800
- }
801
-
802
- func (v * replaceValueWOPlanModifier ) PlanModifyString (ctx context.Context , request planmodifier.StringRequest , response * planmodifier.StringResponse ) {
803
- // Write-only argument values cannot produce a Terraform plan difference. The prior state value for a write-only argument will always be null and the planned state value will also be null, therefore, it cannot produce a diff on its own. The one exception to this case is if the write-only argument is added to requires_replace during Plan Modification, in that case, the write-only argument will always cause a diff/trigger a resource recreation.
804
- var configValueWO types.String
805
- diag := request .Config .GetAttribute (ctx , path .Root ("value_wo" ), & configValueWO )
806
- response .Diagnostics .Append (diag ... )
807
- if response .Diagnostics .HasError () {
808
- return
809
- }
810
-
811
- storedValueWO , diags := request .Private .GetKey (ctx , ValueWOHashedPrivateKey )
812
- response .Diagnostics .Append (diags ... )
813
- if response .Diagnostics .HasError () {
814
- return
815
- }
816
-
817
- if ! configValueWO .IsNull () {
818
- handleConfigValueWO (configValueWO , storedValueWO , response )
819
- } else if len (storedValueWO ) != 0 {
820
- // when `value_wo` was previously set in the config, but the config switched to either `value` or no value whatsoever
821
- response .RequiresReplace = true
822
- }
823
- }
824
-
825
- func handleConfigValueWO (valueWO types.String , storedValueWO []byte , response * planmodifier.StringResponse ) {
826
- if len (storedValueWO ) != 0 {
827
- var hashedStoredValueWO string
828
- err := json .Unmarshal (storedValueWO , & hashedStoredValueWO )
829
- if err != nil {
830
- response .Diagnostics .AddError ("Error unmarshalling stored value_wo" , err .Error ())
831
- return
832
- }
833
- hashedConfigValueWO := generateSHA256Hash (valueWO .ValueString ())
834
- // when an ephemeral value is being used, they will generate a new token on every run. So the previous value_wo will not match the current one.
835
- if hashedStoredValueWO != hashedConfigValueWO {
836
- log .Printf ("[DEBUG] Replacing resource because the value of `value_wo` attribute has changed" )
837
- response .RequiresReplace = true
838
- }
839
- } else {
840
- log .Printf ("[DEBUG] Replacing resource because `value_wo` attribute has been added to a pre-existing variable resource" )
841
- response .RequiresReplace = true
842
- }
843
- }
844
-
845
- func isWriteOnlyValueInPrivateState (req resource.ReadRequest , resp * resource.ReadResponse ) bool {
846
- storedValueWO , diags := req .Private .GetKey (ctx , ValueWOHashedPrivateKey )
847
- resp .Diagnostics .Append (diags ... )
848
- return len (storedValueWO ) != 0
849
- }
850
-
851
780
type updateReadableValuePlanModifier struct {}
852
781
853
782
func (u * updateReadableValuePlanModifier ) Description (ctx context.Context ) string {
@@ -895,9 +824,12 @@ var _ resource.ResourceWithConfigure = &resourceTFEVariable{}
895
824
var _ resource.ResourceWithUpgradeState = & resourceTFEVariable {}
896
825
var _ resource.ResourceWithImportState = & resourceTFEVariable {}
897
826
var _ planmodifier.String = & updateReadableValuePlanModifier {}
898
- var _ planmodifier.String = & replaceValueWOPlanModifier {}
899
827
900
828
// NewResourceVariable is a resource function for the framework provider.
901
829
func NewResourceVariable () resource.Resource {
902
830
return & resourceTFEVariable {}
903
831
}
832
+
833
+ func (r * resourceTFEVariable ) writeOnlyValueStore (private helpers.PrivateState ) * helpers.WriteOnlyValueStore {
834
+ return helpers .NewWriteOnlyValueStore (private , "value_wo" )
835
+ }
0 commit comments