diff --git a/tfe/provider.go b/tfe/provider.go index dc127fa6c..7ca77ef01 100644 --- a/tfe/provider.go +++ b/tfe/provider.go @@ -70,19 +70,20 @@ func Provider() terraform.ResourceProvider { }, ResourcesMap: map[string]*schema.Resource{ - "tfe_oauth_client": resourceTFEOAuthClient(), - "tfe_organization": resourceTFEOrganization(), - "tfe_organization_token": resourceTFEOrganizationToken(), - "tfe_policy_set": resourceTFEPolicySet(), - "tfe_sentinel_policy": resourceTFESentinelPolicy(), - "tfe_ssh_key": resourceTFESSHKey(), - "tfe_team": resourceTFETeam(), - "tfe_team_access": resourceTFETeamAccess(), - "tfe_team_member": resourceTFETeamMember(), - "tfe_team_members": resourceTFETeamMembers(), - "tfe_team_token": resourceTFETeamToken(), - "tfe_workspace": resourceTFEWorkspace(), - "tfe_variable": resourceTFEVariable(), + "tfe_notification_configuration": resourceTFENotificationConfiguration(), + "tfe_oauth_client": resourceTFEOAuthClient(), + "tfe_organization": resourceTFEOrganization(), + "tfe_organization_token": resourceTFEOrganizationToken(), + "tfe_policy_set": resourceTFEPolicySet(), + "tfe_sentinel_policy": resourceTFESentinelPolicy(), + "tfe_ssh_key": resourceTFESSHKey(), + "tfe_team": resourceTFETeam(), + "tfe_team_access": resourceTFETeamAccess(), + "tfe_team_member": resourceTFETeamMember(), + "tfe_team_members": resourceTFETeamMembers(), + "tfe_team_token": resourceTFETeamToken(), + "tfe_workspace": resourceTFEWorkspace(), + "tfe_variable": resourceTFEVariable(), }, ConfigureFunc: providerConfigure, diff --git a/tfe/resource_tfe_notification_configuration.go b/tfe/resource_tfe_notification_configuration.go new file mode 100644 index 000000000..14ee76490 --- /dev/null +++ b/tfe/resource_tfe_notification_configuration.go @@ -0,0 +1,205 @@ +package tfe + +import ( + "fmt" + "log" + + tfe "github.com/hashicorp/go-tfe" + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/helper/validation" +) + +func resourceTFENotificationConfiguration() *schema.Resource { + return &schema.Resource{ + Create: resourceTFENotificationConfigurationCreate, + Read: resourceTFENotificationConfigurationRead, + Update: resourceTFENotificationConfigurationUpdate, + Delete: resourceTFENotificationConfigurationDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + }, + + "destination_type": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice( + []string{ + string(tfe.NotificationDestinationTypeGeneric), + string(tfe.NotificationDestinationTypeSlack), + }, + false, + ), + }, + + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + + "token": { + Type: schema.TypeString, + Optional: true, + Sensitive: true, + }, + + "triggers": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.StringInSlice( + []string{ + string(tfe.NotificationTriggerCreated), + string(tfe.NotificationTriggerPlanning), + string(tfe.NotificationTriggerNeedsAttention), + string(tfe.NotificationTriggerApplying), + string(tfe.NotificationTriggerCompleted), + string(tfe.NotificationTriggerErrored), + }, + false, + ), + }, + }, + + "url": { + Type: schema.TypeString, + Required: true, + }, + + "workspace_external_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + }, + } +} + +func resourceTFENotificationConfigurationCreate(d *schema.ResourceData, meta interface{}) error { + tfeClient := meta.(*tfe.Client) + + // Get workspace + workspaceID := d.Get("workspace_external_id").(string) + + // Get attributes + destinationType := tfe.NotificationDestinationType(d.Get("destination_type").(string)) + enabled := d.Get("enabled").(bool) + name := d.Get("name").(string) + token := d.Get("token").(string) + url := d.Get("url").(string) + + // Throw error if token is set with destinationType of slack + if token != "" && destinationType == tfe.NotificationDestinationTypeSlack { + return fmt.Errorf("Token cannot be set with destination_type of %s", destinationType) + } + + // Create a new options struct + options := tfe.NotificationConfigurationCreateOptions{ + DestinationType: tfe.NotificationDestination(destinationType), + Enabled: tfe.Bool(enabled), + Name: tfe.String(name), + Token: tfe.String(token), + URL: tfe.String(url), + } + + // Add triggers set to the options struct + for _, trigger := range d.Get("triggers").(*schema.Set).List() { + options.Triggers = append(options.Triggers, trigger.(string)) + } + + log.Printf("[DEBUG] Create notification configuration: %s", name) + notificationConfiguration, err := tfeClient.NotificationConfigurations.Create(ctx, workspaceID, options) + if err != nil { + return fmt.Errorf("Error creating notification configuration %s: %v", name, err) + } + + d.SetId(notificationConfiguration.ID) + + return resourceTFENotificationConfigurationRead(d, meta) +} + +func resourceTFENotificationConfigurationRead(d *schema.ResourceData, meta interface{}) error { + tfeClient := meta.(*tfe.Client) + + log.Printf("[DEBUG] Read notification configuration: %s", d.Id()) + notificationConfiguration, err := tfeClient.NotificationConfigurations.Read(ctx, d.Id()) + if err != nil { + if err == tfe.ErrResourceNotFound { + log.Printf("[DEBUG] Notification configuration %s no longer exists", d.Id()) + d.SetId("") + return nil + } + return fmt.Errorf("Error reading notification configuration %s: %v", d.Id(), err) + } + + // Update config + d.Set("destination_type", notificationConfiguration.DestinationType) + d.Set("enabled", notificationConfiguration.Enabled) + d.Set("name", notificationConfiguration.Name) + // Don't set token here, as it is write only + // and setting it here would make it blank + d.Set("triggers", notificationConfiguration.Triggers) + d.Set("url", notificationConfiguration.URL) + + return nil +} + +func resourceTFENotificationConfigurationUpdate(d *schema.ResourceData, meta interface{}) error { + tfeClient := meta.(*tfe.Client) + + // Get attributes + destinationType := tfe.NotificationDestinationType(d.Get("destination_type").(string)) + enabled := d.Get("enabled").(bool) + name := d.Get("name").(string) + token := d.Get("token").(string) + url := d.Get("url").(string) + + // Throw error if token is set with destinationType of slack + if token != "" && destinationType == tfe.NotificationDestinationTypeSlack { + return fmt.Errorf("Token cannot be set with destination_type of %s", destinationType) + } + + // Create a new options struct + options := tfe.NotificationConfigurationUpdateOptions{ + Enabled: tfe.Bool(enabled), + Name: tfe.String(name), + Token: tfe.String(token), + URL: tfe.String(url), + } + + // Add triggers set to the options struct + for _, trigger := range d.Get("triggers").(*schema.Set).List() { + options.Triggers = append(options.Triggers, trigger.(string)) + } + + log.Printf("[DEBUG] Update notification configuration: %s", d.Id()) + _, err := tfeClient.NotificationConfigurations.Update(ctx, d.Id(), options) + if err != nil { + return fmt.Errorf("Error updating notification configuration %s: %v", d.Id(), err) + } + + return resourceTFENotificationConfigurationRead(d, meta) +} + +func resourceTFENotificationConfigurationDelete(d *schema.ResourceData, meta interface{}) error { + tfeClient := meta.(*tfe.Client) + + log.Printf("[DEBUG] Delete notification configuration: %s", d.Id()) + err := tfeClient.NotificationConfigurations.Delete(ctx, d.Id()) + if err != nil { + if err == tfe.ErrResourceNotFound { + return nil + } + return fmt.Errorf("Error deleting notification configuration %s: %v", d.Id(), err) + } + + return nil +} diff --git a/tfe/resource_tfe_notification_configuration_test.go b/tfe/resource_tfe_notification_configuration_test.go new file mode 100644 index 000000000..555951007 --- /dev/null +++ b/tfe/resource_tfe_notification_configuration_test.go @@ -0,0 +1,364 @@ +package tfe + +import ( + "fmt" + "reflect" + "regexp" + "testing" + + "github.com/hashicorp/go-tfe" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccTFENotificationConfiguration_basic(t *testing.T) { + notificationConfiguration := &tfe.NotificationConfiguration{} + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckTFENotificationConfigurationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccTFENotificationConfiguration_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckTFENotificationConfigurationExists( + "tfe_notification_configuration.foobar", notificationConfiguration), + testAccCheckTFENotificationConfigurationAttributes(notificationConfiguration), + resource.TestCheckResourceAttr( + "tfe_notification_configuration.foobar", "destination_type", "generic"), + resource.TestCheckResourceAttr( + "tfe_notification_configuration.foobar", "name", "notification_basic"), + // Just test the number of items in triggers + // Values in triggers attribute are tested by testCheckTFENotificationConfigurationAttributes + resource.TestCheckResourceAttr( + "tfe_notification_configuration.foobar", "triggers.#", "0"), + resource.TestCheckResourceAttr( + "tfe_notification_configuration.foobar", "url", "http://example.com"), + ), + }, + }, + }) +} + +func TestAccTFENotificationConfiguration_update(t *testing.T) { + notificationConfiguration := &tfe.NotificationConfiguration{} + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckTFENotificationConfigurationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccTFENotificationConfiguration_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckTFENotificationConfigurationExists( + "tfe_notification_configuration.foobar", notificationConfiguration), + testAccCheckTFENotificationConfigurationAttributes(notificationConfiguration), + resource.TestCheckResourceAttr( + "tfe_notification_configuration.foobar", "destination_type", "generic"), + resource.TestCheckResourceAttr( + "tfe_notification_configuration.foobar", "name", "notification_basic"), + // Just test the number of items in triggers + // Values in triggers attribute are tested by testCheckTFENotificationConfigurationAttributes + resource.TestCheckResourceAttr( + "tfe_notification_configuration.foobar", "triggers.#", "0"), + resource.TestCheckResourceAttr( + "tfe_notification_configuration.foobar", "url", "http://example.com"), + ), + }, + { + Config: testAccTFENotificationConfiguration_update, + Check: resource.ComposeTestCheckFunc( + testAccCheckTFENotificationConfigurationExists( + "tfe_notification_configuration.foobar", notificationConfiguration), + testAccCheckTFENotificationConfigurationAttributesUpdate(notificationConfiguration), + resource.TestCheckResourceAttr( + "tfe_notification_configuration.foobar", "destination_type", "generic"), + resource.TestCheckResourceAttr( + "tfe_notification_configuration.foobar", "enabled", "true"), + resource.TestCheckResourceAttr( + "tfe_notification_configuration.foobar", "name", "notification_update"), + resource.TestCheckResourceAttr( + "tfe_notification_configuration.foobar", "token", "1234567890_update"), + // Just test the number of items in triggers + // Values in triggers attribute are tested by testCheckTFENotificationConfigurationAttributesUpdate + resource.TestCheckResourceAttr( + "tfe_notification_configuration.foobar", "triggers.#", "2"), + resource.TestCheckResourceAttr( + "tfe_notification_configuration.foobar", "url", "http://example.com/?update=true"), + ), + }, + }, + }) +} + +func TestAccTFENotificationConfiguration_slackWithToken(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckTFENotificationConfigurationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccTFENotificationConfiguration_slackWithToken, + ExpectError: regexp.MustCompile(`Token cannot be set with destination_type of slack`), + }, + }, + }) +} + +func TestAccTFENotificationConfiguration_duplicateTriggers(t *testing.T) { + notificationConfiguration := &tfe.NotificationConfiguration{} + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckTFENotificationConfigurationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccTFENotificationConfiguration_duplicateTriggers, + Check: resource.ComposeTestCheckFunc( + testAccCheckTFENotificationConfigurationExists( + "tfe_notification_configuration.foobar", notificationConfiguration), + testAccCheckTFENotificationConfigurationAttributesDuplicateTriggers(notificationConfiguration), + resource.TestCheckResourceAttr( + "tfe_notification_configuration.foobar", "destination_type", "generic"), + resource.TestCheckResourceAttr( + "tfe_notification_configuration.foobar", "name", "notification_duplicate_triggers"), + // Just test the number of items in triggers + // Values in triggers attribute are tested by testCheckTFENotificationConfigurationAttributes + resource.TestCheckResourceAttr( + "tfe_notification_configuration.foobar", "triggers.#", "1"), + resource.TestCheckResourceAttr( + "tfe_notification_configuration.foobar", "url", "http://example.com"), + ), + }, + }, + }) +} + +func TestAccTFENotificationConfigurationImport(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckTFENotificationConfigurationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccTFENotificationConfiguration_update, + }, + + { + ResourceName: "tfe_notification_configuration.foobar", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"token", "workspace_external_id"}, + }, + }, + }) +} + +func testAccCheckTFENotificationConfigurationExists(n string, notificationConfiguration *tfe.NotificationConfiguration) resource.TestCheckFunc { + return func(s *terraform.State) error { + tfeClient := testAccProvider.Meta().(*tfe.Client) + + 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 := tfeClient.NotificationConfigurations.Read(ctx, rs.Primary.ID) + if err != nil { + return err + } + + *notificationConfiguration = *nc + + return nil + } +} + +func testAccCheckTFENotificationConfigurationAttributes(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 != "http://example.com" { + return fmt.Errorf("Bad URL: %s", notificationConfiguration.URL) + } + + return nil + } +} + +func testAccCheckTFENotificationConfigurationAttributesUpdate(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{tfe.NotificationTriggerCreated, tfe.NotificationTriggerNeedsAttention}) { + return fmt.Errorf("Bad triggers: %v", notificationConfiguration.Triggers) + } + + if notificationConfiguration.URL != "http://example.com/?update=true" { + return fmt.Errorf("Bad URL: %s", notificationConfiguration.URL) + } + + return nil + } +} + +func testAccCheckTFENotificationConfigurationAttributesDuplicateTriggers(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{tfe.NotificationTriggerCreated}) { + return fmt.Errorf("Bad triggers: %v", notificationConfiguration.Triggers) + } + + if notificationConfiguration.URL != "http://example.com" { + return fmt.Errorf("Bad URL: %s", notificationConfiguration.URL) + } + + return nil + } +} + +func testAccCheckTFENotificationConfigurationDestroy(s *terraform.State) error { + tfeClient := testAccProvider.Meta().(*tfe.Client) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "tfe_notification_configuration" { + continue + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No instance ID is set") + } + + _, err := tfeClient.NotificationConfigurations.Read(ctx, rs.Primary.ID) + if err == nil { + return fmt.Errorf("Notification configuration %s still exists", rs.Primary.ID) + } + } + + return nil +} + +const testAccTFENotificationConfiguration_basic = ` +resource "tfe_organization" "foobar" { + name = "terraform-test" + email = "admin@company.com" +} + +resource "tfe_workspace" "foobar" { + name = "workspace-test" + organization = "${tfe_organization.foobar.id}" +} + +resource "tfe_notification_configuration" "foobar" { + name = "notification_basic" + destination_type = "generic" + url = "http://example.com" + workspace_external_id = "${tfe_workspace.foobar.external_id}" +}` + +const testAccTFENotificationConfiguration_update = ` +resource "tfe_organization" "foobar" { + name = "terraform-test" + email = "admin@company.com" +} + +resource "tfe_workspace" "foobar" { + name = "workspace-test" + organization = "${tfe_organization.foobar.id}" +} + +resource "tfe_notification_configuration" "foobar" { + name = "notification_update" + destination_type = "generic" + enabled = true + token = "1234567890_update" + triggers = ["run:created", "run:needs_attention"] + url = "http://example.com/?update=true" + workspace_external_id = "${tfe_workspace.foobar.external_id}" +}` + +const testAccTFENotificationConfiguration_slackWithToken = ` +resource "tfe_organization" "foobar" { + name = "terraform-test" + email = "admin@company.com" +} + +resource "tfe_workspace" "foobar" { + name = "workspace-test" + organization = "${tfe_organization.foobar.id}" +} + +resource "tfe_notification_configuration" "foobar" { + name = "notification_slack_with_token" + destination_type = "slack" + token = "1234567890" + url = "http://example.com" + workspace_external_id = "${tfe_workspace.foobar.external_id}" +}` + +const testAccTFENotificationConfiguration_duplicateTriggers = ` +resource "tfe_organization" "foobar" { + name = "terraform-test" + email = "admin@company.com" +} + +resource "tfe_workspace" "foobar" { + name = "workspace-test" + organization = "${tfe_organization.foobar.id}" +} + +resource "tfe_notification_configuration" "foobar" { + name = "notification_duplicate_triggers" + destination_type = "generic" + triggers = ["run:created", "run:created", "run:created"] + url = "http://example.com" + workspace_external_id = "${tfe_workspace.foobar.external_id}" +}` diff --git a/website/docs/r/notification_configuration.html.markdown b/website/docs/r/notification_configuration.html.markdown new file mode 100644 index 000000000..4c8ccd721 --- /dev/null +++ b/website/docs/r/notification_configuration.html.markdown @@ -0,0 +1,70 @@ +--- +layout: "tfe" +page_title: "Terraform Enterprise: tfe_notification_configuration" +sidebar_current: "docs-resource-tfe-notification-configuration" +description: |- + Manages notifications configurations. +--- + +# tfe_notification_configuration + +Terraform Cloud can be configured to send notifications for run state transitions. +Notification configurations allow you to specify a URL, destination type, and what events will trigger the notification. +Each workspace can have up to 20 notification configurations, and they apply to all runs for that workspace. + +## Example Usage + +Basic usage: + +```hcl +resource "tfe_organization" "test" { + name = "my-org-name" + email = "admin@company.com" +} + +resource "tfe_workspace" "test" { + name = "my-workspace-name" + organization = "${tfe_organization.test.id}" +} + +resource "tfe_notification_configuration" "test" { + name = "my-test-notification-configuration" + enabled = true + destination_type = "generic" + triggers = ["run:created", "run:planning", "run:errored"] + url = "https://example.com" + workspace_external_id = "${tfe_workspace.test.external_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` 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`. A token set for notification configurations + with a destination type of `slack` is not allowed and will result in an error. Defaults to `null`. +* `triggers` - (Optional) The array of triggers for which this notification configuration will + send notifications. Valid values are `run:created`, `run:planning`, `run:needs_attention`, `run:applying` + `run:completed`, `run:errored`. If omitted, no notification triggers are configured. +* `url` - (Required) The HTTP or HTTPS URL of the notification configuration where notification + requests will be made. +* `workspace_external_id` - (Required) The external id of the workspace that owns the notification configuration. + +## Attributes Reference + +* `id` - The ID of the notification configuration. + +## Import + +Notification configurations can be imported; use `` as the import ID. For example: + +```shell +terraform import tfe_notification_configuration.test nc-qV9JnKRkmtMa4zcA +```