From 42fd40a6872343f847e1ae44fd27306feb87ef05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Cie=C5=9Blak?= Date: Wed, 11 Sep 2024 11:14:14 +0200 Subject: [PATCH] feat: Resource monitor v1 readiness (#3052) ## Changes - Add ValuePresent assert to our custom assertions - Add ToConfigValues function for every model - Update Resource Monitor SDK + unit and integration tests - Update Resource Monitor Resource + acc tests - Handle issues connected to the resource monitor (mostly timestamp format difference causing infinite plan or only trigger updates causing SQL compilation error): - #1500 - #1624 - #1716 - #1754 - #1821 - #1832 - #1990 ## Next pr - Adjust examples and update migration notes - Data source (impl, tests, examples, migration notes) ## References * [CREATE RESOURCE MONITOR](https://docs.snowflake.com/en/sql-reference/sql/create-resource-monitor) --- docs/resources/resource_monitor.md | 40 +- docs/resources/warehouse.md | 2 +- .../provider/resource_monitor_resource.go | 18 +- pkg/acceptance/bettertestspoc/README.md | 4 +- .../assert/objectassert/gen/sdk_object_def.go | 5 + .../resource_monitor_snowflake_ext.go | 30 + .../resource_monitor_snowflake_gen.go | 207 +++++ .../assert/resource_assertions.go | 19 +- .../resourceassert/gen/resource_schema_def.go | 4 + .../resource_monitor_resource_ext.go | 38 + .../resource_monitor_resource_gen.go | 137 +++ .../resourceassert/view_resource_gen.go | 10 + .../resource_monitor_show_output_ext.go | 23 + .../resource_monitor_show_output_gen.go | 107 +++ .../bettertestspoc/config/config.go | 20 + .../bettertestspoc/config/model/gen/model.go | 3 +- .../model/resource_monitor_model_gen.go | 148 +++ .../config/model/view_model_gen.go | 8 + .../helpers/resource_monitor_client.go | 12 + .../struct_details_extractor_test.go | 9 +- pkg/resources/resource_monitor.go | 642 ++++++------- .../resource_monitor_acceptance_test.go | 846 ++++++++++++++---- pkg/resources/warehouse.go | 2 +- pkg/schemas/resource_monitor_gen.go | 32 +- pkg/sdk/external_volumes_impl_gen.go | 3 - pkg/sdk/resource_monitor_internal_test.go | 28 - pkg/sdk/resource_monitors.go | 221 ++--- pkg/sdk/resource_monitors_test.go | 64 +- .../resource_monitors_integration_test.go | 288 +++--- 29 files changed, 2109 insertions(+), 861 deletions(-) create mode 100644 pkg/acceptance/bettertestspoc/assert/objectassert/resource_monitor_snowflake_ext.go create mode 100644 pkg/acceptance/bettertestspoc/assert/objectassert/resource_monitor_snowflake_gen.go create mode 100644 pkg/acceptance/bettertestspoc/assert/resourceassert/resource_monitor_resource_ext.go create mode 100644 pkg/acceptance/bettertestspoc/assert/resourceassert/resource_monitor_resource_gen.go create mode 100644 pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert/resource_monitor_show_output_ext.go create mode 100644 pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert/resource_monitor_show_output_gen.go create mode 100644 pkg/acceptance/bettertestspoc/config/model/resource_monitor_model_gen.go delete mode 100644 pkg/sdk/resource_monitor_internal_test.go diff --git a/docs/resources/resource_monitor.md b/docs/resources/resource_monitor.md index 386af79058..fab6feaf15 100644 --- a/docs/resources/resource_monitor.md +++ b/docs/resources/resource_monitor.md @@ -36,27 +36,43 @@ resource "snowflake_resource_monitor" "monitor" { ### Required -- `name` (String) Identifier for the resource monitor; must be unique for your account. +- `name` (String) Identifier for the resource monitor; must be unique for your account. Due to technical limitations (read more [here](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/docs/technical-documentation/identifiers_rework_design_decisions.md#known-limitations-and-identifier-recommendations)), avoid using the following characters: `|`, `.`, `(`, `)`, `"` ### Optional -- `credit_quota` (Number) The number of credits allocated monthly to the resource monitor. +- `credit_quota` (Number) The number of credits allocated to the resource monitor per frequency interval. When total usage for all warehouses assigned to the monitor reaches this number for the current frequency interval, the resource monitor is considered to be at 100% of quota. - `end_timestamp` (String) The date and time when the resource monitor suspends the assigned warehouses. -- `frequency` (String) The frequency interval at which the credit usage resets to 0. If you set a frequency for a resource monitor, you must also set START_TIMESTAMP. -- `notify_triggers` (Set of Number) A list of percentage thresholds at which to send an alert to subscribed users. -- `notify_users` (Set of String) Specifies the list of users to receive email notifications on resource monitors. -- `set_for_account` (Boolean) Specifies whether the resource monitor should be applied globally to your Snowflake account (defaults to false). -- `start_timestamp` (String) The date and time when the resource monitor starts monitoring credit usage for the assigned warehouses. -- `suspend_immediate_trigger` (Number) The number that represents the percentage threshold at which to immediately suspend all warehouses. -- `suspend_immediate_triggers` (Set of Number, Deprecated) A list of percentage thresholds at which to suspend all warehouses. -- `suspend_trigger` (Number) The number that represents the percentage threshold at which to suspend all warehouses. -- `suspend_triggers` (Set of Number, Deprecated) A list of percentage thresholds at which to suspend all warehouses. -- `warehouses` (Set of String) A list of warehouses to apply the resource monitor to. +- `frequency` (String) The frequency interval at which the credit usage resets to 0. Valid values are (case-insensitive): `MONTHLY` | `DAILY` | `WEEKLY` | `YEARLY` | `NEVER`. If you set a `frequency` for a resource monitor, you must also set `start_timestamp`. If you specify `NEVER` for the frequency, the credit usage for the warehouse does not reset. After removing this field from the config, the previously set value will be preserved on the Snowflake side, not the default value. That's due to Snowflake limitation and the lack of unset functionality for this parameter. +- `notify_triggers` (Set of Number) Specifies a list of percentages of the credit quota. After reaching any of the values the users passed in the notify_users field will be notified (to receive the notification they should have notifications enabled). Values over 100 are supported. +- `notify_users` (Set of String) Specifies the list of users (their identifiers) to receive email notifications on resource monitors. +- `start_timestamp` (String) The date and time when the resource monitor starts monitoring credit usage for the assigned warehouses. If you set a `start_timestamp` for a resource monitor, you must also set `frequency`. After removing this field from the config, the previously set value will be preserved on the Snowflake side, not the default value. That's due to Snowflake limitation and the lack of unset functionality for this parameter. +- `suspend_immediate_trigger` (Number) Represents a numeric value specified as a percentage of the credit quota. Values over 100 are supported. After reaching this value, all assigned warehouses immediately cancel any currently running queries or statements. In addition, this action sends a notification to all users who have enabled notifications for themselves. +- `suspend_trigger` (Number) Represents a numeric value specified as a percentage of the credit quota. Values over 100 are supported. After reaching this value, all assigned warehouses while allowing currently running queries to complete will be suspended. No new queries can be executed by the warehouses until the credit quota for the resource monitor is increased. In addition, this action sends a notification to all users who have enabled notifications for themselves. ### Read-Only - `fully_qualified_name` (String) Fully qualified name of the resource. For more information, see [object name resolution](https://docs.snowflake.com/en/sql-reference/name-resolution). - `id` (String) The ID of this resource. +- `show_output` (List of Object) Outputs the result of `SHOW RESOURCE MONITORS` for the given resource monitor. (see [below for nested schema](#nestedatt--show_output)) + + +### Nested Schema for `show_output` + +Read-Only: + +- `comment` (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) ## Import diff --git a/docs/resources/warehouse.md b/docs/resources/warehouse.md index a651c63ae3..3a440710bd 100644 --- a/docs/resources/warehouse.md +++ b/docs/resources/warehouse.md @@ -53,7 +53,7 @@ resource "snowflake_warehouse" "warehouse" { - `fully_qualified_name` (String) Fully qualified name of the resource. For more information, see [object name resolution](https://docs.snowflake.com/en/sql-reference/name-resolution). - `id` (String) The ID of this resource. - `parameters` (List of Object) Outputs the result of `SHOW PARAMETERS IN WAREHOUSE` for the given warehouse. (see [below for nested schema](#nestedatt--parameters)) -- `show_output` (List of Object) Outputs the result of `SHOW WAREHOUSE` for the given warehouse. (see [below for nested schema](#nestedatt--show_output)) +- `show_output` (List of Object) Outputs the result of `SHOW WAREHOUSES` for the given warehouse. (see [below for nested schema](#nestedatt--show_output)) ### Nested Schema for `parameters` diff --git a/framework/provider/resource_monitor_resource.go b/framework/provider/resource_monitor_resource.go index a1a7d97fa6..29013a2a17 100644 --- a/framework/provider/resource_monitor_resource.go +++ b/framework/provider/resource_monitor_resource.go @@ -508,7 +508,7 @@ func (r *ResourceMonitorResource) create(ctx context.Context, data *resourceMoni } if !data.Frequency.IsNull() && data.Frequency.ValueString() != "" { setWith = true - frequency, err := sdk.FrequencyFromString(data.Frequency.ValueString()) + frequency, err := sdk.ToResourceMonitorFrequency(data.Frequency.ValueString()) if err != nil { diags.AddError("Client Error", fmt.Sprintf("Unable to create resource monitor, got error: %s", err)) } @@ -529,7 +529,7 @@ func (r *ResourceMonitorResource) create(ctx context.Context, data *resourceMoni elements := make([]types.String, 0, len(data.NotifyUsers.Elements())) var notifiedUsers []sdk.NotifiedUser for _, e := range elements { - notifiedUsers = append(notifiedUsers, sdk.NotifiedUser{Name: e.ValueString()}) + notifiedUsers = append(notifiedUsers, sdk.NotifiedUser{Name: sdk.NewAccountObjectIdentifier(e.ValueString())}) } with.NotifyUsers = &sdk.NotifyUsers{ Users: notifiedUsers, @@ -600,13 +600,11 @@ func (r *ResourceMonitorResource) read(ctx context.Context, data *resourceMonito data.CreditQuota = types.Float64Value(resourceMonitor.CreditQuota) data.Frequency = types.StringValue(string(resourceMonitor.Frequency)) - switch resourceMonitor.Level { + switch *resourceMonitor.Level { case sdk.ResourceMonitorLevelAccount: data.Level = types.StringValue("ACCOUNT") case sdk.ResourceMonitorLevelWarehouse: data.Level = types.StringValue("WAREHOUSE") - case sdk.ResourceMonitorLevelNull: - data.Level = types.StringValue("NULL") } data.UsedCredits = types.Float64Value(resourceMonitor.UsedCredits) data.RemainingCredits = types.Float64Value(resourceMonitor.RemainingCredits) @@ -637,11 +635,11 @@ func (r *ResourceMonitorResource) read(ctx context.Context, data *resourceMonito "threshold": types.Int64Type, "trigger_action": types.StringType, }) - if len(resourceMonitor.NotifyTriggers) == 0 && resourceMonitor.SuspendAt == nil && resourceMonitor.SuspendImmediateAt == nil { + if len(resourceMonitor.NotifyAt) == 0 && resourceMonitor.SuspendAt == nil && resourceMonitor.SuspendImmediateAt == nil { data.Triggers = types.SetNull(triggersObjectType) } else { var triggers []resourceMonitorTriggerModel - for _, e := range resourceMonitor.NotifyTriggers { + for _, e := range resourceMonitor.NotifyAt { triggers = append(triggers, resourceMonitorTriggerModel{ Threshold: types.Int64Value(int64(e)), TriggerAction: types.StringValue(string(sdk.TriggerActionNotify)), @@ -700,7 +698,7 @@ func (r *ResourceMonitorResource) update(ctx context.Context, plan *resourceMoni if opts.Set == nil { opts.Set = &sdk.ResourceMonitorSet{} } - frequency, err := sdk.FrequencyFromString(plan.Frequency.ValueString()) + frequency, err := sdk.ToResourceMonitorFrequency(plan.Frequency.ValueString()) if err != nil { diags.AddError("Client Error", fmt.Sprintf("Unable to update resource monitor, got error: %s", err)) return plan, nil, diags @@ -713,7 +711,7 @@ func (r *ResourceMonitorResource) update(ctx context.Context, plan *resourceMoni if opts.Set == nil { opts.Set = &sdk.ResourceMonitorSet{} } - frequency, err := sdk.FrequencyFromString(plan.Frequency.ValueString()) + frequency, err := sdk.ToResourceMonitorFrequency(plan.Frequency.ValueString()) if err != nil { diags.AddError("Client Error", fmt.Sprintf("Unable to update resource monitor, got error: %s", err)) return plan, nil, diags @@ -734,7 +732,7 @@ func (r *ResourceMonitorResource) update(ctx context.Context, plan *resourceMoni elements := make([]types.String, 0, len(plan.NotifyUsers.Elements())) plan.NotifyUsers.ElementsAs(ctx, &elements, false) for _, e := range elements { - notifiedUsers = append(notifiedUsers, sdk.NotifiedUser{Name: e.ValueString()}) + notifiedUsers = append(notifiedUsers, sdk.NotifiedUser{Name: sdk.NewAccountObjectIdentifier(e.ValueString())}) } opts.Set.NotifyUsers = &sdk.NotifyUsers{ Users: notifiedUsers, diff --git a/pkg/acceptance/bettertestspoc/README.md b/pkg/acceptance/bettertestspoc/README.md index 09cae6cd38..1ddb5311c2 100644 --- a/pkg/acceptance/bettertestspoc/README.md +++ b/pkg/acceptance/bettertestspoc/README.md @@ -113,12 +113,12 @@ func (w *WarehouseModel) WithWarehouseSizeEnum(warehouseSize sdk.WarehouseSize) Each of the above assertion types/config models has its own generator and cleanup entry in our Makefile. You can generate config models with: ```shell - make clean-resource-model-builder generate-resource-model-builder + make clean-resource-model-builders generate-resource-model-builders ``` You can use cli flags: ```shell - make clean-resource-model-builder generate-resource-model-builder SF_TF_GENERATOR_ARGS='--dry-run --verbose' + make clean-resource-model-builders generate-resource-model-builders SF_TF_GENERATOR_ARGS='--dry-run --verbose' ``` To clean/generate all from this package run diff --git a/pkg/acceptance/bettertestspoc/assert/objectassert/gen/sdk_object_def.go b/pkg/acceptance/bettertestspoc/assert/objectassert/gen/sdk_object_def.go index 798b2dbdcc..2ad877df71 100644 --- a/pkg/acceptance/bettertestspoc/assert/objectassert/gen/sdk_object_def.go +++ b/pkg/acceptance/bettertestspoc/assert/objectassert/gen/sdk_object_def.go @@ -37,6 +37,11 @@ var allStructs = []SdkObjectDef{ ObjectType: sdk.ObjectTypeWarehouse, ObjectStruct: sdk.Warehouse{}, }, + { + IdType: "sdk.AccountObjectIdentifier", + ObjectType: sdk.ObjectTypeResourceMonitor, + ObjectStruct: sdk.ResourceMonitor{}, + }, } func GetSdkObjectDetails() []genhelpers.SdkObjectDetails { diff --git a/pkg/acceptance/bettertestspoc/assert/objectassert/resource_monitor_snowflake_ext.go b/pkg/acceptance/bettertestspoc/assert/objectassert/resource_monitor_snowflake_ext.go new file mode 100644 index 0000000000..020374305c --- /dev/null +++ b/pkg/acceptance/bettertestspoc/assert/objectassert/resource_monitor_snowflake_ext.go @@ -0,0 +1,30 @@ +package objectassert + +import ( + "fmt" + "testing" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" +) + +func (r *ResourceMonitorAssert) HasNonEmptyStartTime() *ResourceMonitorAssert { + r.AddAssertion(func(t *testing.T, o *sdk.ResourceMonitor) error { + t.Helper() + if o.StartTime == "" { + return fmt.Errorf("expected start time to be non empty") + } + return nil + }) + return r +} + +func (r *ResourceMonitorAssert) HasNonEmptyEndTime() *ResourceMonitorAssert { + r.AddAssertion(func(t *testing.T, o *sdk.ResourceMonitor) error { + t.Helper() + if o.StartTime == "" { + return fmt.Errorf("expected end time to be non empty") + } + return nil + }) + return r +} diff --git a/pkg/acceptance/bettertestspoc/assert/objectassert/resource_monitor_snowflake_gen.go b/pkg/acceptance/bettertestspoc/assert/objectassert/resource_monitor_snowflake_gen.go new file mode 100644 index 0000000000..68caed2c64 --- /dev/null +++ b/pkg/acceptance/bettertestspoc/assert/objectassert/resource_monitor_snowflake_gen.go @@ -0,0 +1,207 @@ +// Code generated by assertions generator; DO NOT EDIT. + +package objectassert + +import ( + "fmt" + "slices" + "testing" + "time" + + acc "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" +) + +type ResourceMonitorAssert struct { + *assert.SnowflakeObjectAssert[sdk.ResourceMonitor, sdk.AccountObjectIdentifier] +} + +func ResourceMonitor(t *testing.T, id sdk.AccountObjectIdentifier) *ResourceMonitorAssert { + t.Helper() + return &ResourceMonitorAssert{ + assert.NewSnowflakeObjectAssertWithProvider(sdk.ObjectTypeResourceMonitor, id, acc.TestClient().ResourceMonitor.Show), + } +} + +func ResourceMonitorFromObject(t *testing.T, resourceMonitor *sdk.ResourceMonitor) *ResourceMonitorAssert { + t.Helper() + return &ResourceMonitorAssert{ + assert.NewSnowflakeObjectAssertWithObject(sdk.ObjectTypeResourceMonitor, resourceMonitor.ID(), resourceMonitor), + } +} + +func (r *ResourceMonitorAssert) HasName(expected string) *ResourceMonitorAssert { + r.AddAssertion(func(t *testing.T, o *sdk.ResourceMonitor) error { + t.Helper() + if o.Name != expected { + return fmt.Errorf("expected name: %v; got: %v", expected, o.Name) + } + return nil + }) + return r +} + +func (r *ResourceMonitorAssert) HasCreditQuota(expected float64) *ResourceMonitorAssert { + r.AddAssertion(func(t *testing.T, o *sdk.ResourceMonitor) error { + t.Helper() + if o.CreditQuota != expected { + return fmt.Errorf("expected credit quota: %v; got: %v", expected, o.CreditQuota) + } + return nil + }) + return r +} + +func (r *ResourceMonitorAssert) HasUsedCredits(expected float64) *ResourceMonitorAssert { + r.AddAssertion(func(t *testing.T, o *sdk.ResourceMonitor) error { + t.Helper() + if o.UsedCredits != expected { + return fmt.Errorf("expected used credits: %v; got: %v", expected, o.UsedCredits) + } + return nil + }) + return r +} + +func (r *ResourceMonitorAssert) HasRemainingCredits(expected float64) *ResourceMonitorAssert { + r.AddAssertion(func(t *testing.T, o *sdk.ResourceMonitor) error { + t.Helper() + if o.RemainingCredits != expected { + return fmt.Errorf("expected remaining credits: %v; got: %v", expected, o.RemainingCredits) + } + return nil + }) + return r +} + +func (r *ResourceMonitorAssert) HasLevel(expected sdk.ResourceMonitorLevel) *ResourceMonitorAssert { + r.AddAssertion(func(t *testing.T, o *sdk.ResourceMonitor) error { + t.Helper() + if o.Level == nil { + return fmt.Errorf("expected level to have value; got: nil") + } + if *o.Level != expected { + return fmt.Errorf("expected level: %v; got: %v", expected, *o.Level) + } + return nil + }) + return r +} + +func (r *ResourceMonitorAssert) HasFrequency(expected sdk.Frequency) *ResourceMonitorAssert { + r.AddAssertion(func(t *testing.T, o *sdk.ResourceMonitor) error { + t.Helper() + if o.Frequency != expected { + return fmt.Errorf("expected frequency: %v; got: %v", expected, o.Frequency) + } + return nil + }) + return r +} + +func (r *ResourceMonitorAssert) HasStartTime(expected string) *ResourceMonitorAssert { + r.AddAssertion(func(t *testing.T, o *sdk.ResourceMonitor) error { + t.Helper() + if o.StartTime != expected { + return fmt.Errorf("expected start time: %v; got: %v", expected, o.StartTime) + } + return nil + }) + return r +} + +func (r *ResourceMonitorAssert) HasEndTime(expected string) *ResourceMonitorAssert { + r.AddAssertion(func(t *testing.T, o *sdk.ResourceMonitor) error { + t.Helper() + if o.EndTime != expected { + return fmt.Errorf("expected end time: %v; got: %v", expected, o.EndTime) + } + return nil + }) + return r +} + +func (r *ResourceMonitorAssert) HasNotifyAt(expected []int) *ResourceMonitorAssert { + r.AddAssertion(func(t *testing.T, o *sdk.ResourceMonitor) error { + t.Helper() + if !slices.Equal(o.NotifyAt, expected) { + return fmt.Errorf("expected notify at: %v; got: %v", expected, o.NotifyAt) + } + return nil + }) + return r +} + +func (r *ResourceMonitorAssert) HasSuspendAt(expected int) *ResourceMonitorAssert { + r.AddAssertion(func(t *testing.T, o *sdk.ResourceMonitor) error { + t.Helper() + if o.SuspendAt == nil { + return fmt.Errorf("expected suspend at to have value; got: nil") + } + if *o.SuspendAt != expected { + return fmt.Errorf("expected suspend at: %v; got: %v", expected, *o.SuspendAt) + } + return nil + }) + return r +} + +func (r *ResourceMonitorAssert) HasSuspendImmediateAt(expected int) *ResourceMonitorAssert { + r.AddAssertion(func(t *testing.T, o *sdk.ResourceMonitor) error { + t.Helper() + if o.SuspendImmediateAt == nil { + return fmt.Errorf("expected suspend immediate at to have value; got: nil") + } + if *o.SuspendImmediateAt != expected { + return fmt.Errorf("expected suspend immediate at: %v; got: %v", expected, *o.SuspendImmediateAt) + } + return nil + }) + return r +} + +func (r *ResourceMonitorAssert) HasCreatedOn(expected time.Time) *ResourceMonitorAssert { + r.AddAssertion(func(t *testing.T, o *sdk.ResourceMonitor) error { + t.Helper() + if o.CreatedOn != expected { + return fmt.Errorf("expected created on: %v; got: %v", expected, o.CreatedOn) + } + return nil + }) + return r +} + +func (r *ResourceMonitorAssert) HasOwner(expected string) *ResourceMonitorAssert { + r.AddAssertion(func(t *testing.T, o *sdk.ResourceMonitor) error { + t.Helper() + if o.Owner != expected { + return fmt.Errorf("expected owner: %v; got: %v", expected, o.Owner) + } + return nil + }) + return r +} + +func (r *ResourceMonitorAssert) HasComment(expected string) *ResourceMonitorAssert { + r.AddAssertion(func(t *testing.T, o *sdk.ResourceMonitor) error { + t.Helper() + if o.Comment != expected { + return fmt.Errorf("expected comment: %v; got: %v", expected, o.Comment) + } + return nil + }) + return r +} + +func (r *ResourceMonitorAssert) HasNotifyUsers(expected []string) *ResourceMonitorAssert { + r.AddAssertion(func(t *testing.T, o *sdk.ResourceMonitor) error { + t.Helper() + if !slices.Equal(o.NotifyUsers, expected) { + return fmt.Errorf("expected notify users: %v; got: %v", expected, o.NotifyUsers) + } + return nil + }) + return r +} diff --git a/pkg/acceptance/bettertestspoc/assert/resource_assertions.go b/pkg/acceptance/bettertestspoc/assert/resource_assertions.go index 3a404a4bcf..79f4e47ac0 100644 --- a/pkg/acceptance/bettertestspoc/assert/resource_assertions.go +++ b/pkg/acceptance/bettertestspoc/assert/resource_assertions.go @@ -59,9 +59,9 @@ func NewDatasourceAssert(name string, prefix string, additionalPrefix string) *R type resourceAssertionType string const ( + resourceAssertionTypeValuePresent = "VALUE_PRESENT" resourceAssertionTypeValueSet = "VALUE_SET" resourceAssertionTypeValueNotSet = "VALUE_NOT_SET" - resourceAssertionTypeValuePresent = "VALUE_PRESENT" ) type ResourceAssertion struct { @@ -75,6 +75,10 @@ func (r *ResourceAssert) AddAssertion(assertion ResourceAssertion) { r.assertions = append(r.assertions, assertion) } +func ValuePresent(fieldName string) ResourceAssertion { + return ResourceAssertion{fieldName: fieldName, resourceAssertionType: resourceAssertionTypeValuePresent} +} + func ValueSet(fieldName string, expected string) ResourceAssertion { return ResourceAssertion{fieldName: fieldName, expectedValue: expected, resourceAssertionType: resourceAssertionTypeValueSet} } @@ -105,7 +109,10 @@ func ResourceShowOutputValueSet(fieldName string, expected string) ResourceAsser return ResourceAssertion{fieldName: showOutputPrefix + fieldName, expectedValue: expected, resourceAssertionType: resourceAssertionTypeValueSet} } -// TODO [SNOW-1501905]: generate assertions with resourceAssertionTypeValuePresent +func ResourceShowOutputValueNotSet(fieldName string) ResourceAssertion { + return ResourceAssertion{fieldName: showOutputPrefix + fieldName, resourceAssertionType: resourceAssertionTypeValueNotSet} +} + func ResourceShowOutputValuePresent(fieldName string) ResourceAssertion { return ResourceAssertion{fieldName: showOutputPrefix + fieldName, resourceAssertionType: resourceAssertionTypeValuePresent} } @@ -181,9 +188,13 @@ func (r *ResourceAssert) ToTerraformImportStateCheckFunc(t *testing.T) resource. result = append(result, fmt.Errorf("%s %s assertion [%d/%d]: failed with error: %w", r.id, r.prefix, i+1, len(r.assertions), err)) } case resourceAssertionTypeValueNotSet: - panic("implement") + if err := importchecks.TestCheckResourceAttrNotInInstanceState(r.id, a.fieldName)(s); err != nil { + result = append(result, fmt.Errorf("%s %s assertion [%d/%d]: failed with error: %w", r.id, r.prefix, i+1, len(r.assertions), err)) + } case resourceAssertionTypeValuePresent: - panic("implement") + if err := importchecks.TestCheckResourceAttrInstanceStateSet(r.id, a.fieldName)(s); err != nil { + result = append(result, fmt.Errorf("%s %s assertion [%d/%d]: failed with error: %w", r.id, r.prefix, i+1, len(r.assertions), err)) + } } } diff --git a/pkg/acceptance/bettertestspoc/assert/resourceassert/gen/resource_schema_def.go b/pkg/acceptance/bettertestspoc/assert/resourceassert/gen/resource_schema_def.go index 6c803e714f..ab4b7bb538 100644 --- a/pkg/acceptance/bettertestspoc/assert/resourceassert/gen/resource_schema_def.go +++ b/pkg/acceptance/bettertestspoc/assert/resourceassert/gen/resource_schema_def.go @@ -37,4 +37,8 @@ var allResourceSchemaDefs = []ResourceSchemaDef{ name: "DatabaseRole", schema: resources.DatabaseRole().Schema, }, + { + name: "ResourceMonitor", + schema: resources.ResourceMonitor().Schema, + }, } diff --git a/pkg/acceptance/bettertestspoc/assert/resourceassert/resource_monitor_resource_ext.go b/pkg/acceptance/bettertestspoc/assert/resourceassert/resource_monitor_resource_ext.go new file mode 100644 index 0000000000..3fcbca3865 --- /dev/null +++ b/pkg/acceptance/bettertestspoc/assert/resourceassert/resource_monitor_resource_ext.go @@ -0,0 +1,38 @@ +package resourceassert + +import ( + "fmt" + "strconv" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert" +) + +func (r *ResourceMonitorResourceAssert) HasStartTimestampNotEmpty() *ResourceMonitorResourceAssert { + r.AddAssertion(assert.ValuePresent("start_timestamp")) + return r +} + +func (r *ResourceMonitorResourceAssert) HasEndTimestampNotEmpty() *ResourceMonitorResourceAssert { + r.AddAssertion(assert.ValuePresent("end_timestamp")) + return r +} + +func (r *ResourceMonitorResourceAssert) HasNotifyUsersLen(len int) *ResourceMonitorResourceAssert { + r.AddAssertion(assert.ValueSet("notify_users.#", strconv.FormatInt(int64(len), 10))) + return r +} + +func (r *ResourceMonitorResourceAssert) HasNotifyUser(index int, userName string) *ResourceMonitorResourceAssert { + r.AddAssertion(assert.ValueSet(fmt.Sprintf("notify_users.%d", index), userName)) + return r +} + +func (r *ResourceMonitorResourceAssert) HasNotifyTriggersLen(len int) *ResourceMonitorResourceAssert { + r.AddAssertion(assert.ValueSet("notify_triggers.#", strconv.FormatInt(int64(len), 10))) + return r +} + +func (r *ResourceMonitorResourceAssert) HasNotifyTrigger(index int, threshold int) *ResourceMonitorResourceAssert { + r.AddAssertion(assert.ValueSet(fmt.Sprintf("notify_triggers.%d", index), strconv.Itoa(threshold))) + return r +} diff --git a/pkg/acceptance/bettertestspoc/assert/resourceassert/resource_monitor_resource_gen.go b/pkg/acceptance/bettertestspoc/assert/resourceassert/resource_monitor_resource_gen.go new file mode 100644 index 0000000000..343aec7bfb --- /dev/null +++ b/pkg/acceptance/bettertestspoc/assert/resourceassert/resource_monitor_resource_gen.go @@ -0,0 +1,137 @@ +// Code generated by assertions generator; DO NOT EDIT. + +package resourceassert + +import ( + "testing" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert" +) + +type ResourceMonitorResourceAssert struct { + *assert.ResourceAssert +} + +func ResourceMonitorResource(t *testing.T, name string) *ResourceMonitorResourceAssert { + t.Helper() + + return &ResourceMonitorResourceAssert{ + ResourceAssert: assert.NewResourceAssert(name, "resource"), + } +} + +func ImportedResourceMonitorResource(t *testing.T, id string) *ResourceMonitorResourceAssert { + t.Helper() + + return &ResourceMonitorResourceAssert{ + ResourceAssert: assert.NewImportedResourceAssert(id, "imported resource"), + } +} + +/////////////////////////////////// +// Attribute value string checks // +/////////////////////////////////// + +func (r *ResourceMonitorResourceAssert) HasCreditQuotaString(expected string) *ResourceMonitorResourceAssert { + r.AddAssertion(assert.ValueSet("credit_quota", expected)) + return r +} + +func (r *ResourceMonitorResourceAssert) HasEndTimestampString(expected string) *ResourceMonitorResourceAssert { + r.AddAssertion(assert.ValueSet("end_timestamp", expected)) + return r +} + +func (r *ResourceMonitorResourceAssert) HasFrequencyString(expected string) *ResourceMonitorResourceAssert { + r.AddAssertion(assert.ValueSet("frequency", expected)) + return r +} + +func (r *ResourceMonitorResourceAssert) HasFullyQualifiedNameString(expected string) *ResourceMonitorResourceAssert { + r.AddAssertion(assert.ValueSet("fully_qualified_name", expected)) + return r +} + +func (r *ResourceMonitorResourceAssert) HasNameString(expected string) *ResourceMonitorResourceAssert { + r.AddAssertion(assert.ValueSet("name", expected)) + return r +} + +func (r *ResourceMonitorResourceAssert) HasNotifyTriggersString(expected string) *ResourceMonitorResourceAssert { + r.AddAssertion(assert.ValueSet("notify_triggers", expected)) + return r +} + +func (r *ResourceMonitorResourceAssert) HasNotifyUsersString(expected string) *ResourceMonitorResourceAssert { + r.AddAssertion(assert.ValueSet("notify_users", expected)) + return r +} + +func (r *ResourceMonitorResourceAssert) HasStartTimestampString(expected string) *ResourceMonitorResourceAssert { + r.AddAssertion(assert.ValueSet("start_timestamp", expected)) + return r +} + +func (r *ResourceMonitorResourceAssert) HasSuspendImmediateTriggerString(expected string) *ResourceMonitorResourceAssert { + r.AddAssertion(assert.ValueSet("suspend_immediate_trigger", expected)) + return r +} + +func (r *ResourceMonitorResourceAssert) HasSuspendTriggerString(expected string) *ResourceMonitorResourceAssert { + r.AddAssertion(assert.ValueSet("suspend_trigger", expected)) + return r +} + +//////////////////////////// +// Attribute empty checks // +//////////////////////////// + +func (r *ResourceMonitorResourceAssert) HasNoCreditQuota() *ResourceMonitorResourceAssert { + r.AddAssertion(assert.ValueNotSet("credit_quota")) + return r +} + +func (r *ResourceMonitorResourceAssert) HasNoEndTimestamp() *ResourceMonitorResourceAssert { + r.AddAssertion(assert.ValueNotSet("end_timestamp")) + return r +} + +func (r *ResourceMonitorResourceAssert) HasNoFrequency() *ResourceMonitorResourceAssert { + r.AddAssertion(assert.ValueNotSet("frequency")) + return r +} + +func (r *ResourceMonitorResourceAssert) HasNoFullyQualifiedName() *ResourceMonitorResourceAssert { + r.AddAssertion(assert.ValueNotSet("fully_qualified_name")) + return r +} + +func (r *ResourceMonitorResourceAssert) HasNoName() *ResourceMonitorResourceAssert { + r.AddAssertion(assert.ValueNotSet("name")) + return r +} + +func (r *ResourceMonitorResourceAssert) HasNoNotifyTriggers() *ResourceMonitorResourceAssert { + r.AddAssertion(assert.ValueNotSet("notify_triggers")) + return r +} + +func (r *ResourceMonitorResourceAssert) HasNoNotifyUsers() *ResourceMonitorResourceAssert { + r.AddAssertion(assert.ValueNotSet("notify_users")) + return r +} + +func (r *ResourceMonitorResourceAssert) HasNoStartTimestamp() *ResourceMonitorResourceAssert { + r.AddAssertion(assert.ValueNotSet("start_timestamp")) + return r +} + +func (r *ResourceMonitorResourceAssert) HasNoSuspendImmediateTrigger() *ResourceMonitorResourceAssert { + r.AddAssertion(assert.ValueNotSet("suspend_immediate_trigger")) + return r +} + +func (r *ResourceMonitorResourceAssert) HasNoSuspendTrigger() *ResourceMonitorResourceAssert { + r.AddAssertion(assert.ValueNotSet("suspend_trigger")) + return r +} diff --git a/pkg/acceptance/bettertestspoc/assert/resourceassert/view_resource_gen.go b/pkg/acceptance/bettertestspoc/assert/resourceassert/view_resource_gen.go index 9b2994e865..f95069aa91 100644 --- a/pkg/acceptance/bettertestspoc/assert/resourceassert/view_resource_gen.go +++ b/pkg/acceptance/bettertestspoc/assert/resourceassert/view_resource_gen.go @@ -42,6 +42,11 @@ func (v *ViewResourceAssert) HasChangeTrackingString(expected string) *ViewResou return v } +func (v *ViewResourceAssert) HasColumnString(expected string) *ViewResourceAssert { + v.AddAssertion(assert.ValueSet("column", expected)) + return v +} + func (v *ViewResourceAssert) HasCommentString(expected string) *ViewResourceAssert { v.AddAssertion(assert.ValueSet("comment", expected)) return v @@ -121,6 +126,11 @@ func (v *ViewResourceAssert) HasNoChangeTracking() *ViewResourceAssert { return v } +func (v *ViewResourceAssert) HasNoColumn() *ViewResourceAssert { + v.AddAssertion(assert.ValueNotSet("column")) + return v +} + func (v *ViewResourceAssert) HasNoComment() *ViewResourceAssert { v.AddAssertion(assert.ValueNotSet("comment")) return v 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 new file mode 100644 index 0000000000..02acd913a9 --- /dev/null +++ b/pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert/resource_monitor_show_output_ext.go @@ -0,0 +1,23 @@ +package resourceshowoutputassert + +import "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert" + +func (r *ResourceMonitorShowOutputAssert) HasStartTimeNotEmpty() *ResourceMonitorShowOutputAssert { + r.AddAssertion(assert.ResourceShowOutputValuePresent("start_time")) + return r +} + +func (r *ResourceMonitorShowOutputAssert) HasEndTimeNotEmpty() *ResourceMonitorShowOutputAssert { + r.AddAssertion(assert.ResourceShowOutputValuePresent("end_time")) + return r +} + +func (r *ResourceMonitorShowOutputAssert) HasCreatedOnNotEmpty() *ResourceMonitorShowOutputAssert { + r.AddAssertion(assert.ResourceShowOutputValuePresent("created_on")) + return r +} + +func (r *ResourceMonitorShowOutputAssert) HasOwnerNotEmpty() *ResourceMonitorShowOutputAssert { + r.AddAssertion(assert.ResourceShowOutputValuePresent("owner")) + return r +} diff --git a/pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert/resource_monitor_show_output_gen.go b/pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert/resource_monitor_show_output_gen.go new file mode 100644 index 0000000000..5ee1f9ee40 --- /dev/null +++ b/pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert/resource_monitor_show_output_gen.go @@ -0,0 +1,107 @@ +// Code generated by assertions generator; DO NOT EDIT. + +package resourceshowoutputassert + +import ( + "testing" + "time" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" +) + +// to ensure sdk package is used +var _ = sdk.Object{} + +type ResourceMonitorShowOutputAssert struct { + *assert.ResourceAssert +} + +func ResourceMonitorShowOutput(t *testing.T, name string) *ResourceMonitorShowOutputAssert { + t.Helper() + + r := ResourceMonitorShowOutputAssert{ + ResourceAssert: assert.NewResourceAssert(name, "show_output"), + } + r.AddAssertion(assert.ValueSet("show_output.#", "1")) + return &r +} + +func ImportedResourceMonitorShowOutput(t *testing.T, id string) *ResourceMonitorShowOutputAssert { + t.Helper() + + r := ResourceMonitorShowOutputAssert{ + ResourceAssert: assert.NewImportedResourceAssert(id, "show_output"), + } + r.AddAssertion(assert.ValueSet("show_output.#", "1")) + return &r +} + +//////////////////////////// +// Attribute value checks // +//////////////////////////// + +func (r *ResourceMonitorShowOutputAssert) HasName(expected string) *ResourceMonitorShowOutputAssert { + r.AddAssertion(assert.ResourceShowOutputValueSet("name", expected)) + return r +} + +func (r *ResourceMonitorShowOutputAssert) HasCreditQuota(expected float64) *ResourceMonitorShowOutputAssert { + r.AddAssertion(assert.ResourceShowOutputFloatValueSet("credit_quota", expected)) + return r +} + +func (r *ResourceMonitorShowOutputAssert) HasUsedCredits(expected float64) *ResourceMonitorShowOutputAssert { + r.AddAssertion(assert.ResourceShowOutputFloatValueSet("used_credits", expected)) + return r +} + +func (r *ResourceMonitorShowOutputAssert) HasRemainingCredits(expected float64) *ResourceMonitorShowOutputAssert { + r.AddAssertion(assert.ResourceShowOutputFloatValueSet("remaining_credits", expected)) + return r +} + +func (r *ResourceMonitorShowOutputAssert) HasLevel(expected sdk.ResourceMonitorLevel) *ResourceMonitorShowOutputAssert { + r.AddAssertion(assert.ResourceShowOutputStringUnderlyingValueSet("level", expected)) + return r +} + +func (r *ResourceMonitorShowOutputAssert) HasFrequency(expected sdk.Frequency) *ResourceMonitorShowOutputAssert { + r.AddAssertion(assert.ResourceShowOutputStringUnderlyingValueSet("frequency", expected)) + return r +} + +func (r *ResourceMonitorShowOutputAssert) HasStartTime(expected string) *ResourceMonitorShowOutputAssert { + r.AddAssertion(assert.ResourceShowOutputValueSet("start_time", expected)) + return r +} + +func (r *ResourceMonitorShowOutputAssert) HasEndTime(expected string) *ResourceMonitorShowOutputAssert { + r.AddAssertion(assert.ResourceShowOutputValueSet("end_time", expected)) + return r +} + +func (r *ResourceMonitorShowOutputAssert) HasSuspendAt(expected int) *ResourceMonitorShowOutputAssert { + r.AddAssertion(assert.ResourceShowOutputIntValueSet("suspend_at", expected)) + return r +} + +func (r *ResourceMonitorShowOutputAssert) HasSuspendImmediateAt(expected int) *ResourceMonitorShowOutputAssert { + r.AddAssertion(assert.ResourceShowOutputIntValueSet("suspend_immediate_at", expected)) + return r +} + +func (r *ResourceMonitorShowOutputAssert) HasCreatedOn(expected time.Time) *ResourceMonitorShowOutputAssert { + r.AddAssertion(assert.ResourceShowOutputValueSet("created_on", expected.String())) + return r +} + +func (r *ResourceMonitorShowOutputAssert) HasOwner(expected string) *ResourceMonitorShowOutputAssert { + r.AddAssertion(assert.ResourceShowOutputValueSet("owner", expected)) + return r +} + +func (r *ResourceMonitorShowOutputAssert) HasComment(expected string) *ResourceMonitorShowOutputAssert { + r.AddAssertion(assert.ResourceShowOutputValueSet("comment", expected)) + return r +} diff --git a/pkg/acceptance/bettertestspoc/config/config.go b/pkg/acceptance/bettertestspoc/config/config.go index b2d0a291fa..7bc3a98531 100644 --- a/pkg/acceptance/bettertestspoc/config/config.go +++ b/pkg/acceptance/bettertestspoc/config/config.go @@ -3,9 +3,12 @@ package config import ( "encoding/json" "fmt" + "reflect" "strings" "testing" + tfconfig "github.com/hashicorp/terraform-plugin-testing/config" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/provider/resources" "github.com/stretchr/testify/require" ) @@ -96,6 +99,23 @@ func FromModel(t *testing.T, model ResourceModel) string { return s } +func ConfigVariablesFromModel(t *testing.T, model ResourceModel) tfconfig.Variables { + t.Helper() + variables := make(tfconfig.Variables) + rType := reflect.TypeOf(model).Elem() + rValue := reflect.ValueOf(model).Elem() + for i := 0; i < rType.NumField(); i++ { + field := rType.Field(i) + if jsonTag, ok := field.Tag.Lookup("json"); ok { + name := strings.Split(jsonTag, ",")[0] + if fieldValue, ok := rValue.Field(i).Interface().(tfconfig.Variable); ok { + variables[name] = fieldValue + } + } + } + return variables +} + type nullVariable struct{} // MarshalJSON returns the JSON encoding of nullVariable. diff --git a/pkg/acceptance/bettertestspoc/config/model/gen/model.go b/pkg/acceptance/bettertestspoc/config/model/gen/model.go index 2e5a2da525..4d4065eee1 100644 --- a/pkg/acceptance/bettertestspoc/config/model/gen/model.go +++ b/pkg/acceptance/bettertestspoc/config/model/gen/model.go @@ -69,7 +69,8 @@ func ModelFromResourceSchemaDetails(resourceSchemaDetails genhelpers.ResourceSch Name: resourceSchemaDetails.ObjectName(), Attributes: attributes, PreambleModel: PreambleModel{ - PackageName: packageWithGenerateDirective, + PackageName: packageWithGenerateDirective, + AdditionalStandardImports: []string{}, }, } } diff --git a/pkg/acceptance/bettertestspoc/config/model/resource_monitor_model_gen.go b/pkg/acceptance/bettertestspoc/config/model/resource_monitor_model_gen.go new file mode 100644 index 0000000000..e2ccebc599 --- /dev/null +++ b/pkg/acceptance/bettertestspoc/config/model/resource_monitor_model_gen.go @@ -0,0 +1,148 @@ +// Code generated by config model builder generator; DO NOT EDIT. + +package model + +import ( + tfconfig "github.com/hashicorp/terraform-plugin-testing/config" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/config" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/provider/resources" +) + +type ResourceMonitorModel struct { + CreditQuota tfconfig.Variable `json:"credit_quota,omitempty"` + EndTimestamp tfconfig.Variable `json:"end_timestamp,omitempty"` + Frequency tfconfig.Variable `json:"frequency,omitempty"` + FullyQualifiedName tfconfig.Variable `json:"fully_qualified_name,omitempty"` + Name tfconfig.Variable `json:"name,omitempty"` + NotifyTriggers tfconfig.Variable `json:"notify_triggers,omitempty"` + NotifyUsers tfconfig.Variable `json:"notify_users,omitempty"` + StartTimestamp tfconfig.Variable `json:"start_timestamp,omitempty"` + SuspendImmediateTrigger tfconfig.Variable `json:"suspend_immediate_trigger,omitempty"` + SuspendTrigger tfconfig.Variable `json:"suspend_trigger,omitempty"` + + *config.ResourceModelMeta +} + +///////////////////////////////////////////////// +// Basic builders (resource name and required) // +///////////////////////////////////////////////// + +func ResourceMonitor( + resourceName string, + name string, +) *ResourceMonitorModel { + r := &ResourceMonitorModel{ResourceModelMeta: config.Meta(resourceName, resources.ResourceMonitor)} + r.WithName(name) + return r +} + +func ResourceMonitorWithDefaultMeta( + name string, +) *ResourceMonitorModel { + r := &ResourceMonitorModel{ResourceModelMeta: config.DefaultMeta(resources.ResourceMonitor)} + r.WithName(name) + return r +} + +///////////////////////////////// +// below all the proper values // +///////////////////////////////// + +func (r *ResourceMonitorModel) WithCreditQuota(creditQuota int) *ResourceMonitorModel { + r.CreditQuota = tfconfig.IntegerVariable(creditQuota) + return r +} + +func (r *ResourceMonitorModel) WithEndTimestamp(endTimestamp string) *ResourceMonitorModel { + r.EndTimestamp = tfconfig.StringVariable(endTimestamp) + return r +} + +func (r *ResourceMonitorModel) WithFrequency(frequency string) *ResourceMonitorModel { + r.Frequency = tfconfig.StringVariable(frequency) + return r +} + +func (r *ResourceMonitorModel) WithFullyQualifiedName(fullyQualifiedName string) *ResourceMonitorModel { + r.FullyQualifiedName = tfconfig.StringVariable(fullyQualifiedName) + return r +} + +func (r *ResourceMonitorModel) WithName(name string) *ResourceMonitorModel { + r.Name = tfconfig.StringVariable(name) + return r +} + +// notify_triggers attribute type is not yet supported, so WithNotifyTriggers can't be generated + +// notify_users attribute type is not yet supported, so WithNotifyUsers can't be generated + +func (r *ResourceMonitorModel) WithStartTimestamp(startTimestamp string) *ResourceMonitorModel { + r.StartTimestamp = tfconfig.StringVariable(startTimestamp) + return r +} + +func (r *ResourceMonitorModel) WithSuspendImmediateTrigger(suspendImmediateTrigger int) *ResourceMonitorModel { + r.SuspendImmediateTrigger = tfconfig.IntegerVariable(suspendImmediateTrigger) + return r +} + +func (r *ResourceMonitorModel) WithSuspendTrigger(suspendTrigger int) *ResourceMonitorModel { + r.SuspendTrigger = tfconfig.IntegerVariable(suspendTrigger) + return r +} + +////////////////////////////////////////// +// below it's possible to set any value // +////////////////////////////////////////// + +func (r *ResourceMonitorModel) WithCreditQuotaValue(value tfconfig.Variable) *ResourceMonitorModel { + r.CreditQuota = value + return r +} + +func (r *ResourceMonitorModel) WithEndTimestampValue(value tfconfig.Variable) *ResourceMonitorModel { + r.EndTimestamp = value + return r +} + +func (r *ResourceMonitorModel) WithFrequencyValue(value tfconfig.Variable) *ResourceMonitorModel { + r.Frequency = value + return r +} + +func (r *ResourceMonitorModel) WithFullyQualifiedNameValue(value tfconfig.Variable) *ResourceMonitorModel { + r.FullyQualifiedName = value + return r +} + +func (r *ResourceMonitorModel) WithNameValue(value tfconfig.Variable) *ResourceMonitorModel { + r.Name = value + return r +} + +func (r *ResourceMonitorModel) WithNotifyTriggersValue(value tfconfig.Variable) *ResourceMonitorModel { + r.NotifyTriggers = value + return r +} + +func (r *ResourceMonitorModel) WithNotifyUsersValue(value tfconfig.Variable) *ResourceMonitorModel { + r.NotifyUsers = value + return r +} + +func (r *ResourceMonitorModel) WithStartTimestampValue(value tfconfig.Variable) *ResourceMonitorModel { + r.StartTimestamp = value + return r +} + +func (r *ResourceMonitorModel) WithSuspendImmediateTriggerValue(value tfconfig.Variable) *ResourceMonitorModel { + r.SuspendImmediateTrigger = value + return r +} + +func (r *ResourceMonitorModel) WithSuspendTriggerValue(value tfconfig.Variable) *ResourceMonitorModel { + r.SuspendTrigger = value + return r +} diff --git a/pkg/acceptance/bettertestspoc/config/model/view_model_gen.go b/pkg/acceptance/bettertestspoc/config/model/view_model_gen.go index 2dc0920dd1..1afe8859d8 100644 --- a/pkg/acceptance/bettertestspoc/config/model/view_model_gen.go +++ b/pkg/acceptance/bettertestspoc/config/model/view_model_gen.go @@ -12,6 +12,7 @@ import ( type ViewModel struct { AggregationPolicy tfconfig.Variable `json:"aggregation_policy,omitempty"` ChangeTracking tfconfig.Variable `json:"change_tracking,omitempty"` + Column tfconfig.Variable `json:"column,omitempty"` Comment tfconfig.Variable `json:"comment,omitempty"` CopyGrants tfconfig.Variable `json:"copy_grants,omitempty"` DataMetricFunction tfconfig.Variable `json:"data_metric_function,omitempty"` @@ -73,6 +74,8 @@ func (v *ViewModel) WithChangeTracking(changeTracking string) *ViewModel { return v } +// column attribute type is not yet supported, so WithColumn can't be generated + func (v *ViewModel) WithComment(comment string) *ViewModel { v.Comment = tfconfig.StringVariable(comment) return v @@ -143,6 +146,11 @@ func (v *ViewModel) WithChangeTrackingValue(value tfconfig.Variable) *ViewModel return v } +func (v *ViewModel) WithColumnValue(value tfconfig.Variable) *ViewModel { + v.Column = value + return v +} + func (v *ViewModel) WithCommentValue(value tfconfig.Variable) *ViewModel { v.Comment = value return v diff --git a/pkg/acceptance/helpers/resource_monitor_client.go b/pkg/acceptance/helpers/resource_monitor_client.go index eb2dd01107..794cc57bd0 100644 --- a/pkg/acceptance/helpers/resource_monitor_client.go +++ b/pkg/acceptance/helpers/resource_monitor_client.go @@ -62,6 +62,13 @@ func (c *ResourceMonitorClient) CreateResourceMonitorWithOptions(t *testing.T, o return resourceMonitor, c.DropResourceMonitorFunc(t, id) } +func (c *ResourceMonitorClient) Alter(t *testing.T, id sdk.AccountObjectIdentifier, opts *sdk.AlterResourceMonitorOptions) { + t.Helper() + ctx := context.Background() + err := c.client().Alter(ctx, id, opts) + require.NoError(t, err) +} + func (c *ResourceMonitorClient) DropResourceMonitorFunc(t *testing.T, id sdk.AccountObjectIdentifier) func() { t.Helper() ctx := context.Background() @@ -71,3 +78,8 @@ func (c *ResourceMonitorClient) DropResourceMonitorFunc(t *testing.T, id sdk.Acc require.NoError(t, err) } } + +func (c *ResourceMonitorClient) Show(t *testing.T, id sdk.AccountObjectIdentifier) (*sdk.ResourceMonitor, error) { + t.Helper() + return c.client().ShowByID(context.Background(), id) +} diff --git a/pkg/internal/genhelpers/struct_details_extractor_test.go b/pkg/internal/genhelpers/struct_details_extractor_test.go index 1eae83d7b2..a6ede897d8 100644 --- a/pkg/internal/genhelpers/struct_details_extractor_test.go +++ b/pkg/internal/genhelpers/struct_details_extractor_test.go @@ -16,6 +16,7 @@ import ( // // TODO [SNOW-1501905]: test type of slice fields func Test_ExtractStructDetails(t *testing.T) { + type testIntEnum int type testStruct struct { unexportedString string unexportedInt int @@ -31,8 +32,8 @@ func Test_ExtractStructDetails(t *testing.T) { unexportedStringEnum sdk.WarehouseType unexportedStringEnumPtr *sdk.WarehouseType - unexportedIntEnum sdk.ResourceMonitorLevel - unexportedIntEnumPtr *sdk.ResourceMonitorLevel + unexportedIntEnum testIntEnum + unexportedIntEnumPtr *testIntEnum unexportedAccountIdentifier sdk.AccountIdentifier unexportedExternalObjectIdentifier sdk.ExternalObjectIdentifier @@ -90,8 +91,8 @@ func Test_ExtractStructDetails(t *testing.T) { assertFieldExtracted(structDetails.Fields[10], "unexportedStringEnum", "sdk.WarehouseType", "string") assertFieldExtracted(structDetails.Fields[11], "unexportedStringEnumPtr", "*sdk.WarehouseType", "*string") - assertFieldExtracted(structDetails.Fields[12], "unexportedIntEnum", "sdk.ResourceMonitorLevel", "int") - assertFieldExtracted(structDetails.Fields[13], "unexportedIntEnumPtr", "*sdk.ResourceMonitorLevel", "*int") + assertFieldExtracted(structDetails.Fields[12], "unexportedIntEnum", "genhelpers.testIntEnum", "int") + assertFieldExtracted(structDetails.Fields[13], "unexportedIntEnumPtr", "*genhelpers.testIntEnum", "*int") assertFieldExtracted(structDetails.Fields[14], "unexportedAccountIdentifier", "sdk.AccountIdentifier", "struct") assertFieldExtracted(structDetails.Fields[15], "unexportedExternalObjectIdentifier", "sdk.ExternalObjectIdentifier", "struct") diff --git a/pkg/resources/resource_monitor.go b/pkg/resources/resource_monitor.go index bf862b715b..f56c965418 100644 --- a/pkg/resources/resource_monitor.go +++ b/pkg/resources/resource_monitor.go @@ -2,504 +2,438 @@ package resources import ( "context" + "errors" "fmt" + "reflect" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/logging" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/provider" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/schemas" - - "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) -var validFrequencies = []string{"MONTHLY", "DAILY", "WEEKLY", "YEARLY", "NEVER"} - var resourceMonitorSchema = map[string]*schema.Schema{ "name": { - Type: schema.TypeString, - Required: true, - Description: "Identifier for the resource monitor; must be unique for your account.", - ForceNew: true, + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: blocklistedCharactersFieldDescription("Identifier for the resource monitor; must be unique for your account."), + DiffSuppressFunc: suppressIdentifierQuoting, }, "notify_users": { Type: schema.TypeSet, Optional: true, - Description: "Specifies the list of users to receive email notifications on resource monitors.", + Description: "Specifies the list of users (their identifiers) to receive email notifications on resource monitors.", Elem: &schema.Schema{ Type: schema.TypeString, }, }, "credit_quota": { - Type: schema.TypeInt, - Optional: true, - Computed: true, - Description: "The number of credits allocated monthly to the resource monitor.", + Type: schema.TypeInt, + Optional: true, + ValidateDiagFunc: validation.ToDiagFunc(validation.IntAtLeast(1)), + DiffSuppressFunc: IgnoreChangeToCurrentSnowflakeValueInShow("credit_quota"), + Description: "The number of credits allocated to the resource monitor per frequency interval. When total usage for all warehouses assigned to the monitor reaches this number for the current frequency interval, the resource monitor is considered to be at 100% of quota.", }, "frequency": { - Type: schema.TypeString, - Optional: true, - Computed: true, - Description: "The frequency interval at which the credit usage resets to 0. If you set a frequency for a resource monitor, you must also set START_TIMESTAMP.", - ValidateFunc: validation.StringInSlice(validFrequencies, false), + Type: schema.TypeString, + Optional: true, + RequiredWith: []string{"start_timestamp"}, + ValidateDiagFunc: sdkValidation(sdk.ToResourceMonitorFrequency), + DiffSuppressFunc: SuppressIfAny(NormalizeAndCompare(sdk.ToResourceMonitorFrequency), IgnoreChangeToCurrentSnowflakeValueInShow("frequency")), + Description: fmt.Sprintf("The frequency interval at which the credit usage resets to 0. Valid values are (case-insensitive): %s. If you set a `frequency` for a resource monitor, you must also set `start_timestamp`. If you specify `NEVER` for the frequency, the credit usage for the warehouse does not reset. After removing this field from the config, the previously set value will be preserved on the Snowflake side, not the default value. That's due to Snowflake limitation and the lack of unset functionality for this parameter.", possibleValuesListed(sdk.AllFrequencyValues)), }, "start_timestamp": { - Type: schema.TypeString, - Optional: true, - Computed: true, - Description: "The date and time when the resource monitor starts monitoring credit usage for the assigned warehouses.", + Type: schema.TypeString, + Optional: true, + RequiredWith: []string{"frequency"}, + DiffSuppressFunc: IgnoreChangeToCurrentSnowflakeValueInShow("start_time"), + Description: "The date and time when the resource monitor starts monitoring credit usage for the assigned warehouses. If you set a `start_timestamp` for a resource monitor, you must also set `frequency`. After removing this field from the config, the previously set value will be preserved on the Snowflake side, not the default value. That's due to Snowflake limitation and the lack of unset functionality for this parameter.", }, "end_timestamp": { - Type: schema.TypeString, - Optional: true, - Description: "The date and time when the resource monitor suspends the assigned warehouses.", - }, - "suspend_trigger": { - Type: schema.TypeInt, - Optional: true, - Description: "The number that represents the percentage threshold at which to suspend all warehouses.", - ConflictsWith: []string{"suspend_triggers"}, - }, - "suspend_triggers": { - Type: schema.TypeSet, - Elem: &schema.Schema{Type: schema.TypeInt}, - Optional: true, - Description: "A list of percentage thresholds at which to suspend all warehouses.", - ConflictsWith: []string{"suspend_trigger"}, - Deprecated: "Use suspend_trigger instead", - }, - "suspend_immediate_trigger": { - Type: schema.TypeInt, - Optional: true, - Description: "The number that represents the percentage threshold at which to immediately suspend all warehouses.", - ConflictsWith: []string{"suspend_immediate_triggers"}, - }, - "suspend_immediate_triggers": { - Type: schema.TypeSet, - Elem: &schema.Schema{Type: schema.TypeInt}, - Optional: true, - Description: "A list of percentage thresholds at which to suspend all warehouses.", - ConflictsWith: []string{"suspend_immediate_trigger"}, - Deprecated: "Use suspend_immediate_trigger instead", + Type: schema.TypeString, + Optional: true, + DiffSuppressFunc: IgnoreChangeToCurrentSnowflakeValueInShow("end_time"), + Description: "The date and time when the resource monitor suspends the assigned warehouses.", }, "notify_triggers": { Type: schema.TypeSet, - Elem: &schema.Schema{Type: schema.TypeInt}, Optional: true, - Description: "A list of percentage thresholds at which to send an alert to subscribed users.", + Description: "Specifies a list of percentages of the credit quota. After reaching any of the values the users passed in the notify_users field will be notified (to receive the notification they should have notifications enabled). Values over 100 are supported.", + Elem: &schema.Schema{ + Type: schema.TypeInt, + ValidateDiagFunc: validation.ToDiagFunc(validation.IntAtLeast(1)), + }, }, - "set_for_account": { - Type: schema.TypeBool, - Optional: true, - Description: "Specifies whether the resource monitor should be applied globally to your Snowflake account (defaults to false).", - Default: false, + "suspend_trigger": { + Type: schema.TypeInt, + Optional: true, + Description: "Represents a numeric value specified as a percentage of the credit quota. Values over 100 are supported. After reaching this value, all assigned warehouses while allowing currently running queries to complete will be suspended. No new queries can be executed by the warehouses until the credit quota for the resource monitor is increased. In addition, this action sends a notification to all users who have enabled notifications for themselves.", + ValidateDiagFunc: validation.ToDiagFunc(validation.IntAtLeast(1)), + DiffSuppressFunc: IgnoreChangeToCurrentSnowflakeValueInShow("suspend_at"), }, - "warehouses": { - Type: schema.TypeSet, - Optional: true, - Description: "A list of warehouses to apply the resource monitor to.", - Elem: &schema.Schema{Type: schema.TypeString}, + "suspend_immediate_trigger": { + Type: schema.TypeInt, + Optional: true, + Description: "Represents a numeric value specified as a percentage of the credit quota. Values over 100 are supported. After reaching this value, all assigned warehouses immediately cancel any currently running queries or statements. In addition, this action sends a notification to all users who have enabled notifications for themselves.", + ValidateDiagFunc: validation.ToDiagFunc(validation.IntAtLeast(1)), + DiffSuppressFunc: IgnoreChangeToCurrentSnowflakeValueInShow("suspend_immediately_at"), + }, + ShowOutputAttributeName: { + Type: schema.TypeList, + Computed: true, + Description: "Outputs the result of `SHOW RESOURCE MONITORS` for the given resource monitor.", + Elem: &schema.Resource{ + Schema: schemas.ShowResourceMonitorSchema, + }, }, FullyQualifiedNameAttributeName: schemas.FullyQualifiedNameSchema, } -// ResourceMonitor returns a pointer to the resource representing a resource monitor. func ResourceMonitor() *schema.Resource { return &schema.Resource{ - Create: CreateResourceMonitor, - Read: ReadResourceMonitor, - Update: UpdateResourceMonitor, - Delete: DeleteResourceMonitor, + CreateContext: CreateResourceMonitor, + ReadContext: ReadResourceMonitor(true), + UpdateContext: UpdateResourceMonitor, + DeleteContext: DeleteResourceMonitor, Schema: resourceMonitorSchema, Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, + StateContext: ImportResourceMonitor, }, + + CustomizeDiff: customdiff.All( + ComputedIfAnyAttributeChanged(resourceMonitorSchema, ShowOutputAttributeName, "notify_users", "credit_quota", "frequency", "start_timestamp", "end_timestamp", "notify_triggers", "suspend_trigger", "suspend_immediate_trigger"), + ), } } -func checkAccountAgainstWarehouses(d *schema.ResourceData, name string) error { - account := d.Get("set_for_account").(bool) - v := d.Get("warehouses") +func ImportResourceMonitor(ctx context.Context, d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { + logging.DebugLogger.Printf("[DEBUG] Starting resource monitor import") + client := meta.(*provider.Context).Client - if len(v.(*schema.Set).List()) > 0 && account { - return fmt.Errorf("error creating resource monitor %v on account err = set_for_account cannot be true and give warehouses", name) + id, err := sdk.ParseAccountObjectIdentifier(d.Id()) + if err != nil { + return nil, err } - return nil + + resourceMonitor, err := client.ResourceMonitors.ShowByID(ctx, id) + if err != nil { + return nil, err + } + + if err := d.Set("name", id.Name()); err != nil { + return nil, err + } + if err := d.Set("credit_quota", resourceMonitor.CreditQuota); err != nil { + return nil, err + } + if err := d.Set("frequency", resourceMonitor.Frequency); err != nil { + return nil, err + } + if err := d.Set("start_timestamp", resourceMonitor.StartTime); err != nil { + return nil, err + } + if err := d.Set("end_timestamp", resourceMonitor.EndTime); err != nil { + return nil, err + } + if err := d.Set("notify_triggers", resourceMonitor.NotifyAt); err != nil { + return nil, err + } + if err := d.Set("suspend_trigger", resourceMonitor.SuspendAt); err != nil { + return nil, err + } + if err := d.Set("suspend_immediate_trigger", resourceMonitor.SuspendImmediateAt); err != nil { + return nil, err + } + + return []*schema.ResourceData{d}, nil } -// CreateResourceMonitor implements schema.CreateFunc. -func CreateResourceMonitor(d *schema.ResourceData, meta interface{}) error { +func CreateResourceMonitor(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*provider.Context).Client - name := d.Get("name").(string) + id := sdk.NewAccountObjectIdentifier(d.Get("name").(string)) - check := checkAccountAgainstWarehouses(d, name) + opts := new(sdk.CreateResourceMonitorOptions) + with := new(sdk.ResourceMonitorWith) - if check != nil { - return check + if v, ok := d.GetOk("credit_quota"); ok { + with.CreditQuota = sdk.Pointer(v.(int)) } - ctx := context.Background() - objectIdentifier := sdk.NewAccountObjectIdentifier(name) - - // Set optionals. - opts := &sdk.CreateResourceMonitorOptions{} if v, ok := d.GetOk("notify_users"); ok { - userNames := expandStringList(v.(*schema.Set).List()) - users := []sdk.NotifiedUser{} - for _, name := range userNames { - users = append(users, sdk.NotifiedUser{Name: name}) - } - if opts.With == nil { - opts.With = &sdk.ResourceMonitorWith{} + userIds := expandStringList(v.(*schema.Set).List()) + users := make([]sdk.NotifiedUser, len(userIds)) + for i, userId := range userIds { + users[i] = sdk.NotifiedUser{ + Name: sdk.NewAccountObjectIdentifier(userId), + } } - opts.With.NotifyUsers = &sdk.NotifyUsers{Users: users} + with.NotifyUsers = &sdk.NotifyUsers{Users: users} } - if v, ok := d.GetOk("credit_quota"); ok { - if opts.With == nil { - opts.With = &sdk.ResourceMonitorWith{} - } - opts.With.CreditQuota = sdk.Int(v.(int)) - } if v, ok := d.GetOk("frequency"); ok { - frequency, err := sdk.FrequencyFromString(v.(string)) + frequency, err := sdk.ToResourceMonitorFrequency(v.(string)) if err != nil { - return err + return diag.FromErr(err) } - if opts.With == nil { - opts.With = &sdk.ResourceMonitorWith{} - } - opts.With.Frequency = frequency + with.Frequency = frequency } if v, ok := d.GetOk("start_timestamp"); ok { - if opts.With == nil { - opts.With = &sdk.ResourceMonitorWith{} - } - opts.With.StartTimestamp = sdk.Pointer(v.(string)) + with.StartTimestamp = sdk.Pointer(v.(string)) } + if v, ok := d.GetOk("end_timestamp"); ok { - if opts.With == nil { - opts.With = &sdk.ResourceMonitorWith{} - } - opts.With.EndTimestamp = sdk.Pointer(v.(string)) + with.EndTimestamp = sdk.Pointer(v.(string)) } - triggers := collectResourceMonitorTriggers(d) - if len(triggers) > 0 { - if opts.With == nil { - opts.With = &sdk.ResourceMonitorWith{} + triggers := make([]sdk.TriggerDefinition, 0) + if notifyTriggers, ok := d.GetOk("notify_triggers"); ok { + for _, triggerThreshold := range notifyTriggers.(*schema.Set).List() { + triggers = append(triggers, sdk.TriggerDefinition{ + Threshold: triggerThreshold.(int), + TriggerAction: sdk.TriggerActionNotify, + }) } - opts.With.Triggers = triggers } - err := client.ResourceMonitors.Create(ctx, objectIdentifier, opts) - if err != nil { - return fmt.Errorf("error creating resource monitor %v err = %w", name, err) + if suspendTriggerThreshold, ok := d.GetOk("suspend_trigger"); ok { + triggers = append(triggers, sdk.TriggerDefinition{ + Threshold: suspendTriggerThreshold.(int), + TriggerAction: sdk.TriggerActionSuspend, + }) } - d.SetId(name) - if d.Get("set_for_account").(bool) { - accountOpts := sdk.AlterAccountOptions{ - Set: &sdk.AccountSet{ - ResourceMonitor: objectIdentifier, - }, - } - if err := client.Accounts.Alter(ctx, &accountOpts); err != nil { - return fmt.Errorf("error setting resource monitor %v on account err = %w", name, err) - } + if suspendImmediateTriggerThreshold, ok := d.GetOk("suspend_immediate_trigger"); ok { + triggers = append(triggers, sdk.TriggerDefinition{ + Threshold: suspendImmediateTriggerThreshold.(int), + TriggerAction: sdk.TriggerActionSuspendImmediate, + }) } - if v, ok := d.GetOk("warehouses"); ok { - for _, w := range v.(*schema.Set).List() { - warehouseOpts := sdk.AlterWarehouseOptions{ - Set: &sdk.WarehouseSet{ - ResourceMonitor: objectIdentifier, - }, - } - warehouseId := sdk.NewAccountObjectIdentifier(w.(string)) - if err := client.Warehouses.Alter(ctx, warehouseId, &warehouseOpts); err != nil { - return fmt.Errorf("error setting resource monitor %v on warehouse %v err = %w", name, warehouseId.Name(), err) - } - } + if len(triggers) > 0 { + with.Triggers = triggers } - return ReadResourceMonitor(d, meta) -} - -// ReadResourceMonitor implements schema.ReadFunc. -func ReadResourceMonitor(d *schema.ResourceData, meta interface{}) error { - client := meta.(*provider.Context).Client - id := helpers.DecodeSnowflakeID(d.Id()).(sdk.AccountObjectIdentifier) - - ctx := context.Background() - resourceMonitor, err := client.ResourceMonitors.ShowByID(ctx, id) - if err != nil { - return err - } - if err := d.Set(FullyQualifiedNameAttributeName, id.FullyQualifiedName()); err != nil { - return err + if !reflect.DeepEqual(*with, sdk.ResourceMonitorWith{}) { + opts.With = with } - if err := d.Set("name", resourceMonitor.Name); err != nil { - return err - } - if err := d.Set("frequency", string(resourceMonitor.Frequency)); err != nil { - return err + err := client.ResourceMonitors.Create(ctx, id, opts) + if err != nil { + return diag.FromErr(err) } - if err := d.Set("start_timestamp", resourceMonitor.StartTime); err != nil { - return err - } + d.SetId(helpers.EncodeResourceIdentifier(id)) - if err := d.Set("end_timestamp", resourceMonitor.EndTime); err != nil { - return err - } + return ReadResourceMonitor(false)(ctx, d, meta) +} - if len(resourceMonitor.NotifyUsers) > 0 { - if err := d.Set("notify_users", resourceMonitor.NotifyUsers); err != nil { - return err +func ReadResourceMonitor(withExternalChangesMarking bool) schema.ReadContextFunc { + return func(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*provider.Context).Client + id, err := sdk.ParseAccountObjectIdentifier(d.Id()) + if err != nil { + return diag.FromErr(err) } - } - // Snowflake returns credit_quota as a float, but only accepts input as an int - if err := d.Set("credit_quota", int(resourceMonitor.CreditQuota)); err != nil { - return err - } + resourceMonitor, err := client.ResourceMonitors.ShowByID(ctx, id) + if err != nil { + if errors.Is(err, sdk.ErrObjectNotFound) { + d.SetId("") + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Warning, + Summary: "Failed to query resource monitor. Marking the resource as removed.", + Detail: fmt.Sprintf("Resource Monitor: %s, Err: %s", id.FullyQualifiedName(), err), + }, + } + } + return diag.FromErr(err) + } - // Triggers - if resourceMonitor.SuspendAt != nil { - if err := d.Set("suspend_trigger", *resourceMonitor.SuspendAt); err != nil { - return err + if err := d.Set("notify_users", resourceMonitor.NotifyUsers); err != nil { + return diag.FromErr(err) } - } else { - if err := d.Set("suspend_trigger", nil); err != nil { - return err + + if withExternalChangesMarking { + if err = handleExternalChangesToObjectInShow(d, + showMapping{"credit_quota", "credit_quota", resourceMonitor.CreditQuota, resourceMonitor.CreditQuota, nil}, + showMapping{"frequency", "frequency", string(resourceMonitor.Frequency), resourceMonitor.Frequency, nil}, + showMapping{"start_time", "start_timestamp", resourceMonitor.StartTime, resourceMonitor.StartTime, nil}, + showMapping{"end_time", "end_timestamp", resourceMonitor.EndTime, resourceMonitor.EndTime, nil}, + showMapping{"notify_at", "notify_triggers", resourceMonitor.NotifyAt, resourceMonitor.NotifyAt, nil}, + showMapping{"suspend_at", "suspend_trigger", resourceMonitor.SuspendAt, resourceMonitor.SuspendAt, nil}, + showMapping{"suspend_immediately_at", "suspend_immediate_trigger", resourceMonitor.SuspendImmediateAt, resourceMonitor.SuspendImmediateAt, nil}, + ); err != nil { + return diag.FromErr(err) + } } - } - if resourceMonitor.SuspendImmediateAt != nil { - if err := d.Set("suspend_immediate_trigger", *resourceMonitor.SuspendImmediateAt); err != nil { - return err + if err = setStateToValuesFromConfig(d, warehouseSchema, []string{ + "credit_quota", + "frequency", + "start_timestamp", + "end_timestamp", + "notify_triggers", + "suspend_trigger", + "suspend_immediate_trigger", + }); err != nil { + return diag.FromErr(err) } - } else { - if err := d.Set("suspend_immediate_trigger", nil); err != nil { - return err + + if err = d.Set(ShowOutputAttributeName, []map[string]any{schemas.ResourceMonitorToSchema(resourceMonitor)}); err != nil { + return diag.FromErr(err) } - } - if err := d.Set("notify_triggers", resourceMonitor.NotifyTriggers); err != nil { - return err - } + if err := d.Set(FullyQualifiedNameAttributeName, id.FullyQualifiedName()); err != nil { + return diag.FromErr(err) + } - // Account level - if err := d.Set("set_for_account", resourceMonitor.Level == sdk.ResourceMonitorLevelAccount); err != nil { - return err + return nil } - - return err } -// UpdateResourceMonitor implements schema.UpdateFunc. -func UpdateResourceMonitor(d *schema.ResourceData, meta interface{}) error { +func UpdateResourceMonitor(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*provider.Context).Client - name := d.Get("name").(string) - check := checkAccountAgainstWarehouses(d, name) - - if check != nil { - return check + id, err := sdk.ParseAccountObjectIdentifier(d.Id()) + if err != nil { + return diag.FromErr(err) } - objectIdentifier := helpers.DecodeSnowflakeID(d.Id()).(sdk.AccountObjectIdentifier) - - ctx := context.Background() - var runSetStatement bool - opts := sdk.AlterResourceMonitorOptions{} - set := sdk.ResourceMonitorSet{} + unset := sdk.ResourceMonitorUnset{} + if d.HasChange("credit_quota") { - runSetStatement = true - set.CreditQuota = sdk.Pointer(d.Get("credit_quota").(int)) + if v, ok := d.GetOk("credit_quota"); ok { + set.CreditQuota = sdk.Pointer(v.(int)) + } else { + unset.CreditQuota = sdk.Bool(true) + } } - if d.HasChange("frequency") || d.HasChange("start_timestamp") { - runSetStatement = true - frequency, err := sdk.FrequencyFromString(d.Get("frequency").(string)) + if (d.HasChange("frequency") || d.HasChange("start_timestamp")) && + (d.Get("frequency").(string) != "" && d.Get("start_timestamp").(string) != "") { + frequency, err := sdk.ToResourceMonitorFrequency(d.Get("frequency").(string)) if err != nil { - return err + return diag.FromErr(err) } set.Frequency = frequency set.StartTimestamp = sdk.Pointer(d.Get("start_timestamp").(string)) } if d.HasChange("end_timestamp") { - runSetStatement = true - set.EndTimestamp = sdk.Pointer(d.Get("end_timestamp").(string)) - } - - if d.HasChange("notify_users") { - runSetStatement = true - - userNames := expandStringList(d.Get("notify_users").(*schema.Set).List()) - users := []sdk.NotifiedUser{} - for _, name := range userNames { - users = append(users, sdk.NotifiedUser{Name: name}) - } - set.NotifyUsers = &sdk.NotifyUsers{ - Users: users, + if v, ok := d.GetOk("end_timestamp"); ok { + set.EndTimestamp = sdk.Pointer(v.(string)) + } else { + unset.EndTimestamp = sdk.Bool(true) } } - if set != (sdk.ResourceMonitorSet{}) { - opts.Set = &set - } - - // If ANY of the triggers changed, we collect all triggers and set them - if d.HasChange("suspend_trigger") || d.HasChange("suspend_triggers") || - d.HasChange("suspend_immediate_trigger") || d.HasChange("suspend_immediate_triggers") || - d.HasChange("notify_triggers") { - runSetStatement = true - triggers := collectResourceMonitorTriggers(d) - opts.Triggers = triggers - } - - if runSetStatement { - if err := client.ResourceMonitors.Alter(ctx, objectIdentifier, &opts); err != nil { - return fmt.Errorf("error updating resource monitor %v\n%w", objectIdentifier.Name(), err) + if d.HasChange("notify_users") { + userIds := expandStringList(d.Get("notify_users").(*schema.Set).List()) + if len(userIds) > 0 { + users := make([]sdk.NotifiedUser, len(userIds)) + for i, userId := range userIds { + users[i] = sdk.NotifiedUser{ + Name: sdk.NewAccountObjectIdentifier(userId), + } + } + set.NotifyUsers = &sdk.NotifyUsers{ + Users: users, + } + } else { + unset.NotifyUsers = sdk.Bool(true) } } - // Remove from account - if d.HasChange("set_for_account") && !d.Get("set_for_account").(bool) { - accountOpts := sdk.AlterAccountOptions{ - Set: &sdk.AccountSet{ - ResourceMonitor: sdk.NewAccountObjectIdentifier("NULL"), - }, - } - if err := client.Accounts.Alter(ctx, &accountOpts); err != nil { - return fmt.Errorf("error unsetting resource monitor %v on account err = %w", objectIdentifier.Name(), err) - } - } + if d.HasChanges("notify_triggers", "suspend_trigger", "suspend_immediate_trigger") { + triggers := make([]sdk.TriggerDefinition, 0) - // Remove from all old warehouses - if d.HasChange("warehouses") { - oldV, v := d.GetChange("warehouses") - res := ADiffB(oldV.(*schema.Set).List(), v.(*schema.Set).List()) - for _, w := range res { - warehouseOpts := sdk.AlterWarehouseOptions{ - Unset: &sdk.WarehouseUnset{ - ResourceMonitor: sdk.Bool(true), - }, - } - warehouseId := sdk.NewAccountObjectIdentifier(w) - if err := client.Warehouses.Alter(ctx, warehouseId, &warehouseOpts); err != nil { - return fmt.Errorf("error unsetting resource monitor %v on warehouse %v err = %w", name, warehouseId.Name(), err) + if notifyTriggers, ok := d.GetOk("notify_triggers"); ok { + for _, triggerThreshold := range notifyTriggers.(*schema.Set).List() { + triggers = append(triggers, sdk.TriggerDefinition{ + Threshold: triggerThreshold.(int), + TriggerAction: sdk.TriggerActionNotify, + }) } } - } - // Add to account - if d.HasChange("set_for_account") && d.Get("set_for_account").(bool) { - accountOpts := sdk.AlterAccountOptions{ - Set: &sdk.AccountSet{ - ResourceMonitor: objectIdentifier, - }, + if suspendTriggerThreshold, ok := d.GetOk("suspend_trigger"); ok { + triggers = append(triggers, sdk.TriggerDefinition{ + Threshold: suspendTriggerThreshold.(int), + TriggerAction: sdk.TriggerActionSuspend, + }) } - if err := client.Accounts.Alter(ctx, &accountOpts); err != nil { - return fmt.Errorf("error setting resource monitor %v on account err = %w", name, err) + + if suspendImmediateTriggerThreshold, ok := d.GetOk("suspend_immediate_trigger"); ok { + triggers = append(triggers, sdk.TriggerDefinition{ + Threshold: suspendImmediateTriggerThreshold.(int), + TriggerAction: sdk.TriggerActionSuspendImmediate, + }) } - } - // Add to all new warehouses - if d.HasChange("warehouses") { - oldV, v := d.GetChange("warehouses") - res := ADiffB(v.(*schema.Set).List(), oldV.(*schema.Set).List()) - for _, w := range res { - warehouseOpts := sdk.AlterWarehouseOptions{ - Set: &sdk.WarehouseSet{ - ResourceMonitor: objectIdentifier, + 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.", }, } - warehouseId := sdk.NewAccountObjectIdentifier(w) - if err := client.Warehouses.Alter(ctx, warehouseId, &warehouseOpts); err != nil { - return fmt.Errorf("error setting resource monitor %v on warehouse %v err = %w", name, warehouseId.Name(), err) - } - } - } - - return ReadResourceMonitor(d, meta) -} - -func collectResourceMonitorTriggers(d *schema.ResourceData) []sdk.TriggerDefinition { - triggers := []sdk.TriggerDefinition{} - var suspendTrigger *sdk.TriggerDefinition - if v, ok := d.GetOk("suspend_trigger"); ok { - suspendTrigger = &sdk.TriggerDefinition{ - Threshold: v.(int), - TriggerAction: sdk.TriggerActionSuspend, } } - if v, ok := d.GetOk("suspend_triggers"); ok { - siTrigs := expandIntList(v.(*schema.Set).List()) - for _, threshold := range siTrigs { - if suspendTrigger == nil || suspendTrigger.Threshold > threshold { - suspendTrigger = &sdk.TriggerDefinition{ - Threshold: threshold, - TriggerAction: sdk.TriggerActionSuspend, - } - } + // This is to prevent SQL compilation errors from Snowflake, because you cannot only alter triggers. + // It's going to set credit quota to the same value as before making it pass SQL compilation stage. + if len(opts.Triggers) > 0 && (set == (sdk.ResourceMonitorSet{})) && (unset == (sdk.ResourceMonitorUnset{})) { + if creditQuota, ok := d.GetOk("credit_quota"); ok { + set.CreditQuota = sdk.Pointer(creditQuota.(int)) + } else { + unset.CreditQuota = sdk.Bool(true) } } - if suspendTrigger != nil { - triggers = append(triggers, *suspendTrigger) - } - var suspendImmediateTrigger *sdk.TriggerDefinition - if v, ok := d.GetOk("suspend_immediate_trigger"); ok { - suspendImmediateTrigger = &sdk.TriggerDefinition{ - Threshold: v.(int), - TriggerAction: sdk.TriggerActionSuspendImmediate, + if set != (sdk.ResourceMonitorSet{}) { + opts.Set = &set + if err := client.ResourceMonitors.Alter(ctx, id, &opts); err != nil { + d.Partial(true) + return diag.FromErr(err) } } - if v, ok := d.GetOk("suspend_immediate_triggers"); ok { - siTrigs := expandIntList(v.(*schema.Set).List()) - for _, threshold := range siTrigs { - if suspendImmediateTrigger == nil || (suspendTrigger != nil && suspendTrigger.Threshold > threshold) { - suspendImmediateTrigger = &sdk.TriggerDefinition{ - Threshold: threshold, - TriggerAction: sdk.TriggerActionSuspendImmediate, - } - } + if unset != (sdk.ResourceMonitorUnset{}) { + opts.Unset = &unset + if err := client.ResourceMonitors.Alter(ctx, id, &opts); err != nil { + d.Partial(true) + return diag.FromErr(err) } } - if suspendImmediateTrigger != nil { - triggers = append(triggers, *suspendImmediateTrigger) - } - nTrigs := expandIntList(d.Get("notify_triggers").(*schema.Set).List()) - for _, t := range nTrigs { - triggers = append(triggers, sdk.TriggerDefinition{ - Threshold: t, - TriggerAction: sdk.TriggerActionNotify, - }) - } - return triggers + return ReadResourceMonitor(false)(ctx, d, meta) } -// DeleteResourceMonitor implements schema.DeleteFunc. -func DeleteResourceMonitor(d *schema.ResourceData, meta interface{}) error { +func DeleteResourceMonitor(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*provider.Context).Client - ctx := context.Background() - objectIdentifier := helpers.DecodeSnowflakeID(d.Id()).(sdk.AccountObjectIdentifier) - err := client.ResourceMonitors.Drop(ctx, objectIdentifier, &sdk.DropResourceMonitorOptions{IfExists: sdk.Bool(true)}) + id, err := sdk.ParseAccountObjectIdentifier(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + err = client.ResourceMonitors.Drop(ctx, id, &sdk.DropResourceMonitorOptions{IfExists: sdk.Bool(true)}) if err != nil { - return err + return diag.FromErr(err) } d.SetId("") diff --git a/pkg/resources/resource_monitor_acceptance_test.go b/pkg/resources/resource_monitor_acceptance_test.go index 54aeaf7146..c9461d9345 100644 --- a/pkg/resources/resource_monitor_acceptance_test.go +++ b/pkg/resources/resource_monitor_acceptance_test.go @@ -1,24 +1,33 @@ package resources_test import ( - "encoding/json" - "fmt" "regexp" - "strings" "testing" + "time" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/planchecks" + r "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/resources" + + "github.com/hashicorp/terraform-plugin-testing/plancheck" acc "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert/resourceassert" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/config" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/config/model" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + configvariable "github.com/hashicorp/terraform-plugin-testing/config" - "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/testenvs" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/provider/resources" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/tfversion" - "github.com/stretchr/testify/require" ) -func TestAcc_ResourceMonitor(t *testing.T) { - // TODO test more attributes +func TestAcc_ResourceMonitor_Basic(t *testing.T) { id := acc.TestClient().Ids.RandomAccountObjectIdentifier() + configModel := model.ResourceMonitor("test", id.Name()) resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, @@ -29,55 +38,170 @@ func TestAcc_ResourceMonitor(t *testing.T) { CheckDestroy: acc.CheckDestroy(t, resources.ResourceMonitor), Steps: []resource.TestStep{ { - Config: resourceMonitorConfig(id.Name(), acc.TestWarehouseName), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("snowflake_resource_monitor.test", "name", id.Name()), - resource.TestCheckResourceAttr("snowflake_resource_monitor.test", "fully_qualified_name", id.FullyQualifiedName()), - resource.TestCheckResourceAttr("snowflake_resource_monitor.test", "credit_quota", "100"), - resource.TestCheckResourceAttr("snowflake_resource_monitor.test", "set_for_account", "false"), - resource.TestCheckResourceAttr("snowflake_resource_monitor.test", "notify_triggers.0", "40"), - resource.TestCheckResourceAttr("snowflake_resource_monitor.test", "suspend_trigger", "80"), - resource.TestCheckResourceAttr("snowflake_resource_monitor.test", "suspend_immediate_trigger", "90"), + Config: config.FromModel(t, configModel), + Check: assert.AssertThat(t, + resourceassert.ResourceMonitorResource(t, "snowflake_resource_monitor.test"). + HasNameString(id.Name()). + HasFullyQualifiedNameString(id.FullyQualifiedName()). + HasNoCreditQuota(). + HasNotifyUsersLen(0). + HasNoFrequency(). + HasNoStartTimestamp(). + HasNoEndTimestamp(). + HasNoNotifyTriggers(). + HasNoSuspendTrigger(). + HasNoSuspendImmediateTrigger(), + resourceshowoutputassert.ResourceMonitorShowOutput(t, "snowflake_resource_monitor.test"). + HasName(id.Name()). + HasCreditQuota(0). + HasUsedCredits(0). + HasRemainingCredits(0). + HasLevel(""). + HasFrequency(sdk.FrequencyMonthly). + HasStartTimeNotEmpty(). + HasEndTime(""). + HasSuspendAt(0). + HasSuspendImmediateAt(0). + HasCreatedOnNotEmpty(). + HasOwnerNotEmpty(). + HasComment(""), ), }, - // CHANGE PROPERTIES { - Config: resourceMonitorConfig2(id.Name(), 75), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("snowflake_resource_monitor.test", "name", id.Name()), - resource.TestCheckResourceAttr("snowflake_resource_monitor.test", "fully_qualified_name", id.FullyQualifiedName()), - resource.TestCheckResourceAttr("snowflake_resource_monitor.test", "credit_quota", "150"), - resource.TestCheckResourceAttr("snowflake_resource_monitor.test", "set_for_account", "true"), - resource.TestCheckResourceAttr("snowflake_resource_monitor.test", "notify_triggers.0", "50"), - resource.TestCheckResourceAttr("snowflake_resource_monitor.test", "suspend_trigger", "75"), - resource.TestCheckResourceAttr("snowflake_resource_monitor.test", "suspend_immediate_trigger", "95"), + ResourceName: "snowflake_resource_monitor.test", + ImportState: true, + ImportStateCheck: assert.AssertThatImport(t, + resourceassert.ImportedResourceMonitorResource(t, helpers.EncodeResourceIdentifier(id)). + HasNameString(id.Name()). + HasFullyQualifiedNameString(id.FullyQualifiedName()). + HasCreditQuotaString("0"). + HasNotifyUsersLen(0). + HasFrequencyString(string(sdk.FrequencyMonthly)). + HasStartTimestampNotEmpty(). + HasEndTimestampString(""). + HasNoNotifyTriggers(). + HasSuspendTriggerString("0"). + HasSuspendImmediateTriggerString("0"), ), }, - // CHANGE JUST suspend_trigger; proves https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2316 + }, + }) +} + +func TestAcc_ResourceMonitor_Complete(t *testing.T) { + id := acc.TestClient().Ids.RandomAccountObjectIdentifier() + configModel := model.ResourceMonitor("test", id.Name()). + WithNotifyUsersValue(configvariable.SetVariable(configvariable.StringVariable("JAN_CIESLAK"))). + WithCreditQuota(10). + WithFrequency(string(sdk.FrequencyWeekly)). + WithStartTimestamp(time.Now().Add(time.Hour * 24 * 30).Format("2006-01-02 15:01")). + WithEndTimestamp(time.Now().Add(time.Hour * 24 * 60).Format("2006-01-02 15:01")). + WithNotifyTriggersValue(configvariable.SetVariable( + configvariable.IntegerVariable(100), + configvariable.IntegerVariable(110), + )). + WithSuspendTrigger(120). + WithSuspendImmediateTrigger(150) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + PreCheck: func() { acc.TestAccPreCheck(t) }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: acc.CheckDestroy(t, resources.ResourceMonitor), + Steps: []resource.TestStep{ { - Config: resourceMonitorConfig2(id.Name(), 60), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("snowflake_resource_monitor.test", "name", id.Name()), - resource.TestCheckResourceAttr("snowflake_resource_monitor.test", "fully_qualified_name", id.FullyQualifiedName()), - resource.TestCheckResourceAttr("snowflake_resource_monitor.test", "credit_quota", "150"), - resource.TestCheckResourceAttr("snowflake_resource_monitor.test", "set_for_account", "true"), - resource.TestCheckResourceAttr("snowflake_resource_monitor.test", "notify_triggers.0", "50"), - resource.TestCheckResourceAttr("snowflake_resource_monitor.test", "suspend_trigger", "60"), - resource.TestCheckResourceAttr("snowflake_resource_monitor.test", "suspend_immediate_trigger", "95"), + Config: config.FromModel(t, configModel), + Check: assert.AssertThat(t, + resourceassert.ResourceMonitorResource(t, "snowflake_resource_monitor.test"). + HasNameString(id.Name()). + HasFullyQualifiedNameString(id.FullyQualifiedName()). + HasCreditQuotaString("10"). + HasNotifyUsersLen(1). + HasNotifyUser(0, "JAN_CIESLAK"). + HasFrequencyString(string(sdk.FrequencyWeekly)). + HasStartTimestampString(time.Now().Add(time.Hour*24*30).Format("2006-01-02 15:01")). + HasEndTimestampString(time.Now().Add(time.Hour*24*60).Format("2006-01-02 15:01")). + HasNotifyTriggersLen(2). + HasNotifyTrigger(0, 100). + HasNotifyTrigger(1, 110). + HasSuspendTriggerString("120"). + HasSuspendImmediateTriggerString("150"), + resourceshowoutputassert.ResourceMonitorShowOutput(t, "snowflake_resource_monitor.test"). + HasName(id.Name()). + HasCreditQuota(10). + HasUsedCredits(0). + HasRemainingCredits(10). + HasLevel(""). + HasFrequency(sdk.FrequencyWeekly). + HasStartTimeNotEmpty(). + HasEndTimeNotEmpty(). + HasSuspendAt(120). + HasSuspendImmediateAt(150). + HasCreatedOnNotEmpty(). + HasOwnerNotEmpty(). + HasComment(""), ), }, - // IMPORT { - ResourceName: "snowflake_resource_monitor.test", - ImportState: true, - ImportStateVerify: true, + ResourceName: "snowflake_resource_monitor.test", + ImportState: true, + Config: config.FromModel(t, configModel), + ImportStateCheck: assert.AssertThatImport(t, + resourceassert.ImportedResourceMonitorResource(t, helpers.EncodeResourceIdentifier(id)). + HasNameString(id.Name()). + HasFullyQualifiedNameString(id.FullyQualifiedName()). + HasCreditQuotaString("10"). + HasNotifyUsersLen(1). + HasNotifyUser(0, "JAN_CIESLAK"). + HasFrequencyString(string(sdk.FrequencyWeekly)). + HasStartTimestampNotEmpty(). + HasEndTimestampNotEmpty(). + HasNotifyTriggersLen(2). + HasNotifyTrigger(0, 100). + HasNotifyTrigger(1, 110). + HasSuspendTriggerString("120"). + HasSuspendImmediateTriggerString("150"), + ), }, }, }) } -func TestAcc_ResourceMonitorChangeStartEndTimestamp(t *testing.T) { - name := acc.TestClient().Ids.Alpha() +func TestAcc_ResourceMonitor_Updates(t *testing.T) { + id := acc.TestClient().Ids.RandomAccountObjectIdentifier() + + configModelNothingSet := model.ResourceMonitor("test", id.Name()) + + configModelEverythingSet := model.ResourceMonitor("test", id.Name()). + WithNotifyUsersValue(configvariable.SetVariable(configvariable.StringVariable("JAN_CIESLAK"))). + WithCreditQuota(10). + WithFrequency(string(sdk.FrequencyWeekly)). + WithStartTimestamp(time.Now().Add(time.Hour * 24 * 30).Format("2006-01-02 15:01")). + WithEndTimestamp(time.Now().Add(time.Hour * 24 * 60).Format("2006-01-02 15:01")). + WithNotifyTriggersValue(configvariable.SetVariable( + configvariable.IntegerVariable(100), + configvariable.IntegerVariable(110), + )). + WithSuspendTrigger(120). + WithSuspendImmediateTrigger(150) + + configModelUpdated := model.ResourceMonitor("test", id.Name()). + WithNotifyUsersValue(configvariable.SetVariable(configvariable.StringVariable("JAN_CIESLAK"), configvariable.StringVariable("ARTUR_SAWICKI"))). + WithCreditQuota(20). + WithFrequency(string(sdk.FrequencyMonthly)). + WithStartTimestamp(time.Now().Add(time.Hour * 24 * 40).Format("2006-01-02 15:01")). + WithEndTimestamp(time.Now().Add(time.Hour * 24 * 70).Format("2006-01-02 15:01")). + WithNotifyTriggersValue(configvariable.SetVariable( + configvariable.IntegerVariable(110), + configvariable.IntegerVariable(120), + )). + WithSuspendTrigger(130). + WithSuspendImmediateTrigger(160) + + configModelEverythingUnset := model.ResourceMonitor("test", id.Name()). + WithSuspendTrigger(130) // cannot completely remove all triggers (Snowflake limitation; tested below) resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, @@ -88,91 +212,240 @@ func TestAcc_ResourceMonitorChangeStartEndTimestamp(t *testing.T) { CheckDestroy: acc.CheckDestroy(t, resources.ResourceMonitor), Steps: []resource.TestStep{ { - Config: resourceMonitorConfigInitialTimestamp(name), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("snowflake_resource_monitor.test", "name", name), - resource.TestCheckResourceAttr("snowflake_resource_monitor.test", "frequency", "WEEKLY"), - resource.TestCheckResourceAttr("snowflake_resource_monitor.test", "start_timestamp", "2050-01-01 12:00"), - resource.TestCheckResourceAttr("snowflake_resource_monitor.test", "end_timestamp", "2055-01-01 12:00"), + Config: config.FromModel(t, configModelNothingSet), + Check: assert.AssertThat(t, + resourceassert.ResourceMonitorResource(t, "snowflake_resource_monitor.test"). + HasNameString(id.Name()). + HasFullyQualifiedNameString(id.FullyQualifiedName()). + HasNoCreditQuota(). + HasNotifyUsersLen(0). + HasNoFrequency(). + HasNoStartTimestamp(). + HasNoEndTimestamp(). + HasNotifyTriggersLen(0). + HasNoSuspendTrigger(). + HasNoSuspendImmediateTrigger(), + resourceshowoutputassert.ResourceMonitorShowOutput(t, "snowflake_resource_monitor.test"). + HasName(id.Name()). + HasCreditQuota(0). + HasUsedCredits(0). + HasRemainingCredits(0). + HasLevel(""). + HasFrequency(sdk.FrequencyMonthly). + HasStartTimeNotEmpty(). + HasEndTime(""). + HasSuspendAt(0). + HasSuspendImmediateAt(0). + HasCreatedOnNotEmpty(). + HasOwnerNotEmpty(). + HasComment(""), ), }, + // Set { - Config: resourceMonitorConfigUpdatedTimestamp(name), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("snowflake_resource_monitor.test", "name", name), - resource.TestCheckResourceAttr("snowflake_resource_monitor.test", "frequency", "WEEKLY"), - resource.TestCheckResourceAttr("snowflake_resource_monitor.test", "start_timestamp", "2055-01-01 12:00"), - resource.TestCheckResourceAttr("snowflake_resource_monitor.test", "end_timestamp", "2056-01-01 12:00"), + Config: config.FromModel(t, configModelEverythingSet), + Check: assert.AssertThat(t, + resourceassert.ResourceMonitorResource(t, "snowflake_resource_monitor.test"). + HasNameString(id.Name()). + HasFullyQualifiedNameString(id.FullyQualifiedName()). + HasCreditQuotaString("10"). + HasNotifyUsersLen(1). + HasNotifyUser(0, "JAN_CIESLAK"). + HasFrequencyString(string(sdk.FrequencyWeekly)). + HasStartTimestampString(time.Now().Add(time.Hour*24*30).Format("2006-01-02 15:01")). + HasEndTimestampString(time.Now().Add(time.Hour*24*60).Format("2006-01-02 15:01")). + HasNotifyTriggersLen(2). + HasNotifyTrigger(0, 100). + HasNotifyTrigger(1, 110). + HasSuspendTriggerString("120"). + HasSuspendImmediateTriggerString("150"), + resourceshowoutputassert.ResourceMonitorShowOutput(t, "snowflake_resource_monitor.test"). + HasName(id.Name()). + HasCreditQuota(10). + HasUsedCredits(0). + HasRemainingCredits(10). + HasLevel(""). + HasFrequency(sdk.FrequencyWeekly). + HasStartTimeNotEmpty(). + HasEndTimeNotEmpty(). + HasSuspendAt(120). + HasSuspendImmediateAt(150). + HasCreatedOnNotEmpty(). + HasOwnerNotEmpty(). + HasComment(""), ), }, - // IMPORT + // Update { - ResourceName: "snowflake_resource_monitor.test", - ImportState: true, - ImportStateVerify: true, + Config: config.FromModel(t, configModelUpdated), + Check: assert.AssertThat(t, + resourceassert.ResourceMonitorResource(t, "snowflake_resource_monitor.test"). + HasNameString(id.Name()). + HasFullyQualifiedNameString(id.FullyQualifiedName()). + HasCreditQuotaString("20"). + HasNotifyUsersLen(2). + HasNotifyUser(0, "ARTUR_SAWICKI"). + HasNotifyUser(1, "JAN_CIESLAK"). + HasFrequencyString(string(sdk.FrequencyMonthly)). + HasStartTimestampString(time.Now().Add(time.Hour*24*40).Format("2006-01-02 15:01")). + HasEndTimestampString(time.Now().Add(time.Hour*24*70).Format("2006-01-02 15:01")). + HasNotifyTriggersLen(2). + HasNotifyTrigger(0, 110). + HasNotifyTrigger(1, 120). + HasSuspendTriggerString("130"). + HasSuspendImmediateTriggerString("160"), + resourceshowoutputassert.ResourceMonitorShowOutput(t, "snowflake_resource_monitor.test"). + HasName(id.Name()). + HasCreditQuota(20). + HasUsedCredits(0). + HasRemainingCredits(20). + HasLevel(""). + HasFrequency(sdk.FrequencyMonthly). + HasStartTimeNotEmpty(). + HasEndTimeNotEmpty(). + HasSuspendAt(130). + HasSuspendImmediateAt(160). + HasCreatedOnNotEmpty(). + HasOwnerNotEmpty(). + HasComment(""), + ), + }, + // Unset + { + Config: config.FromModel(t, configModelEverythingUnset), + Check: assert.AssertThat(t, + resourceassert.ResourceMonitorResource(t, "snowflake_resource_monitor.test"). + HasNameString(id.Name()). + HasFullyQualifiedNameString(id.FullyQualifiedName()). + HasCreditQuotaString("0"). + HasNotifyUsersLen(0). + HasFrequencyString(""). + HasStartTimestampString(""). + HasEndTimestampString(""). + HasSuspendTriggerString("130"), + resourceshowoutputassert.ResourceMonitorShowOutput(t, "snowflake_resource_monitor.test"). + HasName(id.Name()). + HasCreditQuota(0). + HasUsedCredits(0). + HasRemainingCredits(0). + HasLevel(""). + HasFrequency(sdk.FrequencyMonthly). + HasStartTimeNotEmpty(). + HasEndTime(""). + HasSuspendAt(130). + HasSuspendImmediateAt(0). + HasCreatedOnNotEmpty(). + HasOwnerNotEmpty(). + HasComment(""), + ), }, }, }) } -func resourceMonitorConfigUpdatedTimestamp(accName string) string { - return fmt.Sprintf(` -resource "snowflake_warehouse" "warehouse" { - name = "test%v" - comment = "foo" - warehouse_size = "XSMALL" -} +func TestAcc_ResourceMonitor_ExternalChanges(t *testing.T) { + id := acc.TestClient().Ids.RandomAccountObjectIdentifier() -resource "snowflake_resource_monitor" "test" { - name = "%v" - frequency = "WEEKLY" - start_timestamp = "2055-01-01 12:00" - end_timestamp = "2056-01-01 12:00" + startTimestamp := time.Now().Add(time.Hour * 24 * 40).Format("2006-01-02 15:01") + endTimestamp := time.Now().Add(time.Hour * 24 * 70).Format("2006-01-02 15:01") + configModelEverythingSet := model.ResourceMonitor("test", id.Name()). + WithNotifyUsersValue(configvariable.SetVariable(configvariable.StringVariable("JAN_CIESLAK"))). + WithCreditQuota(10). + WithFrequency(string(sdk.FrequencyWeekly)). + WithStartTimestamp(startTimestamp). + WithEndTimestamp(endTimestamp). + WithNotifyTriggersValue(configvariable.SetVariable( + configvariable.IntegerVariable(100), + configvariable.IntegerVariable(110), + )). + WithSuspendTrigger(120). + WithSuspendImmediateTrigger(150) -} -`, accName, accName) -} + configModelUpdated := model.ResourceMonitor("test", id.Name()). + WithNotifyUsersValue(configvariable.SetVariable(configvariable.StringVariable("JAN_CIESLAK"), configvariable.StringVariable("ARTUR_SAWICKI"))). + WithCreditQuota(20). + WithFrequency(string(sdk.FrequencyMonthly)). + WithStartTimestamp(startTimestamp). + WithEndTimestamp(endTimestamp). + WithNotifyTriggersValue(configvariable.SetVariable( + configvariable.IntegerVariable(110), + configvariable.IntegerVariable(120), + )). + WithSuspendTrigger(130). + WithSuspendImmediateTrigger(160) -// fix 2 added empy notifiy user -// Config for changed timestamp frequency validation test -func resourceMonitorConfigInitialTimestamp(accName string) string { - return fmt.Sprintf(` -resource "snowflake_warehouse" "warehouse" { - name = "test" - comment = "foo" - warehouse_size = "XSMALL" + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + PreCheck: func() { acc.TestAccPreCheck(t) }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: acc.CheckDestroy(t, resources.ResourceMonitor), + Steps: []resource.TestStep{ + { + Config: config.FromModel(t, configModelEverythingSet), + }, + // Update externally, but match the updated configuration (expected updates to the same values) + { + PreConfig: func() { + acc.TestClient().ResourceMonitor.Alter(t, id, &sdk.AlterResourceMonitorOptions{ + Set: &sdk.ResourceMonitorSet{ + NotifyUsers: &sdk.NotifyUsers{ + Users: []sdk.NotifiedUser{ + {Name: sdk.NewAccountObjectIdentifier("JAN_CIESLAK")}, + {Name: sdk.NewAccountObjectIdentifier("ARTUR_SAWICKI")}, + }, + }, + CreditQuota: sdk.Int(20), + Frequency: sdk.Pointer(sdk.FrequencyMonthly), + StartTimestamp: sdk.String(startTimestamp), + EndTimestamp: sdk.String(endTimestamp), + }, + Triggers: []sdk.TriggerDefinition{ + { + Threshold: 110, + TriggerAction: sdk.TriggerActionNotify, + }, + { + Threshold: 120, + TriggerAction: sdk.TriggerActionNotify, + }, + { + Threshold: 130, + TriggerAction: sdk.TriggerActionSuspend, + }, + { + Threshold: 160, + TriggerAction: sdk.TriggerActionSuspendImmediate, + }, + }, + }) + }, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectEmptyPlan(), + planchecks.PrintPlanDetails(configModelUpdated.ResourceReference(), "credit_quota", "end_timestamp", "frequency", "fully_qualified_name", "name", "notify_triggers", "notify_users", "start_timestamp", "suspend_immediate_trigger", "suspend_trigger", r.ShowOutputAttributeName), + }, + }, + Config: config.FromModel(t, configModelUpdated), + }, + }, + }) } -resource "snowflake_resource_monitor" "test" { - name = "%v" - frequency = "WEEKLY" - start_timestamp = "2050-01-01 12:00" - end_timestamp = "2055-01-01 12:00" +// TestAcc_ResourceMonitor_PartialUpdate covers a situation where alter fails. In the previous versions, the alter would +// fail, but invalid values would be saved in the state anyway. In the new version, the old values in state will be preserved +// because the old values are also stored on the Snowflake side (they weren't altered). +func TestAcc_ResourceMonitor_PartialUpdate(t *testing.T) { + id := acc.TestClient().Ids.RandomAccountObjectIdentifier() -} -`, accName) -} + validTimestamp := time.Now().Add(time.Hour * 24 * 60).Format("2006-01-02 15:01") + configModel := model.ResourceMonitor("test", id.Name()). + WithEndTimestamp(validTimestamp) + + configModelInvalidUpdate := model.ResourceMonitor("test", id.Name()). + WithEndTimestamp(time.Now().Add(time.Hour*24*70).Format("2006-01-02 15:01") + "abc") -func TestAcc_ResourceMonitorUpdateNotifyUsers(t *testing.T) { - userEnv := testenvs.GetOrSkipTest(t, testenvs.ResourceMonitorNotifyUsers) - users := strings.Split(userEnv, ",") - name := acc.TestClient().Ids.Alpha() - config, err := resourceMonitorNotifyUsersConfig(name, users) - if err != nil { - t.Error(err) - } - checks := []resource.TestCheckFunc{ - resource.TestCheckResourceAttr("snowflake_resource_monitor.test", "name", name), - resource.TestCheckResourceAttr("snowflake_resource_monitor.test", "set_for_account", "false"), - } - for _, s := range users { - checks = append(checks, resource.TestCheckTypeSetElemAttr("snowflake_resource_monitor.test", "notify_users.*", s)) - } - empty := []string{} - emptyUsersConfig, err := resourceMonitorNotifyUsersConfig(name, empty) - if err != nil { - t.Error(err) - } resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, PreCheck: func() { acc.TestAccPreCheck(t) }, @@ -182,62 +455,42 @@ func TestAcc_ResourceMonitorUpdateNotifyUsers(t *testing.T) { CheckDestroy: acc.CheckDestroy(t, resources.ResourceMonitor), Steps: []resource.TestStep{ { - Config: emptyUsersConfig, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("snowflake_resource_monitor.test", "name", name), - resource.TestCheckResourceAttr("snowflake_resource_monitor.test", "set_for_account", "false"), - ), + Config: config.FromModel(t, configModel), }, { - Config: config, - Check: resource.ComposeTestCheckFunc(checks...), + Config: config.FromModel(t, configModelInvalidUpdate), + ExpectError: regexp.MustCompile("Invalid date/time format string"), + Check: assert.AssertThat(t, + resourceassert.ResourceMonitorResource(t, "snowflake_resource_monitor.test"). + HasEndTimestampString(validTimestamp), + ), }, + // Without the partials plan check failed. + // The following was printed (indicating the invalid value was saved into the state): + // ComputedIfAnyAttributeChanged: changed key: end_timestamp old: 2024-11-19 10:11abc new: 2024-11-09 10:11 { - ResourceName: "snowflake_resource_monitor.test", - ImportState: true, - ImportStateVerify: true, + Config: config.FromModel(t, configModel), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectEmptyPlan(), + }, + }, + Check: assert.AssertThat(t, + resourceassert.ResourceMonitorResource(t, "snowflake_resource_monitor.test"). + HasEndTimestampString(validTimestamp), + ), }, }, }) } -func resourceMonitorConfig(accName string, warehouse string) string { - return fmt.Sprintf(` -resource "snowflake_resource_monitor" "test" { - name = "%v" - credit_quota = 100 - set_for_account = false - notify_triggers = [40] - suspend_trigger = 80 - suspend_immediate_trigger = 90 - warehouses = ["%s"] -} -`, accName, warehouse) -} - -func resourceMonitorConfig2(accName string, suspendTrigger int) string { - return fmt.Sprintf(` -resource "snowflake_resource_monitor" "test" { - name = "%v" - credit_quota = 150 - set_for_account = true - notify_triggers = [50] - warehouses = [] - suspend_trigger = %d - suspend_immediate_trigger = 95 -} -`, accName, suspendTrigger) -} - // TestAcc_ResourceMonitor_issue2167 proves https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2167 issue. // Second step is purposely error, because tests TestAcc_ResourceMonitorUpdateNotifyUsers and TestAcc_ResourceMonitorNotifyUsers are still skipped. // It can be fixed with them. func TestAcc_ResourceMonitor_issue2167(t *testing.T) { - name := acc.TestClient().Ids.Alpha() - configNoUsers, err := resourceMonitorNotifyUsersConfig(name, []string{}) - require.NoError(t, err) - config, err := resourceMonitorNotifyUsersConfig(name, []string{"non_existing_user"}) - require.NoError(t, err) + id := acc.TestClient().Ids.RandomAccountObjectIdentifier() + configNoUsers := model.ResourceMonitor("test", id.Name()).WithNotifyUsersValue(configvariable.SetVariable()) + configWithNonExistingUser := model.ResourceMonitor("test", id.Name()).WithNotifyUsersValue(configvariable.SetVariable(configvariable.StringVariable("non_existing_user"))) resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, @@ -248,66 +501,281 @@ func TestAcc_ResourceMonitor_issue2167(t *testing.T) { CheckDestroy: acc.CheckDestroy(t, resources.ResourceMonitor), Steps: []resource.TestStep{ { - Config: configNoUsers, + Config: config.FromModel(t, configNoUsers), Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("snowflake_resource_monitor.test", "name", name), + resource.TestCheckResourceAttr("snowflake_resource_monitor.test", "name", id.Name()), ), }, { - Config: config, + Config: config.FromModel(t, configWithNonExistingUser), ExpectError: regexp.MustCompile(`.*090268 \(22023\): User non_existing_user does not exist.*`), }, }, }) } -func TestAcc_ResourceMonitorNotifyUsers(t *testing.T) { - userEnv := testenvs.GetOrSkipTest(t, testenvs.ResourceMonitorNotifyUsers) - users := strings.Split(userEnv, ",") - name := acc.TestClient().Ids.Alpha() - config, err := resourceMonitorNotifyUsersConfig(name, users) - if err != nil { - t.Error(err) - } - checks := []resource.TestCheckFunc{ - resource.TestCheckResourceAttr("snowflake_resource_monitor.test", "name", name), - resource.TestCheckResourceAttr("snowflake_resource_monitor.test", "set_for_account", "false"), - } - for _, s := range users { - checks = append(checks, resource.TestCheckTypeSetElemAttr("snowflake_resource_monitor.test", "notify_users.*", s)) - } +// proves https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1990 is fixed +func TestAcc_ResourceMonitor_Issue1990_RemovingResourceMonitorOutsideOfTerraform(t *testing.T) { + id := acc.TestClient().Ids.RandomAccountObjectIdentifier() + configModel := model.ResourceMonitor("test", id.Name()) + resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, - PreCheck: func() { acc.TestAccPreCheck(t) }, + PreCheck: func() { acc.TestAccPreCheck(t) }, TerraformVersionChecks: []tfversion.TerraformVersionCheck{ tfversion.RequireAbove(tfversion.Version1_5_0), }, CheckDestroy: acc.CheckDestroy(t, resources.ResourceMonitor), Steps: []resource.TestStep{ + // Create resource monitor + { + ExternalProviders: map[string]resource.ExternalProvider{ + "snowflake": { + VersionConstraint: "=0.69.0", + Source: "Snowflake-Labs/snowflake", + }, + }, + Config: config.FromModel(t, configModel), + }, + // Same configuration, but we drop resource monitor externally + { + ExternalProviders: map[string]resource.ExternalProvider{ + "snowflake": { + VersionConstraint: "=0.69.0", + Source: "Snowflake-Labs/snowflake", + }, + }, + PreConfig: func() { + acc.TestClient().ResourceMonitor.DropResourceMonitorFunc(t, id)() + }, + Config: config.FromModel(t, configModel), + ExpectError: regexp.MustCompile("object does not exist or not authorized"), + }, + // Same configuration, but it's the last version where it's still not working { - Config: config, - Check: resource.ComposeTestCheckFunc(checks...), + ExternalProviders: map[string]resource.ExternalProvider{ + "snowflake": { + VersionConstraint: "=0.95.0", + Source: "Snowflake-Labs/snowflake", + }, + }, + Config: config.FromModel(t, configModel), + ExpectError: regexp.MustCompile("object does not exist or not authorized"), }, + // Same configuration, but it's the latest version of the provider (0.96.0 and above) { - ResourceName: "snowflake_resource_monitor.test", - ImportState: true, - ImportStateVerify: true, + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + Config: config.FromModel(t, configModel), }, }, }) } -func resourceMonitorNotifyUsersConfig(accName string, accNotifyUsers []string) (string, error) { - notifyUsers, err := json.Marshal(accNotifyUsers) - if err != nil { - return "", err - } - config := fmt.Sprintf(` -resource "snowflake_resource_monitor" "test" { - name = "%v" - set_for_account = false - notify_users = %v +// proves +// https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1821 +// https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1832 +// https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1624 +// https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1716 +// https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1754 +// are fixed and errors are more meaningful for the user +func TestAcc_ResourceMonitor_Issue_TimestampInfinitePlan(t *testing.T) { + id := acc.TestClient().Ids.RandomAccountObjectIdentifier() + configModel := model.ResourceMonitor("test", id.Name()) + configModelWithDateStartTimestamp := model.ResourceMonitor("test", id.Name()). + WithFrequency(string(sdk.FrequencyWeekly)). + WithStartTimestamp(time.Now().Add(time.Hour * 24 * 30).Format("2006-01-02")). + WithEndTimestamp(time.Now().Add(time.Hour * 24 * 60).Format("2006-01-02")) + configModelWithDateTimeFormat := model.ResourceMonitor("test", id.Name()). + WithFrequency(string(sdk.FrequencyWeekly)). + WithStartTimestamp(time.Now().Add(time.Hour * 24 * 30).Format("2006-01-02 15:04")). + WithEndTimestamp(time.Now().Add(time.Hour * 24 * 60).Format("2006-01-02 15:04")) + + 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{ + // Create resource monitor without the timestamps + { + ExternalProviders: map[string]resource.ExternalProvider{ + "snowflake": { + VersionConstraint: "=0.90.0", + Source: "Snowflake-Labs/snowflake", + }, + }, + Config: config.FromModel(t, configModel), + }, + // Alter resource timestamps to have the following format: 2006-01-02 (produces a plan because of the format difference) + { + ExternalProviders: map[string]resource.ExternalProvider{ + "snowflake": { + VersionConstraint: "=0.90.0", + Source: "Snowflake-Labs/snowflake", + }, + }, + Config: config.FromModel(t, configModelWithDateStartTimestamp), + ExpectNonEmptyPlan: true, + }, + // Alter resource timestamps to have the following format: 2006-01-02 15:04 (won't produce plan because of the internal format mapping to this exact format) + { + ExternalProviders: map[string]resource.ExternalProvider{ + "snowflake": { + VersionConstraint: "=0.90.0", + Source: "Snowflake-Labs/snowflake", + }, + }, + Config: config.FromModel(t, configModelWithDateTimeFormat), + }, + // Destroy the resource + { + ExternalProviders: map[string]resource.ExternalProvider{ + "snowflake": { + VersionConstraint: "=0.90.0", + Source: "Snowflake-Labs/snowflake", + }, + }, + Config: config.FromModel(t, configModelWithDateTimeFormat), + Destroy: true, + }, + // Create resource monitor without the timestamps + { + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + Config: config.FromModel(t, configModel), + }, + // Alter resource timestamps to have the following format: 2006-01-02 (no plan produced) + { + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + Config: config.FromModel(t, configModelWithDateStartTimestamp), + }, + // Alter resource timestamps to have the following format: 2006-01-02 15:04 (no plan produced and the internal mapping is not applied in this version) + { + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + Config: config.FromModel(t, configModelWithDateTimeFormat), + }, + }, + }) } -`, accName, string(notifyUsers)) - return config, nil + +// proves https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1500 is fixed and errors are more meaningful for the user +func TestAcc_ResourceMonitor_Issue1500_CreatingWithOnlyTriggers(t *testing.T) { + id := acc.TestClient().Ids.RandomAccountObjectIdentifier() + configModel := model.ResourceMonitor("test", id.Name()). + WithNotifyTriggersValue(configvariable.SetVariable( + configvariable.IntegerVariable(100), + configvariable.IntegerVariable(110), + )). + WithSuspendTrigger(120). + WithSuspendImmediateTrigger(150) + + 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{ + // Create resource monitor with only triggers (old version) + { + ExternalProviders: map[string]resource.ExternalProvider{ + "snowflake": { + VersionConstraint: "=0.90.0", + Source: "Snowflake-Labs/snowflake", + }, + }, + Config: config.FromModel(t, configModel), + ExpectError: regexp.MustCompile("SQL compilation error"), + }, + // Create resource monitor with only triggers (the latest version) + { + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + Config: config.FromModel(t, configModel), + ExpectError: regexp.MustCompile("due to Snowflake limiltations you cannot create Resource Monitor with only triggers set"), + }, + }, + }) +} + +// proves https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1500 is fixed and errors are more meaningful for the user +func TestAcc_ResourceMonitor_Issue1500_AlteringWithOnlyTriggers(t *testing.T) { + id := acc.TestClient().Ids.RandomAccountObjectIdentifier() + + configModelWithCreditQuota := model.ResourceMonitor("test", id.Name()). + WithCreditQuota(100). + WithNotifyTriggersValue(configvariable.SetVariable( + configvariable.IntegerVariable(100), + configvariable.IntegerVariable(110), + )). + WithSuspendTrigger(120). + WithSuspendImmediateTrigger(150) + + configModelWithUpdatedTriggers := model.ResourceMonitor("test", id.Name()). + WithCreditQuota(100). + WithNotifyTriggersValue(configvariable.SetVariable( + configvariable.IntegerVariable(110), + configvariable.IntegerVariable(120), + )). + WithSuspendTrigger(130). + WithSuspendImmediateTrigger(160) + + 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{ + { + ExternalProviders: map[string]resource.ExternalProvider{ + "snowflake": { + VersionConstraint: "=0.90.0", + Source: "Snowflake-Labs/snowflake", + }, + }, + Config: config.FromModel(t, configModelWithCreditQuota), + }, + // Update only triggers (not allowed in Snowflake) + { + ExternalProviders: map[string]resource.ExternalProvider{ + "snowflake": { + VersionConstraint: "=0.90.0", + Source: "Snowflake-Labs/snowflake", + }, + }, + Config: config.FromModel(t, configModelWithUpdatedTriggers), + // For some reason, not returning error (SQL compilation error should be returned in this case; most likely update was handled incorrectly, or it was handled similarly as in the current version) + }, + // Remove all triggers (not allowed in Snowflake) + { + ExternalProviders: map[string]resource.ExternalProvider{ + "snowflake": { + VersionConstraint: "=0.90.0", + Source: "Snowflake-Labs/snowflake", + }, + }, + Config: config.FromModel(t, configModelWithoutTriggers), + // For some reason, not returning the correct error (SQL compilation error should be returned in this case; most likely update was processed incorrectly) + ExpectError: regexp.MustCompile(`at least one of AlterResourceMonitorOptions fields [Set Triggers] must be set`), + }, + // Upgrade to the latest version + { + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + Config: config.FromModel(t, configModelWithCreditQuota), + }, + // Update only triggers (not allowed in Snowflake) + { + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + Config: config.FromModel(t, configModelWithUpdatedTriggers), + }, + // Update only triggers (not allowed in Snowflake) + { + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + Config: config.FromModel(t, configModelWithoutTriggers), + ExpectError: regexp.MustCompile("Due to Snowflake limitations triggers cannot be completely removed form"), + }, + }, + }) } diff --git a/pkg/resources/warehouse.go b/pkg/resources/warehouse.go index 6232b0bac1..a2e4c43e36 100644 --- a/pkg/resources/warehouse.go +++ b/pkg/resources/warehouse.go @@ -135,7 +135,7 @@ var warehouseSchema = map[string]*schema.Schema{ ShowOutputAttributeName: { Type: schema.TypeList, Computed: true, - Description: "Outputs the result of `SHOW WAREHOUSE` for the given warehouse.", + Description: "Outputs the result of `SHOW WAREHOUSES` for the given warehouse.", Elem: &schema.Resource{ Schema: schemas.ShowWarehouseSchema, }, diff --git a/pkg/schemas/resource_monitor_gen.go b/pkg/schemas/resource_monitor_gen.go index 01977298ec..6176a3cc20 100644 --- a/pkg/schemas/resource_monitor_gen.go +++ b/pkg/schemas/resource_monitor_gen.go @@ -25,6 +25,10 @@ var ShowResourceMonitorSchema = map[string]*schema.Schema{ Type: schema.TypeFloat, Computed: true, }, + "level": { + Type: schema.TypeString, + Computed: true, + }, "frequency": { Type: schema.TypeString, Computed: true, @@ -45,22 +49,18 @@ var ShowResourceMonitorSchema = map[string]*schema.Schema{ Type: schema.TypeInt, Computed: true, }, - "notify_triggers": { - Type: schema.TypeInvalid, + "created_on": { + Type: schema.TypeString, Computed: true, }, - "level": { - Type: schema.TypeInt, + "owner": { + Type: schema.TypeString, Computed: true, }, "comment": { Type: schema.TypeString, Computed: true, }, - "notify_users": { - Type: schema.TypeInvalid, - Computed: true, - }, } var _ = ShowResourceMonitorSchema @@ -71,19 +71,17 @@ func ResourceMonitorToSchema(resourceMonitor *sdk.ResourceMonitor) map[string]an resourceMonitorSchema["credit_quota"] = resourceMonitor.CreditQuota resourceMonitorSchema["used_credits"] = resourceMonitor.UsedCredits resourceMonitorSchema["remaining_credits"] = resourceMonitor.RemainingCredits + if resourceMonitor.Level != nil { + resourceMonitorSchema["level"] = string(*resourceMonitor.Level) + } resourceMonitorSchema["frequency"] = string(resourceMonitor.Frequency) resourceMonitorSchema["start_time"] = resourceMonitor.StartTime resourceMonitorSchema["end_time"] = resourceMonitor.EndTime - if resourceMonitor.SuspendAt != nil { - resourceMonitorSchema["suspend_at"] = resourceMonitor.SuspendAt - } - if resourceMonitor.SuspendImmediateAt != nil { - resourceMonitorSchema["suspend_immediate_at"] = resourceMonitor.SuspendImmediateAt - } - resourceMonitorSchema["notify_triggers"] = resourceMonitor.NotifyTriggers - resourceMonitorSchema["level"] = int(resourceMonitor.Level) + resourceMonitorSchema["suspend_at"] = resourceMonitor.SuspendAt + resourceMonitorSchema["suspend_immediate_at"] = resourceMonitor.SuspendImmediateAt + resourceMonitorSchema["created_on"] = resourceMonitor.CreatedOn.String() + resourceMonitorSchema["owner"] = resourceMonitor.Owner resourceMonitorSchema["comment"] = resourceMonitor.Comment - resourceMonitorSchema["notify_users"] = resourceMonitor.NotifyUsers return resourceMonitorSchema } diff --git a/pkg/sdk/external_volumes_impl_gen.go b/pkg/sdk/external_volumes_impl_gen.go index 8b86d2dbd0..5b7de50826 100644 --- a/pkg/sdk/external_volumes_impl_gen.go +++ b/pkg/sdk/external_volumes_impl_gen.go @@ -86,7 +86,6 @@ func (r *AlterExternalVolumeRequest) toOpts() *AlterExternalVolumeOptions { if r.AddStorageLocation != nil { opts.AddStorageLocation = &ExternalVolumeStorageLocation{} - if r.AddStorageLocation.S3StorageLocationParams != nil { opts.AddStorageLocation.S3StorageLocationParams = &S3StorageLocationParams{ Name: r.AddStorageLocation.S3StorageLocationParams.Name, @@ -95,7 +94,6 @@ func (r *AlterExternalVolumeRequest) toOpts() *AlterExternalVolumeOptions { StorageBaseUrl: r.AddStorageLocation.S3StorageLocationParams.StorageBaseUrl, StorageAwsExternalId: r.AddStorageLocation.S3StorageLocationParams.StorageAwsExternalId, } - if r.AddStorageLocation.S3StorageLocationParams.Encryption != nil { opts.AddStorageLocation.S3StorageLocationParams.Encryption = &ExternalVolumeS3Encryption{ Type: r.AddStorageLocation.S3StorageLocationParams.Encryption.Type, @@ -109,7 +107,6 @@ func (r *AlterExternalVolumeRequest) toOpts() *AlterExternalVolumeOptions { Name: r.AddStorageLocation.GCSStorageLocationParams.Name, StorageBaseUrl: r.AddStorageLocation.GCSStorageLocationParams.StorageBaseUrl, } - if r.AddStorageLocation.GCSStorageLocationParams.Encryption != nil { opts.AddStorageLocation.GCSStorageLocationParams.Encryption = &ExternalVolumeGCSEncryption{ Type: r.AddStorageLocation.GCSStorageLocationParams.Encryption.Type, diff --git a/pkg/sdk/resource_monitor_internal_test.go b/pkg/sdk/resource_monitor_internal_test.go deleted file mode 100644 index 4bc45e8184..0000000000 --- a/pkg/sdk/resource_monitor_internal_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package sdk - -import ( - "database/sql" - "testing" -) - -func TestExtractTriggerInts(t *testing.T) { - // TODO rewrite to use testify/assert - resp := sql.NullString{String: "51%,63%", Valid: true} - out, err := extractTriggerInts(resp) - if err != nil { - t.Error(err) - } - if l := len(out); l != 2 { - t.Errorf("Expected 2 values, got %d", l) - } - - first := 51 - if out[0] != first { - t.Errorf("Expected first value to be 51, got %d", out[0]) - } - - second := 63 - if out[1] != second { - t.Errorf("Expected second value to be 63, got %d", out[1]) - } -} diff --git a/pkg/sdk/resource_monitors.go b/pkg/sdk/resource_monitors.go index 817c4df045..b580bdb628 100644 --- a/pkg/sdk/resource_monitors.go +++ b/pkg/sdk/resource_monitors.go @@ -5,8 +5,12 @@ import ( "database/sql" "errors" "fmt" + "log" "strconv" "strings" + "time" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/collections" ) var ( @@ -35,13 +39,15 @@ type ResourceMonitor struct { CreditQuota float64 UsedCredits float64 RemainingCredits float64 + Level *ResourceMonitorLevel Frequency Frequency StartTime string EndTime string + NotifyAt []int SuspendAt *int SuspendImmediateAt *int - NotifyTriggers []int - Level ResourceMonitorLevel + CreatedOn time.Time + Owner string Comment string NotifyUsers []string } @@ -58,14 +64,17 @@ type resourceMonitorRow struct { NotifyAt sql.NullString `db:"notify_at"` SuspendAt sql.NullString `db:"suspend_at"` SuspendImmediateAt sql.NullString `db:"suspend_immediately_at"` - Owner sql.NullString `db:"owner"` + CreatedOn time.Time `db:"created_on"` + Owner string `db:"owner"` Comment sql.NullString `db:"comment"` NotifyUsers sql.NullString `db:"notify_users"` } func (row *resourceMonitorRow) convert() (*ResourceMonitor, error) { resourceMonitor := &ResourceMonitor{ - Name: row.Name, + Name: row.Name, + CreatedOn: row.CreatedOn, + Owner: row.Owner, } if row.CreditQuota.Valid { creditQuota, err := strconv.ParseFloat(row.CreditQuota.String, 64) @@ -74,6 +83,7 @@ func (row *resourceMonitorRow) convert() (*ResourceMonitor, error) { } resourceMonitor.CreditQuota = creditQuota } + if row.UsedCredits.Valid { usedCredits, err := strconv.ParseFloat(row.UsedCredits.String, 64) if err != nil { @@ -81,6 +91,7 @@ func (row *resourceMonitorRow) convert() (*ResourceMonitor, error) { } resourceMonitor.UsedCredits = usedCredits } + if row.RemainingCredits.Valid { remainingCredits, err := strconv.ParseFloat(row.RemainingCredits.String, 64) if err != nil { @@ -89,28 +100,37 @@ func (row *resourceMonitorRow) convert() (*ResourceMonitor, error) { resourceMonitor.RemainingCredits = remainingCredits } + if row.Level.Valid { + level, err := ToResourceMonitorLevel(row.Level.String) + if err != nil { + log.Printf("[DEBUG] unable to parse resource monitor level: %v", err) + } else { + resourceMonitor.Level = &level + } + } + if row.Frequency.Valid { - frequency, err := FrequencyFromString(row.Frequency.String) + frequency, err := ToResourceMonitorFrequency(row.Frequency.String) if err != nil { return nil, err } resourceMonitor.Frequency = *frequency } + if row.StartTime.Valid { - convertedStartTime, err := ParseTimestampWithOffset(row.StartTime.String, "2006-01-02 15:04") - if err != nil { - return nil, err - } - resourceMonitor.StartTime = convertedStartTime + resourceMonitor.StartTime = row.StartTime.String } if row.EndTime.Valid { - convertedEndTime, err := ParseTimestampWithOffset(row.EndTime.String, "2006-01-02 15:04") - if err != nil { - return nil, err - } - resourceMonitor.EndTime = convertedEndTime + resourceMonitor.EndTime = row.EndTime.String + } + + notifyTriggers, err := extractTriggerInts(row.NotifyAt) + if err != nil { + return nil, err } + resourceMonitor.NotifyAt = notifyTriggers + suspendTriggers, err := extractTriggerInts(row.SuspendAt) if err != nil { return nil, err @@ -118,6 +138,7 @@ func (row *resourceMonitorRow) convert() (*ResourceMonitor, error) { if len(suspendTriggers) > 0 { resourceMonitor.SuspendAt = &suspendTriggers[0] } + suspendImmediateTriggers, err := extractTriggerInts(row.SuspendImmediateAt) if err != nil { return nil, err @@ -125,59 +146,37 @@ func (row *resourceMonitorRow) convert() (*ResourceMonitor, error) { if len(suspendImmediateTriggers) > 0 { resourceMonitor.SuspendImmediateAt = &suspendImmediateTriggers[0] } - notifyTriggers, err := extractTriggerInts(row.NotifyAt) - if err != nil { - return nil, err - } - resourceMonitor.NotifyTriggers = notifyTriggers + if row.Comment.Valid { resourceMonitor.Comment = row.Comment.String } - resourceMonitor.NotifyUsers = extractUsers(row.NotifyUsers) - if row.Level.Valid { - switch row.Level.String { - case "ACCOUNT": - resourceMonitor.Level = ResourceMonitorLevelAccount - case "WAREHOUSE": - resourceMonitor.Level = ResourceMonitorLevelWarehouse - default: - resourceMonitor.Level = ResourceMonitorLevelNull - } - } else { - resourceMonitor.Level = ResourceMonitorLevelNull + if row.NotifyUsers.Valid && row.NotifyUsers.String != "" { + resourceMonitor.NotifyUsers = strings.Split(row.NotifyUsers.String, ", ") } return resourceMonitor, nil } -// extractTriggerInts converts the triggers in the DB (stored as a comma -// separated string with trailing %s) into a slice of ints. +// extractTriggerInts converts the triggers in the DB (stored as a comma separated string with trailing `%` signs) into a slice of ints. func extractTriggerInts(s sql.NullString) ([]int, error) { // Check if this is NULL - if !s.Valid { + if !s.Valid || s.String == "" { return []int{}, nil } ints := strings.Split(s.String, ",") out := make([]int, 0, len(ints)) for _, i := range ints { - myInt, err := strconv.Atoi(i[:len(i)-1]) + numberToParse := strings.TrimRight(i, "%") + myInt, err := strconv.Atoi(numberToParse) if err != nil { - return out, fmt.Errorf("failed to convert %v to integer err = %w", i, err) + return out, fmt.Errorf("failed to convert %v to integer err = %w", numberToParse, err) } out = append(out, myInt) } return out, nil } -func extractUsers(s sql.NullString) []string { - if s.Valid && s.String != "" { - return strings.Split(s.String, ", ") - } else { - return []string{} - } -} - func (v *ResourceMonitor) ID() AccountObjectIdentifier { return NewAccountObjectIdentifier(v.Name) } @@ -191,6 +190,7 @@ type CreateResourceMonitorOptions struct { create bool `ddl:"static" sql:"CREATE"` OrReplace *bool `ddl:"keyword" sql:"OR REPLACE"` resourceMonitor bool `ddl:"static" sql:"RESOURCE MONITOR"` + IfNotExists *bool `ddl:"keyword" sql:"IF NOT EXISTS"` name AccountObjectIdentifier `ddl:"identifier"` With *ResourceMonitorWith `ddl:"keyword" sql:"WITH"` } @@ -208,37 +208,44 @@ func (opts *CreateResourceMonitorOptions) validate() error { if opts == nil { return errors.Join(ErrNilOptions) } + var errs []error + if everyValueSet(opts.OrReplace, opts.IfNotExists) { + errs = append(errs, errOneOf("CreateResourceMonitorOptions", "OrReplace", "IfNotExists")) + } if !ValidObjectIdentifier(opts.name) { - return errors.Join(ErrInvalidObjectIdentifier) + errs = append(errs, errors.Join(ErrInvalidObjectIdentifier)) } - return nil + if valueSet(opts.With) && everyValueNil(opts.With.CreditQuota, opts.With.Frequency, opts.With.StartTimestamp, opts.With.EndTimestamp, opts.With.NotifyUsers) && valueSet(opts.With.Triggers) { + errs = append(errs, fmt.Errorf("due to Snowflake limiltations you cannot create Resource Monitor with only triggers set")) + } + return errors.Join(errs...) } func (v *resourceMonitors) Create(ctx context.Context, id AccountObjectIdentifier, opts *CreateResourceMonitorOptions) error { if opts == nil { opts = &CreateResourceMonitorOptions{} } - opts.name = id - if err := opts.validate(); err != nil { - return err - } - sql, err := structToSQL(opts) - if err != nil { - return err - } - _, err = v.client.exec(ctx, sql) - return err + return validateAndExec(v.client, ctx, opts) } -type ResourceMonitorLevel int +type ResourceMonitorLevel string const ( - ResourceMonitorLevelAccount = iota - ResourceMonitorLevelWarehouse - ResourceMonitorLevelNull + ResourceMonitorLevelAccount ResourceMonitorLevel = "ACCOUNT" + ResourceMonitorLevelWarehouse ResourceMonitorLevel = "WAREHOUSE" ) +func ToResourceMonitorLevel(s string) (ResourceMonitorLevel, error) { + switch level := ResourceMonitorLevel(strings.ToUpper(s)); level { + case ResourceMonitorLevelAccount, + ResourceMonitorLevelWarehouse: + return level, nil + default: + return "", fmt.Errorf("invalid resource monitor level: %s", s) + } +} + type TriggerDefinition struct { Threshold int `ddl:"parameter,no_equals" sql:"ON"` TriggerAction TriggerAction `ddl:"parameter,no_equals" sql:"PERCENT DO"` @@ -252,22 +259,35 @@ const ( TriggerActionNotify TriggerAction = "NOTIFY" ) +func ToResourceMonitorTriggerAction(s string) (*TriggerAction, error) { + switch action := TriggerAction(strings.ToUpper(s)); action { + case TriggerActionSuspend, + TriggerActionSuspendImmediate, + TriggerActionNotify: + return &action, nil + default: + return nil, fmt.Errorf("invalid trigger action type: %s", s) + } +} + type NotifyUsers struct { Users []NotifiedUser `ddl:"list,parentheses,comma"` } type NotifiedUser struct { - Name string `ddl:"keyword,double_quotes"` + Name AccountObjectIdentifier `ddl:"identifier"` } type Frequency string -func FrequencyFromString(s string) (*Frequency, error) { - s = strings.ToUpper(s) - f := Frequency(s) - switch f { - case FrequencyDaily, FrequencyWeekly, FrequencyMonthly, FrequencyYearly, FrequencyNever: - return &f, nil +func ToResourceMonitorFrequency(s string) (*Frequency, error) { + switch frequency := Frequency(strings.ToUpper(s)); frequency { + case FrequencyDaily, + FrequencyWeekly, + FrequencyMonthly, + FrequencyYearly, + FrequencyNever: + return &frequency, nil default: return nil, fmt.Errorf("invalid frequency type: %s", s) } @@ -281,6 +301,14 @@ const ( FrequencyNever Frequency = "NEVER" ) +var AllFrequencyValues = []Frequency{ + FrequencyMonthly, + FrequencyDaily, + FrequencyWeekly, + FrequencyYearly, + FrequencyNever, +} + // AlterResourceMonitorOptions is based on https://docs.snowflake.com/en/sql-reference/sql/alter-resource-monitor. type AlterResourceMonitorOptions struct { alter bool `ddl:"static" sql:"ALTER"` @@ -288,6 +316,7 @@ type AlterResourceMonitorOptions struct { IfExists *bool `ddl:"keyword" sql:"IF EXISTS"` name AccountObjectIdentifier `ddl:"identifier"` Set *ResourceMonitorSet `ddl:"keyword" sql:"SET"` + Unset *ResourceMonitorUnset `ddl:"keyword" sql:"SET"` Triggers []TriggerDefinition `ddl:"keyword,no_comma" sql:"TRIGGERS"` } @@ -299,8 +328,8 @@ func (opts *AlterResourceMonitorOptions) validate() error { if !ValidObjectIdentifier(opts.name) { errs = append(errs, ErrInvalidObjectIdentifier) } - if everyValueNil(opts.Set, opts.Triggers) { - errs = append(errs, errAtLeastOneOf("AlterResourceMonitorOptions", "Set", "Triggers")) + if everyValueNil(opts.Set, opts.Unset, opts.Triggers) { + errs = append(errs, errAtLeastOneOf("AlterResourceMonitorOptions", "Set", "Unset", "Triggers")) } if set := opts.Set; valueSet(set) { if everyValueNil(set.CreditQuota, set.Frequency, set.StartTimestamp, set.EndTimestamp, set.NotifyUsers) { @@ -318,16 +347,7 @@ func (v *resourceMonitors) Alter(ctx context.Context, id AccountObjectIdentifier opts = &AlterResourceMonitorOptions{} } opts.name = id - - if err := opts.validate(); err != nil { - return err - } - sql, err := structToSQL(opts) - if err != nil { - return err - } - _, err = v.client.exec(ctx, sql) - return err + return validateAndExec(v.client, ctx, opts) } type ResourceMonitorSet struct { @@ -339,6 +359,12 @@ type ResourceMonitorSet struct { NotifyUsers *NotifyUsers `ddl:"parameter,equals" sql:"NOTIFY_USERS"` } +type ResourceMonitorUnset struct { + CreditQuota *bool `ddl:"keyword" sql:"CREDIT_QUOTA = null"` + EndTimestamp *bool `ddl:"keyword" sql:"END_TIMESTAMP = null"` + NotifyUsers *bool `ddl:"keyword" sql:"NOTIFY_USERS = ()"` +} + // DropResourceMonitorOptions is based on https://docs.snowflake.com/en/sql-reference/sql/drop-resource-monitor. type DropResourceMonitorOptions struct { drop bool `ddl:"static" sql:"DROP"` @@ -360,15 +386,7 @@ func (opts *DropResourceMonitorOptions) validate() error { func (v *resourceMonitors) Drop(ctx context.Context, id AccountObjectIdentifier, opts *DropResourceMonitorOptions) error { opts = createIfNil(opts) opts.name = id - if err := opts.validate(); err != nil { - return err - } - sql, err := structToSQL(opts) - if err != nil { - return err - } - _, err = v.client.exec(ctx, sql) - return err + return validateAndExec(v.client, ctx, opts) } // ShowResourceMonitorOptions is based on https://docs.snowflake.com/en/sql-reference/sql/show-resource-monitors. @@ -387,27 +405,19 @@ func (opts *ShowResourceMonitorOptions) validate() error { func (v *resourceMonitors) Show(ctx context.Context, opts *ShowResourceMonitorOptions) ([]ResourceMonitor, error) { opts = createIfNil(opts) - if err := opts.validate(); err != nil { - return nil, err - } - sql, err := structToSQL(opts) - if err != nil { - return nil, err - } - var rows []*resourceMonitorRow - err = v.client.query(ctx, &rows, sql) + dbRows, err := validateAndQuery[resourceMonitorRow](v.client, ctx, opts) if err != nil { return nil, err } - resourceMonitors := make([]ResourceMonitor, 0, len(rows)) - for _, row := range rows { + resultList := make([]ResourceMonitor, len(dbRows)) + for i, row := range dbRows { resourceMonitor, err := row.convert() if err != nil { return nil, err } - resourceMonitors = append(resourceMonitors, *resourceMonitor) + resultList[i] = *resourceMonitor } - return resourceMonitors, nil + return resultList, nil } func (v *resourceMonitors) ShowByID(ctx context.Context, id AccountObjectIdentifier) (*ResourceMonitor, error) { @@ -419,10 +429,5 @@ func (v *resourceMonitors) ShowByID(ctx context.Context, id AccountObjectIdentif if err != nil { return nil, err } - for _, resourceMonitor := range resourceMonitors { - if resourceMonitor.Name == id.Name() { - return &resourceMonitor, nil - } - } - return nil, ErrObjectNotExistOrAuthorized + return collections.FindFirst(resourceMonitors, func(r ResourceMonitor) bool { return r.ID().Name() == id.Name() }) } diff --git a/pkg/sdk/resource_monitors_test.go b/pkg/sdk/resource_monitors_test.go index 77c33323f0..05e189a002 100644 --- a/pkg/sdk/resource_monitors_test.go +++ b/pkg/sdk/resource_monitors_test.go @@ -1,8 +1,12 @@ package sdk import ( + "database/sql" + "strconv" "testing" "time" + + "github.com/stretchr/testify/require" ) func TestResourceMonitorCreate(t *testing.T) { @@ -13,12 +17,21 @@ func TestResourceMonitorCreate(t *testing.T) { assertOptsInvalidJoinedErrors(t, opts, ErrInvalidObjectIdentifier) }) + t.Run("validation: OrReplace and IfExists specified", func(t *testing.T) { + opts := &CreateResourceMonitorOptions{ + name: id, + OrReplace: Bool(true), + IfNotExists: Bool(true), + } + assertOptsInvalidJoinedErrors(t, opts, errOneOf("CreateResourceMonitorOptions", "OrReplace", "IfNotExists")) + }) + t.Run("with complete options", func(t *testing.T) { creditQuota := Int(100) frequency := FrequencyMonthly startTimeStamp := "IMMIEDIATELY" endTimeStamp := time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC).String() - notifiedUsers := []NotifiedUser{{Name: "FIRST_USER"}, {Name: "SECOND_USER"}} + notifiedUsers := []NotifiedUser{{Name: NewAccountObjectIdentifier("FIRST_USER")}, {Name: NewAccountObjectIdentifier("SECOND_USER")}} triggers := []TriggerDefinition{ { Threshold: 50, @@ -62,7 +75,7 @@ func TestResourceMonitorAlter(t *testing.T) { opts := &AlterResourceMonitorOptions{ name: id, } - assertOptsInvalidJoinedErrors(t, opts, errAtLeastOneOf("AlterResourceMonitorOptions", "Set", "Triggers")) + assertOptsInvalidJoinedErrors(t, opts, errAtLeastOneOf("AlterResourceMonitorOptions", "Set", "Unset", "Triggers")) }) t.Run("validation: no option for set provided", func(t *testing.T) { @@ -90,8 +103,8 @@ func TestResourceMonitorAlter(t *testing.T) { Set: &ResourceMonitorSet{ NotifyUsers: &NotifyUsers{ Users: []NotifiedUser{ - {Name: "user1"}, - {Name: "user2"}, + {Name: NewAccountObjectIdentifier("user1")}, + {Name: NewAccountObjectIdentifier("user2")}, }, }, }, @@ -113,6 +126,17 @@ func TestResourceMonitorAlter(t *testing.T) { } assertOptsValidAndSQLEquals(t, opts, "ALTER RESOURCE MONITOR %s SET CREDIT_QUOTA = %d FREQUENCY = %s START_TIMESTAMP = '%s'", id.FullyQualifiedName(), *newCreditQuota, newFrequency, newStartTimeStamp) }) + + t.Run("with unset", func(t *testing.T) { + opts := &AlterResourceMonitorOptions{ + name: id, + Unset: &ResourceMonitorUnset{ + CreditQuota: Bool(true), + EndTimestamp: Bool(true), + }, + } + assertOptsValidAndSQLEquals(t, opts, "ALTER RESOURCE MONITOR %s SET CREDIT_QUOTA = null END_TIMESTAMP = null", id.FullyQualifiedName()) + }) } func TestResourceMonitorDrop(t *testing.T) { @@ -156,3 +180,35 @@ func TestResourceMonitorShow(t *testing.T) { assertOptsValidAndSQLEquals(t, opts, "SHOW RESOURCE MONITORS LIKE '%s'", id.Name()) }) } + +func TestExtractTriggerInts(t *testing.T) { + testCases := []struct { + Input sql.NullString + Expected []int + Error string + }{ + {Input: sql.NullString{String: "51%,63%,123%", Valid: true}, Expected: []int{51, 63, 123}}, + {Input: sql.NullString{String: "51%,63%", Valid: true}, Expected: []int{51, 63}}, + {Input: sql.NullString{String: "51%", Valid: true}, Expected: []int{51}}, + {Input: sql.NullString{String: "", Valid: false}, Expected: []int{}}, + {Input: sql.NullString{String: "", Valid: true}, Expected: []int{}}, + {Input: sql.NullString{String: "51,63", Valid: true}, Expected: []int{51, 63}}, + {Input: sql.NullString{String: "1", Valid: true}, Expected: []int{1}}, + {Input: sql.NullString{String: "ab,cd", Valid: true}, Error: "failed to convert ab to integer err = strconv.Atoi"}, + {Input: sql.NullString{String: "12,,34", Valid: true}, Error: "failed to convert to integer err = strconv.Atoi"}, + {Input: sql.NullString{String: ",", Valid: true}, Error: "failed to convert to integer err = strconv.Atoi"}, + {Input: sql.NullString{String: "12.34", Valid: true}, Error: "failed to convert 12.34 to integer err = strconv.Atoi"}, + } + + for _, tc := range testCases { + t.Run("extract trigger ints: "+tc.Input.String+":"+strconv.FormatBool(tc.Input.Valid), func(t *testing.T) { + result, err := extractTriggerInts(tc.Input) + if tc.Error != "" { + require.ErrorContains(t, err, tc.Error) + } else { + require.NoError(t, err) + require.Equal(t, tc.Expected, result) + } + }) + } +} diff --git a/pkg/sdk/testint/resource_monitors_integration_test.go b/pkg/sdk/testint/resource_monitors_integration_test.go index 55eb068834..2f5b609a27 100644 --- a/pkg/sdk/testint/resource_monitors_integration_test.go +++ b/pkg/sdk/testint/resource_monitors_integration_test.go @@ -4,6 +4,11 @@ import ( "testing" "time" + assertions "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert/objectassert" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/collections" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -38,6 +43,18 @@ func TestInt_ResourceMonitorsShow(t *testing.T) { require.NoError(t, err) assert.Equal(t, 0, len(resourceMonitors)) }) + + t.Run("show by id", func(t *testing.T) { + resourceMonitor, err := client.ResourceMonitors.ShowByID(ctx, resourceMonitorTest.ID()) + require.NoError(t, err) + assert.Equal(t, *resourceMonitor, *resourceMonitorTest) + }) + + t.Run("show by id when searching a non-existent resource monitor", func(t *testing.T) { + resourceMonitor, err := client.ResourceMonitors.ShowByID(ctx, NonExistingAccountObjectIdentifier) + require.Error(t, err, collections.ErrObjectNotFound) + assert.Nil(t, resourceMonitor) + }) } func TestInt_ResourceMonitorCreate(t *testing.T) { @@ -47,12 +64,10 @@ func TestInt_ResourceMonitorCreate(t *testing.T) { t.Run("test complete case", func(t *testing.T) { id := testClientHelper().Ids.RandomAccountObjectIdentifier() name := id.Name() - frequency, err := sdk.FrequencyFromString("Monthly") - require.NoError(t, err) + frequency := sdk.FrequencyMonthly startTimeStamp := "IMMEDIATELY" creditQuota := 100 endTimeStamp := time.Now().Add(24 * 10 * time.Hour).Format("2006-01-02 15:04") - triggers := []sdk.TriggerDefinition{ { Threshold: 30, @@ -67,10 +82,11 @@ func TestInt_ResourceMonitorCreate(t *testing.T) { TriggerAction: sdk.TriggerActionNotify, }, } - err = client.ResourceMonitors.Create(ctx, id, &sdk.CreateResourceMonitorOptions{ + + err := client.ResourceMonitors.Create(ctx, id, &sdk.CreateResourceMonitorOptions{ OrReplace: sdk.Bool(true), With: &sdk.ResourceMonitorWith{ - Frequency: frequency, + Frequency: &frequency, CreditQuota: &creditQuota, StartTimestamp: &startTimeStamp, EndTimestamp: &endTimeStamp, @@ -79,34 +95,61 @@ func TestInt_ResourceMonitorCreate(t *testing.T) { Triggers: triggers, }, }) - require.NoError(t, err) - resourceMonitors, err := client.ResourceMonitors.Show(ctx, &sdk.ShowResourceMonitorOptions{ - Like: &sdk.Like{ - Pattern: sdk.String(name), + + t.Cleanup(testClientHelper().ResourceMonitor.DropResourceMonitorFunc(t, id)) + + assertions.AssertThat(t, + objectassert.ResourceMonitor(t, id). + HasName(name). + HasFrequency(frequency). + HasCreditQuota(float64(creditQuota)). + HasNonEmptyStartTime(). + HasNonEmptyEndTime(). + HasNotifyAt([]int{100}). + HasSuspendAt(30). + HasSuspendImmediateAt(50), + ) + }) + + t.Run("validate: only one suspend trigger", func(t *testing.T) { + id := testClientHelper().Ids.RandomAccountObjectIdentifier() + err := client.ResourceMonitors.Create(ctx, id, &sdk.CreateResourceMonitorOptions{ + With: &sdk.ResourceMonitorWith{ + CreditQuota: sdk.Int(100), + Triggers: []sdk.TriggerDefinition{ + { + Threshold: 30, + TriggerAction: sdk.TriggerActionSuspend, + }, + { + Threshold: 50, + TriggerAction: sdk.TriggerActionSuspend, + }, + }, }, }) + require.ErrorContains(t, err, "A resource monitor can have at most one suspend trigger.") + }) - assert.Equal(t, 1, len(resourceMonitors)) - resourceMonitor := resourceMonitors[0] - require.NoError(t, err) - assert.Equal(t, name, resourceMonitor.Name) - assert.Equal(t, *frequency, resourceMonitor.Frequency) - assert.Equal(t, creditQuota, int(resourceMonitor.CreditQuota)) - assert.NotEmpty(t, resourceMonitor.StartTime) - assert.NotEmpty(t, resourceMonitor.EndTime) - assert.Equal(t, creditQuota, int(resourceMonitor.CreditQuota)) - var allThresholds []int - allThresholds = append(allThresholds, *resourceMonitor.SuspendAt) - allThresholds = append(allThresholds, *resourceMonitor.SuspendImmediateAt) - allThresholds = append(allThresholds, resourceMonitor.NotifyTriggers...) - var thresholds []int - for _, trigger := range triggers { - thresholds = append(thresholds, trigger.Threshold) - } - assert.Equal(t, thresholds, allThresholds) - - t.Cleanup(testClientHelper().ResourceMonitor.DropResourceMonitorFunc(t, id)) + t.Run("validate: only one suspend immediate trigger", func(t *testing.T) { + id := testClientHelper().Ids.RandomAccountObjectIdentifier() + err := client.ResourceMonitors.Create(ctx, id, &sdk.CreateResourceMonitorOptions{ + With: &sdk.ResourceMonitorWith{ + CreditQuota: sdk.Int(100), + Triggers: []sdk.TriggerDefinition{ + { + Threshold: 30, + TriggerAction: sdk.TriggerActionSuspendImmediate, + }, + { + Threshold: 50, + TriggerAction: sdk.TriggerActionSuspendImmediate, + }, + }, + }, + }) + require.ErrorContains(t, err, "A resource monitor can have at most one suspend_immediate trigger.") }) t.Run("test no options", func(t *testing.T) { @@ -114,28 +157,21 @@ func TestInt_ResourceMonitorCreate(t *testing.T) { name := id.Name() err := client.ResourceMonitors.Create(ctx, id, nil) - require.NoError(t, err) - resourceMonitors, err := client.ResourceMonitors.Show(ctx, &sdk.ShowResourceMonitorOptions{ - Like: &sdk.Like{ - Pattern: sdk.String(name), - }, - }) - - assert.Equal(t, 1, len(resourceMonitors)) - resourceMonitor := resourceMonitors[0] - require.NoError(t, err) - assert.Equal(t, name, resourceMonitor.Name) - assert.NotEmpty(t, resourceMonitor.StartTime) - assert.Empty(t, resourceMonitor.EndTime) - assert.Empty(t, resourceMonitor.CreditQuota) - assert.Equal(t, sdk.FrequencyMonthly, resourceMonitor.Frequency) - assert.Empty(t, resourceMonitor.NotifyUsers) - assert.Empty(t, resourceMonitor.NotifyTriggers) - assert.Empty(t, resourceMonitor.SuspendAt) - assert.Empty(t, resourceMonitor.SuspendImmediateAt) - t.Cleanup(testClientHelper().ResourceMonitor.DropResourceMonitorFunc(t, id)) + + assertions.AssertThat(t, + objectassert.ResourceMonitor(t, id). + HasName(name). + HasFrequency(sdk.FrequencyMonthly). + HasNonEmptyStartTime(). + HasCreditQuota(0). + HasEndTime(""). + HasNotifyUsers([]string{}). + HasNotifyAt([]int{}). + HasSuspendAt(0). + HasSuspendImmediateAt(0), + ) }) } @@ -148,118 +184,131 @@ func TestInt_ResourceMonitorAlter(t *testing.T) { t.Cleanup(resourceMonitorCleanup) var oldNotifyTriggers []sdk.TriggerDefinition - for _, threshold := range resourceMonitor.NotifyTriggers { + for _, threshold := range resourceMonitor.NotifyAt { oldNotifyTriggers = append(oldNotifyTriggers, sdk.TriggerDefinition{Threshold: threshold, TriggerAction: sdk.TriggerActionNotify}) } - var oldTriggers []sdk.TriggerDefinition - oldTriggers = append(oldTriggers, oldNotifyTriggers...) - oldTriggers = append(oldTriggers, sdk.TriggerDefinition{Threshold: *resourceMonitor.SuspendAt, TriggerAction: sdk.TriggerActionSuspend}) - oldTriggers = append(oldTriggers, sdk.TriggerDefinition{Threshold: *resourceMonitor.SuspendImmediateAt, TriggerAction: sdk.TriggerActionSuspendImmediate}) - newTriggers := oldTriggers + newTriggers := oldNotifyTriggers + newTriggers = append(newTriggers, sdk.TriggerDefinition{Threshold: *resourceMonitor.SuspendAt, TriggerAction: sdk.TriggerActionSuspend}) + newTriggers = append(newTriggers, sdk.TriggerDefinition{Threshold: *resourceMonitor.SuspendImmediateAt, TriggerAction: sdk.TriggerActionSuspendImmediate}) newTriggers = append(newTriggers, sdk.TriggerDefinition{Threshold: 30, TriggerAction: sdk.TriggerActionNotify}) alterOptions := &sdk.AlterResourceMonitorOptions{ Triggers: newTriggers, } err := client.ResourceMonitors.Alter(ctx, resourceMonitor.ID(), alterOptions) require.NoError(t, err) - resourceMonitors, err := client.ResourceMonitors.Show(ctx, &sdk.ShowResourceMonitorOptions{ - Like: &sdk.Like{ - Pattern: sdk.String(resourceMonitor.Name), - }, - }) + + resourceMonitor, err = client.ResourceMonitors.ShowByID(ctx, resourceMonitor.ID()) require.NoError(t, err) - assert.Equal(t, 1, len(resourceMonitors)) - resourceMonitor = &resourceMonitors[0] + var newNotifyTriggers []sdk.TriggerDefinition - for _, threshold := range resourceMonitor.NotifyTriggers { + for _, threshold := range resourceMonitor.NotifyAt { newNotifyTriggers = append(newNotifyTriggers, sdk.TriggerDefinition{Threshold: threshold, TriggerAction: sdk.TriggerActionNotify}) } + var allTriggers []sdk.TriggerDefinition allTriggers = append(allTriggers, newNotifyTriggers...) allTriggers = append(allTriggers, sdk.TriggerDefinition{Threshold: *resourceMonitor.SuspendAt, TriggerAction: sdk.TriggerActionSuspend}) allTriggers = append(allTriggers, sdk.TriggerDefinition{Threshold: *resourceMonitor.SuspendImmediateAt, TriggerAction: sdk.TriggerActionSuspendImmediate}) + assert.ElementsMatch(t, newTriggers, allTriggers) }) - t.Run("when setting credit quota", func(t *testing.T) { + t.Run("when setting and unsetting credit quota", func(t *testing.T) { resourceMonitor, resourceMonitorCleanup := testClientHelper().ResourceMonitor.CreateResourceMonitor(t) t.Cleanup(resourceMonitorCleanup) + creditQuota := 100 - alterOptions := &sdk.AlterResourceMonitorOptions{ + + err := client.ResourceMonitors.Alter(ctx, resourceMonitor.ID(), &sdk.AlterResourceMonitorOptions{ Set: &sdk.ResourceMonitorSet{ CreditQuota: &creditQuota, }, - } - err := client.ResourceMonitors.Alter(ctx, resourceMonitor.ID(), alterOptions) + }) require.NoError(t, err) - resourceMonitors, err := client.ResourceMonitors.Show(ctx, &sdk.ShowResourceMonitorOptions{ - Like: &sdk.Like{ - Pattern: sdk.String(resourceMonitor.Name), + + resourceMonitor, err = client.ResourceMonitors.ShowByID(ctx, resourceMonitor.ID()) + require.NoError(t, err) + assert.Equal(t, creditQuota, int(resourceMonitor.CreditQuota)) + + err = client.ResourceMonitors.Alter(ctx, resourceMonitor.ID(), &sdk.AlterResourceMonitorOptions{ + Unset: &sdk.ResourceMonitorUnset{ + CreditQuota: sdk.Bool(true), }, }) require.NoError(t, err) - assert.Equal(t, 1, len(resourceMonitors)) - resourceMonitor = &resourceMonitors[0] - assert.Equal(t, creditQuota, int(resourceMonitor.CreditQuota)) + + resourceMonitor, err = client.ResourceMonitors.ShowByID(ctx, resourceMonitor.ID()) + require.NoError(t, err) + assert.Equal(t, float64(0), resourceMonitor.CreditQuota) }) t.Run("when changing notify users", func(t *testing.T) { resourceMonitor, resourceMonitorCleanup := testClientHelper().ResourceMonitor.CreateResourceMonitor(t) t.Cleanup(resourceMonitorCleanup) - alterOptions := &sdk.AlterResourceMonitorOptions{ + + err := client.ResourceMonitors.Alter(ctx, resourceMonitor.ID(), &sdk.AlterResourceMonitorOptions{ Set: &sdk.ResourceMonitorSet{ NotifyUsers: &sdk.NotifyUsers{ - Users: []sdk.NotifiedUser{{Name: "ARTUR_SAWICKI"}}, + Users: []sdk.NotifiedUser{{Name: sdk.NewAccountObjectIdentifier("JAN_CIESLAK")}}, }, }, - } - err := client.ResourceMonitors.Alter(ctx, resourceMonitor.ID(), alterOptions) + }) require.NoError(t, err) - resourceMonitors, err := client.ResourceMonitors.Show(ctx, &sdk.ShowResourceMonitorOptions{ - Like: &sdk.Like{ - Pattern: sdk.String(resourceMonitor.Name), + + resourceMonitor, err = client.ResourceMonitors.ShowByID(ctx, resourceMonitor.ID()) + require.NoError(t, err) + assert.Len(t, resourceMonitor.NotifyUsers, 1) + assert.Equal(t, "JAN_CIESLAK", resourceMonitor.NotifyUsers[0]) + + err = client.ResourceMonitors.Alter(ctx, resourceMonitor.ID(), &sdk.AlterResourceMonitorOptions{ + Unset: &sdk.ResourceMonitorUnset{ + NotifyUsers: sdk.Bool(true), }, }) require.NoError(t, err) - assert.Equal(t, 1, len(resourceMonitors)) - resourceMonitor = &resourceMonitors[0] - assert.Len(t, resourceMonitor.NotifyUsers, 1) - assert.Equal(t, "ARTUR_SAWICKI", resourceMonitor.NotifyUsers[0]) + + resourceMonitor, err = client.ResourceMonitors.ShowByID(ctx, resourceMonitor.ID()) + require.NoError(t, err) + assert.Len(t, resourceMonitor.NotifyUsers, 0) }) t.Run("when changing scheduling info", func(t *testing.T) { resourceMonitor, resourceMonitorCleanup := testClientHelper().ResourceMonitor.CreateResourceMonitor(t) t.Cleanup(resourceMonitorCleanup) - frequency, err := sdk.FrequencyFromString("NEVER") - require.NoError(t, err) + + frequency := sdk.FrequencyNever startTimeStamp := "2025-01-01 12:34" endTimeStamp := "2026-01-01 12:34" - alterOptions := &sdk.AlterResourceMonitorOptions{ + err := client.ResourceMonitors.Alter(ctx, resourceMonitor.ID(), &sdk.AlterResourceMonitorOptions{ Set: &sdk.ResourceMonitorSet{ - Frequency: frequency, + Frequency: &frequency, StartTimestamp: &startTimeStamp, EndTimestamp: &endTimeStamp, }, - } - err = client.ResourceMonitors.Alter(ctx, resourceMonitor.ID(), alterOptions) + }) require.NoError(t, err) - resourceMonitors, err := client.ResourceMonitors.Show(ctx, &sdk.ShowResourceMonitorOptions{ - Like: &sdk.Like{ - Pattern: sdk.String(resourceMonitor.Name), + + resourceMonitor, err = client.ResourceMonitors.ShowByID(ctx, resourceMonitor.ID()) + require.NoError(t, err) + + assert.Equal(t, frequency, resourceMonitor.Frequency) + assert.NotEmpty(t, resourceMonitor.StartTime) + assert.NotEmpty(t, resourceMonitor.EndTime) + + err = client.ResourceMonitors.Alter(ctx, resourceMonitor.ID(), &sdk.AlterResourceMonitorOptions{ + Unset: &sdk.ResourceMonitorUnset{ + EndTimestamp: sdk.Bool(true), }, }) require.NoError(t, err) - assert.Equal(t, 1, len(resourceMonitors)) - resourceMonitor = &resourceMonitors[0] - assert.Equal(t, *frequency, resourceMonitor.Frequency) - startTime := resourceMonitor.StartTime - require.NoError(t, err) - endTime := resourceMonitor.EndTime + + resourceMonitor, err = client.ResourceMonitors.ShowByID(ctx, resourceMonitor.ID()) require.NoError(t, err) - assert.Equal(t, startTimeStamp, startTime) - assert.Equal(t, endTimeStamp, endTime) + + assert.NotEmpty(t, resourceMonitor.StartTime) + assert.Empty(t, resourceMonitor.EndTime) }) t.Run("all options together", func(t *testing.T) { @@ -270,30 +319,23 @@ func TestInt_ResourceMonitorAlter(t *testing.T) { newTriggers = append(newTriggers, sdk.TriggerDefinition{Threshold: 30, TriggerAction: sdk.TriggerActionNotify}) creditQuota := 100 - alterOptions := &sdk.AlterResourceMonitorOptions{ + err := client.ResourceMonitors.Alter(ctx, resourceMonitor.ID(), &sdk.AlterResourceMonitorOptions{ Set: &sdk.ResourceMonitorSet{ CreditQuota: &creditQuota, NotifyUsers: &sdk.NotifyUsers{ - Users: []sdk.NotifiedUser{{Name: "ARTUR_SAWICKI"}}, + Users: []sdk.NotifiedUser{{Name: sdk.NewAccountObjectIdentifier("JAN_CIESLAK")}}, }, }, Triggers: newTriggers, - } - err := client.ResourceMonitors.Alter(ctx, resourceMonitor.ID(), alterOptions) - require.NoError(t, err) - resourceMonitors, err := client.ResourceMonitors.Show(ctx, &sdk.ShowResourceMonitorOptions{ - Like: &sdk.Like{ - Pattern: sdk.String(resourceMonitor.Name), - }, }) require.NoError(t, err) - assert.Equal(t, 1, len(resourceMonitors)) - resourceMonitor = &resourceMonitors[0] - assert.Equal(t, creditQuota, int(resourceMonitor.CreditQuota)) - assert.Len(t, resourceMonitor.NotifyUsers, 1) - assert.Equal(t, "ARTUR_SAWICKI", resourceMonitor.NotifyUsers[0]) - assert.Len(t, resourceMonitor.NotifyTriggers, 1) - assert.Equal(t, 30, resourceMonitor.NotifyTriggers[0]) + + assertions.AssertThat(t, + objectassert.ResourceMonitor(t, resourceMonitor.ID()). + HasCreditQuota(float64(creditQuota)). + HasNotifyUsers([]string{"JAN_CIESLAK"}). + HasNotifyAt([]int{30}), + ) }) } @@ -304,16 +346,16 @@ func TestInt_ResourceMonitorDrop(t *testing.T) { t.Run("when resource monitor exists", func(t *testing.T) { resourceMonitor, resourceMonitorCleanup := testClientHelper().ResourceMonitor.CreateResourceMonitor(t) t.Cleanup(resourceMonitorCleanup) - id := resourceMonitor.ID() - err := client.ResourceMonitors.Drop(ctx, id, nil) + + err := client.ResourceMonitors.Drop(ctx, resourceMonitor.ID(), nil) require.NoError(t, err) - _, err = client.ResourceMonitors.ShowByID(ctx, id) - assert.ErrorIs(t, err, sdk.ErrObjectNotExistOrAuthorized) + + _, err = client.ResourceMonitors.ShowByID(ctx, resourceMonitor.ID()) + assert.ErrorIs(t, err, sdk.ErrObjectNotFound) }) t.Run("when resource monitor does not exist", func(t *testing.T) { - id := NonExistingAccountObjectIdentifier - err := client.ResourceMonitors.Drop(ctx, id, nil) + err := client.ResourceMonitors.Drop(ctx, NonExistingAccountObjectIdentifier, nil) assert.ErrorIs(t, err, sdk.ErrObjectNotExistOrAuthorized) }) }