diff --git a/.github/ISSUE_TEMPLATE/01-bug.yml b/.github/ISSUE_TEMPLATE/01-bug.yml index cff3e79c65..3fe9c7b893 100644 --- a/.github/ISSUE_TEMPLATE/01-bug.yml +++ b/.github/ISSUE_TEMPLATE/01-bug.yml @@ -108,6 +108,7 @@ body: - resource:database_role - resource:dynamic_table - resource:email_notification_integration + - resource:execute - resource:external_function - resource:external_oauth_integration - resource:external_table diff --git a/.github/ISSUE_TEMPLATE/02-general-usage.yml b/.github/ISSUE_TEMPLATE/02-general-usage.yml index 1ddd1d306f..9591da908c 100644 --- a/.github/ISSUE_TEMPLATE/02-general-usage.yml +++ b/.github/ISSUE_TEMPLATE/02-general-usage.yml @@ -106,6 +106,7 @@ body: - resource:database_role - resource:dynamic_table - resource:email_notification_integration + - resource:execute - resource:external_function - resource:external_oauth_integration - resource:external_table diff --git a/.github/ISSUE_TEMPLATE/03-documentation.yml b/.github/ISSUE_TEMPLATE/03-documentation.yml index d73349e095..05fd7110e6 100644 --- a/.github/ISSUE_TEMPLATE/03-documentation.yml +++ b/.github/ISSUE_TEMPLATE/03-documentation.yml @@ -40,6 +40,7 @@ body: - resource:database_role - resource:dynamic_table - resource:email_notification_integration + - resource:execute - resource:external_function - resource:external_oauth_integration - resource:external_table diff --git a/.github/ISSUE_TEMPLATE/04-feature-request.yml b/.github/ISSUE_TEMPLATE/04-feature-request.yml index b653f251ae..c66556b86f 100644 --- a/.github/ISSUE_TEMPLATE/04-feature-request.yml +++ b/.github/ISSUE_TEMPLATE/04-feature-request.yml @@ -76,6 +76,7 @@ body: - resource:database_role - resource:dynamic_table - resource:email_notification_integration + - resource:execute - resource:external_function - resource:external_oauth_integration - resource:external_table diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 2fe0dd5647..9904e232e5 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -17,6 +17,16 @@ During resource deleting, provider now uses `UNSET` instead of `SET` with the de #### *(behavior change)* changes in `key` field The value of `key` field is now case-insensitive and is validated. The list of supported values is available in the resource documentation. +### unsafe_execute resource deprecation / new execute resource + +The `snowflake_unsafe_execute` gets deprecated in favor of the new resource `snowflake_execute`. +The `snowflake_execute` was build on top of `snowflake_unsafe_execute` with a few improvements. +The unsafe version will be removed with the v1 release, so please migrate to the `snowflake_execute` resource. + +For no downtime migration, follow our [guide](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/docs/technical-documentation/resource_migration.md). +When importing, remember that the given resource id has to be unique (using UUIDs is recommended). +Also, because of the nature of the resource, first apply after importing is necessary to "copy" values from the configuration to the state. + ### snowflake_oauth_integration_for_partner_applications and snowflake_oauth_integration_for_custom_clients resource changes #### *(behavior change)* `blocked_roles_list` field is no longer required diff --git a/docs/index.md b/docs/index.md index 9a69fadbe9..f04b4eb2b5 100644 --- a/docs/index.md +++ b/docs/index.md @@ -367,6 +367,7 @@ provider "snowflake" { - [snowflake_saml_integration](./docs/resources/saml_integration) - use [snowflake_saml2_integration](./docs/resources/saml2_integration) instead - [snowflake_stream](./docs/resources/stream) - [snowflake_tag_masking_policy_association](./docs/resources/tag_masking_policy_association) +- [snowflake_unsafe_execute](./docs/resources/unsafe_execute) - use [snowflake_execute](./docs/resources/execute) instead ## Currently deprecated datasources diff --git a/docs/resources/execute.md b/docs/resources/execute.md new file mode 100644 index 0000000000..6fbfbb993f --- /dev/null +++ b/docs/resources/execute.md @@ -0,0 +1,128 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "snowflake_execute Resource - terraform-provider-snowflake" +subcategory: "" +description: |- + Resource allowing execution of ANY SQL statement. +--- + +# snowflake_execute (Resource) + +!> **Warning** This is a dangerous resource that allows executing **ANY** SQL statement. It may destroy resources if used incorrectly. It may behave incorrectly combined with other resources. Use at your own risk. + +~> **Note** It can be theoretically used to manage resource that are not supported by the provider. This is risky and may brake other resources if used incorrectly. + +~> **Note** Use `query` parameter with caution. It will fetch **ALL** the results returned by the query provided. Try to limit the number of results by writing query with filters. Query failure does not stop resource creation; it simply results in `query_results` being empty. + +Resource allowing execution of ANY SQL statement. + +## Example Usage + +```terraform +################################## +### simple use cases +################################## + +# create and destroy resource +resource "snowflake_execute" "test" { + execute = "CREATE DATABASE ABC" + revert = "DROP DATABASE ABC" +} + +# create and destroy resource using qualified name +resource "snowflake_execute" "test" { + execute = "CREATE DATABASE \"abc\"" + revert = "DROP DATABASE \"abc\"" +} + +# with query +resource "snowflake_execute" "test" { + execute = "CREATE DATABASE ABC" + revert = "DROP DATABASE ABC" + query = "SHOW DATABASES LIKE '%ABC%'" +} + +################################## +### grants example +################################## + +# grant and revoke privilege USAGE to ROLE on database +resource "snowflake_execute" "test" { + execute = "GRANT USAGE ON DATABASE ABC TO ROLE XYZ" + revert = "REVOKE USAGE ON DATABASE ABC FROM ROLE XYZ" +} + +# grant and revoke with for_each +variable "database_grants" { + type = list(object({ + database_name = string + role_id = string + privileges = list(string) + })) +} + +resource "snowflake_execute" "test" { + for_each = { for index, db_grant in var.database_grants : index => db_grant } + execute = "GRANT ${join(",", each.value.privileges)} ON DATABASE ${each.value.database_name} TO ROLE ${each.value.role_id}" + revert = "REVOKE ${join(",", each.value.privileges)} ON DATABASE ${each.value.database_name} FROM ROLE ${each.value.role_id}" +} + +################################## +### fixing bad configuration +################################## + +# bad revert +# 1 - resource created with a bad revert; it is constructed, revert is not validated before destroy happens +resource "snowflake_execute" "test" { + execute = "CREATE DATABASE ABC" + revert = "SELECT 1" +} + +# 2 - fix the revert first; resource won't be recreated +resource "snowflake_execute" "test" { + execute = "CREATE DATABASE ABC" + revert = "DROP DATABASE ABC" +} + +# bad query +# 1 - resource will be created; query_results will be empty +resource "snowflake_execute" "test" { + execute = "CREATE DATABASE ABC" + revert = "DROP DATABASE ABC" + query = "bad query" +} + +# 2 - fix the query; query_results will be calculated; resource won't be recreated +resource "snowflake_execute" "test" { + execute = "CREATE DATABASE ABC" + revert = "DROP DATABASE ABC" + query = "SHOW DATABASES LIKE '%ABC%'" +} +``` +-> **Note** Instead of using fully_qualified_name, you can reference objects managed outside Terraform by constructing a correct ID, consult [identifiers guide](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/guides/identifiers#new-computed-fully-qualified-name-field-in-resources). + + + +## Schema + +### Required + +- `execute` (String) SQL statement to execute. Forces recreation of resource on change. +- `revert` (String) SQL statement to revert the execute statement. Invoked when resource is being destroyed. + +### Optional + +- `query` (String) Optional SQL statement to do a read. Invoked on every resource refresh and every time it is changed. + +### Read-Only + +- `id` (String) The ID of this resource. +- `query_results` (List of Map of String) List of key-value maps (text to text) retrieved after executing read query. Will be empty if the query results in an error. + +## Import + +Import is supported using the following syntax: + +```shell +terraform import snowflake_execute.example '' +``` diff --git a/docs/resources/unsafe_execute.md b/docs/resources/unsafe_execute.md index 358daaa521..3ac9e7c7b6 100644 --- a/docs/resources/unsafe_execute.md +++ b/docs/resources/unsafe_execute.md @@ -10,12 +10,12 @@ description: |- !> **Warning** This is a dangerous resource that allows executing **ANY** SQL statement. It may destroy resources if used incorrectly. It may behave incorrectly combined with other resources. Use at your own risk. -~> **Note** This resource will be included in the V1 (check [here](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/v1-preparations/ESSENTIAL_GA_OBJECTS.MD)) but may be slightly modified before. Design decisions and changes will be listed in the [migration guide](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/MIGRATION_GUIDE.md#migration-guide). - ~> **Note** It can be theoretically used to manage resource that are not supported by the provider. This is risky and may brake other resources if used incorrectly. ~> **Note** Use `query` parameter with caution. It will fetch **ALL** the results returned by the query provided. Try to limit the number of results by writing query with filters. Query failure does not stop resource creation; it simply results in `query_results` being empty. +~> **Deprecation** This resource is deprecated and will be removed in a future major version release. Please use [snowflake_execute](./execute) instead. + Experimental resource allowing execution of ANY SQL statement. It may destroy resources if used incorrectly. It may behave incorrectly combined with other resources. Use at your own risk. ## Example Usage @@ -139,7 +139,7 @@ resource "snowflake_unsafe_execute" "test" { ### Optional -- `query` (String) Optional SQL statement to do a read. Invoked after creation and every time it is changed. +- `query` (String) Optional SQL statement to do a read. Invoked on every resource refresh and every time it is changed. ### Read-Only diff --git a/examples/additional/deprecated_resources.MD b/examples/additional/deprecated_resources.MD index 557f4af44a..b0a8941854 100644 --- a/examples/additional/deprecated_resources.MD +++ b/examples/additional/deprecated_resources.MD @@ -6,3 +6,4 @@ - [snowflake_saml_integration](./docs/resources/saml_integration) - use [snowflake_saml2_integration](./docs/resources/saml2_integration) instead - [snowflake_stream](./docs/resources/stream) - [snowflake_tag_masking_policy_association](./docs/resources/tag_masking_policy_association) +- [snowflake_unsafe_execute](./docs/resources/unsafe_execute) - use [snowflake_execute](./docs/resources/execute) instead diff --git a/examples/resources/snowflake_execute/import.sh b/examples/resources/snowflake_execute/import.sh new file mode 100644 index 0000000000..65c1257965 --- /dev/null +++ b/examples/resources/snowflake_execute/import.sh @@ -0,0 +1 @@ +terraform import snowflake_execute.example '' diff --git a/examples/resources/snowflake_execute/resource.tf b/examples/resources/snowflake_execute/resource.tf new file mode 100644 index 0000000000..bb6cde5616 --- /dev/null +++ b/examples/resources/snowflake_execute/resource.tf @@ -0,0 +1,79 @@ +################################## +### simple use cases +################################## + +# create and destroy resource +resource "snowflake_execute" "test" { + execute = "CREATE DATABASE ABC" + revert = "DROP DATABASE ABC" +} + +# create and destroy resource using qualified name +resource "snowflake_execute" "test" { + execute = "CREATE DATABASE \"abc\"" + revert = "DROP DATABASE \"abc\"" +} + +# with query +resource "snowflake_execute" "test" { + execute = "CREATE DATABASE ABC" + revert = "DROP DATABASE ABC" + query = "SHOW DATABASES LIKE '%ABC%'" +} + +################################## +### grants example +################################## + +# grant and revoke privilege USAGE to ROLE on database +resource "snowflake_execute" "test" { + execute = "GRANT USAGE ON DATABASE ABC TO ROLE XYZ" + revert = "REVOKE USAGE ON DATABASE ABC FROM ROLE XYZ" +} + +# grant and revoke with for_each +variable "database_grants" { + type = list(object({ + database_name = string + role_id = string + privileges = list(string) + })) +} + +resource "snowflake_execute" "test" { + for_each = { for index, db_grant in var.database_grants : index => db_grant } + execute = "GRANT ${join(",", each.value.privileges)} ON DATABASE ${each.value.database_name} TO ROLE ${each.value.role_id}" + revert = "REVOKE ${join(",", each.value.privileges)} ON DATABASE ${each.value.database_name} FROM ROLE ${each.value.role_id}" +} + +################################## +### fixing bad configuration +################################## + +# bad revert +# 1 - resource created with a bad revert; it is constructed, revert is not validated before destroy happens +resource "snowflake_execute" "test" { + execute = "CREATE DATABASE ABC" + revert = "SELECT 1" +} + +# 2 - fix the revert first; resource won't be recreated +resource "snowflake_execute" "test" { + execute = "CREATE DATABASE ABC" + revert = "DROP DATABASE ABC" +} + +# bad query +# 1 - resource will be created; query_results will be empty +resource "snowflake_execute" "test" { + execute = "CREATE DATABASE ABC" + revert = "DROP DATABASE ABC" + query = "bad query" +} + +# 2 - fix the query; query_results will be calculated; resource won't be recreated +resource "snowflake_execute" "test" { + execute = "CREATE DATABASE ABC" + revert = "DROP DATABASE ABC" + query = "SHOW DATABASES LIKE '%ABC%'" +} diff --git a/pkg/provider/provider.go b/pkg/provider/provider.go index 37e5316e39..327c4ceafb 100644 --- a/pkg/provider/provider.go +++ b/pkg/provider/provider.go @@ -487,6 +487,7 @@ func getResources() map[string]*schema.Resource { "snowflake_database_role": resources.DatabaseRole(), "snowflake_dynamic_table": resources.DynamicTable(), "snowflake_email_notification_integration": resources.EmailNotificationIntegration(), + "snowflake_execute": resources.Execute(), "snowflake_external_function": resources.ExternalFunction(), "snowflake_external_oauth_integration": resources.ExternalOauthIntegration(), "snowflake_external_table": resources.ExternalTable(), diff --git a/pkg/provider/provider_acceptance_test.go b/pkg/provider/provider_acceptance_test.go index 8d9d5b0666..6321957a17 100644 --- a/pkg/provider/provider_acceptance_test.go +++ b/pkg/provider/provider_acceptance_test.go @@ -623,10 +623,10 @@ func TestAcc_Provider_sessionParameters(t *testing.T) { "statement_timeout_in_seconds": tfconfig.IntegerVariable(31337), }, ), - )) + unsafeExecuteShowSessionParameter(), + )) + executeShowSessionParameter(), Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("snowflake_unsafe_execute.t", "query_results.#", "1"), - resource.TestCheckResourceAttr("snowflake_unsafe_execute.t", "query_results.0.value", "31337"), + resource.TestCheckResourceAttr("snowflake_execute.t", "query_results.#", "1"), + resource.TestCheckResourceAttr("snowflake_execute.t", "query_results.0.value", "31337"), ), }, }, @@ -803,9 +803,9 @@ func datasourceModel() config.DatasourceModel { return datasourcemodel.Database("t", acc.TestDatabaseName) } -func unsafeExecuteShowSessionParameter() string { +func executeShowSessionParameter() string { return ` -resource snowflake_unsafe_execute "t" { +resource snowflake_execute "t" { execute = "SELECT 1" query = "SHOW PARAMETERS LIKE 'STATEMENT_TIMEOUT_IN_SECONDS' IN SESSION" revert = "SELECT 1" diff --git a/pkg/provider/resources/resources.go b/pkg/provider/resources/resources.go index 8b43f012e7..4a576e79ed 100644 --- a/pkg/provider/resources/resources.go +++ b/pkg/provider/resources/resources.go @@ -20,6 +20,7 @@ const ( DatabaseRole resource = "snowflake_database_role" DynamicTable resource = "snowflake_dynamic_table" EmailNotificationIntegration resource = "snowflake_email_notification_integration" + Execute resource = "snowflake_execute" ExternalFunction resource = "snowflake_external_function" ExternalTable resource = "snowflake_external_table" ExternalOauthSecurityIntegration resource = "snowflake_external_oauth_security_integration" diff --git a/pkg/resources/execute.go b/pkg/resources/execute.go new file mode 100644 index 0000000000..6ad9b189cd --- /dev/null +++ b/pkg/resources/execute.go @@ -0,0 +1,172 @@ +package resources + +import ( + "context" + "fmt" + "log" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/provider/resources" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/provider" + + "github.com/hashicorp/go-uuid" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +var executeSchema = map[string]*schema.Schema{ + "execute": { + Type: schema.TypeString, + Required: true, + Description: "SQL statement to execute. Forces recreation of resource on change.", + }, + "revert": { + Type: schema.TypeString, + Required: true, + Description: "SQL statement to revert the execute statement. Invoked when resource is being destroyed.", + }, + "query": { + Type: schema.TypeString, + Optional: true, + Description: "Optional SQL statement to do a read. Invoked on every resource refresh and every time it is changed.", + }, + "query_results": { + Type: schema.TypeList, + Computed: true, + Description: "List of key-value maps (text to text) retrieved after executing read query. Will be empty if the query results in an error.", + Elem: &schema.Schema{ + Type: schema.TypeMap, + Elem: &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + }, + }, +} + +func Execute() *schema.Resource { + return &schema.Resource{ + CreateContext: TrackingCreateWrapper(resources.Execute, CreateExecute), + ReadContext: TrackingReadWrapper(resources.Execute, ReadExecute), + UpdateContext: TrackingUpdateWrapper(resources.Execute, UpdateExecute), + DeleteContext: TrackingDeleteWrapper(resources.Execute, DeleteExecute), + + Schema: executeSchema, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Description: "Resource allowing execution of ANY SQL statement.", + + CustomizeDiff: TrackingCustomDiffWrapper(resources.UnsafeExecute, customdiff.All( + customdiff.ForceNewIfChange("execute", func(ctx context.Context, oldValue, newValue, meta any) bool { + return oldValue != "" + }), + func(_ context.Context, diff *schema.ResourceDiff, _ any) error { + if diff.HasChange("query") { + err := diff.SetNewComputed("query_results") + if err != nil { + return err + } + } + return nil + }), + ), + } +} + +func CreateExecute(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*provider.Context).Client + + id, err := uuid.GenerateUUID() + if err != nil { + return diag.FromErr(err) + } + + executeStatement := d.Get("execute").(string) + _, err = client.ExecUnsafe(ctx, executeStatement) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(id) + log.Printf(`[INFO] SQL "%s" applied successfully\n`, executeStatement) + + return ReadExecute(ctx, d, meta) +} + +func UpdateExecute(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + if d.HasChange("query") { + return ReadExecute(ctx, d, meta) + } + return nil +} + +func ReadExecute(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*provider.Context).Client + + readStatement := d.Get("query").(string) + + setNilResults := func() diag.Diagnostics { + log.Printf(`[DEBUG] Clearing query_results`) + err := d.Set("query_results", nil) + if err != nil { + return diag.FromErr(err) + } + return nil + } + + if readStatement == "" { + return setNilResults() + } else { + rows, err := client.QueryUnsafe(ctx, readStatement) + if err != nil { + log.Printf(`[WARN] SQL query "%s" failed with err %v`, readStatement, err) + return setNilResults() + } + log.Printf(`[INFO] SQL query "%s" executed successfully, returned rows count: %d`, readStatement, len(rows)) + rowsTransformed := make([]map[string]any, len(rows)) + for i, row := range rows { + t := make(map[string]any) + for k, v := range row { + if *v == nil { + t[k] = nil + } else { + switch (*v).(type) { + case fmt.Stringer: + t[k] = fmt.Sprintf("%v", *v) + case string: + t[k] = *v + default: + return diag.FromErr(fmt.Errorf("currently only objects convertible to String are supported by query; got %v", *v)) + } + } + } + rowsTransformed[i] = t + } + err = d.Set("query_results", rowsTransformed) + if err != nil { + return diag.FromErr(err) + } + } + + return nil +} + +func DeleteExecute(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*provider.Context).Client + + revertStatement := d.Get("revert").(string) + _, err := client.ExecUnsafe(ctx, revertStatement) + if err != nil { + return diag.FromErr(err) + } + + d.SetId("") + log.Printf(`[INFO] SQL "%s" applied successfully\n`, revertStatement) + + return nil +} diff --git a/pkg/resources/unsafe_execute_acceptance_test.go b/pkg/resources/execute_acceptance_test.go similarity index 78% rename from pkg/resources/unsafe_execute_acceptance_test.go rename to pkg/resources/execute_acceptance_test.go index 33a78be8b0..d97722701f 100644 --- a/pkg/resources/unsafe_execute_acceptance_test.go +++ b/pkg/resources/execute_acceptance_test.go @@ -1,12 +1,15 @@ package resources_test import ( + "context" "errors" "fmt" "regexp" "strings" "testing" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/resources" + acc "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/testenvs" @@ -20,17 +23,17 @@ import ( "github.com/stretchr/testify/require" ) -func TestAcc_UnsafeExecute_basic(t *testing.T) { - id := acc.TestClient().Ids.RandomAccountObjectIdentifierWithPrefix("UNSAFE_EXECUTE_TEST_DATABASE_") +func TestAcc_Execute_basic(t *testing.T) { + id := acc.TestClient().Ids.RandomAccountObjectIdentifierWithPrefix("EXECUTE_TEST_DATABASE_") name := id.Name() - secondId := acc.TestClient().Ids.RandomAccountObjectIdentifierWithPrefix("UNSAFE_EXECUTE_TEST_DATABASE_") + secondId := acc.TestClient().Ids.RandomAccountObjectIdentifierWithPrefix("EXECUTE_TEST_DATABASE_") nameLowerCase := strings.ToLower(secondId.Name()) secondIdLowerCased := sdk.NewAccountObjectIdentifier(nameLowerCase) nameLowerCaseEscaped := fmt.Sprintf(`"%s"`, nameLowerCase) createDatabaseStatement := func(id string) string { return fmt.Sprintf("create database %s", id) } dropDatabaseStatement := func(id string) string { return fmt.Sprintf("drop database %s", id) } - resourceName := "snowflake_unsafe_execute.test" + resourceName := "snowflake_execute.test" createConfigVariables := func(id string) map[string]config.Variable { return map[string]config.Variable{ "execute": config.StringVariable(createDatabaseStatement(id)), @@ -47,7 +50,7 @@ func TestAcc_UnsafeExecute_basic(t *testing.T) { CheckDestroy: testAccCheckDatabaseExistence(t, id, false), Steps: []resource.TestStep{ { - ConfigDirectory: acc.ConfigurationDirectory("TestAcc_UnsafeExecute_commonSetup"), + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_Execute_commonSetup"), ConfigVariables: createConfigVariables(name), ConfigPlanChecks: resource.ConfigPlanChecks{ PreApply: []plancheck.PlanCheck{plancheck.ExpectNonEmptyPlan()}, @@ -73,7 +76,7 @@ func TestAcc_UnsafeExecute_basic(t *testing.T) { CheckDestroy: testAccCheckDatabaseExistence(t, secondIdLowerCased, false), Steps: []resource.TestStep{ { - ConfigDirectory: acc.ConfigurationDirectory("TestAcc_UnsafeExecute_commonSetup"), + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_Execute_commonSetup"), ConfigVariables: createConfigVariables(nameLowerCaseEscaped), ConfigPlanChecks: resource.ConfigPlanChecks{ PreApply: []plancheck.PlanCheck{plancheck.ExpectNonEmptyPlan()}, @@ -91,14 +94,14 @@ func TestAcc_UnsafeExecute_basic(t *testing.T) { }) } -func TestAcc_UnsafeExecute_withRead(t *testing.T) { - id := acc.TestClient().Ids.RandomAccountObjectIdentifierWithPrefix("UNSAFE_EXECUTE_TEST_DATABASE_") +func TestAcc_Execute_withRead(t *testing.T) { + id := acc.TestClient().Ids.RandomAccountObjectIdentifierWithPrefix("EXECUTE_TEST_DATABASE_") name := id.Name() createDatabaseStatement := func(id string) string { return fmt.Sprintf("create database %s", id) } dropDatabaseStatement := func(id string) string { return fmt.Sprintf("drop database %s", id) } showDatabaseStatement := func(id string) string { return fmt.Sprintf("show databases like '%%%s%%'", id) } - resourceName := "snowflake_unsafe_execute.test" + resourceName := "snowflake_execute.test" createConfigVariables := func(id string) map[string]config.Variable { return map[string]config.Variable{ "execute": config.StringVariable(createDatabaseStatement(id)), @@ -116,7 +119,7 @@ func TestAcc_UnsafeExecute_withRead(t *testing.T) { CheckDestroy: testAccCheckDatabaseExistence(t, id, false), Steps: []resource.TestStep{ { - ConfigDirectory: acc.ConfigurationDirectory("TestAcc_UnsafeExecute_withRead"), + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_Execute_withRead"), ConfigVariables: createConfigVariables(name), ConfigPlanChecks: resource.ConfigPlanChecks{ PreApply: []plancheck.PlanCheck{plancheck.ExpectNonEmptyPlan()}, @@ -138,13 +141,13 @@ func TestAcc_UnsafeExecute_withRead(t *testing.T) { }) } -func TestAcc_UnsafeExecute_readRemoved(t *testing.T) { - id := acc.TestClient().Ids.RandomAccountObjectIdentifierWithPrefix("UNSAFE_EXECUTE_TEST_DATABASE_") +func TestAcc_Execute_readRemoved(t *testing.T) { + id := acc.TestClient().Ids.RandomAccountObjectIdentifierWithPrefix("EXECUTE_TEST_DATABASE_") name := id.Name() createDatabaseStatement := func(id string) string { return fmt.Sprintf("create database %s", id) } dropDatabaseStatement := func(id string) string { return fmt.Sprintf("drop database %s", id) } showDatabaseStatement := func(id string) string { return fmt.Sprintf("show databases like '%%%s%%'", id) } - resourceName := "snowflake_unsafe_execute.test" + resourceName := "snowflake_execute.test" resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, @@ -155,7 +158,7 @@ func TestAcc_UnsafeExecute_readRemoved(t *testing.T) { CheckDestroy: testAccCheckDatabaseExistence(t, id, false), Steps: []resource.TestStep{ { - ConfigDirectory: acc.ConfigurationDirectory("TestAcc_UnsafeExecute_withRead"), + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_Execute_withRead"), ConfigVariables: map[string]config.Variable{ "execute": config.StringVariable(createDatabaseStatement(name)), "revert": config.StringVariable(dropDatabaseStatement(name)), @@ -170,7 +173,7 @@ func TestAcc_UnsafeExecute_readRemoved(t *testing.T) { ), }, { - ConfigDirectory: acc.ConfigurationDirectory("TestAcc_UnsafeExecute_withRead"), + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_Execute_withRead"), ConfigVariables: map[string]config.Variable{ "execute": config.StringVariable(createDatabaseStatement(name)), "revert": config.StringVariable(dropDatabaseStatement(name)), @@ -188,13 +191,13 @@ func TestAcc_UnsafeExecute_readRemoved(t *testing.T) { }) } -func TestAcc_UnsafeExecute_badQuery(t *testing.T) { - id := acc.TestClient().Ids.RandomAccountObjectIdentifierWithPrefix("UNSAFE_EXECUTE_TEST_DATABASE_") +func TestAcc_Execute_badQuery(t *testing.T) { + id := acc.TestClient().Ids.RandomAccountObjectIdentifierWithPrefix("EXECUTE_TEST_DATABASE_") name := id.Name() createDatabaseStatement := func(id string) string { return fmt.Sprintf("create database %s", id) } dropDatabaseStatement := func(id string) string { return fmt.Sprintf("drop database %s", id) } showDatabaseStatement := func(id string) string { return fmt.Sprintf("show databases like '%%%s%%'", id) } - resourceName := "snowflake_unsafe_execute.test" + resourceName := "snowflake_execute.test" resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, @@ -205,7 +208,7 @@ func TestAcc_UnsafeExecute_badQuery(t *testing.T) { CheckDestroy: testAccCheckDatabaseExistence(t, id, false), Steps: []resource.TestStep{ { - ConfigDirectory: acc.ConfigurationDirectory("TestAcc_UnsafeExecute_withRead"), + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_Execute_withRead"), ConfigVariables: map[string]config.Variable{ "execute": config.StringVariable(createDatabaseStatement(name)), "revert": config.StringVariable(dropDatabaseStatement(name)), @@ -223,7 +226,7 @@ func TestAcc_UnsafeExecute_badQuery(t *testing.T) { ), }, { - ConfigDirectory: acc.ConfigurationDirectory("TestAcc_UnsafeExecute_withRead"), + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_Execute_withRead"), ConfigVariables: map[string]config.Variable{ "execute": config.StringVariable(createDatabaseStatement(name)), "revert": config.StringVariable(dropDatabaseStatement(name)), @@ -243,7 +246,7 @@ func TestAcc_UnsafeExecute_badQuery(t *testing.T) { }) } -func TestAcc_UnsafeExecute_invalidExecuteStatement(t *testing.T) { +func TestAcc_Execute_invalidExecuteStatement(t *testing.T) { invalidCreateStatement := "create database" invalidDropStatement := "drop database" @@ -262,7 +265,7 @@ func TestAcc_UnsafeExecute_invalidExecuteStatement(t *testing.T) { }, Steps: []resource.TestStep{ { - ConfigDirectory: acc.ConfigurationDirectory("TestAcc_UnsafeExecute_commonSetup"), + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_Execute_commonSetup"), ConfigVariables: createConfigVariables(), ConfigPlanChecks: resource.ConfigPlanChecks{ PreApply: []plancheck.PlanCheck{plancheck.ExpectNonEmptyPlan()}, @@ -273,16 +276,16 @@ func TestAcc_UnsafeExecute_invalidExecuteStatement(t *testing.T) { }) } -func TestAcc_UnsafeExecute_invalidRevertStatement(t *testing.T) { - id := acc.TestClient().Ids.RandomAccountObjectIdentifierWithPrefix("UNSAFE_EXECUTE_TEST_DATABASE_") +func TestAcc_Execute_invalidRevertStatement(t *testing.T) { + id := acc.TestClient().Ids.RandomAccountObjectIdentifierWithPrefix("EXECUTE_TEST_DATABASE_") name := id.Name() - updatedId := acc.TestClient().Ids.RandomAccountObjectIdentifierWithPrefix("UNSAFE_EXECUTE_TEST_DATABASE_") + updatedId := acc.TestClient().Ids.RandomAccountObjectIdentifierWithPrefix("EXECUTE_TEST_DATABASE_") updatedName := updatedId.Name() createDatabaseStatement := func(id string) string { return fmt.Sprintf("create database %s", id) } dropDatabaseStatement := func(id string) string { return fmt.Sprintf("drop database %s", id) } invalidDropStatement := "drop database" - resourceName := "snowflake_unsafe_execute.test" + resourceName := "snowflake_execute.test" resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, @@ -303,7 +306,7 @@ func TestAcc_UnsafeExecute_invalidRevertStatement(t *testing.T) { }, Steps: []resource.TestStep{ { - ConfigDirectory: acc.ConfigurationDirectory("TestAcc_UnsafeExecute_commonSetup"), + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_Execute_commonSetup"), ConfigVariables: map[string]config.Variable{ "execute": config.StringVariable(createDatabaseStatement(name)), "revert": config.StringVariable(invalidDropStatement), @@ -319,7 +322,7 @@ func TestAcc_UnsafeExecute_invalidRevertStatement(t *testing.T) { ), }, { - ConfigDirectory: acc.ConfigurationDirectory("TestAcc_UnsafeExecute_commonSetup"), + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_Execute_commonSetup"), ConfigVariables: map[string]config.Variable{ "execute": config.StringVariable(createDatabaseStatement(updatedName)), "revert": config.StringVariable(invalidDropStatement), @@ -330,7 +333,7 @@ func TestAcc_UnsafeExecute_invalidRevertStatement(t *testing.T) { ExpectError: regexp.MustCompile("SQL compilation error"), }, { - ConfigDirectory: acc.ConfigurationDirectory("TestAcc_UnsafeExecute_commonSetup"), + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_Execute_commonSetup"), ConfigVariables: map[string]config.Variable{ "execute": config.StringVariable(createDatabaseStatement(name)), "revert": config.StringVariable(dropDatabaseStatement(name)), @@ -347,7 +350,7 @@ func TestAcc_UnsafeExecute_invalidRevertStatement(t *testing.T) { ), }, { - ConfigDirectory: acc.ConfigurationDirectory("TestAcc_UnsafeExecute_commonSetup"), + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_Execute_commonSetup"), ConfigVariables: map[string]config.Variable{ "execute": config.StringVariable(createDatabaseStatement(updatedName)), "revert": config.StringVariable(dropDatabaseStatement(updatedName)), @@ -367,15 +370,15 @@ func TestAcc_UnsafeExecute_invalidRevertStatement(t *testing.T) { }) } -func TestAcc_UnsafeExecute_revertUpdated(t *testing.T) { - id := acc.TestClient().Ids.RandomAccountObjectIdentifierWithPrefix("UNSAFE_EXECUTE_TEST_DATABASE_") +func TestAcc_Execute_revertUpdated(t *testing.T) { + id := acc.TestClient().Ids.RandomAccountObjectIdentifierWithPrefix("EXECUTE_TEST_DATABASE_") name := id.Name() execute := fmt.Sprintf("create database %s", name) revert := fmt.Sprintf("drop database %s", name) notMatchingRevert := "select 1" var savedId string - resourceName := "snowflake_unsafe_execute.test" + resourceName := "snowflake_execute.test" createConfigVariables := func(execute string, revert string) map[string]config.Variable { return map[string]config.Variable{ "execute": config.StringVariable(execute), @@ -392,7 +395,7 @@ func TestAcc_UnsafeExecute_revertUpdated(t *testing.T) { CheckDestroy: testAccCheckDatabaseExistence(t, id, false), Steps: []resource.TestStep{ { - ConfigDirectory: acc.ConfigurationDirectory("TestAcc_UnsafeExecute_commonSetup"), + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_Execute_commonSetup"), ConfigVariables: createConfigVariables(execute, notMatchingRevert), ConfigPlanChecks: resource.ConfigPlanChecks{ PreApply: []plancheck.PlanCheck{plancheck.ExpectNonEmptyPlan()}, @@ -409,7 +412,7 @@ func TestAcc_UnsafeExecute_revertUpdated(t *testing.T) { ), }, { - ConfigDirectory: acc.ConfigurationDirectory("TestAcc_UnsafeExecute_commonSetup"), + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_Execute_commonSetup"), ConfigVariables: createConfigVariables(execute, revert), ConfigPlanChecks: resource.ConfigPlanChecks{ PreApply: []plancheck.PlanCheck{plancheck.ExpectNonEmptyPlan()}, @@ -431,20 +434,20 @@ func TestAcc_UnsafeExecute_revertUpdated(t *testing.T) { }) } -func TestAcc_UnsafeExecute_executeUpdated(t *testing.T) { - id := acc.TestClient().Ids.RandomAccountObjectIdentifierWithPrefix("UNSAFE_EXECUTE_TEST_DATABASE_") +func TestAcc_Execute_executeUpdated(t *testing.T) { + id := acc.TestClient().Ids.RandomAccountObjectIdentifierWithPrefix("EXECUTE_TEST_DATABASE_") name := id.Name() execute := fmt.Sprintf("create database %s", name) revert := fmt.Sprintf("drop database %s", name) - newId := acc.TestClient().Ids.RandomAccountObjectIdentifierWithPrefix("UNSAFE_EXECUTE_TEST_DATABASE_") + newId := acc.TestClient().Ids.RandomAccountObjectIdentifierWithPrefix("EXECUTE_TEST_DATABASE_") newName := newId.Name() newExecute := fmt.Sprintf("create database %s", newName) newRevert := fmt.Sprintf("drop database %s", newName) var savedId string - resourceName := "snowflake_unsafe_execute.test" + resourceName := "snowflake_execute.test" createConfigVariables := func(execute string, revert string) map[string]config.Variable { return map[string]config.Variable{ "execute": config.StringVariable(execute), @@ -471,7 +474,7 @@ func TestAcc_UnsafeExecute_executeUpdated(t *testing.T) { }, Steps: []resource.TestStep{ { - ConfigDirectory: acc.ConfigurationDirectory("TestAcc_UnsafeExecute_commonSetup"), + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_Execute_commonSetup"), ConfigVariables: createConfigVariables(execute, revert), ConfigPlanChecks: resource.ConfigPlanChecks{ PreApply: []plancheck.PlanCheck{plancheck.ExpectNonEmptyPlan()}, @@ -488,7 +491,7 @@ func TestAcc_UnsafeExecute_executeUpdated(t *testing.T) { ), }, { - ConfigDirectory: acc.ConfigurationDirectory("TestAcc_UnsafeExecute_commonSetup"), + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_Execute_commonSetup"), ConfigVariables: createConfigVariables(newExecute, newRevert), ConfigPlanChecks: resource.ConfigPlanChecks{ PreApply: []plancheck.PlanCheck{plancheck.ExpectNonEmptyPlan()}, @@ -511,7 +514,7 @@ func TestAcc_UnsafeExecute_executeUpdated(t *testing.T) { }) } -func TestAcc_UnsafeExecute_grants(t *testing.T) { +func TestAcc_Execute_grants(t *testing.T) { _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) acc.TestAccPreCheck(t) @@ -527,7 +530,7 @@ func TestAcc_UnsafeExecute_grants(t *testing.T) { execute := fmt.Sprintf("GRANT %s ON DATABASE %s TO ROLE %s", privilege, database.ID().FullyQualifiedName(), role.ID().FullyQualifiedName()) revert := fmt.Sprintf("REVOKE %s ON DATABASE %s FROM ROLE %s", privilege, database.ID().FullyQualifiedName(), role.ID().FullyQualifiedName()) - resourceName := "snowflake_unsafe_execute.test" + resourceName := "snowflake_execute.test" createConfigVariables := func(execute string, revert string) map[string]config.Variable { return map[string]config.Variable{ "execute": config.StringVariable(execute), @@ -547,7 +550,7 @@ func TestAcc_UnsafeExecute_grants(t *testing.T) { }, Steps: []resource.TestStep{ { - ConfigDirectory: acc.ConfigurationDirectory("TestAcc_UnsafeExecute_commonSetup"), + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_Execute_commonSetup"), ConfigVariables: createConfigVariables(execute, revert), ConfigPlanChecks: resource.ConfigPlanChecks{ PreApply: []plancheck.PlanCheck{plancheck.ExpectNonEmptyPlan()}, @@ -563,19 +566,19 @@ func TestAcc_UnsafeExecute_grants(t *testing.T) { }) } -// TestAcc_UnsafeExecute_grantsComplex test fails with: +// TestAcc_Execute_grantsComplex test fails with: // -// testing_new_config.go:156: unexpected index type (string) for "snowflake_unsafe_execute.test[\"0\"]", for_each is not supported -// testing_new.go:68: unexpected index type (string) for "snowflake_unsafe_execute.test[\"0\"]", for_each is not supported +// testing_new_config.go:156: unexpected index type (string) for "snowflake_execute.test[\"0\"]", for_each is not supported +// testing_new.go:68: unexpected index type (string) for "snowflake_execute.test[\"0\"]", for_each is not supported // // Quick search unveiled this issue: https://github.com/hashicorp/terraform-plugin-sdk/issues/536. // // It also seems that it is working correctly underneath; with TF_LOG set to DEBUG we have: // -// 2023/11/26 17:16:03 [DEBUG] SQL "GRANT CREATE SCHEMA,MODIFY ON DATABASE UNSAFE_EXECUTE_TEST_DATABASE_4397 TO ROLE UNSAFE_EXECUTE_TEST_ROLE_1145" applied successfully -// 2023/11/26 17:16:03 [DEBUG] SQL "GRANT MODIFY,USAGE ON DATABASE UNSAFE_EXECUTE_TEST_DATABASE_3740 TO ROLE UNSAFE_EXECUTE_TEST_ROLE_3008" applied successfully -func TestAcc_UnsafeExecute_grantsComplex(t *testing.T) { - t.Skip("Skipping TestAcc_UnsafeExecute_grantsComplex because of https://github.com/hashicorp/terraform-plugin-sdk/issues/536 issue") +// 2023/11/26 17:16:03 [DEBUG] SQL "GRANT CREATE SCHEMA,MODIFY ON DATABASE EXECUTE_TEST_DATABASE_4397 TO ROLE EXECUTE_TEST_ROLE_1145" applied successfully +// 2023/11/26 17:16:03 [DEBUG] SQL "GRANT MODIFY,USAGE ON DATABASE EXECUTE_TEST_DATABASE_3740 TO ROLE EXECUTE_TEST_ROLE_3008" applied successfully +func TestAcc_Execute_grantsComplex(t *testing.T) { + t.Skip("Skipping TestAcc_Execute_grantsComplex because of https://github.com/hashicorp/terraform-plugin-sdk/issues/536 issue") client := acc.TestClient() @@ -599,8 +602,8 @@ func TestAcc_UnsafeExecute_grantsComplex(t *testing.T) { privilege2 := sdk.AccountObjectPrivilegeModify privilege3 := sdk.AccountObjectPrivilegeUsage - // resourceName1 := "snowflake_unsafe_execute.test.0" - // resourceName2 := "snowflake_unsafe_execute.test.1" + // resourceName1 := "snowflake_execute.test.0" + // resourceName2 := "snowflake_execute.test.1" createConfigVariables := func() map[string]config.Variable { return map[string]config.Variable{ "database_grants": config.ListVariable(config.ObjectVariable(map[string]config.Variable{ @@ -671,8 +674,8 @@ func TestAcc_UnsafeExecute_grantsComplex(t *testing.T) { } // proves https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2491 -func TestAcc_UnsafeExecute_queryResultsBug(t *testing.T) { - resourceName := "snowflake_unsafe_execute.test" +func TestAcc_Execute_queryResultsBug(t *testing.T) { + resourceName := "snowflake_execute.test" resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, @@ -682,7 +685,7 @@ func TestAcc_UnsafeExecute_queryResultsBug(t *testing.T) { }, Steps: []resource.TestStep{ { - Config: unsafeExecuteConfig(108), + Config: executeConfig(108), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(resourceName, "query", "SELECT 108"), resource.TestCheckResourceAttrSet(resourceName, "query_results.#"), @@ -690,7 +693,7 @@ func TestAcc_UnsafeExecute_queryResultsBug(t *testing.T) { ), }, { - Config: unsafeExecuteConfig(96), + Config: executeConfig(96), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(resourceName, "query", "SELECT 96"), resource.TestCheckResourceAttrSet(resourceName, "query_results.#"), @@ -704,20 +707,68 @@ func TestAcc_UnsafeExecute_queryResultsBug(t *testing.T) { }) } -func unsafeExecuteConfig(queryNumber int) string { +func executeConfig(queryNumber int) string { return fmt.Sprintf(` -resource "snowflake_unsafe_execute" "test" { +resource "snowflake_execute" "test" { execute = "SELECT 18" revert = "SELECT 36" query = "SELECT %d" } -output "unsafe" { - value = snowflake_unsafe_execute.test.query_results +output "query_results_output" { + value = snowflake_execute.test.query_results } `, queryNumber) } +func TestAcc_Execute_QueryResultsRecomputedWithoutQueryChanges(t *testing.T) { + resourceName := "snowflake_execute.test" + id := acc.TestClient().Ids.RandomAccountObjectIdentifier() + + 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{ + { + Config: executeConfigCreateDatabase(id), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "query_results.#", "1"), + resource.TestCheckResourceAttr(resourceName, "query_results.0.name", id.Name()), + resource.TestCheckResourceAttr(resourceName, "query_results.0.comment", ""), + ), + }, + { + PreConfig: func() { + acc.TestClient().Database.Alter(t, id, &sdk.AlterDatabaseOptions{ + Set: &sdk.DatabaseSet{ + Comment: sdk.String("some comment"), + }, + }) + }, + Config: executeConfigCreateDatabase(id), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "query_results.#", "1"), + resource.TestCheckResourceAttr(resourceName, "query_results.0.name", id.Name()), + resource.TestCheckResourceAttr(resourceName, "query_results.0.comment", "some comment"), + ), + }, + }, + }) +} + +func executeConfigCreateDatabase(id sdk.AccountObjectIdentifier) string { + return fmt.Sprintf(` +resource "snowflake_execute" "test" { + execute = "CREATE DATABASE \"%[1]s\"" + revert = "DROP DATABASE \"%[1]s\"" + query = "SHOW DATABASES LIKE '%[1]s'" +} +`, id.Name()) +} + func verifyGrantExists(t *testing.T, roleId sdk.AccountObjectIdentifier, privilege sdk.AccountObjectPrivilege, shouldExist bool) func(state *terraform.State) error { t.Helper() return func(state *terraform.State) error { @@ -740,3 +791,59 @@ func verifyGrantExists(t *testing.T, roleId sdk.AccountObjectIdentifier, privile return nil } } + +func TestAcc_Execute_ImportWithRandomId(t *testing.T) { + id := acc.TestClient().Ids.RandomAccountObjectIdentifier() + newId := acc.TestClient().Ids.RandomAccountObjectIdentifier() + + 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() { + _, databaseCleanup := acc.TestClient().Database.CreateDatabaseWithIdentifier(t, id) + t.Cleanup(databaseCleanup) + }, + Config: executeConfigCreateDatabase(id), + ResourceName: "snowflake_execute.test", + ImportState: true, + ImportStatePersist: true, + ImportStateId: "random_id", + ImportStateVerifyIgnore: []string{"query_results"}, + }, + // filling the empty state fields (execute changed from empty) + { + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_execute.test", plancheck.ResourceActionUpdate), + }, + }, + Config: executeConfigCreateDatabase(id), + }, + // change the id in every query to see if: + // 1. execute will trigger force new behavior + // 2. an old database is used in delete (it is) + { + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_execute.test", plancheck.ResourceActionDestroyBeforeCreate), + }, + PostApplyPostRefresh: []plancheck.PlanCheck{ + resources.PlanCheckFunc(func(ctx context.Context, req plancheck.CheckPlanRequest, resp *plancheck.CheckPlanResponse) { + _, err := acc.TestClient().Database.Show(t, id) + if err == nil { + resp.Error = fmt.Errorf("database %s still exist", id.FullyQualifiedName()) + t.Cleanup(acc.TestClient().Database.DropDatabaseFunc(t, id)) + } + }), + }, + }, + Config: executeConfigCreateDatabase(newId), + }, + }, + }) +} diff --git a/pkg/resources/helpers.go b/pkg/resources/helpers.go index 6752840e1e..d7638e6093 100644 --- a/pkg/resources/helpers.go +++ b/pkg/resources/helpers.go @@ -1,9 +1,12 @@ package resources import ( + "context" "fmt" "slices" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "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/diag" @@ -292,3 +295,9 @@ func parseSchemaObjectIdentifierSet(v any) ([]sdk.SchemaObjectIdentifier, error) } return ids, nil } + +type PlanCheckFunc func(ctx context.Context, req plancheck.CheckPlanRequest, resp *plancheck.CheckPlanResponse) + +func (fn PlanCheckFunc) CheckPlan(ctx context.Context, req plancheck.CheckPlanRequest, resp *plancheck.CheckPlanResponse) { + fn(ctx, req, resp) +} diff --git a/pkg/resources/testdata/TestAcc_UnsafeExecute_commonSetup/test.tf b/pkg/resources/testdata/TestAcc_Execute_commonSetup/test.tf similarity index 52% rename from pkg/resources/testdata/TestAcc_UnsafeExecute_commonSetup/test.tf rename to pkg/resources/testdata/TestAcc_Execute_commonSetup/test.tf index a71f75afd3..f5458de821 100644 --- a/pkg/resources/testdata/TestAcc_UnsafeExecute_commonSetup/test.tf +++ b/pkg/resources/testdata/TestAcc_Execute_commonSetup/test.tf @@ -1,4 +1,4 @@ -resource "snowflake_unsafe_execute" "test" { +resource "snowflake_execute" "test" { execute = var.execute revert = var.revert } diff --git a/pkg/resources/testdata/TestAcc_UnsafeExecute_commonSetup/variables.tf b/pkg/resources/testdata/TestAcc_Execute_commonSetup/variables.tf similarity index 100% rename from pkg/resources/testdata/TestAcc_UnsafeExecute_commonSetup/variables.tf rename to pkg/resources/testdata/TestAcc_Execute_commonSetup/variables.tf diff --git a/pkg/resources/testdata/TestAcc_UnsafeExecute_grantsComplex/test.tf b/pkg/resources/testdata/TestAcc_Execute_grantsComplex/test.tf similarity index 88% rename from pkg/resources/testdata/TestAcc_UnsafeExecute_grantsComplex/test.tf rename to pkg/resources/testdata/TestAcc_Execute_grantsComplex/test.tf index 96c70bae50..e553905be9 100644 --- a/pkg/resources/testdata/TestAcc_UnsafeExecute_grantsComplex/test.tf +++ b/pkg/resources/testdata/TestAcc_Execute_grantsComplex/test.tf @@ -1,4 +1,4 @@ -resource "snowflake_unsafe_execute" "test" { +resource "snowflake_execute" "test" { for_each = { for index, db_grant in var.database_grants : index => db_grant } execute = "GRANT ${join(",", each.value.privileges)} ON DATABASE ${each.value.database_name} TO ROLE ${each.value.role_id}" revert = "REVOKE ${join(",", each.value.privileges)} ON DATABASE ${each.value.database_name} FROM ROLE ${each.value.role_id}" diff --git a/pkg/resources/testdata/TestAcc_UnsafeExecute_grantsComplex/variables.tf b/pkg/resources/testdata/TestAcc_Execute_grantsComplex/variables.tf similarity index 100% rename from pkg/resources/testdata/TestAcc_UnsafeExecute_grantsComplex/variables.tf rename to pkg/resources/testdata/TestAcc_Execute_grantsComplex/variables.tf diff --git a/pkg/resources/testdata/TestAcc_UnsafeExecute_withRead/test.tf b/pkg/resources/testdata/TestAcc_Execute_withRead/test.tf similarity index 61% rename from pkg/resources/testdata/TestAcc_UnsafeExecute_withRead/test.tf rename to pkg/resources/testdata/TestAcc_Execute_withRead/test.tf index ff23b05f4e..eb08263587 100644 --- a/pkg/resources/testdata/TestAcc_UnsafeExecute_withRead/test.tf +++ b/pkg/resources/testdata/TestAcc_Execute_withRead/test.tf @@ -1,4 +1,4 @@ -resource "snowflake_unsafe_execute" "test" { +resource "snowflake_execute" "test" { execute = var.execute revert = var.revert query = var.query diff --git a/pkg/resources/testdata/TestAcc_UnsafeExecute_withRead/variables.tf b/pkg/resources/testdata/TestAcc_Execute_withRead/variables.tf similarity index 100% rename from pkg/resources/testdata/TestAcc_UnsafeExecute_withRead/variables.tf rename to pkg/resources/testdata/TestAcc_Execute_withRead/variables.tf diff --git a/pkg/resources/unsafe_execute.go b/pkg/resources/unsafe_execute.go index 8d56630ea6..822900ce02 100644 --- a/pkg/resources/unsafe_execute.go +++ b/pkg/resources/unsafe_execute.go @@ -1,164 +1,12 @@ package resources import ( - "context" - "fmt" - "log" - - "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/provider/resources" - - "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/provider" - - "github.com/hashicorp/go-uuid" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) -var unsafeExecuteSchema = map[string]*schema.Schema{ - "execute": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - Description: "SQL statement to execute. Forces recreation of resource on change.", - }, - "revert": { - Type: schema.TypeString, - Required: true, - Description: "SQL statement to revert the execute statement. Invoked when resource is being destroyed.", - }, - "query": { - Type: schema.TypeString, - Optional: true, - Description: "Optional SQL statement to do a read. Invoked after creation and every time it is changed.", - }, - "query_results": { - Type: schema.TypeList, - Computed: true, - Description: "List of key-value maps (text to text) retrieved after executing read query. Will be empty if the query results in an error.", - Elem: &schema.Schema{ - Type: schema.TypeMap, - Elem: &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - }, - }, -} - func UnsafeExecute() *schema.Resource { - return &schema.Resource{ - Create: CreateUnsafeExecute, - Read: ReadUnsafeExecute, - Delete: DeleteUnsafeExecute, - Update: UpdateUnsafeExecute, - - Schema: unsafeExecuteSchema, - - Description: "Experimental resource allowing execution of ANY SQL statement. It may destroy resources if used incorrectly. It may behave incorrectly combined with other resources. Use at your own risk.", - - CustomizeDiff: TrackingCustomDiffWrapper(resources.UnsafeExecute, func(_ context.Context, diff *schema.ResourceDiff, _ interface{}) error { - if diff.HasChange("query") { - err := diff.SetNewComputed("query_results") - if err != nil { - return err - } - } - return nil - }), - } -} - -func ReadUnsafeExecute(d *schema.ResourceData, meta interface{}) error { - client := meta.(*provider.Context).Client - ctx := context.Background() - - readStatement := d.Get("query").(string) - - setNilResults := func() error { - log.Printf(`[DEBUG] Clearing query_results`) - err := d.Set("query_results", nil) - if err != nil { - return err - } - return nil - } - - if readStatement == "" { - return setNilResults() - } else { - rows, err := client.QueryUnsafe(ctx, readStatement) - if err != nil { - log.Printf(`[WARN] SQL query "%s" failed with err %v`, readStatement, err) - return setNilResults() - } - log.Printf(`[INFO] SQL query "%s" executed successfully, returned rows count: %d`, readStatement, len(rows)) - rowsTransformed := make([]map[string]any, len(rows)) - for i, row := range rows { - t := make(map[string]any) - for k, v := range row { - if *v == nil { - t[k] = nil - } else { - switch (*v).(type) { - case fmt.Stringer: - t[k] = fmt.Sprintf("%v", *v) - case string: - t[k] = *v - default: - return fmt.Errorf("currently only objects convertible to String are supported by query; got %v", *v) - } - } - } - rowsTransformed[i] = t - } - err = d.Set("query_results", rowsTransformed) - if err != nil { - return err - } - } - - return nil -} - -func CreateUnsafeExecute(d *schema.ResourceData, meta interface{}) error { - client := meta.(*provider.Context).Client - ctx := context.Background() - - id, err := uuid.GenerateUUID() - if err != nil { - return err - } - - executeStatement := d.Get("execute").(string) - _, err = client.ExecUnsafe(ctx, executeStatement) - if err != nil { - return err - } - - d.SetId(id) - log.Printf(`[INFO] SQL "%s" applied successfully\n`, executeStatement) - - return ReadUnsafeExecute(d, meta) -} - -func DeleteUnsafeExecute(d *schema.ResourceData, meta interface{}) error { - client := meta.(*provider.Context).Client - ctx := context.Background() - - revertStatement := d.Get("revert").(string) - _, err := client.ExecUnsafe(ctx, revertStatement) - if err != nil { - return err - } - - d.SetId("") - log.Printf(`[INFO] SQL "%s" applied successfully\n`, revertStatement) - - return nil -} - -func UpdateUnsafeExecute(d *schema.ResourceData, meta interface{}) error { - if d.HasChange("query") { - return ReadUnsafeExecute(d, meta) - } - return nil + unsafeExecute := Execute() + unsafeExecute.Description = "Experimental resource allowing execution of ANY SQL statement. It may destroy resources if used incorrectly. It may behave incorrectly combined with other resources. Use at your own risk." + unsafeExecute.DeprecationMessage = "This resource is deprecated and will be removed in a future major version release. Please use snowflake_execute instead." + return unsafeExecute } diff --git a/pkg/scripts/issues/labels.go b/pkg/scripts/issues/labels.go index cb5a130d3e..0d1e0b29c6 100644 --- a/pkg/scripts/issues/labels.go +++ b/pkg/scripts/issues/labels.go @@ -18,6 +18,7 @@ var RepositoryLabels = []string{ "resource:database_role", "resource:dynamic_table", "resource:email_notification_integration", + "resource:execute", "resource:external_function", "resource:external_oauth_integration", "resource:external_table", diff --git a/templates/resources/execute.md.tmpl b/templates/resources/execute.md.tmpl new file mode 100644 index 0000000000..80794cba89 --- /dev/null +++ b/templates/resources/execute.md.tmpl @@ -0,0 +1,40 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "{{.Name}} {{.Type}} - {{.ProviderName}}" +subcategory: "" +description: |- +{{ if gt (len (split .Description "")) 1 -}} +{{ index (split .Description "") 1 | plainmarkdown | trimspace | prefixlines " " }} +{{- else -}} +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} +{{- end }} +--- + +# {{.Name}} ({{.Type}}) + +!> **Warning** This is a dangerous resource that allows executing **ANY** SQL statement. It may destroy resources if used incorrectly. It may behave incorrectly combined with other resources. Use at your own risk. + +~> **Note** It can be theoretically used to manage resource that are not supported by the provider. This is risky and may brake other resources if used incorrectly. + +~> **Note** Use `query` parameter with caution. It will fetch **ALL** the results returned by the query provided. Try to limit the number of results by writing query with filters. Query failure does not stop resource creation; it simply results in `query_results` being empty. + +{{ .Description | trimspace }} + +{{ if .HasExample -}} +## Example Usage + +{{ tffile (printf "examples/resources/%s/resource.tf" .Name)}} +-> **Note** Instead of using fully_qualified_name, you can reference objects managed outside Terraform by constructing a correct ID, consult [identifiers guide](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/guides/identifiers#new-computed-fully-qualified-name-field-in-resources). + + +{{- end }} + +{{ .SchemaMarkdown | trimspace }} +{{- if .HasImport }} + +## Import + +Import is supported using the following syntax: + +{{ codefile "shell" (printf "examples/resources/%s/import.sh" .Name)}} +{{- end }} diff --git a/templates/resources/unsafe_execute.md.tmpl b/templates/resources/unsafe_execute.md.tmpl index da7c2970a9..80794cba89 100644 --- a/templates/resources/unsafe_execute.md.tmpl +++ b/templates/resources/unsafe_execute.md.tmpl @@ -14,8 +14,6 @@ description: |- !> **Warning** This is a dangerous resource that allows executing **ANY** SQL statement. It may destroy resources if used incorrectly. It may behave incorrectly combined with other resources. Use at your own risk. -~> **Note** This resource will be included in the V1 (check [here](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/v1-preparations/ESSENTIAL_GA_OBJECTS.MD)) but may be slightly modified before. Design decisions and changes will be listed in the [migration guide](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/MIGRATION_GUIDE.md#migration-guide). - ~> **Note** It can be theoretically used to manage resource that are not supported by the provider. This is risky and may brake other resources if used incorrectly. ~> **Note** Use `query` parameter with caution. It will fetch **ALL** the results returned by the query provided. Try to limit the number of results by writing query with filters. Query failure does not stop resource creation; it simply results in `query_results` being empty. @@ -38,5 +36,5 @@ description: |- Import is supported using the following syntax: -{{ printf "{{codefile \"shell\" %q}}" .ImportFile }} +{{ codefile "shell" (printf "examples/resources/%s/import.sh" .Name)}} {{- end }}