diff --git a/CHANGELOG.md b/CHANGELOG.md index cc68e8908..237bc00b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.2.0 (Unreleased) +IMPROVEMENTS: +* `resource/database_secret_backend_connection`: Add support for configuring Redshift databases ([#1279](https://github.com/hashicorp/terraform-provider-vault/pull/1279)) + ## 3.1.1 (December 22, 2021) BUGS: * Prevent new `entity` read failures when the `VAULT_TOKEN` environment variable is not set ([#1270](https://github.com/hashicorp/terraform-provider-vault/pull/1270)) diff --git a/vault/resource_database_secret_backend_connection.go b/vault/resource_database_secret_backend_connection.go index 9fbbdc7b7..d16925d7e 100644 --- a/vault/resource_database_secret_backend_connection.go +++ b/vault/resource_database_secret_backend_connection.go @@ -19,6 +19,7 @@ import ( type connectionStringConfig struct { excludeUsernameTemplate bool + includeUserPass bool } const ( @@ -36,6 +37,7 @@ const ( dbBackendPostgres = "postgresql" dbBackendOracle = "oracle" dbBackendSnowflake = "snowflake" + dbBackendRedshift = "redshift" ) var ( @@ -56,6 +58,7 @@ var ( dbBackendPostgres, dbBackendOracle, dbBackendSnowflake, + dbBackendRedshift, } ) @@ -388,11 +391,20 @@ func databaseSecretBackendConnectionResource() *schema.Resource { ConflictsWith: util.CalculateConflictsWith(dbBackendOracle, dbBackendTypes), }, + "redshift": { + Type: schema.TypeList, + Optional: true, + Description: "Connection parameters for the redshift-database-plugin plugin.", + Elem: connectionStringResource(&connectionStringConfig{includeUserPass: true}), + MaxItems: 1, + ConflictsWith: util.CalculateConflictsWith(dbBackendRedshift, dbBackendTypes), + }, + "snowflake": { Type: schema.TypeList, Optional: true, Description: "Connection parameters for the snowflake-database-plugin plugin.", - Elem: snowflakeConnectionStringResource(), + Elem: connectionStringResource(&connectionStringConfig{includeUserPass: true}), MaxItems: 1, ConflictsWith: util.CalculateConflictsWith(dbBackendSnowflake, dbBackendTypes), }, @@ -437,6 +449,19 @@ func connectionStringResource(config *connectionStringConfig) *schema.Resource { }, }, } + if config.includeUserPass { + res.Schema["username"] = &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Description: "The root credential username used in the connection URL", + } + res.Schema["password"] = &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Description: "The root credential password used in the connection URL", + Sensitive: true, + } + } if !config.excludeUsernameTemplate { res.Schema["username_template"] = &schema.Schema{ @@ -475,22 +500,6 @@ func mssqlConnectionStringResource() *schema.Resource { return r } -func snowflakeConnectionStringResource() *schema.Resource { - r := connectionStringResource(&connectionStringConfig{}) - 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, - } - return r -} - func getDatabasePluginName(d *schema.ResourceData) (string, error) { switch { case len(d.Get(dbBackendCassandra).([]interface{})) > 0: @@ -521,6 +530,8 @@ func getDatabasePluginName(d *schema.ResourceData) (string, error) { return "elasticsearch-database-plugin", nil case len(d.Get(dbBackendSnowflake).([]interface{})) > 0: return "snowflake-database-plugin", nil + case len(d.Get(dbBackendRedshift).([]interface{})) > 0: + return "redshift-database-plugin", nil default: return "", fmt.Errorf("at least one database plugin must be configured") } @@ -608,7 +619,9 @@ func getDatabaseAPIData(d *schema.ResourceData) (map[string]interface{}, error) case "elasticsearch-database-plugin": setElasticsearchDatabaseConnectionData(d, "elasticsearch.0.", data) case "snowflake-database-plugin": - setSnowflakeDatabaseConnectionData(d, "snowflake.0.", data) + setDatabaseConnectionDataWithUserPass(d, "snowflake.0.", data) + case "redshift-database-plugin": + setDatabaseConnectionDataWithUserPass(d, "redshift.0.", data) } return data, nil @@ -907,19 +920,14 @@ func setInfluxDBDatabaseConnectionData(d *schema.ResourceData, prefix string, da } } -func setSnowflakeDatabaseConnectionData(d *schema.ResourceData, prefix string, data map[string]interface{}) { +func setDatabaseConnectionDataWithUserPass(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 { diff --git a/vault/resource_database_secret_backend_connection_test.go b/vault/resource_database_secret_backend_connection_test.go index fc48612d5..d30286689 100644 --- a/vault/resource_database_secret_backend_connection_test.go +++ b/vault/resource_database_secret_backend_connection_test.go @@ -687,6 +687,41 @@ func TestAccDatabaseSecretBackendConnection_snowflake(t *testing.T) { }) } +func TestAccDatabaseSecretBackendConnection_redshift(t *testing.T) { + MaybeSkipDBTests(t, dbBackendRedshift) + + url := os.Getenv("REDSHIFT_URL") + if url == "" { + t.Skip("REDSHIFT_URL not set") + } + backend := acctest.RandomWithPrefix("tf-test-db") + name := acctest.RandomWithPrefix("db") + resource.Test(t, resource.TestCase{ + Providers: testProviders, + PreCheck: func() { testutil.TestAccPreCheck(t) }, + CheckDestroy: testAccDatabaseSecretBackendConnectionCheckDestroy, + Steps: []resource.TestStep{ + { + Config: testAccDatabaseSecretBackendConnectionConfig_redshift(name, backend, url), + 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", "root_rotation_statements.#", "1"), + resource.TestCheckResourceAttr("vault_database_secret_backend_connection.test", "root_rotation_statements.0", "FOOBAR"), + resource.TestCheckResourceAttr("vault_database_secret_backend_connection.test", "verify_connection", "true"), + resource.TestCheckResourceAttr("vault_database_secret_backend_connection.test", "redshift.0.connection_url", url), + resource.TestCheckResourceAttr("vault_database_secret_backend_connection.test", "redshift.0.max_open_connections", "2"), + resource.TestCheckResourceAttr("vault_database_secret_backend_connection.test", "redshift.0.max_idle_connections", "0"), + resource.TestCheckResourceAttr("vault_database_secret_backend_connection.test", "redshift.0.max_connection_lifetime", "0"), + ), + }, + }, + }) +} + func testAccDatabaseSecretBackendConnectionCheckDestroy(s *terraform.State) error { client := testProvider.Meta().(*api.Client) @@ -1078,6 +1113,24 @@ resource "vault_database_secret_backend_connection" "test" { `, path, name, url, username, password, userTempl) } +func testAccDatabaseSecretBackendConnectionConfig_redshift(name, path, connURL 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"] + redshift { + connection_url = "%s" + } +} +`, path, name, connURL) +} + 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 f0e1f944a..6944652d2 100644 --- a/website/docs/r/database_secret_backend_connection.md +++ b/website/docs/r/database_secret_backend_connection.md @@ -295,6 +295,27 @@ See the [Vault * `username_template` - (Optional) - [Template](https://www.vaultproject.io/docs/concepts/username-templating) describing how dynamic usernames are generated. +### Redshift Configuration Options + +* `connection_url` - (Required) Specifies the Redshift DSN. See + the [Vault + docs](https://www.vaultproject.io/api-docs/secret/databases/redshift#sample-payload) + for an example. + +* `max_open_connections` - (Optional) The maximum number of open connections to + the database. + +* `max_idle_connections` - (Optional) The maximum number of idle connections to + the database. + +* `max_connection_lifetime` - (Optional) The maximum amount of time a connection may be reused. + +* `username` - (Optional) The root credential username used in the connection URL. + +* `password` - (Optional) The root credential password used in the connection URL. + +* `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.