From 624a07dc70ba9df906df2718fdae4ece3ec4ce6a Mon Sep 17 00:00:00 2001 From: Jakub Michalak Date: Wed, 20 Nov 2024 14:55:55 +0100 Subject: [PATCH] rework tag association --- MIGRATION_GUIDE.md | 40 +++ docs/resources/authentication_policy.md | 2 + docs/resources/masking_policy.md | 2 + docs/resources/network_policy.md | 2 + docs/resources/password_policy.md | 2 + docs/resources/row_access_policy.md | 2 + docs/resources/tag_association.md | 70 ++-- .../snowflake_tag_association/import.sh | 3 +- .../snowflake_tag_association/resource.tf | 41 ++- pkg/acceptance/check_destroy.go | 50 +++ pkg/acceptance/helpers/context_client.go | 20 ++ pkg/acceptance/helpers/tag_client.go | 8 + pkg/helpers/helpers.go | 33 ++ pkg/resources/authentication_policy.go | 1 + pkg/resources/helper_expansion.go | 25 +- pkg/resources/helpers.go | 36 +- pkg/resources/masking_policy.go | 1 + pkg/resources/network_policy.go | 1 + pkg/resources/password_policy.go | 2 + pkg/resources/row_access_policy.go | 1 + pkg/resources/tag_association.go | 155 +++++++-- .../tag_association_acceptance_test.go | 312 +++++++++++++----- .../tag_association_state_upgraders.go | 45 +++ .../TestAcc_TagAssociation/basic/test.tf | 10 +- .../TestAcc_TagAssociation/basic/variables.tf | 8 + .../TestAcc_TagAssociation/column/test.tf | 2 +- .../TestAcc_TagAssociation/issue1202/main.tf | 12 +- .../TestAcc_TagAssociation/issue1909/test.tf | 24 +- .../issue1909/variables.tf | 8 + .../TestAcc_TagAssociation/issue1910/test.tf | 22 +- .../issue1910/variables.tf | 2 +- .../TestAcc_TagAssociation/issue1926/test.tf | 10 +- .../TestAcc_TagAssociation/schema/test.tf | 2 +- pkg/sdk/tags_impl.go | 12 + pkg/sdk/tags_validations.go | 6 - pkg/sdk/testint/tags_integration_test.go | 83 +++-- .../resources/authentication_policy.md.tmpl | 36 ++ templates/resources/masking_policy.md.tmpl | 2 + templates/resources/network_policy.md.tmpl | 2 + templates/resources/password_policy.md.tmpl | 36 ++ templates/resources/row_access_policy.md.tmpl | 2 + ...iation_md.tmpl => tag_association.md.tmpl} | 2 + v1-preparations/ESSENTIAL_GA_OBJECTS.MD | 2 +- 43 files changed, 865 insertions(+), 272 deletions(-) create mode 100644 pkg/resources/tag_association_state_upgraders.go create mode 100644 templates/resources/authentication_policy.md.tmpl create mode 100644 templates/resources/password_policy.md.tmpl rename templates/resources/{tag_association_md.tmpl => tag_association.md.tmpl} (89%) diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 44079dfc95..3e3529ca76 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -7,6 +7,46 @@ across different versions. > [!TIP] > We highly recommend upgrading the versions one by one instead of bulk upgrades. +## v0.98.0 ➞ v0.99.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 like the following: +``` +``` + +The state is migrated automatically. There is no need to adjust configuration files, unless you use resource id like `snowflake_tag_association.example.id`. + + +#### *(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.97.0 ➞ v0.98.0 ### *(new feature)* snowflake_connections datasource diff --git a/docs/resources/authentication_policy.md b/docs/resources/authentication_policy.md index 99b64f0cfe..dcd1feb7b2 100644 --- a/docs/resources/authentication_policy.md +++ b/docs/resources/authentication_policy.md @@ -5,6 +5,8 @@ description: |- Resource used to manage authentication policy objects. For more information, check authentication policy documentation https://docs.snowflake.com/en/sql-reference/sql/create-authentication-policy. --- +-> **Note** According to Snowflake [docs](https://docs.snowflake.com/en/sql-reference/sql/drop-authentication-policy#usage-notes), an authentication policy cannot be dropped successfully if it is currently assigned to another object. Currently, the provider does not unassign such objects automatically. Before dropping the resource, list the assigned objects with `SELECT * from table(information_schema.policy_references(policy_name=>''));` and unassign them manually with `ALTER ...` or with updated Terraform configuration, if possible. + # snowflake_authentication_policy (Resource) Resource used to manage authentication policy objects. For more information, check [authentication policy documentation](https://docs.snowflake.com/en/sql-reference/sql/create-authentication-policy). diff --git a/docs/resources/masking_policy.md b/docs/resources/masking_policy.md index 0806f24147..10c080c4e4 100644 --- a/docs/resources/masking_policy.md +++ b/docs/resources/masking_policy.md @@ -7,6 +7,8 @@ description: |- !> **V1 release candidate** This resource was reworked and is a release candidate for the V1. We do not expect significant changes in it before the V1. We will welcome any feedback and adjust the resource if needed. Any errors reported will be resolved with a higher priority. We encourage checking this resource out before the V1 release. Please follow the [migration guide](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/MIGRATION_GUIDE.md#v0950--v0960) to use it. +-> **Note** According to Snowflake [docs](https://docs.snowflake.com/en/sql-reference/sql/drop-masking-policy#usage-notes), a masking policy cannot be dropped successfully if it is currently assigned to another object. Currently, the provider does not unassign such objects automatically. Before dropping the resource, list the assigned objects with `SELECT * from table(information_schema.policy_references(policy_name=>''));` and unassign them manually with `ALTER ...` or with updated Terraform configuration, if possible. + # snowflake_masking_policy (Resource) Resource used to manage masking policies. For more information, check [masking policies documentation](https://docs.snowflake.com/en/sql-reference/sql/create-masking-policy). diff --git a/docs/resources/network_policy.md b/docs/resources/network_policy.md index 706acad399..58f6c2e693 100644 --- a/docs/resources/network_policy.md +++ b/docs/resources/network_policy.md @@ -7,6 +7,8 @@ description: |- !> **V1 release candidate** This resource was reworked and is a release candidate for the V1. We do not expect significant changes in it before the V1. We will welcome any feedback and adjust the resource if needed. Any errors reported will be resolved with a higher priority. We encourage checking this resource out before the V1 release. Please follow the [migration guide](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/MIGRATION_GUIDE.md#v0920--v0930) to use it. +-> **Note** According to Snowflake [docs](https://docs.snowflake.com/en/sql-reference/sql/drop-network-policy#usage-notes), a network policy cannot be dropped successfully if it is currently assigned to another object. Currently, the provider does not unassign such objects automatically. Before dropping the resource, list the assigned objects with `SELECT * from table(information_schema.policy_references(policy_name=>''));` and unassign them manually with `ALTER ...` or with updated Terraform configuration, if possible. + # snowflake_network_policy (Resource) Resource used to control network traffic. For more information, check an [official guide](https://docs.snowflake.com/en/user-guide/network-policies) on controlling network traffic with network policies. diff --git a/docs/resources/password_policy.md b/docs/resources/password_policy.md index 3214efcf68..32e1f42bc9 100644 --- a/docs/resources/password_policy.md +++ b/docs/resources/password_policy.md @@ -5,6 +5,8 @@ description: |- A password policy specifies the requirements that must be met to create and reset a password to authenticate to Snowflake. --- +-> **Note** According to Snowflake [docs](https://docs.snowflake.com/en/sql-reference/sql/drop-password-policy#usage-notes), a password policy cannot be dropped successfully if it is currently assigned to another object. Currently, the provider does not unassign such objects automatically. Before dropping the resource, list the assigned objects with `SELECT * from table(information_schema.policy_references(policy_name=>''));` and unassign them manually with `ALTER ...` or with updated Terraform configuration, if possible. + # snowflake_password_policy (Resource) A password policy specifies the requirements that must be met to create and reset a password to authenticate to Snowflake. diff --git a/docs/resources/row_access_policy.md b/docs/resources/row_access_policy.md index 3bd3afa7d3..54ec84842e 100644 --- a/docs/resources/row_access_policy.md +++ b/docs/resources/row_access_policy.md @@ -7,6 +7,8 @@ description: |- !> **V1 release candidate** This resource was reworked and is a release candidate for the V1. We do not expect significant changes in it before the V1. We will welcome any feedback and adjust the resource if needed. Any errors reported will be resolved with a higher priority. We encourage checking this resource out before the V1 release. Please follow the [migration guide](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/MIGRATION_GUIDE.md#v0950--v0960) to use it. +-> **Note** According to Snowflake [docs](https://docs.snowflake.com/en/sql-reference/sql/drop-row-access-policy#usage-notes), a row access policy cannot be dropped successfully if it is currently assigned to another object. Currently, the provider does not unassign such objects automatically. Before dropping the resource, list the assigned objects with `SELECT * from table(information_schema.policy_references(policy_name=>''));` and unassign them manually with `ALTER ...` or with updated Terraform configuration, if possible. + # snowflake_row_access_policy (Resource) Resource used to manage row access policy objects. For more information, check [row access policy documentation](https://docs.snowflake.com/en/sql-reference/sql/create-row-access-policy). diff --git a/docs/resources/tag_association.md b/docs/resources/tag_association.md index 77acc40091..5c39787bb5 100644 --- a/docs/resources/tag_association.md +++ b/docs/resources/tag_association.md @@ -2,12 +2,14 @@ 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. +# 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 +31,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 +53,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 +81,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 +96,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 +105,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/check_destroy.go b/pkg/acceptance/check_destroy.go index 57145b726f..306a77d66e 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,55 @@ func CheckUserAuthenticationPolicyAttachmentDestroy(t *testing.T) func(*terrafor } } +// CheckTagValueEmpty is a custom check that should be later incorporated into generic CheckDestroy +func CheckTagValueEmpty(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 + 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) + } + } + tag, err := TestClient().Tag.GetTag(t, tagId, id, objectType) + if err != nil { + if strings.Contains(err.Error(), "does not exist or not authorized") { + // Note: this can happen if the object has been deleted as well; in this case, ignore the error + continue + } + 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 nil + } +} + func TestAccCheckGrantApplicationRoleDestroy(s *terraform.State) error { client := TestAccProvider.Meta().(*provider.Context).Client for _, rs := range s.RootModule().Resources { diff --git a/pkg/acceptance/helpers/context_client.go b/pkg/acceptance/helpers/context_client.go index e2186026e9..a4dc768089 100644 --- a/pkg/acceptance/helpers/context_client.go +++ b/pkg/acceptance/helpers/context_client.go @@ -33,6 +33,26 @@ func (c *ContextClient) CurrentAccount(t *testing.T) string { return currentAccount } +func (c *ContextClient) CurrentAccountName(t *testing.T) string { + t.Helper() + ctx := context.Background() + + accountName, err := c.client().CurrentAccountName(ctx) + require.NoError(t, err) + + return accountName +} + +func (c *ContextClient) CurrentOrganizationName(t *testing.T) string { + t.Helper() + ctx := context.Background() + + orgName, err := c.client().CurrentOrganizationName(ctx) + require.NoError(t, err) + + return orgName +} + func (c *ContextClient) CurrentRole(t *testing.T) sdk.AccountObjectIdentifier { t.Helper() ctx := context.Background() diff --git a/pkg/acceptance/helpers/tag_client.go b/pkg/acceptance/helpers/tag_client.go index 76f826a4a9..95a949cf4f 100644 --- a/pkg/acceptance/helpers/tag_client.go +++ b/pkg/acceptance/helpers/tag_client.go @@ -60,6 +60,14 @@ func (c *TagClient) Unset(t *testing.T, objectType sdk.ObjectType, id sdk.Object require.NoError(t, err) } +func (c *TagClient) GetTag(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) +} + func (c *TagClient) DropTagFunc(t *testing.T, id sdk.SchemaObjectIdentifier) func() { t.Helper() ctx := context.Background() diff --git a/pkg/helpers/helpers.go b/pkg/helpers/helpers.go index 6b3e39d4cf..24c1ae8eba 100644 --- a/pkg/helpers/helpers.go +++ b/pkg/helpers/helpers.go @@ -290,3 +290,36 @@ func ContainsIdentifierIgnoringQuotes(ids []string, id string) bool { return false } + +func ExpandStringList(configured []interface{}) []string { + vs := make([]string, 0, len(configured)) + for _, v := range configured { + val, ok := v.(string) + if ok && val != "" { + vs = append(vs, val) + } + } + 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 + if objectType == sdk.ObjectTypeAccount { + id, err = sdk.ParseAccountIdentifier(idRaw) + if err != nil { + return nil, fmt.Errorf("invalid account id: %w", err) + } + } else { + id, err = sdk.ParseObjectIdentifierString(idRaw) + if err != nil { + return nil, fmt.Errorf("invalid object id: %w", err) + } + } + ids[i] = id + } + return ids, nil +} diff --git a/pkg/resources/authentication_policy.go b/pkg/resources/authentication_policy.go index 16f11927b6..ae009f0279 100644 --- a/pkg/resources/authentication_policy.go +++ b/pkg/resources/authentication_policy.go @@ -489,6 +489,7 @@ func DeleteContextAuthenticationPolicy(ctx context.Context, d *schema.ResourceDa } } + // TODO(SNOW-1818849): unassign policies before dropping if err := client.AuthenticationPolicies.Drop(ctx, sdk.NewDropAuthenticationPolicyRequest(id).WithIfExists(true)); err != nil { return diag.FromErr(err) } diff --git a/pkg/resources/helper_expansion.go b/pkg/resources/helper_expansion.go index dcbe1ceafb..73eeb5eb80 100644 --- a/pkg/resources/helper_expansion.go +++ b/pkg/resources/helper_expansion.go @@ -2,6 +2,9 @@ package resources import ( "slices" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers" + "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 @@ -17,27 +20,11 @@ func expandIntList(configured []interface{}) []int { } func expandStringList(configured []interface{}) []string { - vs := make([]string, 0, len(configured)) - for _, v := range configured { - val, ok := v.(string) - if ok && val != "" { - vs = append(vs, val) - } - } - return vs + return helpers.ExpandStringList(configured) } -func expandStringListWithMapping[T any](configured []any, mapping func(string) (T, error)) ([]T, error) { - stringList := expandStringList(configured) - vs := make([]T, 0, len(configured)) - for _, v := range stringList { - val, err := mapping(v) - if err != nil { - return nil, err - } - vs = append(vs, val) - } - return vs, nil +func ExpandObjectIdentifierSet(configured []any, objectType sdk.ObjectType) ([]sdk.ObjectIdentifier, error) { + return helpers.ExpandObjectIdentifierSet(configured, objectType) } func expandStringListAllowEmpty(configured []interface{}) []string { diff --git a/pkg/resources/helpers.go b/pkg/resources/helpers.go index a7c78d2532..a08153e221 100644 --- a/pkg/resources/helpers.go +++ b/pkg/resources/helpers.go @@ -316,17 +316,41 @@ func ListDiff[T comparable](beforeList []T, afterList []T) (added []T, removed [ added = make([]T, 0) removed = 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) } } - 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 } + +// ListDiffWithCommon Compares two lists (before and after), then compares and returns three lists that include +// added, removed and common items between those lists. +func ListDiffWithCommon[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 _, beforeItem := range beforeList { + if !slices.Contains(afterList, beforeItem) { + removed = append(removed, beforeItem) + } else { + common = append(common, beforeItem) + } + } + + for _, afterItem := range afterList { + if !slices.Contains(beforeList, afterItem) { + added = append(added, afterItem) + } + } + + return added, removed, common +} diff --git a/pkg/resources/masking_policy.go b/pkg/resources/masking_policy.go index 965945e708..04aff275ef 100644 --- a/pkg/resources/masking_policy.go +++ b/pkg/resources/masking_policy.go @@ -364,6 +364,7 @@ func DeleteMaskingPolicy(ctx context.Context, d *schema.ResourceData, meta any) return diag.FromErr(err) } + // TODO(SNOW-1818849): unassign policies before dropping err = client.MaskingPolicies.Drop(ctx, id, &sdk.DropMaskingPolicyOptions{IfExists: sdk.Pointer(true)}) if err != nil { return diag.Diagnostics{ diff --git a/pkg/resources/network_policy.go b/pkg/resources/network_policy.go index 7b590ce169..8265332c76 100644 --- a/pkg/resources/network_policy.go +++ b/pkg/resources/network_policy.go @@ -375,6 +375,7 @@ func DeleteContextNetworkPolicy(ctx context.Context, d *schema.ResourceData, met return diag.FromErr(err) } + // TODO(SNOW-1818849): unassign policies before dropping err = client.NetworkPolicies.Drop(ctx, sdk.NewDropNetworkPolicyRequest(id).WithIfExists(true)) if err != nil { return diag.Diagnostics{ diff --git a/pkg/resources/password_policy.go b/pkg/resources/password_policy.go index 0aa5aaff98..fd2b6d57ae 100644 --- a/pkg/resources/password_policy.go +++ b/pkg/resources/password_policy.go @@ -435,6 +435,8 @@ func DeletePasswordPolicy(d *schema.ResourceData, meta interface{}) error { client := meta.(*provider.Context).Client ctx := context.Background() objectIdentifier := helpers.DecodeSnowflakeID(d.Id()).(sdk.SchemaObjectIdentifier) + + // TODO(SNOW-1818849): unassign policies before dropping err := client.PasswordPolicies.Drop(ctx, objectIdentifier, nil) if err != nil { return err diff --git a/pkg/resources/row_access_policy.go b/pkg/resources/row_access_policy.go index 08949fbbcb..3cdc6725d1 100644 --- a/pkg/resources/row_access_policy.go +++ b/pkg/resources/row_access_policy.go @@ -304,6 +304,7 @@ func DeleteRowAccessPolicy(ctx context.Context, d *schema.ResourceData, meta any client := meta.(*provider.Context).Client + // TODO(SNOW-1818849): unassign policies before dropping err = client.RowAccessPolicies.Drop(ctx, sdk.NewDropRowAccessPolicyRequest(id).WithIfExists(sdk.Pointer(true))) if err != nil { return diag.Diagnostics{ diff --git a/pkg/resources/tag_association.go b/pkg/resources/tag_association.go index 2a61758e19..20e160ebd2 100644 --- a/pkg/resources/tag_association.go +++ b/pkg/resources/tag_association.go @@ -2,16 +2,19 @@ package resources import ( "context" + "errors" "fmt" "log" "time" + "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/sdk" ) @@ -22,9 +25,9 @@ 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": { + "object_identifiers": { Type: schema.TypeSet, MinItems: 1, Required: true, @@ -32,7 +35,7 @@ var tagAssociationSchema = map[string]*schema.Schema{ Elem: &schema.Schema{ Type: schema.TypeString, }, - DiffSuppressFunc: NormalizeAndCompareIdentifiersInSet("object_identifier"), + DiffSuppressFunc: NormalizeAndCompareIdentifiersInSet("object_identifiers"), }, "object_type": { Type: schema.TypeString, @@ -52,7 +55,6 @@ var tagAssociationSchema = map[string]*schema.Schema{ Type: schema.TypeString, Required: true, Description: "Specifies the value of the tag, (e.g. 'finance' or 'engineering')", - ForceNew: true, }, "skip_validation": { Type: schema.TypeBool, @@ -65,6 +67,8 @@ 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: CreateContextTagAssociation, ReadContext: ReadContextTagAssociation, UpdateContext: UpdateContextTagAssociation, @@ -73,14 +77,42 @@ func TagAssociation() *schema.Resource { 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 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)) + } + + 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", idParts[2]); 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) tagId, err := sdk.ParseSchemaObjectIdentifier(tag) @@ -90,15 +122,11 @@ func TagIdentifierAndObjectIdentifier(d *schema.ResourceData) (sdk.SchemaObjectI objectType := sdk.ObjectType(d.Get("object_type").(string)) - idsRaw := expandStringList(d.Get("object_identifier").(*schema.Set).List()) - ids := make([]sdk.ObjectIdentifier, len(idsRaw)) - for i, idRaw := range idsRaw { - id, err := sdk.ParseObjectIdentifierString(idRaw) - if err != nil { - return sdk.SchemaObjectIdentifier{}, nil, "", fmt.Errorf("invalid object id: %w", err) - } - ids[i] = id + ids, err := ExpandObjectIdentifierSet(d.Get("object_identifiers").(*schema.Set).List(), objectType) + if err != nil { + return sdk.SchemaObjectIdentifier{}, nil, "", err } + return tagId, ids, objectType, nil } @@ -138,7 +166,7 @@ func CreateContextTagAssociation(ctx context.Context, d *schema.ResourceData, me } } } - d.SetId(helpers.EncodeSnowflakeID(tagId.FullyQualifiedName(), tagValue, string(objectType))) + d.SetId(helpers.EncodeResourceIdentifier(tagId.FullyQualifiedName(), tagValue, string(objectType))) return ReadContextTagAssociation(ctx, d, meta) } @@ -160,7 +188,7 @@ func ReadContextTagAssociation(ctx context.Context, d *schema.ResourceData, meta correctObjectIds = append(correctObjectIds, oid.FullyQualifiedName()) } } - if err := d.Set("object_identifier", correctObjectIds); err != nil { + if err := d.Set("object_identifiers", correctObjectIds); err != nil { return diag.FromErr(err) } return nil @@ -172,20 +200,52 @@ func UpdateContextTagAssociation(ctx context.Context, d *schema.ResourceData, me if err != nil { return diag.FromErr(err) } - tagValue := d.Get("tag_value").(string) - if d.HasChange("object_identifier") { - o, n := d.GetChange("object_identifier") + if d.HasChange("tag_value") { + o, n := d.GetChange("object_identifiers") + tagValue := d.Get("tag_value").(string) + // Set only on old ids, because: + // - for the new ids we will set the new tag value below + // - for the removed ids we will unset the tag below + // So here we set only for the ids that don't change + 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) + } + + _, _, commonIds := ListDiffWithCommon(oldIds, newIds) + + for _, id := range commonIds { + 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) + } + } + d.SetId(helpers.EncodeResourceIdentifier(tagId.FullyQualifiedName(), tagValue, string(objectType))) + } + if d.HasChange("object_identifiers") { + tagValue := d.Get("tag_value").(string) - oldAllowedValues, err := expandStringListWithMapping(o.(*schema.Set).List(), sdk.ParseObjectIdentifierString) + o, n := d.GetChange("object_identifiers") + + oldIds, err := ExpandObjectIdentifierSet(o.(*schema.Set).List(), objectType) if err != nil { return diag.FromErr(err) } - newAllowedValues, err := expandStringListWithMapping(n.(*schema.Set).List(), sdk.ParseObjectIdentifierString) + newIds, err := ExpandObjectIdentifierSet(n.(*schema.Set).List(), objectType) if err != nil { return diag.FromErr(err) } - addedids, removedids := ListDiff(oldAllowedValues, newAllowedValues) + addedids, removedids := ListDiff(oldIds, newIds) for _, id := range addedids { request := sdk.NewSetTagRequest(objectType, id).WithSetTags([]sdk.TagAssociation{ @@ -200,12 +260,20 @@ func UpdateContextTagAssociation(ctx context.Context, d *schema.ResourceData, me } 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) } } - } return ReadContextTagAssociation(ctx, d, meta) @@ -213,12 +281,21 @@ func UpdateContextTagAssociation(ctx context.Context, d *schema.ResourceData, me func DeleteContextTagAssociation(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*provider.Context).Client - tid, ids, ot, err := TagIdentifierAndObjectIdentifier(d) + tagId, ids, objectType, err := TagIdentifierAndObjectIdentifier(d) if err != nil { return diag.FromErr(err) } - for _, oid := range ids { - request := sdk.NewUnsetTagRequest(ot, oid).WithUnsetTags([]sdk.ObjectIdentifier{tid}).WithIfExists(true) + 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) } @@ -226,3 +303,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 bc70b70115..7f35cc9b70 100644 --- a/pkg/resources/tag_association_acceptance_test.go +++ b/pkg/resources/tag_association_acceptance_test.go @@ -13,18 +13,26 @@ import ( "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" "github.com/hashicorp/terraform-plugin-testing/config" "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" "github.com/hashicorp/terraform-plugin-testing/terraform" "github.com/hashicorp/terraform-plugin-testing/tfversion" ) func TestAcc_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 { + m := func(tagId sdk.SchemaObjectIdentifier, tagValue string) 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), + "tag_name": config.StringVariable(tagId.Name()), + "tag_value": config.StringVariable(tagValue), + "database": config.StringVariable(databaseId.Name()), + "schema": config.StringVariable(acc.TestSchemaName), + "database_fully_qualified_name": config.StringVariable(databaseId.FullyQualifiedName()), } } resource.Test(t, resource.TestCase{ @@ -33,33 +41,91 @@ func TestAcc_TagAssociation(t *testing.T) { TerraformVersionChecks: []tfversion.TerraformVersionCheck{ tfversion.RequireAbove(tfversion.Version1_5_0), }, - CheckDestroy: nil, + CheckDestroy: acc.CheckTagValueEmpty(t), Steps: []resource.TestStep{ { ConfigDirectory: acc.ConfigurationDirectory("TestAcc_TagAssociation/basic"), - ConfigVariables: m(), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(resourceName, "id", helpers.EncodeSnowflakeID(tagId.FullyQualifiedName(), "finance", string("DATABASE"))), - resource.TestCheckResourceAttr(resourceName, "object_type", "DATABASE"), - resource.TestCheckResourceAttr(resourceName, "object_identifier.#", "1"), - resource.TestCheckResourceAttr(resourceName, "object_identifier.0", acc.TestClient().Ids.SchemaId().FullyQualifiedName()), + 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", "finance"), + resource.TestCheckResourceAttr(resourceName, "tag_value", tagValue), ), }, + // external change { PreConfig: func() { - acc.TestClient().Tag.Unset(t, "DATABASE", sdk.NewAccountObjectIdentifier(acc.TestDatabaseName), []sdk.ObjectIdentifier{tagId}) + acc.TestClient().Tag.Unset(t, sdk.ObjectTypeDatabase, sdk.NewAccountObjectIdentifier(acc.TestDatabaseName), []sdk.ObjectIdentifier{tagId}) }, ConfigDirectory: acc.ConfigurationDirectory("TestAcc_TagAssociation/basic"), - ConfigVariables: m(), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(resourceName, "id", helpers.EncodeSnowflakeID(tagId.FullyQualifiedName(), "finance", string("DATABASE"))), - resource.TestCheckResourceAttr(resourceName, "object_type", "DATABASE"), - resource.TestCheckResourceAttr(resourceName, "object_identifier.#", "1"), - resource.TestCheckResourceAttr(resourceName, "object_identifier.0", acc.TestClient().Ids.SchemaId().FullyQualifiedName()), + 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", "finance"), + 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", 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), + ), + }, + { + 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), ), }, }, @@ -67,6 +133,7 @@ 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" @@ -89,11 +156,11 @@ func TestAcc_TagAssociationSchema(t *testing.T) { { ConfigDirectory: acc.ConfigurationDirectory("TestAcc_TagAssociation/schema"), ConfigVariables: m(), - Check: resource.ComposeTestCheckFunc( + 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_identifier.#", "1"), - resource.TestCheckTypeSetElemAttr(resourceName, "object_identifier.*", schemaId.FullyQualifiedName()), + 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"), ), @@ -103,6 +170,8 @@ func TestAcc_TagAssociationSchema(t *testing.T) { } func TestAcc_TagAssociationColumn(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + tagId := acc.TestClient().Ids.RandomSchemaObjectIdentifier() tableId := acc.TestClient().Ids.RandomSchemaObjectIdentifier() columnId := sdk.NewTableColumnIdentifier(tableId.DatabaseName(), tableId.SchemaName(), tableId.Name(), "column") @@ -128,11 +197,11 @@ func TestAcc_TagAssociationColumn(t *testing.T) { { ConfigDirectory: acc.ConfigurationDirectory("TestAcc_TagAssociation/column"), ConfigVariables: m(), - Check: resource.ComposeTestCheckFunc( + 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_identifier.#", "1"), - resource.TestCheckTypeSetElemAttr(resourceName, "object_identifier.*", columnId.FullyQualifiedName()), + 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"), ), @@ -142,12 +211,14 @@ func TestAcc_TagAssociationColumn(t *testing.T) { } func TestAcc_TagAssociationIssue1202(t *testing.T) { - tagName := acc.TestClient().Ids.Alpha() + _ = 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(tagName), + "tag_name": config.StringVariable(tagId.Name()), "table_name": config.StringVariable(tableName), "database": config.StringVariable(acc.TestDatabaseName), "schema": config.StringVariable(acc.TestSchemaName), @@ -164,9 +235,9 @@ 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", fmt.Sprintf("%s|%s|%s", acc.TestDatabaseName, acc.TestSchemaName, tagName)), + resource.TestCheckResourceAttr(resourceName, "tag_id", tagId.FullyQualifiedName()), resource.TestCheckResourceAttr(resourceName, "tag_value", "v1"), ), }, @@ -175,22 +246,24 @@ func TestAcc_TagAssociationIssue1202(t *testing.T) { } func TestAcc_TagAssociationIssue1909(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + tagId := acc.TestClient().Ids.RandomSchemaObjectIdentifier() - tagName := tagId.Name() - 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(tagName), - "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), + "tag_name": config.StringVariable(tagId.Name()), + "table_name": config.StringVariable(tableId1.Name()), + "table_name2": config.StringVariable(tableId2.Name()), + "column_name": config.StringVariable("test.column"), + "column_fully_qualified_name": config.StringVariable(columnId1.FullyQualifiedName()), + "column2_fully_qualified_name": config.StringVariable(columnId2.FullyQualifiedName()), + "database": config.StringVariable(acc.TestDatabaseName), + "schema": config.StringVariable(acc.TestSchemaName), } } resource.Test(t, resource.TestCase{ @@ -204,12 +277,12 @@ func TestAcc_TagAssociationIssue1909(t *testing.T) { { ConfigDirectory: acc.ConfigurationDirectory("TestAcc_TagAssociation/issue1909"), ConfigVariables: m(), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(resourceName, "object_type", "COLUMN"), - resource.TestCheckResourceAttr(resourceName, "tag_id", fmt.Sprintf("%s|%s|%s", acc.TestDatabaseName, acc.TestSchemaName, tagName)), + 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"), ), }, }, @@ -234,18 +307,22 @@ func testAccCheckTableColumnTagAssociation(tagID sdk.SchemaObjectIdentifier, obj } } +// 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) - tagName := acc.TestClient().Ids.Alpha() - accountName := acc.TestClient().Ids.Alpha() + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + + tagId := acc.TestClient().Ids.RandomSchemaObjectIdentifier() + accountName := acc.TestClient().Context.CurrentAccountName(t) + orgName := acc.TestClient().Context.CurrentOrganizationName(t) + accountId := sdk.NewAccountIdentifier(orgName, accountName) resourceName := "snowflake_tag_association.test" m := func() map[string]config.Variable { return map[string]config.Variable{ - "tag_name": config.StringVariable(tagName), - "account_name": config.StringVariable(accountName), - "database": config.StringVariable(acc.TestDatabaseName), - "schema": config.StringVariable(acc.TestSchemaName), + "tag_name": config.StringVariable(tagId.Name()), + "account_fully_qualified_name": config.StringVariable(accountId.FullyQualifiedName()), + "database": config.StringVariable(acc.TestDatabaseName), + "schema": config.StringVariable(acc.TestSchemaName), } } @@ -260,9 +337,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", fmt.Sprintf("%s|%s|%s", acc.TestDatabaseName, acc.TestSchemaName, tagName)), + 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"), ), }, @@ -271,6 +350,8 @@ func TestAcc_TagAssociationAccountIssues1910(t *testing.T) { } func TestAcc_TagAssociationIssue1926(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + tagId := acc.TestClient().Ids.RandomSchemaObjectIdentifier() tableId1 := acc.TestClient().Ids.RandomSchemaObjectIdentifier() columnId1 := sdk.NewTableColumnIdentifier(tableId1.DatabaseName(), tableId1.SchemaName(), tableId1.Name(), "init") @@ -308,11 +389,11 @@ func TestAcc_TagAssociationIssue1926(t *testing.T) { { ConfigDirectory: acc.ConfigurationDirectory("TestAcc_TagAssociation/issue1926"), ConfigVariables: m(), - Check: resource.ComposeTestCheckFunc( + 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_identifier.#", "1"), - resource.TestCheckTypeSetElemAttr(resourceName, "object_identifier.*", columnId1.FullyQualifiedName()), + 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", "TAG_VALUE"), ), @@ -320,18 +401,11 @@ func TestAcc_TagAssociationIssue1926(t *testing.T) { { 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), + 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_identifier.#", "1"), - resource.TestCheckTypeSetElemAttr(resourceName, "object_identifier.*", columnId2.FullyQualifiedName()), + 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"), ), @@ -339,18 +413,75 @@ func TestAcc_TagAssociationIssue1926(t *testing.T) { { 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.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"), + ), + }, + }, + }) +} + +func TestAcc_Tag_migrateFromVersion_0_98_0(t *testing.T) { + t.Setenv(string(testenvs.ConfigureClientOnce), "") + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + acc.TestAccPreCheck(t) + // tag, tagCleanup := acc.TestClient().Tag.CreateTag(t) + // t.Cleanup(tagCleanuop) + tagId := acc.TestClient().Ids.RandomSchemaObjectIdentifier() + resourceName := "snowflake_tag_association.test" + schemaId := acc.TestClient().Ids.SchemaId() + + m := func() config.Variables { + return config.Variables{ + "tag_name": config.StringVariable(tagId.Name()), + "database": config.StringVariable(acc.TestDatabaseName), + "schema": config.StringVariable(acc.TestSchemaName), + "schema_fully_qualified_name": config.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.TestCheckTypeSetElemAttr(resourceName, "object_identifier.*", columnId3.FullyQualifiedName()), + 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), + }, + }, + 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"), ), @@ -358,3 +489,24 @@ func TestAcc_TagAssociationIssue1926(t *testing.T) { }, }) } + +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..c3a6e574ca --- /dev/null +++ b/pkg/resources/tag_association_state_upgraders.go @@ -0,0 +1,45 @@ +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 + database := obj["database"].(string) + schema := obj["schema"].(string) + name := obj["name"].(string) + switch { + case schema != "": + id = sdk.NewSchemaObjectIdentifier(database, schema, name) + case database != "": + id = sdk.NewDatabaseObjectIdentifier(database, name) + default: + id = sdk.NewAccountObjectIdentifier(name) + } + + objectIdentifiers = append(objectIdentifiers, id.FullyQualifiedName()) + } + rawState["object_identifiers"] = objectIdentifiers + + return rawState, nil +} diff --git a/pkg/resources/testdata/TestAcc_TagAssociation/basic/test.tf b/pkg/resources/testdata/TestAcc_TagAssociation/basic/test.tf index a0b03a0788..88c311e40c 100644 --- a/pkg/resources/testdata/TestAcc_TagAssociation/basic/test.tf +++ b/pkg/resources/testdata/TestAcc_TagAssociation/basic/test.tf @@ -2,13 +2,13 @@ resource "snowflake_tag" "test" { name = var.tag_name database = var.database schema = var.schema - allowed_values = ["finance", "hr"] + allowed_values = ["bar", "foo"] comment = "Terraform acceptance test" } resource "snowflake_tag_association" "test" { - object_identifier = [var.database] - object_type = "DATABASE" - tag_id = snowflake_tag.test.fully_qualified_name - 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 26aaef7126..fea0c22120 100644 --- a/pkg/resources/testdata/TestAcc_TagAssociation/column/test.tf +++ b/pkg/resources/testdata/TestAcc_TagAssociation/column/test.tf @@ -16,7 +16,7 @@ resource "snowflake_table" "test" { } resource "snowflake_tag_association" "test" { - object_identifier = [var.column_fully_qualified_name] + object_identifiers = [var.column_fully_qualified_name] object_type = "COLUMN" tag_id = snowflake_tag.test.fully_qualified_name diff --git a/pkg/resources/testdata/TestAcc_TagAssociation/issue1202/main.tf b/pkg/resources/testdata/TestAcc_TagAssociation/issue1202/main.tf index 4c316e627b..52a093e268 100644 --- a/pkg/resources/testdata/TestAcc_TagAssociation/issue1202/main.tf +++ b/pkg/resources/testdata/TestAcc_TagAssociation/issue1202/main.tf @@ -20,12 +20,8 @@ resource "snowflake_tag_association" "test" { // 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..361ee85266 100644 --- a/pkg/resources/testdata/TestAcc_TagAssociation/issue1909/test.tf +++ b/pkg/resources/testdata/TestAcc_TagAssociation/issue1909/test.tf @@ -24,17 +24,19 @@ 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_identifiers = [var.column_fully_qualified_name, var.column2_fully_qualified_name] + # 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_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 3d3973de8a..2f2d42947c 100644 --- a/pkg/resources/testdata/TestAcc_TagAssociation/issue1926/test.tf +++ b/pkg/resources/testdata/TestAcc_TagAssociation/issue1926/test.tf @@ -17,9 +17,9 @@ resource "snowflake_table" "test" { } } resource "snowflake_tag_association" "test" { - object_identifier = [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] + 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/schema/test.tf b/pkg/resources/testdata/TestAcc_TagAssociation/schema/test.tf index 70bb22141e..ba06156efc 100644 --- a/pkg/resources/testdata/TestAcc_TagAssociation/schema/test.tf +++ b/pkg/resources/testdata/TestAcc_TagAssociation/schema/test.tf @@ -6,7 +6,7 @@ resource "snowflake_tag" "test" { } resource "snowflake_tag_association" "test" { - object_identifier = [var.schema_fully_qualified_name] + object_identifiers = [var.schema_fully_qualified_name] object_type = "SCHEMA" tag_id = snowflake_tag.test.fully_qualified_name diff --git a/pkg/sdk/tags_impl.go b/pkg/sdk/tags_impl.go index 90bbf6ba1a..9d1907923f 100644 --- a/pkg/sdk/tags_impl.go +++ b/pkg/sdk/tags_impl.go @@ -149,6 +149,12 @@ func (s *SetTagRequest) toOpts() *setTagOptions { o.column = String(id.Name()) } } + if o.objectType == ObjectTypeAccount { + id, ok := o.objectName.(AccountIdentifier) + if ok { + o.objectName = NewAccountIdentifierFromFullyQualifiedName(id.AccountName()) + } + } return o } @@ -168,5 +174,11 @@ func (s *UnsetTagRequest) toOpts() *unsetTagOptions { o.column = String(id.Name()) } } + if o.objectType == ObjectTypeAccount { + id, ok := o.objectName.(AccountIdentifier) + if ok { + o.objectName = NewAccountIdentifierFromFullyQualifiedName(id.AccountName()) + } + } return o } diff --git a/pkg/sdk/tags_validations.go b/pkg/sdk/tags_validations.go index f6fd74b83b..25a219c533 100644 --- a/pkg/sdk/tags_validations.go +++ b/pkg/sdk/tags_validations.go @@ -154,9 +154,6 @@ func (opts *setTagOptions) validate() error { if !canBeAssociatedWithTag(opts.objectType) { return fmt.Errorf("tagging for object type %s is not supported", opts.objectType) } - if opts.objectType == ObjectTypeAccount { - return fmt.Errorf("tagging for object type ACCOUNT is not supported - use Tags.SetOnCurrentAccount instead") - } return errors.Join(errs...) } @@ -171,8 +168,5 @@ func (opts *unsetTagOptions) validate() error { if !canBeAssociatedWithTag(opts.objectType) { return fmt.Errorf("tagging for object type %s is not supported", opts.objectType) } - if opts.objectType == ObjectTypeAccount { - return fmt.Errorf("tagging for object type ACCOUNT is not supported - use Tags.UnsetOnCurrentAccount instead") - } return errors.Join(errs...) } diff --git a/pkg/sdk/testint/tags_integration_test.go b/pkg/sdk/testint/tags_integration_test.go index e5205212de..73c9c0a731 100644 --- a/pkg/sdk/testint/tags_integration_test.go +++ b/pkg/sdk/testint/tags_integration_test.go @@ -155,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) @@ -316,7 +326,7 @@ func TestInt_TagsAssociations(t *testing.T) { returnedTagValue, err := client.SystemFunctions.GetTag(ctx, tag.ID(), id, objectType) require.NoError(t, err) - assert.Equal(t, tagValue, returnedTagValue) + assert.Equal(t, sdk.Pointer(tagValue), returnedTagValue) err = client.Tags.Unset(ctx, sdk.NewUnsetTagRequest(objectType, id).WithUnsetTags(unsetTags)) require.NoError(t, err) @@ -326,7 +336,7 @@ func TestInt_TagsAssociations(t *testing.T) { assert.Nil(t, returnedTagValue) } - t.Run("TestInt_TagAssociationForAccount", func(t *testing.T) { + t.Run("TestInt_TagAssociationForAccount_locator", func(t *testing.T) { id := testClientHelper().Ids.AccountIdentifierWithLocator() err := client.Accounts.Alter(ctx, &sdk.AlterAccountOptions{ SetTag: tags, @@ -362,6 +372,27 @@ func TestInt_TagsAssociations(t *testing.T) { assert.Nil(t, returnedTagValue) }) + t.Run("TestInt_TagAssociationForAccount", func(t *testing.T) { + accountName := testClientHelper().Context.CurrentAccountName(t) + organizationName := testClientHelper().Context.CurrentOrganizationName(t) + id := sdk.NewAccountIdentifier(organizationName, accountName) + + // test tag sdk method + 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, sdk.Pointer(tagValue), returnedTagValue) + + err = client.Tags.UnsetOnCurrentAccount(ctx, sdk.NewUnsetTagOnCurrentAccountRequest().WithUnsetTags(unsetTags)) + require.NoError(t, err) + + returnedTagValue, err = client.SystemFunctions.GetTag(ctx, tag.ID(), id, sdk.ObjectTypeAccount) + require.NoError(t, err) + assert.Nil(t, returnedTagValue) + }) + accountObjectTestCases := []struct { name string objectType sdk.ObjectType @@ -784,23 +815,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, @@ -935,6 +949,37 @@ func TestInt_TagsAssociations(t *testing.T) { }) } + 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) + + returnedTagValue, err := client.SystemFunctions.GetTag(ctx, tag.ID(), id, sdk.ObjectTypeMaskingPolicy) + require.NoError(t, err) + assert.Equal(t, sdk.Pointer(tagValue), returnedTagValue) + + // 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) + + returnedTagValue, err = client.SystemFunctions.GetTag(ctx, tag.ID(), id, sdk.ObjectTypeMaskingPolicy) + require.NoError(t, err) + assert.Nil(t, returnedTagValue) + + // test object methods + testTagSet(id, sdk.ObjectTypeMaskingPolicy) + }) + columnTestCases := []struct { name string setupObject func() (sdk.TableColumnIdentifier, func()) diff --git a/templates/resources/authentication_policy.md.tmpl b/templates/resources/authentication_policy.md.tmpl new file mode 100644 index 0000000000..0519c7a809 --- /dev/null +++ b/templates/resources/authentication_policy.md.tmpl @@ -0,0 +1,36 @@ +--- +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 }} +--- + +-> **Note** According to Snowflake [docs](https://docs.snowflake.com/en/sql-reference/sql/drop-authentication-policy#usage-notes), an authentication policy cannot be dropped successfully if it is currently assigned to another object. Currently, the provider does not unassign such objects automatically. Before dropping the resource, list the assigned objects with `SELECT * from table(information_schema.policy_references(policy_name=>''));` and unassign them manually with `ALTER ...` or with updated Terraform configuration, if possible. + +# {{.Name}} ({{.Type}}) + +{{ .Description | trimspace }} + +{{ if .HasExample -}} +## Example Usage + +{{ tffile (printf "examples/resources/%s/resource.tf" .Name)}} + +-> **Note** Instead of using fully_qualified_name, you can reference objects managed outside Terraform by constructing a correct ID, consult [identifiers guide](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/guides/identifiers#new-computed-fully-qualified-name-field-in-resources). + + +{{- end }} + +{{ .SchemaMarkdown | trimspace }} +{{- if .HasImport }} + +## Import + +Import is supported using the following syntax: + +{{ codefile "shell" (printf "examples/resources/%s/import.sh" .Name)}} +{{- end }} diff --git a/templates/resources/masking_policy.md.tmpl b/templates/resources/masking_policy.md.tmpl index e09e58f03f..c9279de5a4 100644 --- a/templates/resources/masking_policy.md.tmpl +++ b/templates/resources/masking_policy.md.tmpl @@ -11,6 +11,8 @@ description: |- !> **V1 release candidate** This resource was reworked and is a release candidate for the V1. We do not expect significant changes in it before the V1. We will welcome any feedback and adjust the resource if needed. Any errors reported will be resolved with a higher priority. We encourage checking this resource out before the V1 release. Please follow the [migration guide](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/MIGRATION_GUIDE.md#v0950--v0960) to use it. +-> **Note** According to Snowflake [docs](https://docs.snowflake.com/en/sql-reference/sql/drop-masking-policy#usage-notes), a masking policy cannot be dropped successfully if it is currently assigned to another object. Currently, the provider does not unassign such objects automatically. Before dropping the resource, list the assigned objects with `SELECT * from table(information_schema.policy_references(policy_name=>''));` and unassign them manually with `ALTER ...` or with updated Terraform configuration, if possible. + # {{.Name}} ({{.Type}}) {{ .Description | trimspace }} diff --git a/templates/resources/network_policy.md.tmpl b/templates/resources/network_policy.md.tmpl index 28e2af568d..324fd3565e 100644 --- a/templates/resources/network_policy.md.tmpl +++ b/templates/resources/network_policy.md.tmpl @@ -11,6 +11,8 @@ description: |- !> **V1 release candidate** This resource was reworked and is a release candidate for the V1. We do not expect significant changes in it before the V1. We will welcome any feedback and adjust the resource if needed. Any errors reported will be resolved with a higher priority. We encourage checking this resource out before the V1 release. Please follow the [migration guide](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/MIGRATION_GUIDE.md#v0920--v0930) to use it. +-> **Note** According to Snowflake [docs](https://docs.snowflake.com/en/sql-reference/sql/drop-network-policy#usage-notes), a network policy cannot be dropped successfully if it is currently assigned to another object. Currently, the provider does not unassign such objects automatically. Before dropping the resource, list the assigned objects with `SELECT * from table(information_schema.policy_references(policy_name=>''));` and unassign them manually with `ALTER ...` or with updated Terraform configuration, if possible. + # {{.Name}} ({{.Type}}) {{ .Description | trimspace }} diff --git a/templates/resources/password_policy.md.tmpl b/templates/resources/password_policy.md.tmpl new file mode 100644 index 0000000000..859d534559 --- /dev/null +++ b/templates/resources/password_policy.md.tmpl @@ -0,0 +1,36 @@ +--- +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 }} +--- + +-> **Note** According to Snowflake [docs](https://docs.snowflake.com/en/sql-reference/sql/drop-password-policy#usage-notes), a password policy cannot be dropped successfully if it is currently assigned to another object. Currently, the provider does not unassign such objects automatically. Before dropping the resource, list the assigned objects with `SELECT * from table(information_schema.policy_references(policy_name=>''));` and unassign them manually with `ALTER ...` or with updated Terraform configuration, if possible. + +# {{.Name}} ({{.Type}}) + +{{ .Description | trimspace }} + +{{ if .HasExample -}} +## Example Usage + +{{ tffile (printf "examples/resources/%s/resource.tf" .Name)}} + +-> **Note** Instead of using fully_qualified_name, you can reference objects managed outside Terraform by constructing a correct ID, consult [identifiers guide](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/guides/identifiers#new-computed-fully-qualified-name-field-in-resources). + + +{{- end }} + +{{ .SchemaMarkdown | trimspace }} +{{- if .HasImport }} + +## Import + +Import is supported using the following syntax: + +{{ codefile "shell" (printf "examples/resources/%s/import.sh" .Name)}} +{{- end }} diff --git a/templates/resources/row_access_policy.md.tmpl b/templates/resources/row_access_policy.md.tmpl index e09e58f03f..1b561e4185 100644 --- a/templates/resources/row_access_policy.md.tmpl +++ b/templates/resources/row_access_policy.md.tmpl @@ -11,6 +11,8 @@ description: |- !> **V1 release candidate** This resource was reworked and is a release candidate for the V1. We do not expect significant changes in it before the V1. We will welcome any feedback and adjust the resource if needed. Any errors reported will be resolved with a higher priority. We encourage checking this resource out before the V1 release. Please follow the [migration guide](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/MIGRATION_GUIDE.md#v0950--v0960) to use it. +-> **Note** According to Snowflake [docs](https://docs.snowflake.com/en/sql-reference/sql/drop-row-access-policy#usage-notes), a row access policy cannot be dropped successfully if it is currently assigned to another object. Currently, the provider does not unassign such objects automatically. Before dropping the resource, list the assigned objects with `SELECT * from table(information_schema.policy_references(policy_name=>''));` and unassign them manually with `ALTER ...` or with updated Terraform configuration, if possible. + # {{.Name}} ({{.Type}}) {{ .Description | trimspace }} diff --git a/templates/resources/tag_association_md.tmpl b/templates/resources/tag_association.md.tmpl similarity index 89% rename from templates/resources/tag_association_md.tmpl rename to templates/resources/tag_association.md.tmpl index 7a876a0017..6f9efa5370 100644 --- a/templates/resources/tag_association_md.tmpl +++ b/templates/resources/tag_association.md.tmpl @@ -29,6 +29,8 @@ description: |- ## 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)}} diff --git a/v1-preparations/ESSENTIAL_GA_OBJECTS.MD b/v1-preparations/ESSENTIAL_GA_OBJECTS.MD index 28c250976a..b92976775d 100644 --- a/v1-preparations/ESSENTIAL_GA_OBJECTS.MD +++ b/v1-preparations/ESSENTIAL_GA_OBJECTS.MD @@ -32,7 +32,7 @@ newer provider versions. We will address these while working on the given object | STREAM | 🚀 | [#2975](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2975), [#2413](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2413), [#2201](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2201), [#1150](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1150) | | STREAMLIT | 🚀 | - | | TABLE | 👨‍💻 | [#2997](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2997), [#2844](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2844), [#2839](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2839), [#2735](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2735), [#2733](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2733), [#2683](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2683), [#2676](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2676), [#2674](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2674), [#2629](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2629), [#2418](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2418), [#2415](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2415), [#2406](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2406), [#2236](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2236), [#2035](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2035), [#1823](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1823), [#1799](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1799), [#1764](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1764), [#1600](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1600), [#1387](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1387), [#1272](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1272), [#1271](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1271), [#1248](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1248), [#1241](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1241), [#1146](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1146), [#1032](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1032), [#420](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/420) | -| TAG | 👨‍💻 | [#2943](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2902), [#2598](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2598), [#1910](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1910), [#1909](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1909), [#1862](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1862), [#1806](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1806), [#1657](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1657), [#1496](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1496), [#1443](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1443), [#1394](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1394), [#1372](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1372), [#1074](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1074) | +| TAG | 👨‍💻 | [#2943](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2943), [#2598](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2598), [#1910](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1910), [#1909](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1909), [#1862](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1862), [#1806](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1806), [#1657](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1657), [#1496](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1496), [#1443](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1443), [#1394](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1394), [#1372](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1372), [#1074](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1074) | | TASK | 👨‍💻 | [#1419](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1419), [#1250](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1250), [#1194](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1194), [#1088](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1088) | | VIEW | 🚀 | issues in the older versions: [resources](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues?q=label%3Aresource%3Aview+) and [datasources](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues?q=label%3Adata_source%3Aviews+) | | snowflake_unsafe_execute | 👨‍💻 | [#2934](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2934) |