From eb931b07afc8bccabd316d858f8ffc1839f51b30 Mon Sep 17 00:00:00 2001 From: ChrisIsidora Date: Tue, 17 Aug 2021 19:50:24 +0200 Subject: [PATCH] feat: Added Procedures Datasource (#646) --- docs/resources/procedure_grant.md | 2 +- .../snowflake_procedures/data-source.tf | 4 + pkg/datasources/procedures.go | 128 ++++++++++++++++++ pkg/datasources/procedures_acceptance_test.go | 74 ++++++++++ pkg/provider/provider.go | 1 + pkg/resources/grant_helpers.go | 20 +-- pkg/resources/procedure_grant.go | 8 +- pkg/snowflake/procedure.go | 19 +++ 8 files changed, 240 insertions(+), 16 deletions(-) create mode 100644 examples/data-sources/snowflake_procedures/data-source.tf create mode 100644 pkg/datasources/procedures.go create mode 100644 pkg/datasources/procedures_acceptance_test.go diff --git a/docs/resources/procedure_grant.md b/docs/resources/procedure_grant.md index 51a5f245e6..eacb70704b 100644 --- a/docs/resources/procedure_grant.md +++ b/docs/resources/procedure_grant.md @@ -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. diff --git a/examples/data-sources/snowflake_procedures/data-source.tf b/examples/data-sources/snowflake_procedures/data-source.tf new file mode 100644 index 0000000000..1824ceccf2 --- /dev/null +++ b/examples/data-sources/snowflake_procedures/data-source.tf @@ -0,0 +1,4 @@ +data "snowflake_procedures" "current" { + database = "MYDB" + schema = "MYSCHEMA" +} \ No newline at end of file diff --git a/pkg/datasources/procedures.go b/pkg/datasources/procedures.go new file mode 100644 index 0000000000..fd4d965fa2 --- /dev/null +++ b/pkg/datasources/procedures.go @@ -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[^(]+)\((?P[^)]*)\) RETURN (?P.*)`) + 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 +} diff --git a/pkg/datasources/procedures_acceptance_test.go b/pkg/datasources/procedures_acceptance_test.go new file mode 100644 index 0000000000..7b7e04d17a --- /dev/null +++ b/pkg/datasources/procedures_acceptance_test.go @@ -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) +} diff --git a/pkg/provider/provider.go b/pkg/provider/provider.go index 9fa0216839..23480f37f4 100644 --- a/pkg/provider/provider.go +++ b/pkg/provider/provider.go @@ -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 diff --git a/pkg/resources/grant_helpers.go b/pkg/resources/grant_helpers.go index 43f457b301..83d1056256 100644 --- a/pkg/resources/grant_helpers.go +++ b/pkg/resources/grant_helpers.go @@ -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], + }) } } diff --git a/pkg/resources/procedure_grant.go b/pkg/resources/procedure_grant.go index 0535b58d84..0d854e8a27 100644 --- a/pkg/resources/procedure_grant.go +++ b/pkg/resources/procedure_grant.go @@ -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": { @@ -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 { @@ -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.") diff --git a/pkg/snowflake/procedure.go b/pkg/snowflake/procedure.go index 4d6383c99d..c3a2394099 100644 --- a/pkg/snowflake/procedure.go +++ b/pkg/snowflake/procedure.go @@ -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 @@ -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) +}