diff --git a/CHANGELOG.md b/CHANGELOG.md index 80091b42..8a8c0c86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,11 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- **New resource:** `scalr_slack_integration` ([#249](https://github.com/Scalr/terraform-provider-scalr/pull/249)) + ### Changed + - `scar_enviroment`: removed attribute `cloud_credentials` ([#247](https://github.com/Scalr/terraform-provider-scalr/pull/247)) - `data.scalr_enviroment`: removed attribute `cloud_credentials` ([#247](https://github.com/Scalr/terraform-provider-scalr/pull/247)) ### Fixed + - `scalr_provider_configuration_default`: fixed a bug where unnecessary policy groups updates were occurring for the environment ([#248](https://github.com/Scalr/terraform-provider-scalr/pull/248)) ## [1.0.6] - 2023-05-12 diff --git a/GNUmakefile b/GNUmakefile index 2677230b..299c1e20 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -37,7 +37,7 @@ test: $(BUILD_ENV) xargs -t -n4 go test $(TESTARGS) -timeout=30s -parallel=4 testacc: - $(BUILD_ENV) TF_ACC=1 go test $(TEST) -v $(TESTARGS) -timeout 15m -covermode atomic -coverprofile=covprofile + TF_ACC=1 go test -race $(TEST) -v $(TESTARGS) -timeout 15m -covermode atomic -coverprofile=covprofile notify-upstream: curl -X POST \ diff --git a/docs/resources/scalr_slack_integration.md b/docs/resources/scalr_slack_integration.md new file mode 100644 index 00000000..9e2584a7 --- /dev/null +++ b/docs/resources/scalr_slack_integration.md @@ -0,0 +1,39 @@ + +# Resource `scalr_slack_integration` + +Manage the state of Slack integrations in Scalr. Create, update and destroy. +Slack workspace should be connected to Scalr account before using this resource. + +## Example Usage + +Basic usage: + +```hcl +resource "scalr_slack_integration" "test" { + name = "my-channel" + account_id = "acc-xxxx" + events = ["run_approval_required", "run_success", "run_errored"] + channel_id = "xxxx" # Can be found in slack UI (channel settings/info popup) + environments = ["env-xxxxx"] + workspaces = ["ws-xxxx", "ws-xxxx"] +} +``` + +## Argument Reference + +* `name` - (Required) Name of the Slack integration. +* `channel_id` - (Required) Slack channel ID the event will be sent to. +* `events` - (Required) Terraform run events you would like to receive a Slack notifications for. +Supported values are `run_approval_required`, `run_success`, `run_errored`. +* `environments` - (Required) List of environments where events should be triggered. +* `workspaces` - (Optional) List of workspaces where events should be triggered. +Workspaces should be in provided environments. If no workspace is given for a specified environment, +events will trigger in all of its workspaces. +* `account_id` - (Optional) ID of the account. + + +## Attribute Reference + +All arguments plus: + +* `id` - The ID of the Slack integration. diff --git a/go.mod b/go.mod index fcd24890..2ea01e9e 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ require ( github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce github.com/hashicorp/terraform-plugin-sdk/v2 v2.24.1 github.com/hashicorp/terraform-svchost v0.0.0-20200729002733-f050f53b9734 - github.com/scalr/go-scalr v0.0.0-20230601124243-269bd29a43a8 + github.com/scalr/go-scalr v0.0.0-20230612172707-84e1dfe6bf84 ) require ( diff --git a/go.sum b/go.sum index c866e3d6..2746a687 100644 --- a/go.sum +++ b/go.sum @@ -242,8 +242,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/scalr/go-scalr v0.0.0-20230601124243-269bd29a43a8 h1:FG+CAdkX9RrKzMzo54JpPWLu/iEe28ufk9Ti12RR/yc= -github.com/scalr/go-scalr v0.0.0-20230601124243-269bd29a43a8/go.mod h1:p34SHb25YRvbgft7SUjSDYESeoQhWzAlxGXId/BbaSE= +github.com/scalr/go-scalr v0.0.0-20230612172707-84e1dfe6bf84 h1:YWMFr3mzpTvLKjgC3Hdgtzr+yzyVBLjO7ryp+Gma6dM= +github.com/scalr/go-scalr v0.0.0-20230612172707-84e1dfe6bf84/go.mod h1:p34SHb25YRvbgft7SUjSDYESeoQhWzAlxGXId/BbaSE= github.com/sebdah/goldie v1.0.0/go.mod h1:jXP4hmWywNEwZzhMuv2ccnqTSFpuq8iyQhtQdkkZBH4= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= diff --git a/scalr/provider.go b/scalr/provider.go index ddc03b96..870a2dd7 100644 --- a/scalr/provider.go +++ b/scalr/provider.go @@ -101,6 +101,7 @@ func Provider() *schema.Provider { "scalr_run_trigger": resourceScalrRunTrigger(), "scalr_service_account": resourceScalrServiceAccount(), "scalr_service_account_token": resourceScalrServiceAccountToken(), + "scalr_slack_integration": resourceScalrSlackIntegration(), "scalr_tag": resourceScalrTag(), "scalr_variable": resourceScalrVariable(), "scalr_vcs_provider": resourceScalrVcsProvider(), diff --git a/scalr/resource_scalr_provider_configuration.go b/scalr/resource_scalr_provider_configuration.go index 2975c888..e4a40107 100644 --- a/scalr/resource_scalr_provider_configuration.go +++ b/scalr/resource_scalr_provider_configuration.go @@ -6,6 +6,7 @@ import ( "fmt" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "sync" + "time" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -783,15 +784,18 @@ func changeParameters( for i := 0; i < numParallel; i++ { go func() { + reqCtx, reqCancel := context.WithTimeout(context.Background(), time.Second*10) + defer reqCancel() + for t := range inputCh { if t.createOption != nil { - parameter, err := client.ProviderConfigurationParameters.Create(ctx, configurationID, *t.createOption) + parameter, err := client.ProviderConfigurationParameters.Create(reqCtx, configurationID, *t.createOption) resultCh <- result{created: parameter, err: err} } else if t.updateOption != nil { - parameter, err := client.ProviderConfigurationParameters.Update(ctx, t.updateOption.ID, *t.updateOption) + parameter, err := client.ProviderConfigurationParameters.Update(reqCtx, t.updateOption.ID, *t.updateOption) resultCh <- result{updated: parameter, err: err} } else { - err := client.ProviderConfigurationParameters.Delete(ctx, *t.deleteId) + err := client.ProviderConfigurationParameters.Delete(reqCtx, *t.deleteId) resultCh <- result{deleted: t.deleteId, err: err} } } diff --git a/scalr/resource_scalr_slack_integration.go b/scalr/resource_scalr_slack_integration.go new file mode 100644 index 00000000..777a3572 --- /dev/null +++ b/scalr/resource_scalr_slack_integration.go @@ -0,0 +1,241 @@ +package scalr + +import ( + "context" + "errors" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/scalr/go-scalr" + "log" +) + +func resourceScalrSlackIntegration() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceScalrSlackIntegrationCreate, + ReadContext: resourceScalrSlackIntegrationRead, + UpdateContext: resourceScalrSlackIntegrationUpdate, + DeleteContext: resourceSlackIntegrationDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + SchemaVersion: 0, + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: validation.ToDiagFunc(validation.StringIsNotWhiteSpace), + }, + "events": { + Type: schema.TypeSet, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateDiagFunc: validation.ToDiagFunc( + validation.StringInSlice( + []string{ + scalr.SlackIntegrationEventRunApprovalRequired, + scalr.SlackIntegrationEventRunSuccess, + scalr.SlackIntegrationEventRunErrored, + }, + false, + ), + ), + }, + Required: true, + MinItems: 1, + }, + "channel_id": { + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: validation.ToDiagFunc(validation.StringIsNotWhiteSpace), + }, + "account_id": { + Type: schema.TypeString, + Optional: true, + Computed: true, + DefaultFunc: scalrAccountIDDefaultFunc, + ForceNew: true, + }, + "environments": { + Type: schema.TypeSet, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateDiagFunc: validation.ToDiagFunc(validation.StringIsNotWhiteSpace), + }, + Required: true, + MinItems: 1, + }, + "workspaces": { + Type: schema.TypeSet, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateDiagFunc: validation.ToDiagFunc(validation.StringIsNotWhiteSpace), + }, + Optional: true, + }, + }, + } +} + +func parseEvents(d *schema.ResourceData) []string { + events := d.Get("events").(*schema.Set).List() + eventValues := make([]string, 0) + + for _, event := range events { + eventValues = append(eventValues, event.(string)) + } + + return eventValues +} + +func parseEnvironments(d *schema.ResourceData) []*scalr.Environment { + environments := d.Get("environments").(*schema.Set).List() + environmentValues := make([]*scalr.Environment, 0) + + for _, env := range environments { + environmentValues = append(environmentValues, &scalr.Environment{ID: env.(string)}) + } + + return environmentValues +} + +func parseWorkspaces(d *schema.ResourceData) []*scalr.Workspace { + workspacesI, ok := d.GetOk("workspaces") + if !ok { + return nil + } + workspaces := workspacesI.(*schema.Set).List() + workspaceValues := make([]*scalr.Workspace, 0) + + for _, ws := range workspaces { + workspaceValues = append(workspaceValues, &scalr.Workspace{ID: ws.(string)}) + } + + return workspaceValues +} + +func resourceScalrSlackIntegrationCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + scalrClient := meta.(*scalr.Client) + // Get attributes. + name := d.Get("name").(string) + accountID := d.Get("account_id").(string) + + options := scalr.SlackIntegrationCreateOptions{ + Name: &name, + ChannelId: scalr.String(d.Get("channel_id").(string)), + Events: parseEvents(d), + Account: &scalr.Account{ID: accountID}, + Environments: parseEnvironments(d), + } + workspaces := parseWorkspaces(d) + if workspaces != nil { + options.Workspaces = workspaces + } + + connection, err := scalrClient.SlackIntegrations.GetConnection(ctx, options.Account.ID) + if err != nil { + return diag.Errorf("Error creating slack integration %s: %v", name, err) + } + + if connection.ID == "" { + return diag.Errorf( + "Error creating Slack integration: account %s does not have Slack connection configured."+ + " Connect your Slack workspace to Scalr using UI first.", + accountID, + ) + } + + options.Connection = connection + + log.Printf("[DEBUG] Create slack integration: %s", name) + integration, err := scalrClient.SlackIntegrations.Create(ctx, options) + if err != nil { + return diag.Errorf("Error creating slack integration %s: %v", name, err) + } + d.SetId(integration.ID) + + return resourceScalrSlackIntegrationRead(ctx, d, meta) +} + +func resourceScalrSlackIntegrationRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + scalrClient := meta.(*scalr.Client) + integrationID := d.Id() + + log.Printf("[DEBUG] Read slack integration with ID: %s", integrationID) + slackIntegration, err := scalrClient.SlackIntegrations.Read(ctx, integrationID) + if err != nil { + log.Printf("[DEBUG] slack integration %s no longer exists", integrationID) + d.SetId("") + return nil + } + _ = d.Set("name", slackIntegration.Name) + _ = d.Set("channel_id", slackIntegration.ChannelId) + _ = d.Set("events", slackIntegration.Events) + _ = d.Set("account_id", slackIntegration.Account.ID) + + environmentIDs := make([]string, 0) + for _, environment := range slackIntegration.Environments { + environmentIDs = append(environmentIDs, environment.ID) + } + + _ = d.Set("environments", environmentIDs) + + wsIDs := make([]string, 0) + for _, ws := range slackIntegration.Workspaces { + wsIDs = append(wsIDs, ws.ID) + } + _ = d.Set("workspaces", wsIDs) + + return nil +} + +func resourceScalrSlackIntegrationUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + scalrClient := meta.(*scalr.Client) + options := scalr.SlackIntegrationUpdateOptions{} + + if d.HasChange("name") { + options.Name = scalr.String(d.Get("name").(string)) + } + + if d.HasChange("channel_id") { + options.ChannelId = scalr.String(d.Get("channel_id").(string)) + } + + if d.HasChange("events") { + events := parseEvents(d) + options.Events = events + } + + if d.HasChange("environments") { + envs := parseEnvironments(d) + options.Environments = envs + } + + workspaces := parseWorkspaces(d) + if workspaces != nil { + options.Workspaces = workspaces + } + + log.Printf("[DEBUG] Update slack integration: %s", d.Id()) + _, err := scalrClient.SlackIntegrations.Update(ctx, d.Id(), options) + if err != nil { + return diag.Errorf("Error updating slack integration %s: %v", d.Id(), err) + } + + return nil +} + +func resourceSlackIntegrationDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + scalrClient := meta.(*scalr.Client) + + log.Printf("[DEBUG] Delete slack integration: %s", d.Id()) + err := scalrClient.SlackIntegrations.Delete(ctx, d.Id()) + if err != nil { + if errors.Is(err, scalr.ErrResourceNotFound) { + return nil + } + return diag.Errorf("Error deleting slack integration %s: %v", d.Id(), err) + } + + return nil +} diff --git a/scalr/resource_scalr_slack_integration_test.go b/scalr/resource_scalr_slack_integration_test.go new file mode 100644 index 00000000..492159a3 --- /dev/null +++ b/scalr/resource_scalr_slack_integration_test.go @@ -0,0 +1,121 @@ +package scalr + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccSlackIntegration_basic(t *testing.T) { + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + + scalrClient, _ := createScalrClient() + slackConnection, err := scalrClient.SlackIntegrations.GetConnection(ctx, defaultAccount) + if err != nil { + t.Fatalf("Error fetching Slack connection: %v", err) + return + } + if slackConnection.ID == "" { + t.Skip("Scalr instance doesn't have working slack connection.") + } + }, + ProviderFactories: testAccProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccScalrSlackIntegrationConfig(), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("scalr_slack_integration.test", "id"), + resource.TestCheckResourceAttr( + "scalr_slack_integration.test", + "name", + "test-create", + ), + resource.TestCheckResourceAttr( + "scalr_slack_integration.test", + "channel_id", + "C123", + ), + resource.TestCheckResourceAttr( + "scalr_slack_integration.test", + "account_id", + defaultAccount, + ), + resource.TestCheckTypeSetElemAttr( + "scalr_slack_integration.test", + "events.*", + "run_approval_required", + ), + resource.TestCheckTypeSetElemAttr( + "scalr_slack_integration.test", + "events.*", + "run_errored", + ), + ), + }, + { + Config: testAccScalrSlackIntegrationUpdateConfig(), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("scalr_slack_integration.test", "id"), + resource.TestCheckResourceAttr( + "scalr_slack_integration.test", + "name", + "test-create2", + ), + resource.TestCheckResourceAttr( + "scalr_slack_integration.test", + "channel_id", + "C123", + ), + resource.TestCheckResourceAttr( + "scalr_slack_integration.test", + "account_id", + defaultAccount, + ), + resource.TestCheckTypeSetElemAttr( + "scalr_slack_integration.test", + "events.*", + "run_success", + ), + resource.TestCheckTypeSetElemAttr( + "scalr_slack_integration.test", + "events.*", + "run_errored", + ), + ), + }, + }, + }) +} + +func testAccScalrSlackIntegrationConfig() string { + return fmt.Sprintf(` +resource scalr_environment test { + name = "test-env-slack" + account_id = "%s" +} +resource "scalr_slack_integration" "test" { + name = "test-create" + account_id = scalr_environment.test.account_id + events = ["run_approval_required", "run_errored"] + channel_id = "C123" + environments = [scalr_environment.test.id] +}`, defaultAccount) +} +func testAccScalrSlackIntegrationUpdateConfig() string { + return fmt.Sprintf(` +resource scalr_environment test { + name = "test-env-slack" + account_id = "%s" +} +resource "scalr_slack_integration" "test" { + name = "test-create2" + account_id = scalr_environment.test.account_id + events = ["run_success", "run_errored"] + channel_id = "C123" + environments = [scalr_environment.test.id] +}`, defaultAccount) +}