diff --git a/docs/data-sources/database.md b/docs/data-sources/database.md new file mode 100644 index 0000000000..8d4e198cda --- /dev/null +++ b/docs/data-sources/database.md @@ -0,0 +1,43 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "snowflake_database Data Source - terraform-provider-snowflake" +subcategory: "" +description: |- + +--- + +# snowflake_database (Data Source) + + + +## Example Usage + +```terraform +data "snowflake_database" "this" { + name = "DEMO_DB" +} +``` + + +## Schema + +### Required + +- **name** (String) The database from which to return its metadata. + +### Optional + +- **id** (String) The ID of this resource. + +### Read-Only + +- **comment** (String) +- **created_on** (String) +- **is_current** (Boolean) +- **is_default** (Boolean) +- **options** (String) +- **origin** (String) +- **owner** (String) +- **retention_time** (Number) + + diff --git a/docs/data-sources/databases.md b/docs/data-sources/databases.md new file mode 100644 index 0000000000..8ab04cc578 --- /dev/null +++ b/docs/data-sources/databases.md @@ -0,0 +1,45 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "snowflake_databases Data Source - terraform-provider-snowflake" +subcategory: "" +description: |- + +--- + +# snowflake_databases (Data Source) + + + +## Example Usage + +```terraform +data "snowflake_databases" "this" {} +``` + + +## Schema + +### Optional + +- **id** (String) The ID of this resource. + +### Read-Only + +- **databases** (List of Object) Snowflake databases (see [below for nested schema](#nestedatt--databases)) + + +### Nested Schema for `databases` + +Read-Only: + +- **comment** (String) +- **created_on** (String) +- **is_current** (Boolean) +- **is_default** (Boolean) +- **name** (String) +- **options** (String) +- **origin** (String) +- **owner** (String) +- **retention_time** (Number) + + diff --git a/examples/data-sources/snowflake_database/data-source.tf b/examples/data-sources/snowflake_database/data-source.tf new file mode 100644 index 0000000000..ff46367580 --- /dev/null +++ b/examples/data-sources/snowflake_database/data-source.tf @@ -0,0 +1,3 @@ +data "snowflake_database" "this" { + name = "DEMO_DB" +} diff --git a/examples/data-sources/snowflake_databases/data-source.tf b/examples/data-sources/snowflake_databases/data-source.tf new file mode 100644 index 0000000000..289c9d3563 --- /dev/null +++ b/examples/data-sources/snowflake_databases/data-source.tf @@ -0,0 +1 @@ +data "snowflake_databases" "this" {} diff --git a/pkg/datasources/database.go b/pkg/datasources/database.go new file mode 100644 index 0000000000..039ef6f053 --- /dev/null +++ b/pkg/datasources/database.go @@ -0,0 +1,94 @@ +package datasources + +import ( + "database/sql" + "log" + "strconv" + + "github.com/chanzuckerberg/terraform-provider-snowflake/pkg/snowflake" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/jmoiron/sqlx" +) + +var databaseSchema = map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + Description: "The database from which to return its metadata.", + }, + "comment": { + Type: schema.TypeString, + Computed: true, + }, + "owner": { + Type: schema.TypeString, + Computed: true, + }, + "is_default": { + Type: schema.TypeBool, + Computed: true, + }, + "is_current": { + Type: schema.TypeBool, + Computed: true, + }, + "origin": { + Type: schema.TypeString, + Computed: true, + }, + "retention_time": { + Type: schema.TypeInt, + Computed: true, + }, + "created_on": { + Type: schema.TypeString, + Computed: true, + }, + "options": { + Type: schema.TypeString, + Computed: true, + }, +} + +// Database the Snowflake Database resource +func Database() *schema.Resource { + return &schema.Resource{ + Read: ReadDatabase, + Schema: databaseSchema, + } +} + +// ReadDatabase read the database meta-data information +func ReadDatabase(d *schema.ResourceData, meta interface{}) error { + db := meta.(*sql.DB) + dbx := sqlx.NewDb(db, "snowflake") + log.Printf("[DEBUG] database: %v", d.Get("name")) + dbData, err := snowflake.ListDatabase(dbx, d.Get("name").(string)) + if err != nil { + log.Printf("[DEBUG] list database failed to decode") + d.SetId("") + return nil + } + if dbData == nil || !dbData.DBName.Valid { + log.Printf("[DEBUG] database not found") + d.SetId("") + return nil + } + log.Printf("[DEBUG] list database: %v", dbData) + d.SetId(dbData.DBName.String) + d.Set("comment", dbData.Comment.String) + d.Set("owner", dbData.Owner.String) + d.Set("is_default", dbData.IsDefault.String == "Y") + d.Set("is_current", dbData.IsCurrent.String == "Y") + d.Set("origin", dbData.Origin.String) + d.Set("created_on", dbData.CreatedOn.String) + d.Set("options", dbData.Options.String) + d.Set("retention_time", -1) + if dbData.RetentionTime.Valid { + v, err := strconv.Atoi(dbData.RetentionTime.String) + if err == nil { + d.Set("retention_time", v) + } + } + return nil +} diff --git a/pkg/datasources/database_acceptance_test.go b/pkg/datasources/database_acceptance_test.go new file mode 100644 index 0000000000..910e8b0805 --- /dev/null +++ b/pkg/datasources/database_acceptance_test.go @@ -0,0 +1,46 @@ +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 TestDatabase(t *testing.T) { + databaseName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) + comment := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) + resource.ParallelTest(t, resource.TestCase{ + + Providers: providers(), + Steps: []resource.TestStep{ + { + Config: database(databaseName, comment), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.snowflake_database.t", "name", databaseName), + resource.TestCheckResourceAttr("data.snowflake_database.t", "comment", comment), + resource.TestCheckResourceAttrSet("data.snowflake_database.t", "created_on"), + resource.TestCheckResourceAttrSet("data.snowflake_database.t", "owner"), + resource.TestCheckResourceAttrSet("data.snowflake_database.t", "retention_time"), + resource.TestCheckResourceAttrSet("data.snowflake_database.t", "is_current"), + resource.TestCheckResourceAttrSet("data.snowflake_database.t", "is_default"), + ), + }, + }, + }) +} + +func database(databaseName, comment string) string { + return fmt.Sprintf(` + resource snowflake_database "test_database" { + name = "%v" + comment = "%v" + } + data snowflake_database "t" { + depends_on = [snowflake_database.test_database] + name = "%v" + } + `, databaseName, comment, databaseName) +} diff --git a/pkg/datasources/databases.go b/pkg/datasources/databases.go new file mode 100644 index 0000000000..0f26240902 --- /dev/null +++ b/pkg/datasources/databases.go @@ -0,0 +1,107 @@ +package datasources + +import ( + "database/sql" + "log" + "strconv" + + "github.com/chanzuckerberg/terraform-provider-snowflake/pkg/snowflake" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/jmoiron/sqlx" +) + +var databasesSchema = map[string]*schema.Schema{ + "databases": { + Type: schema.TypeList, + Computed: true, + Description: "Snowflake databases", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Computed: true, + }, + "comment": { + Type: schema.TypeString, + Computed: true, + }, + "owner": { + Type: schema.TypeString, + Computed: true, + }, + "is_default": { + Type: schema.TypeBool, + Computed: true, + }, + "is_current": { + Type: schema.TypeBool, + Computed: true, + }, + "origin": { + Type: schema.TypeString, + Computed: true, + }, + "retention_time": { + Type: schema.TypeInt, + Computed: true, + }, + "created_on": { + Type: schema.TypeString, + Computed: true, + }, + "options": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, +} + +// Databases the Snowflake current account resource +func Databases() *schema.Resource { + return &schema.Resource{ + Read: ReadDatabases, + Schema: databasesSchema, + } +} + +// ReadDatabases read the current snowflake account information +func ReadDatabases(d *schema.ResourceData, meta interface{}) error { + db := meta.(*sql.DB) + dbx := sqlx.NewDb(db, "snowflake") + dbs, err := snowflake.ListDatabases(dbx) + if err != nil { + log.Printf("[DEBUG] list databases failed to decode") + d.SetId("") + return nil + } + log.Printf("[DEBUG] list databases: %v", dbs) + d.SetId("databases_read") + databases := []map[string]interface{}{} + for _, db := range dbs { + dbR := map[string]interface{}{} + if !db.DBName.Valid { + continue + } + dbR["name"] = db.DBName.String + dbR["comment"] = db.Comment.String + dbR["owner"] = db.Owner.String + dbR["is_default"] = db.IsDefault.String == "Y" + dbR["is_current"] = db.IsCurrent.String == "Y" + dbR["origin"] = db.Origin.String + dbR["created_on"] = db.CreatedOn.String + dbR["options"] = db.Options.String + dbR["retention_time"] = -1 + if db.RetentionTime.Valid { + v, err := strconv.Atoi(db.RetentionTime.String) + if err == nil { + dbR["retention_time"] = v + } + } + databases = append(databases, dbR) + + } + d.Set("databases", databases) + return nil +} diff --git a/pkg/datasources/databases_acceptance_test.go b/pkg/datasources/databases_acceptance_test.go new file mode 100644 index 0000000000..86a360cbc2 --- /dev/null +++ b/pkg/datasources/databases_acceptance_test.go @@ -0,0 +1,100 @@ +package datasources_test + +import ( + "fmt" + "strconv" + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestDatabases(t *testing.T) { + databaseName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) + comment := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) + resource.ParallelTest(t, resource.TestCase{ + + Providers: providers(), + Steps: []resource.TestStep{ + { + Config: databases(databaseName, comment), + Check: resource.ComposeTestCheckFunc( + testAccCheckDatabases(databaseName, comment), + ), + }, + }, + }) +} + +func databases(databaseName, comment string) string { + return fmt.Sprintf(` + resource snowflake_database "test_database" { + name = "%v" + comment = "%v" + } + data snowflake_databases "t" { + depends_on = [snowflake_database.test_database] + } + `, databaseName, comment) +} + +func testAccCheckDatabases(databaseName string, comment string) resource.TestCheckFunc { + return func(s *terraform.State) error { + resourceState := s.Modules[0].Resources["data.snowflake_databases.t"] + if resourceState == nil { + return fmt.Errorf("resource not found in state") + } + instanceState := resourceState.Primary + if instanceState == nil { + return fmt.Errorf("resource has no primary instance") + } + if instanceState.ID != "databases_read" { + return fmt.Errorf("expected ID to be 'databases_read', got %s", instanceState.ID) + } + nDbs, err := strconv.Atoi(instanceState.Attributes["databases.#"]) + if err != nil { + return fmt.Errorf("expected a number for field 'databases', got %s", instanceState.Attributes["databases.#"]) + } + if nDbs == 0 { + return fmt.Errorf("expected databases to be greater or equal to 1, got %s", instanceState.Attributes["databases.#"]) + } + dbIdx := -1 + for i := 0; i < nDbs; i++ { + idxName := fmt.Sprintf("databases.%d.name", i) + if instanceState.Attributes[idxName] == databaseName { + dbIdx = i + break + } + } + if dbIdx == -1 { + return fmt.Errorf("database %s not found", databaseName) + } + idxComment := fmt.Sprintf("databases.%d.comment", dbIdx) + if instanceState.Attributes[idxComment] != comment { + return fmt.Errorf("expected comment '%s', got '%s'", comment, instanceState.Attributes[idxComment]) + } + idxCreatedOn := fmt.Sprintf("databases.%d.created_on", dbIdx) + if instanceState.Attributes[idxCreatedOn] == "" { + return fmt.Errorf("expected 'created_on' to be set") + } + idxOwner := fmt.Sprintf("databases.%d.owner", dbIdx) + if instanceState.Attributes[idxOwner] == "" { + return fmt.Errorf("expected 'owner' to be set") + } + idxRetentionTime := fmt.Sprintf("databases.%d.retention_time", dbIdx) + if instanceState.Attributes[idxRetentionTime] == "" { + return fmt.Errorf("expected 'retention_time' to be set") + } + idxIsCurrent := fmt.Sprintf("databases.%d.is_current", dbIdx) + if instanceState.Attributes[idxIsCurrent] == "" { + return fmt.Errorf("expected 'is_current' to be set") + } + idxIsDefault := fmt.Sprintf("databases.%d.is_default", dbIdx) + if instanceState.Attributes[idxIsDefault] == "" { + return fmt.Errorf("expected 'is_default' to be set") + } + return nil + } +} diff --git a/pkg/provider/provider.go b/pkg/provider/provider.go index 9887691c69..7308724486 100644 --- a/pkg/provider/provider.go +++ b/pkg/provider/provider.go @@ -235,6 +235,8 @@ func getDataSources() map[string]*schema.Resource { "snowflake_row_access_policies": datasources.RowAccessPolicies(), "snowflake_functions": datasources.Functions(), "snowflake_procedures": datasources.Procedures(), + "snowflake_databases": datasources.Databases(), + "snowflake_database": datasources.Database(), } return dataSources diff --git a/pkg/snowflake/database.go b/pkg/snowflake/database.go index b234097979..3ef4f82437 100644 --- a/pkg/snowflake/database.go +++ b/pkg/snowflake/database.go @@ -110,3 +110,27 @@ func ListDatabases(sdb *sqlx.DB) ([]database, error) { } return dbs, errors.Wrapf(err, "unable to scan row for %s", stmt) } + +func ListDatabase(sdb *sqlx.DB, databaseName string) (*database, error) { + stmt := fmt.Sprintf("SHOW DATABASES LIKE '%s'", databaseName) + rows, err := sdb.Queryx(stmt) + if err != nil { + return nil, err + } + defer rows.Close() + + dbs := []database{} + err = sqlx.StructScan(rows, &dbs) + if err == sql.ErrNoRows || len(dbs) == 0 { + log.Printf("[DEBUG] no databases found") + return nil, nil + } + db := &database{} + for _, d := range dbs { + if d.DBName.String == databaseName { + db = &d + break + } + } + return db, errors.Wrapf(err, "unable to scan row for %s", stmt) +}