Skip to content

Commit

Permalink
feat: use external functions (#2454)
Browse files Browse the repository at this point in the history
update external func resources to use sdk
  • Loading branch information
sfc-gh-swinkler authored Feb 13, 2024
1 parent 94ac78a commit 417d473
Show file tree
Hide file tree
Showing 20 changed files with 774 additions and 915 deletions.
13 changes: 13 additions & 0 deletions MIGRATION_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,19 @@ It is noted as a behavior change but in some way it is not; with the previous im

We will consider adding `NOT NULL` back because it can be set by `ALTER COLUMN columnX SET NOT NULL`, but first we want to revisit the whole resource design.

### snowflake_external_function resource changes

#### *(behavior change)* return_null_allowed default is now true
The `return_null_allowed` attribute default value is now `true`. This is a behavior change because it was `false` before. The reason it was changed is to match the expected default value in the [documentation](https://docs.snowflake.com/en/sql-reference/sql/create-external-function#optional-parameters) `Default: The default is NULL (i.e. the function can return NULL values).`

#### *(behavior change)* comment is no longer required
The `comment` attribute is now optional. It was required before, but it is not required in Snowflake API.

### snowflake_external_functions data source changes

#### *(behavior change)* schema is now required with database
The `schema` attribute is now required with `database` attribute to match old implementation `SHOW EXTERNAL FUNCTIONS IN SCHEMA "<database>"."<schema>"`. In the future this may change to make schema optional.

## vX.XX.X -> v0.85.0

### Migration from old (grant) resources to new ones
Expand Down
2 changes: 1 addition & 1 deletion docs/data-sources/external_functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ data "snowflake_external_functions" "current" {
<!-- schema generated by tfplugindocs -->
## Schema

### Required
### Optional

- `database` (String) The database from which to return the schemas from.
- `schema` (String) The schema from which to return the external functions from.
Expand Down
2 changes: 1 addition & 1 deletion docs/resources/external_function.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ resource "snowflake_external_function" "test_ext_func" {
- `null_input_behavior` (String) Specifies the behavior of the external function when called with null inputs.
- `request_translator` (String) This specifies the name of the request translator function
- `response_translator` (String) This specifies the name of the response translator function.
- `return_null_allowed` (Boolean) Indicates whether the function can return NULL values or must return only NON-NULL values.
- `return_null_allowed` (Boolean) Indicates whether the function can return NULL values (true) or must return only NON-NULL values (false).

### Read-Only

Expand Down
75 changes: 47 additions & 28 deletions pkg/datasources/external_functions.go
Original file line number Diff line number Diff line change
@@ -1,25 +1,27 @@
package datasources

import (
"context"
"database/sql"
"errors"
"fmt"
"log"
"strings"

"github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/snowflake"
"github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

var externalFunctionsSchema = map[string]*schema.Schema{
"database": {
Type: schema.TypeString,
Required: true,
Optional: true,
Description: "The database from which to return the schemas from.",
},
"schema": {
Type: schema.TypeString,
Required: true,
Description: "The schema from which to return the external functions from.",
Type: schema.TypeString,
Optional: true,
RequiredWith: []string{"database"},
Description: "The schema from which to return the external functions from.",
},
"external_functions": {
Type: schema.TypeList,
Expand Down Expand Up @@ -56,42 +58,59 @@ var externalFunctionsSchema = map[string]*schema.Schema{

func ExternalFunctions() *schema.Resource {
return &schema.Resource{
Read: ReadExternalFunctions,
Schema: externalFunctionsSchema,
ReadContext: ReadContextExternalFunctions,
Schema: externalFunctionsSchema,
}
}

func ReadExternalFunctions(d *schema.ResourceData, meta interface{}) error {
func ReadContextExternalFunctions(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
db := meta.(*sql.DB)
client := sdk.NewClientFromDB(db)
databaseName := d.Get("database").(string)
schemaName := d.Get("schema").(string)

currentExternalFunctions, err := snowflake.ListExternalFunctions(databaseName, schemaName, db)
if errors.Is(err, sql.ErrNoRows) {
// If not found, mark resource to be removed from state file during apply or refresh
log.Printf("[DEBUG] external functions in schema (%s) not found", d.Id())
d.SetId("")
return nil
} else if err != nil {
log.Printf("[DEBUG] unable to parse external functions in schema (%s)", d.Id())
req := sdk.NewShowExternalFunctionRequest()
externalFunctions, err := client.ExternalFunctions.Show(ctx, req)
if err != nil {
d.SetId("")
return nil
}

externalFunctions := []map[string]interface{}{}

for _, externalFunction := range currentExternalFunctions {
externalFunctionsList := []map[string]interface{}{}
for _, externalFunction := range externalFunctions {
externalFunctionMap := map[string]interface{}{}
externalFunctionMap["name"] = externalFunction.Name

// do we filter by database?
currentDatabase := strings.Trim(externalFunction.CatalogName, `"`)
if databaseName != "" {
if currentDatabase != databaseName {
continue
}
externalFunctionMap["database"] = currentDatabase
} else {
externalFunctionMap["database"] = currentDatabase
}

externalFunctionMap["name"] = externalFunction.ExternalFunctionName.String
externalFunctionMap["database"] = externalFunction.DatabaseName.String
externalFunctionMap["schema"] = externalFunction.SchemaName.String
externalFunctionMap["comment"] = externalFunction.Comment.String
externalFunctionMap["language"] = externalFunction.Language.String
// do we filter by schema?
currentSchema := strings.Trim(externalFunction.SchemaName, `"`)
if schemaName != "" {
if currentSchema != schemaName {
continue
}
externalFunctionMap["schema"] = currentSchema
} else {
externalFunctionMap["schema"] = currentSchema
}

externalFunctions = append(externalFunctions, externalFunctionMap)
externalFunctionMap["comment"] = externalFunction.Description
externalFunctionMap["language"] = externalFunction.Language
externalFunctionsList = append(externalFunctionsList, externalFunctionMap)
}

d.SetId(fmt.Sprintf(`%v|%v`, databaseName, schemaName))
return d.Set("external_functions", externalFunctions)
if err := d.Set("external_functions", externalFunctionsList); err != nil {
return diag.FromErr(err)
}
return nil
}
130 changes: 75 additions & 55 deletions pkg/datasources/external_functions_acceptance_test.go
Original file line number Diff line number Diff line change
@@ -1,80 +1,100 @@
package datasources_test

import (
"fmt"
"os"
"strings"
"testing"

acc "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance"

"github.com/hashicorp/terraform-plugin-testing/config"
"github.com/hashicorp/terraform-plugin-testing/helper/acctest"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/tfversion"
)

func TestAcc_ExternalFunctions(t *testing.T) {
databaseName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha))
schemaName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha))
apiName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha))
externalFunctionName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha))
resource.ParallelTest(t, resource.TestCase{
Providers: providers(),
func TestAcc_ExternalFunctions_basic(t *testing.T) {
if _, ok := os.LookupEnv("SKIP_EXTERNAL_FUNCTION_TESTS"); ok {
t.Skip("Skipping TestAcc_ExternalFunction")
}
accName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha))
m := func() map[string]config.Variable {
return map[string]config.Variable{
"database": config.StringVariable(acc.TestDatabaseName),
"schema": config.StringVariable(acc.TestSchemaName),
"name": config.StringVariable(accName),
"api_allowed_prefixes": config.ListVariable(config.StringVariable("https://123456.execute-api.us-west-2.amazonaws.com/prod/")),
"url_of_proxy_and_resource": config.StringVariable("https://123456.execute-api.us-west-2.amazonaws.com/prod/test_func"),
"comment": config.StringVariable("Terraform acceptance test"),
}
}

dataSourceName := "data.snowflake_external_functions.external_functions"
configVariables := m()

resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories,
PreCheck: func() { acc.TestAccPreCheck(t) },
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
tfversion.RequireAbove(tfversion.Version1_5_0),
},
CheckDestroy: nil,
Steps: []resource.TestStep{
{
Config: externalFunctions(databaseName, schemaName, apiName, externalFunctionName),
ConfigDirectory: acc.ConfigurationDirectory("TestAcc_ExternalFunctions/basic"),
ConfigVariables: configVariables,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("data.snowflake_external_functions.t", "database", databaseName),
resource.TestCheckResourceAttr("data.snowflake_external_functions.t", "schema", schemaName),
resource.TestCheckResourceAttrSet("data.snowflake_external_functions.t", "external_functions.#"),
resource.TestCheckResourceAttr("data.snowflake_external_functions.t", "external_functions.#", "1"),
resource.TestCheckResourceAttr("data.snowflake_external_functions.t", "external_functions.0.name", externalFunctionName),
resource.TestCheckResourceAttr(dataSourceName, "database", acc.TestDatabaseName),
resource.TestCheckResourceAttr(dataSourceName, "schema", acc.TestSchemaName),
resource.TestCheckResourceAttrSet(dataSourceName, "external_functions.#"),
resource.TestCheckResourceAttrSet(dataSourceName, "external_functions.0.name"),
resource.TestCheckResourceAttrSet(dataSourceName, "external_functions.0.database"),
resource.TestCheckResourceAttrSet(dataSourceName, "external_functions.0.schema"),
resource.TestCheckResourceAttrSet(dataSourceName, "external_functions.0.comment"),
resource.TestCheckResourceAttrSet(dataSourceName, "external_functions.0.language"),
),
},
},
})
}

