Skip to content

Commit eb69742

Browse files
[TF-24656] implement write only values for tfe_organization_run_task (#1646)
* trying to add WO value to resource * add debug lines to troubleshoot for run task resource data * Write acceptance test * add config state check * Update CHANGELOG.md * trying to add WO value to resource * add debug lines to troubleshoot for run task resource data * Write acceptance test * add config state check * Update CHANGELOG.md * clean up * move isWriteOnlyValueInPrivateState to resourceOrgRunTask * update function * refactor complex if block * whitespace * comment out test * remove commented out test * add stuff back * add to documentation * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md --------- Co-authored-by: uturunku1 <[email protected]>
1 parent 7173538 commit eb69742

File tree

4 files changed

+238
-7
lines changed

4 files changed

+238
-7
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ ENHANCEMENTS:
2121

2222
* resource/tfe_policy_set_parameter: Add `value_wo` write-only attribute, by @ctrombley ([#1641](https://github.com/hashicorp/terraform-provider-tfe/pull/1641))
2323

24+
* resource/tfe_organization_run_task: Add `hmac_key_wo` write-only attribute, by @shwetamurali ([#1646](https://github.com/hashicorp/terraform-provider-tfe/pull/1646))
25+
2426
## v.0.64.0
2527

2628
FEATURES:

internal/provider/resource_tfe_organization_run_task.go

Lines changed: 143 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,15 @@ package provider
55

66
import (
77
"context"
8+
"encoding/json"
89
"errors"
910
"fmt"
11+
"log"
1012
"strings"
1113

1214
tfe "github.com/hashicorp/go-tfe"
15+
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
16+
"github.com/hashicorp/terraform-plugin-framework/path"
1317
"github.com/hashicorp/terraform-plugin-framework/resource"
1418

1519
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
@@ -46,9 +50,10 @@ type modelTFEOrganizationRunTaskV0 struct {
4650
Name types.String `tfsdk:"name"`
4751
Organization types.String `tfsdk:"organization"`
4852
URL types.String `tfsdk:"url"`
53+
HMACKeyWO types.String `tfsdk:"hmac_key_wo"`
4954
}
5055

51-
func modelFromTFEOrganizationRunTask(v *tfe.RunTask, hmacKey types.String) modelTFEOrganizationRunTaskV0 {
56+
func modelFromTFEOrganizationRunTask(v *tfe.RunTask, hmacKey types.String, isWriteOnlyValue bool) modelTFEOrganizationRunTaskV0 {
5257
result := modelTFEOrganizationRunTaskV0{
5358
Category: types.StringValue(v.Category),
5459
Description: types.StringValue(v.Description),
@@ -64,6 +69,11 @@ func modelFromTFEOrganizationRunTask(v *tfe.RunTask, hmacKey types.String) model
6469
result.HMACKey = hmacKey
6570
}
6671

72+
// Don't retrieve values if write-only is being used. Unset the hmac key field before updating the state.
73+
if isWriteOnlyValue {
74+
result.HMACKey = types.StringValue("")
75+
}
76+
6777
return result
6878
}
6979

@@ -131,6 +141,21 @@ func (r *resourceOrgRunTask) Schema(ctx context.Context, req resource.SchemaRequ
131141
Optional: true,
132142
Computed: true,
133143
Default: stringdefault.StaticString(""),
144+
Validators: []validator.String{
145+
stringvalidator.ConflictsWith(path.MatchRoot("hmac_key_wo")),
146+
},
147+
},
148+
"hmac_key_wo": schema.StringAttribute{
149+
Optional: true,
150+
WriteOnly: true,
151+
Sensitive: true,
152+
Description: "HMAC key in write-only mode",
153+
Validators: []validator.String{
154+
stringvalidator.ConflictsWith(path.MatchRoot("hmac_key")),
155+
},
156+
PlanModifiers: []planmodifier.String{
157+
&replaceHMACKeyWOPlanModifier{},
158+
},
134159
},
135160
"enabled": schema.BoolAttribute{
136161
Optional: true,
@@ -146,6 +171,67 @@ func (r *resourceOrgRunTask) Schema(ctx context.Context, req resource.SchemaRequ
146171
}
147172
}
148173

174+
func (r *resourceOrgRunTask) isWriteOnlyHMACKeyInPrivateState(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) bool {
175+
storedValueWO, diags := req.Private.GetKey(ctx, "hmac_key_wo")
176+
resp.Diagnostics.Append(diags...)
177+
return len(storedValueWO) != 0
178+
}
179+
180+
type replaceHMACKeyWOPlanModifier struct{}
181+
182+
func (v *replaceHMACKeyWOPlanModifier) Description(ctx context.Context) string {
183+
return "The resource will be replaced when the value of hmac_key_wo has changed"
184+
}
185+
186+
func (v *replaceHMACKeyWOPlanModifier) MarkdownDescription(ctx context.Context) string {
187+
return v.Description(ctx)
188+
}
189+
190+
func (v *replaceHMACKeyWOPlanModifier) PlanModifyString(ctx context.Context, request planmodifier.StringRequest, response *planmodifier.StringResponse) {
191+
// 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.
192+
var configHMACKeyWO types.String
193+
diag := request.Config.GetAttribute(ctx, path.Root("hmac_key_wo"), &configHMACKeyWO)
194+
response.Diagnostics.Append(diag...)
195+
if response.Diagnostics.HasError() {
196+
return
197+
}
198+
199+
storedHMACWO, diags := request.Private.GetKey(ctx, "hmac_key_wo")
200+
response.Diagnostics.Append(diags...)
201+
if response.Diagnostics.HasError() {
202+
return
203+
}
204+
205+
if configHMACKeyWO.IsNull() {
206+
if len(storedHMACWO) != 0 {
207+
response.RequiresReplace = true
208+
}
209+
return
210+
}
211+
212+
if len(storedHMACWO) == 0 {
213+
log.Printf("[DEBUG] Replacing resource because `hmac_key_wo` attribute has been added to a pre-existing variable resource")
214+
response.RequiresReplace = true
215+
return
216+
}
217+
218+
var hashedStoredHMACWO string
219+
err := json.Unmarshal(storedHMACWO, &hashedStoredHMACWO)
220+
if err != nil {
221+
response.Diagnostics.AddError("Error unmarshalling stored hmac_key_wo", err.Error())
222+
return
223+
}
224+
225+
hashedConfigHMACKeyWO := generateSHA256Hash(configHMACKeyWO.ValueString())
226+
227+
// when an ephemeral value is being used, they will generate a new token on every run.
228+
// So the previous hmac_key_wo will not match the current one.
229+
if hashedStoredHMACWO != hashedConfigHMACKeyWO {
230+
log.Printf("[DEBUG] Replacing resource because the value of `hmac_key_wo` attribute has changed")
231+
response.RequiresReplace = true
232+
}
233+
}
234+
149235
func (r *resourceOrgRunTask) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
150236
var state modelTFEOrganizationRunTaskV0
151237

@@ -168,8 +254,12 @@ func (r *resourceOrgRunTask) Read(ctx context.Context, req resource.ReadRequest,
168254
return
169255
}
170256

171-
result := modelFromTFEOrganizationRunTask(task, state.HMACKey)
172-
257+
isWriteOnlyValue := r.isWriteOnlyHMACKeyInPrivateState(ctx, req, resp) // to avoid reading from written-only values
258+
if resp.Diagnostics.HasError() {
259+
return
260+
}
261+
// update state
262+
result := modelFromTFEOrganizationRunTask(task, state.HMACKey, isWriteOnlyValue)
173263
// Save updated data into Terraform state
174264
resp.Diagnostics.Append(resp.State.Set(ctx, &result)...)
175265
}
@@ -183,6 +273,13 @@ func (r *resourceOrgRunTask) Create(ctx context.Context, req resource.CreateRequ
183273
return
184274
}
185275

276+
var config modelTFEOrganizationRunTaskV0
277+
diags := req.Config.Get(ctx, &config)
278+
resp.Diagnostics.Append(diags...)
279+
if resp.Diagnostics.HasError() {
280+
return
281+
}
282+
186283
var organization string
187284
resp.Diagnostics.Append(r.config.dataOrDefaultOrganization(ctx, req.Plan, &organization)...)
188285

@@ -194,19 +291,34 @@ func (r *resourceOrgRunTask) Create(ctx context.Context, req resource.CreateRequ
194291
Name: plan.Name.ValueString(),
195292
URL: plan.URL.ValueString(),
196293
Category: plan.Category.ValueString(),
197-
HMACKey: plan.HMACKey.ValueStringPointer(),
198294
Enabled: plan.Enabled.ValueBoolPointer(),
199295
Description: plan.Description.ValueStringPointer(),
200296
}
201297

298+
if !config.HMACKeyWO.IsNull() {
299+
options.HMACKey = config.HMACKeyWO.ValueStringPointer()
300+
} else {
301+
options.HMACKey = plan.HMACKey.ValueStringPointer()
302+
}
303+
202304
tflog.Debug(ctx, fmt.Sprintf("Create task %s for organization: %s", options.Name, organization))
203305
task, err := r.config.Client.RunTasks.Create(ctx, organization, options)
204306
if err != nil {
205307
resp.Diagnostics.AddError("Unable to create organization task", err.Error())
206308
return
207309
}
208310

209-
result := modelFromTFEOrganizationRunTask(task, plan.HMACKey)
311+
result := modelFromTFEOrganizationRunTask(task, plan.HMACKey, !config.HMACKeyWO.IsNull())
312+
if !config.HMACKeyWO.IsNull() {
313+
// 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.
314+
hashedValue := generateSHA256Hash(config.HMACKeyWO.ValueString())
315+
diags := resp.Private.SetKey(ctx, "hmac_key_wo", fmt.Appendf(nil, `"%s"`, hashedValue))
316+
resp.Diagnostics.Append(diags...)
317+
} else {
318+
// if the key is not configured as write-only, then remove HMACKeyWO 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.
319+
diags := resp.Private.SetKey(ctx, "hmac_key_wo", []byte(""))
320+
resp.Diagnostics.Append(diags...)
321+
}
210322

211323
// Save data into Terraform state
212324
resp.Diagnostics.Append(resp.State.Set(ctx, &result)...)
@@ -228,6 +340,13 @@ func (r *resourceOrgRunTask) Update(ctx context.Context, req resource.UpdateRequ
228340
return
229341
}
230342

343+
var config modelTFEOrganizationRunTaskV0
344+
diags := req.Config.Get(ctx, &config)
345+
resp.Diagnostics.Append(diags...)
346+
if resp.Diagnostics.HasError() {
347+
return
348+
}
349+
231350
options := tfe.RunTaskUpdateOptions{
232351
Name: plan.Name.ValueStringPointer(),
233352
URL: plan.URL.ValueStringPointer(),
@@ -245,13 +364,15 @@ func (r *resourceOrgRunTask) Update(ctx context.Context, req resource.UpdateRequ
245364
taskID := plan.ID.ValueString()
246365

247366
tflog.Debug(ctx, fmt.Sprintf("Update task %s", taskID))
367+
248368
task, err := r.config.Client.RunTasks.Update(ctx, taskID, options)
249369
if err != nil {
250370
resp.Diagnostics.AddError("Unable to update organization task", err.Error())
251371
return
252372
}
253373

254-
result := modelFromTFEOrganizationRunTask(task, plan.HMACKey)
374+
result := modelFromTFEOrganizationRunTask(task, plan.HMACKey, !config.HMACKeyWO.IsNull())
375+
r.updatePrivateState(ctx, resp, config.HMACKeyWO)
255376

256377
// Save data into Terraform state
257378
resp.Diagnostics.Append(resp.State.Set(ctx, &result)...)
@@ -304,7 +425,22 @@ func (r *resourceOrgRunTask) ImportState(ctx context.Context, req resource.Impor
304425
)
305426
} else {
306427
// We can never import the HMACkey (Write-only) so assume it's the default (empty)
307-
result := modelFromTFEOrganizationRunTask(task, types.StringValue(""))
428+
result := modelFromTFEOrganizationRunTask(task, types.StringValue(""), false)
308429
resp.Diagnostics.Append(resp.State.Set(ctx, &result)...)
309430
}
310431
}
432+
433+
func (r *resourceOrgRunTask) updatePrivateState(ctx context.Context, resp *resource.UpdateResponse, configHMACKeyWO types.String) {
434+
if !configHMACKeyWO.IsNull() {
435+
// 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.
436+
hashedValue := generateSHA256Hash(configHMACKeyWO.ValueString())
437+
diags := resp.Private.SetKey(ctx, "hmac_key_wo", fmt.Appendf(nil, `"%s"`, hashedValue))
438+
resp.Diagnostics.Append(diags...)
439+
} else {
440+
// if key is not configured as write-only, remove hmacKeyWO key from private state
441+
diags := resp.Private.SetKey(ctx, "hmac_key_wo", []byte(""))
442+
resp.Diagnostics.Append(diags...)
443+
}
444+
}
445+
446+
var _ planmodifier.String = &replaceHMACKeyWOPlanModifier{}

internal/provider/resource_tfe_organization_run_task_test.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,11 @@ import (
1111
"time"
1212

1313
tfe "github.com/hashicorp/go-tfe"
14+
"github.com/hashicorp/terraform-plugin-testing/compare"
1415
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
16+
"github.com/hashicorp/terraform-plugin-testing/statecheck"
1517
"github.com/hashicorp/terraform-plugin-testing/terraform"
18+
"github.com/hashicorp/terraform-plugin-testing/tfjsonpath"
1619
)
1720

1821
func TestAccTFEOrganizationRunTask_validateSchemaAttributeUrl(t *testing.T) {
@@ -184,6 +187,70 @@ func TestAccTFEOrganizationRunTask_Read(t *testing.T) {
184187
})
185188
}
186189

190+
func TestAccTFEOrganizationRunTask_HMACWriteOnly(t *testing.T) {
191+
skipUnlessRunTasksDefined(t)
192+
193+
tfeClient, err := getClientUsingEnv()
194+
if err != nil {
195+
t.Fatal(err)
196+
}
197+
198+
org, orgCleanup := createBusinessOrganization(t, tfeClient)
199+
t.Cleanup(orgCleanup)
200+
201+
runTask := &tfe.RunTask{}
202+
rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int()
203+
204+
// Create the value comparer so we can add state values to it during the test steps
205+
compareValuesDiffer := statecheck.CompareValue(compare.ValuesDiffer())
206+
207+
// Note - We cannot easily test updating the HMAC Key as that would require coordination between this test suite
208+
// and the external Run Task service to "magically" allow a different Key. Instead we "update" with the same key
209+
// and manually test HMAC Key changes.
210+
hmacKey := runTasksHMACKey()
211+
212+
resource.Test(t, resource.TestCase{
213+
PreCheck: func() { testAccPreCheck(t) },
214+
ProtoV5ProviderFactories: testAccMuxedProviders,
215+
CheckDestroy: testAccCheckTFEOrganizationRunTaskDestroy,
216+
Steps: []resource.TestStep{
217+
{
218+
Config: testAccTFEOrganizationRunTask_hmacAndHMACWriteOnly(org.Name, rInt, runTasksURL()),
219+
ExpectError: regexp.MustCompile(`Attribute "hmac_key_wo" cannot be specified when "hmac_key" is specified`),
220+
},
221+
{
222+
Config: testAccTFEOrganizationRunTask_hmacWriteOnly(org.Name, rInt, runTasksURL(), hmacKey),
223+
Check: resource.ComposeTestCheckFunc(
224+
testAccCheckTFEOrganizationRunTaskExists("tfe_organization_run_task.foobar", runTask),
225+
resource.TestCheckResourceAttr("tfe_organization_run_task.foobar", "hmac_key", ""),
226+
resource.TestCheckNoResourceAttr("tfe_organization_run_task.foobar", "hmac_key_wo"),
227+
),
228+
// Register the id with the value comparer so we can assert that the
229+
// resource has been replaced in the next step.
230+
ConfigStateChecks: []statecheck.StateCheck{
231+
compareValuesDiffer.AddStateValue(
232+
"tfe_organization_run_task.foobar", tfjsonpath.New("id"),
233+
),
234+
},
235+
},
236+
{
237+
Config: testAccTFEOrganizationRunTask_basic(org.Name, rInt, runTasksURL(), hmacKey),
238+
Check: resource.ComposeTestCheckFunc(
239+
testAccCheckTFEOrganizationRunTaskExists("tfe_organization_run_task.foobar", runTask),
240+
resource.TestCheckResourceAttr("tfe_organization_run_task.foobar", "hmac_key", hmacKey),
241+
resource.TestCheckNoResourceAttr("tfe_organization_run_task.foobar", "hmac_key_wo"),
242+
),
243+
// Ensure that the resource has been replaced
244+
ConfigStateChecks: []statecheck.StateCheck{
245+
compareValuesDiffer.AddStateValue(
246+
"tfe_organization_run_task.foobar", tfjsonpath.New("id"),
247+
),
248+
},
249+
},
250+
},
251+
})
252+
}
253+
187254
func testAccCheckTFEOrganizationRunTaskExists(n string, runTask *tfe.RunTask) resource.TestCheckFunc {
188255
return func(s *terraform.State) error {
189256
config := testAccProvider.Meta().(ConfiguredClient)
@@ -256,3 +323,28 @@ func testAccTFEOrganizationRunTask_update(orgName string, rInt int, runTaskURL,
256323
}
257324
`, orgName, runTaskURL, rInt, runTaskHMACKey)
258325
}
326+
327+
func testAccTFEOrganizationRunTask_hmacWriteOnly(orgName string, rInt int, runTaskURL, runTaskHMACKey string) string {
328+
return fmt.Sprintf(`
329+
resource "tfe_organization_run_task" "foobar" {
330+
organization = "%s"
331+
url = "%s"
332+
name = "foobar-task-%d"
333+
enabled = false
334+
hmac_key_wo = "%s"
335+
}
336+
`, orgName, runTaskURL, rInt, runTaskHMACKey)
337+
}
338+
339+
func testAccTFEOrganizationRunTask_hmacAndHMACWriteOnly(orgName string, rInt int, runTaskURL string) string {
340+
return fmt.Sprintf(`
341+
resource "tfe_organization_run_task" "foobar" {
342+
organization = "%s"
343+
url = "%s"
344+
name = "foobar-task-%d"
345+
enabled = false
346+
hmac_key = "foo"
347+
hmac_key_wo = "foo"
348+
}
349+
`, orgName, runTaskURL, rInt)
350+
}

website/docs/r/organization_run_task.html.markdown

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,4 @@ import ID. For example:
4949
```shell
5050
terraform import tfe_organization_run_task.test my-org-name/task-name
5151
```
52+
-> **Note:** Write-Only argument `hmac_key_wo` is available to use in place of `hmac_key`. Write-Only arguments are supported in HashiCorp Terraform 1.11.0 and later. [Learn more](https://developer.hashicorp.com/terraform/language/v1.11.x/resources/ephemeral#write-only-arguments).

0 commit comments

Comments
 (0)