@@ -5,6 +5,7 @@ package provider
55
66import (
77 "context"
8+ "errors"
89 "fmt"
910
1011 tfe "github.com/hashicorp/go-tfe"
@@ -21,6 +22,8 @@ import (
2122 "github.com/hashicorp/terraform-plugin-framework/schema/validator"
2223 "github.com/hashicorp/terraform-plugin-framework/types"
2324 "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"
2427 "github.com/hashicorp/terraform-provider-tfe/internal/provider/validators"
2528)
2629
@@ -53,25 +56,28 @@ type modelTFETeamNotificationConfiguration struct {
5356 Triggers types.Set `tfsdk:"triggers"`
5457 URL types.String `tfsdk:"url"`
5558 TeamID types.String `tfsdk:"team_id"`
59+ TokenWO types.String `tfsdk:"token_wo"`
5660}
5761
5862// modelFromTFETeamNotificationConfiguration builds a modelTFETeamNotificationConfiguration
5963// 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
6166 result := modelTFETeamNotificationConfiguration {
6267 ID : types .StringValue (v .ID ),
6368 Name : types .StringValue (v .Name ),
6469 DestinationType : types .StringValue (string (v .DestinationType )),
6570 Enabled : types .BoolValue (v .Enabled ),
6671 TeamID : types .StringValue (v .SubscribableChoice .Team .ID ),
72+ Token : types .StringValue ("" ),
6773 }
6874
6975 if len (v .EmailAddresses ) == 0 {
7076 result .EmailAddresses = types .SetNull (types .StringType )
7177 } else {
7278 emailAddresses , diags := types .SetValueFrom (ctx , types .StringType , v .EmailAddresses )
7379 if diags != nil && diags .HasError () {
74- return nil , & diags
80+ return nil , diags
7581 }
7682 result .EmailAddresses = emailAddresses
7783 }
@@ -81,7 +87,7 @@ func modelFromTFETeamNotificationConfiguration(v *tfe.NotificationConfiguration)
8187 } else {
8288 triggers , diags := types .SetValueFrom (ctx , types .StringType , v .Triggers )
8389 if diags != nil && diags .HasError () {
84- return nil , & diags
90+ return nil , diags
8591 }
8692
8793 result .Triggers = triggers
@@ -98,15 +104,19 @@ func modelFromTFETeamNotificationConfiguration(v *tfe.NotificationConfiguration)
98104 result .EmailUserIDs = types .SetValueMust (types .StringType , emailUserIDs )
99105 }
100106
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 ()
103113 }
104114
105115 if v .URL != "" {
106116 result .URL = types .StringValue (v .URL )
107117 }
108118
109- return & result , nil
119+ return & result , diags
110120}
111121
112122func (r * resourceTFETeamNotificationConfiguration ) Schema (ctx context.Context , req resource.SchemaRequest , resp * resource.SchemaResponse ) {
@@ -186,6 +196,25 @@ func (r *resourceTFETeamNotificationConfiguration) Schema(ctx context.Context, r
186196 "destination_type" ,
187197 []string {"email" , "microsoft-teams" , "slack" },
188198 ),
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" ),
189218 },
190219 },
191220
@@ -250,10 +279,11 @@ func (r *resourceTFETeamNotificationConfiguration) Configure(ctx context.Context
250279}
251280
252281func (r * resourceTFETeamNotificationConfiguration ) Create (ctx context.Context , req resource.CreateRequest , resp * resource.CreateResponse ) {
253- var plan modelTFETeamNotificationConfiguration
282+ var plan , config modelTFETeamNotificationConfiguration
254283
255- // Read Terraform plan data into the model
284+ // Read Terraform plan and config data into the model
256285 resp .Diagnostics .Append (req .Plan .Get (ctx , & plan )... )
286+ resp .Diagnostics .Append (req .Config .Get (ctx , & config )... )
257287
258288 if resp .Diagnostics .HasError () {
259289 return
@@ -267,13 +297,20 @@ func (r *resourceTFETeamNotificationConfiguration) Create(ctx context.Context, r
267297 DestinationType : tfe .NotificationDestination (tfe .NotificationDestinationType (plan .DestinationType .ValueString ())),
268298 Enabled : plan .Enabled .ValueBoolPointer (),
269299 Name : plan .Name .ValueStringPointer (),
270- Token : plan .Token .ValueStringPointer (),
271300 URL : plan .URL .ValueStringPointer (),
272301 SubscribableChoice : & tfe.NotificationConfigurationSubscribableChoice {
273302 Team : & tfe.Team {ID : teamID },
274303 },
275304 }
276305
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+
277314 // Add triggers set to the options struct
278315 var triggers []types.String
279316 if diags := plan .Triggers .ElementsAs (ctx , & triggers , true ); diags != nil && diags .HasError () {
@@ -323,9 +360,18 @@ func (r *resourceTFETeamNotificationConfiguration) Create(ctx context.Context, r
323360 tnc .Token = plan .Token .ValueString ()
324361 }
325362
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 )... )
329375 return
330376 }
331377
@@ -346,18 +392,25 @@ func (r *resourceTFETeamNotificationConfiguration) Read(ctx context.Context, req
346392 tflog .Debug (ctx , fmt .Sprintf ("Reading team notification configuration %q" , state .ID .ValueString ()))
347393 tnc , err := r .config .Client .NotificationConfigurations .Read (ctx , state .ID .ValueString ())
348394 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+ }
350401 return
351402 }
352403
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
356409 }
357410
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 ... )
361414 return
362415 }
363416
@@ -368,6 +421,7 @@ func (r *resourceTFETeamNotificationConfiguration) Read(ctx context.Context, req
368421func (r * resourceTFETeamNotificationConfiguration ) Update (ctx context.Context , req resource.UpdateRequest , resp * resource.UpdateResponse ) {
369422 var plan modelTFETeamNotificationConfiguration
370423 var state modelTFETeamNotificationConfiguration
424+ var config modelTFETeamNotificationConfiguration
371425
372426 // Read Terraform plan data into the model
373427 resp .Diagnostics .Append (req .Plan .Get (ctx , & plan )... )
@@ -430,17 +484,16 @@ func (r *resourceTFETeamNotificationConfiguration) Update(ctx context.Context, r
430484 return
431485 }
432486
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 )... )
441490 return
442491 }
443492
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+
444497 // Save data into Terraform state
445498 resp .Diagnostics .Append (resp .State .Set (ctx , & result )... )
446499}
@@ -466,3 +519,7 @@ func (r *resourceTFETeamNotificationConfiguration) Delete(ctx context.Context, r
466519func (r * resourceTFETeamNotificationConfiguration ) ImportState (ctx context.Context , req resource.ImportStateRequest , resp * resource.ImportStateResponse ) {
467520 resp .Diagnostics .Append (resp .State .SetAttribute (ctx , path .Root ("id" ), req .ID )... )
468521}
522+
523+ func (r * resourceTFETeamNotificationConfiguration ) writeOnlyValueStore (private helpers.PrivateState ) * helpers.WriteOnlyValueStore {
524+ return helpers .NewWriteOnlyValueStore (private , "token_wo" )
525+ }
0 commit comments