@@ -5,11 +5,15 @@ package provider
55
66import (
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+
149235func (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 {}
0 commit comments