From baa3a3c92673faff0c48aa12bb9e6718bbb696f5 Mon Sep 17 00:00:00 2001 From: Jakub Michalak Date: Wed, 18 Sep 2024 12:58:50 +0200 Subject: [PATCH] feat: Masking policy resource v1 (#3078) - SDK - improve validations - add tests - improve options parsing (also, get rid of a 3rd party library) - add a TODO to reuse parsing from row access policies after merging https://github.com/Snowflake-Labs/terraform-provider-snowflake/pull/3079 - resource - use new id handling - rename/remove some fields to be consistent with other resources - better ACC tests - misc - adjust helper clients - generate config and assertion builders - adjust docs ## Test Plan * [x] acceptance tests * [x] integration tests * [x] unit tests ## References https://docs.snowflake.com/en/sql-reference/sql/create-masking-policy ## TODO - add tests for issues - rework data source --- MIGRATION_GUIDE.md | 77 ++- docs/resources/masking_policy.md | 140 +++-- .../snowflake_masking_policy/import.sh | 3 +- .../snowflake_masking_policy/resource.tf | 70 ++- go.mod | 1 - go.sum | 2 - .../assert/objectassert/gen/sdk_object_def.go | 5 + .../masking_policy_snowflake_gen.go | 131 ++++ .../resourceassert/gen/resource_schema_def.go | 4 + .../masking_policy_resource.ext.go | 18 + .../masking_policy_resource_gen.go | 127 ++++ .../row_access_policy_resource_ext.go | 2 +- .../database_resource_parameters_gen.go | 202 +++++++ .../masking_policy_show_output_ext.go | 10 + .../masking_policy_show_output_gen.go | 87 +++ .../config/model/masking_policy_model_ext.go | 19 + .../config/model/masking_policy_model_gen.go | 161 +++++ .../model/row_access_policy_model_ext.go | 2 +- .../model/row_access_policy_model_gen.go | 4 +- .../helpers/masking_policy_client.go | 42 +- .../row_access_policies_acceptance_test.go | 2 +- pkg/resources/doc_helpers.go | 4 + pkg/resources/masking_policy.go | 416 +++++++------ .../masking_policy_acceptance_test.go | 557 ++++++++++++++++-- .../masking_policy_state_upgraders.go | 32 + pkg/resources/row_access_policy.go | 17 +- .../row_access_policy_acceptance_test.go | 26 +- pkg/resources/table_acceptance_test.go | 22 +- ...king_policy_application_acceptance_test.go | 10 +- ...king_policy_association_acceptance_test.go | 10 +- .../TestAcc_MaskingPolicy/basic/test.tf | 14 + .../TestAcc_MaskingPolicy/basic/variables.tf | 23 + .../TestAcc_MaskingPolicy/complete/test.tf | 16 + .../complete/variables.tf | 31 + .../basic/test.tf | 14 +- pkg/resources/view_acceptance_test.go | 2 - pkg/schemas/masking_policy.go | 58 ++ pkg/schemas/row_access_policy.go | 2 +- pkg/sdk/common_types.go | 26 + pkg/sdk/errors.go | 1 + pkg/sdk/masking_policy.go | 72 ++- pkg/sdk/masking_policy_test.go | 92 ++- pkg/sdk/row_access_policies_gen.go | 6 +- pkg/sdk/row_access_policies_gen_test.go | 10 +- pkg/sdk/row_access_policies_impl_gen.go | 31 +- .../masking_policy_integration_test.go | 43 +- ...ow_access_policies_gen_integration_test.go | 12 +- v1-preparations/ESSENTIAL_GA_OBJECTS.MD | 4 +- 48 files changed, 2204 insertions(+), 456 deletions(-) create mode 100644 pkg/acceptance/bettertestspoc/assert/objectassert/masking_policy_snowflake_gen.go create mode 100644 pkg/acceptance/bettertestspoc/assert/resourceassert/masking_policy_resource.ext.go create mode 100644 pkg/acceptance/bettertestspoc/assert/resourceassert/masking_policy_resource_gen.go create mode 100644 pkg/acceptance/bettertestspoc/assert/resourceparametersassert/database_resource_parameters_gen.go create mode 100644 pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert/masking_policy_show_output_ext.go create mode 100644 pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert/masking_policy_show_output_gen.go create mode 100644 pkg/acceptance/bettertestspoc/config/model/masking_policy_model_ext.go create mode 100644 pkg/acceptance/bettertestspoc/config/model/masking_policy_model_gen.go create mode 100644 pkg/resources/masking_policy_state_upgraders.go create mode 100644 pkg/resources/testdata/TestAcc_MaskingPolicy/basic/test.tf create mode 100644 pkg/resources/testdata/TestAcc_MaskingPolicy/basic/variables.tf create mode 100644 pkg/resources/testdata/TestAcc_MaskingPolicy/complete/test.tf create mode 100644 pkg/resources/testdata/TestAcc_MaskingPolicy/complete/variables.tf create mode 100644 pkg/schemas/masking_policy.go diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index afaa60e27e9..e3d245d88f8 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -6,6 +6,58 @@ across different versions. ## v0.95.0 ➞ v0.96.0 +### snowflake_masking_policy resource changes +New fields: + - `show_output` field that holds the response from SHOW MASKING POLICIES. + - `describe_output` field that holds the response from DESCRIBE MASKING POLICY. + +#### *(breaking change)* Renamed fields in snowflake_masking_policy resource +Renamed fields: + - `masking_expression` to `body` +Please rename these fields in your configuration files. State will be migrated automatically. + +#### *(breaking change)* Removed fields from snowflake_masking_policy resource +Removed fields: +- `or_replace` +- `if_not_exists` +The value of these field will be removed from the state automatically. + +#### *(breaking change)* Adjusted schema of arguments/signature +The field `signature` is renamed to `arguments` to be consistent with other resources. +Now, arguments are stored without nested `column` field. Please adjust that in your configs, like in the example below. State is migrated automatically. + +The old configuration looks like this: +``` + signature { + column { + name = "val" + type = "VARCHAR" + } + } +``` + +The new configuration looks like this: +``` + argument { + name = "val" + type = "VARCHAR" + } +``` + +#### *(breaking change)* Identifiers related changes +During [identifiers rework](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/ROADMAP.md#identifiers-rework) we decided to +migrate resource ids from pipe-separated to regular Snowflake identifiers (e.g. `|` -> `"".""`). Importing resources also needs to be adjusted (see [example](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/resources/row_access_policy#import)). + +Also, we added diff suppress function that prevents Terraform from showing differences, when only quoting is different. + +No change is required, the state will be migrated automatically. + +#### *(behavior change)* Boolean type changes +To easily handle three-value logic (true, false, unknown) in provider's configs, type of `exempt_other_policies` was changed from boolean to string. + +For more details about default values, please refer to the [changes before v1](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/v1-preparations/CHANGES_BEFORE_V1.md#default-values) document. + + ### *(breaking change)* resource_monitor resource Removed fields: - `set_for_account` (will be settable on account resource, right now, the preferred way is to set it through unsafe_execute resource) @@ -55,12 +107,33 @@ New fields: #### *(breaking change)* Renamed fields in snowflake_row_access_policy resource Renamed fields: - `row_access_expression` to `body` - - `signature` to `arguments` Please rename these fields in your configuration files. State will be migrated automatically. -#### *(breaking change)* Adjusted behavior of arguments/signature +#### *(breaking change)* Adjusted schema of arguments/signature +The field `signature` is renamed to `arguments` to be consistent with other resources. Now, arguments are stored as a list, instead of a map. Please adjust that in your configs. State is migrated automatically. Also, this means that order of the items matters and may be adjusted. + +The old configuration looks like this: +``` + signature = { + A = "VARCHAR", + B = "VARCHAR" + } +``` + +The new configuration looks like this: +``` + argument { + name = "A" + type = "VARCHAR" + } + argument { + name = "B" + type = "VARCHAR" + } +``` + Argument names are now case sensitive. All policies created previously in the provider have upper case argument names. If you used lower case before, please adjust your configs. Values in the state will be migrated to uppercase automatically. #### *(breaking change)* Adjusted behavior on changing name diff --git a/docs/resources/masking_policy.md b/docs/resources/masking_policy.md index da7b771f76c..0c119173f07 100644 --- a/docs/resources/masking_policy.md +++ b/docs/resources/masking_policy.md @@ -2,39 +2,77 @@ page_title: "snowflake_masking_policy Resource - terraform-provider-snowflake" subcategory: "" description: |- - + Resource used to manage masking policies. For more information, check masking policies documentation https://docs.snowflake.com/en/sql-reference/sql/create-masking-policy. --- # snowflake_masking_policy (Resource) - +Resource used to manage masking policies. For more information, check [masking policies documentation](https://docs.snowflake.com/en/sql-reference/sql/create-masking-policy). ## Example Usage ```terraform +# basic resource resource "snowflake_masking_policy" "test" { name = "EXAMPLE_MASKING_POLICY" database = "EXAMPLE_DB" schema = "EXAMPLE_SCHEMA" - signature { - column { - name = "val" - type = "VARCHAR" - } + argument { + name = "ARG1" + type = "VARCHAR" } - masking_expression = <<-EOF - case - when current_role() in ('ROLE_A') then - val - when is_role_in_session( 'ROLE_B' ) then - 'ABC123' - else - '******' - end - EOF - + argument { + name = "ARG2" + type = "NUMBER" + } + argument { + name = "ARG3" + type = "TIMESTAMP_NTZ" + } + body = <<-EOF + case + when current_role() in ('ROLE_A') then + ARG1 + when is_role_in_session( 'ROLE_B' ) then + 'ABC123' + else + '******' + end +EOF return_data_type = "VARCHAR" } + +# resource with all fields set +resource "snowflake_masking_policy" "test" { + name = "EXAMPLE_MASKING_POLICY" + database = "EXAMPLE_DB" + schema = "EXAMPLE_SCHEMA" + argument { + name = "ARG1" + type = "VARCHAR" + } + argument { + name = "ARG2" + type = "NUMBER" + } + argument { + name = "ARG3" + type = "TIMESTAMP_NTZ" + } + body = <<-EOF + case + when current_role() in ('ROLE_A') then + ARG1 + when is_role_in_session( 'ROLE_B' ) then + 'ABC123' + else + '******' + end +EOF + return_data_type = "VARCHAR" + exempt_other_policies = "true" + comment = "example masking policy" +} ``` -> **Note** Instead of using fully_qualified_name, you can reference objects managed outside Terraform by constructing a correct ID, consult [identifiers guide](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/guides/identifiers#new-computed-fully-qualified-name-field-in-resources). @@ -45,45 +83,73 @@ resource "snowflake_masking_policy" "test" { ### Required -- `database` (String) The database in which to create the masking policy. -- `masking_expression` (String) Specifies the SQL expression that transforms the data. -- `name` (String) Specifies the identifier for the masking policy; must be unique for the database and schema in which the masking policy is created. -- `return_data_type` (String) Specifies the data type to return. -- `schema` (String) The schema in which to create the masking policy. -- `signature` (Block List, Min: 1, Max: 1) The signature for the masking policy; specifies the input columns and data types to evaluate at query runtime. (see [below for nested schema](#nestedblock--signature)) +- `argument` (Block List, Min: 1) List of the arguments for the masking policy. The first column and its data type always indicate the column data type values to mask or tokenize in the subsequent policy conditions. Note that you can not specify a virtual column as the first column argument in a conditional masking policy. (see [below for nested schema](#nestedblock--argument)) +- `body` (String) Specifies the SQL expression that transforms the data. To mitigate permadiff on this field, the provider replaces blank characters with a space. This can lead to false positives in cases where a change in case or run of whitespace is semantically significant. +- `database` (String) The database in which to create the masking policy. 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: `|`, `.`, `(`, `)`, `"` +- `name` (String) Specifies the identifier for the masking policy; must be unique for the database and schema in which the masking policy is created. 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: `|`, `.`, `(`, `)`, `"` +- `return_data_type` (String) The return data type must match the input data type of the first column that is specified as an input column. For more information about data types, check [Snowflake docs](https://docs.snowflake.com/en/sql-reference/intro-summary-data-types). +- `schema` (String) The schema in which to create the masking policy. 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 - `comment` (String) Specifies a comment for the masking policy. -- `exempt_other_policies` (Boolean) Specifies whether the row access policy or conditional masking policy can reference a column that is already protected by a masking policy. -- `if_not_exists` (Boolean) Prevent overwriting a previous masking policy with the same name. -- `or_replace` (Boolean) Whether to override a previous masking policy with the same name. +- `exempt_other_policies` (String) Specifies whether the row access policy or conditional masking policy can reference a column that is already protected by a masking policy. Due to Snowflake limitations, when value is chenged, the resource is recreated. Available options are: "true" or "false". When the value is not set in the configuration the provider will put "default" there which means to use the Snowflake default for this value. ### Read-Only +- `describe_output` (List of Object) Outputs the result of `DESCRIBE MASKING POLICY` for the given masking policy. (see [below for nested schema](#nestedatt--describe_output)) - `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 MASKING POLICY` for the given masking policy. (see [below for nested schema](#nestedatt--show_output)) - -### Nested Schema for `signature` + +### Nested Schema for `argument` Required: -- `column` (Block List, Min: 1) (see [below for nested schema](#nestedblock--signature--column)) +- `name` (String) The argument name +- `type` (String) The argument type. VECTOR data types are not yet supported. For more information about data types, check [Snowflake docs](https://docs.snowflake.com/en/sql-reference/intro-summary-data-types). - -### Nested Schema for `signature.column` -Required: + +### Nested Schema for `describe_output` + +Read-Only: + +- `body` (String) +- `name` (String) +- `return_type` (String) +- `signature` (List of Object) (see [below for nested schema](#nestedobjatt--describe_output--signature)) + + +### Nested Schema for `describe_output.signature` + +Read-Only: + +- `name` (String) +- `type` (String) + + + + +### Nested Schema for `show_output` + +Read-Only: -- `name` (String) Specifies the column name to mask. -- `type` (String) Specifies the column type to mask. +- `comment` (String) +- `created_on` (String) +- `database_name` (String) +- `exempt_other_policies` (Boolean) +- `kind` (String) +- `name` (String) +- `owner` (String) +- `owner_role_type` (String) +- `schema_name` (String) ## Import Import is supported using the following syntax: ```shell -# format is database name | schema name | policy name -terraform import snowflake_masking_policy.example 'dbName|schemaName|policyName' +terraform import snowflake_row_access_policy.example '""."".""' ``` diff --git a/examples/resources/snowflake_masking_policy/import.sh b/examples/resources/snowflake_masking_policy/import.sh index 14b6d925512..41f62960212 100644 --- a/examples/resources/snowflake_masking_policy/import.sh +++ b/examples/resources/snowflake_masking_policy/import.sh @@ -1,2 +1 @@ -# format is database name | schema name | policy name -terraform import snowflake_masking_policy.example 'dbName|schemaName|policyName' +terraform import snowflake_row_access_policy.example '""."".""' diff --git a/examples/resources/snowflake_masking_policy/resource.tf b/examples/resources/snowflake_masking_policy/resource.tf index d74736c19f5..e95e19b5ba7 100644 --- a/examples/resources/snowflake_masking_policy/resource.tf +++ b/examples/resources/snowflake_masking_policy/resource.tf @@ -1,23 +1,61 @@ +# basic resource resource "snowflake_masking_policy" "test" { name = "EXAMPLE_MASKING_POLICY" database = "EXAMPLE_DB" schema = "EXAMPLE_SCHEMA" - signature { - column { - name = "val" - type = "VARCHAR" - } + argument { + name = "ARG1" + type = "VARCHAR" } - masking_expression = <<-EOF - case - when current_role() in ('ROLE_A') then - val - when is_role_in_session( 'ROLE_B' ) then - 'ABC123' - else - '******' - end - EOF - + argument { + name = "ARG2" + type = "NUMBER" + } + argument { + name = "ARG3" + type = "TIMESTAMP_NTZ" + } + body = <<-EOF + case + when current_role() in ('ROLE_A') then + ARG1 + when is_role_in_session( 'ROLE_B' ) then + 'ABC123' + else + '******' + end +EOF return_data_type = "VARCHAR" } + +# resource with all fields set +resource "snowflake_masking_policy" "test" { + name = "EXAMPLE_MASKING_POLICY" + database = "EXAMPLE_DB" + schema = "EXAMPLE_SCHEMA" + argument { + name = "ARG1" + type = "VARCHAR" + } + argument { + name = "ARG2" + type = "NUMBER" + } + argument { + name = "ARG3" + type = "TIMESTAMP_NTZ" + } + body = <<-EOF + case + when current_role() in ('ROLE_A') then + ARG1 + when is_role_in_session( 'ROLE_B' ) then + 'ABC123' + else + '******' + end +EOF + return_data_type = "VARCHAR" + exempt_other_policies = "true" + comment = "example masking policy" +} diff --git a/go.mod b/go.mod index 1ff88672d33..7f42d3fbd8b 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,6 @@ require ( github.com/DATA-DOG/go-sqlmock v1.5.2 github.com/avast/retry-go v3.0.0+incompatible github.com/brianvoe/gofakeit/v6 v6.28.0 - github.com/buger/jsonparser v1.1.1 github.com/gookit/color v1.5.4 github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 github.com/hashicorp/go-uuid v1.0.3 diff --git a/go.sum b/go.sum index 78062756a52..f73906bfd73 100644 --- a/go.sum +++ b/go.sum @@ -75,8 +75,6 @@ github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs= github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= -github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= -github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= 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 c0da90f4cdc..5a79ec4827d 100644 --- a/pkg/acceptance/bettertestspoc/assert/objectassert/gen/sdk_object_def.go +++ b/pkg/acceptance/bettertestspoc/assert/objectassert/gen/sdk_object_def.go @@ -47,6 +47,11 @@ var allStructs = []SdkObjectDef{ ObjectType: sdk.ObjectTypeResourceMonitor, ObjectStruct: sdk.ResourceMonitor{}, }, + { + IdType: "sdk.SchemaObjectIdentifier", + ObjectType: sdk.ObjectTypeMaskingPolicy, + ObjectStruct: sdk.MaskingPolicy{}, + }, { IdType: "sdk.SchemaObjectIdentifier", ObjectType: sdk.ObjectTypeAuthenticationPolicy, diff --git a/pkg/acceptance/bettertestspoc/assert/objectassert/masking_policy_snowflake_gen.go b/pkg/acceptance/bettertestspoc/assert/objectassert/masking_policy_snowflake_gen.go new file mode 100644 index 00000000000..918eb16cd5a --- /dev/null +++ b/pkg/acceptance/bettertestspoc/assert/objectassert/masking_policy_snowflake_gen.go @@ -0,0 +1,131 @@ +// Code generated by assertions generator; DO NOT EDIT. + +package objectassert + +import ( + "fmt" + "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 MaskingPolicyAssert struct { + *assert.SnowflakeObjectAssert[sdk.MaskingPolicy, sdk.SchemaObjectIdentifier] +} + +func MaskingPolicy(t *testing.T, id sdk.SchemaObjectIdentifier) *MaskingPolicyAssert { + t.Helper() + return &MaskingPolicyAssert{ + assert.NewSnowflakeObjectAssertWithProvider(sdk.ObjectTypeMaskingPolicy, id, acc.TestClient().MaskingPolicy.Show), + } +} + +func MaskingPolicyFromObject(t *testing.T, maskingPolicy *sdk.MaskingPolicy) *MaskingPolicyAssert { + t.Helper() + return &MaskingPolicyAssert{ + assert.NewSnowflakeObjectAssertWithObject(sdk.ObjectTypeMaskingPolicy, maskingPolicy.ID(), maskingPolicy), + } +} + +func (m *MaskingPolicyAssert) HasCreatedOn(expected time.Time) *MaskingPolicyAssert { + m.AddAssertion(func(t *testing.T, o *sdk.MaskingPolicy) error { + t.Helper() + if o.CreatedOn != expected { + return fmt.Errorf("expected created on: %v; got: %v", expected, o.CreatedOn) + } + return nil + }) + return m +} + +func (m *MaskingPolicyAssert) HasName(expected string) *MaskingPolicyAssert { + m.AddAssertion(func(t *testing.T, o *sdk.MaskingPolicy) error { + t.Helper() + if o.Name != expected { + return fmt.Errorf("expected name: %v; got: %v", expected, o.Name) + } + return nil + }) + return m +} + +func (m *MaskingPolicyAssert) HasDatabaseName(expected string) *MaskingPolicyAssert { + m.AddAssertion(func(t *testing.T, o *sdk.MaskingPolicy) error { + t.Helper() + if o.DatabaseName != expected { + return fmt.Errorf("expected database name: %v; got: %v", expected, o.DatabaseName) + } + return nil + }) + return m +} + +func (m *MaskingPolicyAssert) HasSchemaName(expected string) *MaskingPolicyAssert { + m.AddAssertion(func(t *testing.T, o *sdk.MaskingPolicy) error { + t.Helper() + if o.SchemaName != expected { + return fmt.Errorf("expected schema name: %v; got: %v", expected, o.SchemaName) + } + return nil + }) + return m +} + +func (m *MaskingPolicyAssert) HasKind(expected string) *MaskingPolicyAssert { + m.AddAssertion(func(t *testing.T, o *sdk.MaskingPolicy) error { + t.Helper() + if o.Kind != expected { + return fmt.Errorf("expected kind: %v; got: %v", expected, o.Kind) + } + return nil + }) + return m +} + +func (m *MaskingPolicyAssert) HasOwner(expected string) *MaskingPolicyAssert { + m.AddAssertion(func(t *testing.T, o *sdk.MaskingPolicy) error { + t.Helper() + if o.Owner != expected { + return fmt.Errorf("expected owner: %v; got: %v", expected, o.Owner) + } + return nil + }) + return m +} + +func (m *MaskingPolicyAssert) HasComment(expected string) *MaskingPolicyAssert { + m.AddAssertion(func(t *testing.T, o *sdk.MaskingPolicy) error { + t.Helper() + if o.Comment != expected { + return fmt.Errorf("expected comment: %v; got: %v", expected, o.Comment) + } + return nil + }) + return m +} + +func (m *MaskingPolicyAssert) HasExemptOtherPolicies(expected bool) *MaskingPolicyAssert { + m.AddAssertion(func(t *testing.T, o *sdk.MaskingPolicy) error { + t.Helper() + if o.ExemptOtherPolicies != expected { + return fmt.Errorf("expected exempt other policies: %v; got: %v", expected, o.ExemptOtherPolicies) + } + return nil + }) + return m +} + +func (m *MaskingPolicyAssert) HasOwnerRoleType(expected string) *MaskingPolicyAssert { + m.AddAssertion(func(t *testing.T, o *sdk.MaskingPolicy) error { + t.Helper() + if o.OwnerRoleType != expected { + return fmt.Errorf("expected owner role type: %v; got: %v", expected, o.OwnerRoleType) + } + return nil + }) + return m +} 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 e805b4c30a3..d3670bbba6f 100644 --- a/pkg/acceptance/bettertestspoc/assert/resourceassert/gen/resource_schema_def.go +++ b/pkg/acceptance/bettertestspoc/assert/resourceassert/gen/resource_schema_def.go @@ -45,4 +45,8 @@ var allResourceSchemaDefs = []ResourceSchemaDef{ name: "RowAccessPolicy", schema: resources.RowAccessPolicy().Schema, }, + { + name: "MaskingPolicy", + schema: resources.MaskingPolicy().Schema, + }, } diff --git a/pkg/acceptance/bettertestspoc/assert/resourceassert/masking_policy_resource.ext.go b/pkg/acceptance/bettertestspoc/assert/resourceassert/masking_policy_resource.ext.go new file mode 100644 index 00000000000..94a59143c43 --- /dev/null +++ b/pkg/acceptance/bettertestspoc/assert/resourceassert/masking_policy_resource.ext.go @@ -0,0 +1,18 @@ +package resourceassert + +import ( + "fmt" + "strconv" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" +) + +func (r *MaskingPolicyResourceAssert) HasArguments(args []sdk.TableColumnSignature) *MaskingPolicyResourceAssert { + r.AddAssertion(assert.ValueSet("argument.#", strconv.FormatInt(int64(len(args)), 10))) + for i, v := range args { + r.AddAssertion(assert.ValueSet(fmt.Sprintf("argument.%d.name", i), v.Name)) + r.AddAssertion(assert.ValueSet(fmt.Sprintf("argument.%d.type", i), string(v.Type))) + } + return r +} diff --git a/pkg/acceptance/bettertestspoc/assert/resourceassert/masking_policy_resource_gen.go b/pkg/acceptance/bettertestspoc/assert/resourceassert/masking_policy_resource_gen.go new file mode 100644 index 00000000000..4c49ed3cbd7 --- /dev/null +++ b/pkg/acceptance/bettertestspoc/assert/resourceassert/masking_policy_resource_gen.go @@ -0,0 +1,127 @@ +// Code generated by assertions generator; DO NOT EDIT. + +package resourceassert + +import ( + "testing" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert" +) + +type MaskingPolicyResourceAssert struct { + *assert.ResourceAssert +} + +func MaskingPolicyResource(t *testing.T, name string) *MaskingPolicyResourceAssert { + t.Helper() + + return &MaskingPolicyResourceAssert{ + ResourceAssert: assert.NewResourceAssert(name, "resource"), + } +} + +func ImportedMaskingPolicyResource(t *testing.T, id string) *MaskingPolicyResourceAssert { + t.Helper() + + return &MaskingPolicyResourceAssert{ + ResourceAssert: assert.NewImportedResourceAssert(id, "imported resource"), + } +} + +/////////////////////////////////// +// Attribute value string checks // +/////////////////////////////////// + +func (m *MaskingPolicyResourceAssert) HasArgumentString(expected string) *MaskingPolicyResourceAssert { + m.AddAssertion(assert.ValueSet("argument", expected)) + return m +} + +func (m *MaskingPolicyResourceAssert) HasBodyString(expected string) *MaskingPolicyResourceAssert { + m.AddAssertion(assert.ValueSet("body", expected)) + return m +} + +func (m *MaskingPolicyResourceAssert) HasCommentString(expected string) *MaskingPolicyResourceAssert { + m.AddAssertion(assert.ValueSet("comment", expected)) + return m +} + +func (m *MaskingPolicyResourceAssert) HasDatabaseString(expected string) *MaskingPolicyResourceAssert { + m.AddAssertion(assert.ValueSet("database", expected)) + return m +} + +func (m *MaskingPolicyResourceAssert) HasExemptOtherPoliciesString(expected string) *MaskingPolicyResourceAssert { + m.AddAssertion(assert.ValueSet("exempt_other_policies", expected)) + return m +} + +func (m *MaskingPolicyResourceAssert) HasFullyQualifiedNameString(expected string) *MaskingPolicyResourceAssert { + m.AddAssertion(assert.ValueSet("fully_qualified_name", expected)) + return m +} + +func (m *MaskingPolicyResourceAssert) HasNameString(expected string) *MaskingPolicyResourceAssert { + m.AddAssertion(assert.ValueSet("name", expected)) + return m +} + +func (m *MaskingPolicyResourceAssert) HasReturnDataTypeString(expected string) *MaskingPolicyResourceAssert { + m.AddAssertion(assert.ValueSet("return_data_type", expected)) + return m +} + +func (m *MaskingPolicyResourceAssert) HasSchemaString(expected string) *MaskingPolicyResourceAssert { + m.AddAssertion(assert.ValueSet("schema", expected)) + return m +} + +//////////////////////////// +// Attribute empty checks // +//////////////////////////// + +func (m *MaskingPolicyResourceAssert) HasNoArgument() *MaskingPolicyResourceAssert { + m.AddAssertion(assert.ValueNotSet("argument")) + return m +} + +func (m *MaskingPolicyResourceAssert) HasNoBody() *MaskingPolicyResourceAssert { + m.AddAssertion(assert.ValueNotSet("body")) + return m +} + +func (m *MaskingPolicyResourceAssert) HasNoComment() *MaskingPolicyResourceAssert { + m.AddAssertion(assert.ValueNotSet("comment")) + return m +} + +func (m *MaskingPolicyResourceAssert) HasNoDatabase() *MaskingPolicyResourceAssert { + m.AddAssertion(assert.ValueNotSet("database")) + return m +} + +func (m *MaskingPolicyResourceAssert) HasNoExemptOtherPolicies() *MaskingPolicyResourceAssert { + m.AddAssertion(assert.ValueNotSet("exempt_other_policies")) + return m +} + +func (m *MaskingPolicyResourceAssert) HasNoFullyQualifiedName() *MaskingPolicyResourceAssert { + m.AddAssertion(assert.ValueNotSet("fully_qualified_name")) + return m +} + +func (m *MaskingPolicyResourceAssert) HasNoName() *MaskingPolicyResourceAssert { + m.AddAssertion(assert.ValueNotSet("name")) + return m +} + +func (m *MaskingPolicyResourceAssert) HasNoReturnDataType() *MaskingPolicyResourceAssert { + m.AddAssertion(assert.ValueNotSet("return_data_type")) + return m +} + +func (m *MaskingPolicyResourceAssert) HasNoSchema() *MaskingPolicyResourceAssert { + m.AddAssertion(assert.ValueNotSet("schema")) + return m +} diff --git a/pkg/acceptance/bettertestspoc/assert/resourceassert/row_access_policy_resource_ext.go b/pkg/acceptance/bettertestspoc/assert/resourceassert/row_access_policy_resource_ext.go index ec54107493f..caa272a94d0 100644 --- a/pkg/acceptance/bettertestspoc/assert/resourceassert/row_access_policy_resource_ext.go +++ b/pkg/acceptance/bettertestspoc/assert/resourceassert/row_access_policy_resource_ext.go @@ -8,7 +8,7 @@ import ( "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" ) -func (r *RowAccessPolicyResourceAssert) HasArguments(args []sdk.RowAccessPolicyArgument) *RowAccessPolicyResourceAssert { +func (r *RowAccessPolicyResourceAssert) HasArguments(args []sdk.TableColumnSignature) *RowAccessPolicyResourceAssert { r.AddAssertion(assert.ValueSet("argument.#", strconv.FormatInt(int64(len(args)), 10))) for i, v := range args { r.AddAssertion(assert.ValueSet(fmt.Sprintf("argument.%d.name", i), v.Name)) diff --git a/pkg/acceptance/bettertestspoc/assert/resourceparametersassert/database_resource_parameters_gen.go b/pkg/acceptance/bettertestspoc/assert/resourceparametersassert/database_resource_parameters_gen.go new file mode 100644 index 00000000000..97cf0ea7e87 --- /dev/null +++ b/pkg/acceptance/bettertestspoc/assert/resourceparametersassert/database_resource_parameters_gen.go @@ -0,0 +1,202 @@ +// Code generated by assertions generator; DO NOT EDIT. + +package resourceparametersassert + +import ( + "testing" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" +) + +type DatabaseResourceParametersAssert struct { + *assert.ResourceAssert +} + +func DatabaseResourceParameters(t *testing.T, name string) *DatabaseResourceParametersAssert { + t.Helper() + + d := DatabaseResourceParametersAssert{ + ResourceAssert: assert.NewResourceAssert(name, "parameters"), + } + d.AddAssertion(assert.ValueSet("parameters.#", "1")) + return &d +} + +func ImportedDatabaseResourceParameters(t *testing.T, id string) *DatabaseResourceParametersAssert { + t.Helper() + + d := DatabaseResourceParametersAssert{ + ResourceAssert: assert.NewImportedResourceAssert(id, "imported parameters"), + } + d.AddAssertion(assert.ValueSet("parameters.#", "1")) + return &d +} + +//////////////////////////// +// Parameter value checks // +//////////////////////////// + +func (d *DatabaseResourceParametersAssert) HasDataRetentionTimeInDays(expected int) *DatabaseResourceParametersAssert { + d.AddAssertion(assert.ResourceParameterIntValueSet(sdk.DatabaseParameterDataRetentionTimeInDays, expected)) + return d +} + +func (d *DatabaseResourceParametersAssert) HasMaxDataExtensionTimeInDays(expected int) *DatabaseResourceParametersAssert { + d.AddAssertion(assert.ResourceParameterIntValueSet(sdk.DatabaseParameterMaxDataExtensionTimeInDays, expected)) + return d +} + +func (d *DatabaseResourceParametersAssert) HasExternalVolume(expected string) *DatabaseResourceParametersAssert { + d.AddAssertion(assert.ResourceParameterValueSet(sdk.DatabaseParameterExternalVolume, expected)) + return d +} + +func (d *DatabaseResourceParametersAssert) HasCatalog(expected string) *DatabaseResourceParametersAssert { + d.AddAssertion(assert.ResourceParameterValueSet(sdk.DatabaseParameterCatalog, expected)) + return d +} + +func (d *DatabaseResourceParametersAssert) HasReplaceInvalidCharacters(expected bool) *DatabaseResourceParametersAssert { + d.AddAssertion(assert.ResourceParameterBoolValueSet(sdk.DatabaseParameterReplaceInvalidCharacters, expected)) + return d +} + +func (d *DatabaseResourceParametersAssert) HasDefaultDdlCollation(expected string) *DatabaseResourceParametersAssert { + d.AddAssertion(assert.ResourceParameterValueSet(sdk.DatabaseParameterDefaultDdlCollation, expected)) + return d +} + +func (d *DatabaseResourceParametersAssert) HasStorageSerializationPolicy(expected sdk.StorageSerializationPolicy) *DatabaseResourceParametersAssert { + d.AddAssertion(assert.ResourceParameterStringUnderlyingValueSet(sdk.DatabaseParameterStorageSerializationPolicy, expected)) + return d +} + +func (d *DatabaseResourceParametersAssert) HasLogLevel(expected sdk.LogLevel) *DatabaseResourceParametersAssert { + d.AddAssertion(assert.ResourceParameterStringUnderlyingValueSet(sdk.DatabaseParameterLogLevel, expected)) + return d +} + +func (d *DatabaseResourceParametersAssert) HasTraceLevel(expected sdk.TraceLevel) *DatabaseResourceParametersAssert { + d.AddAssertion(assert.ResourceParameterStringUnderlyingValueSet(sdk.DatabaseParameterTraceLevel, expected)) + return d +} + +func (d *DatabaseResourceParametersAssert) HasSuspendTaskAfterNumFailures(expected int) *DatabaseResourceParametersAssert { + d.AddAssertion(assert.ResourceParameterIntValueSet(sdk.DatabaseParameterSuspendTaskAfterNumFailures, expected)) + return d +} + +func (d *DatabaseResourceParametersAssert) HasTaskAutoRetryAttempts(expected int) *DatabaseResourceParametersAssert { + d.AddAssertion(assert.ResourceParameterIntValueSet(sdk.DatabaseParameterTaskAutoRetryAttempts, expected)) + return d +} + +func (d *DatabaseResourceParametersAssert) HasUserTaskManagedInitialWarehouseSize(expected sdk.WarehouseSize) *DatabaseResourceParametersAssert { + d.AddAssertion(assert.ResourceParameterStringUnderlyingValueSet(sdk.DatabaseParameterUserTaskManagedInitialWarehouseSize, expected)) + return d +} + +func (d *DatabaseResourceParametersAssert) HasUserTaskTimeoutMs(expected int) *DatabaseResourceParametersAssert { + d.AddAssertion(assert.ResourceParameterIntValueSet(sdk.DatabaseParameterUserTaskTimeoutMs, expected)) + return d +} + +func (d *DatabaseResourceParametersAssert) HasUserTaskMinimumTriggerIntervalInSeconds(expected int) *DatabaseResourceParametersAssert { + d.AddAssertion(assert.ResourceParameterIntValueSet(sdk.DatabaseParameterUserTaskMinimumTriggerIntervalInSeconds, expected)) + return d +} + +func (d *DatabaseResourceParametersAssert) HasQuotedIdentifiersIgnoreCase(expected bool) *DatabaseResourceParametersAssert { + d.AddAssertion(assert.ResourceParameterBoolValueSet(sdk.DatabaseParameterQuotedIdentifiersIgnoreCase, expected)) + return d +} + +func (d *DatabaseResourceParametersAssert) HasEnableConsoleOutput(expected bool) *DatabaseResourceParametersAssert { + d.AddAssertion(assert.ResourceParameterBoolValueSet(sdk.DatabaseParameterEnableConsoleOutput, expected)) + return d +} + +//////////////////////////// +// Parameter level checks // +//////////////////////////// + +func (d *DatabaseResourceParametersAssert) HasDataRetentionTimeInDaysLevel(expected sdk.ParameterType) *DatabaseResourceParametersAssert { + d.AddAssertion(assert.ResourceParameterLevelSet(sdk.DatabaseParameterDataRetentionTimeInDays, expected)) + return d +} + +func (d *DatabaseResourceParametersAssert) HasMaxDataExtensionTimeInDaysLevel(expected sdk.ParameterType) *DatabaseResourceParametersAssert { + d.AddAssertion(assert.ResourceParameterLevelSet(sdk.DatabaseParameterMaxDataExtensionTimeInDays, expected)) + return d +} + +func (d *DatabaseResourceParametersAssert) HasExternalVolumeLevel(expected sdk.ParameterType) *DatabaseResourceParametersAssert { + d.AddAssertion(assert.ResourceParameterLevelSet(sdk.DatabaseParameterExternalVolume, expected)) + return d +} + +func (d *DatabaseResourceParametersAssert) HasCatalogLevel(expected sdk.ParameterType) *DatabaseResourceParametersAssert { + d.AddAssertion(assert.ResourceParameterLevelSet(sdk.DatabaseParameterCatalog, expected)) + return d +} + +func (d *DatabaseResourceParametersAssert) HasReplaceInvalidCharactersLevel(expected sdk.ParameterType) *DatabaseResourceParametersAssert { + d.AddAssertion(assert.ResourceParameterLevelSet(sdk.DatabaseParameterReplaceInvalidCharacters, expected)) + return d +} + +func (d *DatabaseResourceParametersAssert) HasDefaultDdlCollationLevel(expected sdk.ParameterType) *DatabaseResourceParametersAssert { + d.AddAssertion(assert.ResourceParameterLevelSet(sdk.DatabaseParameterDefaultDdlCollation, expected)) + return d +} + +func (d *DatabaseResourceParametersAssert) HasStorageSerializationPolicyLevel(expected sdk.ParameterType) *DatabaseResourceParametersAssert { + d.AddAssertion(assert.ResourceParameterLevelSet(sdk.DatabaseParameterStorageSerializationPolicy, expected)) + return d +} + +func (d *DatabaseResourceParametersAssert) HasLogLevelLevel(expected sdk.ParameterType) *DatabaseResourceParametersAssert { + d.AddAssertion(assert.ResourceParameterLevelSet(sdk.DatabaseParameterLogLevel, expected)) + return d +} + +func (d *DatabaseResourceParametersAssert) HasTraceLevelLevel(expected sdk.ParameterType) *DatabaseResourceParametersAssert { + d.AddAssertion(assert.ResourceParameterLevelSet(sdk.DatabaseParameterTraceLevel, expected)) + return d +} + +func (d *DatabaseResourceParametersAssert) HasSuspendTaskAfterNumFailuresLevel(expected sdk.ParameterType) *DatabaseResourceParametersAssert { + d.AddAssertion(assert.ResourceParameterLevelSet(sdk.DatabaseParameterSuspendTaskAfterNumFailures, expected)) + return d +} + +func (d *DatabaseResourceParametersAssert) HasTaskAutoRetryAttemptsLevel(expected sdk.ParameterType) *DatabaseResourceParametersAssert { + d.AddAssertion(assert.ResourceParameterLevelSet(sdk.DatabaseParameterTaskAutoRetryAttempts, expected)) + return d +} + +func (d *DatabaseResourceParametersAssert) HasUserTaskManagedInitialWarehouseSizeLevel(expected sdk.ParameterType) *DatabaseResourceParametersAssert { + d.AddAssertion(assert.ResourceParameterLevelSet(sdk.DatabaseParameterUserTaskManagedInitialWarehouseSize, expected)) + return d +} + +func (d *DatabaseResourceParametersAssert) HasUserTaskTimeoutMsLevel(expected sdk.ParameterType) *DatabaseResourceParametersAssert { + d.AddAssertion(assert.ResourceParameterLevelSet(sdk.DatabaseParameterUserTaskTimeoutMs, expected)) + return d +} + +func (d *DatabaseResourceParametersAssert) HasUserTaskMinimumTriggerIntervalInSecondsLevel(expected sdk.ParameterType) *DatabaseResourceParametersAssert { + d.AddAssertion(assert.ResourceParameterLevelSet(sdk.DatabaseParameterUserTaskMinimumTriggerIntervalInSeconds, expected)) + return d +} + +func (d *DatabaseResourceParametersAssert) HasQuotedIdentifiersIgnoreCaseLevel(expected sdk.ParameterType) *DatabaseResourceParametersAssert { + d.AddAssertion(assert.ResourceParameterLevelSet(sdk.DatabaseParameterQuotedIdentifiersIgnoreCase, expected)) + return d +} + +func (d *DatabaseResourceParametersAssert) HasEnableConsoleOutputLevel(expected sdk.ParameterType) *DatabaseResourceParametersAssert { + d.AddAssertion(assert.ResourceParameterLevelSet(sdk.DatabaseParameterEnableConsoleOutput, expected)) + return d +} diff --git a/pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert/masking_policy_show_output_ext.go b/pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert/masking_policy_show_output_ext.go new file mode 100644 index 00000000000..c31da8c25cc --- /dev/null +++ b/pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert/masking_policy_show_output_ext.go @@ -0,0 +1,10 @@ +package resourceshowoutputassert + +import ( + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert" +) + +func (p *MaskingPolicyShowOutputAssert) HasCreatedOnNotEmpty() *MaskingPolicyShowOutputAssert { + p.AddAssertion(assert.ResourceShowOutputValuePresent("created_on")) + return p +} diff --git a/pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert/masking_policy_show_output_gen.go b/pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert/masking_policy_show_output_gen.go new file mode 100644 index 00000000000..a9cbf888879 --- /dev/null +++ b/pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert/masking_policy_show_output_gen.go @@ -0,0 +1,87 @@ +// 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 MaskingPolicyShowOutputAssert struct { + *assert.ResourceAssert +} + +func MaskingPolicyShowOutput(t *testing.T, name string) *MaskingPolicyShowOutputAssert { + t.Helper() + + m := MaskingPolicyShowOutputAssert{ + ResourceAssert: assert.NewResourceAssert(name, "show_output"), + } + m.AddAssertion(assert.ValueSet("show_output.#", "1")) + return &m +} + +func ImportedMaskingPolicyShowOutput(t *testing.T, id string) *MaskingPolicyShowOutputAssert { + t.Helper() + + m := MaskingPolicyShowOutputAssert{ + ResourceAssert: assert.NewImportedResourceAssert(id, "show_output"), + } + m.AddAssertion(assert.ValueSet("show_output.#", "1")) + return &m +} + +//////////////////////////// +// Attribute value checks // +//////////////////////////// + +func (m *MaskingPolicyShowOutputAssert) HasCreatedOn(expected time.Time) *MaskingPolicyShowOutputAssert { + m.AddAssertion(assert.ResourceShowOutputValueSet("created_on", expected.String())) + return m +} + +func (m *MaskingPolicyShowOutputAssert) HasName(expected string) *MaskingPolicyShowOutputAssert { + m.AddAssertion(assert.ResourceShowOutputValueSet("name", expected)) + return m +} + +func (m *MaskingPolicyShowOutputAssert) HasDatabaseName(expected string) *MaskingPolicyShowOutputAssert { + m.AddAssertion(assert.ResourceShowOutputValueSet("database_name", expected)) + return m +} + +func (m *MaskingPolicyShowOutputAssert) HasSchemaName(expected string) *MaskingPolicyShowOutputAssert { + m.AddAssertion(assert.ResourceShowOutputValueSet("schema_name", expected)) + return m +} + +func (m *MaskingPolicyShowOutputAssert) HasKind(expected string) *MaskingPolicyShowOutputAssert { + m.AddAssertion(assert.ResourceShowOutputValueSet("kind", expected)) + return m +} + +func (m *MaskingPolicyShowOutputAssert) HasOwner(expected string) *MaskingPolicyShowOutputAssert { + m.AddAssertion(assert.ResourceShowOutputValueSet("owner", expected)) + return m +} + +func (m *MaskingPolicyShowOutputAssert) HasComment(expected string) *MaskingPolicyShowOutputAssert { + m.AddAssertion(assert.ResourceShowOutputValueSet("comment", expected)) + return m +} + +func (m *MaskingPolicyShowOutputAssert) HasExemptOtherPolicies(expected bool) *MaskingPolicyShowOutputAssert { + m.AddAssertion(assert.ResourceShowOutputBoolValueSet("exempt_other_policies", expected)) + return m +} + +func (m *MaskingPolicyShowOutputAssert) HasOwnerRoleType(expected string) *MaskingPolicyShowOutputAssert { + m.AddAssertion(assert.ResourceShowOutputValueSet("owner_role_type", expected)) + return m +} diff --git a/pkg/acceptance/bettertestspoc/config/model/masking_policy_model_ext.go b/pkg/acceptance/bettertestspoc/config/model/masking_policy_model_ext.go new file mode 100644 index 00000000000..76079cbde39 --- /dev/null +++ b/pkg/acceptance/bettertestspoc/config/model/masking_policy_model_ext.go @@ -0,0 +1,19 @@ +package model + +import ( + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + "github.com/hashicorp/terraform-plugin-testing/config" + tfconfig "github.com/hashicorp/terraform-plugin-testing/config" +) + +func (p *MaskingPolicyModel) WithArgument(argument []sdk.TableColumnSignature) *MaskingPolicyModel { + maps := make([]config.Variable, len(argument)) + for i, v := range argument { + maps[i] = config.MapVariable(map[string]config.Variable{ + "name": config.StringVariable(v.Name), + "type": config.StringVariable(string(v.Type)), + }) + } + p.Argument = tfconfig.SetVariable(maps...) + return p +} diff --git a/pkg/acceptance/bettertestspoc/config/model/masking_policy_model_gen.go b/pkg/acceptance/bettertestspoc/config/model/masking_policy_model_gen.go new file mode 100644 index 00000000000..2f9f7213fa6 --- /dev/null +++ b/pkg/acceptance/bettertestspoc/config/model/masking_policy_model_gen.go @@ -0,0 +1,161 @@ +// 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" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" +) + +type MaskingPolicyModel struct { + Argument tfconfig.Variable `json:"argument,omitempty"` + Body tfconfig.Variable `json:"body,omitempty"` + Comment tfconfig.Variable `json:"comment,omitempty"` + Database tfconfig.Variable `json:"database,omitempty"` + ExemptOtherPolicies tfconfig.Variable `json:"exempt_other_policies,omitempty"` + FullyQualifiedName tfconfig.Variable `json:"fully_qualified_name,omitempty"` + Name tfconfig.Variable `json:"name,omitempty"` + ReturnDataType tfconfig.Variable `json:"return_data_type,omitempty"` + Schema tfconfig.Variable `json:"schema,omitempty"` + + *config.ResourceModelMeta +} + +///////////////////////////////////////////////// +// Basic builders (resource name and required) // +///////////////////////////////////////////////// + +func MaskingPolicy( + resourceName string, + argument []sdk.TableColumnSignature, + body string, + database string, + name string, + returnDataType string, + schema string, +) *MaskingPolicyModel { + m := &MaskingPolicyModel{ResourceModelMeta: config.Meta(resourceName, resources.MaskingPolicy)} + m.WithArgument(argument) + m.WithBody(body) + m.WithDatabase(database) + m.WithName(name) + m.WithReturnDataType(returnDataType) + m.WithSchema(schema) + return m +} + +func MaskingPolicyWithDefaultMeta( + argument []sdk.TableColumnSignature, + body string, + database string, + name string, + returnDataType string, + schema string, +) *MaskingPolicyModel { + m := &MaskingPolicyModel{ResourceModelMeta: config.DefaultMeta(resources.MaskingPolicy)} + m.WithArgument(argument) + m.WithBody(body) + m.WithDatabase(database) + m.WithName(name) + m.WithReturnDataType(returnDataType) + m.WithSchema(schema) + return m +} + +///////////////////////////////// +// below all the proper values // +///////////////////////////////// + +// argument attribute type is not yet supported, so WithArgument can't be generated + +func (m *MaskingPolicyModel) WithBody(body string) *MaskingPolicyModel { + m.Body = tfconfig.StringVariable(body) + return m +} + +func (m *MaskingPolicyModel) WithComment(comment string) *MaskingPolicyModel { + m.Comment = tfconfig.StringVariable(comment) + return m +} + +func (m *MaskingPolicyModel) WithDatabase(database string) *MaskingPolicyModel { + m.Database = tfconfig.StringVariable(database) + return m +} + +func (m *MaskingPolicyModel) WithExemptOtherPolicies(exemptOtherPolicies string) *MaskingPolicyModel { + m.ExemptOtherPolicies = tfconfig.StringVariable(exemptOtherPolicies) + return m +} + +func (m *MaskingPolicyModel) WithFullyQualifiedName(fullyQualifiedName string) *MaskingPolicyModel { + m.FullyQualifiedName = tfconfig.StringVariable(fullyQualifiedName) + return m +} + +func (m *MaskingPolicyModel) WithName(name string) *MaskingPolicyModel { + m.Name = tfconfig.StringVariable(name) + return m +} + +func (m *MaskingPolicyModel) WithReturnDataType(returnDataType string) *MaskingPolicyModel { + m.ReturnDataType = tfconfig.StringVariable(returnDataType) + return m +} + +func (m *MaskingPolicyModel) WithSchema(schema string) *MaskingPolicyModel { + m.Schema = tfconfig.StringVariable(schema) + return m +} + +////////////////////////////////////////// +// below it's possible to set any value // +////////////////////////////////////////// + +func (m *MaskingPolicyModel) WithArgumentValue(value tfconfig.Variable) *MaskingPolicyModel { + m.Argument = value + return m +} + +func (m *MaskingPolicyModel) WithBodyValue(value tfconfig.Variable) *MaskingPolicyModel { + m.Body = value + return m +} + +func (m *MaskingPolicyModel) WithCommentValue(value tfconfig.Variable) *MaskingPolicyModel { + m.Comment = value + return m +} + +func (m *MaskingPolicyModel) WithDatabaseValue(value tfconfig.Variable) *MaskingPolicyModel { + m.Database = value + return m +} + +func (m *MaskingPolicyModel) WithExemptOtherPoliciesValue(value tfconfig.Variable) *MaskingPolicyModel { + m.ExemptOtherPolicies = value + return m +} + +func (m *MaskingPolicyModel) WithFullyQualifiedNameValue(value tfconfig.Variable) *MaskingPolicyModel { + m.FullyQualifiedName = value + return m +} + +func (m *MaskingPolicyModel) WithNameValue(value tfconfig.Variable) *MaskingPolicyModel { + m.Name = value + return m +} + +func (m *MaskingPolicyModel) WithReturnDataTypeValue(value tfconfig.Variable) *MaskingPolicyModel { + m.ReturnDataType = value + return m +} + +func (m *MaskingPolicyModel) WithSchemaValue(value tfconfig.Variable) *MaskingPolicyModel { + m.Schema = value + return m +} diff --git a/pkg/acceptance/bettertestspoc/config/model/row_access_policy_model_ext.go b/pkg/acceptance/bettertestspoc/config/model/row_access_policy_model_ext.go index 848c07ee5c9..cc8e7858396 100644 --- a/pkg/acceptance/bettertestspoc/config/model/row_access_policy_model_ext.go +++ b/pkg/acceptance/bettertestspoc/config/model/row_access_policy_model_ext.go @@ -6,7 +6,7 @@ import ( tfconfig "github.com/hashicorp/terraform-plugin-testing/config" ) -func (r *RowAccessPolicyModel) WithArgument(argument []sdk.RowAccessPolicyArgument) *RowAccessPolicyModel { +func (r *RowAccessPolicyModel) WithArgument(argument []sdk.TableColumnSignature) *RowAccessPolicyModel { maps := make([]config.Variable, len(argument)) for i, v := range argument { maps[i] = config.MapVariable(map[string]config.Variable{ diff --git a/pkg/acceptance/bettertestspoc/config/model/row_access_policy_model_gen.go b/pkg/acceptance/bettertestspoc/config/model/row_access_policy_model_gen.go index 589bf1660bd..7ede96b9e0d 100644 --- a/pkg/acceptance/bettertestspoc/config/model/row_access_policy_model_gen.go +++ b/pkg/acceptance/bettertestspoc/config/model/row_access_policy_model_gen.go @@ -28,7 +28,7 @@ type RowAccessPolicyModel struct { func RowAccessPolicy( resourceName string, - argument []sdk.RowAccessPolicyArgument, + argument []sdk.TableColumnSignature, body string, database string, name string, @@ -44,7 +44,7 @@ func RowAccessPolicy( } func RowAccessPolicyWithDefaultMeta( - argument []sdk.RowAccessPolicyArgument, + argument []sdk.TableColumnSignature, body string, database string, name string, diff --git a/pkg/acceptance/helpers/masking_policy_client.go b/pkg/acceptance/helpers/masking_policy_client.go index 01fb8571045..f1ed9e816cb 100644 --- a/pkg/acceptance/helpers/masking_policy_client.go +++ b/pkg/acceptance/helpers/masking_policy_client.go @@ -26,11 +26,6 @@ func (c *MaskingPolicyClient) client() sdk.MaskingPolicies { } func (c *MaskingPolicyClient) CreateMaskingPolicy(t *testing.T) (*sdk.MaskingPolicy, func()) { - t.Helper() - return c.CreateMaskingPolicyInSchema(t, c.ids.SchemaId()) -} - -func (c *MaskingPolicyClient) CreateMaskingPolicyInSchema(t *testing.T, schemaId sdk.DatabaseObjectIdentifier) (*sdk.MaskingPolicy, func()) { t.Helper() signature := []sdk.TableColumnSignature{ { @@ -43,7 +38,7 @@ func (c *MaskingPolicyClient) CreateMaskingPolicyInSchema(t *testing.T, schemaId }, } expression := "REPLACE('X', 1, 2)" - return c.CreateMaskingPolicyWithOptions(t, schemaId, signature, sdk.DataTypeVARCHAR, expression, &sdk.CreateMaskingPolicyOptions{}) + return c.CreateMaskingPolicyWithOptions(t, signature, sdk.DataTypeVARCHAR, expression, &sdk.CreateMaskingPolicyOptions{}) } func (c *MaskingPolicyClient) CreateMaskingPolicyIdentity(t *testing.T, columnType sdk.DataType) (*sdk.MaskingPolicy, func()) { @@ -56,14 +51,28 @@ func (c *MaskingPolicyClient) CreateMaskingPolicyIdentity(t *testing.T, columnTy }, } expression := "a" - return c.CreateMaskingPolicyWithOptions(t, c.ids.SchemaId(), signature, columnType, expression, &sdk.CreateMaskingPolicyOptions{}) + return c.CreateMaskingPolicyWithOptions(t, signature, columnType, expression, &sdk.CreateMaskingPolicyOptions{}) +} + +func (c *MaskingPolicyClient) CreateMaskingPolicyWithOptions(t *testing.T, signature []sdk.TableColumnSignature, returns sdk.DataType, expression string, options *sdk.CreateMaskingPolicyOptions) (*sdk.MaskingPolicy, func()) { + t.Helper() + ctx := context.Background() + id := c.ids.RandomSchemaObjectIdentifier() + + err := c.client().Create(ctx, id, signature, returns, expression, options) + require.NoError(t, err) + + maskingPolicy, err := c.client().ShowByID(ctx, id) + require.NoError(t, err) + + return maskingPolicy, c.DropMaskingPolicyFunc(t, id) } -func (c *MaskingPolicyClient) CreateMaskingPolicyWithOptions(t *testing.T, schemaId sdk.DatabaseObjectIdentifier, signature []sdk.TableColumnSignature, returns sdk.DataType, expression string, options *sdk.CreateMaskingPolicyOptions) (*sdk.MaskingPolicy, func()) { +func (c *MaskingPolicyClient) CreateOrReplaceMaskingPolicyWithOptions(t *testing.T, id sdk.SchemaObjectIdentifier, signature []sdk.TableColumnSignature, returns sdk.DataType, expression string, options *sdk.CreateMaskingPolicyOptions) (*sdk.MaskingPolicy, func()) { t.Helper() ctx := context.Background() - id := c.ids.RandomSchemaObjectIdentifierInSchema(schemaId) + options.OrReplace = sdk.Pointer(true) err := c.client().Create(ctx, id, signature, returns, expression, options) require.NoError(t, err) @@ -74,6 +83,14 @@ func (c *MaskingPolicyClient) CreateMaskingPolicyWithOptions(t *testing.T, schem return maskingPolicy, c.DropMaskingPolicyFunc(t, id) } +func (c *MaskingPolicyClient) Alter(t *testing.T, id sdk.SchemaObjectIdentifier, req *sdk.AlterMaskingPolicyOptions) { + t.Helper() + ctx := context.Background() + + err := c.client().Alter(ctx, id, req) + require.NoError(t, err) +} + func (c *MaskingPolicyClient) DropMaskingPolicyFunc(t *testing.T, id sdk.SchemaObjectIdentifier) func() { t.Helper() ctx := context.Background() @@ -83,3 +100,10 @@ func (c *MaskingPolicyClient) DropMaskingPolicyFunc(t *testing.T, id sdk.SchemaO assert.NoError(t, err) } } + +func (c *MaskingPolicyClient) Show(t *testing.T, id sdk.SchemaObjectIdentifier) (*sdk.MaskingPolicy, error) { + t.Helper() + ctx := context.Background() + + return c.client().ShowByID(ctx, id) +} diff --git a/pkg/datasources/row_access_policies_acceptance_test.go b/pkg/datasources/row_access_policies_acceptance_test.go index 66352d1a71c..294b5fe5039 100644 --- a/pkg/datasources/row_access_policies_acceptance_test.go +++ b/pkg/datasources/row_access_policies_acceptance_test.go @@ -24,7 +24,7 @@ func TestAcc_RowAccessPolicies(t *testing.T) { id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() body := "case when current_role() in ('ANALYST') then true else false end" - policyModel := model.RowAccessPolicy("test", []sdk.RowAccessPolicyArgument{ + policyModel := model.RowAccessPolicy("test", []sdk.TableColumnSignature{ { Name: "a", Type: sdk.DataTypeVARCHAR, diff --git a/pkg/resources/doc_helpers.go b/pkg/resources/doc_helpers.go index d85e68f047e..fbfb4c20fe1 100644 --- a/pkg/resources/doc_helpers.go +++ b/pkg/resources/doc_helpers.go @@ -40,3 +40,7 @@ func blocklistedCharactersFieldDescription(description string) string { func diffSuppressStatementFieldDescription(description string) string { return fmt.Sprintf(`%s To mitigate permadiff on this field, the provider replaces blank characters with a space. This can lead to false positives in cases where a change in case or run of whitespace is semantically significant.`, description) } + +func dataTypeFieldDescription(description string) string { + return fmt.Sprintf(`%s For more information about data types, check [Snowflake docs](https://docs.snowflake.com/en/sql-reference/intro-summary-data-types).`, description) +} diff --git a/pkg/resources/masking_policy.go b/pkg/resources/masking_policy.go index f10a1321c91..27600154bea 100644 --- a/pkg/resources/masking_policy.go +++ b/pkg/resources/masking_policy.go @@ -2,259 +2,315 @@ package resources import ( "context" + "errors" + "fmt" + "log" "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/go-cty/cty" + "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" ) var maskingPolicySchema = map[string]*schema.Schema{ - "or_replace": { - Type: schema.TypeBool, - Optional: true, - Default: false, - Description: "Whether to override a previous masking policy with the same name.", - DiffSuppressOnRefresh: true, - DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { - return old != new - }, - }, - "if_not_exists": { - Type: schema.TypeBool, - Optional: true, - Default: false, - Description: "Prevent overwriting a previous masking policy with the same name.", - DiffSuppressOnRefresh: true, - DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { - return old != new - }, - }, "name": { - Type: schema.TypeString, - Required: true, - Description: "Specifies the identifier for the masking policy; must be unique for the database and schema in which the masking policy is created.", + Type: schema.TypeString, + Required: true, + Description: blocklistedCharactersFieldDescription("Specifies the identifier for the masking policy; must be unique for the database and schema in which the masking policy is created."), + DiffSuppressFunc: suppressIdentifierQuoting, }, "database": { - Type: schema.TypeString, - Required: true, - Description: "The database in which to create the masking policy.", - ForceNew: true, + Type: schema.TypeString, + Required: true, + Description: blocklistedCharactersFieldDescription("The database in which to create the masking policy."), + ForceNew: true, + DiffSuppressFunc: suppressIdentifierQuoting, }, "schema": { - Type: schema.TypeString, - Required: true, - Description: "The schema in which to create the masking policy.", - ForceNew: true, + Type: schema.TypeString, + Required: true, + Description: blocklistedCharactersFieldDescription("The schema in which to create the masking policy."), + ForceNew: true, + DiffSuppressFunc: suppressIdentifierQuoting, }, - "signature": { - Type: schema.TypeList, - Required: true, - Description: "The signature for the masking policy; specifies the input columns and data types to evaluate at query runtime.", - MinItems: 1, - MaxItems: 1, + "argument": { + Type: schema.TypeList, + MinItems: 1, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ - "column": { - Type: schema.TypeList, - Required: true, - MinItems: 1, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "name": { - Type: schema.TypeString, - Required: true, - Description: "Specifies the column name to mask.", - }, - "type": { - Type: schema.TypeString, - Required: true, - Description: "Specifies the column type to mask.", - ForceNew: true, - ValidateFunc: dataTypeValidateFunc, - DiffSuppressFunc: dataTypeDiffSuppressFunc, - }, - }, - }, + "name": { + Type: schema.TypeString, + Required: true, + Description: "The argument name", + ForceNew: true, + }, + // TODO(SNOW-1596962): Fully support VECTOR data type sdk.ParseFunctionArgumentsFromString could be a base for another function that takes argument names into consideration. + "type": { + Type: schema.TypeString, + Required: true, + DiffSuppressFunc: NormalizeAndCompare(sdk.ToDataType), + ValidateDiagFunc: sdkValidation(sdk.ToDataType), + Description: dataTypeFieldDescription("The argument type. VECTOR data types are not yet supported."), + ForceNew: true, }, }, }, + Required: true, + Description: "List of the arguments for the masking policy. The first column and its data type always indicate the column data type values to mask or tokenize in the subsequent policy conditions. Note that you can not specify a virtual column as the first column argument in a conditional masking policy.", + ForceNew: true, }, - "masking_expression": { + "body": { Type: schema.TypeString, Required: true, - Description: "Specifies the SQL expression that transforms the data.", - DiffSuppressFunc: ignoreTrimSpaceSuppressFunc, + Description: diffSuppressStatementFieldDescription("Specifies the SQL expression that transforms the data."), + DiffSuppressFunc: DiffSuppressStatement, }, "return_data_type": { Type: schema.TypeString, Required: true, - Description: "Specifies the data type to return.", + Description: dataTypeFieldDescription("The return data type must match the input data type of the first column that is specified as an input column."), ForceNew: true, - ValidateFunc: dataTypeValidateFunc, - DiffSuppressFunc: dataTypeDiffSuppressFunc, + DiffSuppressFunc: NormalizeAndCompare(sdk.ToDataType), + ValidateDiagFunc: sdkValidation(sdk.ToDataType), }, "exempt_other_policies": { - Type: schema.TypeBool, - Optional: true, - Description: "Specifies whether the row access policy or conditional masking policy can reference a column that is already protected by a masking policy.", - Default: false, + Type: schema.TypeString, + Optional: true, + Default: BooleanDefault, + ValidateDiagFunc: validateBooleanString, + DiffSuppressFunc: IgnoreChangeToCurrentSnowflakeValueInShow("exempt_other_policies"), + Description: booleanStringFieldDescription("Specifies whether the row access policy or conditional masking policy can reference a column that is already protected by a masking policy. Due to Snowflake limitations, when value is chenged, the resource is recreated."), + ForceNew: true, }, "comment": { Type: schema.TypeString, Optional: true, Description: "Specifies a comment for the masking policy.", }, + ShowOutputAttributeName: { + Type: schema.TypeList, + Computed: true, + Description: "Outputs the result of `SHOW MASKING POLICY` for the given masking policy.", + Elem: &schema.Resource{ + Schema: schemas.ShowMaskingPolicySchema, + }, + }, + DescribeOutputAttributeName: { + Type: schema.TypeList, + Computed: true, + Description: "Outputs the result of `DESCRIBE MASKING POLICY` for the given masking policy.", + Elem: &schema.Resource{ + Schema: schemas.DescribeMaskingPolicySchema, + }, + }, FullyQualifiedNameAttributeName: schemas.FullyQualifiedNameSchema, } -// DatabaseName|SchemaName|MaskingPolicyName. - // MaskingPolicy returns a pointer to the resource representing a masking policy. func MaskingPolicy() *schema.Resource { return &schema.Resource{ - Create: CreateMaskingPolicy, - Read: ReadMaskingPolicy, - Update: UpdateMaskingPolicy, - Delete: DeleteMaskingPolicy, + SchemaVersion: 1, + + CreateContext: CreateMaskingPolicy, + ReadContext: ReadMaskingPolicy(true), + UpdateContext: UpdateMaskingPolicy, + DeleteContext: DeleteMaskingPolicy, + Description: "Resource used to manage masking policies. For more information, check [masking policies documentation](https://docs.snowflake.com/en/sql-reference/sql/create-masking-policy).", CustomizeDiff: customdiff.All( + ComputedIfAnyAttributeChanged(maskingPolicySchema, ShowOutputAttributeName, "name", "comment"), + ComputedIfAnyAttributeChanged(maskingPolicySchema, DescribeOutputAttributeName, "name", "body"), ComputedIfAnyAttributeChanged(maskingPolicySchema, FullyQualifiedNameAttributeName, "name"), ), Schema: maskingPolicySchema, Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, + StateContext: ImportMaskingPolicy, + }, + + StateUpgraders: []schema.StateUpgrader{ + { + Version: 0, + // setting type to cty.EmptyObject is a bit hacky here but following https://developer.hashicorp.com/terraform/plugin/framework/migrating/resources/state-upgrade#sdkv2-1 would require lots of repetitive code; this should work with cty.EmptyObject + Type: cty.EmptyObject, + Upgrade: v0_95_0_MaskingPolicyStateUpgrader, + }, }, } } -// CreateMaskingPolicy implements schema.CreateFunc. -func CreateMaskingPolicy(d *schema.ResourceData, meta interface{}) error { +func ImportMaskingPolicy(ctx context.Context, d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { + log.Printf("[DEBUG] Starting masking policy import") + client := meta.(*provider.Context).Client + id, err := sdk.ParseSchemaObjectIdentifier(d.Id()) + if err != nil { + return nil, err + } + + policy, err := client.MaskingPolicies.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("database", id.DatabaseName()); err != nil { + return nil, err + } + if err := d.Set("schema", id.SchemaName()); err != nil { + return nil, err + } + if err := d.Set("exempt_other_policies", booleanStringFromBool(policy.ExemptOtherPolicies)); err != nil { + return nil, err + } + policyDescription, err := client.MaskingPolicies.Describe(ctx, id) + if err != nil { + return nil, err + } + if err := d.Set("body", policyDescription.Body); err != nil { + return nil, err + } + if err := d.Set("argument", schemas.MaskingPolicyArgumentsToSchema(policyDescription.Signature)); err != nil { + return nil, err + } + return []*schema.ResourceData{d}, nil +} + +func CreateMaskingPolicy(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*provider.Context).Client name := d.Get("name").(string) databaseName := d.Get("database").(string) schemaName := d.Get("schema").(string) + id := sdk.NewSchemaObjectIdentifier(databaseName, schemaName, name) - expression := d.Get("masking_expression").(string) + expression := d.Get("body").(string) returnDataType := d.Get("return_data_type").(string) - ctx := context.Background() - objectIdentifier := sdk.NewSchemaObjectIdentifier(databaseName, schemaName, name) - - signatureList := d.Get("signature").([]interface{}) - signature := []sdk.TableColumnSignature{} - for _, s := range signatureList { - m := s.(map[string]interface{}) - columns := m["column"].([]interface{}) - for _, c := range columns { - cm := c.(map[string]interface{}) - dt, err := sdk.ToDataType(cm["type"].(string)) - if err != nil { - return err - } - signature = append(signature, sdk.TableColumnSignature{ - Name: cm["name"].(string), - Type: dt, - }) + arguments := d.Get("argument").([]any) + args := make([]sdk.TableColumnSignature, 0) + for _, arg := range arguments { + v := arg.(map[string]any) + dataType, err := sdk.ToDataType(v["type"].(string)) + if err != nil { + return diag.FromErr(err) } + args = append(args, sdk.TableColumnSignature{ + Name: v["name"].(string), + Type: dataType, + }) } returns, err := sdk.ToDataType(returnDataType) if err != nil { - return err + return diag.FromErr(err) } + + // set optionals opts := &sdk.CreateMaskingPolicyOptions{} if comment, ok := d.Get("comment").(string); ok { opts.Comment = sdk.String(comment) } - if exemptOtherPolicies := d.Get("exempt_other_policies").(bool); exemptOtherPolicies { - opts.ExemptOtherPolicies = sdk.Bool(exemptOtherPolicies) + if v := d.Get("exempt_other_policies").(string); v != BooleanDefault { + parsed, err := booleanStringToBool(v) + if err != nil { + return diag.FromErr(err) + } + + opts.ExemptOtherPolicies = sdk.Pointer(parsed) } - err = client.MaskingPolicies.Create(ctx, objectIdentifier, signature, returns, expression, opts) + err = client.MaskingPolicies.Create(ctx, id, args, returns, expression, opts) if err != nil { - return err + return diag.FromErr(err) } - d.SetId(helpers.EncodeSnowflakeID(objectIdentifier)) + d.SetId(helpers.EncodeResourceIdentifier(id)) - return ReadMaskingPolicy(d, meta) + return ReadMaskingPolicy(false)(ctx, d, meta) } -// ReadMaskingPolicy implements schema.ReadFunc. -func ReadMaskingPolicy(d *schema.ResourceData, meta interface{}) error { - client := meta.(*provider.Context).Client - id := helpers.DecodeSnowflakeID(d.Id()).(sdk.SchemaObjectIdentifier) - - ctx := context.Background() - maskingPolicy, err := client.MaskingPolicies.ShowByID(ctx, id) - if err != nil { - return err - } - if err := d.Set(FullyQualifiedNameAttributeName, id.FullyQualifiedName()); err != nil { - return err - } +func ReadMaskingPolicy(withExternalChangesMarking bool) schema.ReadContextFunc { + return func(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*provider.Context).Client + id, err := sdk.ParseSchemaObjectIdentifier(d.Id()) + if err != nil { + return diag.FromErr(err) + } - if err := d.Set("name", maskingPolicy.Name); err != nil { - return err - } + maskingPolicy, err := client.MaskingPolicies.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 masking policy. Marking the resource as removed.", + Detail: fmt.Sprintf("masking policy name: %s, Err: %s", id.FullyQualifiedName(), err), + }, + } + } + return diag.FromErr(err) + } + if err := d.Set(FullyQualifiedNameAttributeName, id.FullyQualifiedName()); err != nil { + return diag.FromErr(err) + } - if err := d.Set("database", maskingPolicy.DatabaseName); err != nil { - return err - } + if err := d.Set("comment", maskingPolicy.Comment); err != nil { + return diag.FromErr(err) + } - if err := d.Set("schema", maskingPolicy.SchemaName); err != nil { - return err - } + maskingPolicyDescription, err := client.MaskingPolicies.Describe(ctx, id) + if err != nil { + return diag.FromErr(err) + } - if err := d.Set("exempt_other_policies", maskingPolicy.ExemptOtherPolicies); err != nil { - return err - } + if err := d.Set("body", maskingPolicyDescription.Body); err != nil { + return diag.FromErr(err) + } - if err := d.Set("comment", maskingPolicy.Comment); err != nil { - return err - } + if err := d.Set("return_data_type", maskingPolicyDescription.ReturnType); err != nil { + return diag.FromErr(err) + } - maskingPolicyDetails, err := client.MaskingPolicies.Describe(ctx, id) - if err != nil { - return err - } + if err := d.Set("argument", schemas.MaskingPolicyArgumentsToSchema(maskingPolicyDescription.Signature)); err != nil { + return diag.FromErr(err) + } - if err := d.Set("masking_expression", maskingPolicyDetails.Body); err != nil { - return err - } + if withExternalChangesMarking { + if err = handleExternalChangesToObjectInShow(d, + showMapping{"exempt_other_policies", "exempt_other_policies", maskingPolicy.ExemptOtherPolicies, booleanStringFromBool(maskingPolicy.ExemptOtherPolicies), nil}, + ); err != nil { + return diag.FromErr(err) + } + } - if err := d.Set("return_data_type", maskingPolicyDetails.ReturnType); err != nil { - return err - } + if err = setStateToValuesFromConfig(d, maskingPolicySchema, []string{ + "exempt_other_policies", + }); err != nil { + return diag.FromErr(err) + } - columns := []map[string]interface{}{} - for _, s := range maskingPolicyDetails.Signature { - columns = append(columns, map[string]interface{}{ - "name": s.Name, - "type": s.Type, - }) - } - signature := []map[string]interface{}{ - {"column": columns}, - } - if err := d.Set("signature", signature); err != nil { - return err + if err = d.Set(ShowOutputAttributeName, []map[string]any{schemas.MaskingPolicyToSchema(maskingPolicy)}); err != nil { + return diag.FromErr(err) + } + if err = d.Set(DescribeOutputAttributeName, []map[string]any{schemas.MaskingPolicyDescriptionToSchema(*maskingPolicyDescription)}); err != nil { + return diag.FromErr(err) + } + return nil } - - return err } -// UpdateMaskingPolicy implements schema.UpdateFunc. -func UpdateMaskingPolicy(d *schema.ResourceData, meta interface{}) error { +func UpdateMaskingPolicy(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*provider.Context).Client - id := helpers.DecodeSnowflakeID(d.Id()).(sdk.SchemaObjectIdentifier) - ctx := context.Background() - + id, err := sdk.ParseSchemaObjectIdentifier(d.Id()) + if err != nil { + return diag.FromErr(err) + } if d.HasChange("name") { newID := sdk.NewSchemaObjectIdentifierInSchema(id.SchemaId(), d.Get("name").(string)) @@ -262,22 +318,22 @@ func UpdateMaskingPolicy(d *schema.ResourceData, meta interface{}) error { NewName: &newID, }) if err != nil { - return err + return diag.FromErr(err) } - d.SetId(helpers.EncodeSnowflakeID(newID)) + d.SetId(helpers.EncodeResourceIdentifier(newID)) id = newID } - if d.HasChange("masking_expression") { - alterOptions := &sdk.AlterMaskingPolicyOptions{} - _, n := d.GetChange("masking_expression") - alterOptions.Set = &sdk.MaskingPolicySet{ - Body: sdk.String(n.(string)), + if d.HasChange("body") { + alterOptions := &sdk.AlterMaskingPolicyOptions{ + Set: &sdk.MaskingPolicySet{ + Body: sdk.Pointer(d.Get("body").(string)), + }, } err := client.MaskingPolicies.Alter(ctx, id, alterOptions) if err != nil { - return err + return diag.FromErr(err) } } @@ -294,22 +350,30 @@ func UpdateMaskingPolicy(d *schema.ResourceData, meta interface{}) error { } err := client.MaskingPolicies.Alter(ctx, id, alterOptions) if err != nil { - return err + return diag.FromErr(err) } } + // exempt_other_policies is handled by ForceNew - return ReadMaskingPolicy(d, meta) + return ReadMaskingPolicy(false)(ctx, d, meta) } -// DeleteMaskingPolicy implements schema.DeleteFunc. -func DeleteMaskingPolicy(d *schema.ResourceData, meta interface{}) error { +func DeleteMaskingPolicy(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*provider.Context).Client - ctx := context.Background() - objectIdentifier := helpers.DecodeSnowflakeID(d.Id()).(sdk.SchemaObjectIdentifier) + id, err := sdk.ParseSchemaObjectIdentifier(d.Id()) + if err != nil { + return diag.FromErr(err) + } - err := client.MaskingPolicies.Drop(ctx, objectIdentifier, &sdk.DropMaskingPolicyOptions{IfExists: sdk.Bool(true)}) + err = client.MaskingPolicies.Drop(ctx, id, &sdk.DropMaskingPolicyOptions{IfExists: sdk.Pointer(true)}) if err != nil { - return err + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Error, + Summary: "Error deleting masking policy", + Detail: fmt.Sprintf("id %v err = %v", id.Name(), err), + }, + } } d.SetId("") diff --git a/pkg/resources/masking_policy_acceptance_test.go b/pkg/resources/masking_policy_acceptance_test.go index 94c0e5494f4..98a3648fb99 100644 --- a/pkg/resources/masking_policy_acceptance_test.go +++ b/pkg/resources/masking_policy_acceptance_test.go @@ -2,71 +2,231 @@ package resources_test import ( "fmt" + "regexp" "testing" "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" + tfconfig "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/internal/snowflakeroles" + r "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/resources" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" "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" ) -func TestAcc_MaskingPolicy(t *testing.T) { - oldId := acc.TestClient().Ids.RandomSchemaObjectIdentifier() - newId := acc.TestClient().Ids.RandomSchemaObjectIdentifier() - comment := "Terraform acceptance test" - comment2 := "Terraform acceptance test 2" +func TestAcc_MaskingPolicy_basic(t *testing.T) { + id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() + resourceName := "snowflake_masking_policy.test" + + body := "case when current_role() in ('ANALYST') then 'true' else 'false' end" + changedBody := "case when current_role() in ('CHANGED') then 'foo' else 'bar' end" + bodyWithBooleanReturnType := "case when current_role() in ('ANALYST') then true else false end" + argument := []sdk.TableColumnSignature{ + { + Name: "A", + Type: sdk.DataTypeVARCHAR, + }, + { + Name: "B", + Type: sdk.DataTypeVARCHAR, + }, + } + argumentWithChangedFirstArgumentType := []sdk.TableColumnSignature{ + { + Name: "A", + Type: sdk.DataTypeBoolean, + }, + { + Name: "B", + Type: sdk.DataTypeVARCHAR, + }, + } + changedArgument := []sdk.TableColumnSignature{ + { + Name: "C", + Type: sdk.DataTypeVARCHAR, + }, + { + Name: "D", + Type: sdk.DataTypeTimestampNTZ, + }, + } + policyModel := model.MaskingPolicy("test", argument, body, id.DatabaseName(), id.Name(), string(sdk.DataTypeVARCHAR), id.SchemaName()) + resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + PreCheck: func() { acc.TestAccPreCheck(t) }, TerraformVersionChecks: []tfversion.TerraformVersionCheck{ tfversion.RequireAbove(tfversion.Version1_5_0), }, - PreCheck: func() { acc.TestAccPreCheck(t) }, CheckDestroy: acc.CheckDestroy(t, resources.MaskingPolicy), Steps: []resource.TestStep{ { - Config: maskingPolicyConfig(oldId.Name(), comment, acc.TestDatabaseName, acc.TestSchemaName), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("snowflake_masking_policy.test", "name", oldId.Name()), - resource.TestCheckResourceAttr("snowflake_masking_policy.test", "fully_qualified_name", oldId.FullyQualifiedName()), - resource.TestCheckResourceAttr("snowflake_masking_policy.test", "database", acc.TestDatabaseName), - resource.TestCheckResourceAttr("snowflake_masking_policy.test", "schema", acc.TestSchemaName), - resource.TestCheckResourceAttr("snowflake_masking_policy.test", "comment", comment), - resource.TestCheckResourceAttr("snowflake_masking_policy.test", "masking_expression", "case when current_role() in ('ANALYST') then val else sha2(val, 512) end"), - resource.TestCheckResourceAttr("snowflake_masking_policy.test", "return_data_type", "VARCHAR"), - resource.TestCheckResourceAttr("snowflake_masking_policy.test", "signature.#", "1"), - resource.TestCheckResourceAttr("snowflake_masking_policy.test", "signature.0.column.#", "1"), - resource.TestCheckResourceAttr("snowflake_masking_policy.test", "signature.0.column.0.name", "val"), - resource.TestCheckResourceAttr("snowflake_masking_policy.test", "signature.0.column.0.type", "VARCHAR"), + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_MaskingPolicy/basic"), + ConfigVariables: tfconfig.ConfigVariablesFromModel(t, policyModel), + Check: assert.AssertThat(t, resourceassert.MaskingPolicyResource(t, resourceName). + HasNameString(id.Name()). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasReturnDataTypeString(string(sdk.DataTypeVARCHAR)). + HasFullyQualifiedNameString(id.FullyQualifiedName()). + HasBodyString(body). + HasExemptOtherPoliciesString(r.BooleanDefault). + HasArguments(argument), + ), + }, + // set all fields + { + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_MaskingPolicy/complete"), + ConfigVariables: tfconfig.ConfigVariablesFromModel(t, policyModel.WithBody(body).WithComment("Terraform acceptance test").WithExemptOtherPolicies(r.BooleanTrue)), + Check: assert.AssertThat(t, resourceassert.MaskingPolicyResource(t, resourceName). + HasNameString(id.Name()). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasExemptOtherPoliciesString(r.BooleanTrue). + HasReturnDataTypeString(string(sdk.DataTypeVARCHAR)). + HasFullyQualifiedNameString(id.FullyQualifiedName()). + HasCommentString("Terraform acceptance test"). + HasBodyString(body). + HasArguments(argument), + resourceshowoutputassert.MaskingPolicyShowOutput(t, resourceName). + HasCreatedOnNotEmpty(). + HasDatabaseName(id.DatabaseName()). + HasKind(string(sdk.PolicyKindMaskingPolicy)). + HasName(id.Name()). + HasExemptOtherPolicies(true). + HasOwner(snowflakeroles.Accountadmin.Name()). + HasOwnerRoleType("ROLE"). + HasSchemaName(id.SchemaName()), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.body", body)), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.name", id.Name())), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.return_type", string(sdk.DataTypeVARCHAR))), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.signature.#", "2")), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.signature.0.name", "A")), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.signature.0.type", string(sdk.DataTypeVARCHAR))), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.signature.1.name", "B")), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.signature.1.type", string(sdk.DataTypeVARCHAR))), ), }, - // rename + change comment + // change fields { - Config: maskingPolicyConfig(newId.Name(), comment2, acc.TestDatabaseName, acc.TestSchemaName), + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_MaskingPolicy/complete"), + ConfigVariables: tfconfig.ConfigVariablesFromModel(t, policyModel.WithBody(bodyWithBooleanReturnType).WithReturnDataType(string(sdk.DataTypeBoolean)).WithArgument(argumentWithChangedFirstArgumentType).WithComment("Terraform acceptance test - changed comment").WithExemptOtherPolicies(r.BooleanFalse)), + Check: assert.AssertThat(t, resourceassert.MaskingPolicyResource(t, resourceName). + HasNameString(id.Name()). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasReturnDataTypeString(string(sdk.DataTypeBoolean)). + HasFullyQualifiedNameString(id.FullyQualifiedName()). + HasExemptOtherPoliciesString(r.BooleanFalse). + HasCommentString("Terraform acceptance test - changed comment"). + HasBodyString(bodyWithBooleanReturnType). + HasArguments(argumentWithChangedFirstArgumentType), + ), + }, + // restore previous types - first argument type, return_type, and returned value in `body` must be the same type + { + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_MaskingPolicy/complete"), + ConfigVariables: tfconfig.ConfigVariablesFromModel(t, policyModel.WithBody(body).WithReturnDataType(string(sdk.DataTypeVARCHAR)).WithArgument(changedArgument).WithExemptOtherPolicies(r.BooleanTrue)), + Check: assert.AssertThat(t, resourceassert.MaskingPolicyResource(t, resourceName). + HasNameString(id.Name()). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasReturnDataTypeString(string(sdk.DataTypeVARCHAR)). + HasFullyQualifiedNameString(id.FullyQualifiedName()). + HasExemptOtherPoliciesString(r.BooleanTrue). + HasCommentString("Terraform acceptance test - changed comment"). + HasBodyString(body). + HasArguments(changedArgument), + ), + }, + // external change on signature + { + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_MaskingPolicy/complete"), + ConfigVariables: tfconfig.ConfigVariablesFromModel(t, policyModel), + PreConfig: func() { + acc.TestClient().MaskingPolicy.CreateOrReplaceMaskingPolicyWithOptions(t, id, argument, sdk.DataTypeVARCHAR, body, &sdk.CreateMaskingPolicyOptions{ + ExemptOtherPolicies: sdk.Pointer(false), + Comment: sdk.Pointer("Terraform acceptance test - changed comment"), + OrReplace: sdk.Pointer(true), + }) + }, ConfigPlanChecks: resource.ConfigPlanChecks{ PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction("snowflake_masking_policy.test", plancheck.ResourceActionUpdate), + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionDestroyBeforeCreate), }, }, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("snowflake_masking_policy.test", "name", newId.Name()), - resource.TestCheckResourceAttr("snowflake_masking_policy.test", "fully_qualified_name", newId.FullyQualifiedName()), - resource.TestCheckResourceAttr("snowflake_masking_policy.test", "comment", comment2), + Check: assert.AssertThat(t, resourceassert.MaskingPolicyResource(t, resourceName). + HasNameString(id.Name()). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasFullyQualifiedNameString(id.FullyQualifiedName()). + HasCommentString("Terraform acceptance test - changed comment"). + HasBodyString(body). + HasArguments(changedArgument), ), }, - // change body and unset comment + // external change on body and exempt other policies { - Config: maskingPolicyConfigMultiline(newId.Name(), acc.TestDatabaseName, acc.TestSchemaName), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("snowflake_masking_policy.test", "masking_expression", "case\n\twhen current_role() in ('ROLE_A') then\n\t\tval\n\twhen is_role_in_session( 'ROLE_B' ) then\n\t\t'ABC123'\n\telse\n\t\t'******'\nend"), - resource.TestCheckResourceAttr("snowflake_masking_policy.test", "comment", ""), + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_MaskingPolicy/complete"), + ConfigVariables: tfconfig.ConfigVariablesFromModel(t, policyModel), + PreConfig: func() { + acc.TestClient().MaskingPolicy.Alter(t, id, &sdk.AlterMaskingPolicyOptions{ + Set: &sdk.MaskingPolicySet{ + Body: sdk.Pointer(changedBody), + }, + }) + }, + Check: assert.AssertThat(t, resourceassert.MaskingPolicyResource(t, resourceName). + HasNameString(id.Name()). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasFullyQualifiedNameString(id.FullyQualifiedName()). + HasCommentString("Terraform acceptance test - changed comment"). + HasBodyString(body). + HasArguments(changedArgument), + ), + }, + { + ConfigVariables: tfconfig.ConfigVariablesFromModel(t, policyModel), + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + // unset comment + { + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_MaskingPolicy/complete"), + ConfigVariables: tfconfig.ConfigVariablesFromModel(t, policyModel.WithComment("")), + PreConfig: func() { + acc.TestClient().MaskingPolicy.Alter(t, id, &sdk.AlterMaskingPolicyOptions{ + Unset: &sdk.MaskingPolicyUnset{ + Comment: sdk.Pointer(true), + }, + }) + }, + Check: assert.AssertThat(t, resourceassert.MaskingPolicyResource(t, resourceName). + HasNameString(id.Name()). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasFullyQualifiedNameString(id.FullyQualifiedName()). + HasCommentString(""). + HasBodyString(body). + HasArguments(changedArgument), ), }, // IMPORT { - ResourceName: "snowflake_masking_policy.test", + ConfigVariables: tfconfig.ConfigVariablesFromModel(t, policyModel), + ResourceName: resourceName, ImportState: true, ImportStateVerify: true, }, @@ -74,7 +234,68 @@ func TestAcc_MaskingPolicy(t *testing.T) { }) } -func maskingPolicyConfig(name string, comment string, databaseName string, schemaName string) string { +func TestAcc_MaskingPolicy_complete(t *testing.T) { + id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() + resourceName := "snowflake_masking_policy.test" + + body := "case when current_role() in ('ANALYST') then 'true' else 'false' end" + argument := []sdk.TableColumnSignature{ + { + Name: "A", + Type: sdk.DataTypeVARCHAR, + }, + { + Name: "B", + Type: sdk.DataTypeVARCHAR, + }, + } + policyModel := model.MaskingPolicy("test", argument, body, id.DatabaseName(), id.Name(), string(sdk.DataTypeVARCHAR), id.SchemaName()).WithComment("foo").WithExemptOtherPolicies(r.BooleanTrue) + + 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.MaskingPolicy), + Steps: []resource.TestStep{ + { + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_MaskingPolicy/complete"), + ConfigVariables: tfconfig.ConfigVariablesFromModel(t, policyModel), + Check: assert.AssertThat(t, resourceassert.MaskingPolicyResource(t, resourceName). + HasNameString(id.Name()). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasExemptOtherPoliciesString(r.BooleanTrue). + HasReturnDataTypeString(string(sdk.DataTypeVARCHAR)). + HasFullyQualifiedNameString(id.FullyQualifiedName()). + HasCommentString("foo"). + HasBodyString(body). + HasArguments(argument), + resourceshowoutputassert.MaskingPolicyShowOutput(t, resourceName). + HasCreatedOnNotEmpty(). + HasDatabaseName(id.DatabaseName()). + HasKind(string(sdk.PolicyKindMaskingPolicy)). + HasName(id.Name()). + HasExemptOtherPolicies(true). + HasOwner(snowflakeroles.Accountadmin.Name()). + HasOwnerRoleType("ROLE"). + HasSchemaName(id.SchemaName()), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.body", body)), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.name", id.Name())), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.return_type", string(sdk.DataTypeVARCHAR))), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.signature.#", "2")), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.signature.0.name", "A")), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.signature.0.type", string(sdk.DataTypeVARCHAR))), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.signature.1.name", "B")), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.signature.1.type", string(sdk.DataTypeVARCHAR))), + ), + }, + }, + }) +} + +func maskingPolicyConfig(name string, databaseName string, schemaName string) string { return fmt.Sprintf(` resource "snowflake_masking_policy" "test" { name = "%s" @@ -88,9 +309,8 @@ resource "snowflake_masking_policy" "test" { } masking_expression = "case when current_role() in ('ANALYST') then val else sha2(val, 512) end" return_data_type = "VARCHAR" - comment = "%s" } -`, name, databaseName, schemaName, comment) +`, name, databaseName, schemaName) } func maskingPolicyConfigMultiline(name string, databaseName string, schemaName string) string { @@ -99,13 +319,11 @@ func maskingPolicyConfigMultiline(name string, databaseName string, schemaName s name = "%s" database = "%s" schema = "%s" - signature { - column { - name = "val" - type = "VARCHAR" - } + argument { + name = "val" + type = "VARCHAR" } - masking_expression = <<-EOF + body = <<-EOF case when current_role() in ('ROLE_A') then val @@ -142,14 +360,13 @@ func TestAcc_MaskingPolicyMultiColumns(t *testing.T) { resource.TestCheckResourceAttr("snowflake_masking_policy.test", "name", accName), resource.TestCheckResourceAttr("snowflake_masking_policy.test", "database", acc.TestDatabaseName), resource.TestCheckResourceAttr("snowflake_masking_policy.test", "schema", acc.TestSchemaName), - resource.TestCheckResourceAttr("snowflake_masking_policy.test", "masking_expression", "case when current_role() in ('ANALYST') then val else sha2(val, 512) end"), - resource.TestCheckResourceAttr("snowflake_masking_policy.test", "return_data_type", "VARCHAR"), - resource.TestCheckResourceAttr("snowflake_masking_policy.test", "signature.#", "1"), - resource.TestCheckResourceAttr("snowflake_masking_policy.test", "signature.0.column.#", "2"), - resource.TestCheckResourceAttr("snowflake_masking_policy.test", "signature.0.column.0.name", "val"), - resource.TestCheckResourceAttr("snowflake_masking_policy.test", "signature.0.column.0.type", "VARCHAR"), - resource.TestCheckResourceAttr("snowflake_masking_policy.test", "signature.0.column.1.name", "val2"), - resource.TestCheckResourceAttr("snowflake_masking_policy.test", "signature.0.column.1.type", "VARCHAR"), + resource.TestCheckResourceAttr("snowflake_masking_policy.test", "body", "case when current_role() in ('ANALYST') then val else sha2(val, 512) end"), + resource.TestCheckResourceAttr("snowflake_masking_policy.test", "return_data_type", string(sdk.DataTypeVARCHAR)), + resource.TestCheckResourceAttr("snowflake_masking_policy.test", "argument.#", "2"), + resource.TestCheckResourceAttr("snowflake_masking_policy.test", "argument.0.name", "val"), + resource.TestCheckResourceAttr("snowflake_masking_policy.test", "argument.0.type", string(sdk.DataTypeVARCHAR)), + resource.TestCheckResourceAttr("snowflake_masking_policy.test", "argument.1.name", "val2"), + resource.TestCheckResourceAttr("snowflake_masking_policy.test", "argument.1.type", string(sdk.DataTypeVARCHAR)), ), }, }, @@ -162,18 +379,16 @@ resource "snowflake_masking_policy" "test" { name = "%s" database = "%s" schema = "%s" - signature { - column { - name = "val" - type = "VARCHAR" - } + argument { + name = "val" + type = "VARCHAR" + } - column { - name = "val2" - type = "VARCHAR" - } + argument { + name = "val2" + type = "VARCHAR" } - masking_expression = "case when current_role() in ('ANALYST') then val else sha2(val, 512) end" + body = "case when current_role() in ('ANALYST') then val else sha2(val, 512) end" return_data_type = "VARCHAR" } `, name, databaseName, schemaName) @@ -182,7 +397,13 @@ resource "snowflake_masking_policy" "test" { func TestAcc_MaskingPolicy_migrateFromVersion_0_94_1(t *testing.T) { id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() resourceName := "snowflake_masking_policy.test" - comment := "foo" + body := "case when current_role() in ('ANALYST') then val else sha2(val, 512) end" + policyModel := model.MaskingPolicy("test", []sdk.TableColumnSignature{ + { + Name: "val", + Type: sdk.DataTypeVARCHAR, + }, + }, body, id.DatabaseName(), id.Name(), string(sdk.DataTypeVARCHAR), id.SchemaName()) resource.Test(t, resource.TestCase{ PreCheck: func() { acc.TestAccPreCheck(t) }, @@ -198,7 +419,7 @@ func TestAcc_MaskingPolicy_migrateFromVersion_0_94_1(t *testing.T) { Source: "Snowflake-Labs/snowflake", }, }, - Config: maskingPolicyConfig(id.Name(), comment, acc.TestDatabaseName, acc.TestSchemaName), + Config: maskingPolicyConfig(id.Name(), acc.TestDatabaseName, acc.TestSchemaName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(resourceName, "name", id.Name()), resource.TestCheckResourceAttr(resourceName, "qualified_name", id.FullyQualifiedName()), @@ -206,7 +427,8 @@ func TestAcc_MaskingPolicy_migrateFromVersion_0_94_1(t *testing.T) { }, { ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, - Config: maskingPolicyConfig(id.Name(), comment, acc.TestDatabaseName, acc.TestSchemaName), + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_MaskingPolicy/basic"), + ConfigVariables: tfconfig.ConfigVariablesFromModel(t, policyModel), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(resourceName, "name", id.Name()), resource.TestCheckResourceAttr(resourceName, "fully_qualified_name", id.FullyQualifiedName()), @@ -216,3 +438,214 @@ func TestAcc_MaskingPolicy_migrateFromVersion_0_94_1(t *testing.T) { }, }) } + +func TestAcc_MaskingPolicy_Rename(t *testing.T) { + id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() + newId := acc.TestClient().Ids.RandomSchemaObjectIdentifier() + resourceName := "snowflake_masking_policy.test" + body := "case when current_role() in ('ANALYST') then 'true' else 'false' end" + policyModel := model.MaskingPolicy("test", []sdk.TableColumnSignature{ + { + Name: "a", + Type: sdk.DataTypeVARCHAR, + }, + }, body, id.DatabaseName(), id.Name(), string(sdk.DataTypeVARCHAR), id.SchemaName()) + + 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.MaskingPolicy), + Steps: []resource.TestStep{ + { + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_MaskingPolicy/basic"), + ConfigVariables: tfconfig.ConfigVariablesFromModel(t, policyModel), + Check: assert.AssertThat(t, resourceassert.MaskingPolicyResource(t, resourceName). + HasNameString(id.Name()). + HasFullyQualifiedNameString(id.FullyQualifiedName()), + ), + }, + // rename + { + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_MaskingPolicy/basic"), + ConfigVariables: tfconfig.ConfigVariablesFromModel(t, policyModel.WithName(newId.Name())), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionUpdate), + }, + }, + Check: assert.AssertThat(t, resourceassert.MaskingPolicyResource(t, resourceName). + HasNameString(newId.Name()). + HasFullyQualifiedNameString(newId.FullyQualifiedName()), + ), + }, + }, + }) +} + +func TestAcc_MaskingPolicy_InvalidDataType(t *testing.T) { + id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() + + body := "case when current_role() in ('ANALYST') then true else false end" + policyModel := model.MaskingPolicy("test", []sdk.TableColumnSignature{ + { + Name: "a", + Type: "invalid-type", + }, + }, body, id.DatabaseName(), id.Name(), string(sdk.DataTypeVARCHAR), id.SchemaName()) + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + PreCheck: func() { acc.TestAccPreCheck(t) }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + Steps: []resource.TestStep{ + { + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_MaskingPolicy/basic"), + ConfigVariables: tfconfig.ConfigVariablesFromModel(t, policyModel), + ExpectError: regexp.MustCompile(`invalid data type: invalid-type`), + }, + }, + }) +} + +func TestAcc_MaskingPolicy_DataTypeAliases(t *testing.T) { + id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() + resourceName := "snowflake_masking_policy.test" + body := "case when current_role() in ('ANALYST') then 'ok' else '***' end" + policyModel := model.MaskingPolicy("test", []sdk.TableColumnSignature{ + { + Name: "a", + Type: "TEXT", + }, + }, body, id.DatabaseName(), id.Name(), string(sdk.DataTypeVARCHAR), id.SchemaName()) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + PreCheck: func() { acc.TestAccPreCheck(t) }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + Steps: []resource.TestStep{ + { + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_MaskingPolicy/basic"), + ConfigVariables: tfconfig.ConfigVariablesFromModel(t, policyModel), + Check: assert.AssertThat(t, resourceassert.MaskingPolicyResource(t, resourceName). + HasNameString(id.Name()). + HasReturnDataTypeString(string(sdk.DataTypeVARCHAR)). + HasArguments([]sdk.TableColumnSignature{ + { + Name: "a", + Type: sdk.DataTypeVARCHAR, + }, + }), + ), + }, + }, + }) +} + +func TestAcc_MaskingPolicy_migrateFromVersion_0_95_0(t *testing.T) { + id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() + resourceName := "snowflake_masking_policy.test" + comment := "Terraform test resource" + body := "case when current_role() in ('ANALYST') then 'true' else 'false' end" + policyModel := model.MaskingPolicy("test", []sdk.TableColumnSignature{ + { + Name: "A", + Type: sdk.DataTypeVARCHAR, + }, + { + Name: "b", + Type: sdk.DataTypeVARCHAR, + }, + }, body, id.DatabaseName(), id.Name(), string(sdk.DataTypeVARCHAR), id.SchemaName()).WithComment(comment).WithExemptOtherPolicies(r.BooleanTrue) + + resource.Test(t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + PreCheck: func() { acc.TestAccPreCheck(t) }, + Steps: []resource.TestStep{ + { + ExternalProviders: map[string]resource.ExternalProvider{ + "snowflake": { + VersionConstraint: "=0.95.0", + Source: "Snowflake-Labs/snowflake", + }, + }, + Config: maskingPolicy_v0_95_0(id, body, comment), + Check: assert.AssertThat(t, resourceassert.MaskingPolicyResource(t, resourceName). + HasNameString(id.Name()). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasCommentString(comment). + HasFullyQualifiedNameString(id.FullyQualifiedName()), + assert.Check(resource.TestCheckResourceAttr(resourceName, "id", helpers.EncodeSnowflakeID(id))), + assert.Check(resource.TestCheckResourceAttr(resourceName, "masking_expression", body)), + assert.Check(resource.TestCheckResourceAttr(resourceName, "signature.#", "1")), + assert.Check(resource.TestCheckResourceAttr(resourceName, "signature.0.column.#", "2")), + assert.Check(resource.TestCheckResourceAttr(resourceName, "signature.0.column.0.name", "A")), + assert.Check(resource.TestCheckResourceAttr(resourceName, "signature.0.column.0.type", string(sdk.DataTypeVARCHAR))), + assert.Check(resource.TestCheckResourceAttr(resourceName, "signature.0.column.1.name", "b")), + assert.Check(resource.TestCheckResourceAttr(resourceName, "signature.0.column.1.type", string(sdk.DataTypeVARCHAR))), + ), + }, + { + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_MaskingPolicy/complete"), + ConfigVariables: tfconfig.ConfigVariablesFromModel(t, policyModel), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionNoop), + }, + }, + Check: assert.AssertThat(t, resourceassert.MaskingPolicyResource(t, resourceName). + HasNameString(id.Name()). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasCommentString(comment). + HasFullyQualifiedNameString(id.FullyQualifiedName()). + HasBodyString(body). + HasArguments([]sdk.TableColumnSignature{ + { + Name: "A", + Type: sdk.DataTypeVARCHAR, + }, + { + Name: "b", + Type: sdk.DataTypeVARCHAR, + }, + }), + assert.Check(resource.TestCheckResourceAttr(resourceName, "id", id.FullyQualifiedName())), + assert.Check(resource.TestCheckNoResourceAttr(resourceName, "masking_expression")), + ), + }, + }, + }) +} + +func maskingPolicy_v0_95_0(id sdk.SchemaObjectIdentifier, expr, comment string) string { + return fmt.Sprintf(` +resource "snowflake_masking_policy" "test" { + name = "%s" + database = "%s" + schema = "%s" + signature { + column { + name = "A" + type = "VARCHAR" + } + column { + name = "b" + type = "VARCHAR" + } + } + return_data_type = "VARCHAR" + masking_expression = "%s" + exempt_other_policies = true + comment = "%s" +}`, id.Name(), id.DatabaseName(), id.SchemaName(), expr, comment) +} diff --git a/pkg/resources/masking_policy_state_upgraders.go b/pkg/resources/masking_policy_state_upgraders.go new file mode 100644 index 00000000000..499a0457672 --- /dev/null +++ b/pkg/resources/masking_policy_state_upgraders.go @@ -0,0 +1,32 @@ +package resources + +import ( + "context" + "fmt" + "strings" +) + +func v0_95_0_MaskingPolicyStateUpgrader(ctx context.Context, rawState map[string]any, meta any) (map[string]any, error) { + if rawState == nil { + return rawState, nil + } + + rawState["body"] = rawState["masking_expression"] + + signature := rawState["signature"].([]any) + if len(signature) != 1 { + return nil, fmt.Errorf("corrupted signature in state: expected list of length 1, got %d", len(signature)) + } + columns := signature[0].(map[string]any)["column"].([]any) + args := make([]map[string]any, 0) + for _, v := range columns { + column := v.(map[string]any) + args = append(args, map[string]any{ + "name": strings.ToUpper(column["name"].(string)), + "type": column["type"].(string), + }) + } + rawState["argument"] = args + + return migratePipeSeparatedObjectIdentifierResourceIdToFullyQualifiedName(ctx, rawState, meta) +} diff --git a/pkg/resources/row_access_policy.go b/pkg/resources/row_access_policy.go index 3c07409f329..4ccc0558e23 100644 --- a/pkg/resources/row_access_policy.go +++ b/pkg/resources/row_access_policy.go @@ -2,6 +2,7 @@ package resources import ( "context" + "errors" "fmt" "log" @@ -53,7 +54,7 @@ var rowAccessPolicySchema = map[string]*schema.Schema{ Required: true, DiffSuppressFunc: NormalizeAndCompare(sdk.ToDataType), ValidateDiagFunc: sdkValidation(sdk.ToDataType), - Description: "The argument type. VECTOR data types are not yet supported. For more information about data types, check [Snowflake docs](https://docs.snowflake.com/en/sql-reference/intro-summary-data-types).", + Description: dataTypeFieldDescription("The argument type. VECTOR data types are not yet supported."), ForceNew: true, }, }, @@ -209,9 +210,17 @@ func ReadRowAccessPolicy(ctx context.Context, d *schema.ResourceData, meta any) rowAccessPolicy, err := client.RowAccessPolicies.ShowByID(ctx, id) if err != nil { - log.Printf("[DEBUG] row access policy (%s) not found", d.Id()) - d.SetId("") - return nil + if errors.Is(err, sdk.ErrObjectNotFound) { + d.SetId("") + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Warning, + Summary: "Failed to query row access policy. Marking the resource as removed.", + Detail: fmt.Sprintf("row access policy name: %s, Err: %s", id.FullyQualifiedName(), err), + }, + } + } + return diag.FromErr(err) } if err := d.Set(FullyQualifiedNameAttributeName, id.FullyQualifiedName()); err != nil { return diag.FromErr(err) diff --git a/pkg/resources/row_access_policy_acceptance_test.go b/pkg/resources/row_access_policy_acceptance_test.go index f8e7b89812b..5dfeddcaed5 100644 --- a/pkg/resources/row_access_policy_acceptance_test.go +++ b/pkg/resources/row_access_policy_acceptance_test.go @@ -26,7 +26,7 @@ func TestAcc_RowAccessPolicy(t *testing.T) { body := "case when current_role() in ('ANALYST') then true else false end" changedBody := "case when current_role() in ('CHANGED') then true else false end" - argument := []sdk.RowAccessPolicyArgument{ + argument := []sdk.TableColumnSignature{ { Name: "A", Type: sdk.DataTypeVARCHAR, @@ -36,7 +36,7 @@ func TestAcc_RowAccessPolicy(t *testing.T) { Type: sdk.DataTypeVARCHAR, }, } - changedArgument := []sdk.RowAccessPolicyArgument{ + changedArgument := []sdk.TableColumnSignature{ { Name: "C", Type: sdk.DataTypeBoolean, @@ -195,7 +195,7 @@ func TestAcc_RowAccessPolicy_Issue2053(t *testing.T) { id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() resourceName := "snowflake_row_access_policy.test" body := "case when current_role() in ('ANALYST') then true else false end" - policyModel := model.RowAccessPolicy("test", []sdk.RowAccessPolicyArgument{ + policyModel := model.RowAccessPolicy("test", []sdk.TableColumnSignature{ { Name: "A", Type: sdk.DataTypeVARCHAR, @@ -253,7 +253,7 @@ func TestAcc_RowAccessPolicy_Rename(t *testing.T) { newId := acc.TestClient().Ids.RandomSchemaObjectIdentifier() resourceName := "snowflake_row_access_policy.test" body := "case when current_role() in ('ANALYST') then true else false end" - policyModel := model.RowAccessPolicy("test", []sdk.RowAccessPolicyArgument{ + policyModel := model.RowAccessPolicy("test", []sdk.TableColumnSignature{ { Name: "a", Type: sdk.DataTypeVARCHAR, @@ -346,7 +346,7 @@ func TestAcc_RowAccessPolicy_InvalidDataType(t *testing.T) { id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() body := "case when current_role() in ('ANALYST') then true else false end" - policyModel := model.RowAccessPolicy("test", []sdk.RowAccessPolicyArgument{ + policyModel := model.RowAccessPolicy("test", []sdk.TableColumnSignature{ { Name: "a", Type: "invalid-type", @@ -372,7 +372,7 @@ func TestAcc_RowAccessPolicy_DataTypeAliases(t *testing.T) { id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() resourceName := "snowflake_row_access_policy.test" body := "case when current_role() in ('ANALYST') then true else false end" - policyModel := model.RowAccessPolicy("test", []sdk.RowAccessPolicyArgument{ + policyModel := model.RowAccessPolicy("test", []sdk.TableColumnSignature{ { Name: "A", Type: "TEXT", @@ -391,7 +391,7 @@ func TestAcc_RowAccessPolicy_DataTypeAliases(t *testing.T) { ConfigVariables: tfconfig.ConfigVariablesFromModel(t, policyModel), Check: assert.AssertThat(t, resourceassert.RowAccessPolicyResource(t, resourceName). HasNameString(id.Name()). - HasArguments([]sdk.RowAccessPolicyArgument{ + HasArguments([]sdk.TableColumnSignature{ { Name: "A", Type: sdk.DataTypeVARCHAR, @@ -403,11 +403,11 @@ func TestAcc_RowAccessPolicy_DataTypeAliases(t *testing.T) { }) } -func TestAcc_view_migrateFromVersion_0_95_0_LowercaseArgName(t *testing.T) { +func TestAcc_RowAccessPolicy_migrateFromVersion_0_95_0_LowercaseArgName(t *testing.T) { id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() resourceName := "snowflake_row_access_policy.test" body := "case when current_role() in ('ANALYST') then true else false end" - policyModel := model.RowAccessPolicy("test", []sdk.RowAccessPolicyArgument{ + policyModel := model.RowAccessPolicy("test", []sdk.TableColumnSignature{ { Name: "A", Type: sdk.DataTypeVARCHAR, @@ -467,7 +467,7 @@ func TestAcc_view_migrateFromVersion_0_95_0_LowercaseArgName(t *testing.T) { HasSchemaString(id.SchemaName()). HasFullyQualifiedNameString(id.FullyQualifiedName()). HasBodyString(body). - HasArguments([]sdk.RowAccessPolicyArgument{ + HasArguments([]sdk.TableColumnSignature{ { Name: "A", Type: sdk.DataTypeVARCHAR, @@ -483,11 +483,11 @@ func TestAcc_view_migrateFromVersion_0_95_0_LowercaseArgName(t *testing.T) { }) } -func TestAcc_view_migrateFromVersion_0_95_0_UppercaseArgName(t *testing.T) { +func TestAcc_RowAccessPolicy_migrateFromVersion_0_95_0_UppercaseArgName(t *testing.T) { id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() resourceName := "snowflake_row_access_policy.test" body := "case when current_role() in ('ANALYST') then true else false end" - policyModel := model.RowAccessPolicy("test", []sdk.RowAccessPolicyArgument{ + policyModel := model.RowAccessPolicy("test", []sdk.TableColumnSignature{ { Name: "A", Type: sdk.DataTypeVARCHAR, @@ -544,7 +544,7 @@ func TestAcc_view_migrateFromVersion_0_95_0_UppercaseArgName(t *testing.T) { HasSchemaString(id.SchemaName()). HasFullyQualifiedNameString(id.FullyQualifiedName()). HasBodyString(body). - HasArguments([]sdk.RowAccessPolicyArgument{ + HasArguments([]sdk.TableColumnSignature{ { Name: "A", Type: sdk.DataTypeVARCHAR, diff --git a/pkg/resources/table_acceptance_test.go b/pkg/resources/table_acceptance_test.go index a855fb2a49c..f0fc3497801 100644 --- a/pkg/resources/table_acceptance_test.go +++ b/pkg/resources/table_acceptance_test.go @@ -1732,13 +1732,11 @@ resource "snowflake_masking_policy" "policy1" { name = "%[1]s1" database = "%[2]s" schema = "%[3]s" - signature { - column { - name = "val" - type = "VARCHAR" - } + argument { + name = "val" + type = "VARCHAR" } - masking_expression = "case when current_role() in ('ANALYST') then val else sha2(val, 512) end" + body = "case when current_role() in ('ANALYST') then val else sha2(val, 512) end" return_data_type = "VARCHAR(16777216)" } @@ -1746,13 +1744,11 @@ resource "snowflake_masking_policy" "policy2" { name = "%[1]s2" database = "%[2]s" schema = "%[3]s" - signature { - column { - name = "val" - type = "VARCHAR" - } + argument { + name = "val" + type = "VARCHAR" } - masking_expression = "case when current_role() in ('ANALYST') then val else sha2(val, 512) end" + body = "case when current_role() in ('ANALYST') then val else sha2(val, 512) end" return_data_type = "VARCHAR(16777216)" } @@ -2251,7 +2247,7 @@ resource "snowflake_table" "test_table" { name = "ID" type = "NUMBER(11,2)" } - + column { name = "SOME_COLUMN" type = "%[4]s" diff --git a/pkg/resources/table_column_masking_policy_application_acceptance_test.go b/pkg/resources/table_column_masking_policy_application_acceptance_test.go index cb0b2139351..25b51a3405f 100644 --- a/pkg/resources/table_column_masking_policy_application_acceptance_test.go +++ b/pkg/resources/table_column_masking_policy_application_acceptance_test.go @@ -40,13 +40,11 @@ resource "snowflake_masking_policy" "test" { name = "mypolicy" database = "%s" schema = "%s" - signature { - column { - name = "val" - type = "VARCHAR" - } + argument { + name = "val" + type = "VARCHAR" } - masking_expression = "case when current_role() in ('ANALYST') then val else sha2(val, 512) end" + body = "case when current_role() in ('ANALYST') then val else sha2(val, 512) end" return_data_type = "VARCHAR" comment = "Terraform acceptance test" } diff --git a/pkg/resources/tag_masking_policy_association_acceptance_test.go b/pkg/resources/tag_masking_policy_association_acceptance_test.go index 06d51bc637d..43da75a3a02 100644 --- a/pkg/resources/tag_masking_policy_association_acceptance_test.go +++ b/pkg/resources/tag_masking_policy_association_acceptance_test.go @@ -106,13 +106,11 @@ resource "snowflake_masking_policy" "test" { name = "%[1]v" database = "%[2]s" schema = "%[3]s" - signature { - column { - name = "val" - type = "VARCHAR" - } + argument { + name = "val" + type = "VARCHAR" } - masking_expression = "case when current_role() in ('ANALYST') then val else sha2(val, 512) end" + body = "case when current_role() in ('ANALYST') then val else sha2(val, 512) end" return_data_type = "VARCHAR(16777216)" comment = "Terraform acceptance test" } diff --git a/pkg/resources/testdata/TestAcc_MaskingPolicy/basic/test.tf b/pkg/resources/testdata/TestAcc_MaskingPolicy/basic/test.tf new file mode 100644 index 00000000000..ee95998aae9 --- /dev/null +++ b/pkg/resources/testdata/TestAcc_MaskingPolicy/basic/test.tf @@ -0,0 +1,14 @@ +resource "snowflake_masking_policy" "test" { + name = var.name + database = var.database + schema = var.schema + dynamic "argument" { + for_each = var.argument + content { + name = argument.value["name"] + type = argument.value["type"] + } + } + body = var.body + return_data_type = var.return_data_type +} diff --git a/pkg/resources/testdata/TestAcc_MaskingPolicy/basic/variables.tf b/pkg/resources/testdata/TestAcc_MaskingPolicy/basic/variables.tf new file mode 100644 index 00000000000..1efa6fb9cad --- /dev/null +++ b/pkg/resources/testdata/TestAcc_MaskingPolicy/basic/variables.tf @@ -0,0 +1,23 @@ +variable "name" { + type = string +} + +variable "database" { + type = string +} + +variable "schema" { + type = string +} + +variable "argument" { + type = set(map(string)) +} + +variable "body" { + type = string +} + +variable "return_data_type" { + type = string +} diff --git a/pkg/resources/testdata/TestAcc_MaskingPolicy/complete/test.tf b/pkg/resources/testdata/TestAcc_MaskingPolicy/complete/test.tf new file mode 100644 index 00000000000..42c89851a77 --- /dev/null +++ b/pkg/resources/testdata/TestAcc_MaskingPolicy/complete/test.tf @@ -0,0 +1,16 @@ +resource "snowflake_masking_policy" "test" { + name = var.name + database = var.database + schema = var.schema + dynamic "argument" { + for_each = var.argument + content { + name = argument.value["name"] + type = argument.value["type"] + } + } + body = var.body + return_data_type = var.return_data_type + exempt_other_policies = var.exempt_other_policies + comment = var.comment +} diff --git a/pkg/resources/testdata/TestAcc_MaskingPolicy/complete/variables.tf b/pkg/resources/testdata/TestAcc_MaskingPolicy/complete/variables.tf new file mode 100644 index 00000000000..c9b4154d34f --- /dev/null +++ b/pkg/resources/testdata/TestAcc_MaskingPolicy/complete/variables.tf @@ -0,0 +1,31 @@ +variable "name" { + type = string +} + +variable "database" { + type = string +} + +variable "schema" { + type = string +} + +variable "argument" { + type = set(map(string)) +} + +variable "body" { + type = string +} + +variable "return_data_type" { + type = string +} + +variable "exempt_other_policies" { + type = string +} + +variable "comment" { + type = string +} diff --git a/pkg/resources/testdata/TestAcc_TagMaskingPolicyAssociation/basic/test.tf b/pkg/resources/testdata/TestAcc_TagMaskingPolicyAssociation/basic/test.tf index 0851584507a..ca8364a3ea3 100644 --- a/pkg/resources/testdata/TestAcc_TagMaskingPolicyAssociation/basic/test.tf +++ b/pkg/resources/testdata/TestAcc_TagMaskingPolicyAssociation/basic/test.tf @@ -10,15 +10,13 @@ resource "snowflake_masking_policy" "test" { name = var.name database = var.database schema = var.schema - signature { - column { - name = "val" - type = "VARCHAR" - } + argument { + name = "val" + type = "VARCHAR" } - masking_expression = "case when current_role() in ('ANALYST') then val else sha2(val, 512) end" - return_data_type = "VARCHAR(16777216)" - comment = "Terraform acceptance test" + body = "case when current_role() in ('ANALYST') then val else sha2(val, 512) end" + return_data_type = "VARCHAR(16777216)" + comment = "Terraform acceptance test" } resource "snowflake_tag_masking_policy_association" "test" { diff --git a/pkg/resources/view_acceptance_test.go b/pkg/resources/view_acceptance_test.go index 9c00b770921..e643348cd12 100644 --- a/pkg/resources/view_acceptance_test.go +++ b/pkg/resources/view_acceptance_test.go @@ -521,7 +521,6 @@ func TestAcc_View_complete(t *testing.T) { t.Cleanup(projectionPolicyCleanup) maskingPolicy, maskingPolicyCleanup := acc.TestClient().MaskingPolicy.CreateMaskingPolicyWithOptions(t, - acc.TestClient().Ids.SchemaId(), []sdk.TableColumnSignature{ { Name: "One", @@ -676,7 +675,6 @@ func TestAcc_View_columns(t *testing.T) { statement := fmt.Sprintf("SELECT id, foo FROM %s", table.ID().FullyQualifiedName()) maskingPolicy, maskingPolicyCleanup := acc.TestClient().MaskingPolicy.CreateMaskingPolicyWithOptions(t, - acc.TestClient().Ids.SchemaId(), []sdk.TableColumnSignature{ { Name: "One", diff --git a/pkg/schemas/masking_policy.go b/pkg/schemas/masking_policy.go new file mode 100644 index 00000000000..95225ed1d32 --- /dev/null +++ b/pkg/schemas/masking_policy.go @@ -0,0 +1,58 @@ +package schemas + +import ( + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +// DescribeMaskingPolicySchema represents output of DESCRIBE query for the single masking policy. +var DescribeMaskingPolicySchema = map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Computed: true, + }, + "signature": { + Type: schema.TypeList, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Computed: true, + }, + "type": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + Computed: true, + }, + "return_type": { + Type: schema.TypeString, + Computed: true, + }, + "body": { + Type: schema.TypeString, + Computed: true, + }, +} + +func MaskingPolicyDescriptionToSchema(details sdk.MaskingPolicyDetails) map[string]any { + return map[string]any{ + "name": details.Name, + "signature": MaskingPolicyArgumentsToSchema(details.Signature), + "return_type": details.ReturnType, + "body": details.Body, + } +} + +func MaskingPolicyArgumentsToSchema(args []sdk.TableColumnSignature) []map[string]any { + schema := make([]map[string]any, len(args)) + for i, v := range args { + schema[i] = map[string]any{ + "name": v.Name, + "type": v.Type, + } + } + return schema +} diff --git a/pkg/schemas/row_access_policy.go b/pkg/schemas/row_access_policy.go index 578f74c8307..b137ad7174f 100644 --- a/pkg/schemas/row_access_policy.go +++ b/pkg/schemas/row_access_policy.go @@ -52,7 +52,7 @@ func RowAccessPolicyDescriptionToSchema(description sdk.RowAccessPolicyDescripti } } -func RowAccessPolicyArgumentsToSchema(args []sdk.RowAccessPolicyArgument) []map[string]any { +func RowAccessPolicyArgumentsToSchema(args []sdk.TableColumnSignature) []map[string]any { schema := make([]map[string]any, len(args)) for i, v := range args { schema[i] = map[string]any{ diff --git a/pkg/sdk/common_types.go b/pkg/sdk/common_types.go index 37a125c6330..1627ab9d2a2 100644 --- a/pkg/sdk/common_types.go +++ b/pkg/sdk/common_types.go @@ -77,6 +77,32 @@ type TableColumnSignature struct { Type DataType `ddl:"keyword"` } +// Format in database is `(column )` +// TODO(SNOW-1596962): Fully support VECTOR data type +// TODO(SNOW-1660588): Use ParseFunctionArgumentsFromString +func ParseTableColumnSignature(signature string) ([]TableColumnSignature, error) { + plainSignature := strings.ReplaceAll(signature, "(", "") + plainSignature = strings.ReplaceAll(plainSignature, ")", "") + signatureParts := strings.Split(plainSignature, ", ") + arguments := make([]TableColumnSignature, len(signatureParts)) + + for i, elem := range signatureParts { + parts := strings.Split(elem, " ") + if len(parts) < 2 { + return []TableColumnSignature{}, fmt.Errorf("expected argument name and type, got %s", elem) + } + dataType, err := ToDataType(parts[len(parts)-1]) + if err != nil { + return []TableColumnSignature{}, err + } + arguments[i] = TableColumnSignature{ + Name: strings.Join(parts[:len(parts)-1], " "), + Type: dataType, + } + } + return arguments, nil +} + type StringProperty struct { Value string DefaultValue string diff --git a/pkg/sdk/errors.go b/pkg/sdk/errors.go index ab09c984dcb..62d68675c7e 100644 --- a/pkg/sdk/errors.go +++ b/pkg/sdk/errors.go @@ -25,6 +25,7 @@ var ( // snowflake-sdk errors. ErrInvalidObjectIdentifier = NewError("invalid object identifier") ErrDifferentDatabase = NewError("database must be the same") + ErrDifferentSchema = NewError("schema must be the same") ) type IntErrType string diff --git a/pkg/sdk/masking_policy.go b/pkg/sdk/masking_policy.go index 7255c1c484b..c6b66705075 100644 --- a/pkg/sdk/masking_policy.go +++ b/pkg/sdk/masking_policy.go @@ -2,12 +2,13 @@ package sdk import ( "context" + "encoding/json" "errors" "fmt" - "strings" + "log" "time" - "github.com/buger/jsonparser" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/collections" ) var _ MaskingPolicies = (*maskingPolicies)(nil) @@ -59,6 +60,9 @@ func (opts *CreateMaskingPolicyOptions) validate() error { if !ValidObjectIdentifier(opts.name) { errs = append(errs, ErrInvalidObjectIdentifier) } + if everyValueSet(opts.OrReplace, opts.IfNotExists) && *opts.OrReplace && *opts.IfNotExists { + errs = append(errs, errOneOf("CreateMaskingPolicyOptions", "OrReplace", "IfNotExists")) + } if !valueSet(opts.signature) { errs = append(errs, errNotSet("CreateMaskingPolicyOptions", "signature")) } @@ -111,8 +115,16 @@ func (opts *AlterMaskingPolicyOptions) validate() error { if !ValidObjectIdentifier(opts.name) { errs = append(errs, ErrInvalidObjectIdentifier) } - if opts.NewName != nil && !ValidObjectIdentifier(opts.NewName) { - errs = append(errs, errInvalidIdentifier("AlterMaskingPolicyOptions", "NewName")) + if opts.NewName != nil { + if !ValidObjectIdentifier(opts.NewName) { + errs = append(errs, errInvalidIdentifier("AlterMaskingPolicyOptions", "NewName")) + } + if opts.name.DatabaseName() != opts.NewName.DatabaseName() { + errs = append(errs, ErrDifferentDatabase) + } + if opts.name.SchemaName() != opts.NewName.SchemaName() { + errs = append(errs, ErrDifferentSchema) + } } if !exactlyOneValueSet(opts.Set, opts.Unset, opts.SetTag, opts.UnsetTag, opts.NewName) { errs = append(errs, errExactlyOneOf("AlterMaskingPolicyOptions", "Set", "Unset", "SetTag", "UnsetTag", "NewName")) @@ -233,6 +245,20 @@ type MaskingPolicy struct { OwnerRoleType string } +type MaskingPolicyOptions struct { + ExemptOtherPolicies bool `json:"EXEMPT_OTHER_POLICIES"` +} + +func ParseMaskingPolicyOptions(s string) (MaskingPolicyOptions, error) { + var options MaskingPolicyOptions + err := json.Unmarshal([]byte(s), &options) + if err != nil { + return MaskingPolicyOptions{}, err + } + + return options, nil +} + func (v *MaskingPolicy) ID() SchemaObjectIdentifier { return NewSchemaObjectIdentifier(v.DatabaseName, v.SchemaName, v.Name) } @@ -255,9 +281,11 @@ type maskingPolicyDBRow struct { } func (row maskingPolicyDBRow) convert() *MaskingPolicy { - exemptOtherPolicies, err := jsonparser.GetBoolean([]byte(row.Options), "EXEMPT_OTHER_POLICIES") + options, err := ParseMaskingPolicyOptions(row.Options) if err != nil { - exemptOtherPolicies = false + log.Printf("[DEBUG] converting masking policy row: error unmarshaling options: %v", err) + log.Printf("[DEBUG] setting exempt_other_policies = false") + options.ExemptOtherPolicies = false } return &MaskingPolicy{ CreatedOn: row.CreatedOn, @@ -267,7 +295,7 @@ func (row maskingPolicyDBRow) convert() *MaskingPolicy { Kind: row.Kind, Owner: row.Owner, Comment: row.Comment, - ExemptOtherPolicies: exemptOtherPolicies, + ExemptOtherPolicies: options.ExemptOtherPolicies, OwnerRoleType: row.OwnerRoleType, } } @@ -295,13 +323,7 @@ func (v *maskingPolicies) ShowByID(ctx context.Context, id SchemaObjectIdentifie if err != nil { return nil, err } - - for _, maskingPolicy := range maskingPolicies { - if maskingPolicy.ID().name == id.Name() { - return &maskingPolicy, nil - } - } - return nil, ErrObjectNotExistOrAuthorized + return collections.FindFirst(maskingPolicies, func(r MaskingPolicy) bool { return r.Name == id.Name() }) } // describeMaskingPolicyOptions is based on https://docs.snowflake.com/en/sql-reference/sql/desc-masking-policy. @@ -346,23 +368,13 @@ func (row maskingPolicyDetailsRow) toMaskingPolicyDetails() *MaskingPolicyDetail ReturnType: dataType, Body: row.Body, } - s := strings.Trim(row.Signature, "()") - parts := strings.Split(s, ",") - for _, part := range parts { - p := strings.Split(strings.TrimSpace(part), " ") - if len(p) != 2 { - continue - } - dType, err := ToDataType(p[1]) - if err != nil { - continue - } - v.Signature = append(v.Signature, TableColumnSignature{ - Name: p[0], - Type: dType, - }) - } + signature, err := ParseTableColumnSignature(row.Signature) + if err != nil { + log.Printf("[DEBUG] parsing table column signature: %v", err) + } else { + v.Signature = signature + } return v } diff --git a/pkg/sdk/masking_policy_test.go b/pkg/sdk/masking_policy_test.go index 56e14d1cb42..1eb5c78726a 100644 --- a/pkg/sdk/masking_policy_test.go +++ b/pkg/sdk/masking_policy_test.go @@ -47,6 +47,18 @@ func TestMaskingPolicyCreate(t *testing.T) { assertOptsInvalidJoinedErrors(t, opts, errNotSet("CreateMaskingPolicyOptions", "returns")) }) + t.Run("validation: both ifNotExists and orReplace present", func(t *testing.T) { + opts := &CreateMaskingPolicyOptions{ + name: id, + signature: signature, + body: expression, + returns: DataTypeVARCHAR, + IfNotExists: Bool(true), + OrReplace: Bool(true), + } + assertOptsInvalidJoinedErrors(t, opts, errOneOf("CreateMaskingPolicyOptions", "OrReplace", "IfNotExists")) + }) + t.Run("only required options", func(t *testing.T) { opts := &CreateMaskingPolicyOptions{ name: id, @@ -63,7 +75,6 @@ func TestMaskingPolicyCreate(t *testing.T) { opts := &CreateMaskingPolicyOptions{ OrReplace: Bool(true), name: id, - IfNotExists: Bool(true), signature: signature, body: expression, returns: DataTypeVARCHAR, @@ -71,11 +82,10 @@ func TestMaskingPolicyCreate(t *testing.T) { ExemptOtherPolicies: Bool(true), } - assertOptsValidAndSQLEquals(t, opts, `CREATE OR REPLACE MASKING POLICY IF NOT EXISTS %s AS ("col1" VARCHAR, "col2" VARCHAR) RETURNS %s -> %s COMMENT = '%s' EXEMPT_OTHER_POLICIES = %t`, id.FullyQualifiedName(), DataTypeVARCHAR, expression, comment, true) + assertOptsValidAndSQLEquals(t, opts, `CREATE OR REPLACE MASKING POLICY %s AS ("col1" VARCHAR, "col2" VARCHAR) RETURNS %s -> %s COMMENT = '%s' EXEMPT_OTHER_POLICIES = %t`, id.FullyQualifiedName(), DataTypeVARCHAR, expression, comment, true) }) } -// TODO: add tests for body and tags func TestMaskingPolicyAlter(t *testing.T) { id := randomSchemaObjectIdentifier() @@ -91,6 +101,35 @@ func TestMaskingPolicyAlter(t *testing.T) { assertOptsInvalidJoinedErrors(t, opts, errExactlyOneOf("AlterMaskingPolicyOptions", "Set", "Unset", "SetTag", "UnsetTag", "NewName")) }) + t.Run("validation: incorrect identifier", func(t *testing.T) { + opts := &AlterMaskingPolicyOptions{ + name: emptySchemaObjectIdentifier, + } + assertOptsInvalidJoinedErrors(t, opts, ErrInvalidObjectIdentifier) + }) + + t.Run("validation: new name from different db", func(t *testing.T) { + newId := randomSchemaObjectIdentifier() + + opts := &AlterMaskingPolicyOptions{ + NewName: &newId, + } + assertOptsInvalidJoinedErrors(t, opts, ErrDifferentDatabase) + assertOptsInvalidJoinedErrors(t, opts, ErrDifferentSchema) + }) + + t.Run("validation: only 1 option allowed at the same time", func(t *testing.T) { + newID := randomSchemaObjectIdentifierInSchema(id.SchemaId()) + opts := &AlterMaskingPolicyOptions{ + name: id, + NewName: &newID, + Set: &MaskingPolicySet{ + Comment: String("foo"), + }, + } + assertOptsInvalidJoinedErrors(t, opts, errExactlyOneOf("AlterMaskingPolicyOptions", "Set", "Unset", "SetTag", "UnsetTag", "NewName")) + }) + t.Run("with set", func(t *testing.T) { newComment := random.Comment() opts := &AlterMaskingPolicyOptions{ @@ -120,6 +159,46 @@ func TestMaskingPolicyAlter(t *testing.T) { } assertOptsValidAndSQLEquals(t, opts, "ALTER MASKING POLICY %s RENAME TO %s", id.FullyQualifiedName(), newID.FullyQualifiedName()) }) + + t.Run("set body", func(t *testing.T) { + opts := &AlterMaskingPolicyOptions{ + name: id, + Set: &MaskingPolicySet{ + Body: Pointer("body"), + }, + } + assertOptsValidAndSQLEquals(t, opts, "ALTER MASKING POLICY %s SET BODY -> body", id.FullyQualifiedName()) + }) + + t.Run("set tags", func(t *testing.T) { + opts := &AlterMaskingPolicyOptions{ + name: id, + IfExists: Pointer(true), + SetTag: []TagAssociation{ + { + Name: NewAccountObjectIdentifier("123"), + Value: "value-123", + }, + { + Name: NewAccountObjectIdentifier("456"), + Value: "value-123", + }, + }, + } + assertOptsValidAndSQLEquals(t, opts, `ALTER MASKING POLICY IF EXISTS %s SET TAG "123" = 'value-123', "456" = 'value-123'`, id.FullyQualifiedName()) + }) + + t.Run("unset tags", func(t *testing.T) { + opts := &AlterMaskingPolicyOptions{ + name: id, + IfExists: Pointer(true), + UnsetTag: []ObjectIdentifier{ + NewAccountObjectIdentifier("123"), + NewAccountObjectIdentifier("456"), + }, + } + assertOptsValidAndSQLEquals(t, opts, `ALTER MASKING POLICY IF EXISTS %s UNSET TAG "123", "456"`, id.FullyQualifiedName()) + }) } func TestMaskingPolicyDrop(t *testing.T) { @@ -130,6 +209,13 @@ func TestMaskingPolicyDrop(t *testing.T) { assertOptsInvalidJoinedErrors(t, opts, ErrInvalidObjectIdentifier) }) + t.Run("validation: incorrect identifier", func(t *testing.T) { + opts := &DropMaskingPolicyOptions{ + name: emptySchemaObjectIdentifier, + } + assertOptsInvalidJoinedErrors(t, opts, ErrInvalidObjectIdentifier) + }) + t.Run("only name", func(t *testing.T) { opts := &DropMaskingPolicyOptions{ name: id, diff --git a/pkg/sdk/row_access_policies_gen.go b/pkg/sdk/row_access_policies_gen.go index 64ba7c40963..7e4269f4334 100644 --- a/pkg/sdk/row_access_policies_gen.go +++ b/pkg/sdk/row_access_policies_gen.go @@ -107,11 +107,7 @@ type describeRowAccessPolicyDBRow struct { type RowAccessPolicyDescription struct { Name string - Signature []RowAccessPolicyArgument + Signature []TableColumnSignature ReturnType string Body string } -type RowAccessPolicyArgument struct { - Name string - Type DataType -} diff --git a/pkg/sdk/row_access_policies_gen_test.go b/pkg/sdk/row_access_policies_gen_test.go index c45d5da68b1..944a91e8f98 100644 --- a/pkg/sdk/row_access_policies_gen_test.go +++ b/pkg/sdk/row_access_policies_gen_test.go @@ -254,16 +254,16 @@ func TestRowAccessPolicies_Describe(t *testing.T) { }) } -func TestRowAccessPolicyDescription_Arguments(t *testing.T) { +func TestRowAccessPolicyDescription_Signature(t *testing.T) { tests := []struct { name string signature string - want []RowAccessPolicyArgument + want []TableColumnSignature }{ { name: "signature with 1 arg", signature: "(A VARCHAR)", - want: []RowAccessPolicyArgument{ + want: []TableColumnSignature{ { Name: "A", Type: "VARCHAR", @@ -273,7 +273,7 @@ func TestRowAccessPolicyDescription_Arguments(t *testing.T) { { name: "signature with multiple args", signature: "(A VARCHAR, B BOOLEAN)", - want: []RowAccessPolicyArgument{ + want: []TableColumnSignature{ { Name: "A", Type: "VARCHAR", @@ -287,7 +287,7 @@ func TestRowAccessPolicyDescription_Arguments(t *testing.T) { { name: "signature with complex name", signature: "(a B VARCHAR)", - want: []RowAccessPolicyArgument{ + want: []TableColumnSignature{ { Name: "a B", Type: "VARCHAR", diff --git a/pkg/sdk/row_access_policies_impl_gen.go b/pkg/sdk/row_access_policies_impl_gen.go index d02092e9cd3..f6f0f1e9053 100644 --- a/pkg/sdk/row_access_policies_impl_gen.go +++ b/pkg/sdk/row_access_policies_impl_gen.go @@ -3,7 +3,6 @@ package sdk import ( "context" "log" - "strings" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/collections" ) @@ -138,31 +137,11 @@ func (r describeRowAccessPolicyDBRow) convert() *RowAccessPolicyDescription { ReturnType: r.ReturnType, Body: r.Body, } - // Format in database is `(column )` - // TODO(SNOW-1596962): Fully support VECTOR data type - // TODO(SNOW-1660588): Use ParseFunctionArgumentsFromString - plainSignature := strings.ReplaceAll(r.Signature, "(", "") - plainSignature = strings.ReplaceAll(plainSignature, ")", "") - signatureParts := strings.Split(plainSignature, ", ") - arguments := make([]RowAccessPolicyArgument, len(signatureParts)) - - for i, e := range signatureParts { - parts := strings.Split(e, " ") - if len(parts) < 2 { - log.Printf("[DEBUG] parsing policy arguments: expected argument name and type, got %s", e) - continue - } - dataType, err := ToDataType(parts[len(parts)-1]) - if err != nil { - log.Printf("[DEBUG] converting row access policy db row: invalid data type %s", dataType) - continue - } - arguments[i] = RowAccessPolicyArgument{ - Name: strings.Join(parts[:len(parts)-1], " "), - Type: dataType, - } + signature, err := ParseTableColumnSignature(r.Signature) + if err != nil { + log.Printf("[DEBUG] parsing table column signature: %v", err) + } else { + rowAccessPolicyDescription.Signature = signature } - rowAccessPolicyDescription.Signature = arguments - return rowAccessPolicyDescription } diff --git a/pkg/sdk/testint/masking_policy_integration_test.go b/pkg/sdk/testint/masking_policy_integration_test.go index 85aa2b6505c..98f5c780f5b 100644 --- a/pkg/sdk/testint/masking_policy_integration_test.go +++ b/pkg/sdk/testint/masking_policy_integration_test.go @@ -66,20 +66,17 @@ func TestInt_MaskingPoliciesShow(t *testing.T) { assert.Equal(t, 0, len(maskingPolicies)) }) - /* - // there appears to be a bug in the Snowflake API. LIMIT is not actually limiting the number of results - t.Run("when limiting the number of results", func(t *testing.T) { - showOptions := &MaskingPolicyShowOptions{ - In: &In{ - Schema: schemaTest.ID(), - }, - Limit: Int(1), - } - maskingPolicies, err := client.MaskingPolicies.Show(ctx, showOptions) - require.NoError(t, err) - assert.Equal(t, 1, len(maskingPolicies)) - }) - */ + t.Run("when limiting the number of results", func(t *testing.T) { + showOptions := &sdk.ShowMaskingPolicyOptions{ + In: &sdk.In{ + Schema: testClientHelper().Ids.SchemaId(), + }, + Limit: sdk.Int(1), + } + maskingPolicies, err := client.MaskingPolicies.Show(ctx, showOptions) + require.NoError(t, err) + assert.Equal(t, 1, len(maskingPolicies)) + }) } func TestInt_MaskingPolicyCreate(t *testing.T) { @@ -361,6 +358,24 @@ func TestInt_MaskingPolicyAlter(t *testing.T) { _, err = client.SystemFunctions.GetTag(ctx, tag.ID(), id, sdk.ObjectTypeMaskingPolicy) assert.Error(t, err) }) + + t.Run("set body", func(t *testing.T) { + maskingPolicy, maskingPolicyCleanup := testClientHelper().MaskingPolicy.CreateMaskingPolicy(t) + id := maskingPolicy.ID() + newBody := "'***'" + t.Cleanup(maskingPolicyCleanup) + + alterOptions := &sdk.AlterMaskingPolicyOptions{ + Set: &sdk.MaskingPolicySet{ + Body: sdk.Pointer(newBody), + }, + } + err := client.MaskingPolicies.Alter(ctx, id, alterOptions) + require.NoError(t, err) + maskingPolicyDetails, err := client.MaskingPolicies.Describe(ctx, id) + require.NoError(t, err) + assert.Equal(t, newBody, maskingPolicyDetails.Body) + }) } func TestInt_MaskingPolicyDrop(t *testing.T) { diff --git a/pkg/sdk/testint/row_access_policies_gen_integration_test.go b/pkg/sdk/testint/row_access_policies_gen_integration_test.go index 516b5c4787a..577bf14ef70 100644 --- a/pkg/sdk/testint/row_access_policies_gen_integration_test.go +++ b/pkg/sdk/testint/row_access_policies_gen_integration_test.go @@ -27,7 +27,7 @@ func TestInt_RowAccessPolicies(t *testing.T) { assert.Equal(t, "ROLE", rowAccessPolicy.OwnerRoleType) } - assertRowAccessPolicyDescription := func(t *testing.T, rowAccessPolicyDescription *sdk.RowAccessPolicyDescription, id sdk.SchemaObjectIdentifier, signature []sdk.RowAccessPolicyArgument, expectedBody string) { + assertRowAccessPolicyDescription := func(t *testing.T, rowAccessPolicyDescription *sdk.RowAccessPolicyDescription, id sdk.SchemaObjectIdentifier, signature []sdk.TableColumnSignature, expectedBody string) { t.Helper() assert.Equal(t, sdk.RowAccessPolicyDescription{ Name: id.Name(), @@ -250,7 +250,7 @@ func TestInt_RowAccessPolicies(t *testing.T) { returnedRowAccessPolicyDescription, err := client.RowAccessPolicies.Describe(ctx, rowAccessPolicy.ID()) require.NoError(t, err) - assertRowAccessPolicyDescription(t, returnedRowAccessPolicyDescription, rowAccessPolicy.ID(), []sdk.RowAccessPolicyArgument{{ + assertRowAccessPolicyDescription(t, returnedRowAccessPolicyDescription, rowAccessPolicy.ID(), []sdk.TableColumnSignature{{ Name: argName, Type: argType, }}, body) @@ -269,7 +269,7 @@ func TestInt_RowAccessPolicies(t *testing.T) { returnedRowAccessPolicyDescription, err := client.RowAccessPolicies.Describe(ctx, rowAccessPolicy.ID()) require.NoError(t, err) - assertRowAccessPolicyDescription(t, returnedRowAccessPolicyDescription, rowAccessPolicy.ID(), []sdk.RowAccessPolicyArgument{{ + assertRowAccessPolicyDescription(t, returnedRowAccessPolicyDescription, rowAccessPolicy.ID(), []sdk.TableColumnSignature{{ Name: argName, Type: sdk.DataTypeTimestampNTZ, }}, body) @@ -288,7 +288,7 @@ func TestInt_RowAccessPolicies(t *testing.T) { returnedRowAccessPolicyDescription, err := client.RowAccessPolicies.Describe(ctx, rowAccessPolicy.ID()) require.NoError(t, err) - assertRowAccessPolicyDescription(t, returnedRowAccessPolicyDescription, rowAccessPolicy.ID(), []sdk.RowAccessPolicyArgument{{ + assertRowAccessPolicyDescription(t, returnedRowAccessPolicyDescription, rowAccessPolicy.ID(), []sdk.TableColumnSignature{{ Name: argName, Type: sdk.DataTypeVARCHAR, }}, body) @@ -377,11 +377,11 @@ func TestInt_RowAccessPoliciesDescribe(t *testing.T) { assert.Equal(t, id.Name(), policyDetails.Name) assert.Equal(t, "BOOLEAN", policyDetails.ReturnType) require.NoError(t, err) - wantArgs := make([]sdk.RowAccessPolicyArgument, len(args)) + wantArgs := make([]sdk.TableColumnSignature, len(args)) for i, arg := range args { dataType, err := sdk.ToDataType(string(arg.Type)) require.NoError(t, err) - wantArgs[i] = sdk.RowAccessPolicyArgument{ + wantArgs[i] = sdk.TableColumnSignature{ Name: arg.Name, Type: dataType, } diff --git a/v1-preparations/ESSENTIAL_GA_OBJECTS.MD b/v1-preparations/ESSENTIAL_GA_OBJECTS.MD index eda2d5c161f..46d0122e53e 100644 --- a/v1-preparations/ESSENTIAL_GA_OBJECTS.MD +++ b/v1-preparations/ESSENTIAL_GA_OBJECTS.MD @@ -24,9 +24,9 @@ newer provider versions. We will address these while working on the given object | USER | 🚀 | issues in the older versions: [resources](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues?q=label%3Aresource%3Auser+) and [datasources](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues?q=label%3Adata_source%3Ausers+) | | WAREHOUSE | 🚀 | issues in the older versions: [resources](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues?q=label%3Aresource%3Awarehouse+) and [datasources](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues?q=label%3Adata_source%3Awarehouses+) | | FUNCTION | 👨‍💻 | [2859](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2859), [#2735](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2735), [#2426](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2426), [#1479](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1479), [#1393](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1393), [#1208](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1208), [#1079](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1079) | -| MASKING POLICY | ❌ | [#2236](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2236), [#2035](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2035), [#1799](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1799), [#1764](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1764), [#1656](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1656), [#1444](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1444), [#1422](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1422), [#1097](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1097) | +| MASKING POLICY | 👨‍💻 | [#2236](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2236), [#2035](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2035), [#1799](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1799), [#1764](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1764), [#1656](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1656), [#1444](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1444), [#1422](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1422), [#1097](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1097) | | PROCEDURE | 👨‍💻 | [#2735](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2735), [#2623](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2623), [#2257](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2257), [#2146](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2146), [#1855](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1855), [#1695](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1695), [#1640](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1640), [#1195](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1195), [#1189](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1189), [#1178](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1178), [#1050](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1050) | -| ROW ACCESS POLICY | 👨‍💻 | [#2053](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2053), [#1600](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1600), [#1151](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1151) | +| ROW ACCESS POLICY | 🚀 | [#2053](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2053), [#1600](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1600), [#1151](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1151) | | SCHEMA | 🚀 | issues in the older versions: [resources](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues?q=label%3Aresource%3Aschema+) and [datasources](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues?q=label%3Adata_source%3Aschemas+) | | STAGE | ❌ | [#2995](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2995), [#2818](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2818), [#2505](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2505), [#1911](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1911), [#1903](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1903), [#1795](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1795), [#1705](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1705), [#1544](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1544), [#1491](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1491), [#1087](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1087), [#265](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/265) | | STREAM | ❌ | [#2975](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2975), [#2413](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2413), [#2201](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2201), [#1150](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1150) |