Skip to content

Commit

Permalink
feat: Add unsafe_execute resource (#2225)
Browse files Browse the repository at this point in the history
* add unsafe exec resource

* Rename resource and make the basic tests pass

* Simplify tests setup

* Rename resource args

* Fix checking db existence

* Add random part to id

* Rename migration

* Remove unnecessary config param

* Remove duplicated checks

* Extract function to generate capitalized name

* Test escaped identifier

* Test unsafe execute with grants

* Add TODO

* Add test with HCL for_each

* Fix before pushing

* Update docs with warning

* Update example usage in docs

* Add negative tests

* Add read (WIP)

* Pass test without query

* Add test for query removed

* Move unsafe client and implementation to SDK

* Fix query unsafe

* Update docs and change query behavior

* Update docs

* Update logs

* Fix test

---------

Co-authored-by: Jan Cieślak <jan.cieslak@snowflake.com>
  • Loading branch information
sfc-gh-asawicki and sfc-gh-jcieslak committed Nov 30, 2023
1 parent c60db80 commit 196134c
Show file tree
Hide file tree
Showing 15 changed files with 1,349 additions and 0 deletions.
143 changes: 143 additions & 0 deletions docs/resources/unsafe_execute.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "snowflake_unsafe_execute Resource - terraform-provider-snowflake"
subcategory: ""
description: |-
Experimental resource used for testing purposes only. Allows to execute ANY SQL statement.
---

# snowflake_unsafe_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. Will be deleted in the upcoming versions. 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.

Experimental resource used for testing purposes only. Allows to execute ANY SQL statement.

## Example Usage

```terraform
##################################
### simple use cases
##################################
# create and destroy resource
resource "snowflake_unsafe_execute" "test" {
execute = "CREATE DATABASE ABC"
revert = "DROP DATABASE ABC"
}
# create and destroy resource using qualified name
resource "snowflake_unsafe_execute" "test" {
execute = "CREATE DATABASE \"abc\""
revert = "DROP DATABASE \"abc\""
}
# with query
resource "snowflake_unsafe_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_unsafe_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_unsafe_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 - simple
# 1 - resource created with a bad revert; it is constructed, revert is not validated before destroy happens
resource "snowflake_unsafe_execute" "test" {
execute = "CREATE DATABASE ABC"
revert = "SELECT 1"
}
# 2 - fix the revert first; resource won't be recreated
resource "snowflake_unsafe_execute" "test" {
execute = "CREATE DATABASE ABC"
revert = "DROP DATABASE ABC"
}
# bad revert - complex (we assume that the problem is spotted after trying to change the execute)
# 1 - resource created with a bad revert; it is constructed, revert is not validated before destroy happens
resource "snowflake_unsafe_execute" "test" {
execute = "CREATE DATABASE ABC"
revert = "SELECT 1"
}
# 2 - try to create different database; it will fail on bad destroy
resource "snowflake_unsafe_execute" "test" {
execute = "CREATE DATABASE XYZ"
revert = "SELECT 1"
}
# 3 - fix the revert first
resource "snowflake_unsafe_execute" "test" {
execute = "CREATE DATABASE ABC"
revert = "DROP DATABASE ABC"
}
# 4 - create different database updating revert also
resource "snowflake_unsafe_execute" "test" {
execute = "CREATE DATABASE XYZ"
revert = "DROP DATABASE XYZ"
}
# bad query
# 1 - resource will be created; query_results will be empty
resource "snowflake_unsafe_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_unsafe_execute" "test" {
execute = "CREATE DATABASE ABC"
revert = "DROP DATABASE ABC"
query = "SHOW DATABASES LIKE '%ABC%'"
}
```

<!-- schema generated by tfplugindocs -->
## 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 after creation 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.
104 changes: 104 additions & 0 deletions examples/resources/snowflake_unsafe_execute/resource.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
##################################
### simple use cases
##################################

# create and destroy resource
resource "snowflake_unsafe_execute" "test" {
execute = "CREATE DATABASE ABC"
revert = "DROP DATABASE ABC"
}

# create and destroy resource using qualified name
resource "snowflake_unsafe_execute" "test" {
execute = "CREATE DATABASE \"abc\""
revert = "DROP DATABASE \"abc\""
}

# with query
resource "snowflake_unsafe_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_unsafe_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_unsafe_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 - simple
# 1 - resource created with a bad revert; it is constructed, revert is not validated before destroy happens
resource "snowflake_unsafe_execute" "test" {
execute = "CREATE DATABASE ABC"
revert = "SELECT 1"
}

# 2 - fix the revert first; resource won't be recreated
resource "snowflake_unsafe_execute" "test" {
execute = "CREATE DATABASE ABC"
revert = "DROP DATABASE ABC"
}

# bad revert - complex (we assume that the problem is spotted after trying to change the execute)
# 1 - resource created with a bad revert; it is constructed, revert is not validated before destroy happens
resource "snowflake_unsafe_execute" "test" {
execute = "CREATE DATABASE ABC"
revert = "SELECT 1"
}

# 2 - try to create different database; it will fail on bad destroy
resource "snowflake_unsafe_execute" "test" {
execute = "CREATE DATABASE XYZ"
revert = "SELECT 1"
}

# 3 - fix the revert first
resource "snowflake_unsafe_execute" "test" {
execute = "CREATE DATABASE ABC"
revert = "DROP DATABASE ABC"
}

# 4 - create different database updating revert also
resource "snowflake_unsafe_execute" "test" {
execute = "CREATE DATABASE XYZ"
revert = "DROP DATABASE XYZ"
}

# bad query
# 1 - resource will be created; query_results will be empty
resource "snowflake_unsafe_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_unsafe_execute" "test" {
execute = "CREATE DATABASE ABC"
revert = "DROP DATABASE ABC"
query = "SHOW DATABASES LIKE '%ABC%'"
}
8 changes: 8 additions & 0 deletions pkg/acceptance/testing.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,11 @@ func ConfigurationSameAsStepN(step int) func(config.TestStepConfigRequest) strin
return filepath.Join("testdata", req.TestName, strconv.Itoa(step))
}
}

