Skip to content

Commit e7c08f5

Browse files
Add write only token_wo attribute to tfe_team_notification_configuration (#1665)
* Resource changes * Implement test * Update CHANGELOG.md * use lastValue approach * Revert "use lastValue approach" This reverts commit 1881650. * Fix failing test cases * Resource changes * Implement test * Update CHANGELOG.md * use lastValue approach * Revert "use lastValue approach" This reverts commit 1881650. * Fix failing test cases * add last value * Update internal/provider/resource_tfe_team_notification_configuration.go Co-authored-by: Chris Trombley <[email protected]> * Update team_notification_configuration.html.markdown * address comments --------- Co-authored-by: Chris Trombley <[email protected]>
1 parent cf4cca2 commit e7c08f5

File tree

4 files changed

+218
-27
lines changed

4 files changed

+218
-27
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ ENHANCEMENTS:
3333
* resource/tfe_saml_settings: Add `private_key_wo` write-only attribute, by @uturunku1 ([#1660](https://github.com/hashicorp/terraform-provider-tfe/pull/1660))
3434

3535
* resource/tfe_ssh_key: Add `key_wo` write-only attribute, by @ctrombley ([#1659](https://github.com/hashicorp/terraform-provider-tfe/pull/1659))
36+
37+
* resource/tfe_team_notification_configuration: Add `token_wo` write-only attribute, by @shwetamurali ([#1665](https://github.com/hashicorp/terraform-provider-tfe/pull/1665))
3638

3739
* resource/tfe_notification_configuration: Add `token_wo` write-only attribute, by @uturunku1 ([#1664](https://github.com/hashicorp/terraform-provider-tfe/pull/1664))
3840

internal/provider/resource_tfe_team_notification_configuration.go

Lines changed: 84 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package provider
55

66
import (
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

112122
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
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

252281
func (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
368421
func (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
466519
func (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+
}

internal/provider/resource_tfe_team_notification_configuration_test.go

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,11 @@ import (
1010
"testing"
1111

1212
"github.com/hashicorp/go-tfe"
13+
"github.com/hashicorp/terraform-plugin-testing/compare"
1314
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
15+
"github.com/hashicorp/terraform-plugin-testing/statecheck"
1416
"github.com/hashicorp/terraform-plugin-testing/terraform"
17+
"github.com/hashicorp/terraform-plugin-testing/tfjsonpath"
1518
)
1619

1720
func TestAccTFETeamNotificationConfiguration_basic(t *testing.T) {
@@ -926,6 +929,72 @@ func testAccCheckTFETeamNotificationConfigurationDestroy(s *terraform.State) err
926929
return nil
927930
}
928931

932+
func TestAccTFETeamNotificationConfiguration_tokenWO(t *testing.T) {
933+
skipUnlessBeta(t)
934+
tfeClient, err := getClientUsingEnv()
935+
if err != nil {
936+
t.Fatal(err)
937+
}
938+
939+
org, cleanupOrg := createPlusOrganization(t, tfeClient)
940+
t.Cleanup(cleanupOrg)
941+
942+
// Create the value comparer so we can add state values to it during the test steps
943+
compareValuesDiffer := statecheck.CompareValue(compare.ValuesDiffer())
944+
945+
resource.Test(t, resource.TestCase{
946+
PreCheck: func() { preCheckTFETeamNotificationConfiguration(t) },
947+
ProtoV5ProviderFactories: testAccMuxedProviders,
948+
CheckDestroy: testAccCheckTFETeamNotificationConfigurationDestroy,
949+
Steps: []resource.TestStep{
950+
{
951+
Config: testAccTFETeamNotificationConfiguration_tokenAndTokenWO(org.Name),
952+
ExpectError: regexp.MustCompile(`Attribute "token_wo" cannot be specified when "token" is specified`),
953+
},
954+
{
955+
Config: testAccTFETeamNotificationConfiguration_tokenWO(org.Name, "1234567890"),
956+
Check: resource.ComposeTestCheckFunc(
957+
resource.TestCheckNoResourceAttr("tfe_team_notification_configuration.foobar", "token"),
958+
resource.TestCheckNoResourceAttr("tfe_team_notification_configuration.foobar", "token_wo"),
959+
),
960+
// Register the id with the value comparer so we can assert that the
961+
// resource has been replaced in the next step.
962+
ConfigStateChecks: []statecheck.StateCheck{
963+
compareValuesDiffer.AddStateValue(
964+
"tfe_team_notification_configuration.foobar", tfjsonpath.New("id"),
965+
),
966+
},
967+
},
968+
{
969+
Config: testAccTFETeamNotificationConfiguration_tokenWO(org.Name, "12345678901"),
970+
Check: resource.ComposeTestCheckFunc(
971+
resource.TestCheckNoResourceAttr("tfe_team_notification_configuration.foobar", "token"),
972+
resource.TestCheckNoResourceAttr("tfe_team_notification_configuration.foobar", "token_wo"),
973+
),
974+
// Register the id with the value comparer so we can assert that the
975+
// resource has been replaced in the next step.
976+
ConfigStateChecks: []statecheck.StateCheck{
977+
compareValuesDiffer.AddStateValue(
978+
"tfe_team_notification_configuration.foobar", tfjsonpath.New("id"),
979+
),
980+
},
981+
},
982+
{
983+
Config: testAccTFETeamNotificationConfiguration_token(org.Name),
984+
Check: resource.ComposeTestCheckFunc(
985+
resource.TestCheckResourceAttr("tfe_team_notification_configuration.foobar", "token", "1234567890"),
986+
),
987+
// Ensure that the resource has been replaced
988+
ConfigStateChecks: []statecheck.StateCheck{
989+
compareValuesDiffer.AddStateValue(
990+
"tfe_team_notification_configuration.foobar", tfjsonpath.New("id"),
991+
),
992+
},
993+
},
994+
},
995+
})
996+
}
997+
929998
func testAccTFETeamNotificationConfiguration_basic(orgName string) string {
930999
return fmt.Sprintf(`
9311000
data "tfe_organization" "foobar" {
@@ -1360,6 +1429,67 @@ resource "tfe_team_notification_configuration" "foobar" {
13601429
}`, orgName, runTasksURL())
13611430
}
13621431

1432+
func testAccTFETeamNotificationConfiguration_token(orgName string) string {
1433+
return fmt.Sprintf(`
1434+
data "tfe_organization" "foobar" {
1435+
name = "%s"
1436+
}
1437+
1438+
resource "tfe_team" "foobar" {
1439+
name = "team-test"
1440+
organization = data.tfe_organization.foobar.name
1441+
}
1442+
1443+
resource "tfe_team_notification_configuration" "foobar" {
1444+
name = "notification_tokenWO_test"
1445+
destination_type = "generic"
1446+
url = "%s"
1447+
token = "1234567890"
1448+
team_id = tfe_team.foobar.id
1449+
}`, orgName, runTasksURL())
1450+
}
1451+
1452+
func testAccTFETeamNotificationConfiguration_tokenWO(orgName string, token string) string {
1453+
return fmt.Sprintf(`
1454+
data "tfe_organization" "foobar" {
1455+
name = "%s"
1456+
}
1457+
1458+
resource "tfe_team" "foobar" {
1459+
name = "team-test"
1460+
organization = data.tfe_organization.foobar.name
1461+
}
1462+
1463+
resource "tfe_team_notification_configuration" "foobar" {
1464+
name = "notification_tokenWO_test"
1465+
destination_type = "generic"
1466+
url = "%s"
1467+
token_wo = "%s"
1468+
team_id = tfe_team.foobar.id
1469+
}`, orgName, runTasksURL(), token)
1470+
}
1471+
1472+
func testAccTFETeamNotificationConfiguration_tokenAndTokenWO(orgName string) string {
1473+
return fmt.Sprintf(`
1474+
data "tfe_organization" "foobar" {
1475+
name = "%s"
1476+
}
1477+
1478+
resource "tfe_team" "foobar" {
1479+
name = "team-test"
1480+
organization = data.tfe_organization.foobar.name
1481+
}
1482+
1483+
resource "tfe_team_notification_configuration" "foobar" {
1484+
name = "notification_tokenWO_test"
1485+
destination_type = "generic"
1486+
url = "%s"
1487+
token = "1234567890"
1488+
token_wo = "1234567890"
1489+
team_id = tfe_team.foobar.id
1490+
}`, orgName, runTasksURL())
1491+
}
1492+
13631493
func preCheckTFETeamNotificationConfiguration(t *testing.T) {
13641494
testAccPreCheck(t)
13651495

0 commit comments

Comments
 (0)