Skip to content

Commit

Permalink
Add a new accounts data source
Browse files Browse the repository at this point in the history
  • Loading branch information
sfc-gh-jmichalak committed Dec 9, 2024
1 parent 548ec42 commit 54272c5
Show file tree
Hide file tree
Showing 15 changed files with 428 additions and 10 deletions.
3 changes: 3 additions & 0 deletions MIGRATION_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ across different versions.
## v0.99.0 ➞ v0.100.0

### snowflake_roles data source deprecation
`snowflake_roles` is now deprecated in favor of `snowflake_account_roles` with the same schema and behavior. It will be removed with the v1 release. Please adjust your configuration files.

### 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.
Expand Down
99 changes: 99 additions & 0 deletions docs/data-sources/account_roles.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
---
page_title: "snowflake_account_roles Data Source - terraform-provider-snowflake"
subcategory: ""
description: |-
Datasource used to get details of filtered account roles. Filtering is aligned with the current possibilities for SHOW ROLES https://docs.snowflake.com/en/sql-reference/sql/show-roles query (like and in_class are all supported). The results of SHOW are encapsulated in one output collection.
---

# snowflake_account_roles (Data Source)

Datasource used to get details of filtered account roles. Filtering is aligned with the current possibilities for [SHOW ROLES](https://docs.snowflake.com/en/sql-reference/sql/show-roles) query (`like` and `in_class` are all supported). The results of SHOW are encapsulated in one output collection.

## Example Usage

```terraform
# Simple usage
data "snowflake_account_roles" "simple" {
}
output "simple_output" {
value = data.snowflake_account_roles.simple.roles
}
# Filtering (like)
data "snowflake_account_roles" "like" {
like = "role-name"
}
output "like_output" {
value = data.snowflake_account_roles.like.roles
}
# Filtering (in class)
data "snowflake_account_roles" "in_class" {
in_class = "SNOWFLAKE.CORE.BUDGET"
}
output "in_class_output" {
value = data.snowflake_account_roles.in_class.roles
}
# Ensure the number of roles is equal to at least one element (with the use of postcondition)
data "snowflake_account_roles" "assert_with_postcondition" {
like = "role-name-%"
lifecycle {
postcondition {
condition = length(self.roles) > 0
error_message = "there should be at least one role"
}
}
}
# Ensure the number of roles is equal to at exactly one element (with the use of check block)
check "role_check" {
data "snowflake_account_roles" "assert_with_check_block" {
like = "role-name"
}
assert {
condition = length(data.snowflake_account_roles.assert_with_check_block.roles) == 1
error_message = "Roles filtered by '${data.snowflake_account_roles.assert_with_check_block.like}' returned ${length(data.snowflake_account_roles.assert_with_check_block.roles)} roles where one was expected"
}
}
```

<!-- schema generated by tfplugindocs -->
## Schema

### Optional

- `in_class` (String) Filters the SHOW GRANTS output by class name.
- `like` (String) Filters the output with **case-insensitive** pattern, with support for SQL wildcard characters (`%` and `_`).

### Read-Only

- `id` (String) The ID of this resource.
- `roles` (List of Object) Holds the aggregated output of all role details queries. (see [below for nested schema](#nestedatt--roles))

<a id="nestedatt--roles"></a>
### Nested Schema for `roles`

Read-Only:

- `show_output` (List of Object) (see [below for nested schema](#nestedobjatt--roles--show_output))

<a id="nestedobjatt--roles--show_output"></a>
### Nested Schema for `roles.show_output`

Read-Only:

- `assigned_to_users` (Number)
- `comment` (String)
- `created_on` (String)
- `granted_roles` (Number)
- `granted_to_roles` (Number)
- `is_current` (Boolean)
- `is_default` (Boolean)
- `is_inherited` (Boolean)
- `name` (String)
- `owner` (String)
2 changes: 2 additions & 0 deletions docs/data-sources/roles.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ description: |-

# snowflake_roles (Data Source)

~> **Deprecation** This resource is deprecated and will be removed in a future major version release. Please use [snowflake_account_roles](./account_roles) instead. <deprecation>

Datasource used to get details of filtered roles. Filtering is aligned with the current possibilities for [SHOW ROLES](https://docs.snowflake.com/en/sql-reference/sql/show-roles) query (`like` and `in_class` are all supported). The results of SHOW are encapsulated in one output collection.

## Example Usage
Expand Down
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -371,3 +371,4 @@ provider "snowflake" {
## Currently deprecated datasources

- [snowflake_role](./docs/data-sources/role) - use [snowflake_roles](./docs/data-sources/roles) instead
- [snowflake_roles](./docs/data-sources/roles) - use [snowflake_account_roles](./docs/data-sources/account_roles) instead
1 change: 1 addition & 0 deletions examples/additional/deprecated_datasources.MD
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
## Currently deprecated datasources

- [snowflake_role](./docs/data-sources/role) - use [snowflake_roles](./docs/data-sources/roles) instead
- [snowflake_roles](./docs/data-sources/roles) - use [snowflake_account_roles](./docs/data-sources/account_roles) instead
48 changes: 48 additions & 0 deletions examples/data-sources/snowflake_account_roles/data-source.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Simple usage
data "snowflake_account_roles" "simple" {
}

output "simple_output" {
value = data.snowflake_account_roles.simple.roles
}

# Filtering (like)
data "snowflake_account_roles" "like" {
like = "role-name"
}

output "like_output" {
value = data.snowflake_account_roles.like.roles
}

# Filtering (in class)
data "snowflake_account_roles" "in_class" {
in_class = "SNOWFLAKE.CORE.BUDGET"
}

output "in_class_output" {
value = data.snowflake_account_roles.in_class.roles
}

# Ensure the number of roles is equal to at least one element (with the use of postcondition)
data "snowflake_account_roles" "assert_with_postcondition" {
like = "role-name-%"
lifecycle {
postcondition {
condition = length(self.roles) > 0
error_message = "there should be at least one role"
}
}
}

# Ensure the number of roles is equal to at exactly one element (with the use of check block)
check "role_check" {
data "snowflake_account_roles" "assert_with_check_block" {
like = "role-name"
}

assert {
condition = length(data.snowflake_account_roles.assert_with_check_block.roles) == 1
error_message = "Roles filtered by '${data.snowflake_account_roles.assert_with_check_block.like}' returned ${length(data.snowflake_account_roles.assert_with_check_block.roles)} roles where one was expected"
}
}
100 changes: 100 additions & 0 deletions pkg/datasources/account_roles.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package datasources

import (
"context"
"fmt"

"github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/provider/datasources"

"github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/resources"
"github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/schemas"

"github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/provider"

"github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

var accountRolesSchema = map[string]*schema.Schema{
"like": {
Type: schema.TypeString,
Optional: true,
Description: "Filters the output with **case-insensitive** pattern, with support for SQL wildcard characters (`%` and `_`).",
},
"in_class": {
Type: schema.TypeString,
Optional: true,
ValidateDiagFunc: resources.IsValidIdentifier[sdk.SchemaObjectIdentifier](),
Description: "Filters the SHOW GRANTS output by class name.",
},
"roles": {
Type: schema.TypeList,
Computed: true,
Description: "Holds the aggregated output of all role details queries.",
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
resources.ShowOutputAttributeName: {
Type: schema.TypeList,
Computed: true,
Description: "Holds the output of SHOW ROLES.",
Elem: &schema.Resource{
Schema: schemas.ShowRoleSchema,
},
},
},
},
},
}

func AccountRoles() *schema.Resource {
return &schema.Resource{
ReadContext: TrackingReadWrapper(datasources.Roles, ReadRoles),
Schema: accountRolesSchema,
Description: "Datasource used to get details of filtered account roles. Filtering is aligned with the current possibilities for [SHOW ROLES](https://docs.snowflake.com/en/sql-reference/sql/show-roles) query (`like` and `in_class` are all supported). The results of SHOW are encapsulated in one output collection.",
}
}

func ReadAccountRoles(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
client := meta.(*provider.Context).Client

req := sdk.NewShowRoleRequest()

if likePattern, ok := d.GetOk("like"); ok {
req.WithLike(sdk.NewLikeRequest(likePattern.(string)))
}

if className, ok := d.GetOk("in_class"); ok {
req.WithInClass(sdk.RolesInClass{
Class: sdk.NewSchemaObjectIdentifierFromFullyQualifiedName(className.(string)),
})
}

roles, err := client.Roles.Show(ctx, req)
if err != nil {
return diag.Diagnostics{
diag.Diagnostic{
Severity: diag.Error,
Summary: "Failed to show account roles",
Detail: fmt.Sprintf("Error: %s", err),
},
}
}

d.SetId("account_roles_read")

flattenedAccountRoles := make([]map[string]any, len(roles))
for i, role := range roles {
role := role
flattenedAccountRoles[i] = map[string]any{
resources.ShowOutputAttributeName: []map[string]any{schemas.RoleToSchema(&role)},
}
}

err = d.Set("account_roles", flattenedAccountRoles)
if err != nil {
return diag.FromErr(err)
}

return nil
}
116 changes: 116 additions & 0 deletions pkg/datasources/account_roles_acceptance_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package datasources_test

import (
"fmt"
"strconv"
"testing"

acc "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance"

"github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/helpers/random"
"github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/testenvs"
"github.com/hashicorp/terraform-plugin-testing/config"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/terraform"
"github.com/hashicorp/terraform-plugin-testing/tfversion"
)

func TestAcc_AccountRoles_Complete(t *testing.T) {
_ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance)
acc.TestAccPreCheck(t)

accountRoleNamePrefix := random.AlphaN(10)
accountRoleName1 := acc.TestClient().Ids.AlphaWithPrefix(accountRoleNamePrefix + "1")
accountRoleName2 := acc.TestClient().Ids.AlphaWithPrefix(accountRoleNamePrefix + "2")
accountRoleName3 := acc.TestClient().Ids.Alpha()
dbRoleName := acc.TestClient().Ids.AlphaWithPrefix(accountRoleNamePrefix + "db")
comment := random.Comment()

// Proof that database role with the same prefix is not in the output of SHOW ROLES.
dbRole, dbRoleCleanup := acc.TestClient().DatabaseRole.CreateDatabaseRoleWithName(t, dbRoleName)
t.Cleanup(dbRoleCleanup)

likeVariables := config.Variables{
"account_role_name_1": config.StringVariable(accountRoleName1),
"account_role_name_2": config.StringVariable(accountRoleName2),
"account_role_name_3": config.StringVariable(accountRoleName3),
"comment": config.StringVariable(comment),
"like": config.StringVariable(accountRoleNamePrefix + "%"),
}

resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories,
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
tfversion.RequireAbove(tfversion.Version1_5_0),
},
Steps: []resource.TestStep{
{
ConfigDirectory: config.TestStepDirectory(),
ConfigVariables: likeVariables,
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("data.snowflake_account_roles.test", "roles.#", "2"),
accountRolesDataSourceContainsRole(accountRoleName1, comment),
accountRolesDataSourceContainsRole(accountRoleName2, comment),
accountRolesDataSourceDoesNotContainRole(accountRoleName3, comment),
accountRolesDataSourceDoesNotContainRole(dbRole.ID().FullyQualifiedName(), comment),
),
},
{
ConfigDirectory: config.TestStepDirectory(),
ConfigVariables: config.Variables{},
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttrWith("data.snowflake_account_roles.test", "roles.#", func(value string) error {
numberOfRoles, err := strconv.ParseInt(value, 10, 8)
if err != nil {
return err
}

if numberOfRoles == 0 {
return fmt.Errorf("expected roles to be non-empty")
}

return nil
}),
),
},
},
})
}

func accountRolesDataSourceDoesNotContainRole(name string, comment string) func(s *terraform.State) error {
return func(state *terraform.State) error {
err := accountRolesDataSourceContainsRole(name, comment)(state)
if err != nil && err.Error() == fmt.Sprintf("role %s not found", name) {
return nil
}
return fmt.Errorf("expected %s not to be present", name)
}
}

func accountRolesDataSourceContainsRole(name string, comment string) func(s *terraform.State) error {
return func(s *terraform.State) error {
for _, rs := range s.RootModule().Resources {
if rs.Type != "snowflake_account_roles" {
continue
}

iter, err := strconv.ParseInt(rs.Primary.Attributes["roles.#"], 10, 32)
if err != nil {
return err
}

for i := 0; i < int(iter); i++ {
if rs.Primary.Attributes[fmt.Sprintf("roles.%d.show_output.0.name", i)] == name {
actualComment := rs.Primary.Attributes[fmt.Sprintf("roles.%d.show_output.0.comment", i)]
if actualComment != comment {
return fmt.Errorf("expected comment: %s, but got: %s", comment, actualComment)
}

return nil
}
}
}

return fmt.Errorf("role %s not found", name)
}
}
Loading

0 comments on commit 54272c5

Please sign in to comment.