func externalFunctions(databaseName string, schemaName string, apiName string, externalFunctionName string) string {
return fmt.Sprintf(`
resource snowflake_database "test_database" {
name = "%v"
}
resource snowflake_schema "test_schema"{
name = "%v"
database = snowflake_database.test_database.name
func TestAcc_ExternalFunctions_no_database(t *testing.T) {
if _, ok := os.LookupEnv("SKIP_EXTERNAL_FUNCTION_TESTS"); ok {
t.Skip("Skipping TestAcc_ExternalFunction")
}
resource "snowflake_api_integration" "test_api_int" {
name = "%v"
api_provider = "aws_api_gateway"
api_aws_role_arn = "arn:aws:iam::000000000001:/role/test"
api_allowed_prefixes = ["https://123456.execute-api.us-west-2.amazonaws.com/prod/"]
enabled = true
}
resource "snowflake_external_function" "test_func" {
name = "%v"
database = snowflake_database.test_database.name
schema = snowflake_schema.test_schema.name
arg {
name = "arg1"
type = "varchar"
accName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha))
m := func() map[string]config.Variable {
return map[string]config.Variable{
"database": config.StringVariable(acc.TestDatabaseName),
"schema": config.StringVariable(acc.TestSchemaName),
"name": config.StringVariable(accName),
"api_allowed_prefixes": config.ListVariable(config.StringVariable("https://123456.execute-api.us-west-2.amazonaws.com/prod/")),
"url_of_proxy_and_resource": config.StringVariable("https://123456.execute-api.us-west-2.amazonaws.com/prod/test_func"),
"comment": config.StringVariable("Terraform acceptance test"),
}
arg {
name = "arg2"
type = "varchar"
}
comment = "Terraform acceptance test"
return_type = "varchar"
return_behavior = "IMMUTABLE"
api_integration = snowflake_api_integration.test_api_int.name
url_of_proxy_and_resource = "https://123456.execute-api.us-west-2.amazonaws.com/prod/test_func"
}

data snowflake_external_functions "t" {
database = snowflake_external_function.test_func.database
schema = snowflake_external_function.test_func.schema
depends_on = [snowflake_external_function.test_func]
}
`, databaseName, schemaName, apiName, externalFunctionName)
dataSourceName := "data.snowflake_external_functions.external_functions"
configVariables := m()

resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories,
PreCheck: func() { acc.TestAccPreCheck(t) },
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
tfversion.RequireAbove(tfversion.Version1_5_0),
},
CheckDestroy: nil,
Steps: []resource.TestStep{
{
ConfigDirectory: acc.ConfigurationDirectory("TestAcc_ExternalFunctions/no_filter"),
ConfigVariables: configVariables,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet(dataSourceName, "external_functions.#"),
resource.TestCheckResourceAttrSet(dataSourceName, "external_functions.0.name"),
resource.TestCheckResourceAttrSet(dataSourceName, "external_functions.0.comment"),
resource.TestCheckResourceAttrSet(dataSourceName, "external_functions.0.language"),
),
},
},
})
}
31 changes: 31 additions & 0 deletions pkg/datasources/testdata/TestAcc_ExternalFunctions/basic/test.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
resource "snowflake_api_integration" "test_api_int" {
name = var.name
api_provider = "aws_api_gateway"
api_aws_role_arn = "arn:aws:iam::000000000001:/role/test"
api_allowed_prefixes = var.api_allowed_prefixes
enabled = true
}

