Skip to content

Commit

Permalink
chore: Handle generic check destroy in acceptance tests (#2716)
Browse files Browse the repository at this point in the history
- Added check destroy to almost all objects (check below).
- Changed signatures of ShowByID methods requiring request and not just
a plain id
- Removed checks from datasources (just a handful) - current approach is
to check the resource under test removal from SF and datasource is not
created on the SF side
- Corrected the bad checks that were the results of copy-paste
- Moved existing complex checks with annotation to rework them later

Follow-up needed:
- all the existing CheckDestroy's were addressed but there still may be
some tests without CheckDestroy specified; ideally, we should have an
automatic way (e.g. as part of architest, to validate this); it will be
added to one of the future improvements

Did not work on:
- old *_grant resources and grant_privileges_to_role - deprecated
resources
- *_parameter resources - no showbyid nor existing custom check
- snowflake_account_password_policy_attachment - no showbyid nor
existing custom check
- snowflake_external_oauth_integration, snowflake_oauth_integration,
snowflake_saml_integration, snowflake_scim_integration - no SDK
- snowflake_network_policy_attachment - no showbyid nor existing custom
check
- snowflake_table_column_masking_policy_application - no showbyid nor
existing custom check
- snowflake_table_constraint - no showbyid nor existing custom check
- snowflake_tag_association - no showbyid nor existing custom check
- snowflake_tag_masking_policy_association no showbyid nor existing
custom check
- snowflake_unsafe_execute - a bit different use case (will be handled
with the helpers cleanup)
- snowflake_user_public_keys - no showbyid nor existing custom check

The ones that does not have showbyid or existing check will be handled
as part of upcoming resource redesign.

Supported objects:
- snowflake_account
- snowflake_alert
- snowflake_api_integration
- snowflake_database
- snowflake_database_role
- snowflake_dynamic_table
- snowflake_email_notification_integration
- snowflake_external_function
- snowflake_external_table
- snowflake_failover_group
- snowflake_file_format
- snowflake_function
- snowflake_managed_account
- snowflake_masking_policy
- snowflake_materialized_view
- snowflake_network_policy
- snowflake_notification_integration
- snowflake_password_policy
- snowflake_pipe
- snowflake_procedure
- snowflake_resource_monitor
- snowflake_role
- snowflake_row_access_policy
- snowflake_schema
- snowflake_sequence
- snowflake_share
- snowflake_stage
- snowflake_storage_integration
- snowflake_stream
- snowflake_table
- snowflake_tag
- snowflake_task
- snowflake_user
- snowflake_view
- snowflake_warehouse
  • Loading branch information
sfc-gh-asawicki authored Apr 17, 2024
1 parent 74e2b6b commit 63a5324
Show file tree
Hide file tree
Showing 72 changed files with 841 additions and 897 deletions.
365 changes: 365 additions & 0 deletions pkg/acceptance/check_destroy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,365 @@
package acceptance

import (
"context"
"errors"
"fmt"
"reflect"
"strings"
"testing"

"github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers"
"github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/provider/resources"
"github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk"
"github.com/hashicorp/terraform-plugin-testing/terraform"
)

func CheckDestroy(t *testing.T, resource resources.Resource) func(*terraform.State) error {
t.Helper()
client := Client(t)
t.Logf("running check destroy for resource %s", resource)

return func(s *terraform.State) error {
for _, rs := range s.RootModule().Resources {
if rs.Type != resource.String() {
continue
}
t.Logf("found resource %s in state", resource)
ctx := context.Background()
id := decodeSnowflakeId(rs, resource)
if id == nil {
return fmt.Errorf("could not get the id of %s", resource)
}
showById, ok := showByIdFunctions[resource]
if !ok {
return fmt.Errorf("unsupported show by id in cleanup for %s, with id %v", resource, id.FullyQualifiedName())
}
if showById(ctx, client, id) == nil {
return fmt.Errorf("%s %v still exists", resource, id.FullyQualifiedName())
} else {
t.Logf("resource %s (%v) was dropped successfully in Snowflake", resource, id.FullyQualifiedName())
}
}
return nil
}
}

func decodeSnowflakeId(rs *terraform.ResourceState, resource resources.Resource) sdk.ObjectIdentifier {
switch resource {
case resources.ExternalFunction:
return sdk.NewSchemaObjectIdentifierFromFullyQualifiedName(rs.Primary.ID)
case resources.Function:
return sdk.NewSchemaObjectIdentifierFromFullyQualifiedName(rs.Primary.ID)
case resources.Procedure:
return sdk.NewSchemaObjectIdentifierFromFullyQualifiedName(rs.Primary.ID)
default:
return helpers.DecodeSnowflakeID(rs.Primary.ID)
}
}

type showByIdFunc func(context.Context, *sdk.Client, sdk.ObjectIdentifier) error

var showByIdFunctions = map[resources.Resource]showByIdFunc{
resources.Account: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error {
return runShowById(ctx, id, client.Accounts.ShowByID)
},
resources.Alert: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error {
return runShowById(ctx, id, client.Alerts.ShowByID)
},
resources.ApiIntegration: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error {
return runShowById(ctx, id, client.ApiIntegrations.ShowByID)
},
resources.Database: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error {
return runShowById(ctx, id, client.Databases.ShowByID)
},
resources.DatabaseRole: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error {
return runShowById(ctx, id, client.DatabaseRoles.ShowByID)
},
resources.DynamicTable: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error {
return runShowById(ctx, id, client.DynamicTables.ShowByID)
},
resources.EmailNotificationIntegration: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error {
return runShowById(ctx, id, client.NotificationIntegrations.ShowByID)
},
resources.ExternalFunction: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error {
return runShowById(ctx, id, client.ExternalFunctions.ShowByID)
},
resources.ExternalTable: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error {
return runShowById(ctx, id, client.ExternalTables.ShowByID)
},
resources.FailoverGroup: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error {
return runShowById(ctx, id, client.FailoverGroups.ShowByID)
},
resources.FileFormat: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error {
return runShowById(ctx, id, client.FileFormats.ShowByID)
},
resources.Function: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error {
return runShowById(ctx, id, client.Functions.ShowByID)
},
resources.ManagedAccount: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error {
return runShowById(ctx, id, client.ManagedAccounts.ShowByID)
},
resources.MaskingPolicy: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error {
return runShowById(ctx, id, client.MaskingPolicies.ShowByID)
},
resources.MaterializedView: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error {
return runShowById(ctx, id, client.MaterializedViews.ShowByID)
},
resources.NetworkPolicy: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error {
return runShowById(ctx, id, client.NetworkPolicies.ShowByID)
},
resources.NotificationIntegration: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error {
return runShowById(ctx, id, client.NotificationIntegrations.ShowByID)
},
resources.PasswordPolicy: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error {
return runShowById(ctx, id, client.PasswordPolicies.ShowByID)
},
resources.Pipe: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error {
return runShowById(ctx, id, client.Pipes.ShowByID)
},
resources.Procedure: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error {
return runShowById(ctx, id, client.Procedures.ShowByID)
},
resources.ResourceMonitor: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error {
return runShowById(ctx, id, client.ResourceMonitors.ShowByID)
},
resources.Role: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error {
return runShowById(ctx, id, client.Roles.ShowByID)
},
resources.RowAccessPolicy: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error {
return runShowById(ctx, id, client.RowAccessPolicies.ShowByID)
},
resources.Schema: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error {
return runShowById(ctx, id, client.Schemas.ShowByID)
},
resources.Sequence: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error {
return runShowById(ctx, id, client.Sequences.ShowByID)
},
resources.Share: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error {
return runShowById(ctx, id, client.Shares.ShowByID)
},
resources.Stage: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error {
return runShowById(ctx, id, client.Stages.ShowByID)
},
resources.StorageIntegration: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error {
return runShowById(ctx, id, client.StorageIntegrations.ShowByID)
},
resources.Stream: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error {
return runShowById(ctx, id, client.Streams.ShowByID)
},
resources.Table: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error {
return runShowById(ctx, id, client.Tables.ShowByID)
},
resources.Tag: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error {
return runShowById(ctx, id, client.Tags.ShowByID)
},
resources.Task: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error {
return runShowById(ctx, id, client.Tasks.ShowByID)
},
resources.User: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error {
return runShowById(ctx, id, client.Users.ShowByID)
},
resources.View: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error {
return runShowById(ctx, id, client.Views.ShowByID)
},
resources.Warehouse: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error {
return runShowById(ctx, id, client.Warehouses.ShowByID)
},
}

func runShowById[T any, U sdk.AccountObjectIdentifier | sdk.DatabaseObjectIdentifier | sdk.SchemaObjectIdentifier | sdk.TableColumnIdentifier](ctx context.Context, id sdk.ObjectIdentifier, show func(ctx context.Context, id U) (T, error)) error {
idCast, err := asId[U](id)
if err != nil {
return err
}
_, err = show(ctx, *idCast)
return err
}

func asId[T sdk.AccountObjectIdentifier | sdk.DatabaseObjectIdentifier | sdk.SchemaObjectIdentifier | sdk.TableColumnIdentifier](id sdk.ObjectIdentifier) (*T, error) {
if idCast, ok := id.(T); !ok {
return nil, fmt.Errorf("expected %s identifier type, but got: %T", reflect.TypeOf(new(T)).Elem().Name(), id)
} else {
return &idCast, nil
}
}

// CheckGrantAccountRoleDestroy is a custom checks that should be later incorporated into generic CheckDestroy
func CheckGrantAccountRoleDestroy(t *testing.T) func(*terraform.State) error {
t.Helper()
client := Client(t)

return func(s *terraform.State) error {
for _, rs := range s.RootModule().Resources {
if rs.Type != "snowflake_grant_account_role" {
continue
}
ctx := context.Background()
parts := strings.Split(rs.Primary.ID, "|")
roleName := parts[0]
roleIdentifier := sdk.NewAccountObjectIdentifierFromFullyQualifiedName(roleName)
objectType := parts[1]
targetIdentifier := parts[2]
grants, err := client.Grants.Show(ctx, &sdk.ShowGrantOptions{
Of: &sdk.ShowGrantsOf{
Role: roleIdentifier,
},
})
if err != nil {
return nil
}

var found bool
for _, grant := range grants {
if grant.GrantedTo == sdk.ObjectType(objectType) {
if grant.GranteeName.FullyQualifiedName() == targetIdentifier {
found = true
break
}
}
}
if found {
return fmt.Errorf("role grant %v still exists", rs.Primary.ID)
}
}
return nil
}
}

// CheckGrantDatabaseRoleDestroy is a custom checks that should be later incorporated into generic CheckDestroy
func CheckGrantDatabaseRoleDestroy(t *testing.T) func(*terraform.State) error {
t.Helper()
client := Client(t)

return func(s *terraform.State) error {
for _, rs := range s.RootModule().Resources {
if rs.Type != "snowflake_grant_database_role" {
continue
}
ctx := context.Background()
id := rs.Primary.ID
ids := strings.Split(id, "|")
databaseRoleName := ids[0]
objectType := ids[1]
parentRoleName := ids[2]
grants, err := client.Grants.Show(ctx, &sdk.ShowGrantOptions{
Of: &sdk.ShowGrantsOf{
DatabaseRole: sdk.NewDatabaseObjectIdentifierFromFullyQualifiedName(databaseRoleName),
},
})
if err != nil {
continue
}
for _, grant := range grants {
if grant.GrantedTo == sdk.ObjectType(objectType) {
if grant.GranteeName.FullyQualifiedName() == parentRoleName {
return fmt.Errorf("database role grant %v still exists", grant)
}
}
}
}
return nil
}
}

// CheckAccountRolePrivilegesRevoked is a custom checks that should be later incorporated into generic CheckDestroy
func CheckAccountRolePrivilegesRevoked(t *testing.T) func(*terraform.State) error {
t.Helper()
client := Client(t)

return func(s *terraform.State) error {
for _, rs := range s.RootModule().Resources {
if rs.Type != "snowflake_grant_privileges_to_account_role" {
continue
}
ctx := context.Background()

id := sdk.NewAccountObjectIdentifierFromFullyQualifiedName(rs.Primary.Attributes["account_role_name"])
grants, err := client.Grants.Show(ctx, &sdk.ShowGrantOptions{
To: &sdk.ShowGrantsTo{
Role: id,
},
})
if err != nil {
if errors.Is(err, sdk.ErrObjectNotExistOrAuthorized) {
continue
}
return err
}
var grantedPrivileges []string
for _, grant := range grants {
grantedPrivileges = append(grantedPrivileges, grant.Privilege)
}
if len(grantedPrivileges) > 0 {
return fmt.Errorf("account role (%s) is still granted, granted privileges %v", id.FullyQualifiedName(), grantedPrivileges)
}
}
return nil
}
}

// CheckDatabaseRolePrivilegesRevoked is a custom checks that should be later incorporated into generic CheckDestroy
func CheckDatabaseRolePrivilegesRevoked(t *testing.T) func(*terraform.State) error {
t.Helper()
client := Client(t)

return func(s *terraform.State) error {
for _, rs := range s.RootModule().Resources {
if rs.Type != "snowflake_grant_privileges_to_database_role" {
continue
}
ctx := context.Background()

id := sdk.NewDatabaseObjectIdentifierFromFullyQualifiedName(rs.Primary.Attributes["database_role_name"])
grants, err := client.Grants.Show(ctx, &sdk.ShowGrantOptions{
To: &sdk.ShowGrantsTo{
DatabaseRole: id,
},
})
if err != nil {
return err
}
var grantedPrivileges []string
for _, grant := range grants {
// usage is the default privilege available after creation (it won't be revoked)
if grant.Privilege != "USAGE" {
grantedPrivileges = append(grantedPrivileges, grant.Privilege)
}
}
if len(grantedPrivileges) > 0 {
return fmt.Errorf("database role (%s) is still granted, granted privileges %v", id.FullyQualifiedName(), grantedPrivileges)
}
}
return nil
}
}

// CheckUserPasswordPolicyAttachmentDestroy is a custom checks that should be later incorporated into generic CheckDestroy
func CheckUserPasswordPolicyAttachmentDestroy(t *testing.T) func(*terraform.State) error {
t.Helper()
client := Client(t)

return func(s *terraform.State) error {
for _, rs := range s.RootModule().Resources {
if rs.Type != "snowflake_user_password_policy_attachment" {
continue
}
ctx := context.Background()
policyReferences, err := client.PolicyReferences.GetForEntity(ctx, sdk.NewGetForEntityPolicyReferenceRequest(
sdk.NewAccountObjectIdentifierFromFullyQualifiedName(rs.Primary.Attributes["user_name"]),
sdk.PolicyEntityDomainUser,
))
if err != nil {
if strings.Contains(err.Error(), "does not exist or not authorized") {
// Note: this can happen if the Policy Reference or the User has been deleted as well; in this case, ignore the error
continue
}
return err
}
if len(policyReferences) > 0 {
return fmt.Errorf("user password policy attachment %v still exists", policyReferences[0].PolicyName)
}
}
return nil
}
}
Loading

0 comments on commit 63a5324

Please sign in to comment.