From 6c420a49d6d485af56abcc27dadf287d9c22e38b Mon Sep 17 00:00:00 2001 From: Jakub Michalak Date: Fri, 13 Sep 2024 11:02:38 +0200 Subject: [PATCH] feat: Row access policy resource v1 (#3063) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - add show and desc output - rename fields - change signature to arguments - implement renaming - fix permadiff on body - adjust identifiers handling - adjust examples - gen resource asserts (config builders are not working because we have a required list argument) - improve handling data types - move parsing signature to sdk - support proper casing in arg names ## Test Plan * [x] acceptance tests * [ ] … ## References https://docs.snowflake.com/en/sql-reference/sql/create-row-access-policy https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2053 https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1151 ## TODO (next PR) - rework data source --- MIGRATION_GUIDE.md | 41 +- docs/resources/row_access_policy.md | 70 ++- .../snowflake_row_access_policy/import.sh | 3 +- .../snowflake_row_access_policy/resource.tf | 17 +- .../assert/objectassert/gen/sdk_object_def.go | 5 + .../row_access_policy_snowflake_gen.go | 130 ++++ .../resourceassert/gen/resource_schema_def.go | 4 + .../row_access_policy_resource_ext.go | 18 + .../row_access_policy_resource_gen.go | 107 ++++ .../row_access_policy_show_output_ext.go | 10 + .../row_access_policy_show_output_gen.go | 86 +++ .../model/row_access_policy_model_ext.go | 19 + .../model/row_access_policy_model_gen.go | 135 +++++ .../helpers/row_access_policy_client.go | 38 +- pkg/resources/doc_helpers.go | 4 + pkg/resources/row_access_policy.go | 291 ++++++--- .../row_access_policy_acceptance_test.go | 559 ++++++++++++++++-- .../row_access_policy_state_upgraders.go | 28 + .../TestAcc_RowAccessPolicy/1/test.tf | 11 - .../TestAcc_RowAccessPolicy/2/test.tf | 11 - .../TestAcc_RowAccessPolicy/2/variables.tf | 11 - .../TestAcc_RowAccessPolicy/3/test.tf | 11 - .../TestAcc_RowAccessPolicy/3/variables.tf | 11 - .../TestAcc_RowAccessPolicy/basic/test.tf | 13 + .../{1 => basic}/variables.tf | 8 + .../TestAcc_RowAccessPolicy/complete/test.tf | 14 + .../complete/variables.tf | 23 + pkg/schemas/row_access_policy.go | 45 ++ pkg/sdk/row_access_policies_gen.go | 30 +- pkg/sdk/row_access_policies_gen_test.go | 88 ++- ...ow_access_policies_gen_integration_test.go | 171 +++--- templates/resources/row_access_policy.md.tmpl | 35 ++ 32 files changed, 1756 insertions(+), 291 deletions(-) create mode 100644 pkg/acceptance/bettertestspoc/assert/objectassert/row_access_policy_snowflake_gen.go create mode 100644 pkg/acceptance/bettertestspoc/assert/resourceassert/row_access_policy_resource_ext.go create mode 100644 pkg/acceptance/bettertestspoc/assert/resourceassert/row_access_policy_resource_gen.go create mode 100644 pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert/row_access_policy_show_output_ext.go create mode 100644 pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert/row_access_policy_show_output_gen.go create mode 100644 pkg/acceptance/bettertestspoc/config/model/row_access_policy_model_ext.go create mode 100644 pkg/acceptance/bettertestspoc/config/model/row_access_policy_model_gen.go create mode 100644 pkg/resources/row_access_policy_state_upgraders.go delete mode 100644 pkg/resources/testdata/TestAcc_RowAccessPolicy/1/test.tf delete mode 100644 pkg/resources/testdata/TestAcc_RowAccessPolicy/2/test.tf delete mode 100644 pkg/resources/testdata/TestAcc_RowAccessPolicy/2/variables.tf delete mode 100644 pkg/resources/testdata/TestAcc_RowAccessPolicy/3/test.tf delete mode 100644 pkg/resources/testdata/TestAcc_RowAccessPolicy/3/variables.tf create mode 100644 pkg/resources/testdata/TestAcc_RowAccessPolicy/basic/test.tf rename pkg/resources/testdata/TestAcc_RowAccessPolicy/{1 => basic}/variables.tf (56%) create mode 100644 pkg/resources/testdata/TestAcc_RowAccessPolicy/complete/test.tf create mode 100644 pkg/resources/testdata/TestAcc_RowAccessPolicy/complete/variables.tf create mode 100644 pkg/schemas/row_access_policy.go create mode 100644 templates/resources/row_access_policy.md.tmpl diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 974e1789f7b..9a933195c29 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -4,6 +4,39 @@ This document is meant to help you migrate your Terraform config to the new newe describe deprecations or breaking changes and help you to change your configuration to keep the same (or similar) behavior across different versions. +## v0.95.0 ➞ v0.96.0 +### snowflake_row_access_policy resource changes +New fields: + - `show_output` field that holds the response from SHOW ROW ACCESS POLICIES. + - `describe_output` field that holds the response from DESCRIBE ROW ACCESS POLICY. + +#### *(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 +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. + +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 +Previously, after changing `name` field, the resource was recreated. Now, the object is renamed with `RENAME TO`. + +#### *(breaking change)* Mitigating permadiff on `body` +Previously, `body` of a policy was compared as a raw string. This led to permament diff because of leading newlines (see https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2053). + +Now, similarly to handling statements in other resources, we replace blank characters with a space. The provider can cause false positives in cases where a change in case or run of whitespace is semantically significant. + +#### *(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. + ## v0.94.x ➞ v0.95.0 ### *(breaking change)* database roles data source; field rename, schema structure changes, and adding missing filtering options @@ -37,7 +70,7 @@ New output fields Breaking changes: - `database` and `schema` are right now under `in` field -- `views` field now organizes output of show under `show_output` field and the output of describe under `describe_output` field. +- `views` field now organizes output of show under `show_output` field and the output of describe under `describe_output` field. ### snowflake_view resource changes New fields: @@ -133,7 +166,7 @@ Because of introducing a new `fully_qualified_name` field for all of the resourc Correctly handle the situation when stage was rename/deleted externally (earlier it resulted in a permanent loop). No action is required on the user's side. -Connected issues: [#2972](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2972) +Connected issues: [#2972](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2972) ### snowflake_table resource changes @@ -289,8 +322,8 @@ Type changes: - `disabled`: bool -> string (To easily handle three-value logic (true, false, unknown) in provider's configs, read more in https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/751239b7d2fee4757471db6c03b952d4728ee099/v1-preparations/CHANGES_BEFORE_V1.md?plain=1#L24) #### *(breaking change)* refactored snowflake_users datasource -> **IMPORTANT NOTE:** when querying users you don't have permissions to, the querying options are limited. -You won't get almost any field in `show_output` (only empty or default values), the DESCRIBE command cannot be called, so you have to set `with_describe = false`. +> **IMPORTANT NOTE:** when querying users you don't have permissions to, the querying options are limited. +You won't get almost any field in `show_output` (only empty or default values), the DESCRIBE command cannot be called, so you have to set `with_describe = false`. Only `parameters` output is not affected by the lack of privileges. Changes: diff --git a/docs/resources/row_access_policy.md b/docs/resources/row_access_policy.md index f7339bd879d..17173abf53b 100644 --- a/docs/resources/row_access_policy.md +++ b/docs/resources/row_access_policy.md @@ -5,6 +5,8 @@ description: |- --- +!> **V1 release candidate** This resource was reworked and is a release candidate for the V1. We do not expect significant changes in it before the V1. We will welcome any feedback and adjust the resource if needed. Any errors reported will be resolved with a higher priority. We encourage checking this resource out before the V1 release. Please follow the [migration guide](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/MIGRATION_GUIDE.md#v0950--v0960) to use it. + # snowflake_row_access_policy (Resource) @@ -16,14 +18,22 @@ resource "snowflake_row_access_policy" "example_row_access_policy" { name = "EXAMPLE_ROW_ACCESS_POLICY" database = "EXAMPLE_DB" schema = "EXAMPLE_SCHEMA" - signature = { - A = "VARCHAR", - B = "VARCHAR" + argument { + name = "ARG1" + type = "VARCHAR" + } + argument { + name = "ARG2" + type = "NUMBER" } - row_access_expression = "case when current_role() in ('ANALYST') then true else false end" + argument { + name = "ARG3" + type = "TIMESTAMP_NTZ" + } + body = "case when current_role() in ('ANALYST') then true else false end" + comment = "comment" } ``` - -> **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). @@ -32,11 +42,11 @@ resource "snowflake_row_access_policy" "example_row_access_policy" { ### Required -- `database` (String) The database in which to create the row access policy. -- `name` (String) Specifies the identifier for the row access policy; must be unique for the database and schema in which the row access policy is created. -- `row_access_expression` (String) Specifies the SQL expression. The expression can be any boolean-valued SQL expression. -- `schema` (String) The schema in which to create the row access policy. -- `signature` (Map of String) Specifies signature (arguments) for the row access policy (uppercase and sorted to avoid recreation of resource). A signature specifies a set of attributes that must be considered to determine whether the row is accessible. The attribute values come from the database object (e.g. table or view) to be protected by the row access policy. +- `argument` (Block List, Min: 1) List of the arguments for the row access policy. A signature specifies a set of attributes that must be considered to determine whether the row is accessible. The attribute values come from the database object (e.g. table or view) to be protected by the row access policy. If any argument name or type is changed, the resource is recreated. (see [below for nested schema](#nestedblock--argument)) +- `body` (String) Specifies the SQL expression. The expression can be any boolean-valued SQL expression. 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 row access 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 row access policy; must be unique for the database and schema in which the row access 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: `|`, `.`, `(`, `)`, `"` +- `schema` (String) The schema in which to create the row access 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 @@ -44,14 +54,50 @@ resource "snowflake_row_access_policy" "example_row_access_policy" { ### Read-Only +- `describe_output` (List of Object) Outputs the result of `DESCRIBE ROW ACCESS POLICY` for the given row access 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 ROW ACCESS POLICY` for the given row access policy. (see [below for nested schema](#nestedatt--show_output)) + + +### Nested Schema for `argument` + +Required: + +- `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 `describe_output` + +Read-Only: + +- `body` (String) +- `name` (String) +- `return_type` (String) +- `signature` (String) + + + +### Nested Schema for `show_output` + +Read-Only: + +- `comment` (String) +- `created_on` (String) +- `database_name` (String) +- `kind` (String) +- `name` (String) +- `options` (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_row_access_policy.example 'dbName|schemaName|policyName' +terraform import snowflake_row_access_policy.example '""."".""' ``` diff --git a/examples/resources/snowflake_row_access_policy/import.sh b/examples/resources/snowflake_row_access_policy/import.sh index e314b353a6a..2bf4d01b96c 100644 --- a/examples/resources/snowflake_row_access_policy/import.sh +++ b/examples/resources/snowflake_row_access_policy/import.sh @@ -1,2 +1 @@ -# format is database name | schema name | policy name -terraform import snowflake_row_access_policy.example 'dbName|schemaName|policyName' +terraform import snowflake_row_access_policy.example '""."".""' diff --git a/examples/resources/snowflake_row_access_policy/resource.tf b/examples/resources/snowflake_row_access_policy/resource.tf index a3f68a570a9..c4ff60b7beb 100644 --- a/examples/resources/snowflake_row_access_policy/resource.tf +++ b/examples/resources/snowflake_row_access_policy/resource.tf @@ -2,9 +2,18 @@ resource "snowflake_row_access_policy" "example_row_access_policy" { name = "EXAMPLE_ROW_ACCESS_POLICY" database = "EXAMPLE_DB" schema = "EXAMPLE_SCHEMA" - signature = { - A = "VARCHAR", - B = "VARCHAR" + argument { + name = "ARG1" + type = "VARCHAR" } - row_access_expression = "case when current_role() in ('ANALYST') then true else false end" + argument { + name = "ARG2" + type = "NUMBER" + } + argument { + name = "ARG3" + type = "TIMESTAMP_NTZ" + } + body = "case when current_role() in ('ANALYST') then true else false end" + comment = "comment" } 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 2ad877df711..019edbaca23 100644 --- a/pkg/acceptance/bettertestspoc/assert/objectassert/gen/sdk_object_def.go +++ b/pkg/acceptance/bettertestspoc/assert/objectassert/gen/sdk_object_def.go @@ -22,6 +22,11 @@ var allStructs = []SdkObjectDef{ ObjectType: sdk.ObjectTypeDatabaseRole, ObjectStruct: sdk.DatabaseRole{}, }, + { + IdType: "sdk.SchemaObjectIdentifier", + ObjectType: sdk.ObjectTypeRowAccessPolicy, + ObjectStruct: sdk.RowAccessPolicy{}, + }, { IdType: "sdk.AccountObjectIdentifier", ObjectType: sdk.ObjectTypeUser, diff --git a/pkg/acceptance/bettertestspoc/assert/objectassert/row_access_policy_snowflake_gen.go b/pkg/acceptance/bettertestspoc/assert/objectassert/row_access_policy_snowflake_gen.go new file mode 100644 index 00000000000..285ad82d0f2 --- /dev/null +++ b/pkg/acceptance/bettertestspoc/assert/objectassert/row_access_policy_snowflake_gen.go @@ -0,0 +1,130 @@ +// Code generated by assertions generator; DO NOT EDIT. + +package objectassert + +import ( + "fmt" + "testing" + + 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 RowAccessPolicyAssert struct { + *assert.SnowflakeObjectAssert[sdk.RowAccessPolicy, sdk.SchemaObjectIdentifier] +} + +func RowAccessPolicy(t *testing.T, id sdk.SchemaObjectIdentifier) *RowAccessPolicyAssert { + t.Helper() + return &RowAccessPolicyAssert{ + assert.NewSnowflakeObjectAssertWithProvider(sdk.ObjectTypeRowAccessPolicy, id, acc.TestClient().RowAccessPolicy.Show), + } +} + +func RowAccessPolicyFromObject(t *testing.T, rowAccessPolicy *sdk.RowAccessPolicy) *RowAccessPolicyAssert { + t.Helper() + return &RowAccessPolicyAssert{ + assert.NewSnowflakeObjectAssertWithObject(sdk.ObjectTypeRowAccessPolicy, rowAccessPolicy.ID(), rowAccessPolicy), + } +} + +func (r *RowAccessPolicyAssert) HasCreatedOn(expected string) *RowAccessPolicyAssert { + r.AddAssertion(func(t *testing.T, o *sdk.RowAccessPolicy) error { + t.Helper() + if o.CreatedOn != expected { + return fmt.Errorf("expected created on: %v; got: %v", expected, o.CreatedOn) + } + return nil + }) + return r +} + +func (r *RowAccessPolicyAssert) HasName(expected string) *RowAccessPolicyAssert { + r.AddAssertion(func(t *testing.T, o *sdk.RowAccessPolicy) error { + t.Helper() + if o.Name != expected { + return fmt.Errorf("expected name: %v; got: %v", expected, o.Name) + } + return nil + }) + return r +} + +func (r *RowAccessPolicyAssert) HasDatabaseName(expected string) *RowAccessPolicyAssert { + r.AddAssertion(func(t *testing.T, o *sdk.RowAccessPolicy) error { + t.Helper() + if o.DatabaseName != expected { + return fmt.Errorf("expected database name: %v; got: %v", expected, o.DatabaseName) + } + return nil + }) + return r +} + +func (r *RowAccessPolicyAssert) HasSchemaName(expected string) *RowAccessPolicyAssert { + r.AddAssertion(func(t *testing.T, o *sdk.RowAccessPolicy) error { + t.Helper() + if o.SchemaName != expected { + return fmt.Errorf("expected schema name: %v; got: %v", expected, o.SchemaName) + } + return nil + }) + return r +} + +func (r *RowAccessPolicyAssert) HasKind(expected string) *RowAccessPolicyAssert { + r.AddAssertion(func(t *testing.T, o *sdk.RowAccessPolicy) error { + t.Helper() + if o.Kind != expected { + return fmt.Errorf("expected kind: %v; got: %v", expected, o.Kind) + } + return nil + }) + return r +} + +func (r *RowAccessPolicyAssert) HasOwner(expected string) *RowAccessPolicyAssert { + r.AddAssertion(func(t *testing.T, o *sdk.RowAccessPolicy) error { + t.Helper() + if o.Owner != expected { + return fmt.Errorf("expected owner: %v; got: %v", expected, o.Owner) + } + return nil + }) + return r +} + +func (r *RowAccessPolicyAssert) HasComment(expected string) *RowAccessPolicyAssert { + r.AddAssertion(func(t *testing.T, o *sdk.RowAccessPolicy) error { + t.Helper() + if o.Comment != expected { + return fmt.Errorf("expected comment: %v; got: %v", expected, o.Comment) + } + return nil + }) + return r +} + +func (r *RowAccessPolicyAssert) HasOptions(expected string) *RowAccessPolicyAssert { + r.AddAssertion(func(t *testing.T, o *sdk.RowAccessPolicy) error { + t.Helper() + if o.Options != expected { + return fmt.Errorf("expected options: %v; got: %v", expected, o.Options) + } + return nil + }) + return r +} + +func (r *RowAccessPolicyAssert) HasOwnerRoleType(expected string) *RowAccessPolicyAssert { + r.AddAssertion(func(t *testing.T, o *sdk.RowAccessPolicy) error { + t.Helper() + if o.OwnerRoleType != expected { + return fmt.Errorf("expected owner role type: %v; got: %v", expected, o.OwnerRoleType) + } + return nil + }) + return r +} 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 ab4b7bb538d..e805b4c30a3 100644 --- a/pkg/acceptance/bettertestspoc/assert/resourceassert/gen/resource_schema_def.go +++ b/pkg/acceptance/bettertestspoc/assert/resourceassert/gen/resource_schema_def.go @@ -41,4 +41,8 @@ var allResourceSchemaDefs = []ResourceSchemaDef{ name: "ResourceMonitor", schema: resources.ResourceMonitor().Schema, }, + { + name: "RowAccessPolicy", + schema: resources.RowAccessPolicy().Schema, + }, } 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 new file mode 100644 index 00000000000..725e0012eac --- /dev/null +++ b/pkg/acceptance/bettertestspoc/assert/resourceassert/row_access_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 *RowAccessPolicyResourceAssert) HasArguments(args []sdk.RowAccessPolicyArgument) *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)) + r.AddAssertion(assert.ValueSet(fmt.Sprintf("argument.%d.type", i), v.Type)) + } + return r +} diff --git a/pkg/acceptance/bettertestspoc/assert/resourceassert/row_access_policy_resource_gen.go b/pkg/acceptance/bettertestspoc/assert/resourceassert/row_access_policy_resource_gen.go new file mode 100644 index 00000000000..a73404790bd --- /dev/null +++ b/pkg/acceptance/bettertestspoc/assert/resourceassert/row_access_policy_resource_gen.go @@ -0,0 +1,107 @@ +// Code generated by assertions generator; DO NOT EDIT. + +package resourceassert + +import ( + "testing" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert" +) + +type RowAccessPolicyResourceAssert struct { + *assert.ResourceAssert +} + +func RowAccessPolicyResource(t *testing.T, name string) *RowAccessPolicyResourceAssert { + t.Helper() + + return &RowAccessPolicyResourceAssert{ + ResourceAssert: assert.NewResourceAssert(name, "resource"), + } +} + +func ImportedRowAccessPolicyResource(t *testing.T, id string) *RowAccessPolicyResourceAssert { + t.Helper() + + return &RowAccessPolicyResourceAssert{ + ResourceAssert: assert.NewImportedResourceAssert(id, "imported resource"), + } +} + +/////////////////////////////////// +// Attribute value string checks // +/////////////////////////////////// + +func (r *RowAccessPolicyResourceAssert) HasArgumentString(expected string) *RowAccessPolicyResourceAssert { + r.AddAssertion(assert.ValueSet("argument", expected)) + return r +} + +func (r *RowAccessPolicyResourceAssert) HasBodyString(expected string) *RowAccessPolicyResourceAssert { + r.AddAssertion(assert.ValueSet("body", expected)) + return r +} + +func (r *RowAccessPolicyResourceAssert) HasCommentString(expected string) *RowAccessPolicyResourceAssert { + r.AddAssertion(assert.ValueSet("comment", expected)) + return r +} + +func (r *RowAccessPolicyResourceAssert) HasDatabaseString(expected string) *RowAccessPolicyResourceAssert { + r.AddAssertion(assert.ValueSet("database", expected)) + return r +} + +func (r *RowAccessPolicyResourceAssert) HasFullyQualifiedNameString(expected string) *RowAccessPolicyResourceAssert { + r.AddAssertion(assert.ValueSet("fully_qualified_name", expected)) + return r +} + +func (r *RowAccessPolicyResourceAssert) HasNameString(expected string) *RowAccessPolicyResourceAssert { + r.AddAssertion(assert.ValueSet("name", expected)) + return r +} + +func (r *RowAccessPolicyResourceAssert) HasSchemaString(expected string) *RowAccessPolicyResourceAssert { + r.AddAssertion(assert.ValueSet("schema", expected)) + return r +} + +//////////////////////////// +// Attribute empty checks // +//////////////////////////// + +func (r *RowAccessPolicyResourceAssert) HasNoArgument() *RowAccessPolicyResourceAssert { + r.AddAssertion(assert.ValueNotSet("argument")) + return r +} + +func (r *RowAccessPolicyResourceAssert) HasNoBody() *RowAccessPolicyResourceAssert { + r.AddAssertion(assert.ValueNotSet("body")) + return r +} + +func (r *RowAccessPolicyResourceAssert) HasNoComment() *RowAccessPolicyResourceAssert { + r.AddAssertion(assert.ValueNotSet("comment")) + return r +} + +func (r *RowAccessPolicyResourceAssert) HasNoDatabase() *RowAccessPolicyResourceAssert { + r.AddAssertion(assert.ValueNotSet("database")) + return r +} + +func (r *RowAccessPolicyResourceAssert) HasNoFullyQualifiedName() *RowAccessPolicyResourceAssert { + r.AddAssertion(assert.ValueNotSet("fully_qualified_name")) + return r +} + +func (r *RowAccessPolicyResourceAssert) HasNoName() *RowAccessPolicyResourceAssert { + r.AddAssertion(assert.ValueNotSet("name")) + return r +} + +func (r *RowAccessPolicyResourceAssert) HasNoSchema() *RowAccessPolicyResourceAssert { + r.AddAssertion(assert.ValueNotSet("schema")) + return r +} diff --git a/pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert/row_access_policy_show_output_ext.go b/pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert/row_access_policy_show_output_ext.go new file mode 100644 index 00000000000..a63b1e3b237 --- /dev/null +++ b/pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert/row_access_policy_show_output_ext.go @@ -0,0 +1,10 @@ +package resourceshowoutputassert + +import ( + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert" +) + +func (r *RowAccessPolicyShowOutputAssert) HasCreatedOnNotEmpty() *RowAccessPolicyShowOutputAssert { + r.AddAssertion(assert.ResourceShowOutputValuePresent("created_on")) + return r +} diff --git a/pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert/row_access_policy_show_output_gen.go b/pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert/row_access_policy_show_output_gen.go new file mode 100644 index 00000000000..b08670ce734 --- /dev/null +++ b/pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert/row_access_policy_show_output_gen.go @@ -0,0 +1,86 @@ +// Code generated by assertions generator; DO NOT EDIT. + +package resourceshowoutputassert + +import ( + "testing" + + "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 RowAccessPolicyShowOutputAssert struct { + *assert.ResourceAssert +} + +func RowAccessPolicyShowOutput(t *testing.T, name string) *RowAccessPolicyShowOutputAssert { + t.Helper() + + r := RowAccessPolicyShowOutputAssert{ + ResourceAssert: assert.NewResourceAssert(name, "show_output"), + } + r.AddAssertion(assert.ValueSet("show_output.#", "1")) + return &r +} + +func ImportedRowAccessPolicyShowOutput(t *testing.T, id string) *RowAccessPolicyShowOutputAssert { + t.Helper() + + r := RowAccessPolicyShowOutputAssert{ + ResourceAssert: assert.NewImportedResourceAssert(id, "show_output"), + } + r.AddAssertion(assert.ValueSet("show_output.#", "1")) + return &r +} + +//////////////////////////// +// Attribute value checks // +//////////////////////////// + +func (r *RowAccessPolicyShowOutputAssert) HasCreatedOn(expected string) *RowAccessPolicyShowOutputAssert { + r.AddAssertion(assert.ResourceShowOutputValueSet("created_on", expected)) + return r +} + +func (r *RowAccessPolicyShowOutputAssert) HasName(expected string) *RowAccessPolicyShowOutputAssert { + r.AddAssertion(assert.ResourceShowOutputValueSet("name", expected)) + return r +} + +func (r *RowAccessPolicyShowOutputAssert) HasDatabaseName(expected string) *RowAccessPolicyShowOutputAssert { + r.AddAssertion(assert.ResourceShowOutputValueSet("database_name", expected)) + return r +} + +func (r *RowAccessPolicyShowOutputAssert) HasSchemaName(expected string) *RowAccessPolicyShowOutputAssert { + r.AddAssertion(assert.ResourceShowOutputValueSet("schema_name", expected)) + return r +} + +func (r *RowAccessPolicyShowOutputAssert) HasKind(expected string) *RowAccessPolicyShowOutputAssert { + r.AddAssertion(assert.ResourceShowOutputValueSet("kind", expected)) + return r +} + +func (r *RowAccessPolicyShowOutputAssert) HasOwner(expected string) *RowAccessPolicyShowOutputAssert { + r.AddAssertion(assert.ResourceShowOutputValueSet("owner", expected)) + return r +} + +func (r *RowAccessPolicyShowOutputAssert) HasComment(expected string) *RowAccessPolicyShowOutputAssert { + r.AddAssertion(assert.ResourceShowOutputValueSet("comment", expected)) + return r +} + +func (r *RowAccessPolicyShowOutputAssert) HasOptions(expected string) *RowAccessPolicyShowOutputAssert { + r.AddAssertion(assert.ResourceShowOutputValueSet("options", expected)) + return r +} + +func (r *RowAccessPolicyShowOutputAssert) HasOwnerRoleType(expected string) *RowAccessPolicyShowOutputAssert { + r.AddAssertion(assert.ResourceShowOutputValueSet("owner_role_type", expected)) + return r +} 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 new file mode 100644 index 00000000000..fc11800ba3c --- /dev/null +++ b/pkg/acceptance/bettertestspoc/config/model/row_access_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 (r *RowAccessPolicyModel) WithArgument(argument []sdk.RowAccessPolicyArgument) *RowAccessPolicyModel { + 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(v.Type), + }) + } + r.Argument = tfconfig.SetVariable(maps...) + return r +} 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 new file mode 100644 index 00000000000..589bf1660bd --- /dev/null +++ b/pkg/acceptance/bettertestspoc/config/model/row_access_policy_model_gen.go @@ -0,0 +1,135 @@ +// 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 RowAccessPolicyModel 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"` + FullyQualifiedName tfconfig.Variable `json:"fully_qualified_name,omitempty"` + Name tfconfig.Variable `json:"name,omitempty"` + Schema tfconfig.Variable `json:"schema,omitempty"` + + *config.ResourceModelMeta +} + +///////////////////////////////////////////////// +// Basic builders (resource name and required) // +///////////////////////////////////////////////// + +func RowAccessPolicy( + resourceName string, + argument []sdk.RowAccessPolicyArgument, + body string, + database string, + name string, + schema string, +) *RowAccessPolicyModel { + r := &RowAccessPolicyModel{ResourceModelMeta: config.Meta(resourceName, resources.RowAccessPolicy)} + r.WithArgument(argument) + r.WithBody(body) + r.WithDatabase(database) + r.WithName(name) + r.WithSchema(schema) + return r +} + +func RowAccessPolicyWithDefaultMeta( + argument []sdk.RowAccessPolicyArgument, + body string, + database string, + name string, + schema string, +) *RowAccessPolicyModel { + r := &RowAccessPolicyModel{ResourceModelMeta: config.DefaultMeta(resources.RowAccessPolicy)} + r.WithArgument(argument) + r.WithBody(body) + r.WithDatabase(database) + r.WithName(name) + r.WithSchema(schema) + return r +} + +///////////////////////////////// +// below all the proper values // +///////////////////////////////// + +// argument attribute type is not yet supported, so WithArgument can't be generated + +func (r *RowAccessPolicyModel) WithBody(body string) *RowAccessPolicyModel { + r.Body = tfconfig.StringVariable(body) + return r +} + +func (r *RowAccessPolicyModel) WithComment(comment string) *RowAccessPolicyModel { + r.Comment = tfconfig.StringVariable(comment) + return r +} + +func (r *RowAccessPolicyModel) WithDatabase(database string) *RowAccessPolicyModel { + r.Database = tfconfig.StringVariable(database) + return r +} + +func (r *RowAccessPolicyModel) WithFullyQualifiedName(fullyQualifiedName string) *RowAccessPolicyModel { + r.FullyQualifiedName = tfconfig.StringVariable(fullyQualifiedName) + return r +} + +func (r *RowAccessPolicyModel) WithName(name string) *RowAccessPolicyModel { + r.Name = tfconfig.StringVariable(name) + return r +} + +func (r *RowAccessPolicyModel) WithSchema(schema string) *RowAccessPolicyModel { + r.Schema = tfconfig.StringVariable(schema) + return r +} + +////////////////////////////////////////// +// below it's possible to set any value // +////////////////////////////////////////// + +func (r *RowAccessPolicyModel) WithArgumentValue(value tfconfig.Variable) *RowAccessPolicyModel { + r.Argument = value + return r +} + +func (r *RowAccessPolicyModel) WithBodyValue(value tfconfig.Variable) *RowAccessPolicyModel { + r.Body = value + return r +} + +func (r *RowAccessPolicyModel) WithCommentValue(value tfconfig.Variable) *RowAccessPolicyModel { + r.Comment = value + return r +} + +func (r *RowAccessPolicyModel) WithDatabaseValue(value tfconfig.Variable) *RowAccessPolicyModel { + r.Database = value + return r +} + +func (r *RowAccessPolicyModel) WithFullyQualifiedNameValue(value tfconfig.Variable) *RowAccessPolicyModel { + r.FullyQualifiedName = value + return r +} + +func (r *RowAccessPolicyModel) WithNameValue(value tfconfig.Variable) *RowAccessPolicyModel { + r.Name = value + return r +} + +func (r *RowAccessPolicyModel) WithSchemaValue(value tfconfig.Variable) *RowAccessPolicyModel { + r.Schema = value + return r +} diff --git a/pkg/acceptance/helpers/row_access_policy_client.go b/pkg/acceptance/helpers/row_access_policy_client.go index 2ae9be20995..b8fbfbfdc97 100644 --- a/pkg/acceptance/helpers/row_access_policy_client.go +++ b/pkg/acceptance/helpers/row_access_policy_client.go @@ -31,12 +31,18 @@ func (c *RowAccessPolicyClient) CreateRowAccessPolicy(t *testing.T) (*sdk.RowAcc func (c *RowAccessPolicyClient) CreateRowAccessPolicyWithDataType(t *testing.T, datatype sdk.DataType) (*sdk.RowAccessPolicy, func()) { t.Helper() + + arg := sdk.NewCreateRowAccessPolicyArgsRequest("A", datatype) + return c.CreateRowAccessPolicyWithArguments(t, []sdk.CreateRowAccessPolicyArgsRequest{*arg}) +} + +func (c *RowAccessPolicyClient) CreateRowAccessPolicyWithArguments(t *testing.T, args []sdk.CreateRowAccessPolicyArgsRequest) (*sdk.RowAccessPolicy, func()) { + t.Helper() ctx := context.Background() id := c.ids.RandomSchemaObjectIdentifier() - arg := sdk.NewCreateRowAccessPolicyArgsRequest("A", datatype) body := "true" - createRequest := sdk.NewCreateRowAccessPolicyRequest(id, []sdk.CreateRowAccessPolicyArgsRequest{*arg}, body) + createRequest := sdk.NewCreateRowAccessPolicyRequest(id, args, body) err := c.client().Create(ctx, createRequest) require.NoError(t, err) @@ -47,6 +53,27 @@ func (c *RowAccessPolicyClient) CreateRowAccessPolicyWithDataType(t *testing.T, return rowAccessPolicy, c.DropRowAccessPolicyFunc(t, id) } +func (c *RowAccessPolicyClient) CreateRowAccessPolicyWithRequest(t *testing.T, req sdk.CreateRowAccessPolicyRequest) (*sdk.RowAccessPolicy, func()) { + t.Helper() + ctx := context.Background() + + err := c.client().Create(ctx, &req) + require.NoError(t, err) + + rowAccessPolicy, err := c.client().ShowByID(ctx, req.GetName()) + require.NoError(t, err) + + return rowAccessPolicy, c.DropRowAccessPolicyFunc(t, req.GetName()) +} + +func (c *RowAccessPolicyClient) Alter(t *testing.T, req sdk.AlterRowAccessPolicyRequest) { + t.Helper() + ctx := context.Background() + + err := c.client().Alter(ctx, &req) + require.NoError(t, err) +} + func (c *RowAccessPolicyClient) DropRowAccessPolicyFunc(t *testing.T, id sdk.SchemaObjectIdentifier) func() { t.Helper() ctx := context.Background() @@ -56,3 +83,10 @@ func (c *RowAccessPolicyClient) DropRowAccessPolicyFunc(t *testing.T, id sdk.Sch require.NoError(t, err) } } + +func (c *RowAccessPolicyClient) Show(t *testing.T, id sdk.SchemaObjectIdentifier) (*sdk.RowAccessPolicy, error) { + t.Helper() + ctx := context.Background() + + return c.client().ShowByID(ctx, id) +} diff --git a/pkg/resources/doc_helpers.go b/pkg/resources/doc_helpers.go index 9fe20cf8171..d85e68f047e 100644 --- a/pkg/resources/doc_helpers.go +++ b/pkg/resources/doc_helpers.go @@ -36,3 +36,7 @@ func withPrivilegedRolesDescription(description, paramName string) string { func blocklistedCharactersFieldDescription(description string) string { return fmt.Sprintf(`%s 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: %s`, description, characterList([]rune{'|', '.', '(', ')', '"'})) } + +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) +} diff --git a/pkg/resources/row_access_policy.go b/pkg/resources/row_access_policy.go index 544ec1cad23..c9fd812af31 100644 --- a/pkg/resources/row_access_policy.go +++ b/pkg/resources/row_access_policy.go @@ -4,88 +4,186 @@ import ( "context" "fmt" "log" - "strings" "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 rowAccessPolicySchema = map[string]*schema.Schema{ "name": { - Type: schema.TypeString, - Required: true, - Description: "Specifies the identifier for the row access policy; must be unique for the database and schema in which the row access policy is created.", - ForceNew: true, + Type: schema.TypeString, + Required: true, + Description: blocklistedCharactersFieldDescription("Specifies the identifier for the row access policy; must be unique for the database and schema in which the row access policy is created."), + DiffSuppressFunc: suppressIdentifierQuoting, }, "database": { - Type: schema.TypeString, - Required: true, - Description: "The database in which to create the row access policy.", - ForceNew: true, + Type: schema.TypeString, + Required: true, + Description: blocklistedCharactersFieldDescription("The database in which to create the row access policy."), + ForceNew: true, + DiffSuppressFunc: suppressIdentifierQuoting, }, "schema": { - Type: schema.TypeString, - Required: true, - Description: "The schema in which to create the row access policy.", - ForceNew: true, + Type: schema.TypeString, + Required: true, + Description: blocklistedCharactersFieldDescription("The schema in which to create the row access policy."), + ForceNew: true, + DiffSuppressFunc: suppressIdentifierQuoting, }, - // TODO [SNOW-1020074]: Implement DiffSuppressFunc and test after https://github.com/hashicorp/terraform-plugin-sdk/issues/477 is solved. - "signature": { - Type: schema.TypeMap, - Elem: &schema.Schema{Type: schema.TypeString}, + "argument": { + Type: schema.TypeList, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "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: "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).", + ForceNew: true, + }, + }, + }, Required: true, + Description: "List of the arguments for the row access policy. A signature specifies a set of attributes that must be considered to determine whether the row is accessible. The attribute values come from the database object (e.g. table or view) to be protected by the row access policy. If any argument name or type is changed, the resource is recreated.", ForceNew: true, - Description: "Specifies signature (arguments) for the row access policy (uppercase and sorted to avoid recreation of resource). A signature specifies a set of attributes that must be considered to determine whether the row is accessible. The attribute values come from the database object (e.g. table or view) to be protected by the row access policy.", }, - "row_access_expression": { - Type: schema.TypeString, - Required: true, - Description: "Specifies the SQL expression. The expression can be any boolean-valued SQL expression.", + "body": { + Type: schema.TypeString, + Required: true, + Description: diffSuppressStatementFieldDescription("Specifies the SQL expression. The expression can be any boolean-valued SQL expression."), + DiffSuppressFunc: DiffSuppressStatement, }, "comment": { Type: schema.TypeString, Optional: true, Description: "Specifies a comment for the row access policy.", }, + ShowOutputAttributeName: { + Type: schema.TypeList, + Computed: true, + Description: "Outputs the result of `SHOW ROW ACCESS POLICY` for the given row access policy.", + Elem: &schema.Resource{ + Schema: schemas.ShowRowAccessPolicySchema, + }, + }, + DescribeOutputAttributeName: { + Type: schema.TypeList, + Computed: true, + Description: "Outputs the result of `DESCRIBE ROW ACCESS POLICY` for the given row access policy.", + Elem: &schema.Resource{ + Schema: schemas.RowAccessPolicyDescribeSchema, + }, + }, FullyQualifiedNameAttributeName: schemas.FullyQualifiedNameSchema, } // RowAccessPolicy returns a pointer to the resource representing a row access policy. func RowAccessPolicy() *schema.Resource { return &schema.Resource{ - Create: CreateRowAccessPolicy, - Read: ReadRowAccessPolicy, - Update: UpdateRowAccessPolicy, - Delete: DeleteRowAccessPolicy, + SchemaVersion: 1, + + CreateContext: CreateRowAccessPolicy, + ReadContext: ReadRowAccessPolicy, + UpdateContext: UpdateRowAccessPolicy, + DeleteContext: DeleteRowAccessPolicy, Schema: rowAccessPolicySchema, Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, + StateContext: ImportRowAccessPolicy, + }, + + CustomizeDiff: customdiff.All( + ComputedIfAnyAttributeChanged(rowAccessPolicySchema, ShowOutputAttributeName, "comment", "name"), + ComputedIfAnyAttributeChanged(rowAccessPolicySchema, DescribeOutputAttributeName, "body", "name", "signature"), + ComputedIfAnyAttributeChanged(rowAccessPolicySchema, FullyQualifiedNameAttributeName, "name"), + ), + + 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_RowAccessPolicyStateUpgrader, + }, }, } } -// CreateRowAccessPolicy implements schema.CreateFunc. -func CreateRowAccessPolicy(d *schema.ResourceData, meta interface{}) error { +func ImportRowAccessPolicy(ctx context.Context, d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { + log.Printf("[DEBUG] Starting row access policy import") + client := meta.(*provider.Context).Client + id, err := sdk.ParseSchemaObjectIdentifier(d.Id()) + if err != nil { + return nil, err + } + + policy, err := client.RowAccessPolicies.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("comment", policy.Comment); err != nil { + return nil, err + } + policyDescription, err := client.RowAccessPolicies.Describe(ctx, id) + if err != nil { + return nil, err + } + if err := d.Set("body", policyDescription.Body); err != nil { + return nil, err + } + args, err := policyDescription.Arguments() + if err != nil { + return nil, err + } + if err := d.Set("argument", schemas.RowAccessPolicyArgumentsToSchema(args)); err != nil { + return nil, err + } + return []*schema.ResourceData{d}, nil +} + +func CreateRowAccessPolicy(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*provider.Context).Client - ctx := context.Background() databaseName := d.Get("database").(string) schemaName := d.Get("schema").(string) name := d.Get("name").(string) id := sdk.NewSchemaObjectIdentifier(databaseName, schemaName, name) - signature := d.Get("signature").(map[string]any) - rowAccessExpression := d.Get("row_access_expression").(string) + arguments := d.Get("argument").([]any) + rowAccessExpression := d.Get("body").(string) args := make([]sdk.CreateRowAccessPolicyArgsRequest, 0) - for k, v := range signature { - dataType := sdk.DataType(v.(string)) - args = append(args, *sdk.NewCreateRowAccessPolicyArgsRequest(k, dataType)) + 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.NewCreateRowAccessPolicyArgsRequest(v["name"].(string), dataType)) } createRequest := sdk.NewCreateRowAccessPolicyRequest(id, args, rowAccessExpression) @@ -97,19 +195,20 @@ func CreateRowAccessPolicy(d *schema.ResourceData, meta interface{}) error { err := client.RowAccessPolicies.Create(ctx, createRequest) if err != nil { - return fmt.Errorf("error creating row access policy %v err = %w", name, err) + return diag.FromErr(fmt.Errorf("error creating row access policy %v err = %w", name, err)) } - d.SetId(helpers.EncodeSnowflakeID(id)) + d.SetId(helpers.EncodeResourceIdentifier(id)) - return ReadRowAccessPolicy(d, meta) + return ReadRowAccessPolicy(ctx, d, meta) } -// ReadRowAccessPolicy implements schema.ReadFunc. -func ReadRowAccessPolicy(d *schema.ResourceData, meta interface{}) error { +func ReadRowAccessPolicy(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*provider.Context).Client - ctx := context.Background() - id := helpers.DecodeSnowflakeID(d.Id()).(sdk.SchemaObjectIdentifier) + id, err := sdk.ParseSchemaObjectIdentifier(d.Id()) + if err != nil { + return diag.FromErr(err) + } rowAccessPolicy, err := client.RowAccessPolicies.ShowByID(ctx, id) if err != nil { @@ -118,101 +217,103 @@ func ReadRowAccessPolicy(d *schema.ResourceData, meta interface{}) error { return nil } if err := d.Set(FullyQualifiedNameAttributeName, id.FullyQualifiedName()); err != nil { - return err - } - - if err := d.Set("name", rowAccessPolicy.Name); err != nil { - return err - } - - if err := d.Set("database", rowAccessPolicy.DatabaseName); err != nil { - return err - } - - if err := d.Set("schema", rowAccessPolicy.SchemaName); err != nil { - return err + return diag.FromErr(err) } if err := d.Set("comment", rowAccessPolicy.Comment); err != nil { - return err + return diag.FromErr(err) } rowAccessPolicyDescription, err := client.RowAccessPolicies.Describe(ctx, id) if err != nil { - return err + return diag.FromErr(err) } - if err := d.Set("row_access_expression", rowAccessPolicyDescription.Body); err != nil { - return err + if err := d.Set("body", rowAccessPolicyDescription.Body); err != nil { + return diag.FromErr(err) } - - if err := d.Set("signature", parseSignature(rowAccessPolicyDescription.Signature)); err != nil { - return err + args, err := rowAccessPolicyDescription.Arguments() + if err != nil { + return diag.FromErr(err) } - - return err + if err := d.Set("argument", schemas.RowAccessPolicyArgumentsToSchema(args)); err != nil { + return diag.FromErr(err) + } + if err = d.Set(ShowOutputAttributeName, []map[string]any{schemas.RowAccessPolicyToSchema(rowAccessPolicy)}); err != nil { + return diag.FromErr(err) + } + if err = d.Set(DescribeOutputAttributeName, []map[string]any{schemas.RowAccessPolicyDescriptionToSchema(*rowAccessPolicyDescription)}); err != nil { + return diag.FromErr(err) + } + return nil } // UpdateRowAccessPolicy implements schema.UpdateFunc. -func UpdateRowAccessPolicy(d *schema.ResourceData, meta interface{}) error { +func UpdateRowAccessPolicy(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*provider.Context).Client - ctx := context.Background() - id := helpers.DecodeSnowflakeID(d.Id()).(sdk.SchemaObjectIdentifier) + 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)) + + err := client.RowAccessPolicies.Alter(ctx, sdk.NewAlterRowAccessPolicyRequest(id).WithRenameTo(&newId)) + if err != nil { + return diag.FromErr(fmt.Errorf("error renaming view %v err = %w", d.Id(), err)) + } + + d.SetId(helpers.EncodeResourceIdentifier(newId)) + id = newId + } if d.HasChange("comment") { comment := d.Get("comment") if c := comment.(string); c == "" { err := client.RowAccessPolicies.Alter(ctx, sdk.NewAlterRowAccessPolicyRequest(id).WithUnsetComment(sdk.Bool(true))) if err != nil { - return fmt.Errorf("error unsetting comment for row access policy on %v err = %w", d.Id(), err) + return diag.FromErr(fmt.Errorf("error unsetting comment for row access policy on %v err = %w", d.Id(), err)) } } else { err := client.RowAccessPolicies.Alter(ctx, sdk.NewAlterRowAccessPolicyRequest(id).WithSetComment(sdk.String(c))) if err != nil { - return fmt.Errorf("error updating comment for row access policy on %v err = %w", d.Id(), err) + return diag.FromErr(fmt.Errorf("error updating comment for row access policy on %v err = %w", d.Id(), err)) } } } - if d.HasChange("row_access_expression") { - rowAccessExpression := d.Get("row_access_expression").(string) + if d.HasChange("body") { + rowAccessExpression := d.Get("body").(string) err := client.RowAccessPolicies.Alter(ctx, sdk.NewAlterRowAccessPolicyRequest(id).WithSetBody(sdk.String(rowAccessExpression))) if err != nil { - return fmt.Errorf("error updating row access policy expression on %v err = %w", d.Id(), err) + return diag.FromErr(fmt.Errorf("error updating row access policy expression on %v err = %w", d.Id(), err)) } } - return ReadRowAccessPolicy(d, meta) + return ReadRowAccessPolicy(ctx, d, meta) } -// DeleteRowAccessPolicy implements schema.DeleteFunc. -func DeleteRowAccessPolicy(d *schema.ResourceData, meta interface{}) error { +func DeleteRowAccessPolicy(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + id, err := sdk.ParseSchemaObjectIdentifier(d.Id()) + if err != nil { + return diag.FromErr(err) + } + client := meta.(*provider.Context).Client - ctx := context.Background() - id := helpers.DecodeSnowflakeID(d.Id()).(sdk.SchemaObjectIdentifier) - err := client.RowAccessPolicies.Drop(ctx, sdk.NewDropRowAccessPolicyRequest(id)) + err = client.RowAccessPolicies.Drop(ctx, sdk.NewDropRowAccessPolicyRequest(id).WithIfExists(sdk.Pointer(true))) if err != nil { - return err + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Error, + Summary: "Error deleting row access policy", + Detail: fmt.Sprintf("id %v err = %v", id.Name(), err), + }, + } } d.SetId("") return nil } - -// TODO [SNOW-1020074]: should we put signature parsing to the SDK? -func parseSignature(signature string) map[string]interface{} { - // Format in database is `(column )` - plainSignature := strings.ReplaceAll(signature, "(", "") - plainSignature = strings.ReplaceAll(plainSignature, ")", "") - signatureParts := strings.Split(plainSignature, ", ") - signatureMap := map[string]interface{}{} - - for _, e := range signatureParts { - parts := strings.Split(e, " ") - signatureMap[parts[0]] = parts[1] - } - - return signatureMap -} diff --git a/pkg/resources/row_access_policy_acceptance_test.go b/pkg/resources/row_access_policy_acceptance_test.go index c9544c203a0..796f1df1672 100644 --- a/pkg/resources/row_access_policy_acceptance_test.go +++ b/pkg/resources/row_access_policy_acceptance_test.go @@ -1,25 +1,52 @@ package resources_test import ( + "fmt" + "regexp" "testing" 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/internal/snowflakeroles" + "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/config" "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" "github.com/hashicorp/terraform-plugin-testing/tfversion" ) func TestAcc_RowAccessPolicy(t *testing.T) { id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() - m := func() map[string]config.Variable { - return map[string]config.Variable{ - "name": config.StringVariable(id.Name()), - "database": config.StringVariable(acc.TestDatabaseName), - "schema": config.StringVariable(acc.TestSchemaName), - } + resourceName := "snowflake_row_access_policy.test" + + 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{ + { + Name: "A", + Type: string(sdk.DataTypeVARCHAR), + }, + { + Name: "B", + Type: string(sdk.DataTypeVARCHAR), + }, } + changedArgument := []sdk.RowAccessPolicyArgument{ + { + Name: "C", + Type: string(sdk.DataTypeBoolean), + }, + { + Name: "D", + Type: string(sdk.DataTypeTimestampNTZ), + }, + } + policyModel := model.RowAccessPolicy("test", argument, body, id.DatabaseName(), id.Name(), id.SchemaName()).WithComment("Terraform acceptance test") resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, @@ -30,56 +57,504 @@ func TestAcc_RowAccessPolicy(t *testing.T) { CheckDestroy: acc.CheckDestroy(t, resources.RowAccessPolicy), Steps: []resource.TestStep{ { - ConfigDirectory: config.TestStepDirectory(), - ConfigVariables: m(), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("snowflake_row_access_policy.test", "name", id.Name()), - resource.TestCheckResourceAttr("snowflake_row_access_policy.test", "fully_qualified_name", id.FullyQualifiedName()), - resource.TestCheckResourceAttr("snowflake_row_access_policy.test", "database", acc.TestDatabaseName), - resource.TestCheckResourceAttr("snowflake_row_access_policy.test", "schema", acc.TestSchemaName), - resource.TestCheckResourceAttr("snowflake_row_access_policy.test", "comment", "Terraform acceptance test"), - resource.TestCheckResourceAttr("snowflake_row_access_policy.test", "row_access_expression", "case when current_role() in ('ANALYST') then true else false end"), - resource.TestCheckResourceAttr("snowflake_row_access_policy.test", "signature.N", "VARCHAR"), - resource.TestCheckResourceAttr("snowflake_row_access_policy.test", "signature.V", "VARCHAR"), + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_RowAccessPolicy/complete"), + ConfigVariables: tfconfig.ConfigVariablesFromModel(t, policyModel), + Check: assert.AssertThat(t, resourceassert.RowAccessPolicyResource(t, resourceName). + HasNameString(id.Name()). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasFullyQualifiedNameString(id.FullyQualifiedName()). + HasCommentString("Terraform acceptance test"). + HasBodyString(body). + HasArguments(argument), + resourceshowoutputassert.RowAccessPolicyShowOutput(t, resourceName). + HasCreatedOnNotEmpty(). + HasDatabaseName(id.DatabaseName()). + HasKind(string(sdk.PolicyKindRowAccessPolicy)). + HasName(id.Name()). + HasOptions(""). + HasOwner(snowflakeroles.Accountadmin.Name()). + HasOwnerRoleType("ROLE"). + HasSchemaName(id.SchemaName()). + HasComment("Terraform acceptance test"), + 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", "BOOLEAN")), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.signature", "(A VARCHAR, B VARCHAR)")), ), }, // change comment and expression { - ConfigDirectory: config.TestStepDirectory(), - ConfigVariables: m(), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("snowflake_row_access_policy.test", "name", id.Name()), - resource.TestCheckResourceAttr("snowflake_row_access_policy.test", "fully_qualified_name", id.FullyQualifiedName()), - resource.TestCheckResourceAttr("snowflake_row_access_policy.test", "database", acc.TestDatabaseName), - resource.TestCheckResourceAttr("snowflake_row_access_policy.test", "schema", acc.TestSchemaName), - resource.TestCheckResourceAttr("snowflake_row_access_policy.test", "comment", "Terraform acceptance test - changed comment"), - resource.TestCheckResourceAttr("snowflake_row_access_policy.test", "row_access_expression", "case when current_role() in ('ANALYST') then false else true end"), - resource.TestCheckResourceAttr("snowflake_row_access_policy.test", "signature.N", "VARCHAR"), - resource.TestCheckResourceAttr("snowflake_row_access_policy.test", "signature.V", "VARCHAR"), + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_RowAccessPolicy/complete"), + ConfigVariables: tfconfig.ConfigVariablesFromModel(t, policyModel.WithBody(changedBody).WithComment("Terraform acceptance test - changed comment")), + Check: assert.AssertThat(t, resourceassert.RowAccessPolicyResource(t, resourceName). + HasNameString(id.Name()). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasFullyQualifiedNameString(id.FullyQualifiedName()). + HasCommentString("Terraform acceptance test - changed comment"). + HasBodyString(changedBody). + HasArguments(argument), ), }, // change signature { - ConfigDirectory: config.TestStepDirectory(), - ConfigVariables: m(), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("snowflake_row_access_policy.test", "name", id.Name()), - resource.TestCheckResourceAttr("snowflake_row_access_policy.test", "fully_qualified_name", id.FullyQualifiedName()), - resource.TestCheckResourceAttr("snowflake_row_access_policy.test", "database", acc.TestDatabaseName), - resource.TestCheckResourceAttr("snowflake_row_access_policy.test", "schema", acc.TestSchemaName), - resource.TestCheckResourceAttr("snowflake_row_access_policy.test", "comment", "Terraform acceptance test - changed comment"), - resource.TestCheckResourceAttr("snowflake_row_access_policy.test", "row_access_expression", "case when current_role() in ('ANALYST') then false else true end"), - resource.TestCheckResourceAttr("snowflake_row_access_policy.test", "signature.V", "BOOLEAN"), - resource.TestCheckResourceAttr("snowflake_row_access_policy.test", "signature.X", "TIMESTAMP_NTZ"), + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_RowAccessPolicy/complete"), + ConfigVariables: tfconfig.ConfigVariablesFromModel(t, policyModel.WithArgument(changedArgument)), + Check: assert.AssertThat(t, resourceassert.RowAccessPolicyResource(t, resourceName). + HasNameString(id.Name()). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasFullyQualifiedNameString(id.FullyQualifiedName()). + HasCommentString("Terraform acceptance test - changed comment"). + HasBodyString(changedBody). + HasArguments(changedArgument), + ), + }, + // external change on signature + { + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_RowAccessPolicy/complete"), + ConfigVariables: tfconfig.ConfigVariablesFromModel(t, policyModel), + PreConfig: func() { + arg := sdk.NewCreateRowAccessPolicyArgsRequest("A", sdk.DataTypeBoolean) + createRequest := sdk.NewCreateRowAccessPolicyRequest(id, []sdk.CreateRowAccessPolicyArgsRequest{*arg}, "case when current_role() in ('ANALYST') then false else true end") + acc.TestClient().RowAccessPolicy.CreateRowAccessPolicyWithRequest(t, *createRequest.WithOrReplace(sdk.Pointer(true))) + }, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionDestroyBeforeCreate), + }, + }, + Check: assert.AssertThat(t, resourceassert.RowAccessPolicyResource(t, resourceName). + HasNameString(id.Name()). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasFullyQualifiedNameString(id.FullyQualifiedName()). + HasCommentString("Terraform acceptance test - changed comment"). + HasBodyString(changedBody). + HasArguments(changedArgument), + ), + }, + // external change on body + { + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_RowAccessPolicy/complete"), + ConfigVariables: tfconfig.ConfigVariablesFromModel(t, policyModel), + PreConfig: func() { + acc.TestClient().RowAccessPolicy.Alter(t, *sdk.NewAlterRowAccessPolicyRequest(id).WithSetBody(sdk.Pointer("case when current_role() in ('EXTERNAL') then false else true end"))) + }, + Check: assert.AssertThat(t, resourceassert.RowAccessPolicyResource(t, resourceName). + HasNameString(id.Name()). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasFullyQualifiedNameString(id.FullyQualifiedName()). + HasCommentString("Terraform acceptance test - changed comment"). + HasBodyString(changedBody). + HasArguments(changedArgument), + ), + }, + { + ConfigVariables: tfconfig.ConfigVariablesFromModel(t, policyModel), + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + // unset comment + { + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_RowAccessPolicy/complete"), + ConfigVariables: tfconfig.ConfigVariablesFromModel(t, policyModel.WithComment("")), + PreConfig: func() { + acc.TestClient().RowAccessPolicy.Alter(t, *sdk.NewAlterRowAccessPolicyRequest(id).WithSetBody(sdk.Pointer("case when current_role() in ('EXTERNAL') then false else true end"))) + }, + Check: assert.AssertThat(t, resourceassert.RowAccessPolicyResource(t, resourceName). + HasNameString(id.Name()). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasFullyQualifiedNameString(id.FullyQualifiedName()). + HasCommentString(""). + HasBodyString(changedBody). + HasArguments(changedArgument), ), }, // IMPORT { - ConfigVariables: m(), - ResourceName: "snowflake_row_access_policy.test", + ConfigVariables: tfconfig.ConfigVariablesFromModel(t, policyModel), + ResourceName: resourceName, ImportState: true, ImportStateVerify: true, }, }, }) } + +// proves https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2053 is fixed +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{ + { + Name: "A", + Type: string(sdk.DataTypeVARCHAR), + }, + }, body, id.DatabaseName(), id.Name(), id.SchemaName()) + 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", + }, + }, + // these configs have "weird" format on purpose - to test against handling new lines during diff correctly + Config: rowAccessPolicy_v0_95_0_WithHeredoc(id, ` case + when current_role() in ('ANALYST') then true + else false + end +`), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PostApplyPostRefresh: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionUpdate), + }, + }, + ExpectNonEmptyPlan: true, + }, + { + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_RowAccessPolicy/basic"), + ConfigVariables: tfconfig.ConfigVariablesFromModel(t, policyModel), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionNoop), + }, + }, + Check: assert.AssertThat(t, resourceassert.RowAccessPolicyResource(t, resourceName). + HasNameString(id.Name()). + HasBodyString(`case + when current_role() in ('ANALYST') then true + else false +end`), + ), + }, + }, + }) +} + +func TestAcc_RowAccessPolicy_Rename(t *testing.T) { + id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() + 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{ + { + Name: "a", + Type: string(sdk.DataTypeVARCHAR), + }, + }, body, id.DatabaseName(), id.Name(), 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.RowAccessPolicy), + Steps: []resource.TestStep{ + { + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_RowAccessPolicy/basic"), + ConfigVariables: tfconfig.ConfigVariablesFromModel(t, policyModel), + Check: assert.AssertThat(t, resourceassert.RowAccessPolicyResource(t, resourceName). + HasNameString(id.Name()). + HasFullyQualifiedNameString(id.FullyQualifiedName()), + ), + }, + // rename + { + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_RowAccessPolicy/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.RowAccessPolicyResource(t, resourceName). + HasNameString(newId.Name()). + HasFullyQualifiedNameString(newId.FullyQualifiedName()), + ), + }, + }, + }) +} + +func rowAccessPolicy_v0_95_0(id sdk.SchemaObjectIdentifier, expr string) string { + return fmt.Sprintf(` +resource "snowflake_row_access_policy" "test" { + name = "%s" + database = "%s" + schema = "%s" + signature = { + A = "VARCHAR", + b = "VARCHAR", + } + row_access_expression = "%s" +}`, id.Name(), id.DatabaseName(), id.SchemaName(), expr) +} + +func rowAccessPolicy_v0_95_0_WithHeredoc(id sdk.SchemaObjectIdentifier, expr string) string { + return fmt.Sprintf(` +resource "snowflake_row_access_policy" "test" { + name = "%s" + database = "%s" + schema = "%s" + signature = { + A = "VARCHAR", + } + row_access_expression = <<-EOT +%s +EOT +}`, id.Name(), id.DatabaseName(), id.SchemaName(), expr) +} + +func rowAccessPolicy_v0_96_0(id sdk.SchemaObjectIdentifier) string { + return fmt.Sprintf(` +resource "snowflake_row_access_policy" "test" { + name = "%s" + database = "%s" + schema = "%s" + argument { + name = "A" + type = "VARCHAR" + } + row_access_expression = <<-EOT + case + when current_role() in ('ANALYST') then true + else false + end +EOT +}`, id.Name(), id.DatabaseName(), id.SchemaName()) +} + +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{ + { + Name: "a", + Type: "invalid-type", + }, + }, body, id.DatabaseName(), id.Name(), 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_RowAccessPolicy/basic"), + ConfigVariables: tfconfig.ConfigVariablesFromModel(t, policyModel), + ExpectError: regexp.MustCompile(`invalid data type: invalid-type`), + }, + }, + }) +} + +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{ + { + Name: "A", + Type: "TEXT", + }, + }, body, id.DatabaseName(), id.Name(), 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_RowAccessPolicy/basic"), + ConfigVariables: tfconfig.ConfigVariablesFromModel(t, policyModel), + Check: assert.AssertThat(t, resourceassert.RowAccessPolicyResource(t, resourceName). + HasNameString(id.Name()). + HasArguments([]sdk.RowAccessPolicyArgument{ + { + Name: "A", + Type: string(sdk.DataTypeVARCHAR), + }, + }), + ), + }, + }, + }) +} + +func TestAcc_view_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{ + { + Name: "A", + Type: string(sdk.DataTypeVARCHAR), + }, + { + Name: "b", + Type: string(sdk.DataTypeVARCHAR), + }, + }, body, id.DatabaseName(), id.Name(), id.SchemaName()) + + 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: rowAccessPolicy_v0_95_0(id, body), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PostApplyPostRefresh: []plancheck.PlanCheck{ + // expect change - arg name is lower case which causes a diff + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionDestroyBeforeCreate), + }, + }, + ExpectNonEmptyPlan: true, + Check: assert.AssertThat(t, resourceassert.RowAccessPolicyResource(t, resourceName). + HasNameString(id.Name()). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasFullyQualifiedNameString(id.FullyQualifiedName()), + assert.Check(resource.TestCheckResourceAttr(resourceName, "row_access_expression", body)), + assert.Check(resource.TestCheckResourceAttr(resourceName, "signature.A", "VARCHAR")), + assert.Check(resource.TestCheckResourceAttr(resourceName, "signature.B", "VARCHAR")), + ), + }, + { + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_RowAccessPolicy/basic"), + ConfigVariables: tfconfig.ConfigVariablesFromModel(t, policyModel), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionDestroyBeforeCreate), + }, + PostApplyPostRefresh: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionNoop), + }, + }, + Check: assert.AssertThat(t, resourceassert.RowAccessPolicyResource(t, resourceName). + HasNameString(id.Name()). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasFullyQualifiedNameString(id.FullyQualifiedName()). + HasBodyString(body). + HasArguments([]sdk.RowAccessPolicyArgument{ + { + Name: "A", + Type: string(sdk.DataTypeVARCHAR), + }, + { + Name: "b", + Type: string(sdk.DataTypeVARCHAR), + }, + }), + ), + }, + }, + }) +} + +func TestAcc_view_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{ + { + Name: "A", + Type: string(sdk.DataTypeVARCHAR), + }, + { + Name: "B", + Type: string(sdk.DataTypeVARCHAR), + }, + }, body, id.DatabaseName(), id.Name(), id.SchemaName()) + + 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: rowAccessPolicy_v0_95_0(id, body), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PostApplyPostRefresh: []plancheck.PlanCheck{ + // expect change - arg name is lower case which causes a diff + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionDestroyBeforeCreate), + }, + }, + ExpectNonEmptyPlan: true, + Check: assert.AssertThat(t, resourceassert.RowAccessPolicyResource(t, resourceName). + HasNameString(id.Name()). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasFullyQualifiedNameString(id.FullyQualifiedName()), + assert.Check(resource.TestCheckResourceAttr(resourceName, "row_access_expression", body)), + assert.Check(resource.TestCheckResourceAttr(resourceName, "signature.A", "VARCHAR")), + assert.Check(resource.TestCheckResourceAttr(resourceName, "signature.B", "VARCHAR")), + ), + }, + { + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_RowAccessPolicy/basic"), + ConfigVariables: tfconfig.ConfigVariablesFromModel(t, policyModel), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionNoop), + }, + PostApplyPostRefresh: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionNoop), + }, + }, + Check: assert.AssertThat(t, resourceassert.RowAccessPolicyResource(t, resourceName). + HasNameString(id.Name()). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasFullyQualifiedNameString(id.FullyQualifiedName()). + HasBodyString(body). + HasArguments([]sdk.RowAccessPolicyArgument{ + { + Name: "A", + Type: string(sdk.DataTypeVARCHAR), + }, + { + Name: "B", + Type: string(sdk.DataTypeVARCHAR), + }, + }), + ), + }, + }, + }) +} diff --git a/pkg/resources/row_access_policy_state_upgraders.go b/pkg/resources/row_access_policy_state_upgraders.go new file mode 100644 index 00000000000..a61efd6f9cc --- /dev/null +++ b/pkg/resources/row_access_policy_state_upgraders.go @@ -0,0 +1,28 @@ +package resources + +import ( + "context" + "strings" +) + +func v0_95_0_RowAccessPolicyStateUpgrader(ctx context.Context, rawState map[string]any, meta any) (map[string]any, error) { + if rawState == nil { + return rawState, nil + } + + rawState["body"] = rawState["row_access_expression"] + delete(rawState, "row_access_expression") + + signature := rawState["signature"].(map[string]any) + args := make([]map[string]any, 0) + for k, v := range signature { + args = append(args, map[string]any{ + "name": strings.ToUpper(k), + "type": v, + }) + } + rawState["argument"] = args + delete(rawState, "signature") + + return migratePipeSeparatedObjectIdentifierResourceIdToFullyQualifiedName(ctx, rawState, meta) +} diff --git a/pkg/resources/testdata/TestAcc_RowAccessPolicy/1/test.tf b/pkg/resources/testdata/TestAcc_RowAccessPolicy/1/test.tf deleted file mode 100644 index 96fd17c5ace..00000000000 --- a/pkg/resources/testdata/TestAcc_RowAccessPolicy/1/test.tf +++ /dev/null @@ -1,11 +0,0 @@ -resource "snowflake_row_access_policy" "test" { - name = var.name - database = var.database - schema = var.schema - signature = { - N = "VARCHAR" - V = "VARCHAR", - } - row_access_expression = "case when current_role() in ('ANALYST') then true else false end" - comment = "Terraform acceptance test" -} diff --git a/pkg/resources/testdata/TestAcc_RowAccessPolicy/2/test.tf b/pkg/resources/testdata/TestAcc_RowAccessPolicy/2/test.tf deleted file mode 100644 index e1976bdbd4c..00000000000 --- a/pkg/resources/testdata/TestAcc_RowAccessPolicy/2/test.tf +++ /dev/null @@ -1,11 +0,0 @@ -resource "snowflake_row_access_policy" "test" { - name = var.name - database = var.database - schema = var.schema - signature = { - N = "VARCHAR" - V = "VARCHAR", - } - row_access_expression = "case when current_role() in ('ANALYST') then false else true end" - comment = "Terraform acceptance test - changed comment" -} diff --git a/pkg/resources/testdata/TestAcc_RowAccessPolicy/2/variables.tf b/pkg/resources/testdata/TestAcc_RowAccessPolicy/2/variables.tf deleted file mode 100644 index 31dd643cf2b..00000000000 --- a/pkg/resources/testdata/TestAcc_RowAccessPolicy/2/variables.tf +++ /dev/null @@ -1,11 +0,0 @@ -variable "name" { - type = string -} - -variable "database" { - type = string -} - -variable "schema" { - type = string -} diff --git a/pkg/resources/testdata/TestAcc_RowAccessPolicy/3/test.tf b/pkg/resources/testdata/TestAcc_RowAccessPolicy/3/test.tf deleted file mode 100644 index 7e0fb12032d..00000000000 --- a/pkg/resources/testdata/TestAcc_RowAccessPolicy/3/test.tf +++ /dev/null @@ -1,11 +0,0 @@ -resource "snowflake_row_access_policy" "test" { - name = var.name - database = var.database - schema = var.schema - signature = { - V = "BOOLEAN", - X = "TIMESTAMP_NTZ" - } - row_access_expression = "case when current_role() in ('ANALYST') then false else true end" - comment = "Terraform acceptance test - changed comment" -} diff --git a/pkg/resources/testdata/TestAcc_RowAccessPolicy/3/variables.tf b/pkg/resources/testdata/TestAcc_RowAccessPolicy/3/variables.tf deleted file mode 100644 index 31dd643cf2b..00000000000 --- a/pkg/resources/testdata/TestAcc_RowAccessPolicy/3/variables.tf +++ /dev/null @@ -1,11 +0,0 @@ -variable "name" { - type = string -} - -variable "database" { - type = string -} - -variable "schema" { - type = string -} diff --git a/pkg/resources/testdata/TestAcc_RowAccessPolicy/basic/test.tf b/pkg/resources/testdata/TestAcc_RowAccessPolicy/basic/test.tf new file mode 100644 index 00000000000..32b7731ead4 --- /dev/null +++ b/pkg/resources/testdata/TestAcc_RowAccessPolicy/basic/test.tf @@ -0,0 +1,13 @@ +resource "snowflake_row_access_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 +} diff --git a/pkg/resources/testdata/TestAcc_RowAccessPolicy/1/variables.tf b/pkg/resources/testdata/TestAcc_RowAccessPolicy/basic/variables.tf similarity index 56% rename from pkg/resources/testdata/TestAcc_RowAccessPolicy/1/variables.tf rename to pkg/resources/testdata/TestAcc_RowAccessPolicy/basic/variables.tf index 31dd643cf2b..6de3eb1d18d 100644 --- a/pkg/resources/testdata/TestAcc_RowAccessPolicy/1/variables.tf +++ b/pkg/resources/testdata/TestAcc_RowAccessPolicy/basic/variables.tf @@ -9,3 +9,11 @@ variable "database" { variable "schema" { type = string } + +variable "argument" { + type = set(map(string)) +} + +variable "body" { + type = string +} diff --git a/pkg/resources/testdata/TestAcc_RowAccessPolicy/complete/test.tf b/pkg/resources/testdata/TestAcc_RowAccessPolicy/complete/test.tf new file mode 100644 index 00000000000..bf7bb7d9e9c --- /dev/null +++ b/pkg/resources/testdata/TestAcc_RowAccessPolicy/complete/test.tf @@ -0,0 +1,14 @@ +resource "snowflake_row_access_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 + comment = var.comment +} diff --git a/pkg/resources/testdata/TestAcc_RowAccessPolicy/complete/variables.tf b/pkg/resources/testdata/TestAcc_RowAccessPolicy/complete/variables.tf new file mode 100644 index 00000000000..c1ee6963640 --- /dev/null +++ b/pkg/resources/testdata/TestAcc_RowAccessPolicy/complete/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 "comment" { + type = string +} diff --git a/pkg/schemas/row_access_policy.go b/pkg/schemas/row_access_policy.go new file mode 100644 index 00000000000..ddb04f49aa8 --- /dev/null +++ b/pkg/schemas/row_access_policy.go @@ -0,0 +1,45 @@ +package schemas + +import ( + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +var RowAccessPolicyDescribeSchema = map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Computed: true, + }, + "signature": { + Type: schema.TypeString, + Computed: true, + }, + "return_type": { + Type: schema.TypeString, + Computed: true, + }, + "body": { + Type: schema.TypeString, + Computed: true, + }, +} + +func RowAccessPolicyDescriptionToSchema(description sdk.RowAccessPolicyDescription) map[string]any { + return map[string]any{ + "name": description.Name, + "signature": description.Signature, + "return_type": description.ReturnType, + "body": description.Body, + } +} + +func RowAccessPolicyArgumentsToSchema(args []sdk.RowAccessPolicyArgument) []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/sdk/row_access_policies_gen.go b/pkg/sdk/row_access_policies_gen.go index 7b99767eb38..6fdb9af2fb0 100644 --- a/pkg/sdk/row_access_policies_gen.go +++ b/pkg/sdk/row_access_policies_gen.go @@ -3,6 +3,8 @@ package sdk import ( "context" "database/sql" + "fmt" + "strings" ) type RowAccessPolicies interface { @@ -29,7 +31,7 @@ type CreateRowAccessPolicyOptions struct { } type CreateRowAccessPolicyArgs struct { - Name string `ddl:"keyword,no_quotes"` + Name string `ddl:"keyword,double_quotes"` Type DataType `ddl:"keyword,no_quotes"` } @@ -110,3 +112,29 @@ type RowAccessPolicyDescription struct { ReturnType string Body string } +type RowAccessPolicyArgument struct { + Name string + Type string +} + +// TODO(SNOW-1596962): Fully support VECTOR data type +// TODO(SNOW-1660588): Use ParseFunctionArgumentsFromString +func (d *RowAccessPolicyDescription) Arguments() ([]RowAccessPolicyArgument, error) { + // Format in database is `(column )` + plainSignature := strings.ReplaceAll(d.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 { + return nil, fmt.Errorf("parsing policy arguments: expected argument name and type, got %s", e) + } + arguments[i] = RowAccessPolicyArgument{ + Name: strings.Join(parts[:len(parts)-1], " "), + Type: parts[len(parts)-1], + } + } + return arguments, nil +} diff --git a/pkg/sdk/row_access_policies_gen_test.go b/pkg/sdk/row_access_policies_gen_test.go index 71e8e68395b..4363fac206c 100644 --- a/pkg/sdk/row_access_policies_gen_test.go +++ b/pkg/sdk/row_access_policies_gen_test.go @@ -1,6 +1,10 @@ package sdk -import "testing" +import ( + "testing" + + "github.com/stretchr/testify/require" +) func TestRowAccessPolicies_Create(t *testing.T) { id := randomSchemaObjectIdentifier() @@ -49,7 +53,7 @@ func TestRowAccessPolicies_Create(t *testing.T) { t.Run("one parameter", func(t *testing.T) { opts := defaultOpts() - assertOptsValidAndSQLEquals(t, opts, "CREATE ROW ACCESS POLICY %s AS (n VARCHAR) RETURNS BOOLEAN -> true", id.FullyQualifiedName()) + assertOptsValidAndSQLEquals(t, opts, `CREATE ROW ACCESS POLICY %s AS ("n" VARCHAR) RETURNS BOOLEAN -> true`, id.FullyQualifiedName()) }) t.Run("two parameters", func(t *testing.T) { @@ -61,14 +65,14 @@ func TestRowAccessPolicies_Create(t *testing.T) { Name: "h", Type: DataTypeVARCHAR, }} - assertOptsValidAndSQLEquals(t, opts, "CREATE ROW ACCESS POLICY %s AS (n VARCHAR, h VARCHAR) RETURNS BOOLEAN -> true", id.FullyQualifiedName()) + assertOptsValidAndSQLEquals(t, opts, `CREATE ROW ACCESS POLICY %s AS ("n" VARCHAR, "h" VARCHAR) RETURNS BOOLEAN -> true`, id.FullyQualifiedName()) }) t.Run("all options", func(t *testing.T) { opts := defaultOpts() opts.OrReplace = Bool(true) opts.Comment = String("some comment") - assertOptsValidAndSQLEquals(t, opts, "CREATE OR REPLACE ROW ACCESS POLICY %s AS (n VARCHAR) RETURNS BOOLEAN -> true COMMENT = 'some comment'", id.FullyQualifiedName()) + assertOptsValidAndSQLEquals(t, opts, `CREATE OR REPLACE ROW ACCESS POLICY %s AS ("n" VARCHAR) RETURNS BOOLEAN -> true COMMENT = 'some comment'`, id.FullyQualifiedName()) }) } @@ -243,3 +247,79 @@ func TestRowAccessPolicies_Describe(t *testing.T) { assertOptsValidAndSQLEquals(t, opts, "DESCRIBE ROW ACCESS POLICY %s", id.FullyQualifiedName()) }) } + +func TestRowAccessPolicyDescription_Arguments(t *testing.T) { + tests := []struct { + name string + signature string + want []RowAccessPolicyArgument + }{ + { + name: "signature with 1 arg", + signature: "(A VARCHAR)", + want: []RowAccessPolicyArgument{ + { + Name: "A", + Type: "VARCHAR", + }, + }, + }, + { + name: "signature with multiple args", + signature: "(A VARCHAR, B BOOLEAN)", + want: []RowAccessPolicyArgument{ + { + Name: "A", + Type: "VARCHAR", + }, + { + Name: "B", + Type: "BOOLEAN", + }, + }, + }, + { + name: "signature with complex name", + signature: "(a B VARCHAR)", + want: []RowAccessPolicyArgument{ + { + Name: "a B", + Type: "VARCHAR", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := &RowAccessPolicyDescription{ + Signature: tt.signature, + } + got, err := d.Arguments() + require.NoError(t, err) + require.Equal(t, tt.want, got) + }) + } +} + +func TestRowAccessPolicyDescription_ArgumentsInvalid(t *testing.T) { + tests := []struct { + name string + signature string + err string + }{ + { + name: "signature without data type", + signature: "(A)", + err: "parsing policy arguments: expected argument name and type, got A", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := &RowAccessPolicyDescription{ + Signature: tt.signature, + } + _, err := d.Arguments() + require.ErrorContains(t, err, tt.err) + }) + } +} 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 ad6dc6f1d62..8e89b9ebf0d 100644 --- a/pkg/sdk/testint/row_access_policies_gen_integration_test.go +++ b/pkg/sdk/testint/row_access_policies_gen_integration_test.go @@ -1,9 +1,7 @@ package testint import ( - "errors" "fmt" - "strings" "testing" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/helpers/random" @@ -40,13 +38,6 @@ func TestInt_RowAccessPolicies(t *testing.T) { }, *rowAccessPolicyDescription) } - cleanupRowAccessPolicyProvider := func(id sdk.SchemaObjectIdentifier) func() { - return func() { - err := client.RowAccessPolicies.Drop(ctx, sdk.NewDropRowAccessPolicyRequest(id)) - require.NoError(t, err) - } - } - createRowAccessPolicyRequest := func(t *testing.T, args []sdk.CreateRowAccessPolicyArgsRequest, body string) *sdk.CreateRowAccessPolicyRequest { t.Helper() id := testClientHelper().Ids.RandomSchemaObjectIdentifier() @@ -66,38 +57,20 @@ func TestInt_RowAccessPolicies(t *testing.T) { return createRowAccessPolicyRequest(t, []sdk.CreateRowAccessPolicyArgsRequest{*args}, body) } - createRowAccessPolicyWithRequest := func(t *testing.T, request *sdk.CreateRowAccessPolicyRequest) *sdk.RowAccessPolicy { - t.Helper() - id := request.GetName() - - err := client.RowAccessPolicies.Create(ctx, request) - require.NoError(t, err) - t.Cleanup(cleanupRowAccessPolicyProvider(id)) - - rowAccessPolicy, err := client.RowAccessPolicies.ShowByID(ctx, id) - require.NoError(t, err) - - return rowAccessPolicy - } - - createRowAccessPolicy := func(t *testing.T) *sdk.RowAccessPolicy { - t.Helper() - return createRowAccessPolicyWithRequest(t, createRowAccessPolicyBasicRequest(t)) - } - t.Run("create row access policy: no optionals", func(t *testing.T) { request := createRowAccessPolicyBasicRequest(t) - rowAccessPolicy := createRowAccessPolicyWithRequest(t, request) + rowAccessPolicy, cleanup := testClientHelper().RowAccessPolicy.CreateRowAccessPolicyWithRequest(t, *request) + t.Cleanup(cleanup) assertRowAccessPolicy(t, rowAccessPolicy, request.GetName(), "") }) t.Run("create row access policy: full", func(t *testing.T) { - request := createRowAccessPolicyBasicRequest(t) - request.Comment = sdk.String("some comment") + request := createRowAccessPolicyBasicRequest(t).WithComment(sdk.Pointer("some comment")) - rowAccessPolicy := createRowAccessPolicyWithRequest(t, request) + rowAccessPolicy, cleanup := testClientHelper().RowAccessPolicy.CreateRowAccessPolicyWithRequest(t, *request) + t.Cleanup(cleanup) assertRowAccessPolicy(t, rowAccessPolicy, request.GetName(), "some comment") }) @@ -133,9 +106,9 @@ func TestInt_RowAccessPolicies(t *testing.T) { err = client.RowAccessPolicies.Alter(ctx, alterRequest) if err != nil { - t.Cleanup(cleanupRowAccessPolicyProvider(id)) + t.Cleanup(testClientHelper().RowAccessPolicy.DropRowAccessPolicyFunc(t, id)) } else { - t.Cleanup(cleanupRowAccessPolicyProvider(newId)) + t.Cleanup(testClientHelper().RowAccessPolicy.DropRowAccessPolicyFunc(t, newId)) } require.NoError(t, err) @@ -149,7 +122,8 @@ func TestInt_RowAccessPolicies(t *testing.T) { }) t.Run("alter row access policy: set and unset comment", func(t *testing.T) { - rowAccessPolicy := createRowAccessPolicy(t) + rowAccessPolicy, cleanup := testClientHelper().RowAccessPolicy.CreateRowAccessPolicy(t) + t.Cleanup(cleanup) id := rowAccessPolicy.ID() alterRequest := sdk.NewAlterRowAccessPolicyRequest(id).WithSetComment(sdk.String("new comment")) @@ -172,7 +146,8 @@ func TestInt_RowAccessPolicies(t *testing.T) { }) t.Run("alter row access policy: set body", func(t *testing.T) { - rowAccessPolicy := createRowAccessPolicy(t) + rowAccessPolicy, cleanup := testClientHelper().RowAccessPolicy.CreateRowAccessPolicy(t) + t.Cleanup(cleanup) id := rowAccessPolicy.ID() alterRequest := sdk.NewAlterRowAccessPolicyRequest(id).WithSetBody(sdk.String("false")) @@ -198,7 +173,8 @@ func TestInt_RowAccessPolicies(t *testing.T) { tag, tagCleanup := testClientHelper().Tag.CreateTag(t) t.Cleanup(tagCleanup) - rowAccessPolicy := createRowAccessPolicy(t) + rowAccessPolicy, cleanup := testClientHelper().RowAccessPolicy.CreateRowAccessPolicy(t) + t.Cleanup(cleanup) id := rowAccessPolicy.ID() tagValue := "abc" @@ -231,8 +207,10 @@ func TestInt_RowAccessPolicies(t *testing.T) { }) t.Run("show row access policy: default", func(t *testing.T) { - rowAccessPolicy1 := createRowAccessPolicy(t) - rowAccessPolicy2 := createRowAccessPolicy(t) + rowAccessPolicy1, cleanup1 := testClientHelper().RowAccessPolicy.CreateRowAccessPolicy(t) + t.Cleanup(cleanup1) + rowAccessPolicy2, cleanup2 := testClientHelper().RowAccessPolicy.CreateRowAccessPolicy(t) + t.Cleanup(cleanup2) showRequest := sdk.NewShowRowAccessPolicyRequest() returnedRowAccessPolicies, err := client.RowAccessPolicies.Show(ctx, showRequest) @@ -243,8 +221,10 @@ func TestInt_RowAccessPolicies(t *testing.T) { }) t.Run("show row access policy: with options", func(t *testing.T) { - rowAccessPolicy1 := createRowAccessPolicy(t) - rowAccessPolicy2 := createRowAccessPolicy(t) + rowAccessPolicy1, cleanup1 := testClientHelper().RowAccessPolicy.CreateRowAccessPolicy(t) + t.Cleanup(cleanup1) + rowAccessPolicy2, cleanup2 := testClientHelper().RowAccessPolicy.CreateRowAccessPolicy(t) + t.Cleanup(cleanup2) showRequest := sdk.NewShowRowAccessPolicyRequest(). WithLike(&sdk.Like{Pattern: &rowAccessPolicy1.Name}). @@ -264,42 +244,45 @@ func TestInt_RowAccessPolicies(t *testing.T) { body := "true" request := createRowAccessPolicyRequest(t, []sdk.CreateRowAccessPolicyArgsRequest{*args}, body) - rowAccessPolicy := createRowAccessPolicyWithRequest(t, request) + rowAccessPolicy, cleanup := testClientHelper().RowAccessPolicy.CreateRowAccessPolicyWithRequest(t, *request) + t.Cleanup(cleanup) returnedRowAccessPolicyDescription, err := client.RowAccessPolicies.Describe(ctx, rowAccessPolicy.ID()) require.NoError(t, err) - assertRowAccessPolicyDescription(t, returnedRowAccessPolicyDescription, rowAccessPolicy.ID(), fmt.Sprintf("(%s %s)", strings.ToUpper(argName), argType), body) + assertRowAccessPolicyDescription(t, returnedRowAccessPolicyDescription, rowAccessPolicy.ID(), fmt.Sprintf("(%s %s)", argName, argType), body) }) - t.Run("describe row access policy: with data type normalization", func(t *testing.T) { + t.Run("describe row access policy: with timestamp data type normalization", func(t *testing.T) { argName := random.AlphaN(5) argType := sdk.DataTypeTimestamp args := sdk.NewCreateRowAccessPolicyArgsRequest(argName, argType) body := "true" request := createRowAccessPolicyRequest(t, []sdk.CreateRowAccessPolicyArgsRequest{*args}, body) - rowAccessPolicy := createRowAccessPolicyWithRequest(t, request) + rowAccessPolicy, cleanup := testClientHelper().RowAccessPolicy.CreateRowAccessPolicyWithRequest(t, *request) + t.Cleanup(cleanup) returnedRowAccessPolicyDescription, err := client.RowAccessPolicies.Describe(ctx, rowAccessPolicy.ID()) require.NoError(t, err) - assertRowAccessPolicyDescription(t, returnedRowAccessPolicyDescription, rowAccessPolicy.ID(), fmt.Sprintf("(%s %s)", strings.ToUpper(argName), sdk.DataTypeTimestampNTZ), body) + assertRowAccessPolicyDescription(t, returnedRowAccessPolicyDescription, rowAccessPolicy.ID(), fmt.Sprintf("(%s %s)", argName, sdk.DataTypeTimestampNTZ), body) }) - t.Run("describe row access policy: with data type normalization", func(t *testing.T) { + t.Run("describe row access policy: with varchar data type normalization", func(t *testing.T) { argName := random.AlphaN(5) argType := sdk.DataType("VARCHAR(200)") args := sdk.NewCreateRowAccessPolicyArgsRequest(argName, argType) body := "true" request := createRowAccessPolicyRequest(t, []sdk.CreateRowAccessPolicyArgsRequest{*args}, body) - rowAccessPolicy := createRowAccessPolicyWithRequest(t, request) + rowAccessPolicy, cleanup := testClientHelper().RowAccessPolicy.CreateRowAccessPolicyWithRequest(t, *request) + t.Cleanup(cleanup) returnedRowAccessPolicyDescription, err := client.RowAccessPolicies.Describe(ctx, rowAccessPolicy.ID()) require.NoError(t, err) - assertRowAccessPolicyDescription(t, returnedRowAccessPolicyDescription, rowAccessPolicy.ID(), fmt.Sprintf("(%s %s)", strings.ToUpper(argName), sdk.DataTypeVARCHAR), body) + assertRowAccessPolicyDescription(t, returnedRowAccessPolicyDescription, rowAccessPolicy.ID(), fmt.Sprintf("(%s %s)", argName, sdk.DataTypeVARCHAR), body) }) t.Run("describe row access policy: non-existing", func(t *testing.T) { @@ -312,25 +295,6 @@ func TestInt_RowAccessPoliciesShowByID(t *testing.T) { client := testClient(t) ctx := testContext(t) - cleanupRowAccessPolicyHandle := func(id sdk.SchemaObjectIdentifier) func() { - return func() { - err := client.RowAccessPolicies.Drop(ctx, sdk.NewDropRowAccessPolicyRequest(id)) - if errors.Is(err, sdk.ErrObjectNotExistOrAuthorized) { - return - } - require.NoError(t, err) - } - } - - createRowAccessPolicyHandle := func(t *testing.T, id sdk.SchemaObjectIdentifier) { - t.Helper() - - args := sdk.NewCreateRowAccessPolicyArgsRequest(random.AlphaN(5), sdk.DataTypeVARCHAR) - err := client.RowAccessPolicies.Create(ctx, sdk.NewCreateRowAccessPolicyRequest(id, []sdk.CreateRowAccessPolicyArgsRequest{*args}, "true")) - require.NoError(t, err) - t.Cleanup(cleanupRowAccessPolicyHandle(id)) - } - t.Run("show by id - same name in different schemas", func(t *testing.T) { schema, schemaCleanup := testClientHelper().Schema.CreateSchema(t) t.Cleanup(schemaCleanup) @@ -338,8 +302,17 @@ func TestInt_RowAccessPoliciesShowByID(t *testing.T) { id1 := testClientHelper().Ids.RandomSchemaObjectIdentifier() id2 := testClientHelper().Ids.NewSchemaObjectIdentifierInSchema(id1.Name(), schema.ID()) - createRowAccessPolicyHandle(t, id1) - createRowAccessPolicyHandle(t, id2) + body := "true" + argName := random.AlphaN(5) + argType := sdk.DataTypeVARCHAR + arg := sdk.NewCreateRowAccessPolicyArgsRequest(argName, argType) + + req1 := sdk.NewCreateRowAccessPolicyRequest(id1, []sdk.CreateRowAccessPolicyArgsRequest{*arg}, body) + req2 := sdk.NewCreateRowAccessPolicyRequest(id2, []sdk.CreateRowAccessPolicyArgsRequest{*arg}, body) + _, cleanup1 := testClientHelper().RowAccessPolicy.CreateRowAccessPolicyWithRequest(t, *req1) + t.Cleanup(cleanup1) + _, cleanup2 := testClientHelper().RowAccessPolicy.CreateRowAccessPolicyWithRequest(t, *req2) + t.Cleanup(cleanup2) e1, err := client.RowAccessPolicies.ShowByID(ctx, id1) require.NoError(t, err) @@ -350,3 +323,61 @@ func TestInt_RowAccessPoliciesShowByID(t *testing.T) { require.Equal(t, id2, e2.ID()) }) } + +func TestInt_RowAccessPoliciesDescribe(t *testing.T) { + client := testClient(t) + ctx := testContext(t) + + t.Run("describe", func(t *testing.T) { + args := []sdk.CreateRowAccessPolicyArgsRequest{ + *sdk.NewCreateRowAccessPolicyArgsRequest("A", "NUMBER(2, 0)"), + *sdk.NewCreateRowAccessPolicyArgsRequest("B", "DECIMAL"), + *sdk.NewCreateRowAccessPolicyArgsRequest("C", "INTEGER"), + *sdk.NewCreateRowAccessPolicyArgsRequest("D", sdk.DataTypeFloat), + *sdk.NewCreateRowAccessPolicyArgsRequest("E", "DOUBLE"), + *sdk.NewCreateRowAccessPolicyArgsRequest("F", "VARCHAR(20)"), + *sdk.NewCreateRowAccessPolicyArgsRequest("G", "CHAR"), + *sdk.NewCreateRowAccessPolicyArgsRequest("H", sdk.DataTypeString), + *sdk.NewCreateRowAccessPolicyArgsRequest("I", "TEXT"), + *sdk.NewCreateRowAccessPolicyArgsRequest("J", sdk.DataTypeBinary), + *sdk.NewCreateRowAccessPolicyArgsRequest("K", "VARBINARY"), + *sdk.NewCreateRowAccessPolicyArgsRequest("L", sdk.DataTypeBoolean), + *sdk.NewCreateRowAccessPolicyArgsRequest("M", sdk.DataTypeDate), + *sdk.NewCreateRowAccessPolicyArgsRequest("N", "DATETIME"), + *sdk.NewCreateRowAccessPolicyArgsRequest("O", sdk.DataTypeTime), + *sdk.NewCreateRowAccessPolicyArgsRequest("P", sdk.DataTypeTimestamp), + *sdk.NewCreateRowAccessPolicyArgsRequest("R", sdk.DataTypeTimestampLTZ), + *sdk.NewCreateRowAccessPolicyArgsRequest("S", sdk.DataTypeTimestampNTZ), + *sdk.NewCreateRowAccessPolicyArgsRequest("T", sdk.DataTypeTimestampTZ), + *sdk.NewCreateRowAccessPolicyArgsRequest("U", sdk.DataTypeVariant), + *sdk.NewCreateRowAccessPolicyArgsRequest("V", sdk.DataTypeObject), + *sdk.NewCreateRowAccessPolicyArgsRequest("W", sdk.DataTypeArray), + *sdk.NewCreateRowAccessPolicyArgsRequest("X", sdk.DataTypeGeography), + *sdk.NewCreateRowAccessPolicyArgsRequest("Y", sdk.DataTypeGeometry), + // TODO(SNOW-1596962): Fully support VECTOR data type sdk.ParseFunctionArgumentsFromString could be a base for another function that takes argument names into consideration. + // *sdk.NewCreateRowAccessPolicyArgsRequest("Z", "VECTOR(INT, 16)"), + } + + policy, cleanup := testClientHelper().RowAccessPolicy.CreateRowAccessPolicyWithArguments(t, args) + t.Cleanup(cleanup) + + id := policy.ID() + policyDetails, err := client.RowAccessPolicies.Describe(ctx, id) + require.NoError(t, err) + assert.Equal(t, "true", policyDetails.Body) + assert.Equal(t, id.Name(), policyDetails.Name) + assert.Equal(t, "BOOLEAN", policyDetails.ReturnType) + gotArgs, err := policyDetails.Arguments() + require.NoError(t, err) + wantArgs := make([]sdk.RowAccessPolicyArgument, len(args)) + for i, arg := range args { + dataType, err := sdk.ToDataType(string(arg.Type)) + require.NoError(t, err) + wantArgs[i] = sdk.RowAccessPolicyArgument{ + Name: arg.Name, + Type: string(dataType), + } + } + assert.Equal(t, wantArgs, gotArgs) + }) +} diff --git a/templates/resources/row_access_policy.md.tmpl b/templates/resources/row_access_policy.md.tmpl new file mode 100644 index 00000000000..e09e58f03f4 --- /dev/null +++ b/templates/resources/row_access_policy.md.tmpl @@ -0,0 +1,35 @@ +--- +page_title: "{{.Name}} {{.Type}} - {{.ProviderName}}" +subcategory: "" +description: |- +{{ if gt (len (split .Description "")) 1 -}} +{{ index (split .Description "") 1 | plainmarkdown | trimspace | prefixlines " " }} +{{- else -}} +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} +{{- end }} +--- + +!> **V1 release candidate** This resource was reworked and is a release candidate for the V1. We do not expect significant changes in it before the V1. We will welcome any feedback and adjust the resource if needed. Any errors reported will be resolved with a higher priority. We encourage checking this resource out before the V1 release. Please follow the [migration guide](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/MIGRATION_GUIDE.md#v0950--v0960) to use it. + +# {{.Name}} ({{.Type}}) + +{{ .Description | trimspace }} + +{{ if .HasExample -}} +## Example Usage + +{{ tffile (printf "examples/resources/%s/resource.tf" .Name)}} +-> **Note** Instead of using fully_qualified_name, you can reference objects managed outside Terraform by constructing a correct ID, consult [identifiers guide](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/guides/identifiers#new-computed-fully-qualified-name-field-in-resources). + + +{{- end }} + +{{ .SchemaMarkdown | trimspace }} +{{- if .HasImport }} + +## Import + +Import is supported using the following syntax: + +{{ codefile "shell" (printf "examples/resources/%s/import.sh" .Name)}} +{{- end }}