diff --git a/.goreleaser.yml b/.goreleaser.yml index e35c0bb88b..181fb7f3d5 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,6 +1,11 @@ builds: - env: - - CGO_ENABLED=0 + - >- + {{- if or (eq .Os "darwin") (eq .Os "windows") }} + CGO_ENABLED=1 + {{- else }} + CGO_ENABLED=0 + {{- end }} goos: - windows - linux diff --git a/CHANGELOG.md b/CHANGELOG.md index 181b28c7a2..ee487b4c9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,29 @@ # Changelog +## [0.83.1](https://github.com/Snowflake-Labs/terraform-provider-snowflake/compare/v0.83.0...v0.83.1) (2024-01-12) + + +### πŸ› **Bug fixes:** + +* Revert goreleaser mfa token caching ([#2343](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2343)) ([9a98031](https://github.com/Snowflake-Labs/terraform-provider-snowflake/commit/9a9803107f395ca350bdbb9fb8dc0f17820b8eb6)) + +## [0.83.0](https://github.com/Snowflake-Labs/terraform-provider-snowflake/compare/v0.82.0...v0.83.0) (2024-01-11) + + +### πŸŽ‰ **What's new:** + +* Add create streamlit privilege to the SDK ([#2303](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2303)) ([be01d5f](https://github.com/Snowflake-Labs/terraform-provider-snowflake/commit/be01d5fdab4f2d31db9c4c849b349e657c0352c8)) +* grant privileges to database role resource ([#2306](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2306)) ([0311cf8](https://github.com/Snowflake-Labs/terraform-provider-snowflake/commit/0311cf8554f0fbd202b489a54c9428f55c52a490)) + + +### πŸ› **Bug fixes:** + +* Add secondary account and fix tests ([#2324](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2324)) ([da6ca73](https://github.com/Snowflake-Labs/terraform-provider-snowflake/commit/da6ca733c7527c8918d1e1beb86d1641d94062ec)) +* external tables issues ([#2334](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2334)) ([ae41691](https://github.com/Snowflake-Labs/terraform-provider-snowflake/commit/ae416917be72ab55c6f1b758dd7e06269831fabc)) +* Fix test because of the date ([#2312](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2312)) ([9a9ea33](https://github.com/Snowflake-Labs/terraform-provider-snowflake/commit/9a9ea3331f090201c2d686ed7e726eb7d9cef926)) +* Fix warehouse read and resource monitor empty set ([#2319](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2319)) ([05f96c6](https://github.com/Snowflake-Labs/terraform-provider-snowflake/commit/05f96c699e4e36db4b28380d9ac3577d4f50a709)), closes [#2318](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2318) [#2316](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2316) +* goreleaser for mfa token caching ([#2320](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2320)) ([4fef709](https://github.com/Snowflake-Labs/terraform-provider-snowflake/commit/4fef709a376f5905effcc3439b1ad4cb9043ffca)) + ## [0.82.0](https://github.com/Snowflake-Labs/terraform-provider-snowflake/compare/v0.81.0...v0.82.0) (2023-12-21) diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 690f3269f0..0501f1261a 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -88,3 +88,7 @@ provider "snowflake" { params = {} } ``` + +#### *(behavior change)* authenticator (JWT) + +Before the change `authenticator` parameter did not have to be set for private key authentication and was deduced by the provider. The change is a result of the introduced configuration alignment with an underlying [gosnowflake driver](https://github.com/snowflakedb/gosnowflake). The authentication type is required there, and it defaults to user+password one. From this version, set `authenticator` to `JWT` explicitly. diff --git a/Makefile b/Makefile index d4a9d88bbb..50d82787f1 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,8 @@ export SKIP_EMAIL_INTEGRATION_TESTS=true -export SKIP_EXTERNAL_TABLE_TEST=true export SKIP_NOTIFICATION_INTEGRATION_TESTS=true export SKIP_SAML_INTEGRATION_TESTS=true export SKIP_STREAM_TEST=true +export SKIP_MANAGED_ACCOUNT_INT_TEST=true export BASE_BINARY_NAME=terraform-provider-snowflake export TERRAFORM_PLUGINS_DIR=$(HOME)/.terraform.d/plugins export TERRAFORM_PLUGIN_LOCAL_INSTALL=$(TERRAFORM_PLUGINS_DIR)/$(BASE_BINARY_NAME) diff --git a/README.md b/README.md index 5b4e49a116..aed701d47f 100644 --- a/README.md +++ b/README.md @@ -66,59 +66,59 @@ Integration status - indicates if given resource / datasource is using new SDK. | Object Type | SDK migration status | Resource name | Datasource name | Integration status | |-------------------------------------|----------------------|------------------------------------------------|-------------------------------|--------------------| | Account | βœ… | snowflake_account | snowflake_account | βœ… | -| Managed Account | ❌ | snowflake_managed_account | snowflake_managed_account | ❌ | +| Managed Account | πŸ‘¨β€πŸ’» | snowflake_managed_account | snowflake_managed_account | ❌ | | User | βœ… | snowflake_user | snowflake_user | βœ… | | Database Role | βœ… | snowflake_database_role | snowflake_database_role | βœ… | | Role | βœ… | snowflake_role | snowflake_role | πŸ‘¨β€πŸ’» | -| Grant Privilege to Application Role | ❌ | snowflake_grant_privileges_to_application_role | snowflake_grants | ❌ | -| Grant Privilege to Database Role | βœ… | snowflake_grant_privileges_to_database_role | snowflake_grants | πŸ‘¨β€πŸ’» | -| Grant Privilege to Role | ❌ | snowflake_grant_privileges_to_role | snowflake_grants | βœ… | -| Grant Role | ❌ | snowflake_grant_role | snowflake_grants | ❌ | -| Grant Database Role | βœ… | snowflake_grant_database_role | snowflake_grants | ❌ | -| Grant Application Role | ❌ | snowflake_grant_application_role | snowflake_grants | ❌ | +| Grant Privilege to Application Role | βœ… | snowflake_grant_privileges_to_application_role | snowflake_grants | ❌ | +| Grant Privilege to Database Role | βœ… | snowflake_grant_privileges_to_database_role | snowflake_grants | βœ… | +| Grant Privilege to Role | βœ… | snowflake_grant_privileges_to_role | snowflake_grants | βœ… | +| Grant Role | βœ… | snowflake_grant_role | snowflake_grants | πŸ‘¨β€πŸ’» | +| Grant Database Role | βœ… | snowflake_grant_database_role | snowflake_grants | πŸ‘¨β€πŸ’» | +| Grant Application Role | βœ… | snowflake_grant_application_role | snowflake_grants | ❌ | | Grant Privilege to Share | βœ… | snowflake_grant_privileges_to_share | snowflake_grants | ❌ | | Grant Ownership | βœ… | snowflake_grant_ownership | snowflake_grants | ❌ | | API Integration | ❌ | snowflake_api_integration | snowflake_integrations | ❌ | | Notification Integration | ❌ | snowflake_notification_integration | snowflake_integrations | ❌ | | Security Integration | ❌ | snowflake_security_integration | snowflake_integrations | ❌ | -| Storage Integration | ❌ | snowflake_storage_integration | snowflake_integrations | ❌ | +| Storage Integration | βœ… | snowflake_storage_integration | snowflake_integrations | ❌ | | Network Policy | βœ… | snowflake_network_policy | snowflake_network_policy | βœ… | | Password Policy | βœ… | snowflake_password_policy | snowflake_password_policy | βœ… | | Session Policy | βœ… | snowflake_session_policy | snowflake_session_policy | ❌ | | Replication Group | ❌ | snowflake_replication_group | snowflake_replication_group | ❌ | | Failover Group | βœ… | snowflake_failover_group | snowflake_failover_group | βœ… | | Connection | ❌ | snowflake_connection | snowflake_connection | ❌ | -| Account Parameters | βœ… | snowflake_account_parameter | snowflake_parameters | ❌ | -| Session Parameters | βœ… | snowflake_session_parameter | snowflake_parameters | ❌ | -| Object Parameters | βœ… | snowflake_object_parameter | snowflake_parameters | ❌ | +| Account Parameters | βœ… | snowflake_account_parameter | snowflake_parameters | βœ… | +| Session Parameters | βœ… | snowflake_session_parameter | snowflake_parameters | βœ… | +| Object Parameters | βœ… | snowflake_object_parameter | snowflake_parameters | βœ… | | Warehouse | βœ… | snowflake_warehouse | snowflake_warehouse | 🟨 | | Resource Monitor | βœ… | snowflake_resource_monitor | snowflake_resource_monitor | βœ… | | Database | βœ… | snowflake_database | snowflake_database | βœ… | | Schema | βœ… | snowflake_schema | snowflake_schema | βœ… | | Share | βœ… | snowflake_share | snowflake_share | βœ… | -| Table | πŸ‘¨β€πŸ’» | snowflake_table | snowflake_table | ❌ | -| Dynamic Table | βœ… | snowflake_dynamic_table | snowflake_dynamic_table | ❌ | -| External Table | βœ… | snowflake_external_table | snowflake_external_table | ❌ | -| Event Table | ❌ | snowflake_event_table | snowflake_event_table | ❌ | -| View | ❌ | snowflake_view | snowflake_view | ❌ | +| Table | βœ… | snowflake_table | snowflake_table | ❌ | +| Dynamic Table | βœ… | snowflake_dynamic_table | snowflake_dynamic_table | βœ… | +| External Table | βœ… | snowflake_external_table | snowflake_external_table | βœ… | +| Event Table | βœ… | snowflake_event_table | snowflake_event_table | ❌ | +| View | βœ… | snowflake_view | snowflake_view | ❌ | | Materialized View | ❌ | snowflake_materialized_view | snowflake_materialized_view | ❌ | | Sequence | ❌ | snowflake_sequence | snowflake_sequence | ❌ | -| Function | ❌ | snowflake_function | snowflake_function | ❌ | -| External Function | ❌ | snowflake_external_function | snowflake_external_function | ❌ | -| Stored Procedure | ❌ | snowflake_stored_procedure | snowflake_stored_procedure | ❌ | +| Function | βœ… | snowflake_function | snowflake_function | ❌ | +| External Function | βœ… | snowflake_external_function | snowflake_external_function | ❌ | +| Stored Procedure | βœ… | snowflake_stored_procedure | snowflake_stored_procedure | ❌ | | Stream | βœ… | snowflake_stream | snowflake_stream | βœ… | -| Task | βœ… | snowflake_task | snowflake_task | ❌ | +| Task | βœ… | snowflake_task | snowflake_task | βœ… | | Masking Policy | βœ… | snowflake_masking_policy | snowflake_masking_policy | βœ… | -| Row Access Policy | ❌ | snowflake_row_access_policy | snowflake_row_access_policy | ❌ | +| Row Access Policy | πŸ‘¨β€πŸ’» | snowflake_row_access_policy | snowflake_row_access_policy | ❌ | | Tag | βœ… | snowflake_tag | snowflake_tag | ❌ | | Secret | ❌ | snowflake_secret | snowflake_secret | ❌ | -| Stage | ❌ | snowflake_stage | snowflake_stage | ❌ | +| Stage | 🟨 | snowflake_stage | snowflake_stage | ❌ | | File Format | βœ… | snowflake_file_format | snowflake_file_format | βœ… | | Pipe | βœ… | snowflake_pipe | snowflake_pipe | βœ… | | Alert | βœ… | snowflake_alert | snowflake_alert | βœ… | -| Application | ❌ | snowflake_application | snowflake_application | ❌ | -| Application Package | ❌ | snowflake_application_package | snowflake_application_package | ❌ | -| Application Role | ❌ | snowflake_application_role | snowflake_application_role | ❌ | +| Application | πŸ‘¨β€πŸ’» | snowflake_application | snowflake_application | ❌ | +| Application Package | πŸ‘¨β€πŸ’» | snowflake_application_package | snowflake_application_package | ❌ | +| Application Role | βœ… | snowflake_application_role | snowflake_application_role | ❌ | | Streamlit | ❌ | snowflake_streamlit | snowflake_streamlit | ❌ | | Versioned Schema | ❌ | snowflake_versioned_schema | snowflake_versioned_schema | ❌ | | Tag Association | ❌ | snowflake_tag_association | snowflake_tag_association | ❌ | diff --git a/docs/index.md b/docs/index.md index fdbc20787f..50f3f883a8 100644 --- a/docs/index.md +++ b/docs/index.md @@ -52,7 +52,7 @@ provider "snowflake" { ### Optional - `account` (String) Specifies your Snowflake account identifier assigned, by Snowflake. For information about account identifiers, see the [Snowflake documentation](https://docs.snowflake.com/en/user-guide/admin-account-identifier.html). Can also be sourced from the `SNOWFLAKE_ACCOUNT` environment variable. Required unless using `profile`. -- `authenticator` (String) Specifies the [authentication type](https://pkg.go.dev/github.com/snowflakedb/gosnowflake#AuthType) to use when connecting to Snowflake. Valid values include: Snowflake, OAuth, ExternalBrowser, Okta, JWT, TokenAccessor, UsernamePasswordMFA. Can also be sourced from the `SNOWFLAKE_AUTHENTICATOR` environment variable. +- `authenticator` (String) Specifies the [authentication type](https://pkg.go.dev/github.com/snowflakedb/gosnowflake#AuthType) to use when connecting to Snowflake. Valid values include: Snowflake, OAuth, ExternalBrowser, Okta, JWT, TokenAccessor, UsernamePasswordMFA. Can also be sourced from the `SNOWFLAKE_AUTHENTICATOR` environment variable. It has to be set explicitly to JWT for private key authentication. - `browser_auth` (Boolean, Deprecated) Required when `oauth_refresh_token` is used. Can also be sourced from `SNOWFLAKE_USE_BROWSER_AUTH` environment variable. - `client_ip` (String) IP address for network checks. Can also be sourced from the `SNOWFLAKE_CLIENT_IP` environment variable. - `client_request_mfa_token` (Boolean) When true the MFA token is cached in the credential manager. True by default in Windows/OSX. False for Linux. Can also be sourced from the `SNOWFLAKE_CLIENT_REQUEST_MFA_TOKEN` environment variable. diff --git a/docs/resources/external_table.md b/docs/resources/external_table.md index 364f7fb3a9..d0c43febea 100644 --- a/docs/resources/external_table.md +++ b/docs/resources/external_table.md @@ -53,6 +53,7 @@ resource "snowflake_external_table" "external_table" { - `partition_by` (List of String) Specifies any partition columns to evaluate for the external table. - `pattern` (String) Specifies the file names and/or paths on the external stage to match. - `refresh_on_create` (Boolean) Specifies weather to refresh when an external table is created. +- `table_format` (String) Identifies the external table table type. For now, only "delta" for Delta Lake table format is supported. - `tag` (Block List, Deprecated) Definitions of a tag to associate with the resource. (see [below for nested schema](#nestedblock--tag)) ### Read-Only diff --git a/docs/resources/grant_account_role.md b/docs/resources/grant_account_role.md new file mode 100644 index 0000000000..1249406239 --- /dev/null +++ b/docs/resources/grant_account_role.md @@ -0,0 +1,75 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "snowflake_grant_account_role Resource - terraform-provider-snowflake" +subcategory: "" +description: |- + +--- + +# snowflake_grant_account_role (Resource) + + + +## Example Usage + +```terraform +################################## +### grant account role to account role +################################## + +resource "snowflake_role" "role" { + name = var.role_name +} + +resource "snowflake_role" "parent_role" { + name = var.parent_role_name +} + +resource "snowflake_grant_account_role" "g" { + role_name = snowflake_role.role.name + parent_role_name = snowflake_role.parent_role.name +} + + +################################## +### grant account role to user +################################## + +resource "snowflake_role" "role" { + name = var.role_name +} + +resource "snowflake_user" "user" { + name = var.user_name +} + +resource "snowflake_grant_account_role" "g" { + role_name = snowflake_role.role.name + user_name = snowflake_user.user.name +} +``` + + +## Schema + +### Required + +- `role_name` (String) The fully qualified name of the role which will be granted to the user or parent role. + +### Optional + +- `parent_role_name` (String) The fully qualified name of the parent role which will create a parent-child relationship between the roles. +- `user_name` (String) The fully qualified name of the user on which specified role will be granted. + +### Read-Only + +- `id` (String) The ID of this resource. + +## Import + +Import is supported using the following syntax: + +```shell +# format is role_name (string) | grantee_object_type (ROLE|USER) | grantee_name (string) +terraform import "\"test_role\"|ROLE|\"test_parent_role\"" +``` diff --git a/docs/resources/grant_database_role.md b/docs/resources/grant_database_role.md new file mode 100644 index 0000000000..e29b98994b --- /dev/null +++ b/docs/resources/grant_database_role.md @@ -0,0 +1,87 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "snowflake_grant_database_role Resource - terraform-provider-snowflake" +subcategory: "" +description: |- + +--- + +# snowflake_grant_database_role (Resource) + + + +## Example Usage + +```terraform +################################## +### grant database role to account role +################################## + +resource "snowflake_database_role" "database_role" { + database = var.database + name = var.database_role_name +} + +resource "snowflake_role" "parent_role" { + name = var.parent_role_name +} + +resource "snowflake_grant_database_role" "g" { + database_role_name = "\"${var.database}\".\"${snowflake_database_role.database_role.name}\"" + parent_role_name = snowflake_role.parent_role.name +} + +################################## +### grant database role to database role +################################## + +resource "snowflake_database_role" "database_role" { + database = var.database + name = var.database_role_name +} + +resource "snowflake_database_role" "parent_database_role" { + database = var.database + name = var.parent_database_role_name +} + +resource "snowflake_grant_database_role" "g" { + database_role_name = "\"${var.database}\".\"${snowflake_database_role.database_role.name}\"" + parent_database_role_name = "\"${var.database}\".\"${snowflake_database_role.parent_database_role.name}\"" +} + +################################## +### grant database role to share +################################## + +resource "snowflake_grant_database_role" "g" { + database_role_name = "\"${var.database}\".\"${snowflake_database_role.database_role.name}\"" + share_name = snowflake_share.share.name +} +``` + + +## Schema + +### Required + +- `database_role_name` (String) The fully qualified name of the database role which will be granted to share or parent role. + +### Optional + +- `parent_database_role_name` (String) The fully qualified name of the parent database role which will create a parent-child relationship between the roles. +- `parent_role_name` (String) The fully qualified name of the parent account role which will create a parent-child relationship between the roles. +- `share_name` (String) The fully qualified name of the share on which privileges will be granted. + +### Read-Only + +- `id` (String) The ID of this resource. + +## Import + +Import is supported using the following syntax: + +```shell +# format is database_role_name (string) | object_type (ROLE|DATABASE ROLE|SHARE) | grantee_name (string) +terraform import "\"ABC\".\"test_db_role\"|ROLE|\"test_parent_role\"" +``` diff --git a/docs/resources/grant_privileges_to_database_role.md b/docs/resources/grant_privileges_to_database_role.md new file mode 100644 index 0000000000..067ee9caea --- /dev/null +++ b/docs/resources/grant_privileges_to_database_role.md @@ -0,0 +1,309 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "snowflake_grant_privileges_to_database_role Resource - terraform-provider-snowflake" +subcategory: "" +description: |- + +--- + + +!> **Warning** Be careful when using `always_apply` field. It will always produce a plan (even when no changes were made) and can be harmful in some setups. For more details why we decided to introduce it to go our document explaining those design decisions (coming soon). + +# snowflake_grant_privileges_to_database_role (Resource) + + + +## Example Usage + +```terraform +resource "snowflake_database_role" "db_role" { + database = "database" + name = "db_role_name" +} + +################################## +### on database privileges +################################## + +# list of privileges +resource "snowflake_grant_privileges_to_database_role" "example" { + privileges = ["CREATE", "MONITOR"] + database_role_name = "\"${snowflake_database_role.db_role.database}\".\"${snowflake_database_role.db_role.name}\"" + on_database = snowflake_database_role.db_role.database +} + +# all privileges + grant option +resource "snowflake_grant_privileges_to_database_role" "example" { + database_role_name = "\"${snowflake_database_role.db_role.database}\".\"${snowflake_database_role.db_role.name}\"" + on_database = snowflake_database_role.db_role.database + all_privileges = true + with_grant_option = true +} + +# all privileges + grant option + always apply +resource "snowflake_grant_privileges_to_database_role" "example" { + database_role_name = "\"${snowflake_database_role.db_role.database}\".\"${snowflake_database_role.db_role.name}\"" + on_database = snowflake_database_role.db_role.database + always_apply = true + all_privileges = true + with_grant_option = true +} + +################################## +### schema privileges +################################## + +# list of privileges +resource "snowflake_grant_privileges_to_database_role" "example" { + privileges = ["MODIFY", "CREATE TABLE"] + database_role_name = "\"${snowflake_database_role.db_role.database}\".\"${snowflake_database_role.db_role.name}\"" + on_schema { + schema_name = "\"${snowflake_database_role.db_role.database}\".\"my_schema\"" # note this is a fully qualified name! + } +} + +# all privileges + grant option +resource "snowflake_grant_privileges_to_database_role" "example" { + database_role_name = "\"${snowflake_database_role.db_role.database}\".\"${snowflake_database_role.db_role.name}\"" + on_schema { + schema_name = "\"${snowflake_database_role.db_role.database}\".\"my_schema\"" # note this is a fully qualified name! + } + all_privileges = true + with_grant_option = true +} + +# all schemas in database +resource "snowflake_grant_privileges_to_database_role" "example" { + privileges = ["MODIFY", "CREATE TABLE"] + database_role_name = "\"${snowflake_database_role.db_role.database}\".\"${snowflake_database_role.db_role.name}\"" + on_schema { + all_schemas_in_database = snowflake_database_role.db_role.database + } +} + +# future schemas in database +resource "snowflake_grant_privileges_to_database_role" "example" { + privileges = ["MODIFY", "CREATE TABLE"] + database_role_name = "\"${snowflake_database_role.db_role.database}\".\"${snowflake_database_role.db_role.name}\"" + on_schema { + future_schemas_in_database = snowflake_database_role.db_role.database + } +} + +################################## +### schema object privileges +################################## + +# list of privileges +resource "snowflake_grant_privileges_to_database_role" "example" { + privileges = ["SELECT", "REFERENCES"] + database_role_name = "\"${snowflake_database_role.db_role.database}\".\"${snowflake_database_role.db_role.name}\"" + on_schema_object { + object_type = "VIEW" + object_name = "\"${snowflake_database_role.db_role.database}\".\"my_schema\".\"my_view\"" # note this is a fully qualified name! + } +} + +# all privileges + grant option +resource "snowflake_grant_privileges_to_database_role" "example" { + database_role_name = "\"${snowflake_database_role.db_role.database}\".\"${snowflake_database_role.db_role.name}\"" + on_schema_object { + object_type = "VIEW" + object_name = "\"${snowflake_database_role.db_role.database}\".\"my_schema\".\"my_view\"" # note this is a fully qualified name! + } + all_privileges = true + with_grant_option = true +} + +# all in database +resource "snowflake_grant_privileges_to_database_role" "example" { + privileges = ["SELECT", "INSERT"] + database_role_name = "\"${snowflake_database_role.db_role.database}\".\"${snowflake_database_role.db_role.name}\"" + on_schema_object { + all { + object_type_plural = "TABLES" + in_database = snowflake_database_role.db_role.database + } + } +} + +# all in schema +resource "snowflake_grant_privileges_to_database_role" "example" { + privileges = ["SELECT", "INSERT"] + database_role_name = "\"${snowflake_database_role.db_role.database}\".\"${snowflake_database_role.db_role.name}\"" + on_schema_object { + all { + object_type_plural = "TABLES" + in_schema = "\"${snowflake_database_role.db_role.database}\".\"my_schema\"" # note this is a fully qualified name! + } + } +} + +# future in database +resource "snowflake_grant_privileges_to_database_role" "example" { + privileges = ["SELECT", "INSERT"] + database_role_name = "\"${snowflake_database_role.db_role.database}\".\"${snowflake_database_role.db_role.name}\"" + on_schema_object { + future { + object_type_plural = "TABLES" + in_database = snowflake_database_role.db_role.database + } + } +} + +# future in schema +resource "snowflake_grant_privileges_to_database_role" "example" { + privileges = ["SELECT", "INSERT"] + database_role_name = "\"${snowflake_database_role.db_role.database}\".\"${snowflake_database_role.db_role.name}\"" + on_schema_object { + future { + object_type_plural = "TABLES" + in_schema = "\"${snowflake_database_role.db_role.database}\".\"my_schema\"" # note this is a fully qualified name! + } + } +} +``` + + +## Schema + +### Required + +- `database_role_name` (String) The fully qualified name of the database role to which privileges will be granted. + +### Optional + +- `all_privileges` (Boolean) Grant all privileges on the database role. +- `always_apply` (Boolean) If true, the resource will always produce a β€œplan” and on β€œapply” it will re-grant defined privileges. It is supposed to be used only in β€œgrant privileges on all X’s in database / schema Y” or β€œgrant all privileges to X” scenarios to make sure that every new object in a given database / schema is granted by the account role and every new privilege is granted to the database role. Important note: this flag is not compliant with the Terraform assumptions of the config being eventually convergent (producing an empty plan). +- `always_apply_trigger` (String) This field should not be set and its main purpose is to achieve the functionality described by always_apply field. This is value will be flipped to the opposite value on every terraform apply, thus creating a new plan that will re-apply grants. +- `on_database` (String) The fully qualified name of the database on which privileges will be granted. +- `on_schema` (Block List, Max: 1) Specifies the schema on which privileges will be granted. (see [below for nested schema](#nestedblock--on_schema)) +- `on_schema_object` (Block List, Max: 1) Specifies the schema object on which privileges will be granted. (see [below for nested schema](#nestedblock--on_schema_object)) +- `privileges` (Set of String) The privileges to grant on the database role. +- `with_grant_option` (Boolean) If specified, allows the recipient role to grant the privileges to other roles. + +### Read-Only + +- `id` (String) The ID of this resource. + + +### Nested Schema for `on_schema` + +Optional: + +- `all_schemas_in_database` (String) The fully qualified name of the database. +- `future_schemas_in_database` (String) The fully qualified name of the database. +- `schema_name` (String) The fully qualified name of the schema. + + + +### Nested Schema for `on_schema_object` + +Optional: + +- `all` (Block List, Max: 1) Configures the privilege to be granted on all objects in either a database or schema. (see [below for nested schema](#nestedblock--on_schema_object--all)) +- `future` (Block List, Max: 1) Configures the privilege to be granted on future objects in either a database or schema. (see [below for nested schema](#nestedblock--on_schema_object--future)) +- `object_name` (String) The fully qualified name of the object on which privileges will be granted. +- `object_type` (String) The object type of the schema object on which privileges will be granted. Valid values are: ALERT | DYNAMIC TABLE | EVENT TABLE | FILE FORMAT | FUNCTION | PROCEDURE | SECRET | SEQUENCE | PIPE | MASKING POLICY | PASSWORD POLICY | ROW ACCESS POLICY | SESSION POLICY | TAG | STAGE | STREAM | TABLE | EXTERNAL TABLE | TASK | VIEW | MATERIALIZED VIEW | NETWORK RULE | PACKAGES POLICY | ICEBERG TABLE + + +### Nested Schema for `on_schema_object.all` + +Required: + +- `object_type_plural` (String) The plural object type of the schema object on which privileges will be granted. Valid values are: ALERTS | DYNAMIC TABLES | EVENT TABLES | FILE FORMATS | FUNCTIONS | PROCEDURES | SECRETS | SEQUENCES | PIPES | MASKING POLICIES | PASSWORD POLICIES | ROW ACCESS POLICIES | SESSION POLICIES | TAGS | STAGES | STREAMS | TABLES | EXTERNAL TABLES | TASKS | VIEWS | MATERIALIZED VIEWS | NETWORK RULES | PACKAGES POLICIES | ICEBERG TABLES + +Optional: + +- `in_database` (String) +- `in_schema` (String) + + + +### Nested Schema for `on_schema_object.future` + +Required: + +- `object_type_plural` (String) The plural object type of the schema object on which privileges will be granted. Valid values are: ALERTS | DYNAMIC TABLES | EVENT TABLES | FILE FORMATS | FUNCTIONS | PROCEDURES | SECRETS | SEQUENCES | PIPES | MASKING POLICIES | PASSWORD POLICIES | ROW ACCESS POLICIES | SESSION POLICIES | TAGS | STAGES | STREAMS | TABLES | EXTERNAL TABLES | TASKS | VIEWS | MATERIALIZED VIEWS | NETWORK RULES | PACKAGES POLICIES | ICEBERG TABLES + +Optional: + +- `in_database` (String) +- `in_schema` (String) + +## Import + +~> **Note** All the ..._name parts should be fully qualified names, e.g. for database object it is `"".""` +~> **Note** To import all_privileges write ALL or ALL PRIVILEGES in place of `` + +Import is supported using the following syntax: + +`terraform import "|||||"` + +where: +- database_role_name - fully qualified identifier +- with_grant_option - boolean +- always_apply - boolean +- privileges - list of privileges, comma separated; to import all_privileges write "ALL" or "ALL PRIVILEGES" +- grant_type - enum +- grant_data - enum data + +It has varying number of parts, depending on grant_type. All the possible types are: + +### OnDatabase +`terraform import "||||OnDatabase|"` + +### OnSchema + +On schema contains inner types for all options. + +#### OnSchema +`terraform import "||||OnSchema|OnSchema|"` + +#### OnAllSchemasInDatabase +`terraform import "||||OnSchema|OnAllSchemasInDatabase|"` + +#### OnFutureSchemasInDatabase +`terraform import "||||OnSchema|OnFutureSchemasInDatabase|"` + +### OnSchemaObject + +On schema object contains inner types for all options. + +#### OnObject +`terraform import "||||OnSchemaObject|OnObject||"` + +#### OnAll + +On all contains inner types for all options. + +##### InDatabase +`terraform import "||||OnSchemaObject|OnAll||InDatabase|"` + +##### InSchema +`terraform import "||||OnSchemaObject|OnAll||InSchema|"` + +#### OnFuture + +On future contains inner types for all options. + +##### InDatabase +`terraform import "||||OnSchemaObject|OnFuture||InDatabase|"` + +##### InSchema +`terraform import "||||OnSchemaObject|OnFuture||InSchema|"` + +### Import examples + +#### Grant all privileges OnDatabase +`terraform import "\"test_db\".\"test_db_role\"|false|false|ALL|OnDatabase|\"test_db\""` + +#### Grant list of privileges OnAllSchemasInDatabase +`terraform import "\"test_db\".\"test_db_role\"|false|false|CREATE TAG,CREATE TABLE|OnSchema|OnAllSchemasInDatabase|\"test_db\""` + +#### Grant list of privileges on table +`terraform import "\"test_db\".\"test_db_role\"|false|false|SELECT,DELETE,INSERT|OnSchemaObject|OnObject|TABLE|\"test_db\".\"test_schema\".\"test_table\""` + +#### Grant list of privileges OnAll tables in schema +`terraform import "\"test_db\".\"test_db_role\"|false|false|SELECT,DELETE,INSERT|OnSchemaObject|OnAll|TABLES|InSchema|\"test_db\".\"test_schema\""` + diff --git a/examples/resources/snowflake_grant_account_role/import.sh b/examples/resources/snowflake_grant_account_role/import.sh new file mode 100644 index 0000000000..f308c94fa5 --- /dev/null +++ b/examples/resources/snowflake_grant_account_role/import.sh @@ -0,0 +1,2 @@ +# format is role_name (string) | grantee_object_type (ROLE|USER) | grantee_name (string) +terraform import "\"test_role\"|ROLE|\"test_parent_role\"" diff --git a/examples/resources/snowflake_grant_account_role/resource.tf b/examples/resources/snowflake_grant_account_role/resource.tf new file mode 100644 index 0000000000..75e7891cc9 --- /dev/null +++ b/examples/resources/snowflake_grant_account_role/resource.tf @@ -0,0 +1,34 @@ +################################## +### grant account role to account role +################################## + +resource "snowflake_role" "role" { + name = var.role_name +} + +resource "snowflake_role" "parent_role" { + name = var.parent_role_name +} + +resource "snowflake_grant_account_role" "g" { + role_name = snowflake_role.role.name + parent_role_name = snowflake_role.parent_role.name +} + + +################################## +### grant account role to user +################################## + +resource "snowflake_role" "role" { + name = var.role_name +} + +resource "snowflake_user" "user" { + name = var.user_name +} + +resource "snowflake_grant_account_role" "g" { + role_name = snowflake_role.role.name + user_name = snowflake_user.user.name +} diff --git a/examples/resources/snowflake_grant_database_role/import.sh b/examples/resources/snowflake_grant_database_role/import.sh new file mode 100644 index 0000000000..878d3c901a --- /dev/null +++ b/examples/resources/snowflake_grant_database_role/import.sh @@ -0,0 +1,2 @@ +# format is database_role_name (string) | object_type (ROLE|DATABASE ROLE|SHARE) | grantee_name (string) +terraform import "\"ABC\".\"test_db_role\"|ROLE|\"test_parent_role\"" diff --git a/examples/resources/snowflake_grant_database_role/resource.tf b/examples/resources/snowflake_grant_database_role/resource.tf new file mode 100644 index 0000000000..f3f331cf05 --- /dev/null +++ b/examples/resources/snowflake_grant_database_role/resource.tf @@ -0,0 +1,45 @@ +################################## +### grant database role to account role +################################## + +resource "snowflake_database_role" "database_role" { + database = var.database + name = var.database_role_name +} + +resource "snowflake_role" "parent_role" { + name = var.parent_role_name +} + +resource "snowflake_grant_database_role" "g" { + database_role_name = "\"${var.database}\".\"${snowflake_database_role.database_role.name}\"" + parent_role_name = snowflake_role.parent_role.name +} + +################################## +### grant database role to database role +################################## + +resource "snowflake_database_role" "database_role" { + database = var.database + name = var.database_role_name +} + +resource "snowflake_database_role" "parent_database_role" { + database = var.database + name = var.parent_database_role_name +} + +resource "snowflake_grant_database_role" "g" { + database_role_name = "\"${var.database}\".\"${snowflake_database_role.database_role.name}\"" + parent_database_role_name = "\"${var.database}\".\"${snowflake_database_role.parent_database_role.name}\"" +} + +################################## +### grant database role to share +################################## + +resource "snowflake_grant_database_role" "g" { + database_role_name = "\"${var.database}\".\"${snowflake_database_role.database_role.name}\"" + share_name = snowflake_share.share.name +} diff --git a/examples/resources/snowflake_grant_privileges_to_database_role/resource.tf b/examples/resources/snowflake_grant_privileges_to_database_role/resource.tf new file mode 100644 index 0000000000..a5de908db6 --- /dev/null +++ b/examples/resources/snowflake_grant_privileges_to_database_role/resource.tf @@ -0,0 +1,146 @@ +resource "snowflake_database_role" "db_role" { + database = "database" + name = "db_role_name" +} + +################################## +### on database privileges +################################## + +# list of privileges +resource "snowflake_grant_privileges_to_database_role" "example" { + privileges = ["CREATE", "MONITOR"] + database_role_name = "\"${snowflake_database_role.db_role.database}\".\"${snowflake_database_role.db_role.name}\"" + on_database = snowflake_database_role.db_role.database +} + +# all privileges + grant option +resource "snowflake_grant_privileges_to_database_role" "example" { + database_role_name = "\"${snowflake_database_role.db_role.database}\".\"${snowflake_database_role.db_role.name}\"" + on_database = snowflake_database_role.db_role.database + all_privileges = true + with_grant_option = true +} + +# all privileges + grant option + always apply +resource "snowflake_grant_privileges_to_database_role" "example" { + database_role_name = "\"${snowflake_database_role.db_role.database}\".\"${snowflake_database_role.db_role.name}\"" + on_database = snowflake_database_role.db_role.database + always_apply = true + all_privileges = true + with_grant_option = true +} + +################################## +### schema privileges +################################## + +# list of privileges +resource "snowflake_grant_privileges_to_database_role" "example" { + privileges = ["MODIFY", "CREATE TABLE"] + database_role_name = "\"${snowflake_database_role.db_role.database}\".\"${snowflake_database_role.db_role.name}\"" + on_schema { + schema_name = "\"${snowflake_database_role.db_role.database}\".\"my_schema\"" # note this is a fully qualified name! + } +} + +# all privileges + grant option +resource "snowflake_grant_privileges_to_database_role" "example" { + database_role_name = "\"${snowflake_database_role.db_role.database}\".\"${snowflake_database_role.db_role.name}\"" + on_schema { + schema_name = "\"${snowflake_database_role.db_role.database}\".\"my_schema\"" # note this is a fully qualified name! + } + all_privileges = true + with_grant_option = true +} + +# all schemas in database +resource "snowflake_grant_privileges_to_database_role" "example" { + privileges = ["MODIFY", "CREATE TABLE"] + database_role_name = "\"${snowflake_database_role.db_role.database}\".\"${snowflake_database_role.db_role.name}\"" + on_schema { + all_schemas_in_database = snowflake_database_role.db_role.database + } +} + +# future schemas in database +resource "snowflake_grant_privileges_to_database_role" "example" { + privileges = ["MODIFY", "CREATE TABLE"] + database_role_name = "\"${snowflake_database_role.db_role.database}\".\"${snowflake_database_role.db_role.name}\"" + on_schema { + future_schemas_in_database = snowflake_database_role.db_role.database + } +} + +################################## +### schema object privileges +################################## + +# list of privileges +resource "snowflake_grant_privileges_to_database_role" "example" { + privileges = ["SELECT", "REFERENCES"] + database_role_name = "\"${snowflake_database_role.db_role.database}\".\"${snowflake_database_role.db_role.name}\"" + on_schema_object { + object_type = "VIEW" + object_name = "\"${snowflake_database_role.db_role.database}\".\"my_schema\".\"my_view\"" # note this is a fully qualified name! + } +} + +# all privileges + grant option +resource "snowflake_grant_privileges_to_database_role" "example" { + database_role_name = "\"${snowflake_database_role.db_role.database}\".\"${snowflake_database_role.db_role.name}\"" + on_schema_object { + object_type = "VIEW" + object_name = "\"${snowflake_database_role.db_role.database}\".\"my_schema\".\"my_view\"" # note this is a fully qualified name! + } + all_privileges = true + with_grant_option = true +} + +# all in database +resource "snowflake_grant_privileges_to_database_role" "example" { + privileges = ["SELECT", "INSERT"] + database_role_name = "\"${snowflake_database_role.db_role.database}\".\"${snowflake_database_role.db_role.name}\"" + on_schema_object { + all { + object_type_plural = "TABLES" + in_database = snowflake_database_role.db_role.database + } + } +} + +# all in schema +resource "snowflake_grant_privileges_to_database_role" "example" { + privileges = ["SELECT", "INSERT"] + database_role_name = "\"${snowflake_database_role.db_role.database}\".\"${snowflake_database_role.db_role.name}\"" + on_schema_object { + all { + object_type_plural = "TABLES" + in_schema = "\"${snowflake_database_role.db_role.database}\".\"my_schema\"" # note this is a fully qualified name! + } + } +} + +# future in database +resource "snowflake_grant_privileges_to_database_role" "example" { + privileges = ["SELECT", "INSERT"] + database_role_name = "\"${snowflake_database_role.db_role.database}\".\"${snowflake_database_role.db_role.name}\"" + on_schema_object { + future { + object_type_plural = "TABLES" + in_database = snowflake_database_role.db_role.database + } + } +} + +# future in schema +resource "snowflake_grant_privileges_to_database_role" "example" { + privileges = ["SELECT", "INSERT"] + database_role_name = "\"${snowflake_database_role.db_role.database}\".\"${snowflake_database_role.db_role.name}\"" + on_schema_object { + future { + object_type_plural = "TABLES" + in_schema = "\"${snowflake_database_role.db_role.database}\".\"my_schema\"" # note this is a fully qualified name! + } + } +} diff --git a/framework/provider/resource_monitor_resource.go b/framework/provider/resource_monitor_resource.go index 921387a376..588c509e19 100644 --- a/framework/provider/resource_monitor_resource.go +++ b/framework/provider/resource_monitor_resource.go @@ -736,7 +736,7 @@ func (r *ResourceMonitorResource) update(ctx context.Context, plan *resourceMoni for _, e := range elements { notifiedUsers = append(notifiedUsers, sdk.NotifiedUser{Name: e.ValueString()}) } - opts.NotifyUsers = &sdk.NotifyUsers{ + opts.Set.NotifyUsers = &sdk.NotifyUsers{ Users: notifiedUsers, } } diff --git a/pkg/acceptance/testing.go b/pkg/acceptance/testing.go index 0cd29e41f4..d6725ca491 100644 --- a/pkg/acceptance/testing.go +++ b/pkg/acceptance/testing.go @@ -91,3 +91,11 @@ func ConfigurationDirectory(directory string) func(config.TestStepConfigRequest) return filepath.Join("testdata", directory) } } + +// ConfigurationInnerDirectory is similar to ConfigurationSameAsStepN, but instead of index-based directories, +// you can choose a particular one by name. +func ConfigurationInnerDirectory(innerDirectory string) func(config.TestStepConfigRequest) string { + return func(req config.TestStepConfigRequest) string { + return filepath.Join("testdata", req.TestName, innerDirectory) + } +} diff --git a/pkg/provider/provider.go b/pkg/provider/provider.go index f51cb5e706..00486c65bb 100644 --- a/pkg/provider/provider.go +++ b/pkg/provider/provider.go @@ -105,7 +105,7 @@ func Provider() *schema.Provider { }, "authenticator": { Type: schema.TypeString, - Description: "Specifies the [authentication type](https://pkg.go.dev/github.com/snowflakedb/gosnowflake#AuthType) to use when connecting to Snowflake. Valid values include: Snowflake, OAuth, ExternalBrowser, Okta, JWT, TokenAccessor, UsernamePasswordMFA. Can also be sourced from the `SNOWFLAKE_AUTHENTICATOR` environment variable.", + Description: "Specifies the [authentication type](https://pkg.go.dev/github.com/snowflakedb/gosnowflake#AuthType) to use when connecting to Snowflake. Valid values include: Snowflake, OAuth, ExternalBrowser, Okta, JWT, TokenAccessor, UsernamePasswordMFA. Can also be sourced from the `SNOWFLAKE_AUTHENTICATOR` environment variable. It has to be set explicitly to JWT for private key authentication.", Optional: true, DefaultFunc: schema.EnvDefaultFunc("SNOWFLAKE_AUTHENTICATOR", nil), ValidateFunc: func(val interface{}, key string) (warns []string, errs []error) { @@ -443,7 +443,10 @@ func getResources() map[string]*schema.Resource { "snowflake_failover_group": resources.FailoverGroup(), "snowflake_file_format": resources.FileFormat(), "snowflake_function": resources.Function(), + "snowflake_grant_account_role": resources.GrantAccountRole(), + "snowflake_grant_database_role": resources.GrantDatabaseRole(), "snowflake_grant_privileges_to_role": resources.GrantPrivilegesToRole(), + "snowflake_grant_privileges_to_database_role": resources.GrantPrivilegesToDatabaseRole(), "snowflake_managed_account": resources.ManagedAccount(), "snowflake_masking_policy": resources.MaskingPolicy(), "snowflake_materialized_view": resources.MaterializedView(), diff --git a/pkg/resources/dynamic_table_acceptance_test.go b/pkg/resources/dynamic_table_acceptance_test.go index a685561706..6a59b1c37d 100644 --- a/pkg/resources/dynamic_table_acceptance_test.go +++ b/pkg/resources/dynamic_table_acceptance_test.go @@ -301,6 +301,7 @@ func testAccCheckDynamicTableDestroy(s *terraform.State) error { return nil } +// TODO [SNOW-926148]: currently this dynamic table is not cleaned in the test; it is removed when the whole database is removed - this currently happens in a sweeper func createDynamicTableOutsideTerraform(t *testing.T, schemaName string, dynamicTableName string, query string) { t.Helper() client, err := sdk.NewDefaultClient() diff --git a/pkg/resources/email_notification_integration_acceptance_test.go b/pkg/resources/email_notification_integration_acceptance_test.go index ace8bcaf9c..11c29a06b2 100644 --- a/pkg/resources/email_notification_integration_acceptance_test.go +++ b/pkg/resources/email_notification_integration_acceptance_test.go @@ -10,7 +10,7 @@ import ( "github.com/hashicorp/terraform-plugin-testing/helper/resource" ) -// TODO: use email of our service user +// TODO [SNOW-1007539]: use email of our service user func TestAcc_EmailNotificationIntegration(t *testing.T) { emailIntegrationName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) verifiedEmail := "artur.sawicki@snowflake.com" diff --git a/pkg/resources/external_table.go b/pkg/resources/external_table.go index 7a4aad4e8a..f9d73c4237 100644 --- a/pkg/resources/external_table.go +++ b/pkg/resources/external_table.go @@ -6,6 +6,8 @@ import ( "fmt" "log" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -30,6 +32,13 @@ var externalTableSchema = map[string]*schema.Schema{ ForceNew: true, Description: "The database in which to create the external table.", }, + "table_format": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: `Identifies the external table table type. For now, only "delta" for Delta Lake table format is supported.`, + ValidateFunc: validation.StringInSlice([]string{"delta"}, true), + }, "column": { Type: schema.TypeList, Required: true, @@ -152,7 +161,6 @@ func CreateExternalTable(d *schema.ResourceData, meta any) error { id := sdk.NewSchemaObjectIdentifier(database, schema, name) location := d.Get("location").(string) fileFormat := d.Get("file_format").(string) - req := sdk.NewCreateExternalTableRequest(id, location).WithRawFileFormat(&fileFormat) tableColumns := d.Get("column").([]any) columnRequests := make([]*sdk.ExternalTableColumnRequest, len(tableColumns)) @@ -161,51 +169,82 @@ func CreateExternalTable(d *schema.ResourceData, meta any) error { for key, val := range col.(map[string]any) { columnDef[key] = val.(string) } - - name := columnDef["name"] - dataTypeString := columnDef["type"] - dataType, err := sdk.ToDataType(dataTypeString) - if err != nil { - return fmt.Errorf(`failed to parse datatype: %s`, dataTypeString) - } - as := columnDef["as"] - columnRequests[i] = sdk.NewExternalTableColumnRequest(name, dataType, as) + columnRequests[i] = sdk.NewExternalTableColumnRequest( + columnDef["name"], + sdk.DataType(columnDef["type"]), + columnDef["as"], + ) } - req.WithColumns(columnRequests) - - req.WithAutoRefresh(sdk.Bool(d.Get("auto_refresh").(bool))) - req.WithRefreshOnCreate(sdk.Bool(d.Get("refresh_on_create").(bool))) - req.WithCopyGrants(sdk.Bool(d.Get("copy_grants").(bool))) + autoRefresh := sdk.Bool(d.Get("auto_refresh").(bool)) + refreshOnCreate := sdk.Bool(d.Get("refresh_on_create").(bool)) + copyGrants := sdk.Bool(d.Get("copy_grants").(bool)) + var partitionBy []string if v, ok := d.GetOk("partition_by"); ok { - partitionBy := expandStringList(v.([]any)) - req.WithPartitionBy(partitionBy) + partitionBy = expandStringList(v.([]any)) } + var pattern *string if v, ok := d.GetOk("pattern"); ok { - req.WithPattern(sdk.String(v.(string))) + pattern = sdk.String(v.(string)) } + var awsSnsTopic *string if v, ok := d.GetOk("aws_sns_topic"); ok { - req.WithAwsSnsTopic(sdk.String(v.(string))) + awsSnsTopic = sdk.String(v.(string)) } + var comment *string if v, ok := d.GetOk("comment"); ok { - req.WithComment(sdk.String(v.(string))) + comment = sdk.String(v.(string)) } + var tagAssociationRequests []*sdk.TagAssociationRequest if _, ok := d.GetOk("tag"); ok { tagAssociations := getPropertyTags(d, "tag") - tagAssociationRequests := make([]*sdk.TagAssociationRequest, len(tagAssociations)) + tagAssociationRequests = make([]*sdk.TagAssociationRequest, len(tagAssociations)) for i, t := range tagAssociations { tagAssociationRequests[i] = sdk.NewTagAssociationRequest(t.Name, t.Value) } - req.WithTag(tagAssociationRequests) } - if err := client.ExternalTables.Create(ctx, req); err != nil { - return err + switch { + case d.Get("table_format").(string) == "delta": + err := client.ExternalTables.CreateDeltaLake( + ctx, + sdk.NewCreateDeltaLakeExternalTableRequest(id, location). + WithColumns(columnRequests). + WithPartitionBy(partitionBy). + WithRefreshOnCreate(refreshOnCreate). + WithAutoRefresh(autoRefresh). + WithRawFileFormat(&fileFormat). + WithCopyGrants(copyGrants). + WithComment(comment). + WithTag(tagAssociationRequests), + ) + if err != nil { + return err + } + default: + err := client.ExternalTables.Create( + ctx, + sdk.NewCreateExternalTableRequest(id, location). + WithColumns(columnRequests). + WithPartitionBy(partitionBy). + WithRefreshOnCreate(refreshOnCreate). + WithAutoRefresh(autoRefresh). + WithPattern(pattern). + WithRawFileFormat(&fileFormat). + WithAwsSnsTopic(awsSnsTopic). + WithCopyGrants(copyGrants). + WithComment(comment). + WithTag(tagAssociationRequests), + ) + if err != nil { + return err + } } + d.SetId(helpers.EncodeSnowflakeID(id)) return ReadExternalTable(d, meta) diff --git a/pkg/resources/external_table_acceptance_test.go b/pkg/resources/external_table_acceptance_test.go index 1cb407b126..556eb49b70 100644 --- a/pkg/resources/external_table_acceptance_test.go +++ b/pkg/resources/external_table_acceptance_test.go @@ -3,11 +3,17 @@ package resources_test import ( "context" "database/sql" + "encoding/json" "fmt" + "log" "os" + "slices" "strings" "testing" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/stretchr/testify/require" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" "github.com/hashicorp/terraform-plugin-testing/config" "github.com/hashicorp/terraform-plugin-testing/terraform" @@ -19,27 +25,128 @@ import ( ) func TestAcc_ExternalTable_basic(t *testing.T) { - env := os.Getenv("SKIP_EXTERNAL_TABLE_TEST") - if env != "" { - t.Skip("Skipping TestAcc_ExternalTable") + shouldSkip, awsBucketURL, awsKeyId, awsSecretKey := externalTableTestEnvs() + if shouldSkip { + t.Skip("Skipping TestAcc_ExternalTable_basic") } + name := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) - bucketURL := os.Getenv("AWS_EXTERNAL_BUCKET_URL") - if bucketURL == "" { - t.Skip("Skipping TestAcc_ExternalTable") + resourceName := "snowflake_external_table.test_table" + + innerDirectory := "/external_tables_test_data/" + configVariables := map[string]config.Variable{ + "name": config.StringVariable(name), + "location": config.StringVariable(awsBucketURL), + "aws_key_id": config.StringVariable(awsKeyId), + "aws_secret_key": config.StringVariable(awsSecretKey), + "database": config.StringVariable(acc.TestDatabaseName), + "schema": config.StringVariable(acc.TestSchemaName), } - roleName := os.Getenv("AWS_EXTERNAL_ROLE_NAME") - if roleName == "" { - t.Skip("Skipping TestAcc_ExternalTable") + + data, err := json.Marshal([]struct { + Name string `json:"name"` + Age int `json:"age"` + }{ + { + Name: "one", + Age: 11, + }, + { + Name: "two", + Age: 22, + }, + { + Name: "three", + Age: 33, + }, + }) + require.NoError(t, err) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + PreCheck: func() { acc.TestAccPreCheck(t) }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: testAccCheckExternalTableDestroy, + Steps: []resource.TestStep{ + { + ConfigDirectory: config.TestStepDirectory(), + ConfigVariables: configVariables, + }, + { + PreConfig: func() { + publishExternalTablesTestData(sdk.NewSchemaObjectIdentifier(acc.TestDatabaseName, acc.TestSchemaName, name), data) + }, + ConfigDirectory: config.TestStepDirectory(), + ConfigVariables: configVariables, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "name", name), + resource.TestCheckResourceAttr(resourceName, "database", acc.TestDatabaseName), + resource.TestCheckResourceAttr(resourceName, "schema", acc.TestSchemaName), + resource.TestCheckResourceAttr(resourceName, "location", fmt.Sprintf(`@"%s"."%s"."%s"%s`, acc.TestDatabaseName, acc.TestSchemaName, name, innerDirectory)), + resource.TestCheckResourceAttr(resourceName, "file_format", "TYPE = JSON, STRIP_OUTER_ARRAY = TRUE"), + resource.TestCheckResourceAttr(resourceName, "comment", "Terraform acceptance test"), + resource.TestCheckResourceAttr(resourceName, "column.#", "2"), + resource.TestCheckResourceAttr(resourceName, "column.0.name", "name"), + resource.TestCheckResourceAttr(resourceName, "column.0.type", "string"), + resource.TestCheckResourceAttr(resourceName, "column.0.as", "value:name::string"), + resource.TestCheckResourceAttr(resourceName, "column.1.name", "age"), + resource.TestCheckResourceAttr(resourceName, "column.1.type", "number"), + resource.TestCheckResourceAttr(resourceName, "column.1.as", "value:age::number"), + ), + }, + { + ConfigDirectory: acc.ConfigurationSameAsStepN(2), + ConfigVariables: configVariables, + Check: externalTableContainsData(name, func(rows []map[string]*any) bool { + expectedNames := []string{"one", "two", "three"} + names := make([]string, 3) + for i, row := range rows { + nameValue, ok := row["NAME"] + if !ok { + return false + } + + if nameValue == nil { + return false + } + + nameStringValue, ok := (*nameValue).(string) + if !ok { + return false + } + + names[i] = nameStringValue + } + + return !slices.ContainsFunc(expectedNames, func(expectedName string) bool { + return !slices.Contains(names, expectedName) + }) + }), + }, + }, + }) +} + +// proves https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2310 is fixed +func TestAcc_ExternalTable_CorrectDataTypes(t *testing.T) { + shouldSkip, awsBucketURL, awsKeyId, awsSecretKey := externalTableTestEnvs() + if shouldSkip { + t.Skip("Skipping TestAcc_ExternalTable_CorrectDataTypes") } + + name := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) resourceName := "snowflake_external_table.test_table" + innerDirectory := "/external_tables_test_data/" configVariables := map[string]config.Variable{ - "name": config.StringVariable(name), - "location": config.StringVariable(bucketURL), - "aws_arn": config.StringVariable(roleName), - "database": config.StringVariable(acc.TestDatabaseName), - "schema": config.StringVariable(acc.TestSchemaName), + "name": config.StringVariable(name), + "location": config.StringVariable(awsBucketURL), + "aws_key_id": config.StringVariable(awsKeyId), + "aws_secret_key": config.StringVariable(awsSecretKey), + "database": config.StringVariable(acc.TestDatabaseName), + "schema": config.StringVariable(acc.TestSchemaName), } resource.Test(t, resource.TestCase{ @@ -53,26 +160,286 @@ func TestAcc_ExternalTable_basic(t *testing.T) { { ConfigDirectory: config.TestNameDirectory(), ConfigVariables: configVariables, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PostApplyPostRefresh: []plancheck.PlanCheck{ + plancheck.ExpectEmptyPlan(), + }, + }, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(resourceName, "name", name), resource.TestCheckResourceAttr(resourceName, "database", acc.TestDatabaseName), resource.TestCheckResourceAttr(resourceName, "schema", acc.TestSchemaName), - resource.TestCheckResourceAttr(resourceName, "location", fmt.Sprintf(`@"%s"."%s"."%s"`, acc.TestDatabaseName, acc.TestSchemaName, name)), - resource.TestCheckResourceAttr(resourceName, "file_format", "TYPE = CSV"), + resource.TestCheckResourceAttr(resourceName, "location", fmt.Sprintf(`@"%s"."%s"."%s"%s`, acc.TestDatabaseName, acc.TestSchemaName, name, innerDirectory)), + resource.TestCheckResourceAttr(resourceName, "file_format", "TYPE = JSON, STRIP_OUTER_ARRAY = TRUE"), resource.TestCheckResourceAttr(resourceName, "comment", "Terraform acceptance test"), resource.TestCheckResourceAttr(resourceName, "column.#", "2"), - resource.TestCheckResourceAttr(resourceName, "column[0].name", "column1"), - resource.TestCheckResourceAttr(resourceName, "column[0].type", "STRING"), - resource.TestCheckResourceAttr(resourceName, "column[0].as", "TO_VARCHAR(TO_TIMESTAMP_NTZ(value:unix_timestamp_property::NUMBER, 3), 'yyyy-mm-dd-hh')"), - resource.TestCheckResourceAttr(resourceName, "column[1].name", "column2"), - resource.TestCheckResourceAttr(resourceName, "column[1].type", "TIMESTAMP_NTZ(9)"), - resource.TestCheckResourceAttr(resourceName, "column[1].as", "($1:\"CreatedDate\"::timestamp)"), + resource.TestCheckResourceAttr(resourceName, "column.0.name", "name"), + resource.TestCheckResourceAttr(resourceName, "column.0.type", "varchar(200)"), + resource.TestCheckResourceAttr(resourceName, "column.0.as", "value:name::string"), + resource.TestCheckResourceAttr(resourceName, "column.1.name", "age"), + resource.TestCheckResourceAttr(resourceName, "column.1.type", "number(2, 2)"), + resource.TestCheckResourceAttr(resourceName, "column.1.as", "value:age::number"), + expectTableToHaveColumnDataTypes(name, []sdk.DataType{ + sdk.DataTypeVariant, + "VARCHAR(200)", + "NUMBER(2,2)", + }), + ), + }, + }, + }) +} + +// proves https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2293 is fixed +func TestAcc_ExternalTable_CanCreateWithPartitions(t *testing.T) { + shouldSkip, awsBucketURL, awsKeyId, awsSecretKey := externalTableTestEnvs() + if shouldSkip { + t.Skip("Skipping TestAcc_ExternalTable_CanCreateWithPartitions") + } + + name := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) + resourceName := "snowflake_external_table.test_table" + + innerDirectory := "/external_tables_test_data/" + configVariables := map[string]config.Variable{ + "name": config.StringVariable(name), + "location": config.StringVariable(awsBucketURL), + "aws_key_id": config.StringVariable(awsKeyId), + "aws_secret_key": config.StringVariable(awsSecretKey), + "database": config.StringVariable(acc.TestDatabaseName), + "schema": config.StringVariable(acc.TestSchemaName), + } + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + PreCheck: func() { acc.TestAccPreCheck(t) }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: testAccCheckExternalTableDestroy, + Steps: []resource.TestStep{ + { + ConfigDirectory: config.TestNameDirectory(), + ConfigVariables: configVariables, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PostApplyPostRefresh: []plancheck.PlanCheck{ + plancheck.ExpectEmptyPlan(), + }, + }, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "name", name), + resource.TestCheckResourceAttr(resourceName, "database", acc.TestDatabaseName), + resource.TestCheckResourceAttr(resourceName, "schema", acc.TestSchemaName), + resource.TestCheckResourceAttr(resourceName, "location", fmt.Sprintf(`@"%s"."%s"."%s"%s`, acc.TestDatabaseName, acc.TestSchemaName, name, innerDirectory)), + resource.TestCheckResourceAttr(resourceName, "file_format", "TYPE = JSON, STRIP_OUTER_ARRAY = TRUE"), + resource.TestCheckResourceAttr(resourceName, "comment", "Terraform acceptance test"), + resource.TestCheckResourceAttr(resourceName, "partition_by.#", "1"), + resource.TestCheckResourceAttr(resourceName, "partition_by.0", "filename"), + resource.TestCheckResourceAttr(resourceName, "column.#", "3"), + resource.TestCheckResourceAttr(resourceName, "column.0.name", "filename"), + resource.TestCheckResourceAttr(resourceName, "column.0.type", "string"), + resource.TestCheckResourceAttr(resourceName, "column.0.as", "metadata$filename"), + resource.TestCheckResourceAttr(resourceName, "column.1.name", "name"), + resource.TestCheckResourceAttr(resourceName, "column.1.type", "varchar(200)"), + resource.TestCheckResourceAttr(resourceName, "column.1.as", "value:name::string"), + resource.TestCheckResourceAttr(resourceName, "column.2.name", "age"), + resource.TestCheckResourceAttr(resourceName, "column.2.type", "number(2, 2)"), + resource.TestCheckResourceAttr(resourceName, "column.2.as", "value:age::number"), + expectTableDDLContains(name, "partition by (FILENAME)"), ), }, }, }) } +// proves https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1564 is implemented +func TestAcc_ExternalTable_DeltaLake(t *testing.T) { + shouldSkip, awsBucketURL, awsKeyId, awsSecretKey := externalTableTestEnvs() + if shouldSkip { + t.Skip("Skipping TestAcc_ExternalTable_DeltaLake") + } + + name := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) + resourceName := "snowflake_external_table.test_table" + + innerDirectory := "/external_tables_test_data/" + configVariables := map[string]config.Variable{ + "name": config.StringVariable(name), + "location": config.StringVariable(awsBucketURL), + "aws_key_id": config.StringVariable(awsKeyId), + "aws_secret_key": config.StringVariable(awsSecretKey), + "database": config.StringVariable(acc.TestDatabaseName), + "schema": config.StringVariable(acc.TestSchemaName), + } + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + PreCheck: func() { acc.TestAccPreCheck(t) }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: testAccCheckExternalTableDestroy, + Steps: []resource.TestStep{ + { + ConfigDirectory: config.TestNameDirectory(), + ConfigVariables: configVariables, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PostApplyPostRefresh: []plancheck.PlanCheck{ + plancheck.ExpectEmptyPlan(), + }, + }, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "name", name), + resource.TestCheckResourceAttr(resourceName, "database", acc.TestDatabaseName), + resource.TestCheckResourceAttr(resourceName, "schema", acc.TestSchemaName), + resource.TestCheckResourceAttr(resourceName, "location", fmt.Sprintf(`@"%s"."%s"."%s"%s`, acc.TestDatabaseName, acc.TestSchemaName, name, innerDirectory)), + resource.TestCheckResourceAttr(resourceName, "file_format", "TYPE = PARQUET"), + resource.TestCheckResourceAttr(resourceName, "comment", "Terraform acceptance test"), + resource.TestCheckResourceAttr(resourceName, "table_format", "delta"), + resource.TestCheckResourceAttr(resourceName, "partition_by.#", "1"), + resource.TestCheckResourceAttr(resourceName, "partition_by.0", "filename"), + resource.TestCheckResourceAttr(resourceName, "column.#", "2"), + resource.TestCheckResourceAttr(resourceName, "column.0.name", "filename"), + resource.TestCheckResourceAttr(resourceName, "column.0.type", "string"), + resource.TestCheckResourceAttr(resourceName, "column.0.as", "metadata$filename"), + resource.TestCheckResourceAttr(resourceName, "column.1.name", "name"), + resource.TestCheckResourceAttr(resourceName, "column.1.type", "string"), + resource.TestCheckResourceAttr(resourceName, "column.1.as", "value:name::string"), + func(state *terraform.State) error { + client := sdk.NewClientFromDB(acc.TestAccProvider.Meta().(*sql.DB)) + ctx := context.Background() + id := sdk.NewSchemaObjectIdentifier(acc.TestDatabaseName, acc.TestSchemaName, name) + result, err := client.ExternalTables.ShowByID(ctx, sdk.NewShowExternalTableByIDRequest(id)) + if err != nil { + return err + } + if result.TableFormat != "DELTA" { + return fmt.Errorf("expeted table_format: DELTA, got: %s", result.TableFormat) + } + return nil + }, + ), + }, + }, + }) +} + +func externalTableTestEnvs() (bool, string, string, string) { + shouldSkip := os.Getenv("SKIP_EXTERNAL_TABLE_TEST") + awsBucketURL := os.Getenv("AWS_EXTERNAL_BUCKET_URL") + awsKeyId := os.Getenv("AWS_EXTERNAL_KEY_ID") + awsSecretKey := os.Getenv("AWS_EXTERNAL_SECRET_KEY") + return shouldSkip != "" || awsBucketURL == "" || awsKeyId == "" || awsSecretKey == "", awsBucketURL, awsKeyId, awsSecretKey +} + +func externalTableContainsData(name string, contains func(rows []map[string]*any) bool) func(state *terraform.State) error { + return func(state *terraform.State) error { + client := sdk.NewClientFromDB(acc.TestAccProvider.Meta().(*sql.DB)) + ctx := context.Background() + id := sdk.NewSchemaObjectIdentifier(acc.TestDatabaseName, acc.TestSchemaName, name) + rows, err := client.QueryUnsafe(ctx, fmt.Sprintf("select * from %s", id.FullyQualifiedName())) + if err != nil { + return err + } + + jsonRows, err := json.MarshalIndent(rows, "", " ") + if err != nil { + return err + } + log.Printf("Retrieved rows for %s: %v", id.FullyQualifiedName(), string(jsonRows)) + + if !contains(rows) { + return fmt.Errorf("unexpected data returned by external table %s", id.FullyQualifiedName()) + } + + return nil + } +} + +func publishExternalTablesTestData(stageName sdk.SchemaObjectIdentifier, data []byte) { + client, err := sdk.NewDefaultClient() + if err != nil { + log.Fatal(err) + } + ctx := context.Background() + + _, err = client.ExecForTests(ctx, fmt.Sprintf(`copy into @%s/external_tables_test_data/test_data from (select parse_json('%s')) overwrite = true`, stageName.FullyQualifiedName(), string(data))) + if err != nil { + log.Fatal(err) + } +} + +func expectTableToHaveColumnDataTypes(tableName string, expectedDataTypes []sdk.DataType) func(s *terraform.State) error { + return func(s *terraform.State) error { + client := sdk.NewClientFromDB(acc.TestAccProvider.Meta().(*sql.DB)) + ctx := context.Background() + id := sdk.NewSchemaObjectIdentifier(acc.TestDatabaseName, acc.TestSchemaName, tableName) + columnsDesc, err := client.ExternalTables.DescribeColumns(ctx, sdk.NewDescribeExternalTableColumnsRequest(id)) + if err != nil { + return err + } + + actualTableDataTypes := make([]sdk.DataType, len(columnsDesc)) + for i, desc := range columnsDesc { + actualTableDataTypes[i] = desc.Type + } + + slices.SortFunc(expectedDataTypes, func(a, b sdk.DataType) int { + return strings.Compare(string(a), string(b)) + }) + slices.SortFunc(actualTableDataTypes, func(a, b sdk.DataType) int { + return strings.Compare(string(a), string(b)) + }) + + if !slices.Equal(expectedDataTypes, actualTableDataTypes) { + return fmt.Errorf("expected table %s to have columns with data types: %v, got: %v", tableName, expectedDataTypes, actualTableDataTypes) + } + + return nil + } +} + +func expectTableDDLContains(tableName string, substr string) func(s *terraform.State) error { + return func(s *terraform.State) error { + client := sdk.NewClientFromDB(acc.TestAccProvider.Meta().(*sql.DB)) + ctx := context.Background() + id := sdk.NewSchemaObjectIdentifier(acc.TestDatabaseName, acc.TestSchemaName, tableName) + + rows, err := client.QueryUnsafe(ctx, fmt.Sprintf("select get_ddl('table', '%s')", id.FullyQualifiedName())) + if err != nil { + return err + } + + if len(rows) != 1 { + return fmt.Errorf("unexpectedly returned more than one row: %d", len(rows)) + } + + row := rows[0] + + if len(row) != 1 { + return fmt.Errorf("unexpectedly returned more than one columns: %d", len(row)) + } + + for _, v := range row { + if v == nil { + return fmt.Errorf("unexpectedly row value of ddl is nil") + } + + ddl, ok := (*v).(string) + + if !ok { + return fmt.Errorf("unexpectedly ddl is not type string") + } + + if !strings.Contains(ddl, substr) { + return fmt.Errorf("expected '%s' to be a substring of '%s'", substr, ddl) + } + } + + return nil + } +} + func testAccCheckExternalTableDestroy(s *terraform.State) error { db := acc.TestAccProvider.Meta().(*sql.DB) client := sdk.NewClientFromDB(db) diff --git a/pkg/resources/grant_account_role.go b/pkg/resources/grant_account_role.go new file mode 100644 index 0000000000..65e5f4952a --- /dev/null +++ b/pkg/resources/grant_account_role.go @@ -0,0 +1,181 @@ +package resources + +import ( + "context" + "database/sql" + "fmt" + "log" + "strings" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +var grantAccountRoleSchema = map[string]*schema.Schema{ + "role_name": { + Type: schema.TypeString, + Required: true, + Description: "The fully qualified name of the role which will be granted to the user or parent role.", + ForceNew: true, + ValidateDiagFunc: IsValidIdentifier[sdk.AccountObjectIdentifier](), + }, + "user_name": { + Type: schema.TypeString, + Optional: true, + Description: "The fully qualified name of the user on which specified role will be granted.", + ForceNew: true, + ValidateDiagFunc: IsValidIdentifier[sdk.AccountObjectIdentifier](), + ExactlyOneOf: []string{ + "user_name", + "parent_role_name", + }, + }, + "parent_role_name": { + Type: schema.TypeString, + Optional: true, + Description: "The fully qualified name of the parent role which will create a parent-child relationship between the roles.", + ForceNew: true, + ValidateDiagFunc: IsValidIdentifier[sdk.AccountObjectIdentifier](), + ExactlyOneOf: []string{ + "user_name", + "parent_role_name", + }, + }, +} + +func GrantAccountRole() *schema.Resource { + return &schema.Resource{ + Create: CreateGrantAccountRole, + Read: ReadGrantAccountRole, + Delete: DeleteGrantAccountRole, + Schema: grantAccountRoleSchema, + Importer: &schema.ResourceImporter{ + StateContext: func(ctx context.Context, d *schema.ResourceData, m interface{}) ([]*schema.ResourceData, error) { + parts := strings.Split(d.Id(), helpers.IDDelimiter) + if len(parts) != 3 { + return nil, fmt.Errorf("invalid ID specified: %v, expected ||", d.Id()) + } + if err := d.Set("role_name", strings.Trim(parts[0], "\"")); err != nil { + return nil, err + } + switch parts[1] { + case "ROLE": + if err := d.Set("parent_role_name", strings.Trim(parts[2], "\"")); err != nil { + return nil, err + } + case "USER": + if err := d.Set("user_name", strings.Trim(parts[2], "\"")); err != nil { + return nil, err + } + default: + return nil, fmt.Errorf("invalid object type specified: %v, expected ROLE or USER", parts[1]) + } + + return []*schema.ResourceData{d}, nil + }, + }, + } +} + +// CreateGrantAccountRole implements schema.CreateFunc. +func CreateGrantAccountRole(d *schema.ResourceData, meta interface{}) error { + db := meta.(*sql.DB) + client := sdk.NewClientFromDB(db) + ctx := context.Background() + roleName := d.Get("role_name").(string) + roleIdentifier := sdk.NewAccountObjectIdentifierFromFullyQualifiedName(roleName) + // format of snowflakeResourceID is || + var snowflakeResourceID string + if parentRoleName, ok := d.GetOk("parent_role_name"); ok && parentRoleName.(string) != "" { + parentRoleIdentifier := sdk.NewAccountObjectIdentifierFromFullyQualifiedName(parentRoleName.(string)) + snowflakeResourceID = helpers.EncodeSnowflakeID(roleIdentifier.FullyQualifiedName(), sdk.ObjectTypeRole.String(), parentRoleIdentifier.FullyQualifiedName()) + req := sdk.NewGrantRoleRequest(roleIdentifier, sdk.GrantRole{ + Role: &parentRoleIdentifier, + }) + if err := client.Roles.Grant(ctx, req); err != nil { + return err + } + } else if userName, ok := d.GetOk("user_name"); ok && userName.(string) != "" { + userIdentifier := sdk.NewAccountObjectIdentifierFromFullyQualifiedName(userName.(string)) + snowflakeResourceID = helpers.EncodeSnowflakeID(roleIdentifier.FullyQualifiedName(), sdk.ObjectTypeUser.String(), userIdentifier.FullyQualifiedName()) + req := sdk.NewGrantRoleRequest(roleIdentifier, sdk.GrantRole{ + User: &userIdentifier, + }) + if err := client.Roles.Grant(ctx, req); err != nil { + return err + } + } else { + return fmt.Errorf("invalid role grant specified: %v", d) + } + d.SetId(snowflakeResourceID) + return ReadGrantAccountRole(d, meta) +} + +func ReadGrantAccountRole(d *schema.ResourceData, meta interface{}) error { + db := meta.(*sql.DB) + client := sdk.NewClientFromDB(db) + parts := strings.Split(d.Id(), helpers.IDDelimiter) + if len(parts) != 3 { + return fmt.Errorf("invalid ID specified: %v, expected ||", d.Id()) + } + roleName := parts[0] + roleIdentifier := sdk.NewAccountObjectIdentifierFromFullyQualifiedName(roleName) + objectType := parts[1] + targetIdentifier := parts[2] + ctx := context.Background() + grants, err := client.Grants.Show(ctx, &sdk.ShowGrantOptions{ + Of: &sdk.ShowGrantsOf{ + Role: roleIdentifier, + }, + }) + if err != nil { + log.Printf("[DEBUG] role (%s) not found", roleIdentifier.FullyQualifiedName()) + d.SetId("") + return nil + } + + var found bool + for _, grant := range grants { + if grant.GrantedTo == sdk.ObjectType(objectType) { + if grant.GranteeName.FullyQualifiedName() == targetIdentifier { + found = true + break + } + } + } + if !found { + log.Printf("[DEBUG] role grant (%s) not found", d.Id()) + d.SetId("") + } + + return nil +} + +func DeleteGrantAccountRole(d *schema.ResourceData, meta interface{}) error { + db := meta.(*sql.DB) + client := sdk.NewClientFromDB(db) + parts := strings.Split(d.Id(), helpers.IDDelimiter) + if len(parts) != 3 { + return fmt.Errorf("invalid ID specified: %v, expected ||", d.Id()) + } + id := sdk.NewAccountObjectIdentifierFromFullyQualifiedName(parts[0]) + objectType := parts[1] + granteeName := parts[2] + ctx := context.Background() + granteeIdentifier := sdk.NewAccountObjectIdentifierFromFullyQualifiedName(granteeName) + switch objectType { + case "ROLE": + if err := client.Roles.Revoke(ctx, sdk.NewRevokeRoleRequest(id, sdk.RevokeRole{Role: &granteeIdentifier})); err != nil { + return err + } + case "USER": + if err := client.Roles.Revoke(ctx, sdk.NewRevokeRoleRequest(id, sdk.RevokeRole{User: &granteeIdentifier})); err != nil { + return err + } + default: + return fmt.Errorf("invalid object type specified: %v, expected ROLE or USER", objectType) + } + d.SetId("") + return nil +} diff --git a/pkg/resources/grant_account_role_acceptance_test.go b/pkg/resources/grant_account_role_acceptance_test.go new file mode 100644 index 0000000000..5a2e4f9e4a --- /dev/null +++ b/pkg/resources/grant_account_role_acceptance_test.go @@ -0,0 +1,134 @@ +package resources_test + +import ( + "context" + "database/sql" + "fmt" + "strings" + "testing" + + acc "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + + "github.com/hashicorp/terraform-plugin-testing/config" + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestAcc_GrantAccountRole_accountRole(t *testing.T) { + roleName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) + parentRoleName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) + resourceName := "snowflake_grant_account_role.g" + m := func() map[string]config.Variable { + return map[string]config.Variable{ + "role_name": config.StringVariable(roleName), + "parent_role_name": config.StringVariable(parentRoleName), + } + } + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + PreCheck: func() { acc.TestAccPreCheck(t) }, + CheckDestroy: testAccCheckGrantAccountRoleDestroy, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + Steps: []resource.TestStep{ + { + ConfigDirectory: config.StaticDirectory("testdata/TestAcc_GrantAccountRole/account_role"), + ConfigVariables: m(), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "role_name", roleName), + resource.TestCheckResourceAttr(resourceName, "parent_role_name", parentRoleName), + resource.TestCheckResourceAttr(resourceName, "id", fmt.Sprintf(`"%v"|ROLE|"%v"`, roleName, parentRoleName)), + ), + }, + // import + { + ConfigDirectory: config.StaticDirectory("testdata/TestAcc_GrantAccountRole/account_role"), + ConfigVariables: m(), + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAcc_GrantAccountRole_user(t *testing.T) { + roleName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) + userName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) + resourceName := "snowflake_grant_account_role.g" + m := func() map[string]config.Variable { + return map[string]config.Variable{ + "role_name": config.StringVariable(roleName), + "user_name": config.StringVariable(userName), + } + } + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + PreCheck: func() { acc.TestAccPreCheck(t) }, + CheckDestroy: testAccCheckGrantAccountRoleDestroy, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + Steps: []resource.TestStep{ + { + ConfigDirectory: config.StaticDirectory("testdata/TestAcc_GrantAccountRole/user"), + ConfigVariables: m(), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "role_name", roleName), + resource.TestCheckResourceAttr(resourceName, "user_name", userName), + resource.TestCheckResourceAttr(resourceName, "id", fmt.Sprintf(`"%v"|USER|"%v"`, roleName, userName)), + ), + }, + // import + { + ConfigDirectory: config.StaticDirectory("testdata/TestAcc_GrantAccountRole/user"), + ConfigVariables: m(), + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckGrantAccountRoleDestroy(s *terraform.State) error { + db := acc.TestAccProvider.Meta().(*sql.DB) + client := sdk.NewClientFromDB(db) + for _, rs := range s.RootModule().Resources { + if rs.Type != "snowflake_grant_account_role" { + continue + } + ctx := context.Background() + parts := strings.Split(rs.Primary.ID, "|") + roleName := parts[0] + roleIdentifier := sdk.NewAccountObjectIdentifierFromFullyQualifiedName(roleName) + objectType := parts[1] + targetIdentifier := parts[2] + grants, err := client.Grants.Show(ctx, &sdk.ShowGrantOptions{ + Of: &sdk.ShowGrantsOf{ + Role: roleIdentifier, + }, + }) + if err != nil { + return nil + } + + var found bool + for _, grant := range grants { + if grant.GrantedTo == sdk.ObjectType(objectType) { + if grant.GranteeName.FullyQualifiedName() == targetIdentifier { + found = true + break + } + } + } + if found { + return fmt.Errorf("role grant %v still exists", rs.Primary.ID) + } + } + return nil +} diff --git a/pkg/resources/grant_database_role.go b/pkg/resources/grant_database_role.go new file mode 100644 index 0000000000..c39e46de5c --- /dev/null +++ b/pkg/resources/grant_database_role.go @@ -0,0 +1,227 @@ +package resources + +import ( + "context" + "database/sql" + "fmt" + "log" + "strings" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +var grantDatabaseRoleSchema = map[string]*schema.Schema{ + "database_role_name": { + Type: schema.TypeString, + Required: true, + Description: "The fully qualified name of the database role which will be granted to share or parent role.", + ForceNew: true, + ValidateDiagFunc: IsValidIdentifier[sdk.DatabaseObjectIdentifier](), + }, + "parent_role_name": { + Type: schema.TypeString, + Optional: true, + Description: "The fully qualified name of the parent account role which will create a parent-child relationship between the roles.", + ForceNew: true, + ValidateDiagFunc: IsValidIdentifier[sdk.AccountObjectIdentifier](), + ExactlyOneOf: []string{ + "parent_role_name", + "parent_database_role_name", + "share_name", + }, + }, + "parent_database_role_name": { + Type: schema.TypeString, + Optional: true, + Description: "The fully qualified name of the parent database role which will create a parent-child relationship between the roles.", + ForceNew: true, + ValidateDiagFunc: IsValidIdentifier[sdk.DatabaseObjectIdentifier](), + ExactlyOneOf: []string{ + "parent_role_name", + "parent_database_role_name", + "share_name", + }, + }, + "share_name": { + Type: schema.TypeString, + Optional: true, + Description: "The fully qualified name of the share on which privileges will be granted.", + ForceNew: true, + ValidateDiagFunc: IsValidIdentifier[sdk.AccountObjectIdentifier](), + ExactlyOneOf: []string{ + "parent_role_name", + "parent_database_role_name", + "share_name", + }, + }, +} + +func GrantDatabaseRole() *schema.Resource { + return &schema.Resource{ + Create: CreateGrantDatabaseRole, + Read: ReadGrantDatabaseRole, + Delete: DeleteGrantDatabaseRole, + Schema: grantDatabaseRoleSchema, + Importer: &schema.ResourceImporter{ + StateContext: func(ctx context.Context, d *schema.ResourceData, m interface{}) ([]*schema.ResourceData, error) { + parts := strings.Split(d.Id(), helpers.IDDelimiter) + if len(parts) != 3 { + return nil, fmt.Errorf("invalid ID specified: %v, expected ||", d.Id()) + } + if err := d.Set("database_role_name", parts[0]); err != nil { + return nil, err + } + switch parts[1] { + case "ROLE": + if err := d.Set("parent_role_name", strings.Trim(parts[2], "\"")); err != nil { + return nil, err + } + case "DATABASE ROLE": + if err := d.Set("parent_database_role_name", parts[2]); err != nil { + return nil, err + } + case "SHARE": + if err := d.Set("share_name", strings.Trim(parts[2], "\"")); err != nil { + return nil, err + } + default: + return nil, fmt.Errorf("invalid object type specified: %v, expected ROLE, DATABASE ROLE, or SHARE", parts[1]) + } + + return []*schema.ResourceData{d}, nil + }, + }, + } +} + +// CreateGrantDatabaseRole implements schema.CreateFunc. +func CreateGrantDatabaseRole(d *schema.ResourceData, meta interface{}) error { + db := meta.(*sql.DB) + client := sdk.NewClientFromDB(db) + ctx := context.Background() + databaseRoleName := d.Get("database_role_name").(string) + databaseRoleIdentifier := sdk.NewDatabaseObjectIdentifierFromFullyQualifiedName(databaseRoleName) + // format of snowflakeResourceID is || + var snowflakeResourceID string + if parentRoleName, ok := d.GetOk("parent_role_name"); ok && parentRoleName.(string) != "" { + parentRoleIdentifier := sdk.NewAccountObjectIdentifierFromFullyQualifiedName(parentRoleName.(string)) + snowflakeResourceID = helpers.EncodeSnowflakeID(databaseRoleIdentifier.FullyQualifiedName(), sdk.ObjectTypeRole.String(), parentRoleIdentifier.FullyQualifiedName()) + req := sdk.NewGrantDatabaseRoleRequest(databaseRoleIdentifier).WithAccountRole(parentRoleIdentifier) + if err := client.DatabaseRoles.Grant(ctx, req); err != nil { + return err + } + } else if parentDatabaseRoleName, ok := d.GetOk("parent_database_role_name"); ok && parentDatabaseRoleName.(string) != "" { + parentRoleIdentifier := sdk.NewDatabaseObjectIdentifierFromFullyQualifiedName(parentDatabaseRoleName.(string)) + snowflakeResourceID = helpers.EncodeSnowflakeID(databaseRoleIdentifier.FullyQualifiedName(), sdk.ObjectTypeDatabaseRole.String(), parentRoleIdentifier.FullyQualifiedName()) + req := sdk.NewGrantDatabaseRoleRequest(databaseRoleIdentifier).WithDatabaseRole(parentRoleIdentifier) + if err := client.DatabaseRoles.Grant(ctx, req); err != nil { + return err + } + } else if shareName, ok := d.GetOk("share_name"); ok && shareName.(string) != "" { + shareIdentifier := sdk.NewAccountObjectIdentifierFromFullyQualifiedName(shareName.(string)) + snowflakeResourceID = helpers.EncodeSnowflakeID(databaseRoleIdentifier.FullyQualifiedName(), sdk.ObjectTypeShare.String(), shareIdentifier.FullyQualifiedName()) + req := sdk.NewGrantDatabaseRoleToShareRequest(databaseRoleIdentifier, shareIdentifier) + if err := client.DatabaseRoles.GrantToShare(ctx, req); err != nil { + return err + } + } + d.SetId(snowflakeResourceID) + return ReadGrantDatabaseRole(d, meta) +} + +// ReadGrantDatabaseRole implements schema.ReadFunc. +func ReadGrantDatabaseRole(d *schema.ResourceData, meta interface{}) error { + db := meta.(*sql.DB) + client := sdk.NewClientFromDB(db) + parts := strings.Split(d.Id(), helpers.IDDelimiter) + databaseRoleName := parts[0] + databaseRoleIdentifier := sdk.NewDatabaseObjectIdentifierFromFullyQualifiedName(databaseRoleName) + objectType := parts[1] + targetIdentifier := parts[2] + ctx := context.Background() + grants, err := client.Grants.Show(ctx, &sdk.ShowGrantOptions{ + Of: &sdk.ShowGrantsOf{ + DatabaseRole: databaseRoleIdentifier, + }, + }) + if err != nil { + log.Printf("[DEBUG] database role (%s) not found", databaseRoleIdentifier.FullyQualifiedName()) + d.SetId("") + return nil + } + + var found bool + for _, grant := range grants { + if grant.GrantedTo == sdk.ObjectType(objectType) { + if grant.GrantedTo == sdk.ObjectTypeRole || grant.GrantedTo == sdk.ObjectTypeShare { + if grant.GranteeName.FullyQualifiedName() == targetIdentifier { + found = true + break + } + } else { + /* + note that grantee_name is not saved as a valid identifier in the + SHOW GRANTS OF DATABASE ROLE command + for example, "ABC"."test_parent_role" is saved as ABC."test_parent_role" + or "ABC"."test_parent_role" is saved as ABC.test_parent_role + and our internal mapper thereby fails to parse it correctly, returning "ABC."test_parent_role" + so this funny string replacement is needed to make it work + */ + s := grant.GranteeName.FullyQualifiedName() + if !strings.Contains(s, "\"") { + parts := strings.Split(s, ".") + s = sdk.NewDatabaseObjectIdentifier(parts[0], parts[1]).FullyQualifiedName() + } else { + parts := strings.Split(s, "\".\"") + if len(parts) < 2 { + parts = strings.Split(s, "\".") + if len(parts) < 2 { + parts = strings.Split(s, ".\"") + } + } + s = sdk.NewDatabaseObjectIdentifier(parts[0], parts[1]).FullyQualifiedName() + } + if s == targetIdentifier { + found = true + break + } + } + } + } + if !found { + log.Printf("[DEBUG] database role grant (%s) not found", d.Id()) + d.SetId("") + } + + return nil +} + +// DeleteGrantDatabaseRole implements schema.DeleteFunc. +func DeleteGrantDatabaseRole(d *schema.ResourceData, meta interface{}) error { + db := meta.(*sql.DB) + client := sdk.NewClientFromDB(db) + + parts := strings.Split(d.Id(), "|") + id := sdk.NewDatabaseObjectIdentifierFromFullyQualifiedName(parts[0]) + objectType := parts[1] + granteeName := parts[2] + ctx := context.Background() + switch objectType { + case "ROLE": + if err := client.DatabaseRoles.Revoke(ctx, sdk.NewRevokeDatabaseRoleRequest(id).WithAccountRole(sdk.NewAccountObjectIdentifierFromFullyQualifiedName(granteeName))); err != nil { + return err + } + case "DATABASE ROLE": + if err := client.DatabaseRoles.Revoke(ctx, sdk.NewRevokeDatabaseRoleRequest(id).WithDatabaseRole(sdk.NewDatabaseObjectIdentifierFromFullyQualifiedName(granteeName))); err != nil { + return err + } + case "SHARE": + if err := client.DatabaseRoles.RevokeFromShare(ctx, sdk.NewRevokeDatabaseRoleFromShareRequest(id, sdk.NewAccountObjectIdentifierFromFullyQualifiedName(granteeName))); err != nil { + return err + } + } + d.SetId("") + return nil +} diff --git a/pkg/resources/grant_database_role_acceptance_test.go b/pkg/resources/grant_database_role_acceptance_test.go new file mode 100644 index 0000000000..edfe53c4d9 --- /dev/null +++ b/pkg/resources/grant_database_role_acceptance_test.go @@ -0,0 +1,173 @@ +package resources_test + +import ( + "context" + "database/sql" + "fmt" + "strings" + "testing" + + acc "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + "github.com/hashicorp/terraform-plugin-testing/config" + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestAcc_GrantDatabaseRole_databaseRole(t *testing.T) { + databaseRoleName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) + parentDatabaseRoleName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) + resourceName := "snowflake_grant_database_role.g" + m := func() map[string]config.Variable { + return map[string]config.Variable{ + "database": config.StringVariable(acc.TestDatabaseName), + "database_role_name": config.StringVariable(databaseRoleName), + "parent_database_role_name": config.StringVariable(parentDatabaseRoleName), + } + } + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + PreCheck: func() { acc.TestAccPreCheck(t) }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: testAccCheckGrantDatabaseRoleDestroy, + Steps: []resource.TestStep{ + { + ConfigDirectory: config.StaticDirectory("testdata/TestAcc_GrantDatabaseRole/database_role"), + ConfigVariables: m(), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "database_role_name", fmt.Sprintf(`"%v"."%v"`, acc.TestDatabaseName, databaseRoleName)), + resource.TestCheckResourceAttr(resourceName, "parent_database_role_name", fmt.Sprintf(`"%v"."%v"`, acc.TestDatabaseName, parentDatabaseRoleName)), + resource.TestCheckResourceAttr(resourceName, "id", fmt.Sprintf(`"%v"."%v"|DATABASE ROLE|"%v"."%v"`, acc.TestDatabaseName, databaseRoleName, acc.TestDatabaseName, parentDatabaseRoleName)), + ), + }, + // test import + { + ConfigDirectory: config.StaticDirectory("testdata/TestAcc_GrantDatabaseRole/database_role"), + ConfigVariables: m(), + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAcc_GrantDatabaseRole_accountRole(t *testing.T) { + databaseRoleName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) + parentRoleName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) + resourceName := "snowflake_grant_database_role.g" + m := func() map[string]config.Variable { + return map[string]config.Variable{ + "database": config.StringVariable(acc.TestDatabaseName), + "database_role_name": config.StringVariable(databaseRoleName), + "parent_role_name": config.StringVariable(parentRoleName), + } + } + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + PreCheck: func() { acc.TestAccPreCheck(t) }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: testAccCheckGrantDatabaseRoleDestroy, + Steps: []resource.TestStep{ + { + ConfigDirectory: config.StaticDirectory("testdata/TestAcc_GrantDatabaseRole/account_role"), + ConfigVariables: m(), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "database_role_name", fmt.Sprintf(`"%v"."%v"`, acc.TestDatabaseName, databaseRoleName)), + resource.TestCheckResourceAttr(resourceName, "parent_role_name", fmt.Sprintf("%v", parentRoleName)), + resource.TestCheckResourceAttr(resourceName, "id", fmt.Sprintf(`"%v"."%v"|ROLE|"%v"`, acc.TestDatabaseName, databaseRoleName, parentRoleName)), + ), + }, + // test import + { + ConfigDirectory: config.StaticDirectory("testdata/TestAcc_GrantDatabaseRole/account_role"), + ConfigVariables: m(), + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +/* +todo: once snowflake_grant_privileges_to_share is implemented. Cannot test this without having 'GRANT USAGE ON DATABASE TO SHARE ', +func TestAcc_GrantDatabaseRole_share(t *testing.T) { + databaseRoleName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) + shareName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) + resourceName := "snowflake_grant_database_role.g" + m := func() map[string]config.Variable { + return map[string]config.Variable{ + "database": config.StringVariable(acc.TestDatabaseName), + "database_role_name": config.StringVariable(databaseRoleName), + "share_name": config.StringVariable(shareName), + } + } + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + PreCheck: func() { acc.TestAccPreCheck(t) }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: testAccCheckDynamicTableDestroy, + Steps: []resource.TestStep{ + { + ConfigDirectory: config.StaticDirectory("testdata/TestAcc_GrantDatabaseRole/share"), + ConfigVariables: m(), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "database_role_name", databaseRoleName), + resource.TestCheckResourceAttr(resourceName, "share_name", shareName), + resource.TestCheckResourceAttr(resourceName, "id", fmt.Sprintf(`%v|%v|%v`, databaseRoleName, "SHARE", shareName)), + ), + }, + // test import + { + ConfigDirectory: config.StaticDirectory("testdata/TestAcc_GrantDatabaseRole/share"), + ConfigVariables: m(), + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} +*/ + +func testAccCheckGrantDatabaseRoleDestroy(s *terraform.State) error { + db := acc.TestAccProvider.Meta().(*sql.DB) + client := sdk.NewClientFromDB(db) + for _, rs := range s.RootModule().Resources { + if rs.Type != "snowflake_grant_database_role" { + continue + } + ctx := context.Background() + id := rs.Primary.ID + ids := strings.Split(id, "|") + databaseRoleName := ids[0] + objectType := ids[1] + parentRoleName := ids[2] + grants, err := client.Grants.Show(ctx, &sdk.ShowGrantOptions{ + Of: &sdk.ShowGrantsOf{ + DatabaseRole: sdk.NewDatabaseObjectIdentifierFromFullyQualifiedName(databaseRoleName), + }, + }) + if err != nil { + continue + } + for _, grant := range grants { + if grant.GrantedTo == sdk.ObjectType(objectType) { + if grant.GranteeName.FullyQualifiedName() == parentRoleName { + return fmt.Errorf("database role grant %v still exists", grant) + } + } + } + } + return nil +} diff --git a/pkg/resources/grant_privileges_to_database_role.go b/pkg/resources/grant_privileges_to_database_role.go new file mode 100644 index 0000000000..8f4b9f0cb3 --- /dev/null +++ b/pkg/resources/grant_privileges_to_database_role.go @@ -0,0 +1,980 @@ +package resources + +import ( + "context" + "database/sql" + "fmt" + "slices" + "strings" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + "github.com/hashicorp/go-cty/cty" + "github.com/hashicorp/go-uuid" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +// TODO: Handle IMPORTED PRIVILEGES privilege (after second account will be added - SNOW-976501) + +var grantPrivilegesToDatabaseRoleSchema = map[string]*schema.Schema{ + "database_role_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The fully qualified name of the database role to which privileges will be granted.", + ValidateDiagFunc: IsValidIdentifier[sdk.DatabaseObjectIdentifier](), + }, + "privileges": { + Type: schema.TypeSet, + Optional: true, + Description: "The privileges to grant on the database role.", + ExactlyOneOf: []string{ + "privileges", + "all_privileges", + }, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateDiagFunc: isNotOwnershipGrant(), + }, + }, + "all_privileges": { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: "Grant all privileges on the database role.", + ExactlyOneOf: []string{ + "privileges", + "all_privileges", + }, + }, + "with_grant_option": { + Type: schema.TypeBool, + Optional: true, + Default: false, + ForceNew: true, + Description: "If specified, allows the recipient role to grant the privileges to other roles.", + }, + "always_apply": { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: "If true, the resource will always produce a β€œplan” and on β€œapply” it will re-grant defined privileges. It is supposed to be used only in β€œgrant privileges on all X’s in database / schema Y” or β€œgrant all privileges to X” scenarios to make sure that every new object in a given database / schema is granted by the account role and every new privilege is granted to the database role. Important note: this flag is not compliant with the Terraform assumptions of the config being eventually convergent (producing an empty plan).", + }, + "always_apply_trigger": { + Type: schema.TypeString, + Optional: true, + Default: "", + Description: "This field should not be set and its main purpose is to achieve the functionality described by always_apply field. This is value will be flipped to the opposite value on every terraform apply, thus creating a new plan that will re-apply grants.", + }, + "on_database": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "The fully qualified name of the database on which privileges will be granted.", + ValidateDiagFunc: IsValidIdentifier[sdk.AccountObjectIdentifier](), + ExactlyOneOf: []string{ + "on_database", + "on_schema", + "on_schema_object", + }, + }, + "on_schema": { + Type: schema.TypeList, + Optional: true, + ForceNew: true, + Description: "Specifies the schema on which privileges will be granted.", + MaxItems: 1, + ExactlyOneOf: []string{ + "on_database", + "on_schema", + "on_schema_object", + }, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "schema_name": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "The fully qualified name of the schema.", + ValidateDiagFunc: IsValidIdentifier[sdk.DatabaseObjectIdentifier](), + ExactlyOneOf: []string{ + "on_schema.0.schema_name", + "on_schema.0.all_schemas_in_database", + "on_schema.0.future_schemas_in_database", + }, + }, + "all_schemas_in_database": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "The fully qualified name of the database.", + ValidateDiagFunc: IsValidIdentifier[sdk.AccountObjectIdentifier](), + ExactlyOneOf: []string{ + "on_schema.0.schema_name", + "on_schema.0.all_schemas_in_database", + "on_schema.0.future_schemas_in_database", + }, + }, + "future_schemas_in_database": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "The fully qualified name of the database.", + ValidateDiagFunc: IsValidIdentifier[sdk.AccountObjectIdentifier](), + ExactlyOneOf: []string{ + "on_schema.0.schema_name", + "on_schema.0.all_schemas_in_database", + "on_schema.0.future_schemas_in_database", + }, + }, + }, + }, + }, + "on_schema_object": { + Type: schema.TypeList, + Optional: true, + ForceNew: true, + Description: "Specifies the schema object on which privileges will be granted.", + MaxItems: 1, + ExactlyOneOf: []string{ + "on_database", + "on_schema", + "on_schema_object", + }, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "object_type": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "The object type of the schema object on which privileges will be granted. Valid values are: ALERT | DYNAMIC TABLE | EVENT TABLE | FILE FORMAT | FUNCTION | PROCEDURE | SECRET | SEQUENCE | PIPE | MASKING POLICY | PASSWORD POLICY | ROW ACCESS POLICY | SESSION POLICY | TAG | STAGE | STREAM | TABLE | EXTERNAL TABLE | TASK | VIEW | MATERIALIZED VIEW | NETWORK RULE | PACKAGES POLICY | ICEBERG TABLE", + RequiredWith: []string{ + "on_schema_object.0.object_name", + }, + ConflictsWith: []string{ + "on_schema_object.0.all", + "on_schema_object.0.future", + }, + ValidateDiagFunc: ValidObjectType(), + }, + "object_name": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "The fully qualified name of the object on which privileges will be granted.", + RequiredWith: []string{ + "on_schema_object.0.object_type", + }, + ExactlyOneOf: []string{ + "on_schema_object.0.object_name", + "on_schema_object.0.all", + "on_schema_object.0.future", + }, + ValidateDiagFunc: IsValidIdentifier[sdk.SchemaObjectIdentifier](), + }, + "all": { + Type: schema.TypeList, + Optional: true, + ForceNew: true, + Description: "Configures the privilege to be granted on all objects in either a database or schema.", + MaxItems: 1, + Elem: &schema.Resource{ + Schema: grantPrivilegesOnDatabaseRoleBulkOperationSchema, + }, + ConflictsWith: []string{ + "on_schema_object.0.object_type", + }, + ExactlyOneOf: []string{ + "on_schema_object.0.object_name", + "on_schema_object.0.all", + "on_schema_object.0.future", + }, + }, + "future": { + Type: schema.TypeList, + Optional: true, + ForceNew: true, + Description: "Configures the privilege to be granted on future objects in either a database or schema.", + MaxItems: 1, + Elem: &schema.Resource{ + Schema: grantPrivilegesOnDatabaseRoleBulkOperationSchema, + }, + ConflictsWith: []string{ + "on_schema_object.0.object_type", + }, + ExactlyOneOf: []string{ + "on_schema_object.0.object_name", + "on_schema_object.0.all", + "on_schema_object.0.future", + }, + }, + }, + }, + }, +} + +var grantPrivilegesOnDatabaseRoleBulkOperationSchema = map[string]*schema.Schema{ + "object_type_plural": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The plural object type of the schema object on which privileges will be granted. Valid values are: ALERTS | DYNAMIC TABLES | EVENT TABLES | FILE FORMATS | FUNCTIONS | PROCEDURES | SECRETS | SEQUENCES | PIPES | MASKING POLICIES | PASSWORD POLICIES | ROW ACCESS POLICIES | SESSION POLICIES | TAGS | STAGES | STREAMS | TABLES | EXTERNAL TABLES | TASKS | VIEWS | MATERIALIZED VIEWS | NETWORK RULES | PACKAGES POLICIES | ICEBERG TABLES", + ValidateDiagFunc: ValidPluralObjectType(), + }, + "in_database": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateDiagFunc: IsValidIdentifier[sdk.AccountObjectIdentifier](), + }, + "in_schema": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateDiagFunc: IsValidIdentifier[sdk.DatabaseObjectIdentifier](), + }, +} + +func isNotOwnershipGrant() func(value any, path cty.Path) diag.Diagnostics { + return func(value any, path cty.Path) diag.Diagnostics { + var diags diag.Diagnostics + if privilege, ok := value.(string); ok && strings.ToUpper(privilege) == "OWNERSHIP" { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: "Unsupported privilege 'OWNERSHIP'", + // TODO: Change when a new resource for granting ownership will be available (SNOW-991423) + Detail: "Granting ownership is only allowed in dedicated resources (snowflake_user_ownership_grant, snowflake_role_ownership_grant)", + AttributePath: nil, + }) + } + return diags + } +} + +func GrantPrivilegesToDatabaseRole() *schema.Resource { + return &schema.Resource{ + CreateContext: CreateGrantPrivilegesToDatabaseRole, + ReadContext: ReadGrantPrivilegesToDatabaseRole, + DeleteContext: DeleteGrantPrivilegesToDatabaseRole, + UpdateContext: UpdateGrantPrivilegesToDatabaseRole, + + Schema: grantPrivilegesToDatabaseRoleSchema, + Importer: &schema.ResourceImporter{ + StateContext: ImportGrantPrivilegesToDatabaseRole, + }, + } +} + +func ImportGrantPrivilegesToDatabaseRole(ctx context.Context, d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { + id, err := ParseGrantPrivilegesToDatabaseRoleId(d.Id()) + if err != nil { + return nil, err + } + if err := d.Set("database_role_name", id.DatabaseRoleName.FullyQualifiedName()); err != nil { + return nil, err + } + if err := d.Set("with_grant_option", id.WithGrantOption); err != nil { + return nil, err + } + if err := d.Set("always_apply", id.AlwaysApply); err != nil { + return nil, err + } + if err := d.Set("all_privileges", id.AllPrivileges); err != nil { + return nil, err + } + if err := d.Set("privileges", id.Privileges); err != nil { + return nil, err + } + + switch id.Kind { + case OnDatabaseDatabaseRoleGrantKind: + if err := d.Set("on_database", id.Data.(*OnDatabaseGrantData).DatabaseName.FullyQualifiedName()); err != nil { + return nil, err + } + case OnSchemaDatabaseRoleGrantKind: + data := id.Data.(*OnSchemaGrantData) + onSchema := make(map[string]any) + + switch data.Kind { + case OnSchemaSchemaGrantKind: + onSchema["schema_name"] = data.SchemaName.FullyQualifiedName() + case OnAllSchemasInDatabaseSchemaGrantKind: + onSchema["all_schemas_in_database"] = data.DatabaseName.FullyQualifiedName() + case OnFutureSchemasInDatabaseSchemaGrantKind: + onSchema["future_schemas_in_database"] = data.DatabaseName.FullyQualifiedName() + } + + if err := d.Set("on_schema", []any{onSchema}); err != nil { + return nil, err + } + case OnSchemaObjectDatabaseRoleGrantKind: + data := id.Data.(*OnSchemaObjectGrantData) + onSchemaObject := make(map[string]any) + + switch data.Kind { + case OnObjectSchemaObjectGrantKind: + onSchemaObject["object_type"] = data.Object.ObjectType.String() + onSchemaObject["object_name"] = data.Object.Name.FullyQualifiedName() + case OnAllSchemaObjectGrantKind: + onAll := make(map[string]any) + + onAll["object_type_plural"] = data.OnAllOrFuture.ObjectNamePlural.String() + switch data.OnAllOrFuture.Kind { + case InDatabaseBulkOperationGrantKind: + onAll["in_database"] = data.OnAllOrFuture.Database.FullyQualifiedName() + case InSchemaBulkOperationGrantKind: + onAll["in_schema"] = data.OnAllOrFuture.Schema.FullyQualifiedName() + } + + onSchemaObject["all"] = []any{onAll} + case OnFutureSchemaObjectGrantKind: + onFuture := make(map[string]any) + + onFuture["object_type_plural"] = data.OnAllOrFuture.ObjectNamePlural.String() + switch data.OnAllOrFuture.Kind { + case InDatabaseBulkOperationGrantKind: + onFuture["in_database"] = data.OnAllOrFuture.Database.FullyQualifiedName() + case InSchemaBulkOperationGrantKind: + onFuture["in_schema"] = data.OnAllOrFuture.Schema.FullyQualifiedName() + } + + onSchemaObject["future"] = []any{onFuture} + } + + if err := d.Set("on_schema_object", []any{onSchemaObject}); err != nil { + return nil, err + } + } + + return []*schema.ResourceData{d}, nil +} + +func CreateGrantPrivilegesToDatabaseRole(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + db := meta.(*sql.DB) + client := sdk.NewClientFromDB(db) + + id := createGrantPrivilegesToDatabaseRoleIdFromSchema(d) + err := client.Grants.GrantPrivilegesToDatabaseRole( + ctx, + getDatabaseRolePrivilegesFromSchema(d), + getDatabaseRoleGrantOn(d), + sdk.NewDatabaseObjectIdentifierFromFullyQualifiedName(d.Get("database_role_name").(string)), + &sdk.GrantPrivilegesToDatabaseRoleOptions{ + WithGrantOption: sdk.Bool(d.Get("with_grant_option").(bool)), + }, + ) + if err != nil { + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Error, + Summary: "An error occurred when granting privileges to database role", + Detail: fmt.Sprintf("Id: %s\nDatabase role name: %s\nError: %s", id.String(), id.DatabaseRoleName, err.Error()), + }, + } + } + + d.SetId(id.String()) + + return ReadGrantPrivilegesToDatabaseRole(ctx, d, meta) +} + +func UpdateGrantPrivilegesToDatabaseRole(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + db := meta.(*sql.DB) + client := sdk.NewClientFromDB(db) + id, err := ParseGrantPrivilegesToDatabaseRoleId(d.Id()) + if err != nil { + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Error, + Summary: "Failed to parse internal identifier", + Detail: fmt.Sprintf("Id: %s\nError: %s", d.Id(), err.Error()), + }, + } + } + + if d.HasChange("all_privileges") { + _, allPrivileges := d.GetChange("all_privileges") + + if !allPrivileges.(bool) { + err = client.Grants.RevokePrivilegesFromDatabaseRole(ctx, &sdk.DatabaseRoleGrantPrivileges{ + AllPrivileges: sdk.Bool(true), + }, + getDatabaseRoleGrantOn(d), + id.DatabaseRoleName, + new(sdk.RevokePrivilegesFromDatabaseRoleOptions), + ) + + if err != nil { + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Error, + Summary: "Failed to revoke all privileges", + Detail: fmt.Sprintf("Id: %s\nError: %s", d.Id(), err.Error()), + }, + } + } + } + + id.AllPrivileges = allPrivileges.(bool) + } + + if d.HasChange("privileges") { + shouldGrantAndRevoke := true + + // Skip if all_privileges was set to true + if d.HasChange("all_privileges") { + if _, allPrivileges := d.GetChange("all_privileges"); allPrivileges.(bool) { + shouldGrantAndRevoke = false + id.Privileges = []string{} + } + } + + if shouldGrantAndRevoke { + before, after := d.GetChange("privileges") + privilegesBeforeChange := expandStringList(before.(*schema.Set).List()) + privilegesAfterChange := expandStringList(after.(*schema.Set).List()) + + var privilegesToAdd, privilegesToRemove []string + + for _, privilegeBeforeChange := range privilegesBeforeChange { + if !slices.Contains(privilegesAfterChange, privilegeBeforeChange) { + privilegesToRemove = append(privilegesToRemove, privilegeBeforeChange) + } + } + + for _, privilegeAfterChange := range privilegesAfterChange { + if !slices.Contains(privilegesBeforeChange, privilegeAfterChange) { + privilegesToAdd = append(privilegesToAdd, privilegeAfterChange) + } + } + + grantOn := getDatabaseRoleGrantOn(d) + + if len(privilegesToAdd) > 0 { + err = client.Grants.GrantPrivilegesToDatabaseRole( + ctx, + getDatabaseRolePrivileges( + false, + privilegesToAdd, + id.Kind == OnDatabaseDatabaseRoleGrantKind, + id.Kind == OnSchemaDatabaseRoleGrantKind, + id.Kind == OnSchemaObjectDatabaseRoleGrantKind, + ), + grantOn, + id.DatabaseRoleName, + new(sdk.GrantPrivilegesToDatabaseRoleOptions), + ) + if err != nil { + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Error, + Summary: "Failed to grant added privileges", + Detail: fmt.Sprintf("Id: %s\nPrivileges to add: %v\nError: %s", d.Id(), privilegesToAdd, err.Error()), + }, + } + } + } + + if len(privilegesToRemove) > 0 { + err = client.Grants.RevokePrivilegesFromDatabaseRole( + ctx, + getDatabaseRolePrivileges( + false, + privilegesToRemove, + id.Kind == OnDatabaseDatabaseRoleGrantKind, + id.Kind == OnSchemaDatabaseRoleGrantKind, + id.Kind == OnSchemaObjectDatabaseRoleGrantKind, + ), + grantOn, + id.DatabaseRoleName, + new(sdk.RevokePrivilegesFromDatabaseRoleOptions), + ) + if err != nil { + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Error, + Summary: "Failed to revoke removed privileges", + Detail: fmt.Sprintf("Id: %s\nPrivileges to remove: %v\nError: %s", d.Id(), privilegesToRemove, err.Error()), + }, + } + } + } + + id.Privileges = privilegesAfterChange + } + } + + if d.HasChange("all_privileges") { + _, allPrivileges := d.GetChange("all_privileges") + + if allPrivileges.(bool) { + err = client.Grants.GrantPrivilegesToDatabaseRole(ctx, &sdk.DatabaseRoleGrantPrivileges{ + AllPrivileges: sdk.Bool(true), + }, + getDatabaseRoleGrantOn(d), + id.DatabaseRoleName, + new(sdk.GrantPrivilegesToDatabaseRoleOptions), + ) + + if err != nil { + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Error, + Summary: "Failed to grant all privileges", + Detail: fmt.Sprintf("Id: %s\nError: %s", d.Id(), err.Error()), + }, + } + } + } + + id.AllPrivileges = allPrivileges.(bool) + } + + if d.HasChange("always_apply") { + id.AlwaysApply = d.Get("always_apply").(bool) + } + + if id.AlwaysApply { + err := client.Grants.GrantPrivilegesToDatabaseRole( + ctx, + getDatabaseRolePrivilegesFromSchema(d), + getDatabaseRoleGrantOn(d), + id.DatabaseRoleName, + &sdk.GrantPrivilegesToDatabaseRoleOptions{ + WithGrantOption: &id.WithGrantOption, + }, + ) + if err != nil { + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Error, + Summary: "Always apply. An error occurred when granting privileges to database role", + Detail: fmt.Sprintf("Id: %s\nDatabase role name: %s\nError: %s", d.Id(), id.DatabaseRoleName, err.Error()), + }, + } + } + } + + d.SetId(id.String()) + + return ReadGrantPrivilegesToDatabaseRole(ctx, d, meta) +} + +func DeleteGrantPrivilegesToDatabaseRole(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + db := meta.(*sql.DB) + client := sdk.NewClientFromDB(db) + id, err := ParseGrantPrivilegesToDatabaseRoleId(d.Id()) + if err != nil { + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Error, + Summary: "Failed to parse internal identifier", + Detail: fmt.Sprintf("Id: %s\nError: %s", d.Id(), err.Error()), + }, + } + } + + err = client.Grants.RevokePrivilegesFromDatabaseRole( + ctx, + getDatabaseRolePrivilegesFromSchema(d), + getDatabaseRoleGrantOn(d), + id.DatabaseRoleName, + &sdk.RevokePrivilegesFromDatabaseRoleOptions{}, + ) + if err != nil { + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Error, + Summary: "An error occurred when revoking privileges from database role", + Detail: fmt.Sprintf("Id: %s\nDatabase role name: %s\nError: %s", d.Id(), id.DatabaseRoleName, err.Error()), + }, + } + } + + d.SetId("") + + return nil +} + +func ReadGrantPrivilegesToDatabaseRole(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + id, err := ParseGrantPrivilegesToDatabaseRoleId(d.Id()) + if err != nil { + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Error, + Summary: "Failed to parse internal identifier", + Detail: fmt.Sprintf("Id: %s\nError: %s", d.Id(), err.Error()), + }, + } + } + + if id.AlwaysApply { + triggerId, err := uuid.GenerateUUID() + if err != nil { + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Error, + Summary: "Failed to generate UUID", + Detail: fmt.Sprintf("Original error: %s", err.Error()), + }, + } + } + + // Change the value of always_apply_trigger to produce a plan + if err := d.Set("always_apply_trigger", triggerId); err != nil { + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Error, + Summary: "Error setting always_apply_trigger for database role", + Detail: fmt.Sprintf("Id: %s\nError: %s", d.Id(), err.Error()), + }, + } + } + } + + if id.AllPrivileges { + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Warning, + Summary: "Show with all_privileges option is skipped.", + // TODO: link to the design decisions doc (SNOW-990811) + Detail: "See our document on design decisions for grants: ", + }, + } + } + + opts, grantedOn, diags := prepareShowGrantsRequest(id) + if len(diags) != 0 { + return diags + } + + db := meta.(*sql.DB) + client := sdk.NewClientFromDB(db) + grants, err := client.Grants.Show(ctx, opts) + if err != nil { + return append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: "Failed to retrieve grants", + Detail: fmt.Sprintf("Id: %s\nError: %s", d.Id(), err.Error()), + }) + } + + var privileges []string + + for _, grant := range grants { + // Accept only DATABASE ROLEs + if grant.GrantTo != sdk.ObjectTypeDatabaseRole && grant.GrantedTo != sdk.ObjectTypeDatabaseRole { + continue + } + // Only consider privileges that are already present in the ID, so we + // don't delete privileges managed by other resources. + if !slices.Contains(id.Privileges, grant.Privilege) { + continue + } + if id.WithGrantOption == grant.GrantOption && id.DatabaseRoleName.Name() == grant.GranteeName.Name() { + // Future grants do not have grantedBy, only current grants do. + // If grantedby is an empty string, it means terraform could not have created the grant + if (opts.Future == nil || !*opts.Future) && grant.GrantedBy.Name() == "" { + continue + } + // grant_on is for future grants, granted_on is for current grants. + // They function the same way though in a test for matching the object type + if grantedOn == grant.GrantedOn || grantedOn == grant.GrantOn { + privileges = append(privileges, grant.Privilege) + } + } + } + + if err := d.Set("privileges", privileges); err != nil { + return append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: "Error setting privileges for database role", + Detail: fmt.Sprintf("Id: %s\nPrivileges: %v\nError: %s", d.Id(), privileges, err.Error()), + }) + } + + return diags +} + +func prepareShowGrantsRequest(id GrantPrivilegesToDatabaseRoleId) (*sdk.ShowGrantOptions, sdk.ObjectType, diag.Diagnostics) { + opts := new(sdk.ShowGrantOptions) + var grantedOn sdk.ObjectType + + switch id.Kind { + case OnDatabaseDatabaseRoleGrantKind: + grantedOn = sdk.ObjectTypeDatabase + data := id.Data.(*OnDatabaseGrantData) + opts.On = &sdk.ShowGrantsOn{ + Object: &sdk.Object{ + ObjectType: sdk.ObjectTypeDatabase, + Name: data.DatabaseName, + }, + } + case OnSchemaDatabaseRoleGrantKind: + grantedOn = sdk.ObjectTypeSchema + data := id.Data.(*OnSchemaGrantData) + + switch data.Kind { + case OnSchemaSchemaGrantKind: + opts.On = &sdk.ShowGrantsOn{ + Object: &sdk.Object{ + ObjectType: sdk.ObjectTypeSchema, + Name: data.SchemaName, + }, + } + case OnAllSchemasInDatabaseSchemaGrantKind: + return nil, "", diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Warning, + Summary: "Show with OnAllSchemasInDatabase option is skipped.", + // TODO: link to the design decisions doc (SNOW-990811) + Detail: "See our document on design decisions for grants: ", + }, + } + case OnFutureSchemasInDatabaseSchemaGrantKind: + opts.Future = sdk.Bool(true) + opts.In = &sdk.ShowGrantsIn{ + Database: data.DatabaseName, + } + } + case OnSchemaObjectDatabaseRoleGrantKind: + data := id.Data.(*OnSchemaObjectGrantData) + + switch data.Kind { + case OnObjectSchemaObjectGrantKind: + grantedOn = data.Object.ObjectType + opts.On = &sdk.ShowGrantsOn{ + Object: data.Object, + } + case OnAllSchemaObjectGrantKind: + return nil, "", diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Warning, + Summary: "Show with OnAll option is skipped.", + // TODO: link to the design decisions doc (SNOW-990811) + Detail: "See our document on design decisions for grants: ", + }, + } + case OnFutureSchemaObjectGrantKind: + grantedOn = data.OnAllOrFuture.ObjectNamePlural.Singular() + opts.Future = sdk.Bool(true) + + switch data.OnAllOrFuture.Kind { + case InDatabaseBulkOperationGrantKind: + opts.In = &sdk.ShowGrantsIn{ + Database: data.OnAllOrFuture.Database, + } + case InSchemaBulkOperationGrantKind: + opts.In = &sdk.ShowGrantsIn{ + Schema: data.OnAllOrFuture.Schema, + } + } + } + } + + return opts, grantedOn, nil +} + +func getDatabaseRolePrivilegesFromSchema(d *schema.ResourceData) *sdk.DatabaseRoleGrantPrivileges { + _, onDatabaseOk := d.GetOk("on_database") + _, onSchemaOk := d.GetOk("on_schema") + _, onSchemaObjectOk := d.GetOk("on_schema_object") + + return getDatabaseRolePrivileges( + d.Get("all_privileges").(bool), + expandStringList(d.Get("privileges").(*schema.Set).List()), + onDatabaseOk, + onSchemaOk, + onSchemaObjectOk, + ) +} + +func getDatabaseRolePrivileges(allPrivileges bool, privileges []string, onDatabase bool, onSchema bool, onSchemaObject bool) *sdk.DatabaseRoleGrantPrivileges { + databaseRoleGrantPrivileges := new(sdk.DatabaseRoleGrantPrivileges) + + if allPrivileges { + databaseRoleGrantPrivileges.AllPrivileges = sdk.Bool(true) + return databaseRoleGrantPrivileges + } + + switch { + case onDatabase: + databasePrivileges := make([]sdk.AccountObjectPrivilege, len(privileges)) + for i, privilege := range privileges { + databasePrivileges[i] = sdk.AccountObjectPrivilege(privilege) + } + databaseRoleGrantPrivileges.DatabasePrivileges = databasePrivileges + case onSchema: + schemaPrivileges := make([]sdk.SchemaPrivilege, len(privileges)) + for i, privilege := range privileges { + schemaPrivileges[i] = sdk.SchemaPrivilege(privilege) + } + databaseRoleGrantPrivileges.SchemaPrivileges = schemaPrivileges + case onSchemaObject: + schemaObjectPrivileges := make([]sdk.SchemaObjectPrivilege, len(privileges)) + for i, privilege := range privileges { + schemaObjectPrivileges[i] = sdk.SchemaObjectPrivilege(privilege) + } + databaseRoleGrantPrivileges.SchemaObjectPrivileges = schemaObjectPrivileges + } + + return databaseRoleGrantPrivileges +} + +func getDatabaseRoleGrantOn(d *schema.ResourceData) *sdk.DatabaseRoleGrantOn { + onDatabase, onDatabaseOk := d.GetOk("on_database") + onSchemaBlock, onSchemaOk := d.GetOk("on_schema") + onSchemaObjectBlock, onSchemaObjectOk := d.GetOk("on_schema_object") + on := new(sdk.DatabaseRoleGrantOn) + + switch { + case onDatabaseOk: + on.Database = sdk.Pointer(sdk.NewAccountObjectIdentifierFromFullyQualifiedName(onDatabase.(string))) + case onSchemaOk: + onSchema := onSchemaBlock.([]any)[0].(map[string]any) + + grantOnSchema := new(sdk.GrantOnSchema) + + schemaName := onSchema["schema_name"].(string) + schemaNameOk := len(schemaName) > 0 + + allSchemasInDatabase := onSchema["all_schemas_in_database"].(string) + allSchemasInDatabaseOk := len(allSchemasInDatabase) > 0 + + futureSchemasInDatabase := onSchema["future_schemas_in_database"].(string) + futureSchemasInDatabaseOk := len(futureSchemasInDatabase) > 0 + + switch { + case schemaNameOk: + grantOnSchema.Schema = sdk.Pointer(sdk.NewDatabaseObjectIdentifierFromFullyQualifiedName(schemaName)) + case allSchemasInDatabaseOk: + grantOnSchema.AllSchemasInDatabase = sdk.Pointer(sdk.NewAccountObjectIdentifierFromFullyQualifiedName(allSchemasInDatabase)) + case futureSchemasInDatabaseOk: + grantOnSchema.FutureSchemasInDatabase = sdk.Pointer(sdk.NewAccountObjectIdentifierFromFullyQualifiedName(futureSchemasInDatabase)) + } + + on.Schema = grantOnSchema + case onSchemaObjectOk: + onSchemaObject := onSchemaObjectBlock.([]any)[0].(map[string]any) + + grantOnSchemaObject := new(sdk.GrantOnSchemaObject) + + objectType := onSchemaObject["object_type"].(string) + objectTypeOk := len(objectType) > 0 + + objectName := onSchemaObject["object_name"].(string) + objectNameOk := len(objectName) > 0 + + all := onSchemaObject["all"].([]any) + allOk := len(all) > 0 + + future := onSchemaObject["future"].([]any) + futureOk := len(future) > 0 + + switch { + case objectTypeOk && objectNameOk: + grantOnSchemaObject.SchemaObject = &sdk.Object{ + ObjectType: sdk.ObjectType(objectType), + Name: sdk.NewSchemaObjectIdentifierFromFullyQualifiedName(objectName), + } + case allOk: + grantOnSchemaObject.All = getGrantOnSchemaObjectIn(all[0].(map[string]any)) + case futureOk: + grantOnSchemaObject.Future = getGrantOnSchemaObjectIn(future[0].(map[string]any)) + } + + on.SchemaObject = grantOnSchemaObject + } + + return on +} + +func getGrantOnSchemaObjectIn(allOrFuture map[string]any) *sdk.GrantOnSchemaObjectIn { + pluralObjectType := sdk.PluralObjectType(allOrFuture["object_type_plural"].(string)) + grantOnSchemaObjectIn := &sdk.GrantOnSchemaObjectIn{ + PluralObjectType: pluralObjectType, + } + + if inDatabase, ok := allOrFuture["in_database"].(string); ok && len(inDatabase) > 0 { + grantOnSchemaObjectIn.InDatabase = sdk.Pointer(sdk.NewAccountObjectIdentifierFromFullyQualifiedName(inDatabase)) + } + + if inSchema, ok := allOrFuture["in_schema"].(string); ok && len(inSchema) > 0 { + grantOnSchemaObjectIn.InSchema = sdk.Pointer(sdk.NewDatabaseObjectIdentifierFromFullyQualifiedName(inSchema)) + } + + return grantOnSchemaObjectIn +} + +func createGrantPrivilegesToDatabaseRoleIdFromSchema(d *schema.ResourceData) *GrantPrivilegesToDatabaseRoleId { + id := new(GrantPrivilegesToDatabaseRoleId) + id.DatabaseRoleName = sdk.NewDatabaseObjectIdentifierFromFullyQualifiedName(d.Get("database_role_name").(string)) + id.AllPrivileges = d.Get("all_privileges").(bool) + if p, ok := d.GetOk("privileges"); ok { + id.Privileges = expandStringList(p.(*schema.Set).List()) + } + id.WithGrantOption = d.Get("with_grant_option").(bool) + + on := getDatabaseRoleGrantOn(d) + switch { + case on.Database != nil: + id.Kind = OnDatabaseDatabaseRoleGrantKind + id.Data = &OnDatabaseGrantData{ + DatabaseName: *on.Database, + } + case on.Schema != nil: + onSchemaGrantData := new(OnSchemaGrantData) + + switch { + case on.Schema.Schema != nil: + onSchemaGrantData.Kind = OnSchemaSchemaGrantKind + onSchemaGrantData.SchemaName = on.Schema.Schema + case on.Schema.AllSchemasInDatabase != nil: + onSchemaGrantData.Kind = OnAllSchemasInDatabaseSchemaGrantKind + onSchemaGrantData.DatabaseName = on.Schema.AllSchemasInDatabase + case on.Schema.FutureSchemasInDatabase != nil: + onSchemaGrantData.Kind = OnFutureSchemasInDatabaseSchemaGrantKind + onSchemaGrantData.DatabaseName = on.Schema.FutureSchemasInDatabase + } + + id.Kind = OnSchemaDatabaseRoleGrantKind + id.Data = onSchemaGrantData + case on.SchemaObject != nil: + onSchemaObjectGrantData := new(OnSchemaObjectGrantData) + + switch { + case on.SchemaObject.SchemaObject != nil: + onSchemaObjectGrantData.Kind = OnObjectSchemaObjectGrantKind + onSchemaObjectGrantData.Object = on.SchemaObject.SchemaObject + case on.SchemaObject.All != nil: + onSchemaObjectGrantData.Kind = OnAllSchemaObjectGrantKind + onSchemaObjectGrantData.OnAllOrFuture = getBulkOperationGrantData(on.SchemaObject.All) + case on.SchemaObject.Future != nil: + onSchemaObjectGrantData.Kind = OnFutureSchemaObjectGrantKind + onSchemaObjectGrantData.OnAllOrFuture = getBulkOperationGrantData(on.SchemaObject.Future) + } + + id.Kind = OnSchemaObjectDatabaseRoleGrantKind + id.Data = onSchemaObjectGrantData + } + + return id +} + +func getBulkOperationGrantData(in *sdk.GrantOnSchemaObjectIn) *BulkOperationGrantData { + bulkOperationGrantData := &BulkOperationGrantData{ + ObjectNamePlural: in.PluralObjectType, + } + + if in.InDatabase != nil { + bulkOperationGrantData.Kind = InDatabaseBulkOperationGrantKind + bulkOperationGrantData.Database = in.InDatabase + } + + if in.InSchema != nil { + bulkOperationGrantData.Kind = InSchemaBulkOperationGrantKind + bulkOperationGrantData.Schema = in.InSchema + } + + return bulkOperationGrantData +} diff --git a/pkg/resources/grant_privileges_to_database_role_acceptance_test.go b/pkg/resources/grant_privileges_to_database_role_acceptance_test.go new file mode 100644 index 0000000000..451c66f048 --- /dev/null +++ b/pkg/resources/grant_privileges_to_database_role_acceptance_test.go @@ -0,0 +1,852 @@ +package resources_test + +import ( + "context" + "database/sql" + "fmt" + "log" + "regexp" + "slices" + "testing" + + acc "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + "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/terraform" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestAcc_GrantPrivilegesToDatabaseRole_OnDatabase(t *testing.T) { + name := "test_database_role_name" + configVariables := config.Variables{ + "name": config.StringVariable(name), + "privileges": config.ListVariable( + config.StringVariable(string(sdk.AccountObjectPrivilegeCreateSchema)), + config.StringVariable(string(sdk.AccountObjectPrivilegeModify)), + config.StringVariable(string(sdk.AccountObjectPrivilegeUsage)), + ), + "database": config.StringVariable(acc.TestDatabaseName), + "with_grant_option": config.BoolVariable(true), + } + resourceName := "snowflake_grant_privileges_to_database_role.test" + + databaseRoleName := sdk.NewDatabaseObjectIdentifier(acc.TestDatabaseName, name).FullyQualifiedName() + databaseName := sdk.NewAccountObjectIdentifier(acc.TestDatabaseName).FullyQualifiedName() + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + PreCheck: func() { acc.TestAccPreCheck(t) }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: testAccCheckDatabaseRolePrivilegesRevoked, + Steps: []resource.TestStep{ + { + PreConfig: func() { createDatabaseRoleOutsideTerraform(t, name) }, + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantPrivilegesToDatabaseRole/OnDatabase"), + ConfigVariables: configVariables, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "database_role_name", databaseRoleName), + resource.TestCheckResourceAttr(resourceName, "privileges.#", "3"), + resource.TestCheckResourceAttr(resourceName, "privileges.0", string(sdk.AccountObjectPrivilegeCreateSchema)), + resource.TestCheckResourceAttr(resourceName, "privileges.1", string(sdk.AccountObjectPrivilegeModify)), + resource.TestCheckResourceAttr(resourceName, "privileges.2", string(sdk.AccountObjectPrivilegeUsage)), + resource.TestCheckResourceAttr(resourceName, "on_database", databaseName), + resource.TestCheckResourceAttr(resourceName, "with_grant_option", "true"), + resource.TestCheckResourceAttr(resourceName, "id", fmt.Sprintf("%s|true|false|CREATE SCHEMA,MODIFY,USAGE|OnDatabase|%s", databaseRoleName, databaseName)), + ), + }, + { + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantPrivilegesToDatabaseRole/OnDatabase"), + ConfigVariables: configVariables, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAcc_GrantPrivilegesToDatabaseRole_OnDatabase_PrivilegesReversed(t *testing.T) { + name := "test_database_role_name" + configVariables := config.Variables{ + "name": config.StringVariable(name), + "privileges": config.ListVariable( + config.StringVariable(string(sdk.AccountObjectPrivilegeUsage)), + config.StringVariable(string(sdk.AccountObjectPrivilegeModify)), + config.StringVariable(string(sdk.AccountObjectPrivilegeCreateSchema)), + ), + "database": config.StringVariable(acc.TestDatabaseName), + "with_grant_option": config.BoolVariable(true), + } + resourceName := "snowflake_grant_privileges_to_database_role.test" + + databaseRoleName := sdk.NewDatabaseObjectIdentifier(acc.TestDatabaseName, name).FullyQualifiedName() + databaseName := sdk.NewAccountObjectIdentifier(acc.TestDatabaseName).FullyQualifiedName() + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + PreCheck: func() { acc.TestAccPreCheck(t) }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: testAccCheckDatabaseRolePrivilegesRevoked, + Steps: []resource.TestStep{ + { + PreConfig: func() { createDatabaseRoleOutsideTerraform(t, name) }, + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantPrivilegesToDatabaseRole/OnDatabase"), + ConfigVariables: configVariables, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "database_role_name", databaseRoleName), + resource.TestCheckResourceAttr(resourceName, "privileges.#", "3"), + resource.TestCheckResourceAttr(resourceName, "privileges.0", string(sdk.AccountObjectPrivilegeCreateSchema)), + resource.TestCheckResourceAttr(resourceName, "privileges.1", string(sdk.AccountObjectPrivilegeModify)), + resource.TestCheckResourceAttr(resourceName, "privileges.2", string(sdk.AccountObjectPrivilegeUsage)), + resource.TestCheckResourceAttr(resourceName, "on_database", databaseName), + resource.TestCheckResourceAttr(resourceName, "with_grant_option", "true"), + resource.TestCheckResourceAttr(resourceName, "id", fmt.Sprintf("%s|true|false|CREATE SCHEMA,MODIFY,USAGE|OnDatabase|%s", databaseRoleName, databaseName)), + ), + }, + { + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantPrivilegesToDatabaseRole/OnDatabase"), + ConfigVariables: configVariables, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAcc_GrantPrivilegesToDatabaseRole_OnSchema(t *testing.T) { + name := "test_database_role_name" + configVariables := config.Variables{ + "name": config.StringVariable(name), + "privileges": config.ListVariable( + config.StringVariable(string(sdk.SchemaPrivilegeCreateTable)), + config.StringVariable(string(sdk.SchemaPrivilegeModify)), + ), + "database": config.StringVariable(acc.TestDatabaseName), + "schema": config.StringVariable(acc.TestSchemaName), + "with_grant_option": config.BoolVariable(false), + } + resourceName := "snowflake_grant_privileges_to_database_role.test" + + databaseRoleName := sdk.NewDatabaseObjectIdentifier(acc.TestDatabaseName, name).FullyQualifiedName() + schemaName := sdk.NewDatabaseObjectIdentifier(acc.TestDatabaseName, acc.TestSchemaName).FullyQualifiedName() + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + PreCheck: func() { acc.TestAccPreCheck(t) }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: testAccCheckDatabaseRolePrivilegesRevoked, + Steps: []resource.TestStep{ + { + PreConfig: func() { createDatabaseRoleOutsideTerraform(t, name) }, + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantPrivilegesToDatabaseRole/OnSchema"), + ConfigVariables: configVariables, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "database_role_name", databaseRoleName), + resource.TestCheckResourceAttr(resourceName, "privileges.#", "2"), + resource.TestCheckResourceAttr(resourceName, "privileges.0", string(sdk.SchemaPrivilegeCreateTable)), + resource.TestCheckResourceAttr(resourceName, "privileges.1", string(sdk.SchemaPrivilegeModify)), + resource.TestCheckResourceAttr(resourceName, "on_schema.#", "1"), + resource.TestCheckResourceAttr(resourceName, "on_schema.0.schema_name", schemaName), + resource.TestCheckResourceAttr(resourceName, "with_grant_option", "false"), + resource.TestCheckResourceAttr(resourceName, "id", fmt.Sprintf("%s|false|false|CREATE TABLE,MODIFY|OnSchema|OnSchema|%s", databaseRoleName, schemaName)), + ), + }, + { + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantPrivilegesToDatabaseRole/OnSchema"), + ConfigVariables: configVariables, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAcc_GrantPrivilegesToDatabaseRole_OnSchema_ExactlyOneOf(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + PreCheck: func() { acc.TestAccPreCheck(t) }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: testAccCheckDatabaseRolePrivilegesRevoked, + Steps: []resource.TestStep{ + { + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantPrivilegesToDatabaseRole/OnSchema_ExactlyOneOf"), + PlanOnly: true, + ExpectError: regexp.MustCompile("Error: Invalid combination of arguments"), + }, + }, + }) +} + +func TestAcc_GrantPrivilegesToDatabaseRole_OnAllSchemasInDatabase(t *testing.T) { + name := "test_database_role_name" + configVariables := config.Variables{ + "name": config.StringVariable(name), + "privileges": config.ListVariable( + config.StringVariable(string(sdk.SchemaPrivilegeCreateTable)), + config.StringVariable(string(sdk.SchemaPrivilegeModify)), + ), + "database": config.StringVariable(acc.TestDatabaseName), + "with_grant_option": config.BoolVariable(false), + } + resourceName := "snowflake_grant_privileges_to_database_role.test" + + databaseRoleName := sdk.NewDatabaseObjectIdentifier(acc.TestDatabaseName, name).FullyQualifiedName() + databaseName := sdk.NewAccountObjectIdentifier(acc.TestDatabaseName).FullyQualifiedName() + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + PreCheck: func() { acc.TestAccPreCheck(t) }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: testAccCheckDatabaseRolePrivilegesRevoked, + Steps: []resource.TestStep{ + { + PreConfig: func() { createDatabaseRoleOutsideTerraform(t, name) }, + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantPrivilegesToDatabaseRole/OnAllSchemasInDatabase"), + ConfigVariables: configVariables, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "database_role_name", databaseRoleName), + resource.TestCheckResourceAttr(resourceName, "privileges.#", "2"), + resource.TestCheckResourceAttr(resourceName, "privileges.0", string(sdk.SchemaPrivilegeCreateTable)), + resource.TestCheckResourceAttr(resourceName, "privileges.1", string(sdk.SchemaPrivilegeModify)), + resource.TestCheckResourceAttr(resourceName, "on_schema.#", "1"), + resource.TestCheckResourceAttr(resourceName, "on_schema.0.all_schemas_in_database", databaseName), + resource.TestCheckResourceAttr(resourceName, "with_grant_option", "false"), + resource.TestCheckResourceAttr(resourceName, "id", fmt.Sprintf("%s|false|false|CREATE TABLE,MODIFY|OnSchema|OnAllSchemasInDatabase|%s", databaseRoleName, databaseName)), + ), + }, + { + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantPrivilegesToDatabaseRole/OnAllSchemasInDatabase"), + ConfigVariables: configVariables, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAcc_GrantPrivilegesToDatabaseRole_OnFutureSchemasInDatabase(t *testing.T) { + name := "test_database_role_name" + configVariables := config.Variables{ + "name": config.StringVariable(name), + "privileges": config.ListVariable( + config.StringVariable(string(sdk.SchemaPrivilegeCreateTable)), + config.StringVariable(string(sdk.SchemaPrivilegeModify)), + ), + "database": config.StringVariable(acc.TestDatabaseName), + "with_grant_option": config.BoolVariable(false), + } + resourceName := "snowflake_grant_privileges_to_database_role.test" + + databaseRoleName := sdk.NewDatabaseObjectIdentifier(acc.TestDatabaseName, name).FullyQualifiedName() + databaseName := sdk.NewAccountObjectIdentifier(acc.TestDatabaseName).FullyQualifiedName() + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + PreCheck: func() { acc.TestAccPreCheck(t) }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: testAccCheckDatabaseRolePrivilegesRevoked, + Steps: []resource.TestStep{ + { + PreConfig: func() { createDatabaseRoleOutsideTerraform(t, name) }, + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantPrivilegesToDatabaseRole/OnFutureSchemasInDatabase"), + ConfigVariables: configVariables, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "database_role_name", databaseRoleName), + resource.TestCheckResourceAttr(resourceName, "privileges.#", "2"), + resource.TestCheckResourceAttr(resourceName, "privileges.0", string(sdk.SchemaPrivilegeCreateTable)), + resource.TestCheckResourceAttr(resourceName, "privileges.1", string(sdk.SchemaPrivilegeModify)), + resource.TestCheckResourceAttr(resourceName, "on_schema.#", "1"), + resource.TestCheckResourceAttr(resourceName, "on_schema.0.future_schemas_in_database", databaseName), + resource.TestCheckResourceAttr(resourceName, "with_grant_option", "false"), + resource.TestCheckResourceAttr(resourceName, "id", fmt.Sprintf("%s|false|false|CREATE TABLE,MODIFY|OnSchema|OnFutureSchemasInDatabase|%s", databaseRoleName, databaseName)), + ), + }, + { + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantPrivilegesToDatabaseRole/OnFutureSchemasInDatabase"), + ConfigVariables: configVariables, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAcc_GrantPrivilegesToDatabaseRole_OnSchemaObject_OnObject(t *testing.T) { + name := "test_database_role_name" + tblName := "test_database_role_table_name" + configVariables := config.Variables{ + "name": config.StringVariable(name), + "table_name": config.StringVariable(tblName), + "privileges": config.ListVariable( + config.StringVariable(string(sdk.SchemaObjectPrivilegeInsert)), + config.StringVariable(string(sdk.SchemaObjectPrivilegeUpdate)), + ), + "database": config.StringVariable(acc.TestDatabaseName), + "schema": config.StringVariable(acc.TestSchemaName), + "with_grant_option": config.BoolVariable(false), + } + resourceName := "snowflake_grant_privileges_to_database_role.test" + + databaseRoleName := sdk.NewDatabaseObjectIdentifier(acc.TestDatabaseName, name).FullyQualifiedName() + tableName := sdk.NewSchemaObjectIdentifier(acc.TestDatabaseName, acc.TestSchemaName, tblName).FullyQualifiedName() + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + PreCheck: func() { acc.TestAccPreCheck(t) }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: testAccCheckDatabaseRolePrivilegesRevoked, + Steps: []resource.TestStep{ + { + PreConfig: func() { createDatabaseRoleOutsideTerraform(t, name) }, + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantPrivilegesToDatabaseRole/OnSchemaObject_OnObject"), + ConfigVariables: configVariables, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "database_role_name", databaseRoleName), + resource.TestCheckResourceAttr(resourceName, "privileges.#", "2"), + resource.TestCheckResourceAttr(resourceName, "privileges.0", string(sdk.SchemaObjectPrivilegeInsert)), + resource.TestCheckResourceAttr(resourceName, "privileges.1", string(sdk.SchemaObjectPrivilegeUpdate)), + resource.TestCheckResourceAttr(resourceName, "on_schema_object.#", "1"), + resource.TestCheckResourceAttr(resourceName, "on_schema_object.0.object_type", string(sdk.ObjectTypeTable)), + resource.TestCheckResourceAttr(resourceName, "on_schema_object.0.object_name", tableName), + resource.TestCheckResourceAttr(resourceName, "with_grant_option", "false"), + resource.TestCheckResourceAttr(resourceName, "id", fmt.Sprintf("%s|false|false|INSERT,UPDATE|OnSchemaObject|OnObject|TABLE|%s", databaseRoleName, tableName)), + ), + }, + { + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantPrivilegesToDatabaseRole/OnSchemaObject_OnObject"), + ConfigVariables: configVariables, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAcc_GrantPrivilegesToDatabaseRole_OnSchemaObject_OnObject_OwnershipPrivilege(t *testing.T) { + name := "test_database_role_name" + tableName := "test_database_role_table_name" + configVariables := config.Variables{ + "name": config.StringVariable(name), + "table_name": config.StringVariable(tableName), + "privileges": config.ListVariable( + config.StringVariable(string(sdk.SchemaObjectOwnership)), + ), + "database": config.StringVariable(acc.TestDatabaseName), + "schema": config.StringVariable(acc.TestSchemaName), + "with_grant_option": config.BoolVariable(false), + } + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + PreCheck: func() { acc.TestAccPreCheck(t) }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: testAccCheckDatabaseRolePrivilegesRevoked, + Steps: []resource.TestStep{ + { + PreConfig: func() { createDatabaseRoleOutsideTerraform(t, name) }, + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantPrivilegesToDatabaseRole/OnSchemaObject_OnObject"), + ConfigVariables: configVariables, + ExpectError: regexp.MustCompile("Unsupported privilege 'OWNERSHIP'"), + }, + }, + }) +} + +func TestAcc_GrantPrivilegesToDatabaseRole_OnSchemaObject_OnAll_InDatabase(t *testing.T) { + name := "test_database_role_name" + configVariables := config.Variables{ + "name": config.StringVariable(name), + "privileges": config.ListVariable( + config.StringVariable(string(sdk.SchemaObjectPrivilegeInsert)), + config.StringVariable(string(sdk.SchemaObjectPrivilegeUpdate)), + ), + "database": config.StringVariable(acc.TestDatabaseName), + "with_grant_option": config.BoolVariable(false), + } + resourceName := "snowflake_grant_privileges_to_database_role.test" + + databaseRoleName := sdk.NewDatabaseObjectIdentifier(acc.TestDatabaseName, name).FullyQualifiedName() + databaseName := sdk.NewAccountObjectIdentifier(acc.TestDatabaseName).FullyQualifiedName() + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + PreCheck: func() { acc.TestAccPreCheck(t) }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: testAccCheckDatabaseRolePrivilegesRevoked, + Steps: []resource.TestStep{ + { + PreConfig: func() { createDatabaseRoleOutsideTerraform(t, name) }, + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantPrivilegesToDatabaseRole/OnSchemaObject_OnAll_InDatabase"), + ConfigVariables: configVariables, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "database_role_name", databaseRoleName), + resource.TestCheckResourceAttr(resourceName, "privileges.#", "2"), + resource.TestCheckResourceAttr(resourceName, "privileges.0", string(sdk.SchemaObjectPrivilegeInsert)), + resource.TestCheckResourceAttr(resourceName, "privileges.1", string(sdk.SchemaObjectPrivilegeUpdate)), + resource.TestCheckResourceAttr(resourceName, "on_schema_object.#", "1"), + resource.TestCheckResourceAttr(resourceName, "on_schema_object.0.all.#", "1"), + resource.TestCheckResourceAttr(resourceName, "on_schema_object.0.all.0.object_type_plural", string(sdk.PluralObjectTypeTables)), + resource.TestCheckResourceAttr(resourceName, "on_schema_object.0.all.0.in_database", databaseName), + resource.TestCheckResourceAttr(resourceName, "with_grant_option", "false"), + resource.TestCheckResourceAttr(resourceName, "id", fmt.Sprintf("%s|false|false|INSERT,UPDATE|OnSchemaObject|OnAll|TABLES|InDatabase|%s", databaseRoleName, databaseName)), + ), + }, + { + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantPrivilegesToDatabaseRole/OnSchemaObject_OnAll_InDatabase"), + ConfigVariables: configVariables, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAcc_GrantPrivilegesToDatabaseRole_OnSchemaObject_OnFuture_InDatabase(t *testing.T) { + name := "test_database_role_name" + configVariables := config.Variables{ + "name": config.StringVariable(name), + "privileges": config.ListVariable( + config.StringVariable(string(sdk.SchemaObjectPrivilegeInsert)), + config.StringVariable(string(sdk.SchemaObjectPrivilegeUpdate)), + ), + "database": config.StringVariable(acc.TestDatabaseName), + "with_grant_option": config.BoolVariable(false), + } + resourceName := "snowflake_grant_privileges_to_database_role.test" + + databaseRoleName := sdk.NewDatabaseObjectIdentifier(acc.TestDatabaseName, name).FullyQualifiedName() + databaseName := sdk.NewAccountObjectIdentifier(acc.TestDatabaseName).FullyQualifiedName() + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + PreCheck: func() { acc.TestAccPreCheck(t) }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: testAccCheckDatabaseRolePrivilegesRevoked, + Steps: []resource.TestStep{ + { + PreConfig: func() { createDatabaseRoleOutsideTerraform(t, name) }, + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantPrivilegesToDatabaseRole/OnSchemaObject_OnFuture_InDatabase"), + ConfigVariables: configVariables, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "database_role_name", databaseRoleName), + resource.TestCheckResourceAttr(resourceName, "privileges.#", "2"), + resource.TestCheckResourceAttr(resourceName, "privileges.0", string(sdk.SchemaObjectPrivilegeInsert)), + resource.TestCheckResourceAttr(resourceName, "privileges.1", string(sdk.SchemaObjectPrivilegeUpdate)), + resource.TestCheckResourceAttr(resourceName, "on_schema_object.#", "1"), + resource.TestCheckResourceAttr(resourceName, "on_schema_object.0.future.#", "1"), + resource.TestCheckResourceAttr(resourceName, "on_schema_object.0.future.0.object_type_plural", string(sdk.PluralObjectTypeTables)), + resource.TestCheckResourceAttr(resourceName, "on_schema_object.0.future.0.in_database", databaseName), + resource.TestCheckResourceAttr(resourceName, "with_grant_option", "false"), + resource.TestCheckResourceAttr(resourceName, "id", fmt.Sprintf("%s|false|false|INSERT,UPDATE|OnSchemaObject|OnFuture|TABLES|InDatabase|%s", databaseRoleName, databaseName)), + ), + }, + { + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantPrivilegesToDatabaseRole/OnSchemaObject_OnFuture_InDatabase"), + ConfigVariables: configVariables, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAcc_GrantPrivilegesToDatabaseRole_UpdatePrivileges(t *testing.T) { + name := "test_database_role_name" + configVariables := func(allPrivileges bool, privileges []sdk.AccountObjectPrivilege) config.Variables { + configVariables := config.Variables{ + "name": config.StringVariable(name), + "database": config.StringVariable(acc.TestDatabaseName), + } + if allPrivileges { + configVariables["all_privileges"] = config.BoolVariable(allPrivileges) + } + if len(privileges) > 0 { + configPrivileges := make([]config.Variable, len(privileges)) + for i, privilege := range privileges { + configPrivileges[i] = config.StringVariable(string(privilege)) + } + configVariables["privileges"] = config.ListVariable(configPrivileges...) + } + return configVariables + } + resourceName := "snowflake_grant_privileges_to_database_role.test" + + databaseRoleName := sdk.NewDatabaseObjectIdentifier(acc.TestDatabaseName, name).FullyQualifiedName() + databaseName := sdk.NewAccountObjectIdentifier(acc.TestDatabaseName).FullyQualifiedName() + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + PreCheck: func() { acc.TestAccPreCheck(t) }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: testAccCheckDatabaseRolePrivilegesRevoked, + Steps: []resource.TestStep{ + { + PreConfig: func() { createDatabaseRoleOutsideTerraform(t, name) }, + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantPrivilegesToDatabaseRole/UpdatePrivileges/privileges"), + ConfigVariables: configVariables(false, []sdk.AccountObjectPrivilege{ + sdk.AccountObjectPrivilegeCreateSchema, + sdk.AccountObjectPrivilegeModify, + }), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "all_privileges", "false"), + resource.TestCheckResourceAttr(resourceName, "privileges.#", "2"), + resource.TestCheckResourceAttr(resourceName, "privileges.0", string(sdk.AccountObjectPrivilegeCreateSchema)), + resource.TestCheckResourceAttr(resourceName, "privileges.1", string(sdk.AccountObjectPrivilegeModify)), + resource.TestCheckResourceAttr(resourceName, "id", fmt.Sprintf("%s|false|false|CREATE SCHEMA,MODIFY|OnDatabase|%s", databaseRoleName, databaseName)), + ), + }, + { + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantPrivilegesToDatabaseRole/UpdatePrivileges/privileges"), + ConfigVariables: configVariables(false, []sdk.AccountObjectPrivilege{ + sdk.AccountObjectPrivilegeCreateSchema, + sdk.AccountObjectPrivilegeMonitor, + sdk.AccountObjectPrivilegeUsage, + }), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "all_privileges", "false"), + resource.TestCheckResourceAttr(resourceName, "privileges.#", "3"), + resource.TestCheckResourceAttr(resourceName, "privileges.0", string(sdk.AccountObjectPrivilegeCreateSchema)), + resource.TestCheckResourceAttr(resourceName, "privileges.1", string(sdk.AccountObjectPrivilegeMonitor)), + resource.TestCheckResourceAttr(resourceName, "privileges.2", string(sdk.AccountObjectPrivilegeUsage)), + resource.TestCheckResourceAttr(resourceName, "id", fmt.Sprintf("%s|false|false|CREATE SCHEMA,USAGE,MONITOR|OnDatabase|%s", databaseRoleName, databaseName)), + ), + }, + { + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantPrivilegesToDatabaseRole/UpdatePrivileges/all_privileges"), + ConfigVariables: configVariables(true, []sdk.AccountObjectPrivilege{}), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "all_privileges", "true"), + resource.TestCheckResourceAttr(resourceName, "privileges.#", "0"), + resource.TestCheckResourceAttr(resourceName, "id", fmt.Sprintf("%s|false|false|ALL|OnDatabase|%s", databaseRoleName, databaseName)), + ), + }, + { + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantPrivilegesToDatabaseRole/UpdatePrivileges/privileges"), + ConfigVariables: configVariables(false, []sdk.AccountObjectPrivilege{ + sdk.AccountObjectPrivilegeModify, + sdk.AccountObjectPrivilegeMonitor, + }), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "all_privileges", "false"), + resource.TestCheckResourceAttr(resourceName, "privileges.#", "2"), + resource.TestCheckResourceAttr(resourceName, "privileges.0", string(sdk.AccountObjectPrivilegeModify)), + resource.TestCheckResourceAttr(resourceName, "privileges.1", string(sdk.AccountObjectPrivilegeMonitor)), + resource.TestCheckResourceAttr(resourceName, "id", fmt.Sprintf("%s|false|false|MODIFY,MONITOR|OnDatabase|%s", databaseRoleName, databaseName)), + ), + }, + }, + }) +} + +func TestAcc_GrantPrivilegesToDatabaseRole_UpdatePrivileges_SnowflakeChecked(t *testing.T) { + name := "test_database_role_name" + schemaName := "test_database_role_schema_name" + configVariables := func(allPrivileges bool, privileges []string, schemaName string) config.Variables { + configVariables := config.Variables{ + "name": config.StringVariable(name), + "database": config.StringVariable(acc.TestDatabaseName), + } + if allPrivileges { + configVariables["all_privileges"] = config.BoolVariable(allPrivileges) + } + if len(privileges) > 0 { + configPrivileges := make([]config.Variable, len(privileges)) + for i, privilege := range privileges { + configPrivileges[i] = config.StringVariable(privilege) + } + configVariables["privileges"] = config.ListVariable(configPrivileges...) + } + if len(schemaName) > 0 { + configVariables["schema_name"] = config.StringVariable(schemaName) + } + return configVariables + } + + databaseRoleName := sdk.NewDatabaseObjectIdentifier(acc.TestDatabaseName, name) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + PreCheck: func() { acc.TestAccPreCheck(t) }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: testAccCheckDatabaseRolePrivilegesRevoked, + Steps: []resource.TestStep{ + { + PreConfig: func() { createDatabaseRoleOutsideTerraform(t, name) }, + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantPrivilegesToDatabaseRole/UpdatePrivileges_SnowflakeChecked/privileges"), + ConfigVariables: configVariables(false, []string{ + sdk.AccountObjectPrivilegeCreateSchema.String(), + sdk.AccountObjectPrivilegeModify.String(), + }, ""), + Check: queriedPrivilegesEqualTo( + databaseRoleName, + sdk.AccountObjectPrivilegeCreateSchema.String(), + sdk.AccountObjectPrivilegeModify.String(), + ), + }, + { + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantPrivilegesToDatabaseRole/UpdatePrivileges_SnowflakeChecked/all_privileges"), + ConfigVariables: configVariables(true, []string{}, ""), + Check: queriedPrivilegesContainAtLeast( + databaseRoleName, + sdk.AccountObjectPrivilegeCreateDatabaseRole.String(), + sdk.AccountObjectPrivilegeCreateSchema.String(), + sdk.AccountObjectPrivilegeModify.String(), + sdk.AccountObjectPrivilegeMonitor.String(), + sdk.AccountObjectPrivilegeUsage.String(), + ), + }, + { + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantPrivilegesToDatabaseRole/UpdatePrivileges_SnowflakeChecked/privileges"), + ConfigVariables: configVariables(false, []string{ + sdk.AccountObjectPrivilegeModify.String(), + sdk.AccountObjectPrivilegeMonitor.String(), + }, ""), + Check: queriedPrivilegesEqualTo( + databaseRoleName, + sdk.AccountObjectPrivilegeModify.String(), + sdk.AccountObjectPrivilegeMonitor.String(), + ), + }, + { + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantPrivilegesToDatabaseRole/UpdatePrivileges_SnowflakeChecked/on_schema"), + ConfigVariables: configVariables(false, []string{ + sdk.SchemaPrivilegeCreateTask.String(), + sdk.SchemaPrivilegeCreateExternalTable.String(), + }, schemaName), + Check: queriedPrivilegesEqualTo( + databaseRoleName, + sdk.SchemaPrivilegeCreateTask.String(), + sdk.SchemaPrivilegeCreateExternalTable.String(), + ), + }, + }, + }) +} + +func TestAcc_GrantPrivilegesToDatabaseRole_AlwaysApply(t *testing.T) { + name := "test_database_role_name" + configVariables := func(alwaysApply bool) config.Variables { + return config.Variables{ + "name": config.StringVariable(name), + "all_privileges": config.BoolVariable(true), + "database": config.StringVariable(acc.TestDatabaseName), + "always_apply": config.BoolVariable(alwaysApply), + } + } + resourceName := "snowflake_grant_privileges_to_database_role.test" + + databaseRoleName := sdk.NewDatabaseObjectIdentifier(acc.TestDatabaseName, name).FullyQualifiedName() + databaseName := sdk.NewAccountObjectIdentifier(acc.TestDatabaseName).FullyQualifiedName() + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + PreCheck: func() { acc.TestAccPreCheck(t) }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: testAccCheckDatabaseRolePrivilegesRevoked, + Steps: []resource.TestStep{ + { + PreConfig: func() { createDatabaseRoleOutsideTerraform(t, name) }, + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantPrivilegesToDatabaseRole/AlwaysApply"), + ConfigVariables: configVariables(false), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PostApplyPostRefresh: []plancheck.PlanCheck{ + plancheck.ExpectEmptyPlan(), + }, + }, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "always_apply", "false"), + resource.TestCheckResourceAttr(resourceName, "id", fmt.Sprintf("%s|false|false|ALL|OnDatabase|%s", databaseRoleName, databaseName)), + ), + }, + { + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantPrivilegesToDatabaseRole/AlwaysApply"), + ConfigVariables: configVariables(true), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "always_apply", "true"), + resource.TestCheckResourceAttr(resourceName, "id", fmt.Sprintf("%s|false|true|ALL|OnDatabase|%s", databaseRoleName, databaseName)), + ), + ExpectNonEmptyPlan: true, + }, + { + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantPrivilegesToDatabaseRole/AlwaysApply"), + ConfigVariables: configVariables(true), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectNonEmptyPlan(), + }, + }, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "always_apply", "true"), + resource.TestCheckResourceAttr(resourceName, "id", fmt.Sprintf("%s|false|true|ALL|OnDatabase|%s", databaseRoleName, databaseName)), + ), + ExpectNonEmptyPlan: true, + }, + { + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantPrivilegesToDatabaseRole/AlwaysApply"), + ConfigVariables: configVariables(true), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectNonEmptyPlan(), + }, + }, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "always_apply", "true"), + resource.TestCheckResourceAttr(resourceName, "id", fmt.Sprintf("%s|false|true|ALL|OnDatabase|%s", databaseRoleName, databaseName)), + ), + ExpectNonEmptyPlan: true, + }, + { + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantPrivilegesToDatabaseRole/AlwaysApply"), + ConfigVariables: configVariables(false), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PostApplyPostRefresh: []plancheck.PlanCheck{ + plancheck.ExpectEmptyPlan(), + }, + }, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "always_apply", "false"), + resource.TestCheckResourceAttr(resourceName, "id", fmt.Sprintf("%s|false|false|ALL|OnDatabase|%s", databaseRoleName, databaseName)), + ), + }, + }, + }) +} + +func createDatabaseRoleOutsideTerraform(t *testing.T, name string) { + t.Helper() + client, err := sdk.NewDefaultClient() + if err != nil { + t.Fatal(err) + } + ctx := context.Background() + databaseRoleId := sdk.NewDatabaseObjectIdentifier(acc.TestDatabaseName, name) + if err := client.DatabaseRoles.Create(ctx, sdk.NewCreateDatabaseRoleRequest(databaseRoleId).WithOrReplace(true)); err != nil { + t.Fatal(fmt.Errorf("error database role (%s): %w", databaseRoleId.FullyQualifiedName(), err)) + } +} + +// queriedPrivilegesEqualTo will check if all the privileges specified in the argument are granted in Snowflake. +// Any additional grants (other than usage and ownership) will be treated as an error. +func queriedPrivilegesEqualTo(databaseRoleName sdk.DatabaseObjectIdentifier, privileges ...string) func(s *terraform.State) error { + return func(s *terraform.State) error { + db := acc.TestAccProvider.Meta().(*sql.DB) + ctx := context.Background() + client := sdk.NewClientFromDB(db) + grants, err := client.Grants.Show(ctx, &sdk.ShowGrantOptions{ + To: &sdk.ShowGrantsTo{ + DatabaseRole: databaseRoleName, + }, + }) + if err != nil { + return err + } + for _, grant := range grants { + if grant.Privilege == "USAGE" || grant.Privilege == "OWNERSHIP" { + log.Printf("Skipping check for %s privilege as its one of the privileges that are implicitly granted by Snowflake", grant.Privilege) + continue + } + if !slices.Contains(privileges, grant.Privilege) { + return fmt.Errorf("grant not expected, grant: %v, not in %v", grants, privileges) + } + } + + return nil + } +} + +// queriedPrivilegesContainAtLeast will check if all the privileges specified in the argument are granted in Snowflake. +// Any additional grants will be ignored. +func queriedPrivilegesContainAtLeast(databaseRoleName sdk.DatabaseObjectIdentifier, privileges ...string) func(s *terraform.State) error { + return func(s *terraform.State) error { + db := acc.TestAccProvider.Meta().(*sql.DB) + ctx := context.Background() + client := sdk.NewClientFromDB(db) + grants, err := client.Grants.Show(ctx, &sdk.ShowGrantOptions{ + To: &sdk.ShowGrantsTo{ + DatabaseRole: databaseRoleName, + }, + }) + if err != nil { + return err + } + var grantedPrivileges []string + for _, grant := range grants { + grantedPrivileges = append(grantedPrivileges, grant.Privilege) + } + notAllPrivilegesInGrantedPrivileges := slices.ContainsFunc(privileges, func(privilege string) bool { + return !slices.Contains(grantedPrivileges, privilege) + }) + if notAllPrivilegesInGrantedPrivileges { + return fmt.Errorf("not every privilege from the list: %v was found in grant privileges: %v, for database role name: %s", privileges, grantedPrivileges, databaseRoleName.FullyQualifiedName()) + } + + return nil + } +} + +func testAccCheckDatabaseRolePrivilegesRevoked(s *terraform.State) error { + db := acc.TestAccProvider.Meta().(*sql.DB) + client := sdk.NewClientFromDB(db) + for _, rs := range s.RootModule().Resources { + if rs.Type != "snowflake_grant_privileges_to_database_role" { + continue + } + ctx := context.Background() + + id := sdk.NewDatabaseObjectIdentifierFromFullyQualifiedName(rs.Primary.Attributes["database_role_name"]) + grants, err := client.Grants.Show(ctx, &sdk.ShowGrantOptions{ + To: &sdk.ShowGrantsTo{ + DatabaseRole: id, + }, + }) + if err != nil { + return err + } + var grantedPrivileges []string + for _, grant := range grants { + // usage is the default privilege available after creation (it won't be revoked) + if grant.Privilege != "USAGE" { + grantedPrivileges = append(grantedPrivileges, grant.Privilege) + } + } + if len(grantedPrivileges) > 0 { + return fmt.Errorf("database role (%s) still grants , granted privileges %v", id.FullyQualifiedName(), grantedPrivileges) + } + } + return nil +} diff --git a/pkg/resources/grant_privileges_to_database_role_identifier.go b/pkg/resources/grant_privileges_to_database_role_identifier.go new file mode 100644 index 0000000000..f297e968d8 --- /dev/null +++ b/pkg/resources/grant_privileges_to_database_role_identifier.go @@ -0,0 +1,234 @@ +package resources + +import ( + "fmt" + "strconv" + "strings" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" +) + +type DatabaseRoleGrantKind string + +const ( + OnDatabaseDatabaseRoleGrantKind DatabaseRoleGrantKind = "OnDatabase" + OnSchemaDatabaseRoleGrantKind DatabaseRoleGrantKind = "OnSchema" + OnSchemaObjectDatabaseRoleGrantKind DatabaseRoleGrantKind = "OnSchemaObject" +) + +type OnSchemaGrantKind string + +const ( + OnSchemaSchemaGrantKind OnSchemaGrantKind = "OnSchema" + OnAllSchemasInDatabaseSchemaGrantKind OnSchemaGrantKind = "OnAllSchemasInDatabase" + OnFutureSchemasInDatabaseSchemaGrantKind OnSchemaGrantKind = "OnFutureSchemasInDatabase" +) + +type OnSchemaObjectGrantKind string + +const ( + OnObjectSchemaObjectGrantKind OnSchemaObjectGrantKind = "OnObject" + OnAllSchemaObjectGrantKind OnSchemaObjectGrantKind = "OnAll" + OnFutureSchemaObjectGrantKind OnSchemaObjectGrantKind = "OnFuture" +) + +type GrantPrivilegesToDatabaseRoleId struct { + DatabaseRoleName sdk.DatabaseObjectIdentifier + WithGrantOption bool + AlwaysApply bool + AllPrivileges bool + Privileges []string + Kind DatabaseRoleGrantKind + Data fmt.Stringer +} + +func (g *GrantPrivilegesToDatabaseRoleId) String() string { + var parts []string + parts = append(parts, g.DatabaseRoleName.FullyQualifiedName()) + parts = append(parts, strconv.FormatBool(g.WithGrantOption)) + parts = append(parts, strconv.FormatBool(g.AlwaysApply)) + if g.AllPrivileges { + parts = append(parts, "ALL") + } else { + parts = append(parts, strings.Join(g.Privileges, ",")) + } + parts = append(parts, string(g.Kind)) + parts = append(parts, g.Data.String()) + return strings.Join(parts, helpers.IDDelimiter) +} + +type OnDatabaseGrantData struct { + DatabaseName sdk.AccountObjectIdentifier +} + +func (d *OnDatabaseGrantData) String() string { + return d.DatabaseName.FullyQualifiedName() +} + +type OnSchemaGrantData struct { + Kind OnSchemaGrantKind + SchemaName *sdk.DatabaseObjectIdentifier + DatabaseName *sdk.AccountObjectIdentifier +} + +func (d *OnSchemaGrantData) String() string { + var parts []string + parts = append(parts, string(d.Kind)) + switch d.Kind { + case OnSchemaSchemaGrantKind: + parts = append(parts, d.SchemaName.FullyQualifiedName()) + case OnAllSchemasInDatabaseSchemaGrantKind, OnFutureSchemasInDatabaseSchemaGrantKind: + parts = append(parts, d.DatabaseName.FullyQualifiedName()) + } + return strings.Join(parts, helpers.IDDelimiter) +} + +type OnSchemaObjectGrantData struct { + Kind OnSchemaObjectGrantKind + Object *sdk.Object + OnAllOrFuture *BulkOperationGrantData +} + +func (d *OnSchemaObjectGrantData) String() string { + var parts []string + parts = append(parts, string(d.Kind)) + switch d.Kind { + case OnObjectSchemaObjectGrantKind: + parts = append(parts, fmt.Sprintf("%s|%s", d.Object.ObjectType, d.Object.Name.FullyQualifiedName())) + case OnAllSchemaObjectGrantKind, OnFutureSchemaObjectGrantKind: + parts = append(parts, d.OnAllOrFuture.ObjectNamePlural.String()) + parts = append(parts, string(d.OnAllOrFuture.Kind)) + switch d.OnAllOrFuture.Kind { + case InDatabaseBulkOperationGrantKind: + parts = append(parts, d.OnAllOrFuture.Database.FullyQualifiedName()) + case InSchemaBulkOperationGrantKind: + parts = append(parts, d.OnAllOrFuture.Schema.FullyQualifiedName()) + } + } + return strings.Join(parts, helpers.IDDelimiter) +} + +type BulkOperationGrantKind string + +const ( + InDatabaseBulkOperationGrantKind BulkOperationGrantKind = "InDatabase" + InSchemaBulkOperationGrantKind BulkOperationGrantKind = "InSchema" +) + +type BulkOperationGrantData struct { + ObjectNamePlural sdk.PluralObjectType + Kind BulkOperationGrantKind + Database *sdk.AccountObjectIdentifier + Schema *sdk.DatabaseObjectIdentifier +} + +func ParseGrantPrivilegesToDatabaseRoleId(id string) (GrantPrivilegesToDatabaseRoleId, error) { + var databaseRoleId GrantPrivilegesToDatabaseRoleId + + parts := strings.Split(id, helpers.IDDelimiter) + if len(parts) < 6 { + return databaseRoleId, sdk.NewError(`database role identifier should hold at least 5 parts "|||||"`) + } + + // TODO: Identifier parsing should be replaced with better version introduced in SNOW-999049. + // Right now, it's same as sdk.NewDatabaseObjectIdentifierFromFullyQualifiedName, but with error handling. + databaseRoleNameParts := strings.Split(parts[0], ".") + if len(databaseRoleNameParts) == 0 || + (len(databaseRoleNameParts) == 1 && databaseRoleNameParts[0] == "") || + (len(databaseRoleNameParts) == 2 && databaseRoleNameParts[1] == "") || + len(databaseRoleNameParts) > 2 { + return databaseRoleId, sdk.NewError(fmt.Sprintf(`invalid DatabaseRoleName value: %s, should be a fully qualified name of database object .`, parts[0])) + } + databaseRoleId.DatabaseRoleName = sdk.NewDatabaseObjectIdentifier( + strings.Trim(databaseRoleNameParts[0], `"`), + strings.Trim(databaseRoleNameParts[1], `"`), + ) + + if parts[1] != "false" && parts[1] != "true" { + return databaseRoleId, sdk.NewError(fmt.Sprintf(`invalid WithGrantOption value: %s, should be either "true" or "false"`, parts[1])) + } + databaseRoleId.WithGrantOption = parts[1] == "true" + + if parts[2] != "false" && parts[2] != "true" { + return databaseRoleId, sdk.NewError(fmt.Sprintf(`invalid AlwaysApply value: %s, should be either "true" or "false"`, parts[2])) + } + databaseRoleId.AlwaysApply = parts[2] == "true" + + privileges := strings.Split(parts[3], ",") + if len(privileges) == 0 || (len(privileges) == 1 && privileges[0] == "") { + return databaseRoleId, sdk.NewError(fmt.Sprintf(`invalid Privileges value: %s, should be either a comma separated list of privileges or "ALL" / "ALL PRIVILEGES" for all privileges`, parts[3])) + } + if len(privileges) == 1 && (privileges[0] == "ALL" || privileges[0] == "ALL PRIVILEGES") { + databaseRoleId.AllPrivileges = true + } else { + databaseRoleId.Privileges = privileges + } + databaseRoleId.Kind = DatabaseRoleGrantKind(parts[4]) + + switch databaseRoleId.Kind { + case OnDatabaseDatabaseRoleGrantKind: + databaseRoleId.Data = &OnDatabaseGrantData{ + DatabaseName: sdk.NewAccountObjectIdentifierFromFullyQualifiedName(parts[5]), + } + case OnSchemaDatabaseRoleGrantKind: + if len(parts) < 7 { + return databaseRoleId, sdk.NewError(`database role identifier should hold at least 7 parts "||||||..."`) + } + onSchemaGrantData := OnSchemaGrantData{ + Kind: OnSchemaGrantKind(parts[5]), + } + switch onSchemaGrantData.Kind { + case OnSchemaSchemaGrantKind: + onSchemaGrantData.SchemaName = sdk.Pointer(sdk.NewDatabaseObjectIdentifierFromFullyQualifiedName(parts[6])) + case OnAllSchemasInDatabaseSchemaGrantKind, OnFutureSchemasInDatabaseSchemaGrantKind: + onSchemaGrantData.DatabaseName = sdk.Pointer(sdk.NewAccountObjectIdentifier(parts[6])) + default: + return databaseRoleId, sdk.NewError(fmt.Sprintf("invalid OnSchemaGrantKind: %s", onSchemaGrantData.Kind)) + } + databaseRoleId.Data = &onSchemaGrantData + case OnSchemaObjectDatabaseRoleGrantKind: + if len(parts) < 7 { + return databaseRoleId, sdk.NewError(`database role identifier should hold at least 7 parts "||||||..."`) + } + onSchemaObjectGrantData := OnSchemaObjectGrantData{ + Kind: OnSchemaObjectGrantKind(parts[5]), + } + switch onSchemaObjectGrantData.Kind { + case OnObjectSchemaObjectGrantKind: + if len(parts) != 8 { + return databaseRoleId, sdk.NewError(`database role identifier should hold 8 parts "||||OnSchemaObject|OnObject||"`) + } + onSchemaObjectGrantData.Object = &sdk.Object{ + ObjectType: sdk.ObjectType(parts[6]), + Name: sdk.NewSchemaObjectIdentifierFromFullyQualifiedName(parts[7]), + } + case OnAllSchemaObjectGrantKind, OnFutureSchemaObjectGrantKind: + bulkOperationGrantData := &BulkOperationGrantData{ + ObjectNamePlural: sdk.PluralObjectType(parts[6]), + } + if len(parts) > 7 { + if len(parts) != 9 { + return databaseRoleId, sdk.NewError(`database role identifier should hold 9 parts "||||OnSchemaObject|On[All or Future]||In[Database or Schema]|"`) + } + bulkOperationGrantData.Kind = BulkOperationGrantKind(parts[7]) + switch bulkOperationGrantData.Kind { + case InDatabaseBulkOperationGrantKind: + bulkOperationGrantData.Database = sdk.Pointer(sdk.NewAccountObjectIdentifierFromFullyQualifiedName(parts[8])) + case InSchemaBulkOperationGrantKind: + bulkOperationGrantData.Schema = sdk.Pointer(sdk.NewDatabaseObjectIdentifierFromFullyQualifiedName(parts[8])) + default: + return databaseRoleId, sdk.NewError(fmt.Sprintf("invalid BulkOperationGrantKind: %s", bulkOperationGrantData.Kind)) + } + } + onSchemaObjectGrantData.OnAllOrFuture = bulkOperationGrantData + default: + return databaseRoleId, sdk.NewError(fmt.Sprintf("invalid OnSchemaObjectGrantKind: %s", onSchemaObjectGrantData.Kind)) + } + databaseRoleId.Data = &onSchemaObjectGrantData + default: + return databaseRoleId, sdk.NewError(fmt.Sprintf("invalid DatabaseRoleGrantKind: %s", databaseRoleId.Kind)) + } + + return databaseRoleId, nil +} diff --git a/pkg/resources/grant_privileges_to_database_role_identifier_test.go b/pkg/resources/grant_privileges_to_database_role_identifier_test.go new file mode 100644 index 0000000000..6d6668816b --- /dev/null +++ b/pkg/resources/grant_privileges_to_database_role_identifier_test.go @@ -0,0 +1,429 @@ +package resources + +import ( + "testing" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + "github.com/stretchr/testify/assert" +) + +func TestParseGrantPrivilegesToDatabaseRoleId(t *testing.T) { + testCases := []struct { + Name string + Identifier string + Expected GrantPrivilegesToDatabaseRoleId + Error string + }{ + { + Name: "grant database role on database", + Identifier: `"database-name"."database-role"|false|false|CREATE SCHEMA,USAGE,MONITOR|OnDatabase|"on-database-name"`, + Expected: GrantPrivilegesToDatabaseRoleId{ + DatabaseRoleName: sdk.NewDatabaseObjectIdentifier("database-name", "database-role"), + WithGrantOption: false, + Privileges: []string{"CREATE SCHEMA", "USAGE", "MONITOR"}, + Kind: OnDatabaseDatabaseRoleGrantKind, + Data: &OnDatabaseGrantData{ + DatabaseName: sdk.NewAccountObjectIdentifier("on-database-name"), + }, + }, + }, + { + Name: "grant database role on database - always apply with grant option", + Identifier: `"database-name"."database-role"|true|true|CREATE SCHEMA,USAGE,MONITOR|OnDatabase|"on-database-name"`, + Expected: GrantPrivilegesToDatabaseRoleId{ + DatabaseRoleName: sdk.NewDatabaseObjectIdentifier("database-name", "database-role"), + WithGrantOption: true, + AlwaysApply: true, + Privileges: []string{"CREATE SCHEMA", "USAGE", "MONITOR"}, + Kind: OnDatabaseDatabaseRoleGrantKind, + Data: &OnDatabaseGrantData{ + DatabaseName: sdk.NewAccountObjectIdentifier("on-database-name"), + }, + }, + }, + { + Name: "grant database role on database - all privileges", + Identifier: `"database-name"."database-role"|false|false|ALL|OnDatabase|"on-database-name"`, + Expected: GrantPrivilegesToDatabaseRoleId{ + DatabaseRoleName: sdk.NewDatabaseObjectIdentifier("database-name", "database-role"), + WithGrantOption: false, + AllPrivileges: true, + Privileges: nil, + Kind: OnDatabaseDatabaseRoleGrantKind, + Data: &OnDatabaseGrantData{ + DatabaseName: sdk.NewAccountObjectIdentifier("on-database-name"), + }, + }, + }, + { + Name: "grant database role on schema with schema name", + Identifier: `"database-name"."database-role"|false|false|CREATE SCHEMA,USAGE,MONITOR|OnSchema|OnSchema|"database-name"."schema-name"`, + Expected: GrantPrivilegesToDatabaseRoleId{ + DatabaseRoleName: sdk.NewDatabaseObjectIdentifier("database-name", "database-role"), + WithGrantOption: false, + Privileges: []string{"CREATE SCHEMA", "USAGE", "MONITOR"}, + Kind: OnSchemaDatabaseRoleGrantKind, + Data: &OnSchemaGrantData{ + Kind: OnSchemaSchemaGrantKind, + SchemaName: sdk.Pointer(sdk.NewDatabaseObjectIdentifier("database-name", "schema-name")), + }, + }, + }, + { + Name: "grant database role on all schemas in database", + Identifier: `"database-name"."database-role"|false|false|CREATE SCHEMA,USAGE,MONITOR|OnSchema|OnAllSchemasInDatabase|"database-name-123"`, + Expected: GrantPrivilegesToDatabaseRoleId{ + DatabaseRoleName: sdk.NewDatabaseObjectIdentifier("database-name", "database-role"), + WithGrantOption: false, + Privileges: []string{"CREATE SCHEMA", "USAGE", "MONITOR"}, + Kind: OnSchemaDatabaseRoleGrantKind, + Data: &OnSchemaGrantData{ + Kind: OnAllSchemasInDatabaseSchemaGrantKind, + DatabaseName: sdk.Pointer(sdk.NewAccountObjectIdentifier("database-name-123")), + }, + }, + }, + { + Name: "grant database role on future schemas in database", + Identifier: `"database-name"."database-role"|false|false|CREATE SCHEMA,USAGE,MONITOR|OnSchema|OnFutureSchemasInDatabase|"database-name-123"`, + Expected: GrantPrivilegesToDatabaseRoleId{ + DatabaseRoleName: sdk.NewDatabaseObjectIdentifier("database-name", "database-role"), + WithGrantOption: false, + Privileges: []string{"CREATE SCHEMA", "USAGE", "MONITOR"}, + Kind: OnSchemaDatabaseRoleGrantKind, + Data: &OnSchemaGrantData{ + Kind: OnFutureSchemasInDatabaseSchemaGrantKind, + DatabaseName: sdk.Pointer(sdk.NewAccountObjectIdentifier("database-name-123")), + }, + }, + }, + { + Name: "grant database role on schema object with on object option", + Identifier: `"database-name"."database-role"|false|false|CREATE SCHEMA,USAGE,MONITOR|OnSchemaObject|OnObject|TABLE|"database-name"."schema-name"."table-name"`, + Expected: GrantPrivilegesToDatabaseRoleId{ + DatabaseRoleName: sdk.NewDatabaseObjectIdentifier("database-name", "database-role"), + WithGrantOption: false, + Privileges: []string{"CREATE SCHEMA", "USAGE", "MONITOR"}, + Kind: OnSchemaObjectDatabaseRoleGrantKind, + Data: &OnSchemaObjectGrantData{ + Kind: OnObjectSchemaObjectGrantKind, + Object: &sdk.Object{ + ObjectType: sdk.ObjectTypeTable, + Name: sdk.NewSchemaObjectIdentifier("database-name", "schema-name", "table-name"), + }, + }, + }, + }, + { + Name: "grant database role on schema object with on all option", + Identifier: `"database-name"."database-role"|false|false|CREATE SCHEMA,USAGE,MONITOR|OnSchemaObject|OnAll|TABLES`, + Expected: GrantPrivilegesToDatabaseRoleId{ + DatabaseRoleName: sdk.NewDatabaseObjectIdentifier("database-name", "database-role"), + WithGrantOption: false, + Privileges: []string{"CREATE SCHEMA", "USAGE", "MONITOR"}, + Kind: OnSchemaObjectDatabaseRoleGrantKind, + Data: &OnSchemaObjectGrantData{ + Kind: OnAllSchemaObjectGrantKind, + OnAllOrFuture: &BulkOperationGrantData{ + ObjectNamePlural: "TABLES", + }, + }, + }, + }, + { + Name: "grant database role on schema object with on all option in database", + Identifier: `"database-name"."database-role"|false|false|CREATE SCHEMA,USAGE,MONITOR|OnSchemaObject|OnAll|TABLES|InDatabase|"database-name-123"`, + Expected: GrantPrivilegesToDatabaseRoleId{ + DatabaseRoleName: sdk.NewDatabaseObjectIdentifier("database-name", "database-role"), + WithGrantOption: false, + Privileges: []string{"CREATE SCHEMA", "USAGE", "MONITOR"}, + Kind: OnSchemaObjectDatabaseRoleGrantKind, + Data: &OnSchemaObjectGrantData{ + Kind: OnAllSchemaObjectGrantKind, + OnAllOrFuture: &BulkOperationGrantData{ + ObjectNamePlural: "TABLES", + Kind: InDatabaseBulkOperationGrantKind, + Database: sdk.Pointer(sdk.NewAccountObjectIdentifier("database-name-123")), + }, + }, + }, + }, + { + Name: "grant database role on schema object with on all option in schema", + Identifier: `"database-name"."database-role"|false|false|CREATE SCHEMA,USAGE,MONITOR|OnSchemaObject|OnAll|TABLES|InSchema|"database-name"."schema-name"`, + Expected: GrantPrivilegesToDatabaseRoleId{ + DatabaseRoleName: sdk.NewDatabaseObjectIdentifier("database-name", "database-role"), + WithGrantOption: false, + Privileges: []string{"CREATE SCHEMA", "USAGE", "MONITOR"}, + Kind: OnSchemaObjectDatabaseRoleGrantKind, + Data: &OnSchemaObjectGrantData{ + Kind: OnAllSchemaObjectGrantKind, + OnAllOrFuture: &BulkOperationGrantData{ + ObjectNamePlural: "TABLES", + Kind: InSchemaBulkOperationGrantKind, + Schema: sdk.Pointer(sdk.NewDatabaseObjectIdentifier("database-name", "schema-name")), + }, + }, + }, + }, + { + Name: "grant database role on schema object with on future option", + Identifier: `"database-name"."database-role"|false|false|CREATE SCHEMA,USAGE,MONITOR|OnSchemaObject|OnFuture|TABLES`, + Expected: GrantPrivilegesToDatabaseRoleId{ + DatabaseRoleName: sdk.NewDatabaseObjectIdentifier("database-name", "database-role"), + WithGrantOption: false, + Privileges: []string{"CREATE SCHEMA", "USAGE", "MONITOR"}, + Kind: OnSchemaObjectDatabaseRoleGrantKind, + Data: &OnSchemaObjectGrantData{ + Kind: OnFutureSchemaObjectGrantKind, + OnAllOrFuture: &BulkOperationGrantData{ + ObjectNamePlural: "TABLES", + }, + }, + }, + }, + { + Name: "grant database role on schema object with on all option in database", + Identifier: `"database-name"."database-role"|false|false|CREATE SCHEMA,USAGE,MONITOR|OnSchemaObject|OnFuture|TABLES|InDatabase|"database-name-123"`, + Expected: GrantPrivilegesToDatabaseRoleId{ + DatabaseRoleName: sdk.NewDatabaseObjectIdentifier("database-name", "database-role"), + WithGrantOption: false, + Privileges: []string{"CREATE SCHEMA", "USAGE", "MONITOR"}, + Kind: OnSchemaObjectDatabaseRoleGrantKind, + Data: &OnSchemaObjectGrantData{ + Kind: OnFutureSchemaObjectGrantKind, + OnAllOrFuture: &BulkOperationGrantData{ + ObjectNamePlural: "TABLES", + Kind: InDatabaseBulkOperationGrantKind, + Database: sdk.Pointer(sdk.NewAccountObjectIdentifier("database-name-123")), + }, + }, + }, + }, + { + Name: "grant database role on schema object with on all option in schema", + Identifier: `"database-name"."database-role"|false|false|CREATE SCHEMA,USAGE,MONITOR|OnSchemaObject|OnFuture|TABLES|InSchema|"database-name"."schema-name"`, + Expected: GrantPrivilegesToDatabaseRoleId{ + DatabaseRoleName: sdk.NewDatabaseObjectIdentifier("database-name", "database-role"), + WithGrantOption: false, + Privileges: []string{"CREATE SCHEMA", "USAGE", "MONITOR"}, + Kind: OnSchemaObjectDatabaseRoleGrantKind, + Data: &OnSchemaObjectGrantData{ + Kind: OnFutureSchemaObjectGrantKind, + OnAllOrFuture: &BulkOperationGrantData{ + ObjectNamePlural: "TABLES", + Kind: InSchemaBulkOperationGrantKind, + Schema: sdk.Pointer(sdk.NewDatabaseObjectIdentifier("database-name", "schema-name")), + }, + }, + }, + }, + { + Name: "validation: grant database role not enough parts", + Identifier: `"database-name"."role-name"|false|false`, + Error: "database role identifier should hold at least 5 parts", + }, + { + Name: "validation: grant database role not enough parts for OnDatabase kind", + Identifier: `"database-name"."role-name"|false|false|CREATE SCHEMA,USAGE,MONITOR|OnDatabase`, + Error: "database role identifier should hold at least 5 parts", + }, + { + Name: "validation: grant database role not enough parts for OnSchema kind", + Identifier: `"database-name"."role-name"|false|false|CREATE SCHEMA,USAGE,MONITOR|OnSchema|OnAllSchemasInDatabase`, + Error: "database role identifier should hold at least 7 parts", + }, + { + Name: "validation: grant database role not enough parts for OnSchemaObject kind", + Identifier: `"database-name"."role-name"|false|false|CREATE SCHEMA,USAGE,MONITOR|OnSchemaObject|OnObject`, + Error: "database role identifier should hold at least 7 parts", + }, + { + Name: "validation: grant database role not enough parts for OnSchemaObject kind", + Identifier: `"database-name"."role-name"|false|false|CREATE SCHEMA,USAGE,MONITOR|OnSchemaObject|OnObject|TABLE`, + Error: "database role identifier should hold 8 parts", + }, + { + Name: "validation: grant database role not enough parts for OnSchemaObject.InDatabase kind", + Identifier: `"database-name"."role-name"|false|false|CREATE SCHEMA,USAGE,MONITOR|OnSchemaObject|OnAll|TABLES|InDatabase`, + Error: "database role identifier should hold 9 parts", + }, + { + Name: "validation: grant database role invalid DatabaseRoleGrantKind kind", + Identifier: `"database-name"."role-name"|false|false|CREATE SCHEMA,USAGE,MONITOR|some-kind|some-data`, + Error: "invalid DatabaseRoleGrantKind: some-kind", + }, + { + Name: "validation: grant database role invalid OnSchemaGrantKind kind", + Identifier: `"database-name"."role-name"|false|false|CREATE SCHEMA,USAGE,MONITOR|OnSchema|some-kind|some-data`, + Error: "invalid OnSchemaGrantKind: some-kind", + }, + { + Name: "validation: grant database role invalid OnSchemaObjectGrantKind kind", + Identifier: `"database-name"."role-name"|false|false|CREATE SCHEMA,USAGE,MONITOR|OnSchemaObject|some-kind|some-data`, + Error: "invalid OnSchemaObjectGrantKind: some-kind", + }, + { + Name: "validation: grant database role empty privileges", + Identifier: `"database-name"."database-role"|false|false||OnDatabase|"on-database-name"`, + Error: `invalid Privileges value: , should be either a comma separated list of privileges or "ALL" / "ALL PRIVILEGES" for all privileges`, + }, + { + Name: "validation: grant database role empty with grant option", + Identifier: `"database-name"."database-role"||false|ALL PRIVILEGES|OnDatabase|"on-database-name"`, + Error: `invalid WithGrantOption value: , should be either "true" or "false"`, + }, + { + Name: "validation: grant database role empty always apply", + Identifier: `"database-name"."database-role"|false||ALL PRIVILEGES|OnDatabase|"on-database-name"`, + Error: `invalid AlwaysApply value: , should be either "true" or "false"`, + }, + { + Name: "validation: grant database role empty database role name", + Identifier: `|false|false|ALL PRIVILEGES|OnDatabase|"on-database-name"`, + Error: "invalid DatabaseRoleName value: , should be a fully qualified name of database object .", + }, + { + Name: "validation: grant database role empty type", + Identifier: `"database-name"."database-role"|false|false|ALL PRIVILEGES||"on-database-name"`, + Error: "invalid DatabaseRoleGrantKind: ", + }, + } + + for _, tt := range testCases { + t.Run(tt.Name, func(t *testing.T) { + id, err := ParseGrantPrivilegesToDatabaseRoleId(tt.Identifier) + if tt.Error == "" { + assert.NoError(t, err) + assert.Equal(t, tt.Expected, id) + } else { + assert.ErrorContains(t, err, tt.Error) + } + }) + } +} + +func TestGrantPrivilegesToDatabaseRoleIdString(t *testing.T) { + testCases := []struct { + Name string + Identifier GrantPrivilegesToDatabaseRoleId + Expected string + Error string + }{ + { + Name: "grant database role on database", + Identifier: GrantPrivilegesToDatabaseRoleId{ + DatabaseRoleName: sdk.NewDatabaseObjectIdentifier("database-name", "role-name"), + WithGrantOption: true, + AllPrivileges: true, + Kind: OnDatabaseDatabaseRoleGrantKind, + AlwaysApply: true, + Data: &OnDatabaseGrantData{ + DatabaseName: sdk.NewAccountObjectIdentifier("database-name"), + }, + }, + Expected: `"database-name"."role-name"|true|true|ALL|OnDatabase|"database-name"`, + }, + { + Name: "grant database role on schema on schema", + Identifier: GrantPrivilegesToDatabaseRoleId{ + DatabaseRoleName: sdk.NewDatabaseObjectIdentifier("database-name", "role-name"), + WithGrantOption: false, + Privileges: []string{"CREATE SCHEMA", "USAGE", "MONITOR"}, + Kind: OnSchemaDatabaseRoleGrantKind, + Data: &OnSchemaGrantData{ + Kind: OnSchemaSchemaGrantKind, + SchemaName: sdk.Pointer(sdk.NewDatabaseObjectIdentifier("database-name", "schema-name")), + }, + }, + Expected: `"database-name"."role-name"|false|false|CREATE SCHEMA,USAGE,MONITOR|OnSchema|OnSchema|"database-name"."schema-name"`, + }, + { + Name: "grant database role on all schemas in database", + Identifier: GrantPrivilegesToDatabaseRoleId{ + DatabaseRoleName: sdk.NewDatabaseObjectIdentifier("database-name", "role-name"), + WithGrantOption: false, + Privileges: []string{"CREATE SCHEMA", "USAGE", "MONITOR"}, + Kind: OnSchemaDatabaseRoleGrantKind, + Data: &OnSchemaGrantData{ + Kind: OnAllSchemasInDatabaseSchemaGrantKind, + DatabaseName: sdk.Pointer(sdk.NewAccountObjectIdentifier("database-name")), + }, + }, + Expected: `"database-name"."role-name"|false|false|CREATE SCHEMA,USAGE,MONITOR|OnSchema|OnAllSchemasInDatabase|"database-name"`, + }, + { + Name: "grant database role on future schemas in database", + Identifier: GrantPrivilegesToDatabaseRoleId{ + DatabaseRoleName: sdk.NewDatabaseObjectIdentifier("database-name", "role-name"), + WithGrantOption: false, + Privileges: []string{"CREATE SCHEMA", "USAGE", "MONITOR"}, + Kind: OnSchemaDatabaseRoleGrantKind, + Data: &OnSchemaGrantData{ + Kind: OnFutureSchemasInDatabaseSchemaGrantKind, + DatabaseName: sdk.Pointer(sdk.NewAccountObjectIdentifier("database-name")), + }, + }, + Expected: `"database-name"."role-name"|false|false|CREATE SCHEMA,USAGE,MONITOR|OnSchema|OnFutureSchemasInDatabase|"database-name"`, + }, + { + Name: "grant database role on schema object on object", + Identifier: GrantPrivilegesToDatabaseRoleId{ + DatabaseRoleName: sdk.NewDatabaseObjectIdentifier("database-name", "role-name"), + WithGrantOption: false, + Privileges: []string{"CREATE SCHEMA", "USAGE", "MONITOR"}, + Kind: OnSchemaObjectDatabaseRoleGrantKind, + Data: &OnSchemaObjectGrantData{ + Kind: OnObjectSchemaObjectGrantKind, + Object: &sdk.Object{ + ObjectType: sdk.ObjectTypeTable, + Name: sdk.NewSchemaObjectIdentifier("database-name", "schema-name", "table-name"), + }, + }, + }, + Expected: `"database-name"."role-name"|false|false|CREATE SCHEMA,USAGE,MONITOR|OnSchemaObject|OnObject|TABLE|"database-name"."schema-name"."table-name"`, + }, + { + Name: "grant database role on schema object on all tables in database", + Identifier: GrantPrivilegesToDatabaseRoleId{ + DatabaseRoleName: sdk.NewDatabaseObjectIdentifier("database-name", "role-name"), + WithGrantOption: false, + Privileges: []string{"CREATE SCHEMA", "USAGE", "MONITOR"}, + Kind: OnSchemaObjectDatabaseRoleGrantKind, + Data: &OnSchemaObjectGrantData{ + Kind: OnAllSchemaObjectGrantKind, + OnAllOrFuture: &BulkOperationGrantData{ + ObjectNamePlural: sdk.PluralObjectTypeTables, + Kind: InDatabaseBulkOperationGrantKind, + Database: sdk.Pointer(sdk.NewAccountObjectIdentifier("database-name")), + }, + }, + }, + Expected: `"database-name"."role-name"|false|false|CREATE SCHEMA,USAGE,MONITOR|OnSchemaObject|OnAll|TABLES|InDatabase|"database-name"`, + }, + { + Name: "grant database role on schema object on all tables in schema", + Identifier: GrantPrivilegesToDatabaseRoleId{ + DatabaseRoleName: sdk.NewDatabaseObjectIdentifier("database-name", "role-name"), + WithGrantOption: false, + Privileges: []string{"CREATE SCHEMA", "USAGE", "MONITOR"}, + Kind: OnSchemaObjectDatabaseRoleGrantKind, + Data: &OnSchemaObjectGrantData{ + Kind: OnAllSchemaObjectGrantKind, + OnAllOrFuture: &BulkOperationGrantData{ + ObjectNamePlural: sdk.PluralObjectTypeTables, + Kind: InSchemaBulkOperationGrantKind, + Schema: sdk.Pointer(sdk.NewDatabaseObjectIdentifier("database-name", "schema-name")), + }, + }, + }, + Expected: `"database-name"."role-name"|false|false|CREATE SCHEMA,USAGE,MONITOR|OnSchemaObject|OnAll|TABLES|InSchema|"database-name"."schema-name"`, + }, + } + + for _, tt := range testCases { + t.Run(tt.Name, func(t *testing.T) { + assert.Equal(t, tt.Expected, tt.Identifier.String()) + }) + } +} diff --git a/pkg/resources/grant_privileges_to_role.go b/pkg/resources/grant_privileges_to_role.go index 8de01aa7ae..25deef01bc 100644 --- a/pkg/resources/grant_privileges_to_role.go +++ b/pkg/resources/grant_privileges_to_role.go @@ -40,17 +40,17 @@ var grantPrivilegesToRoleSchema = map[string]*schema.Schema{ Type: schema.TypeBool, Optional: true, Default: false, + ForceNew: true, Description: "If true, the privileges will be granted on the account.", ConflictsWith: []string{"on_account_object", "on_schema", "on_schema_object"}, - ForceNew: true, }, "on_account_object": { Type: schema.TypeList, Optional: true, + ForceNew: true, MaxItems: 1, ConflictsWith: []string{"on_account", "on_schema", "on_schema_object"}, Description: "Specifies the account object on which privileges will be granted ", - ForceNew: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "object_type": { @@ -91,22 +91,24 @@ var grantPrivilegesToRoleSchema = map[string]*schema.Schema{ Optional: true, Description: "The fully qualified name of the schema.", ConflictsWith: []string{"on_schema.0.all_schemas_in_database", "on_schema.0.future_schemas_in_database"}, - ValidateDiagFunc: IsValidIdentifier[sdk.DatabaseObjectIdentifier](), ForceNew: true, + ValidateDiagFunc: IsValidIdentifier[sdk.DatabaseObjectIdentifier](), }, "all_schemas_in_database": { - Type: schema.TypeString, - Optional: true, - Description: "The fully qualified name of the database.", - ConflictsWith: []string{"on_schema.0.schema_name", "on_schema.0.future_schemas_in_database"}, - ForceNew: true, + Type: schema.TypeString, + Optional: true, + Description: "The fully qualified name of the database.", + ConflictsWith: []string{"on_schema.0.schema_name", "on_schema.0.future_schemas_in_database"}, + ForceNew: true, + ValidateDiagFunc: IsValidIdentifier[sdk.AccountObjectIdentifier](), }, "future_schemas_in_database": { - Type: schema.TypeString, - Optional: true, - Description: "The fully qualified name of the database.", - ConflictsWith: []string{"on_schema.0.schema_name", "on_schema.0.all_schemas_in_database"}, - ForceNew: true, + Type: schema.TypeString, + Optional: true, + Description: "The fully qualified name of the database.", + ConflictsWith: []string{"on_schema.0.schema_name", "on_schema.0.all_schemas_in_database"}, + ForceNew: true, + ValidateDiagFunc: IsValidIdentifier[sdk.AccountObjectIdentifier](), }, }, }, @@ -121,45 +123,22 @@ var grantPrivilegesToRoleSchema = map[string]*schema.Schema{ Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "object_type": { - Type: schema.TypeString, - Optional: true, - Description: "The object type of the schema object on which privileges will be granted. Valid values are: ALERT | DYNAMIC TABLE | EVENT TABLE | FILE FORMAT | FUNCTION | ICEBERG TABLE | PROCEDURE | SECRET | SEQUENCE | PIPE | MASKING POLICY | PASSWORD POLICY | ROW ACCESS POLICY | SESSION POLICY | TAG | STAGE | STREAM | TABLE | EXTERNAL TABLE | TASK | VIEW | MATERIALIZED VIEW", - RequiredWith: []string{"on_schema_object.0.object_name"}, - ConflictsWith: []string{"on_schema_object.0.all", "on_schema_object.0.future"}, - ForceNew: true, - ValidateFunc: validation.StringInSlice([]string{ - "ALERT", - "DYNAMIC TABLE", - "EVENT TABLE", - "FILE FORMAT", - "FUNCTION", - "ICEBERG TABLE", - "PROCEDURE", - "SECRET", - "SEQUENCE", - "PIPE", - "MASKING POLICY", - "PASSWORD POLICY", - "ROW ACCESS POLICY", - "SESSION POLICY", - "TAG", - "STAGE", - "STREAM", - "TABLE", - "EXTERNAL TABLE", - "TASK", - "VIEW", - "MATERIALIZED VIEW", - }, true), + Type: schema.TypeString, + Optional: true, + Description: "The object type of the schema object on which privileges will be granted. Valid values are: ALERT | DYNAMIC TABLE | EVENT TABLE | FILE FORMAT | FUNCTION | ICEBERG TABLE | PROCEDURE | SECRET | SEQUENCE | PIPE | MASKING POLICY | PASSWORD POLICY | ROW ACCESS POLICY | SESSION POLICY | TAG | STAGE | STREAM | TABLE | EXTERNAL TABLE | TASK | VIEW | MATERIALIZED VIEW", + RequiredWith: []string{"on_schema_object.0.object_name"}, + ConflictsWith: []string{"on_schema_object.0.all", "on_schema_object.0.future"}, + ForceNew: true, + ValidateDiagFunc: ValidObjectType(), }, "object_name": { Type: schema.TypeString, Optional: true, + ForceNew: true, Description: "The fully qualified name of the object on which privileges will be granted.", RequiredWith: []string{"on_schema_object.0.object_type"}, ConflictsWith: []string{"on_schema_object.0.all", "on_schema_object.0.future"}, ValidateDiagFunc: IsValidIdentifier[sdk.SchemaObjectIdentifier](), - ForceNew: true, }, "all": { Type: schema.TypeList, @@ -170,50 +149,27 @@ var grantPrivilegesToRoleSchema = map[string]*schema.Schema{ Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "object_type_plural": { - Type: schema.TypeString, - Required: true, - Description: "The plural object type of the schema object on which privileges will be granted. Valid values are: ALERTS | DYNAMIC TABLES | EVENT TABLES | FILE FORMATS | FUNCTIONS | ICEBERG TABLES | PROCEDURES | SECRETS | SEQUENCES | PIPES | MASKING POLICIES | PASSWORD POLICIES | ROW ACCESS POLICIES | SESSION POLICIES | TAGS | STAGES | STREAMS | TABLES | EXTERNAL TABLES | TASKS | VIEWS | MATERIALIZED VIEWS", - ForceNew: true, - ValidateFunc: validation.StringInSlice([]string{ - "ALERTS", - "DYNAMIC TABLES", - "EVENT TABLES", - "FILE FORMATS", - "FUNCTIONS", - "ICEBERG TABLES", - "PROCEDURES", - "SECRETS", - "SEQUENCES", - "PIPES", - "MASKING POLICIES", - "PASSWORD POLICIES", - "ROW ACCESS POLICIES", - "SESSION POLICIES", - "TAGS", - "STAGES", - "STREAMS", - "TABLES", - "EXTERNAL TABLES", - "TASKS", - "VIEWS", - "MATERIALIZED VIEWS", - }, true), + Type: schema.TypeString, + Required: true, + Description: "The plural object type of the schema object on which privileges will be granted. Valid values are: ALERTS | DYNAMIC TABLES | EVENT TABLES | FILE FORMATS | FUNCTIONS | ICEBERG TABLES | PROCEDURES | SECRETS | SEQUENCES | PIPES | MASKING POLICIES | PASSWORD POLICIES | ROW ACCESS POLICIES | SESSION POLICIES | TAGS | STAGES | STREAMS | TABLES | EXTERNAL TABLES | TASKS | VIEWS | MATERIALIZED VIEWS", + ForceNew: true, + ValidateDiagFunc: ValidPluralObjectType(), }, "in_database": { Type: schema.TypeString, Optional: true, + ForceNew: true, Description: "The fully qualified name of the database.", ConflictsWith: []string{"on_schema_object.0.all.in_schema"}, ValidateDiagFunc: IsValidIdentifier[sdk.AccountObjectIdentifier](), - ForceNew: true, }, "in_schema": { Type: schema.TypeString, Optional: true, + ForceNew: true, Description: "The fully qualified name of the schema.", ConflictsWith: []string{"on_schema_object.0.all.in_database"}, ValidateDiagFunc: IsValidIdentifier[sdk.DatabaseObjectIdentifier](), - ForceNew: true, }, }, }, @@ -227,34 +183,11 @@ var grantPrivilegesToRoleSchema = map[string]*schema.Schema{ Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "object_type_plural": { - Type: schema.TypeString, - Required: true, - Description: "The plural object type of the schema object on which privileges will be granted. Valid values are: ALERTS | DYNAMIC TABLES | EVENT TABLES | FILE FORMATS | FUNCTIONS | ICEBERG TABLES | PROCEDURES | SECRETS | SEQUENCES | PIPES | MASKING POLICIES | PASSWORD POLICIES | ROW ACCESS POLICIES | SESSION POLICIES | TAGS | STAGES | STREAMS | TABLES | EXTERNAL TABLES | TASKS | VIEWS | MATERIALIZED VIEWS", - ForceNew: true, - ValidateFunc: validation.StringInSlice([]string{ - "ALERTS", - "DYNAMIC TABLES", - "EVENT TABLES", - "FILE FORMATS", - "FUNCTIONS", - "ICEBERG TABLES", - "PROCEDURES", - "SECRETS", - "SEQUENCES", - "PIPES", - "MASKING POLICIES", - "PASSWORD POLICIES", - "ROW ACCESS POLICIES", - "SESSION POLICIES", - "TAGS", - "STAGES", - "STREAMS", - "TABLES", - "EXTERNAL TABLES", - "TASKS", - "VIEWS", - "MATERIALIZED VIEWS", - }, true), + Type: schema.TypeString, + Required: true, + Description: "The plural object type of the schema object on which privileges will be granted. Valid values are: ALERTS | DYNAMIC TABLES | EVENT TABLES | FILE FORMATS | FUNCTIONS | ICEBERG TABLES | PROCEDURES | SECRETS | SEQUENCES | PIPES | MASKING POLICIES | PASSWORD POLICIES | ROW ACCESS POLICIES | SESSION POLICIES | TAGS | STAGES | STREAMS | TABLES | EXTERNAL TABLES | TASKS | VIEWS | MATERIALIZED VIEWS", + ForceNew: true, + ValidateDiagFunc: ValidPluralObjectType(), }, "in_database": { Type: schema.TypeString, @@ -279,10 +212,11 @@ var grantPrivilegesToRoleSchema = map[string]*schema.Schema{ }, }, "role_name": { - Type: schema.TypeString, - Required: true, - Description: "The fully qualified name of the role to which privileges will be granted.", - ForceNew: true, + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The fully qualified name of the role to which privileges will be granted.", + ValidateDiagFunc: IsValidIdentifier[sdk.AccountObjectIdentifier](), }, "with_grant_option": { Type: schema.TypeBool, diff --git a/pkg/resources/resource_monitor.go b/pkg/resources/resource_monitor.go index 59e416c69e..e4ad6a196f 100644 --- a/pkg/resources/resource_monitor.go +++ b/pkg/resources/resource_monitor.go @@ -317,24 +317,12 @@ func UpdateResourceMonitor(d *schema.ResourceData, meta interface{}) error { ctx := context.Background() var runSetStatement bool - opts := sdk.AlterResourceMonitorOptions{Set: &sdk.ResourceMonitorSet{}} - - if d.HasChange("notify_users") { - runSetStatement = true - - userNames := expandStringList(d.Get("notify_users").(*schema.Set).List()) - users := []sdk.NotifiedUser{} - for _, name := range userNames { - users = append(users, sdk.NotifiedUser{Name: name}) - } - opts.NotifyUsers = &sdk.NotifyUsers{ - Users: users, - } - } + opts := sdk.AlterResourceMonitorOptions{} + set := sdk.ResourceMonitorSet{} if d.HasChange("credit_quota") { runSetStatement = true - opts.Set.CreditQuota = sdk.Pointer(d.Get("credit_quota").(int)) + set.CreditQuota = sdk.Pointer(d.Get("credit_quota").(int)) } if d.HasChange("frequency") || d.HasChange("start_timestamp") { @@ -343,13 +331,30 @@ func UpdateResourceMonitor(d *schema.ResourceData, meta interface{}) error { if err != nil { return err } - opts.Set.Frequency = frequency - opts.Set.StartTimestamp = sdk.Pointer(d.Get("start_timestamp").(string)) + set.Frequency = frequency + set.StartTimestamp = sdk.Pointer(d.Get("start_timestamp").(string)) } if d.HasChange("end_timestamp") { runSetStatement = true - opts.Set.EndTimestamp = sdk.Pointer(d.Get("end_timestamp").(string)) + set.EndTimestamp = sdk.Pointer(d.Get("end_timestamp").(string)) + } + + if d.HasChange("notify_users") { + runSetStatement = true + + userNames := expandStringList(d.Get("notify_users").(*schema.Set).List()) + users := []sdk.NotifiedUser{} + for _, name := range userNames { + users = append(users, sdk.NotifiedUser{Name: name}) + } + set.NotifyUsers = &sdk.NotifyUsers{ + Users: users, + } + } + + if set != (sdk.ResourceMonitorSet{}) { + opts.Set = &set } // If ANY of the triggers changed, we collect all triggers and set them diff --git a/pkg/resources/resource_monitor_acceptance_test.go b/pkg/resources/resource_monitor_acceptance_test.go index ec340de422..ff121977ca 100644 --- a/pkg/resources/resource_monitor_acceptance_test.go +++ b/pkg/resources/resource_monitor_acceptance_test.go @@ -36,7 +36,7 @@ func TestAcc_ResourceMonitor(t *testing.T) { }, // CHANGE PROPERTIES { - Config: resourceMonitorConfig2(name), + Config: resourceMonitorConfig2(name, 75), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("snowflake_resource_monitor.test", "name", name), resource.TestCheckResourceAttr("snowflake_resource_monitor.test", "credit_quota", "150"), @@ -46,6 +46,18 @@ func TestAcc_ResourceMonitor(t *testing.T) { resource.TestCheckResourceAttr("snowflake_resource_monitor.test", "suspend_immediate_trigger", "95"), ), }, + // CHANGE JUST suspend_trigger; proves https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2316 + { + Config: resourceMonitorConfig2(name, 60), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("snowflake_resource_monitor.test", "name", name), + resource.TestCheckResourceAttr("snowflake_resource_monitor.test", "credit_quota", "150"), + resource.TestCheckResourceAttr("snowflake_resource_monitor.test", "set_for_account", "true"), + resource.TestCheckResourceAttr("snowflake_resource_monitor.test", "notify_triggers.0", "50"), + resource.TestCheckResourceAttr("snowflake_resource_monitor.test", "suspend_trigger", "60"), + resource.TestCheckResourceAttr("snowflake_resource_monitor.test", "suspend_immediate_trigger", "95"), + ), + }, // IMPORT { ResourceName: "snowflake_resource_monitor.test", @@ -198,7 +210,7 @@ resource "snowflake_resource_monitor" "test" { `, accName) } -func resourceMonitorConfig2(accName string) string { +func resourceMonitorConfig2(accName string, suspendTrigger int) string { return fmt.Sprintf(` resource "snowflake_warehouse" "warehouse" { name = "test" @@ -212,10 +224,10 @@ resource "snowflake_resource_monitor" "test" { set_for_account = true notify_triggers = [50] warehouses = [] - suspend_trigger = 75 + suspend_trigger = %d suspend_immediate_trigger = 95 } -`, accName) +`, accName, suspendTrigger) } // TestAcc_ResourceMonitor_issue2167 proves https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2167 issue. diff --git a/pkg/resources/table_constraint.go b/pkg/resources/table_constraint.go index 0876c8ea63..1d15395f20 100644 --- a/pkg/resources/table_constraint.go +++ b/pkg/resources/table_constraint.go @@ -6,6 +6,7 @@ import ( "log" "strings" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/snowflake" snowflakeValidation "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/validation" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -32,11 +33,11 @@ var tableConstraintSchema = map[string]*schema.Schema{ }, }, "table_id": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - Description: "Idenfifier for table to create constraint on. Must be of the form Note: format must follow: \"\".\"\".\"\" or \"..\" or \"|.\" (snowflake_table.my_table.id)", - ValidateFunc: snowflakeValidation.ValidateFullyQualifiedObjectID, + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Idenfifier for table to create constraint on. Must be of the form Note: format must follow: \"\".\"\".\"\" or \"..\" or \"|.\" (snowflake_table.my_table.id)", + ValidateDiagFunc: IsValidIdentifier[sdk.SchemaObjectIdentifier](), }, "columns": { Type: schema.TypeList, diff --git a/pkg/resources/tag_association.go b/pkg/resources/tag_association.go index a71ae67b2e..273d94cca6 100644 --- a/pkg/resources/tag_association.go +++ b/pkg/resources/tag_association.go @@ -12,6 +12,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/snowflake" snowflakeValidation "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/validation" ) @@ -64,11 +65,11 @@ var tagAssociationSchema = map[string]*schema.Schema{ ForceNew: true, }, "tag_id": { - Type: schema.TypeString, - Required: true, - Description: "Specifies the identifier for the tag. Note: format must follow: \"databaseName\".\"schemaName\".\"tagName\" or \"databaseName.schemaName.tagName\" or \"databaseName|schemaName.tagName\" (snowflake_tag.tag.id)", - ValidateFunc: snowflakeValidation.ValidateFullyQualifiedObjectID, - ForceNew: true, + Type: schema.TypeString, + Required: true, + Description: "Specifies the identifier for the tag. Note: format must follow: \"databaseName\".\"schemaName\".\"tagName\" or \"databaseName.schemaName.tagName\" or \"databaseName|schemaName.tagName\" (snowflake_tag.tag.id)", + ValidateDiagFunc: IsValidIdentifier[sdk.SchemaObjectIdentifier](), + ForceNew: true, }, "tag_value": { Type: schema.TypeString, diff --git a/pkg/resources/tag_masking_policy_association.go b/pkg/resources/tag_masking_policy_association.go index 62b49c64bc..445b384218 100644 --- a/pkg/resources/tag_masking_policy_association.go +++ b/pkg/resources/tag_masking_policy_association.go @@ -15,7 +15,6 @@ import ( "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/snowflake" - snowflakeValidation "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/validation" ) const ( @@ -24,11 +23,11 @@ const ( var mpAttachmentPolicySchema = map[string]*schema.Schema{ "tag_id": { - Type: schema.TypeString, - Required: true, - Description: "Specifies the identifier for the tag. Note: format must follow: \"databaseName\".\"schemaName\".\"tagName\" or \"databaseName.schemaName.tagName\" or \"databaseName|schemaName.tagName\" (snowflake_tag.tag.id)", - ValidateFunc: snowflakeValidation.ValidateFullyQualifiedObjectID, - ForceNew: true, + Type: schema.TypeString, + Required: true, + Description: "Specifies the identifier for the tag. Note: format must follow: \"databaseName\".\"schemaName\".\"tagName\" or \"databaseName.schemaName.tagName\" or \"databaseName|schemaName.tagName\" (snowflake_tag.tag.id)", + ValidateDiagFunc: IsValidIdentifier[sdk.SchemaObjectIdentifier](), + ForceNew: true, }, "masking_policy_id": { Type: schema.TypeString, diff --git a/pkg/resources/task.go b/pkg/resources/task.go index ae9f53d7da..d6eed48516 100644 --- a/pkg/resources/task.go +++ b/pkg/resources/task.go @@ -530,7 +530,7 @@ func UpdateTask(d *schema.ResourceData, meta interface{}) error { toAdd = append(toAdd, sdk.NewSchemaObjectIdentifier(taskId.DatabaseName(), taskId.SchemaName(), dep)) } } - // TODO [SNOW-884987]: for now leaving old copy-pasted implementation; extract function for task suspension in following change + // TODO [SNOW-1007541]: for now leaving old copy-pasted implementation; extract function for task suspension in following change if len(toAdd) > 0 { // need to suspend any new root tasks from dependencies before adding them for _, dep := range toAdd { diff --git a/pkg/resources/testdata/TestAcc_ExternalTable_CanCreateWithPartitions/test.tf b/pkg/resources/testdata/TestAcc_ExternalTable_CanCreateWithPartitions/test.tf new file mode 100644 index 0000000000..978684a35a --- /dev/null +++ b/pkg/resources/testdata/TestAcc_ExternalTable_CanCreateWithPartitions/test.tf @@ -0,0 +1,35 @@ +resource "snowflake_stage" "test" { + name = var.name + url = var.location + database = var.database + schema = var.schema + credentials = "aws_key_id = '${var.aws_key_id}' aws_secret_key = '${var.aws_secret_key}'" + file_format = "TYPE = JSON NULL_IF = []" +} + +resource "snowflake_external_table" "test_table" { + name = var.name + database = var.database + schema = var.schema + comment = "Terraform acceptance test" + column { + name = "filename" + type = "string" + as = "metadata$filename" + } + column { + name = "name" + type = "varchar(200)" + as = "value:name::string" + } + column { + name = "age" + type = "number(2, 2)" + as = "value:age::number" + } + partition_by = ["filename"] + auto_refresh = false + refresh_on_create = true + file_format = "TYPE = JSON, STRIP_OUTER_ARRAY = TRUE" + location = "@\"${var.database}\".\"${var.schema}\".\"${snowflake_stage.test.name}\"/external_tables_test_data/" +} diff --git a/pkg/resources/testdata/TestAcc_ExternalTable_basic/variables.tf b/pkg/resources/testdata/TestAcc_ExternalTable_CanCreateWithPartitions/variables.tf similarity index 71% rename from pkg/resources/testdata/TestAcc_ExternalTable_basic/variables.tf rename to pkg/resources/testdata/TestAcc_ExternalTable_CanCreateWithPartitions/variables.tf index a447ded368..9badff675d 100644 --- a/pkg/resources/testdata/TestAcc_ExternalTable_basic/variables.tf +++ b/pkg/resources/testdata/TestAcc_ExternalTable_CanCreateWithPartitions/variables.tf @@ -6,7 +6,11 @@ variable "location" { type = string } -variable "aws_arn" { +variable "aws_key_id" { + type = string +} + +variable "aws_secret_key" { type = string } diff --git a/pkg/resources/testdata/TestAcc_ExternalTable_CorrectDataTypes/test.tf b/pkg/resources/testdata/TestAcc_ExternalTable_CorrectDataTypes/test.tf new file mode 100644 index 0000000000..bcd409e0e6 --- /dev/null +++ b/pkg/resources/testdata/TestAcc_ExternalTable_CorrectDataTypes/test.tf @@ -0,0 +1,29 @@ +resource "snowflake_stage" "test" { + name = var.name + url = var.location + database = var.database + schema = var.schema + credentials = "aws_key_id = '${var.aws_key_id}' aws_secret_key = '${var.aws_secret_key}'" + file_format = "TYPE = JSON NULL_IF = []" +} + +resource "snowflake_external_table" "test_table" { + name = var.name + database = var.database + schema = var.schema + comment = "Terraform acceptance test" + column { + name = "name" + type = "varchar(200)" + as = "value:name::string" + } + column { + name = "age" + type = "number(2, 2)" + as = "value:age::number" + } + auto_refresh = false + refresh_on_create = true + file_format = "TYPE = JSON, STRIP_OUTER_ARRAY = TRUE" + location = "@\"${var.database}\".\"${var.schema}\".\"${snowflake_stage.test.name}\"/external_tables_test_data/" +} diff --git a/pkg/resources/testdata/TestAcc_ExternalTable_CorrectDataTypes/variables.tf b/pkg/resources/testdata/TestAcc_ExternalTable_CorrectDataTypes/variables.tf new file mode 100644 index 0000000000..9badff675d --- /dev/null +++ b/pkg/resources/testdata/TestAcc_ExternalTable_CorrectDataTypes/variables.tf @@ -0,0 +1,23 @@ +variable "name" { + type = string +} + +variable "location" { + type = string +} + +variable "aws_key_id" { + type = string +} + +variable "aws_secret_key" { + type = string +} + +variable "database" { + type = string +} + +variable "schema" { + type = string +} diff --git a/pkg/resources/testdata/TestAcc_ExternalTable_DeltaLake/test.tf b/pkg/resources/testdata/TestAcc_ExternalTable_DeltaLake/test.tf new file mode 100644 index 0000000000..205c58648b --- /dev/null +++ b/pkg/resources/testdata/TestAcc_ExternalTable_DeltaLake/test.tf @@ -0,0 +1,31 @@ +resource "snowflake_stage" "test" { + name = var.name + url = var.location + database = var.database + schema = var.schema + credentials = "aws_key_id = '${var.aws_key_id}' aws_secret_key = '${var.aws_secret_key}'" + file_format = "TYPE = PARQUET NULL_IF = []" +} + +resource "snowflake_external_table" "test_table" { + name = var.name + database = var.database + schema = var.schema + comment = "Terraform acceptance test" + table_format = "delta" + column { + name = "filename" + type = "string" + as = "metadata$filename" + } + column { + name = "name" + type = "string" + as = "value:name::string" + } + partition_by = ["filename"] + auto_refresh = false + refresh_on_create = false + file_format = "TYPE = PARQUET" + location = "@\"${var.database}\".\"${var.schema}\".\"${snowflake_stage.test.name}\"/external_tables_test_data/" +} diff --git a/pkg/resources/testdata/TestAcc_ExternalTable_DeltaLake/variables.tf b/pkg/resources/testdata/TestAcc_ExternalTable_DeltaLake/variables.tf new file mode 100644 index 0000000000..9badff675d --- /dev/null +++ b/pkg/resources/testdata/TestAcc_ExternalTable_DeltaLake/variables.tf @@ -0,0 +1,23 @@ +variable "name" { + type = string +} + +variable "location" { + type = string +} + +variable "aws_key_id" { + type = string +} + +variable "aws_secret_key" { + type = string +} + +variable "database" { + type = string +} + +variable "schema" { + type = string +} diff --git a/pkg/resources/testdata/TestAcc_ExternalTable_basic/1/test.tf b/pkg/resources/testdata/TestAcc_ExternalTable_basic/1/test.tf new file mode 100644 index 0000000000..b56902890c --- /dev/null +++ b/pkg/resources/testdata/TestAcc_ExternalTable_basic/1/test.tf @@ -0,0 +1,8 @@ +resource "snowflake_stage" "test" { + name = var.name + url = var.location + database = var.database + schema = var.schema + credentials = "aws_key_id = '${var.aws_key_id}' aws_secret_key = '${var.aws_secret_key}'" + file_format = "TYPE = JSON NULL_IF = []" +} diff --git a/pkg/resources/testdata/TestAcc_ExternalTable_basic/1/variables.tf b/pkg/resources/testdata/TestAcc_ExternalTable_basic/1/variables.tf new file mode 100644 index 0000000000..9badff675d --- /dev/null +++ b/pkg/resources/testdata/TestAcc_ExternalTable_basic/1/variables.tf @@ -0,0 +1,23 @@ +variable "name" { + type = string +} + +variable "location" { + type = string +} + +variable "aws_key_id" { + type = string +} + +variable "aws_secret_key" { + type = string +} + +variable "database" { + type = string +} + +variable "schema" { + type = string +} diff --git a/pkg/resources/testdata/TestAcc_ExternalTable_basic/2/test.tf b/pkg/resources/testdata/TestAcc_ExternalTable_basic/2/test.tf new file mode 100644 index 0000000000..f8fa2ac2fc --- /dev/null +++ b/pkg/resources/testdata/TestAcc_ExternalTable_basic/2/test.tf @@ -0,0 +1,29 @@ +resource "snowflake_stage" "test" { + name = var.name + url = var.location + database = var.database + schema = var.schema + credentials = "aws_key_id = '${var.aws_key_id}' aws_secret_key = '${var.aws_secret_key}'" + file_format = "TYPE = JSON NULL_IF = []" +} + +resource "snowflake_external_table" "test_table" { + name = var.name + database = var.database + schema = var.schema + comment = "Terraform acceptance test" + column { + name = "name" + type = "string" + as = "value:name::string" + } + column { + name = "age" + type = "number" + as = "value:age::number" + } + auto_refresh = false + refresh_on_create = true + file_format = "TYPE = JSON, STRIP_OUTER_ARRAY = TRUE" + location = "@\"${var.database}\".\"${var.schema}\".\"${snowflake_stage.test.name}\"/external_tables_test_data/" +} diff --git a/pkg/resources/testdata/TestAcc_ExternalTable_basic/2/variables.tf b/pkg/resources/testdata/TestAcc_ExternalTable_basic/2/variables.tf new file mode 100644 index 0000000000..9badff675d --- /dev/null +++ b/pkg/resources/testdata/TestAcc_ExternalTable_basic/2/variables.tf @@ -0,0 +1,23 @@ +variable "name" { + type = string +} + +variable "location" { + type = string +} + +variable "aws_key_id" { + type = string +} + +variable "aws_secret_key" { + type = string +} + +variable "database" { + type = string +} + +variable "schema" { + type = string +} diff --git a/pkg/resources/testdata/TestAcc_ExternalTable_basic/test.tf b/pkg/resources/testdata/TestAcc_ExternalTable_basic/test.tf deleted file mode 100644 index c8efacb28f..0000000000 --- a/pkg/resources/testdata/TestAcc_ExternalTable_basic/test.tf +++ /dev/null @@ -1,33 +0,0 @@ -resource "snowflake_storage_integration" "i" { - name = var.name - storage_allowed_locations = [var.location] - storage_provider = "S3" - storage_aws_role_arn = var.aws_arn -} - -resource "snowflake_stage" "test" { - name = var.name - url = var.location - database = var.database - schema = var.schema - storage_integration = snowflake_storage_integration.i.name -} - -resource "snowflake_external_table" "test_table" { - name = var.name - database = var.database - schema = var.schema - comment = "Terraform acceptance test" - column { - name = "column1" - type = "STRING" - as = "TO_VARCHAR(TO_TIMESTAMP_NTZ(value:unix_timestamp_property::NUMBER, 3), 'yyyy-mm-dd-hh')" - } - column { - name = "column2" - type = "TIMESTAMP_NTZ(9)" - as = "($1:\"CreatedDate\"::timestamp)" - } - file_format = "TYPE = CSV" - location = "@\"${var.database}\".\"${var.schema}\".\"${snowflake_stage.test.name}\"" -} diff --git a/pkg/resources/testdata/TestAcc_GrantAccountRole/account_role/test.tf b/pkg/resources/testdata/TestAcc_GrantAccountRole/account_role/test.tf new file mode 100644 index 0000000000..6e13214569 --- /dev/null +++ b/pkg/resources/testdata/TestAcc_GrantAccountRole/account_role/test.tf @@ -0,0 +1,20 @@ +variable "role_name" { + type = string +} + +variable "parent_role_name" { + type = string +} + +resource "snowflake_role" "role" { + name = var.role_name +} + +resource "snowflake_role" "parent_role" { + name = var.parent_role_name +} + +resource "snowflake_grant_account_role" "g" { + role_name = snowflake_role.role.name + parent_role_name = snowflake_role.parent_role.name +} diff --git a/pkg/resources/testdata/TestAcc_GrantAccountRole/user/test.tf b/pkg/resources/testdata/TestAcc_GrantAccountRole/user/test.tf new file mode 100644 index 0000000000..4e200570f4 --- /dev/null +++ b/pkg/resources/testdata/TestAcc_GrantAccountRole/user/test.tf @@ -0,0 +1,20 @@ +variable "role_name" { + type = string +} + +variable "user_name" { + type = string +} + +resource "snowflake_role" "role" { + name = var.role_name +} + +resource "snowflake_user" "user" { + name = var.user_name +} + +resource "snowflake_grant_account_role" "g" { + role_name = snowflake_role.role.name + user_name = snowflake_user.user.name +} diff --git a/pkg/resources/testdata/TestAcc_GrantDatabaseRole/account_role/test.tf b/pkg/resources/testdata/TestAcc_GrantDatabaseRole/account_role/test.tf new file mode 100644 index 0000000000..4ed7e1936b --- /dev/null +++ b/pkg/resources/testdata/TestAcc_GrantDatabaseRole/account_role/test.tf @@ -0,0 +1,25 @@ +variable "database_role_name" { + type = string +} + +variable "parent_role_name" { + type = string +} + +variable "database" { + type = string +} + +resource "snowflake_database_role" "database_role" { + database = var.database + name = var.database_role_name +} + +resource "snowflake_role" "parent_role" { + name = var.parent_role_name +} + +resource "snowflake_grant_database_role" "g" { + database_role_name = "\"${var.database}\".\"${snowflake_database_role.database_role.name}\"" + parent_role_name = snowflake_role.parent_role.name +} diff --git a/pkg/resources/testdata/TestAcc_GrantDatabaseRole/database_role/test.tf b/pkg/resources/testdata/TestAcc_GrantDatabaseRole/database_role/test.tf new file mode 100644 index 0000000000..e78bbd2af2 --- /dev/null +++ b/pkg/resources/testdata/TestAcc_GrantDatabaseRole/database_role/test.tf @@ -0,0 +1,26 @@ +variable "database_role_name" { + type = string +} + +variable "parent_database_role_name" { + type = string +} + +variable "database" { + type = string +} + +resource "snowflake_database_role" "database_role" { + database = var.database + name = var.database_role_name +} + +resource "snowflake_database_role" "parent_database_role" { + database = var.database + name = var.parent_database_role_name +} + +resource "snowflake_grant_database_role" "g" { + database_role_name = "\"${var.database}\".\"${snowflake_database_role.database_role.name}\"" + parent_database_role_name = "\"${var.database}\".\"${snowflake_database_role.parent_database_role.name}\"" +} diff --git a/pkg/resources/testdata/TestAcc_GrantDatabaseRole/share/test.tf b/pkg/resources/testdata/TestAcc_GrantDatabaseRole/share/test.tf new file mode 100644 index 0000000000..ef0003088a --- /dev/null +++ b/pkg/resources/testdata/TestAcc_GrantDatabaseRole/share/test.tf @@ -0,0 +1,27 @@ +variable "database_role_name" { + type = string +} + +variable "share_name" { + type = string +} + +variable "database" { + type = string +} + +resource "snowflake_database_role" "database_role" { + database = var.database + name = var.database_role_name +} + +resource "snowflake_share" "share" { + name = var.share_name +} + +// todo: add grant_privileges_to_share resource + +resource "snowflake_grant_database_role" "g" { + database_role_name = snowflake_database_role.database_role.name + share_name = snowflake_share.name +} diff --git a/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/AlwaysApply/test.tf b/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/AlwaysApply/test.tf new file mode 100644 index 0000000000..4b855bb708 --- /dev/null +++ b/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/AlwaysApply/test.tf @@ -0,0 +1,6 @@ +resource "snowflake_grant_privileges_to_database_role" "test" { + database_role_name = "${var.database}.${var.name}" + all_privileges = var.all_privileges + on_database = var.database + always_apply = var.always_apply +} diff --git a/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/AlwaysApply/variables.tf b/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/AlwaysApply/variables.tf new file mode 100644 index 0000000000..563945ecc7 --- /dev/null +++ b/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/AlwaysApply/variables.tf @@ -0,0 +1,15 @@ +variable "name" { + type = string +} + +variable "all_privileges" { + type = bool +} + +variable "database" { + type = string +} + +variable "always_apply" { + type = bool +} diff --git a/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/OnAllSchemasInDatabase/test.tf b/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/OnAllSchemasInDatabase/test.tf new file mode 100644 index 0000000000..ebde56ec07 --- /dev/null +++ b/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/OnAllSchemasInDatabase/test.tf @@ -0,0 +1,9 @@ +resource "snowflake_grant_privileges_to_database_role" "test" { + database_role_name = "\"${var.database}\".\"${var.name}\"" + privileges = var.privileges + with_grant_option = var.with_grant_option + + on_schema { + all_schemas_in_database = "\"${var.database}\"" + } +} diff --git a/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/OnAllSchemasInDatabase/variables.tf b/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/OnAllSchemasInDatabase/variables.tf new file mode 100644 index 0000000000..0e22e903d7 --- /dev/null +++ b/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/OnAllSchemasInDatabase/variables.tf @@ -0,0 +1,15 @@ +variable "name" { + type = string +} + +variable "privileges" { + type = list(string) +} + +variable "database" { + type = string +} + +variable "with_grant_option" { + type = bool +} diff --git a/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/OnDatabase/test.tf b/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/OnDatabase/test.tf new file mode 100644 index 0000000000..83113802be --- /dev/null +++ b/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/OnDatabase/test.tf @@ -0,0 +1,6 @@ +resource "snowflake_grant_privileges_to_database_role" "test" { + database_role_name = "\"${var.database}\".\"${var.name}\"" + privileges = var.privileges + on_database = "\"${var.database}\"" + with_grant_option = var.with_grant_option +} diff --git a/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/OnDatabase/variables.tf b/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/OnDatabase/variables.tf new file mode 100644 index 0000000000..0e22e903d7 --- /dev/null +++ b/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/OnDatabase/variables.tf @@ -0,0 +1,15 @@ +variable "name" { + type = string +} + +variable "privileges" { + type = list(string) +} + +variable "database" { + type = string +} + +variable "with_grant_option" { + type = bool +} diff --git a/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/OnFutureSchemasInDatabase/test.tf b/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/OnFutureSchemasInDatabase/test.tf new file mode 100644 index 0000000000..dcecbfd504 --- /dev/null +++ b/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/OnFutureSchemasInDatabase/test.tf @@ -0,0 +1,9 @@ +resource "snowflake_grant_privileges_to_database_role" "test" { + database_role_name = "\"${var.database}\".\"${var.name}\"" + privileges = var.privileges + with_grant_option = var.with_grant_option + + on_schema { + future_schemas_in_database = "\"${var.database}\"" + } +} diff --git a/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/OnFutureSchemasInDatabase/variables.tf b/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/OnFutureSchemasInDatabase/variables.tf new file mode 100644 index 0000000000..0e22e903d7 --- /dev/null +++ b/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/OnFutureSchemasInDatabase/variables.tf @@ -0,0 +1,15 @@ +variable "name" { + type = string +} + +variable "privileges" { + type = list(string) +} + +variable "database" { + type = string +} + +variable "with_grant_option" { + type = bool +} diff --git a/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/OnSchema/test.tf b/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/OnSchema/test.tf new file mode 100644 index 0000000000..ef5613bda3 --- /dev/null +++ b/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/OnSchema/test.tf @@ -0,0 +1,9 @@ +resource "snowflake_grant_privileges_to_database_role" "test" { + database_role_name = "\"${var.database}\".\"${var.name}\"" + privileges = var.privileges + with_grant_option = var.with_grant_option + + on_schema { + schema_name = "\"${var.database}\".\"${var.schema}\"" + } +} diff --git a/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/OnSchema/variables.tf b/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/OnSchema/variables.tf new file mode 100644 index 0000000000..44c69f32ee --- /dev/null +++ b/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/OnSchema/variables.tf @@ -0,0 +1,19 @@ +variable "name" { + type = string +} + +variable "privileges" { + type = list(string) +} + +variable "database" { + type = string +} + +variable "schema" { + type = string +} + +variable "with_grant_option" { + type = bool +} diff --git a/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/OnSchemaObject_OnAll_InDatabase/test.tf b/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/OnSchemaObject_OnAll_InDatabase/test.tf new file mode 100644 index 0000000000..230a702d23 --- /dev/null +++ b/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/OnSchemaObject_OnAll_InDatabase/test.tf @@ -0,0 +1,12 @@ +resource "snowflake_grant_privileges_to_database_role" "test" { + database_role_name = "\"${var.database}\".\"${var.name}\"" + privileges = var.privileges + with_grant_option = var.with_grant_option + + on_schema_object { + all { + object_type_plural = "TABLES" + in_database = "\"${var.database}\"" + } + } +} diff --git a/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/OnSchemaObject_OnAll_InDatabase/variables.tf b/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/OnSchemaObject_OnAll_InDatabase/variables.tf new file mode 100644 index 0000000000..0e22e903d7 --- /dev/null +++ b/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/OnSchemaObject_OnAll_InDatabase/variables.tf @@ -0,0 +1,15 @@ +variable "name" { + type = string +} + +variable "privileges" { + type = list(string) +} + +variable "database" { + type = string +} + +variable "with_grant_option" { + type = bool +} diff --git a/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/OnSchemaObject_OnFuture_InDatabase/test.tf b/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/OnSchemaObject_OnFuture_InDatabase/test.tf new file mode 100644 index 0000000000..3463a24a8f --- /dev/null +++ b/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/OnSchemaObject_OnFuture_InDatabase/test.tf @@ -0,0 +1,12 @@ +resource "snowflake_grant_privileges_to_database_role" "test" { + database_role_name = "\"${var.database}\".\"${var.name}\"" + privileges = var.privileges + with_grant_option = var.with_grant_option + + on_schema_object { + future { + object_type_plural = "TABLES" + in_database = "\"${var.database}\"" + } + } +} diff --git a/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/OnSchemaObject_OnFuture_InDatabase/variables.tf b/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/OnSchemaObject_OnFuture_InDatabase/variables.tf new file mode 100644 index 0000000000..0e22e903d7 --- /dev/null +++ b/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/OnSchemaObject_OnFuture_InDatabase/variables.tf @@ -0,0 +1,15 @@ +variable "name" { + type = string +} + +variable "privileges" { + type = list(string) +} + +variable "database" { + type = string +} + +variable "with_grant_option" { + type = bool +} diff --git a/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/OnSchemaObject_OnObject/test.tf b/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/OnSchemaObject_OnObject/test.tf new file mode 100644 index 0000000000..2d480820ae --- /dev/null +++ b/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/OnSchemaObject_OnObject/test.tf @@ -0,0 +1,22 @@ +resource "snowflake_table" "test" { + database = var.database + schema = var.schema + name = var.table_name + + column { + name = "id" + type = "NUMBER(38,0)" + } +} + +resource "snowflake_grant_privileges_to_database_role" "test" { + depends_on = [snowflake_table.test] + database_role_name = "\"${var.database}\".\"${var.name}\"" + privileges = var.privileges + with_grant_option = var.with_grant_option + + on_schema_object { + object_type = "TABLE" + object_name = "\"${var.database}\".\"${var.schema}\".\"${var.table_name}\"" + } +} diff --git a/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/OnSchemaObject_OnObject/variables.tf b/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/OnSchemaObject_OnObject/variables.tf new file mode 100644 index 0000000000..e779508540 --- /dev/null +++ b/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/OnSchemaObject_OnObject/variables.tf @@ -0,0 +1,23 @@ +variable "name" { + type = string +} + +variable "table_name" { + type = string +} + +variable "privileges" { + type = list(string) +} + +variable "database" { + type = string +} + +variable "schema" { + type = string +} + +variable "with_grant_option" { + type = bool +} diff --git a/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/OnSchema_ExactlyOneOf/test.tf b/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/OnSchema_ExactlyOneOf/test.tf new file mode 100644 index 0000000000..aea96bd446 --- /dev/null +++ b/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/OnSchema_ExactlyOneOf/test.tf @@ -0,0 +1,9 @@ +resource "snowflake_grant_privileges_to_database_role" "test" { + database_role_name = "some_database.role_name" + privileges = ["USAGE"] + + on_schema { + schema_name = "some_database.schema_name" + all_schemas_in_database = "some_database" + } +} diff --git a/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/UpdatePrivileges/all_privileges/test.tf b/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/UpdatePrivileges/all_privileges/test.tf new file mode 100644 index 0000000000..3fc26c3028 --- /dev/null +++ b/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/UpdatePrivileges/all_privileges/test.tf @@ -0,0 +1,5 @@ +resource "snowflake_grant_privileges_to_database_role" "test" { + database_role_name = "\"${var.database}\".\"${var.name}\"" + all_privileges = var.all_privileges + on_database = "\"${var.database}\"" +} diff --git a/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/UpdatePrivileges/all_privileges/variables.tf b/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/UpdatePrivileges/all_privileges/variables.tf new file mode 100644 index 0000000000..cb4441bfce --- /dev/null +++ b/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/UpdatePrivileges/all_privileges/variables.tf @@ -0,0 +1,11 @@ +variable "name" { + type = string +} + +variable "all_privileges" { + type = bool +} + +variable "database" { + type = string +} diff --git a/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/UpdatePrivileges/privileges/test.tf b/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/UpdatePrivileges/privileges/test.tf new file mode 100644 index 0000000000..c1ea0cb24f --- /dev/null +++ b/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/UpdatePrivileges/privileges/test.tf @@ -0,0 +1,5 @@ +resource "snowflake_grant_privileges_to_database_role" "test" { + database_role_name = "\"${var.database}\".\"${var.name}\"" + privileges = var.privileges + on_database = "\"${var.database}\"" +} diff --git a/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/UpdatePrivileges/privileges/variables.tf b/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/UpdatePrivileges/privileges/variables.tf new file mode 100644 index 0000000000..27eccc7883 --- /dev/null +++ b/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/UpdatePrivileges/privileges/variables.tf @@ -0,0 +1,11 @@ +variable "name" { + type = string +} + +variable "privileges" { + type = list(string) +} + +variable "database" { + type = string +} diff --git a/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/UpdatePrivileges_SnowflakeChecked/all_privileges/test.tf b/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/UpdatePrivileges_SnowflakeChecked/all_privileges/test.tf new file mode 100644 index 0000000000..3fc26c3028 --- /dev/null +++ b/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/UpdatePrivileges_SnowflakeChecked/all_privileges/test.tf @@ -0,0 +1,5 @@ +resource "snowflake_grant_privileges_to_database_role" "test" { + database_role_name = "\"${var.database}\".\"${var.name}\"" + all_privileges = var.all_privileges + on_database = "\"${var.database}\"" +} diff --git a/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/UpdatePrivileges_SnowflakeChecked/all_privileges/variables.tf b/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/UpdatePrivileges_SnowflakeChecked/all_privileges/variables.tf new file mode 100644 index 0000000000..cb4441bfce --- /dev/null +++ b/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/UpdatePrivileges_SnowflakeChecked/all_privileges/variables.tf @@ -0,0 +1,11 @@ +variable "name" { + type = string +} + +variable "all_privileges" { + type = bool +} + +variable "database" { + type = string +} diff --git a/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/UpdatePrivileges_SnowflakeChecked/on_schema/test.tf b/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/UpdatePrivileges_SnowflakeChecked/on_schema/test.tf new file mode 100644 index 0000000000..f7bd4d9f19 --- /dev/null +++ b/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/UpdatePrivileges_SnowflakeChecked/on_schema/test.tf @@ -0,0 +1,13 @@ +resource "snowflake_schema" "test" { + database = var.database + name = var.schema_name +} + +resource "snowflake_grant_privileges_to_database_role" "test" { + depends_on = [snowflake_schema.test] + database_role_name = "\"${var.database}\".\"${var.name}\"" + privileges = var.privileges + on_schema { + schema_name = "${var.database}.${var.schema_name}" + } +} diff --git a/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/UpdatePrivileges_SnowflakeChecked/on_schema/variables.tf b/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/UpdatePrivileges_SnowflakeChecked/on_schema/variables.tf new file mode 100644 index 0000000000..90d9c04448 --- /dev/null +++ b/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/UpdatePrivileges_SnowflakeChecked/on_schema/variables.tf @@ -0,0 +1,15 @@ +variable "name" { + type = string +} + +variable "privileges" { + type = list(string) +} + +variable "database" { + type = string +} + +variable "schema_name" { + type = string +} diff --git a/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/UpdatePrivileges_SnowflakeChecked/privileges/test.tf b/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/UpdatePrivileges_SnowflakeChecked/privileges/test.tf new file mode 100644 index 0000000000..c1ea0cb24f --- /dev/null +++ b/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/UpdatePrivileges_SnowflakeChecked/privileges/test.tf @@ -0,0 +1,5 @@ +resource "snowflake_grant_privileges_to_database_role" "test" { + database_role_name = "\"${var.database}\".\"${var.name}\"" + privileges = var.privileges + on_database = "\"${var.database}\"" +} diff --git a/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/UpdatePrivileges_SnowflakeChecked/privileges/variables.tf b/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/UpdatePrivileges_SnowflakeChecked/privileges/variables.tf new file mode 100644 index 0000000000..27eccc7883 --- /dev/null +++ b/pkg/resources/testdata/TestAcc_GrantPrivilegesToDatabaseRole/UpdatePrivileges_SnowflakeChecked/privileges/variables.tf @@ -0,0 +1,11 @@ +variable "name" { + type = string +} + +variable "privileges" { + type = list(string) +} + +variable "database" { + type = string +} diff --git a/pkg/resources/validators.go b/pkg/resources/validators.go index a517a7e0ab..b14e122d7f 100644 --- a/pkg/resources/validators.go +++ b/pkg/resources/validators.go @@ -3,6 +3,7 @@ package resources import ( "fmt" "reflect" + "strings" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" @@ -108,3 +109,80 @@ func getExpectedIdentifierForm(id any) string { } return "" } + +func ValidObjectType() schema.SchemaValidateDiagFunc { + return StringInSlice([]string{ + sdk.ObjectTypeAlert.String(), + sdk.ObjectTypeDynamicTable.String(), + sdk.ObjectTypeEventTable.String(), + sdk.ObjectTypeFileFormat.String(), + sdk.ObjectTypeFunction.String(), + sdk.ObjectTypeProcedure.String(), + sdk.ObjectTypeSecret.String(), + sdk.ObjectTypeSequence.String(), + sdk.ObjectTypePipe.String(), + sdk.ObjectTypeMaskingPolicy.String(), + sdk.ObjectTypePasswordPolicy.String(), + sdk.ObjectTypeRowAccessPolicy.String(), + sdk.ObjectTypeSessionPolicy.String(), + sdk.ObjectTypeTag.String(), + sdk.ObjectTypeStage.String(), + sdk.ObjectTypeStream.String(), + sdk.ObjectTypeTable.String(), + sdk.ObjectTypeExternalTable.String(), + sdk.ObjectTypeTask.String(), + sdk.ObjectTypeView.String(), + sdk.ObjectTypeMaterializedView.String(), + sdk.ObjectTypeNetworkRule.String(), + sdk.ObjectTypePackagesPolicy.String(), + sdk.ObjectTypeIcebergTable.String(), + }, true) +} + +func ValidPluralObjectType() schema.SchemaValidateDiagFunc { + return StringInSlice( + []string{ + sdk.PluralObjectTypeAlerts.String(), + sdk.PluralObjectTypeDynamicTables.String(), + sdk.PluralObjectTypeEventTables.String(), + sdk.PluralObjectTypeFileFormats.String(), + sdk.PluralObjectTypeFunctions.String(), + sdk.PluralObjectTypeProcedures.String(), + sdk.PluralObjectTypeSecrets.String(), + sdk.PluralObjectTypeSequences.String(), + sdk.PluralObjectTypePipes.String(), + sdk.PluralObjectTypeMaskingPolicies.String(), + sdk.PluralObjectTypePasswordPolicies.String(), + sdk.PluralObjectTypeRowAccessPolicies.String(), + sdk.PluralObjectTypeSessionPolicies.String(), + sdk.PluralObjectTypeTags.String(), + sdk.PluralObjectTypeStages.String(), + sdk.PluralObjectTypeStreams.String(), + sdk.PluralObjectTypeTables.String(), + sdk.PluralObjectTypeExternalTables.String(), + sdk.PluralObjectTypeTasks.String(), + sdk.PluralObjectTypeViews.String(), + sdk.PluralObjectTypeMaterializedViews.String(), + sdk.PluralObjectTypeNetworkRules.String(), + sdk.PluralObjectTypePackagesPolicies.String(), + sdk.PluralObjectTypeIcebergTables.String(), + }, true) +} + +// StringInSlice has the same implementation as validation.StringInSlice, but adapted to schema.SchemaValidateDiagFunc +func StringInSlice(valid []string, ignoreCase bool) schema.SchemaValidateDiagFunc { + return func(i interface{}, path cty.Path) diag.Diagnostics { + v, ok := i.(string) + if !ok { + return diag.Errorf("expected type of %v to be string", path) + } + + for _, str := range valid { + if v == str || (ignoreCase && strings.EqualFold(v, str)) { + return nil + } + } + + return diag.Errorf("expected %v to be one of %q, got %s", path, valid, v) + } +} diff --git a/pkg/resources/warehouse.go b/pkg/resources/warehouse.go index 2c7e053899..7116155b66 100644 --- a/pkg/resources/warehouse.go +++ b/pkg/resources/warehouse.go @@ -5,6 +5,7 @@ import ( "database/sql" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/logging" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" snowflakevalidation "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/validation" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -264,6 +265,12 @@ func ReadWarehouse(d *schema.ResourceData, meta interface{}) error { if err = d.Set("enable_query_acceleration", w.EnableQueryAcceleration); err != nil { return err } + + err = readWarehouseObjectProperties(d, id, client, ctx) + if err != nil { + return err + } + if w.EnableQueryAcceleration { if err = d.Set("query_acceleration_max_scale_factor", w.QueryAccelerationMaxScaleFactor); err != nil { return err @@ -273,6 +280,37 @@ func ReadWarehouse(d *schema.ResourceData, meta interface{}) error { return nil } +func readWarehouseObjectProperties(d *schema.ResourceData, warehouseId sdk.AccountObjectIdentifier, client *sdk.Client, ctx context.Context) error { + statementTimeoutInSecondsParameter, err := client.Parameters.ShowObjectParameter(ctx, "STATEMENT_TIMEOUT_IN_SECONDS", sdk.Object{ObjectType: sdk.ObjectTypeWarehouse, Name: warehouseId}) + if err != nil { + return err + } + logging.DebugLogger.Printf("[DEBUG] STATEMENT_TIMEOUT_IN_SECONDS parameter was fetched: %v", statementTimeoutInSecondsParameter) + if err = d.Set("statement_timeout_in_seconds", sdk.ToInt(statementTimeoutInSecondsParameter.Value)); err != nil { + return err + } + + statementQueuedTimeoutInSecondsParameter, err := client.Parameters.ShowObjectParameter(ctx, "STATEMENT_QUEUED_TIMEOUT_IN_SECONDS", sdk.Object{ObjectType: sdk.ObjectTypeWarehouse, Name: warehouseId}) + if err != nil { + return err + } + logging.DebugLogger.Printf("[DEBUG] STATEMENT_QUEUED_TIMEOUT_IN_SECONDS parameter was fetched: %v", statementQueuedTimeoutInSecondsParameter) + if err = d.Set("statement_queued_timeout_in_seconds", sdk.ToInt(statementQueuedTimeoutInSecondsParameter.Value)); err != nil { + return err + } + + maxConcurrencyLevelParameter, err := client.Parameters.ShowObjectParameter(ctx, "MAX_CONCURRENCY_LEVEL", sdk.Object{ObjectType: sdk.ObjectTypeWarehouse, Name: warehouseId}) + if err != nil { + return err + } + logging.DebugLogger.Printf("[DEBUG] MAX_CONCURRENCY_LEVEL parameter was fetched: %v", maxConcurrencyLevelParameter) + if err = d.Set("max_concurrency_level", sdk.ToInt(maxConcurrencyLevelParameter.Value)); err != nil { + return err + } + + return nil +} + // UpdateWarehouse implements schema.UpdateFunc. func UpdateWarehouse(d *schema.ResourceData, meta interface{}) error { db := meta.(*sql.DB) diff --git a/pkg/resources/warehouse_acceptance_test.go b/pkg/resources/warehouse_acceptance_test.go index 48eb3e5ead..70ecc481cb 100644 --- a/pkg/resources/warehouse_acceptance_test.go +++ b/pkg/resources/warehouse_acceptance_test.go @@ -1,14 +1,18 @@ package resources_test import ( + "context" "fmt" "os" "strings" "testing" acc "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/stretchr/testify/require" ) func TestAcc_Warehouse(t *testing.T) { @@ -31,6 +35,7 @@ func TestAcc_Warehouse(t *testing.T) { resource.TestCheckResourceAttr("snowflake_warehouse.w", "comment", "test comment"), resource.TestCheckResourceAttr("snowflake_warehouse.w", "auto_suspend", "60"), resource.TestCheckResourceAttrSet("snowflake_warehouse.w", "warehouse_size"), + resource.TestCheckResourceAttr("snowflake_warehouse.w", "max_concurrency_level", "8"), ), }, // RENAME @@ -45,14 +50,45 @@ func TestAcc_Warehouse(t *testing.T) { }, // CHANGE PROPERTIES { - Config: wConfig2(prefix2, "X-LARGE"), + Config: wConfig2(prefix2, "X-LARGE", 20), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("snowflake_warehouse.w", "name", prefix2), resource.TestCheckResourceAttr("snowflake_warehouse.w", "comment", "test comment 2"), resource.TestCheckResourceAttr("snowflake_warehouse.w", "auto_suspend", "60"), resource.TestCheckResourceAttr("snowflake_warehouse.w", "warehouse_size", "XLARGE"), + resource.TestCheckResourceAttr("snowflake_warehouse.w", "max_concurrency_level", "20"), ), }, + // CHANGE JUST max_concurrency_level + { + Config: wConfig2(prefix2, "XLARGE", 16), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{plancheck.ExpectNonEmptyPlan()}, + }, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("snowflake_warehouse.w", "name", prefix2), + resource.TestCheckResourceAttr("snowflake_warehouse.w", "comment", "test comment 2"), + resource.TestCheckResourceAttr("snowflake_warehouse.w", "auto_suspend", "60"), + resource.TestCheckResourceAttr("snowflake_warehouse.w", "warehouse_size", "XLARGE"), + resource.TestCheckResourceAttr("snowflake_warehouse.w", "max_concurrency_level", "16"), + ), + }, + // CHANGE max_concurrency_level EXTERNALLY proves https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2318 + { + PreConfig: func() { alterWarehouseMaxConcurrencyLevelExternally(t, prefix2, 10) }, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{plancheck.ExpectNonEmptyPlan()}, + }, + Config: wConfig2(prefix2, "XLARGE", 16), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("snowflake_warehouse.w", "name", prefix2), + resource.TestCheckResourceAttr("snowflake_warehouse.w", "comment", "test comment 2"), + resource.TestCheckResourceAttr("snowflake_warehouse.w", "auto_suspend", "60"), + resource.TestCheckResourceAttr("snowflake_warehouse.w", "warehouse_size", "XLARGE"), + resource.TestCheckResourceAttr("snowflake_warehouse.w", "max_concurrency_level", "16"), + ), + }, + // IMPORT { ResourceName: "snowflake_warehouse.w", @@ -112,7 +148,7 @@ resource "snowflake_warehouse" "w" { return fmt.Sprintf(s, prefix) } -func wConfig2(prefix string, size string) string { +func wConfig2(prefix string, size string, maxConcurrencyLevel int) string { s := ` resource "snowflake_warehouse" "w" { name = "%s" @@ -126,9 +162,10 @@ resource "snowflake_warehouse" "w" { auto_resume = true initially_suspended = true wait_for_provisioning = false + max_concurrency_level = %d } ` - return fmt.Sprintf(s, prefix, size) + return fmt.Sprintf(s, prefix, size, maxConcurrencyLevel) } func wConfigPattern(prefix string) string { @@ -142,3 +179,14 @@ resource "snowflake_warehouse" "w2" { ` return fmt.Sprintf(s, prefix, prefix) } + +func alterWarehouseMaxConcurrencyLevelExternally(t *testing.T, warehouseId string, level int) { + t.Helper() + + client, err := sdk.NewDefaultClient() + require.NoError(t, err) + ctx := context.Background() + + err = client.Warehouses.Alter(ctx, sdk.NewAccountObjectIdentifier(warehouseId), &sdk.AlterWarehouseOptions{Set: &sdk.WarehouseSet{MaxConcurrencyLevel: sdk.Int(level)}}) + require.NoError(t, err) +} diff --git a/pkg/sdk/client.go b/pkg/sdk/client.go index 3d0c5c97ee..2accdbc55c 100644 --- a/pkg/sdk/client.go +++ b/pkg/sdk/client.go @@ -52,6 +52,7 @@ type Client struct { FileFormats FileFormats Functions Functions Grants Grants + ManagedAccounts ManagedAccounts MaskingPolicies MaskingPolicies NetworkPolicies NetworkPolicies Parameters Parameters @@ -60,11 +61,13 @@ type Client struct { Procedures Procedures ResourceMonitors ResourceMonitors Roles Roles + RowAccessPolicies RowAccessPolicies Schemas Schemas SessionPolicies SessionPolicies Sessions Sessions Shares Shares Stages Stages + StorageIntegrations StorageIntegrations Streams Streams Tables Tables Tags Tags @@ -194,6 +197,7 @@ func (c *Client) initialize() { c.FileFormats = &fileFormats{client: c} c.Functions = &functions{client: c} c.Grants = &grants{client: c} + c.ManagedAccounts = &managedAccounts{client: c} c.MaskingPolicies = &maskingPolicies{client: c} c.NetworkPolicies = &networkPolicies{client: c} c.Parameters = ¶meters{client: c} @@ -203,11 +207,13 @@ func (c *Client) initialize() { c.ReplicationFunctions = &replicationFunctions{client: c} c.ResourceMonitors = &resourceMonitors{client: c} c.Roles = &roles{client: c} + c.RowAccessPolicies = &rowAccessPolicies{client: c} c.Schemas = &schemas{client: c} c.SessionPolicies = &sessionPolicies{client: c} c.Sessions = &sessions{client: c} c.Shares = &shares{client: c} c.Stages = &stages{client: c} + c.StorageIntegrations = &storageIntegrations{client: c} c.Streams = &streams{client: c} c.SystemFunctions = &systemFunctions{client: c} c.Tables = &tables{client: c} diff --git a/pkg/sdk/external_tables.go b/pkg/sdk/external_tables.go index fdf5bc5378..e6ebff779e 100644 --- a/pkg/sdk/external_tables.go +++ b/pkg/sdk/external_tables.go @@ -211,7 +211,7 @@ type CreateWithManualPartitioningExternalTableOptions struct { CloudProviderParams *CloudProviderParams PartitionBy []string `ddl:"keyword,parentheses" sql:"PARTITION BY"` Location string `ddl:"parameter" sql:"LOCATION"` - UserSpecifiedPartitionType *bool `ddl:"keyword" sql:"PARTITION_TYPE = USER_SPECIFIED"` + userSpecifiedPartitionType bool `ddl:"static" sql:"PARTITION_TYPE = USER_SPECIFIED"` FileFormat []ExternalTableFileFormat `ddl:"parameter,parentheses" sql:"FILE_FORMAT"` // RawFileFormat was introduced, because of the decision taken during https://github.com/Snowflake-Labs/terraform-provider-snowflake/pull/2228 // that for now the snowflake_external_table resource should continue on using raw file format, which wasn't previously supported by the new SDK. @@ -225,24 +225,23 @@ type CreateWithManualPartitioningExternalTableOptions struct { // CreateDeltaLakeExternalTableOptions based on https://docs.snowflake.com/en/sql-reference/sql/create-external-table type CreateDeltaLakeExternalTableOptions struct { - create bool `ddl:"static" sql:"CREATE"` - OrReplace *bool `ddl:"keyword" sql:"OR REPLACE"` - externalTable bool `ddl:"static" sql:"EXTERNAL TABLE"` - IfNotExists *bool `ddl:"keyword" sql:"IF NOT EXISTS"` - name SchemaObjectIdentifier `ddl:"identifier"` - Columns []ExternalTableColumn `ddl:"list,parentheses"` - CloudProviderParams *CloudProviderParams - PartitionBy []string `ddl:"keyword,parentheses" sql:"PARTITION BY"` - Location string `ddl:"parameter" sql:"LOCATION"` - RefreshOnCreate *bool `ddl:"parameter" sql:"REFRESH_ON_CREATE"` - AutoRefresh *bool `ddl:"parameter" sql:"AUTO_REFRESH"` - UserSpecifiedPartitionType *bool `ddl:"keyword" sql:"PARTITION_TYPE = USER_SPECIFIED"` - FileFormat []ExternalTableFileFormat `ddl:"parameter,parentheses" sql:"FILE_FORMAT"` + create bool `ddl:"static" sql:"CREATE"` + OrReplace *bool `ddl:"keyword" sql:"OR REPLACE"` + externalTable bool `ddl:"static" sql:"EXTERNAL TABLE"` + IfNotExists *bool `ddl:"keyword" sql:"IF NOT EXISTS"` + name SchemaObjectIdentifier `ddl:"identifier"` + Columns []ExternalTableColumn `ddl:"list,parentheses"` + CloudProviderParams *CloudProviderParams + PartitionBy []string `ddl:"keyword,parentheses" sql:"PARTITION BY"` + Location string `ddl:"parameter" sql:"LOCATION"` + RefreshOnCreate *bool `ddl:"parameter" sql:"REFRESH_ON_CREATE"` + AutoRefresh *bool `ddl:"parameter" sql:"AUTO_REFRESH"` + FileFormat []ExternalTableFileFormat `ddl:"parameter,parentheses" sql:"FILE_FORMAT"` // RawFileFormat was introduced, because of the decision taken during https://github.com/Snowflake-Labs/terraform-provider-snowflake/pull/2228 // that for now the snowflake_external_table resource should continue on using raw file format, which wasn't previously supported by the new SDK. // In the future it should most likely be replaced by a more structured version FileFormat RawFileFormat *RawFileFormat `ddl:"list,parentheses" sql:"FILE_FORMAT ="` - DeltaTableFormat *bool `ddl:"keyword" sql:"TABLE_FORMAT = DELTA"` + deltaTableFormat bool `ddl:"static" sql:"TABLE_FORMAT = DELTA"` CopyGrants *bool `ddl:"keyword" sql:"COPY GRANTS"` Comment *string `ddl:"parameter,single_quotes" sql:"COMMENT"` RowAccessPolicy *TableRowAccessPolicy `ddl:"keyword"` diff --git a/pkg/sdk/external_tables_dto.go b/pkg/sdk/external_tables_dto.go index d27b0850d3..5aceccbc24 100644 --- a/pkg/sdk/external_tables_dto.go +++ b/pkg/sdk/external_tables_dto.go @@ -268,6 +268,7 @@ func (s *CreateExternalTableRequest) toOpts() *CreateExternalTableOptions { name: s.name, Columns: columns, CloudProviderParams: cloudProviderParams, + PartitionBy: s.partitionBy, Location: s.location, RefreshOnCreate: s.refreshOnCreate, AutoRefresh: s.autoRefresh, @@ -283,20 +284,19 @@ func (s *CreateExternalTableRequest) toOpts() *CreateExternalTableOptions { } type CreateWithManualPartitioningExternalTableRequest struct { - orReplace *bool - ifNotExists *bool - name SchemaObjectIdentifier // required - columns []*ExternalTableColumnRequest - cloudProviderParams *CloudProviderParamsRequest - partitionBy []string - location string // required - userSpecifiedPartitionType *bool - rawFileFormat *string - fileFormat *ExternalTableFileFormatRequest - copyGrants *bool - comment *string - rowAccessPolicy *RowAccessPolicyRequest - tag []*TagAssociationRequest + orReplace *bool + ifNotExists *bool + name SchemaObjectIdentifier // required + columns []*ExternalTableColumnRequest + cloudProviderParams *CloudProviderParamsRequest + partitionBy []string + location string // required + rawFileFormat *string + fileFormat *ExternalTableFileFormatRequest + copyGrants *bool + comment *string + rowAccessPolicy *RowAccessPolicyRequest + tag []*TagAssociationRequest } func (v *CreateWithManualPartitioningExternalTableRequest) toOpts() *CreateWithManualPartitioningExternalTableOptions { @@ -337,41 +337,38 @@ func (v *CreateWithManualPartitioningExternalTableRequest) toOpts() *CreateWithM } return &CreateWithManualPartitioningExternalTableOptions{ - OrReplace: v.orReplace, - IfNotExists: v.ifNotExists, - name: v.name, - Columns: columns, - CloudProviderParams: cloudProviderParams, - PartitionBy: v.partitionBy, - Location: v.location, - UserSpecifiedPartitionType: v.userSpecifiedPartitionType, - RawFileFormat: rawFileFormat, - FileFormat: fileFormat, - CopyGrants: v.copyGrants, - Comment: v.comment, - RowAccessPolicy: rowAccessPolicy, - Tag: tag, + OrReplace: v.orReplace, + IfNotExists: v.ifNotExists, + name: v.name, + Columns: columns, + CloudProviderParams: cloudProviderParams, + PartitionBy: v.partitionBy, + Location: v.location, + RawFileFormat: rawFileFormat, + FileFormat: fileFormat, + CopyGrants: v.copyGrants, + Comment: v.comment, + RowAccessPolicy: rowAccessPolicy, + Tag: tag, } } type CreateDeltaLakeExternalTableRequest struct { - orReplace *bool - ifNotExists *bool - name SchemaObjectIdentifier // required - columns []*ExternalTableColumnRequest - cloudProviderParams *CloudProviderParamsRequest - partitionBy []string - location string // required - userSpecifiedPartitionType *bool - refreshOnCreate *bool - autoRefresh *bool - rawFileFormat *string - fileFormat *ExternalTableFileFormatRequest - deltaTableFormat *bool - copyGrants *bool - comment *string - rowAccessPolicy *RowAccessPolicyRequest - tag []*TagAssociationRequest + orReplace *bool + ifNotExists *bool + name SchemaObjectIdentifier // required + columns []*ExternalTableColumnRequest + cloudProviderParams *CloudProviderParamsRequest + partitionBy []string + location string // required + refreshOnCreate *bool + autoRefresh *bool + rawFileFormat *string + fileFormat *ExternalTableFileFormatRequest + copyGrants *bool + comment *string + rowAccessPolicy *RowAccessPolicyRequest + tag []*TagAssociationRequest } func (v *CreateDeltaLakeExternalTableRequest) toOpts() *CreateDeltaLakeExternalTableOptions { @@ -412,23 +409,21 @@ func (v *CreateDeltaLakeExternalTableRequest) toOpts() *CreateDeltaLakeExternalT } return &CreateDeltaLakeExternalTableOptions{ - OrReplace: v.orReplace, - IfNotExists: v.ifNotExists, - name: v.name, - Columns: columns, - CloudProviderParams: cloudProviderParams, - PartitionBy: v.partitionBy, - Location: v.location, - UserSpecifiedPartitionType: v.userSpecifiedPartitionType, - RefreshOnCreate: v.refreshOnCreate, - AutoRefresh: v.autoRefresh, - RawFileFormat: rawFileFormat, - FileFormat: fileFormat, - DeltaTableFormat: v.deltaTableFormat, - CopyGrants: v.copyGrants, - Comment: v.comment, - RowAccessPolicy: rowAccessPolicy, - Tag: tag, + OrReplace: v.orReplace, + IfNotExists: v.ifNotExists, + name: v.name, + Columns: columns, + CloudProviderParams: cloudProviderParams, + PartitionBy: v.partitionBy, + Location: v.location, + RefreshOnCreate: v.refreshOnCreate, + AutoRefresh: v.autoRefresh, + RawFileFormat: rawFileFormat, + FileFormat: fileFormat, + CopyGrants: v.copyGrants, + Comment: v.comment, + RowAccessPolicy: rowAccessPolicy, + Tag: tag, } } diff --git a/pkg/sdk/external_tables_dto_builders_gen.go b/pkg/sdk/external_tables_dto_builders_gen.go index c4a40af879..a466ac8b54 100644 --- a/pkg/sdk/external_tables_dto_builders_gen.go +++ b/pkg/sdk/external_tables_dto_builders_gen.go @@ -2,6 +2,8 @@ package sdk +import () + func NewCreateExternalTableRequest( name SchemaObjectIdentifier, location string, @@ -99,8 +101,8 @@ func NewExternalTableColumnRequest( return &s } -func (s *ExternalTableColumnRequest) WithNotNull() *ExternalTableColumnRequest { - s.notNull = Bool(true) +func (s *ExternalTableColumnRequest) WithNotNull(notNull *bool) *ExternalTableColumnRequest { + s.notNull = notNull return s } @@ -275,16 +277,6 @@ func (s *NullStringRequest) WithStr(str string) *NullStringRequest { return s } -func NewRowAccessPolicyRequest( - name SchemaObjectIdentifier, - on []string, -) *RowAccessPolicyRequest { - s := RowAccessPolicyRequest{} - s.Name = name - s.On = on - return &s -} - func NewCreateWithManualPartitioningExternalTableRequest( name SchemaObjectIdentifier, location string, @@ -320,11 +312,6 @@ func (s *CreateWithManualPartitioningExternalTableRequest) WithPartitionBy(parti return s } -func (s *CreateWithManualPartitioningExternalTableRequest) WithUserSpecifiedPartitionType(userSpecifiedPartitionType *bool) *CreateWithManualPartitioningExternalTableRequest { - s.userSpecifiedPartitionType = userSpecifiedPartitionType - return s -} - func (s *CreateWithManualPartitioningExternalTableRequest) WithRawFileFormat(rawFileFormat *string) *CreateWithManualPartitioningExternalTableRequest { s.rawFileFormat = rawFileFormat return s @@ -390,11 +377,6 @@ func (s *CreateDeltaLakeExternalTableRequest) WithPartitionBy(partitionBy []stri return s } -func (s *CreateDeltaLakeExternalTableRequest) WithUserSpecifiedPartitionType(userSpecifiedPartitionType *bool) *CreateDeltaLakeExternalTableRequest { - s.userSpecifiedPartitionType = userSpecifiedPartitionType - return s -} - func (s *CreateDeltaLakeExternalTableRequest) WithRefreshOnCreate(refreshOnCreate *bool) *CreateDeltaLakeExternalTableRequest { s.refreshOnCreate = refreshOnCreate return s @@ -415,11 +397,6 @@ func (s *CreateDeltaLakeExternalTableRequest) WithFileFormat(fileFormat *Externa return s } -func (s *CreateDeltaLakeExternalTableRequest) WithDeltaTableFormat(deltaTableFormat *bool) *CreateDeltaLakeExternalTableRequest { - s.deltaTableFormat = deltaTableFormat - return s -} - func (s *CreateDeltaLakeExternalTableRequest) WithCopyGrants(copyGrants *bool) *CreateDeltaLakeExternalTableRequest { s.copyGrants = copyGrants return s diff --git a/pkg/sdk/external_tables_test.go b/pkg/sdk/external_tables_test.go index 57eee6fa61..325bd87557 100644 --- a/pkg/sdk/external_tables_test.go +++ b/pkg/sdk/external_tables_test.go @@ -180,7 +180,7 @@ func TestExternalTablesCreateWithManualPartitioning(t *testing.T) { }, Comment: String("some_comment"), } - assertOptsValidAndSQLEquals(t, opts, `CREATE OR REPLACE EXTERNAL TABLE "db"."schema"."external_table" (column varchar AS (value::column::varchar) NOT NULL CONSTRAINT my_constraint UNIQUE) INTEGRATION = '123' LOCATION = @s1/logs/ FILE_FORMAT = (TYPE = JSON) COPY GRANTS COMMENT = 'some_comment' ROW ACCESS POLICY "db"."schema"."row_access_policy" ON (value1, value2) TAG ("tag1" = 'value1', "tag2" = 'value2')`) + assertOptsValidAndSQLEquals(t, opts, `CREATE OR REPLACE EXTERNAL TABLE "db"."schema"."external_table" (column varchar AS (value::column::varchar) NOT NULL CONSTRAINT my_constraint UNIQUE) INTEGRATION = '123' LOCATION = @s1/logs/ PARTITION_TYPE = USER_SPECIFIED FILE_FORMAT = (TYPE = JSON) COPY GRANTS COMMENT = 'some_comment' ROW ACCESS POLICY "db"."schema"."row_access_policy" ON (value1, value2) TAG ("tag1" = 'value1', "tag2" = 'value2')`) }) t.Run("invalid options", func(t *testing.T) { @@ -216,7 +216,7 @@ func TestExternalTablesCreateWithManualPartitioning(t *testing.T) { Location: "@s1/logs/", RawFileFormat: &RawFileFormat{Format: "TYPE = JSON"}, } - assertOptsValidAndSQLEquals(t, opts, `CREATE EXTERNAL TABLE "db"."schema"."external_table" (column varchar AS (value::column::varchar) NOT NULL CONSTRAINT my_constraint UNIQUE) LOCATION = @s1/logs/ FILE_FORMAT = (TYPE = JSON)`) + assertOptsValidAndSQLEquals(t, opts, `CREATE EXTERNAL TABLE "db"."schema"."external_table" (column varchar AS (value::column::varchar) NOT NULL CONSTRAINT my_constraint UNIQUE) LOCATION = @s1/logs/ PARTITION_TYPE = USER_SPECIFIED FILE_FORMAT = (TYPE = JSON)`) }) t.Run("validation: neither raw file format is set, nor file format", func(t *testing.T) { @@ -263,8 +263,7 @@ func TestExternalTablesCreateDeltaLake(t *testing.T) { Name: String("JSON"), }, }, - DeltaTableFormat: Bool(true), - CopyGrants: Bool(true), + CopyGrants: Bool(true), RowAccessPolicy: &TableRowAccessPolicy{ Name: NewSchemaObjectIdentifier("db", "schema", "row_access_policy"), On: []string{"value1", "value2"}, @@ -317,7 +316,7 @@ func TestExternalTablesCreateDeltaLake(t *testing.T) { Location: "@s1/logs/", RawFileFormat: &RawFileFormat{Format: "TYPE = JSON"}, } - assertOptsValidAndSQLEquals(t, opts, `CREATE EXTERNAL TABLE "db"."schema"."external_table" (column varchar AS (value::column::varchar) NOT NULL CONSTRAINT my_constraint UNIQUE) LOCATION = @s1/logs/ FILE_FORMAT = (TYPE = JSON)`) + assertOptsValidAndSQLEquals(t, opts, `CREATE EXTERNAL TABLE "db"."schema"."external_table" (column varchar AS (value::column::varchar) NOT NULL CONSTRAINT my_constraint UNIQUE) LOCATION = @s1/logs/ FILE_FORMAT = (TYPE = JSON) TABLE_FORMAT = DELTA`) }) t.Run("validation: neither raw file format is set, nor file format", func(t *testing.T) { diff --git a/pkg/sdk/grants.go b/pkg/sdk/grants.go index f4dc79fcb4..191d7388ce 100644 --- a/pkg/sdk/grants.go +++ b/pkg/sdk/grants.go @@ -97,6 +97,7 @@ type DatabaseRoleGrantPrivileges struct { DatabasePrivileges []AccountObjectPrivilege `ddl:"-"` SchemaPrivileges []SchemaPrivilege `ddl:"-"` SchemaObjectPrivileges []SchemaObjectPrivilege `ddl:"-"` + AllPrivileges *bool `ddl:"keyword" sql:"ALL PRIVILEGES"` } type DatabaseRoleGrantOn struct { @@ -186,8 +187,9 @@ type ShowGrantsTo struct { } type ShowGrantsOf struct { - Role AccountObjectIdentifier `ddl:"identifier" sql:"ROLE"` - Share AccountObjectIdentifier `ddl:"identifier" sql:"SHARE"` + Role AccountObjectIdentifier `ddl:"identifier" sql:"ROLE"` + DatabaseRole DatabaseObjectIdentifier `ddl:"identifier" sql:"DATABASE ROLE"` + Share AccountObjectIdentifier `ddl:"identifier" sql:"SHARE"` } type grantRow struct { @@ -211,7 +213,7 @@ type Grant struct { Name ObjectIdentifier GrantedTo ObjectType GrantTo ObjectType - GranteeName AccountObjectIdentifier + GranteeName ObjectIdentifier GrantOption bool GrantedBy AccountObjectIdentifier } diff --git a/pkg/sdk/grants_test.go b/pkg/sdk/grants_test.go index 927c954bcd..8aecb1cb7a 100644 --- a/pkg/sdk/grants_test.go +++ b/pkg/sdk/grants_test.go @@ -351,7 +351,7 @@ func TestGrants_GrantPrivilegesToDatabaseRole(t *testing.T) { t.Run("validation: no privileges set", func(t *testing.T) { opts := defaultGrantsForDb() opts.privileges = &DatabaseRoleGrantPrivileges{} - assertOptsInvalidJoinedErrors(t, opts, errExactlyOneOf("DatabaseRoleGrantPrivileges", "DatabasePrivileges", "SchemaPrivileges", "SchemaObjectPrivileges")) + assertOptsInvalidJoinedErrors(t, opts, errExactlyOneOf("DatabaseRoleGrantPrivileges", "DatabasePrivileges", "SchemaPrivileges", "SchemaObjectPrivileges", "AllPrivileges")) }) t.Run("validation: too many privileges set", func(t *testing.T) { @@ -360,7 +360,7 @@ func TestGrants_GrantPrivilegesToDatabaseRole(t *testing.T) { DatabasePrivileges: []AccountObjectPrivilege{AccountObjectPrivilegeCreateSchema}, SchemaPrivileges: []SchemaPrivilege{SchemaPrivilegeCreateAlert}, } - assertOptsInvalidJoinedErrors(t, opts, errExactlyOneOf("DatabaseRoleGrantPrivileges", "DatabasePrivileges", "SchemaPrivileges", "SchemaObjectPrivileges")) + assertOptsInvalidJoinedErrors(t, opts, errExactlyOneOf("DatabaseRoleGrantPrivileges", "DatabasePrivileges", "SchemaPrivileges", "SchemaObjectPrivileges", "AllPrivileges")) }) t.Run("validation: no on set", func(t *testing.T) { @@ -480,6 +480,14 @@ func TestGrants_GrantPrivilegesToDatabaseRole(t *testing.T) { } assertOptsValidAndSQLEquals(t, opts, `GRANT APPLY ON FUTURE TABLES IN SCHEMA "db1"."schema1" TO DATABASE ROLE "db1"."role1"`) }) + + t.Run("grant all privileges", func(t *testing.T) { + opts := defaultGrantsForSchemaObject() + opts.privileges = &DatabaseRoleGrantPrivileges{ + AllPrivileges: Bool(true), + } + assertOptsValidAndSQLEquals(t, opts, `GRANT ALL PRIVILEGES ON TABLE "db1"."schema1"."table1" TO DATABASE ROLE "db1"."role1"`) + }) } func TestGrants_RevokePrivilegesFromDatabaseRoleRole(t *testing.T) { @@ -537,7 +545,7 @@ func TestGrants_RevokePrivilegesFromDatabaseRoleRole(t *testing.T) { t.Run("validation: no privileges set", func(t *testing.T) { opts := defaultGrantsForDb() opts.privileges = &DatabaseRoleGrantPrivileges{} - assertOptsInvalidJoinedErrors(t, opts, errExactlyOneOf("DatabaseRoleGrantPrivileges", "DatabasePrivileges", "SchemaPrivileges", "SchemaObjectPrivileges")) + assertOptsInvalidJoinedErrors(t, opts, errExactlyOneOf("DatabaseRoleGrantPrivileges", "DatabasePrivileges", "SchemaPrivileges", "SchemaObjectPrivileges", "AllPrivileges")) }) t.Run("validation: too many privileges set", func(t *testing.T) { @@ -546,7 +554,7 @@ func TestGrants_RevokePrivilegesFromDatabaseRoleRole(t *testing.T) { DatabasePrivileges: []AccountObjectPrivilege{AccountObjectPrivilegeCreateSchema}, SchemaPrivileges: []SchemaPrivilege{SchemaPrivilegeCreateAlert}, } - assertOptsInvalidJoinedErrors(t, opts, errExactlyOneOf("DatabaseRoleGrantPrivileges", "DatabasePrivileges", "SchemaPrivileges", "SchemaObjectPrivileges")) + assertOptsInvalidJoinedErrors(t, opts, errExactlyOneOf("DatabaseRoleGrantPrivileges", "DatabasePrivileges", "SchemaPrivileges", "SchemaObjectPrivileges", "AllPrivileges")) }) t.Run("validation: nil on set", func(t *testing.T) { @@ -1009,6 +1017,16 @@ func TestGrantShow(t *testing.T) { assertOptsValidAndSQLEquals(t, opts, "SHOW GRANTS OF ROLE %s", roleID.FullyQualifiedName()) }) + t.Run("of database role", func(t *testing.T) { + roleID := RandomDatabaseObjectIdentifier() + opts := &ShowGrantOptions{ + Of: &ShowGrantsOf{ + DatabaseRole: roleID, + }, + } + assertOptsValidAndSQLEquals(t, opts, "SHOW GRANTS OF DATABASE ROLE %s", roleID.FullyQualifiedName()) + }) + t.Run("of share", func(t *testing.T) { shareID := RandomAccountObjectIdentifier() opts := &ShowGrantOptions{ diff --git a/pkg/sdk/grants_validations.go b/pkg/sdk/grants_validations.go index cfddde6a30..339a78a5e8 100644 --- a/pkg/sdk/grants_validations.go +++ b/pkg/sdk/grants_validations.go @@ -160,8 +160,8 @@ func (opts *GrantPrivilegesToDatabaseRoleOptions) validate() error { func (v *DatabaseRoleGrantPrivileges) validate() error { var errs []error - if !exactlyOneValueSet(v.DatabasePrivileges, v.SchemaPrivileges, v.SchemaObjectPrivileges) { - errs = append(errs, errExactlyOneOf("DatabaseRoleGrantPrivileges", "DatabasePrivileges", "SchemaPrivileges", "SchemaObjectPrivileges")) + if !exactlyOneValueSet(v.DatabasePrivileges, v.SchemaPrivileges, v.SchemaObjectPrivileges, v.AllPrivileges) { + errs = append(errs, errExactlyOneOf("DatabaseRoleGrantPrivileges", "DatabasePrivileges", "SchemaPrivileges", "SchemaObjectPrivileges", "AllPrivileges")) } if valueSet(v.DatabasePrivileges) { allowedPrivileges := []AccountObjectPrivilege{ diff --git a/pkg/sdk/internal/random/random_helpers.go b/pkg/sdk/internal/random/random_helpers.go index d9ddac43c0..5734dcbdd9 100644 --- a/pkg/sdk/internal/random/random_helpers.go +++ b/pkg/sdk/internal/random/random_helpers.go @@ -30,6 +30,10 @@ func AlphanumericN(num int) string { return gofakeit.Password(true, true, true, false, false, num) } +func AlphaN(num int) string { + return gofakeit.Password(true, true, false, false, false, num) +} + func StringRange(min, max int) string { return gofakeit.Password(true, true, true, true, false, IntRange(min, max)) } diff --git a/pkg/sdk/managed_accounts_def.go b/pkg/sdk/managed_accounts_def.go new file mode 100644 index 0000000000..2124b6e258 --- /dev/null +++ b/pkg/sdk/managed_accounts_def.go @@ -0,0 +1,70 @@ +package sdk + +import g "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk/poc/generator" + +//go:generate go run ./poc/main.go + +var managedAccountDbRow = g.DbStruct("managedAccountDBRow"). + Text("name"). + Text("cloud"). + Text("region"). + Text("locator"). + Text("created_on"). + Text("url"). + Text("account_locator_url"). + Bool("is_reader"). + OptionalText("comment") + +var managedAccount = g.PlainStruct("ManagedAccount"). + Text("Name"). + Text("Cloud"). + Text("Region"). + Text("Locator"). + Text("CreatedOn"). + Text("URL"). + Text("AccountLocatorURL"). + Bool("IsReader"). + OptionalText("Comment") + +var ManagedAccountsDef = g.NewInterface( + "ManagedAccounts", + "ManagedAccount", + g.KindOfT[AccountObjectIdentifier](), +). + CreateOperation( + "https://docs.snowflake.com/en/sql-reference/sql/create-managed-account", + g.NewQueryStruct("CreateManagedAccount"). + Create(). + SQL("MANAGED ACCOUNT"). + Name(). + QueryStructField( + "CreateManagedAccountParams", + g.NewQueryStruct("CreateManagedAccountParams"). + TextAssignment("ADMIN_NAME", g.ParameterOptions().SingleQuotes().Required()). + TextAssignment("ADMIN_PASSWORD", g.ParameterOptions().SingleQuotes().Required()). + PredefinedQueryStructField("typeProvider", "string", g.StaticOptions().SQL("TYPE = READER")). + OptionalComment(). + WithValidation(g.ValidateValueSet, "AdminName"). + WithValidation(g.ValidateValueSet, "AdminPassword"), + g.ListOptions().NoParentheses().Required(), + ). + WithValidation(g.ValidIdentifier, "name"), + ). + DropOperation( + "https://docs.snowflake.com/en/sql-reference/sql/drop-managed-account", + g.NewQueryStruct("DropManagedAccount"). + Drop(). + SQL("MANAGED ACCOUNT"). + Name(). + WithValidation(g.ValidIdentifier, "name"), + ). + ShowOperation( + "https://docs.snowflake.com/en/sql-reference/sql/show-managed-accounts", + managedAccountDbRow, + managedAccount, + g.NewQueryStruct("ShowManagedAccounts"). + Show(). + SQL("MANAGED ACCOUNTS"). + OptionalLike(), + ). + ShowByIdOperation() diff --git a/pkg/sdk/managed_accounts_dto_builders_gen.go b/pkg/sdk/managed_accounts_dto_builders_gen.go new file mode 100644 index 0000000000..7523608a2d --- /dev/null +++ b/pkg/sdk/managed_accounts_dto_builders_gen.go @@ -0,0 +1,47 @@ +// Code generated by dto builder generator; DO NOT EDIT. + +package sdk + +import () + +func NewCreateManagedAccountRequest( + name AccountObjectIdentifier, + CreateManagedAccountParams CreateManagedAccountParamsRequest, +) *CreateManagedAccountRequest { + s := CreateManagedAccountRequest{} + s.name = name + s.CreateManagedAccountParams = CreateManagedAccountParams + return &s +} + +func NewCreateManagedAccountParamsRequest( + AdminName string, + AdminPassword string, +) *CreateManagedAccountParamsRequest { + s := CreateManagedAccountParamsRequest{} + s.AdminName = AdminName + s.AdminPassword = AdminPassword + return &s +} + +func (s *CreateManagedAccountParamsRequest) WithComment(Comment *string) *CreateManagedAccountParamsRequest { + s.Comment = Comment + return s +} + +func NewDropManagedAccountRequest( + name AccountObjectIdentifier, +) *DropManagedAccountRequest { + s := DropManagedAccountRequest{} + s.name = name + return &s +} + +func NewShowManagedAccountRequest() *ShowManagedAccountRequest { + return &ShowManagedAccountRequest{} +} + +func (s *ShowManagedAccountRequest) WithLike(Like *Like) *ShowManagedAccountRequest { + s.Like = Like + return s +} diff --git a/pkg/sdk/managed_accounts_dto_gen.go b/pkg/sdk/managed_accounts_dto_gen.go new file mode 100644 index 0000000000..cbb0a51884 --- /dev/null +++ b/pkg/sdk/managed_accounts_dto_gen.go @@ -0,0 +1,32 @@ +package sdk + +//go:generate go run ./dto-builder-generator/main.go + +var ( + _ optionsProvider[CreateManagedAccountOptions] = new(CreateManagedAccountRequest) + _ optionsProvider[DropManagedAccountOptions] = new(DropManagedAccountRequest) + _ optionsProvider[ShowManagedAccountOptions] = new(ShowManagedAccountRequest) +) + +type CreateManagedAccountRequest struct { + name AccountObjectIdentifier // required + CreateManagedAccountParams CreateManagedAccountParamsRequest // required +} + +func (r *CreateManagedAccountRequest) GetName() AccountObjectIdentifier { + return r.name +} + +type CreateManagedAccountParamsRequest struct { + AdminName string // required + AdminPassword string // required + Comment *string +} + +type DropManagedAccountRequest struct { + name AccountObjectIdentifier // required +} + +type ShowManagedAccountRequest struct { + Like *Like +} diff --git a/pkg/sdk/managed_accounts_gen.go b/pkg/sdk/managed_accounts_gen.go new file mode 100644 index 0000000000..88aa479e61 --- /dev/null +++ b/pkg/sdk/managed_accounts_gen.go @@ -0,0 +1,66 @@ +package sdk + +import ( + "context" + "database/sql" +) + +type ManagedAccounts interface { + Create(ctx context.Context, request *CreateManagedAccountRequest) error + Drop(ctx context.Context, request *DropManagedAccountRequest) error + Show(ctx context.Context, request *ShowManagedAccountRequest) ([]ManagedAccount, error) + ShowByID(ctx context.Context, id AccountObjectIdentifier) (*ManagedAccount, error) +} + +// CreateManagedAccountOptions is based on https://docs.snowflake.com/en/sql-reference/sql/create-managed-account. +type CreateManagedAccountOptions struct { + create bool `ddl:"static" sql:"CREATE"` + managedAccount bool `ddl:"static" sql:"MANAGED ACCOUNT"` + name AccountObjectIdentifier `ddl:"identifier"` + CreateManagedAccountParams CreateManagedAccountParams `ddl:"list,no_parentheses"` +} + +type CreateManagedAccountParams struct { + AdminName string `ddl:"parameter,single_quotes" sql:"ADMIN_NAME"` + AdminPassword string `ddl:"parameter,single_quotes" sql:"ADMIN_PASSWORD"` + typeProvider string `ddl:"static" sql:"TYPE = READER"` + Comment *string `ddl:"parameter,single_quotes" sql:"COMMENT"` +} + +// DropManagedAccountOptions is based on https://docs.snowflake.com/en/sql-reference/sql/drop-managed-account. +type DropManagedAccountOptions struct { + drop bool `ddl:"static" sql:"DROP"` + managedAccount bool `ddl:"static" sql:"MANAGED ACCOUNT"` + name AccountObjectIdentifier `ddl:"identifier"` +} + +// ShowManagedAccountOptions is based on https://docs.snowflake.com/en/sql-reference/sql/show-managed-accounts. +type ShowManagedAccountOptions struct { + show bool `ddl:"static" sql:"SHOW"` + managedAccounts bool `ddl:"static" sql:"MANAGED ACCOUNTS"` + Like *Like `ddl:"keyword" sql:"LIKE"` +} + +type managedAccountDBRow struct { + Name string `db:"name"` + Cloud string `db:"cloud"` + Region string `db:"region"` + Locator string `db:"locator"` + CreatedOn string `db:"created_on"` + Url string `db:"url"` + AccountLocatorUrl string `db:"account_locator_url"` + IsReader bool `db:"is_reader"` + Comment sql.NullString `db:"comment"` +} + +type ManagedAccount struct { + Name string + Cloud string + Region string + Locator string + CreatedOn string + URL string + AccountLocatorURL string + IsReader bool + Comment string +} diff --git a/pkg/sdk/managed_accounts_gen_test.go b/pkg/sdk/managed_accounts_gen_test.go new file mode 100644 index 0000000000..e3f4d81b96 --- /dev/null +++ b/pkg/sdk/managed_accounts_gen_test.go @@ -0,0 +1,104 @@ +package sdk + +import "testing" + +func TestManagedAccounts_Create(t *testing.T) { + id := RandomAccountObjectIdentifier() + + // Minimal valid CreateManagedAccountOptions + defaultOpts := func() *CreateManagedAccountOptions { + return &CreateManagedAccountOptions{ + name: id, + CreateManagedAccountParams: CreateManagedAccountParams{ + AdminName: "admin", + AdminPassword: "password", + }, + } + } + + t.Run("validation: nil options", func(t *testing.T) { + var opts *CreateManagedAccountOptions = nil + assertOptsInvalidJoinedErrors(t, opts, ErrNilOptions) + }) + + t.Run("validation: valid identifier for [opts.name]", func(t *testing.T) { + opts := defaultOpts() + opts.name = NewAccountObjectIdentifier("") + assertOptsInvalidJoinedErrors(t, opts, ErrInvalidObjectIdentifier) + }) + + t.Run("validation: [opts.CreateManagedAccountParams.AdminName] should be set", func(t *testing.T) { + opts := defaultOpts() + opts.CreateManagedAccountParams.AdminName = "" + assertOptsInvalidJoinedErrors(t, opts, errNotSet("CreateManagedAccountOptions.CreateManagedAccountParams", "AdminName")) + }) + + t.Run("validation: [opts.CreateManagedAccountParams.AdminPassword] should be set", func(t *testing.T) { + opts := defaultOpts() + opts.CreateManagedAccountParams.AdminPassword = "" + assertOptsInvalidJoinedErrors(t, opts, errNotSet("CreateManagedAccountOptions.CreateManagedAccountParams", "AdminPassword")) + }) + + t.Run("basic", func(t *testing.T) { + opts := defaultOpts() + assertOptsValidAndSQLEquals(t, opts, "CREATE MANAGED ACCOUNT %s ADMIN_NAME = 'admin', ADMIN_PASSWORD = 'password', TYPE = READER", id.FullyQualifiedName()) + }) + + t.Run("all options", func(t *testing.T) { + opts := defaultOpts() + opts.CreateManagedAccountParams.Comment = String("comment") + assertOptsValidAndSQLEquals(t, opts, "CREATE MANAGED ACCOUNT %s ADMIN_NAME = 'admin', ADMIN_PASSWORD = 'password', TYPE = READER, COMMENT = 'comment'", id.FullyQualifiedName()) + }) +} + +func TestManagedAccounts_Drop(t *testing.T) { + id := RandomAccountObjectIdentifier() + + // Minimal valid DropManagedAccountOptions + defaultOpts := func() *DropManagedAccountOptions { + return &DropManagedAccountOptions{ + name: id, + } + } + + t.Run("validation: nil options", func(t *testing.T) { + var opts *DropManagedAccountOptions = nil + assertOptsInvalidJoinedErrors(t, opts, ErrNilOptions) + }) + + t.Run("validation: valid identifier for [opts.name]", func(t *testing.T) { + opts := defaultOpts() + opts.name = NewAccountObjectIdentifier("") + assertOptsInvalidJoinedErrors(t, opts, ErrInvalidObjectIdentifier) + }) + + t.Run("basic", func(t *testing.T) { + opts := defaultOpts() + assertOptsValidAndSQLEquals(t, opts, "DROP MANAGED ACCOUNT %s", id.FullyQualifiedName()) + }) +} + +func TestManagedAccounts_Show(t *testing.T) { + // Minimal valid ShowManagedAccountOptions + defaultOpts := func() *ShowManagedAccountOptions { + return &ShowManagedAccountOptions{} + } + + t.Run("validation: nil options", func(t *testing.T) { + var opts *ShowManagedAccountOptions = nil + assertOptsInvalidJoinedErrors(t, opts, ErrNilOptions) + }) + + t.Run("basic", func(t *testing.T) { + opts := defaultOpts() + assertOptsValidAndSQLEquals(t, opts, "SHOW MANAGED ACCOUNTS") + }) + + t.Run("all options", func(t *testing.T) { + opts := defaultOpts() + opts.Like = &Like{ + Pattern: String("myaccount"), + } + assertOptsValidAndSQLEquals(t, opts, "SHOW MANAGED ACCOUNTS LIKE 'myaccount'") + }) +} diff --git a/pkg/sdk/managed_accounts_impl_gen.go b/pkg/sdk/managed_accounts_impl_gen.go new file mode 100644 index 0000000000..62db79b682 --- /dev/null +++ b/pkg/sdk/managed_accounts_impl_gen.go @@ -0,0 +1,84 @@ +package sdk + +import ( + "context" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk/internal/collections" +) + +var _ ManagedAccounts = (*managedAccounts)(nil) + +type managedAccounts struct { + client *Client +} + +func (v *managedAccounts) Create(ctx context.Context, request *CreateManagedAccountRequest) error { + opts := request.toOpts() + return validateAndExec(v.client, ctx, opts) +} + +func (v *managedAccounts) Drop(ctx context.Context, request *DropManagedAccountRequest) error { + opts := request.toOpts() + return validateAndExec(v.client, ctx, opts) +} + +func (v *managedAccounts) Show(ctx context.Context, request *ShowManagedAccountRequest) ([]ManagedAccount, error) { + opts := request.toOpts() + dbRows, err := validateAndQuery[managedAccountDBRow](v.client, ctx, opts) + if err != nil { + return nil, err + } + resultList := convertRows[managedAccountDBRow, ManagedAccount](dbRows) + return resultList, nil +} + +func (v *managedAccounts) ShowByID(ctx context.Context, id AccountObjectIdentifier) (*ManagedAccount, error) { + managedAccounts, err := v.Show(ctx, NewShowManagedAccountRequest().WithLike(&Like{String(id.Name())})) + if err != nil { + return nil, err + } + return collections.FindOne(managedAccounts, func(r ManagedAccount) bool { return r.Name == id.Name() }) +} + +func (r *CreateManagedAccountRequest) toOpts() *CreateManagedAccountOptions { + opts := &CreateManagedAccountOptions{ + name: r.name, + } + opts.CreateManagedAccountParams = CreateManagedAccountParams{ + AdminName: r.CreateManagedAccountParams.AdminName, + AdminPassword: r.CreateManagedAccountParams.AdminPassword, + Comment: r.CreateManagedAccountParams.Comment, + } + return opts +} + +func (r *DropManagedAccountRequest) toOpts() *DropManagedAccountOptions { + opts := &DropManagedAccountOptions{ + name: r.name, + } + return opts +} + +func (r *ShowManagedAccountRequest) toOpts() *ShowManagedAccountOptions { + opts := &ShowManagedAccountOptions{ + Like: r.Like, + } + return opts +} + +func (r managedAccountDBRow) convert() *ManagedAccount { + managedAccount := &ManagedAccount{ + Name: r.Name, + Cloud: r.Cloud, + Region: r.Region, + Locator: r.Locator, + CreatedOn: r.CreatedOn, + URL: r.Url, + AccountLocatorURL: r.AccountLocatorUrl, + IsReader: r.IsReader, + } + if r.Comment.Valid { + managedAccount.Comment = r.Comment.String + } + return managedAccount +} diff --git a/pkg/sdk/managed_accounts_validations_gen.go b/pkg/sdk/managed_accounts_validations_gen.go new file mode 100644 index 0000000000..0d1106fc49 --- /dev/null +++ b/pkg/sdk/managed_accounts_validations_gen.go @@ -0,0 +1,45 @@ +package sdk + +var ( + _ validatable = new(CreateManagedAccountOptions) + _ validatable = new(DropManagedAccountOptions) + _ validatable = new(ShowManagedAccountOptions) +) + +func (opts *CreateManagedAccountOptions) validate() error { + if opts == nil { + return ErrNilOptions + } + var errs []error + if !ValidObjectIdentifier(opts.name) { + errs = append(errs, ErrInvalidObjectIdentifier) + } + if valueSet(opts.CreateManagedAccountParams) { + if !valueSet(opts.CreateManagedAccountParams.AdminName) { + errs = append(errs, errNotSet("CreateManagedAccountOptions.CreateManagedAccountParams", "AdminName")) + } + if !valueSet(opts.CreateManagedAccountParams.AdminPassword) { + errs = append(errs, errNotSet("CreateManagedAccountOptions.CreateManagedAccountParams", "AdminPassword")) + } + } + return JoinErrors(errs...) +} + +func (opts *DropManagedAccountOptions) validate() error { + if opts == nil { + return ErrNilOptions + } + var errs []error + if !ValidObjectIdentifier(opts.name) { + errs = append(errs, ErrInvalidObjectIdentifier) + } + return JoinErrors(errs...) +} + +func (opts *ShowManagedAccountOptions) validate() error { + if opts == nil { + return ErrNilOptions + } + var errs []error + return JoinErrors(errs...) +} diff --git a/pkg/sdk/object_types.go b/pkg/sdk/object_types.go index 6b9ea5b19a..ea858b9441 100644 --- a/pkg/sdk/object_types.go +++ b/pkg/sdk/object_types.go @@ -60,6 +60,8 @@ const ( ObjectTypeColumn ObjectType = "COLUMN" ObjectTypeIcebergTable ObjectType = "ICEBERG TABLE" ObjectTypeExternalVolume ObjectType = "EXTERNAL VOLUME" + ObjectTypeNetworkRule ObjectType = "NETWORK RULE" + ObjectTypePackagesPolicy ObjectType = "PACKAGES POLICY" ) func (o ObjectType) String() string { @@ -112,6 +114,8 @@ func objectTypeSingularToPluralMap() map[ObjectType]PluralObjectType { ObjectTypeStreamlit: PluralObjectTypeStreamlits, ObjectTypeIcebergTable: PluralObjectTypeIcebergTables, ObjectTypeExternalVolume: PluralObjectTypeExternalVolumes, + ObjectTypeNetworkRule: PluralObjectTypeNetworkRules, + ObjectTypePackagesPolicy: PluralObjectTypePackagesPolicies, } } @@ -204,6 +208,8 @@ const ( PluralObjectTypeStreamlits PluralObjectType = "STREAMLITS" PluralObjectTypeIcebergTables PluralObjectType = "ICEBERG TABLES" PluralObjectTypeExternalVolumes PluralObjectType = "EXTERNAL VOLUMES" + PluralObjectTypeNetworkRules PluralObjectType = "NETWORK RULES" + PluralObjectTypePackagesPolicies PluralObjectType = "PACKAGES POLICIES" ) func (p PluralObjectType) String() string { diff --git a/pkg/sdk/poc/generator/db_struct.go b/pkg/sdk/poc/generator/db_struct.go index 03490dc4ae..8e1a53f379 100644 --- a/pkg/sdk/poc/generator/db_struct.go +++ b/pkg/sdk/poc/generator/db_struct.go @@ -29,6 +29,10 @@ func (v *dbStruct) Text(dbName string) *dbStruct { return v.Field(dbName, "string") } +func (v *dbStruct) Time(dbName string) *dbStruct { + return v.Field(dbName, "time.Time") +} + func (v *dbStruct) OptionalText(dbName string) *dbStruct { return v.Field(dbName, "sql.NullString") } diff --git a/pkg/sdk/poc/generator/field_transformers.go b/pkg/sdk/poc/generator/field_transformers.go index 3c7f460233..c995f110ea 100644 --- a/pkg/sdk/poc/generator/field_transformers.go +++ b/pkg/sdk/poc/generator/field_transformers.go @@ -6,6 +6,25 @@ type FieldTransformer interface { Transform(f *Field) *Field } +func StaticOptions() *StaticTransformer { + return new(StaticTransformer) +} + +type StaticTransformer struct { + sqlPrefix string +} + +func (v *StaticTransformer) SQL(sqlPrefix string) *StaticTransformer { + v.sqlPrefix = sqlPrefix + return v +} + +func (v *StaticTransformer) Transform(f *Field) *Field { + addTagIfMissing(f.Tags, "ddl", "static") + addTagIfMissing(f.Tags, "sql", v.sqlPrefix) + return f +} + type KeywordTransformer struct { required bool sqlPrefix string diff --git a/pkg/sdk/poc/generator/keyword_builders.go b/pkg/sdk/poc/generator/keyword_builders.go index dadb98d34f..dc72e34b7d 100644 --- a/pkg/sdk/poc/generator/keyword_builders.go +++ b/pkg/sdk/poc/generator/keyword_builders.go @@ -117,3 +117,11 @@ func (v *QueryStruct) OptionalLimit() *QueryStruct { func (v *QueryStruct) OptionalCopyGrants() *QueryStruct { return v.OptionalSQL("COPY GRANTS") } + +func (v *QueryStruct) BodyWithPrecedingArrow() *QueryStruct { + return v.PredefinedQueryStructField("body", "string", ParameterOptions().NoEquals().NoQuotes().SQL("->").Required()) +} + +func (v *QueryStruct) OptionalSetBodyWithPrecedingArrow() *QueryStruct { + return v.PredefinedQueryStructField("SetBody", "*string", ParameterOptions().NoEquals().NoQuotes().SQL("SET BODY ->")) +} diff --git a/pkg/sdk/poc/generator/plain_struct.go b/pkg/sdk/poc/generator/plain_struct.go index f6fa3eb155..d0e529b38d 100644 --- a/pkg/sdk/poc/generator/plain_struct.go +++ b/pkg/sdk/poc/generator/plain_struct.go @@ -29,6 +29,10 @@ func (v *plainStruct) Text(name string) *plainStruct { return v.Field(name, "string") } +func (v *plainStruct) Time(name string) *plainStruct { + return v.Field(name, "time.Time") +} + func (v *plainStruct) OptionalText(name string) *plainStruct { return v.Field(name, "*string") } diff --git a/pkg/sdk/poc/main.go b/pkg/sdk/poc/main.go index acb572a984..d8cf6fcf89 100644 --- a/pkg/sdk/poc/main.go +++ b/pkg/sdk/poc/main.go @@ -28,6 +28,9 @@ var definitionMapping = map[string]*generator.Interface{ "procedures_def.go": sdk.ProceduresDef, "event_tables_def.go": sdk.EventTablesDef, "application_packages_def.go": sdk.ApplicationPackagesDef, + "storage_integration_def.go": sdk.StorageIntegrationDef, + "managed_accounts_def.go": sdk.ManagedAccountsDef, + "row_access_policies_def.go": sdk.RowAccessPoliciesDef, } func main() { diff --git a/pkg/sdk/resource_monitors.go b/pkg/sdk/resource_monitors.go index 1363f38b1e..16098547c8 100644 --- a/pkg/sdk/resource_monitors.go +++ b/pkg/sdk/resource_monitors.go @@ -288,7 +288,6 @@ type AlterResourceMonitorOptions struct { IfExists *bool `ddl:"keyword" sql:"IF EXISTS"` name AccountObjectIdentifier `ddl:"identifier"` Set *ResourceMonitorSet `ddl:"keyword" sql:"SET"` - NotifyUsers *NotifyUsers `ddl:"parameter,equals" sql:"NOTIFY_USERS"` Triggers []TriggerDefinition `ddl:"keyword,no_comma" sql:"TRIGGERS"` } @@ -300,11 +299,14 @@ func (opts *AlterResourceMonitorOptions) validate() error { if !ValidObjectIdentifier(opts.name) { errs = append(errs, ErrInvalidObjectIdentifier) } - if everyValueNil(opts.Set, opts.NotifyUsers, opts.Triggers) { - errs = append(errs, errAtLeastOneOf("AlterResourceMonitorOptions", "Set", "NotifyUsers", "Triggers")) + if everyValueNil(opts.Set, opts.Triggers) { + errs = append(errs, errAtLeastOneOf("AlterResourceMonitorOptions", "Set", "Triggers")) } - if valueSet(opts.Set) { - if (opts.Set.Frequency != nil && opts.Set.StartTimestamp == nil) || (opts.Set.Frequency == nil && opts.Set.StartTimestamp != nil) { + if set := opts.Set; valueSet(set) { + if everyValueNil(set.CreditQuota, set.Frequency, set.StartTimestamp, set.EndTimestamp, set.NotifyUsers) { + errs = append(errs, errAtLeastOneOf("ResourceMonitorSet", "CreditQuota", "Frequency", "StartTimestamp", "EndTimestamp", "NotifyUsers")) + } + if (set.Frequency != nil && set.StartTimestamp == nil) || (set.Frequency == nil && set.StartTimestamp != nil) { errs = append(errs, errors.New("must specify frequency and start time together")) } } @@ -330,10 +332,11 @@ func (v *resourceMonitors) Alter(ctx context.Context, id AccountObjectIdentifier type ResourceMonitorSet struct { // at least one - CreditQuota *int `ddl:"parameter,equals" sql:"CREDIT_QUOTA"` - Frequency *Frequency `ddl:"parameter,equals" sql:"FREQUENCY"` - StartTimestamp *string `ddl:"parameter,equals,single_quotes" sql:"START_TIMESTAMP"` - EndTimestamp *string `ddl:"parameter,equals,single_quotes" sql:"END_TIMESTAMP"` + CreditQuota *int `ddl:"parameter,equals" sql:"CREDIT_QUOTA"` + Frequency *Frequency `ddl:"parameter,equals" sql:"FREQUENCY"` + StartTimestamp *string `ddl:"parameter,equals,single_quotes" sql:"START_TIMESTAMP"` + EndTimestamp *string `ddl:"parameter,equals,single_quotes" sql:"END_TIMESTAMP"` + NotifyUsers *NotifyUsers `ddl:"parameter,equals" sql:"NOTIFY_USERS"` } // dropResourceMonitorOptions is based on https://docs.snowflake.com/en/sql-reference/sql/drop-resource-monitor. diff --git a/pkg/sdk/resource_monitors_test.go b/pkg/sdk/resource_monitors_test.go index 532820bac1..b44d0ec85b 100644 --- a/pkg/sdk/resource_monitors_test.go +++ b/pkg/sdk/resource_monitors_test.go @@ -62,7 +62,15 @@ func TestResourceMonitorAlter(t *testing.T) { opts := &AlterResourceMonitorOptions{ name: id, } - assertOptsInvalidJoinedErrors(t, opts, errAtLeastOneOf("AlterResourceMonitorOptions", "Set", "NotifyUsers", "Triggers")) + assertOptsInvalidJoinedErrors(t, opts, errAtLeastOneOf("AlterResourceMonitorOptions", "Set", "Triggers")) + }) + + t.Run("validation: no option for set provided", func(t *testing.T) { + opts := &AlterResourceMonitorOptions{ + name: id, + Set: &ResourceMonitorSet{}, + } + assertOptsInvalidJoinedErrors(t, opts, errAtLeastOneOf("ResourceMonitorSet", "CreditQuota", "Frequency", "StartTimestamp", "EndTimestamp", "NotifyUsers")) }) t.Run("with a single set", func(t *testing.T) { @@ -76,6 +84,21 @@ func TestResourceMonitorAlter(t *testing.T) { assertOptsValidAndSQLEquals(t, opts, "ALTER RESOURCE MONITOR %s SET CREDIT_QUOTA = %d", id.FullyQualifiedName(), *newCreditQuota) }) + t.Run("set notify users", func(t *testing.T) { + opts := &AlterResourceMonitorOptions{ + name: id, + Set: &ResourceMonitorSet{ + NotifyUsers: &NotifyUsers{ + Users: []NotifiedUser{ + {Name: "user1"}, + {Name: "user2"}, + }, + }, + }, + } + assertOptsValidAndSQLEquals(t, opts, "ALTER RESOURCE MONITOR %s SET NOTIFY_USERS = (\"user1\", \"user2\")", id.FullyQualifiedName()) + }) + t.Run("with a multitple set", func(t *testing.T) { newCreditQuota := Int(50) newFrequency := FrequencyYearly diff --git a/pkg/sdk/row_access_policies_def.go b/pkg/sdk/row_access_policies_def.go new file mode 100644 index 0000000000..cd6e24553f --- /dev/null +++ b/pkg/sdk/row_access_policies_def.go @@ -0,0 +1,111 @@ +package sdk + +import g "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk/poc/generator" + +//go:generate go run ./poc/main.go + +var rowAccessPolicyDbRow = g.DbStruct("rowAccessPolicyDBRow"). + Text("created_on"). + Text("name"). + Text("database_name"). + Text("schema_name"). + Text("kind"). + Text("owner"). + OptionalText("comment"). + Text("options"). + Text("owner_role_type") + +var rowAccessPolicy = g.PlainStruct("RowAccessPolicy"). + Text("CreatedOn"). + Text("Name"). + Text("DatabaseName"). + Text("SchemaName"). + Text("Kind"). + Text("Owner"). + Text("Comment"). + Text("Options"). + Text("OwnerRoleType") + +var RowAccessPoliciesDef = g.NewInterface( + "RowAccessPolicies", + "RowAccessPolicy", + g.KindOfT[SchemaObjectIdentifier](), +). + CreateOperation( + "https://docs.snowflake.com/en/sql-reference/sql/create-row-access-policy", + g.NewQueryStruct("CreateRowAccessPolicy"). + Create(). + OrReplace(). + SQL("ROW ACCESS POLICY"). + IfNotExists(). + Name(). + SQL("AS"). + ListQueryStructField( + "args", + g.NewQueryStruct("CreateRowAccessPolicyArgs"). + Text("Name", g.KeywordOptions().NoQuotes().Required()). + PredefinedQueryStructField("Type", "DataType", g.KeywordOptions().NoQuotes().Required()), + g.ParameterOptions().Parentheses().NoEquals().Required(), + ). + SQL("RETURNS BOOLEAN"). + BodyWithPrecedingArrow(). + OptionalComment(). + WithValidation(g.ValidIdentifier, "name"). + WithValidation(g.ValidateValueSet, "args"). + WithValidation(g.ValidateValueSet, "body"). + WithValidation(g.ConflictingFields, "OrReplace", "IfNotExists"), + ). + AlterOperation( + "https://docs.snowflake.com/en/sql-reference/sql/alter-row-access-policy", + g.NewQueryStruct("AlterRowAccessPolicy"). + Alter(). + SQL("ROW ACCESS POLICY"). + Name(). + OptionalIdentifier("RenameTo", g.KindOfT[SchemaObjectIdentifier](), g.IdentifierOptions().SQL("RENAME TO")). + OptionalSetBodyWithPrecedingArrow(). + OptionalSetTags(). + OptionalUnsetTags(). + OptionalTextAssignment("SET COMMENT", g.ParameterOptions().SingleQuotes()). + OptionalSQL("UNSET COMMENT"). + WithValidation(g.ValidIdentifier, "name"). + WithValidation(g.ExactlyOneValueSet, "RenameTo", "SetBody", "SetTags", "UnsetTags", "SetComment", "UnsetComment"), + ). + DropOperation( + "https://docs.snowflake.com/en/sql-reference/sql/drop-row-access-policy", + g.NewQueryStruct("DropRowAccessPolicy"). + Drop(). + SQL("ROW ACCESS POLICY"). + IfExists(). + Name(). + WithValidation(g.ValidIdentifier, "name"), + ). + ShowOperation( + "https://docs.snowflake.com/en/sql-reference/sql/show-row-access-policies", + rowAccessPolicyDbRow, + rowAccessPolicy, + g.NewQueryStruct("ShowRowAccessPolicies"). + Show(). + SQL("ROW ACCESS POLICIES"). + OptionalLike(). + OptionalIn(), + ). + ShowByIdOperation(). + DescribeOperation( + g.DescriptionMappingKindSingleValue, + "https://docs.snowflake.com/en/sql-reference/sql/desc-row-access-policy", + g.DbStruct("describeRowAccessPolicyDBRow"). + Field("name", "string"). + Field("signature", "string"). + Field("return_type", "string"). + Field("body", "string"), + g.PlainStruct("RowAccessPolicyDescription"). + Field("Name", "string"). + Field("Signature", "string"). + Field("ReturnType", "string"). + Field("Body", "string"), + g.NewQueryStruct("DescribeRowAccessPolicy"). + Describe(). + SQL("ROW ACCESS POLICY"). + Name(). + WithValidation(g.ValidIdentifier, "name"), + ) diff --git a/pkg/sdk/row_access_policies_dto_builders_gen.go b/pkg/sdk/row_access_policies_dto_builders_gen.go new file mode 100644 index 0000000000..db5e787d4b --- /dev/null +++ b/pkg/sdk/row_access_policies_dto_builders_gen.go @@ -0,0 +1,119 @@ +// Code generated by dto builder generator; DO NOT EDIT. + +package sdk + +import () + +func NewCreateRowAccessPolicyRequest( + name SchemaObjectIdentifier, + args []CreateRowAccessPolicyArgsRequest, + body string, +) *CreateRowAccessPolicyRequest { + s := CreateRowAccessPolicyRequest{} + s.name = name + s.args = args + s.body = body + return &s +} + +func (r *CreateRowAccessPolicyRequest) GetName() SchemaObjectIdentifier { + return r.name +} + +func (s *CreateRowAccessPolicyRequest) WithOrReplace(OrReplace *bool) *CreateRowAccessPolicyRequest { + s.OrReplace = OrReplace + return s +} + +func (s *CreateRowAccessPolicyRequest) WithIfNotExists(IfNotExists *bool) *CreateRowAccessPolicyRequest { + s.IfNotExists = IfNotExists + return s +} + +func (s *CreateRowAccessPolicyRequest) WithComment(Comment *string) *CreateRowAccessPolicyRequest { + s.Comment = Comment + return s +} + +func NewCreateRowAccessPolicyArgsRequest( + Name string, + Type DataType, +) *CreateRowAccessPolicyArgsRequest { + s := CreateRowAccessPolicyArgsRequest{} + s.Name = Name + s.Type = Type + return &s +} + +func NewAlterRowAccessPolicyRequest( + name SchemaObjectIdentifier, +) *AlterRowAccessPolicyRequest { + s := AlterRowAccessPolicyRequest{} + s.name = name + return &s +} + +func (s *AlterRowAccessPolicyRequest) WithRenameTo(RenameTo *SchemaObjectIdentifier) *AlterRowAccessPolicyRequest { + s.RenameTo = RenameTo + return s +} + +func (s *AlterRowAccessPolicyRequest) WithSetBody(SetBody *string) *AlterRowAccessPolicyRequest { + s.SetBody = SetBody + return s +} + +func (s *AlterRowAccessPolicyRequest) WithSetTags(SetTags []TagAssociation) *AlterRowAccessPolicyRequest { + s.SetTags = SetTags + return s +} + +func (s *AlterRowAccessPolicyRequest) WithUnsetTags(UnsetTags []ObjectIdentifier) *AlterRowAccessPolicyRequest { + s.UnsetTags = UnsetTags + return s +} + +func (s *AlterRowAccessPolicyRequest) WithSetComment(SetComment *string) *AlterRowAccessPolicyRequest { + s.SetComment = SetComment + return s +} + +func (s *AlterRowAccessPolicyRequest) WithUnsetComment(UnsetComment *bool) *AlterRowAccessPolicyRequest { + s.UnsetComment = UnsetComment + return s +} + +func NewDropRowAccessPolicyRequest( + name SchemaObjectIdentifier, +) *DropRowAccessPolicyRequest { + s := DropRowAccessPolicyRequest{} + s.name = name + return &s +} + +func (s *DropRowAccessPolicyRequest) WithIfExists(IfExists *bool) *DropRowAccessPolicyRequest { + s.IfExists = IfExists + return s +} + +func NewShowRowAccessPolicyRequest() *ShowRowAccessPolicyRequest { + return &ShowRowAccessPolicyRequest{} +} + +func (s *ShowRowAccessPolicyRequest) WithLike(Like *Like) *ShowRowAccessPolicyRequest { + s.Like = Like + return s +} + +func (s *ShowRowAccessPolicyRequest) WithIn(In *In) *ShowRowAccessPolicyRequest { + s.In = In + return s +} + +func NewDescribeRowAccessPolicyRequest( + name SchemaObjectIdentifier, +) *DescribeRowAccessPolicyRequest { + s := DescribeRowAccessPolicyRequest{} + s.name = name + return &s +} diff --git a/pkg/sdk/row_access_policies_dto_gen.go b/pkg/sdk/row_access_policies_dto_gen.go new file mode 100644 index 0000000000..5b028c12d0 --- /dev/null +++ b/pkg/sdk/row_access_policies_dto_gen.go @@ -0,0 +1,49 @@ +package sdk + +//go:generate go run ./dto-builder-generator/main.go + +var ( + _ optionsProvider[CreateRowAccessPolicyOptions] = new(CreateRowAccessPolicyRequest) + _ optionsProvider[AlterRowAccessPolicyOptions] = new(AlterRowAccessPolicyRequest) + _ optionsProvider[DropRowAccessPolicyOptions] = new(DropRowAccessPolicyRequest) + _ optionsProvider[ShowRowAccessPolicyOptions] = new(ShowRowAccessPolicyRequest) + _ optionsProvider[DescribeRowAccessPolicyOptions] = new(DescribeRowAccessPolicyRequest) +) + +type CreateRowAccessPolicyRequest struct { + OrReplace *bool + IfNotExists *bool + name SchemaObjectIdentifier // required + args []CreateRowAccessPolicyArgsRequest // required + body string // required + Comment *string +} + +type CreateRowAccessPolicyArgsRequest struct { + Name string // required + Type DataType // required +} + +type AlterRowAccessPolicyRequest struct { + name SchemaObjectIdentifier // required + RenameTo *SchemaObjectIdentifier + SetBody *string + SetTags []TagAssociation + UnsetTags []ObjectIdentifier + SetComment *string + UnsetComment *bool +} + +type DropRowAccessPolicyRequest struct { + IfExists *bool + name SchemaObjectIdentifier // required +} + +type ShowRowAccessPolicyRequest struct { + Like *Like + In *In +} + +type DescribeRowAccessPolicyRequest struct { + name SchemaObjectIdentifier // required +} diff --git a/pkg/sdk/row_access_policies_gen.go b/pkg/sdk/row_access_policies_gen.go new file mode 100644 index 0000000000..7b99767eb3 --- /dev/null +++ b/pkg/sdk/row_access_policies_gen.go @@ -0,0 +1,112 @@ +package sdk + +import ( + "context" + "database/sql" +) + +type RowAccessPolicies interface { + Create(ctx context.Context, request *CreateRowAccessPolicyRequest) error + Alter(ctx context.Context, request *AlterRowAccessPolicyRequest) error + Drop(ctx context.Context, request *DropRowAccessPolicyRequest) error + Show(ctx context.Context, request *ShowRowAccessPolicyRequest) ([]RowAccessPolicy, error) + ShowByID(ctx context.Context, id SchemaObjectIdentifier) (*RowAccessPolicy, error) + Describe(ctx context.Context, id SchemaObjectIdentifier) (*RowAccessPolicyDescription, error) +} + +// CreateRowAccessPolicyOptions is based on https://docs.snowflake.com/en/sql-reference/sql/create-row-access-policy. +type CreateRowAccessPolicyOptions struct { + create bool `ddl:"static" sql:"CREATE"` + OrReplace *bool `ddl:"keyword" sql:"OR REPLACE"` + rowAccessPolicy bool `ddl:"static" sql:"ROW ACCESS POLICY"` + IfNotExists *bool `ddl:"keyword" sql:"IF NOT EXISTS"` + name SchemaObjectIdentifier `ddl:"identifier"` + as bool `ddl:"static" sql:"AS"` + args []CreateRowAccessPolicyArgs `ddl:"parameter,parentheses,no_equals"` + returnsBoolean bool `ddl:"static" sql:"RETURNS BOOLEAN"` + body string `ddl:"parameter,no_quotes,no_equals" sql:"->"` + Comment *string `ddl:"parameter,single_quotes" sql:"COMMENT"` +} + +type CreateRowAccessPolicyArgs struct { + Name string `ddl:"keyword,no_quotes"` + Type DataType `ddl:"keyword,no_quotes"` +} + +// AlterRowAccessPolicyOptions is based on https://docs.snowflake.com/en/sql-reference/sql/alter-row-access-policy. +type AlterRowAccessPolicyOptions struct { + alter bool `ddl:"static" sql:"ALTER"` + rowAccessPolicy bool `ddl:"static" sql:"ROW ACCESS POLICY"` + name SchemaObjectIdentifier `ddl:"identifier"` + RenameTo *SchemaObjectIdentifier `ddl:"identifier" sql:"RENAME TO"` + SetBody *string `ddl:"parameter,no_quotes,no_equals" sql:"SET BODY ->"` + SetTags []TagAssociation `ddl:"keyword" sql:"SET TAG"` + UnsetTags []ObjectIdentifier `ddl:"keyword" sql:"UNSET TAG"` + SetComment *string `ddl:"parameter,single_quotes" sql:"SET COMMENT"` + UnsetComment *bool `ddl:"keyword" sql:"UNSET COMMENT"` +} + +// DropRowAccessPolicyOptions is based on https://docs.snowflake.com/en/sql-reference/sql/drop-row-access-policy. +type DropRowAccessPolicyOptions struct { + drop bool `ddl:"static" sql:"DROP"` + rowAccessPolicy bool `ddl:"static" sql:"ROW ACCESS POLICY"` + IfExists *bool `ddl:"keyword" sql:"IF EXISTS"` + name SchemaObjectIdentifier `ddl:"identifier"` +} + +// ShowRowAccessPolicyOptions is based on https://docs.snowflake.com/en/sql-reference/sql/show-row-access-policies. +type ShowRowAccessPolicyOptions struct { + show bool `ddl:"static" sql:"SHOW"` + rowAccessPolicies bool `ddl:"static" sql:"ROW ACCESS POLICIES"` + Like *Like `ddl:"keyword" sql:"LIKE"` + In *In `ddl:"keyword" sql:"IN"` +} + +type rowAccessPolicyDBRow struct { + CreatedOn string `db:"created_on"` + Name string `db:"name"` + DatabaseName string `db:"database_name"` + SchemaName string `db:"schema_name"` + Kind string `db:"kind"` + Owner string `db:"owner"` + Comment sql.NullString `db:"comment"` + Options string `db:"options"` + OwnerRoleType string `db:"owner_role_type"` +} + +type RowAccessPolicy struct { + CreatedOn string + Name string + DatabaseName string + SchemaName string + Kind string + Owner string + Comment string + Options string + OwnerRoleType string +} + +func (v *RowAccessPolicy) ID() SchemaObjectIdentifier { + return NewSchemaObjectIdentifier(v.DatabaseName, v.SchemaName, v.Name) +} + +// DescribeRowAccessPolicyOptions is based on https://docs.snowflake.com/en/sql-reference/sql/desc-row-access-policy. +type DescribeRowAccessPolicyOptions struct { + describe bool `ddl:"static" sql:"DESCRIBE"` + rowAccessPolicy bool `ddl:"static" sql:"ROW ACCESS POLICY"` + name SchemaObjectIdentifier `ddl:"identifier"` +} + +type describeRowAccessPolicyDBRow struct { + Name string `db:"name"` + Signature string `db:"signature"` + ReturnType string `db:"return_type"` + Body string `db:"body"` +} + +type RowAccessPolicyDescription struct { + Name string + Signature string + ReturnType string + Body string +} diff --git a/pkg/sdk/row_access_policies_gen_test.go b/pkg/sdk/row_access_policies_gen_test.go new file mode 100644 index 0000000000..89b5ff3886 --- /dev/null +++ b/pkg/sdk/row_access_policies_gen_test.go @@ -0,0 +1,245 @@ +package sdk + +import "testing" + +func TestRowAccessPolicies_Create(t *testing.T) { + id := RandomSchemaObjectIdentifier() + + // Minimal valid CreateRowAccessPolicyOptions + defaultOpts := func() *CreateRowAccessPolicyOptions { + return &CreateRowAccessPolicyOptions{ + name: id, + args: []CreateRowAccessPolicyArgs{{ + Name: "n", + Type: DataTypeVARCHAR, + }}, + body: "true", + } + } + + t.Run("validation: nil options", func(t *testing.T) { + var opts *CreateRowAccessPolicyOptions = nil + assertOptsInvalidJoinedErrors(t, opts, ErrNilOptions) + }) + + t.Run("validation: valid identifier for [opts.name]", func(t *testing.T) { + opts := defaultOpts() + opts.name = NewSchemaObjectIdentifier("", "", "") + assertOptsInvalidJoinedErrors(t, opts, ErrInvalidObjectIdentifier) + }) + + t.Run("validation: conflicting fields for [opts.OrReplace opts.IfNotExists]", func(t *testing.T) { + opts := defaultOpts() + opts.OrReplace = Bool(true) + opts.IfNotExists = Bool(true) + assertOptsInvalidJoinedErrors(t, opts, errOneOf("CreateRowAccessPolicyOptions", "OrReplace", "IfNotExists")) + }) + + t.Run("validation: [opts.args] should be set", func(t *testing.T) { + opts := defaultOpts() + opts.args = []CreateRowAccessPolicyArgs{} + assertOptsInvalidJoinedErrors(t, opts, errNotSet("CreateRowAccessPolicyOptions", "args")) + }) + + t.Run("validation: [opts.body] should be set", func(t *testing.T) { + opts := defaultOpts() + opts.body = "" + assertOptsInvalidJoinedErrors(t, opts, errNotSet("CreateRowAccessPolicyOptions", "body")) + }) + + 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()) + }) + + t.Run("two parameters", func(t *testing.T) { + opts := defaultOpts() + opts.args = []CreateRowAccessPolicyArgs{{ + Name: "n", + Type: DataTypeVARCHAR, + }, { + Name: "h", + Type: DataTypeVARCHAR, + }} + 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()) + }) +} + +func TestRowAccessPolicies_Alter(t *testing.T) { + id := RandomSchemaObjectIdentifier() + + // Minimal valid AlterRowAccessPolicyOptions + defaultOpts := func() *AlterRowAccessPolicyOptions { + return &AlterRowAccessPolicyOptions{ + name: id, + } + } + + t.Run("validation: nil options", func(t *testing.T) { + var opts *AlterRowAccessPolicyOptions = nil + assertOptsInvalidJoinedErrors(t, opts, ErrNilOptions) + }) + + t.Run("validation: valid identifier for [opts.name]", func(t *testing.T) { + opts := defaultOpts() + opts.name = NewSchemaObjectIdentifier("", "", "") + assertOptsInvalidJoinedErrors(t, opts, ErrInvalidObjectIdentifier) + }) + + t.Run("validation: exactly one field from [opts.RenameTo opts.SetBody opts.SetTags opts.UnsetTags opts.SetComment opts.UnsetComment] should be present", func(t *testing.T) { + opts := defaultOpts() + assertOptsInvalidJoinedErrors(t, opts, errExactlyOneOf("AlterRowAccessPolicyOptions", "RenameTo", "SetBody", "SetTags", "UnsetTags", "SetComment", "UnsetComment")) + }) + + t.Run("validation: exactly one field from [opts.RenameTo opts.SetBody opts.SetTags opts.UnsetTags opts.SetComment opts.UnsetComment] should be present - more present", func(t *testing.T) { + opts := defaultOpts() + opts.SetComment = String("comment") + opts.UnsetComment = Bool(true) + assertOptsInvalidJoinedErrors(t, opts, errExactlyOneOf("AlterRowAccessPolicyOptions", "RenameTo", "SetBody", "SetTags", "UnsetTags", "SetComment", "UnsetComment")) + }) + + t.Run("rename", func(t *testing.T) { + newId := RandomSchemaObjectIdentifier() + + opts := defaultOpts() + opts.RenameTo = &newId + assertOptsValidAndSQLEquals(t, opts, "ALTER ROW ACCESS POLICY %s RENAME TO %s", id.FullyQualifiedName(), newId.FullyQualifiedName()) + }) + + t.Run("set body", func(t *testing.T) { + opts := defaultOpts() + opts.SetBody = String("true") + assertOptsValidAndSQLEquals(t, opts, "ALTER ROW ACCESS POLICY %s SET BODY -> true", id.FullyQualifiedName()) + }) + + t.Run("set comment", func(t *testing.T) { + opts := defaultOpts() + opts.SetComment = String("comment") + assertOptsValidAndSQLEquals(t, opts, "ALTER ROW ACCESS POLICY %s SET COMMENT = 'comment'", id.FullyQualifiedName()) + }) + + t.Run("unset comment", func(t *testing.T) { + opts := defaultOpts() + opts.UnsetComment = Bool(true) + assertOptsValidAndSQLEquals(t, opts, "ALTER ROW ACCESS POLICY %s UNSET COMMENT", id.FullyQualifiedName()) + }) + + t.Run("set tags", func(t *testing.T) { + opts := defaultOpts() + opts.SetTags = []TagAssociation{ + { + Name: NewAccountObjectIdentifier("tag1"), + Value: "value1", + }, + { + Name: NewAccountObjectIdentifier("tag2"), + Value: "value2", + }, + } + assertOptsValidAndSQLEquals(t, opts, `ALTER ROW ACCESS POLICY %s SET TAG "tag1" = 'value1', "tag2" = 'value2'`, id.FullyQualifiedName()) + }) + + t.Run("unset tags", func(t *testing.T) { + opts := defaultOpts() + opts.UnsetTags = []ObjectIdentifier{ + NewAccountObjectIdentifier("tag1"), + NewAccountObjectIdentifier("tag2"), + } + assertOptsValidAndSQLEquals(t, opts, `ALTER ROW ACCESS POLICY %s UNSET TAG "tag1", "tag2"`, id.FullyQualifiedName()) + }) +} + +func TestRowAccessPolicies_Drop(t *testing.T) { + id := RandomSchemaObjectIdentifier() + + // Minimal valid DropRowAccessPolicyOptions + defaultOpts := func() *DropRowAccessPolicyOptions { + return &DropRowAccessPolicyOptions{ + name: id, + } + } + + t.Run("validation: nil options", func(t *testing.T) { + var opts *DropRowAccessPolicyOptions = nil + assertOptsInvalidJoinedErrors(t, opts, ErrNilOptions) + }) + + t.Run("validation: valid identifier for [opts.name]", func(t *testing.T) { + opts := defaultOpts() + opts.name = NewSchemaObjectIdentifier("", "", "") + assertOptsInvalidJoinedErrors(t, opts, ErrInvalidObjectIdentifier) + }) + + t.Run("basic", func(t *testing.T) { + opts := defaultOpts() + assertOptsValidAndSQLEquals(t, opts, "DROP ROW ACCESS POLICY %s", id.FullyQualifiedName()) + }) + + t.Run("all options", func(t *testing.T) { + opts := defaultOpts() + opts.IfExists = Bool(true) + assertOptsValidAndSQLEquals(t, opts, "DROP ROW ACCESS POLICY IF EXISTS %s", id.FullyQualifiedName()) + }) +} + +func TestRowAccessPolicies_Show(t *testing.T) { + // Minimal valid ShowRowAccessPolicyOptions + defaultOpts := func() *ShowRowAccessPolicyOptions { + return &ShowRowAccessPolicyOptions{} + } + + t.Run("validation: nil options", func(t *testing.T) { + var opts *ShowRowAccessPolicyOptions = nil + assertOptsInvalidJoinedErrors(t, opts, ErrNilOptions) + }) + + t.Run("basic", func(t *testing.T) { + opts := defaultOpts() + assertOptsValidAndSQLEquals(t, opts, "SHOW ROW ACCESS POLICIES") + }) + + t.Run("all options", func(t *testing.T) { + opts := defaultOpts() + opts.Like = &Like{ + Pattern: String("myaccount"), + } + opts.In = &In{ + Account: Bool(true), + } + assertOptsValidAndSQLEquals(t, opts, "SHOW ROW ACCESS POLICIES LIKE 'myaccount' IN ACCOUNT") + }) +} + +func TestRowAccessPolicies_Describe(t *testing.T) { + id := RandomSchemaObjectIdentifier() + + // Minimal valid DescribeRowAccessPolicyOptions + defaultOpts := func() *DescribeRowAccessPolicyOptions { + return &DescribeRowAccessPolicyOptions{ + name: id, + } + } + + t.Run("validation: nil options", func(t *testing.T) { + var opts *DescribeRowAccessPolicyOptions = nil + assertOptsInvalidJoinedErrors(t, opts, ErrNilOptions) + }) + + t.Run("validation: valid identifier for [opts.name]", func(t *testing.T) { + opts := defaultOpts() + opts.name = NewSchemaObjectIdentifier("", "", "") + assertOptsInvalidJoinedErrors(t, opts, ErrInvalidObjectIdentifier) + }) + + t.Run("basic", func(t *testing.T) { + opts := defaultOpts() + assertOptsValidAndSQLEquals(t, opts, "DESCRIBE ROW ACCESS POLICY %s", id.FullyQualifiedName()) + }) +} diff --git a/pkg/sdk/row_access_policies_impl_gen.go b/pkg/sdk/row_access_policies_impl_gen.go new file mode 100644 index 0000000000..0a020b2e40 --- /dev/null +++ b/pkg/sdk/row_access_policies_impl_gen.go @@ -0,0 +1,140 @@ +package sdk + +import ( + "context" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk/internal/collections" +) + +var _ RowAccessPolicies = (*rowAccessPolicies)(nil) + +type rowAccessPolicies struct { + client *Client +} + +func (v *rowAccessPolicies) Create(ctx context.Context, request *CreateRowAccessPolicyRequest) error { + opts := request.toOpts() + return validateAndExec(v.client, ctx, opts) +} + +func (v *rowAccessPolicies) Alter(ctx context.Context, request *AlterRowAccessPolicyRequest) error { + opts := request.toOpts() + return validateAndExec(v.client, ctx, opts) +} + +func (v *rowAccessPolicies) Drop(ctx context.Context, request *DropRowAccessPolicyRequest) error { + opts := request.toOpts() + return validateAndExec(v.client, ctx, opts) +} + +func (v *rowAccessPolicies) Show(ctx context.Context, request *ShowRowAccessPolicyRequest) ([]RowAccessPolicy, error) { + opts := request.toOpts() + dbRows, err := validateAndQuery[rowAccessPolicyDBRow](v.client, ctx, opts) + if err != nil { + return nil, err + } + resultList := convertRows[rowAccessPolicyDBRow, RowAccessPolicy](dbRows) + return resultList, nil +} + +func (v *rowAccessPolicies) ShowByID(ctx context.Context, id SchemaObjectIdentifier) (*RowAccessPolicy, error) { + request := NewShowRowAccessPolicyRequest().WithIn(&In{Schema: NewDatabaseObjectIdentifier(id.DatabaseName(), id.SchemaName())}).WithLike(&Like{String(id.Name())}) + rowAccessPolicies, err := v.Show(ctx, request) + if err != nil { + return nil, err + } + return collections.FindOne(rowAccessPolicies, func(r RowAccessPolicy) bool { return r.Name == id.Name() }) +} + +func (v *rowAccessPolicies) Describe(ctx context.Context, id SchemaObjectIdentifier) (*RowAccessPolicyDescription, error) { + opts := &DescribeRowAccessPolicyOptions{ + name: id, + } + result, err := validateAndQueryOne[describeRowAccessPolicyDBRow](v.client, ctx, opts) + if err != nil { + return nil, err + } + return result.convert(), nil +} + +func (r *CreateRowAccessPolicyRequest) toOpts() *CreateRowAccessPolicyOptions { + opts := &CreateRowAccessPolicyOptions{ + OrReplace: r.OrReplace, + IfNotExists: r.IfNotExists, + name: r.name, + + body: r.body, + Comment: r.Comment, + } + if r.args != nil { + s := make([]CreateRowAccessPolicyArgs, len(r.args)) + for i, v := range r.args { + s[i] = CreateRowAccessPolicyArgs(v) + } + opts.args = s + } + return opts +} + +func (r *AlterRowAccessPolicyRequest) toOpts() *AlterRowAccessPolicyOptions { + opts := &AlterRowAccessPolicyOptions{ + name: r.name, + RenameTo: r.RenameTo, + SetBody: r.SetBody, + SetTags: r.SetTags, + UnsetTags: r.UnsetTags, + SetComment: r.SetComment, + UnsetComment: r.UnsetComment, + } + return opts +} + +func (r *DropRowAccessPolicyRequest) toOpts() *DropRowAccessPolicyOptions { + opts := &DropRowAccessPolicyOptions{ + IfExists: r.IfExists, + name: r.name, + } + return opts +} + +func (r *ShowRowAccessPolicyRequest) toOpts() *ShowRowAccessPolicyOptions { + opts := &ShowRowAccessPolicyOptions{ + Like: r.Like, + In: r.In, + } + return opts +} + +func (r rowAccessPolicyDBRow) convert() *RowAccessPolicy { + rowAccessPolicy := &RowAccessPolicy{ + CreatedOn: r.CreatedOn, + Name: r.Name, + DatabaseName: r.DatabaseName, + SchemaName: r.SchemaName, + Kind: r.Kind, + Owner: r.Owner, + Options: r.Options, + OwnerRoleType: r.OwnerRoleType, + } + if r.Comment.Valid { + rowAccessPolicy.Comment = r.Comment.String + } + return rowAccessPolicy +} + +func (r *DescribeRowAccessPolicyRequest) toOpts() *DescribeRowAccessPolicyOptions { + opts := &DescribeRowAccessPolicyOptions{ + name: r.name, + } + return opts +} + +func (r describeRowAccessPolicyDBRow) convert() *RowAccessPolicyDescription { + rowAccessPolicyDescription := &RowAccessPolicyDescription{ + Name: r.Name, + Signature: r.Signature, + ReturnType: r.ReturnType, + Body: r.Body, + } + return rowAccessPolicyDescription +} diff --git a/pkg/sdk/row_access_policies_validations_gen.go b/pkg/sdk/row_access_policies_validations_gen.go new file mode 100644 index 0000000000..87ebb06fc7 --- /dev/null +++ b/pkg/sdk/row_access_policies_validations_gen.go @@ -0,0 +1,73 @@ +package sdk + +var ( + _ validatable = new(CreateRowAccessPolicyOptions) + _ validatable = new(AlterRowAccessPolicyOptions) + _ validatable = new(DropRowAccessPolicyOptions) + _ validatable = new(ShowRowAccessPolicyOptions) + _ validatable = new(DescribeRowAccessPolicyOptions) +) + +func (opts *CreateRowAccessPolicyOptions) validate() error { + if opts == nil { + return ErrNilOptions + } + var errs []error + if !ValidObjectIdentifier(opts.name) { + errs = append(errs, ErrInvalidObjectIdentifier) + } + if !valueSet(opts.args) { + errs = append(errs, errNotSet("CreateRowAccessPolicyOptions", "args")) + } + if !valueSet(opts.body) { + errs = append(errs, errNotSet("CreateRowAccessPolicyOptions", "body")) + } + if everyValueSet(opts.OrReplace, opts.IfNotExists) { + errs = append(errs, errOneOf("CreateRowAccessPolicyOptions", "OrReplace", "IfNotExists")) + } + return JoinErrors(errs...) +} + +func (opts *AlterRowAccessPolicyOptions) validate() error { + if opts == nil { + return ErrNilOptions + } + var errs []error + if !ValidObjectIdentifier(opts.name) { + errs = append(errs, ErrInvalidObjectIdentifier) + } + if !exactlyOneValueSet(opts.RenameTo, opts.SetBody, opts.SetTags, opts.UnsetTags, opts.SetComment, opts.UnsetComment) { + errs = append(errs, errExactlyOneOf("AlterRowAccessPolicyOptions", "RenameTo", "SetBody", "SetTags", "UnsetTags", "SetComment", "UnsetComment")) + } + return JoinErrors(errs...) +} + +func (opts *DropRowAccessPolicyOptions) validate() error { + if opts == nil { + return ErrNilOptions + } + var errs []error + if !ValidObjectIdentifier(opts.name) { + errs = append(errs, ErrInvalidObjectIdentifier) + } + return JoinErrors(errs...) +} + +func (opts *ShowRowAccessPolicyOptions) validate() error { + if opts == nil { + return ErrNilOptions + } + var errs []error + return JoinErrors(errs...) +} + +func (opts *DescribeRowAccessPolicyOptions) validate() error { + if opts == nil { + return ErrNilOptions + } + var errs []error + if !ValidObjectIdentifier(opts.name) { + errs = append(errs, ErrInvalidObjectIdentifier) + } + return JoinErrors(errs...) +} diff --git a/pkg/sdk/storage_integration_def.go b/pkg/sdk/storage_integration_def.go new file mode 100644 index 0000000000..e626a6e325 --- /dev/null +++ b/pkg/sdk/storage_integration_def.go @@ -0,0 +1,145 @@ +package sdk + +import g "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk/poc/generator" + +//go:generate go run ./poc/main.go + +var StorageLocationDef = g.NewQueryStruct("StorageLocation").Text("Path", g.KeywordOptions().SingleQuotes().Required()) + +var StorageIntegrationDef = g.NewInterface( + "StorageIntegrations", + "StorageIntegration", + g.KindOfT[AccountObjectIdentifier](), +). + CreateOperation( + "https://docs.snowflake.com/en/sql-reference/sql/create-storage-integration", + g.NewQueryStruct("CreateStorageIntegration"). + Create(). + OrReplace(). + SQL("STORAGE INTEGRATION"). + IfNotExists(). + Name(). + PredefinedQueryStructField("externalStageType", "string", g.StaticOptions().SQL("TYPE = EXTERNAL_STAGE")). + OptionalQueryStructField( + "S3StorageProviderParams", + g.NewQueryStruct("S3StorageParams"). + PredefinedQueryStructField("storageProvider", "string", g.StaticOptions().SQL("STORAGE_PROVIDER = 'S3'")). + TextAssignment("STORAGE_AWS_ROLE_ARN", g.ParameterOptions().SingleQuotes().Required()). + OptionalTextAssignment("STORAGE_AWS_OBJECT_ACL", g.ParameterOptions().SingleQuotes()), + g.KeywordOptions(), + ). + OptionalQueryStructField( + "GCSStorageProviderParams", + g.NewQueryStruct("GCSStorageParams"). + PredefinedQueryStructField("storageProvider", "string", g.StaticOptions().SQL("STORAGE_PROVIDER = 'GCS'")), + g.KeywordOptions(), + ). + OptionalQueryStructField( + "AzureStorageProviderParams", + g.NewQueryStruct("AzureStorageParams"). + PredefinedQueryStructField("storageProvider", "string", g.StaticOptions().SQL("STORAGE_PROVIDER = 'AZURE'")). + OptionalTextAssignment("AZURE_TENANT_ID", g.ParameterOptions().SingleQuotes().Required()), + g.KeywordOptions(), + ). + BooleanAssignment("ENABLED", g.ParameterOptions().Required()). + ListAssignment("STORAGE_ALLOWED_LOCATIONS", "StorageLocation", g.ParameterOptions().Parentheses().Required()). + ListAssignment("STORAGE_BLOCKED_LOCATIONS", "StorageLocation", g.ParameterOptions().Parentheses()). + OptionalComment(). + WithValidation(g.ValidIdentifier, "name"). + WithValidation(g.ConflictingFields, "IfNotExists", "OrReplace"). + WithValidation(g.ExactlyOneValueSet, "S3StorageProviderParams", "GCSStorageProviderParams", "AzureStorageProviderParams"), + StorageLocationDef, + ). + AlterOperation( + "https://docs.snowflake.com/en/sql-reference/sql/alter-storage-integration", + g.NewQueryStruct("AlterStorageIntegration"). + Alter(). + SQL("STORAGE INTEGRATION"). + IfExists(). + Name(). + OptionalQueryStructField( + "Set", + g.NewQueryStruct("StorageIntegrationSet"). + OptionalQueryStructField( + "SetS3Params", + g.NewQueryStruct("SetS3StorageParams"). + TextAssignment("STORAGE_AWS_ROLE_ARN", g.ParameterOptions().SingleQuotes().Required()). + OptionalTextAssignment("STORAGE_AWS_OBJECT_ACL", g.ParameterOptions().SingleQuotes()), + g.KeywordOptions(), + ). + OptionalQueryStructField( + "SetAzureParams", + g.NewQueryStruct("SetAzureStorageParams"). + TextAssignment("AZURE_TENANT_ID", g.ParameterOptions().SingleQuotes().Required()), + g.KeywordOptions(), + ). + BooleanAssignment("ENABLED", g.ParameterOptions()). + ListAssignment("STORAGE_ALLOWED_LOCATIONS", "StorageLocation", g.ParameterOptions().Parentheses()). + ListAssignment("STORAGE_BLOCKED_LOCATIONS", "StorageLocation", g.ParameterOptions().Parentheses()). + OptionalComment(), + g.KeywordOptions().SQL("SET"), + ). + OptionalQueryStructField( + "Unset", + g.NewQueryStruct("StorageIntegrationUnset"). + OptionalSQL("ENABLED"). + OptionalSQL("STORAGE_BLOCKED_LOCATIONS"). + OptionalSQL("COMMENT"), + g.ListOptions().SQL("UNSET"), + ). + OptionalSetTags(). + OptionalUnsetTags(). + WithValidation(g.ValidIdentifier, "name"). + WithValidation(g.ConflictingFields, "IfExists", "UnsetTags"). + WithValidation(g.ExactlyOneValueSet, "Set", "Unset", "SetTags", "UnsetTags"), + ). + DropOperation( + "https://docs.snowflake.com/en/sql-reference/sql/drop-integration", + g.NewQueryStruct("DropStorageIntegration"). + Drop(). + SQL("STORAGE INTEGRATION"). + IfExists(). + Name(). + WithValidation(g.ValidIdentifier, "name"), + ). + ShowOperation( + "https://docs.snowflake.com/en/sql-reference/sql/show-integrations", + g.DbStruct("showStorageIntegrationsDbRow"). + Text("name"). + Text("type"). + Text("category"). + Bool("enabled"). + Text("comment"). + Time("created_on"), + g.PlainStruct("StorageIntegration"). + Text("Name"). + Text("StorageType"). + Text("Category"). + Bool("Enabled"). + Text("Comment"). + Time("CreatedOn"), + g.NewQueryStruct("ShowStorageIntegrations"). + Show(). + SQL("STORAGE INTEGRATIONS"). + OptionalLike(), + ). + ShowByIdOperation(). + DescribeOperation( + g.DescriptionMappingKindSlice, + "https://docs.snowflake.com/en/sql-reference/sql/desc-integration", + g.DbStruct("descStorageIntegrationsDbRow"). + Text("property"). + Text("property_type"). + Text("property_value"). + Text("property_default"), + g.PlainStruct("StorageIntegrationProperty"). + Text("Name"). + Text("Type"). + Text("Value"). + Text("Default"), + g.NewQueryStruct("DescribeStorageIntegration"). + Describe(). + SQL("STORAGE INTEGRATION"). + Name(). + WithValidation(g.ValidIdentifier, "name"), + ) diff --git a/pkg/sdk/storage_integration_dto_builders_gen.go b/pkg/sdk/storage_integration_dto_builders_gen.go new file mode 100644 index 0000000000..211c61a426 --- /dev/null +++ b/pkg/sdk/storage_integration_dto_builders_gen.go @@ -0,0 +1,214 @@ +// Code generated by dto builder generator; DO NOT EDIT. + +package sdk + +import () + +func NewCreateStorageIntegrationRequest( + name AccountObjectIdentifier, + Enabled bool, + StorageAllowedLocations []StorageLocation, +) *CreateStorageIntegrationRequest { + s := CreateStorageIntegrationRequest{} + s.name = name + s.Enabled = Enabled + s.StorageAllowedLocations = StorageAllowedLocations + return &s +} + +func (s *CreateStorageIntegrationRequest) WithOrReplace(OrReplace *bool) *CreateStorageIntegrationRequest { + s.OrReplace = OrReplace + return s +} + +func (s *CreateStorageIntegrationRequest) WithIfNotExists(IfNotExists *bool) *CreateStorageIntegrationRequest { + s.IfNotExists = IfNotExists + return s +} + +func (s *CreateStorageIntegrationRequest) WithS3StorageProviderParams(S3StorageProviderParams *S3StorageParamsRequest) *CreateStorageIntegrationRequest { + s.S3StorageProviderParams = S3StorageProviderParams + return s +} + +func (s *CreateStorageIntegrationRequest) WithGCSStorageProviderParams(GCSStorageProviderParams *GCSStorageParamsRequest) *CreateStorageIntegrationRequest { + s.GCSStorageProviderParams = GCSStorageProviderParams + return s +} + +func (s *CreateStorageIntegrationRequest) WithAzureStorageProviderParams(AzureStorageProviderParams *AzureStorageParamsRequest) *CreateStorageIntegrationRequest { + s.AzureStorageProviderParams = AzureStorageProviderParams + return s +} + +func (s *CreateStorageIntegrationRequest) WithStorageBlockedLocations(StorageBlockedLocations []StorageLocation) *CreateStorageIntegrationRequest { + s.StorageBlockedLocations = StorageBlockedLocations + return s +} + +func (s *CreateStorageIntegrationRequest) WithComment(Comment *string) *CreateStorageIntegrationRequest { + s.Comment = Comment + return s +} + +func NewS3StorageParamsRequest( + StorageAwsRoleArn string, +) *S3StorageParamsRequest { + s := S3StorageParamsRequest{} + s.StorageAwsRoleArn = StorageAwsRoleArn + return &s +} + +func (s *S3StorageParamsRequest) WithStorageAwsObjectAcl(StorageAwsObjectAcl *string) *S3StorageParamsRequest { + s.StorageAwsObjectAcl = StorageAwsObjectAcl + return s +} + +func NewGCSStorageParamsRequest() *GCSStorageParamsRequest { + return &GCSStorageParamsRequest{} +} + +func NewAzureStorageParamsRequest( + AzureTenantId *string, +) *AzureStorageParamsRequest { + s := AzureStorageParamsRequest{} + s.AzureTenantId = AzureTenantId + return &s +} + +func NewAlterStorageIntegrationRequest( + name AccountObjectIdentifier, +) *AlterStorageIntegrationRequest { + s := AlterStorageIntegrationRequest{} + s.name = name + return &s +} + +func (s *AlterStorageIntegrationRequest) WithIfExists(IfExists *bool) *AlterStorageIntegrationRequest { + s.IfExists = IfExists + return s +} + +func (s *AlterStorageIntegrationRequest) WithSet(Set *StorageIntegrationSetRequest) *AlterStorageIntegrationRequest { + s.Set = Set + return s +} + +func (s *AlterStorageIntegrationRequest) WithUnset(Unset *StorageIntegrationUnsetRequest) *AlterStorageIntegrationRequest { + s.Unset = Unset + return s +} + +func (s *AlterStorageIntegrationRequest) WithSetTags(SetTags []TagAssociation) *AlterStorageIntegrationRequest { + s.SetTags = SetTags + return s +} + +func (s *AlterStorageIntegrationRequest) WithUnsetTags(UnsetTags []ObjectIdentifier) *AlterStorageIntegrationRequest { + s.UnsetTags = UnsetTags + return s +} + +func NewStorageIntegrationSetRequest() *StorageIntegrationSetRequest { + return &StorageIntegrationSetRequest{} +} + +func (s *StorageIntegrationSetRequest) WithSetS3Params(SetS3Params *SetS3StorageParamsRequest) *StorageIntegrationSetRequest { + s.SetS3Params = SetS3Params + return s +} + +func (s *StorageIntegrationSetRequest) WithSetAzureParams(SetAzureParams *SetAzureStorageParamsRequest) *StorageIntegrationSetRequest { + s.SetAzureParams = SetAzureParams + return s +} + +func (s *StorageIntegrationSetRequest) WithEnabled(Enabled bool) *StorageIntegrationSetRequest { + s.Enabled = Enabled + return s +} + +func (s *StorageIntegrationSetRequest) WithStorageAllowedLocations(StorageAllowedLocations []StorageLocation) *StorageIntegrationSetRequest { + s.StorageAllowedLocations = StorageAllowedLocations + return s +} + +func (s *StorageIntegrationSetRequest) WithStorageBlockedLocations(StorageBlockedLocations []StorageLocation) *StorageIntegrationSetRequest { + s.StorageBlockedLocations = StorageBlockedLocations + return s +} + +func (s *StorageIntegrationSetRequest) WithComment(Comment *string) *StorageIntegrationSetRequest { + s.Comment = Comment + return s +} + +func NewSetS3StorageParamsRequest( + StorageAwsRoleArn string, +) *SetS3StorageParamsRequest { + s := SetS3StorageParamsRequest{} + s.StorageAwsRoleArn = StorageAwsRoleArn + return &s +} + +func (s *SetS3StorageParamsRequest) WithStorageAwsObjectAcl(StorageAwsObjectAcl *string) *SetS3StorageParamsRequest { + s.StorageAwsObjectAcl = StorageAwsObjectAcl + return s +} + +func NewSetAzureStorageParamsRequest( + AzureTenantId string, +) *SetAzureStorageParamsRequest { + s := SetAzureStorageParamsRequest{} + s.AzureTenantId = AzureTenantId + return &s +} + +func NewStorageIntegrationUnsetRequest() *StorageIntegrationUnsetRequest { + return &StorageIntegrationUnsetRequest{} +} + +func (s *StorageIntegrationUnsetRequest) WithEnabled(Enabled *bool) *StorageIntegrationUnsetRequest { + s.Enabled = Enabled + return s +} + +func (s *StorageIntegrationUnsetRequest) WithStorageBlockedLocations(StorageBlockedLocations *bool) *StorageIntegrationUnsetRequest { + s.StorageBlockedLocations = StorageBlockedLocations + return s +} + +func (s *StorageIntegrationUnsetRequest) WithComment(Comment *bool) *StorageIntegrationUnsetRequest { + s.Comment = Comment + return s +} + +func NewDropStorageIntegrationRequest( + name AccountObjectIdentifier, +) *DropStorageIntegrationRequest { + s := DropStorageIntegrationRequest{} + s.name = name + return &s +} + +func (s *DropStorageIntegrationRequest) WithIfExists(IfExists *bool) *DropStorageIntegrationRequest { + s.IfExists = IfExists + return s +} + +func NewShowStorageIntegrationRequest() *ShowStorageIntegrationRequest { + return &ShowStorageIntegrationRequest{} +} + +func (s *ShowStorageIntegrationRequest) WithLike(Like *Like) *ShowStorageIntegrationRequest { + s.Like = Like + return s +} + +func NewDescribeStorageIntegrationRequest( + name AccountObjectIdentifier, +) *DescribeStorageIntegrationRequest { + s := DescribeStorageIntegrationRequest{} + s.name = name + return &s +} diff --git a/pkg/sdk/storage_integration_dto_gen.go b/pkg/sdk/storage_integration_dto_gen.go new file mode 100644 index 0000000000..271ec9905a --- /dev/null +++ b/pkg/sdk/storage_integration_dto_gen.go @@ -0,0 +1,81 @@ +package sdk + +//go:generate go run ./dto-builder-generator/main.go + +var ( + _ optionsProvider[CreateStorageIntegrationOptions] = new(CreateStorageIntegrationRequest) + _ optionsProvider[AlterStorageIntegrationOptions] = new(AlterStorageIntegrationRequest) + _ optionsProvider[DropStorageIntegrationOptions] = new(DropStorageIntegrationRequest) + _ optionsProvider[ShowStorageIntegrationOptions] = new(ShowStorageIntegrationRequest) + _ optionsProvider[DescribeStorageIntegrationOptions] = new(DescribeStorageIntegrationRequest) +) + +type CreateStorageIntegrationRequest struct { + OrReplace *bool + IfNotExists *bool + name AccountObjectIdentifier // required + S3StorageProviderParams *S3StorageParamsRequest + GCSStorageProviderParams *GCSStorageParamsRequest + AzureStorageProviderParams *AzureStorageParamsRequest + Enabled bool // required + StorageAllowedLocations []StorageLocation // required + StorageBlockedLocations []StorageLocation + Comment *string +} + +type S3StorageParamsRequest struct { + StorageAwsRoleArn string // required + StorageAwsObjectAcl *string +} + +type GCSStorageParamsRequest struct{} + +type AzureStorageParamsRequest struct { + AzureTenantId *string // required +} + +type AlterStorageIntegrationRequest struct { + IfExists *bool + name AccountObjectIdentifier // required + Set *StorageIntegrationSetRequest + Unset *StorageIntegrationUnsetRequest + SetTags []TagAssociation + UnsetTags []ObjectIdentifier +} + +type StorageIntegrationSetRequest struct { + SetS3Params *SetS3StorageParamsRequest + SetAzureParams *SetAzureStorageParamsRequest + Enabled bool + StorageAllowedLocations []StorageLocation + StorageBlockedLocations []StorageLocation + Comment *string +} + +type SetS3StorageParamsRequest struct { + StorageAwsRoleArn string // required + StorageAwsObjectAcl *string +} + +type SetAzureStorageParamsRequest struct { + AzureTenantId string // required +} + +type StorageIntegrationUnsetRequest struct { + Enabled *bool + StorageBlockedLocations *bool + Comment *bool +} + +type DropStorageIntegrationRequest struct { + IfExists *bool + name AccountObjectIdentifier // required +} + +type ShowStorageIntegrationRequest struct { + Like *Like +} + +type DescribeStorageIntegrationRequest struct { + name AccountObjectIdentifier // required +} diff --git a/pkg/sdk/storage_integration_gen.go b/pkg/sdk/storage_integration_gen.go new file mode 100644 index 0000000000..9aa15ffa01 --- /dev/null +++ b/pkg/sdk/storage_integration_gen.go @@ -0,0 +1,141 @@ +package sdk + +import ( + "context" + "time" +) + +type StorageIntegrations interface { + Create(ctx context.Context, request *CreateStorageIntegrationRequest) error + Alter(ctx context.Context, request *AlterStorageIntegrationRequest) error + Drop(ctx context.Context, request *DropStorageIntegrationRequest) error + Show(ctx context.Context, request *ShowStorageIntegrationRequest) ([]StorageIntegration, error) + ShowByID(ctx context.Context, id AccountObjectIdentifier) (*StorageIntegration, error) + Describe(ctx context.Context, id AccountObjectIdentifier) ([]StorageIntegrationProperty, error) +} + +// CreateStorageIntegrationOptions is based on https://docs.snowflake.com/en/sql-reference/sql/create-storage-integration. +type CreateStorageIntegrationOptions struct { + create bool `ddl:"static" sql:"CREATE"` + OrReplace *bool `ddl:"keyword" sql:"OR REPLACE"` + storageIntegration bool `ddl:"static" sql:"STORAGE INTEGRATION"` + IfNotExists *bool `ddl:"keyword" sql:"IF NOT EXISTS"` + name AccountObjectIdentifier `ddl:"identifier"` + externalStageType string `ddl:"static" sql:"TYPE = EXTERNAL_STAGE"` + S3StorageProviderParams *S3StorageParams `ddl:"keyword"` + GCSStorageProviderParams *GCSStorageParams `ddl:"keyword"` + AzureStorageProviderParams *AzureStorageParams `ddl:"keyword"` + Enabled bool `ddl:"parameter" sql:"ENABLED"` + StorageAllowedLocations []StorageLocation `ddl:"parameter,parentheses" sql:"STORAGE_ALLOWED_LOCATIONS"` + StorageBlockedLocations []StorageLocation `ddl:"parameter,parentheses" sql:"STORAGE_BLOCKED_LOCATIONS"` + Comment *string `ddl:"parameter,single_quotes" sql:"COMMENT"` +} + +type StorageLocation struct { + Path string `ddl:"keyword,single_quotes"` +} + +type S3StorageParams struct { + storageProvider string `ddl:"static" sql:"STORAGE_PROVIDER = 'S3'"` + StorageAwsRoleArn string `ddl:"parameter,single_quotes" sql:"STORAGE_AWS_ROLE_ARN"` + StorageAwsObjectAcl *string `ddl:"parameter,single_quotes" sql:"STORAGE_AWS_OBJECT_ACL"` +} + +type GCSStorageParams struct { + storageProvider string `ddl:"static" sql:"STORAGE_PROVIDER = 'GCS'"` +} + +type AzureStorageParams struct { + storageProvider string `ddl:"static" sql:"STORAGE_PROVIDER = 'AZURE'"` + AzureTenantId *string `ddl:"parameter,single_quotes" sql:"AZURE_TENANT_ID"` +} + +// AlterStorageIntegrationOptions is based on https://docs.snowflake.com/en/sql-reference/sql/alter-storage-integration. +type AlterStorageIntegrationOptions struct { + alter bool `ddl:"static" sql:"ALTER"` + storageIntegration bool `ddl:"static" sql:"STORAGE INTEGRATION"` + IfExists *bool `ddl:"keyword" sql:"IF EXISTS"` + name AccountObjectIdentifier `ddl:"identifier"` + Set *StorageIntegrationSet `ddl:"keyword" sql:"SET"` + Unset *StorageIntegrationUnset `ddl:"list" sql:"UNSET"` + SetTags []TagAssociation `ddl:"keyword" sql:"SET TAG"` + UnsetTags []ObjectIdentifier `ddl:"keyword" sql:"UNSET TAG"` +} + +type StorageIntegrationSet struct { + SetS3Params *SetS3StorageParams `ddl:"keyword"` + SetAzureParams *SetAzureStorageParams `ddl:"keyword"` + Enabled bool `ddl:"parameter" sql:"ENABLED"` + StorageAllowedLocations []StorageLocation `ddl:"parameter,parentheses" sql:"STORAGE_ALLOWED_LOCATIONS"` + StorageBlockedLocations []StorageLocation `ddl:"parameter,parentheses" sql:"STORAGE_BLOCKED_LOCATIONS"` + Comment *string `ddl:"parameter,single_quotes" sql:"COMMENT"` +} + +type SetS3StorageParams struct { + StorageAwsRoleArn string `ddl:"parameter,single_quotes" sql:"STORAGE_AWS_ROLE_ARN"` + StorageAwsObjectAcl *string `ddl:"parameter,single_quotes" sql:"STORAGE_AWS_OBJECT_ACL"` +} + +type SetAzureStorageParams struct { + AzureTenantId string `ddl:"parameter,single_quotes" sql:"AZURE_TENANT_ID"` +} + +type StorageIntegrationUnset struct { + Enabled *bool `ddl:"keyword" sql:"ENABLED"` + StorageBlockedLocations *bool `ddl:"keyword" sql:"STORAGE_BLOCKED_LOCATIONS"` + Comment *bool `ddl:"keyword" sql:"COMMENT"` +} + +// DropStorageIntegrationOptions is based on https://docs.snowflake.com/en/sql-reference/sql/drop-integration. +type DropStorageIntegrationOptions struct { + drop bool `ddl:"static" sql:"DROP"` + storageIntegration bool `ddl:"static" sql:"STORAGE INTEGRATION"` + IfExists *bool `ddl:"keyword" sql:"IF EXISTS"` + name AccountObjectIdentifier `ddl:"identifier"` +} + +// ShowStorageIntegrationOptions is based on https://docs.snowflake.com/en/sql-reference/sql/show-integrations. +type ShowStorageIntegrationOptions struct { + show bool `ddl:"static" sql:"SHOW"` + storageIntegrations bool `ddl:"static" sql:"STORAGE INTEGRATIONS"` + Like *Like `ddl:"keyword" sql:"LIKE"` +} + +type showStorageIntegrationsDbRow struct { + Name string `db:"name"` + Type string `db:"type"` + Category string `db:"category"` + Enabled bool `db:"enabled"` + Comment string `db:"comment"` + CreatedOn time.Time `db:"created_on"` +} + +type StorageIntegration struct { + Name string + StorageType string + Category string + Enabled bool + Comment string + CreatedOn time.Time +} + +// DescribeStorageIntegrationOptions is based on https://docs.snowflake.com/en/sql-reference/sql/desc-integration. +type DescribeStorageIntegrationOptions struct { + describe bool `ddl:"static" sql:"DESCRIBE"` + storageIntegration bool `ddl:"static" sql:"STORAGE INTEGRATION"` + name AccountObjectIdentifier `ddl:"identifier"` +} + +type descStorageIntegrationsDbRow struct { + Property string `db:"property"` + PropertyType string `db:"property_type"` + PropertyValue string `db:"property_value"` + PropertyDefault string `db:"property_default"` +} + +type StorageIntegrationProperty struct { + Name string + Type string + Value string + Default string +} diff --git a/pkg/sdk/storage_integration_gen_test.go b/pkg/sdk/storage_integration_gen_test.go new file mode 100644 index 0000000000..a321104d54 --- /dev/null +++ b/pkg/sdk/storage_integration_gen_test.go @@ -0,0 +1,284 @@ +package sdk + +import "testing" + +func TestStorageIntegrations_Create(t *testing.T) { + id := RandomAccountObjectIdentifier() + + // Minimal valid CreateStorageIntegrationOptions + defaultOpts := func() *CreateStorageIntegrationOptions { + return &CreateStorageIntegrationOptions{ + name: id, + S3StorageProviderParams: &S3StorageParams{ + StorageAwsRoleArn: "arn:aws:iam::001234567890:role/role", + }, + Enabled: true, + StorageAllowedLocations: []StorageLocation{{Path: "allowed-loc-1"}, {Path: "allowed-loc-2"}}, + } + } + + t.Run("validation: nil options", func(t *testing.T) { + var opts *CreateStorageIntegrationOptions = nil + assertOptsInvalidJoinedErrors(t, opts, ErrNilOptions) + }) + + t.Run("validation: valid identifier for [opts.name]", func(t *testing.T) { + opts := defaultOpts() + opts.name = NewAccountObjectIdentifier("") + assertOptsInvalidJoinedErrors(t, opts, ErrInvalidObjectIdentifier) + }) + + t.Run("validation: conflicting fields for [opts.IfNotExists opts.OrReplace]", func(t *testing.T) { + opts := defaultOpts() + opts.IfNotExists = Bool(true) + opts.OrReplace = Bool(true) + assertOptsInvalidJoinedErrors(t, opts, errOneOf("CreateStorageIntegrationOptions", "IfNotExists", "OrReplace")) + }) + + t.Run("validation: exactly one field from [opts.S3StorageProviderParams opts.GCSStorageProviderParams opts.AzureStorageProviderParams] should be present - none set", func(t *testing.T) { + opts := defaultOpts() + opts.S3StorageProviderParams = nil + assertOptsInvalidJoinedErrors(t, opts, errExactlyOneOf("CreateStorageIntegrationOptions", "S3StorageProviderParams", "GCSStorageProviderParams", "AzureStorageProviderParams")) + }) + + t.Run("validation: exactly one field from [opts.S3StorageProviderParams opts.GCSStorageProviderParams opts.AzureStorageProviderParams] should be present - two set", func(t *testing.T) { + opts := defaultOpts() + opts.GCSStorageProviderParams = new(GCSStorageParams) + assertOptsInvalidJoinedErrors(t, opts, errExactlyOneOf("CreateStorageIntegrationOptions", "S3StorageProviderParams", "GCSStorageProviderParams", "AzureStorageProviderParams")) + }) + + t.Run("basic", func(t *testing.T) { + opts := defaultOpts() + assertOptsValidAndSQLEquals(t, opts, `CREATE STORAGE INTEGRATION %s TYPE = EXTERNAL_STAGE STORAGE_PROVIDER = 'S3' STORAGE_AWS_ROLE_ARN = 'arn:aws:iam::001234567890:role/role' ENABLED = true STORAGE_ALLOWED_LOCATIONS = ('allowed-loc-1', 'allowed-loc-2')`, id.FullyQualifiedName()) + }) + + t.Run("all options - s3", func(t *testing.T) { + opts := defaultOpts() + opts.IfNotExists = Bool(true) + opts.S3StorageProviderParams = &S3StorageParams{ + StorageAwsRoleArn: "arn:aws:iam::001234567890:role/role", + StorageAwsObjectAcl: String("bucket-owner-full-control"), + } + opts.StorageBlockedLocations = []StorageLocation{{Path: "blocked-loc-1"}, {Path: "blocked-loc-2"}} + opts.Comment = String("some comment") + assertOptsValidAndSQLEquals(t, opts, `CREATE STORAGE INTEGRATION IF NOT EXISTS %s TYPE = EXTERNAL_STAGE STORAGE_PROVIDER = 'S3' STORAGE_AWS_ROLE_ARN = 'arn:aws:iam::001234567890:role/role' STORAGE_AWS_OBJECT_ACL = 'bucket-owner-full-control' ENABLED = true STORAGE_ALLOWED_LOCATIONS = ('allowed-loc-1', 'allowed-loc-2') STORAGE_BLOCKED_LOCATIONS = ('blocked-loc-1', 'blocked-loc-2') COMMENT = 'some comment'`, id.FullyQualifiedName()) + }) + + t.Run("all options - gcs", func(t *testing.T) { + opts := defaultOpts() + opts.OrReplace = Bool(true) + opts.S3StorageProviderParams = nil + opts.GCSStorageProviderParams = new(GCSStorageParams) + opts.StorageBlockedLocations = []StorageLocation{{Path: "blocked-loc-1"}, {Path: "blocked-loc-2"}} + opts.Comment = String("some comment") + assertOptsValidAndSQLEquals(t, opts, `CREATE OR REPLACE STORAGE INTEGRATION %s TYPE = EXTERNAL_STAGE STORAGE_PROVIDER = 'GCS' ENABLED = true STORAGE_ALLOWED_LOCATIONS = ('allowed-loc-1', 'allowed-loc-2') STORAGE_BLOCKED_LOCATIONS = ('blocked-loc-1', 'blocked-loc-2') COMMENT = 'some comment'`, id.FullyQualifiedName()) + }) + + t.Run("all options - azure", func(t *testing.T) { + opts := defaultOpts() + opts.OrReplace = Bool(true) + opts.S3StorageProviderParams = nil + opts.AzureStorageProviderParams = &AzureStorageParams{ + AzureTenantId: String("azure-tenant-id"), + } + opts.StorageBlockedLocations = []StorageLocation{{Path: "blocked-loc-1"}, {Path: "blocked-loc-2"}} + opts.Comment = String("some comment") + assertOptsValidAndSQLEquals(t, opts, `CREATE OR REPLACE STORAGE INTEGRATION %s TYPE = EXTERNAL_STAGE STORAGE_PROVIDER = 'AZURE' AZURE_TENANT_ID = 'azure-tenant-id' ENABLED = true STORAGE_ALLOWED_LOCATIONS = ('allowed-loc-1', 'allowed-loc-2') STORAGE_BLOCKED_LOCATIONS = ('blocked-loc-1', 'blocked-loc-2') COMMENT = 'some comment'`, id.FullyQualifiedName()) + }) +} + +func TestStorageIntegrations_Alter(t *testing.T) { + id := RandomAccountObjectIdentifier() + + // Minimal valid AlterStorageIntegrationOptions + defaultOpts := func() *AlterStorageIntegrationOptions { + return &AlterStorageIntegrationOptions{ + name: id, + } + } + + t.Run("validation: nil options", func(t *testing.T) { + var opts *AlterStorageIntegrationOptions = nil + assertOptsInvalidJoinedErrors(t, opts, ErrNilOptions) + }) + + t.Run("validation: valid identifier for [opts.name]", func(t *testing.T) { + opts := defaultOpts() + opts.name = NewAccountObjectIdentifier("") + assertOptsInvalidJoinedErrors(t, opts, ErrInvalidObjectIdentifier) + }) + + t.Run("validation: conflicting fields for [opts.IfExists opts.UnsetTags]", func(t *testing.T) { + opts := defaultOpts() + opts.IfExists = Bool(true) + opts.UnsetTags = []ObjectIdentifier{ + NewAccountObjectIdentifier("one"), + } + assertOptsInvalidJoinedErrors(t, opts, errOneOf("AlterStorageIntegrationOptions", "IfExists", "UnsetTags")) + }) + + t.Run("validation: exactly one field from [opts.Set opts.Unset opts.SetTags opts.UnsetTags] should be present - none set", func(t *testing.T) { + opts := defaultOpts() + assertOptsInvalidJoinedErrors(t, opts, errExactlyOneOf("AlterStorageIntegrationOptions", "Set", "Unset", "SetTags", "UnsetTags")) + }) + + t.Run("validation: exactly one field from [opts.Set opts.Unset opts.SetTags opts.UnsetTags] should be present - two set", func(t *testing.T) { + opts := defaultOpts() + opts.SetTags = []TagAssociation{ + { + Name: NewAccountObjectIdentifier("name"), + Value: "value", + }, + } + opts.UnsetTags = []ObjectIdentifier{ + NewAccountObjectIdentifier("one"), + } + assertOptsInvalidJoinedErrors(t, opts, errExactlyOneOf("AlterStorageIntegrationOptions", "Set", "Unset", "SetTags", "UnsetTags")) + }) + + t.Run("set - s3", func(t *testing.T) { + opts := defaultOpts() + opts.Set = &StorageIntegrationSet{ + SetS3Params: &SetS3StorageParams{ + StorageAwsRoleArn: "new-aws-role-arn", + StorageAwsObjectAcl: String("new-aws-object-acl"), + }, + Enabled: false, + StorageAllowedLocations: []StorageLocation{{Path: "new-allowed-location"}}, + StorageBlockedLocations: []StorageLocation{{Path: "new-blocked-location"}}, + Comment: String("changed comment"), + } + assertOptsValidAndSQLEquals(t, opts, "ALTER STORAGE INTEGRATION %s SET STORAGE_AWS_ROLE_ARN = 'new-aws-role-arn' STORAGE_AWS_OBJECT_ACL = 'new-aws-object-acl' ENABLED = false STORAGE_ALLOWED_LOCATIONS = ('new-allowed-location') STORAGE_BLOCKED_LOCATIONS = ('new-blocked-location') COMMENT = 'changed comment'", id.FullyQualifiedName()) + }) + + t.Run("set - azure", func(t *testing.T) { + opts := defaultOpts() + opts.Set = &StorageIntegrationSet{ + SetAzureParams: &SetAzureStorageParams{ + AzureTenantId: "new-azure-tenant-id", + }, + Enabled: false, + StorageAllowedLocations: []StorageLocation{{Path: "new-allowed-location"}}, + StorageBlockedLocations: []StorageLocation{{Path: "new-blocked-location"}}, + Comment: String("changed comment"), + } + assertOptsValidAndSQLEquals(t, opts, "ALTER STORAGE INTEGRATION %s SET AZURE_TENANT_ID = 'new-azure-tenant-id' ENABLED = false STORAGE_ALLOWED_LOCATIONS = ('new-allowed-location') STORAGE_BLOCKED_LOCATIONS = ('new-blocked-location') COMMENT = 'changed comment'", id.FullyQualifiedName()) + }) + + t.Run("set tags", func(t *testing.T) { + opts := defaultOpts() + opts.IfExists = Bool(true) + opts.SetTags = []TagAssociation{ + { + Name: NewAccountObjectIdentifier("name"), + Value: "value", + }, + { + Name: NewAccountObjectIdentifier("second-name"), + Value: "second-value", + }, + } + assertOptsValidAndSQLEquals(t, opts, `ALTER STORAGE INTEGRATION IF EXISTS %s SET TAG "name" = 'value', "second-name" = 'second-value'`, id.FullyQualifiedName()) + }) + + t.Run("unset", func(t *testing.T) { + opts := defaultOpts() + opts.Unset = &StorageIntegrationUnset{ + Enabled: Bool(true), + StorageBlockedLocations: Bool(true), + Comment: Bool(true), + } + assertOptsValidAndSQLEquals(t, opts, "ALTER STORAGE INTEGRATION %s UNSET ENABLED, STORAGE_BLOCKED_LOCATIONS, COMMENT", id.FullyQualifiedName()) + }) + + t.Run("unset tags", func(t *testing.T) { + opts := defaultOpts() + opts.UnsetTags = []ObjectIdentifier{ + NewAccountObjectIdentifier("name"), + NewAccountObjectIdentifier("second-name"), + } + assertOptsValidAndSQLEquals(t, opts, `ALTER STORAGE INTEGRATION %s UNSET TAG "name", "second-name"`, id.FullyQualifiedName()) + }) +} + +func TestStorageIntegrations_Drop(t *testing.T) { + id := RandomAccountObjectIdentifier() + + // Minimal valid DropStorageIntegrationOptions + defaultOpts := func() *DropStorageIntegrationOptions { + return &DropStorageIntegrationOptions{ + name: id, + } + } + + t.Run("validation: nil options", func(t *testing.T) { + var opts *DropStorageIntegrationOptions = nil + assertOptsInvalidJoinedErrors(t, opts, ErrNilOptions) + }) + + t.Run("validation: valid identifier for [opts.name]", func(t *testing.T) { + opts := defaultOpts() + opts.name = NewAccountObjectIdentifier("") + assertOptsInvalidJoinedErrors(t, opts, ErrInvalidObjectIdentifier) + }) + + t.Run("all options", func(t *testing.T) { + opts := defaultOpts() + opts.IfExists = Bool(true) + assertOptsValidAndSQLEquals(t, opts, "DROP STORAGE INTEGRATION IF EXISTS %s", id.FullyQualifiedName()) + }) +} + +func TestStorageIntegrations_Show(t *testing.T) { + id := RandomAccountObjectIdentifier() + + // Minimal valid ShowStorageIntegrationOptions + defaultOpts := func() *ShowStorageIntegrationOptions { + return &ShowStorageIntegrationOptions{} + } + + t.Run("validation: nil options", func(t *testing.T) { + var opts *ShowStorageIntegrationOptions = nil + assertOptsInvalidJoinedErrors(t, opts, ErrNilOptions) + }) + + t.Run("basic", func(t *testing.T) { + opts := defaultOpts() + assertOptsValidAndSQLEquals(t, opts, "SHOW STORAGE INTEGRATIONS") + }) + + t.Run("all options", func(t *testing.T) { + opts := defaultOpts() + opts.Like = &Like{ + Pattern: String(id.Name()), + } + assertOptsValidAndSQLEquals(t, opts, "SHOW STORAGE INTEGRATIONS LIKE '%s'", id.Name()) + }) +} + +func TestStorageIntegrations_Describe(t *testing.T) { + id := RandomAccountObjectIdentifier() + + // Minimal valid DescribeStorageIntegrationOptions + defaultOpts := func() *DescribeStorageIntegrationOptions { + return &DescribeStorageIntegrationOptions{ + name: id, + } + } + + t.Run("validation: nil options", func(t *testing.T) { + var opts *DescribeStorageIntegrationOptions = nil + assertOptsInvalidJoinedErrors(t, opts, ErrNilOptions) + }) + + t.Run("validation: valid identifier for [opts.name]", func(t *testing.T) { + opts := defaultOpts() + opts.name = NewAccountObjectIdentifier("") + assertOptsInvalidJoinedErrors(t, opts, ErrInvalidObjectIdentifier) + }) + + t.Run("all options", func(t *testing.T) { + opts := defaultOpts() + assertOptsValidAndSQLEquals(t, opts, "DESCRIBE STORAGE INTEGRATION %s", id.FullyQualifiedName()) + }) +} diff --git a/pkg/sdk/storage_integration_impl_gen.go b/pkg/sdk/storage_integration_impl_gen.go new file mode 100644 index 0000000000..c003747e75 --- /dev/null +++ b/pkg/sdk/storage_integration_impl_gen.go @@ -0,0 +1,166 @@ +package sdk + +import ( + "context" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk/internal/collections" +) + +var _ StorageIntegrations = (*storageIntegrations)(nil) + +type storageIntegrations struct { + client *Client +} + +func (v *storageIntegrations) Create(ctx context.Context, request *CreateStorageIntegrationRequest) error { + opts := request.toOpts() + return validateAndExec(v.client, ctx, opts) +} + +func (v *storageIntegrations) Alter(ctx context.Context, request *AlterStorageIntegrationRequest) error { + opts := request.toOpts() + return validateAndExec(v.client, ctx, opts) +} + +func (v *storageIntegrations) Drop(ctx context.Context, request *DropStorageIntegrationRequest) error { + opts := request.toOpts() + return validateAndExec(v.client, ctx, opts) +} + +func (v *storageIntegrations) Show(ctx context.Context, request *ShowStorageIntegrationRequest) ([]StorageIntegration, error) { + opts := request.toOpts() + dbRows, err := validateAndQuery[showStorageIntegrationsDbRow](v.client, ctx, opts) + if err != nil { + return nil, err + } + resultList := convertRows[showStorageIntegrationsDbRow, StorageIntegration](dbRows) + return resultList, nil +} + +func (v *storageIntegrations) ShowByID(ctx context.Context, id AccountObjectIdentifier) (*StorageIntegration, error) { + storageIntegrations, err := v.Show(ctx, NewShowStorageIntegrationRequest().WithLike(&Like{ + Pattern: String(id.Name()), + })) + if err != nil { + return nil, err + } + return collections.FindOne(storageIntegrations, func(r StorageIntegration) bool { return r.Name == id.Name() }) +} + +func (v *storageIntegrations) Describe(ctx context.Context, id AccountObjectIdentifier) ([]StorageIntegrationProperty, error) { + opts := &DescribeStorageIntegrationOptions{ + name: id, + } + rows, err := validateAndQuery[descStorageIntegrationsDbRow](v.client, ctx, opts) + if err != nil { + return nil, err + } + return convertRows[descStorageIntegrationsDbRow, StorageIntegrationProperty](rows), nil +} + +func (r *CreateStorageIntegrationRequest) toOpts() *CreateStorageIntegrationOptions { + opts := &CreateStorageIntegrationOptions{ + OrReplace: r.OrReplace, + IfNotExists: r.IfNotExists, + name: r.name, + + Enabled: r.Enabled, + StorageAllowedLocations: r.StorageAllowedLocations, + StorageBlockedLocations: r.StorageBlockedLocations, + Comment: r.Comment, + } + if r.S3StorageProviderParams != nil { + opts.S3StorageProviderParams = &S3StorageParams{ + StorageAwsRoleArn: r.S3StorageProviderParams.StorageAwsRoleArn, + StorageAwsObjectAcl: r.S3StorageProviderParams.StorageAwsObjectAcl, + } + } + if r.GCSStorageProviderParams != nil { + opts.GCSStorageProviderParams = &GCSStorageParams{} + } + if r.AzureStorageProviderParams != nil { + opts.AzureStorageProviderParams = &AzureStorageParams{ + AzureTenantId: r.AzureStorageProviderParams.AzureTenantId, + } + } + return opts +} + +func (r *AlterStorageIntegrationRequest) toOpts() *AlterStorageIntegrationOptions { + opts := &AlterStorageIntegrationOptions{ + IfExists: r.IfExists, + name: r.name, + + SetTags: r.SetTags, + UnsetTags: r.UnsetTags, + } + if r.Set != nil { + opts.Set = &StorageIntegrationSet{ + Enabled: r.Set.Enabled, + StorageAllowedLocations: r.Set.StorageAllowedLocations, + StorageBlockedLocations: r.Set.StorageBlockedLocations, + Comment: r.Set.Comment, + } + if r.Set.SetS3Params != nil { + opts.Set.SetS3Params = &SetS3StorageParams{ + StorageAwsRoleArn: r.Set.SetS3Params.StorageAwsRoleArn, + StorageAwsObjectAcl: r.Set.SetS3Params.StorageAwsObjectAcl, + } + } + if r.Set.SetAzureParams != nil { + opts.Set.SetAzureParams = &SetAzureStorageParams{ + AzureTenantId: r.Set.SetAzureParams.AzureTenantId, + } + } + } + if r.Unset != nil { + opts.Unset = &StorageIntegrationUnset{ + Enabled: r.Unset.Enabled, + StorageBlockedLocations: r.Unset.StorageBlockedLocations, + Comment: r.Unset.Comment, + } + } + return opts +} + +func (r *DropStorageIntegrationRequest) toOpts() *DropStorageIntegrationOptions { + opts := &DropStorageIntegrationOptions{ + IfExists: r.IfExists, + name: r.name, + } + return opts +} + +func (r *ShowStorageIntegrationRequest) toOpts() *ShowStorageIntegrationOptions { + opts := &ShowStorageIntegrationOptions{ + Like: r.Like, + } + return opts +} + +func (r showStorageIntegrationsDbRow) convert() *StorageIntegration { + return &StorageIntegration{ + Name: r.Name, + StorageType: r.Type, + Category: r.Category, + Enabled: r.Enabled, + Comment: r.Comment, + CreatedOn: r.CreatedOn, + } +} + +func (r *DescribeStorageIntegrationRequest) toOpts() *DescribeStorageIntegrationOptions { + opts := &DescribeStorageIntegrationOptions{ + name: r.name, + } + return opts +} + +func (r descStorageIntegrationsDbRow) convert() *StorageIntegrationProperty { + return &StorageIntegrationProperty{ + Name: r.Property, + Type: r.PropertyType, + Value: r.PropertyValue, + Default: r.PropertyDefault, + } +} diff --git a/pkg/sdk/storage_integration_validations_gen.go b/pkg/sdk/storage_integration_validations_gen.go new file mode 100644 index 0000000000..7290fe7027 --- /dev/null +++ b/pkg/sdk/storage_integration_validations_gen.go @@ -0,0 +1,73 @@ +package sdk + +var ( + _ validatable = new(CreateStorageIntegrationOptions) + _ validatable = new(AlterStorageIntegrationOptions) + _ validatable = new(DropStorageIntegrationOptions) + _ validatable = new(ShowStorageIntegrationOptions) + _ validatable = new(DescribeStorageIntegrationOptions) +) + +func (opts *CreateStorageIntegrationOptions) validate() error { + if opts == nil { + return ErrNilOptions + } + var errs []error + if !ValidObjectIdentifier(opts.name) { + errs = append(errs, ErrInvalidObjectIdentifier) + } + if everyValueSet(opts.IfNotExists, opts.OrReplace) { + errs = append(errs, errOneOf("CreateStorageIntegrationOptions", "IfNotExists", "OrReplace")) + } + if !exactlyOneValueSet(opts.S3StorageProviderParams, opts.GCSStorageProviderParams, opts.AzureStorageProviderParams) { + errs = append(errs, errExactlyOneOf("CreateStorageIntegrationOptions", "S3StorageProviderParams", "GCSStorageProviderParams", "AzureStorageProviderParams")) + } + return JoinErrors(errs...) +} + +func (opts *AlterStorageIntegrationOptions) validate() error { + if opts == nil { + return ErrNilOptions + } + var errs []error + if !ValidObjectIdentifier(opts.name) { + errs = append(errs, ErrInvalidObjectIdentifier) + } + if everyValueSet(opts.IfExists, opts.UnsetTags) { + errs = append(errs, errOneOf("AlterStorageIntegrationOptions", "IfExists", "UnsetTags")) + } + if !exactlyOneValueSet(opts.Set, opts.Unset, opts.SetTags, opts.UnsetTags) { + errs = append(errs, errExactlyOneOf("AlterStorageIntegrationOptions", "Set", "Unset", "SetTags", "UnsetTags")) + } + return JoinErrors(errs...) +} + +func (opts *DropStorageIntegrationOptions) validate() error { + if opts == nil { + return ErrNilOptions + } + var errs []error + if !ValidObjectIdentifier(opts.name) { + errs = append(errs, ErrInvalidObjectIdentifier) + } + return JoinErrors(errs...) +} + +func (opts *ShowStorageIntegrationOptions) validate() error { + if opts == nil { + return ErrNilOptions + } + var errs []error + return JoinErrors(errs...) +} + +func (opts *DescribeStorageIntegrationOptions) validate() error { + if opts == nil { + return ErrNilOptions + } + var errs []error + if !ValidObjectIdentifier(opts.name) { + errs = append(errs, ErrInvalidObjectIdentifier) + } + return JoinErrors(errs...) +} diff --git a/pkg/sdk/tables.go b/pkg/sdk/tables.go index e43079cfa2..7712b7a4ba 100644 --- a/pkg/sdk/tables.go +++ b/pkg/sdk/tables.go @@ -8,6 +8,12 @@ import ( var _ convertibleRow[Table] = new(tableDBRow) +// TODO [SNOW-1007542]: add missing features: +// - show columns (https://docs.snowflake.com/en/sql-reference/sql/show-columns) +// - show primary keys (https://docs.snowflake.com/en/sql-reference/sql/show-primary-keys) +// - describe search optimization (https://docs.snowflake.com/en/sql-reference/sql/desc-search-optimization) +// - truncate table (https://docs.snowflake.com/en/sql-reference/sql/truncate-table) +// - undrop table (https://docs.snowflake.com/en/sql-reference/sql/undrop-table) type Tables interface { Create(ctx context.Context, req *CreateTableRequest) error CreateAsSelect(ctx context.Context, req *CreateTableAsSelectRequest) error diff --git a/pkg/sdk/testint/databases_integration_test.go b/pkg/sdk/testint/databases_integration_test.go index 7b0c086f0c..7441ff7040 100644 --- a/pkg/sdk/testint/databases_integration_test.go +++ b/pkg/sdk/testint/databases_integration_test.go @@ -109,45 +109,52 @@ func TestInt_DatabasesCreate(t *testing.T) { } func TestInt_CreateShared(t *testing.T) { - t.Skipf("Snowflake secondary account is not configured. Must be set in ~./snowflake/config.yml with profile name: %s", secondaryAccountProfile) client := testClient(t) + secondaryClient := testSecondaryClient(t) ctx := testContext(t) - databaseTest, databaseCleanup := createDatabase(t, client) + + databaseTest, databaseCleanup := createDatabase(t, secondaryClient) t.Cleanup(databaseCleanup) - shareTest, _ := createShare(t, client) - // t.Cleanup(shareCleanup) - err := client.Grants.GrantPrivilegeToShare(ctx, sdk.ObjectPrivilegeUsage, &sdk.GrantPrivilegeToShareOn{ + + shareTest, shareCleanup := createShare(t, secondaryClient) + t.Cleanup(shareCleanup) + + err := secondaryClient.Grants.GrantPrivilegeToShare(ctx, sdk.ObjectPrivilegeUsage, &sdk.GrantPrivilegeToShareOn{ Database: databaseTest.ID(), }, shareTest.ID()) require.NoError(t, err) t.Cleanup(func() { - err = client.Grants.RevokePrivilegeFromShare(ctx, sdk.ObjectPrivilegeUsage, &sdk.RevokePrivilegeFromShareOn{ + err := secondaryClient.Grants.RevokePrivilegeFromShare(ctx, sdk.ObjectPrivilegeUsage, &sdk.RevokePrivilegeFromShareOn{ Database: databaseTest.ID(), }, shareTest.ID()) + require.NoError(t, err) }) - require.NoError(t, err) - secondaryClient := testSecondaryClient(t) + accountsToSet := []sdk.AccountIdentifier{ - getAccountIdentifier(t, secondaryClient), + getAccountIdentifier(t, client), } + // first add the account. - err = client.Shares.Alter(ctx, shareTest.ID(), &sdk.AlterShareOptions{ + err = secondaryClient.Shares.Alter(ctx, shareTest.ID(), &sdk.AlterShareOptions{ IfExists: sdk.Bool(true), Set: &sdk.ShareSet{ Accounts: accountsToSet, }, }) + require.NoError(t, err) databaseID := sdk.RandomAccountObjectIdentifier() - err = secondaryClient.Databases.CreateShared(ctx, databaseID, shareTest.ExternalID(), nil) - require.NoError(t, err) - database, err := secondaryClient.Databases.ShowByID(ctx, databaseID) + err = client.Databases.CreateShared(ctx, databaseID, shareTest.ExternalID(), nil) require.NoError(t, err) - assert.Equal(t, databaseID.Name(), database.Name) t.Cleanup(func() { - err = secondaryClient.Databases.Drop(ctx, databaseID, nil) + err = client.Databases.Drop(ctx, databaseID, nil) require.NoError(t, err) }) + + database, err := client.Databases.ShowByID(ctx, databaseID) + require.NoError(t, err) + + assert.Equal(t, databaseID.Name(), database.Name) } func TestInt_DatabasesCreateSecondary(t *testing.T) { @@ -269,36 +276,38 @@ func TestInt_AlterReplication(t *testing.T) { func TestInt_AlterFailover(t *testing.T) { client := testClient(t) + secondaryClient := testSecondaryClient(t) ctx := testContext(t) - databaseTest, databaseCleanup := createDatabase(t, client) + + databaseTest, databaseCleanup := createDatabase(t, secondaryClient) t.Cleanup(databaseCleanup) - secondaryClient := testSecondaryClient(t) toAccounts := []sdk.AccountIdentifier{ - getAccountIdentifier(t, secondaryClient), + getAccountIdentifier(t, client), } + t.Run("enable and disable failover", func(t *testing.T) { - opts := &sdk.AlterDatabaseFailoverOptions{ + err := secondaryClient.Databases.AlterFailover(ctx, databaseTest.ID(), &sdk.AlterDatabaseFailoverOptions{ EnableFailover: &sdk.EnableFailover{ ToAccounts: toAccounts, }, - } - err := client.Databases.AlterFailover(ctx, databaseTest.ID(), opts) + }) + // TODO: has to be enabled by ORGADMIN (SNOW-1002025) if strings.Contains(err.Error(), "Accounts enabled for failover must also be enabled for replication. Enable replication to account") { t.Skip("Skipping test because secondary account not enabled for replication") } require.NoError(t, err) - opts = &sdk.AlterDatabaseFailoverOptions{ + + err = secondaryClient.Databases.AlterFailover(ctx, databaseTest.ID(), &sdk.AlterDatabaseFailoverOptions{ DisableFailover: &sdk.DisableFailover{ ToAccounts: toAccounts, }, - } - err = client.Databases.AlterFailover(ctx, databaseTest.ID(), opts) + }) require.NoError(t, err) - opts = &sdk.AlterDatabaseFailoverOptions{ + + err = secondaryClient.Databases.AlterFailover(ctx, databaseTest.ID(), &sdk.AlterDatabaseFailoverOptions{ Primary: sdk.Bool(true), - } - err = client.Databases.AlterFailover(ctx, databaseTest.ID(), opts) + }) require.NoError(t, err) }) } diff --git a/pkg/sdk/testint/external_tables_integration_test.go b/pkg/sdk/testint/external_tables_integration_test.go index cbe21326d4..45542dede1 100644 --- a/pkg/sdk/testint/external_tables_integration_test.go +++ b/pkg/sdk/testint/external_tables_integration_test.go @@ -51,7 +51,6 @@ func TestInt_ExternalTables(t *testing.T) { WithFileFormat(sdk.NewExternalTableFileFormatRequest().WithFileFormatType(&sdk.ExternalTableFileFormatTypeJSON)). WithOrReplace(sdk.Bool(true)). WithColumns(columnsWithPartition). - WithUserSpecifiedPartitionType(sdk.Bool(true)). WithPartitionBy([]string{"part_date"}). WithCopyGrants(sdk.Bool(true)). WithComment(sdk.String("some_comment")). @@ -158,7 +157,6 @@ func TestInt_ExternalTables(t *testing.T) { WithOrReplace(sdk.Bool(true)). WithColumns(columnsWithPartition). WithPartitionBy([]string{"filename"}). - WithDeltaTableFormat(sdk.Bool(true)). WithAutoRefresh(sdk.Bool(false)). WithRefreshOnCreate(sdk.Bool(false)). WithCopyGrants(sdk.Bool(true)). diff --git a/pkg/sdk/testint/failover_groups_integration_test.go b/pkg/sdk/testint/failover_groups_integration_test.go index e43c778c01..cb048695ac 100644 --- a/pkg/sdk/testint/failover_groups_integration_test.go +++ b/pkg/sdk/testint/failover_groups_integration_test.go @@ -29,7 +29,7 @@ func TestInt_FailoverGroupsCreate(t *testing.T) { sdk.PluralObjectTypeDatabases, } allowedAccounts := []sdk.AccountIdentifier{ - getSecondaryAccountIdentifier(t), + getAccountIdentifier(t, testSecondaryClient(t)), } replicationSchedule := "10 MINUTE" err := client.FailoverGroups.Create(ctx, id, objectTypes, allowedAccounts, &sdk.CreateFailoverGroupOptions{ @@ -80,7 +80,7 @@ func TestInt_FailoverGroupsCreate(t *testing.T) { sdk.PluralObjectTypeIntegrations, } allowedAccounts := []sdk.AccountIdentifier{ - getSecondaryAccountIdentifier(t), + getAccountIdentifier(t, testSecondaryClient(t)), } allowedIntegrationTypes := []sdk.IntegrationType{ sdk.IntegrationTypeAPIIntegrations, @@ -105,6 +105,7 @@ func TestInt_FailoverGroupsCreate(t *testing.T) { } func TestInt_CreateSecondaryReplicationGroup(t *testing.T) { + // TODO: Business Critical Snowflake Edition (SNOW-1002023) if os.Getenv("SNOWFLAKE_TEST_BUSINESS_CRITICAL_FEATURES") != "1" { t.Skip("Skipping TestInt_FailoverGroupsCreate") } @@ -386,7 +387,7 @@ func TestInt_FailoverGroupsAlterSource(t *testing.T) { failoverGroup, cleanupFailoverGroup := createFailoverGroup(t, client) t.Cleanup(cleanupFailoverGroup) - secondaryAccountID := getSecondaryAccountIdentifier(t) + secondaryAccountID := getAccountIdentifier(t, testSecondaryClient(t)) // first add target account opts := &sdk.AlterSourceFailoverGroupOptions{ Add: &sdk.FailoverGroupAdd{ @@ -538,6 +539,7 @@ func TestInt_FailoverGroupsAlterSource(t *testing.T) { } func TestInt_FailoverGroupsAlterTarget(t *testing.T) { + // TODO: Business Critical Snowflake Edition (SNOW-1002023) if os.Getenv("SNOWFLAKE_TEST_BUSINESS_CRITICAL_FEATURES") != "1" { t.Skip("Skipping TestInt_FailoverGroupsCreate") } diff --git a/pkg/sdk/testint/helpers_test.go b/pkg/sdk/testint/helpers_test.go index 861108ef30..80d4c940fb 100644 --- a/pkg/sdk/testint/helpers_test.go +++ b/pkg/sdk/testint/helpers_test.go @@ -5,6 +5,7 @@ import ( "database/sql" "errors" "fmt" + "os" "path/filepath" "testing" @@ -17,6 +18,26 @@ const ( nycWeatherDataURL = "s3://snowflake-workshop-lab/weather-nyc" ) +var ( + awsBucketUrl, awsBucketUrlIsSet = os.LookupEnv("AWS_EXTERNAL_BUCKET_URL") + awsKeyId, awsKeyIdIsSet = os.LookupEnv("AWS_EXTERNAL_KEY_ID") + awsSecretKey, awsSecretKeyIsSet = os.LookupEnv("AWS_EXTERNAL_SECRET_KEY") + awsRoleARN, awsRoleARNIsSet = os.LookupEnv("AWS_EXTERNAL_ROLE_ARN") + + gcsBucketUrl, gcsBucketUrlIsSet = os.LookupEnv("GCS_EXTERNAL_BUCKET_URL") + + azureBucketUrl, azureBucketUrlIsSet = os.LookupEnv("AZURE_EXTERNAL_BUCKET_URL") + azureTenantId, azureTenantIdIsSet = os.LookupEnv("AZURE_EXTERNAL_TENANT_ID") + + hasExternalEnvironmentVariablesSet = awsBucketUrlIsSet && + awsKeyIdIsSet && + awsSecretKeyIsSet && + awsRoleARNIsSet && + gcsBucketUrlIsSet && + azureBucketUrlIsSet && + azureTenantIdIsSet +) + // there is no direct way to get the account identifier from Snowflake API, but you can get it if you know // the account locator and by filtering the list of accounts in replication accounts by the account locator func getAccountIdentifier(t *testing.T, client *sdk.Client) sdk.AccountIdentifier { @@ -34,37 +55,6 @@ func getAccountIdentifier(t *testing.T, client *sdk.Client) sdk.AccountIdentifie return sdk.AccountIdentifier{} } -func getSecondaryAccountIdentifier(t *testing.T) sdk.AccountIdentifier { - t.Helper() - client := testSecondaryClient(t) - return getAccountIdentifier(t, client) -} - -const ( - secondaryAccountProfile = "secondary_test_account" -) - -// TODO: for now we leave it as is, later it would be nice to configure it also once in TestMain -func testSecondaryClient(t *testing.T) *sdk.Client { - t.Helper() - - client, err := testClientFromProfile(t, secondaryAccountProfile) - if err != nil { - t.Skipf("Snowflake secondary account not configured. Must be set in ~./snowflake/config.yml with profile name: %s", secondaryAccountProfile) - } - - return client -} - -func testClientFromProfile(t *testing.T, profile string) (*sdk.Client, error) { - t.Helper() - config, err := sdk.ProfileConfig(profile) - if err != nil { - return nil, err - } - return sdk.NewClient(config) -} - func useWarehouse(t *testing.T, client *sdk.Client, warehouseID sdk.AccountObjectIdentifier) func() { t.Helper() ctx := context.Background() @@ -81,17 +71,17 @@ func createDatabase(t *testing.T, client *sdk.Client) (*sdk.Database, func()) { return createDatabaseWithOptions(t, client, sdk.RandomAccountObjectIdentifier(), &sdk.CreateDatabaseOptions{}) } -func createDatabaseWithOptions(t *testing.T, client *sdk.Client, id sdk.AccountObjectIdentifier, _ *sdk.CreateDatabaseOptions) (*sdk.Database, func()) { +func createDatabaseWithOptions(t *testing.T, client *sdk.Client, id sdk.AccountObjectIdentifier, opts *sdk.CreateDatabaseOptions) (*sdk.Database, func()) { t.Helper() ctx := context.Background() - err := client.Databases.Create(ctx, id, nil) + err := client.Databases.Create(ctx, id, opts) require.NoError(t, err) database, err := client.Databases.ShowByID(ctx, id) require.NoError(t, err) return database, func() { err := client.Databases.Drop(ctx, id, nil) require.NoError(t, err) - err = client.Sessions.UseSchema(ctx, testSchema(t).ID()) + err = client.Sessions.UseSchema(ctx, sdk.NewDatabaseObjectIdentifier(TestDatabaseName, TestSchemaName)) require.NoError(t, err) } } @@ -759,11 +749,15 @@ func createRowAccessPolicy(t *testing.T, client *sdk.Client, schema *sdk.Schema) t.Helper() ctx := context.Background() id := sdk.NewSchemaObjectIdentifier(schema.DatabaseName, schema.Name, random.String()) - _, err := client.ExecForTests(ctx, fmt.Sprintf(`CREATE ROW ACCESS POLICY %s AS (A NUMBER) RETURNS BOOLEAN -> TRUE`, id.FullyQualifiedName())) + + arg := sdk.NewCreateRowAccessPolicyArgsRequest("A", sdk.DataTypeNumber) + body := "true" + createRequest := sdk.NewCreateRowAccessPolicyRequest(id, []sdk.CreateRowAccessPolicyArgsRequest{*arg}, body) + err := client.RowAccessPolicies.Create(ctx, createRequest) require.NoError(t, err) return id, func() { - _, err := client.ExecForTests(ctx, fmt.Sprintf(`DROP ROW ACCESS POLICY %s`, id.FullyQualifiedName())) + err := client.RowAccessPolicies.Drop(ctx, sdk.NewDropRowAccessPolicyRequest(id)) require.NoError(t, err) } } diff --git a/pkg/sdk/testint/managed_accounts_gen_integration_test.go b/pkg/sdk/testint/managed_accounts_gen_integration_test.go new file mode 100644 index 0000000000..9eab34187d --- /dev/null +++ b/pkg/sdk/testint/managed_accounts_gen_integration_test.go @@ -0,0 +1,142 @@ +package testint + +import ( + "os" + "strings" + "testing" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk/internal/collections" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk/internal/random" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TODO [SNOW-1011985]: unskip the tests +func TestInt_ManagedAccounts(t *testing.T) { + if _, ok := os.LookupEnv("SKIP_MANAGED_ACCOUNT_INT_TEST"); ok { + t.Skip("Skipping TestInt_ManagedAccounts due to error: 090337 (23001): Number of managed accounts allowed exceeded the limit. Please contact Snowflake support.") + } + + client := testClient(t) + ctx := testContext(t) + + assertManagedAccount := func(t *testing.T, managedAccount *sdk.ManagedAccount, id sdk.AccountObjectIdentifier, comment string) { + t.Helper() + assert.Equal(t, id.Name(), managedAccount.Name) + assert.Equal(t, "aws", managedAccount.Cloud) + assert.NotEmpty(t, managedAccount.Region) + assert.NotEmpty(t, managedAccount.Locator) + assert.NotEmpty(t, managedAccount.CreatedOn) + assert.NotEmpty(t, managedAccount.URL) + assert.NotEmpty(t, managedAccount.AccountLocatorURL) + assert.Equal(t, true, managedAccount.IsReader) + assert.Equal(t, comment, managedAccount.Comment) + } + + cleanupMangedAccountProvider := func(id sdk.AccountObjectIdentifier) func() { + return func() { + err := client.ManagedAccounts.Drop(ctx, sdk.NewDropManagedAccountRequest(id)) + require.NoError(t, err) + } + } + + createManagedAccountBasicRequest := func(t *testing.T) *sdk.CreateManagedAccountRequest { + t.Helper() + // 090348 (42602): Account name or alias is invalid: (...) can only contain capital letters, numbers, and underscores + name := strings.ToUpper(random.AlphanumericN(10)) + id := sdk.NewAccountObjectIdentifier(name) + + // 090088 (22000): ADMIN_NAME can only contain letters, numbers and underscores. + // 090089 (22000): ADMIN_NAME must start with a letter. + adminName := random.AlphaN(1) + random.AlphanumericN(12) + adminPassword := random.String() + params := sdk.NewCreateManagedAccountParamsRequest(adminName, adminPassword) + + return sdk.NewCreateManagedAccountRequest(id, *params) + } + + createManagedAccountWithRequest := func(t *testing.T, request *sdk.CreateManagedAccountRequest) *sdk.ManagedAccount { + t.Helper() + id := request.GetName() + + err := client.ManagedAccounts.Create(ctx, request) + require.NoError(t, err) + t.Cleanup(cleanupMangedAccountProvider(id)) + + managedAccount, err := client.ManagedAccounts.ShowByID(ctx, id) + require.NoError(t, err) + + return managedAccount + } + + createManagedAccount := func(t *testing.T) *sdk.ManagedAccount { + t.Helper() + return createManagedAccountWithRequest(t, createManagedAccountBasicRequest(t)) + } + + t.Run("create managed account: no optionals", func(t *testing.T) { + request := createManagedAccountBasicRequest(t) + + managedAccount := createManagedAccountWithRequest(t, request) + + assertManagedAccount(t, managedAccount, request.GetName(), "") + }) + + t.Run("create managed account: full", func(t *testing.T) { + request := createManagedAccountBasicRequest(t) + request.CreateManagedAccountParams.Comment = sdk.String("some comment") + + managedAccount := createManagedAccountWithRequest(t, request) + + assertManagedAccount(t, managedAccount, request.GetName(), "some comment") + }) + + t.Run("drop managed account: existing", func(t *testing.T) { + request := createManagedAccountBasicRequest(t) + id := request.GetName() + + err := client.ManagedAccounts.Create(ctx, request) + require.NoError(t, err) + + err = client.ManagedAccounts.Drop(ctx, sdk.NewDropManagedAccountRequest(id)) + require.NoError(t, err) + + _, err = client.ManagedAccounts.ShowByID(ctx, id) + assert.ErrorIs(t, err, collections.ErrObjectNotFound) + }) + + t.Run("drop managed account: non-existing", func(t *testing.T) { + id := sdk.NewAccountObjectIdentifier("does_not_exist") + + err := client.ManagedAccounts.Drop(ctx, sdk.NewDropManagedAccountRequest(id)) + assert.ErrorIs(t, err, sdk.ErrObjectNotExistOrAuthorized) + }) + + t.Run("show managed account: default", func(t *testing.T) { + managedAccount1 := createManagedAccount(t) + managedAccount2 := createManagedAccount(t) + + showRequest := sdk.NewShowManagedAccountRequest() + returnedManagedAccounts, err := client.ManagedAccounts.Show(ctx, showRequest) + require.NoError(t, err) + + assert.Equal(t, 2, len(returnedManagedAccounts)) + assert.Contains(t, returnedManagedAccounts, *managedAccount1) + assert.Contains(t, returnedManagedAccounts, *managedAccount2) + }) + + t.Run("show managed account: with like", func(t *testing.T) { + managedAccount1 := createManagedAccount(t) + managedAccount2 := createManagedAccount(t) + + showRequest := sdk.NewShowManagedAccountRequest(). + WithLike(&sdk.Like{Pattern: &managedAccount1.Name}) + returnedManagedAccounts, err := client.ManagedAccounts.Show(ctx, showRequest) + + require.NoError(t, err) + assert.Equal(t, 1, len(returnedManagedAccounts)) + assert.Contains(t, returnedManagedAccounts, *managedAccount1) + assert.NotContains(t, returnedManagedAccounts, *managedAccount2) + }) +} diff --git a/pkg/sdk/testint/resource_monitors_integration_test.go b/pkg/sdk/testint/resource_monitors_integration_test.go index 0a359dc9cc..106a118f85 100644 --- a/pkg/sdk/testint/resource_monitors_integration_test.go +++ b/pkg/sdk/testint/resource_monitors_integration_test.go @@ -211,6 +211,30 @@ func TestInt_ResourceMonitorAlter(t *testing.T) { assert.Equal(t, creditQuota, int(resourceMonitor.CreditQuota)) }) + t.Run("when changing notify users", func(t *testing.T) { + resourceMonitor, resourceMonitorCleanup := createResourceMonitor(t, client) + t.Cleanup(resourceMonitorCleanup) + alterOptions := &sdk.AlterResourceMonitorOptions{ + Set: &sdk.ResourceMonitorSet{ + NotifyUsers: &sdk.NotifyUsers{ + Users: []sdk.NotifiedUser{{Name: "ARTUR_SAWICKI"}}, + }, + }, + } + err := client.ResourceMonitors.Alter(ctx, resourceMonitor.ID(), alterOptions) + require.NoError(t, err) + resourceMonitors, err := client.ResourceMonitors.Show(ctx, &sdk.ShowResourceMonitorOptions{ + Like: &sdk.Like{ + Pattern: sdk.String(resourceMonitor.Name), + }, + }) + require.NoError(t, err) + assert.Equal(t, 1, len(resourceMonitors)) + resourceMonitor = &resourceMonitors[0] + assert.Len(t, resourceMonitor.NotifyUsers, 1) + assert.Equal(t, "ARTUR_SAWICKI", resourceMonitor.NotifyUsers[0]) + }) + t.Run("when changing scheduling info", func(t *testing.T) { resourceMonitor, resourceMonitorCleanup := createResourceMonitor(t, client) t.Cleanup(resourceMonitorCleanup) @@ -256,11 +280,11 @@ func TestInt_ResourceMonitorAlter(t *testing.T) { alterOptions := &sdk.AlterResourceMonitorOptions{ Set: &sdk.ResourceMonitorSet{ CreditQuota: &creditQuota, + NotifyUsers: &sdk.NotifyUsers{ + Users: []sdk.NotifiedUser{{Name: "ARTUR_SAWICKI"}}, + }, }, Triggers: newTriggers, - NotifyUsers: &sdk.NotifyUsers{ - Users: []sdk.NotifiedUser{{Name: "ARTUR_SAWICKI"}}, - }, } err := client.ResourceMonitors.Alter(ctx, resourceMonitor.ID(), alterOptions) require.NoError(t, 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 new file mode 100644 index 0000000000..71b8d79419 --- /dev/null +++ b/pkg/sdk/testint/row_access_policies_gen_integration_test.go @@ -0,0 +1,285 @@ +package testint + +import ( + "fmt" + "strings" + "testing" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk/internal/collections" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk/internal/random" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestInt_RowAccessPolicies(t *testing.T) { + client := testClient(t) + ctx := testContext(t) + + assertRowAccessPolicy := func(t *testing.T, rowAccessPolicy *sdk.RowAccessPolicy, id sdk.SchemaObjectIdentifier, comment string) { + t.Helper() + assert.NotEmpty(t, rowAccessPolicy.CreatedOn) + assert.Equal(t, id.Name(), rowAccessPolicy.Name) + assert.Equal(t, id.DatabaseName(), rowAccessPolicy.DatabaseName) + assert.Equal(t, id.SchemaName(), rowAccessPolicy.SchemaName) + assert.Equal(t, "ROW_ACCESS_POLICY", rowAccessPolicy.Kind) + assert.Equal(t, "ACCOUNTADMIN", rowAccessPolicy.Owner) + assert.Equal(t, comment, rowAccessPolicy.Comment) + assert.Empty(t, rowAccessPolicy.Options) + assert.Equal(t, "ROLE", rowAccessPolicy.OwnerRoleType) + } + + assertRowAccessPolicyDescription := func(t *testing.T, rowAccessPolicyDescription *sdk.RowAccessPolicyDescription, id sdk.SchemaObjectIdentifier, expectedSignature string, expectedBody string) { + t.Helper() + assert.Equal(t, sdk.RowAccessPolicyDescription{ + Name: id.Name(), + Signature: expectedSignature, + ReturnType: "BOOLEAN", + Body: expectedBody, + }, *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() + name := random.String() + id := sdk.NewSchemaObjectIdentifier(testDb(t).Name, testSchema(t).Name, name) + + return sdk.NewCreateRowAccessPolicyRequest(id, args, body) + } + + createRowAccessPolicyBasicRequest := func(t *testing.T) *sdk.CreateRowAccessPolicyRequest { + t.Helper() + + argName := random.AlphaN(5) + argType := sdk.DataTypeVARCHAR + args := sdk.NewCreateRowAccessPolicyArgsRequest(argName, argType) + + body := "true" + + 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) + + 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") + + rowAccessPolicy := createRowAccessPolicyWithRequest(t, request) + + assertRowAccessPolicy(t, rowAccessPolicy, request.GetName(), "some comment") + }) + + t.Run("drop row access policy: existing", func(t *testing.T) { + request := createRowAccessPolicyBasicRequest(t) + id := request.GetName() + + err := client.RowAccessPolicies.Create(ctx, request) + require.NoError(t, err) + + err = client.RowAccessPolicies.Drop(ctx, sdk.NewDropRowAccessPolicyRequest(id)) + require.NoError(t, err) + + _, err = client.RowAccessPolicies.ShowByID(ctx, id) + assert.ErrorIs(t, err, collections.ErrObjectNotFound) + }) + + t.Run("drop row access policy: non-existing", func(t *testing.T) { + id := sdk.NewSchemaObjectIdentifier(testDb(t).Name, testSchema(t).Name, "does_not_exist") + + err := client.RowAccessPolicies.Drop(ctx, sdk.NewDropRowAccessPolicyRequest(id)) + assert.ErrorIs(t, err, sdk.ErrObjectNotExistOrAuthorized) + }) + + t.Run("alter row access policy: rename", func(t *testing.T) { + createRequest := createRowAccessPolicyBasicRequest(t) + id := createRequest.GetName() + + err := client.RowAccessPolicies.Create(ctx, createRequest) + require.NoError(t, err) + + newName := random.String() + newId := sdk.NewSchemaObjectIdentifier(testDb(t).Name, testSchema(t).Name, newName) + alterRequest := sdk.NewAlterRowAccessPolicyRequest(id).WithRenameTo(&newId) + + err = client.RowAccessPolicies.Alter(ctx, alterRequest) + if err != nil { + t.Cleanup(cleanupRowAccessPolicyProvider(id)) + } else { + t.Cleanup(cleanupRowAccessPolicyProvider(newId)) + } + require.NoError(t, err) + + _, err = client.RowAccessPolicies.ShowByID(ctx, id) + assert.ErrorIs(t, err, collections.ErrObjectNotFound) + + rowAccessPolicy, err := client.RowAccessPolicies.ShowByID(ctx, newId) + require.NoError(t, err) + + assertRowAccessPolicy(t, rowAccessPolicy, newId, "") + }) + + t.Run("alter row access policy: set and unset comment", func(t *testing.T) { + rowAccessPolicy := createRowAccessPolicy(t) + id := rowAccessPolicy.ID() + + alterRequest := sdk.NewAlterRowAccessPolicyRequest(id).WithSetComment(sdk.String("new comment")) + err := client.RowAccessPolicies.Alter(ctx, alterRequest) + require.NoError(t, err) + + alteredRowAccessPolicy, err := client.RowAccessPolicies.ShowByID(ctx, id) + require.NoError(t, err) + + assert.Equal(t, "new comment", alteredRowAccessPolicy.Comment) + + alterRequest = sdk.NewAlterRowAccessPolicyRequest(id).WithUnsetComment(sdk.Bool(true)) + err = client.RowAccessPolicies.Alter(ctx, alterRequest) + require.NoError(t, err) + + alteredRowAccessPolicy, err = client.RowAccessPolicies.ShowByID(ctx, id) + require.NoError(t, err) + + assert.Equal(t, "", alteredRowAccessPolicy.Comment) + }) + + t.Run("alter row access policy: set body", func(t *testing.T) { + rowAccessPolicy := createRowAccessPolicy(t) + id := rowAccessPolicy.ID() + + alterRequest := sdk.NewAlterRowAccessPolicyRequest(id).WithSetBody(sdk.String("false")) + err := client.RowAccessPolicies.Alter(ctx, alterRequest) + require.NoError(t, err) + + alteredRowAccessPolicyDescription, err := client.RowAccessPolicies.Describe(ctx, id) + require.NoError(t, err) + + assert.Equal(t, "false", alteredRowAccessPolicyDescription.Body) + + alterRequest = sdk.NewAlterRowAccessPolicyRequest(id).WithSetBody(sdk.String("true")) + err = client.RowAccessPolicies.Alter(ctx, alterRequest) + require.NoError(t, err) + + alteredRowAccessPolicyDescription, err = client.RowAccessPolicies.Describe(ctx, id) + require.NoError(t, err) + + assert.Equal(t, "true", alteredRowAccessPolicyDescription.Body) + }) + + t.Run("alter row access policy: set and unset tags", func(t *testing.T) { + tag, tagCleanup := createTag(t, client, testDb(t), testSchema(t)) + t.Cleanup(tagCleanup) + + rowAccessPolicy := createRowAccessPolicy(t) + id := rowAccessPolicy.ID() + + tagValue := "abc" + tags := []sdk.TagAssociation{ + { + Name: tag.ID(), + Value: tagValue, + }, + } + alterRequestSetTags := sdk.NewAlterRowAccessPolicyRequest(id).WithSetTags(tags) + + err := client.RowAccessPolicies.Alter(ctx, alterRequestSetTags) + require.NoError(t, err) + + returnedTagValue, err := client.SystemFunctions.GetTag(ctx, tag.ID(), id, sdk.ObjectTypeRowAccessPolicy) + require.NoError(t, err) + + assert.Equal(t, tagValue, returnedTagValue) + + unsetTags := []sdk.ObjectIdentifier{ + tag.ID(), + } + alterRequestUnsetTags := sdk.NewAlterRowAccessPolicyRequest(id).WithUnsetTags(unsetTags) + + err = client.RowAccessPolicies.Alter(ctx, alterRequestUnsetTags) + require.NoError(t, err) + + _, err = client.SystemFunctions.GetTag(ctx, tag.ID(), id, sdk.ObjectTypeRowAccessPolicy) + require.Error(t, err) + }) + + t.Run("show row access policy: default", func(t *testing.T) { + rowAccessPolicy1 := createRowAccessPolicy(t) + rowAccessPolicy2 := createRowAccessPolicy(t) + + showRequest := sdk.NewShowRowAccessPolicyRequest() + returnedRowAccessPolicies, err := client.RowAccessPolicies.Show(ctx, showRequest) + require.NoError(t, err) + + assert.Equal(t, 2, len(returnedRowAccessPolicies)) + assert.Contains(t, returnedRowAccessPolicies, *rowAccessPolicy1) + assert.Contains(t, returnedRowAccessPolicies, *rowAccessPolicy2) + }) + + t.Run("show row access policy: with options", func(t *testing.T) { + rowAccessPolicy1 := createRowAccessPolicy(t) + rowAccessPolicy2 := createRowAccessPolicy(t) + + showRequest := sdk.NewShowRowAccessPolicyRequest(). + WithLike(&sdk.Like{Pattern: &rowAccessPolicy1.Name}). + WithIn(&sdk.In{Schema: sdk.NewDatabaseObjectIdentifier(testDb(t).Name, testSchema(t).Name)}) + returnedRowAccessPolicies, err := client.RowAccessPolicies.Show(ctx, showRequest) + + require.NoError(t, err) + assert.Equal(t, 1, len(returnedRowAccessPolicies)) + assert.Contains(t, returnedRowAccessPolicies, *rowAccessPolicy1) + assert.NotContains(t, returnedRowAccessPolicies, *rowAccessPolicy2) + }) + + t.Run("describe row access policy: existing", func(t *testing.T) { + argName := random.AlphaN(5) + argType := sdk.DataTypeVARCHAR + args := sdk.NewCreateRowAccessPolicyArgsRequest(argName, argType) + body := "true" + + request := createRowAccessPolicyRequest(t, []sdk.CreateRowAccessPolicyArgsRequest{*args}, body) + rowAccessPolicy := createRowAccessPolicyWithRequest(t, request) + + 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) + }) + + t.Run("describe row access policy: non-existing", func(t *testing.T) { + id := sdk.NewSchemaObjectIdentifier(testDb(t).Name, testSchema(t).Name, "does_not_exist") + + _, err := client.RowAccessPolicies.Describe(ctx, id) + assert.ErrorIs(t, err, sdk.ErrObjectNotExistOrAuthorized) + }) +} diff --git a/pkg/sdk/testint/setup_test.go b/pkg/sdk/testint/setup_test.go index 4508bfdbc9..2af6cbe572 100644 --- a/pkg/sdk/testint/setup_test.go +++ b/pkg/sdk/testint/setup_test.go @@ -11,6 +11,16 @@ import ( "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk/internal/random" ) +const ( + secondaryAccountProfile = "secondary_test_account" +) + +var ( + TestWarehouseName = "int_test_wh_" + random.UUID() + TestDatabaseName = "int_test_db_" + random.UUID() + TestSchemaName = "int_test_sc_" + random.UUID() +) + var itc integrationTestContext func TestMain(m *testing.M) { @@ -45,6 +55,12 @@ func cleanup() { if itc.schemaCleanup != nil { defer itc.schemaCleanup() } + if itc.secondaryDatabaseCleanup != nil { + defer itc.secondaryDatabaseCleanup() + } + if itc.secondarySchemaCleanup != nil { + defer itc.secondarySchemaCleanup() + } } type integrationTestContext struct { @@ -57,11 +73,21 @@ type integrationTestContext struct { schemaCleanup func() warehouse *sdk.Warehouse warehouseCleanup func() + + secondaryClient *sdk.Client + secondaryCtx context.Context + + secondaryDatabase *sdk.Database + secondaryDatabaseCleanup func() + secondarySchema *sdk.Schema + secondarySchemaCleanup func() + secondaryWarehouse *sdk.Warehouse + secondaryWarehouseCleanup func() } func (itc *integrationTestContext) initialize() error { log.Println("Initializing integration test context") - var err error + c, err := sdk.NewDefaultClient() if err != nil { return err @@ -90,12 +116,43 @@ func (itc *integrationTestContext) initialize() error { itc.warehouse = wh itc.warehouseCleanup = whCleanup + config, err := sdk.ProfileConfig(secondaryAccountProfile) + if err != nil { + return err + } + secondaryClient, err := sdk.NewClient(config) + if err != nil { + return err + } + itc.secondaryClient = secondaryClient + itc.secondaryCtx = context.Background() + + secondaryDb, secondaryDbCleanup, err := createDb(itc.secondaryClient, itc.secondaryCtx) + if err != nil { + return err + } + itc.secondaryDatabase = secondaryDb + itc.secondaryDatabaseCleanup = secondaryDbCleanup + + secondarySchema, secondarySchemaCleanup, err := createSc(itc.secondaryClient, itc.secondaryCtx, itc.database) + if err != nil { + return err + } + itc.secondarySchema = secondarySchema + itc.secondarySchemaCleanup = secondarySchemaCleanup + + secondaryWarehouse, secondaryWarehouseCleanup, err := createWh(itc.secondaryClient, itc.secondaryCtx) + if err != nil { + return err + } + itc.secondaryWarehouse = secondaryWarehouse + itc.secondaryWarehouseCleanup = secondaryWarehouseCleanup + return nil } func createDb(client *sdk.Client, ctx context.Context) (*sdk.Database, func(), error) { - name := "int_test_db_" + random.UUID() - id := sdk.NewAccountObjectIdentifier(name) + id := sdk.NewAccountObjectIdentifier(TestDatabaseName) err := client.Databases.Create(ctx, id, nil) if err != nil { return nil, nil, err @@ -107,21 +164,19 @@ func createDb(client *sdk.Client, ctx context.Context) (*sdk.Database, func(), e } func createSc(client *sdk.Client, ctx context.Context, db *sdk.Database) (*sdk.Schema, func(), error) { - name := "int_test_sc_" + random.UUID() - id := sdk.NewDatabaseObjectIdentifier(db.Name, name) + id := sdk.NewDatabaseObjectIdentifier(db.Name, TestSchemaName) err := client.Schemas.Create(ctx, id, nil) if err != nil { return nil, nil, err } - schema, err := client.Schemas.ShowByID(ctx, sdk.NewDatabaseObjectIdentifier(db.Name, name)) + schema, err := client.Schemas.ShowByID(ctx, sdk.NewDatabaseObjectIdentifier(db.Name, TestSchemaName)) return schema, func() { _ = client.Schemas.Drop(ctx, id, nil) }, err } func createWh(client *sdk.Client, ctx context.Context) (*sdk.Warehouse, func(), error) { - name := "int_test_wh_" + random.UUID() - id := sdk.NewAccountObjectIdentifier(name) + id := sdk.NewAccountObjectIdentifier(TestWarehouseName) err := client.Warehouses.Create(ctx, id, nil) if err != nil { return nil, nil, err @@ -167,3 +222,28 @@ func testWarehouse(t *testing.T) *sdk.Warehouse { t.Helper() return itc.warehouse } + +func testSecondaryClient(t *testing.T) *sdk.Client { + t.Helper() + return itc.secondaryClient +} + +func testSecondaryContext(t *testing.T) context.Context { + t.Helper() + return itc.secondaryCtx +} + +func testSecondaryDb(t *testing.T) *sdk.Database { + t.Helper() + return itc.secondaryDatabase +} + +func testSecondarySchema(t *testing.T) *sdk.Schema { + t.Helper() + return itc.secondarySchema +} + +func testSecondaryWarehouse(t *testing.T) *sdk.Warehouse { + t.Helper() + return itc.secondaryWarehouse +} diff --git a/pkg/sdk/testint/shares_integration_test.go b/pkg/sdk/testint/shares_integration_test.go index 1df89f1d1a..0050a1e43c 100644 --- a/pkg/sdk/testint/shares_integration_test.go +++ b/pkg/sdk/testint/shares_integration_test.go @@ -125,10 +125,10 @@ func TestInt_SharesDrop(t *testing.T) { func TestInt_SharesAlter(t *testing.T) { client := testClient(t) + secondaryClient := testSecondaryClient(t) ctx := testContext(t) t.Run("add and remove accounts", func(t *testing.T) { - t.Skipf("Snowflake secondary account is not configured. Must be set in ~./snowflake/config.yml with profile name: %s", secondaryAccountProfile) shareTest, shareCleanup := createShare(t, client) t.Cleanup(shareCleanup) err := client.Grants.GrantPrivilegeToShare(ctx, sdk.ObjectPrivilegeUsage, &sdk.GrantPrivilegeToShareOn{ @@ -141,7 +141,6 @@ func TestInt_SharesAlter(t *testing.T) { }, shareTest.ID()) }) require.NoError(t, err) - secondaryClient := testSecondaryClient(t) accountsToAdd := []sdk.AccountIdentifier{ getAccountIdentifier(t, secondaryClient), } @@ -184,37 +183,43 @@ func TestInt_SharesAlter(t *testing.T) { }) t.Run("set accounts", func(t *testing.T) { - t.Skipf("Snowflake secondary account is not configured. Must be set in ~./snowflake/config.yml with profile name: %s", secondaryAccountProfile) - shareTest, shareCleanup := createShare(t, client) + db, dbCleanup := createDatabase(t, secondaryClient) + t.Cleanup(dbCleanup) + + shareTest, shareCleanup := createShare(t, secondaryClient) t.Cleanup(shareCleanup) - err := client.Grants.GrantPrivilegeToShare(ctx, sdk.ObjectPrivilegeUsage, &sdk.GrantPrivilegeToShareOn{ - Database: testDb(t).ID(), + + err := secondaryClient.Grants.GrantPrivilegeToShare(ctx, sdk.ObjectPrivilegeUsage, &sdk.GrantPrivilegeToShareOn{ + Database: db.ID(), }, shareTest.ID()) require.NoError(t, err) t.Cleanup(func() { - err = client.Grants.RevokePrivilegeFromShare(ctx, sdk.ObjectPrivilegeUsage, &sdk.RevokePrivilegeFromShareOn{ - Database: testDb(t).ID(), + err := secondaryClient.Grants.RevokePrivilegeFromShare(ctx, sdk.ObjectPrivilegeUsage, &sdk.RevokePrivilegeFromShareOn{ + Database: db.ID(), }, shareTest.ID()) + require.NoError(t, err) }) - require.NoError(t, err) - secondaryClient := testSecondaryClient(t) + accountsToSet := []sdk.AccountIdentifier{ - getAccountIdentifier(t, secondaryClient), + getAccountIdentifier(t, client), } + // first add the account. - err = client.Shares.Alter(ctx, shareTest.ID(), &sdk.AlterShareOptions{ + err = secondaryClient.Shares.Alter(ctx, shareTest.ID(), &sdk.AlterShareOptions{ IfExists: sdk.Bool(true), Set: &sdk.ShareSet{ Accounts: accountsToSet, }, }) require.NoError(t, err) - shares, err := client.Shares.Show(ctx, &sdk.ShowShareOptions{ + + shares, err := secondaryClient.Shares.Show(ctx, &sdk.ShowShareOptions{ Like: &sdk.Like{ Pattern: sdk.String(shareTest.Name.Name()), }, }) require.NoError(t, err) + assert.Equal(t, 1, len(shares)) share := shares[0] assert.Equal(t, accountsToSet, share.To) @@ -223,6 +228,7 @@ func TestInt_SharesAlter(t *testing.T) { t.Run("set and unset comment", func(t *testing.T) { shareTest, shareCleanup := createShare(t, client) t.Cleanup(shareCleanup) + err := client.Grants.GrantPrivilegeToShare(ctx, sdk.ObjectPrivilegeUsage, &sdk.GrantPrivilegeToShareOn{ Database: testDb(t).ID(), }, shareTest.ID()) @@ -242,12 +248,14 @@ func TestInt_SharesAlter(t *testing.T) { }, }) require.NoError(t, err) + shares, err := client.Shares.Show(ctx, &sdk.ShowShareOptions{ Like: &sdk.Like{ Pattern: sdk.String(shareTest.Name.Name()), }, }) require.NoError(t, err) + assert.Equal(t, 1, len(shares)) share := shares[0] assert.Equal(t, comment, share.Comment) @@ -260,12 +268,14 @@ func TestInt_SharesAlter(t *testing.T) { }, }) require.NoError(t, err) + shares, err = client.Shares.Show(ctx, &sdk.ShowShareOptions{ Like: &sdk.Like{ Pattern: sdk.String(shareTest.Name.Name()), }, }) require.NoError(t, err) + assert.Equal(t, 1, len(shares)) share = shares[0] assert.Equal(t, "", share.Comment) @@ -346,39 +356,40 @@ func TestInt_ShareDescribeProvider(t *testing.T) { require.NoError(t, err) }) - t.Run("describe share by name", func(t *testing.T) { - shareDetails, err := client.Shares.DescribeProvider(ctx, shareTest.ID()) - require.NoError(t, err) - assert.Equal(t, 1, len(shareDetails.SharedObjects)) - sharedObject := shareDetails.SharedObjects[0] - assert.Equal(t, sdk.ObjectTypeDatabase, sharedObject.Kind) - assert.Equal(t, testDb(t).ID(), sharedObject.Name) - }) + shareDetails, err := client.Shares.DescribeProvider(ctx, shareTest.ID()) + require.NoError(t, err) + + assert.Equal(t, 1, len(shareDetails.SharedObjects)) + sharedObject := shareDetails.SharedObjects[0] + assert.Equal(t, sdk.ObjectTypeDatabase, sharedObject.Kind) + assert.Equal(t, testDb(t).ID(), sharedObject.Name) }) } func TestInt_ShareDescribeConsumer(t *testing.T) { - consumerClient := testSecondaryClient(t) ctx := testContext(t) - providerClient := testClient(t) + providerClient := testSecondaryClient(t) + consumerClient := testClient(t) t.Run("describe share", func(t *testing.T) { - t.Skipf("Snowflake secondary account is not configured. Must be set in ~./snowflake/config.yml with profile name: %s", secondaryAccountProfile) + db, dbCleanup := createDatabase(t, providerClient) + t.Cleanup(dbCleanup) + shareTest, shareCleanup := createShare(t, providerClient) t.Cleanup(shareCleanup) err := providerClient.Grants.GrantPrivilegeToShare(ctx, sdk.ObjectPrivilegeUsage, &sdk.GrantPrivilegeToShareOn{ - Database: testDb(t).ID(), + Database: db.ID(), }, shareTest.ID()) require.NoError(t, err) t.Cleanup(func() { err = providerClient.Grants.RevokePrivilegeFromShare(ctx, sdk.ObjectPrivilegeUsage, &sdk.RevokePrivilegeFromShareOn{ - Database: testDb(t).ID(), + Database: db.ID(), }, shareTest.ID()) require.NoError(t, err) }) - // add consumer account to share. + // add a consumer account to share. err = providerClient.Shares.Alter(ctx, shareTest.ID(), &sdk.AlterShareOptions{ Add: &sdk.ShareAdd{ Accounts: []sdk.AccountIdentifier{ @@ -387,13 +398,13 @@ func TestInt_ShareDescribeConsumer(t *testing.T) { }, }) require.NoError(t, err) - t.Run("describe consume share", func(t *testing.T) { - shareDetails, err := consumerClient.Shares.DescribeConsumer(ctx, shareTest.ExternalID()) - require.NoError(t, err) - assert.Equal(t, 1, len(shareDetails.SharedObjects)) - sharedObject := shareDetails.SharedObjects[0] - assert.Equal(t, sdk.ObjectTypeDatabase, sharedObject.Kind) - assert.Equal(t, sdk.NewAccountObjectIdentifier(""), sharedObject.Name) - }) + + shareDetails, err := consumerClient.Shares.DescribeConsumer(ctx, shareTest.ExternalID()) + require.NoError(t, err) + + assert.Equal(t, 1, len(shareDetails.SharedObjects)) + sharedObject := shareDetails.SharedObjects[0] + assert.Equal(t, sdk.ObjectTypeDatabase, sharedObject.Kind) + assert.Equal(t, sdk.NewAccountObjectIdentifier(""), sharedObject.Name) }) } diff --git a/pkg/sdk/testint/storage_integration_gen_integration_test.go b/pkg/sdk/testint/storage_integration_gen_integration_test.go new file mode 100644 index 0000000000..4240b993d0 --- /dev/null +++ b/pkg/sdk/testint/storage_integration_gen_integration_test.go @@ -0,0 +1,362 @@ +package testint + +import ( + "strconv" + "strings" + "testing" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk/internal/collections" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestInt_StorageIntegrations(t *testing.T) { + client := testClient(t) + ctx := testContext(t) + + if !hasExternalEnvironmentVariablesSet { + t.Skip("Skipping TestInt_StorageIntegrations (External env variables are not set)") + } + + assertStorageIntegrationShowResult := func(t *testing.T, s *sdk.StorageIntegration, name sdk.AccountObjectIdentifier, comment string) { + t.Helper() + assert.Equal(t, name.Name(), s.Name) + assert.Equal(t, true, s.Enabled) + assert.Equal(t, "EXTERNAL_STAGE", s.StorageType) + assert.Equal(t, "STORAGE", s.Category) + assert.Equal(t, comment, s.Comment) + } + + findProp := func(t *testing.T, props []sdk.StorageIntegrationProperty, name string) *sdk.StorageIntegrationProperty { + t.Helper() + prop, err := collections.FindOne(props, func(property sdk.StorageIntegrationProperty) bool { return property.Name == name }) + require.NoError(t, err) + return prop + } + + assertS3StorageIntegrationDescResult := func( + t *testing.T, + props []sdk.StorageIntegrationProperty, + enabled bool, + allowedLocations []sdk.StorageLocation, + blockedLocations []sdk.StorageLocation, + comment string, + ) { + t.Helper() + allowed := make([]string, len(allowedLocations)) + for i, a := range allowedLocations { + allowed[i] = a.Path + } + blocked := make([]string, len(blockedLocations)) + for i, b := range blockedLocations { + blocked[i] = b.Path + } + assert.Equal(t, "Boolean", findProp(t, props, "ENABLED").Type) + assert.Equal(t, strconv.FormatBool(enabled), findProp(t, props, "ENABLED").Value) + assert.Equal(t, "false", findProp(t, props, "ENABLED").Default) + assert.Equal(t, "S3", findProp(t, props, "STORAGE_PROVIDER").Value) + assert.Equal(t, strings.Join(allowed, ","), findProp(t, props, "STORAGE_ALLOWED_LOCATIONS").Value) + assert.Equal(t, strings.Join(blocked, ","), findProp(t, props, "STORAGE_BLOCKED_LOCATIONS").Value) + assert.NotEmpty(t, findProp(t, props, "STORAGE_AWS_IAM_USER_ARN").Value) + assert.NotEmpty(t, findProp(t, props, "STORAGE_AWS_ROLE_ARN").Value) + assert.NotEmpty(t, findProp(t, props, "STORAGE_AWS_EXTERNAL_ID").Value) + assert.Equal(t, comment, findProp(t, props, "COMMENT").Value) + } + + assertGCSStorageIntegrationDescResult := func( + t *testing.T, + props []sdk.StorageIntegrationProperty, + enabled bool, + allowedLocations []sdk.StorageLocation, + blockedLocations []sdk.StorageLocation, + comment string, + ) { + t.Helper() + allowed := make([]string, len(allowedLocations)) + for i, a := range allowedLocations { + allowed[i] = a.Path + } + blocked := make([]string, len(blockedLocations)) + for i, b := range blockedLocations { + blocked[i] = b.Path + } + assert.Equal(t, "Boolean", findProp(t, props, "ENABLED").Type) + assert.Equal(t, strconv.FormatBool(enabled), findProp(t, props, "ENABLED").Value) + assert.Equal(t, "false", findProp(t, props, "ENABLED").Default) + assert.Equal(t, "GCS", findProp(t, props, "STORAGE_PROVIDER").Value) + assert.Equal(t, strings.Join(allowed, ","), findProp(t, props, "STORAGE_ALLOWED_LOCATIONS").Value) + assert.Equal(t, strings.Join(blocked, ","), findProp(t, props, "STORAGE_BLOCKED_LOCATIONS").Value) + assert.NotEmpty(t, findProp(t, props, "STORAGE_GCP_SERVICE_ACCOUNT").Value) + assert.Equal(t, comment, findProp(t, props, "COMMENT").Value) + } + + assertAzureStorageIntegrationDescResult := func( + t *testing.T, + props []sdk.StorageIntegrationProperty, + enabled bool, + allowedLocations []sdk.StorageLocation, + blockedLocations []sdk.StorageLocation, + comment string, + ) { + t.Helper() + allowed := make([]string, len(allowedLocations)) + for i, a := range allowedLocations { + allowed[i] = a.Path + } + blocked := make([]string, len(blockedLocations)) + for i, b := range blockedLocations { + blocked[i] = b.Path + } + assert.Equal(t, "Boolean", findProp(t, props, "ENABLED").Type) + assert.Equal(t, strconv.FormatBool(enabled), findProp(t, props, "ENABLED").Value) + assert.Equal(t, "false", findProp(t, props, "ENABLED").Default) + assert.Equal(t, "AZURE", findProp(t, props, "STORAGE_PROVIDER").Value) + assert.Equal(t, strings.Join(allowed, ","), findProp(t, props, "STORAGE_ALLOWED_LOCATIONS").Value) + assert.Equal(t, strings.Join(blocked, ","), findProp(t, props, "STORAGE_BLOCKED_LOCATIONS").Value) + assert.NotEmpty(t, findProp(t, props, "AZURE_TENANT_ID").Value) + assert.NotEmpty(t, findProp(t, props, "AZURE_CONSENT_URL").Value) + assert.NotEmpty(t, findProp(t, props, "AZURE_MULTI_TENANT_APP_NAME").Value) + assert.Equal(t, comment, findProp(t, props, "COMMENT").Value) + } + + allowedLocations := func(prefix string) []sdk.StorageLocation { + return []sdk.StorageLocation{ + { + Path: prefix + "/allowed-location", + }, + { + Path: prefix + "/allowed-location2", + }, + } + } + s3AllowedLocations := allowedLocations(awsBucketUrl) + gcsAllowedLocations := allowedLocations(gcsBucketUrl) + azureAllowedLocations := allowedLocations(azureBucketUrl) + + blockedLocations := func(prefix string) []sdk.StorageLocation { + return []sdk.StorageLocation{ + { + Path: prefix + "/blocked-location", + }, + { + Path: prefix + "/blocked-location2", + }, + } + } + s3BlockedLocations := blockedLocations(awsBucketUrl) + gcsBlockedLocations := blockedLocations(gcsBucketUrl) + azureBlockedLocations := blockedLocations(azureBucketUrl) + + createS3StorageIntegration := func(t *testing.T) sdk.AccountObjectIdentifier { + t.Helper() + + id := sdk.RandomAccountObjectIdentifier() + req := sdk.NewCreateStorageIntegrationRequest(id, true, s3AllowedLocations). + WithIfNotExists(sdk.Bool(true)). + WithS3StorageProviderParams(sdk.NewS3StorageParamsRequest(awsRoleARN)). + WithStorageBlockedLocations(s3BlockedLocations). + WithComment(sdk.String("some comment")) + + err := client.StorageIntegrations.Create(ctx, req) + require.NoError(t, err) + + t.Cleanup(func() { + err := client.StorageIntegrations.Drop(ctx, sdk.NewDropStorageIntegrationRequest(id)) + require.NoError(t, err) + }) + + return id + } + + createGCSStorageIntegration := func(t *testing.T) sdk.AccountObjectIdentifier { + t.Helper() + + id := sdk.RandomAccountObjectIdentifier() + req := sdk.NewCreateStorageIntegrationRequest(id, true, gcsAllowedLocations). + WithIfNotExists(sdk.Bool(true)). + WithGCSStorageProviderParams(sdk.NewGCSStorageParamsRequest()). + WithStorageBlockedLocations(gcsBlockedLocations). + WithComment(sdk.String("some comment")) + + err := client.StorageIntegrations.Create(ctx, req) + require.NoError(t, err) + + t.Cleanup(func() { + err := client.StorageIntegrations.Drop(ctx, sdk.NewDropStorageIntegrationRequest(id)) + require.NoError(t, err) + }) + + return id + } + + createAzureStorageIntegration := func(t *testing.T) sdk.AccountObjectIdentifier { + t.Helper() + + id := sdk.RandomAccountObjectIdentifier() + req := sdk.NewCreateStorageIntegrationRequest(id, true, azureAllowedLocations). + WithIfNotExists(sdk.Bool(true)). + WithAzureStorageProviderParams(sdk.NewAzureStorageParamsRequest(sdk.String(azureTenantId))). + WithStorageBlockedLocations(azureBlockedLocations). + WithComment(sdk.String("some comment")) + + err := client.StorageIntegrations.Create(ctx, req) + require.NoError(t, err) + + t.Cleanup(func() { + err := client.StorageIntegrations.Drop(ctx, sdk.NewDropStorageIntegrationRequest(id)) + require.NoError(t, err) + }) + + return id + } + + t.Run("Create - S3", func(t *testing.T) { + id := createS3StorageIntegration(t) + + storageIntegration, err := client.StorageIntegrations.ShowByID(ctx, id) + require.NoError(t, err) + + assertStorageIntegrationShowResult(t, storageIntegration, id, "some comment") + }) + + t.Run("Create - GCS", func(t *testing.T) { + id := createGCSStorageIntegration(t) + + storageIntegration, err := client.StorageIntegrations.ShowByID(ctx, id) + require.NoError(t, err) + + assertStorageIntegrationShowResult(t, storageIntegration, id, "some comment") + }) + + t.Run("Create - Azure", func(t *testing.T) { + id := createAzureStorageIntegration(t) + + storageIntegration, err := client.StorageIntegrations.ShowByID(ctx, id) + require.NoError(t, err) + + assertStorageIntegrationShowResult(t, storageIntegration, id, "some comment") + }) + + t.Run("Alter - set - S3", func(t *testing.T) { + id := createS3StorageIntegration(t) + + changedS3AllowedLocations := append([]sdk.StorageLocation{{Path: awsBucketUrl + "/allowed-location3"}}, s3AllowedLocations...) + changedS3BlockedLocations := append([]sdk.StorageLocation{{Path: awsBucketUrl + "/blocked-location3"}}, s3BlockedLocations...) + req := sdk.NewAlterStorageIntegrationRequest(id). + WithSet( + sdk.NewStorageIntegrationSetRequest(). + WithSetS3Params(sdk.NewSetS3StorageParamsRequest(awsRoleARN)). + WithEnabled(true). + WithStorageAllowedLocations(changedS3AllowedLocations). + WithStorageBlockedLocations(changedS3BlockedLocations). + WithComment(sdk.String("changed comment")), + ) + err := client.StorageIntegrations.Alter(ctx, req) + require.NoError(t, err) + + props, err := client.StorageIntegrations.Describe(ctx, id) + require.NoError(t, err) + + assertS3StorageIntegrationDescResult(t, props, true, changedS3AllowedLocations, changedS3BlockedLocations, "changed comment") + }) + + t.Run("Alter - set - Azure", func(t *testing.T) { + id := createAzureStorageIntegration(t) + + changedAzureAllowedLocations := append([]sdk.StorageLocation{{Path: azureBucketUrl + "/allowed-location3"}}, azureAllowedLocations...) + changedAzureBlockedLocations := append([]sdk.StorageLocation{{Path: azureBucketUrl + "/blocked-location3"}}, azureBlockedLocations...) + req := sdk.NewAlterStorageIntegrationRequest(id). + WithSet( + sdk.NewStorageIntegrationSetRequest(). + WithSetAzureParams(sdk.NewSetAzureStorageParamsRequest(azureTenantId)). + WithEnabled(true). + WithStorageAllowedLocations(changedAzureAllowedLocations). + WithStorageBlockedLocations(changedAzureBlockedLocations). + WithComment(sdk.String("changed comment")), + ) + err := client.StorageIntegrations.Alter(ctx, req) + require.NoError(t, err) + + props, err := client.StorageIntegrations.Describe(ctx, id) + require.NoError(t, err) + + assertAzureStorageIntegrationDescResult(t, props, true, changedAzureAllowedLocations, changedAzureBlockedLocations, "changed comment") + }) + + t.Run("Alter - unset", func(t *testing.T) { + id := createS3StorageIntegration(t) + + req := sdk.NewAlterStorageIntegrationRequest(id). + WithUnset( + sdk.NewStorageIntegrationUnsetRequest(). + WithEnabled(sdk.Bool(true)). + WithStorageBlockedLocations(sdk.Bool(true)). + WithComment(sdk.Bool(true)), + ) + err := client.StorageIntegrations.Alter(ctx, req) + require.NoError(t, err) + + props, err := client.StorageIntegrations.Describe(ctx, id) + require.NoError(t, err) + + assertS3StorageIntegrationDescResult(t, props, false, s3AllowedLocations, []sdk.StorageLocation{}, "") + }) + + t.Run("Alter - set and unset tags", func(t *testing.T) { + id := createS3StorageIntegration(t) + + tag, tagCleanup := createTag(t, client, testDb(t), testSchema(t)) + t.Cleanup(tagCleanup) + + err := client.StorageIntegrations.Alter(ctx, sdk.NewAlterStorageIntegrationRequest(id). + WithSetTags([]sdk.TagAssociation{ + { + Name: tag.ID(), + Value: "tag-value", + }, + })) + require.NoError(t, err) + + tagValue, err := client.SystemFunctions.GetTag(ctx, tag.ID(), id, sdk.ObjectTypeIntegration) + require.NoError(t, err) + + assert.Equal(t, "tag-value", tagValue) + + err = client.StorageIntegrations.Alter(ctx, sdk.NewAlterStorageIntegrationRequest(id). + WithUnsetTags([]sdk.ObjectIdentifier{ + tag.ID(), + })) + require.NoError(t, err) + + _, err = client.SystemFunctions.GetTag(ctx, tag.ID(), id, sdk.ObjectTypeIntegration) + require.Error(t, err, sdk.ErrObjectNotExistOrAuthorized) + }) + + t.Run("Describe - S3", func(t *testing.T) { + id := createS3StorageIntegration(t) + + desc, err := client.StorageIntegrations.Describe(ctx, id) + require.NoError(t, err) + + assertS3StorageIntegrationDescResult(t, desc, true, s3AllowedLocations, s3BlockedLocations, "some comment") + }) + + t.Run("Describe - GCS", func(t *testing.T) { + id := createGCSStorageIntegration(t) + + desc, err := client.StorageIntegrations.Describe(ctx, id) + require.NoError(t, err) + + assertGCSStorageIntegrationDescResult(t, desc, true, gcsAllowedLocations, gcsBlockedLocations, "some comment") + }) + + t.Run("Describe - Azure", func(t *testing.T) { + id := createAzureStorageIntegration(t) + + desc, err := client.StorageIntegrations.Describe(ctx, id) + require.NoError(t, err) + + assertAzureStorageIntegrationDescResult(t, desc, true, azureAllowedLocations, azureBlockedLocations, "some comment") + }) +} diff --git a/pkg/sdk/testint/tables_integration_test.go b/pkg/sdk/testint/tables_integration_test.go index a8275d32b9..5198b5eec1 100644 --- a/pkg/sdk/testint/tables_integration_test.go +++ b/pkg/sdk/testint/tables_integration_test.go @@ -209,7 +209,7 @@ func TestInt_Table(t *testing.T) { assertColumns(t, expectedColumns, tableColumns) }) - // TODO: fix this test, it should create two integer column but is creating 3 text ones instead + // TODO [SNOW-1007542]: fix this test, it should create two integer column but is creating 3 text ones instead t.Run("create table using template", func(t *testing.T) { fileFormat, fileFormatCleanup := createFileFormat(t, client, schema.ID()) t.Cleanup(fileFormatCleanup) @@ -645,7 +645,7 @@ func TestInt_Table(t *testing.T) { assert.Equal(t, table.Comment, "") }) - // TODO: check added constraints + // TODO [SNOW-1007542]: check added constraints // Add method similar to getTableColumnsFor based on https://docs.snowflake.com/en/sql-reference/info-schema/table_constraints. t.Run("alter constraint: add", func(t *testing.T) { name := random.String() @@ -676,7 +676,7 @@ func TestInt_Table(t *testing.T) { require.NoError(t, err) }) - // TODO: check renamed constraint + // TODO [SNOW-1007542]: check renamed constraint t.Run("alter constraint: rename", func(t *testing.T) { name := random.String() id := sdk.NewSchemaObjectIdentifier(database.Name, schema.Name, name) @@ -701,7 +701,7 @@ func TestInt_Table(t *testing.T) { require.NoError(t, err) }) - // TODO: check altered constraint + // TODO [SNOW-1007542]: check altered constraint t.Run("alter constraint: alter", func(t *testing.T) { t.Skip("Test is failing: generated statement is not compiling but it is aligned with Snowflake docs https://docs.snowflake.com/en/sql-reference/sql/alter-table#syntax. Requires further investigation.") name := random.String() @@ -723,7 +723,7 @@ func TestInt_Table(t *testing.T) { require.NoError(t, err) }) - // TODO: check dropped constraint + // TODO [SNOW-1007542]: check dropped constraint t.Run("alter constraint: drop", func(t *testing.T) { t.Skip("Test is failing: generated statement is not compiling but it is aligned with Snowflake docs https://docs.snowflake.com/en/sql-reference/sql/alter-table#syntax. Requires further investigation.") name := random.String() @@ -830,7 +830,7 @@ func TestInt_Table(t *testing.T) { assertColumns(t, expectedColumns, currentColumns) }) - // TODO: check search optimization - after adding https://docs.snowflake.com/en/sql-reference/sql/desc-search-optimization + // TODO [SNOW-1007542]: check search optimization - after adding https://docs.snowflake.com/en/sql-reference/sql/desc-search-optimization t.Run("add search optimization", func(t *testing.T) { name := random.String() id := sdk.NewSchemaObjectIdentifier(database.Name, schema.Name, name) @@ -850,7 +850,7 @@ func TestInt_Table(t *testing.T) { require.NoError(t, err) }) - // TODO: try to check more sets (ddl collation, max data extension time in days, etc.) + // TODO [SNOW-1007542]: try to check more sets (ddl collation, max data extension time in days, etc.) t.Run("set: with complete options", func(t *testing.T) { name := random.String() id := sdk.NewSchemaObjectIdentifier(database.Name, schema.Name, name) diff --git a/pkg/snowflake/masking_policy_application.go b/pkg/snowflake/masking_policy_application.go index 8e3c486d4b..ca8d50b305 100644 --- a/pkg/snowflake/masking_policy_application.go +++ b/pkg/snowflake/masking_policy_application.go @@ -38,10 +38,10 @@ func (m *TableColumnMaskingPolicyApplicationManager) Read(x *TableColumnMaskingP } func (m *TableColumnMaskingPolicyApplicationManager) Parse(rows *sql.Rows, column string) (string, error) { - var name, sqlType, kind, null, defaultValue, primaryKey, uniqueKey, check, expression, comment, policyName sql.NullString + var name, sqlType, kind, null, defaultValue, primaryKey, uniqueKey, check, expression, comment, policyName, privacyDomain sql.NullString for rows.Next() { - if err := rows.Scan(&name, &sqlType, &kind, &null, &defaultValue, &primaryKey, &uniqueKey, &check, &expression, &comment, &policyName); err != nil { + if err := rows.Scan(&name, &sqlType, &kind, &null, &defaultValue, &primaryKey, &uniqueKey, &check, &expression, &comment, &policyName, &privacyDomain); err != nil { return "", err } diff --git a/pkg/validation/validation.go b/pkg/validation/validation.go index 1ea65cb8d6..6c82bb27f7 100644 --- a/pkg/validation/validation.go +++ b/pkg/validation/validation.go @@ -154,25 +154,6 @@ func ValidateAdminName(i interface{}, k string) (s []string, errors []error) { return } -func ValidateFullyQualifiedObjectID(i interface{}, _ string) (s []string, errors []error) { - v, _ := i.(string) - if strings.Contains(v, ".") { //nolint:gocritic // todo: please fix this - tagArray := strings.Split(v, ".") - if len(tagArray) != 3 { - errors = append(errors, fmt.Errorf("%v, is not a valid id. If using period delimiter, three parts must be specified ..", v)) - } - } else if strings.Contains(v, "|") { - tagArray := strings.Split(v, "|") - if len(tagArray) != 3 { - errors = append(errors, fmt.Errorf("%v, is not a valid id. If using pipe delimiter, three parts must be specified ||", v)) - } - } else { - errors = append(errors, fmt.Errorf("%v, is not a valid id. please use one of the following formats:"+ - "\n''.''.'' or ||", v)) - } - return -} - func FormatFullyQualifiedObjectID(dbName, schemaName, objectName string) string { var n strings.Builder diff --git a/templates/resources/grant_privileges_to_database_role.md.tmpl b/templates/resources/grant_privileges_to_database_role.md.tmpl new file mode 100644 index 0000000000..6423a74cab --- /dev/null +++ b/templates/resources/grant_privileges_to_database_role.md.tmpl @@ -0,0 +1,99 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "{{.Name}} {{.Type}} - {{.ProviderName}}" +subcategory: "" +description: |- +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} +--- + +{{/* SNOW-990811 */}} +!> **Warning** Be careful when using `always_apply` field. It will always produce a plan (even when no changes were made) and can be harmful in some setups. For more details why we decided to introduce it to go our document explaining those design decisions (coming soon). + +# {{.Name}} ({{.Type}}) + +{{ .Description | trimspace }} + +{{ if .HasExample -}} +## Example Usage + +{{ tffile (printf "examples/resources/%s/resource.tf" .Name)}} +{{- end }} + +{{ .SchemaMarkdown | trimspace }} + +## Import + +~> **Note** All the ..._name parts should be fully qualified names, e.g. for database object it is `"".""` +~> **Note** To import all_privileges write ALL or ALL PRIVILEGES in place of `` + +Import is supported using the following syntax: + +`terraform import "|||||"` + +where: +- database_role_name - fully qualified identifier +- with_grant_option - boolean +- always_apply - boolean +- privileges - list of privileges, comma separated; to import all_privileges write "ALL" or "ALL PRIVILEGES" +- grant_type - enum +- grant_data - enum data + +It has varying number of parts, depending on grant_type. All the possible types are: + +### OnDatabase +`terraform import "||||OnDatabase|"` + +### OnSchema + +On schema contains inner types for all options. + +#### OnSchema +`terraform import "||||OnSchema|OnSchema|"` + +#### OnAllSchemasInDatabase +`terraform import "||||OnSchema|OnAllSchemasInDatabase|"` + +#### OnFutureSchemasInDatabase +`terraform import "||||OnSchema|OnFutureSchemasInDatabase|"` + +### OnSchemaObject + +On schema object contains inner types for all options. + +#### OnObject +`terraform import "||||OnSchemaObject|OnObject||"` + +#### OnAll + +On all contains inner types for all options. + +##### InDatabase +`terraform import "||||OnSchemaObject|OnAll||InDatabase|"` + +##### InSchema +`terraform import "||||OnSchemaObject|OnAll||InSchema|"` + +#### OnFuture + +On future contains inner types for all options. + +##### InDatabase +`terraform import "||||OnSchemaObject|OnFuture||InDatabase|"` + +##### InSchema +`terraform import "||||OnSchemaObject|OnFuture||InSchema|"` + +### Import examples + +#### Grant all privileges OnDatabase +`terraform import "\"test_db\".\"test_db_role\"|false|false|ALL|OnDatabase|\"test_db\""` + +#### Grant list of privileges OnAllSchemasInDatabase +`terraform import "\"test_db\".\"test_db_role\"|false|false|CREATE TAG,CREATE TABLE|OnSchema|OnAllSchemasInDatabase|\"test_db\""` + +#### Grant list of privileges on table +`terraform import "\"test_db\".\"test_db_role\"|false|false|SELECT,DELETE,INSERT|OnSchemaObject|OnObject|TABLE|\"test_db\".\"test_schema\".\"test_table\""` + +#### Grant list of privileges OnAll tables in schema +`terraform import "\"test_db\".\"test_db_role\"|false|false|SELECT,DELETE,INSERT|OnSchemaObject|OnAll|TABLES|InSchema|\"test_db\".\"test_schema\""` +