From 04f6d54a83cf4e9ea4b292087eefa056114eb5b5 Mon Sep 17 00:00:00 2001 From: Jakub Michalak Date: Thu, 5 Dec 2024 10:46:23 +0100 Subject: [PATCH] feat: Tag association v1 readiness (#3210) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - rework `tag_association` resource - return `nil` from GetTag instead of failing - add more tests regarding tag/masking policy: assert that `ALTER MASKING POLICY SET TAG` differs from `ALTER TAG SET MASKING POLICY` - support tagging account for identifiers with org name - support `IF EXISTS` for unsetting tags - add notes about manually unassigning policies from objects, add a todo with an issue number - fix a wrong issue number in essential objects list ## Test Plan * [x] acceptance tests * [ ] … ## References https://docs.snowflake.com/en/user-guide/object-tagging https://docs.snowflake.com/en/sql-reference/functions/system_get_tag #3145 #1910 #2943 #3235 ## TODO - use generated config and asserts, remove old test `tf` files ## Ideas - extract a separate resource for tagging accounts? --- MIGRATION_GUIDE.md | 78 ++- docs/resources/tag_association.md | 76 +-- .../snowflake_tag_association/import.sh | 3 +- .../snowflake_tag_association/resource.tf | 41 +- pkg/acceptance/bettertestspoc/README.md | 15 +- .../resourceassert/gen/resource_schema_def.go | 4 + .../tag_association_resource_ext.go | 17 + .../tag_association_resource_gen.go | 97 +++ .../assert/resourceassert/tag_resource_gen.go | 4 +- .../model/primary_connection_model_gen.go | 11 + .../stream_on_directory_table_model_gen.go | 22 + .../stream_on_external_table_model_gen.go | 22 + .../config/model/stream_on_table_model_gen.go | 22 + .../config/model/stream_on_view_model_gen.go | 22 + .../config/model/tag_association_model_ext.go | 16 + .../config/model/tag_association_model_gen.go | 120 ++++ .../config/model/tag_model_gen.go | 2 +- pkg/acceptance/check_destroy.go | 70 ++ pkg/acceptance/helpers/tag_client.go | 24 + pkg/resources/grant_ownership.go | 9 +- pkg/resources/grant_ownership_identifier.go | 2 +- pkg/resources/grant_ownership_test.go | 2 +- pkg/resources/helper_expansion.go | 26 + pkg/resources/helpers.go | 44 +- pkg/resources/helpers_test.go | 77 +++ pkg/resources/tag_association.go | 320 +++++---- .../tag_association_acceptance_test.go | 607 ++++++++++++++---- .../tag_association_state_upgraders.go | 38 ++ pkg/resources/tag_association_test.go | 105 ++- .../TestAcc_TagAssociation/basic/test.tf | 12 +- .../TestAcc_TagAssociation/basic/variables.tf | 8 + .../TestAcc_TagAssociation/column/test.tf | 11 +- .../column/variables.tf | 8 + .../TestAcc_TagAssociation/issue1202/main.tf | 17 +- .../TestAcc_TagAssociation/issue1909/test.tf | 18 +- .../issue1909/variables.tf | 8 + .../TestAcc_TagAssociation/issue1910/test.tf | 22 +- .../issue1910/variables.tf | 2 +- .../TestAcc_TagAssociation/issue1926/test.tf | 19 +- .../issue1926/variables.tf | 4 + .../TestAcc_TagAssociation/schema/test.tf | 7 +- .../schema/variables.tf | 4 + pkg/sdk/object_types.go | 77 +++ pkg/sdk/object_types_test.go | 105 +++ pkg/sdk/shares.go | 1 + pkg/sdk/system_functions.go | 16 +- pkg/sdk/tags.go | 1 + pkg/sdk/tags_dto.go | 1 + pkg/sdk/tags_dto_builders.go | 5 + pkg/sdk/tags_impl.go | 1 + pkg/sdk/tags_test.go | 34 +- pkg/sdk/testint/databases_integration_test.go | 6 +- pkg/sdk/testint/roles_integration_test.go | 4 +- pkg/sdk/testint/schemas_integration_test.go | 6 +- .../testint/streams_gen_integration_test.go | 2 +- .../system_functions_integration_test.go | 6 +- pkg/sdk/testint/tags_integration_test.go | 130 ++-- pkg/sdk/testint/tasks_gen_integration_test.go | 2 +- .../testint/warehouses_integration_test.go | 4 +- templates/resources/tag_association.md.tmpl | 43 ++ 60 files changed, 1953 insertions(+), 527 deletions(-) create mode 100644 pkg/acceptance/bettertestspoc/assert/resourceassert/tag_association_resource_ext.go create mode 100644 pkg/acceptance/bettertestspoc/assert/resourceassert/tag_association_resource_gen.go create mode 100644 pkg/acceptance/bettertestspoc/config/model/tag_association_model_ext.go create mode 100644 pkg/acceptance/bettertestspoc/config/model/tag_association_model_gen.go create mode 100644 pkg/resources/tag_association_state_upgraders.go create mode 100644 pkg/sdk/object_types_test.go create mode 100644 templates/resources/tag_association.md.tmpl diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index ad1af3ec80..a73c7d1d0d 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -6,7 +6,77 @@ across different versions. > [!TIP] > We highly recommend upgrading the versions one by one instead of bulk upgrades. - + +## v0.99.0 ➞ v0.100.0 + +### snowflake_tag_association resource changes +#### *(behavior change)* new id format +In order to provide more functionality for tagging objects, we have changed the resource id from `"TAG_DATABASE"."TAG_SCHEMA"."TAG_NAME"` to `"TAG_DATABASE"."TAG_SCHEMA"."TAG_NAME"|TAG_VALUE|OBJECT_TYPE`. This allows to group tags associations per tag ID, tag value and object type in one resource. +``` +resource "snowflake_tag_association" "gold_warehouses" { + object_identifiers = [snowflake_warehouse.w1.fully_qualified_name, snowflake_warehouse.w2.fully_qualified_name] + object_type = "WAREHOUSE" + tag_id = snowflake_tag.tier.fully_qualified_name + tag_value = "gold" +} +resource "snowflake_tag_association" "silver_warehouses" { + object_identifiers = [snowflake_warehouse.w3.fully_qualified_name] + object_type = "WAREHOUSE" + tag_id = snowflake_tag.tier.fully_qualified_name + tag_value = "silver" +} +resource "snowflake_tag_association" "silver_databases" { + object_identifiers = [snowflake_database.d1.fully_qualified_name] + object_type = "DATABASE" + tag_id = snowflake_tag.tier.fully_qualified_name + tag_value = "silver" +} +``` + +Note that if you want to promote silver instances to gold, you can not simply change `tag_value` in `silver_warehouses`. Instead, you should first remove `object_identifiers` from `silver_warehouses`, run `terraform apply`, and then add the relevant `object_identifiers` in `gold_warehouses`, like this (note that `silver_warehouses` resource was deleted): +``` +resource "snowflake_tag_association" "gold_warehouses" { + object_identifiers = [snowflake_warehouse.w1.fully_qualified_name, snowflake_warehouse.w2.fully_qualified_name, snowflake_warehouse.w3.fully_qualified_name] + object_type = "WAREHOUSE" + tag_id = snowflake_tag.tier.fully_qualified_name + tag_value = "gold" +} +``` +and run `terraform apply` again. + +Note that the order of operations is not deterministic in this case, and if you do these operations in one step, it is possible that the tag value will be changed first, and unset later because of removing the resource with old value. + +The state is migrated automatically. There is no need to adjust configuration files, unless you use resource id `snowflake_tag_association.example.id` as a reference in other resources. + +#### *(behavior change)* changed fields +Behavior of some fields was changed: +- `object_identifier` was renamed to `object_identifiers` and it is now a set of fully qualified names. Change your configurations from +``` +resource "snowflake_tag_association" "table_association" { + object_identifier { + name = snowflake_table.test.name + database = snowflake_database.test.name + schema = snowflake_schema.test.name + } + object_type = "TABLE" + tag_id = snowflake_tag.test.fully_qualified_name + tag_value = "engineering" +} +``` +to +``` +resource "snowflake_tag_association" "table_association" { + object_identifiers = [snowflake_table.test.fully_qualified_name] + object_type = "TABLE" + tag_id = snowflake_tag.test.fully_qualified_name + tag_value = "engineering" +} +``` +- `tag_id` has now suppressed identifier quoting to prevent issues with Terraform showing permament differences, like [this one](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2982) +- `object_type` and `tag_id` are now marked as ForceNew + +The state is migrated automatically. Please adjust your configuration files. + ## v0.98.0 ➞ v0.99.0 ### snowflake_tasks data source changes @@ -39,7 +109,7 @@ data "snowflake_tasks" "new_tasks" { in { # for IN SCHEMA specify: schema = "." - + # for IN DATABASE specify: database = "" } @@ -65,7 +135,7 @@ New fields: - `config` - enables to specify JSON-formatted metadata that can be retrieved in the `sql_statement` by using [SYSTEM$GET_TASK_GRAPH_CONFIG](https://docs.snowflake.com/en/sql-reference/functions/system_get_task_graph_config). - `show_output` and `parameters` fields added for holding SHOW and SHOW PARAMETERS output (see [raw Snowflake output](./v1-preparations/CHANGES_BEFORE_V1.md#raw-snowflake-output)). - Added support for finalizer tasks with `finalize` field. It conflicts with `after` and `schedule` (see [finalizer tasks](https://docs.snowflake.com/en/user-guide/tasks-graphs#release-and-cleanup-of-task-graphs)). - + Changes: - `enabled` field changed to `started` and type changed to string with only boolean values available (see ["empty" values](./v1-preparations/CHANGES_BEFORE_V1.md#empty-values)). It is also now required field, so make sure it's explicitly set (previously it was optional with the default value set to `false`). - `allow_overlapping_execution` type was changed to string with only boolean values available (see ["empty" values](./v1-preparations/CHANGES_BEFORE_V1.md#empty-values)). Previously, it had the default set to `false` which will be migrated. If nothing will be set the provider will plan the change to `default` value. If you want to make sure it's turned off, set it explicitly to `false`. @@ -132,7 +202,7 @@ resource "snowflake_task" "example" { ``` - `after` field type was changed from `list` to `set` and the values were changed from names to fully qualified names. - + Before: ```terraform resource "snowflake_task" "example" { diff --git a/docs/resources/tag_association.md b/docs/resources/tag_association.md index 77acc40091..fef230c0c4 100644 --- a/docs/resources/tag_association.md +++ b/docs/resources/tag_association.md @@ -2,12 +2,20 @@ page_title: "snowflake_tag_association Resource - terraform-provider-snowflake" subcategory: "" description: |- - + Resource used to manage tag associations. For more information, check object tagging documentation https://docs.snowflake.com/en/user-guide/object-tagging. --- -# snowflake_tag_association (Resource) +!> **V1 release candidate** This resource was reworked and is a release candidate for the V1. We do not expect significant changes in it before the V1. We will welcome any feedback and adjust the resource if needed. Any errors reported will be resolved with a higher priority. We encourage checking this resource out before the V1 release. Please follow the [migration guide](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/MIGRATION_GUIDE.md#v0980--v0990) to use it. + +-> **Note** For `ACCOUNT` object type, only identifiers with organization name are supported. See [account identifier docs](https://docs.snowflake.com/en/user-guide/admin-account-identifier#format-1-preferred-account-name-in-your-organization) for more details. + +-> **Note** Tag association resource ID has the following format: `"TAG_DATABASE"."TAG_SCHEMA"."TAG_NAME"|TAG_VALUE|OBJECT_TYPE`. This means that a tuple of tag ID, tag value and object type should be unique across the resources. If you want to specify this combination for more than one object, you should use only one `tag_association` resource with specified `object_identifiers` set. +-> **Note** If you want to change tag value to a value that is already present in another `tag_association` resource, first remove the relevant `object_identifiers` from the resource with the old value, run `terraform apply`, then add the relevant `object_identifiers` in the resource with new value, and run `terrafrom apply` once again. + +# snowflake_tag_association (Resource) +Resource used to manage tag associations. For more information, check [object tagging documentation](https://docs.snowflake.com/en/user-guide/object-tagging). ## Example Usage @@ -29,12 +37,10 @@ resource "snowflake_tag" "test" { } resource "snowflake_tag_association" "db_association" { - object_identifier { - name = snowflake_database.test.name - } - object_type = "DATABASE" - tag_id = snowflake_tag.test.id - tag_value = "finance" + object_identifiers = [snowflake_database.test.fully_qualified_name] + object_type = "DATABASE" + tag_id = snowflake_tag.test.fully_qualified_name + tag_value = "finance" } resource "snowflake_table" "test" { @@ -53,28 +59,26 @@ resource "snowflake_table" "test" { } resource "snowflake_tag_association" "table_association" { - object_identifier { - name = snowflake_table.test.name - database = snowflake_database.test.name - schema = snowflake_schema.test.name - } - object_type = "TABLE" - tag_id = snowflake_tag.test.id - tag_value = "engineering" + object_identifiers = [snowflake_table.test.fully_qualified_name] + object_type = "TABLE" + tag_id = snowflake_tag.test.fully_qualified_name + tag_value = "engineering" } resource "snowflake_tag_association" "column_association" { - object_identifier { - name = "${snowflake_table.test.name}.column_name" - database = snowflake_database.test.name - schema = snowflake_schema.test.name - } - object_type = "COLUMN" - tag_id = snowflake_tag.test.id - tag_value = "engineering" + object_identifiers = [snowflake_database.test.fully_qualified_name] + object_type = "COLUMN" + tag_id = snowflake_tag.test.fully_qualified_name + tag_value = "engineering" } -``` +resource "snowflake_tag_association" "account_association" { + object_identifiers = ["\"ORGANIZATION_NAME\".\"ACCOUNT_NAME\""] + object_type = "ACCOUNT" + tag_id = snowflake_tag.test.fully_qualified_name + tag_value = "engineering" +} +``` -> **Note** Instead of using fully_qualified_name, you can reference objects managed outside Terraform by constructing a correct ID, consult [identifiers guide](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/guides/identifiers#new-computed-fully-qualified-name-field-in-resources). @@ -83,9 +87,9 @@ resource "snowflake_tag_association" "column_association" { ### Required -- `object_identifier` (Block List, Min: 1) Specifies the object identifier for the tag association. (see [below for nested schema](#nestedblock--object_identifier)) +- `object_identifiers` (Set of String) Specifies the object identifiers for the tag association. - `object_type` (String) Specifies the type of object to add a tag. Allowed object types: [ACCOUNT APPLICATION APPLICATION PACKAGE DATABASE FAILOVER GROUP INTEGRATION NETWORK POLICY REPLICATION GROUP ROLE SHARE USER WAREHOUSE DATABASE ROLE SCHEMA ALERT SNOWFLAKE.CORE.BUDGET SNOWFLAKE.ML.CLASSIFICATION EXTERNAL FUNCTION EXTERNAL TABLE FUNCTION GIT REPOSITORY ICEBERG TABLE MATERIALIZED VIEW PIPE MASKING POLICY PASSWORD POLICY ROW ACCESS POLICY SESSION POLICY PRIVACY POLICY PROCEDURE STAGE STREAM TABLE TASK VIEW COLUMN EVENT TABLE]. -- `tag_id` (String) 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) +- `tag_id` (String) Specifies the identifier for the tag. - `tag_value` (String) Specifies the value of the tag, (e.g. 'finance' or 'engineering') ### Optional @@ -98,19 +102,6 @@ resource "snowflake_tag_association" "column_association" { - `id` (String) The ID of this resource. - -### Nested Schema for `object_identifier` - -Required: - -- `name` (String) Name of the object to associate the tag with. - -Optional: - -- `database` (String) Name of the database that the object was created in. -- `schema` (String) Name of the schema that the object was created in. - - ### Nested Schema for `timeouts` @@ -120,9 +111,10 @@ Optional: ## Import +~> **Note** Due to technical limitations of Terraform SDK, `object_identifiers` are not set during import state. Please run `terraform refresh` after importing to get this field populated. + Import is supported using the following syntax: ```shell -# format is dbName.schemaName.tagName or dbName.schemaName.tagName -terraform import snowflake_tag_association.example 'dbName.schemaName.tagName' +terraform import snowflake_tag_association.example '"TAG_DATABASE"."TAG_SCHEMA"."TAG_NAME"|TAG_VALUE|OBJECT_TYPE' ``` diff --git a/examples/resources/snowflake_tag_association/import.sh b/examples/resources/snowflake_tag_association/import.sh index 8b55fc9a15..a3339e11e9 100644 --- a/examples/resources/snowflake_tag_association/import.sh +++ b/examples/resources/snowflake_tag_association/import.sh @@ -1,2 +1 @@ -# format is dbName.schemaName.tagName or dbName.schemaName.tagName -terraform import snowflake_tag_association.example 'dbName.schemaName.tagName' +terraform import snowflake_tag_association.example '"TAG_DATABASE"."TAG_SCHEMA"."TAG_NAME"|TAG_VALUE|OBJECT_TYPE' diff --git a/examples/resources/snowflake_tag_association/resource.tf b/examples/resources/snowflake_tag_association/resource.tf index 36d5fbf7de..00a3cc1324 100644 --- a/examples/resources/snowflake_tag_association/resource.tf +++ b/examples/resources/snowflake_tag_association/resource.tf @@ -15,12 +15,10 @@ resource "snowflake_tag" "test" { } resource "snowflake_tag_association" "db_association" { - object_identifier { - name = snowflake_database.test.name - } - object_type = "DATABASE" - tag_id = snowflake_tag.test.id - tag_value = "finance" + object_identifiers = [snowflake_database.test.fully_qualified_name] + object_type = "DATABASE" + tag_id = snowflake_tag.test.fully_qualified_name + tag_value = "finance" } resource "snowflake_table" "test" { @@ -39,23 +37,22 @@ resource "snowflake_table" "test" { } resource "snowflake_tag_association" "table_association" { - object_identifier { - name = snowflake_table.test.name - database = snowflake_database.test.name - schema = snowflake_schema.test.name - } - object_type = "TABLE" - tag_id = snowflake_tag.test.id - tag_value = "engineering" + object_identifiers = [snowflake_table.test.fully_qualified_name] + object_type = "TABLE" + tag_id = snowflake_tag.test.fully_qualified_name + tag_value = "engineering" } resource "snowflake_tag_association" "column_association" { - object_identifier { - name = "${snowflake_table.test.name}.column_name" - database = snowflake_database.test.name - schema = snowflake_schema.test.name - } - object_type = "COLUMN" - tag_id = snowflake_tag.test.id - tag_value = "engineering" + object_identifiers = [snowflake_database.test.fully_qualified_name] + object_type = "COLUMN" + tag_id = snowflake_tag.test.fully_qualified_name + tag_value = "engineering" +} + +resource "snowflake_tag_association" "account_association" { + object_identifiers = ["\"ORGANIZATION_NAME\".\"ACCOUNT_NAME\""] + object_type = "ACCOUNT" + tag_id = snowflake_tag.test.fully_qualified_name + tag_value = "engineering" } diff --git a/pkg/acceptance/bettertestspoc/README.md b/pkg/acceptance/bettertestspoc/README.md index 82a7b98f17..a7c4d4eec0 100644 --- a/pkg/acceptance/bettertestspoc/README.md +++ b/pkg/acceptance/bettertestspoc/README.md @@ -217,7 +217,7 @@ it will result in: object WAREHOUSE["XHZJCKAT_35D0BCC1_7797_974E_ACAF_C622C56FA2D2"] assertion [7/13]: failed with error: expected scaling policy: ECONOMY; got: STANDARD object WAREHOUSE["XHZJCKAT_35D0BCC1_7797_974E_ACAF_C622C56FA2D2"] assertion [8/13]: failed with error: expected auto suspend: 123; got: 600 object WAREHOUSE["XHZJCKAT_35D0BCC1_7797_974E_ACAF_C622C56FA2D2"] assertion [9/13]: failed with error: expected auto resume: false; got: true - object WAREHOUSE["XHZJCKAT_35D0BCC1_7797_974E_ACAF_C622C56FA2D2"] assertion [10/13]: failed with error: expected resource monitor: some-id; got: + object WAREHOUSE["XHZJCKAT_35D0BCC1_7797_974E_ACAF_C622C56FA2D2"] assertion [10/13]: failed with error: expected resource monitor: some-id; got: object WAREHOUSE["XHZJCKAT_35D0BCC1_7797_974E_ACAF_C622C56FA2D2"] assertion [11/13]: failed with error: expected comment: bad comment; got: Who does encouraging eagerly annoying dream several their scold straightaway. object WAREHOUSE["XHZJCKAT_35D0BCC1_7797_974E_ACAF_C622C56FA2D2"] assertion [12/13]: failed with error: expected enable query acceleration: true; got: false object WAREHOUSE["XHZJCKAT_35D0BCC1_7797_974E_ACAF_C622C56FA2D2"] assertion [13/13]: failed with error: expected query acceleration max scale factor: 12; got: 8 @@ -281,17 +281,17 @@ it will result in: WBJKHLAT_2E52D1E6_D23D_33A0_F568_4EBDBE083B65 imported resource assertion [6/12]: failed with error: expected: ECONOMY, got: STANDARD WBJKHLAT_2E52D1E6_D23D_33A0_F568_4EBDBE083B65 imported resource assertion [7/12]: failed with error: expected: 123, got: 600 WBJKHLAT_2E52D1E6_D23D_33A0_F568_4EBDBE083B65 imported resource assertion [8/12]: failed with error: expected: false, got: true - WBJKHLAT_2E52D1E6_D23D_33A0_F568_4EBDBE083B65 imported resource assertion [9/12]: failed with error: expected: abc, got: + WBJKHLAT_2E52D1E6_D23D_33A0_F568_4EBDBE083B65 imported resource assertion [9/12]: failed with error: expected: abc, got: WBJKHLAT_2E52D1E6_D23D_33A0_F568_4EBDBE083B65 imported resource assertion [10/12]: failed with error: expected: bad comment, got: Promise my huh off certain you bravery dynasty with Roman. WBJKHLAT_2E52D1E6_D23D_33A0_F568_4EBDBE083B65 imported resource assertion [11/12]: failed with error: expected: true, got: false WBJKHLAT_2E52D1E6_D23D_33A0_F568_4EBDBE083B65 imported resource assertion [12/12]: failed with error: expected: 16, got: 8 check 9/11 error: WBJKHLAT_2E52D1E6_D23D_33A0_F568_4EBDBE083B65 imported parameters assertion [2/7]: failed with error: expected: 1, got: 8 - WBJKHLAT_2E52D1E6_D23D_33A0_F568_4EBDBE083B65 imported parameters assertion [3/7]: failed with error: expected: WAREHOUSE, got: + WBJKHLAT_2E52D1E6_D23D_33A0_F568_4EBDBE083B65 imported parameters assertion [3/7]: failed with error: expected: WAREHOUSE, got: WBJKHLAT_2E52D1E6_D23D_33A0_F568_4EBDBE083B65 imported parameters assertion [4/7]: failed with error: expected: 23, got: 0 - WBJKHLAT_2E52D1E6_D23D_33A0_F568_4EBDBE083B65 imported parameters assertion [5/7]: failed with error: expected: WAREHOUSE, got: + WBJKHLAT_2E52D1E6_D23D_33A0_F568_4EBDBE083B65 imported parameters assertion [5/7]: failed with error: expected: WAREHOUSE, got: WBJKHLAT_2E52D1E6_D23D_33A0_F568_4EBDBE083B65 imported parameters assertion [6/7]: failed with error: expected: 1232, got: 172800 - WBJKHLAT_2E52D1E6_D23D_33A0_F568_4EBDBE083B65 imported parameters assertion [7/7]: failed with error: expected: WAREHOUSE, got: + WBJKHLAT_2E52D1E6_D23D_33A0_F568_4EBDBE083B65 imported parameters assertion [7/7]: failed with error: expected: WAREHOUSE, got: check 10/11 error: object WAREHOUSE["WBJKHLAT_2E52D1E6_D23D_33A0_F568_4EBDBE083B65"] assertion [1/13]: failed with error: expected name: bad name; got: WBJKHLAT_2E52D1E6_D23D_33A0_F568_4EBDBE083B65 object WAREHOUSE["WBJKHLAT_2E52D1E6_D23D_33A0_F568_4EBDBE083B65"] assertion [2/13]: failed with error: expected state: SUSPENDED; got: STARTED @@ -302,7 +302,7 @@ it will result in: object WAREHOUSE["WBJKHLAT_2E52D1E6_D23D_33A0_F568_4EBDBE083B65"] assertion [7/13]: failed with error: expected scaling policy: ECONOMY; got: STANDARD object WAREHOUSE["WBJKHLAT_2E52D1E6_D23D_33A0_F568_4EBDBE083B65"] assertion [8/13]: failed with error: expected auto suspend: 123; got: 600 object WAREHOUSE["WBJKHLAT_2E52D1E6_D23D_33A0_F568_4EBDBE083B65"] assertion [9/13]: failed with error: expected auto resume: false; got: true - object WAREHOUSE["WBJKHLAT_2E52D1E6_D23D_33A0_F568_4EBDBE083B65"] assertion [10/13]: failed with error: expected resource monitor: some-id; got: + object WAREHOUSE["WBJKHLAT_2E52D1E6_D23D_33A0_F568_4EBDBE083B65"] assertion [10/13]: failed with error: expected resource monitor: some-id; got: object WAREHOUSE["WBJKHLAT_2E52D1E6_D23D_33A0_F568_4EBDBE083B65"] assertion [11/13]: failed with error: expected comment: bad comment; got: Promise my huh off certain you bravery dynasty with Roman. object WAREHOUSE["WBJKHLAT_2E52D1E6_D23D_33A0_F568_4EBDBE083B65"] assertion [12/13]: failed with error: expected enable query acceleration: true; got: false object WAREHOUSE["WBJKHLAT_2E52D1E6_D23D_33A0_F568_4EBDBE083B65"] assertion [13/13]: failed with error: expected query acceleration max scale factor: 12; got: 8 @@ -331,7 +331,7 @@ it will result in: ``` it will result in: ``` - commons.go:101: + commons.go:101: Error Trace: /Users/asawicki/Projects/terraform-provider-snowflake/pkg/sdk/testint/warehouses_integration_test.go:149 Error: Received unexpected error: object WAREHOUSE["VKSENEIT_535F314F_6549_348F_370E_AB430EE4BC7B"] assertion [1/13]: failed with error: expected name: bad name; got: VKSENEIT_535F314F_6549_348F_370E_AB430EE4BC7B @@ -402,6 +402,7 @@ func (w *WarehouseDatasourceShowOutputAssert) IsEmpty() { - consider duplicating the builders template from resource (currently same template used for datasources and provider which limits the customization possibilities for just one block type) - consider merging ResourceModel with DatasourceModel (currently the implementation is really similar) - remove schema.TypeMap workaround or make it wiser (e.g. during generation we could programmatically gather all schema.TypeMap and use this workaround only for them) +- support asserting resource id in `assert/resourceassert/*_gen.go` ## Known limitations - generating provider config may misbehave when used only with one object/map paramter (like `params`), e.g.: diff --git a/pkg/acceptance/bettertestspoc/assert/resourceassert/gen/resource_schema_def.go b/pkg/acceptance/bettertestspoc/assert/resourceassert/gen/resource_schema_def.go index bcbe79ed5b..0352763bb0 100644 --- a/pkg/acceptance/bettertestspoc/assert/resourceassert/gen/resource_schema_def.go +++ b/pkg/acceptance/bettertestspoc/assert/resourceassert/gen/resource_schema_def.go @@ -109,6 +109,10 @@ var allResourceSchemaDefs = []ResourceSchemaDef{ name: "Tag", schema: resources.Tag().Schema, }, + { + name: "TagAssociation", + schema: resources.TagAssociation().Schema, + }, { name: "Task", schema: resources.Task().Schema, diff --git a/pkg/acceptance/bettertestspoc/assert/resourceassert/tag_association_resource_ext.go b/pkg/acceptance/bettertestspoc/assert/resourceassert/tag_association_resource_ext.go new file mode 100644 index 0000000000..d7ae9ed91d --- /dev/null +++ b/pkg/acceptance/bettertestspoc/assert/resourceassert/tag_association_resource_ext.go @@ -0,0 +1,17 @@ +package resourceassert + +import ( + "fmt" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert" +) + +func (t *TagAssociationResourceAssert) HasObjectIdentifiersLength(len int) *TagAssociationResourceAssert { + t.AddAssertion(assert.ValueSet("object_identifiers.#", fmt.Sprintf("%d", len))) + return t +} + +func (t *TagAssociationResourceAssert) HasIdString(expected string) *TagAssociationResourceAssert { + t.AddAssertion(assert.ValueSet("id", expected)) + return t +} diff --git a/pkg/acceptance/bettertestspoc/assert/resourceassert/tag_association_resource_gen.go b/pkg/acceptance/bettertestspoc/assert/resourceassert/tag_association_resource_gen.go new file mode 100644 index 0000000000..d9c10ba7b2 --- /dev/null +++ b/pkg/acceptance/bettertestspoc/assert/resourceassert/tag_association_resource_gen.go @@ -0,0 +1,97 @@ +// Code generated by assertions generator; DO NOT EDIT. + +package resourceassert + +import ( + "testing" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert" +) + +type TagAssociationResourceAssert struct { + *assert.ResourceAssert +} + +func TagAssociationResource(t *testing.T, name string) *TagAssociationResourceAssert { + t.Helper() + + return &TagAssociationResourceAssert{ + ResourceAssert: assert.NewResourceAssert(name, "resource"), + } +} + +func ImportedTagAssociationResource(t *testing.T, id string) *TagAssociationResourceAssert { + t.Helper() + + return &TagAssociationResourceAssert{ + ResourceAssert: assert.NewImportedResourceAssert(id, "imported resource"), + } +} + +/////////////////////////////////// +// Attribute value string checks // +/////////////////////////////////// + +func (t *TagAssociationResourceAssert) HasObjectIdentifiersString(expected string) *TagAssociationResourceAssert { + t.AddAssertion(assert.ValueSet("object_identifiers", expected)) + return t +} + +func (t *TagAssociationResourceAssert) HasObjectNameString(expected string) *TagAssociationResourceAssert { + t.AddAssertion(assert.ValueSet("object_name", expected)) + return t +} + +func (t *TagAssociationResourceAssert) HasObjectTypeString(expected string) *TagAssociationResourceAssert { + t.AddAssertion(assert.ValueSet("object_type", expected)) + return t +} + +func (t *TagAssociationResourceAssert) HasSkipValidationString(expected string) *TagAssociationResourceAssert { + t.AddAssertion(assert.ValueSet("skip_validation", expected)) + return t +} + +func (t *TagAssociationResourceAssert) HasTagIdString(expected string) *TagAssociationResourceAssert { + t.AddAssertion(assert.ValueSet("tag_id", expected)) + return t +} + +func (t *TagAssociationResourceAssert) HasTagValueString(expected string) *TagAssociationResourceAssert { + t.AddAssertion(assert.ValueSet("tag_value", expected)) + return t +} + +//////////////////////////// +// Attribute empty checks // +//////////////////////////// + +func (t *TagAssociationResourceAssert) HasNoObjectIdentifiers() *TagAssociationResourceAssert { + t.AddAssertion(assert.ValueNotSet("object_identifiers")) + return t +} + +func (t *TagAssociationResourceAssert) HasNoObjectName() *TagAssociationResourceAssert { + t.AddAssertion(assert.ValueNotSet("object_name")) + return t +} + +func (t *TagAssociationResourceAssert) HasNoObjectType() *TagAssociationResourceAssert { + t.AddAssertion(assert.ValueNotSet("object_type")) + return t +} + +func (t *TagAssociationResourceAssert) HasNoSkipValidation() *TagAssociationResourceAssert { + t.AddAssertion(assert.ValueNotSet("skip_validation")) + return t +} + +func (t *TagAssociationResourceAssert) HasNoTagId() *TagAssociationResourceAssert { + t.AddAssertion(assert.ValueNotSet("tag_id")) + return t +} + +func (t *TagAssociationResourceAssert) HasNoTagValue() *TagAssociationResourceAssert { + t.AddAssertion(assert.ValueNotSet("tag_value")) + return t +} diff --git a/pkg/acceptance/bettertestspoc/assert/resourceassert/tag_resource_gen.go b/pkg/acceptance/bettertestspoc/assert/resourceassert/tag_resource_gen.go index 27102d9656..919658aff2 100644 --- a/pkg/acceptance/bettertestspoc/assert/resourceassert/tag_resource_gen.go +++ b/pkg/acceptance/bettertestspoc/assert/resourceassert/tag_resource_gen.go @@ -52,8 +52,8 @@ func (t *TagResourceAssert) HasFullyQualifiedNameString(expected string) *TagRes return t } -func (t *TagResourceAssert) HasMaskingPolicyString(expected string) *TagResourceAssert { - t.AddAssertion(assert.ValueSet("masking_policy", expected)) +func (t *TagResourceAssert) HasMaskingPoliciesString(expected string) *TagResourceAssert { + t.AddAssertion(assert.ValueSet("masking_policies", expected)) return t } diff --git a/pkg/acceptance/bettertestspoc/config/model/primary_connection_model_gen.go b/pkg/acceptance/bettertestspoc/config/model/primary_connection_model_gen.go index f8f29bf1cf..3cbb735d91 100644 --- a/pkg/acceptance/bettertestspoc/config/model/primary_connection_model_gen.go +++ b/pkg/acceptance/bettertestspoc/config/model/primary_connection_model_gen.go @@ -13,6 +13,7 @@ type PrimaryConnectionModel struct { Comment tfconfig.Variable `json:"comment,omitempty"` EnableFailoverToAccounts tfconfig.Variable `json:"enable_failover_to_accounts,omitempty"` FullyQualifiedName tfconfig.Variable `json:"fully_qualified_name,omitempty"` + IsPrimary tfconfig.Variable `json:"is_primary,omitempty"` Name tfconfig.Variable `json:"name,omitempty"` *config.ResourceModelMeta @@ -55,6 +56,11 @@ func (p *PrimaryConnectionModel) WithFullyQualifiedName(fullyQualifiedName strin return p } +func (p *PrimaryConnectionModel) WithIsPrimary(isPrimary bool) *PrimaryConnectionModel { + p.IsPrimary = tfconfig.BoolVariable(isPrimary) + return p +} + func (p *PrimaryConnectionModel) WithName(name string) *PrimaryConnectionModel { p.Name = tfconfig.StringVariable(name) return p @@ -79,6 +85,11 @@ func (p *PrimaryConnectionModel) WithFullyQualifiedNameValue(value tfconfig.Vari return p } +func (p *PrimaryConnectionModel) WithIsPrimaryValue(value tfconfig.Variable) *PrimaryConnectionModel { + p.IsPrimary = value + return p +} + func (p *PrimaryConnectionModel) WithNameValue(value tfconfig.Variable) *PrimaryConnectionModel { p.Name = value return p diff --git a/pkg/acceptance/bettertestspoc/config/model/stream_on_directory_table_model_gen.go b/pkg/acceptance/bettertestspoc/config/model/stream_on_directory_table_model_gen.go index 4956e940c9..cc2270d46c 100644 --- a/pkg/acceptance/bettertestspoc/config/model/stream_on_directory_table_model_gen.go +++ b/pkg/acceptance/bettertestspoc/config/model/stream_on_directory_table_model_gen.go @@ -17,6 +17,8 @@ type StreamOnDirectoryTableModel struct { Name tfconfig.Variable `json:"name,omitempty"` Schema tfconfig.Variable `json:"schema,omitempty"` Stage tfconfig.Variable `json:"stage,omitempty"` + Stale tfconfig.Variable `json:"stale,omitempty"` + StreamType tfconfig.Variable `json:"stream_type,omitempty"` *config.ResourceModelMeta } @@ -93,6 +95,16 @@ func (s *StreamOnDirectoryTableModel) WithStage(stage string) *StreamOnDirectory return s } +func (s *StreamOnDirectoryTableModel) WithStale(stale bool) *StreamOnDirectoryTableModel { + s.Stale = tfconfig.BoolVariable(stale) + return s +} + +func (s *StreamOnDirectoryTableModel) WithStreamType(streamType string) *StreamOnDirectoryTableModel { + s.StreamType = tfconfig.StringVariable(streamType) + return s +} + ////////////////////////////////////////// // below it's possible to set any value // ////////////////////////////////////////// @@ -131,3 +143,13 @@ func (s *StreamOnDirectoryTableModel) WithStageValue(value tfconfig.Variable) *S s.Stage = value return s } + +func (s *StreamOnDirectoryTableModel) WithStaleValue(value tfconfig.Variable) *StreamOnDirectoryTableModel { + s.Stale = value + return s +} + +func (s *StreamOnDirectoryTableModel) WithStreamTypeValue(value tfconfig.Variable) *StreamOnDirectoryTableModel { + s.StreamType = value + return s +} diff --git a/pkg/acceptance/bettertestspoc/config/model/stream_on_external_table_model_gen.go b/pkg/acceptance/bettertestspoc/config/model/stream_on_external_table_model_gen.go index 09c87c5e23..de66941879 100644 --- a/pkg/acceptance/bettertestspoc/config/model/stream_on_external_table_model_gen.go +++ b/pkg/acceptance/bettertestspoc/config/model/stream_on_external_table_model_gen.go @@ -20,6 +20,8 @@ type StreamOnExternalTableModel struct { InsertOnly tfconfig.Variable `json:"insert_only,omitempty"` Name tfconfig.Variable `json:"name,omitempty"` Schema tfconfig.Variable `json:"schema,omitempty"` + Stale tfconfig.Variable `json:"stale,omitempty"` + StreamType tfconfig.Variable `json:"stream_type,omitempty"` *config.ResourceModelMeta } @@ -105,6 +107,16 @@ func (s *StreamOnExternalTableModel) WithSchema(schema string) *StreamOnExternal return s } +func (s *StreamOnExternalTableModel) WithStale(stale bool) *StreamOnExternalTableModel { + s.Stale = tfconfig.BoolVariable(stale) + return s +} + +func (s *StreamOnExternalTableModel) WithStreamType(streamType string) *StreamOnExternalTableModel { + s.StreamType = tfconfig.StringVariable(streamType) + return s +} + ////////////////////////////////////////// // below it's possible to set any value // ////////////////////////////////////////// @@ -158,3 +170,13 @@ func (s *StreamOnExternalTableModel) WithSchemaValue(value tfconfig.Variable) *S s.Schema = value return s } + +func (s *StreamOnExternalTableModel) WithStaleValue(value tfconfig.Variable) *StreamOnExternalTableModel { + s.Stale = value + return s +} + +func (s *StreamOnExternalTableModel) WithStreamTypeValue(value tfconfig.Variable) *StreamOnExternalTableModel { + s.StreamType = value + return s +} diff --git a/pkg/acceptance/bettertestspoc/config/model/stream_on_table_model_gen.go b/pkg/acceptance/bettertestspoc/config/model/stream_on_table_model_gen.go index 05cbdb51e9..3c337e9e9a 100644 --- a/pkg/acceptance/bettertestspoc/config/model/stream_on_table_model_gen.go +++ b/pkg/acceptance/bettertestspoc/config/model/stream_on_table_model_gen.go @@ -20,6 +20,8 @@ type StreamOnTableModel struct { Name tfconfig.Variable `json:"name,omitempty"` Schema tfconfig.Variable `json:"schema,omitempty"` ShowInitialRows tfconfig.Variable `json:"show_initial_rows,omitempty"` + Stale tfconfig.Variable `json:"stale,omitempty"` + StreamType tfconfig.Variable `json:"stream_type,omitempty"` Table tfconfig.Variable `json:"table,omitempty"` *config.ResourceModelMeta @@ -106,6 +108,16 @@ func (s *StreamOnTableModel) WithShowInitialRows(showInitialRows string) *Stream return s } +func (s *StreamOnTableModel) WithStale(stale bool) *StreamOnTableModel { + s.Stale = tfconfig.BoolVariable(stale) + return s +} + +func (s *StreamOnTableModel) WithStreamType(streamType string) *StreamOnTableModel { + s.StreamType = tfconfig.StringVariable(streamType) + return s +} + func (s *StreamOnTableModel) WithTable(table string) *StreamOnTableModel { s.Table = tfconfig.StringVariable(table) return s @@ -165,6 +177,16 @@ func (s *StreamOnTableModel) WithShowInitialRowsValue(value tfconfig.Variable) * return s } +func (s *StreamOnTableModel) WithStaleValue(value tfconfig.Variable) *StreamOnTableModel { + s.Stale = value + return s +} + +func (s *StreamOnTableModel) WithStreamTypeValue(value tfconfig.Variable) *StreamOnTableModel { + s.StreamType = value + return s +} + func (s *StreamOnTableModel) WithTableValue(value tfconfig.Variable) *StreamOnTableModel { s.Table = value return s diff --git a/pkg/acceptance/bettertestspoc/config/model/stream_on_view_model_gen.go b/pkg/acceptance/bettertestspoc/config/model/stream_on_view_model_gen.go index b627e07a5c..d4942dd8a6 100644 --- a/pkg/acceptance/bettertestspoc/config/model/stream_on_view_model_gen.go +++ b/pkg/acceptance/bettertestspoc/config/model/stream_on_view_model_gen.go @@ -20,6 +20,8 @@ type StreamOnViewModel struct { Name tfconfig.Variable `json:"name,omitempty"` Schema tfconfig.Variable `json:"schema,omitempty"` ShowInitialRows tfconfig.Variable `json:"show_initial_rows,omitempty"` + Stale tfconfig.Variable `json:"stale,omitempty"` + StreamType tfconfig.Variable `json:"stream_type,omitempty"` View tfconfig.Variable `json:"view,omitempty"` *config.ResourceModelMeta @@ -106,6 +108,16 @@ func (s *StreamOnViewModel) WithShowInitialRows(showInitialRows string) *StreamO return s } +func (s *StreamOnViewModel) WithStale(stale bool) *StreamOnViewModel { + s.Stale = tfconfig.BoolVariable(stale) + return s +} + +func (s *StreamOnViewModel) WithStreamType(streamType string) *StreamOnViewModel { + s.StreamType = tfconfig.StringVariable(streamType) + return s +} + func (s *StreamOnViewModel) WithView(view string) *StreamOnViewModel { s.View = tfconfig.StringVariable(view) return s @@ -165,6 +177,16 @@ func (s *StreamOnViewModel) WithShowInitialRowsValue(value tfconfig.Variable) *S return s } +func (s *StreamOnViewModel) WithStaleValue(value tfconfig.Variable) *StreamOnViewModel { + s.Stale = value + return s +} + +func (s *StreamOnViewModel) WithStreamTypeValue(value tfconfig.Variable) *StreamOnViewModel { + s.StreamType = value + return s +} + func (s *StreamOnViewModel) WithViewValue(value tfconfig.Variable) *StreamOnViewModel { s.View = value return s diff --git a/pkg/acceptance/bettertestspoc/config/model/tag_association_model_ext.go b/pkg/acceptance/bettertestspoc/config/model/tag_association_model_ext.go new file mode 100644 index 0000000000..7d565264fc --- /dev/null +++ b/pkg/acceptance/bettertestspoc/config/model/tag_association_model_ext.go @@ -0,0 +1,16 @@ +package model + +import ( + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + tfconfig "github.com/hashicorp/terraform-plugin-testing/config" +) + +func (t *TagAssociationModel) WithObjectIdentifiers(objectIdentifiers ...sdk.ObjectIdentifier) *TagAssociationModel { + objectIdentifiersStringVariables := make([]tfconfig.Variable, len(objectIdentifiers)) + for i, v := range objectIdentifiers { + objectIdentifiersStringVariables[i] = tfconfig.StringVariable(v.FullyQualifiedName()) + } + + t.ObjectIdentifiers = tfconfig.SetVariable(objectIdentifiersStringVariables...) + return t +} diff --git a/pkg/acceptance/bettertestspoc/config/model/tag_association_model_gen.go b/pkg/acceptance/bettertestspoc/config/model/tag_association_model_gen.go new file mode 100644 index 0000000000..12457d4973 --- /dev/null +++ b/pkg/acceptance/bettertestspoc/config/model/tag_association_model_gen.go @@ -0,0 +1,120 @@ +// Code generated by config model builder generator; DO NOT EDIT. + +package model + +import ( + tfconfig "github.com/hashicorp/terraform-plugin-testing/config" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/config" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/provider/resources" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" +) + +type TagAssociationModel struct { + ObjectIdentifiers tfconfig.Variable `json:"object_identifiers,omitempty"` + ObjectName tfconfig.Variable `json:"object_name,omitempty"` + ObjectType tfconfig.Variable `json:"object_type,omitempty"` + SkipValidation tfconfig.Variable `json:"skip_validation,omitempty"` + TagId tfconfig.Variable `json:"tag_id,omitempty"` + TagValue tfconfig.Variable `json:"tag_value,omitempty"` + + *config.ResourceModelMeta +} + +///////////////////////////////////////////////// +// Basic builders (resource name and required) // +///////////////////////////////////////////////// + +func TagAssociation( + resourceName string, + objectIdentifiers []sdk.ObjectIdentifier, + objectType string, + tagId string, + tagValue string, +) *TagAssociationModel { + t := &TagAssociationModel{ResourceModelMeta: config.Meta(resourceName, resources.TagAssociation)} + t.WithObjectIdentifiers(objectIdentifiers...) + t.WithObjectType(objectType) + t.WithTagId(tagId) + t.WithTagValue(tagValue) + return t +} + +func TagAssociationWithDefaultMeta( + objectIdentifiers []sdk.ObjectIdentifier, + objectType string, + tagId string, + tagValue string, +) *TagAssociationModel { + t := &TagAssociationModel{ResourceModelMeta: config.DefaultMeta(resources.TagAssociation)} + t.WithObjectIdentifiers(objectIdentifiers...) + t.WithObjectType(objectType) + t.WithTagId(tagId) + t.WithTagValue(tagValue) + return t +} + +///////////////////////////////// +// below all the proper values // +///////////////////////////////// + +// object_identifiers attribute type is not yet supported, so WithObjectIdentifiers can't be generated + +func (t *TagAssociationModel) WithObjectName(objectName string) *TagAssociationModel { + t.ObjectName = tfconfig.StringVariable(objectName) + return t +} + +func (t *TagAssociationModel) WithObjectType(objectType string) *TagAssociationModel { + t.ObjectType = tfconfig.StringVariable(objectType) + return t +} + +func (t *TagAssociationModel) WithSkipValidation(skipValidation bool) *TagAssociationModel { + t.SkipValidation = tfconfig.BoolVariable(skipValidation) + return t +} + +func (t *TagAssociationModel) WithTagId(tagId string) *TagAssociationModel { + t.TagId = tfconfig.StringVariable(tagId) + return t +} + +func (t *TagAssociationModel) WithTagValue(tagValue string) *TagAssociationModel { + t.TagValue = tfconfig.StringVariable(tagValue) + return t +} + +////////////////////////////////////////// +// below it's possible to set any value // +////////////////////////////////////////// + +func (t *TagAssociationModel) WithObjectIdentifiersValue(value tfconfig.Variable) *TagAssociationModel { + t.ObjectIdentifiers = value + return t +} + +func (t *TagAssociationModel) WithObjectNameValue(value tfconfig.Variable) *TagAssociationModel { + t.ObjectName = value + return t +} + +func (t *TagAssociationModel) WithObjectTypeValue(value tfconfig.Variable) *TagAssociationModel { + t.ObjectType = value + return t +} + +func (t *TagAssociationModel) WithSkipValidationValue(value tfconfig.Variable) *TagAssociationModel { + t.SkipValidation = value + return t +} + +func (t *TagAssociationModel) WithTagIdValue(value tfconfig.Variable) *TagAssociationModel { + t.TagId = value + return t +} + +func (t *TagAssociationModel) WithTagValueValue(value tfconfig.Variable) *TagAssociationModel { + t.TagValue = value + return t +} diff --git a/pkg/acceptance/bettertestspoc/config/model/tag_model_gen.go b/pkg/acceptance/bettertestspoc/config/model/tag_model_gen.go index 91b5bb9eff..a649154cb2 100644 --- a/pkg/acceptance/bettertestspoc/config/model/tag_model_gen.go +++ b/pkg/acceptance/bettertestspoc/config/model/tag_model_gen.go @@ -71,7 +71,7 @@ func (t *TagModel) WithFullyQualifiedName(fullyQualifiedName string) *TagModel { return t } -// masking_policy attribute type is not yet supported, so WithMaskingPolicy can't be generated +// masking_policies attribute type is not yet supported, so WithMaskingPolicies can't be generated func (t *TagModel) WithName(name string) *TagModel { t.Name = tfconfig.StringVariable(name) diff --git a/pkg/acceptance/check_destroy.go b/pkg/acceptance/check_destroy.go index 57145b726f..404ad98917 100644 --- a/pkg/acceptance/check_destroy.go +++ b/pkg/acceptance/check_destroy.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "reflect" + "strconv" "strings" "testing" @@ -490,6 +491,75 @@ func CheckUserAuthenticationPolicyAttachmentDestroy(t *testing.T) func(*terrafor } } +// CheckResourceTagUnset is a custom check that should be later incorporated into generic CheckDestroy +func CheckResourceTagUnset(t *testing.T) func(*terraform.State) error { + t.Helper() + + return func(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "snowflake_tag_association" { + continue + } + objectType := sdk.ObjectType(rs.Primary.Attributes["object_type"]) + tagId, err := sdk.ParseSchemaObjectIdentifier(rs.Primary.Attributes["tag_id"]) + if err != nil { + return err + } + idLen, err := strconv.Atoi(rs.Primary.Attributes["object_identifiers.#"]) + if err != nil { + return err + } + for i := 0; i < idLen; i++ { + idRaw := rs.Primary.Attributes[fmt.Sprintf("object_identifiers.%d", i)] + var id sdk.ObjectIdentifier + // TODO(SNOW-1229218): Use a common mapper to get object id. + if objectType == sdk.ObjectTypeAccount { + id, err = sdk.ParseAccountIdentifier(idRaw) + if err != nil { + return fmt.Errorf("invalid account id: %w", err) + } + } else { + id, err = sdk.ParseObjectIdentifierString(idRaw) + if err != nil { + return fmt.Errorf("invalid object id: %w", err) + } + } + if err := assertTagUnset(t, tagId, id, objectType); err != nil { + return err + } + } + } + return nil + } +} + +// CheckTagUnset is a custom check that should be later incorporated into generic CheckDestroy +func CheckTagUnset(t *testing.T, tagId sdk.SchemaObjectIdentifier, id sdk.ObjectIdentifier, objectType sdk.ObjectType) func(*terraform.State) error { + t.Helper() + + return func(s *terraform.State) error { + return assertTagUnset(t, tagId, id, objectType) + } +} + +func assertTagUnset(t *testing.T, tagId sdk.SchemaObjectIdentifier, id sdk.ObjectIdentifier, objectType sdk.ObjectType) error { + t.Helper() + + tag, err := TestClient().Tag.GetForObject(t, tagId, id, objectType) + if err != nil { + if strings.Contains(err.Error(), "does not exist or not authorized") { + // Note: this can happen if the referenced object was deleted before; in this case, ignore the error + t.Logf("could not get tag for %v : %v, continuing...", id.FullyQualifiedName(), err) + return nil + } + return err + } + if tag != nil { + return fmt.Errorf("tag %s for object %s expected to be empty, got %s", tagId.FullyQualifiedName(), id.FullyQualifiedName(), *tag) + } + return err +} + func TestAccCheckGrantApplicationRoleDestroy(s *terraform.State) error { client := TestAccProvider.Meta().(*provider.Context).Client for _, rs := range s.RootModule().Resources { diff --git a/pkg/acceptance/helpers/tag_client.go b/pkg/acceptance/helpers/tag_client.go index 90d7cbcb8b..c32598f9b2 100644 --- a/pkg/acceptance/helpers/tag_client.go +++ b/pkg/acceptance/helpers/tag_client.go @@ -52,6 +52,22 @@ func (c *TagClient) CreateWithRequest(t *testing.T, req *sdk.CreateTagRequest) ( return tag, c.DropTagFunc(t, req.GetName()) } +func (c *TagClient) Unset(t *testing.T, objectType sdk.ObjectType, id sdk.ObjectIdentifier, unsetTags []sdk.ObjectIdentifier) { + t.Helper() + ctx := context.Background() + + err := c.client().Unset(ctx, sdk.NewUnsetTagRequest(objectType, id).WithUnsetTags(unsetTags)) + require.NoError(t, err) +} + +func (c *TagClient) Set(t *testing.T, objectType sdk.ObjectType, id sdk.ObjectIdentifier, setTags []sdk.TagAssociation) { + t.Helper() + ctx := context.Background() + + err := c.client().Set(ctx, sdk.NewSetTagRequest(objectType, id).WithSetTags(setTags)) + require.NoError(t, err) +} + func (c *TagClient) Alter(t *testing.T, req *sdk.AlterTagRequest) { t.Helper() ctx := context.Background() @@ -75,3 +91,11 @@ func (c *TagClient) Show(t *testing.T, id sdk.SchemaObjectIdentifier) (*sdk.Tag, return c.client().ShowByID(ctx, id) } + +func (c *TagClient) GetForObject(t *testing.T, tagId sdk.SchemaObjectIdentifier, objectId sdk.ObjectIdentifier, objectType sdk.ObjectType) (*string, error) { + t.Helper() + ctx := context.Background() + client := c.context.client.SystemFunctions + + return client.GetTag(ctx, tagId, objectId, objectType) +} diff --git a/pkg/resources/grant_ownership.go b/pkg/resources/grant_ownership.go index 7173f18380..24077bcd48 100644 --- a/pkg/resources/grant_ownership.go +++ b/pkg/resources/grant_ownership.go @@ -408,7 +408,7 @@ func ReadGrantOwnership(ctx context.Context, d *schema.ResourceData, meta any) d } // TODO(SNOW-1229218): Make sdk.ObjectType + string objectName to sdk.ObjectIdentifier mapping available in the sdk (for all object types). -func getOnObjectIdentifier(objectType sdk.ObjectType, objectName string) (sdk.ObjectIdentifier, error) { +func GetOnObjectIdentifier(objectType sdk.ObjectType, objectName string) (sdk.ObjectIdentifier, error) { switch objectType { case sdk.ObjectTypeComputePool, sdk.ObjectTypeDatabase, @@ -458,6 +458,9 @@ func getOnObjectIdentifier(objectType sdk.ObjectType, objectName string) (sdk.Ob sdk.ObjectTypeProcedure, sdk.ObjectTypeExternalFunction: return sdk.ParseSchemaObjectIdentifierWithArguments(objectName) + case sdk.ObjectTypeColumn: + return sdk.ParseTableColumnIdentifier(objectName) + default: return nil, sdk.NewError(fmt.Sprintf("object_type %s is not supported, please create a feature request for the provider if given object_type should be supported", objectType)) } @@ -475,7 +478,7 @@ func getOwnershipGrantOn(d *schema.ResourceData) (*sdk.OwnershipGrantOn, error) switch { case len(onObjectType) > 0 && len(onObjectName) > 0: objectType := sdk.ObjectType(strings.ToUpper(onObjectType)) - objectName, err := getOnObjectIdentifier(objectType, onObjectName) + objectName, err := GetOnObjectIdentifier(objectType, onObjectName) if err != nil { return nil, err } @@ -626,7 +629,7 @@ func createGrantOwnershipIdFromSchema(d *schema.ResourceData) (*GrantOwnershipId case len(objectType) > 0 && len(objectName) > 0: id.Kind = OnObjectGrantOwnershipKind objectType := sdk.ObjectType(objectType) - objectName, err := getOnObjectIdentifier(objectType, objectName) + objectName, err := GetOnObjectIdentifier(objectType, objectName) if err != nil { return nil, err } diff --git a/pkg/resources/grant_ownership_identifier.go b/pkg/resources/grant_ownership_identifier.go index 2b233d6932..2eff1f7f43 100644 --- a/pkg/resources/grant_ownership_identifier.go +++ b/pkg/resources/grant_ownership_identifier.go @@ -125,7 +125,7 @@ func ParseGrantOwnershipId(id string) (*GrantOwnershipId, error) { return grantOwnershipId, sdk.NewError(`grant ownership identifier should consist of 6 parts "|||OnObject||"`) } objectType := sdk.ObjectType(parts[4]) - objectName, err := getOnObjectIdentifier(objectType, parts[5]) + objectName, err := GetOnObjectIdentifier(objectType, parts[5]) if err != nil { return nil, err } diff --git a/pkg/resources/grant_ownership_test.go b/pkg/resources/grant_ownership_test.go index 208346de46..4cfe2fe3ae 100644 --- a/pkg/resources/grant_ownership_test.go +++ b/pkg/resources/grant_ownership_test.go @@ -89,7 +89,7 @@ func TestGetOnObjectIdentifier(t *testing.T) { for _, tt := range testCases { tt := tt t.Run(tt.Name, func(t *testing.T) { - id, err := getOnObjectIdentifier(tt.ObjectType, tt.ObjectName) + id, err := GetOnObjectIdentifier(tt.ObjectType, tt.ObjectName) if tt.Error == "" { assert.NoError(t, err) assert.Equal(t, tt.Expected, id) diff --git a/pkg/resources/helper_expansion.go b/pkg/resources/helper_expansion.go index 82a00efcad..fe29b26c15 100644 --- a/pkg/resources/helper_expansion.go +++ b/pkg/resources/helper_expansion.go @@ -1,7 +1,10 @@ package resources import ( + "fmt" "slices" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" ) // borrowed from https://github.com/terraform-providers/terraform-provider-aws/blob/master/aws/structure.go#L924:6 @@ -27,6 +30,29 @@ func expandStringList(configured []interface{}) []string { return vs } +func ExpandObjectIdentifierSet(configured []any, objectType sdk.ObjectType) ([]sdk.ObjectIdentifier, error) { + vs := expandStringList(configured) + ids := make([]sdk.ObjectIdentifier, len(vs)) + for i, idRaw := range vs { + var id sdk.ObjectIdentifier + var err error + // TODO(SNOW-1229218): Use a common mapper to get object id. + if objectType == sdk.ObjectTypeAccount { + id, err = sdk.ParseAccountIdentifier(idRaw) + if err != nil { + return nil, fmt.Errorf("invalid account id: %w", err) + } + } else { + id, err = GetOnObjectIdentifier(objectType, idRaw) + if err != nil { + return nil, fmt.Errorf("invalid object id: %w", err) + } + } + ids[i] = id + } + return ids, nil +} + func expandStringListAllowEmpty(configured []interface{}) []string { // Allow empty values during expansion vs := make([]string, 0, len(configured)) diff --git a/pkg/resources/helpers.go b/pkg/resources/helpers.go index 3c47984e7e..c436f8d084 100644 --- a/pkg/resources/helpers.go +++ b/pkg/resources/helpers.go @@ -76,14 +76,18 @@ func ignoreCaseAndTrimSpaceSuppressFunc(_, old, new string, _ *schema.ResourceDa return strings.EqualFold(strings.TrimSpace(old), strings.TrimSpace(new)) } -func getTagObjectIdentifier(v map[string]any) sdk.ObjectIdentifier { - if _, ok := v["database"]; ok { - if _, ok := v["schema"]; ok { - return sdk.NewSchemaObjectIdentifier(v["database"].(string), v["schema"].(string), v["name"].(string)) - } - return sdk.NewDatabaseObjectIdentifier(v["database"].(string), v["name"].(string)) +func getTagObjectIdentifier(obj map[string]any) sdk.ObjectIdentifier { + database := obj["database"].(string) + schema := obj["schema"].(string) + name := obj["name"].(string) + switch { + case schema != "": + return sdk.NewSchemaObjectIdentifier(database, schema, name) + case database != "": + return sdk.NewDatabaseObjectIdentifier(database, name) + default: + return sdk.NewAccountObjectIdentifier(name) } - return sdk.NewAccountObjectIdentifier(v["name"].(string)) } func getPropertyTags(d *schema.ResourceData, key string) []sdk.TagAssociation { @@ -310,25 +314,35 @@ func JoinDiags(diagnostics ...diag.Diagnostics) diag.Diagnostics { return result } -// ListDiff Compares two lists (before and after), then compares and returns two lists that include +// ListDiff compares two lists (before and after), then compares and returns two lists that include // added and removed items between those lists. func ListDiff[T comparable](beforeList []T, afterList []T) (added []T, removed []T) { + added, removed, _ = ListDiffWithCommonItems(beforeList, afterList) + return +} + +// ListDiffWithCommonItems compares two lists (before and after), then compares and returns three lists that include +// added, removed and common items between those lists. +func ListDiffWithCommonItems[T comparable](beforeList []T, afterList []T) (added []T, removed []T, common []T) { added = make([]T, 0) removed = make([]T, 0) + common = make([]T, 0) - for _, privilegeBeforeChange := range beforeList { - if !slices.Contains(afterList, privilegeBeforeChange) { - removed = append(removed, privilegeBeforeChange) + for _, beforeItem := range beforeList { + if !slices.Contains(afterList, beforeItem) { + removed = append(removed, beforeItem) + } else { + common = append(common, beforeItem) } } - for _, privilegeAfterChange := range afterList { - if !slices.Contains(beforeList, privilegeAfterChange) { - added = append(added, privilegeAfterChange) + for _, afterItem := range afterList { + if !slices.Contains(beforeList, afterItem) { + added = append(added, afterItem) } } - return added, removed + return added, removed, common } // parseSchemaObjectIdentifierSet is a helper function to parse a given schema object identifier list from ResourceData. diff --git a/pkg/resources/helpers_test.go b/pkg/resources/helpers_test.go index c143d40f03..c60807c9ac 100644 --- a/pkg/resources/helpers_test.go +++ b/pkg/resources/helpers_test.go @@ -260,6 +260,83 @@ func TestListDiff(t *testing.T) { } } +func TestListDiffWithCommonItems(t *testing.T) { + testCases := []struct { + Name string + Before []any + After []any + Added []any + Removed []any + Common []any + }{ + { + Name: "no changes", + Before: []any{1, 2, 3, 4}, + After: []any{1, 2, 3, 4}, + Removed: []any{}, + Added: []any{}, + Common: []any{1, 2, 3, 4}, + }, + { + Name: "only removed", + Before: []any{1, 2, 3, 4}, + After: []any{}, + Removed: []any{1, 2, 3, 4}, + Added: []any{}, + Common: []any{}, + }, + { + Name: "only added", + Before: []any{}, + After: []any{1, 2, 3, 4}, + Removed: []any{}, + Added: []any{1, 2, 3, 4}, + Common: []any{}, + }, + { + Name: "added repeated items", + Before: []any{2}, + After: []any{1, 2, 1}, + Removed: []any{}, + Added: []any{1, 1}, + Common: []any{2}, + }, + { + Name: "removed repeated items", + Before: []any{1, 2, 1}, + After: []any{2}, + Removed: []any{1, 1}, + Added: []any{}, + Common: []any{2}, + }, + { + Name: "simple diff: ints", + Before: []any{1, 2, 3, 4, 5, 6, 7, 8, 9}, + After: []any{1, 3, 5, 7, 9, 12, 13, 14}, + Removed: []any{2, 4, 6, 8}, + Added: []any{12, 13, 14}, + Common: []any{1, 3, 5, 7, 9}, + }, + { + Name: "simple diff: strings", + Before: []any{"one", "two", "three", "four"}, + After: []any{"five", "two", "four", "six"}, + Removed: []any{"one", "three"}, + Added: []any{"five", "six"}, + Common: []any{"two", "four"}, + }, + } + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + added, removed, common := resources.ListDiffWithCommonItems(tc.Before, tc.After) + assert.Equal(t, tc.Added, added) + assert.Equal(t, tc.Removed, removed) + assert.Equal(t, tc.Common, common) + }) + } +} + func Test_DataTypeIssue3007DiffSuppressFunc(t *testing.T) { testCases := []struct { name string diff --git a/pkg/resources/tag_association.go b/pkg/resources/tag_association.go index f9141ab1fe..e76e5fc69f 100644 --- a/pkg/resources/tag_association.go +++ b/pkg/resources/tag_association.go @@ -2,21 +2,21 @@ package resources import ( "context" + "errors" "fmt" "log" - "slices" - "strings" "time" - "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/provider/resources" - + "github.com/hashicorp/go-cty/cty" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" "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/helpers" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/collections" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/provider" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/provider/resources" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" ) @@ -26,54 +26,37 @@ var tagAssociationSchema = map[string]*schema.Schema{ Optional: true, Description: "Specifies the object identifier for the tag association.", ForceNew: true, - Deprecated: "Use `object_identifier` instead", + Deprecated: "Use `object_identifiers` instead", }, - "object_identifier": { - Type: schema.TypeList, + "object_identifiers": { + Type: schema.TypeSet, MinItems: 1, Required: true, - Description: "Specifies the object identifier for the tag association.", - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "name": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - Description: "Name of the object to associate the tag with.", - }, - "database": { - Type: schema.TypeString, - Optional: true, - ForceNew: true, - Description: "Name of the database that the object was created in.", - }, - "schema": { - Type: schema.TypeString, - Optional: true, - ForceNew: true, - Description: "Name of the schema that the object was created in.", - }, - }, + Description: "Specifies the object identifiers for the tag association.", + Elem: &schema.Schema{ + Type: schema.TypeString, }, + DiffSuppressFunc: NormalizeAndCompareIdentifiersInSet("object_identifiers"), }, "object_type": { - Type: schema.TypeString, - Required: true, - Description: fmt.Sprintf("Specifies the type of object to add a tag. Allowed object types: %v.", sdk.TagAssociationAllowedObjectTypesString), - ValidateFunc: validation.StringInSlice(sdk.TagAssociationAllowedObjectTypesString, true), - ForceNew: true, + Type: schema.TypeString, + Required: true, + Description: fmt.Sprintf("Specifies the type of object to add a tag. Allowed object types: %v.", sdk.TagAssociationAllowedObjectTypesString), + ValidateFunc: validation.StringInSlice(sdk.TagAssociationAllowedObjectTypesString, true), + DiffSuppressFunc: ignoreCaseSuppressFunc, + 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)", - ForceNew: true, + Type: schema.TypeString, + Required: true, + Description: "Specifies the identifier for the tag.", + ForceNew: true, + DiffSuppressFunc: suppressIdentifierQuoting, }, "tag_value": { Type: schema.TypeString, Required: true, Description: "Specifies the value of the tag, (e.g. 'finance' or 'engineering')", - ForceNew: true, }, "skip_validation": { Type: schema.TypeBool, @@ -86,81 +69,89 @@ var tagAssociationSchema = map[string]*schema.Schema{ // TagAssociation returns a pointer to the resource representing a schema. func TagAssociation() *schema.Resource { return &schema.Resource{ + SchemaVersion: 1, + CreateContext: TrackingCreateWrapper(resources.TagAssociation, CreateContextTagAssociation), ReadContext: TrackingReadWrapper(resources.TagAssociation, ReadContextTagAssociation), UpdateContext: TrackingUpdateWrapper(resources.TagAssociation, UpdateContextTagAssociation), DeleteContext: TrackingDeleteWrapper(resources.TagAssociation, DeleteContextTagAssociation), + Description: "Resource used to manage tag associations. For more information, check [object tagging documentation](https://docs.snowflake.com/en/user-guide/object-tagging).", + Schema: tagAssociationSchema, Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, + StateContext: ImportTagAssociation, }, Timeouts: &schema.ResourceTimeout{ Create: schema.DefaultTimeout(70 * time.Minute), }, + + StateUpgraders: []schema.StateUpgrader{ + { + Version: 0, + // setting type to cty.EmptyObject is a bit hacky here but following https://developer.hashicorp.com/terraform/plugin/framework/migrating/resources/state-upgrade#sdkv2-1 would require lots of repetitive code; this should work with cty.EmptyObject + Type: cty.EmptyObject, + Upgrade: v0_98_0_TagAssociationStateUpgrader, + }, + }, } } -func TagIdentifierAndObjectIdentifier(d *schema.ResourceData) (sdk.SchemaObjectIdentifier, []sdk.ObjectIdentifier, sdk.ObjectType) { +func ImportTagAssociation(ctx context.Context, d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { + log.Printf("[DEBUG] Starting tag association import") + idParts := helpers.ParseResourceIdentifier(d.Id()) + if len(idParts) != 3 { + return nil, fmt.Errorf("invalid resource id: expected 3 arguments, but got %d", len(idParts)) + } + objectType, err := sdk.ToObjectType(idParts[2]) + if err != nil { + return nil, err + } + + if err := d.Set("tag_id", idParts[0]); err != nil { + return nil, err + } + if err := d.Set("tag_value", idParts[1]); err != nil { + return nil, err + } + if err := d.Set("object_type", objectType); err != nil { + return nil, err + } + return []*schema.ResourceData{d}, nil +} + +func TagIdentifierAndObjectIdentifier(d *schema.ResourceData) (sdk.SchemaObjectIdentifier, []sdk.ObjectIdentifier, sdk.ObjectType, error) { tag := d.Get("tag_id").(string) - objectType := sdk.ObjectType(d.Get("object_type").(string)) - - tagDatabase, tagSchema, tagName := ParseFullyQualifiedObjectID(tag) - tid := sdk.NewSchemaObjectIdentifier(tagDatabase, tagSchema, tagName) - - var identifiers []sdk.ObjectIdentifier - for _, item := range d.Get("object_identifier").([]interface{}) { - m := item.(map[string]interface{}) - name := strings.Trim(m["name"].(string), `"`) - var databaseName, schemaName string - if v, ok := m["database"]; ok { - databaseName = strings.Trim(v.(string), `"`) - if databaseName == "" && slices.Contains(sdk.TagAssociationTagObjectTypeIsSchemaObjectType, objectType) { - databaseName = tagDatabase - } - } - if v, ok := m["schema"]; ok { - schemaName = strings.Trim(v.(string), `"`) - if schemaName == "" && slices.Contains(sdk.TagAssociationTagObjectTypeIsSchemaObjectType, objectType) { - schemaName = tagSchema - } - } - switch { - case databaseName != "" && schemaName != "": - if objectType == sdk.ObjectTypeColumn { - fields := strings.Split(name, ".") - if len(fields) > 1 { - tableName := strings.ReplaceAll(fields[0], `"`, "") - var parts []string - for i := 1; i < len(fields); i++ { - parts = append(parts, strings.ReplaceAll(fields[i], `"`, "")) - } - columnName := strings.Join(parts, ".") - identifiers = append(identifiers, sdk.NewTableColumnIdentifier(databaseName, schemaName, tableName, columnName)) - } else { - identifiers = append(identifiers, sdk.NewSchemaObjectIdentifier(databaseName, schemaName, name)) - } - } else { - identifiers = append(identifiers, sdk.NewSchemaObjectIdentifier(databaseName, schemaName, name)) - } - case databaseName != "": - identifiers = append(identifiers, sdk.NewDatabaseObjectIdentifier(databaseName, name)) - default: - identifiers = append(identifiers, sdk.NewAccountObjectIdentifier(name)) - } + tagId, err := sdk.ParseSchemaObjectIdentifier(tag) + if err != nil { + return sdk.SchemaObjectIdentifier{}, nil, "", fmt.Errorf("invalid tag id: %w", err) + } + + objectType, err := sdk.ToObjectType(d.Get("object_type").(string)) + if err != nil { + return sdk.SchemaObjectIdentifier{}, nil, "", err + } + + ids, err := ExpandObjectIdentifierSet(d.Get("object_identifiers").(*schema.Set).List(), objectType) + if err != nil { + return sdk.SchemaObjectIdentifier{}, nil, "", err } - return tid, identifiers, objectType + + return tagId, ids, objectType, nil } -func CreateContextTagAssociation(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { +func CreateContextTagAssociation(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*provider.Context).Client tagValue := d.Get("tag_value").(string) - tid, ids, ot := TagIdentifierAndObjectIdentifier(d) + tagId, ids, objectType, err := TagIdentifierAndObjectIdentifier(d) + if err != nil { + return diag.FromErr(err) + } for _, oid := range ids { - request := sdk.NewSetTagRequest(ot, oid).WithSetTags([]sdk.TagAssociation{ + request := sdk.NewSetTagRequest(objectType, oid).WithSetTags([]sdk.TagAssociation{ { - Name: tid, + Name: tagId, Value: tagValue, }, }) @@ -171,12 +162,12 @@ func CreateContextTagAssociation(ctx context.Context, d *schema.ResourceData, me if !skipValidate { log.Println("[DEBUG] validating tag creation") if err := retry.RetryContext(ctx, d.Timeout(schema.TimeoutCreate)-time.Minute, func() *retry.RetryError { - tag, err := client.SystemFunctions.GetTag(ctx, tid, oid, ot) + tag, err := client.SystemFunctions.GetTag(ctx, tagId, oid, objectType) if err != nil { return retry.NonRetryableError(fmt.Errorf("error getting tag: %w", err)) } // if length of response is zero, tag association was not found. retry - if len(tag) == 0 { + if tag == nil { return retry.RetryableError(fmt.Errorf("expected tag association to be created but not yet created")) } return nil @@ -185,63 +176,124 @@ func CreateContextTagAssociation(ctx context.Context, d *schema.ResourceData, me } } } - d.SetId(helpers.EncodeSnowflakeID(tid.DatabaseName(), tid.SchemaName(), tid.Name())) + d.SetId(helpers.EncodeResourceIdentifier(tagId.FullyQualifiedName(), tagValue, string(objectType))) return ReadContextTagAssociation(ctx, d, meta) } -func ReadContextTagAssociation(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - diags := diag.Diagnostics{} +func ReadContextTagAssociation(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*provider.Context).Client + tagValue := d.Get("tag_value").(string) - tid, ids, ot := TagIdentifierAndObjectIdentifier(d) + tagId, ids, objectType, err := TagIdentifierAndObjectIdentifier(d) + if err != nil { + return diag.FromErr(err) + } + var correctObjectIds []string for _, oid := range ids { - tagValue, err := client.SystemFunctions.GetTag(ctx, tid, oid, ot) + objectTagValue, err := client.SystemFunctions.GetTag(ctx, tagId, oid, objectType) if err != nil { return diag.FromErr(err) } - if err := d.Set("tag_value", tagValue); err != nil { - return diag.FromErr(err) + if objectTagValue != nil && *objectTagValue == tagValue { + correctObjectIds = append(correctObjectIds, oid.FullyQualifiedName()) } } - return diags + if err := d.Set("object_identifiers", correctObjectIds); err != nil { + return diag.FromErr(err) + } + // ensure that object_type is upper case in the state + if err := d.Set("object_type", objectType); err != nil { + return diag.FromErr(err) + } + return nil } -func UpdateContextTagAssociation(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { +func UpdateContextTagAssociation(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*provider.Context).Client - tid, ids, ot := TagIdentifierAndObjectIdentifier(d) - for _, oid := range ids { - if d.HasChange("skip_validation") { - o, n := d.GetChange("skip_validation") - log.Printf("[DEBUG] skip_validation changed from %v to %v", o, n) + tagId, _, objectType, err := TagIdentifierAndObjectIdentifier(d) + if err != nil { + return diag.FromErr(err) + } + if d.HasChanges("object_identifiers", "tag_value") { + tagValue := d.Get("tag_value").(string) + + o, n := d.GetChange("object_identifiers") + + oldIds, err := ExpandObjectIdentifierSet(o.(*schema.Set).List(), objectType) + if err != nil { + return diag.FromErr(err) + } + newIds, err := ExpandObjectIdentifierSet(n.(*schema.Set).List(), objectType) + if err != nil { + return diag.FromErr(err) + } + + addedIds, removedIds, commonIds := ListDiffWithCommonItems(oldIds, newIds) + + for _, id := range addedIds { + request := sdk.NewSetTagRequest(objectType, id).WithSetTags([]sdk.TagAssociation{ + { + Name: tagId, + Value: tagValue, + }, + }) + if err := client.Tags.Set(ctx, request); err != nil { + return diag.FromErr(err) + } + } + + for _, id := range removedIds { + if objectType == sdk.ObjectTypeColumn { + skip, err := skipColumnIfDoesNotExist(ctx, client, id) + if err != nil { + return diag.FromErr(err) + } + if skip { + continue + } + } + request := sdk.NewUnsetTagRequest(objectType, id).WithUnsetTags([]sdk.ObjectIdentifier{tagId}).WithIfExists(true) + if err := client.Tags.Unset(ctx, request); err != nil { + return diag.FromErr(err) + } } + if d.HasChange("tag_value") { - tagValue, ok := d.GetOk("tag_value") - if ok { - request := sdk.NewSetTagRequest(ot, oid).WithSetTags([]sdk.TagAssociation{ + for _, id := range commonIds { + request := sdk.NewSetTagRequest(objectType, id).WithSetTags([]sdk.TagAssociation{ { - Name: tid, - Value: tagValue.(string), + Name: tagId, + Value: tagValue, }, }) if err := client.Tags.Set(ctx, request); err != nil { return diag.FromErr(err) } - } else { - request := sdk.NewUnsetTagRequest(ot, oid).WithUnsetTags([]sdk.ObjectIdentifier{tid}) - if err := client.Tags.Unset(ctx, request); err != nil { - return diag.FromErr(err) - } } + d.SetId(helpers.EncodeResourceIdentifier(tagId.FullyQualifiedName(), tagValue, string(objectType))) } } + return ReadContextTagAssociation(ctx, d, meta) } func DeleteContextTagAssociation(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*provider.Context).Client - tid, ids, ot := TagIdentifierAndObjectIdentifier(d) - for _, oid := range ids { - request := sdk.NewUnsetTagRequest(ot, oid).WithUnsetTags([]sdk.ObjectIdentifier{tid}) + tagId, ids, objectType, err := TagIdentifierAndObjectIdentifier(d) + if err != nil { + return diag.FromErr(err) + } + for _, id := range ids { + if objectType == sdk.ObjectTypeColumn { + skip, err := skipColumnIfDoesNotExist(ctx, client, id) + if err != nil { + return diag.FromErr(err) + } + if skip { + continue + } + } + request := sdk.NewUnsetTagRequest(objectType, id).WithUnsetTags([]sdk.ObjectIdentifier{tagId}).WithIfExists(true) if err := client.Tags.Unset(ctx, request); err != nil { return diag.FromErr(err) } @@ -249,3 +301,31 @@ func DeleteContextTagAssociation(ctx context.Context, d *schema.ResourceData, me d.SetId("") return nil } + +// we need to skip the column manually, because ALTER COLUMN lacks IF EXISTS +func skipColumnIfDoesNotExist(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) (bool, error) { + columnId, ok := id.(sdk.TableColumnIdentifier) + if !ok { + return false, errors.New("invalid column identifier") + } + // TODO [SNOW-1007542]: use SHOW COLUMNS + _, err := client.Tables.ShowByID(ctx, columnId.SchemaObjectId()) + if err != nil { + if errors.Is(err, sdk.ErrObjectNotFound) { + log.Printf("[DEBUG] table %s not found, skipping\n", columnId.SchemaObjectId()) + return true, nil + } + return false, err + } + columns, err := client.Tables.DescribeColumns(ctx, sdk.NewDescribeTableColumnsRequest(columnId.SchemaObjectId())) + if err != nil { + return false, err + } + if _, err := collections.FindFirst(columns, func(c sdk.TableColumnDetails) bool { + return c.Name == columnId.Name() + }); err != nil { + log.Printf("[DEBUG] column %s not found in table %s, skipping\n", columnId.Name(), columnId.SchemaObjectId()) + return true, nil + } + return false, nil +} diff --git a/pkg/resources/tag_association_acceptance_test.go b/pkg/resources/tag_association_acceptance_test.go index 37e1d54804..86e616e529 100644 --- a/pkg/resources/tag_association_acceptance_test.go +++ b/pkg/resources/tag_association_acceptance_test.go @@ -3,27 +3,41 @@ package resources_test import ( "context" "fmt" + "strings" "testing" acc "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert/resourceassert" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/config" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/config/model" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/testenvs" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/provider" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" - "github.com/hashicorp/terraform-plugin-testing/config" + tfconfig "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_TagAssociation(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) tagId := acc.TestClient().Ids.RandomSchemaObjectIdentifier() + tag2Id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() + tagValue := "foo" + tagValue2 := "bar" + databaseId := acc.TestClient().Ids.DatabaseId() resourceName := "snowflake_tag_association.test" - m := func() map[string]config.Variable { - return map[string]config.Variable{ - "tag_name": config.StringVariable(tagId.Name()), - "database": config.StringVariable(acc.TestDatabaseName), - "schema": config.StringVariable(acc.TestSchemaName), + m := func(tagId sdk.SchemaObjectIdentifier, tagValue string) map[string]tfconfig.Variable { + return map[string]tfconfig.Variable{ + "tag_name": tfconfig.StringVariable(tagId.Name()), + "tag_value": tfconfig.StringVariable(tagValue), + "database": tfconfig.StringVariable(databaseId.Name()), + "schema": tfconfig.StringVariable(acc.TestSchemaName), + "database_fully_qualified_name": tfconfig.StringVariable(databaseId.FullyQualifiedName()), } } resource.Test(t, resource.TestCase{ @@ -32,15 +46,229 @@ func TestAcc_TagAssociation(t *testing.T) { TerraformVersionChecks: []tfversion.TerraformVersionCheck{ tfversion.RequireAbove(tfversion.Version1_5_0), }, - CheckDestroy: nil, + CheckDestroy: acc.CheckResourceTagUnset(t), Steps: []resource.TestStep{ { ConfigDirectory: acc.ConfigurationDirectory("TestAcc_TagAssociation/basic"), - ConfigVariables: m(), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(resourceName, "object_type", "DATABASE"), + ConfigVariables: m(tagId, tagValue), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "id", helpers.EncodeSnowflakeID(tagId.FullyQualifiedName(), tagValue, string(sdk.ObjectTypeDatabase))), + resource.TestCheckResourceAttr(resourceName, "object_type", string(sdk.ObjectTypeDatabase)), + resource.TestCheckResourceAttr(resourceName, "object_identifiers.#", "1"), + resource.TestCheckTypeSetElemAttr(resourceName, "object_identifiers.*", acc.TestClient().Ids.DatabaseId().FullyQualifiedName()), + resource.TestCheckResourceAttr(resourceName, "tag_id", tagId.FullyQualifiedName()), + resource.TestCheckResourceAttr(resourceName, "tag_value", tagValue), + ), + }, + // external change - unset tag + { + PreConfig: func() { + acc.TestClient().Tag.Unset(t, sdk.ObjectTypeDatabase, databaseId, []sdk.ObjectIdentifier{tagId}) + }, + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_TagAssociation/basic"), + ConfigVariables: m(tagId, tagValue), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "id", helpers.EncodeSnowflakeID(tagId.FullyQualifiedName(), tagValue, string(sdk.ObjectTypeDatabase))), + resource.TestCheckResourceAttr(resourceName, "object_type", string(sdk.ObjectTypeDatabase)), + resource.TestCheckResourceAttr(resourceName, "object_identifiers.#", "1"), + resource.TestCheckTypeSetElemAttr(resourceName, "object_identifiers.*", acc.TestClient().Ids.DatabaseId().FullyQualifiedName()), + resource.TestCheckResourceAttr(resourceName, "tag_id", tagId.FullyQualifiedName()), + resource.TestCheckResourceAttr(resourceName, "tag_value", tagValue), + ), + }, + // external change - set a different value + { + PreConfig: func() { + acc.TestClient().Tag.Set(t, sdk.ObjectTypeDatabase, databaseId, []sdk.TagAssociation{ + { + Name: tagId, + Value: "external", + }, + }) + }, + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_TagAssociation/basic"), + ConfigVariables: m(tagId, tagValue), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "id", helpers.EncodeSnowflakeID(tagId.FullyQualifiedName(), tagValue, string(sdk.ObjectTypeDatabase))), + resource.TestCheckResourceAttr(resourceName, "object_type", string(sdk.ObjectTypeDatabase)), + resource.TestCheckResourceAttr(resourceName, "object_identifiers.#", "1"), + resource.TestCheckTypeSetElemAttr(resourceName, "object_identifiers.*", acc.TestClient().Ids.DatabaseId().FullyQualifiedName()), + resource.TestCheckResourceAttr(resourceName, "tag_id", tagId.FullyQualifiedName()), + resource.TestCheckResourceAttr(resourceName, "tag_value", tagValue), + ), + }, + // change tag value + { + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_TagAssociation/basic"), + ConfigVariables: m(tagId, tagValue2), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionUpdate), + }, + }, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "id", helpers.EncodeSnowflakeID(tagId.FullyQualifiedName(), tagValue2, string(sdk.ObjectTypeDatabase))), + resource.TestCheckResourceAttr(resourceName, "object_type", string(sdk.ObjectTypeDatabase)), + resource.TestCheckResourceAttr(resourceName, "object_identifiers.#", "1"), + resource.TestCheckTypeSetElemAttr(resourceName, "object_identifiers.*", acc.TestClient().Ids.DatabaseId().FullyQualifiedName()), resource.TestCheckResourceAttr(resourceName, "tag_id", tagId.FullyQualifiedName()), - resource.TestCheckResourceAttr(resourceName, "tag_value", "finance"), + resource.TestCheckResourceAttr(resourceName, "tag_value", tagValue2), + ), + }, + // change tag id + { + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_TagAssociation/basic"), + ConfigVariables: m(tag2Id, tagValue2), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionDestroyBeforeCreate), + }, + }, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "id", helpers.EncodeSnowflakeID(tag2Id.FullyQualifiedName(), tagValue2, string(sdk.ObjectTypeDatabase))), + resource.TestCheckResourceAttr(resourceName, "object_type", string(sdk.ObjectTypeDatabase)), + resource.TestCheckResourceAttr(resourceName, "object_identifiers.#", "1"), + resource.TestCheckTypeSetElemAttr(resourceName, "object_identifiers.*", acc.TestClient().Ids.DatabaseId().FullyQualifiedName()), + resource.TestCheckResourceAttr(resourceName, "tag_id", tag2Id.FullyQualifiedName()), + resource.TestCheckResourceAttr(resourceName, "tag_value", tagValue2), + acc.CheckTagUnset(t, tagId, acc.TestClient().Ids.DatabaseId(), sdk.ObjectTypeDatabase), + ), + }, + { + ConfigVariables: m(tag2Id, tagValue2), + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_TagAssociation/basic"), + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + // object_identifiers does not get set because during the import, the configuration is considered as empty + ImportStateVerifyIgnore: []string{"skip_validation", "object_identifiers.#", "object_identifiers.0"}, + }, + // after refreshing the state, object_identifiers is correct + { + RefreshState: true, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "id", helpers.EncodeSnowflakeID(tag2Id.FullyQualifiedName(), tagValue2, string(sdk.ObjectTypeDatabase))), + resource.TestCheckResourceAttr(resourceName, "object_type", string(sdk.ObjectTypeDatabase)), + resource.TestCheckResourceAttr(resourceName, "object_identifiers.#", "1"), + resource.TestCheckTypeSetElemAttr(resourceName, "object_identifiers.*", acc.TestClient().Ids.DatabaseId().FullyQualifiedName()), + resource.TestCheckResourceAttr(resourceName, "tag_id", tag2Id.FullyQualifiedName()), + resource.TestCheckResourceAttr(resourceName, "tag_value", tagValue2), + ), + }, + }, + }) +} + +func TestAcc_TagAssociation_objectIdentifiers(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + acc.TestAccPreCheck(t) + + tag, tagCleanup := acc.TestClient().Tag.CreateTag(t) + t.Cleanup(tagCleanup) + dbRole1, dbRole1Cleanup := acc.TestClient().DatabaseRole.CreateDatabaseRole(t) + t.Cleanup(dbRole1Cleanup) + dbRole2, dbRole2Cleanup := acc.TestClient().DatabaseRole.CreateDatabaseRole(t) + t.Cleanup(dbRole2Cleanup) + dbRole3, dbRole3Cleanup := acc.TestClient().DatabaseRole.CreateDatabaseRole(t) + t.Cleanup(dbRole3Cleanup) + + model12 := model.TagAssociation("test", []sdk.ObjectIdentifier{dbRole1.ID(), dbRole2.ID()}, string(sdk.ObjectTypeDatabaseRole), tag.ID().FullyQualifiedName(), "foo") + model123 := model.TagAssociation("test", []sdk.ObjectIdentifier{dbRole1.ID(), dbRole2.ID(), dbRole3.ID()}, string(sdk.ObjectTypeDatabaseRole), tag.ID().FullyQualifiedName(), "foo") + model13 := model.TagAssociation("test", []sdk.ObjectIdentifier{dbRole1.ID(), dbRole3.ID()}, string(sdk.ObjectTypeDatabaseRole), tag.ID().FullyQualifiedName(), "foo") + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: resource.ComposeAggregateTestCheckFunc( + acc.CheckResourceTagUnset(t), + ), + Steps: []resource.TestStep{ + { + Config: config.FromModel(t, model12), + Check: assert.AssertThat(t, resourceassert.TagAssociationResource(t, model12.ResourceReference()). + HasObjectTypeString(string(sdk.ObjectTypeDatabaseRole)). + HasTagIdString(tag.ID().FullyQualifiedName()). + HasObjectIdentifiersLength(2). + HasTagValueString("foo"), + assert.Check(resource.TestCheckTypeSetElemAttr(model12.ResourceReference(), "object_identifiers.*", dbRole1.ID().FullyQualifiedName())), + assert.Check(resource.TestCheckTypeSetElemAttr(model12.ResourceReference(), "object_identifiers.*", dbRole2.ID().FullyQualifiedName())), + ), + }, + { + Config: config.FromModel(t, model123), + Check: assert.AssertThat(t, resourceassert.TagAssociationResource(t, model12.ResourceReference()). + HasObjectTypeString(string(sdk.ObjectTypeDatabaseRole)). + HasTagIdString(tag.ID().FullyQualifiedName()). + HasObjectIdentifiersLength(3). + HasTagValueString("foo"), + assert.Check(resource.TestCheckTypeSetElemAttr(model12.ResourceReference(), "object_identifiers.*", dbRole1.ID().FullyQualifiedName())), + assert.Check(resource.TestCheckTypeSetElemAttr(model12.ResourceReference(), "object_identifiers.*", dbRole2.ID().FullyQualifiedName())), + assert.Check(resource.TestCheckTypeSetElemAttr(model12.ResourceReference(), "object_identifiers.*", dbRole3.ID().FullyQualifiedName())), + ), + }, + { + Config: config.FromModel(t, model13), + Check: assert.AssertThat(t, resourceassert.TagAssociationResource(t, model13.ResourceReference()). + HasObjectTypeString(string(sdk.ObjectTypeDatabaseRole)). + HasTagIdString(tag.ID().FullyQualifiedName()). + HasObjectIdentifiersLength(2). + HasTagValueString("foo"), + assert.Check(resource.TestCheckTypeSetElemAttr(model13.ResourceReference(), "object_identifiers.*", dbRole1.ID().FullyQualifiedName())), + assert.Check(resource.TestCheckTypeSetElemAttr(model13.ResourceReference(), "object_identifiers.*", dbRole3.ID().FullyQualifiedName())), + assert.Check(acc.CheckTagUnset(t, tag.ID(), dbRole2.ID(), sdk.ObjectTypeDatabaseRole)), + ), + }, + }, + }) +} + +func TestAcc_TagAssociation_objectType(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + acc.TestAccPreCheck(t) + + tag, tagCleanup := acc.TestClient().Tag.CreateTag(t) + t.Cleanup(tagCleanup) + role, roleCleanup := acc.TestClient().Role.CreateRole(t) + t.Cleanup(roleCleanup) + dbRole, dbRoleCleanup := acc.TestClient().DatabaseRole.CreateDatabaseRole(t) + t.Cleanup(dbRoleCleanup) + + baseModel := model.TagAssociation("test", []sdk.ObjectIdentifier{role.ID()}, string(sdk.ObjectTypeRole), tag.ID().FullyQualifiedName(), "foo") + modelWithDifferentObjectType := model.TagAssociation("test", []sdk.ObjectIdentifier{dbRole.ID()}, string(sdk.ObjectTypeDatabaseRole), tag.ID().FullyQualifiedName(), "foo") + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: resource.ComposeAggregateTestCheckFunc( + acc.CheckResourceTagUnset(t), + ), + Steps: []resource.TestStep{ + { + Config: config.FromModel(t, baseModel), + Check: assert.AssertThat(t, resourceassert.TagAssociationResource(t, baseModel.ResourceReference()). + HasObjectTypeString(string(sdk.ObjectTypeRole)). + HasTagIdString(tag.ID().FullyQualifiedName()). + HasObjectIdentifiersLength(1). + HasTagValueString("foo"), + ), + }, + { + Config: config.FromModel(t, modelWithDifferentObjectType), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(modelWithDifferentObjectType.ResourceReference(), plancheck.ResourceActionDestroyBeforeCreate), + }, + }, + Check: assert.AssertThat(t, resourceassert.TagAssociationResource(t, baseModel.ResourceReference()). + HasObjectTypeString(string(sdk.ObjectTypeDatabaseRole)). + HasTagIdString(tag.ID().FullyQualifiedName()). + HasObjectIdentifiersLength(1). + HasTagValueString("foo"), + assert.Check(acc.CheckTagUnset(t, tag.ID(), role.ID(), sdk.ObjectTypeRole)), ), }, }, @@ -48,13 +276,16 @@ func TestAcc_TagAssociation(t *testing.T) { } func TestAcc_TagAssociationSchema(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) tagId := acc.TestClient().Ids.RandomSchemaObjectIdentifier() + schemaId := acc.TestClient().Ids.SchemaId() resourceName := "snowflake_tag_association.test" - m := func() map[string]config.Variable { - return map[string]config.Variable{ - "tag_name": config.StringVariable(tagId.Name()), - "database": config.StringVariable(acc.TestDatabaseName), - "schema": config.StringVariable(acc.TestSchemaName), + m := func() map[string]tfconfig.Variable { + return map[string]tfconfig.Variable{ + "tag_name": tfconfig.StringVariable(tagId.Name()), + "database": tfconfig.StringVariable(acc.TestDatabaseName), + "schema": tfconfig.StringVariable(acc.TestSchemaName), + "schema_fully_qualified_name": tfconfig.StringVariable(schemaId.FullyQualifiedName()), } } resource.Test(t, resource.TestCase{ @@ -68,8 +299,46 @@ func TestAcc_TagAssociationSchema(t *testing.T) { { ConfigDirectory: acc.ConfigurationDirectory("TestAcc_TagAssociation/schema"), ConfigVariables: m(), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(resourceName, "object_type", "SCHEMA"), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "id", helpers.EncodeSnowflakeID(tagId.FullyQualifiedName(), "TAG_VALUE", string(sdk.ObjectTypeSchema))), + resource.TestCheckResourceAttr(resourceName, "object_type", string(sdk.ObjectTypeSchema)), + resource.TestCheckResourceAttr(resourceName, "object_identifiers.#", "1"), + resource.TestCheckTypeSetElemAttr(resourceName, "object_identifiers.*", schemaId.FullyQualifiedName()), + resource.TestCheckResourceAttr(resourceName, "tag_id", tagId.FullyQualifiedName()), + resource.TestCheckResourceAttr(resourceName, "tag_value", "TAG_VALUE"), + ), + }, + }, + }) +} + +// proves https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/3235 is fixed +func TestAcc_TagAssociation_lowercaseObjectType(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + acc.TestAccPreCheck(t) + + tag, tagCleanup := acc.TestClient().Tag.CreateTag(t) + t.Cleanup(tagCleanup) + objectType := strings.ToLower(string(sdk.ObjectTypeSchema)) + objectId := acc.TestClient().Ids.SchemaId() + + model := model.TagAssociation("test", []sdk.ObjectIdentifier{objectId}, objectType, tag.ID().FullyQualifiedName(), "foo") + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + PreCheck: func() { acc.TestAccPreCheck(t) }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: nil, + Steps: []resource.TestStep{ + { + Config: config.FromModel(t, model), + Check: assert.AssertThat(t, resourceassert.TagAssociationResource(t, model.ResourceReference()). + HasIdString(helpers.EncodeSnowflakeID(tag.ID().FullyQualifiedName(), "foo", string(sdk.ObjectTypeSchema))). + HasObjectTypeString(string(sdk.ObjectTypeSchema)). + HasTagIdString(tag.ID().FullyQualifiedName()). + HasObjectIdentifiersLength(1). + HasTagValueString("foo"), ), }, }, @@ -77,15 +346,20 @@ func TestAcc_TagAssociationSchema(t *testing.T) { } func TestAcc_TagAssociationColumn(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + tagId := acc.TestClient().Ids.RandomSchemaObjectIdentifier() - tableName := acc.TestClient().Ids.Alpha() + tableId := acc.TestClient().Ids.RandomSchemaObjectIdentifier() + columnId := sdk.NewTableColumnIdentifier(tableId.DatabaseName(), tableId.SchemaName(), tableId.Name(), "column") resourceName := "snowflake_tag_association.test" - m := func() map[string]config.Variable { - return map[string]config.Variable{ - "tag_name": config.StringVariable(tagId.Name()), - "table_name": config.StringVariable(tableName), - "database": config.StringVariable(acc.TestDatabaseName), - "schema": config.StringVariable(acc.TestSchemaName), + m := func() map[string]tfconfig.Variable { + return map[string]tfconfig.Variable{ + "tag_name": tfconfig.StringVariable(tagId.Name()), + "table_name": tfconfig.StringVariable(tableId.Name()), + "database": tfconfig.StringVariable(acc.TestDatabaseName), + "schema": tfconfig.StringVariable(acc.TestSchemaName), + "column": tfconfig.StringVariable("column"), + "column_fully_qualified_name": tfconfig.StringVariable(columnId.FullyQualifiedName()), } } resource.Test(t, resource.TestCase{ @@ -99,29 +373,31 @@ func TestAcc_TagAssociationColumn(t *testing.T) { { ConfigDirectory: acc.ConfigurationDirectory("TestAcc_TagAssociation/column"), ConfigVariables: m(), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(resourceName, "object_type", "COLUMN"), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "id", helpers.EncodeSnowflakeID(tagId.FullyQualifiedName(), "TAG_VALUE", string(sdk.ObjectTypeColumn))), + resource.TestCheckResourceAttr(resourceName, "object_type", string(sdk.ObjectTypeColumn)), + resource.TestCheckResourceAttr(resourceName, "object_identifiers.#", "1"), + resource.TestCheckTypeSetElemAttr(resourceName, "object_identifiers.*", columnId.FullyQualifiedName()), resource.TestCheckResourceAttr(resourceName, "tag_id", tagId.FullyQualifiedName()), resource.TestCheckResourceAttr(resourceName, "tag_value", "TAG_VALUE"), - resource.TestCheckResourceAttr(resourceName, "object_identifier.0.%", "3"), - resource.TestCheckResourceAttr(resourceName, "object_identifier.0.name", fmt.Sprintf("%s.column_name", tableName)), - resource.TestCheckResourceAttr(resourceName, "object_identifier.0.database", acc.TestDatabaseName), - resource.TestCheckResourceAttr(resourceName, "object_identifier.0.schema", acc.TestSchemaName)), + ), }, }, }) } func TestAcc_TagAssociationIssue1202(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + tagId := acc.TestClient().Ids.RandomSchemaObjectIdentifier() tableName := acc.TestClient().Ids.Alpha() resourceName := "snowflake_tag_association.test" - m := func() map[string]config.Variable { - return map[string]config.Variable{ - "tag_name": config.StringVariable(tagId.Name()), - "table_name": config.StringVariable(tableName), - "database": config.StringVariable(acc.TestDatabaseName), - "schema": config.StringVariable(acc.TestSchemaName), + m := func() map[string]tfconfig.Variable { + return map[string]tfconfig.Variable{ + "tag_name": tfconfig.StringVariable(tagId.Name()), + "table_name": tfconfig.StringVariable(tableName), + "database": tfconfig.StringVariable(acc.TestDatabaseName), + "schema": tfconfig.StringVariable(acc.TestSchemaName), } } resource.Test(t, resource.TestCase{ @@ -135,7 +411,7 @@ func TestAcc_TagAssociationIssue1202(t *testing.T) { { ConfigDirectory: acc.ConfigurationDirectory("TestAcc_TagAssociation/issue1202"), ConfigVariables: m(), - Check: resource.ComposeTestCheckFunc( + Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr(resourceName, "object_type", "TABLE"), resource.TestCheckResourceAttr(resourceName, "tag_id", tagId.FullyQualifiedName()), resource.TestCheckResourceAttr(resourceName, "tag_value", "v1"), @@ -146,21 +422,24 @@ func TestAcc_TagAssociationIssue1202(t *testing.T) { } func TestAcc_TagAssociationIssue1909(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + tagId := acc.TestClient().Ids.RandomSchemaObjectIdentifier() - tableName := acc.TestClient().Ids.Alpha() - tableName2 := acc.TestClient().Ids.Alpha() - columnName := "test.column" + tableId1 := acc.TestClient().Ids.RandomSchemaObjectIdentifier() + tableId2 := acc.TestClient().Ids.RandomSchemaObjectIdentifier() + columnId1 := sdk.NewTableColumnIdentifier(tableId1.DatabaseName(), tableId1.SchemaName(), tableId1.Name(), "test.column") + columnId2 := sdk.NewTableColumnIdentifier(tableId2.DatabaseName(), tableId2.SchemaName(), tableId2.Name(), "test.column") resourceName := "snowflake_tag_association.test" - objectID := sdk.NewTableColumnIdentifier(acc.TestDatabaseName, acc.TestSchemaName, tableName, columnName) - objectID2 := sdk.NewTableColumnIdentifier(acc.TestDatabaseName, acc.TestSchemaName, tableName2, columnName) - m := func() map[string]config.Variable { - return map[string]config.Variable{ - "tag_name": config.StringVariable(tagId.Name()), - "table_name": config.StringVariable(tableName), - "table_name2": config.StringVariable(tableName2), - "column_name": config.StringVariable("test.column"), - "database": config.StringVariable(acc.TestDatabaseName), - "schema": config.StringVariable(acc.TestSchemaName), + m := func() map[string]tfconfig.Variable { + return map[string]tfconfig.Variable{ + "tag_name": tfconfig.StringVariable(tagId.Name()), + "table_name": tfconfig.StringVariable(tableId1.Name()), + "table_name2": tfconfig.StringVariable(tableId2.Name()), + "column_name": tfconfig.StringVariable("test.column"), + "column_fully_qualified_name": tfconfig.StringVariable(columnId1.FullyQualifiedName()), + "column2_fully_qualified_name": tfconfig.StringVariable(columnId2.FullyQualifiedName()), + "database": tfconfig.StringVariable(acc.TestDatabaseName), + "schema": tfconfig.StringVariable(acc.TestSchemaName), } } resource.Test(t, resource.TestCase{ @@ -174,12 +453,12 @@ func TestAcc_TagAssociationIssue1909(t *testing.T) { { ConfigDirectory: acc.ConfigurationDirectory("TestAcc_TagAssociation/issue1909"), ConfigVariables: m(), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(resourceName, "object_type", "COLUMN"), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "object_type", string(sdk.ObjectTypeColumn)), resource.TestCheckResourceAttr(resourceName, "tag_id", tagId.FullyQualifiedName()), resource.TestCheckResourceAttr(resourceName, "tag_value", "v1"), - testAccCheckTableColumnTagAssociation(tagId, objectID, "v1"), - testAccCheckTableColumnTagAssociation(tagId, objectID2, "v1"), + testAccCheckTableColumnTagAssociation(tagId, columnId1, "v1"), + testAccCheckTableColumnTagAssociation(tagId, columnId2, "v1"), ), }, }, @@ -194,25 +473,30 @@ func testAccCheckTableColumnTagAssociation(tagID sdk.SchemaObjectIdentifier, obj if err != nil { return err } - if tagValue != tv { - return fmt.Errorf("expected tag value %s, got %s", tagValue, tv) + if tv == nil { + return fmt.Errorf("expected tag value %s, got nil", tagValue) + } + if tagValue != *tv { + return fmt.Errorf("expected tag value %s, got %s", tagValue, *tv) } return nil } } +// TODO(SNOW-1165821): use a separate account with ORGADMIN in CI + func TestAcc_TagAssociationAccountIssues1910(t *testing.T) { - // todo: use role with ORGADMIN in CI (SNOW-1165821) - _ = testenvs.GetOrSkipTest(t, testenvs.TestAccountCreate) + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + tagId := acc.TestClient().Ids.RandomSchemaObjectIdentifier() - accountName := acc.TestClient().Ids.Alpha() + accountId := acc.TestClient().Context.CurrentAccountIdentifier(t) resourceName := "snowflake_tag_association.test" - m := func() map[string]config.Variable { - return map[string]config.Variable{ - "tag_name": config.StringVariable(tagId.Name()), - "account_name": config.StringVariable(accountName), - "database": config.StringVariable(acc.TestDatabaseName), - "schema": config.StringVariable(acc.TestSchemaName), + m := func() map[string]tfconfig.Variable { + return map[string]tfconfig.Variable{ + "tag_name": tfconfig.StringVariable(tagId.Name()), + "account_fully_qualified_name": tfconfig.StringVariable(accountId.FullyQualifiedName()), + "database": tfconfig.StringVariable(acc.TestDatabaseName), + "schema": tfconfig.StringVariable(acc.TestSchemaName), } } @@ -227,9 +511,11 @@ func TestAcc_TagAssociationAccountIssues1910(t *testing.T) { { ConfigDirectory: acc.ConfigurationDirectory("TestAcc_TagAssociation/issue1910"), ConfigVariables: m(), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(resourceName, "object_type", "ACCOUNT"), - resource.TestCheckResourceAttr(resourceName, "tag_id", tagId.Name()), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "object_type", string(sdk.ObjectTypeAccount)), + resource.TestCheckResourceAttr(resourceName, "object_identifiers.#", "1"), + resource.TestCheckTypeSetElemAttr(resourceName, "object_identifiers.*", accountId.FullyQualifiedName()), + resource.TestCheckResourceAttr(resourceName, "tag_id", tagId.FullyQualifiedName()), resource.TestCheckResourceAttr(resourceName, "tag_value", "v1"), ), }, @@ -238,29 +524,34 @@ func TestAcc_TagAssociationAccountIssues1910(t *testing.T) { } func TestAcc_TagAssociationIssue1926(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + tagId := acc.TestClient().Ids.RandomSchemaObjectIdentifier() - tableName := acc.TestClient().Ids.Alpha() + tableId1 := acc.TestClient().Ids.RandomSchemaObjectIdentifier() + columnId1 := sdk.NewTableColumnIdentifier(tableId1.DatabaseName(), tableId1.SchemaName(), tableId1.Name(), "init") resourceName := "snowflake_tag_association.test" - columnName := "test.column" - m := func() map[string]config.Variable { - return map[string]config.Variable{ - "tag_name": config.StringVariable(tagId.Name()), - "table_name": config.StringVariable(tableName), - "column_name": config.StringVariable(columnName), - "database": config.StringVariable(acc.TestDatabaseName), - "schema": config.StringVariable(acc.TestSchemaName), + m := func() map[string]tfconfig.Variable { + return map[string]tfconfig.Variable{ + "tag_name": tfconfig.StringVariable(tagId.Name()), + "table_name": tfconfig.StringVariable(tableId1.Name()), + "column_name": tfconfig.StringVariable(columnId1.Name()), + "column_fully_qualified_name": tfconfig.StringVariable(columnId1.FullyQualifiedName()), + "database": tfconfig.StringVariable(acc.TestDatabaseName), + "schema": tfconfig.StringVariable(acc.TestSchemaName), } } m2 := m() - tableName2 := "table.test" - columnName2 := "column" - columnName3 := "column.test" - m2["table_name"] = config.StringVariable(tableName2) - m2["column_name"] = config.StringVariable(columnName2) + tableId2 := acc.TestClient().Ids.RandomSchemaObjectIdentifierWithPrefix("table.test") + columnId2 := sdk.NewTableColumnIdentifier(tableId2.DatabaseName(), tableId2.SchemaName(), tableId2.Name(), "column") + columnId3 := sdk.NewTableColumnIdentifier(tableId2.DatabaseName(), tableId2.SchemaName(), tableId2.Name(), "column.test") + m2["table_name"] = tfconfig.StringVariable(tableId2.Name()) + m2["column_name"] = tfconfig.StringVariable(columnId2.Name()) + m2["column_fully_qualified_name"] = tfconfig.StringVariable(columnId2.FullyQualifiedName()) m3 := m() - m3["table_name"] = config.StringVariable(tableName2) - m3["column_name"] = config.StringVariable(columnName3) + m3["table_name"] = tfconfig.StringVariable(tableId2.Name()) + m3["column_name"] = tfconfig.StringVariable(columnId3.Name()) + m3["column_fully_qualified_name"] = tfconfig.StringVariable(columnId3.FullyQualifiedName()) resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, PreCheck: func() { acc.TestAccPreCheck(t) }, @@ -272,44 +563,122 @@ func TestAcc_TagAssociationIssue1926(t *testing.T) { { ConfigDirectory: acc.ConfigurationDirectory("TestAcc_TagAssociation/issue1926"), ConfigVariables: m(), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(resourceName, "object_type", "COLUMN"), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "id", helpers.EncodeSnowflakeID(tagId.FullyQualifiedName(), "TAG_VALUE", string(sdk.ObjectTypeColumn))), + resource.TestCheckResourceAttr(resourceName, "object_type", string(sdk.ObjectTypeColumn)), + resource.TestCheckResourceAttr(resourceName, "object_identifiers.#", "1"), + resource.TestCheckTypeSetElemAttr(resourceName, "object_identifiers.*", columnId1.FullyQualifiedName()), resource.TestCheckResourceAttr(resourceName, "tag_id", tagId.FullyQualifiedName()), - resource.TestCheckResourceAttr(resourceName, "tag_value", "v1"), - resource.TestCheckResourceAttr(resourceName, "object_identifier.0.%", "3"), - resource.TestCheckResourceAttr(resourceName, "object_identifier.0.name", fmt.Sprintf("%s.%s", tableName, columnName)), - resource.TestCheckResourceAttr(resourceName, "object_identifier.0.database", acc.TestDatabaseName), - resource.TestCheckResourceAttr(resourceName, "object_identifier.0.schema", acc.TestSchemaName), + resource.TestCheckResourceAttr(resourceName, "tag_value", "TAG_VALUE"), + ), + }, + { + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_TagAssociation/issue1926"), + ConfigVariables: m2, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "id", helpers.EncodeSnowflakeID(tagId.FullyQualifiedName(), "TAG_VALUE", string(sdk.ObjectTypeColumn))), + resource.TestCheckResourceAttr(resourceName, "object_type", string(sdk.ObjectTypeColumn)), + resource.TestCheckResourceAttr(resourceName, "object_identifiers.#", "1"), + resource.TestCheckTypeSetElemAttr(resourceName, "object_identifiers.*", columnId2.FullyQualifiedName()), + resource.TestCheckResourceAttr(resourceName, "tag_id", tagId.FullyQualifiedName()), + resource.TestCheckResourceAttr(resourceName, "tag_value", "TAG_VALUE"), + ), + }, + { + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_TagAssociation/issue1926"), + ConfigVariables: m3, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "id", helpers.EncodeSnowflakeID(tagId.FullyQualifiedName(), "TAG_VALUE", string(sdk.ObjectTypeColumn))), + resource.TestCheckResourceAttr(resourceName, "object_type", string(sdk.ObjectTypeColumn)), + resource.TestCheckResourceAttr(resourceName, "object_identifiers.#", "1"), + resource.TestCheckTypeSetElemAttr(resourceName, "object_identifiers.*", columnId3.FullyQualifiedName()), + resource.TestCheckResourceAttr(resourceName, "tag_id", tagId.FullyQualifiedName()), + resource.TestCheckResourceAttr(resourceName, "tag_value", "TAG_VALUE"), ), }, - /* - todo: (SNOW-1205719) uncomment this - { - ConfigDirectory: acc.ConfigurationDirectory("TestAcc_TagAssociation/issue1926"), - ConfigVariables: m2, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(resourceName, "object_type", "COLUMN"), - resource.TestCheckResourceAttr(resourceName, "tag_id", fmt.Sprintf("%s|%s|%s", acc.TestDatabaseName, acc.TestSchemaName, tagName)), - resource.TestCheckResourceAttr(resourceName, "tag_value", "v1"), - resource.TestCheckResourceAttr(resourceName, "object_identifier.0.%", "3"), - resource.TestCheckResourceAttr(resourceName, "object_identifier.0.name", fmt.Sprintf("%s.%s", tableName2, columnName2)), - resource.TestCheckResourceAttr(resourceName, "object_identifier.0.database", acc.TestDatabaseName), - resource.TestCheckResourceAttr(resourceName, "object_identifier.0.schema", acc.TestSchemaName), - ), + }, + }) +} + +func TestAcc_TagAssociation_migrateFromVersion_0_98_0(t *testing.T) { + t.Setenv(string(testenvs.ConfigureClientOnce), "") + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + acc.TestAccPreCheck(t) + tagId := acc.TestClient().Ids.RandomSchemaObjectIdentifier() + resourceName := "snowflake_tag_association.test" + schemaId := acc.TestClient().Ids.SchemaId() + + m := func() tfconfig.Variables { + return tfconfig.Variables{ + "tag_name": tfconfig.StringVariable(tagId.Name()), + "database": tfconfig.StringVariable(acc.TestDatabaseName), + "schema": tfconfig.StringVariable(acc.TestSchemaName), + "schema_fully_qualified_name": tfconfig.StringVariable(schemaId.FullyQualifiedName()), + } + } + + resource.Test(t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + + Steps: []resource.TestStep{ + { + ExternalProviders: acc.ExternalProviderWithExactVersion("0.98.0"), + Config: tagAssociation_v_0_98_0(tagId, "TAG_VALUE", sdk.ObjectTypeSchema, schemaId), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "id", helpers.EncodeSnowflakeID(tagId.DatabaseName(), tagId.SchemaName(), tagId.Name())), + resource.TestCheckResourceAttr(resourceName, "object_type", string(sdk.ObjectTypeSchema)), + resource.TestCheckResourceAttr(resourceName, "object_identifier.#", "1"), + resource.TestCheckResourceAttr(resourceName, "object_identifier.0.name", schemaId.Name()), + resource.TestCheckResourceAttr(resourceName, "object_identifier.0.database", schemaId.DatabaseName()), + resource.TestCheckResourceAttr(resourceName, "object_identifier.0.schema", ""), + resource.TestCheckResourceAttr(resourceName, "tag_id", tagId.FullyQualifiedName()), + resource.TestCheckResourceAttr(resourceName, "tag_value", "TAG_VALUE"), + ), + }, + { + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_TagAssociation/schema"), + ConfigVariables: m(), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionNoop), + }, + PostApplyPostRefresh: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionNoop), + }, }, - { - ConfigDirectory: acc.ConfigurationDirectory("TestAcc_TagAssociation/issue1926"), - ConfigVariables: m3, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(resourceName, "object_type", "COLUMN"), - resource.TestCheckResourceAttr(resourceName, "tag_id", fmt.Sprintf("%s|%s|%s", acc.TestDatabaseName, acc.TestSchemaName, tagName)), - resource.TestCheckResourceAttr(resourceName, "tag_value", "v1"), - resource.TestCheckResourceAttr(resourceName, "object_identifier.0.%", "3"), - resource.TestCheckResourceAttr(resourceName, "object_identifier.0.name", fmt.Sprintf("%s.%s", tableName2, columnName3)), - resource.TestCheckResourceAttr(resourceName, "object_identifier.0.database", acc.TestDatabaseName), - resource.TestCheckResourceAttr(resourceName, "object_identifier.0.schema", acc.TestSchemaName), - ), - },*/ + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "id", helpers.EncodeSnowflakeID(tagId.FullyQualifiedName(), "TAG_VALUE", string(sdk.ObjectTypeSchema))), + resource.TestCheckResourceAttr(resourceName, "object_type", string(sdk.ObjectTypeSchema)), + resource.TestCheckResourceAttr(resourceName, "object_identifiers.#", "1"), + resource.TestCheckTypeSetElemAttr(resourceName, "object_identifiers.*", schemaId.FullyQualifiedName()), + resource.TestCheckResourceAttr(resourceName, "tag_id", tagId.FullyQualifiedName()), + resource.TestCheckResourceAttr(resourceName, "tag_value", "TAG_VALUE"), + ), + }, }, }) } + +func tagAssociation_v_0_98_0(tagId sdk.SchemaObjectIdentifier, tagValue string, objectType sdk.ObjectType, objectId sdk.DatabaseObjectIdentifier) string { + s := ` +resource "snowflake_tag_association" "test" { + tag_id = snowflake_tag.test.fully_qualified_name + tag_value = "%[1]s" + object_type = "%[2]s" + object_identifier { + name = "%[3]s" + database = "%[4]s" + } +} + +resource "snowflake_tag" "test" { + name = "%[5]s" + database = "%[6]s" + schema = "%[7]s" +} +` + return fmt.Sprintf(s, tagValue, objectType, objectId.Name(), objectId.DatabaseName(), tagId.Name(), tagId.DatabaseName(), tagId.SchemaName()) +} diff --git a/pkg/resources/tag_association_state_upgraders.go b/pkg/resources/tag_association_state_upgraders.go new file mode 100644 index 0000000000..5072c55d47 --- /dev/null +++ b/pkg/resources/tag_association_state_upgraders.go @@ -0,0 +1,38 @@ +package resources + +import ( + "context" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" +) + +func v0_98_0_TagAssociationStateUpgrader(ctx context.Context, rawState map[string]any, meta any) (map[string]any, error) { + if rawState == nil { + return rawState, nil + } + tagId, err := sdk.ParseSchemaObjectIdentifier(rawState["tag_id"].(string)) + if err != nil { + return nil, err + } + tagValue := rawState["tag_value"].(string) + objectType := rawState["object_type"].(string) + + rawState["id"] = helpers.EncodeSnowflakeID(tagId.FullyQualifiedName(), tagValue, objectType) + + objectIdentifiersOld := rawState["object_identifier"].([]any) + objectIdentifiers := make([]string, 0, len(objectIdentifiersOld)) + for _, objectIdentifierOld := range objectIdentifiersOld { + obj := objectIdentifierOld.(map[string]any) + var id sdk.ObjectIdentifier + if objectType == string(sdk.ObjectTypeAccount) { + id = sdk.NewAccountIdentifierFromFullyQualifiedName(obj["name"].(string)) + } else { + id = getTagObjectIdentifier(obj) + } + objectIdentifiers = append(objectIdentifiers, id.FullyQualifiedName()) + } + rawState["object_identifiers"] = objectIdentifiers + + return rawState, nil +} diff --git a/pkg/resources/tag_association_test.go b/pkg/resources/tag_association_test.go index 57ebe30aff..efd605d3bc 100644 --- a/pkg/resources/tag_association_test.go +++ b/pkg/resources/tag_association_test.go @@ -7,75 +7,118 @@ import ( "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestTagIdentifierAndObjectIdentifier(t *testing.T) { + tagId := sdk.NewSchemaObjectIdentifier("test_db", "test_schema", "test_tag") + t.Run("account identifier", func(t *testing.T) { + in := map[string]any{ + "tag_id": tagId.FullyQualifiedName(), + "object_type": "ACCOUNT", + "object_identifiers": []any{ + "orgname.accountname", + }, + } + d := schema.TestResourceDataRaw(t, resources.TagAssociation().Schema, in) + tid, identifiers, objectType, err := resources.TagIdentifierAndObjectIdentifier(d) + require.NoError(t, err) + assert.Equal(t, tagId, tid) + assert.Len(t, identifiers, 1) + assert.Equal(t, `"orgname"."accountname"`, identifiers[0].FullyQualifiedName()) + assert.Equal(t, sdk.ObjectTypeAccount, objectType) + }) t.Run("account object identifier", func(t *testing.T) { - in := map[string]interface{}{ - "tag_id": "\"test_db\".\"test_schema\".\"test_tag\"", + in := map[string]any{ + "tag_id": tagId.FullyQualifiedName(), "object_type": "DATABASE", - "object_identifier": []interface{}{map[string]interface{}{ - "name": "test_db", - }}, + "object_identifiers": []any{ + "test_db", + }, } d := schema.TestResourceDataRaw(t, resources.TagAssociation().Schema, in) - tid, identifiers, objectType := resources.TagIdentifierAndObjectIdentifier(d) - assert.Equal(t, sdk.NewSchemaObjectIdentifier("test_db", "test_schema", "test_tag"), tid) + tid, identifiers, objectType, err := resources.TagIdentifierAndObjectIdentifier(d) + require.NoError(t, err) + assert.Equal(t, tagId, tid) assert.Len(t, identifiers, 1) assert.Equal(t, "\"test_db\"", identifiers[0].FullyQualifiedName()) assert.Equal(t, sdk.ObjectTypeDatabase, objectType) }) t.Run("database object identifier", func(t *testing.T) { - in := map[string]interface{}{ - "tag_id": "\"test_db\".\"test_schema\".\"test_tag\"", + in := map[string]any{ + "tag_id": tagId.FullyQualifiedName(), "object_type": "SCHEMA", - "object_identifier": []interface{}{map[string]interface{}{ - "name": "test_schema", - "database": "test_db", - }}, + "object_identifiers": []any{ + "test_db.test_schema", + }, } d := schema.TestResourceDataRaw(t, resources.TagAssociation().Schema, in) - tid, identifiers, objectType := resources.TagIdentifierAndObjectIdentifier(d) - assert.Equal(t, sdk.NewSchemaObjectIdentifier("test_db", "test_schema", "test_tag"), tid) + tid, identifiers, objectType, err := resources.TagIdentifierAndObjectIdentifier(d) + require.NoError(t, err) + assert.Equal(t, tagId, tid) assert.Len(t, identifiers, 1) assert.Equal(t, "\"test_db\".\"test_schema\"", identifiers[0].FullyQualifiedName()) assert.Equal(t, sdk.ObjectTypeSchema, objectType) }) t.Run("schema object identifier", func(t *testing.T) { - in := map[string]interface{}{ - "tag_id": "\"test_db\".\"test_schema\".\"test_tag\"", + in := map[string]any{ + "tag_id": tagId.FullyQualifiedName(), "object_type": "TABLE", - "object_identifier": []interface{}{map[string]interface{}{ - "name": "test_table", - "database": "test_db", - "schema": "test_schema", - }}, + "object_identifiers": []any{ + "test_db.test_schema.test_table", + }, } d := schema.TestResourceDataRaw(t, resources.TagAssociation().Schema, in) - tid, identifiers, objectType := resources.TagIdentifierAndObjectIdentifier(d) - assert.Equal(t, sdk.NewSchemaObjectIdentifier("test_db", "test_schema", "test_tag"), tid) + tid, identifiers, objectType, err := resources.TagIdentifierAndObjectIdentifier(d) + require.NoError(t, err) + assert.Equal(t, tagId, tid) assert.Len(t, identifiers, 1) assert.Equal(t, "\"test_db\".\"test_schema\".\"test_table\"", identifiers[0].FullyQualifiedName()) assert.Equal(t, sdk.ObjectTypeTable, objectType) }) t.Run("column object identifier", func(t *testing.T) { - in := map[string]interface{}{ + in := map[string]any{ "tag_id": "\"test_db\".\"test_schema\".\"test_tag\"", "object_type": "COLUMN", - "object_identifier": []interface{}{map[string]interface{}{ - "name": "test_table.test_column", - "database": "test_db", - "schema": "test_schema", - }}, + "object_identifiers": []any{ + "test_db.test_schema.test_table.test_column", + }, } d := schema.TestResourceDataRaw(t, resources.TagAssociation().Schema, in) - tid, identifiers, objectType := resources.TagIdentifierAndObjectIdentifier(d) + tid, identifiers, objectType, err := resources.TagIdentifierAndObjectIdentifier(d) + require.NoError(t, err) assert.Equal(t, sdk.NewSchemaObjectIdentifier("test_db", "test_schema", "test_tag"), tid) assert.Len(t, identifiers, 1) assert.Equal(t, "\"test_db\".\"test_schema\".\"test_table\".\"test_column\"", identifiers[0].FullyQualifiedName()) assert.Equal(t, sdk.ObjectTypeColumn, objectType) }) + + t.Run("invalid object identifier", func(t *testing.T) { + in := map[string]any{ + "tag_id": tagId.FullyQualifiedName(), + "object_type": "COLUMN", + "object_identifiers": []any{ + "\"", + }, + } + d := schema.TestResourceDataRaw(t, resources.TagAssociation().Schema, in) + _, _, _, err := resources.TagIdentifierAndObjectIdentifier(d) + require.ErrorContains(t, err, `unable to read identifier: ", err = parse error on line 1, column 2: extraneous or missing " in quoted-field`) + }) + + t.Run("invalid tag identifier", func(t *testing.T) { + in := map[string]any{ + "tag_id": "\"test_schema\".\"test_tag\"", + "object_type": "DATABASE", + "object_identifiers": []any{ + "test_db", + }, + } + d := schema.TestResourceDataRaw(t, resources.TagAssociation().Schema, in) + _, _, _, err := resources.TagIdentifierAndObjectIdentifier(d) + require.ErrorContains(t, err, `unexpected number of parts 2 in identifier "test_schema"."test_tag", expected 3 in a form of ".."`) + }) } diff --git a/pkg/resources/testdata/TestAcc_TagAssociation/basic/test.tf b/pkg/resources/testdata/TestAcc_TagAssociation/basic/test.tf index ba9ae97faa..69d7ab1172 100644 --- a/pkg/resources/testdata/TestAcc_TagAssociation/basic/test.tf +++ b/pkg/resources/testdata/TestAcc_TagAssociation/basic/test.tf @@ -2,15 +2,13 @@ resource "snowflake_tag" "test" { name = var.tag_name database = var.database schema = var.schema - allowed_values = ["finance", "hr"] + allowed_values = ["bar", "foo", "external"] comment = "Terraform acceptance test" } resource "snowflake_tag_association" "test" { - object_identifier { - name = var.database - } - object_type = "DATABASE" - tag_id = snowflake_tag.test.id - tag_value = "finance" + object_identifiers = [var.database_fully_qualified_name] + object_type = "DATABASE" + tag_id = snowflake_tag.test.fully_qualified_name + tag_value = var.tag_value } diff --git a/pkg/resources/testdata/TestAcc_TagAssociation/basic/variables.tf b/pkg/resources/testdata/TestAcc_TagAssociation/basic/variables.tf index 45a4ceb1c8..10ad654156 100644 --- a/pkg/resources/testdata/TestAcc_TagAssociation/basic/variables.tf +++ b/pkg/resources/testdata/TestAcc_TagAssociation/basic/variables.tf @@ -9,3 +9,11 @@ variable "database" { variable "schema" { type = string } + +variable "database_fully_qualified_name" { + type = string +} + +variable "tag_value" { + type = string +} diff --git a/pkg/resources/testdata/TestAcc_TagAssociation/column/test.tf b/pkg/resources/testdata/TestAcc_TagAssociation/column/test.tf index d10067998b..fea0c22120 100644 --- a/pkg/resources/testdata/TestAcc_TagAssociation/column/test.tf +++ b/pkg/resources/testdata/TestAcc_TagAssociation/column/test.tf @@ -10,19 +10,16 @@ resource "snowflake_table" "test" { schema = var.schema column { - name = "column_name" + name = var.column type = "VARIANT" } } resource "snowflake_tag_association" "test" { - object_identifier { - database = var.database - schema = var.schema - name = "${snowflake_table.test.name}.${snowflake_table.test.column[0].name}" - } + object_identifiers = [var.column_fully_qualified_name] object_type = "COLUMN" - tag_id = snowflake_tag.test.id + tag_id = snowflake_tag.test.fully_qualified_name tag_value = "TAG_VALUE" + depends_on = [snowflake_table.test] } diff --git a/pkg/resources/testdata/TestAcc_TagAssociation/column/variables.tf b/pkg/resources/testdata/TestAcc_TagAssociation/column/variables.tf index bf4d1ec509..8707532841 100644 --- a/pkg/resources/testdata/TestAcc_TagAssociation/column/variables.tf +++ b/pkg/resources/testdata/TestAcc_TagAssociation/column/variables.tf @@ -13,3 +13,11 @@ variable "database" { variable "schema" { type = string } + +variable "column" { + type = string +} + +variable "column_fully_qualified_name" { + type = string +} diff --git a/pkg/resources/testdata/TestAcc_TagAssociation/issue1202/main.tf b/pkg/resources/testdata/TestAcc_TagAssociation/issue1202/main.tf index 4c316e627b..dd81b42a82 100644 --- a/pkg/resources/testdata/TestAcc_TagAssociation/issue1202/main.tf +++ b/pkg/resources/testdata/TestAcc_TagAssociation/issue1202/main.tf @@ -15,17 +15,8 @@ resource "snowflake_table" "test" { } resource "snowflake_tag_association" "test" { - // we need to set the object_identifier to avoid the following error: - // provider_test.go:17: err: resource snowflake_tag_association: object_identifier: Optional or Required must be set, not both - // we should consider deprecating object_identifier in favor of object_name - // https://github.com/Snowflake-Labs/terraform-provider-snowflake/pull/2534#discussion_r1507570740 - // object_name = "\"${var.database}\".\"${var.schema}\".\"${var.table_name}\"" - object_identifier { - database = var.database - schema = var.schema - name = snowflake_table.test.name - } - object_type = "TABLE" - tag_id = snowflake_tag.test.id - tag_value = "v1" + object_identifiers = [snowflake_table.test.fully_qualified_name] + object_type = "TABLE" + tag_id = snowflake_tag.test.fully_qualified_name + tag_value = "v1" } diff --git a/pkg/resources/testdata/TestAcc_TagAssociation/issue1909/test.tf b/pkg/resources/testdata/TestAcc_TagAssociation/issue1909/test.tf index 01940e1183..519df8f05e 100644 --- a/pkg/resources/testdata/TestAcc_TagAssociation/issue1909/test.tf +++ b/pkg/resources/testdata/TestAcc_TagAssociation/issue1909/test.tf @@ -24,17 +24,9 @@ resource "snowflake_table" "test2" { } resource "snowflake_tag_association" "test" { - object_identifier { - database = var.database - schema = var.schema - name = "${snowflake_table.test.name}.${snowflake_table.test.column[0].name}" - } - object_identifier { - database = var.database - schema = var.schema - name = "${snowflake_table.test2.name}.${snowflake_table.test2.column[0].name}" - } - object_type = "COLUMN" - tag_id = snowflake_tag.test.id - tag_value = "v1" + object_identifiers = [var.column_fully_qualified_name, var.column2_fully_qualified_name] + object_type = "COLUMN" + tag_id = snowflake_tag.test.fully_qualified_name + tag_value = "v1" + depends_on = [snowflake_table.test, snowflake_table.test2] } diff --git a/pkg/resources/testdata/TestAcc_TagAssociation/issue1909/variables.tf b/pkg/resources/testdata/TestAcc_TagAssociation/issue1909/variables.tf index 58c3e62640..ff45812815 100644 --- a/pkg/resources/testdata/TestAcc_TagAssociation/issue1909/variables.tf +++ b/pkg/resources/testdata/TestAcc_TagAssociation/issue1909/variables.tf @@ -21,3 +21,11 @@ variable "database" { variable "schema" { type = string } + +variable "column_fully_qualified_name" { + type = string +} + +variable "column2_fully_qualified_name" { + type = string +} diff --git a/pkg/resources/testdata/TestAcc_TagAssociation/issue1910/test.tf b/pkg/resources/testdata/TestAcc_TagAssociation/issue1910/test.tf index c05081252c..8f5408595e 100644 --- a/pkg/resources/testdata/TestAcc_TagAssociation/issue1910/test.tf +++ b/pkg/resources/testdata/TestAcc_TagAssociation/issue1910/test.tf @@ -4,23 +4,9 @@ resource "snowflake_tag" "test" { schema = var.schema } -resource "snowflake_account" "test" { - name = var.account_name - admin_name = "someadmin" - admin_password = "123456" - first_name = "Ad" - last_name = "Min" - email = "admin@example.com" - must_change_password = false - edition = "BUSINESS_CRITICAL" - grace_period_in_days = 4 -} - resource "snowflake_tag_association" "test" { - object_identifier { - name = snowflake_account.test.name - } - object_type = "ACCOUNT" - tag_id = snowflake_tag.test.id - tag_value = "v1" + object_identifiers = [var.account_fully_qualified_name] + object_type = "ACCOUNT" + tag_id = snowflake_tag.test.fully_qualified_name + tag_value = "v1" } diff --git a/pkg/resources/testdata/TestAcc_TagAssociation/issue1910/variables.tf b/pkg/resources/testdata/TestAcc_TagAssociation/issue1910/variables.tf index 032442851b..e7f2ec926a 100644 --- a/pkg/resources/testdata/TestAcc_TagAssociation/issue1910/variables.tf +++ b/pkg/resources/testdata/TestAcc_TagAssociation/issue1910/variables.tf @@ -2,7 +2,7 @@ variable "tag_name" { type = string } -variable "account_name" { +variable "account_fully_qualified_name" { type = string } diff --git a/pkg/resources/testdata/TestAcc_TagAssociation/issue1926/test.tf b/pkg/resources/testdata/TestAcc_TagAssociation/issue1926/test.tf index 44e3c95437..8401d09080 100644 --- a/pkg/resources/testdata/TestAcc_TagAssociation/issue1926/test.tf +++ b/pkg/resources/testdata/TestAcc_TagAssociation/issue1926/test.tf @@ -7,18 +7,21 @@ resource "snowflake_table" "test" { name = var.table_name database = var.database schema = var.schema + // TODO(SNOW-1348114): use only one column, if possible. + // We need a dummy column here because a table must have at least one column, and when we rename the second one in the config, it gets dropped for a moment. + column { + name = "DUMMY" + type = "VARIANT" + } column { name = var.column_name type = "VARIANT" } } resource "snowflake_tag_association" "test" { - object_identifier { - database = var.database - schema = var.schema - name = "${snowflake_table.test.name}.${snowflake_table.test.column[0].name}" - } - object_type = "COLUMN" - tag_id = snowflake_tag.test.id - tag_value = "v1" + object_identifiers = [var.column_fully_qualified_name] + object_type = "COLUMN" + tag_id = snowflake_tag.test.fully_qualified_name + tag_value = "TAG_VALUE" + depends_on = [snowflake_table.test] } diff --git a/pkg/resources/testdata/TestAcc_TagAssociation/issue1926/variables.tf b/pkg/resources/testdata/TestAcc_TagAssociation/issue1926/variables.tf index 222b3e3b4e..778af61e64 100644 --- a/pkg/resources/testdata/TestAcc_TagAssociation/issue1926/variables.tf +++ b/pkg/resources/testdata/TestAcc_TagAssociation/issue1926/variables.tf @@ -17,3 +17,7 @@ variable "database" { variable "schema" { type = string } + +variable "column_fully_qualified_name" { + type = string +} diff --git a/pkg/resources/testdata/TestAcc_TagAssociation/schema/test.tf b/pkg/resources/testdata/TestAcc_TagAssociation/schema/test.tf index 5ebee25710..ba06156efc 100644 --- a/pkg/resources/testdata/TestAcc_TagAssociation/schema/test.tf +++ b/pkg/resources/testdata/TestAcc_TagAssociation/schema/test.tf @@ -6,12 +6,9 @@ resource "snowflake_tag" "test" { } resource "snowflake_tag_association" "test" { - object_identifier { - database = var.database - name = var.schema - } + object_identifiers = [var.schema_fully_qualified_name] object_type = "SCHEMA" - tag_id = snowflake_tag.test.id + tag_id = snowflake_tag.test.fully_qualified_name tag_value = "TAG_VALUE" } diff --git a/pkg/resources/testdata/TestAcc_TagAssociation/schema/variables.tf b/pkg/resources/testdata/TestAcc_TagAssociation/schema/variables.tf index 45a4ceb1c8..aa9cba3679 100644 --- a/pkg/resources/testdata/TestAcc_TagAssociation/schema/variables.tf +++ b/pkg/resources/testdata/TestAcc_TagAssociation/schema/variables.tf @@ -9,3 +9,7 @@ variable "database" { variable "schema" { type = string } + +variable "schema_fully_qualified_name" { + type = string +} diff --git a/pkg/sdk/object_types.go b/pkg/sdk/object_types.go index 6c65d694fe..d2cecefe37 100644 --- a/pkg/sdk/object_types.go +++ b/pkg/sdk/object_types.go @@ -1,6 +1,7 @@ package sdk import ( + "fmt" "slices" "strings" ) @@ -89,6 +90,82 @@ func (o ObjectType) IsWithArguments() bool { return slices.Contains([]ObjectType{ObjectTypeExternalFunction, ObjectTypeFunction, ObjectTypeProcedure}, o) } +var allObjectTypes = []ObjectType{ + ObjectTypeAccount, + ObjectTypeManagedAccount, + ObjectTypeUser, + ObjectTypeDatabaseRole, + ObjectTypeDataset, + ObjectTypeRole, + ObjectTypeIntegration, + ObjectTypeNetworkPolicy, + ObjectTypePasswordPolicy, + ObjectTypeSessionPolicy, + ObjectTypePrivacyPolicy, + ObjectTypeReplicationGroup, + ObjectTypeFailoverGroup, + ObjectTypeConnection, + ObjectTypeParameter, + ObjectTypeWarehouse, + ObjectTypeResourceMonitor, + ObjectTypeDatabase, + ObjectTypeSchema, + ObjectTypeShare, + ObjectTypeTable, + ObjectTypeDynamicTable, + ObjectTypeCortexSearchService, + ObjectTypeExternalTable, + ObjectTypeEventTable, + ObjectTypeView, + ObjectTypeMaterializedView, + ObjectTypeSequence, + ObjectTypeSnapshot, + ObjectTypeFunction, + ObjectTypeExternalFunction, + ObjectTypeProcedure, + ObjectTypeStream, + ObjectTypeTask, + ObjectTypeMaskingPolicy, + ObjectTypeRowAccessPolicy, + ObjectTypeTag, + ObjectTypeSecret, + ObjectTypeStage, + ObjectTypeFileFormat, + ObjectTypePipe, + ObjectTypeAlert, + ObjectTypeBudget, + ObjectTypeClassification, + ObjectTypeApplication, + ObjectTypeApplicationPackage, + ObjectTypeApplicationRole, + ObjectTypeStreamlit, + ObjectTypeColumn, + ObjectTypeIcebergTable, + ObjectTypeExternalVolume, + ObjectTypeNetworkRule, + ObjectTypeNotebook, + ObjectTypePackagesPolicy, + ObjectTypeComputePool, + ObjectTypeAggregationPolicy, + ObjectTypeAuthenticationPolicy, + ObjectTypeHybridTable, + ObjectTypeImageRepository, + ObjectTypeProjectionPolicy, + ObjectTypeDataMetricFunction, + ObjectTypeGitRepository, + ObjectTypeModel, + ObjectTypeService, +} + +// TODO(SNOW-1834370): use ToObjectType in other places with type conversion (instead of sdk.ObjectType) +func ToObjectType(s string) (ObjectType, error) { + s = strings.ToUpper(s) + if !slices.Contains(allObjectTypes, ObjectType(s)) { + return "", fmt.Errorf("invalid object type: %s", s) + } + return ObjectType(s), nil +} + func objectTypeSingularToPluralMap() map[ObjectType]PluralObjectType { return map[ObjectType]PluralObjectType{ ObjectTypeAccount: PluralObjectTypeAccounts, diff --git a/pkg/sdk/object_types_test.go b/pkg/sdk/object_types_test.go new file mode 100644 index 0000000000..3a9d34871f --- /dev/null +++ b/pkg/sdk/object_types_test.go @@ -0,0 +1,105 @@ +package sdk + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_ToObjectType(t *testing.T) { + type test struct { + input string + want ObjectType + } + + valid := []test{ + // Case insensitive. + {input: "schema", want: ObjectTypeSchema}, + + // Supported Values. + {input: "ACCOUNT", want: ObjectTypeAccount}, + {input: "MANAGED ACCOUNT", want: ObjectTypeManagedAccount}, + {input: "USER", want: ObjectTypeUser}, + {input: "DATABASE ROLE", want: ObjectTypeDatabaseRole}, + {input: "DATASET", want: ObjectTypeDataset}, + {input: "ROLE", want: ObjectTypeRole}, + {input: "INTEGRATION", want: ObjectTypeIntegration}, + {input: "NETWORK POLICY", want: ObjectTypeNetworkPolicy}, + {input: "PASSWORD POLICY", want: ObjectTypePasswordPolicy}, + {input: "SESSION POLICY", want: ObjectTypeSessionPolicy}, + {input: "PRIVACY POLICY", want: ObjectTypePrivacyPolicy}, + {input: "REPLICATION GROUP", want: ObjectTypeReplicationGroup}, + {input: "FAILOVER GROUP", want: ObjectTypeFailoverGroup}, + {input: "CONNECTION", want: ObjectTypeConnection}, + {input: "PARAMETER", want: ObjectTypeParameter}, + {input: "WAREHOUSE", want: ObjectTypeWarehouse}, + {input: "RESOURCE MONITOR", want: ObjectTypeResourceMonitor}, + {input: "DATABASE", want: ObjectTypeDatabase}, + {input: "SCHEMA", want: ObjectTypeSchema}, + {input: "SHARE", want: ObjectTypeShare}, + {input: "TABLE", want: ObjectTypeTable}, + {input: "DYNAMIC TABLE", want: ObjectTypeDynamicTable}, + {input: "CORTEX SEARCH SERVICE", want: ObjectTypeCortexSearchService}, + {input: "EXTERNAL TABLE", want: ObjectTypeExternalTable}, + {input: "EVENT TABLE", want: ObjectTypeEventTable}, + {input: "VIEW", want: ObjectTypeView}, + {input: "MATERIALIZED VIEW", want: ObjectTypeMaterializedView}, + {input: "SEQUENCE", want: ObjectTypeSequence}, + {input: "SNAPSHOT", want: ObjectTypeSnapshot}, + {input: "FUNCTION", want: ObjectTypeFunction}, + {input: "EXTERNAL FUNCTION", want: ObjectTypeExternalFunction}, + {input: "PROCEDURE", want: ObjectTypeProcedure}, + {input: "STREAM", want: ObjectTypeStream}, + {input: "TASK", want: ObjectTypeTask}, + {input: "MASKING POLICY", want: ObjectTypeMaskingPolicy}, + {input: "ROW ACCESS POLICY", want: ObjectTypeRowAccessPolicy}, + {input: "TAG", want: ObjectTypeTag}, + {input: "SECRET", want: ObjectTypeSecret}, + {input: "STAGE", want: ObjectTypeStage}, + {input: "FILE FORMAT", want: ObjectTypeFileFormat}, + {input: "PIPE", want: ObjectTypePipe}, + {input: "ALERT", want: ObjectTypeAlert}, + {input: "SNOWFLAKE.CORE.BUDGET", want: ObjectTypeBudget}, + {input: "SNOWFLAKE.ML.CLASSIFICATION", want: ObjectTypeClassification}, + {input: "APPLICATION", want: ObjectTypeApplication}, + {input: "APPLICATION PACKAGE", want: ObjectTypeApplicationPackage}, + {input: "APPLICATION ROLE", want: ObjectTypeApplicationRole}, + {input: "STREAMLIT", want: ObjectTypeStreamlit}, + {input: "COLUMN", want: ObjectTypeColumn}, + {input: "ICEBERG TABLE", want: ObjectTypeIcebergTable}, + {input: "EXTERNAL VOLUME", want: ObjectTypeExternalVolume}, + {input: "NETWORK RULE", want: ObjectTypeNetworkRule}, + {input: "NOTEBOOK", want: ObjectTypeNotebook}, + {input: "PACKAGES POLICY", want: ObjectTypePackagesPolicy}, + {input: "COMPUTE POOL", want: ObjectTypeComputePool}, + {input: "AGGREGATION POLICY", want: ObjectTypeAggregationPolicy}, + {input: "AUTHENTICATION POLICY", want: ObjectTypeAuthenticationPolicy}, + {input: "HYBRID TABLE", want: ObjectTypeHybridTable}, + {input: "IMAGE REPOSITORY", want: ObjectTypeImageRepository}, + {input: "PROJECTION POLICY", want: ObjectTypeProjectionPolicy}, + {input: "DATA METRIC FUNCTION", want: ObjectTypeDataMetricFunction}, + {input: "GIT REPOSITORY", want: ObjectTypeGitRepository}, + {input: "MODEL", want: ObjectTypeModel}, + {input: "SERVICE", want: ObjectTypeService}, + } + + invalid := []test{ + {input: ""}, + {input: "foo"}, + } + + for _, tc := range valid { + t.Run(tc.input, func(t *testing.T) { + got, err := ToObjectType(tc.input) + require.NoError(t, err) + require.Equal(t, tc.want, got) + }) + } + + for _, tc := range invalid { + t.Run(tc.input, func(t *testing.T) { + _, err := ToObjectType(tc.input) + require.Error(t, err) + }) + } +} diff --git a/pkg/sdk/shares.go b/pkg/sdk/shares.go index aa92fb0790..9a7656fa06 100644 --- a/pkg/sdk/shares.go +++ b/pkg/sdk/shares.go @@ -341,6 +341,7 @@ type shareDetailsRow struct { func (row *shareDetailsRow) toShareInfo() *ShareInfo { objectType := ObjectType(row.Kind) trimmedS := strings.Trim(row.Name, "\"") + // TODO(SNOW-1229218): Use a common mapper to get object id. id := objectType.GetObjectIdentifier(trimmedS) return &ShareInfo{ Kind: objectType, diff --git a/pkg/sdk/system_functions.go b/pkg/sdk/system_functions.go index 6777a013a4..9ea79c1671 100644 --- a/pkg/sdk/system_functions.go +++ b/pkg/sdk/system_functions.go @@ -2,6 +2,7 @@ package sdk import ( "context" + "database/sql" "encoding/json" "fmt" "slices" @@ -11,7 +12,7 @@ import ( ) type SystemFunctions interface { - GetTag(ctx context.Context, tagID ObjectIdentifier, objectID ObjectIdentifier, objectType ObjectType) (string, error) + GetTag(ctx context.Context, tagID ObjectIdentifier, objectID ObjectIdentifier, objectType ObjectType) (*string, error) PipeStatus(pipeId SchemaObjectIdentifier) (PipeExecutionState, error) // PipeForceResume unpauses a pipe after ownership transfer. Snowflake will throw an error whenever a pipe changes its owner, // and someone tries to unpause it. To unpause a pipe after ownership transfer, this system function has to be called instead of ALTER PIPE. @@ -26,21 +27,24 @@ type systemFunctions struct { client *Client } -func (c *systemFunctions) GetTag(ctx context.Context, tagID ObjectIdentifier, objectID ObjectIdentifier, objectType ObjectType) (string, error) { +func (c *systemFunctions) GetTag(ctx context.Context, tagID ObjectIdentifier, objectID ObjectIdentifier, objectType ObjectType) (*string, error) { objectType, err := normalizeGetTagObjectType(objectType) if err != nil { - return "", err + return nil, err } s := &struct { - Tag string `db:"TAG"` + Tag sql.NullString `db:"TAG"` }{} sql := fmt.Sprintf(`SELECT SYSTEM$GET_TAG('%s', '%s', '%v') AS "TAG"`, tagID.FullyQualifiedName(), objectID.FullyQualifiedName(), objectType) err = c.client.queryOne(ctx, s, sql) if err != nil { - return "", err + return nil, err + } + if !s.Tag.Valid { + return nil, nil } - return s.Tag, nil + return &s.Tag.String, nil } // normalize object types for some values because of errors like below diff --git a/pkg/sdk/tags.go b/pkg/sdk/tags.go index 0fb31322ce..a0f2b8f6c2 100644 --- a/pkg/sdk/tags.go +++ b/pkg/sdk/tags.go @@ -30,6 +30,7 @@ type setTagOptions struct { type unsetTagOptions struct { alter bool `ddl:"static" sql:"ALTER"` objectType ObjectType `ddl:"keyword"` + IfExists *bool `ddl:"keyword" sql:"IF EXISTS"` objectName ObjectIdentifier `ddl:"identifier"` column *string `ddl:"parameter,no_equals,double_quotes" sql:"MODIFY COLUMN"` UnsetTags []ObjectIdentifier `ddl:"keyword" sql:"UNSET TAG"` diff --git a/pkg/sdk/tags_dto.go b/pkg/sdk/tags_dto.go index 10580f9ceb..95eb54fdc2 100644 --- a/pkg/sdk/tags_dto.go +++ b/pkg/sdk/tags_dto.go @@ -22,6 +22,7 @@ type UnsetTagRequest struct { objectType ObjectType // required objectName ObjectIdentifier // required + IfExists *bool UnsetTags []ObjectIdentifier } diff --git a/pkg/sdk/tags_dto_builders.go b/pkg/sdk/tags_dto_builders.go index 505d0ca854..20610294d7 100644 --- a/pkg/sdk/tags_dto_builders.go +++ b/pkg/sdk/tags_dto_builders.go @@ -24,6 +24,11 @@ func (s *UnsetTagRequest) WithUnsetTags(tags []ObjectIdentifier) *UnsetTagReques return s } +func (s *UnsetTagRequest) WithIfExists(ifExists bool) *UnsetTagRequest { + s.IfExists = &ifExists + return s +} + func NewSetTagOnCurrentAccountRequest() *SetTagOnCurrentAccountRequest { return &SetTagOnCurrentAccountRequest{} } diff --git a/pkg/sdk/tags_impl.go b/pkg/sdk/tags_impl.go index e80c934ff1..882189deb1 100644 --- a/pkg/sdk/tags_impl.go +++ b/pkg/sdk/tags_impl.go @@ -163,6 +163,7 @@ func (s *SetTagRequest) toOpts() *setTagOptions { func (s *UnsetTagRequest) toOpts() *unsetTagOptions { o := &unsetTagOptions{ objectType: s.objectType, + IfExists: s.IfExists, objectName: s.objectName, UnsetTags: s.UnsetTags, } diff --git a/pkg/sdk/tags_test.go b/pkg/sdk/tags_test.go index 94ed4c641c..0a9046ab78 100644 --- a/pkg/sdk/tags_test.go +++ b/pkg/sdk/tags_test.go @@ -388,6 +388,21 @@ func TestTagSet(t *testing.T) { assertOptsValidAndSQLEquals(t, opts, `ALTER %s %s SET TAG "tag1" = 'value1'`, opts.objectType, id.FullyQualifiedName()) }) + t.Run("set on account", func(t *testing.T) { + accountId := randomAccountIdentifier() + opts := &setTagOptions{ + objectType: ObjectTypeStage, + objectName: accountId, + SetTags: []TagAssociation{ + { + Name: NewAccountObjectIdentifier("tag1"), + Value: "value1", + }, + }, + } + assertOptsValidAndSQLEquals(t, opts, `ALTER %s %s SET TAG "tag1" = 'value1'`, opts.objectType, accountId.FullyQualifiedName()) + }) + t.Run("set with column", func(t *testing.T) { objectId := randomTableColumnIdentifierInSchemaObject(id) tagId := randomSchemaObjectIdentifier() @@ -434,7 +449,21 @@ func TestTagUnset(t *testing.T) { NewAccountObjectIdentifier("tag1"), NewAccountObjectIdentifier("tag2"), } - assertOptsValidAndSQLEquals(t, opts, `ALTER %s %s UNSET TAG "tag1", "tag2"`, opts.objectType, id.FullyQualifiedName()) + opts.IfExists = Pointer(true) + assertOptsValidAndSQLEquals(t, opts, `ALTER %s IF EXISTS %s UNSET TAG "tag1", "tag2"`, opts.objectType, id.FullyQualifiedName()) + }) + + t.Run("unset on account", func(t *testing.T) { + accountId := randomAccountIdentifier() + opts := &unsetTagOptions{ + objectType: ObjectTypeStage, + objectName: accountId, + UnsetTags: []ObjectIdentifier{ + NewAccountObjectIdentifier("tag1"), + NewAccountObjectIdentifier("tag2"), + }, + } + assertOptsValidAndSQLEquals(t, opts, `ALTER %s %s UNSET TAG "tag1", "tag2"`, opts.objectType, accountId.FullyQualifiedName()) }) t.Run("unset with column", func(t *testing.T) { @@ -448,8 +477,9 @@ func TestTagUnset(t *testing.T) { tagId1, tagId2, }, + IfExists: Pointer(true), } opts := request.toOpts() - assertOptsValidAndSQLEquals(t, opts, `ALTER %s %s MODIFY COLUMN "%s" UNSET TAG %s, %s`, opts.objectType, id.FullyQualifiedName(), objectId.Name(), tagId1.FullyQualifiedName(), tagId2.FullyQualifiedName()) + assertOptsValidAndSQLEquals(t, opts, `ALTER %s IF EXISTS %s MODIFY COLUMN "%s" UNSET TAG %s, %s`, opts.objectType, id.FullyQualifiedName(), objectId.Name(), tagId1.FullyQualifiedName(), tagId2.FullyQualifiedName()) }) } diff --git a/pkg/sdk/testint/databases_integration_test.go b/pkg/sdk/testint/databases_integration_test.go index 8dc2408eee..0c0b513e69 100644 --- a/pkg/sdk/testint/databases_integration_test.go +++ b/pkg/sdk/testint/databases_integration_test.go @@ -138,11 +138,11 @@ func TestInt_DatabasesCreate(t *testing.T) { tag1Value, err := client.SystemFunctions.GetTag(ctx, tagTest.ID(), database.ID(), sdk.ObjectTypeDatabase) require.NoError(t, err) - assert.Equal(t, "v1", tag1Value) + assert.Equal(t, sdk.Pointer("v1"), tag1Value) tag2Value, err := client.SystemFunctions.GetTag(ctx, tag2Test.ID(), database.ID(), sdk.ObjectTypeDatabase) require.NoError(t, err) - assert.Equal(t, "v2", tag2Value) + assert.Equal(t, sdk.Pointer("v2"), tag2Value) }) } @@ -249,7 +249,7 @@ func TestInt_DatabasesCreateShared(t *testing.T) { tag1Value, err := client.SystemFunctions.GetTag(ctx, testTag.ID(), database.ID(), sdk.ObjectTypeDatabase) require.NoError(t, err) - assert.Equal(t, "v1", tag1Value) + assert.Equal(t, sdk.Pointer("v1"), tag1Value) } func TestInt_DatabasesCreateSecondary(t *testing.T) { diff --git a/pkg/sdk/testint/roles_integration_test.go b/pkg/sdk/testint/roles_integration_test.go index 5c173a1ada..2c9551bb88 100644 --- a/pkg/sdk/testint/roles_integration_test.go +++ b/pkg/sdk/testint/roles_integration_test.go @@ -78,11 +78,11 @@ func TestInt_Roles(t *testing.T) { // verify tags tag1Value, err := client.SystemFunctions.GetTag(ctx, tag.ID(), role.ID(), sdk.ObjectTypeRole) require.NoError(t, err) - assert.Equal(t, "v1", tag1Value) + assert.Equal(t, sdk.Pointer("v1"), tag1Value) tag2Value, err := client.SystemFunctions.GetTag(ctx, tag2.ID(), role.ID(), sdk.ObjectTypeRole) require.NoError(t, err) - assert.Equal(t, "v2", tag2Value) + assert.Equal(t, sdk.Pointer("v2"), tag2Value) }) t.Run("alter rename to", func(t *testing.T) { diff --git a/pkg/sdk/testint/schemas_integration_test.go b/pkg/sdk/testint/schemas_integration_test.go index a1eaa75c3c..d395339d44 100644 --- a/pkg/sdk/testint/schemas_integration_test.go +++ b/pkg/sdk/testint/schemas_integration_test.go @@ -153,7 +153,7 @@ func TestInt_Schemas(t *testing.T) { tv, err := client.SystemFunctions.GetTag(ctx, tag.ID(), schemaID, sdk.ObjectTypeSchema) require.NoError(t, err) - assert.Equal(t, tagValue, tv) + assert.Equal(t, &tagValue, tv) }) t.Run("create: complete", func(t *testing.T) { @@ -245,11 +245,11 @@ func TestInt_Schemas(t *testing.T) { tag1Value, err := client.SystemFunctions.GetTag(ctx, tagTest.ID(), schema.ID(), sdk.ObjectTypeSchema) require.NoError(t, err) - assert.Equal(t, "v1", tag1Value) + assert.Equal(t, sdk.Pointer("v1"), tag1Value) tag2Value, err := client.SystemFunctions.GetTag(ctx, tag2Test.ID(), schema.ID(), sdk.ObjectTypeSchema) require.NoError(t, err) - assert.Equal(t, "v2", tag2Value) + assert.Equal(t, sdk.Pointer("v2"), tag2Value) }) t.Run("alter: rename to", func(t *testing.T) { diff --git a/pkg/sdk/testint/streams_gen_integration_test.go b/pkg/sdk/testint/streams_gen_integration_test.go index f5cd1b4581..8fce3d0cb3 100644 --- a/pkg/sdk/testint/streams_gen_integration_test.go +++ b/pkg/sdk/testint/streams_gen_integration_test.go @@ -46,7 +46,7 @@ func TestInt_Streams(t *testing.T) { tag1Value, err := client.SystemFunctions.GetTag(ctx, tag.ID(), id, sdk.ObjectTypeStream) require.NoError(t, err) - assert.Equal(t, "v1", tag1Value) + assert.Equal(t, sdk.Pointer("v1"), tag1Value) assertions.AssertThatObject(t, objectassert.Stream(t, id). HasName(id.Name()). diff --git a/pkg/sdk/testint/system_functions_integration_test.go b/pkg/sdk/testint/system_functions_integration_test.go index 02208e84c2..7e018eceae 100644 --- a/pkg/sdk/testint/system_functions_integration_test.go +++ b/pkg/sdk/testint/system_functions_integration_test.go @@ -34,7 +34,7 @@ func TestInt_GetTag(t *testing.T) { require.NoError(t, err) s, err := client.SystemFunctions.GetTag(ctx, tagTest.ID(), maskingPolicyTest.ID(), sdk.ObjectTypeMaskingPolicy) require.NoError(t, err) - assert.Equal(t, tagValue, s) + assert.Equal(t, &tagValue, s) }) t.Run("masking policy with no set tag", func(t *testing.T) { @@ -42,8 +42,8 @@ func TestInt_GetTag(t *testing.T) { t.Cleanup(maskingPolicyCleanup) s, err := client.SystemFunctions.GetTag(ctx, tagTest.ID(), maskingPolicyTest.ID(), sdk.ObjectTypeMaskingPolicy) - require.Error(t, err) - assert.Equal(t, "", s) + require.NoError(t, err) + assert.Nil(t, s) }) t.Run("unsupported object type", func(t *testing.T) { _, err := client.SystemFunctions.GetTag(ctx, tagTest.ID(), testClientHelper().Ids.RandomAccountObjectIdentifier(), sdk.ObjectTypeSequence) diff --git a/pkg/sdk/testint/tags_integration_test.go b/pkg/sdk/testint/tags_integration_test.go index c3d9414750..996e44e070 100644 --- a/pkg/sdk/testint/tags_integration_test.go +++ b/pkg/sdk/testint/tags_integration_test.go @@ -16,6 +16,7 @@ import ( "github.com/stretchr/testify/require" ) +// TODO(SNOW-1813223): cleanup tests func TestInt_Tags(t *testing.T) { client := testClient(t) ctx := context.Background() @@ -154,6 +155,16 @@ func TestInt_Tags(t *testing.T) { err := client.Tags.Alter(ctx, sdk.NewAlterTagRequest(id).WithSet(set)) require.NoError(t, err) + ref, err := testClientHelper().PolicyReferences.GetPolicyReference(t, tag.ID(), sdk.PolicyEntityDomainTag) + require.NoError(t, err) + assert.Equal(t, policyTest.ID().Name(), ref.PolicyName) + assert.Equal(t, sdk.PolicyKindMaskingPolicy, ref.PolicyKind) + + // assert that setting masking policy does not apply the tag on the masking policy + returnedTagValue, err := client.SystemFunctions.GetTag(ctx, id, policyTest.ID(), sdk.ObjectTypeMaskingPolicy) + require.NoError(t, err) + assert.Nil(t, returnedTagValue) + unset := sdk.NewTagUnsetRequest().WithMaskingPolicies(policies) err = client.Tags.Alter(ctx, sdk.NewAlterTagRequest(id).WithUnset(unset)) require.NoError(t, err) @@ -309,19 +320,28 @@ func TestInt_TagsAssociations(t *testing.T) { tag.ID(), } - testTagSet := func(id sdk.ObjectIdentifier, objectType sdk.ObjectType) { - err := client.Tags.Set(ctx, sdk.NewSetTagRequest(objectType, id).WithSetTags(tags)) + assertTagSet := func(id sdk.ObjectIdentifier, objectType sdk.ObjectType) { + returnedTagValue, err := client.SystemFunctions.GetTag(ctx, tag.ID(), id, objectType) require.NoError(t, err) + assert.Equal(t, sdk.Pointer(tagValue), returnedTagValue) + } + assertTagUnset := func(id sdk.ObjectIdentifier, objectType sdk.ObjectType) { returnedTagValue, err := client.SystemFunctions.GetTag(ctx, tag.ID(), id, objectType) require.NoError(t, err) - assert.Equal(t, tagValue, returnedTagValue) + assert.Nil(t, returnedTagValue) + } + + testTagSet := func(id sdk.ObjectIdentifier, objectType sdk.ObjectType) { + err := client.Tags.Set(ctx, sdk.NewSetTagRequest(objectType, id).WithSetTags(tags)) + require.NoError(t, err) + + assertTagSet(id, objectType) err = client.Tags.Unset(ctx, sdk.NewUnsetTagRequest(objectType, id).WithUnsetTags(unsetTags)) require.NoError(t, err) - _, err = client.SystemFunctions.GetTag(ctx, tag.ID(), id, objectType) - require.ErrorContains(t, err, "sql: Scan error on column index 0, name \"TAG\": converting NULL to string is unsupported") + assertTagUnset(id, objectType) } t.Run("TestInt_TagAssociationForAccountLocator", func(t *testing.T) { @@ -331,31 +351,25 @@ func TestInt_TagsAssociations(t *testing.T) { }) require.NoError(t, err) - returnedTagValue, err := client.SystemFunctions.GetTag(ctx, tag.ID(), id, sdk.ObjectTypeAccount) - require.NoError(t, err) - assert.Equal(t, tagValue, returnedTagValue) + assertTagSet(id, sdk.ObjectTypeAccount) err = client.Accounts.Alter(ctx, &sdk.AlterAccountOptions{ UnsetTag: unsetTags, }) require.NoError(t, err) - _, err = client.SystemFunctions.GetTag(ctx, tag.ID(), id, sdk.ObjectTypeAccount) - require.ErrorContains(t, err, "sql: Scan error on column index 0, name \"TAG\": converting NULL to string is unsupported") + assertTagUnset(id, sdk.ObjectTypeAccount) // test tag sdk method err = client.Tags.SetOnCurrentAccount(ctx, sdk.NewSetTagOnCurrentAccountRequest().WithSetTags(tags)) require.NoError(t, err) - returnedTagValue, err = client.SystemFunctions.GetTag(ctx, tag.ID(), id, sdk.ObjectTypeAccount) - require.NoError(t, err) - assert.Equal(t, tagValue, returnedTagValue) + assertTagSet(id, sdk.ObjectTypeAccount) err = client.Tags.UnsetOnCurrentAccount(ctx, sdk.NewUnsetTagOnCurrentAccountRequest().WithUnsetTags(unsetTags)) require.NoError(t, err) - _, err = client.SystemFunctions.GetTag(ctx, tag.ID(), id, sdk.ObjectTypeAccount) - require.ErrorContains(t, err, "sql: Scan error on column index 0, name \"TAG\": converting NULL to string is unsupported") + assertTagUnset(id, sdk.ObjectTypeAccount) }) t.Run("TestInt_TagAssociationForAccount", func(t *testing.T) { @@ -363,15 +377,12 @@ func TestInt_TagsAssociations(t *testing.T) { err := client.Tags.Set(ctx, sdk.NewSetTagRequest(sdk.ObjectTypeAccount, id).WithSetTags(tags)) require.NoError(t, err) - returnedTagValue, err := client.SystemFunctions.GetTag(ctx, tag.ID(), id, sdk.ObjectTypeAccount) - require.NoError(t, err) - assert.Equal(t, tagValue, returnedTagValue) + assertTagSet(id, sdk.ObjectTypeAccount) err = client.Tags.Unset(ctx, sdk.NewUnsetTagRequest(sdk.ObjectTypeAccount, id).WithUnsetTags(unsetTags)) require.NoError(t, err) - _, err = client.SystemFunctions.GetTag(ctx, tag.ID(), id, sdk.ObjectTypeAccount) - require.ErrorContains(t, err, "sql: Scan error on column index 0, name \"TAG\": converting NULL to string is unsupported") + assertTagUnset(id, sdk.ObjectTypeAccount) }) accountObjectTestCases := []struct { @@ -634,15 +645,12 @@ func TestInt_TagsAssociations(t *testing.T) { err := tc.setTags(id, tags) require.NoError(t, err) - returnedTagValue, err := client.SystemFunctions.GetTag(ctx, tag.ID(), id, tc.objectType) - require.NoError(t, err) - assert.Equal(t, tagValue, returnedTagValue) + assertTagSet(id, tc.objectType) err = tc.unsetTags(id, unsetTags) require.NoError(t, err) - _, err = client.SystemFunctions.GetTag(ctx, tag.ID(), id, tc.objectType) - require.ErrorContains(t, err, "sql: Scan error on column index 0, name \"TAG\": converting NULL to string is unsupported") + assertTagUnset(id, tc.objectType) // test object methods testTagSet(id, tc.objectType) @@ -725,15 +733,12 @@ func TestInt_TagsAssociations(t *testing.T) { err := tc.setTags(id, tags) require.NoError(t, err) - returnedTagValue, err := client.SystemFunctions.GetTag(ctx, tag.ID(), id, tc.objectType) - require.NoError(t, err) - assert.Equal(t, tagValue, returnedTagValue) + assertTagSet(id, tc.objectType) err = tc.unsetTags(id, unsetTags) require.NoError(t, err) - _, err = client.SystemFunctions.GetTag(ctx, tag.ID(), id, tc.objectType) - require.ErrorContains(t, err, "sql: Scan error on column index 0, name \"TAG\": converting NULL to string is unsupported") + assertTagUnset(id, tc.objectType) // test object methods testTagSet(id, tc.objectType) @@ -794,23 +799,6 @@ func TestInt_TagsAssociations(t *testing.T) { }) }, }, - { - name: "MaskingPolicy", - objectType: sdk.ObjectTypeMaskingPolicy, - setupObject: func() (IDProvider[sdk.SchemaObjectIdentifier], func()) { - return testClientHelper().MaskingPolicy.CreateMaskingPolicy(t) - }, - setTags: func(id sdk.SchemaObjectIdentifier, tags []sdk.TagAssociation) error { - return client.MaskingPolicies.Alter(ctx, id, &sdk.AlterMaskingPolicyOptions{ - SetTag: tags, - }) - }, - unsetTags: func(id sdk.SchemaObjectIdentifier, tags []sdk.ObjectIdentifier) error { - return client.MaskingPolicies.Alter(ctx, id, &sdk.AlterMaskingPolicyOptions{ - UnsetTag: tags, - }) - }, - }, { name: "RowAccessPolicy", objectType: sdk.ObjectTypeRowAccessPolicy, @@ -929,21 +917,45 @@ func TestInt_TagsAssociations(t *testing.T) { err := tc.setTags(id, tags) require.NoError(t, err) - returnedTagValue, err := client.SystemFunctions.GetTag(ctx, tag.ID(), id, tc.objectType) - require.NoError(t, err) - assert.Equal(t, tagValue, returnedTagValue) + assertTagSet(id, tc.objectType) err = tc.unsetTags(id, unsetTags) require.NoError(t, err) - _, err = client.SystemFunctions.GetTag(ctx, tag.ID(), id, tc.objectType) - require.ErrorContains(t, err, "sql: Scan error on column index 0, name \"TAG\": converting NULL to string is unsupported") + assertTagUnset(id, tc.objectType) // test object methods testTagSet(id, tc.objectType) }) } + t.Run("schema object MaskingPolicy", func(t *testing.T) { + maskingPolicy, cleanup := testClientHelper().MaskingPolicy.CreateMaskingPolicy(t) + t.Cleanup(cleanup) + id := maskingPolicy.ID() + err := client.MaskingPolicies.Alter(ctx, id, &sdk.AlterMaskingPolicyOptions{ + SetTag: tags, + }) + require.NoError(t, err) + + assertTagSet(id, sdk.ObjectTypeMaskingPolicy) + + // assert that setting masking policy does not apply the tag on the masking policy + refs, err := testClientHelper().PolicyReferences.GetPolicyReferences(t, tag.ID(), sdk.PolicyEntityDomainTag) + require.NoError(t, err) + assert.Len(t, refs, 0) + + err = client.MaskingPolicies.Alter(ctx, id, &sdk.AlterMaskingPolicyOptions{ + UnsetTag: unsetTags, + }) + require.NoError(t, err) + + assertTagUnset(id, sdk.ObjectTypeMaskingPolicy) + + // test object methods + testTagSet(id, sdk.ObjectTypeMaskingPolicy) + }) + columnTestCases := []struct { name string setupObject func() (sdk.TableColumnIdentifier, func()) @@ -994,15 +1006,12 @@ func TestInt_TagsAssociations(t *testing.T) { err := tc.setTags(id, tags) require.NoError(t, err) - returnedTagValue, err := client.SystemFunctions.GetTag(ctx, tag.ID(), id, sdk.ObjectTypeColumn) - require.NoError(t, err) - assert.Equal(t, tagValue, returnedTagValue) + assertTagSet(id, sdk.ObjectTypeColumn) err = tc.unsetTags(id, unsetTags) require.NoError(t, err) - _, err = client.SystemFunctions.GetTag(ctx, tag.ID(), id, sdk.ObjectTypeColumn) - require.ErrorContains(t, err, "sql: Scan error on column index 0, name \"TAG\": converting NULL to string is unsupported") + assertTagUnset(id, sdk.ObjectTypeColumn) // test object methods testTagSet(id, sdk.ObjectTypeColumn) @@ -1071,15 +1080,12 @@ func TestInt_TagsAssociations(t *testing.T) { err := tc.setTags(id, tags) require.NoError(t, err) - returnedTagValue, err := client.SystemFunctions.GetTag(ctx, tag.ID(), id, tc.objectType) - require.NoError(t, err) - assert.Equal(t, tagValue, returnedTagValue) + assertTagSet(id, tc.objectType) err = tc.unsetTags(id, unsetTags) require.NoError(t, err) - _, err = client.SystemFunctions.GetTag(ctx, tag.ID(), id, tc.objectType) - require.ErrorContains(t, err, "sql: Scan error on column index 0, name \"TAG\": converting NULL to string is unsupported") + assertTagUnset(id, tc.objectType) // test object methods testTagSet(id, tc.objectType) diff --git a/pkg/sdk/testint/tasks_gen_integration_test.go b/pkg/sdk/testint/tasks_gen_integration_test.go index dc38fcc205..6304a830d4 100644 --- a/pkg/sdk/testint/tasks_gen_integration_test.go +++ b/pkg/sdk/testint/tasks_gen_integration_test.go @@ -493,7 +493,7 @@ func TestInt_Tasks(t *testing.T) { returnedTagValue, err := client.SystemFunctions.GetTag(ctx, tag.ID(), task.ID(), sdk.ObjectTypeTask) require.NoError(t, err) - assert.Equal(t, "v1", returnedTagValue) + assert.Equal(t, sdk.Pointer("v1"), returnedTagValue) }) t.Run("clone task: default", func(t *testing.T) { diff --git a/pkg/sdk/testint/warehouses_integration_test.go b/pkg/sdk/testint/warehouses_integration_test.go index e2dce5e08c..659705f775 100644 --- a/pkg/sdk/testint/warehouses_integration_test.go +++ b/pkg/sdk/testint/warehouses_integration_test.go @@ -166,10 +166,10 @@ func TestInt_Warehouses(t *testing.T) { tag1Value, err := client.SystemFunctions.GetTag(ctx, tag.ID(), warehouse.ID(), sdk.ObjectTypeWarehouse) require.NoError(t, err) - assert.Equal(t, "v1", tag1Value) + assert.Equal(t, sdk.Pointer("v1"), tag1Value) tag2Value, err := client.SystemFunctions.GetTag(ctx, tag2.ID(), warehouse.ID(), sdk.ObjectTypeWarehouse) require.NoError(t, err) - assert.Equal(t, "v2", tag2Value) + assert.Equal(t, sdk.Pointer("v2"), tag2Value) }) t.Run("create: no options", func(t *testing.T) { diff --git a/templates/resources/tag_association.md.tmpl b/templates/resources/tag_association.md.tmpl new file mode 100644 index 0000000000..a514fdf39d --- /dev/null +++ b/templates/resources/tag_association.md.tmpl @@ -0,0 +1,43 @@ +--- +page_title: "{{.Name}} {{.Type}} - {{.ProviderName}}" +subcategory: "" +description: |- +{{ if gt (len (split .Description "")) 1 -}} +{{ index (split .Description "") 1 | plainmarkdown | trimspace | prefixlines " " }} +{{- else -}} +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} +{{- end }} +--- + +!> **V1 release candidate** This resource was reworked and is a release candidate for the V1. We do not expect significant changes in it before the V1. We will welcome any feedback and adjust the resource if needed. Any errors reported will be resolved with a higher priority. We encourage checking this resource out before the V1 release. Please follow the [migration guide](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/MIGRATION_GUIDE.md#v0980--v0990) to use it. + +-> **Note** For `ACCOUNT` object type, only identifiers with organization name are supported. See [account identifier docs](https://docs.snowflake.com/en/user-guide/admin-account-identifier#format-1-preferred-account-name-in-your-organization) for more details. + +-> **Note** Tag association resource ID has the following format: `"TAG_DATABASE"."TAG_SCHEMA"."TAG_NAME"|TAG_VALUE|OBJECT_TYPE`. This means that a tuple of tag ID, tag value and object type should be unique across the resources. If you want to specify this combination for more than one object, you should use only one `tag_association` resource with specified `object_identifiers` set. + +-> **Note** If you want to change tag value to a value that is already present in another `tag_association` resource, first remove the relevant `object_identifiers` from the resource with the old value, run `terraform apply`, then add the relevant `object_identifiers` in the resource with new value, and run `terrafrom apply` once again. + +# {{.Name}} ({{.Type}}) + +{{ .Description | trimspace }} + +{{ if .HasExample -}} +## Example Usage + +{{ tffile (printf "examples/resources/%s/resource.tf" .Name)}} +-> **Note** Instead of using fully_qualified_name, you can reference objects managed outside Terraform by constructing a correct ID, consult [identifiers guide](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/guides/identifiers#new-computed-fully-qualified-name-field-in-resources). + + +{{- end }} + +{{ .SchemaMarkdown | trimspace }} +{{- if .HasImport }} + +## Import + +~> **Note** Due to technical limitations of Terraform SDK, `object_identifiers` are not set during import state. Please run `terraform refresh` after importing to get this field populated. + +Import is supported using the following syntax: + +{{ codefile "shell" (printf "examples/resources/%s/import.sh" .Name)}} +{{- end }}