// ConfigurationDirectory should be used to obtain configuration if the same can be shared between multiple tests to avoid duplication of configuration and var files.
// Based on config.TestNameDirectory. Similar to config.StaticDirectory but prefixed provided directory with `testdata`.
func ConfigurationDirectory(directory string) func(config.TestStepConfigRequest) string {
return func(req config.TestStepConfigRequest) string {
return filepath.Join("testdata", directory)
}
}
1 change: 1 addition & 0 deletions pkg/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,7 @@ func getResources() map[string]*schema.Resource {
"snowflake_tag_association": resources.TagAssociation(),
"snowflake_tag_masking_policy_association": resources.TagMaskingPolicyAssociation(),
"snowflake_task": resources.Task(),
"snowflake_unsafe_execute": resources.UnsafeExecute(),
"snowflake_user": resources.User(),
"snowflake_user_ownership_grant": resources.UserOwnershipGrant(),
"snowflake_user_public_keys": resources.UserPublicKeys(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
resource "snowflake_unsafe_execute" "test" {
execute = var.execute
revert = var.revert
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
variable "execute" {
type = string
}

variable "revert" {
type = string
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
resource "snowflake_unsafe_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}"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
variable "database_grants" {
type = list(object({
database_name = string
role_id = string
privileges = list(string)
}))
}
5 changes: 5 additions & 0 deletions pkg/resources/testdata/TestAcc_UnsafeExecute_withRead/test.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
resource "snowflake_unsafe_execute" "test" {
execute = var.execute
revert = var.revert
query = var.query
}
11 changes: 11 additions & 0 deletions pkg/resources/testdata/TestAcc_UnsafeExecute_withRead/variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
variable "execute" {
type = string
}

variable "revert" {
type = string
}

variable "query" {
type = string
}
Loading

0 comments on commit 196134c

Please sign in to comment.