diff --git a/CHANGELOG.md b/CHANGELOG.md index 876d5416..0df90c43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- `scalr_vcs_provider`: added new attribute `agent_pool_id` ([#233](https://github.com/Scalr/terraform-provider-scalr/pull/233)) +- `data.scalr_vcs_provider`: added new attribute `agent_pool_id` ([#233](https://github.com/Scalr/terraform-provider-scalr/pull/233)) +- `scalr_agent_pool`: added new attribute `vcs_enabled` ([#233](https://github.com/Scalr/terraform-provider-scalr/pull/233)) +- `data.scalr_agent_pool`: added new attribute `vcs_enabled` ([#233](https://github.com/Scalr/terraform-provider-scalr/pull/232)) + +### Fixed + +- `data.scalr_module_version`: if there are several module versions with the same version, select the version that has the 'is-root-module' flag set to true. ([#229](https://github.com/Scalr/terraform-provider-scalr/pull/229)) + +### Required + +- scalr-server >= `8.64.0` + +## [1.0.5] - 2023-04-21 + +### Changed + - `data.scalr_workspace`: added new optional `id` argument, `name` became optional, one of or both can be specified ([#228](https://github.com/Scalr/terraform-provider-scalr/pull/228)) - `data.scalr_role`: added new optional `id` argument, `name` became optional, one of or both can be specified ([#228](https://github.com/Scalr/terraform-provider-scalr/pull/228)) - `data.scalr_iam_team`: added new optional `id` argument, `name` became optional, one of or both can be specified ([#228](https://github.com/Scalr/terraform-provider-scalr/pull/228)) @@ -23,11 +40,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `data.scalr_environment`: optional `id` and `name` arguments can be used together ([#228](https://github.com/Scalr/terraform-provider-scalr/pull/228)) - `data.scalr_endpoint`: optional `id` and `name` arguments can be used together ([#228](https://github.com/Scalr/terraform-provider-scalr/pull/228)) - `data.scalr_service_account`: optional `id` and `email` arguments can be used together ([#228](https://github.com/Scalr/terraform-provider-scalr/pull/228)) -- `scalr_vcs_provider`: added new attribute `agent_pool_id` ([#233](https://github.com/Scalr/terraform-provider-scalr/pull/233)) -- `data.scalr_vcs_provider`: added new attribute `agent_pool_id` ([#233](https://github.com/Scalr/terraform-provider-scalr/pull/233)) -- `scalr_agent_pool`: added new attribute `vcs_enabled` ([#233](https://github.com/Scalr/terraform-provider-scalr/pull/233)) -- `data.scalr_agent_pool`: added new attribute `vcs_enabled` ([#233](https://github.com/Scalr/terraform-provider-scalr/pull/232)) - `scalr_workspace_run_schedule`: make `apply-schedule` and `destroy-schedule` attributes nullable ([#231](https://github.com/Scalr/terraform-provider-scalr/pull/231)) +- `scalr_webhook`: ([#234](https://github.com/Scalr/terraform-provider-scalr/pull/234)) + - endpoint arguments are now included in the webhook resource: `url`, `secret_key`, `timeout` and `max_attempts` + This manifests the new way webhook integration will work further on, deprecating the `endpoint_id` argument + and merging the endpoint information into the webhook. During the deprecation period both old-style and new-style + webhooks are supported. The support for old-style webhooks will be dropped in the next major release. + - added new optional `header` argument (new-style webhooks only) - additional headers to set in the webhook request + - added new optional `environments` argument (new-style webhooks only) - environments that the webhook is shared to +- `data.scalr_webhook`: extended with new attributes from new-style webhook - `url`, `secret_key`, `timeout`, +`max_attempts`, `header`, `environments` ([#234](https://github.com/Scalr/terraform-provider-scalr/pull/234)) ### Fixed @@ -35,6 +57,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `scalr_vcs_provider`: fix handling resource destroy when resource no longer exists ([#235](https://github.com/Scalr/terraform-provider-scalr/pull/235)) - `scalr_webhook`: fix handling resource destroy when resource no longer exists ([#235](https://github.com/Scalr/terraform-provider-scalr/pull/235)) +### Deprecated + +- `scalr_endpoint` is deprecated and will be removed in the next major version ([#234](https://github.com/Scalr/terraform-provider-scalr/pull/234)) +- `data.scalr_endpoint` is deprecated and will be removed in the next major version ([#234](https://github.com/Scalr/terraform-provider-scalr/pull/234)) +- `scalr_webhook`: ([#234](https://github.com/Scalr/terraform-provider-scalr/pull/234)) + - attribute `endpoint_id` is deprecated + - attribute `environment_id` is deprecated + - attribute `workspace_id` is deprecated +- `data.scalr_webhook`: ([#234](https://github.com/Scalr/terraform-provider-scalr/pull/234)) + - attribute `endpoint_id` is deprecated + - attribute `environment_id` is deprecated + - attribute `workspace_id` is deprecated + +### Required + +- scalr-server >= `8.63.0` + ## [1.0.4] - 2023-03-13 ### Fixed @@ -621,7 +660,8 @@ Requires Scalr 8.0.1-beta.20200625 at least - Initial release. -[Unreleased]: https://github.com/Scalr/terraform-provider-scalr/compare/v1.0.4...HEAD +[Unreleased]: https://github.com/Scalr/terraform-provider-scalr/compare/v1.0.5...HEAD +[1.0.5]: https://github.com/Scalr/terraform-provider-scalr/releases/tag/v1.0.5 [1.0.4]: https://github.com/Scalr/terraform-provider-scalr/releases/tag/v1.0.4 [1.0.3]: https://github.com/Scalr/terraform-provider-scalr/releases/tag/v1.0.3 [1.0.2]: https://github.com/Scalr/terraform-provider-scalr/releases/tag/v1.0.2 diff --git a/docs/data-sources/scalr_endpoint.md b/docs/data-sources/scalr_endpoint.md index 251f9c32..fdbfae7e 100644 --- a/docs/data-sources/scalr_endpoint.md +++ b/docs/data-sources/scalr_endpoint.md @@ -3,6 +3,8 @@ Retrieves the details of a webhook endpoint. +> **WARNING:** This datasource is deprecated and will be removed in the next major version. + ## Example Usage ```hcl diff --git a/docs/data-sources/scalr_webhook.md b/docs/data-sources/scalr_webhook.md index b900be11..3afb2b0a 100644 --- a/docs/data-sources/scalr_webhook.md +++ b/docs/data-sources/scalr_webhook.md @@ -32,7 +32,18 @@ Arguments `id` and `name` are both optional, specify at least one of them to obt All arguments plus: * `enabled` - Boolean indicates if the webhook is enabled. -* `endpoint_id` - ID of the endpoint, in the format `ep-`. -* `environment_id` - ID of the environment, in the format `env-`. +* `endpoint_id` - (Deprecated) ID of the endpoint, in the format `ep-`. +* `environment_id` - (Deprecated) ID of the environment, in the format `env-`. +* `workspace_id` - (Deprecated) ID of the workspace, in the format `ws-`. * `events` - List of event IDs. * `last_triggered_at` - Date/time when webhook was last triggered. +* `url` - Endpoint URL. +* `secret_key` - Secret key to sign the webhook payload. +* `max_attempts` - Max delivery attempts of the payload. +* `timeout` - Endpoint timeout (in seconds). +* `environments` - The list of environment identifiers that the webhook is shared to, +or `["*"]` if shared with all environments. +* `header` - (Set of header objects) Additional headers to set in the webhook request. + The `header` block item contains: + * `name` - The name of the header. + * `value` - The value of the header. diff --git a/docs/resources/scalr_endpoint.md b/docs/resources/scalr_endpoint.md index c5b72bb4..f2f2b675 100644 --- a/docs/resources/scalr_endpoint.md +++ b/docs/resources/scalr_endpoint.md @@ -3,6 +3,8 @@ Manage the state of endpoints in Scalr. Create, update and destroy +> **WARNING:** This resource is deprecated and will be removed in the next major version. + ## Example Usage Basic usage: diff --git a/docs/resources/scalr_webhook.md b/docs/resources/scalr_webhook.md index 7669a25d..5e50b2ec 100644 --- a/docs/resources/scalr_webhook.md +++ b/docs/resources/scalr_webhook.md @@ -21,11 +21,22 @@ resource "scalr_webhook" "example" { ## Argument Reference * `name` - (Required) Name of the webhook. +* `account_id` - (Optional) ID of the account, in the format `acc-`. * `enabled` - (Optional) Set (true/false) to enable/disable the webhook. -* `endpoint_id` - (Required) ID of the endpoint, in the format `ep-`. -* `workspace_id` - (Optional) ID of the workspace, in the format `ws-`. -* `environment_id` - (Required if workspace ID is empty) ID of the environment, in the format `env-`. +* `endpoint_id` - (Deprecated) ID of the endpoint, in the format `ep-`. +* `workspace_id` - (Deprecated) ID of the workspace, in the format `ws-`. +* `environment_id` - (Deprecated) ID of the environment, in the format `env-`. * `events` - (Required) List of event IDs. +* `url` - (Optional) Endpoint URL. Required if `endpoint_id` is not set. +* `secret_key` - (Optional) Secret key to sign the webhook payload. +* `max_attempts` - (Optional) Max delivery attempts of the payload. +* `timeout` - (Optional) Endpoint timeout (in seconds). +* `environments` - (Optional) The list of environment identifiers that the webhook is shared to. +Use `["*"]` to share with all environments. +* `header` - (Optional, set of header objects) Additional headers to set in the webhook request. + The `header` block item contains: + * `name` - The name of the header. + * `value` - The value of the header. ## Attributes diff --git a/go.mod b/go.mod index 60ff779c..05854e22 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-20230419170500-fde618c901e4 + github.com/scalr/go-scalr v0.0.0-20230424091123-5437ed6e096e ) require ( diff --git a/go.sum b/go.sum index 9478ee0c..269fa201 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-20230419170500-fde618c901e4 h1:Nqaw2N8N7djvSBK02g4MFTcIo9fUZaLeJS5kZmshhe4= -github.com/scalr/go-scalr v0.0.0-20230419170500-fde618c901e4/go.mod h1:p34SHb25YRvbgft7SUjSDYESeoQhWzAlxGXId/BbaSE= +github.com/scalr/go-scalr v0.0.0-20230424091123-5437ed6e096e h1:dnFu7ozDG1exX+KnYqtfStR/nYobB2k1OhqT+jIEVgg= +github.com/scalr/go-scalr v0.0.0-20230424091123-5437ed6e096e/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/data_source_scalr_endpoint.go b/scalr/data_source_scalr_endpoint.go index 70d3b2a3..d5994cd2 100644 --- a/scalr/data_source_scalr_endpoint.go +++ b/scalr/data_source_scalr_endpoint.go @@ -12,6 +12,9 @@ import ( func dataSourceScalrEndpoint() *schema.Resource { return &schema.Resource{ + DeprecationMessage: "Datasource `scalr_endpoint` is deprecated, the endpoint information" + + " is included in the `scalr_webhook` resource.", + ReadContext: dataSourceScalrEndpointRead, Schema: map[string]*schema.Schema{ diff --git a/scalr/data_source_scalr_module_version.go b/scalr/data_source_scalr_module_version.go index cadf1fca..bdfb9894 100644 --- a/scalr/data_source_scalr_module_version.go +++ b/scalr/data_source_scalr_module_version.go @@ -7,6 +7,7 @@ import ( "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" ) @@ -19,8 +20,9 @@ func dataSourceModuleVersion() *schema.Resource { Required: true, }, "version": { - Type: schema.TypeString, - Optional: true, + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringIsNotWhiteSpace, }, "id": { Type: schema.TypeString, @@ -46,7 +48,19 @@ func dataSourceModuleVersionRead(ctx context.Context, d *schema.ResourceData, me var version string if v, ok := d.GetOk("version"); ok { version = v.(string) - mv, err = scalrClient.ModuleVersions.ReadBySemanticVersion(ctx, module.ID, version) + ml, err := scalrClient.ModuleVersions.List(ctx, scalr.ModuleVersionListOptions{Module: module.ID, Version: &version}) + if err != nil { + return diag.Errorf("Could not find module %s with version %s", module.ID, version) + } + for _, item := range ml.Items { + if item.IsRootModule { + mv = item + break + } + } + if mv == nil { + return diag.Errorf("Could not find module with source %s and version %s", source, version) + } } else { if module.ModuleVersion == nil { return diag.FromErr(errors.New("The module has no version tags")) @@ -55,9 +69,6 @@ func dataSourceModuleVersionRead(ctx context.Context, d *schema.ResourceData, me } if err != nil { - if errors.Is(err, scalr.ErrResourceNotFound) { - return diag.Errorf("Could not find module with source %s and version %s", source, version) - } return diag.Errorf("Error retrieving module version: %v", err) } log.Printf("[DEBUG] Download module version by source %s version: %s", source, version) diff --git a/scalr/data_source_scalr_webhook.go b/scalr/data_source_scalr_webhook.go index 8b9a0d44..34772ddd 100644 --- a/scalr/data_source_scalr_webhook.go +++ b/scalr/data_source_scalr_webhook.go @@ -50,6 +50,8 @@ func dataSourceScalrWebhook() *schema.Resource { "endpoint_id": { Type: schema.TypeString, Computed: true, + Deprecated: "Attribute `endpoint_id` is deprecated, the endpoint information" + + " is included in the `scalr_webhook` resource.", }, "account_id": { @@ -62,13 +64,59 @@ func dataSourceScalrWebhook() *schema.Resource { "environment_id": { Type: schema.TypeString, Computed: true, - Optional: true, + Deprecated: "The attribute `environment_id` is deprecated. The webhook is created on the" + + " account level and the environments to which it is exposed" + + " are controlled by the `environments` attribute.", }, "workspace_id": { + Type: schema.TypeString, + Computed: true, + Deprecated: "The attribute `workspace_id` is deprecated.", + }, + + "url": { Type: schema.TypeString, Computed: true, - Optional: true, + }, + + "secret_key": { + Type: schema.TypeString, + Computed: true, + Sensitive: true, + }, + + "timeout": { + Type: schema.TypeInt, + Computed: true, + }, + + "max_attempts": { + Type: schema.TypeInt, + Computed: true, + }, + + "header": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Computed: true, + }, + "value": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + + "environments": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, }, }, } @@ -82,16 +130,18 @@ func dataSourceScalrWebhookRead(ctx context.Context, d *schema.ResourceData, met webhookName := d.Get("name").(string) accountID := d.Get("account_id").(string) - var webhook *scalr.Webhook + var newWebhook *scalr.WebhookIntegration var err error log.Printf("[DEBUG] Read configuration of webhook with ID '%s' and name '%s'", webhookID, webhookName) + // First read from new API by ID or search by name, as the new API + // works both with old-style and new-style webhooks if webhookID != "" { - webhook, err = scalrClient.Webhooks.Read(ctx, webhookID) + newWebhook, err = scalrClient.WebhookIntegrations.Read(ctx, webhookID) if err != nil { return diag.Errorf("Error retrieving webhook: %v", err) } - if webhookName != "" && webhookName != webhook.Name { + if webhookName != "" && webhookName != newWebhook.Name { return diag.Errorf("Could not find webhook with ID '%s' and name '%s'", webhookID, webhookName) } } else { @@ -99,38 +149,72 @@ func dataSourceScalrWebhookRead(ctx context.Context, d *schema.ResourceData, met Name: &webhookName, Account: &accountID, } - webhook, err = GetWebhookByName(ctx, options, scalrClient) + newWebhook, err = GetWebhookByName(ctx, options, scalrClient) if err != nil { return diag.Errorf("Error retrieving webhook: %v", err) } - if webhookID != "" && webhookID != webhook.ID { + if webhookID != "" && webhookID != newWebhook.ID { return diag.Errorf("Could not find webhook with ID '%s' and name '%s'", webhookID, webhookName) } } + // Having the webhook found, read from old API then + // to populate deprecated fields available only in old API + oldWebhook, err := scalrClient.Webhooks.Read(ctx, newWebhook.ID) + if err != nil { + return diag.Errorf("Error retrieving webhook: %v", err) + } // Update the config. - _ = d.Set("name", webhook.Name) - _ = d.Set("enabled", webhook.Enabled) - _ = d.Set("last_triggered_at", webhook.LastTriggeredAt) + _ = d.Set("name", newWebhook.Name) + _ = d.Set("account_id", newWebhook.Account.ID) + _ = d.Set("enabled", newWebhook.Enabled) + _ = d.Set("last_triggered_at", newWebhook.LastTriggeredAt) + _ = d.Set("url", newWebhook.Url) + _ = d.Set("secret_key", newWebhook.SecretKey) + _ = d.Set("timeout", newWebhook.Timeout) + _ = d.Set("max_attempts", newWebhook.MaxAttempts) events := make([]string, 0) - if webhook.Events != nil { - for _, event := range webhook.Events { + if newWebhook.Events != nil { + for _, event := range newWebhook.Events { events = append(events, event.ID) } } _ = d.Set("events", events) - if webhook.Workspace != nil { - _ = d.Set("workspace_id", webhook.Workspace.ID) + headers := make([]map[string]interface{}, 0) + if newWebhook.Headers != nil { + for _, header := range newWebhook.Headers { + headers = append(headers, map[string]interface{}{ + "name": header.Name, + "value": header.Value, + }) + } } - if webhook.Environment != nil { - _ = d.Set("environment_id", webhook.Environment.ID) + _ = d.Set("header", headers) + + if newWebhook.IsShared { + _ = d.Set("environments", []string{"*"}) + } else { + environmentIDs := make([]string, 0) + for _, environment := range newWebhook.Environments { + environmentIDs = append(environmentIDs, environment.ID) + } + _ = d.Set("environments", environmentIDs) + } + + // Add deprecated attributes from old-style webhook + if oldWebhook.Workspace != nil { + _ = d.Set("workspace_id", oldWebhook.Workspace.ID) + } + if oldWebhook.Environment != nil { + _ = d.Set("environment_id", oldWebhook.Environment.ID) } - if webhook.Endpoint != nil { - _ = d.Set("endpoint_id", webhook.Endpoint.ID) + if oldWebhook.Endpoint != nil { + _ = d.Set("endpoint_id", oldWebhook.Endpoint.ID) } - d.SetId(webhook.ID) + + d.SetId(newWebhook.ID) return nil } diff --git a/scalr/data_source_scalr_webhook_test.go b/scalr/data_source_scalr_webhook_test.go index 48f86d0d..ce8b1caa 100644 --- a/scalr/data_source_scalr_webhook_test.go +++ b/scalr/data_source_scalr_webhook_test.go @@ -37,7 +37,7 @@ func TestAccWebhookDataSource_basic(t *testing.T) { PlanOnly: true, }, { - Config: testAccWebhookDataSourceConfig(rInt), + Config: testAccOldWebhookDataSourceConfig(rInt), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr( "data.scalr_webhook.test", "name", fmt.Sprintf("webhook-test-%d", rInt)), @@ -47,6 +47,55 @@ func TestAccWebhookDataSource_basic(t *testing.T) { "data.scalr_webhook.test", "endpoint_id"), resource.TestCheckResourceAttrSet( "data.scalr_webhook.test", "workspace_id"), + // Attributes from related endpoint + resource.TestCheckResourceAttr( + "data.scalr_webhook.test", "url", "https://example.com/webhook"), + resource.TestCheckResourceAttrSet( + "data.scalr_webhook.test", "secret_key"), + resource.TestCheckResourceAttr( + "data.scalr_webhook.test", "timeout", "15"), + resource.TestCheckResourceAttr( + "data.scalr_webhook.test", "max_attempts", "3"), + // New attributes + resource.TestCheckResourceAttr( + "data.scalr_webhook.test", "header.#", "0"), + resource.TestCheckResourceAttr( + "data.scalr_webhook.test", "environments.#", "1"), + resource.TestCheckResourceAttrPair( + "data.scalr_webhook.test", + "environments.0", + "scalr_environment.test", + "id"), + ), + }, + { + Config: testAccWebhookDataSourceConfig(rInt), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr( + "data.scalr_webhook.test-new", "name", fmt.Sprintf("webhook-test-new-%d", rInt)), + resource.TestCheckResourceAttr( + "data.scalr_webhook.test-new", "enabled", "false"), + resource.TestCheckResourceAttr( + "data.scalr_webhook.test-new", "url", "https://example.com/webhook"), + resource.TestCheckResourceAttrSet( + "data.scalr_webhook.test-new", "secret_key"), + resource.TestCheckResourceAttrSet( + "data.scalr_webhook.test-new", "timeout"), + resource.TestCheckResourceAttr( + "data.scalr_webhook.test-new", "max_attempts", "2"), + resource.TestCheckResourceAttr( + "data.scalr_webhook.test-new", "header.#", "2"), + resource.TestCheckResourceAttr( + "data.scalr_webhook.test-new", "environments.#", "1"), + resource.TestCheckResourceAttrPair( + "data.scalr_webhook.test-new", + "environments.0", + "scalr_environment.test-new", + "id"), + // Deprecated attributes + resource.TestCheckNoResourceAttr("data.scalr_webhook.test-new", "endpoint_id"), + resource.TestCheckNoResourceAttr("data.scalr_webhook.test-new", "workspace_id"), + resource.TestCheckNoResourceAttr("data.scalr_webhook.test-new", "environment_id"), ), }, { @@ -89,7 +138,7 @@ func TestAccWebhookDataSource_basic(t *testing.T) { }) } -func testAccWebhookDataSourceConfig(rInt int) string { +func testAccOldWebhookDataSourceConfig(rInt int) string { return fmt.Sprintf(` resource scalr_environment test { name = "test-env-%[1]d" @@ -102,7 +151,7 @@ resource scalr_workspace test { } resource scalr_endpoint test { - name = "test endpoint-%[1]d" + name = "test-endpoint-%[1]d" timeout = 15 max_attempts = 3 url = "https://example.com/webhook" @@ -122,6 +171,36 @@ data scalr_webhook test { }`, rInt, defaultAccount) } +func testAccWebhookDataSourceConfig(rInt int) string { + return fmt.Sprintf(` +resource scalr_environment test-new { + name = "test-env-new-%[1]d" + account_id = "%s" +} + +resource scalr_webhook test-new { + account_id = "%[2]s" + enabled = false + name = "webhook-test-new-%[1]d" + events = ["run:completed", "run:errored"] + environments = [scalr_environment.test-new.id] + url = "https://example.com/webhook" + max_attempts = 2 + header { + name = "header-1" + value = "value-1" + } + header { + name = "header-2" + value = "value-2" + } +} + +data scalr_webhook test-new { + id = scalr_webhook.test-new.id +}`, rInt, defaultAccount) +} + func testAccWebhookDataSourceAccessByNameConfig(rInt int) string { return fmt.Sprintf(` resource scalr_environment test { diff --git a/scalr/helpers.go b/scalr/helpers.go index 8bccae39..3b606245 100644 --- a/scalr/helpers.go +++ b/scalr/helpers.go @@ -109,12 +109,12 @@ type GetWebhookByNameOptions struct { Account *string } -func GetWebhookByName(ctx context.Context, options GetWebhookByNameOptions, scalrClient *scalr.Client) (*scalr.Webhook, error) { - listOptions := scalr.WebhookListOptions{ - Name: options.Name, +func GetWebhookByName(ctx context.Context, options GetWebhookByNameOptions, scalrClient *scalr.Client) (*scalr.WebhookIntegration, error) { + listOptions := scalr.WebhookIntegrationListOptions{ + Query: options.Name, Account: options.Account, } - whl, err := scalrClient.Webhooks.List(ctx, listOptions) + whl, err := scalrClient.WebhookIntegrations.List(ctx, listOptions) if err != nil { return nil, fmt.Errorf("Error retrieving webhooks: %v", err) } @@ -123,7 +123,7 @@ func GetWebhookByName(ctx context.Context, options GetWebhookByNameOptions, scal return nil, fmt.Errorf("Webhook with name '%s' not found or user unauthorized", *options.Name) } - var matchedWebhooks []*scalr.Webhook + var matchedWebhooks []*scalr.WebhookIntegration // filter in endpoint search endpoints that contains query string, this is why we need to do exact match on our side. for _, wh := range whl.Items { diff --git a/scalr/resource_scalr_endpoint.go b/scalr/resource_scalr_endpoint.go index fe0f64cf..bbfe5825 100644 --- a/scalr/resource_scalr_endpoint.go +++ b/scalr/resource_scalr_endpoint.go @@ -12,6 +12,8 @@ import ( func resourceScalrEndpoint() *schema.Resource { return &schema.Resource{ + DeprecationMessage: "Resource `scalr_endpoint` is deprecated, the endpoint information" + + " is included in the `scalr_webhook` resource.", CreateContext: resourceScalrEndpointCreate, ReadContext: resourceScalrEndpointRead, UpdateContext: resourceScalrEndpointUpdate, diff --git a/scalr/resource_scalr_webhook.go b/scalr/resource_scalr_webhook.go index 4f837f58..30213cc4 100644 --- a/scalr/resource_scalr_webhook.go +++ b/scalr/resource_scalr_webhook.go @@ -4,22 +4,14 @@ import ( "context" "errors" "fmt" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "log" - "strings" + "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" ) -var ( - eventDefinitions = map[string]bool{ - "run:completed": true, - "run:errored": true, - "run:needs_attention": true, - } -) - func resourceScalrWebhook() *schema.Resource { return &schema.Resource{ CreateContext: resourceScalrWebhookCreate, @@ -29,17 +21,26 @@ func resourceScalrWebhook() *schema.Resource { Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, - + CustomizeDiff: forceRecreateIf(), + SchemaVersion: 1, + StateUpgraders: []schema.StateUpgrader{ + { + Version: 0, + Type: resourceScalrWebhookResourceV0().CoreConfigSchema().ImpliedType(), + Upgrade: resourceScalrWebhookStateUpgradeV0, + }, + }, Schema: map[string]*schema.Schema{ "name": { - Type: schema.TypeString, - Required: true, + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: validation.ToDiagFunc(validation.StringIsNotWhiteSpace), }, "enabled": { Type: schema.TypeBool, Optional: true, - Computed: true, + Default: true, }, "last_triggered_at": { @@ -48,30 +49,139 @@ func resourceScalrWebhook() *schema.Resource { }, "events": { - Type: schema.TypeList, - Elem: &schema.Schema{Type: schema.TypeString}, + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateDiagFunc: validation.ToDiagFunc( + validation.StringInSlice( + []string{"run:completed", "run:errored", "run:needs_attention"}, + false, + ), + ), + }, Required: true, + MinItems: 1, }, "endpoint_id": { Type: schema.TypeString, - Required: true, + Optional: true, + Deprecated: "Attribute `endpoint_id` is deprecated, please set the endpoint information" + + " in the webhook itself.", + // If `endpoint_id` is set in configuration, we consider this an old-style webhook, + // therefore using old API to create it. + // That's why it conflicts with the fields that are only in new-style webhooks, so + // user has two distinct sets of arguments for old and new webhooks. + // One of `endpoint_id` or `url` must be set, and this defines which style will be chosen. + ConflictsWith: []string{ + "url", "secret_key", "timeout", "max_attempts", "header", "environments", "account_id", + }, + AtLeastOneOf: []string{"url"}, }, - "workspace_id": { - Type: schema.TypeString, + "url": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateDiagFunc: validation.ToDiagFunc(validation.StringIsNotWhiteSpace), + ConflictsWith: []string{"endpoint_id", "workspace_id", "environment_id"}, + }, + + "secret_key": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Sensitive: true, + }, + + "timeout": { + Type: schema.TypeInt, + Optional: true, + Default: 15, + ValidateDiagFunc: validation.ToDiagFunc(validation.IntBetween(0, 365*24*3600)), + }, + + "max_attempts": { + Type: schema.TypeInt, + Optional: true, + Default: 3, + ValidateDiagFunc: validation.ToDiagFunc(validation.IntBetween(1, 1000)), + }, + + "header": { + Type: schema.TypeSet, Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotWhiteSpace, + }, + "value": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotWhiteSpace, + }, + }, + }, + }, + + "account_id": { + Type: schema.TypeString, + Optional: true, + Computed: true, + DefaultFunc: webhookAccountIDDefaultFunc, + ForceNew: true, + }, + + "workspace_id": { + Type: schema.TypeString, + Optional: true, + Deprecated: "The attribute `workspace_id` is deprecated.", }, "environment_id": { Type: schema.TypeString, Optional: true, Computed: true, + Deprecated: "The attribute `environment_id` is deprecated. The webhook is created on the" + + " account level and the environments to which it is exposed" + + " are controlled by the `environments` attribute.", + ConflictsWith: []string{"environments"}, + }, + + "environments": { + Type: schema.TypeSet, + Optional: true, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, }, }, } } +func forceRecreateIf() schema.CustomizeDiffFunc { + // Destroy and recreate a webhook when `endpoint_id` has changed from having a value to unset, + // which means switching from old-style to new-style webhook - and vice versa, + // so we don't mix both style. + return func(ctx context.Context, d *schema.ResourceDiff, meta interface{}) error { + oldId, newId := d.GetChange("endpoint_id") + if (oldId.(string) == "") != (newId.(string) == "") { + return d.ForceNew("endpoint_id") + } + return nil + } +} + +// webhookAccountIDDefaultFunc returns default account id, if present. +// It won't return an error, as `account_id` is optional and computed +// for old-style webhooks. +func webhookAccountIDDefaultFunc() (interface{}, error) { + accID, _ := getDefaultScalrAccountID() + return accID, nil +} + // remove after https://scalr-labs.atlassian.net/browse/SCALRCORE-16234 func getResourceScope(ctx context.Context, scalrClient *scalr.Client, workspaceID string, environmentID string) (*scalr.Workspace, *scalr.Environment, *scalr.Account, error) { @@ -110,20 +220,6 @@ func getResourceScope(ctx context.Context, scalrClient *scalr.Client, workspaceI return workspace, environment, account, nil } -func validateEventDefinitions(eventName string) error { - if val, ok := eventDefinitions[eventName]; ok && val { - return nil - } - i := 0 - eventDefinitionsQuoted := make([]string, len(eventDefinitions)) - for eventDefinition := range eventDefinitions { - eventDefinitionsQuoted[i] = fmt.Sprintf("'%s'", eventDefinition) - i++ - } - return fmt.Errorf( - "Invalid value for events '%s'. Allowed values: %s", eventName, strings.Join(eventDefinitionsQuoted, ", ")) -} - func parseEventDefinitions(d *schema.ResourceData) ([]*scalr.EventDefinition, error) { eventDefinitions := make([]*scalr.EventDefinition, 0) @@ -135,18 +231,26 @@ func parseEventDefinitions(d *schema.ResourceData) ([]*scalr.EventDefinition, er for _, eventID := range eventIds { id := eventID.(string) - if err := validateEventDefinitions(id); err != nil { - return nil, err - } eventDefinitions = append(eventDefinitions, &scalr.EventDefinition{ID: id}) } return eventDefinitions, nil } -func resourceScalrWebhookCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - scalrClient := meta.(*scalr.Client) +func parseHeaders(d *schema.ResourceData) []*scalr.WebhookHeader { + headers := d.Get("header").(*schema.Set) + headerValues := make([]*scalr.WebhookHeader, 0) + for _, headerI := range headers.List() { + header := headerI.(map[string]interface{}) + headerValues = append(headerValues, &scalr.WebhookHeader{ + Name: header["name"].(string), + Value: header["value"].(string), + }) + } + return headerValues +} +func createOldWebhook(ctx context.Context, d *schema.ResourceData, scalrClient *scalr.Client) error { // Get attributes. name := d.Get("name").(string) endpointID := d.Get("endpoint_id").(string) @@ -155,12 +259,12 @@ func resourceScalrWebhookCreate(ctx context.Context, d *schema.ResourceData, met workspace, environment, account, err := getResourceScope(ctx, scalrClient, workspaceID, environmentID) if err != nil { - return diag.FromErr(err) + return err } eventDefinitions, err := parseEventDefinitions(d) if err != nil { - return diag.FromErr(err) + return err } // Create a new options struct. @@ -174,38 +278,155 @@ func resourceScalrWebhookCreate(ctx context.Context, d *schema.ResourceData, met Account: account, } + if workspaceID != "" { + options.Workspace = &scalr.Workspace{ID: workspaceID} + } + if environmentID != "" { + options.Environment = &scalr.Environment{ID: environmentID} + } + if environmentID != "" { + options.Environment = &scalr.Environment{ID: environmentID} + } + log.Printf("[DEBUG] Create webhook: %s", name) webhook, err := scalrClient.Webhooks.Create(ctx, options) if err != nil { - return diag.Errorf("Error creating webhook %s: %v", name, err) + return fmt.Errorf("Error creating webhook %s: %v", name, err) } d.SetId(webhook.ID) + return nil +} - return resourceScalrWebhookRead(ctx, d, meta) +func createNewWebhook(ctx context.Context, d *schema.ResourceData, scalrClient *scalr.Client) error { + name := d.Get("name").(string) + accountId := d.Get("account_id").(string) + + if accountId == "" { + return fmt.Errorf("Attribute `account_id` is required when creating new-style webhook") + } + + eventDefinitions, err := parseEventDefinitions(d) + if err != nil { + return err + } + + options := scalr.WebhookIntegrationCreateOptions{ + Name: &name, + Url: scalr.String(d.Get("url").(string)), + Account: &scalr.Account{ID: accountId}, + Events: eventDefinitions, + Enabled: scalr.Bool(d.Get("enabled").(bool)), + Timeout: scalr.Int(d.Get("timeout").(int)), + MaxAttempts: scalr.Int(d.Get("max_attempts").(int)), + } + + if secretKey, ok := d.GetOk("secret_key"); ok { + options.SecretKey = scalr.String(secretKey.(string)) + } + + if environmentsI, ok := d.GetOk("environments"); ok { + environments := environmentsI.(*schema.Set).List() + if (len(environments) == 1) && (environments[0].(string) == "*") { + options.IsShared = scalr.Bool(true) + } else if len(environments) > 0 { + environmentValues := make([]*scalr.Environment, 0) + for _, env := range environments { + if env.(string) == "*" { + return fmt.Errorf( + "You cannot simultaneously enable the webhook for all and a limited list of environments. Please remove either wildcard or environment identifiers.", + ) + } + environmentValues = append(environmentValues, &scalr.Environment{ID: env.(string)}) + } + options.Environments = environmentValues + } + } + + if _, ok := d.GetOk("header"); ok { + options.Headers = parseHeaders(d) + } + + webhook, err := scalrClient.WebhookIntegrations.Create(ctx, options) + if err != nil { + return fmt.Errorf("Error creating webhook %s: %v", name, err) + } + + d.SetId(webhook.ID) + + return nil } -func resourceScalrWebhookRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { +func resourceScalrWebhookCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { scalrClient := meta.(*scalr.Client) + endpointID := d.Get("endpoint_id").(string) - // Get the ID + var err error + // Here the old method is kept and is used to create old-style webhooks (with `endpoint_id` attribute set). + // After deprecation period it should be easy to remove it completely. + // Same for updating a webhook. + if endpointID != "" { + err = createOldWebhook(ctx, d, scalrClient) + } else { + err = createNewWebhook(ctx, d, scalrClient) + } + if err != nil { + return diag.FromErr(err) + } + + return resourceScalrWebhookRead(ctx, d, meta) +} + +func readOldWebhook(ctx context.Context, d *schema.ResourceData, scalrClient *scalr.Client) error { webhookID := d.Id() - log.Printf("[DEBUG] Read endpoint with ID: %s", webhookID) webhook, err := scalrClient.Webhooks.Read(ctx, webhookID) if err != nil { if errors.Is(err, scalr.ErrResourceNotFound) { - log.Printf("[DEBUG] Webhook %s no longer exists", webhookID) - d.SetId("") - return nil + return fmt.Errorf("Could not find webhook %s: %v", webhookID, err) } - return diag.Errorf("Error retrieving webhook: %v", err) + return fmt.Errorf("Error retrieving webhook: %v", err) + } + + if webhook.Workspace != nil { + _ = d.Set("workspace_id", webhook.Workspace.ID) + } else { + _ = d.Set("workspace_id", nil) + } + if webhook.Environment != nil { + _ = d.Set("environment_id", webhook.Environment.ID) + } else { + _ = d.Set("environment_id", nil) + } + if webhook.Endpoint != nil { + _ = d.Set("endpoint_id", webhook.Endpoint.ID) + } else { + _ = d.Set("endpoint_id", nil) + } + + return nil +} + +func readNewWebhook(ctx context.Context, d *schema.ResourceData, scalrClient *scalr.Client) error { + webhookID := d.Id() + + webhook, err := scalrClient.WebhookIntegrations.Read(ctx, webhookID) + if err != nil { + if errors.Is(err, scalr.ErrResourceNotFound) { + return fmt.Errorf("Could not find webhook %s: %v", webhookID, err) + } + return fmt.Errorf("Error retrieving webhook: %v", err) } // Update the config. _ = d.Set("name", webhook.Name) + _ = d.Set("account_id", webhook.Account.ID) _ = d.Set("enabled", webhook.Enabled) _ = d.Set("last_triggered_at", webhook.LastTriggeredAt) + _ = d.Set("url", webhook.Url) + _ = d.Set("secret_key", webhook.SecretKey) + _ = d.Set("timeout", webhook.Timeout) + _ = d.Set("max_attempts", webhook.MaxAttempts) events := make([]string, 0) if webhook.Events != nil { @@ -215,25 +436,50 @@ func resourceScalrWebhookRead(ctx context.Context, d *schema.ResourceData, meta } _ = d.Set("events", events) - if webhook.Workspace != nil { - _ = d.Set("workspace_id", webhook.Workspace.ID) - } - if webhook.Environment != nil { - _ = d.Set("environment_id", webhook.Environment.ID) + headers := make([]map[string]interface{}, 0) + if webhook.Headers != nil { + for _, header := range webhook.Headers { + headers = append(headers, map[string]interface{}{ + "name": header.Name, + "value": header.Value, + }) + } } - if webhook.Endpoint != nil { - _ = d.Set("endpoint_id", webhook.Endpoint.ID) + _ = d.Set("header", headers) + + if webhook.IsShared { + _ = d.Set("environments", []string{"*"}) + } else { + environmentIDs := make([]string, 0) + for _, environment := range webhook.Environments { + environmentIDs = append(environmentIDs, environment.ID) + } + _ = d.Set("environments", environmentIDs) } return nil } -func resourceScalrWebhookUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { +func resourceScalrWebhookRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { scalrClient := meta.(*scalr.Client) + // Reading the webhook differs from create or update methods. + // We read from both old and new API and basically merge the fields from both resources, + // therefore keeping deprecated attributes in place for now and extending with the new ones. + if err := readOldWebhook(ctx, d, scalrClient); err != nil { + return diag.FromErr(err) + } + if err := readNewWebhook(ctx, d, scalrClient); err != nil { + return diag.FromErr(err) + } + + return nil +} + +func updateOldWebhook(ctx context.Context, d *schema.ResourceData, scalrClient *scalr.Client) error { eventDefinitions, err := parseEventDefinitions(d) if err != nil { - return diag.FromErr(err) + return err } // Create a new options struct. @@ -247,7 +493,94 @@ func resourceScalrWebhookUpdate(ctx context.Context, d *schema.ResourceData, met log.Printf("[DEBUG] Update webhook: %s", d.Id()) _, err = scalrClient.Webhooks.Update(ctx, d.Id(), options) if err != nil { - return diag.Errorf("Error updating webhook %s: %v", d.Id(), err) + return fmt.Errorf("Error updating webhook %s: %v", d.Id(), err) + } + + return nil +} + +func updateNewWebhook(ctx context.Context, d *schema.ResourceData, scalrClient *scalr.Client) error { + + options := scalr.WebhookIntegrationUpdateOptions{} + + if d.HasChange("name") { + options.Name = scalr.String(d.Get("name").(string)) + } + + if d.HasChange("url") { + options.Url = scalr.String(d.Get("url").(string)) + } + + if d.HasChange("enabled") { + options.Enabled = scalr.Bool(d.Get("enabled").(bool)) + } + + if d.HasChange("secret_key") { + options.SecretKey = scalr.String(d.Get("secret_key").(string)) + } + + if d.HasChange("timeout") { + options.Timeout = scalr.Int(d.Get("timeout").(int)) + } + + if d.HasChange("max_attempts") { + options.MaxAttempts = scalr.Int(d.Get("max_attempts").(int)) + } + + if d.HasChange("header") { + options.Headers = parseHeaders(d) + } + + eventDefinitions, err := parseEventDefinitions(d) + if err != nil { + return err + } + options.Events = eventDefinitions + + if environmentsI, ok := d.GetOk("environments"); ok { + environments := environmentsI.(*schema.Set).List() + if (len(environments) == 1) && (environments[0].(string) == "*") { + options.IsShared = scalr.Bool(true) + options.Environments = make([]*scalr.Environment, 0) + } else { + options.IsShared = scalr.Bool(false) + environmentValues := make([]*scalr.Environment, 0) + for _, env := range environments { + if env.(string) == "*" { + return fmt.Errorf( + "You cannot simultaneously enable the webhook for all and a limited list of environments. Please remove either wildcard or environment identifiers.", + ) + } + environmentValues = append(environmentValues, &scalr.Environment{ID: env.(string)}) + } + options.Environments = environmentValues + } + } else { + options.IsShared = scalr.Bool(false) + options.Environments = make([]*scalr.Environment, 0) + } + + log.Printf("[DEBUG] Update webhook: %s", d.Id()) + _, err = scalrClient.WebhookIntegrations.Update(ctx, d.Id(), options) + if err != nil { + return fmt.Errorf("Error updating webhook %s: %v", d.Id(), err) + } + + return nil +} + +func resourceScalrWebhookUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + scalrClient := meta.(*scalr.Client) + endpointID := d.Get("endpoint_id").(string) + + var err error + if endpointID != "" { + err = updateOldWebhook(ctx, d, scalrClient) + } else { + err = updateNewWebhook(ctx, d, scalrClient) + } + if err != nil { + return diag.FromErr(err) } return resourceScalrWebhookRead(ctx, d, meta) @@ -257,7 +590,7 @@ func resourceScalrWebhookDelete(ctx context.Context, d *schema.ResourceData, met scalrClient := meta.(*scalr.Client) log.Printf("[DEBUG] Delete webhook: %s", d.Id()) - err := scalrClient.Webhooks.Delete(ctx, d.Id()) + err := scalrClient.WebhookIntegrations.Delete(ctx, d.Id()) if err != nil { if errors.Is(err, scalr.ErrResourceNotFound) { return nil diff --git a/scalr/resource_scalr_webhook_migrate.go b/scalr/resource_scalr_webhook_migrate.go new file mode 100644 index 00000000..661f459c --- /dev/null +++ b/scalr/resource_scalr_webhook_migrate.go @@ -0,0 +1,91 @@ +package scalr + +import ( + "context" + "fmt" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/scalr/go-scalr" +) + +func resourceScalrWebhookResourceV0() *schema.Resource { + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + }, + + "enabled": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + }, + + "last_triggered_at": { + Type: schema.TypeString, + Computed: true, + }, + + "events": { + Type: schema.TypeList, + Elem: &schema.Schema{Type: schema.TypeString}, + Required: true, + }, + + "endpoint_id": { + Type: schema.TypeString, + Required: true, + }, + + "workspace_id": { + Type: schema.TypeString, + Optional: true, + }, + + "environment_id": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + }, + } +} + +func resourceScalrWebhookStateUpgradeV0(ctx context.Context, rawState map[string]interface{}, meta interface{}) (map[string]interface{}, error) { + scalrClient := meta.(*scalr.Client) + webhookId := rawState["id"].(string) + + webhook, err := scalrClient.WebhookIntegrations.Read(ctx, webhookId) + if err != nil { + return nil, fmt.Errorf("Error reading configuration of webhook %s: %w", webhookId, err) + } + + rawState["account_id"] = webhook.Account.ID + rawState["url"] = webhook.Url + rawState["secret_key"] = webhook.SecretKey + rawState["timeout"] = webhook.Timeout + rawState["max_attempts"] = webhook.MaxAttempts + + headers := make([]map[string]interface{}, 0) + if webhook.Headers != nil { + for _, header := range webhook.Headers { + headers = append(headers, map[string]interface{}{ + "name": header.Name, + "value": header.Value, + }) + } + } + rawState["header"] = headers + + if webhook.IsShared { + rawState["environments"] = []string{"*"} + } else { + environmentIDs := make([]string, len(webhook.Environments)) + for _, environment := range webhook.Environments { + environmentIDs = append(environmentIDs, environment.ID) + } + rawState["environments"] = environmentIDs + } + + return rawState, nil +} diff --git a/scalr/resource_scalr_webhook_test.go b/scalr/resource_scalr_webhook_test.go index 780ecb4e..d00b290e 100644 --- a/scalr/resource_scalr_webhook_test.go +++ b/scalr/resource_scalr_webhook_test.go @@ -39,6 +39,11 @@ func TestAccWebhook_update(t *testing.T) { PreCheck: func() { testAccPreCheck(t) }, ProviderFactories: testAccProviderFactories, Steps: []resource.TestStep{ + { + Config: testAccWebhookConfigUpdateEmptyEvent(rInt), + PlanOnly: true, + ExpectError: regexp.MustCompile("expected events to be one of"), + }, { Config: testAccWebhookConfig(rInt), Check: resource.ComposeAggregateTestCheckFunc( @@ -65,10 +70,6 @@ func TestAccWebhook_update(t *testing.T) { "data.scalr_webhook.test", "workspace_id"), ), }, - { - Config: testAccWebhookConfigUpdateEmptyEvent(rInt), - ExpectError: regexp.MustCompile("Got error during parsing events: 0-th value is empty"), - }, }, }) }