diff --git a/vault/resource_database_secret_backend_connection.go b/vault/resource_database_secret_backend_connection.go index 61e6075e3..aa4c8b007 100644 --- a/vault/resource_database_secret_backend_connection.go +++ b/vault/resource_database_secret_backend_connection.go @@ -16,7 +16,7 @@ import ( var ( databaseSecretBackendConnectionBackendFromPathRegex = regexp.MustCompile("^(.+)/config/.+$") databaseSecretBackendConnectionNameFromPathRegex = regexp.MustCompile("^.+/config/(.+$)") - dbBackendTypes = []string{"cassandra", "hana", "mongodb", "mssql", "mysql", "mysql_rds", "mysql_aurora", "mysql_legacy", "postgresql", "oracle", "elasticsearch"} + dbBackendTypes = []string{"cassandra", "hana", "mongodb", "mssql", "mysql", "mysql_rds", "mysql_aurora", "mysql_legacy", "postgresql", "oracle", "elasticsearch", "snowflake"} ) func databaseSecretBackendConnectionResource() *schema.Resource { @@ -273,6 +273,15 @@ func databaseSecretBackendConnectionResource() *schema.Resource { ConflictsWith: util.CalculateConflictsWith("oracle", dbBackendTypes), }, + "snowflake": { + Type: schema.TypeList, + Optional: true, + Description: "Connection parameters for the snowflake-database-plugin plugin.", + Elem: snowflakeConnectionStringResource(), + MaxItems: 1, + ConflictsWith: util.CalculateConflictsWith("snowflake", dbBackendTypes), + }, + "backend": { Type: schema.TypeString, Required: true, @@ -331,6 +340,28 @@ func mysqlConnectionStringResource() *schema.Resource { return r } +func snowflakeConnectionStringResource() *schema.Resource { + r := connectionStringResource() + r.Schema["username"] = &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Description: "The AccountAdmin level user using to connect to snowflake", + } + r.Schema["password"] = &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Description: "The password with the provided user", + Sensitive: true, + } + r.Schema["username_template"] = &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Description: "Template describing how dynamic usernames are generated.", + Sensitive: false, + } + return r +} + func getDatabasePluginName(d *schema.ResourceData) (string, error) { switch { case len(d.Get("cassandra").([]interface{})) > 0: @@ -357,6 +388,8 @@ func getDatabasePluginName(d *schema.ResourceData) (string, error) { return "postgresql-database-plugin", nil case len(d.Get("elasticsearch").([]interface{})) > 0: return "elasticsearch-database-plugin", nil + case len(d.Get("snowflake").([]interface{})) > 0: + return "snowflake-database-plugin", nil default: return "", fmt.Errorf("at least one database plugin must be configured") } @@ -441,6 +474,8 @@ func getDatabaseAPIData(d *schema.ResourceData) (map[string]interface{}, error) setDatabaseConnectionData(d, "postgresql.0.", data) case "elasticsearch-database-plugin": setElasticsearchDatabaseConnectionData(d, "elasticsearch.0.", data) + case "snowflake-database-plugin": + setSnowflakeDatabaseConnectionData(d, "snowflake.0.", data) } return data, nil @@ -540,6 +575,38 @@ func getElasticsearchConnectionDetailsFromResponse(d *schema.ResourceData, prefi return []map[string]interface{}{result} } +func getSnowflakeConnectionDetailsFromResponse(d *schema.ResourceData, prefix string, resp *api.Secret) []map[string]interface{} { + commonDetails := getConnectionDetailsFromResponse(d, prefix, resp) + details := resp.Data["connection_details"] + data, ok := details.(map[string]interface{}) + if !ok { + return nil + } + result := commonDetails[0] + + if v, ok := data["username"]; ok { + result["username"] = v.(string) + } + + if v, ok := d.GetOk(prefix + "password"); ok { + result["password"] = v.(string) + } else { + if v, ok := data["password"]; ok { + result["password"] = v.(string) + } + } + + if v, ok := d.GetOk(prefix + "username_template"); ok { + result["username_template"] = v.(string) + } else { + if v, ok := data["username_template"]; ok { + result["username_template"] = v.(string) + } + } + + return []map[string]interface{}{result} +} + func setDatabaseConnectionData(d *schema.ResourceData, prefix string, data map[string]interface{}) { if v, ok := d.GetOk(prefix + "connection_url"); ok { data["connection_url"] = v.(string) @@ -579,6 +646,21 @@ func setElasticsearchDatabaseConnectionData(d *schema.ResourceData, prefix strin } } +func setSnowflakeDatabaseConnectionData(d *schema.ResourceData, prefix string, data map[string]interface{}) { + setDatabaseConnectionData(d, prefix, data) + if v, ok := d.GetOk(prefix + "username"); ok { + data["username"] = v.(string) + } + + if v, ok := d.GetOk(prefix + "password"); ok { + data["password"] = v.(string) + } + + if v, ok := d.GetOk(prefix + "username_template"); ok { + data["username_template"] = v.(string) + } +} + func databaseSecretBackendConnectionCreate(d *schema.ResourceData, meta interface{}) error { client := meta.(*api.Client) @@ -748,6 +830,8 @@ func databaseSecretBackendConnectionRead(d *schema.ResourceData, meta interface{ d.Set("postgresql", getConnectionDetailsFromResponse(d, "postgresql.0.", resp)) case "elasticsearch-database-plugin": d.Set("elasticsearch", getElasticsearchConnectionDetailsFromResponse(d, "elasticsearch.0.", resp)) + case "snowflake-database-plugin": + d.Set("snowflake", getSnowflakeConnectionDetailsFromResponse(d, "snowflake.0.", resp)) } if err != nil { diff --git a/vault/resource_database_secret_backend_connection_test.go b/vault/resource_database_secret_backend_connection_test.go index 8f9fa2997..a5d59b9b2 100644 --- a/vault/resource_database_secret_backend_connection_test.go +++ b/vault/resource_database_secret_backend_connection_test.go @@ -571,6 +571,40 @@ func TestAccDatabaseSecretBackendConnection_elasticsearch(t *testing.T) { }) } +func TestAccDatabaseSecretBackendConnection_snowflake(t *testing.T) { + url := os.Getenv("SNOWFLAKE_URL") + if url == "" { + t.Skip("SNOWFLAKE_URL not set") + } + username := os.Getenv("SNOWFLAKE_USERNAME") + password := os.Getenv("SNOWFLAKE_PASSWORD") + backend := acctest.RandomWithPrefix("tf-test-db") + name := acctest.RandomWithPrefix("db") + + config := testAccDatabaseSecretBackendConnectionConfig_snowflake(name, backend, url, username, password) + resource.Test(t, resource.TestCase{ + Providers: testProviders, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccDatabaseSecretBackendConnectionCheckDestroy, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("vault_database_secret_backend_connection.test", "name", name), + resource.TestCheckResourceAttr("vault_database_secret_backend_connection.test", "backend", backend), + resource.TestCheckResourceAttr("vault_database_secret_backend_connection.test", "allowed_roles.#", "2"), + resource.TestCheckResourceAttr("vault_database_secret_backend_connection.test", "allowed_roles.0", "dev"), + resource.TestCheckResourceAttr("vault_database_secret_backend_connection.test", "allowed_roles.1", "prod"), + resource.TestCheckResourceAttr("vault_database_secret_backend_connection.test", "verify_connection", "true"), + resource.TestCheckResourceAttr("vault_database_secret_backend_connection.test", "snowflake.0.connection_url", url), + resource.TestCheckResourceAttr("vault_database_secret_backend_connection.test", "snowflake.0.username", username), + resource.TestCheckResourceAttr("vault_database_secret_backend_connection.test", "snowflake.0.password", password), + ), + }, + }, + }) +} + func testAccDatabaseSecretBackendConnectionCheckDestroy(s *terraform.State) error { client := testProvider.Meta().(*api.Client) @@ -904,6 +938,28 @@ resource "vault_database_secret_backend_connection" "test" { `, path, name, connURL) } +func testAccDatabaseSecretBackendConnectionConfig_snowflake(name, path, url, username, password string) string { + return fmt.Sprintf(` +resource "vault_mount" "db" { + path = "%s" + type = "database" +} + +resource "vault_database_secret_backend_connection" "test" { + backend = "${vault_mount.db.path}" + name = "%s" + allowed_roles = ["dev", "prod"] + root_rotation_statements = ["FOOBAR"] + + snowflake { + connection_url = "%s" + username = "%s" + password = "%s" + } +} +`, path, name, url, username, password) +} + func newMySQLConnection(t *testing.T, connURL string, username string, password string) *sql.DB { dbURL := dbutil.QueryHelper(connURL, map[string]string{ "username": username, diff --git a/website/docs/r/database_secret_backend_connection.md b/website/docs/r/database_secret_backend_connection.md index b18268318..a0b533a2f 100644 --- a/website/docs/r/database_secret_backend_connection.md +++ b/website/docs/r/database_secret_backend_connection.md @@ -79,6 +79,8 @@ The following arguments are supported: * `elasticsearch` - (Optional) A nested block containing configuration options for Elasticsearch connections. +* `snowflake` - (Optional) A nested block containing configuration options for Snowflake connections. + Exactly one of the nested blocks of configuration options must be supplied. ### Cassandra Configuration Options @@ -217,6 +219,28 @@ Exactly one of the nested blocks of configuration options must be supplied. * `password` - (Required) The password to be used in the connection. +### Snowflake Configuration Options + +* `connection_url` - (Required) A URL containing connection information. See + the [Vault + docs](https://www.vaultproject.io/api-docs/secret/databases/snowflake#sample-payload) + for an example. + +* `max_open_connections` - (Optional) The maximum number of open connections to + use. + +* `max_idle_connections` - (Optional) The maximum number of idle connections to + maintain. + +* `max_connection_lifetime` - (Optional) The maximum number of seconds to keep + a connection alive for. + +* `username` - (Optional) The username to be used in the connection (the account admin level). + +* `password` - (Optional) The password to be used in the connection. + +* `username_template` - (Optional) - [Template](https://www.vaultproject.io/docs/concepts/username-templating) describing how dynamic usernames are generated. + ## Attributes Reference No additional attributes are exported by this resource.