Skip to content

Commit fb43166

Browse files
committed
update tfe_variable and tfe_test_variable to use helpers.NewWriteOnlyValueStore and planmodifiers.NewReplaceForWriteOnlyStringValue
1 parent eb69742 commit fb43166

File tree

2 files changed

+41
-121
lines changed

2 files changed

+41
-121
lines changed

internal/provider/resource_tfe_test_variable.go

Lines changed: 18 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import (
2222
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
2323
"github.com/hashicorp/terraform-plugin-framework/types"
2424
"github.com/hashicorp/terraform-plugin-log/tflog"
25+
"github.com/hashicorp/terraform-provider-tfe/internal/provider/helpers"
26+
"github.com/hashicorp/terraform-provider-tfe/internal/provider/planmodifiers"
2527
)
2628

2729
type resourceTFETestVariable struct {
@@ -152,7 +154,7 @@ func (r *resourceTFETestVariable) Schema(ctx context.Context, req resource.Schem
152154
stringvalidator.ConflictsWith(path.MatchRoot("value")),
153155
},
154156
PlanModifiers: []planmodifier.String{
155-
&replaceValueWOPlanModifier{},
157+
planmodifiers.NewReplaceForWriteOnlyStringValue("value_wo"),
156158
},
157159
},
158160
"category": schema.StringAttribute{
@@ -250,7 +252,7 @@ func (r *resourceTFETestVariable) Create(ctx context.Context, req resource.Creat
250252
Sensitive: data.Sensitive.ValueBoolPointer(),
251253
Description: data.Description.ValueStringPointer(),
252254
}
253-
255+
// Set Value from `value_wo` if set, otherwise use the normal value
254256
if !config.ValueWO.IsNull() {
255257
options.Value = config.ValueWO.ValueStringPointer()
256258
} else {
@@ -270,17 +272,9 @@ func (r *resourceTFETestVariable) Create(ctx context.Context, req resource.Creat
270272
// We got a variable, so set state to new values
271273
result := modelFromTFETestVariable(*variable, data.Value, moduleID, !config.ValueWO.IsNull())
272274

273-
if !config.ValueWO.IsNull() {
274-
// 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.
275-
hashedValue := generateSHA256Hash(config.ValueWO.ValueString())
276-
diags := resp.Private.SetKey(ctx, ValueWOHashedPrivateKey, fmt.Appendf(nil, `"%s"`, hashedValue))
277-
resp.Diagnostics.Append(diags...)
278-
} else {
279-
// 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.
280-
diags := resp.Private.SetKey(ctx, ValueWOHashedPrivateKey, []byte(""))
281-
resp.Diagnostics.Append(diags...)
282-
}
283-
275+
// Store the hashed write-only value in the private state
276+
store := r.writeOnlyValueStore(resp.Private)
277+
resp.Diagnostics.Append(store.SetPriorValue(ctx, config.ValueWO)...)
284278
if resp.Diagnostics.HasError() {
285279
return
286280
}
@@ -322,13 +316,14 @@ func (r *resourceTFETestVariable) Read(ctx context.Context, req resource.ReadReq
322316
return
323317
}
324318

325-
isWriteOnlyValue := isWriteOnlyValueInPrivateState(req, resp) // to avoid reading from written-only values
326-
if resp.Diagnostics.HasError() {
319+
isWriteOnly, diags := r.writeOnlyValueStore(resp.Private).PriorValueExists(ctx)
320+
resp.Diagnostics.Append(diags...)
321+
if diags.HasError() {
327322
return
328323
}
329324

330325
// We got a variable, so update state:
331-
result := modelFromTFETestVariable(*variable, data.Value, moduleID, isWriteOnlyValue)
326+
result := modelFromTFETestVariable(*variable, data.Value, moduleID, isWriteOnly)
332327
diags = resp.State.Set(ctx, &result)
333328
resp.Diagnostics.Append(diags...)
334329
}
@@ -383,12 +378,14 @@ func (r *resourceTFETestVariable) Update(ctx context.Context, req resource.Updat
383378
)
384379
return
385380
}
386-
// Update state
387-
result := modelFromTFETestVariable(*variable, plan.Value, moduleID, !config.ValueWO.IsNull())
388-
r.updatePrivateState(ctx, resp, config.ValueWO)
381+
// Store the hashed write-only value in the private state
382+
store := r.writeOnlyValueStore(resp.Private)
383+
resp.Diagnostics.Append(store.SetPriorValue(ctx, config.ValueWO)...)
389384
if resp.Diagnostics.HasError() {
390385
return
391386
}
387+
// Update state
388+
result := modelFromTFETestVariable(*variable, plan.Value, moduleID, !config.ValueWO.IsNull())
392389
diags = resp.State.Set(ctx, &result)
393390
resp.Diagnostics.Append(diags...)
394391
}
@@ -421,15 +418,6 @@ func (r *resourceTFETestVariable) Delete(ctx context.Context, req resource.Delet
421418
// Resource is implicitly deleted from resp.State if diagnostics have no errors.
422419
}
423420

424-
func (r *resourceTFETestVariable) updatePrivateState(ctx context.Context, resp *resource.UpdateResponse, configValueWO types.String) {
425-
if !configValueWO.IsNull() {
426-
// 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.
427-
hashedValue := generateSHA256Hash(configValueWO.ValueString())
428-
diags := resp.Private.SetKey(ctx, ValueWOHashedPrivateKey, fmt.Appendf(nil, `"%s"`, hashedValue))
429-
resp.Diagnostics.Append(diags...)
430-
} else {
431-
// if value is not configured as write-only, remove valueWO key from private state
432-
diags := resp.Private.SetKey(ctx, ValueWOHashedPrivateKey, []byte(""))
433-
resp.Diagnostics.Append(diags...)
434-
}
421+
func (r *resourceTFETestVariable) writeOnlyValueStore(private helpers.PrivateState) *helpers.WriteOnlyValueStore {
422+
return helpers.NewWriteOnlyValueStore(private, "value_wo")
435423
}

internal/provider/resource_tfe_variable.go

Lines changed: 23 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ package provider
55

66
import (
77
"context"
8-
"encoding/json"
98
"errors"
109
"fmt"
1110
"log"
@@ -23,6 +22,8 @@ import (
2322
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
2423
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
2524
"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"
2627
)
2728

2829
// resourceTFEVariable implements the tfe_variable resource type. Note: Much of
@@ -180,7 +181,7 @@ func (r *resourceTFEVariable) Schema(ctx context.Context, req resource.SchemaReq
180181
stringvalidator.ConflictsWith(path.MatchRoot("value")),
181182
},
182183
PlanModifiers: []planmodifier.String{
183-
&replaceValueWOPlanModifier{},
184+
planmodifiers.NewReplaceForWriteOnlyStringValue("value_wo"),
184185
},
185186
},
186187
"category": schema.StringAttribute{
@@ -316,6 +317,7 @@ func (r *resourceTFEVariable) createWithWorkspace(ctx context.Context, req resou
316317
Description: data.Description.ValueStringPointer(),
317318
}
318319

320+
// Set Value from `value_wo` if set, otherwise use the normal value
319321
if !config.ValueWO.IsNull() {
320322
options.Value = config.ValueWO.ValueStringPointer()
321323
} else {
@@ -334,15 +336,11 @@ func (r *resourceTFEVariable) createWithWorkspace(ctx context.Context, req resou
334336
// Got a variable back, so set state to new values
335337
result := modelFromTFEVariable(*variable, data.Value, !config.ValueWO.IsNull())
336338

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
346344
}
347345

348346
diags = resp.State.Set(ctx, &result)
@@ -422,12 +420,14 @@ func (r *resourceTFEVariable) readWithWorkspace(ctx context.Context, req resourc
422420
return
423421
}
424422

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() {
427427
return
428428
}
429429
// update state
430-
result := modelFromTFEVariable(*variable, data.Value, isWriteOnlyValue)
430+
result := modelFromTFEVariable(*variable, data.Value, isWriteOnly)
431431
diags = resp.State.Set(ctx, &result)
432432
resp.Diagnostics.Append(diags...)
433433
}
@@ -528,30 +528,18 @@ func (r *resourceTFEVariable) updateWithWorkspace(ctx context.Context, req resou
528528
)
529529
return
530530
}
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)...)
534534
if resp.Diagnostics.HasError() {
535535
return
536536
}
537-
537+
// Update state
538+
result := modelFromTFEVariable(*variable, plan.Value, !config.ValueWO.IsNull())
538539
diags = resp.State.Set(ctx, &result)
539540
resp.Diagnostics.Append(diags...)
540541
}
541542

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-
555543
// updateWithVariableSet is the variable set version of Update.
556544
func (r *resourceTFEVariable) updateWithVariableSet(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
557545
// 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
789777
resp.Diagnostics.Append(diags...)
790778
}
791779

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-
851780
type updateReadableValuePlanModifier struct{}
852781

853782
func (u *updateReadableValuePlanModifier) Description(ctx context.Context) string {
@@ -895,9 +824,12 @@ var _ resource.ResourceWithConfigure = &resourceTFEVariable{}
895824
var _ resource.ResourceWithUpgradeState = &resourceTFEVariable{}
896825
var _ resource.ResourceWithImportState = &resourceTFEVariable{}
897826
var _ planmodifier.String = &updateReadableValuePlanModifier{}
898-
var _ planmodifier.String = &replaceValueWOPlanModifier{}
899827

900828
// NewResourceVariable is a resource function for the framework provider.
901829
func NewResourceVariable() resource.Resource {
902830
return &resourceTFEVariable{}
903831
}
832+
833+
func (r *resourceTFEVariable) writeOnlyValueStore(private helpers.PrivateState) *helpers.WriteOnlyValueStore {
834+
return helpers.NewWriteOnlyValueStore(private, "value_wo")
835+
}

0 commit comments

Comments
 (0)