diff --git a/docs/data-sources/account_roles.md b/docs/data-sources/account_roles.md new file mode 100644 index 00000000000..3eca8fb8cde --- /dev/null +++ b/docs/data-sources/account_roles.md @@ -0,0 +1,43 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "snowflake_account_roles Data Source - terraform-provider-snowflake" +subcategory: "" +description: |- + +--- + +# snowflake_account_roles (Data Source) + + + +## Example Usage + +```terraform +data "snowflake_account_roles" "all" { +} + +data "snowflake_account_roles" "by_pattern" { + pattern = "some_prefix_%" +} +``` + + +## Schema + +### Optional + +- `pattern` (String) Filters the command output by object name. + +### Read-Only + +- `id` (String) The ID of this resource. +- `roles` (List of Object) List of all the roles which you can view across your entire account, including the system-defined roles and any custom roles that exist. (see [below for nested schema](#nestedatt--roles)) + + +### Nested Schema for `roles` + +Read-Only: + +- `comment` (String) +- `name` (String) +- `owner` (String) diff --git a/docs/resources/account_role.md b/docs/resources/account_role.md new file mode 100644 index 00000000000..631a9e9b58f --- /dev/null +++ b/docs/resources/account_role.md @@ -0,0 +1,57 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "snowflake_account_role Resource - terraform-provider-snowflake" +subcategory: "" +description: |- + +--- + +# snowflake_account_role (Resource) + + + +## Example Usage + +```terraform +resource "snowflake_account_role" "role" { + name = "role_name" + comment = "comment" +} +``` + + +## Schema + +### Required + +- `name` (String) + +### Optional + +- `comment` (String) +- `tag` (Block List, Deprecated) Definitions of a tag to associate with the resource. (see [below for nested schema](#nestedblock--tag)) + +### Read-Only + +- `id` (String) The ID of this resource. + + +### Nested Schema for `tag` + +Required: + +- `name` (String) Tag name, e.g. department. +- `value` (String) Tag value, e.g. marketing_info. + +Optional: + +- `database` (String) Name of the database that the tag was created in. +- `schema` (String) Name of the schema that the tag was created in. + +## Import + +Import is supported using the following syntax: + +```shell +terraform import snowflake_account_role.example roleName +``` diff --git a/examples/data-sources/snowflake_account_roles/data-source.tf b/examples/data-sources/snowflake_account_roles/data-source.tf new file mode 100644 index 00000000000..c5846c0fab6 --- /dev/null +++ b/examples/data-sources/snowflake_account_roles/data-source.tf @@ -0,0 +1,6 @@ +data "snowflake_account_roles" "all" { +} + +data "snowflake_account_roles" "by_pattern" { + pattern = "some_prefix_%" +} diff --git a/examples/resources/snowflake_account_role/import.sh b/examples/resources/snowflake_account_role/import.sh new file mode 100644 index 00000000000..96c1cd42765 --- /dev/null +++ b/examples/resources/snowflake_account_role/import.sh @@ -0,0 +1 @@ +terraform import snowflake_account_role.example roleName diff --git a/examples/resources/snowflake_account_role/resource.tf b/examples/resources/snowflake_account_role/resource.tf new file mode 100644 index 00000000000..740f215598f --- /dev/null +++ b/examples/resources/snowflake_account_role/resource.tf @@ -0,0 +1,4 @@ +resource "snowflake_account_role" "role" { + name = "role_name" + comment = "comment" +} diff --git a/pkg/datasources/account_roles.go b/pkg/datasources/account_roles.go new file mode 100644 index 00000000000..c0c9e993bea --- /dev/null +++ b/pkg/datasources/account_roles.go @@ -0,0 +1,95 @@ +package datasources + +import ( + "context" + "database/sql" + "fmt" + + "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{ + "pattern": { + Type: schema.TypeString, + Optional: true, + Description: "Filters the command output by object name.", + }, + "roles": { + Type: schema.TypeList, + Computed: true, + Description: "List of all the roles which you can view across your entire account, including the system-defined roles and any custom roles that exist.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Computed: true, + Description: "Identifier for the role.", + }, + "comment": { + Type: schema.TypeString, + Computed: true, + Description: "The comment on the role", + }, + "owner": { + Type: schema.TypeString, + Computed: true, + Description: "The owner of the role", + }, + }, + }, + }, +} + +func AccountRoles() *schema.Resource { + return &schema.Resource{ + ReadContext: ReadAccountRoles, + Schema: accountRolesSchema, + } +} + +func ReadAccountRoles(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + db := meta.(*sql.DB) + client := sdk.NewClientFromDB(db) + + req := sdk.NewShowRoleRequest() + if pattern, ok := d.GetOk("pattern"); ok { + req.WithLike(sdk.NewLikeRequest(pattern.(string))) + } + + roles, err := client.Roles.Show(ctx, req) + if err != nil { + d.SetId("") + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Error, + Summary: "Failed to show account roles", + Detail: fmt.Sprintf("Search pattern: %v, err: %s", d.Get("pattern").(string), err), + }, + } + } + + mappedRoles := make([]map[string]any, len(roles)) + for i, role := range roles { + mappedRoles[i] = map[string]any{ + "name": role.Name, + "comment": role.Comment, + "owner": role.Owner, + } + } + + if err := d.Set("roles", mappedRoles); err != nil { + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Error, + Summary: "Failed to set roles", + Detail: fmt.Sprintf("Search pattern: %v, err: %s", d.Get("pattern").(string), err), + }, + } + } + + d.SetId("roles_read") + + return nil +} diff --git a/pkg/datasources/account_roles_acceptance_test.go b/pkg/datasources/account_roles_acceptance_test.go new file mode 100644 index 00000000000..0c0cbfc2f0a --- /dev/null +++ b/pkg/datasources/account_roles_acceptance_test.go @@ -0,0 +1,86 @@ +package datasources_test + +import ( + "fmt" + "strconv" + "strings" + "testing" + + acc "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance" + "github.com/hashicorp/terraform-plugin-testing/config" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-plugin-testing/tfversion" + + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAcc_AccountRoles_basic(t *testing.T) { + accountRoleNamePrefix := "account_roles_test_prefix_" + accountRoleName1 := accountRoleNamePrefix + strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) + accountRoleName2 := accountRoleNamePrefix + strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) + accountRoleName3 := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) + comment := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) + + configVariables := config.Variables{ + "account_role_name_1": config.StringVariable(accountRoleName1), + "account_role_name_2": config.StringVariable(accountRoleName2), + "account_role_name_3": config.StringVariable(accountRoleName3), + "pattern": config.StringVariable(accountRoleNamePrefix + "%"), + "comment": config.StringVariable(comment), + } + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + PreCheck: func() { acc.TestAccPreCheck(t) }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + Steps: []resource.TestStep{ + { + ConfigDirectory: config.TestNameDirectory(), + ConfigVariables: configVariables, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.snowflake_account_roles.test", "roles.#", "2"), + containsAccountRole(accountRoleName1, comment), + containsAccountRole(accountRoleName2, comment), + func(state *terraform.State) error { + err := containsAccountRole(accountRoleName3, comment)(state) + if err.Error() == fmt.Sprintf("role %s not found", accountRoleName3) { + return nil + } + return fmt.Errorf("expected %s not to be present", accountRoleName3) + }, + ), + }, + }, + }) +} + +func containsAccountRole(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.name", i)] == name { + actualComment := rs.Primary.Attributes[fmt.Sprintf("roles.%d.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) + } +} diff --git a/pkg/datasources/role.go b/pkg/datasources/role.go index 6cf81c4271b..4792af75c22 100644 --- a/pkg/datasources/role.go +++ b/pkg/datasources/role.go @@ -25,8 +25,9 @@ var roleSchema = map[string]*schema.Schema{ // Role Snowflake Role resource. func Role() *schema.Resource { return &schema.Resource{ - Read: ReadRole, - Schema: roleSchema, + Read: ReadRole, + Schema: roleSchema, + DeprecationMessage: "This resource is deprecated and will be removed in a future major version release. Please use snowflake_account_roles instead.", Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, diff --git a/pkg/datasources/roles.go b/pkg/datasources/roles.go index 781bed6e73d..4d94fd043e7 100644 --- a/pkg/datasources/roles.go +++ b/pkg/datasources/roles.go @@ -1,87 +1,13 @@ package datasources import ( - "database/sql" - "errors" - "log" - - "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/snowflake" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) -var rolesSchema = map[string]*schema.Schema{ - "pattern": { - Type: schema.TypeString, - Optional: true, - Description: "Filters the command output by object name.", - }, - "roles": { - Type: schema.TypeList, - Computed: true, - Description: "List of all the roles which you can view across your entire account, including the system-defined roles and any custom roles that exist.", - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "name": { - Type: schema.TypeString, - Computed: true, - Description: "Identifier for the role.", - }, - "comment": { - Type: schema.TypeString, - Computed: true, - Description: "The comment on the role", - }, - "owner": { - Type: schema.TypeString, - Computed: true, - Description: "The owner of the role", - }, - }, - }, - }, -} - -// Roles Snowflake Roles resource. func Roles() *schema.Resource { return &schema.Resource{ - Read: ReadRoles, - Schema: rolesSchema, - } -} - -// ReadRoles Reads the database metadata information. -func ReadRoles(d *schema.ResourceData, meta interface{}) error { - db := meta.(*sql.DB) - d.SetId("roles_read") - rolePattern := d.Get("pattern").(string) - - listRoles, err := snowflake.ListRoles(db, rolePattern) - if errors.Is(err, sql.ErrNoRows) { - log.Printf("[DEBUG] no roles found in account (%s)", d.Id()) - d.SetId("") - return nil - } else if err != nil { - log.Println("[DEBUG] failed to list roles") - d.SetId("") - return nil - } - - log.Printf("[DEBUG] list roles: %v", listRoles) - - roles := []map[string]interface{}{} - for _, role := range listRoles { - roleMap := map[string]interface{}{} - if !role.Name.Valid { - continue - } - roleMap["name"] = role.Name.String - roleMap["comment"] = role.Comment.String - roleMap["owner"] = role.Owner.String - roles = append(roles, roleMap) - } - - if err := d.Set("roles", roles); err != nil { - return err + ReadContext: ReadAccountRoles, + Schema: accountRolesSchema, + DeprecationMessage: "This resource is deprecated and will be removed in a future major version release. Please use snowflake_account_roles instead.", } - return nil } diff --git a/pkg/datasources/testdata/TestAcc_AccountRoles_basic/test.tf b/pkg/datasources/testdata/TestAcc_AccountRoles_basic/test.tf new file mode 100644 index 00000000000..68d5033a36a --- /dev/null +++ b/pkg/datasources/testdata/TestAcc_AccountRoles_basic/test.tf @@ -0,0 +1,23 @@ +resource "snowflake_account_role" "test1" { + name = var.account_role_name_1 + comment = var.comment +} + +resource "snowflake_account_role" "test2" { + name = var.account_role_name_2 + comment = var.comment +} + +resource "snowflake_account_role" "test3" { + name = var.account_role_name_3 + comment = var.comment +} + +data "snowflake_account_roles" "test" { + depends_on = [ + snowflake_account_role.test1, + snowflake_account_role.test2, + snowflake_account_role.test3, + ] + pattern = var.pattern +} diff --git a/pkg/datasources/testdata/TestAcc_AccountRoles_basic/variables.tf b/pkg/datasources/testdata/TestAcc_AccountRoles_basic/variables.tf new file mode 100644 index 00000000000..c632a5527e0 --- /dev/null +++ b/pkg/datasources/testdata/TestAcc_AccountRoles_basic/variables.tf @@ -0,0 +1,19 @@ +variable "account_role_name_1" { + type = string +} + +variable "account_role_name_2" { + type = string +} + +variable "account_role_name_3" { + type = string +} + +variable "comment" { + type = string +} + +variable "pattern" { + type = string +} diff --git a/pkg/provider/provider.go b/pkg/provider/provider.go index b72a7f76bb7..21b11488dbb 100644 --- a/pkg/provider/provider.go +++ b/pkg/provider/provider.go @@ -431,6 +431,7 @@ func getResources() map[string]*schema.Resource { "snowflake_account": resources.Account(), "snowflake_account_password_policy_attachment": resources.AccountPasswordPolicyAttachment(), "snowflake_account_parameter": resources.AccountParameter(), + "snowflake_account_role": resources.AccountRole(), "snowflake_alert": resources.Alert(), "snowflake_api_integration": resources.APIIntegration(), "snowflake_database": resources.Database(), @@ -517,6 +518,7 @@ func getDataSources() map[string]*schema.Resource { "snowflake_resource_monitors": datasources.ResourceMonitors(), "snowflake_role": datasources.Role(), "snowflake_roles": datasources.Roles(), + "snowflake_account_roles": datasources.AccountRoles(), "snowflake_row_access_policies": datasources.RowAccessPolicies(), "snowflake_schemas": datasources.Schemas(), "snowflake_sequences": datasources.Sequences(), diff --git a/pkg/resources/account_role.go b/pkg/resources/account_role.go new file mode 100644 index 00000000000..63cf3e7205e --- /dev/null +++ b/pkg/resources/account_role.go @@ -0,0 +1,253 @@ +package resources + +import ( + "context" + "database/sql" + "fmt" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +var accountRoleSchema = map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: IsValidIdentifier[sdk.AccountObjectIdentifier](), + }, + "comment": { + Type: schema.TypeString, + Optional: true, + }, + "tag": tagReferenceSchema, +} + +func AccountRole() *schema.Resource { + return &schema.Resource{ + CreateContext: CreateAccountRole, + ReadContext: ReadAccountRole, + DeleteContext: DeleteAccountRole, + UpdateContext: UpdateAccountRole, + + Schema: accountRoleSchema, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + } +} + +func CreateAccountRole(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + db := meta.(*sql.DB) + client := sdk.NewClientFromDB(db) + + name := d.Get("name").(string) + id, err := helpers.DecodeSnowflakeParameterID(name) + if err != nil { + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Error, + Summary: "Failed to parse account role name", + Detail: fmt.Sprintf("Account role name: %s, err: %s", name, err), + }, + } + } + req := sdk.NewCreateRoleRequest(id.(sdk.AccountObjectIdentifier)) + + if v, ok := d.GetOk("comment"); ok { + req.WithComment(v.(string)) + } + + if _, ok := d.GetOk("tag"); ok { + req.WithTag(getPropertyTags(d, "tag")) + } + + err = client.Roles.Create(ctx, req) + if err != nil { + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Error, + Summary: "Failed to create account role", + Detail: fmt.Sprintf("Account role name: %s, err: %s", name, err), + }, + } + } + + d.SetId(helpers.EncodeSnowflakeID(id)) + + return ReadAccountRole(ctx, d, meta) +} + +func ReadAccountRole(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + db := meta.(*sql.DB) + client := sdk.NewClientFromDB(db) + id := helpers.DecodeSnowflakeID(d.Id()).(sdk.AccountObjectIdentifier) + + accountRole, err := client.Roles.ShowByID(ctx, sdk.NewShowByIdRoleRequest(id)) + if err != nil { + if err.Error() == "object does not exist" { + d.SetId("") + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Warning, + Summary: "Account role not found; marking it as removed", + Detail: fmt.Sprintf("Account role name: %s, err: %s", id.FullyQualifiedName(), err), + }, + } + } + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Error, + Summary: "Failed to show account role by id", + Detail: fmt.Sprintf("Account role name: %s, err: %s", id.FullyQualifiedName(), err), + }, + } + } + + if err := d.Set("name", accountRole.Name); err != nil { + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Error, + Summary: "Failed to set account role name", + Detail: fmt.Sprintf("Account role name: %s, err: %s", accountRole.Name, err), + }, + } + } + + if err := d.Set("comment", accountRole.Comment); err != nil { + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Error, + Summary: "Failed to set account role comment", + Detail: fmt.Sprintf("Account role name: %s, comment: %s, err: %s", accountRole.Name, accountRole.Comment, err), + }, + } + } + + d.SetId(helpers.EncodeSnowflakeID(id)) + + return nil +} + +func UpdateAccountRole(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + db := meta.(*sql.DB) + client := sdk.NewClientFromDB(db) + id := helpers.DecodeSnowflakeID(d.Id()).(sdk.AccountObjectIdentifier) + + if d.HasChange("comment") { + if v, ok := d.GetOk("comment"); ok { + err := client.Roles.Alter(ctx, sdk.NewAlterRoleRequest(id).WithSetComment(v.(string))) + if err != nil { + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Error, + Summary: "Failed to set account role comment", + Detail: fmt.Sprintf("Account role name: %s, comment: %s, err: %s", id.FullyQualifiedName(), v, err), + }, + } + } + } else { + err := client.Roles.Alter(ctx, sdk.NewAlterRoleRequest(id).WithUnsetComment(true)) + if err != nil { + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Error, + Summary: "Failed to unset account role comment", + Detail: fmt.Sprintf("Account role name: %s, err: %s", id.FullyQualifiedName(), err), + }, + } + } + } + } + + if d.HasChange("tag") { + unsetTags, setTags := GetTagsDiff(d, "tag") + + if len(unsetTags) > 0 { + err := client.Roles.Alter(ctx, sdk.NewAlterRoleRequest(id).WithUnsetTags(unsetTags)) + if err != nil { + tagNames := make([]string, len(unsetTags)) + for i, v := range unsetTags { + tagNames[i] = v.FullyQualifiedName() + } + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Error, + Summary: "Failed to unset account role tags", + Detail: fmt.Sprintf("Account role name: %s, tags to unset: %v, err: %s", id.FullyQualifiedName(), tagNames, err), + }, + } + } + } + + if len(setTags) > 0 { + err := client.Roles.Alter(ctx, sdk.NewAlterRoleRequest(id).WithSetTags(setTags)) + if err != nil { + tagNames := make([]string, len(unsetTags)) + for i, v := range unsetTags { + tagNames[i] = v.FullyQualifiedName() + } + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Error, + Summary: "Failed to set account role tags", + Detail: fmt.Sprintf("Account role name: %s, tags to set: %v, err: %s", id.FullyQualifiedName(), tagNames, err), + }, + } + } + } + } + + if d.HasChange("name") { + _, newName := d.GetChange("name") + + newId, err := helpers.DecodeSnowflakeParameterID(newName.(string)) + if err != nil { + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Error, + Summary: "Failed to parse account role name", + Detail: fmt.Sprintf("Account role name: %s, err: %s", newName, err), + }, + } + } + + err = client.Roles.Alter(ctx, sdk.NewAlterRoleRequest(id).WithRenameTo(newId.(sdk.AccountObjectIdentifier))) + if err != nil { + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Error, + Summary: "Failed to rename account role name", + Detail: fmt.Sprintf("Previous account role name: %s, new account role name: %s, err: %s", id, newName, err), + }, + } + } + + d.SetId(helpers.EncodeSnowflakeID(newId)) + } + + return nil +} + +func DeleteAccountRole(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + db := meta.(*sql.DB) + client := sdk.NewClientFromDB(db) + id := helpers.DecodeSnowflakeID(d.Id()).(sdk.AccountObjectIdentifier) + + err := client.Roles.Drop(ctx, sdk.NewDropRoleRequest(id)) + if err != nil { + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Error, + Summary: "Failed to drop account role", + Detail: fmt.Sprintf("Account role name: %s, err: %s", d.Id(), err), + }, + } + } + + d.SetId("") + + return nil +} diff --git a/pkg/resources/account_role_acceptance_test.go b/pkg/resources/account_role_acceptance_test.go new file mode 100644 index 00000000000..5d58cbcfc33 --- /dev/null +++ b/pkg/resources/account_role_acceptance_test.go @@ -0,0 +1,127 @@ +package resources_test + +import ( + "context" + "database/sql" + "fmt" + "strings" + "testing" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + "github.com/hashicorp/terraform-plugin-testing/config" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-plugin-testing/tfversion" + + acc "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance" + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAcc_AccountRole_basic(t *testing.T) { + name := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) + comment := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) + configVariables := map[string]config.Variable{ + "name": config.StringVariable(name), + "comment": config.StringVariable(comment), + } + resourceName := "snowflake_account_role.test" + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + PreCheck: func() { acc.TestAccPreCheck(t) }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: testAccCheckAccountRoleDestroy(name), + Steps: []resource.TestStep{ + { + ConfigDirectory: config.TestNameDirectory(), + ConfigVariables: configVariables, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "name", name), + resource.TestCheckResourceAttr(resourceName, "comment", comment), + resource.TestCheckResourceAttr(resourceName, "id", name), + ), + }, + // test import + { + ConfigDirectory: config.TestNameDirectory(), + ConfigVariables: configVariables, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAcc_AccountRole_updates(t *testing.T) { + configVariables := func(name string, comment string) config.Variables { + return config.Variables{ + "name": config.StringVariable(name), + "comment": config.StringVariable(comment), + } + } + + name := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) + newName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) + comment := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) + NewComment := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) + resourceName := "snowflake_account_role.test" + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + PreCheck: func() { acc.TestAccPreCheck(t) }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: testAccCheckAccountRoleDestroy(name), + Steps: []resource.TestStep{ + { + ConfigDirectory: config.TestNameDirectory(), + ConfigVariables: configVariables(name, comment), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "name", name), + resource.TestCheckResourceAttr(resourceName, "comment", comment), + resource.TestCheckResourceAttr(resourceName, "id", name), + ), + }, + { + ConfigDirectory: config.TestNameDirectory(), + ConfigVariables: configVariables(newName, NewComment), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "name", newName), + resource.TestCheckResourceAttr(resourceName, "comment", NewComment), + resource.TestCheckResourceAttr(resourceName, "id", newName), + ), + }, + // test import + { + ConfigDirectory: config.TestNameDirectory(), + ConfigVariables: configVariables(newName, NewComment), + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckAccountRoleDestroy(accountRoleName string) func(state *terraform.State) error { + return func(state *terraform.State) error { + db := acc.TestAccProvider.Meta().(*sql.DB) + client := sdk.NewClientFromDB(db) + for _, rs := range state.RootModule().Resources { + if rs.Type != "snowflake_account_role" { + continue + } + ctx := context.Background() + id := sdk.NewAccountObjectIdentifier(rs.Primary.Attributes["name"]) + _, err := client.Roles.ShowByID(ctx, sdk.NewShowByIdRoleRequest(id)) + if err == nil { + return fmt.Errorf("account role %v still exists", accountRoleName) + } + } + return nil + } +} diff --git a/pkg/resources/database_role_acceptance_test.go b/pkg/resources/database_role_acceptance_test.go index 071ef0f9b13..9729a449eba 100644 --- a/pkg/resources/database_role_acceptance_test.go +++ b/pkg/resources/database_role_acceptance_test.go @@ -10,14 +10,12 @@ import ( "github.com/hashicorp/terraform-plugin-testing/helper/resource" ) -var ( - resourceName = "snowflake_database_role.test_db_role" - dbRoleName = "db_role_" + strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) - comment = "dummy" - comment2 = "test comment" -) - func TestAcc_DatabaseRole(t *testing.T) { + resourceName := "snowflake_database_role.test_db_role" + dbRoleName := "db_role_" + strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) + comment := "dummy" + comment2 := "test comment" + resource.Test(t, resource.TestCase{ Providers: acc.TestAccProviders(), PreCheck: func() { acc.TestAccPreCheck(t) }, diff --git a/pkg/resources/dynamic_table.go b/pkg/resources/dynamic_table.go index 90cbdd1c228..520e02b24ff 100644 --- a/pkg/resources/dynamic_table.go +++ b/pkg/resources/dynamic_table.go @@ -11,7 +11,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) -var dynamicTableShema = map[string]*schema.Schema{ +var dynamicTableSchema = map[string]*schema.Schema{ "or_replace": { Type: schema.TypeBool, Optional: true, @@ -144,7 +144,7 @@ func DynamicTable() *schema.Resource { Update: UpdateDynamicTable, Delete: DeleteDynamicTable, - Schema: dynamicTableShema, + Schema: dynamicTableSchema, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, diff --git a/pkg/resources/external_table.go b/pkg/resources/external_table.go index f9d73c42379..5b1235ca163 100644 --- a/pkg/resources/external_table.go +++ b/pkg/resources/external_table.go @@ -285,18 +285,22 @@ func UpdateExternalTable(d *schema.ResourceData, meta any) error { if d.HasChange("tag") { unsetTags, setTags := GetTagsDiff(d, "tag") - err := client.ExternalTables.Alter(ctx, sdk.NewAlterExternalTableRequest(id).WithUnsetTag(unsetTags)) - if err != nil { - return fmt.Errorf("error setting tags on %v, err = %w", d.Id(), err) + if len(unsetTags) > 0 { + err := client.ExternalTables.Alter(ctx, sdk.NewAlterExternalTableRequest(id).WithUnsetTag(unsetTags)) + if err != nil { + return fmt.Errorf("error setting tags on %v, err = %w", d.Id(), err) + } } - tagAssociationRequests := make([]*sdk.TagAssociationRequest, len(setTags)) - for i, t := range setTags { - tagAssociationRequests[i] = sdk.NewTagAssociationRequest(t.Name, t.Value) - } - err = client.ExternalTables.Alter(ctx, sdk.NewAlterExternalTableRequest(id).WithSetTag(tagAssociationRequests)) - if err != nil { - return fmt.Errorf("error setting tags on %v, err = %w", d.Id(), err) + if len(setTags) > 0 { + tagAssociationRequests := make([]*sdk.TagAssociationRequest, len(setTags)) + for i, t := range setTags { + tagAssociationRequests[i] = sdk.NewTagAssociationRequest(t.Name, t.Value) + } + err := client.ExternalTables.Alter(ctx, sdk.NewAlterExternalTableRequest(id).WithSetTag(tagAssociationRequests)) + if err != nil { + return fmt.Errorf("error setting tags on %v, err = %w", d.Id(), err) + } } } diff --git a/pkg/resources/role.go b/pkg/resources/role.go index 60268121d21..d63266e7816 100644 --- a/pkg/resources/role.go +++ b/pkg/resources/role.go @@ -1,154 +1,20 @@ package resources import ( - "database/sql" - "errors" - "log" - - "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" - - "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/snowflake" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) -var roleSchema = map[string]*schema.Schema{ - "name": { - Type: schema.TypeString, - Required: true, - ValidateFunc: func(val interface{}, key string) ([]string, []error) { - additionalCharsToIgnoreValidation := []string{".", " ", ":", "(", ")"} - return sdk.ValidateIdentifier(val, additionalCharsToIgnoreValidation) - }, - }, - "comment": { - Type: schema.TypeString, - Optional: true, - // TODO validation - }, - "tag": tagReferenceSchema, -} - func Role() *schema.Resource { return &schema.Resource{ - Create: CreateRole, - Read: ReadRole, - Delete: DeleteRole, - Update: UpdateRole, + CreateContext: CreateAccountRole, + ReadContext: ReadAccountRole, + DeleteContext: DeleteAccountRole, + UpdateContext: UpdateAccountRole, + DeprecationMessage: "This resource is deprecated and will be removed in a future major version release. Please use snowflake_account_role instead.", - Schema: roleSchema, + Schema: accountRoleSchema, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, } } - -func CreateRole(d *schema.ResourceData, meta interface{}) error { - name := d.Get("name").(string) - db := meta.(*sql.DB) - builder := snowflake.NewRoleBuilder(db, name) - if v, ok := d.GetOk("comment"); ok { - builder.WithComment(v.(string)) - } - if v, ok := d.GetOk("tag"); ok { - tags := getTags(v) - builder.WithTags(tags.toSnowflakeTagValues()) - } - err := builder.Create() - if err != nil { - return err - } - d.SetId(name) - return ReadRole(d, meta) -} - -func ReadRole(d *schema.ResourceData, meta interface{}) error { - db := meta.(*sql.DB) - id := d.Id() - // If the name is not set (such as during import) then use the id - name := d.Get("name").(string) - if name == "" { - name = id - } - - builder := snowflake.NewRoleBuilder(db, name) - role, err := builder.Show() - if errors.Is(err, sql.ErrNoRows) { - log.Printf("[WARN] role (%s) not found", name) - d.SetId("") - return nil - } else if err != nil { - return err - } - if err := d.Set("name", role.Name.String); err != nil { - return err - } - if err := d.Set("comment", role.Comment.String); err != nil { - return err - } - return nil -} - -func UpdateRole(d *schema.ResourceData, meta interface{}) error { - db := meta.(*sql.DB) - name := d.Get("name").(string) - builder := snowflake.NewRoleBuilder(db, name) - - if d.HasChange("name") { - o, n := d.GetChange("name") - builder.WithName(o.(string)) - err := builder.Rename(n.(string)) - if err != nil { - return err - } - builder.WithName(n.(string)) - } - - if d.HasChange("comment") { - o, n := d.GetChange("comment") - if n == nil || n.(string) == "" { - builder.WithComment(o.(string)) - err := builder.UnsetComment() - if err != nil { - return err - } - } else { - err := builder.SetComment(n.(string)) - if err != nil { - return err - } - } - } - - if d.HasChange("tag") { - o, n := d.GetChange("tag") - removed, added, changed := getTags(o).diffs(getTags(n)) - for _, tA := range removed { - err := builder.UnsetTag(tA.toSnowflakeTagValue()) - if err != nil { - return err - } - } - for _, tA := range added { - err := builder.SetTag(tA.toSnowflakeTagValue()) - if err != nil { - return err - } - } - for _, tA := range changed { - err := builder.ChangeTag(tA.toSnowflakeTagValue()) - if err != nil { - return err - } - } - } - - return nil -} - -func DeleteRole(d *schema.ResourceData, meta interface{}) error { - db := meta.(*sql.DB) - name := d.Get("name").(string) - builder := snowflake.NewRoleBuilder(db, name) - err := builder.Drop() - return err -} diff --git a/pkg/resources/schema.go b/pkg/resources/schema.go index bdd30aa3ca7..7dc7474541b 100644 --- a/pkg/resources/schema.go +++ b/pkg/resources/schema.go @@ -231,18 +231,22 @@ func UpdateSchema(d *schema.ResourceData, meta interface{}) error { if d.HasChange("tag") { unsetTags, setTags := GetTagsDiff(d, "tag") - err := client.Schemas.Alter(ctx, id, &sdk.AlterSchemaOptions{ - UnsetTag: unsetTags, - }) - if err != nil { - return fmt.Errorf("error occurred when dropping tags on %v, err = %w", d.Id(), err) + if len(unsetTags) > 0 { + err := client.Schemas.Alter(ctx, id, &sdk.AlterSchemaOptions{ + UnsetTag: unsetTags, + }) + if err != nil { + return fmt.Errorf("error occurred when dropping tags on %v, err = %w", d.Id(), err) + } } - err = client.Schemas.Alter(ctx, id, &sdk.AlterSchemaOptions{ - SetTag: setTags, - }) - if err != nil { - return fmt.Errorf("error occurred when setting tags on %v, err = %w", d.Id(), err) + if len(setTags) > 0 { + err := client.Schemas.Alter(ctx, id, &sdk.AlterSchemaOptions{ + SetTag: setTags, + }) + if err != nil { + return fmt.Errorf("error occurred when setting tags on %v, err = %w", d.Id(), err) + } } } diff --git a/pkg/resources/testdata/TestAcc_AccountRole_basic/test.tf b/pkg/resources/testdata/TestAcc_AccountRole_basic/test.tf new file mode 100644 index 00000000000..1aa83161f42 --- /dev/null +++ b/pkg/resources/testdata/TestAcc_AccountRole_basic/test.tf @@ -0,0 +1,4 @@ +resource "snowflake_account_role" "test" { + name = var.name + comment = var.comment +} diff --git a/pkg/resources/testdata/TestAcc_AccountRole_basic/variables.tf b/pkg/resources/testdata/TestAcc_AccountRole_basic/variables.tf new file mode 100644 index 00000000000..821eeebe895 --- /dev/null +++ b/pkg/resources/testdata/TestAcc_AccountRole_basic/variables.tf @@ -0,0 +1,7 @@ +variable "name" { + type = string +} + +variable "comment" { + type = string +} diff --git a/pkg/resources/testdata/TestAcc_AccountRole_updates/test.tf b/pkg/resources/testdata/TestAcc_AccountRole_updates/test.tf new file mode 100644 index 00000000000..1aa83161f42 --- /dev/null +++ b/pkg/resources/testdata/TestAcc_AccountRole_updates/test.tf @@ -0,0 +1,4 @@ +resource "snowflake_account_role" "test" { + name = var.name + comment = var.comment +} diff --git a/pkg/resources/testdata/TestAcc_AccountRole_updates/variables.tf b/pkg/resources/testdata/TestAcc_AccountRole_updates/variables.tf new file mode 100644 index 00000000000..821eeebe895 --- /dev/null +++ b/pkg/resources/testdata/TestAcc_AccountRole_updates/variables.tf @@ -0,0 +1,7 @@ +variable "name" { + type = string +} + +variable "comment" { + type = string +}