diff --git a/go.mod b/go.mod index 1bca6424a5..00da7a83b9 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/gookit/color v1.5.4 github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 github.com/hashicorp/go-uuid v1.0.3 + github.com/hashicorp/terraform-json v0.18.0 github.com/hashicorp/terraform-plugin-framework v1.4.2 github.com/hashicorp/terraform-plugin-framework-validators v0.12.0 github.com/hashicorp/terraform-plugin-go v0.20.0 @@ -87,7 +88,6 @@ require ( github.com/hashicorp/hcl/v2 v2.19.1 // indirect github.com/hashicorp/logutils v1.0.0 // indirect github.com/hashicorp/terraform-exec v0.19.0 // indirect - github.com/hashicorp/terraform-json v0.18.0 // indirect github.com/hashicorp/terraform-registry-address v0.2.3 // indirect github.com/hashicorp/terraform-svchost v0.1.1 // indirect github.com/hashicorp/yamux v0.1.1 // indirect diff --git a/pkg/architests/resources_acceptance_tests_arch_test.go b/pkg/architests/resources_acceptance_tests_arch_test.go index 008df3df01..7e3ddad5ca 100644 --- a/pkg/architests/resources_acceptance_tests_arch_test.go +++ b/pkg/architests/resources_acceptance_tests_arch_test.go @@ -1,6 +1,7 @@ package architests import ( + "regexp" "testing" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/architest" @@ -25,7 +26,7 @@ func TestArchCheck_AcceptanceTests_Resources(t *testing.T) { }) t.Run("there are no acceptance tests in other test files in the directory", func(t *testing.T) { - otherTestFiles := resourcesFiles.Filter(architest.FileNameFilterWithExclusionsProvider(architest.TestFileRegex, architest.AcceptanceTestFileRegex)) + otherTestFiles := resourcesFiles.Filter(architest.FileNameFilterWithExclusionsProvider(architest.TestFileRegex, architest.AcceptanceTestFileRegex, regexp.MustCompile("helpers_test.go"))) otherTestFiles.All(func(file *architest.File) { file.ExportedMethods().All(func(method *architest.Method) { diff --git a/pkg/resources/database_acceptance_test.go b/pkg/resources/database_acceptance_test.go index 2be05bd021..96e3f936e3 100644 --- a/pkg/resources/database_acceptance_test.go +++ b/pkg/resources/database_acceptance_test.go @@ -489,3 +489,26 @@ func checkAccountAndDatabaseDataRetentionTime(id sdk.AccountObjectIdentifier, ex return nil } } + +func createDatabaseOutsideTerraform(t *testing.T, name string) func() { + t.Helper() + client, err := sdk.NewDefaultClient() + if err != nil { + t.Fatal(err) + } + ctx := context.Background() + + if err := client.Databases.Create(ctx, sdk.NewAccountObjectIdentifier(name), new(sdk.CreateDatabaseOptions)); err != nil { + if err != nil { + t.Fatal(err) + } + } + + return func() { + if err := client.Databases.Drop(ctx, sdk.NewAccountObjectIdentifier(name), new(sdk.DropDatabaseOptions)); err != nil { + if err != nil { + t.Fatal(err) + } + } + } +} diff --git a/pkg/resources/grant_privileges_to_account_role.go b/pkg/resources/grant_privileges_to_account_role.go index 6581a45839..5d5e853a6e 100644 --- a/pkg/resources/grant_privileges_to_account_role.go +++ b/pkg/resources/grant_privileges_to_account_role.go @@ -2,6 +2,7 @@ package resources import ( "context" + "errors" "fmt" "log" "slices" @@ -738,9 +739,31 @@ func ReadGrantPrivilegesToAccountRole(ctx context.Context, d *schema.ResourceDat client := meta.(*provider.Context).Client + // TODO(SNOW-891217): Use custom error. Right now, "object does not exist" error is hidden in sdk/internal/collections package + if _, err := client.Roles.ShowByID(ctx, sdk.NewShowByIdRoleRequest(id.RoleName)); err != nil && err.Error() == "object does not exist" { + d.SetId("") + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Warning, + Summary: "Failed to retrieve account role. Marking the resource as removed.", + Detail: fmt.Sprintf("Id: %s", d.Id()), + }, + } + } + logging.DebugLogger.Printf("[DEBUG] About to show grants") grants, err := client.Grants.Show(ctx, opts) if err != nil { + if errors.Is(err, sdk.ErrObjectNotExistOrAuthorized) { + d.SetId("") + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Warning, + Summary: "Failed to retrieve grants. Target object not found. Marking the resource as removed.", + Detail: fmt.Sprintf("Id: %s", d.Id()), + }, + } + } return diag.Diagnostics{ diag.Diagnostic{ Severity: diag.Error, diff --git a/pkg/resources/grant_privileges_to_account_role_acceptance_test.go b/pkg/resources/grant_privileges_to_account_role_acceptance_test.go index bda96ed19d..e843630959 100644 --- a/pkg/resources/grant_privileges_to_account_role_acceptance_test.go +++ b/pkg/resources/grant_privileges_to_account_role_acceptance_test.go @@ -450,7 +450,7 @@ func TestAcc_GrantPrivilegesToAccountRole_OnSchemaObject_OnObject_OwnershipPrivi CheckDestroy: testAccCheckAccountRolePrivilegesRevoked(name), Steps: []resource.TestStep{ { - PreConfig: func() { createDatabaseRoleOutsideTerraform(t, name) }, + PreConfig: func() { createAccountRoleOutsideTerraform(t, name) }, ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantPrivilegesToAccountRole/OnSchemaObject_OnObject"), ConfigVariables: configVariables, ExpectError: regexp.MustCompile("Unsupported privilege 'OWNERSHIP'"), @@ -1358,6 +1358,92 @@ func revokeAndGrantPrivilegesOnTableToAccountRole( } } +// proves https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2621 doesn't apply to this resource +func TestAcc_GrantPrivilegesToAccountRole_RemoveGrantedObjectOutsideTerraform(t *testing.T) { + name := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) + roleName := sdk.NewAccountObjectIdentifier(name).FullyQualifiedName() + databaseName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) + configVariables := config.Variables{ + "name": config.StringVariable(roleName), + "database": config.StringVariable(databaseName), + "privileges": config.ListVariable( + config.StringVariable(string(sdk.AccountObjectPrivilegeCreateDatabaseRole)), + config.StringVariable(string(sdk.AccountObjectPrivilegeCreateSchema)), + ), + "with_grant_option": config.BoolVariable(true), + } + + var databaseCleanup func() + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + PreCheck: func() { acc.TestAccPreCheck(t) }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: testAccCheckAccountRolePrivilegesRevoked(name), + Steps: []resource.TestStep{ + { + PreConfig: func() { + databaseCleanup = createDatabaseOutsideTerraform(t, databaseName) + createAccountRoleOutsideTerraform(t, name) + }, + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantPrivilegesToAccountRole/OnAccountObject"), + ConfigVariables: configVariables, + }, + { + PreConfig: func() { databaseCleanup() }, + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantPrivilegesToAccountRole/OnAccountObject"), + ConfigVariables: configVariables, + // The error occurs in the Create operation, indicating the Read operation removed the resource from the state in the previous step. + ExpectError: regexp.MustCompile("An error occurred when granting privileges to account role"), + }, + }, + }) +} + +// proves https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2621 doesn't apply to this resource +func TestAcc_GrantPrivilegesToAccountRole_RemoveAccountRoleOutsideTerraform(t *testing.T) { + name := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) + roleName := sdk.NewAccountObjectIdentifier(name).FullyQualifiedName() + databaseName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) + configVariables := config.Variables{ + "name": config.StringVariable(roleName), + "database": config.StringVariable(databaseName), + "privileges": config.ListVariable( + config.StringVariable(string(sdk.AccountObjectPrivilegeCreateDatabaseRole)), + config.StringVariable(string(sdk.AccountObjectPrivilegeCreateSchema)), + ), + "with_grant_option": config.BoolVariable(true), + } + + var roleCleanup func() + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + PreCheck: func() { acc.TestAccPreCheck(t) }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: testAccCheckAccountRolePrivilegesRevoked(name), + Steps: []resource.TestStep{ + { + PreConfig: func() { + t.Cleanup(createDatabaseOutsideTerraform(t, databaseName)) + roleCleanup = createAccountRoleOutsideTerraform(t, name) + }, + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantPrivilegesToAccountRole/OnAccountObject"), + ConfigVariables: configVariables, + }, + { + PreConfig: func() { roleCleanup() }, + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantPrivilegesToAccountRole/OnAccountObject"), + ConfigVariables: configVariables, + // The error occurs in the Create operation, indicating the Read operation removed the resource from the state in the previous step. + ExpectError: regexp.MustCompile("An error occurred when granting privileges to account role"), + }, + }, + }) +} + func getSecondaryAccountName(t *testing.T) (string, error) { t.Helper() config, err := sdk.ProfileConfig(testprofiles.Secondary) @@ -1420,7 +1506,7 @@ func dropSharedDatabaseOnSecondaryAccount(t *testing.T, databaseName string, sha ) } -func createAccountRoleOutsideTerraform(t *testing.T, name string) { +func createAccountRoleOutsideTerraform(t *testing.T, name string) func() { t.Helper() client, err := sdk.NewDefaultClient() if err != nil { @@ -1431,6 +1517,12 @@ func createAccountRoleOutsideTerraform(t *testing.T, name string) { if err := client.Roles.Create(ctx, sdk.NewCreateRoleRequest(roleId).WithOrReplace(true)); err != nil { t.Fatal(fmt.Errorf("error account role (%s): %w", roleId.FullyQualifiedName(), err)) } + + return func() { + if err := client.Roles.Drop(ctx, sdk.NewDropRoleRequest(roleId).WithIfExists(true)); err != nil { + t.Fatal(fmt.Errorf("error account role (%s): %w", roleId.FullyQualifiedName(), err)) + } + } } func testAccCheckAccountRolePrivilegesRevoked(name string) func(*terraform.State) error { diff --git a/pkg/resources/grant_privileges_to_database_role.go b/pkg/resources/grant_privileges_to_database_role.go index 13f1de48db..3c5dbad671 100644 --- a/pkg/resources/grant_privileges_to_database_role.go +++ b/pkg/resources/grant_privileges_to_database_role.go @@ -2,6 +2,7 @@ package resources import ( "context" + "errors" "fmt" "log" "slices" @@ -653,8 +654,30 @@ func ReadGrantPrivilegesToDatabaseRole(ctx context.Context, d *schema.ResourceDa } client := meta.(*provider.Context).Client + // TODO(SNOW-891217): Use custom error. Right now, "object does not exist" error is hidden in sdk/internal/collections package + if _, err := client.DatabaseRoles.ShowByID(ctx, id.DatabaseRoleName); err != nil && err.Error() == "object does not exist" { + d.SetId("") + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Warning, + Summary: "Failed to retrieve database role. Marking the resource as removed.", + Detail: fmt.Sprintf("Id: %s", d.Id()), + }, + } + } + grants, err := client.Grants.Show(ctx, opts) if err != nil { + if errors.Is(err, sdk.ErrObjectNotExistOrAuthorized) { + d.SetId("") + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Warning, + Summary: "Failed to retrieve grants. Target object not found. Marking the resource as removed.", + Detail: fmt.Sprintf("Id: %s", d.Id()), + }, + } + } return diag.Diagnostics{ diag.Diagnostic{ Severity: diag.Error, diff --git a/pkg/resources/grant_privileges_to_database_role_acceptance_test.go b/pkg/resources/grant_privileges_to_database_role_acceptance_test.go index bbb478b486..0697c6dc2e 100644 --- a/pkg/resources/grant_privileges_to_database_role_acceptance_test.go +++ b/pkg/resources/grant_privileges_to_database_role_acceptance_test.go @@ -46,7 +46,7 @@ func TestAcc_GrantPrivilegesToDatabaseRole_OnDatabase(t *testing.T) { CheckDestroy: testAccCheckDatabaseRolePrivilegesRevoked, Steps: []resource.TestStep{ { - PreConfig: func() { createDatabaseRoleOutsideTerraform(t, name) }, + PreConfig: func() { createDatabaseRoleOutsideTerraform(t, acc.TestDatabaseName, name) }, ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantPrivilegesToDatabaseRole/OnDatabase"), ConfigVariables: configVariables, Check: resource.ComposeTestCheckFunc( @@ -97,7 +97,7 @@ func TestAcc_GrantPrivilegesToDatabaseRole_OnDatabase_PrivilegesReversed(t *test CheckDestroy: testAccCheckDatabaseRolePrivilegesRevoked, Steps: []resource.TestStep{ { - PreConfig: func() { createDatabaseRoleOutsideTerraform(t, name) }, + PreConfig: func() { createDatabaseRoleOutsideTerraform(t, acc.TestDatabaseName, name) }, ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantPrivilegesToDatabaseRole/OnDatabase"), ConfigVariables: configVariables, Check: resource.ComposeTestCheckFunc( @@ -148,7 +148,7 @@ func TestAcc_GrantPrivilegesToDatabaseRole_OnSchema(t *testing.T) { CheckDestroy: testAccCheckDatabaseRolePrivilegesRevoked, Steps: []resource.TestStep{ { - PreConfig: func() { createDatabaseRoleOutsideTerraform(t, name) }, + PreConfig: func() { createDatabaseRoleOutsideTerraform(t, acc.TestDatabaseName, name) }, ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantPrivilegesToDatabaseRole/OnSchema"), ConfigVariables: configVariables, Check: resource.ComposeTestCheckFunc( @@ -216,7 +216,7 @@ func TestAcc_GrantPrivilegesToDatabaseRole_OnAllSchemasInDatabase(t *testing.T) CheckDestroy: testAccCheckDatabaseRolePrivilegesRevoked, Steps: []resource.TestStep{ { - PreConfig: func() { createDatabaseRoleOutsideTerraform(t, name) }, + PreConfig: func() { createDatabaseRoleOutsideTerraform(t, acc.TestDatabaseName, name) }, ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantPrivilegesToDatabaseRole/OnAllSchemasInDatabase"), ConfigVariables: configVariables, Check: resource.ComposeTestCheckFunc( @@ -266,7 +266,7 @@ func TestAcc_GrantPrivilegesToDatabaseRole_OnFutureSchemasInDatabase(t *testing. CheckDestroy: testAccCheckDatabaseRolePrivilegesRevoked, Steps: []resource.TestStep{ { - PreConfig: func() { createDatabaseRoleOutsideTerraform(t, name) }, + PreConfig: func() { createDatabaseRoleOutsideTerraform(t, acc.TestDatabaseName, name) }, ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantPrivilegesToDatabaseRole/OnFutureSchemasInDatabase"), ConfigVariables: configVariables, Check: resource.ComposeTestCheckFunc( @@ -319,7 +319,7 @@ func TestAcc_GrantPrivilegesToDatabaseRole_OnSchemaObject_OnObject(t *testing.T) CheckDestroy: testAccCheckDatabaseRolePrivilegesRevoked, Steps: []resource.TestStep{ { - PreConfig: func() { createDatabaseRoleOutsideTerraform(t, name) }, + PreConfig: func() { createDatabaseRoleOutsideTerraform(t, acc.TestDatabaseName, name) }, ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantPrivilegesToDatabaseRole/OnSchemaObject_OnObject"), ConfigVariables: configVariables, Check: resource.ComposeTestCheckFunc( @@ -368,7 +368,7 @@ func TestAcc_GrantPrivilegesToDatabaseRole_OnSchemaObject_OnObject_OwnershipPriv CheckDestroy: testAccCheckDatabaseRolePrivilegesRevoked, Steps: []resource.TestStep{ { - PreConfig: func() { createDatabaseRoleOutsideTerraform(t, name) }, + PreConfig: func() { createDatabaseRoleOutsideTerraform(t, acc.TestDatabaseName, name) }, ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantPrivilegesToDatabaseRole/OnSchemaObject_OnObject"), ConfigVariables: configVariables, ExpectError: regexp.MustCompile("Unsupported privilege 'OWNERSHIP'"), @@ -403,7 +403,7 @@ func TestAcc_GrantPrivilegesToDatabaseRole_OnSchemaObject_OnAll_InDatabase(t *te CheckDestroy: testAccCheckDatabaseRolePrivilegesRevoked, Steps: []resource.TestStep{ { - PreConfig: func() { createDatabaseRoleOutsideTerraform(t, name) }, + PreConfig: func() { createDatabaseRoleOutsideTerraform(t, acc.TestDatabaseName, name) }, ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantPrivilegesToDatabaseRole/OnSchemaObject_OnAll_InDatabase"), ConfigVariables: configVariables, Check: resource.ComposeTestCheckFunc( @@ -454,7 +454,7 @@ func TestAcc_GrantPrivilegesToDatabaseRole_OnSchemaObject_OnAllPipes(t *testing. CheckDestroy: testAccCheckDatabaseRolePrivilegesRevoked, Steps: []resource.TestStep{ { - PreConfig: func() { createDatabaseRoleOutsideTerraform(t, name) }, + PreConfig: func() { createDatabaseRoleOutsideTerraform(t, acc.TestDatabaseName, name) }, ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantPrivilegesToDatabaseRole/OnAllPipes"), ConfigVariables: configVariables, Check: resource.ComposeTestCheckFunc( @@ -506,7 +506,7 @@ func TestAcc_GrantPrivilegesToDatabaseRole_OnSchemaObject_OnFuture_InDatabase(t CheckDestroy: testAccCheckDatabaseRolePrivilegesRevoked, Steps: []resource.TestStep{ { - PreConfig: func() { createDatabaseRoleOutsideTerraform(t, name) }, + PreConfig: func() { createDatabaseRoleOutsideTerraform(t, acc.TestDatabaseName, name) }, ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantPrivilegesToDatabaseRole/OnSchemaObject_OnFuture_InDatabase"), ConfigVariables: configVariables, Check: resource.ComposeTestCheckFunc( @@ -556,7 +556,7 @@ func TestAcc_GrantPrivilegesToDatabaseRole_OnSchemaObject_OnFuture_Streamlits_In CheckDestroy: testAccCheckDatabaseRolePrivilegesRevoked, Steps: []resource.TestStep{ { - PreConfig: func() { createDatabaseRoleOutsideTerraform(t, name) }, + PreConfig: func() { createDatabaseRoleOutsideTerraform(t, acc.TestDatabaseName, name) }, ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantPrivilegesToDatabaseRole/OnSchemaObject_OnFuture_InDatabase"), ConfigVariables: configVariables, ExpectError: regexp.MustCompile("Unsupported feature 'STREAMLIT'"), @@ -590,7 +590,7 @@ func TestAcc_GrantPrivilegesToDatabaseRole_OnSchemaObject_OnAll_Streamlits_InDat CheckDestroy: testAccCheckAccountRolePrivilegesRevoked(name), Steps: []resource.TestStep{ { - PreConfig: func() { createDatabaseRoleOutsideTerraform(t, name) }, + PreConfig: func() { createDatabaseRoleOutsideTerraform(t, acc.TestDatabaseName, name) }, ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantPrivilegesToDatabaseRole/OnSchemaObject_OnAll_InDatabase"), ConfigVariables: configVariables, Check: resource.ComposeTestCheckFunc( @@ -642,7 +642,7 @@ func TestAcc_GrantPrivilegesToDatabaseRole_UpdatePrivileges(t *testing.T) { CheckDestroy: testAccCheckDatabaseRolePrivilegesRevoked, Steps: []resource.TestStep{ { - PreConfig: func() { createDatabaseRoleOutsideTerraform(t, name) }, + PreConfig: func() { createDatabaseRoleOutsideTerraform(t, acc.TestDatabaseName, name) }, ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantPrivilegesToDatabaseRole/UpdatePrivileges/privileges"), ConfigVariables: configVariables(false, []sdk.AccountObjectPrivilege{ sdk.AccountObjectPrivilegeCreateSchema, @@ -734,7 +734,7 @@ func TestAcc_GrantPrivilegesToDatabaseRole_UpdatePrivileges_SnowflakeChecked(t * CheckDestroy: testAccCheckDatabaseRolePrivilegesRevoked, Steps: []resource.TestStep{ { - PreConfig: func() { createDatabaseRoleOutsideTerraform(t, name) }, + PreConfig: func() { createDatabaseRoleOutsideTerraform(t, acc.TestDatabaseName, name) }, ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantPrivilegesToDatabaseRole/UpdatePrivileges_SnowflakeChecked/privileges"), ConfigVariables: configVariables(false, []string{ sdk.AccountObjectPrivilegeCreateSchema.String(), @@ -810,7 +810,7 @@ func TestAcc_GrantPrivilegesToDatabaseRole_AlwaysApply(t *testing.T) { CheckDestroy: testAccCheckDatabaseRolePrivilegesRevoked, Steps: []resource.TestStep{ { - PreConfig: func() { createDatabaseRoleOutsideTerraform(t, name) }, + PreConfig: func() { createDatabaseRoleOutsideTerraform(t, acc.TestDatabaseName, name) }, ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantPrivilegesToDatabaseRole/AlwaysApply"), ConfigVariables: configVariables(false), ConfigPlanChecks: resource.ConfigPlanChecks{ @@ -905,7 +905,7 @@ func TestAcc_GrantPrivilegesToDatabaseRole_MLPrivileges(t *testing.T) { CheckDestroy: testAccCheckAccountRolePrivilegesRevoked(name), Steps: []resource.TestStep{ { - PreConfig: func() { createDatabaseRoleOutsideTerraform(t, name) }, + PreConfig: func() { createDatabaseRoleOutsideTerraform(t, acc.TestDatabaseName, name) }, ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantPrivilegesToDatabaseRole/OnSchema"), ConfigVariables: configVariables, Check: resource.ComposeTestCheckFunc( @@ -950,7 +950,7 @@ func TestAcc_GrantPrivilegesToDatabaseRole_ChangeWithGrantOptionsOutsideOfTerraf CheckDestroy: testAccCheckDatabaseRolePrivilegesRevoked, Steps: []resource.TestStep{ { - PreConfig: func() { createDatabaseRoleOutsideTerraform(t, name) }, + PreConfig: func() { createDatabaseRoleOutsideTerraform(t, acc.TestDatabaseName, name) }, ConfigPlanChecks: resource.ConfigPlanChecks{ PostApplyPostRefresh: []plancheck.PlanCheck{ plancheck.ExpectEmptyPlan(), @@ -1001,7 +1001,7 @@ func TestAcc_GrantPrivilegesToDatabaseRole_ChangeWithGrantOptionsOutsideOfTerraf CheckDestroy: testAccCheckDatabaseRolePrivilegesRevoked, Steps: []resource.TestStep{ { - PreConfig: func() { createDatabaseRoleOutsideTerraform(t, name) }, + PreConfig: func() { createDatabaseRoleOutsideTerraform(t, acc.TestDatabaseName, name) }, ConfigPlanChecks: resource.ConfigPlanChecks{ PostApplyPostRefresh: []plancheck.PlanCheck{ plancheck.ExpectEmptyPlan(), @@ -1031,17 +1031,105 @@ func TestAcc_GrantPrivilegesToDatabaseRole_ChangeWithGrantOptionsOutsideOfTerraf }) } -func createDatabaseRoleOutsideTerraform(t *testing.T, name string) { +// proves https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2621 doesn't apply to this resource +func TestAcc_GrantPrivilegesToDatabaseRole_RemoveGrantedObjectOutsideTerraform(t *testing.T) { + name := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) + databaseName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) + configVariables := config.Variables{ + "name": config.StringVariable(name), + "database": config.StringVariable(databaseName), + "privileges": config.ListVariable( + config.StringVariable(string(sdk.AccountObjectPrivilegeCreateSchema)), + ), + "with_grant_option": config.BoolVariable(true), + } + + var databaseCleanup func() + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + PreCheck: func() { acc.TestAccPreCheck(t) }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: testAccCheckDatabaseRolePrivilegesRevoked, + Steps: []resource.TestStep{ + { + PreConfig: func() { + databaseCleanup = createDatabaseOutsideTerraform(t, databaseName) + createDatabaseRoleOutsideTerraform(t, databaseName, name) + }, + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantPrivilegesToDatabaseRole/OnDatabase"), + ConfigVariables: configVariables, + }, + { + PreConfig: func() { databaseCleanup() }, + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantPrivilegesToDatabaseRole/OnDatabase"), + ConfigVariables: configVariables, + // The error occurs in the Create operation, indicating the Read operation removed the resource from the state in the previous step. + ExpectError: regexp.MustCompile("An error occurred when granting privileges to database role"), + }, + }, + }) +} + +// proves https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2621 doesn't apply to this resource +func TestAcc_GrantPrivilegesToDatabaseRole_RemoveDatabaseRoleOutsideTerraform(t *testing.T) { + name := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) + databaseName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) + configVariables := config.Variables{ + "name": config.StringVariable(name), + "database": config.StringVariable(databaseName), + "privileges": config.ListVariable( + config.StringVariable(string(sdk.AccountObjectPrivilegeCreateSchema)), + ), + "with_grant_option": config.BoolVariable(true), + } + + var databaseRoleCleanup func() + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + PreCheck: func() { acc.TestAccPreCheck(t) }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: testAccCheckDatabaseRolePrivilegesRevoked, + Steps: []resource.TestStep{ + { + PreConfig: func() { + t.Cleanup(createDatabaseOutsideTerraform(t, databaseName)) + databaseRoleCleanup = createDatabaseRoleOutsideTerraform(t, databaseName, name) + }, + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantPrivilegesToDatabaseRole/OnDatabase"), + ConfigVariables: configVariables, + }, + { + PreConfig: func() { databaseRoleCleanup() }, + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantPrivilegesToDatabaseRole/OnDatabase"), + ConfigVariables: configVariables, + // The error occurs in the Create operation, indicating the Read operation removed the resource from the state in the previous step. + ExpectError: regexp.MustCompile("An error occurred when granting privileges to database role"), + }, + }, + }) +} + +func createDatabaseRoleOutsideTerraform(t *testing.T, databaseName string, name string) func() { t.Helper() client, err := sdk.NewDefaultClient() if err != nil { t.Fatal(err) } ctx := context.Background() - databaseRoleId := sdk.NewDatabaseObjectIdentifier(acc.TestDatabaseName, name) + databaseRoleId := sdk.NewDatabaseObjectIdentifier(databaseName, name) if err := client.DatabaseRoles.Create(ctx, sdk.NewCreateDatabaseRoleRequest(databaseRoleId).WithOrReplace(true)); err != nil { t.Fatal(fmt.Errorf("error database role (%s): %w", databaseRoleId.FullyQualifiedName(), err)) } + + return func() { + if err := client.DatabaseRoles.Drop(ctx, sdk.NewDropDatabaseRoleRequest(databaseRoleId).WithIfExists(true)); err != nil { + t.Fatal(fmt.Errorf("error database role (%s): %w", databaseRoleId.FullyQualifiedName(), err)) + } + } } func queriedPrivilegesToDatabaseRoleEqualTo(databaseRoleName sdk.DatabaseObjectIdentifier, privileges ...string) func(s *terraform.State) error { diff --git a/pkg/resources/grant_privileges_to_role.go b/pkg/resources/grant_privileges_to_role.go index c54ed16fe3..31f69e3660 100644 --- a/pkg/resources/grant_privileges_to_role.go +++ b/pkg/resources/grant_privileges_to_role.go @@ -2,6 +2,7 @@ package resources import ( "context" + "errors" "fmt" "log" "slices" @@ -827,10 +828,21 @@ func setRolePrivilegeOptions(privileges []string, allPrivileges bool, onAccount } func readRoleGrantPrivileges(ctx context.Context, client *sdk.Client, grantedOn sdk.ObjectType, id GrantPrivilegesToRoleID, opts *sdk.ShowGrantOptions, d *schema.ResourceData) error { + if _, err := client.Roles.ShowByID(ctx, sdk.NewShowByIdRoleRequest(sdk.NewAccountObjectIdentifier(id.RoleName))); err != nil && err.Error() == "object does not exist" { + d.SetId("") + log.Printf("[DEBUG] Failed to retrieve account role. Marking the resource as removed.") + return nil + } + logging.DebugLogger.Printf("[DEBUG] About to show grants") grants, err := client.Grants.Show(ctx, opts) logging.DebugLogger.Printf("[DEBUG] After showing grants: err = %v", err) if err != nil { + if errors.Is(err, sdk.ErrObjectNotExistOrAuthorized) { + d.SetId("") + log.Printf("[DEBUG] Failed to show grants: %s. Marking object as removed.", err) + return nil + } return fmt.Errorf("error retrieving grants for account role: %w", err) } diff --git a/pkg/resources/grant_privileges_to_share.go b/pkg/resources/grant_privileges_to_share.go index 76c2b3fd14..73f05a30e3 100644 --- a/pkg/resources/grant_privileges_to_share.go +++ b/pkg/resources/grant_privileges_to_share.go @@ -2,6 +2,7 @@ package resources import ( "context" + "errors" "fmt" "log" "slices" @@ -305,8 +306,29 @@ func ReadGrantPrivilegesToShare(ctx context.Context, d *schema.ResourceData, met } client := meta.(*provider.Context).Client + if _, err := client.Shares.ShowByID(ctx, id.ShareName); err != nil && errors.Is(err, sdk.ErrObjectNotExistOrAuthorized) { + d.SetId("") + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Warning, + Summary: "Failed to retrieve share. Marking the resource as removed.", + Detail: fmt.Sprintf("Id: %s", d.Id()), + }, + } + } + grants, err := client.Grants.Show(ctx, opts) if err != nil { + if errors.Is(err, sdk.ErrObjectNotExistOrAuthorized) { + d.SetId("") + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Warning, + Summary: "Failed to retrieve grants. Object not found. Marking the resource as removed.", + Detail: fmt.Sprintf("Id: %s\nError: %s", d.Id(), err), + }, + } + } return diag.Diagnostics{ diag.Diagnostic{ Severity: diag.Error, diff --git a/pkg/resources/grant_privileges_to_share_acceptance_test.go b/pkg/resources/grant_privileges_to_share_acceptance_test.go index 15d92a5767..17bdbb4c8c 100644 --- a/pkg/resources/grant_privileges_to_share_acceptance_test.go +++ b/pkg/resources/grant_privileges_to_share_acceptance_test.go @@ -521,6 +521,45 @@ func TestAcc_GrantPrivilegesToShare_NoOnOption(t *testing.T) { }) } +// proves https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2621 doesn't apply to this resource +func TestAcc_GrantPrivilegesToShare_RemoveShareOutsideTerraform(t *testing.T) { + databaseName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) + shareName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) + + configVariables := config.Variables{ + "to_share": config.StringVariable(shareName), + "database": config.StringVariable(databaseName), + "privileges": config.ListVariable( + config.StringVariable(sdk.ObjectPrivilegeUsage.String()), + ), + } + + var shareCleanup func() + 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{ + { + PreConfig: func() { + shareCleanup = createShareOutsideTerraform(t, shareName) + }, + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantPrivilegesToShare/OnCustomShare"), + ConfigVariables: configVariables, + }, + { + PreConfig: func() { shareCleanup() }, + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantPrivilegesToShare/OnCustomShare"), + ConfigVariables: configVariables, + // The error occurs in the Create operation, indicating the Read operation removed the resource from the state in the previous step. + ExpectError: regexp.MustCompile("An error occurred when granting privileges to share"), + }, + }, + }) +} + func testAccCheckSharePrivilegesRevoked() func(*terraform.State) error { return func(state *terraform.State) error { for _, rs := range state.RootModule().Resources { @@ -552,3 +591,26 @@ func testAccCheckSharePrivilegesRevoked() func(*terraform.State) error { return nil } } + +func createShareOutsideTerraform(t *testing.T, name string) func() { + t.Helper() + client, err := sdk.NewDefaultClient() + if err != nil { + t.Fatal(err) + } + ctx := context.Background() + + if err := client.Shares.Create(ctx, sdk.NewAccountObjectIdentifier(name), new(sdk.CreateShareOptions)); err != nil { + if err != nil { + t.Fatal(err) + } + } + + return func() { + if err := client.Shares.Drop(ctx, sdk.NewAccountObjectIdentifier(name)); err != nil { + if err != nil { + t.Fatal(err) + } + } + } +} diff --git a/pkg/resources/helpers_test.go b/pkg/resources/helpers_test.go index bb0221b64d..d65409b83d 100644 --- a/pkg/resources/helpers_test.go +++ b/pkg/resources/helpers_test.go @@ -6,6 +6,9 @@ import ( "slices" "testing" + tfjson "github.com/hashicorp/terraform-json" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/provider" acc "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance" @@ -551,3 +554,21 @@ func updateAccountParameter(t *testing.T, client *sdk.Client, parameter sdk.Acco require.NoError(t, err) } } + +type planCheck func(ctx context.Context, req plancheck.CheckPlanRequest, resp *plancheck.CheckPlanResponse) + +func (fn planCheck) CheckPlan(ctx context.Context, req plancheck.CheckPlanRequest, resp *plancheck.CheckPlanResponse) { + fn(ctx, req, resp) +} + +func expectsCreatePlan(resourceAddress string) planCheck { + return func(ctx context.Context, req plancheck.CheckPlanRequest, resp *plancheck.CheckPlanResponse) { + for _, rc := range req.Plan.ResourceChanges { + if rc.Address == resourceAddress && rc.Change != nil && slices.Contains(rc.Change.Actions, tfjson.ActionCreate) { + return + } + } + + resp.Error = fmt.Errorf("expected plan to contain create request for %s", resourceAddress) + } +} diff --git a/pkg/resources/schema_acceptance_test.go b/pkg/resources/schema_acceptance_test.go index bf97cec444..bafc1c6f61 100644 --- a/pkg/resources/schema_acceptance_test.go +++ b/pkg/resources/schema_acceptance_test.go @@ -3,6 +3,7 @@ package resources_test import ( "context" "fmt" + "regexp" "strconv" "strings" "testing" @@ -308,6 +309,76 @@ func TestAcc_Schema_DefaultDataRetentionTime_SetOutsideOfTerraform(t *testing.T) }) } +func TestAcc_Schema_RemoveDatabaseOutsideOfTerraform(t *testing.T) { + schemaName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) + configVariables := map[string]config.Variable{ + "schema_name": config.StringVariable(schemaName), + "database_name": config.StringVariable(acc.TestDatabaseName), + } + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + PreCheck: func() { acc.TestAccPreCheck(t) }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: testAccCheckSchemaDestroy, + Steps: []resource.TestStep{ + { + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_Schema_RemoveOutsideOfTerraform"), + ConfigVariables: configVariables, + }, + { + PreConfig: func() { + removeSchemaOutsideOfTerraform(t, acc.TestDatabaseName, schemaName) + }, + RefreshState: true, + ExpectNonEmptyPlan: true, + RefreshPlanChecks: resource.RefreshPlanChecks{ + PostRefresh: []plancheck.PlanCheck{ + expectsCreatePlan("snowflake_schema.test"), + }, + }, + }, + }, + }) +} + +func TestAcc_Schema_RemoveSchemaOutsideOfTerraform(t *testing.T) { + databaseName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) + schemaName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) + configVariables := map[string]config.Variable{ + "schema_name": config.StringVariable(schemaName), + "database_name": config.StringVariable(databaseName), + } + + cleanupDatabase := createDatabaseOutsideTerraform(t, databaseName) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + PreCheck: func() { acc.TestAccPreCheck(t) }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: testAccCheckSchemaDestroy, + Steps: []resource.TestStep{ + { + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_Schema_RemoveOutsideOfTerraform"), + ConfigVariables: configVariables, + }, + { + PreConfig: func() { + cleanupDatabase() + }, + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_Schema_RemoveOutsideOfTerraform"), + ConfigVariables: configVariables, + // The error occurs in the Create operation, indicating the Read operation removed the resource from the state in the previous step. + ExpectError: regexp.MustCompile("error creating schema"), + }, + }, + }) +} + func checkDatabaseAndSchemaDataRetentionTime(id sdk.DatabaseObjectIdentifier, expectedDatabaseRetentionsDays int, expectedSchemaRetentionDays int) func(state *terraform.State) error { return func(state *terraform.State) error { client := acc.TestAccProvider.Meta().(*provider.Context).Client @@ -381,3 +452,14 @@ func testAccCheckSchemaDestroy(s *terraform.State) error { } return nil } + +func removeSchemaOutsideOfTerraform(t *testing.T, databaseName string, schemaName string) { + t.Helper() + + client, err := sdk.NewDefaultClient() + require.NoError(t, err) + ctx := context.Background() + + err = client.Schemas.Drop(ctx, sdk.NewDatabaseObjectIdentifier(databaseName, schemaName), new(sdk.DropSchemaOptions)) + require.NoError(t, err) +} diff --git a/pkg/resources/testdata/TestAcc_GrantPrivilegesToShare/OnCustomShare/test.tf b/pkg/resources/testdata/TestAcc_GrantPrivilegesToShare/OnCustomShare/test.tf new file mode 100644 index 0000000000..2dd4981e15 --- /dev/null +++ b/pkg/resources/testdata/TestAcc_GrantPrivilegesToShare/OnCustomShare/test.tf @@ -0,0 +1,9 @@ +resource "snowflake_database" "test" { + name = var.database +} + +resource "snowflake_grant_privileges_to_share" "test" { + to_share = var.to_share + privileges = var.privileges + on_database = snowflake_database.test.name +} diff --git a/pkg/resources/testdata/TestAcc_GrantPrivilegesToShare/OnCustomShare/variables.tf b/pkg/resources/testdata/TestAcc_GrantPrivilegesToShare/OnCustomShare/variables.tf new file mode 100644 index 0000000000..8f3b5923f0 --- /dev/null +++ b/pkg/resources/testdata/TestAcc_GrantPrivilegesToShare/OnCustomShare/variables.tf @@ -0,0 +1,11 @@ +variable "to_share" { + type = string +} + +variable "privileges" { + type = list(string) +} + +variable "database" { + type = string +} diff --git a/pkg/resources/testdata/TestAcc_Schema_RemoveOutsideOfTerraform/test.tf b/pkg/resources/testdata/TestAcc_Schema_RemoveOutsideOfTerraform/test.tf new file mode 100644 index 0000000000..af36440e58 --- /dev/null +++ b/pkg/resources/testdata/TestAcc_Schema_RemoveOutsideOfTerraform/test.tf @@ -0,0 +1,4 @@ +resource "snowflake_schema" "test" { + name = var.schema_name + database = var.database_name +} \ No newline at end of file diff --git a/pkg/resources/testdata/TestAcc_Schema_RemoveOutsideOfTerraform/variables.tf b/pkg/resources/testdata/TestAcc_Schema_RemoveOutsideOfTerraform/variables.tf new file mode 100644 index 0000000000..062e50b07f --- /dev/null +++ b/pkg/resources/testdata/TestAcc_Schema_RemoveOutsideOfTerraform/variables.tf @@ -0,0 +1,7 @@ +variable "schema_name" { + type = string +} + +variable "database_name" { + type = string +}