@@ -5,6 +5,7 @@ package provider
5
5
6
6
import (
7
7
"context"
8
+ "errors"
8
9
"fmt"
9
10
10
11
tfe "github.com/hashicorp/go-tfe"
@@ -21,6 +22,8 @@ import (
21
22
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
22
23
"github.com/hashicorp/terraform-plugin-framework/types"
23
24
"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"
24
27
"github.com/hashicorp/terraform-provider-tfe/internal/provider/validators"
25
28
)
26
29
@@ -53,25 +56,28 @@ type modelTFETeamNotificationConfiguration struct {
53
56
Triggers types.Set `tfsdk:"triggers"`
54
57
URL types.String `tfsdk:"url"`
55
58
TeamID types.String `tfsdk:"team_id"`
59
+ TokenWO types.String `tfsdk:"token_wo"`
56
60
}
57
61
58
62
// modelFromTFETeamNotificationConfiguration builds a modelTFETeamNotificationConfiguration
59
63
// struct from a tfe.TeamNotificationConfiguration value.
60
- func modelFromTFETeamNotificationConfiguration (v * tfe.NotificationConfiguration ) (* modelTFETeamNotificationConfiguration , * diag.Diagnostics ) {
64
+ func modelFromTFETeamNotificationConfiguration (v * tfe.NotificationConfiguration , isWriteOnly bool , lastValue types.String ) (* modelTFETeamNotificationConfiguration , diag.Diagnostics ) {
65
+ var diags diag.Diagnostics
61
66
result := modelTFETeamNotificationConfiguration {
62
67
ID : types .StringValue (v .ID ),
63
68
Name : types .StringValue (v .Name ),
64
69
DestinationType : types .StringValue (string (v .DestinationType )),
65
70
Enabled : types .BoolValue (v .Enabled ),
66
71
TeamID : types .StringValue (v .SubscribableChoice .Team .ID ),
72
+ Token : types .StringValue ("" ),
67
73
}
68
74
69
75
if len (v .EmailAddresses ) == 0 {
70
76
result .EmailAddresses = types .SetNull (types .StringType )
71
77
} else {
72
78
emailAddresses , diags := types .SetValueFrom (ctx , types .StringType , v .EmailAddresses )
73
79
if diags != nil && diags .HasError () {
74
- return nil , & diags
80
+ return nil , diags
75
81
}
76
82
result .EmailAddresses = emailAddresses
77
83
}
@@ -81,7 +87,7 @@ func modelFromTFETeamNotificationConfiguration(v *tfe.NotificationConfiguration)
81
87
} else {
82
88
triggers , diags := types .SetValueFrom (ctx , types .StringType , v .Triggers )
83
89
if diags != nil && diags .HasError () {
84
- return nil , & diags
90
+ return nil , diags
85
91
}
86
92
87
93
result .Triggers = triggers
@@ -98,15 +104,19 @@ func modelFromTFETeamNotificationConfiguration(v *tfe.NotificationConfiguration)
98
104
result .EmailUserIDs = types .SetValueMust (types .StringType , emailUserIDs )
99
105
}
100
106
101
- if v .Token != "" {
102
- result .Token = types .StringValue (v .Token )
107
+ if lastValue .String () != "" {
108
+ result .Token = lastValue
109
+ }
110
+
111
+ if isWriteOnly {
112
+ result .Token = types .StringNull ()
103
113
}
104
114
105
115
if v .URL != "" {
106
116
result .URL = types .StringValue (v .URL )
107
117
}
108
118
109
- return & result , nil
119
+ return & result , diags
110
120
}
111
121
112
122
func (r * resourceTFETeamNotificationConfiguration ) Schema (ctx context.Context , req resource.SchemaRequest , resp * resource.SchemaResponse ) {
@@ -186,6 +196,25 @@ func (r *resourceTFETeamNotificationConfiguration) Schema(ctx context.Context, r
186
196
"destination_type" ,
187
197
[]string {"email" , "microsoft-teams" , "slack" },
188
198
),
199
+ stringvalidator .ConflictsWith (path .MatchRoot ("token_wo" )),
200
+ stringvalidator .PreferWriteOnlyAttribute (path .MatchRoot ("token_wo" )),
201
+ },
202
+ },
203
+
204
+ "token_wo" : schema.StringAttribute {
205
+ Description : "A write-only secure token for the notification configuration, guaranteed not to be written to plan or state artifacts." ,
206
+ Optional : true ,
207
+ WriteOnly : true ,
208
+ Sensitive : true ,
209
+ Validators : []validator.String {
210
+ validators .AttributeValueConflictValidator (
211
+ "destination_type" ,
212
+ []string {"email" , "microsoft-teams" , "slack" },
213
+ ),
214
+ stringvalidator .ConflictsWith (path .MatchRoot ("token" )),
215
+ },
216
+ PlanModifiers : []planmodifier.String {
217
+ planmodifiers .NewReplaceForWriteOnlyStringValue ("token_wo" ),
189
218
},
190
219
},
191
220
@@ -250,10 +279,11 @@ func (r *resourceTFETeamNotificationConfiguration) Configure(ctx context.Context
250
279
}
251
280
252
281
func (r * resourceTFETeamNotificationConfiguration ) Create (ctx context.Context , req resource.CreateRequest , resp * resource.CreateResponse ) {
253
- var plan modelTFETeamNotificationConfiguration
282
+ var plan , config modelTFETeamNotificationConfiguration
254
283
255
- // Read Terraform plan data into the model
284
+ // Read Terraform plan and config data into the model
256
285
resp .Diagnostics .Append (req .Plan .Get (ctx , & plan )... )
286
+ resp .Diagnostics .Append (req .Config .Get (ctx , & config )... )
257
287
258
288
if resp .Diagnostics .HasError () {
259
289
return
@@ -267,13 +297,20 @@ func (r *resourceTFETeamNotificationConfiguration) Create(ctx context.Context, r
267
297
DestinationType : tfe .NotificationDestination (tfe .NotificationDestinationType (plan .DestinationType .ValueString ())),
268
298
Enabled : plan .Enabled .ValueBoolPointer (),
269
299
Name : plan .Name .ValueStringPointer (),
270
- Token : plan .Token .ValueStringPointer (),
271
300
URL : plan .URL .ValueStringPointer (),
272
301
SubscribableChoice : & tfe.NotificationConfigurationSubscribableChoice {
273
302
Team : & tfe.Team {ID : teamID },
274
303
},
275
304
}
276
305
306
+ // Set Token from `token_wo` if set, otherwise use the normal value
307
+ isWriteOnly := ! config .TokenWO .IsNull ()
308
+ if isWriteOnly {
309
+ options .Token = config .TokenWO .ValueStringPointer ()
310
+ } else {
311
+ options .Token = plan .Token .ValueStringPointer ()
312
+ }
313
+
277
314
// Add triggers set to the options struct
278
315
var triggers []types.String
279
316
if diags := plan .Triggers .ElementsAs (ctx , & triggers , true ); diags != nil && diags .HasError () {
@@ -323,9 +360,18 @@ func (r *resourceTFETeamNotificationConfiguration) Create(ctx context.Context, r
323
360
tnc .Token = plan .Token .ValueString ()
324
361
}
325
362
326
- result , diags := modelFromTFETeamNotificationConfiguration (tnc )
327
- if diags != nil && diags .HasError () {
328
- resp .Diagnostics .Append ((* diags )... )
363
+ result , diags := modelFromTFETeamNotificationConfiguration (tnc , isWriteOnly , plan .Token )
364
+ if diags .HasError () {
365
+ resp .Diagnostics .Append (diags ... )
366
+ return
367
+ }
368
+
369
+ // Write the hashed private token to the state if it was provided
370
+ store := r .writeOnlyValueStore (resp .Private )
371
+ resp .Diagnostics .Append (store .SetPriorValue (ctx , config .TokenWO )... )
372
+
373
+ if diags .HasError () {
374
+ resp .Diagnostics .Append ((diags )... )
329
375
return
330
376
}
331
377
@@ -346,18 +392,25 @@ func (r *resourceTFETeamNotificationConfiguration) Read(ctx context.Context, req
346
392
tflog .Debug (ctx , fmt .Sprintf ("Reading team notification configuration %q" , state .ID .ValueString ()))
347
393
tnc , err := r .config .Client .NotificationConfigurations .Read (ctx , state .ID .ValueString ())
348
394
if err != nil {
349
- resp .Diagnostics .AddError ("Unable to read team notification configuration" , err .Error ())
395
+ if errors .Is (err , tfe .ErrResourceNotFound ) {
396
+ tflog .Debug (ctx , fmt .Sprintf ("`Notification configuration %s no longer exists" , state .ID ))
397
+ resp .State .RemoveResource (ctx )
398
+ } else {
399
+ resp .Diagnostics .AddError ("Error reading notification configuration" , "Could not read notification configuration, unexpected error: " + err .Error ())
400
+ }
350
401
return
351
402
}
352
403
353
- // Restore token from state because it is write only
354
- if ! state .Token .IsNull () {
355
- tnc .Token = state .Token .ValueString ()
404
+ // Check if the parameter is write-only
405
+ isWriteOnly , diags := r .writeOnlyValueStore (resp .Private ).PriorValueExists (ctx )
406
+ resp .Diagnostics .Append (diags ... )
407
+ if diags .HasError () {
408
+ return
356
409
}
357
410
358
- result , diags := modelFromTFETeamNotificationConfiguration (tnc )
359
- if diags != nil && diags .HasError () {
360
- resp .Diagnostics .Append (( * diags ) ... )
411
+ result , diags := modelFromTFETeamNotificationConfiguration (tnc , isWriteOnly , state . Token )
412
+ if diags .HasError () {
413
+ resp .Diagnostics .Append (diags ... )
361
414
return
362
415
}
363
416
@@ -368,6 +421,7 @@ func (r *resourceTFETeamNotificationConfiguration) Read(ctx context.Context, req
368
421
func (r * resourceTFETeamNotificationConfiguration ) Update (ctx context.Context , req resource.UpdateRequest , resp * resource.UpdateResponse ) {
369
422
var plan modelTFETeamNotificationConfiguration
370
423
var state modelTFETeamNotificationConfiguration
424
+ var config modelTFETeamNotificationConfiguration
371
425
372
426
// Read Terraform plan data into the model
373
427
resp .Diagnostics .Append (req .Plan .Get (ctx , & plan )... )
@@ -430,17 +484,16 @@ func (r *resourceTFETeamNotificationConfiguration) Update(ctx context.Context, r
430
484
return
431
485
}
432
486
433
- // Restore token from plan because it is write only
434
- if ! plan .Token .IsNull () {
435
- tnc .Token = plan .Token .ValueString ()
436
- }
437
-
438
- result , diags := modelFromTFETeamNotificationConfiguration (tnc )
439
- if diags != nil && diags .HasError () {
440
- resp .Diagnostics .Append ((* diags )... )
487
+ result , diags := modelFromTFETeamNotificationConfiguration (tnc , ! config .TokenWO .IsNull (), plan .Token )
488
+ if diags .HasError () {
489
+ resp .Diagnostics .Append ((diags )... )
441
490
return
442
491
}
443
492
493
+ // Write the hashed private key to the state if it was provided
494
+ store := r .writeOnlyValueStore (resp .Private )
495
+ resp .Diagnostics .Append (store .SetPriorValue (ctx , config .TokenWO )... )
496
+
444
497
// Save data into Terraform state
445
498
resp .Diagnostics .Append (resp .State .Set (ctx , & result )... )
446
499
}
@@ -466,3 +519,7 @@ func (r *resourceTFETeamNotificationConfiguration) Delete(ctx context.Context, r
466
519
func (r * resourceTFETeamNotificationConfiguration ) ImportState (ctx context.Context , req resource.ImportStateRequest , resp * resource.ImportStateResponse ) {
467
520
resp .Diagnostics .Append (resp .State .SetAttribute (ctx , path .Root ("id" ), req .ID )... )
468
521
}
522
+
523
+ func (r * resourceTFETeamNotificationConfiguration ) writeOnlyValueStore (private helpers.PrivateState ) * helpers.WriteOnlyValueStore {
524
+ return helpers .NewWriteOnlyValueStore (private , "token_wo" )
525
+ }
0 commit comments