Skip to content

Commit

Permalink
feat: Added Procedures Datasource (Snowflake-Labs#646)
Browse files Browse the repository at this point in the history
  • Loading branch information
ChrisIsidora authored and jtzero committed Aug 19, 2021
1 parent a16090a commit eb931b0
Show file tree
Hide file tree
Showing 8 changed files with 240 additions and 16 deletions.
2 changes: 1 addition & 1 deletion docs/resources/procedure_grant.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ resource snowflake_procedure_grant grant {

### Optional

- **arguments** (Block List) List of the arguments for the procedure (must be present if procedure_name is present) (see [below for nested schema](#nestedblock--arguments))
- **arguments** (Block List) List of the arguments for the procedure (must be present if procedure has arguments and procedure_name is present) (see [below for nested schema](#nestedblock--arguments))
- **id** (String) The ID of this resource.
- **on_future** (Boolean) When this is set to true and a schema_name is provided, apply this grant on all future procedures in the given schema. When this is true and no schema_name is provided apply this grant on all future procedures in the given database. The procedure_name and shares fields must be unset in order to use on_future.
- **privilege** (String) The privilege to grant on the current or future procedure.
Expand Down
4 changes: 4 additions & 0 deletions examples/data-sources/snowflake_procedures/data-source.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
data "snowflake_procedures" "current" {
database = "MYDB"
schema = "MYSCHEMA"
}
128 changes: 128 additions & 0 deletions pkg/datasources/procedures.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package datasources

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

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

var proceduresSchema = map[string]*schema.Schema{
"database": {
Type: schema.TypeString,
Required: 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 procedures from.",
},
"procedures": {
Type: schema.TypeList,
Computed: true,
Description: "The procedures in the schema",
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
Computed: true,
},
"database": {
Type: schema.TypeString,
Computed: true,
},
"schema": {
Type: schema.TypeString,
Computed: true,
},
"comment": {
Type: schema.TypeString,
Optional: true,
Computed: true,
},
"argument_types": {
Type: schema.TypeList,
Elem: &schema.Schema{Type: schema.TypeString},
Optional: true,
Computed: true,
},
"return_type": {
Type: schema.TypeString,
Optional: true,
Computed: true,
},
},
},
},
}

func Procedures() *schema.Resource {
return &schema.Resource{
Read: ReadProcedures,
Schema: proceduresSchema,
}
}

func ReadProcedures(d *schema.ResourceData, meta interface{}) error {
db := meta.(*sql.DB)
databaseName := d.Get("database").(string)
schemaName := d.Get("schema").(string)

currentProcedures, err := snowflake.ListProcedures(databaseName, schemaName, db)
if err == sql.ErrNoRows {
// If not found, mark resource to be removed from statefile during apply or refresh
log.Printf("[DEBUG] procedures in schema (%s) not found", d.Id())
d.SetId("")
return nil
} else if err != nil {
log.Printf("[DEBUG] unable to parse procedures in schema (%s)", d.Id())
d.SetId("")
return nil
}

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

for _, procedure := range currentProcedures {
procedureMap := map[string]interface{}{}

procedureSignatureMap, err := parseArguments(procedure.Arguments.String)
if err != nil {
return err
}

procedureMap["name"] = procedure.Name.String
procedureMap["database"] = procedure.DatabaseName.String
procedureMap["schema"] = procedure.SchemaName.String
procedureMap["comment"] = procedure.Comment.String
procedureMap["argument_types"] = procedureSignatureMap["argumentTypes"].([]string)
procedureMap["return_type"] = procedureSignatureMap["returnType"].(string)

procedures = append(procedures, procedureMap)
}

d.SetId(fmt.Sprintf(`%v|%v`, databaseName, schemaName))
return d.Set("procedures", procedures)
}

func parseArguments(arguments string) (map[string]interface{}, error) {
r := regexp.MustCompile(`(?P<callable_name>[^(]+)\((?P<argument_signature>[^)]*)\) RETURN (?P<return_type>.*)`)
matches := r.FindStringSubmatch(arguments)
if len(matches) == 0 {
return nil, errors.New(fmt.Sprintf(`Could not parse arguments: %v`, arguments))
}
callableSignatureMap := make(map[string]interface{})

argumentTypes := strings.Split(matches[2], ", ")

callableSignatureMap["callableName"] = matches[1]
callableSignatureMap["argumentTypes"] = argumentTypes
callableSignatureMap["returnType"] = matches[3]

return callableSignatureMap, nil
}
74 changes: 74 additions & 0 deletions pkg/datasources/procedures_acceptance_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package datasources_test

import (
"fmt"
"strings"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
)

func TestAccProcedures(t *testing.T) {
databaseName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha))
schemaName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha))
procedureName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha))
procedureWithArgumentsName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha))
resource.ParallelTest(t, resource.TestCase{
Providers: providers(),
Steps: []resource.TestStep{
{
Config: procedures(databaseName, schemaName, procedureName, procedureWithArgumentsName),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("data.snowflake_procedures.t", "database", databaseName),
resource.TestCheckResourceAttr("data.snowflake_procedures.t", "schema", schemaName),
resource.TestCheckResourceAttrSet("data.snowflake_procedures.t", "procedures.#"),
resource.TestCheckResourceAttr("data.snowflake_procedures.t", "procedures.#", "2"),
),
},
},
})
}

