Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Tag association v1 readiness #3210

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
55 changes: 55 additions & 0 deletions MIGRATION_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,61 @@ across different versions.

## 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 in one resource.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so it means that if I want to add the same tag, same value to the same object type, then I have to put all of them in one resource, right? (here it would mean that changing silver to gold in the second resource is not permitted because both these resources would have the same id)

If I understand this correctly, then I think it would be good to add this info, directly to the resource docs, and here too.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, added more explanation.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the operation deterministic if done in one go? I mean it's possible that value will be first added in the first resource and later removed as part of second resource removal, right?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(because unset does not take the value)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, true. Added more instructions here and in the resource docs.

```
resource "snowflake_tag_association" "gold_warehouses" {
object_identifiers = [snowflake_warehouse.w1.fully_qualified_name, snowflake_warehouse.w2.fully_qualified_name]
object_type = "WAREHOUSE"
tag_id = snowflake_tag.tier.fully_qualified_name
tag_value = "gold"
}
resource "snowflake_tag_association" "silver_warehouses" {
object_identifiers = [snowflake_warehouse.w3.fully_qualified_name]
object_type = "WAREHOUSE"
tag_id = snowflake_tag.tier.fully_qualified_name
tag_value = "silver"
}
resource "snowflake_tag_association" "silver_databases" {
object_identifiers = [snowflake_database.d1.fully_qualified_name]
object_type = "DATABASE"
tag_id = snowflake_tag.tier.fully_qualified_name
tag_value = "silver"
}
```

The state is migrated automatically. There is no need to adjust configuration files, unless you use resource id like `snowflake_tag_association.example.id`.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand this part: There is no need to adjust configuration files, unless you use resource id like snowflake_tag_association.example.id., could you elaborate?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rephrased.

sfc-gh-asawicki marked this conversation as resolved.
Show resolved Hide resolved

#### *(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.

### snowflake_tag_masking_policy_association deprecation
`snowflake_tag_masking_policy_association` is now deprecated in favor of `snowflake_tag` with a new `masking_policy` field. It will be removed with the v1 release. Please adjust your configuration files.

Expand Down
2 changes: 2 additions & 0 deletions docs/resources/authentication_policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=>'<string>'));` and unassign them manually with `ALTER ...` or with updated Terraform configuration, if possible.
sfc-gh-asawicki marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's suggest going through terraform config changes in two steps and manual SQL as an alternative. Also, let's add example to guides/ and reference it here.

In guide let's show:

  • the example config creating policy assignment
  • error that will be a result of just a removal
  • step 1 - unnasigning policy (by changing config and running tf plan_apply)
  • step 2 - successful removal

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change was merged in #3226. I'll add such a guide in a next PR.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will keep it open then.


# 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).
Expand Down
2 changes: 2 additions & 0 deletions docs/resources/masking_policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=>'<string>'));` 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).
Expand Down
2 changes: 2 additions & 0 deletions docs/resources/network_policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=>'<string>'));` 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.
Expand Down
2 changes: 2 additions & 0 deletions docs/resources/password_policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=>'<string>'));` 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.
Expand Down
2 changes: 2 additions & 0 deletions docs/resources/row_access_policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=>'<string>'));` 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).
Expand Down
72 changes: 30 additions & 42 deletions docs/resources/tag_association.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@
page_title: "snowflake_tag_association Resource - terraform-provider-snowflake"
subcategory: ""
description: |-

Resource used to manage tag associations. For more information, check object tagging documentation https://docs.snowflake.com/en/user-guide/object-tagging.
---

# snowflake_tag_association (Resource)
!> **V1 release candidate** This resource was reworked and is a release candidate for the V1. We do not expect significant changes in it before the V1. We will welcome any feedback and adjust the resource if needed. Any errors reported will be resolved with a higher priority. We encourage checking this resource out before the V1 release. Please follow the [migration guide](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/MIGRATION_GUIDE.md#v0980--v0990) to use it.

-> **Note** For `ACCOUNT` object type, only identifiers with organization name are supported. See [account identifier docs](https://docs.snowflake.com/en/user-guide/admin-account-identifier#format-1-preferred-account-name-in-your-organization) for more details.

# 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

Expand All @@ -29,12 +33,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" {
Expand All @@ -53,28 +55,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).
<!-- TODO(SNOW-1634854): include an example showing both methods-->

Expand All @@ -83,9 +83,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
Expand All @@ -98,19 +98,6 @@ resource "snowflake_tag_association" "column_association" {

- `id` (String) The ID of this resource.

<a id="nestedblock--object_identifier"></a>
### 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.


<a id="nestedblock--timeouts"></a>
### Nested Schema for `timeouts`

Expand All @@ -120,9 +107,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'
```
3 changes: 1 addition & 2 deletions examples/resources/snowflake_tag_association/import.sh
Original file line number Diff line number Diff line change
@@ -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'
41 changes: 19 additions & 22 deletions examples/resources/snowflake_tag_association/resource.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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" {
Expand All @@ -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"
}
50 changes: 50 additions & 0 deletions pkg/acceptance/check_destroy.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"reflect"
"strconv"
"strings"
"testing"

Expand Down Expand Up @@ -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().SystemFunctions.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
sfc-gh-asawicki marked this conversation as resolved.
Show resolved Hide resolved
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 {
Expand Down
29 changes: 29 additions & 0 deletions pkg/acceptance/helpers/context_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,35 @@ func (c *ContextClient) CurrentAccount(t *testing.T) string {
return currentAccount
}

func (c *ContextClient) CurrentAccountName(t *testing.T) string {
sfc-gh-jcieslak marked this conversation as resolved.
Show resolved Hide resolved
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) CurrentAccountIdentifier(t *testing.T) sdk.AccountIdentifier {
t.Helper()

orgName := c.CurrentOrganizationName(t)
accountName := c.CurrentAccountName(t)

return sdk.NewAccountIdentifier(orgName, accountName)
}

func (c *ContextClient) CurrentRole(t *testing.T) sdk.AccountObjectIdentifier {
t.Helper()
ctx := context.Background()
Expand Down
Loading
Loading