diff --git a/docs/resources/notification_destination.md b/docs/resources/notification_destination.md new file mode 100644 index 0000000000..21c1170402 --- /dev/null +++ b/docs/resources/notification_destination.md @@ -0,0 +1,99 @@ +--- +subcategory: "Workspace" +--- +# databricks_notification_destination Resource + +This resource allows you to manage [Notification Destinations](https://docs.databricks.com/api/workspace/notificationdestinations). Notification destinations are used to send notifications for query alerts and jobs to destinations outside of Databricks. Only workspace admins can create, update, and delete notification destinations. + +## Example Usage + +`Email` notification destination: + +```hcl +resource "databricks_notification_destination" "ndresource" { + display_name = "Notification Destination" + config { + email { + addresses = ["abc@gmail.com"] + } + } +} +``` +`Slack` notification destination: + +```hcl +resource "databricks_notification_destination" "ndresource" { + display_name = "Notification Destination" + config { + slack { + url = "https://hooks.slack.com/services/..." + } + } +} +``` +`PagerDuty` notification destination: + +```hcl +resource "databricks_notification_destination" "ndresource" { + display_name = "Notification Destination" + config { + pagerduty { + integration_key = "xxxxxx" + } + } +} +``` +`Microsoft Teams` notification destination: + +```hcl +resource "databricks_notification_destination" "ndresource" { + display_name = "Notification Destination" + config { + microsoft_teams { + url = "https://outlook.office.com/webhook/..." + } + } +} +``` +`Generic Webhook` notification destination: + +```hcl +resource "databricks_notification_destination" "ndresource" { + display_name = "Notification Destination" + config { + generic_webhook { + url = "https://example.com/webhook" + username = "username" // Optional + password = "password" // Optional + } + } +} +``` + + +## Argument Reference + +The following arguments are supported: + +* `display_name` - (Required) The display name of the Notification Destination. +* `config` - (Required) The configuration of the Notification Destination. It must contain exactly one of the following blocks: + * `email` - The email configuration of the Notification Destination. It must contain the following: + * `addresses` - (Required) The list of email addresses to send notifications to. + * `slack` - The Slack configuration of the Notification Destination. It must contain the following: + * `url` - (Required) The Slack webhook URL. + * `pagerduty` - The PagerDuty configuration of the Notification Destination. It must contain the following: + * `integration_key` - (Required) The PagerDuty integration key. + * `microsoft_teams` - The Microsoft Teams configuration of the Notification Destination. It must contain the following: + * `url` - (Required) The Microsoft Teams webhook URL. + * `generic_webhook` - The Generic Webhook configuration of the Notification Destination. It must contain the following: + * `url` - (Required) The Generic Webhook URL. + * `username` - (Optional) The username for basic authentication. + * `password` - (Optional) The password for basic authentication. + +-> **NOTE** If the type of notification destination is changed, the existing notification destination will be deleted and a new notification destination will be created with the new type. + +## Attribute Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - The unique ID of the Notification Destination. diff --git a/internal/acceptance/notification_destination_test.go b/internal/acceptance/notification_destination_test.go new file mode 100644 index 0000000000..c2648a579e --- /dev/null +++ b/internal/acceptance/notification_destination_test.go @@ -0,0 +1,204 @@ +package acceptance + +import ( + "context" + "testing" + + "github.com/databricks/databricks-sdk-go/service/settings" + "github.com/databricks/terraform-provider-databricks/common" + "github.com/databricks/terraform-provider-databricks/qa" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func checkND(t *testing.T, display_name string, config_type settings.DestinationType) resource.TestCheckFunc { + return resourceCheck("databricks_notification_destination.this", func(ctx context.Context, client *common.DatabricksClient, id string) error { + w, err := client.WorkspaceClient() + if err != nil { + return err + } + ndResource, err := w.NotificationDestinations.Get(ctx, settings.GetNotificationDestinationRequest{ + Id: id, + }) + if err != nil { + return err + } + assert.Equal(t, config_type, ndResource.DestinationType) + assert.Equal(t, display_name, ndResource.DisplayName) + require.NoError(t, err) + return nil + }) +} + +func TestAccNDEmail(t *testing.T) { + display_name := "Email Notification Destination - " + qa.RandomName() + workspaceLevel(t, step{ + Template: ` + resource "databricks_notification_destination" "this" { + display_name = "` + display_name + `" + config { + email { + addresses = ["` + qa.RandomEmail() + `"] + } + } + } + `, + }, step{ + Template: ` + resource "databricks_notification_destination" "this" { + display_name = "` + display_name + `" + config { + email { + addresses = ["` + qa.RandomEmail() + `", "` + qa.RandomEmail() + `"] + } + } + } + `, + Check: checkND(t, display_name, settings.DestinationTypeEmail), + }) +} + +func TestAccNDSlack(t *testing.T) { + display_name := "Notification Destination - " + qa.RandomName() + workspaceLevel(t, step{ + Template: ` + resource "databricks_notification_destination" "this" { + display_name = "` + display_name + `" + config { + slack { + url = "https://hooks.slack.com/services/{var.RANDOM}" + } + } + } + `, + Check: checkND(t, display_name, settings.DestinationTypeSlack), + }, step{ + Template: ` + resource "databricks_notification_destination" "this" { + display_name = "` + display_name + `" + config { + slack { + url = "https://hooks.slack.com/services/{var.RANDOM}" + } + } + } + `, + Check: checkND(t, display_name, settings.DestinationTypeSlack), + }) +} + +func TestAccNDMicrosoftTeams(t *testing.T) { + display_name := "Notification Destination - " + qa.RandomName() + workspaceLevel(t, step{ + Template: ` + resource "databricks_notification_destination" "this" { + display_name = "` + display_name + `" + config { + microsoft_teams { + url = "https://outlook.office.com/webhook/{var.RANDOM}" + } + } + } + `, + }, step{ + Template: ` + resource "databricks_notification_destination" "this" { + display_name = "` + display_name + `" + config { + microsoft_teams { + url = "https://outlook.office.com/webhook/{var.RANDOM}" + } + } + } + `, + Check: checkND(t, display_name, settings.DestinationTypeMicrosoftTeams), + }) +} + +func TestAccNDPagerduty(t *testing.T) { + display_name := "Notification Destination - " + qa.RandomName() + workspaceLevel(t, step{ + Template: ` + resource "databricks_notification_destination" "this" { + display_name = "` + display_name + `" + config { + pagerduty { + integration_key = "{var.RANDOM}" + } + } + } + `, + }, step{ + Template: ` + resource "databricks_notification_destination" "this" { + display_name = "` + display_name + `" + config { + pagerduty { + integration_key = "{var.RANDOM}" + } + } + } + `, + Check: checkND(t, display_name, settings.DestinationTypePagerduty), + }) +} + +func TestAccNDGenericWebhook(t *testing.T) { + display_name := "Notification Destination - " + qa.RandomName() + workspaceLevel(t, step{ + Template: ` + resource "databricks_notification_destination" "this" { + display_name = "` + display_name + `" + config { + generic_webhook { + url = "https://webhook.site/{var.RANDOM}" + password = "password" + } + } + } + `, + }, step{ + Template: ` + resource "databricks_notification_destination" "this" { + display_name = "` + display_name + `" + config { + generic_webhook { + url = "https://webhook.site/{var.RANDOM}" + username = "username2" + } + } + } + `, + Check: checkND(t, display_name, settings.DestinationTypeWebhook), + }) +} + +func TestAccConfigTypeChange(t *testing.T) { + display_name := "Notification Destination - " + qa.RandomName() + workspaceLevel(t, step{ + Template: ` + resource "databricks_notification_destination" "this" { + display_name = "` + display_name + `" + config { + slack { + url = "https://hooks.slack.com/services/{var.RANDOM}" + } + } + } + `, + Check: checkND(t, display_name, settings.DestinationTypeSlack), + }, step{ + Template: ` + resource "databricks_notification_destination" "this" { + display_name = "` + display_name + `" + config { + microsoft_teams { + url = "https://outlook.office.com/webhook/{var.RANDOM}" + } + } + } + `, + Check: checkND(t, display_name, settings.DestinationTypeMicrosoftTeams), + }) +} diff --git a/provider/provider.go b/provider/provider.go index 32eee067c5..c33cf9330a 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -173,6 +173,7 @@ func DatabricksProvider() *schema.Provider { "databricks_mws_vpc_endpoint": mws.ResourceMwsVpcEndpoint().ToResource(), "databricks_mws_workspaces": mws.ResourceMwsWorkspaces().ToResource(), "databricks_notebook": workspace.ResourceNotebook().ToResource(), + "databricks_notification_destination": settings.ResourceNotificationDestination().ToResource(), "databricks_obo_token": tokens.ResourceOboToken().ToResource(), "databricks_online_table": catalog.ResourceOnlineTable().ToResource(), "databricks_permission_assignment": access.ResourcePermissionAssignment().ToResource(), diff --git a/settings/resource_notification_destination.go b/settings/resource_notification_destination.go new file mode 100644 index 0000000000..40a75ecd14 --- /dev/null +++ b/settings/resource_notification_destination.go @@ -0,0 +1,167 @@ +package settings + +import ( + "context" + + "github.com/databricks/databricks-sdk-go" + "github.com/databricks/databricks-sdk-go/service/settings" + "github.com/databricks/terraform-provider-databricks/common" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func setStruct(s *settings.NotificationDestination, readND *settings.NotificationDestination) { + if readND.Config != nil && s.Config != nil { + switch readND.DestinationType { + case settings.DestinationTypeSlack: + if readND.Config.Slack != nil && s.Config.Slack != nil { + readND.Config.Slack.Url = s.Config.Slack.Url + } + case settings.DestinationTypePagerduty: + if readND.Config.Pagerduty != nil && s.Config.Pagerduty != nil { + readND.Config.Pagerduty.IntegrationKey = s.Config.Pagerduty.IntegrationKey + } + case settings.DestinationTypeMicrosoftTeams: + if readND.Config.MicrosoftTeams != nil && s.Config.MicrosoftTeams != nil { + readND.Config.MicrosoftTeams.Url = s.Config.MicrosoftTeams.Url + } + case settings.DestinationTypeWebhook: + if readND.Config.GenericWebhook != nil && s.Config.GenericWebhook != nil { + if readND.Config.GenericWebhook.UrlSet { + readND.Config.GenericWebhook.Url = s.Config.GenericWebhook.Url + } + if readND.Config.GenericWebhook.PasswordSet { + readND.Config.GenericWebhook.Password = s.Config.GenericWebhook.Password + } + if readND.Config.GenericWebhook.UsernameSet { + readND.Config.GenericWebhook.Username = s.Config.GenericWebhook.Username + } + } + } + } +} + +func Create(ctx context.Context, d *schema.ResourceData, w *databricks.WorkspaceClient) error { + var newNDrequest settings.CreateNotificationDestinationRequest + common.DataToStructPointer(d, ndSchema, &newNDrequest) + createdND, err := w.NotificationDestinations.Create(ctx, newNDrequest) + if err != nil { + return err + } + d.SetId(createdND.Id) + return nil +} + +func Read(ctx context.Context, d *schema.ResourceData, w *databricks.WorkspaceClient) error { + var tempND settings.NotificationDestination + common.DataToStructPointer(d, ndSchema, &tempND) + + readND, err := w.NotificationDestinations.Get(ctx, settings.GetNotificationDestinationRequest{ + Id: d.Id(), + }) + if err != nil { + return err + } + setStruct(&tempND, readND) + return common.StructToData(readND, ndSchema, d) +} + +func Update(ctx context.Context, d *schema.ResourceData, w *databricks.WorkspaceClient) error { + var updateNDRequest settings.UpdateNotificationDestinationRequest + common.DataToStructPointer(d, ndSchema, &updateNDRequest) + updateNDRequest.Id = d.Id() + _, err := w.NotificationDestinations.Update(ctx, updateNDRequest) + if err != nil { + return err + } + return nil +} + +func Delete(ctx context.Context, d *schema.ResourceData, w *databricks.WorkspaceClient) error { + return w.NotificationDestinations.Delete(ctx, settings.DeleteNotificationDestinationRequest{ + Id: d.Id(), + }) +} + +type NDStruct struct { + settings.NotificationDestination +} + +func (NDStruct) CustomizeSchema(s *common.CustomizableSchema) *common.CustomizableSchema { + // Required fields + s.SchemaPath("display_name").SetRequired() + + // Computed fields + s.SchemaPath("id").SetComputed() + s.SchemaPath("destination_type").SetComputed() + s.SchemaPath("config", "slack", "url_set").SetComputed() + s.SchemaPath("config", "pagerduty", "integration_key_set").SetComputed() + s.SchemaPath("config", "microsoft_teams", "url_set").SetComputed() + s.SchemaPath("config", "generic_webhook", "url_set").SetComputed() + s.SchemaPath("config", "generic_webhook", "password_set").SetComputed() + s.SchemaPath("config", "generic_webhook", "username_set").SetComputed() + + // ForceNew fields + s.SchemaPath("config", "slack").SetForceNew() + s.SchemaPath("config", "pagerduty").SetForceNew() + s.SchemaPath("config", "microsoft_teams").SetForceNew() + s.SchemaPath("config", "generic_webhook").SetForceNew() + s.SchemaPath("config", "email").SetForceNew() + + // ConflictsWith fields + config_eoo := []string{"config.0.slack", "config.0.pagerduty", "config.0.microsoft_teams", "config.0.generic_webhook", "config.0.email"} + s.SchemaPath("config", "slack").SetExactlyOneOf(config_eoo) + + // RequiredWith fields + s.SchemaPath("config", "slack").SetRequiredWith([]string{"config.0.slack.0.url"}) + s.SchemaPath("config", "pagerduty").SetRequiredWith([]string{"config.0.pagerduty.0.integration_key"}) + s.SchemaPath("config", "microsoft_teams").SetRequiredWith([]string{"config.0.microsoft_teams.0.url"}) + s.SchemaPath("config", "generic_webhook").SetRequiredWith([]string{"config.0.generic_webhook.0.url"}) + s.SchemaPath("config", "email").SetRequiredWith([]string{"config.0.email.0.addresses"}) + + // Sensitive fields + s.SchemaPath("config", "generic_webhook", "password").SetSensitive() + s.SchemaPath("config", "generic_webhook", "username").SetSensitive() + s.SchemaPath("config", "generic_webhook", "url").SetSensitive() + s.SchemaPath("config", "microsoft_teams", "url").SetSensitive() + s.SchemaPath("config", "pagerduty", "integration_key").SetSensitive() + s.SchemaPath("config", "slack", "url").SetSensitive() + + return s +} + +var ndSchema = common.StructToSchema(NDStruct{}, nil) + +func ResourceNotificationDestination() common.Resource { + return common.Resource{ + Schema: ndSchema, + Create: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error { + + w, err := c.WorkspaceClient() + if err != nil { + return err + } + return Create(ctx, d, w) + }, + Read: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error { + w, err := c.WorkspaceClient() + if err != nil { + return err + } + return Read(ctx, d, w) + }, + Update: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error { + w, err := c.WorkspaceClient() + if err != nil { + return err + } + return Update(ctx, d, w) + }, + Delete: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error { + w, err := c.WorkspaceClient() + if err != nil { + return err + } + return Delete(ctx, d, w) + }, + } +} diff --git a/settings/resource_notification_destination_test.go b/settings/resource_notification_destination_test.go new file mode 100644 index 0000000000..f552b90d0a --- /dev/null +++ b/settings/resource_notification_destination_test.go @@ -0,0 +1,180 @@ +package settings + +import ( + "testing" + + "github.com/databricks/databricks-sdk-go/experimental/mocks" + "github.com/databricks/databricks-sdk-go/service/settings" + "github.com/databricks/terraform-provider-databricks/qa" + + "github.com/stretchr/testify/mock" +) + +func TestNDCreate(t *testing.T) { + qa.ResourceFixture{ + MockWorkspaceClientFunc: func(w *mocks.MockWorkspaceClient) { + e := w.GetMockNotificationDestinationsAPI().EXPECT() + e.Create(mock.Anything, settings.CreateNotificationDestinationRequest{ + DisplayName: "Notification Destination", + Config: &settings.Config{ + GenericWebhook: &settings.GenericWebhookConfig{ + Url: "https://webhook.site/abc", + Password: "password", + }, + }, + }).Return(&settings.NotificationDestination{ + Id: "xyz", + DisplayName: "Notification Destination", + DestinationType: "WEBHOOK", + Config: &settings.Config{ + GenericWebhook: &settings.GenericWebhookConfig{ + UrlSet: true, + PasswordSet: true, + }, + }, + }, nil) + e.Get(mock.Anything, settings.GetNotificationDestinationRequest{ + Id: "xyz", + }).Return(&settings.NotificationDestination{ + Id: "xyz", + DisplayName: "Notification Destination", + DestinationType: "WEBHOOK", + Config: &settings.Config{ + GenericWebhook: &settings.GenericWebhookConfig{ + UrlSet: true, + PasswordSet: true, + }, + }, + }, nil) + }, + Resource: ResourceNotificationDestination(), + Create: true, + HCL: ` + display_name = "Notification Destination" + config { + generic_webhook { + url = "https://webhook.site/abc" + password = "password" + } + } + `, + }.ApplyAndExpectData(t, map[string]any{ + "id": "xyz", + "display_name": "Notification Destination", + }) +} + +func TestNDRead(t *testing.T) { + qa.ResourceFixture{ + MockWorkspaceClientFunc: func(w *mocks.MockWorkspaceClient) { + w.GetMockNotificationDestinationsAPI().EXPECT().Get(mock.Anything, settings.GetNotificationDestinationRequest{ + Id: "xyz", + }).Return(&settings.NotificationDestination{ + Id: "xyz", + DisplayName: "Notification Destination", + DestinationType: "EMAIL", + Config: &settings.Config{ + Email: &settings.EmailConfig{ + Addresses: []string{"abc@email.com"}, + }, + }, + }, nil) + }, + Resource: ResourceNotificationDestination(), + Read: true, + ID: "xyz", + HCL: ` + display_name = "Notification Destination" + config { + email { + addresses = ["abc@email.com"] + } + } + `, + }.ApplyAndExpectData(t, map[string]any{ + "id": "xyz", + "display_name": "Notification Destination", + }) +} + +func TestNDUpdate(t *testing.T) { + qa.ResourceFixture{ + MockWorkspaceClientFunc: func(w *mocks.MockWorkspaceClient) { + e := w.GetMockNotificationDestinationsAPI().EXPECT() + e.Update(mock.Anything, settings.UpdateNotificationDestinationRequest{ + Id: "xyz", + DisplayName: "Notification Destination - 2", + Config: &settings.Config{ + Email: &settings.EmailConfig{ + Addresses: []string{"pqr@email.com"}, + }, + }, + }).Return(&settings.NotificationDestination{ + Id: "xyz", + DisplayName: "Notification Destination - 2", + DestinationType: "EMAIL", + Config: &settings.Config{ + Email: &settings.EmailConfig{ + Addresses: []string{"pqr@email.com"}, + }, + }, + }, nil) + e.Get(mock.Anything, settings.GetNotificationDestinationRequest{ + Id: "xyz", + }).Return(&settings.NotificationDestination{ + Id: "xyz", + DisplayName: "Notification Destination - 2", + DestinationType: "EMAIL", + Config: &settings.Config{ + Email: &settings.EmailConfig{ + Addresses: []string{"pqr@email.com"}, + }, + }, + }, nil) + }, + Resource: ResourceNotificationDestination(), + Update: true, + ID: "xyz", + HCL: ` + display_name = "Notification Destination - 2" + config { + email { + addresses = ["pqr@email.com"] + } + } + `, + InstanceState: map[string]string{ + "id": "xyz", + "display_name": "Notification Destination", + "config.#": "1", + "config.0.email.#": "1", + "config.0.email.0.addresses.#": "1", + "config.0.email.0.addresses.0": "abc@email.com", + }, + }.ApplyAndExpectData(t, map[string]any{ + "id": "xyz", + "display_name": "Notification Destination - 2", + }) +} + +func TestNDDelete(t *testing.T) { + qa.ResourceFixture{ + MockWorkspaceClientFunc: func(w *mocks.MockWorkspaceClient) { + w.GetMockNotificationDestinationsAPI().EXPECT().Delete(mock.Anything, settings.DeleteNotificationDestinationRequest{ + Id: "xyz", + }).Return(nil) + }, + Resource: ResourceNotificationDestination(), + Delete: true, + ID: "xyz", + HCL: ` + display_name = "Notification Destination" + config { + generic_webhook { + url = "https://webhook.site/abc" + password = "password" + } + } + `, + }.ApplyNoError(t) +}