resource "snowflake_external_function" "external_function" {
name = var.name
database = var.database
schema = var.schema
arg {
name = "ARG1"
type = "VARCHAR"
}
arg {
name = "ARG2"
type = "VARCHAR"
}
comment = var.comment
return_type = "VARIANT"
return_behavior = "IMMUTABLE"
api_integration = snowflake_api_integration.test_api_int.name
url_of_proxy_and_resource = var.url_of_proxy_and_resource
}

data "snowflake_external_functions" "external_functions" {
database = snowflake_external_function.external_function.database
schema = snowflake_external_function.external_function.schema
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
variable "database" {
type = string
}

variable "schema" {
type = string
}

variable "name" {
type = string
}

variable "api_allowed_prefixes" {
type = list(string)
}

variable "url_of_proxy_and_resource" {
type = string
}

variable "comment" {
type = string
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
resource "snowflake_api_integration" "test_api_int" {
name = var.name
api_provider = "aws_api_gateway"
api_aws_role_arn = "arn:aws:iam::000000000001:/role/test"
api_allowed_prefixes = var.api_allowed_prefixes
enabled = true
}

resource "snowflake_external_function" "external_function" {
name = var.name
database = var.database
schema = var.schema
arg {
name = "ARG1"
type = "VARCHAR"
}
arg {
name = "ARG2"
type = "VARCHAR"
}
comment = var.comment
return_type = "VARIANT"
return_behavior = "IMMUTABLE"
api_integration = snowflake_api_integration.test_api_int.name
url_of_proxy_and_resource = var.url_of_proxy_and_resource
}

data "snowflake_external_functions" "external_functions" {}
Loading

0 comments on commit 417d473

Please sign in to comment.