@@ -5,11 +5,15 @@ package provider
5
5
6
6
import (
7
7
"context"
8
+ "encoding/json"
8
9
"errors"
9
10
"fmt"
11
+ "log"
10
12
"strings"
11
13
12
14
tfe "github.com/hashicorp/go-tfe"
15
+ "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
16
+ "github.com/hashicorp/terraform-plugin-framework/path"
13
17
"github.com/hashicorp/terraform-plugin-framework/resource"
14
18
15
19
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
@@ -46,9 +50,10 @@ type modelTFEOrganizationRunTaskV0 struct {
46
50
Name types.String `tfsdk:"name"`
47
51
Organization types.String `tfsdk:"organization"`
48
52
URL types.String `tfsdk:"url"`
53
+ HMACKeyWO types.String `tfsdk:"hmac_key_wo"`
49
54
}
50
55
51
- func modelFromTFEOrganizationRunTask (v * tfe.RunTask , hmacKey types.String ) modelTFEOrganizationRunTaskV0 {
56
+ func modelFromTFEOrganizationRunTask (v * tfe.RunTask , hmacKey types.String , isWriteOnlyValue bool ) modelTFEOrganizationRunTaskV0 {
52
57
result := modelTFEOrganizationRunTaskV0 {
53
58
Category : types .StringValue (v .Category ),
54
59
Description : types .StringValue (v .Description ),
@@ -64,6 +69,11 @@ func modelFromTFEOrganizationRunTask(v *tfe.RunTask, hmacKey types.String) model
64
69
result .HMACKey = hmacKey
65
70
}
66
71
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
+
67
77
return result
68
78
}
69
79
@@ -131,6 +141,21 @@ func (r *resourceOrgRunTask) Schema(ctx context.Context, req resource.SchemaRequ
131
141
Optional : true ,
132
142
Computed : true ,
133
143
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
+ },
134
159
},
135
160
"enabled" : schema.BoolAttribute {
136
161
Optional : true ,
@@ -146,6 +171,67 @@ func (r *resourceOrgRunTask) Schema(ctx context.Context, req resource.SchemaRequ
146
171
}
147
172
}
148
173
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
+
149
235
func (r * resourceOrgRunTask ) Read (ctx context.Context , req resource.ReadRequest , resp * resource.ReadResponse ) {
150
236
var state modelTFEOrganizationRunTaskV0
151
237
@@ -168,8 +254,12 @@ func (r *resourceOrgRunTask) Read(ctx context.Context, req resource.ReadRequest,
168
254
return
169
255
}
170
256
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 )
173
263
// Save updated data into Terraform state
174
264
resp .Diagnostics .Append (resp .State .Set (ctx , & result )... )
175
265
}
@@ -183,6 +273,13 @@ func (r *resourceOrgRunTask) Create(ctx context.Context, req resource.CreateRequ
183
273
return
184
274
}
185
275
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
+
186
283
var organization string
187
284
resp .Diagnostics .Append (r .config .dataOrDefaultOrganization (ctx , req .Plan , & organization )... )
188
285
@@ -194,19 +291,34 @@ func (r *resourceOrgRunTask) Create(ctx context.Context, req resource.CreateRequ
194
291
Name : plan .Name .ValueString (),
195
292
URL : plan .URL .ValueString (),
196
293
Category : plan .Category .ValueString (),
197
- HMACKey : plan .HMACKey .ValueStringPointer (),
198
294
Enabled : plan .Enabled .ValueBoolPointer (),
199
295
Description : plan .Description .ValueStringPointer (),
200
296
}
201
297
298
+ if ! config .HMACKeyWO .IsNull () {
299
+ options .HMACKey = config .HMACKeyWO .ValueStringPointer ()
300
+ } else {
301
+ options .HMACKey = plan .HMACKey .ValueStringPointer ()
302
+ }
303
+
202
304
tflog .Debug (ctx , fmt .Sprintf ("Create task %s for organization: %s" , options .Name , organization ))
203
305
task , err := r .config .Client .RunTasks .Create (ctx , organization , options )
204
306
if err != nil {
205
307
resp .Diagnostics .AddError ("Unable to create organization task" , err .Error ())
206
308
return
207
309
}
208
310
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
+ }
210
322
211
323
// Save data into Terraform state
212
324
resp .Diagnostics .Append (resp .State .Set (ctx , & result )... )
@@ -228,6 +340,13 @@ func (r *resourceOrgRunTask) Update(ctx context.Context, req resource.UpdateRequ
228
340
return
229
341
}
230
342
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
+
231
350
options := tfe.RunTaskUpdateOptions {
232
351
Name : plan .Name .ValueStringPointer (),
233
352
URL : plan .URL .ValueStringPointer (),
@@ -245,13 +364,15 @@ func (r *resourceOrgRunTask) Update(ctx context.Context, req resource.UpdateRequ
245
364
taskID := plan .ID .ValueString ()
246
365
247
366
tflog .Debug (ctx , fmt .Sprintf ("Update task %s" , taskID ))
367
+
248
368
task , err := r .config .Client .RunTasks .Update (ctx , taskID , options )
249
369
if err != nil {
250
370
resp .Diagnostics .AddError ("Unable to update organization task" , err .Error ())
251
371
return
252
372
}
253
373
254
- result := modelFromTFEOrganizationRunTask (task , plan .HMACKey )
374
+ result := modelFromTFEOrganizationRunTask (task , plan .HMACKey , ! config .HMACKeyWO .IsNull ())
375
+ r .updatePrivateState (ctx , resp , config .HMACKeyWO )
255
376
256
377
// Save data into Terraform state
257
378
resp .Diagnostics .Append (resp .State .Set (ctx , & result )... )
@@ -304,7 +425,22 @@ func (r *resourceOrgRunTask) ImportState(ctx context.Context, req resource.Impor
304
425
)
305
426
} else {
306
427
// 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 )
308
429
resp .Diagnostics .Append (resp .State .Set (ctx , & result )... )
309
430
}
310
431
}
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 {}
0 commit comments