diff --git a/CHANGELOG.md b/CHANGELOG.md index cf7e88c93..4104d418e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ## Unreleased +FEATURES: +* **New Resource:** `tfe_team_notification_configuration` is a new resource for managing team notification configurations, by @jfreda ([#1540](https://github.com/hashicorp/terraform-provider-tfe/pull/1540)) + ## v0.62.0 FEATURES: diff --git a/internal/provider/helper_test.go b/internal/provider/helper_test.go index 4ff682a67..5353f8b96 100644 --- a/internal/provider/helper_test.go +++ b/internal/provider/helper_test.go @@ -66,6 +66,17 @@ func createBusinessOrganization(t *testing.T, client *tfe.Client) (*tfe.Organiza return org, orgCleanup } +func createPlusOrganization(t *testing.T, client *tfe.Client) (*tfe.Organization, func()) { + org, orgCleanup := createOrganization(t, client, tfe.OrganizationCreateOptions{ + Name: tfe.String("tst-" + randomString(t)), + Email: tfe.String(fmt.Sprintf("%s@tfe.local", randomString(t))), + }) + + newSubscriptionUpdater(org).WithPlusEntitlementPlan().Update(t) + + return org, orgCleanup +} + func createTrialOrganization(t *testing.T, client *tfe.Client) (*tfe.Organization, func()) { org, orgCleanup := createOrganization(t, client, tfe.OrganizationCreateOptions{ Name: tfe.String("tst-" + randomString(t)), diff --git a/internal/provider/provider_next.go b/internal/provider/provider_next.go index 65dde8488..a345f6df8 100644 --- a/internal/provider/provider_next.go +++ b/internal/provider/provider_next.go @@ -138,6 +138,7 @@ func (p *frameworkProvider) Resources(ctx context.Context) []func() resource.Res NewResourceWorkspaceSettings, NewSAMLSettingsResource, NewStackResource, + NewTeamNotificationConfigurationResource, NewTestVariableResource, NewWorkspaceRunTaskResource, } diff --git a/internal/provider/resource_tfe_team_notification_configuration.go b/internal/provider/resource_tfe_team_notification_configuration.go new file mode 100644 index 000000000..74e899686 --- /dev/null +++ b/internal/provider/resource_tfe_team_notification_configuration.go @@ -0,0 +1,468 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "context" + "fmt" + + tfe "github.com/hashicorp/go-tfe" + "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-provider-tfe/internal/provider/validators" +) + +// Ensure provider defined types fully satisfy framework interfaces. +var _ resource.Resource = &resourceTFETeamNotificationConfiguration{} +var _ resource.ResourceWithConfigure = &resourceTFETeamNotificationConfiguration{} +var _ resource.ResourceWithImportState = &resourceTFETeamNotificationConfiguration{} + +func NewTeamNotificationConfigurationResource() resource.Resource { + return &resourceTFETeamNotificationConfiguration{} +} + +// resourceTFETeamNotificationConfiguration implements the tfe_team_notification_configuration resource type +type resourceTFETeamNotificationConfiguration struct { + config ConfiguredClient +} + +func (r *resourceTFETeamNotificationConfiguration) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_team_notification_configuration" +} + +type modelTFETeamNotificationConfiguration struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + DestinationType types.String `tfsdk:"destination_type"` + EmailAddresses types.Set `tfsdk:"email_addresses"` + EmailUserIDs types.Set `tfsdk:"email_user_ids"` + Enabled types.Bool `tfsdk:"enabled"` + Token types.String `tfsdk:"token"` + Triggers types.Set `tfsdk:"triggers"` + URL types.String `tfsdk:"url"` + TeamID types.String `tfsdk:"team_id"` +} + +// modelFromTFETeamNotificationConfiguration builds a modelTFETeamNotificationConfiguration +// struct from a tfe.TeamNotificationConfiguration value. +func modelFromTFETeamNotificationConfiguration(v *tfe.NotificationConfiguration) (*modelTFETeamNotificationConfiguration, *diag.Diagnostics) { + result := modelTFETeamNotificationConfiguration{ + ID: types.StringValue(v.ID), + Name: types.StringValue(v.Name), + DestinationType: types.StringValue(string(v.DestinationType)), + Enabled: types.BoolValue(v.Enabled), + TeamID: types.StringValue(v.SubscribableChoice.Team.ID), + } + + if len(v.EmailAddresses) == 0 { + result.EmailAddresses = types.SetNull(types.StringType) + } else { + emailAddresses, diags := types.SetValueFrom(ctx, types.StringType, v.EmailAddresses) + if diags != nil && diags.HasError() { + return nil, &diags + } + result.EmailAddresses = emailAddresses + } + + if len(v.Triggers) == 0 { + result.Triggers = types.SetNull(types.StringType) + } else { + triggers, diags := types.SetValueFrom(ctx, types.StringType, v.Triggers) + if diags != nil && diags.HasError() { + return nil, &diags + } + + result.Triggers = triggers + } + + if len(v.EmailUsers) == 0 { + result.EmailUserIDs = types.SetNull(types.StringType) + } else { + emailUserIDs := make([]attr.Value, len(v.EmailUsers)) + for i, emailUser := range v.EmailUsers { + emailUserIDs[i] = types.StringValue(emailUser.ID) + } + + result.EmailUserIDs = types.SetValueMust(types.StringType, emailUserIDs) + } + + if v.Token != "" { + result.Token = types.StringValue(v.Token) + } + + if v.URL != "" { + result.URL = types.StringValue(v.URL) + } + + return &result, nil +} + +func (r *resourceTFETeamNotificationConfiguration) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Defines a team notification configuration resource.", + Version: 0, + + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "ID of the team notification configuration.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + + "name": schema.StringAttribute{ + Description: "Name of the team notification configuration.", + Required: true, + }, + + "destination_type": schema.StringAttribute{ + Description: "The type of notification configuration payload to send.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.OneOf( + string(tfe.NotificationDestinationTypeEmail), + string(tfe.NotificationDestinationTypeGeneric), + string(tfe.NotificationDestinationTypeSlack), + string(tfe.NotificationDestinationTypeMicrosoftTeams), + ), + }, + }, + + "email_addresses": schema.SetAttribute{ + Description: "A list of email addresses. This value must not be provided if `destination_type` is `generic`, `microsoft-teams`, or `slack`.", + Optional: true, + Computed: true, + ElementType: types.StringType, + Validators: []validator.Set{ + validators.AttributeValueConflictValidator( + "destination_type", + []string{"generic", "microsoft-teams", "slack"}, + ), + }, + }, + + "email_user_ids": schema.SetAttribute{ + Description: "A list of user IDs. This value must not be provided if `destination_type` is `generic`, `microsoft-teams`, or `slack`.", + Optional: true, + Computed: true, + ElementType: types.StringType, + Validators: []validator.Set{ + validators.AttributeValueConflictValidator( + "destination_type", + []string{"generic", "microsoft-teams", "slack"}, + ), + }, + }, + + "enabled": schema.BoolAttribute{ + Description: "Whether the team notification configuration should be enabled or not. Disabled configurations will not send any notifications. Defaults to `false`.", + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + }, + + "token": schema.StringAttribute{ + Description: "A write-only secure token for the notification configuration, which can be used by the receiving server to verify request authenticity when configured for notification configurations with a destination type of `generic`. Defaults to `null`. This value _must not_ be provided if `destination_type` is `email`, `microsoft-teams`, or `slack`.", + Optional: true, + Sensitive: true, + Validators: []validator.String{ + validators.AttributeValueConflictValidator( + "destination_type", + []string{"email", "microsoft-teams", "slack"}, + ), + }, + }, + + "triggers": schema.SetAttribute{ + Description: "The array of triggers for which this team notification configuration will send notifications. If omitted, no notification triggers are configured.", + Optional: true, + ElementType: types.StringType, + Validators: []validator.Set{ + setvalidator.ValueStringsAre( + stringvalidator.OneOf( + string(tfe.NotificationTriggerChangeRequestCreated), + ), + ), + }, + }, + + "url": schema.StringAttribute{ + Description: "The HTTP or HTTPS URL where notification requests will be made. This value must not be provided if `email_addresses` or `email_user_ids` is present, or if `destination_type` is `email`.", + Optional: true, + Validators: []validator.String{ + validators.AttributeRequiredIfValueString( + "destination_type", + []string{"generic", "microsoft-teams", "slack"}, + ), + validators.AttributeValueConflictValidator( + "destination_type", + []string{"email"}, + ), + stringvalidator.ConflictsWith( + path.MatchRelative().AtParent().AtName("email_addresses"), + path.MatchRelative().AtParent().AtName("email_user_ids"), + ), + }, + }, + + "team_id": schema.StringAttribute{ + Description: "The ID of the team that owns the notification configuration.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + }, + } +} + +// Configure implements resource.ResourceWithConfigure +func (r *resourceTFETeamNotificationConfiguration) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(ConfiguredClient) + if !ok { + resp.Diagnostics.AddError( + "Unexpected resource Configure type", + fmt.Sprintf("Expected tfe.ConfiguredClient, got %T. This is a bug in the tfe provider, so please report it on GitHub.", req.ProviderData), + ) + } + r.config = client +} + +func (r *resourceTFETeamNotificationConfiguration) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan modelTFETeamNotificationConfiguration + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + + if resp.Diagnostics.HasError() { + return + } + + // Get team + teamID := plan.TeamID.ValueString() + + // Create a new options struct + options := tfe.NotificationConfigurationCreateOptions{ + DestinationType: tfe.NotificationDestination(tfe.NotificationDestinationType(plan.DestinationType.ValueString())), + Enabled: plan.Enabled.ValueBoolPointer(), + Name: plan.Name.ValueStringPointer(), + Token: plan.Token.ValueStringPointer(), + URL: plan.URL.ValueStringPointer(), + SubscribableChoice: &tfe.NotificationConfigurationSubscribableChoice{ + Team: &tfe.Team{ID: teamID}, + }, + } + + // Add triggers set to the options struct + var triggers []types.String + if diags := plan.Triggers.ElementsAs(ctx, &triggers, true); diags != nil && diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + options.Triggers = []tfe.NotificationTriggerType{} + for _, trigger := range triggers { + options.Triggers = append(options.Triggers, tfe.NotificationTriggerType(trigger.ValueString())) + } + + // Add email_addresses set to the options struct + emailAddresses := make([]types.String, len(plan.EmailAddresses.Elements())) + if diags := plan.EmailAddresses.ElementsAs(ctx, &emailAddresses, true); diags != nil && diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + + options.EmailAddresses = []string{} + for _, emailAddress := range emailAddresses { + options.EmailAddresses = append(options.EmailAddresses, emailAddress.ValueString()) + } + + // Add email_user_ids set to the options struct + emailUserIDs := make([]types.String, len(plan.EmailUserIDs.Elements())) + if diags := plan.EmailUserIDs.ElementsAs(ctx, &emailUserIDs, true); diags != nil && diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + options.EmailUsers = []*tfe.User{} + for _, emailUserID := range emailUserIDs { + options.EmailUsers = append(options.EmailUsers, &tfe.User{ID: emailUserID.ValueString()}) + } + + tflog.Debug(ctx, "Creating team notification configuration") + tnc, err := r.config.Client.NotificationConfigurations.Create(ctx, teamID, options) + if err != nil { + resp.Diagnostics.AddError("Unable to create team notification configuration", err.Error()) + return + } else if len(tnc.EmailUsers) != len(plan.EmailUserIDs.Elements()) { + resp.Diagnostics.AddError("Email user IDs produced an inconsistent result", "API returned a different number of email user IDs than were provided in the plan.") + return + } + + // Restore token from plan because it is write only + if !plan.Token.IsNull() { + tnc.Token = plan.Token.ValueString() + } + + result, diags := modelFromTFETeamNotificationConfiguration(tnc) + if diags != nil && diags.HasError() { + resp.Diagnostics.Append((*diags)...) + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &result)...) +} + +func (r *resourceTFETeamNotificationConfiguration) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state modelTFETeamNotificationConfiguration + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + + if resp.Diagnostics.HasError() { + return + } + + tflog.Debug(ctx, fmt.Sprintf("Reading team notification configuration %q", state.ID.ValueString())) + tnc, err := r.config.Client.NotificationConfigurations.Read(ctx, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to read team notification configuration", err.Error()) + return + } + + // Restore token from state because it is write only + if !state.Token.IsNull() { + tnc.Token = state.Token.ValueString() + } + + result, diags := modelFromTFETeamNotificationConfiguration(tnc) + if diags != nil && diags.HasError() { + resp.Diagnostics.Append((*diags)...) + return + } + + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &result)...) +} + +func (r *resourceTFETeamNotificationConfiguration) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan modelTFETeamNotificationConfiguration + var state modelTFETeamNotificationConfiguration + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + + if resp.Diagnostics.HasError() { + return + } + + // Create a new options struct + options := tfe.NotificationConfigurationUpdateOptions{ + Enabled: plan.Enabled.ValueBoolPointer(), + Name: plan.Name.ValueStringPointer(), + Token: plan.Token.ValueStringPointer(), + URL: plan.URL.ValueStringPointer(), + } + + // Add triggers set to the options struct + triggers := make([]types.String, len(plan.Triggers.Elements())) + if diags := plan.Triggers.ElementsAs(ctx, &triggers, true); diags != nil && diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + options.Triggers = []tfe.NotificationTriggerType{} + for _, trigger := range triggers { + options.Triggers = append(options.Triggers, tfe.NotificationTriggerType(trigger.ValueString())) + } + + // Add email_addresses set to the options struct + emailAddresses := make([]types.String, len(plan.EmailAddresses.Elements())) + if diags := plan.EmailAddresses.ElementsAs(ctx, &emailAddresses, true); diags != nil && diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + options.EmailAddresses = []string{} + for _, emailAddress := range emailAddresses { + options.EmailAddresses = append(options.EmailAddresses, emailAddress.ValueString()) + } + + // Add email_user_ids set to the options struct + emailUserIDs := make([]types.String, len(plan.EmailUserIDs.Elements())) + if diags := plan.EmailUserIDs.ElementsAs(ctx, &emailUserIDs, true); diags != nil && diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + options.EmailUsers = []*tfe.User{} + for _, emailUserID := range emailUserIDs { + options.EmailUsers = append(options.EmailUsers, &tfe.User{ID: emailUserID.ValueString()}) + } + + tflog.Debug(ctx, "Updating team notification configuration") + tnc, err := r.config.Client.NotificationConfigurations.Update(ctx, state.ID.ValueString(), options) + if err != nil { + resp.Diagnostics.AddError("Unable to update team notification configuration", err.Error()) + return + } else if len(tnc.EmailUsers) != len(plan.EmailUserIDs.Elements()) { + resp.Diagnostics.AddError("Email user IDs produced an inconsistent result", "API returned a different number of email user IDs than were provided in the plan.") + return + } + + // Restore token from plan because it is write only + if !plan.Token.IsNull() { + tnc.Token = plan.Token.ValueString() + } + + result, diags := modelFromTFETeamNotificationConfiguration(tnc) + if diags != nil && diags.HasError() { + resp.Diagnostics.Append((*diags)...) + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &result)...) +} + +func (r *resourceTFETeamNotificationConfiguration) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state modelTFETeamNotificationConfiguration + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + + if resp.Diagnostics.HasError() { + return + } + + tflog.Debug(ctx, "Deleting team notification configuration") + err := r.config.Client.NotificationConfigurations.Delete(ctx, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Unable to delete team notification configuration", err.Error()) + return + } +} + +func (r *resourceTFETeamNotificationConfiguration) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), req.ID)...) +} diff --git a/internal/provider/resource_tfe_team_notification_configuration_test.go b/internal/provider/resource_tfe_team_notification_configuration_test.go new file mode 100644 index 000000000..ef21b5b01 --- /dev/null +++ b/internal/provider/resource_tfe_team_notification_configuration_test.go @@ -0,0 +1,1373 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "fmt" + "reflect" + "regexp" + "testing" + + "github.com/hashicorp/go-tfe" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccTFETeamNotificationConfiguration_basic(t *testing.T) { + skipUnlessBeta(t) + tfeClient, err := getClientUsingEnv() + if err != nil { + t.Fatal(err) + } + + org, cleanupOrg := createPlusOrganization(t, tfeClient) + t.Cleanup(cleanupOrg) + + notificationConfiguration := &tfe.NotificationConfiguration{} + + resource.Test(t, resource.TestCase{ + PreCheck: func() { preCheckTFETeamNotificationConfiguration(t) }, + ProtoV5ProviderFactories: testAccMuxedProviders, + CheckDestroy: testAccCheckTFETeamNotificationConfigurationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccTFETeamNotificationConfiguration_basic(org.Name), + Check: resource.ComposeTestCheckFunc( + testAccCheckTFETeamNotificationConfigurationExists( + "tfe_team_notification_configuration.foobar", notificationConfiguration), + testAccCheckTFETeamNotificationConfigurationAttributes(notificationConfiguration), + resource.TestCheckResourceAttr( + "tfe_team_notification_configuration.foobar", "destination_type", "generic"), + resource.TestCheckResourceAttr( + "tfe_team_notification_configuration.foobar", "name", "notification_basic"), + // Just test the number of items in triggers + // Values in triggers attribute are tested by testCheckTFETeamNotificationConfigurationAttributes + resource.TestCheckResourceAttr( + "tfe_team_notification_configuration.foobar", "triggers.#", "0"), + resource.TestCheckResourceAttr( + "tfe_team_notification_configuration.foobar", "url", runTasksURL()), + ), + }, + }, + }) +} + +func TestAccTFETeamNotificationConfiguration_emailUserIDs(t *testing.T) { + skipUnlessBeta(t) + tfeClient, err := getClientUsingEnv() + if err != nil { + t.Fatal(err) + } + + org, cleanupOrg := createPlusOrganization(t, tfeClient) + t.Cleanup(cleanupOrg) + + notificationConfiguration := &tfe.NotificationConfiguration{} + + resource.Test(t, resource.TestCase{ + PreCheck: func() { preCheckTFETeamNotificationConfiguration(t) }, + ProtoV5ProviderFactories: testAccMuxedProviders, + CheckDestroy: testAccCheckTFETeamNotificationConfigurationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccTFETeamNotificationConfiguration_emailUserIDs(org.Name), + Check: resource.ComposeTestCheckFunc( + testAccCheckTFETeamNotificationConfigurationExists( + "tfe_team_notification_configuration.foobar", notificationConfiguration), + testAccCheckTFETeamNotificationConfigurationAttributesEmailUserIDs(notificationConfiguration), + resource.TestCheckResourceAttr( + "tfe_team_notification_configuration.foobar", "destination_type", "email"), + resource.TestCheckResourceAttr( + "tfe_team_notification_configuration.foobar", "name", "notification_email"), + // Just test the number of items in triggers + // Values in triggers attribute are tested by testCheckTFETeamNotificationConfigurationAttributesEmailUserIDs + resource.TestCheckResourceAttr( + "tfe_team_notification_configuration.foobar", "triggers.#", "0"), + resource.TestCheckResourceAttr( + "tfe_team_notification_configuration.foobar", "email_user_ids.#", "0"), + ), + }, + }, + }) +} + +func TestAccTFETeamNotificationConfiguration_update(t *testing.T) { + skipUnlessBeta(t) + tfeClient, err := getClientUsingEnv() + if err != nil { + t.Fatal(err) + } + + org, cleanupOrg := createPlusOrganization(t, tfeClient) + t.Cleanup(cleanupOrg) + + notificationConfiguration := &tfe.NotificationConfiguration{} + + resource.Test(t, resource.TestCase{ + PreCheck: func() { preCheckTFETeamNotificationConfiguration(t) }, + ProtoV5ProviderFactories: testAccMuxedProviders, + CheckDestroy: testAccCheckTFETeamNotificationConfigurationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccTFETeamNotificationConfiguration_basic(org.Name), + Check: resource.ComposeTestCheckFunc( + testAccCheckTFETeamNotificationConfigurationExists( + "tfe_team_notification_configuration.foobar", notificationConfiguration), + testAccCheckTFETeamNotificationConfigurationAttributes(notificationConfiguration), + resource.TestCheckResourceAttr( + "tfe_team_notification_configuration.foobar", "destination_type", "generic"), + resource.TestCheckResourceAttr( + "tfe_team_notification_configuration.foobar", "name", "notification_basic"), + // Just test the number of items in triggers + // Values in triggers attribute are tested by testCheckTFETeamNotificationConfigurationAttributes + resource.TestCheckResourceAttr( + "tfe_team_notification_configuration.foobar", "triggers.#", "0"), + resource.TestCheckResourceAttr( + "tfe_team_notification_configuration.foobar", "url", runTasksURL()), + ), + }, + { + Config: testAccTFETeamNotificationConfiguration_update(org.Name), + Check: resource.ComposeTestCheckFunc( + testAccCheckTFETeamNotificationConfigurationExists( + "tfe_team_notification_configuration.foobar", notificationConfiguration), + testAccCheckTFETeamNotificationConfigurationAttributesUpdate(notificationConfiguration), + resource.TestCheckResourceAttr( + "tfe_team_notification_configuration.foobar", "destination_type", "generic"), + resource.TestCheckResourceAttr( + "tfe_team_notification_configuration.foobar", "enabled", "true"), + resource.TestCheckResourceAttr( + "tfe_team_notification_configuration.foobar", "name", "notification_update"), + resource.TestCheckResourceAttr( + "tfe_team_notification_configuration.foobar", "token", "1234567890_update"), + // Just test the number of items in triggers + // Values in triggers attribute are tested by testCheckTFETeamNotificationConfigurationAttributesUpdate + resource.TestCheckResourceAttr( + "tfe_team_notification_configuration.foobar", "triggers.#", "1"), + resource.TestCheckResourceAttr( + "tfe_team_notification_configuration.foobar", "url", fmt.Sprintf("%s?update=true", runTasksURL())), + ), + }, + }, + }) +} + +func TestAccTFETeamNotificationConfiguration_updateEmailUserIDs(t *testing.T) { + skipUnlessBeta(t) + tfeClient, err := getClientUsingEnv() + if err != nil { + t.Fatal(err) + } + + org, cleanupOrg := createPlusOrganization(t, tfeClient) + t.Cleanup(cleanupOrg) + + notificationConfiguration := &tfe.NotificationConfiguration{} + + resource.Test(t, resource.TestCase{ + PreCheck: func() { preCheckTFETeamNotificationConfiguration(t) }, + ProtoV5ProviderFactories: testAccMuxedProviders, + CheckDestroy: testAccCheckTFETeamNotificationConfigurationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccTFETeamNotificationConfiguration_emailUserIDs(org.Name), + Check: resource.ComposeTestCheckFunc( + testAccCheckTFETeamNotificationConfigurationExists( + "tfe_team_notification_configuration.foobar", notificationConfiguration), + testAccCheckTFETeamNotificationConfigurationAttributesEmailUserIDs(notificationConfiguration), + resource.TestCheckResourceAttr( + "tfe_team_notification_configuration.foobar", "destination_type", "email"), + resource.TestCheckResourceAttr( + "tfe_team_notification_configuration.foobar", "name", "notification_email"), + // Just test the number of items in triggers + // Values in triggers attribute are tested by testCheckTFETeamNotificationConfigurationAttributesEmailUserIDs + resource.TestCheckResourceAttr( + "tfe_team_notification_configuration.foobar", "triggers.#", "0"), + resource.TestCheckResourceAttr( + "tfe_team_notification_configuration.foobar", "email_user_ids.#", "0"), + ), + }, + { + Config: testAccTFETeamNotificationConfiguration_updateEmailUserIDs(org.Name), + Check: resource.ComposeTestCheckFunc( + testAccCheckTFETeamNotificationConfigurationExists( + "tfe_team_notification_configuration.foobar", notificationConfiguration), + testAccCheckTFETeamNotificationConfigurationAttributesUpdateEmailUserIDs(notificationConfiguration), + resource.TestCheckResourceAttr( + "tfe_team_notification_configuration.foobar", "destination_type", "email"), + resource.TestCheckResourceAttr( + "tfe_team_notification_configuration.foobar", "enabled", "true"), + resource.TestCheckResourceAttr( + "tfe_team_notification_configuration.foobar", "name", "notification_email_update"), + // Just test the number of items in triggers + // Values in triggers attribute are tested by testCheckTFETeamNotificationConfigurationAttributesUpdateEmailUserIDs + resource.TestCheckResourceAttr( + "tfe_team_notification_configuration.foobar", "triggers.#", "1"), + resource.TestCheckResourceAttr( + "tfe_team_notification_configuration.foobar", "email_user_ids.#", "1"), + ), + }, + }, + }) +} + +func TestAccTFETeamNotificationConfiguration_validateSchemaAttributesEmail(t *testing.T) { + tfeClient, err := getClientUsingEnv() + if err != nil { + t.Fatal(err) + } + + org, cleanupOrg := createPlusOrganization(t, tfeClient) + t.Cleanup(cleanupOrg) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { preCheckTFETeamNotificationConfiguration(t) }, + ProtoV5ProviderFactories: testAccMuxedProviders, + Steps: []resource.TestStep{ + { + Config: testAccTFETeamNotificationConfiguration_emailWithURL(org.Name), + ExpectError: regexp.MustCompile(`The attribute 'url' cannot be set when 'destination_type' is 'email'`), + }, + { + Config: testAccTFETeamNotificationConfiguration_emailWithToken(org.Name), + ExpectError: regexp.MustCompile(`The attribute 'token' cannot be set when 'destination_type' is 'email'`), + }, + }, + }) +} + +func TestAccTFETeamNotificationConfiguration_validateSchemaAttributesGeneric(t *testing.T) { + tfeClient, err := getClientUsingEnv() + if err != nil { + t.Fatal(err) + } + + org, cleanupOrg := createPlusOrganization(t, tfeClient) + t.Cleanup(cleanupOrg) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { preCheckTFETeamNotificationConfiguration(t) }, + ProtoV5ProviderFactories: testAccMuxedProviders, + Steps: []resource.TestStep{ + { + Config: testAccTFETeamNotificationConfiguration_genericWithEmailAddresses(org.Name), + ExpectError: regexp.MustCompile(`(?s).*The attribute 'email_addresses' cannot be set when 'destination_type' is.*'generic'`), + }, + { + Config: testAccTFETeamNotificationConfiguration_genericWithEmailUserIDs(org.Name), + ExpectError: regexp.MustCompile(`(?s).*The attribute 'email_user_ids' cannot be set when 'destination_type' is.*'generic'`), + }, + { + Config: testAccTFETeamNotificationConfiguration_genericWithoutURL(org.Name), + ExpectError: regexp.MustCompile(`The attribute 'url' is required when 'destination_type' is 'generic'`), + }, + }, + }) +} + +func TestAccTFETeamNotificationConfiguration_validateSchemaAttributesSlack(t *testing.T) { + tfeClient, err := getClientUsingEnv() + if err != nil { + t.Fatal(err) + } + + org, cleanupOrg := createPlusOrganization(t, tfeClient) + t.Cleanup(cleanupOrg) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { preCheckTFETeamNotificationConfiguration(t) }, + ProtoV5ProviderFactories: testAccMuxedProviders, + Steps: []resource.TestStep{ + { + Config: testAccTFETeamNotificationConfiguration_slackWithEmailAddresses(org.Name), + ExpectError: regexp.MustCompile(`(?s).*The attribute 'email_addresses' cannot be set when 'destination_type' is.*'slack'`), + }, + { + Config: testAccTFETeamNotificationConfiguration_slackWithEmailUserIDs(org.Name), + ExpectError: regexp.MustCompile(`(?s).*The attribute 'email_user_ids' cannot be set when 'destination_type' is.*'slack'`), + }, + { + Config: testAccTFETeamNotificationConfiguration_slackWithToken(org.Name), + ExpectError: regexp.MustCompile(`The attribute 'token' cannot be set when 'destination_type' is 'slack'`), + }, + { + Config: testAccTFETeamNotificationConfiguration_slackWithoutURL(org.Name), + ExpectError: regexp.MustCompile(`The attribute 'url' is required when 'destination_type' is 'slack'`), + }, + }, + }) +} + +func TestAccTFETeamNotificationConfiguration_validateSchemaAttributesMicrosoftTeams(t *testing.T) { + tfeClient, err := getClientUsingEnv() + if err != nil { + t.Fatal(err) + } + + org, cleanupOrg := createPlusOrganization(t, tfeClient) + t.Cleanup(cleanupOrg) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { preCheckTFETeamNotificationConfiguration(t) }, + ProtoV5ProviderFactories: testAccMuxedProviders, + Steps: []resource.TestStep{ + { + Config: testAccTFETeamNotificationConfiguration_microsoftTeamsWithEmailAddresses(org.Name), + ExpectError: regexp.MustCompile(`(?s).*The attribute 'email_addresses' cannot be set when 'destination_type' is.*'microsoft-teams'`), + }, + { + Config: testAccTFETeamNotificationConfiguration_microsoftTeamsWithEmailUserIDs(org.Name), + ExpectError: regexp.MustCompile(`(?s).*The attribute 'email_user_ids' cannot be set when 'destination_type' is.*'microsoft-teams'`), + }, + { + Config: testAccTFETeamNotificationConfiguration_microsoftTeamsWithToken(org.Name), + ExpectError: regexp.MustCompile(`(?s).*The attribute 'token' cannot be set when 'destination_type' is.*'microsoft-teams'`), + }, + { + Config: testAccTFETeamNotificationConfiguration_microsoftTeamsWithoutURL(org.Name), + ExpectError: regexp.MustCompile(`The attribute 'url' is required when 'destination_type' is 'microsoft-teams'`), + }, + }, + }) +} + +func TestAccTFETeamNotificationConfiguration_validateSchemaAttributesBadDestinationType(t *testing.T) { + tfeClient, err := getClientUsingEnv() + if err != nil { + t.Fatal(err) + } + + org, cleanupOrg := createPlusOrganization(t, tfeClient) + t.Cleanup(cleanupOrg) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { preCheckTFETeamNotificationConfiguration(t) }, + ProtoV5ProviderFactories: testAccMuxedProviders, + Steps: []resource.TestStep{ + { + Config: testAccTFETeamNotificationConfiguration_badDestinationType(org.Name), + ExpectError: regexp.MustCompile(`.*Invalid Attribute Value Match.*`), + }, + }, + }) +} + +func TestAccTFETeamNotificationConfiguration_updateValidateSchemaAttributesEmail(t *testing.T) { + skipUnlessBeta(t) + tfeClient, err := getClientUsingEnv() + if err != nil { + t.Fatal(err) + } + + org, cleanupOrg := createPlusOrganization(t, tfeClient) + t.Cleanup(cleanupOrg) + + notificationConfiguration := &tfe.NotificationConfiguration{} + + resource.Test(t, resource.TestCase{ + PreCheck: func() { preCheckTFETeamNotificationConfiguration(t) }, + ProtoV5ProviderFactories: testAccMuxedProviders, + CheckDestroy: testAccCheckTFETeamNotificationConfigurationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccTFETeamNotificationConfiguration_emailUserIDs(org.Name), + Check: resource.ComposeTestCheckFunc( + testAccCheckTFETeamNotificationConfigurationExists( + "tfe_team_notification_configuration.foobar", notificationConfiguration), + testAccCheckTFETeamNotificationConfigurationAttributesEmailUserIDs(notificationConfiguration), + resource.TestCheckResourceAttr( + "tfe_team_notification_configuration.foobar", "destination_type", "email"), + resource.TestCheckResourceAttr( + "tfe_team_notification_configuration.foobar", "name", "notification_email"), + // Just test the number of items in triggers + // Values in triggers attribute are tested by testCheckTFETeamNotificationConfigurationAttributesEmailUserIDs + resource.TestCheckResourceAttr( + "tfe_team_notification_configuration.foobar", "triggers.#", "0"), + resource.TestCheckResourceAttr( + "tfe_team_notification_configuration.foobar", "email_user_ids.#", "0"), + ), + }, + { + Config: testAccTFETeamNotificationConfiguration_emailWithURL(org.Name), + ExpectError: regexp.MustCompile(`The attribute 'url' cannot be set when 'destination_type' is 'email'`), + }, + { + Config: testAccTFETeamNotificationConfiguration_emailWithToken(org.Name), + ExpectError: regexp.MustCompile(`The attribute 'token' cannot be set when 'destination_type' is 'email'`), + }, + { + Config: testAccTFETeamNotificationConfiguration_emailUserIDs(org.Name), + }, + }, + }) +} + +func TestAccTFETeamNotificationConfiguration_updateValidateSchemaAttributesGeneric(t *testing.T) { + skipUnlessBeta(t) + tfeClient, err := getClientUsingEnv() + if err != nil { + t.Fatal(err) + } + + org, cleanupOrg := createPlusOrganization(t, tfeClient) + t.Cleanup(cleanupOrg) + + notificationConfiguration := &tfe.NotificationConfiguration{} + + resource.Test(t, resource.TestCase{ + PreCheck: func() { preCheckTFETeamNotificationConfiguration(t) }, + ProtoV5ProviderFactories: testAccMuxedProviders, + CheckDestroy: testAccCheckTFETeamNotificationConfigurationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccTFETeamNotificationConfiguration_basic(org.Name), + Check: resource.ComposeTestCheckFunc( + testAccCheckTFETeamNotificationConfigurationExists( + "tfe_team_notification_configuration.foobar", notificationConfiguration), + testAccCheckTFETeamNotificationConfigurationAttributes(notificationConfiguration), + resource.TestCheckResourceAttr( + "tfe_team_notification_configuration.foobar", "destination_type", "generic"), + resource.TestCheckResourceAttr( + "tfe_team_notification_configuration.foobar", "name", "notification_basic"), + // Just test the number of items in triggers + // Values in triggers attribute are tested by testCheckTFETeamNotificationConfigurationAttributes + resource.TestCheckResourceAttr( + "tfe_team_notification_configuration.foobar", "triggers.#", "0"), + resource.TestCheckResourceAttr( + "tfe_team_notification_configuration.foobar", "url", runTasksURL()), + ), + }, + { + Config: testAccTFETeamNotificationConfiguration_genericWithEmailAddresses(org.Name), + ExpectError: regexp.MustCompile(`(?s).*The attribute 'email_addresses' cannot be set when 'destination_type' is.*'generic'`), + }, + { + Config: testAccTFETeamNotificationConfiguration_genericWithEmailUserIDs(org.Name), + ExpectError: regexp.MustCompile(`(?s).*The attribute 'email_user_ids' cannot be set when 'destination_type' is.*'generic'`), + }, + { + Config: testAccTFETeamNotificationConfiguration_genericWithoutURL(org.Name), + ExpectError: regexp.MustCompile(`The attribute 'url' is required when 'destination_type' is 'generic'`), + }, + { + Config: testAccTFETeamNotificationConfiguration_basic(org.Name), + }, + }, + }) +} + +func TestAccTFETeamNotificationConfiguration_updateValidateSchemaAttributesSlack(t *testing.T) { + skipUnlessBeta(t) + tfeClient, err := getClientUsingEnv() + if err != nil { + t.Fatal(err) + } + + org, cleanupOrg := createPlusOrganization(t, tfeClient) + t.Cleanup(cleanupOrg) + + notificationConfiguration := &tfe.NotificationConfiguration{} + + resource.Test(t, resource.TestCase{ + PreCheck: func() { preCheckTFETeamNotificationConfiguration(t) }, + ProtoV5ProviderFactories: testAccMuxedProviders, + CheckDestroy: testAccCheckTFETeamNotificationConfigurationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccTFETeamNotificationConfiguration_slack(org.Name), + Check: resource.ComposeTestCheckFunc( + testAccCheckTFETeamNotificationConfigurationExists( + "tfe_team_notification_configuration.foobar", notificationConfiguration), + testAccCheckTFETeamNotificationConfigurationAttributesSlack(notificationConfiguration), + resource.TestCheckResourceAttr( + "tfe_team_notification_configuration.foobar", "destination_type", "slack"), + resource.TestCheckResourceAttr( + "tfe_team_notification_configuration.foobar", "name", "notification_slack"), + // Just test the number of items in triggers + // Values in triggers attribute are tested by testCheckTFETeamNotificationConfigurationAttributes + resource.TestCheckResourceAttr( + "tfe_team_notification_configuration.foobar", "triggers.#", "0"), + resource.TestCheckResourceAttr( + "tfe_team_notification_configuration.foobar", "url", runTasksURL()), + ), + }, + { + Config: testAccTFETeamNotificationConfiguration_slackWithEmailAddresses(org.Name), + ExpectError: regexp.MustCompile(`(?s).*The attribute 'email_addresses' cannot be set when 'destination_type' is.*'slack'`), + }, + { + Config: testAccTFETeamNotificationConfiguration_slackWithEmailUserIDs(org.Name), + ExpectError: regexp.MustCompile(`(?s).*The attribute 'email_user_ids' cannot be set when 'destination_type' is.*'slack'`), + }, + { + Config: testAccTFETeamNotificationConfiguration_slackWithToken(org.Name), + ExpectError: regexp.MustCompile(`The attribute 'token' cannot be set when 'destination_type' is 'slack'`), + }, + { + Config: testAccTFETeamNotificationConfiguration_slackWithoutURL(org.Name), + ExpectError: regexp.MustCompile(`The attribute 'url' is required when 'destination_type' is 'slack'`), + }, + { + Config: testAccTFETeamNotificationConfiguration_slack(org.Name), + }, + }, + }) +} + +func TestAccTFETeamNotificationConfiguration_updateValidateSchemaAttributesMicrosoftTeams(t *testing.T) { + skipUnlessBeta(t) + tfeClient, err := getClientUsingEnv() + if err != nil { + t.Fatal(err) + } + + org, cleanupOrg := createPlusOrganization(t, tfeClient) + t.Cleanup(cleanupOrg) + + notificationConfiguration := &tfe.NotificationConfiguration{} + + resource.Test(t, resource.TestCase{ + PreCheck: func() { preCheckTFETeamNotificationConfiguration(t) }, + ProtoV5ProviderFactories: testAccMuxedProviders, + CheckDestroy: testAccCheckTFETeamNotificationConfigurationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccTFETeamNotificationConfiguration_microsoftTeams(org.Name), + Check: resource.ComposeTestCheckFunc( + testAccCheckTFETeamNotificationConfigurationExists( + "tfe_team_notification_configuration.foobar", notificationConfiguration), + testAccCheckTFETeamNotificationConfigurationAttributesMicrosoftTeams(notificationConfiguration), + resource.TestCheckResourceAttr( + "tfe_team_notification_configuration.foobar", "destination_type", "microsoft-teams"), + resource.TestCheckResourceAttr( + "tfe_team_notification_configuration.foobar", "name", "notification_msteams"), + resource.TestCheckResourceAttr( + "tfe_team_notification_configuration.foobar", "url", runTasksURL()), + ), + }, + { + Config: testAccTFETeamNotificationConfiguration_microsoftTeamsWithEmailAddresses(org.Name), + ExpectError: regexp.MustCompile(`(?s).*The attribute 'email_addresses' cannot be set when 'destination_type' is.*'microsoft-teams'`), + }, + { + Config: testAccTFETeamNotificationConfiguration_microsoftTeamsWithEmailUserIDs(org.Name), + ExpectError: regexp.MustCompile(`(?s).*The attribute 'email_user_ids' cannot be set when 'destination_type' is.*'microsoft-teams'`), + }, + { + Config: testAccTFETeamNotificationConfiguration_microsoftTeamsWithToken(org.Name), + ExpectError: regexp.MustCompile(`(?s).*The attribute 'token' cannot be set when 'destination_type' is.*'microsoft-teams'`), + }, + { + Config: testAccTFETeamNotificationConfiguration_microsoftTeamsWithoutURL(org.Name), + ExpectError: regexp.MustCompile(`The attribute 'url' is required when 'destination_type' is 'microsoft-teams'`), + }, + { + Config: testAccTFETeamNotificationConfiguration_microsoftTeams(org.Name), + }, + }, + }) +} + +func TestAccTFETeamNotificationConfiguration_duplicateTriggers(t *testing.T) { + skipUnlessBeta(t) + tfeClient, err := getClientUsingEnv() + if err != nil { + t.Fatal(err) + } + + org, cleanupOrg := createPlusOrganization(t, tfeClient) + t.Cleanup(cleanupOrg) + + notificationConfiguration := &tfe.NotificationConfiguration{} + + resource.Test(t, resource.TestCase{ + PreCheck: func() { preCheckTFETeamNotificationConfiguration(t) }, + ProtoV5ProviderFactories: testAccMuxedProviders, + CheckDestroy: testAccCheckTFETeamNotificationConfigurationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccTFETeamNotificationConfiguration_duplicateTriggers(org.Name), + Check: resource.ComposeTestCheckFunc( + testAccCheckTFETeamNotificationConfigurationExists( + "tfe_team_notification_configuration.foobar", notificationConfiguration), + testAccCheckTFETeamNotificationConfigurationAttributesDuplicateTriggers(notificationConfiguration), + resource.TestCheckResourceAttr( + "tfe_team_notification_configuration.foobar", "destination_type", "generic"), + resource.TestCheckResourceAttr( + "tfe_team_notification_configuration.foobar", "name", "notification_duplicate_triggers"), + // Just test the number of items in triggers + // Values in triggers attribute are tested by testCheckTFETeamNotificationConfigurationAttributes + resource.TestCheckResourceAttr( + "tfe_team_notification_configuration.foobar", "triggers.#", "1"), + resource.TestCheckResourceAttr( + "tfe_team_notification_configuration.foobar", "url", runTasksURL()), + ), + }, + }, + }) +} + +func TestAccTFETeamNotificationConfigurationImport_basic(t *testing.T) { + skipUnlessBeta(t) + tfeClient, err := getClientUsingEnv() + if err != nil { + t.Fatal(err) + } + + org, cleanupOrg := createPlusOrganization(t, tfeClient) + t.Cleanup(cleanupOrg) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { preCheckTFETeamNotificationConfiguration(t) }, + ProtoV5ProviderFactories: testAccMuxedProviders, + CheckDestroy: testAccCheckTFETeamNotificationConfigurationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccTFETeamNotificationConfiguration_update(org.Name), + }, + + { + ResourceName: "tfe_team_notification_configuration.foobar", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"token"}, + }, + }, + }) +} + +func TestAccTFETeamNotificationConfigurationImport_emailUserIDs(t *testing.T) { + skipUnlessBeta(t) + tfeClient, err := getClientUsingEnv() + if err != nil { + t.Fatal(err) + } + + org, cleanupOrg := createPlusOrganization(t, tfeClient) + t.Cleanup(cleanupOrg) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { preCheckTFETeamNotificationConfiguration(t) }, + ProtoV5ProviderFactories: testAccMuxedProviders, + CheckDestroy: testAccCheckTFETeamNotificationConfigurationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccTFETeamNotificationConfiguration_updateEmailUserIDs(org.Name), + }, + + { + ResourceName: "tfe_team_notification_configuration.foobar", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"token"}, + }, + }, + }) +} + +func TestAccTFETeamNotificationConfigurationImport_emptyEmailUserIDs(t *testing.T) { + skipUnlessBeta(t) + tfeClient, err := getClientUsingEnv() + if err != nil { + t.Fatal(err) + } + + org, cleanupOrg := createPlusOrganization(t, tfeClient) + t.Cleanup(cleanupOrg) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { preCheckTFETeamNotificationConfiguration(t) }, + ProtoV5ProviderFactories: testAccMuxedProviders, + CheckDestroy: testAccCheckTFETeamNotificationConfigurationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccTFETeamNotificationConfiguration_emailUserIDs(org.Name), + }, + + { + ResourceName: "tfe_team_notification_configuration.foobar", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"token"}, + }, + }, + }) +} + +func testAccCheckTFETeamNotificationConfigurationExists(n string, notificationConfiguration *tfe.NotificationConfiguration) resource.TestCheckFunc { + return func(s *terraform.State) error { + config := testAccProvider.Meta().(ConfiguredClient) + + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No instance ID is set") + } + + nc, err := config.Client.NotificationConfigurations.Read(ctx, rs.Primary.ID) + if err != nil { + return err + } + + *notificationConfiguration = *nc + + return nil + } +} + +func testAccCheckTFETeamNotificationConfigurationAttributes(notificationConfiguration *tfe.NotificationConfiguration) resource.TestCheckFunc { + return func(s *terraform.State) error { + if notificationConfiguration.Name != "notification_basic" { + return fmt.Errorf("Bad name: %s", notificationConfiguration.Name) + } + + if notificationConfiguration.DestinationType != tfe.NotificationDestinationTypeGeneric { + return fmt.Errorf("Bad destination type: %s", notificationConfiguration.DestinationType) + } + + if notificationConfiguration.Enabled != false { + return fmt.Errorf("Bad enabled value: %t", notificationConfiguration.Enabled) + } + + // Token is write only, can't read it + + if !reflect.DeepEqual(notificationConfiguration.Triggers, []string{}) { + return fmt.Errorf("Bad triggers: %v", notificationConfiguration.Triggers) + } + + if notificationConfiguration.URL != runTasksURL() { + return fmt.Errorf("Bad URL: %s", notificationConfiguration.URL) + } + + return nil + } +} + +func testAccCheckTFETeamNotificationConfigurationAttributesUpdate(notificationConfiguration *tfe.NotificationConfiguration) resource.TestCheckFunc { + return func(s *terraform.State) error { + if notificationConfiguration.Name != "notification_update" { + return fmt.Errorf("Bad name: %s", notificationConfiguration.Name) + } + + if notificationConfiguration.DestinationType != tfe.NotificationDestinationTypeGeneric { + return fmt.Errorf("Bad destination type: %s", notificationConfiguration.DestinationType) + } + + if notificationConfiguration.Enabled != true { + return fmt.Errorf("Bad enabled value: %t", notificationConfiguration.Enabled) + } + + // Token is write only, can't read it + + if !reflect.DeepEqual(notificationConfiguration.Triggers, []string{string(tfe.NotificationTriggerChangeRequestCreated)}) { + return fmt.Errorf("Bad triggers: %v", notificationConfiguration.Triggers) + } + + if notificationConfiguration.URL != fmt.Sprintf("%s?update=true", runTasksURL()) { + return fmt.Errorf("Bad URL: %s", notificationConfiguration.URL) + } + + return nil + } +} + +func testAccCheckTFETeamNotificationConfigurationAttributesEmailUserIDs(notificationConfiguration *tfe.NotificationConfiguration) resource.TestCheckFunc { + return func(s *terraform.State) error { + if notificationConfiguration.Name != "notification_email" { + return fmt.Errorf("Bad name: %s", notificationConfiguration.Name) + } + + if notificationConfiguration.DestinationType != tfe.NotificationDestinationTypeEmail { + return fmt.Errorf("Bad destination type: %s", notificationConfiguration.DestinationType) + } + + if notificationConfiguration.Enabled != false { + return fmt.Errorf("Bad enabled value: %t", notificationConfiguration.Enabled) + } + + // Token is write only, can't read it + + if !reflect.DeepEqual(notificationConfiguration.Triggers, []string{}) { + return fmt.Errorf("Bad triggers: %v", notificationConfiguration.Triggers) + } + + if len(notificationConfiguration.EmailUsers) != 0 { + return fmt.Errorf("Wrong number of email users: %v", len(notificationConfiguration.EmailUsers)) + } + + return nil + } +} + +func testAccCheckTFETeamNotificationConfigurationAttributesUpdateEmailUserIDs(notificationConfiguration *tfe.NotificationConfiguration) resource.TestCheckFunc { + return func(s *terraform.State) error { + if notificationConfiguration.Name != "notification_email_update" { + return fmt.Errorf("Bad name: %s", notificationConfiguration.Name) + } + + if notificationConfiguration.DestinationType != tfe.NotificationDestinationTypeEmail { + return fmt.Errorf("Bad destination type: %s", notificationConfiguration.DestinationType) + } + + if notificationConfiguration.Enabled != true { + return fmt.Errorf("Bad enabled value: %t", notificationConfiguration.Enabled) + } + + // Token is write only, can't read it + + if !reflect.DeepEqual(notificationConfiguration.Triggers, []string{string(tfe.NotificationTriggerChangeRequestCreated)}) { + return fmt.Errorf("Bad triggers: %v", notificationConfiguration.Triggers) + } + + if len(notificationConfiguration.EmailUsers) != 1 { + return fmt.Errorf("Wrong number of email users: %v", len(notificationConfiguration.EmailUsers)) + } + + return nil + } +} + +func testAccCheckTFETeamNotificationConfigurationAttributesSlack(notificationConfiguration *tfe.NotificationConfiguration) resource.TestCheckFunc { + return func(s *terraform.State) error { + if notificationConfiguration.Name != "notification_slack" { + return fmt.Errorf("Bad name: %s", notificationConfiguration.Name) + } + + if notificationConfiguration.DestinationType != tfe.NotificationDestinationTypeSlack { + return fmt.Errorf("Bad destination type: %s", notificationConfiguration.DestinationType) + } + + if notificationConfiguration.Enabled != false { + return fmt.Errorf("Bad enabled value: %t", notificationConfiguration.Enabled) + } + + // Token is write only, can't read it + + if !reflect.DeepEqual(notificationConfiguration.Triggers, []string{}) { + return fmt.Errorf("Bad triggers: %v", notificationConfiguration.Triggers) + } + + return nil + } +} + +func testAccCheckTFETeamNotificationConfigurationAttributesMicrosoftTeams(notificationConfiguration *tfe.NotificationConfiguration) resource.TestCheckFunc { + return func(s *terraform.State) error { + if notificationConfiguration.Name != "notification_msteams" { + return fmt.Errorf("Bad name: %s", notificationConfiguration.Name) + } + + if notificationConfiguration.DestinationType != tfe.NotificationDestinationTypeMicrosoftTeams { + return fmt.Errorf("Bad destination type: %s", notificationConfiguration.DestinationType) + } + + if notificationConfiguration.Enabled != false { + return fmt.Errorf("Bad enabled value: %t", notificationConfiguration.Enabled) + } + + if !reflect.DeepEqual(notificationConfiguration.Triggers, []string{}) { + return fmt.Errorf("Bad triggers: %v", notificationConfiguration.Triggers) + } + + if notificationConfiguration.URL != runTasksURL() { + return fmt.Errorf("Bad URL: %s", notificationConfiguration.URL) + } + + return nil + } +} + +func testAccCheckTFETeamNotificationConfigurationAttributesDuplicateTriggers(notificationConfiguration *tfe.NotificationConfiguration) resource.TestCheckFunc { + return func(s *terraform.State) error { + if notificationConfiguration.Name != "notification_duplicate_triggers" { + return fmt.Errorf("Bad name: %s", notificationConfiguration.Name) + } + + if notificationConfiguration.DestinationType != tfe.NotificationDestinationTypeGeneric { + return fmt.Errorf("Bad destination type: %s", notificationConfiguration.DestinationType) + } + + if notificationConfiguration.Enabled != false { + return fmt.Errorf("Bad enabled value: %t", notificationConfiguration.Enabled) + } + + // Token is write only, can't read it + + if !reflect.DeepEqual(notificationConfiguration.Triggers, []string{string(tfe.NotificationTriggerChangeRequestCreated)}) { + return fmt.Errorf("Bad triggers: %v", notificationConfiguration.Triggers) + } + + if notificationConfiguration.URL != runTasksURL() { + return fmt.Errorf("Bad URL: %s", notificationConfiguration.URL) + } + + return nil + } +} + +func testAccCheckTFETeamNotificationConfigurationDestroy(s *terraform.State) error { + config := testAccProvider.Meta().(ConfiguredClient) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "tfe_team_notification_configuration" { + continue + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No instance ID is set") + } + + _, err := config.Client.NotificationConfigurations.Read(ctx, rs.Primary.ID) + if err == nil { + return fmt.Errorf("Notification configuration %s still exists", rs.Primary.ID) + } + } + + return nil +} + +func testAccTFETeamNotificationConfiguration_basic(orgName string) string { + return fmt.Sprintf(` +data "tfe_organization" "foobar" { + name = "%s" +} + +resource "tfe_team" "foobar" { + name = "team-test" + organization = data.tfe_organization.foobar.name +} + +resource "tfe_team_notification_configuration" "foobar" { + name = "notification_basic" + destination_type = "generic" + url = "%s" + team_id = tfe_team.foobar.id +}`, orgName, runTasksURL()) +} + +func testAccTFETeamNotificationConfiguration_emailUserIDs(orgName string) string { + return fmt.Sprintf(` +data "tfe_organization" "foobar" { + name = "%s" +} + +resource "tfe_team" "foobar" { + name = "team-test" + organization = data.tfe_organization.foobar.name +} + +resource "tfe_organization_membership" "foobar" { + organization = data.tfe_organization.foobar.name + email = "foo@foobar.com" +} + +resource "tfe_team_notification_configuration" "foobar" { + name = "notification_email" + destination_type = "email" + team_id = tfe_team.foobar.id +}`, orgName) +} + +func testAccTFETeamNotificationConfiguration_slack(orgName string) string { + return fmt.Sprintf(` +data "tfe_organization" "foobar" { + name = "%s" +} + +resource "tfe_team" "foobar" { + name = "team-test" + organization = data.tfe_organization.foobar.name +} + +resource "tfe_team_notification_configuration" "foobar" { + name = "notification_slack" + destination_type = "slack" + url = "%s" + team_id = tfe_team.foobar.id +}`, orgName, runTasksURL()) +} + +func testAccTFETeamNotificationConfiguration_microsoftTeams(orgName string) string { + return fmt.Sprintf(` +data "tfe_organization" "foobar" { + name = "%s" +} + +resource "tfe_team" "foobar" { + name = "team-test" + organization = data.tfe_organization.foobar.name +} + +resource "tfe_team_notification_configuration" "foobar" { + name = "notification_msteams" + destination_type = "microsoft-teams" + url = "%s" + team_id = tfe_team.foobar.id +}`, orgName, runTasksURL()) +} + +func testAccTFETeamNotificationConfiguration_badDestinationType(orgName string) string { + return fmt.Sprintf(` +data "tfe_organization" "foobar" { + name = "%s" +} + +resource "tfe_team" "foobar" { + name = "team-test" + organization = data.tfe_organization.foobar.name +} + +resource "tfe_team_notification_configuration" "foobar" { + name = "notification_basic" + destination_type = "bad_type" + url = "%s" + team_id = tfe_team.foobar.id +}`, orgName, runTasksURL()) +} + +func testAccTFETeamNotificationConfiguration_update(orgName string) string { + return fmt.Sprintf(` +data "tfe_organization" "foobar" { + name = "%s" +} + +resource "tfe_team" "foobar" { + name = "team-test" + organization = data.tfe_organization.foobar.name +} + +resource "tfe_team_notification_configuration" "foobar" { + name = "notification_update" + destination_type = "generic" + enabled = true + token = "1234567890_update" + triggers = ["change_request:created"] + url = "%s?update=true" + team_id = tfe_team.foobar.id +}`, orgName, runTasksURL()) +} + +func testAccTFETeamNotificationConfiguration_updateEmailUserIDs(orgName string) string { + return fmt.Sprintf(`data "tfe_organization" "foobar" { + name = "%s" +} + +resource "tfe_team" "foobar" { + name = "team-test" + organization = data.tfe_organization.foobar.name +} + +resource "tfe_organization_membership" "foobar" { + organization = data.tfe_organization.foobar.name + email = "foo@foobar.com" +} + +resource "tfe_team_organization_member" "foobar" { + team_id = tfe_team.foobar.id + organization_membership_id = tfe_organization_membership.foobar.id +} + +resource "tfe_team_notification_configuration" "foobar" { + name = "notification_email_update" + destination_type = "email" + email_user_ids = [tfe_organization_membership.foobar.user_id] + enabled = true + triggers = ["change_request:created"] + team_id = tfe_team.foobar.id + + depends_on = [tfe_team_organization_member.foobar] +}`, orgName) +} + +func testAccTFETeamNotificationConfiguration_emailWithURL(orgName string) string { + return fmt.Sprintf(` +data "tfe_organization" "foobar" { + name = "%s" +} + +resource "tfe_team" "foobar" { + name = "team-test" + organization = data.tfe_organization.foobar.name +} + +resource "tfe_team_notification_configuration" "foobar" { + name = "notification_email_with_url" + destination_type = "email" + url = "%s" + team_id = tfe_team.foobar.id +}`, orgName, runTasksURL()) +} + +func testAccTFETeamNotificationConfiguration_emailWithToken(orgName string) string { + return fmt.Sprintf(` +data "tfe_organization" "foobar" { + name = "%s" +} + +resource "tfe_team" "foobar" { + name = "team-test" + organization = data.tfe_organization.foobar.name +} + +resource "tfe_team_notification_configuration" "foobar" { + name = "notification_email_with_token" + destination_type = "email" + token = "1234567890" + team_id = tfe_team.foobar.id +}`, orgName) +} + +func testAccTFETeamNotificationConfiguration_genericWithEmailAddresses(orgName string) string { + return fmt.Sprintf(` +data "tfe_organization" "foobar" { + name = "%s" +} + +resource "tfe_team" "foobar" { + name = "team-test" + organization = data.tfe_organization.foobar.name +} + +resource "tfe_team_notification_configuration" "foobar" { + name = "notification_generic_with_email_addresses" + destination_type = "generic" + email_addresses = ["test@example.com", "test2@example.com"] + team_id = tfe_team.foobar.id +}`, orgName) +} + +func testAccTFETeamNotificationConfiguration_genericWithEmailUserIDs(orgName string) string { + return fmt.Sprintf(` +data "tfe_organization" "foobar" { + name = "%s" +} + +resource "tfe_team" "foobar" { + name = "team-test" + organization = data.tfe_organization.foobar.name +} + +resource "tfe_organization_membership" "foobar" { + organization = data.tfe_organization.foobar.name + email = "foo@foobar.com" +} + +resource "tfe_team_notification_configuration" "foobar" { + name = "notification_generic_with_email_user_ids" + destination_type = "generic" + email_user_ids = [tfe_organization_membership.foobar.id] + team_id = tfe_team.foobar.id +}`, orgName) +} + +func testAccTFETeamNotificationConfiguration_genericWithoutURL(orgName string) string { + return fmt.Sprintf(` +data "tfe_organization" "foobar" { + name = "%s" +} + +resource "tfe_team" "foobar" { + name = "team-test" + organization = data.tfe_organization.foobar.name +} + +resource "tfe_team_notification_configuration" "foobar" { + name = "notification_generic_without_url" + destination_type = "generic" + team_id = tfe_team.foobar.id +}`, orgName) +} + +func testAccTFETeamNotificationConfiguration_slackWithEmailAddresses(orgName string) string { + return fmt.Sprintf(` +data "tfe_organization" "foobar" { + name = "%s" +} + +resource "tfe_team" "foobar" { + name = "team-test" + organization = data.tfe_organization.foobar.name +} + +resource "tfe_team_notification_configuration" "foobar" { + name = "notification_slack_with_email_addresses" + destination_type = "slack" + email_addresses = ["test@example.com", "test2@example.com"] + team_id = tfe_team.foobar.id +}`, orgName) +} + +func testAccTFETeamNotificationConfiguration_slackWithEmailUserIDs(orgName string) string { + return fmt.Sprintf(` +data "tfe_organization" "foobar" { + name = "%s" +} + +resource "tfe_team" "foobar" { + name = "team-test" + organization = data.tfe_organization.foobar.name +} + +resource "tfe_organization_membership" "foobar" { + organization = data.tfe_organization.foobar.name + email = "foo@foobar.com" +} + +resource "tfe_team_notification_configuration" "foobar" { + name = "notification_slack_with_email_user_ids" + destination_type = "slack" + email_user_ids = [tfe_organization_membership.foobar.id] + team_id = tfe_team.foobar.id +}`, orgName) +} + +func testAccTFETeamNotificationConfiguration_slackWithToken(orgName string) string { + return fmt.Sprintf(` +data "tfe_organization" "foobar" { + name = "%s" +} + +resource "tfe_team" "foobar" { + name = "team-test" + organization = data.tfe_organization.foobar.name +} + +resource "tfe_team_notification_configuration" "foobar" { + name = "notification_slack_with_token" + destination_type = "slack" + token = "1234567890" + url = "%s" + team_id = tfe_team.foobar.id +}`, orgName, runTasksURL()) +} + +func testAccTFETeamNotificationConfiguration_slackWithoutURL(orgName string) string { + return fmt.Sprintf(` +data "tfe_organization" "foobar" { + name = "%s" +} + +resource "tfe_team" "foobar" { + name = "team-test" + organization = data.tfe_organization.foobar.name +} + +resource "tfe_team_notification_configuration" "foobar" { + name = "notification_slack_without_url" + destination_type = "slack" + team_id = tfe_team.foobar.id +}`, orgName) +} + +func testAccTFETeamNotificationConfiguration_microsoftTeamsWithEmailAddresses(orgName string) string { + return fmt.Sprintf(` +data "tfe_organization" "foobar" { + name = "%s" +} + +resource "tfe_team" "foobar" { + name = "team-test" + organization = data.tfe_organization.foobar.name +} + +resource "tfe_team_notification_configuration" "foobar" { + name = "notification_msteams_with_email_addresses" + destination_type = "microsoft-teams" + email_addresses = ["test@example.com", "test2@example.com"] + team_id = tfe_team.foobar.id +}`, orgName) +} + +func testAccTFETeamNotificationConfiguration_microsoftTeamsWithEmailUserIDs(orgName string) string { + return fmt.Sprintf(` +data "tfe_organization" "foobar" { + name = "%s" +} + +resource "tfe_team" "foobar" { + name = "team-test" + organization = data.tfe_organization.foobar.name +} + +resource "tfe_organization_membership" "foobar" { + organization = data.tfe_organization.foobar.name + email = "foo@foobar.com" +} + +resource "tfe_team_notification_configuration" "foobar" { + name = "notification_msteams_with_email_user_ids" + destination_type = "microsoft-teams" + email_user_ids = [tfe_organization_membership.foobar.id] + team_id = tfe_team.foobar.id +}`, orgName) +} + +func testAccTFETeamNotificationConfiguration_microsoftTeamsWithToken(orgName string) string { + return fmt.Sprintf(` +data "tfe_organization" "foobar" { + name = "%s" +} + +resource "tfe_team" "foobar" { + name = "team-test" + organization = data.tfe_organization.foobar.name +} + +resource "tfe_team_notification_configuration" "foobar" { + name = "notification_msteams_with_token" + destination_type = "microsoft-teams" + token = "1234567890" + url = "%s" + team_id = tfe_team.foobar.id +}`, orgName, runTasksURL()) +} + +func testAccTFETeamNotificationConfiguration_microsoftTeamsWithoutURL(orgName string) string { + return fmt.Sprintf(` +data "tfe_organization" "foobar" { + name = "%s" +} + +resource "tfe_team" "foobar" { + name = "team-test" + organization = data.tfe_organization.foobar.name +} + +resource "tfe_team_notification_configuration" "foobar" { + name = "notification_msteams_without_url" + destination_type = "microsoft-teams" + team_id = tfe_team.foobar.id +}`, orgName) +} + +func testAccTFETeamNotificationConfiguration_duplicateTriggers(orgName string) string { + return fmt.Sprintf(` +data "tfe_organization" "foobar" { + name = "%s" +} + +resource "tfe_team" "foobar" { + name = "team-test" + organization = data.tfe_organization.foobar.name +} + +resource "tfe_team_notification_configuration" "foobar" { + name = "notification_duplicate_triggers" + destination_type = "generic" + triggers = ["change_request:created", "change_request:created", "change_request:created"] + url = "%s" + team_id = tfe_team.foobar.id +}`, orgName, runTasksURL()) +} + +func preCheckTFETeamNotificationConfiguration(t *testing.T) { + testAccPreCheck(t) + + if runTasksURL() == "" { + t.Skip("RUN_TASKS_URL must be set for team notification configuration acceptance tests") + } +} diff --git a/internal/provider/subscription_updater_test.go b/internal/provider/subscription_updater_test.go index 50813447e..c3d7d7645 100644 --- a/internal/provider/subscription_updater_test.go +++ b/internal/provider/subscription_updater_test.go @@ -27,11 +27,12 @@ type featureSetListOptions struct { } type updateFeatureSetOptions struct { - Type string `jsonapi:"primary,subscription"` - RunsCeiling *int `jsonapi:"attr,runs-ceiling,omitempty"` - ContractStartAt *time.Time `jsonapi:"attr,contract-start-at,iso8601,omitempty"` - ContractUserLimit *int `jsonapi:"attr,contract-user-limit,omitempty"` - ContractApplyLimit *int `jsonapi:"attr,contract-apply-limit,omitempty"` + Type string `jsonapi:"primary,subscription"` + RunsCeiling *int `jsonapi:"attr,runs-ceiling,omitempty"` + ContractStartAt *time.Time `jsonapi:"attr,contract-start-at,iso8601,omitempty"` + ContractUserLimit *int `jsonapi:"attr,contract-user-limit,omitempty"` + ContractApplyLimit *int `jsonapi:"attr,contract-apply-limit,omitempty"` + ContractManagedResourcesLimit *int `jsonapi:"attr,contract-managed-resources-limit,omitempty"` FeatureSet *featureSet `jsonapi:"relation,feature-set"` } @@ -49,6 +50,19 @@ func newSubscriptionUpdater(organization *tfe.Organization) *organizationSubscri } } +func (b *organizationSubscriptionUpdater) WithPlusEntitlementPlan() *organizationSubscriptionUpdater { + b.planName = "Plus (entitlement)" + + start := time.Now() + ceiling := 1 + managedResourcesLimit := 1000 + + b.updateOpts.ContractStartAt = &start + b.updateOpts.RunsCeiling = &ceiling + b.updateOpts.ContractManagedResourcesLimit = &managedResourcesLimit + return b +} + func (b *organizationSubscriptionUpdater) WithBusinessPlan() *organizationSubscriptionUpdater { b.planName = "Business" diff --git a/internal/provider/validators/attribute_required_if_value_string.go b/internal/provider/validators/attribute_required_if_value_string.go new file mode 100644 index 000000000..6ffb09528 --- /dev/null +++ b/internal/provider/validators/attribute_required_if_value_string.go @@ -0,0 +1,53 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package validators + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type attributeRequiredIfValueStringValidator struct { + attributeName string + requiredValues []string +} + +func (v attributeRequiredIfValueStringValidator) Description(ctx context.Context) string { + return fmt.Sprintf("Ensures the attribute is required if '%s' is one of %v", v.attributeName, v.requiredValues) +} + +func (v attributeRequiredIfValueStringValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +func (v attributeRequiredIfValueStringValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { + var attributeValue types.String + diags := req.Config.GetAttribute(ctx, path.Root(v.attributeName), &attributeValue) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + for _, requiredValue := range v.requiredValues { + if attributeValue.ValueString() == requiredValue && (req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown()) { + resp.Diagnostics.AddAttributeError( + req.Path, + "Missing Required Attribute", + fmt.Sprintf("The attribute '%s' is required when '%s' is '%s'", req.Path, v.attributeName, requiredValue), + ) + return + } + } +} + +func AttributeRequiredIfValueString(attributeName string, requiredValues []string) validator.String { + return attributeRequiredIfValueStringValidator{ + attributeName: attributeName, + requiredValues: requiredValues, + } +} diff --git a/internal/provider/validators/attribute_value_conflict.go b/internal/provider/validators/attribute_value_conflict.go new file mode 100644 index 000000000..a2644ad88 --- /dev/null +++ b/internal/provider/validators/attribute_value_conflict.go @@ -0,0 +1,77 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package validators + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type attributeValueConflictValidator struct { + attributeName string + conflictingValues []string +} + +func (v attributeValueConflictValidator) Description(ctx context.Context) string { + return fmt.Sprintf("Ensures the attribute is not set if %s is one of %v", v.attributeName, v.conflictingValues) +} + +func (v attributeValueConflictValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +func (v attributeValueConflictValidator) ValidateSet(ctx context.Context, req validator.SetRequest, resp *validator.SetResponse) { + if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { + return + } + + var attributeValue types.String + diags := req.Config.GetAttribute(ctx, path.Root(v.attributeName), &attributeValue) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + for _, conflictingValue := range v.conflictingValues { + if attributeValue.ValueString() == conflictingValue { + resp.Diagnostics.AddError( + "Invalid Attribute Value", + fmt.Sprintf("The attribute '%s' cannot be set when '%s' is '%s'", req.Path, v.attributeName, conflictingValue), + ) + + return + } + } +} + +func (v attributeValueConflictValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { + if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { + return + } + + var attributeValue types.String + diags := req.Config.GetAttribute(ctx, path.Root(v.attributeName), &attributeValue) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + for _, conflictingValue := range v.conflictingValues { + if attributeValue.ValueString() == conflictingValue { + resp.Diagnostics.AddError( + "Invalid Attribute Value", + fmt.Sprintf("The attribute '%s' cannot be set when '%s' is '%s'", req.Path, v.attributeName, conflictingValue), + ) + return + } + } +} + +func AttributeValueConflictValidator(attributeName string, conflictingValues []string) attributeValueConflictValidator { + return attributeValueConflictValidator{attributeName: attributeName, conflictingValues: conflictingValues} +} diff --git a/website/docs/r/team_notification_configuration.html.markdown b/website/docs/r/team_notification_configuration.html.markdown new file mode 100644 index 000000000..9231cffb7 --- /dev/null +++ b/website/docs/r/team_notification_configuration.html.markdown @@ -0,0 +1,142 @@ +--- +layout: "tfe" +page_title: "Terraform Enterprise: tfe_team_notification_configuration" +description: |- + Manages team notifications configurations. +--- + +# tfe_team_notification_configuration + +HCP Terraform can be configured to send notifications to a team for certain events. +Team notification configurations allow you to specify a URL, destination type, and what events will trigger the notification. +Each team can have up to 20 notification configurations, and they apply to configured events for all workspaces that the configured team has access to. + +## Example Usage + +Basic usage: + +```hcl +resource "tfe_organization" "test" { + name = "my-org-name" + email = "admin@company.com" +} + +resource "tfe_team" "test" { + name = "my-team-name" + organization = tfe_organization.test.id +} + +resource "tfe_team_notification_configuration" "test" { + name = "my-test-notification-configuration" + enabled = true + destination_type = "generic" + triggers = ["change_request:created"] + url = "https://example.com" + team_id = tfe_team.test.id +} +``` + +With `destination_type` of `email`: + +```hcl +resource "tfe_organization" "test" { + name = "my-org-name" + email = "admin@company.com" +} + +resource "tfe_team" "test" { + name = "my-team-name" + organization = tfe_organization.test.id +} + +data "tfe_organization_membership" "test" { + organization = tfe_organization.test.name + email = "example@example.com" +} + +resource "tfe_team_organization_member" "test" { + team_id = tfe_team.test.id + organization_membership_id = data.tfe_organization_membership.test.id +} + +resource "tfe_team_notification_configuration" "test" { + name = "my-test-email-notification-configuration" + enabled = true + destination_type = "email" + email_user_ids = [tfe_organization_membership.test.user_id] + triggers = ["change_request:created"] + team_id = tfe_team.test.id +} +``` + +(**TFE only**) With `destination_type` of `email`, using `email_addresses` list and `email_users`: + +```hcl +resource "tfe_organization" "test" { + name = "my-org-name" + email = "admin@company.com" +} + +resource "tfe_team" "test" { + name = "my-team-name" + organization = tfe_organization.test.id +} + +data "tfe_organization_membership" "test" { + organization = tfe_organization.test.name + email = "example@example.com" +} + +resource "tfe_team_organization_member" "test" { + team_id = tfe_team.test.id + organization_membership_id = data.tfe_organization_membership.test.id +} + +resource "tfe_team_notification_configuration" "test" { + name = "my-test-email-notification-configuration" + enabled = true + destination_type = "email" + email_user_ids = [tfe_organization_membership.test.user_id] + email_addresses = ["user1@company.com", "user2@company.com", "user3@company.com"] + triggers = ["change_request:created"] + team_id = tfe_team.test.id +} +``` + +## Argument Reference + +The following arguments are supported: + +- `name` - (Required) Name of the notification configuration. +- `destination_type` - (Required) The type of notification configuration payload to send. + Valid values are: + - `generic` + - `email` available in HCP Terraform or Terraform Enterprise v202005-1 or later + - `slack` + - `microsoft-teams` available in HCP Terraform or Terraform Enterprise v202206-1 or later +- `team_id` - (Required) The ID of the team that owns the notification configuration. +- `url` - (Required if `destination_type` is `generic`, `microsoft-teams`, or `slack`) The HTTP or HTTPS URL of the notification configuration where notification requests will be made. This value _must not_ be provided if `destination_type` is `email`. +- `email_addresses` - (Optional) **TFE only** A list of email addresses. This value + _must not_ be provided if `destination_type` is `generic`, `microsoft-teams`, or `slack`. +- `email_user_ids` - (Optional) A list of user IDs. This value _must not_ be provided + if `destination_type` is `generic`, `microsoft-teams`, or `slack`. +- `enabled` - (Optional) Whether the notification configuration should be enabled or not. + Disabled configurations will not send any notifications. Defaults to `false`. +- `token` - (Optional) A write-only secure token for the notification configuration, which can + be used by the receiving server to verify request authenticity when configured for notification + configurations with a destination type of `generic`. Defaults to `null`. + This value _must not_ be provided if `destination_type` is `email`, `microsoft-teams`, or `slack`. +- `triggers` - (Optional) The array of triggers for which this notification configuration will + send notifications. Currently, the only valid value is `change_request:created`. + +## Attributes Reference + +- `id` - The ID of the notification configuration. + +## Import + +Team notification configurations can be imported; use `` as the import ID. For example: + +```shell +terraform import tfe_team_notification_configuration.test nc-qV9JnKRkmtMa4zcA +```