func procedures(databaseName string, schemaName string, procedureName string, procedureWithArgumentsName string) string {
s := `
resource "snowflake_database" "test_database" {
name = "%v"
comment = "Terraform acceptance test"
}
resource "snowflake_schema" "test_schema" {
name = "%v"
database = snowflake_database.test_database.name
comment = "Terraform acceptance test"
}
resource "snowflake_procedure" "test_proc_simple" {
name = "%v"
database = snowflake_database.test_database.name
schema = snowflake_schema.test_schema.name
return_type = "VARCHAR"
statement = "return \"Hi\""
}
resource "snowflake_procedure" "test_proc" {
name = "%v"
database = snowflake_database.test_database.name
schema = snowflake_schema.test_schema.name
arguments {
name = "arg1"
type = "varchar"
}
comment = "Terraform acceptance test"
return_type = "varchar"
statement = "var X=3\nreturn X"
}
data snowflake_procedures "t" {
database = snowflake_database.test_database.name
schema = snowflake_schema.test_schema.name
depends_on = [snowflake_procedure.test_proc_simple, snowflake_procedure.test_proc]
}
`
return fmt.Sprintf(s, databaseName, schemaName, procedureName, procedureWithArgumentsName)
}
1 change: 1 addition & 0 deletions pkg/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ func getDataSources() map[string]*schema.Resource {
"snowflake_resource_monitors": datasources.ResourceMonitors(),
"snowflake_storage_integrations": datasources.StorageIntegrations(),
"snowflake_row_access_policies": datasources.RowAccessPolicies(),
"snowflake_procedures": datasources.Procedures(),
}

return dataSources
Expand Down
20 changes: 11 additions & 9 deletions pkg/resources/grant_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -412,17 +412,19 @@ func parseCallableObjectName(objectName string) (map[string]interface{}, error)

argumentsSignatures := strings.Split(matches[2], ", ")

arguments := make([]interface{}, len(argumentsSignatures))
argumentTypes := make([]string, len(argumentsSignatures))
argumentNames := make([]string, len(argumentsSignatures))
arguments := []interface{}{}
argumentTypes := []string{}
argumentNames := []string{}

for i, argumentSignature := range argumentsSignatures {
signatureComponents := strings.Split(argumentSignature, " ")
argumentNames[i] = signatureComponents[0]
argumentTypes[i] = signatureComponents[1]
arguments[i] = map[string]interface{}{
"name": argumentNames[i],
"type": argumentTypes[i],
if argumentSignature != "" {
signatureComponents := strings.Split(argumentSignature, " ")
argumentNames = append(argumentNames, signatureComponents[0])
argumentTypes = append(argumentTypes, signatureComponents[1])
arguments = append(arguments, map[string]interface{}{
"name": argumentNames[i],
"type": argumentTypes[i],
})
}
}

Expand Down
8 changes: 2 additions & 6 deletions pkg/resources/procedure_grant.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ var procedureGrantSchema = map[string]*schema.Schema{
},
},
Optional: true,
Description: "List of the arguments for the procedure (must be present if procedure_name is present)",
Description: "List of the arguments for the procedure (must be present if procedure has arguments and procedure_name is present)",
ForceNew: true,
},
"return_type": {
Expand Down Expand Up @@ -125,11 +125,6 @@ func CreateProcedureGrant(d *schema.ResourceData, meta interface{}) error {
)
if name, ok := d.GetOk("procedure_name"); ok {
procedureName = name.(string)
if args, ok := d.GetOk("arguments"); ok {
arguments = args.([]interface{})
} else {
return errors.New("arguments must be set when specifying procedure_name.")
}
if ret, ok := d.GetOk("return_type"); ok {
returnType = strings.ToUpper(ret.(string))
} else {
Expand All @@ -141,6 +136,7 @@ func CreateProcedureGrant(d *schema.ResourceData, meta interface{}) error {
priv := d.Get("privilege").(string)
futureProcedures := d.Get("on_future").(bool)
grantOption := d.Get("with_grant_option").(bool)
arguments = d.Get("arguments").([]interface{})

if (procedureName == "") && !futureProcedures {
return errors.New("procedure_name must be set unless on_future is true.")
Expand Down
19 changes: 19 additions & 0 deletions pkg/snowflake/procedure.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import (
"database/sql"
"errors"
"fmt"
"log"
"strings"

"github.com/jmoiron/sqlx"
pe "github.com/pkg/errors"
)

// ProcedureBuilder abstracts the creation of Stored Procedure
Expand Down Expand Up @@ -266,3 +268,20 @@ func ScanProcedures(rows *sqlx.Rows) ([]*procedure, error) {
}
return pcs, rows.Err()
}

func ListProcedures(databaseName string, schemaName string, db *sql.DB) ([]procedure, error) {
stmt := fmt.Sprintf(`SHOW PROCEDURES IN SCHEMA "%s"."%v"`, databaseName, schemaName)
rows, err := Query(db, stmt)
if err != nil {
return nil, err
}
defer rows.Close()

dbs := []procedure{}
err = sqlx.StructScan(rows, &dbs)
if err == sql.ErrNoRows {
log.Printf("[DEBUG] no procedures found")
return nil, nil
}
return dbs, pe.Wrapf(err, "unable to scan row for %s", stmt)
}

0 comments on commit eb931b0

Please sign in to comment.