Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable Snowflake as a Database Resource #983

Merged
merged 12 commits into from
Aug 31, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 85 additions & 1 deletion vault/resource_database_secret_backend_connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand All @@ -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")
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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 {
Expand Down
56 changes: 56 additions & 0 deletions vault/resource_database_secret_backend_connection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,40 @@ func TestAccDatabaseSecretBackendConnection_elasticsearch(t *testing.T) {
})
}

func TestAccDatabaseSecretBackendConnection_snowflake(t *testing.T) {
url := os.Getenv("SNOWFLAKE_URL")
bjsemrad marked this conversation as resolved.
Show resolved Hide resolved
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)

Expand Down Expand Up @@ -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,
Expand Down
24 changes: 24 additions & 0 deletions website/docs/r/database_secret_backend_connection.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down