From 01b66cf0f2977071285bf5988c790cb257b61b3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Cie=C5=9Blak?= Date: Tue, 17 Sep 2024 14:00:11 +0200 Subject: [PATCH] feat: Resource monitor v1 readiness part 2 (#3064) ## Changes - Refactor data source - Update examples and documentation for resource and data source - Update examples for database role ## Test Plan * [x] acceptance tests ## References * [SHOW RESOURCE MONITORS](https://docs.snowflake.com/en/sql-reference/sql/show-resource-monitors) --- MIGRATION_GUIDE.md | 26 +++ docs/data-sources/database_roles.md | 61 ++++++- docs/data-sources/resource_monitors.md | 69 +++++++- docs/resources/resource_monitor.md | 37 ++++- .../snowflake_database_roles/data-source.tf | 59 ++++++- .../data-source.tf | 41 ++++- .../snowflake_resource_monitor/resource.tf | 28 +++- .../resource_monitor_show_output_ext.go | 17 +- .../bettertestspoc/config/config.go | 2 + pkg/datasources/database_roles.go | 1 + pkg/datasources/resource_monitors.go | 84 +++++----- .../resource_monitors_acceptance_test.go | 54 +++++-- pkg/resources/custom_diffs.go | 12 ++ pkg/resources/custom_diffs_test.go | 84 ++++++++++ pkg/resources/resource_monitor.go | 12 +- .../resource_monitor_acceptance_test.go | 150 ++++++++++++++++++ .../data-sources/resource_monitors.md.tmpl | 24 +++ templates/resources/resource_monitor.md.tmpl | 41 +++++ 18 files changed, 709 insertions(+), 93 deletions(-) create mode 100644 templates/data-sources/resource_monitors.md.tmpl create mode 100644 templates/resources/resource_monitor.md.tmpl diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 0fd26a4223..afaa60e27e 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -5,6 +5,32 @@ describe deprecations or breaking changes and help you to change your configurat across different versions. ## v0.95.0 ➞ v0.96.0 + +### *(breaking change)* resource_monitor resource +Removed fields: +- `set_for_account` (will be settable on account resource, right now, the preferred way is to set it through unsafe_execute resource) +- `warehouses` (can be set on warehouse resource, optionally through unsafe_execute resource only if the warehouse is not managed by Terraform) +- `suspend_triggers` (now, `suspend_trigger` should be used) +- `suspend_immediate_triggers` (now, `suspend_immediate_trigger` should be used) + +### *(breaking change)* resource_monitor data source +Changes: +- New filtering option `like` +- Now, the output of `SHOW RESOURCE MONITORS` is now inside `resource_monitors.*.show_output`. Here's the list of currently available fields: + - `name` + - `credit_quota` + - `used_credits` + - `remaining_credits` + - `level` + - `frequency` + - `start_time` + - `end_time` + - `suspend_at` + - `suspend_immediate_at` + - `created_on` + - `owner` + - `comment` + ### snowflake_row_access_policies data source changes New filtering options: - `in` diff --git a/docs/data-sources/database_roles.md b/docs/data-sources/database_roles.md index ea95e8737b..baeb080b28 100644 --- a/docs/data-sources/database_roles.md +++ b/docs/data-sources/database_roles.md @@ -2,20 +2,73 @@ page_title: "snowflake_database_roles Data Source - terraform-provider-snowflake" subcategory: "" description: |- - + Datasource used to get details of filtered database roles. Filtering is aligned with the current possibilities for SHOW DATABASE ROLES https://docs.snowflake.com/en/sql-reference/sql/show-database-roles query (like and limit are supported). The results of SHOW is encapsulated in show_output collection. --- !> **V1 release candidate** This data source was reworked and is a release candidate for the V1. We do not expect significant changes in it before the V1. We will welcome any feedback and adjust the data source if needed. Any errors reported will be resolved with a higher priority. We encourage checking this data source out before the V1 release. Please follow the [migration guide](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/MIGRATION_GUIDE.md#v0920--v0930) to use it. # snowflake_database_roles (Data Source) - +Datasource used to get details of filtered database roles. Filtering is aligned with the current possibilities for [SHOW DATABASE ROLES](https://docs.snowflake.com/en/sql-reference/sql/show-database-roles) query (`like` and `limit` are supported). The results of SHOW is encapsulated in show_output collection. ## Example Usage ```terraform -data "snowflake_database_roles" "db_roles" { - database = "MYDB" +# Simple usage +data "snowflake_database_roles" "simple" { + in_database = "database-name" +} + +output "simple_output" { + value = data.snowflake_database_roles.simple.database_roles +} + +# Filtering (like) +data "snowflake_database_roles" "like" { + in_database = "database-name" + like = "database_role-name" +} + +output "like_output" { + value = data.snowflake_database_roles.like.database_roles +} + +# Filtering (limit) +data "snowflake_database_roles" "limit" { + in_database = "database-name" + limit { + rows = 10 + from = "prefix-" + } +} + +output "limit_output" { + value = data.snowflake_database_roles.limit.database_roles +} + +# Ensure the number of database roles is equal to at least one element (with the use of postcondition) +data "snowflake_database_roles" "assert_with_postcondition" { + in_database = "database-name" + like = "database_role-name-%" + lifecycle { + postcondition { + condition = length(self.database_roles) > 0 + error_message = "there should be at least one database role" + } + } +} + +# Ensure the number of database roles is equal to at exactly one element (with the use of check block) +check "database_role_check" { + data "snowflake_resource_monitors" "assert_with_check_block" { + in_database = "database-name" + like = "database_role-name" + } + + assert { + condition = length(data.snowflake_database_roles.assert_with_check_block.database_roles) == 1 + error_message = "Database roles filtered by '${data.snowflake_database_roles.assert_with_check_block.like}' returned ${length(data.snowflake_database_roles.assert_with_check_block.database_roles)} database roles where one was expected" + } } ``` diff --git a/docs/data-sources/resource_monitors.md b/docs/data-sources/resource_monitors.md index da386955ea..f0da9f3394 100644 --- a/docs/data-sources/resource_monitors.md +++ b/docs/data-sources/resource_monitors.md @@ -2,34 +2,93 @@ page_title: "snowflake_resource_monitors Data Source - terraform-provider-snowflake" subcategory: "" description: |- - + Datasource used to get details of filtered resource monitors. Filtering is aligned with the current possibilities for SHOW RESOURCE MONITORS https://docs.snowflake.com/en/sql-reference/sql/show-resource-monitors query (like is supported). The results of SHOW is encapsulated in show_output collection. --- -# snowflake_resource_monitors (Data Source) +!> **V1 release candidate** This data source was reworked and is a release candidate for the V1. We do not expect significant changes in it before the V1. We will welcome any feedback and adjust the data source if needed. Any errors reported will be resolved with a higher priority. We encourage checking this data source out before the V1 release. Please follow the [migration guide](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/MIGRATION_GUIDE.md#v0950--v0960) to use it. +# snowflake_resource_monitors (Data Source) +Datasource used to get details of filtered resource monitors. Filtering is aligned with the current possibilities for [SHOW RESOURCE MONITORS](https://docs.snowflake.com/en/sql-reference/sql/show-resource-monitors) query (`like` is supported). The results of SHOW is encapsulated in show_output collection. ## Example Usage ```terraform -data "snowflake_resource_monitors" "current" { +# Simple usage +data "snowflake_resource_monitors" "simple" { +} + +output "simple_output" { + value = data.snowflake_resource_monitors.simple.resource_monitors +} + +# Filtering (like) +data "snowflake_resource_monitors" "like" { + like = "resource-monitor-name" +} + +output "like_output" { + value = data.snowflake_resource_monitors.like.resource_monitors +} + +# Ensure the number of resource monitors is equal to at least one element (with the use of postcondition) +data "snowflake_resource_monitors" "assert_with_postcondition" { + like = "resource-monitor-name-%" + lifecycle { + postcondition { + condition = length(self.resource_monitors) > 0 + error_message = "there should be at least one resource monitor" + } + } +} + +# Ensure the number of resource monitors is equal to at exactly one element (with the use of check block) +check "resource_monitor_check" { + data "snowflake_resource_monitors" "assert_with_check_block" { + like = "resource-monitor-name" + } + + assert { + condition = length(data.snowflake_resource_monitors.assert_with_check_block.resource_monitors) == 1 + error_message = "Resource monitors filtered by '${data.snowflake_resource_monitors.assert_with_check_block.like}' returned ${length(data.snowflake_resource_monitors.assert_with_check_block.resource_monitors)} resource monitors where one was expected" + } } ``` ## Schema +### Optional + +- `like` (String) Filters the output with **case-insensitive** pattern, with support for SQL wildcard characters (`%` and `_`). + ### Read-Only - `id` (String) The ID of this resource. -- `resource_monitors` (List of Object) The resource monitors in the database (see [below for nested schema](#nestedatt--resource_monitors)) +- `resource_monitors` (List of Object) Holds the aggregated output of all resource monitor details queries. (see [below for nested schema](#nestedatt--resource_monitors)) ### Nested Schema for `resource_monitors` Read-Only: +- `show_output` (List of Object) (see [below for nested schema](#nestedobjatt--resource_monitors--show_output)) + + +### Nested Schema for `resource_monitors.show_output` + +Read-Only: + - `comment` (String) -- `credit_quota` (String) +- `created_on` (String) +- `credit_quota` (Number) +- `end_time` (String) - `frequency` (String) +- `level` (String) - `name` (String) +- `owner` (String) +- `remaining_credits` (Number) +- `start_time` (String) +- `suspend_at` (Number) +- `suspend_immediate_at` (Number) +- `used_credits` (Number) diff --git a/docs/resources/resource_monitor.md b/docs/resources/resource_monitor.md index fab6feaf15..dea0fa29b3 100644 --- a/docs/resources/resource_monitor.md +++ b/docs/resources/resource_monitor.md @@ -5,6 +5,14 @@ description: |- --- +!> **V1 release candidate** This resource was reworked and is a release candidate for the V1. We do not expect significant changes in it before the V1. We will welcome any feedback and adjust the resource if needed. Any errors reported will be resolved with a higher priority. We encourage checking this resource out before the V1 release. Please follow the [migration guide](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/MIGRATION_GUIDE.md#v0950--v0960) to use it. + +~> **Note** For more details about resource monitor usage, please visit [this guide on Snowflake documentation page](https://docs.snowflake.com/en/user-guide/resource-monitors). + +**! Warning !** Due to Snowflake limitations, the following actions are not supported: +- Cannot create resource monitors with only triggers set, any other attribute has to be set. +- Once a resource monitor has at least one trigger assigned, it cannot fully unset them (has to have at least one trigger, doesn't matter of which type). That's why when you unset all the triggers on a resource monitor, it will be automatically recreated. + # snowflake_resource_monitor (Resource) @@ -12,22 +20,35 @@ description: |- ## Example Usage ```terraform -resource "snowflake_resource_monitor" "monitor" { - name = "monitor" +// Note: Without credit quota and triggers specified in the configuration, the resource monitor is not performing any work. +// More on resource monitor usage: https://docs.snowflake.com/en/user-guide/resource-monitors. +resource "snowflake_resource_monitor" "minimal" { + name = "resource-monitor-name" +} + +// Note: Resource monitors have to be attached to account or warehouse to be able to track credit usage. +resource "snowflake_resource_monitor" "minimal_working" { + name = "resource-monitor-name" + credit_quota = 100 + suspend_trigger = 100 + notify_users = ["USERONE", "USERTWO"] +} + +resource "snowflake_resource_monitor" "complete" { + name = "resource-monitor-name" credit_quota = 100 frequency = "DAILY" - start_timestamp = "2020-12-07 00:00" - end_timestamp = "2021-12-07 00:00" + start_timestamp = "2030-12-07 00:00" + end_timestamp = "2035-12-07 00:00" - notify_triggers = [40, 50] - suspend_triggers = 50 - suspend_immediate_triggers = 90 + notify_triggers = [40, 50] + suspend_trigger = 50 + suspend_immediate_trigger = 90 notify_users = ["USERONE", "USERTWO"] } ``` - -> **Note** Instead of using fully_qualified_name, you can reference objects managed outside Terraform by constructing a correct ID, consult [identifiers guide](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/guides/identifiers#new-computed-fully-qualified-name-field-in-resources). diff --git a/examples/data-sources/snowflake_database_roles/data-source.tf b/examples/data-sources/snowflake_database_roles/data-source.tf index d87ea770ae..ff07fdc68b 100644 --- a/examples/data-sources/snowflake_database_roles/data-source.tf +++ b/examples/data-sources/snowflake_database_roles/data-source.tf @@ -1,3 +1,56 @@ -data "snowflake_database_roles" "db_roles" { - database = "MYDB" -} \ No newline at end of file +# Simple usage +data "snowflake_database_roles" "simple" { + in_database = "database-name" +} + +output "simple_output" { + value = data.snowflake_database_roles.simple.database_roles +} + +# Filtering (like) +data "snowflake_database_roles" "like" { + in_database = "database-name" + like = "database_role-name" +} + +output "like_output" { + value = data.snowflake_database_roles.like.database_roles +} + +# Filtering (limit) +data "snowflake_database_roles" "limit" { + in_database = "database-name" + limit { + rows = 10 + from = "prefix-" + } +} + +output "limit_output" { + value = data.snowflake_database_roles.limit.database_roles +} + +# Ensure the number of database roles is equal to at least one element (with the use of postcondition) +data "snowflake_database_roles" "assert_with_postcondition" { + in_database = "database-name" + like = "database_role-name-%" + lifecycle { + postcondition { + condition = length(self.database_roles) > 0 + error_message = "there should be at least one database role" + } + } +} + +# Ensure the number of database roles is equal to at exactly one element (with the use of check block) +check "database_role_check" { + data "snowflake_resource_monitors" "assert_with_check_block" { + in_database = "database-name" + like = "database_role-name" + } + + assert { + condition = length(data.snowflake_database_roles.assert_with_check_block.database_roles) == 1 + error_message = "Database roles filtered by '${data.snowflake_database_roles.assert_with_check_block.like}' returned ${length(data.snowflake_database_roles.assert_with_check_block.database_roles)} database roles where one was expected" + } +} diff --git a/examples/data-sources/snowflake_resource_monitors/data-source.tf b/examples/data-sources/snowflake_resource_monitors/data-source.tf index aec8f1fcf6..4ad25784cc 100644 --- a/examples/data-sources/snowflake_resource_monitors/data-source.tf +++ b/examples/data-sources/snowflake_resource_monitors/data-source.tf @@ -1,2 +1,39 @@ -data "snowflake_resource_monitors" "current" { -} \ No newline at end of file +# Simple usage +data "snowflake_resource_monitors" "simple" { +} + +output "simple_output" { + value = data.snowflake_resource_monitors.simple.resource_monitors +} + +# Filtering (like) +data "snowflake_resource_monitors" "like" { + like = "resource-monitor-name" +} + +output "like_output" { + value = data.snowflake_resource_monitors.like.resource_monitors +} + +# Ensure the number of resource monitors is equal to at least one element (with the use of postcondition) +data "snowflake_resource_monitors" "assert_with_postcondition" { + like = "resource-monitor-name-%" + lifecycle { + postcondition { + condition = length(self.resource_monitors) > 0 + error_message = "there should be at least one resource monitor" + } + } +} + +# Ensure the number of resource monitors is equal to at exactly one element (with the use of check block) +check "resource_monitor_check" { + data "snowflake_resource_monitors" "assert_with_check_block" { + like = "resource-monitor-name" + } + + assert { + condition = length(data.snowflake_resource_monitors.assert_with_check_block.resource_monitors) == 1 + error_message = "Resource monitors filtered by '${data.snowflake_resource_monitors.assert_with_check_block.like}' returned ${length(data.snowflake_resource_monitors.assert_with_check_block.resource_monitors)} resource monitors where one was expected" + } +} diff --git a/examples/resources/snowflake_resource_monitor/resource.tf b/examples/resources/snowflake_resource_monitor/resource.tf index 7bcd191351..45273e869d 100644 --- a/examples/resources/snowflake_resource_monitor/resource.tf +++ b/examples/resources/snowflake_resource_monitor/resource.tf @@ -1,14 +1,28 @@ -resource "snowflake_resource_monitor" "monitor" { - name = "monitor" +// Note: Without credit quota and triggers specified in the configuration, the resource monitor is not performing any work. +// More on resource monitor usage: https://docs.snowflake.com/en/user-guide/resource-monitors. +resource "snowflake_resource_monitor" "minimal" { + name = "resource-monitor-name" +} + +// Note: Resource monitors have to be attached to account or warehouse to be able to track credit usage. +resource "snowflake_resource_monitor" "minimal_working" { + name = "resource-monitor-name" + credit_quota = 100 + suspend_trigger = 100 + notify_users = ["USERONE", "USERTWO"] +} + +resource "snowflake_resource_monitor" "complete" { + name = "resource-monitor-name" credit_quota = 100 frequency = "DAILY" - start_timestamp = "2020-12-07 00:00" - end_timestamp = "2021-12-07 00:00" + start_timestamp = "2030-12-07 00:00" + end_timestamp = "2035-12-07 00:00" - notify_triggers = [40, 50] - suspend_triggers = 50 - suspend_immediate_triggers = 90 + notify_triggers = [40, 50] + suspend_trigger = 50 + suspend_immediate_trigger = 90 notify_users = ["USERONE", "USERTWO"] } diff --git a/pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert/resource_monitor_show_output_ext.go b/pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert/resource_monitor_show_output_ext.go index 02acd913a9..aee983b4b9 100644 --- a/pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert/resource_monitor_show_output_ext.go +++ b/pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert/resource_monitor_show_output_ext.go @@ -1,6 +1,21 @@ package resourceshowoutputassert -import "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert" +import ( + "testing" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert" +) + +// ResourceMonitorDatasourceShowOutput is a temporary workaround to have better show output assertions in data source acceptance tests. +func ResourceMonitorDatasourceShowOutput(t *testing.T, name string) *ResourceMonitorShowOutputAssert { + t.Helper() + + u := ResourceMonitorShowOutputAssert{ + ResourceAssert: assert.NewDatasourceAssert("data."+name, "show_output", "resource_monitors.0."), + } + u.AddAssertion(assert.ValueSet("show_output.#", "1")) + return &u +} func (r *ResourceMonitorShowOutputAssert) HasStartTimeNotEmpty() *ResourceMonitorShowOutputAssert { r.AddAssertion(assert.ResourceShowOutputValuePresent("start_time")) diff --git a/pkg/acceptance/bettertestspoc/config/config.go b/pkg/acceptance/bettertestspoc/config/config.go index 7bc3a98531..3135e9a2a8 100644 --- a/pkg/acceptance/bettertestspoc/config/config.go +++ b/pkg/acceptance/bettertestspoc/config/config.go @@ -99,6 +99,8 @@ func FromModel(t *testing.T, model ResourceModel) string { return s } +// ConfigVariablesFromModel constructs config.Variables needed in acceptance tests that are using ConfigVariables in +// combination with ConfigDirectory. It's necessary for cases not supported by FromModel, like lists of objects. func ConfigVariablesFromModel(t *testing.T, model ResourceModel) tfconfig.Variables { t.Helper() variables := make(tfconfig.Variables) diff --git a/pkg/datasources/database_roles.go b/pkg/datasources/database_roles.go index ce45c48257..615951e890 100644 --- a/pkg/datasources/database_roles.go +++ b/pkg/datasources/database_roles.go @@ -66,6 +66,7 @@ func DatabaseRoles() *schema.Resource { return &schema.Resource{ ReadContext: ReadDatabaseRoles, Schema: databaseRolesSchema, + Description: "Datasource used to get details of filtered database roles. Filtering is aligned with the current possibilities for [SHOW DATABASE ROLES](https://docs.snowflake.com/en/sql-reference/sql/show-database-roles) query (`like` and `limit` are supported). The results of SHOW is encapsulated in show_output collection.", } } diff --git a/pkg/datasources/resource_monitors.go b/pkg/datasources/resource_monitors.go index d07265e482..0af5724842 100644 --- a/pkg/datasources/resource_monitors.go +++ b/pkg/datasources/resource_monitors.go @@ -2,39 +2,35 @@ package datasources import ( "context" - "fmt" - "log" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/provider" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/resources" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/schemas" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) var resourceMonitorsSchema = map[string]*schema.Schema{ + "like": { + Type: schema.TypeString, + Optional: true, + Description: "Filters the output with **case-insensitive** pattern, with support for SQL wildcard characters (`%` and `_`).", + }, "resource_monitors": { Type: schema.TypeList, Computed: true, - Description: "The resource monitors in the database", + Description: "Holds the aggregated output of all resource monitor details queries.", Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ - "name": { - Type: schema.TypeString, - Computed: true, - }, - "frequency": { - Type: schema.TypeString, - Computed: true, - }, - "credit_quota": { - Type: schema.TypeString, - Optional: true, - Computed: true, - }, - "comment": { - Type: schema.TypeString, - Optional: true, - Computed: true, + resources.ShowOutputAttributeName: { + Type: schema.TypeList, + Computed: true, + Description: "Holds the output of SHOW RESOURCE MONITORS.", + Elem: &schema.Resource{ + Schema: schemas.ShowResourceMonitorSchema, + }, }, }, }, @@ -43,41 +39,41 @@ var resourceMonitorsSchema = map[string]*schema.Schema{ func ResourceMonitors() *schema.Resource { return &schema.Resource{ - Read: ReadResourceMonitors, - Schema: resourceMonitorsSchema, + ReadContext: ReadResourceMonitors, + Schema: resourceMonitorsSchema, + Description: "Datasource used to get details of filtered resource monitors. Filtering is aligned with the current possibilities for [SHOW RESOURCE MONITORS](https://docs.snowflake.com/en/sql-reference/sql/show-resource-monitors) query (`like` is supported). The results of SHOW is encapsulated in show_output collection.", } } -func ReadResourceMonitors(d *schema.ResourceData, meta interface{}) error { +func ReadResourceMonitors(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*provider.Context).Client - ctx := context.Background() - account, err := client.ContextFunctions.CurrentSessionDetails(ctx) - if err != nil { - log.Print("[DEBUG] unable to retrieve current account") - d.SetId("") - return nil - } + opts := new(sdk.ShowResourceMonitorOptions) - d.SetId(fmt.Sprintf("%s.%s", account.Account, account.Region)) + if likePattern, ok := d.GetOk("like"); ok { + opts.Like = &sdk.Like{ + Pattern: sdk.String(likePattern.(string)), + } + } - extractedResourceMonitors, err := client.ResourceMonitors.Show(ctx, &sdk.ShowResourceMonitorOptions{}) + resourceMonitors, err := client.ResourceMonitors.Show(ctx, opts) if err != nil { - log.Printf("[DEBUG] unable to parse resource monitors in account (%s)", d.Id()) - d.SetId("") - return nil + return diag.FromErr(err) } + d.SetId("resource_monitors_read") - resourceMonitors := make([]map[string]any, len(extractedResourceMonitors)) - - for i, resourceMonitor := range extractedResourceMonitors { - resourceMonitors[i] = map[string]any{ - "name": resourceMonitor.Name, - "frequency": resourceMonitor.Frequency, - "credit_quota": fmt.Sprintf("%f", resourceMonitor.CreditQuota), - "comment": resourceMonitor.Comment, + flattenedResourceMonitors := make([]map[string]any, len(resourceMonitors)) + for i, resourceMonitor := range resourceMonitors { + resourceMonitor := resourceMonitor + flattenedResourceMonitors[i] = map[string]any{ + resources.ShowOutputAttributeName: []map[string]any{schemas.ResourceMonitorToSchema(&resourceMonitor)}, } } - return d.Set("resource_monitors", resourceMonitors) + err = d.Set("resource_monitors", flattenedResourceMonitors) + if err != nil { + return diag.FromErr(err) + } + + return nil } diff --git a/pkg/datasources/resource_monitors_acceptance_test.go b/pkg/datasources/resource_monitors_acceptance_test.go index b1abf7df28..d1cc0681de 100644 --- a/pkg/datasources/resource_monitors_acceptance_test.go +++ b/pkg/datasources/resource_monitors_acceptance_test.go @@ -4,6 +4,10 @@ import ( "fmt" "testing" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + acc "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance" "github.com/hashicorp/terraform-plugin-testing/helper/resource" @@ -11,7 +15,9 @@ import ( ) func TestAcc_ResourceMonitors(t *testing.T) { - resourceMonitorName := acc.TestClient().Ids.Alpha() + prefix := "data_source_resource_monitor_" + resourceMonitorName := acc.TestClient().Ids.RandomAccountObjectIdentifierWithPrefix(prefix) + resourceMonitorName2 := acc.TestClient().Ids.RandomAccountObjectIdentifierWithPrefix(prefix) resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, @@ -19,28 +25,54 @@ func TestAcc_ResourceMonitors(t *testing.T) { TerraformVersionChecks: []tfversion.TerraformVersionCheck{ tfversion.RequireAbove(tfversion.Version1_5_0), }, - CheckDestroy: nil, Steps: []resource.TestStep{ + // Filter by prefix pattern (expect 2 items) { - Config: resourceMonitors(resourceMonitorName), + Config: resourceMonitors(resourceMonitorName.Name(), resourceMonitorName2.Name(), prefix+"%"), Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttrSet("data.snowflake_resource_monitors.s", "resource_monitors.#"), - resource.TestCheckResourceAttrSet("data.snowflake_resource_monitors.s", "resource_monitors.0.name"), + resource.TestCheckResourceAttr("data.snowflake_resource_monitors.test", "resource_monitors.#", "2"), + ), + }, + // Filter by exact name (expect 1 item) + { + Config: resourceMonitors(resourceMonitorName.Name(), resourceMonitorName2.Name(), resourceMonitorName.Name()), + Check: assert.AssertThat(t, + assert.Check(resource.TestCheckResourceAttr("data.snowflake_resource_monitors.test", "resource_monitors.#", "1")), + resourceshowoutputassert.ResourceMonitorDatasourceShowOutput(t, "snowflake_resource_monitors.test"). + HasName(resourceMonitorName.Name()). + HasCreditQuota(5). + HasUsedCredits(0). + HasRemainingCredits(5). + HasLevel(""). + HasFrequency(sdk.FrequencyMonthly). + HasStartTimeNotEmpty(). + HasEndTime(""). + HasSuspendAt(0). + HasSuspendImmediateAt(0). + HasCreatedOnNotEmpty(). + HasOwnerNotEmpty(). + HasComment(""), ), }, }, }) } -func resourceMonitors(resourceMonitorName string) string { +func resourceMonitors(resourceMonitorName, resourceMonitorName2, searchPrefix string) string { return fmt.Sprintf(` - resource snowflake_resource_monitor "s"{ - name = "%v" + resource "snowflake_resource_monitor" "rm1" { + name = "%s" credit_quota = 5 } - data snowflake_resource_monitors "s" { - depends_on = [snowflake_resource_monitor.s] + resource "snowflake_resource_monitor" "rm2" { + name = "%s" + credit_quota = 15 + } + + data "snowflake_resource_monitors" "test" { + depends_on = [ snowflake_resource_monitor.rm1, snowflake_resource_monitor.rm2 ] + like = "%s" } - `, resourceMonitorName) + `, resourceMonitorName, resourceMonitorName2, searchPrefix) } diff --git a/pkg/resources/custom_diffs.go b/pkg/resources/custom_diffs.go index e7c8bce2c3..e685bae834 100644 --- a/pkg/resources/custom_diffs.go +++ b/pkg/resources/custom_diffs.go @@ -165,3 +165,15 @@ func ParametersCustomDiff[T ~string](parametersProvider func(context.Context, Re return customdiff.All(diffFunctions...)(ctx, d, meta) } } + +func ForceNewIfAllKeysAreNotSet(key string, keys ...string) schema.CustomizeDiffFunc { + return customdiff.ForceNewIf(key, func(ctx context.Context, d *schema.ResourceDiff, meta any) bool { + allUnset := true + for _, k := range keys { + if _, ok := d.GetOk(k); ok { + allUnset = false + } + } + return allUnset + }) +} diff --git a/pkg/resources/custom_diffs_test.go b/pkg/resources/custom_diffs_test.go index 14fcb2c848..19d5a338e9 100644 --- a/pkg/resources/custom_diffs_test.go +++ b/pkg/resources/custom_diffs_test.go @@ -5,6 +5,8 @@ import ( "strings" "testing" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/provider" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/resources" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" @@ -553,3 +555,85 @@ func Test_ComputedIfAnyAttributeChanged(t *testing.T) { assert.Nil(t, diff.Attributes["computed_value"]) }) } + +func TestForceNewIfAllKeysAreNotSet(t *testing.T) { + tests := []struct { + name string + stateValue map[string]string + rawConfigValue map[string]any + wantForceNew bool + }{ + { + name: "all values set to unset", + stateValue: map[string]string{ + "value": "123", + "value2": "string value", + "value3": "[one two]", + }, + rawConfigValue: map[string]any{}, + wantForceNew: true, + }, + { + name: "only value set to unset", + stateValue: map[string]string{ + "value": "123", + }, + rawConfigValue: map[string]any{}, + wantForceNew: true, + }, + { + name: "only value2 set to unset", + stateValue: map[string]string{ + "value2": "string value", + }, + rawConfigValue: map[string]any{}, + wantForceNew: true, + }, + { + name: "only value3 set to unset", + stateValue: map[string]string{ + "value3": "[one two]", + }, + rawConfigValue: map[string]any{}, + // We expect here to not re-create because value3 doesn't have a custom diff on it + // and the rest custom diffs don't work when the values are not set. + wantForceNew: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &schema.Provider{ + ResourcesMap: map[string]*schema.Resource{ + "test": { + Schema: map[string]*schema.Schema{ + "value": { + Type: schema.TypeInt, + }, + "value2": { + Type: schema.TypeString, + }, + "value3": { + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + CustomizeDiff: customdiff.All( + resources.ForceNewIfAllKeysAreNotSet("value", "value", "value2", "value3"), + resources.ForceNewIfAllKeysAreNotSet("value2", "value", "value2", "value3"), + ), + }, + }, + } + diff := calculateDiffFromAttributes( + t, + p, + tt.stateValue, + tt.rawConfigValue, + ) + assert.Equal(t, tt.wantForceNew, diff.RequiresNew()) + }) + } +} diff --git a/pkg/resources/resource_monitor.go b/pkg/resources/resource_monitor.go index f56c965418..f2b8d63db0 100644 --- a/pkg/resources/resource_monitor.go +++ b/pkg/resources/resource_monitor.go @@ -109,6 +109,9 @@ func ResourceMonitor() *schema.Resource { CustomizeDiff: customdiff.All( ComputedIfAnyAttributeChanged(resourceMonitorSchema, ShowOutputAttributeName, "notify_users", "credit_quota", "frequency", "start_timestamp", "end_timestamp", "notify_triggers", "suspend_trigger", "suspend_immediate_trigger"), + ForceNewIfAllKeysAreNotSet("notify_triggers", "notify_triggers", "suspend_trigger", "suspend_immediate_trigger"), + ForceNewIfAllKeysAreNotSet("suspend_trigger", "notify_triggers", "suspend_trigger", "suspend_immediate_trigger"), + ForceNewIfAllKeysAreNotSet("suspend_immediate_trigger", "notify_triggers", "suspend_trigger", "suspend_immediate_trigger"), ), } } @@ -383,15 +386,8 @@ func UpdateResourceMonitor(ctx context.Context, d *schema.ResourceData, meta any if len(triggers) > 0 { opts.Triggers = triggers - } else { - return diag.Diagnostics{ - diag.Diagnostic{ - Severity: diag.Error, - Summary: "Failed to update resource monitor.", - Detail: "Due to Snowflake limitations triggers cannot be completely removed form resource monitor after having at least 1 trigger. The only way it to re-create resource monitor without any triggers specified.", - }, - } } + // Else ForceNew, because Snowflake doesn't allow fully unsetting the triggers } // This is to prevent SQL compilation errors from Snowflake, because you cannot only alter triggers. diff --git a/pkg/resources/resource_monitor_acceptance_test.go b/pkg/resources/resource_monitor_acceptance_test.go index 3904bb4d15..304112d60c 100644 --- a/pkg/resources/resource_monitor_acceptance_test.go +++ b/pkg/resources/resource_monitor_acceptance_test.go @@ -1,6 +1,7 @@ package resources_test import ( + "fmt" "regexp" "testing" "time" @@ -779,3 +780,152 @@ func TestAcc_ResourceMonitor_Issue1500_AlteringWithOnlyTriggers(t *testing.T) { }, }) } + +func TestAcc_ResourceMonitor_RemovingAllTriggers(t *testing.T) { + id := acc.TestClient().Ids.RandomAccountObjectIdentifier() + + configModelWithNotifyTriggers := model.ResourceMonitor("test", id.Name()). + WithCreditQuota(100). + WithNotifyTriggersValue(configvariable.SetVariable( + configvariable.IntegerVariable(100), + configvariable.IntegerVariable(110), + )) + + configModelWithSuspendTrigger := model.ResourceMonitor("test", id.Name()). + WithCreditQuota(100). + WithSuspendTrigger(120) + + configModelWithSuspendImmediateTrigger := model.ResourceMonitor("test", id.Name()). + WithCreditQuota(100). + WithSuspendImmediateTrigger(120) + + configModelWithAllTriggers := model.ResourceMonitor("test", id.Name()). + WithCreditQuota(100). + WithNotifyTriggersValue(configvariable.SetVariable( + configvariable.IntegerVariable(100), + configvariable.IntegerVariable(110), + )). + WithSuspendTrigger(120). + WithSuspendImmediateTrigger(150) + + configModelWithoutTriggers := model.ResourceMonitor("test", id.Name()). + WithCreditQuota(100) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acc.TestAccPreCheck(t) }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: acc.CheckDestroy(t, resources.ResourceMonitor), + Steps: []resource.TestStep{ + // Config with all triggers + { + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + Config: config.FromModel(t, configModelWithAllTriggers), + }, + // No triggers (force new expected) + { + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_resource_monitor.test", plancheck.ResourceActionDestroyBeforeCreate), + }, + }, + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + Config: config.FromModel(t, configModelWithoutTriggers), + }, + // Config with only notify triggers + { + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + Config: config.FromModel(t, configModelWithNotifyTriggers), + }, + // No triggers (force new expected) + { + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_resource_monitor.test", plancheck.ResourceActionDestroyBeforeCreate), + }, + }, + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + Config: config.FromModel(t, configModelWithoutTriggers), + }, + // Config with only suspend trigger + { + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + Config: config.FromModel(t, configModelWithSuspendTrigger), + }, + // No triggers (force new expected) + { + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_resource_monitor.test", plancheck.ResourceActionDestroyBeforeCreate), + }, + }, + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + Config: config.FromModel(t, configModelWithoutTriggers), + }, + // Config with only suspend immediate trigger + { + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + Config: config.FromModel(t, configModelWithSuspendImmediateTrigger), + }, + // No triggers (force new expected) + { + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_resource_monitor.test", plancheck.ResourceActionDestroyBeforeCreate), + }, + }, + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + Config: config.FromModel(t, configModelWithoutTriggers), + }, + }, + }) +} + +// proves that fields that were present in the previous versions are not kept in the state after the upgrade +func TestAcc_ResourceMonitor_SetForWarehouse(t *testing.T) { + id := acc.TestClient().Ids.RandomAccountObjectIdentifier() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acc.TestAccPreCheck(t) }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: acc.CheckDestroy(t, resources.ResourceMonitor), + Steps: []resource.TestStep{ + { + ExternalProviders: map[string]resource.ExternalProvider{ + "snowflake": { + VersionConstraint: "=0.90.0", + Source: "Snowflake-Labs/snowflake", + }, + }, + Config: fmt.Sprintf(` +resource "snowflake_resource_monitor" "test" { + name = "%s" + credit_quota = 100 + suspend_trigger = 100 + warehouses = [ "SNOWFLAKE" ] +} +`, id.Name()), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("snowflake_resource_monitor.test", "warehouses.#", "1"), + ), + }, + { + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + Config: fmt.Sprintf(` +resource "snowflake_resource_monitor" "test" { + name = "%s" + credit_quota = 100 + suspend_trigger = 100 +} +`, id.Name()), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckNoResourceAttr("snowflake_resource_monitor.test", "warehouses"), + resource.TestCheckNoResourceAttr("snowflake_resource_monitor.test", "warehouses.#"), + ), + }, + }, + }) +} diff --git a/templates/data-sources/resource_monitors.md.tmpl b/templates/data-sources/resource_monitors.md.tmpl new file mode 100644 index 0000000000..abd91a8e36 --- /dev/null +++ b/templates/data-sources/resource_monitors.md.tmpl @@ -0,0 +1,24 @@ +--- +page_title: "{{.Name}} {{.Type}} - {{.ProviderName}}" +subcategory: "" +description: |- +{{ if gt (len (split .Description "")) 1 -}} +{{ index (split .Description "") 1 | plainmarkdown | trimspace | prefixlines " " }} +{{- else -}} +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} +{{- end }} +--- + +!> **V1 release candidate** This data source was reworked and is a release candidate for the V1. We do not expect significant changes in it before the V1. We will welcome any feedback and adjust the data source if needed. Any errors reported will be resolved with a higher priority. We encourage checking this data source out before the V1 release. Please follow the [migration guide](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/MIGRATION_GUIDE.md#v0950--v0960) to use it. + +# {{.Name}} ({{.Type}}) + +{{ .Description | trimspace }} + +{{ if .HasExample -}} +## Example Usage + +{{ tffile (printf "examples/data-sources/%s/data-source.tf" .Name)}} +{{- end }} + +{{ .SchemaMarkdown | trimspace }} diff --git a/templates/resources/resource_monitor.md.tmpl b/templates/resources/resource_monitor.md.tmpl new file mode 100644 index 0000000000..0e6a0993bb --- /dev/null +++ b/templates/resources/resource_monitor.md.tmpl @@ -0,0 +1,41 @@ +--- +page_title: "{{.Name}} {{.Type}} - {{.ProviderName}}" +subcategory: "" +description: |- +{{ if gt (len (split .Description "")) 1 -}} +{{ index (split .Description "") 1 | plainmarkdown | trimspace | prefixlines " " }} +{{- else -}} +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} +{{- end }} +--- + +!> **V1 release candidate** This resource was reworked and is a release candidate for the V1. We do not expect significant changes in it before the V1. We will welcome any feedback and adjust the resource if needed. Any errors reported will be resolved with a higher priority. We encourage checking this resource out before the V1 release. Please follow the [migration guide](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/MIGRATION_GUIDE.md#v0950--v0960) to use it. + +~> **Note** For more details about resource monitor usage, please visit [this guide on Snowflake documentation page](https://docs.snowflake.com/en/user-guide/resource-monitors). + +**! Warning !** Due to Snowflake limitations, the following actions are not supported: +- Cannot create resource monitors with only triggers set, any other attribute has to be set. +- Once a resource monitor has at least one trigger assigned, it cannot fully unset them (has to have at least one trigger, doesn't matter of which type). That's why when you unset all the triggers on a resource monitor, it will be automatically recreated. + +# {{.Name}} ({{.Type}}) + +{{ .Description | trimspace }} + +{{ if .HasExample -}} +## Example Usage + +{{ tffile (printf "examples/resources/%s/resource.tf" .Name)}} +-> **Note** Instead of using fully_qualified_name, you can reference objects managed outside Terraform by constructing a correct ID, consult [identifiers guide](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/guides/identifiers#new-computed-fully-qualified-name-field-in-resources). + + +{{- end }} + +{{ .SchemaMarkdown | trimspace }} +{{- if .HasImport }} + +## Import + +Import is supported using the following syntax: + +{{ codefile "shell" (printf "examples/resources/%s/import.sh" .Name)}} +{{- end }}