`postgresql_database` resource provider should now be feature complete.

* Add support to import databases.  See docs.
* Add support for renaming databases
* Add support for all known PostgreSQL database attributes, including:
  * "allow_connections"
  * "lc_ctype"
  * "lc_collate"
  * "connection_limit"
  * "encoding"
  * "is_template"
  * "owner"
  * "tablespace_name"
  * "template"
This commit is contained in:
Sean Chittenden 2016-11-06 01:57:59 -07:00
parent 59f4ad6fd1
commit 5280c37bea
No known key found for this signature in database
GPG Key ID: 4EBC9DC16C2E5E16
4 changed files with 389 additions and 85 deletions

View File

@ -2,7 +2,9 @@ package postgresql
import (
"database/sql"
"errors"
"fmt"
"log"
"strings"
"github.com/hashicorp/errwrap"
@ -10,71 +12,89 @@ import (
"github.com/lib/pq"
)
const (
dbAllowConnsAttr = "allow_connections"
dbCTypeAttr = "lc_ctype"
dbCollationAttr = "lc_collate"
dbConnLimitAttr = "connection_limit"
dbEncodingAttr = "encoding"
dbIsTemplateAttr = "is_template"
dbNameAttr = "name"
dbOwnerAttr = "owner"
dbTablespaceAttr = "tablespace_name"
dbTemplateAttr = "template"
)
func resourcePostgreSQLDatabase() *schema.Resource {
return &schema.Resource{
Create: resourcePostgreSQLDatabaseCreate,
Read: resourcePostgreSQLDatabaseRead,
Update: resourcePostgreSQLDatabaseUpdate,
Delete: resourcePostgreSQLDatabaseDelete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},
Schema: map[string]*schema.Schema{
"name": {
dbNameAttr: {
Type: schema.TypeString,
Required: true,
Description: "The PostgreSQL database name to connect to",
},
"owner": {
dbOwnerAttr: {
Type: schema.TypeString,
Optional: true,
Computed: true,
Description: "The role name of the user who will own the new database",
},
"template": {
dbTemplateAttr: {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
Computed: true,
Description: "The name of the template from which to create the new database",
},
"encoding": {
dbEncodingAttr: {
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
Description: "Character set encoding to use in the new database",
},
"lc_collate": {
dbCollationAttr: {
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
Description: "Collation order (LC_COLLATE) to use in the new database",
},
"lc_ctype": {
dbCTypeAttr: {
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
Description: "Character classification (LC_CTYPE) to use in the new database",
},
"tablespace_name": {
dbTablespaceAttr: {
Type: schema.TypeString,
Optional: true,
Computed: true,
Description: "The name of the tablespace that will be associated with the new database",
},
"connection_limit": {
dbConnLimitAttr: {
Type: schema.TypeInt,
Optional: true,
Computed: true,
Description: "How many concurrent connections can be made to this database",
ValidateFunc: validateConnLimit,
},
"allow_connections": {
dbAllowConnsAttr: {
Type: schema.TypeBool,
Optional: true,
Computed: true,
Description: "If false then no one can connect to this database",
},
"is_template": {
dbIsTemplateAttr: {
Type: schema.TypeBool,
Optional: true,
Computed: true,
@ -93,46 +113,101 @@ func validateConnLimit(v interface{}, key string) (warnings []string, errors []e
}
func resourcePostgreSQLDatabaseCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*Client)
conn, err := client.Connect()
c := meta.(*Client)
conn, err := c.Connect()
if err != nil {
return errwrap.Wrapf("Error connecting to PostgreSQL: {{err}}", err)
}
defer conn.Close()
connUsername := client.username
const numOptions = 9
createOpts := make([]string, 0, numOptions)
stringOpts := []struct {
hclKey string
sqlKey string
}{
{"owner", "OWNER"},
{"template", "TEMPLATE"},
{"encoding", "ENCODING"},
{"lc_collate", "LC_COLLATE"},
{"lc_ctype", "LC_CTYPE"},
{"tablespace_name", "TABLESPACE"},
{dbOwnerAttr, "OWNER"},
{dbTemplateAttr, "TEMPLATE"},
{dbEncodingAttr, "ENCODING"},
{dbCollationAttr, "LC_COLLATE"},
{dbCTypeAttr, "LC_CTYPE"},
{dbTablespaceAttr, "TABLESPACE"},
}
intOpts := []struct {
hclKey string
sqlKey string
}{
{dbConnLimitAttr, "CONNECTION LIMIT"},
}
boolOpts := []struct {
hclKey string
sqlKey string
}{
{dbAllowConnsAttr, "ALLOW_CONNECTIONS"},
{dbIsTemplateAttr, "IS_TEMPLATE"},
}
createOpts := make([]string, 0, len(stringOpts)+len(intOpts)+len(boolOpts))
for _, opt := range stringOpts {
v, ok := d.GetOk(opt.hclKey)
var val string
if !ok {
// Set the owner to the connection username
if opt.hclKey == "owner" && v.(string) == "" {
val = connUsername
} else {
switch {
case opt.hclKey == dbOwnerAttr && v.(string) == "":
// No owner specified in the config, default to using
// the connecting username.
val = c.username
case strings.ToUpper(v.(string)) == "DEFAULT" &&
(opt.hclKey == dbTemplateAttr ||
opt.hclKey == dbEncodingAttr ||
opt.hclKey == dbCollationAttr ||
opt.hclKey == dbCTypeAttr):
// Use the defaults from the template database
// as opposed to best practices.
fallthrough
default:
continue
}
}
val = v.(string)
// Set the owner to the connection username
if opt.hclKey == "owner" && val == "" {
val = connUsername
switch {
case opt.hclKey == dbOwnerAttr && (val == "" || strings.ToUpper(val) == "DEFAULT"):
// Owner was blank/DEFAULT, default to using the connecting username.
val = c.username
d.Set(dbOwnerAttr, val)
case opt.hclKey == dbTablespaceAttr && (val == "" || strings.ToUpper(val) == "DEFAULT"):
val = "pg_default"
d.Set(dbTablespaceAttr, val)
case opt.hclKey == dbTemplateAttr:
if val == "" {
val = "template0"
d.Set(dbTemplateAttr, val)
} else if strings.ToUpper(val) == "DEFAULT" {
val = ""
}
case opt.hclKey == dbEncodingAttr:
if val == "" {
val = "UTF8"
d.Set(dbEncodingAttr, val)
} else if strings.ToUpper(val) == "DEFAULT" {
val = ""
}
case opt.hclKey == dbCollationAttr:
if val == "" {
val = "C"
d.Set(dbCollationAttr, val)
} else if strings.ToUpper(val) == "DEFAULT" {
val = ""
}
case opt.hclKey == dbCTypeAttr:
if val == "" {
val = "C"
d.Set(dbCTypeAttr, val)
} else if strings.ToUpper(val) == "DEFAULT" {
val = ""
}
}
if val != "" {
@ -140,12 +215,6 @@ func resourcePostgreSQLDatabaseCreate(d *schema.ResourceData, meta interface{})
}
}
intOpts := []struct {
hclKey string
sqlKey string
}{
{"connection_limit", "CONNECTION LIMIT"},
}
for _, opt := range intOpts {
v, ok := d.GetOk(opt.hclKey)
if !ok {
@ -156,13 +225,6 @@ func resourcePostgreSQLDatabaseCreate(d *schema.ResourceData, meta interface{})
createOpts = append(createOpts, fmt.Sprintf("%s=%d", opt.sqlKey, val))
}
boolOpts := []struct {
hclKey string
sqlKey string
}{
{"allow_connections", "ALLOW_CONNECTIONS"},
{"is_template", "IS_TEMPLATE"},
}
for _, opt := range boolOpts {
v, ok := d.GetOk(opt.hclKey)
if !ok {
@ -176,7 +238,7 @@ func resourcePostgreSQLDatabaseCreate(d *schema.ResourceData, meta interface{})
createOpts = append(createOpts, fmt.Sprintf("%s=%s", opt.sqlKey, valStr))
}
dbName := d.Get("name").(string)
dbName := d.Get(dbNameAttr).(string)
createStr := strings.Join(createOpts, " ")
if len(createOpts) > 0 {
createStr = " WITH " + createStr
@ -193,22 +255,14 @@ func resourcePostgreSQLDatabaseCreate(d *schema.ResourceData, meta interface{})
}
func resourcePostgreSQLDatabaseDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*Client)
conn, err := client.Connect()
c := meta.(*Client)
conn, err := c.Connect()
if err != nil {
return errwrap.Wrapf("Error connecting to PostgreSQL: {{err}}", err)
}
defer conn.Close()
dbName := d.Get("name").(string)
connUsername := client.username
dbOwner := d.Get("owner").(string)
//needed in order to set the owner of the db if the connection user is not a superuser
err = grantRoleMembership(conn, dbOwner, connUsername)
if err != nil {
return err
}
dbName := d.Get(dbNameAttr).(string)
query := fmt.Sprintf("DROP DATABASE %s", pq.QuoteIdentifier(dbName))
_, err = conn.Query(query)
if err != nil {
@ -221,50 +275,94 @@ func resourcePostgreSQLDatabaseDelete(d *schema.ResourceData, meta interface{})
}
func resourcePostgreSQLDatabaseRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*Client)
conn, err := client.Connect()
c := meta.(*Client)
conn, err := c.Connect()
if err != nil {
return err
}
defer conn.Close()
dbName := d.Get("name").(string)
var owner string
err = conn.QueryRow("SELECT pg_catalog.pg_get_userbyid(d.datdba) from pg_database d WHERE datname=$1", dbName).Scan(&owner)
dbId := d.Id()
var dbName, ownerName string
err = conn.QueryRow("SELECT d.datname, pg_catalog.pg_get_userbyid(d.datdba) from pg_database d WHERE datname=$1", dbId).Scan(&dbName, &ownerName)
switch {
case err == sql.ErrNoRows:
log.Printf("[WARN] PostgreSQL database (%s) not found", d.Id())
d.SetId("")
return nil
case err != nil:
return errwrap.Wrapf("Error reading database: {{err}}", err)
}
var dbEncoding, dbCollation, dbCType, dbTablespaceName string
var dbConnLimit int
var dbAllowConns, dbIsTemplate bool
err = conn.QueryRow(`SELECT pg_catalog.pg_encoding_to_char(d.encoding), d.datcollate, d.datctype, ts.spcname, d.datconnlimit, d.datallowconn, d.datistemplate FROM pg_catalog.pg_database AS d, pg_catalog.pg_tablespace AS ts WHERE d.datname = $1 AND d.dattablespace = ts.oid`, dbId).
Scan(
&dbEncoding, &dbCollation, &dbCType, &dbTablespaceName,
&dbConnLimit, &dbAllowConns, &dbIsTemplate,
)
switch {
case err == sql.ErrNoRows:
log.Printf("[WARN] PostgreSQL database (%s) not found", d.Id())
d.SetId("")
return nil
case err != nil:
return errwrap.Wrapf("Error reading database: {{err}}", err)
default:
d.Set("owner", owner)
d.Set(dbNameAttr, dbName)
d.Set(dbOwnerAttr, ownerName)
d.Set(dbEncodingAttr, dbEncoding)
d.Set(dbCollationAttr, dbCollation)
d.Set(dbCTypeAttr, dbCType)
d.Set(dbTablespaceAttr, dbTablespaceName)
d.Set(dbConnLimitAttr, dbConnLimit)
d.Set(dbAllowConnsAttr, dbAllowConns)
d.Set(dbIsTemplateAttr, dbIsTemplate)
dbTemplate := d.Get(dbTemplateAttr).(string)
if dbTemplate == "" {
dbTemplate = "template0"
}
d.Set(dbTemplateAttr, dbTemplate)
d.SetId(dbName)
return nil
}
}
func resourcePostgreSQLDatabaseUpdate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*Client)
conn, err := client.Connect()
c := meta.(*Client)
conn, err := c.Connect()
if err != nil {
return err
}
defer conn.Close()
dbName := d.Get("name").(string)
if d.HasChange("owner") {
owner := d.Get("owner").(string)
if owner != "" {
query := fmt.Sprintf("ALTER DATABASE %s OWNER TO %s", pq.QuoteIdentifier(dbName), pq.QuoteIdentifier(owner))
_, err := conn.Query(query)
if err != nil {
return errwrap.Wrapf("Error updating owner: {{err}}", err)
}
}
if err := setDBName(conn, d); err != nil {
return err
}
if err := setDBOwner(conn, d); err != nil {
return err
}
if err := setDBTablespace(conn, d); err != nil {
return err
}
if err := setDBConnLimit(conn, d); err != nil {
return err
}
if err := setDBAllowConns(conn, d); err != nil {
return err
}
if err := setDBIsTemplate(conn, d); err != nil {
return err
}
// Empty values: ALTER DATABASE name RESET configuration_parameter;
return resourcePostgreSQLDatabaseRead(d, meta)
}
@ -273,7 +371,7 @@ func grantRoleMembership(conn *sql.DB, dbOwner string, connUsername string) erro
query := fmt.Sprintf("GRANT %s TO %s", pq.QuoteIdentifier(dbOwner), pq.QuoteIdentifier(connUsername))
_, err := conn.Query(query)
if err != nil {
//is already member or role
// is already member or role
if strings.Contains(err.Error(), "duplicate key value violates unique constraint") {
return nil
}
@ -282,3 +380,109 @@ func grantRoleMembership(conn *sql.DB, dbOwner string, connUsername string) erro
}
return nil
}
func setDBName(conn *sql.DB, d *schema.ResourceData) error {
if !d.HasChange(dbNameAttr) {
return nil
}
oraw, nraw := d.GetChange(dbNameAttr)
o := oraw.(string)
n := nraw.(string)
if n == "" {
return errors.New("Error setting database name to an empty string")
}
query := fmt.Sprintf("ALTER DATABASE %s RENAME TO %s", pq.QuoteIdentifier(o), pq.QuoteIdentifier(n))
if _, err := conn.Query(query); err != nil {
return errwrap.Wrapf("Error updating database name: {{err}}", err)
}
d.SetId(n)
return nil
}
func setDBOwner(conn *sql.DB, d *schema.ResourceData) error {
if !d.HasChange(dbOwnerAttr) {
return nil
}
owner := d.Get(dbOwnerAttr).(string)
if owner == "" {
return nil
}
dbName := d.Get(dbNameAttr).(string)
query := fmt.Sprintf("ALTER DATABASE %s OWNER TO %s", pq.QuoteIdentifier(dbName), pq.QuoteIdentifier(owner))
if _, err := conn.Query(query); err != nil {
return errwrap.Wrapf("Error updating database OWNER: {{err}}", err)
}
return nil
}
func setDBTablespace(conn *sql.DB, d *schema.ResourceData) error {
if !d.HasChange(dbTablespaceAttr) {
return nil
}
tbspName := d.Get(dbTablespaceAttr).(string)
dbName := d.Get(dbNameAttr).(string)
var query string
if tbspName == "" || strings.ToUpper(tbspName) == "DEFAULT" {
query = fmt.Sprintf("ALTER DATABASE %s RESET TABLESPACE", pq.QuoteIdentifier(dbName))
} else {
query = fmt.Sprintf("ALTER DATABASE %s SET TABLESPACE %s", pq.QuoteIdentifier(dbName), pq.QuoteIdentifier(tbspName))
}
if _, err := conn.Query(query); err != nil {
return errwrap.Wrapf("Error updating database TABLESPACE: {{err}}", err)
}
return nil
}
func setDBConnLimit(conn *sql.DB, d *schema.ResourceData) error {
if !d.HasChange(dbConnLimitAttr) {
return nil
}
connLimit := d.Get(dbConnLimitAttr).(int)
dbName := d.Get(dbNameAttr).(string)
query := fmt.Sprintf("ALTER DATABASE %s CONNECTION LIMIT = %d", pq.QuoteIdentifier(dbName), connLimit)
if _, err := conn.Query(query); err != nil {
return errwrap.Wrapf("Error updating database CONNECTION LIMIT: {{err}}", err)
}
return nil
}
func setDBAllowConns(conn *sql.DB, d *schema.ResourceData) error {
if !d.HasChange(dbAllowConnsAttr) {
return nil
}
allowConns := d.Get(dbAllowConnsAttr).(bool)
dbName := d.Get(dbNameAttr).(string)
query := fmt.Sprintf("ALTER DATABASE %s ALLOW_CONNECTIONS %t", pq.QuoteIdentifier(dbName), allowConns)
if _, err := conn.Query(query); err != nil {
return errwrap.Wrapf("Error updating database ALLOW_CONNECTIONS: {{err}}", err)
}
return nil
}
func setDBIsTemplate(conn *sql.DB, d *schema.ResourceData) error {
if !d.HasChange(dbIsTemplateAttr) {
return nil
}
isTemplate := d.Get(dbIsTemplateAttr).(bool)
dbName := d.Get(dbNameAttr).(string)
query := fmt.Sprintf("ALTER DATABASE %s IS_TEMPLATE %t", pq.QuoteIdentifier(dbName), isTemplate)
if _, err := conn.Query(query); err != nil {
return errwrap.Wrapf("Error updating database IS_TEMPLATE: {{err}}", err)
}
return nil
}

View File

@ -25,6 +25,26 @@ func TestAccPostgresqlDatabase_Basic(t *testing.T) {
"postgresql_database.mydb", "name", "mydb"),
resource.TestCheckResourceAttr(
"postgresql_database.mydb", "owner", "myrole"),
resource.TestCheckResourceAttr(
"postgresql_database.all_opts", "owner", "myrole"),
resource.TestCheckResourceAttr(
"postgresql_database.all_opts", "name", "all_opts_name"),
resource.TestCheckResourceAttr(
"postgresql_database.all_opts", "template", "template0"),
resource.TestCheckResourceAttr(
"postgresql_database.all_opts", "encoding", "UTF8"),
resource.TestCheckResourceAttr(
"postgresql_database.all_opts", "lc_collate", "C"),
resource.TestCheckResourceAttr(
"postgresql_database.all_opts", "lc_ctype", "C"),
resource.TestCheckResourceAttr(
"postgresql_database.all_opts", "tablespace_name", "pg_default"),
resource.TestCheckResourceAttr(
"postgresql_database.all_opts", "connection_limit", "-1"),
resource.TestCheckResourceAttr(
"postgresql_database.all_opts", "allow_connections", "false"),
resource.TestCheckResourceAttr(
"postgresql_database.all_opts", "is_template", "false"),
),
},
},
@ -134,11 +154,11 @@ resource "postgresql_database" "mydb2" {
owner = "${postgresql_role.myrole.name}"
}
resource "postgresql_database" "mydb3" {
name = "mydb3"
resource "postgresql_database" "all_opts" {
name = "all_opts_name"
owner = "${postgresql_role.myrole.name}"
template = "template1"
encoding = "SQL_ASCII"
template = "template0"
encoding = "UTF8"
lc_collate = "C"
lc_ctype = "C"
tablespace_name = "pg_default"

View File

@ -159,6 +159,9 @@ To make a resource importable, please see the
* openstack_networking_secgroup_v2
* openstack_networking_subnet_v2
### PostgreSQL
* postgresql_database
### Triton

View File

@ -8,8 +8,8 @@ description: |-
# postgresql\_database
The ``postgresql_database`` resource creates and manages a database on a PostgreSQL
server.
The ``postgresql_database`` resource creates and manages a database instance on
a PostgreSQL server.
## Usage
@ -18,13 +18,90 @@ server.
resource "postgresql_database" "my_db" {
name = "my_db"
owner = "my_role"
template = "template0"
collation = "C"
connection_limit = -1
allow_connections = true
}
```
## Argument Reference
* `name` - (Required) The name of the database. Must be unique on the PostgreSQL server instance
where it is configured.
* `name` - (Required) The name of the database. Must be unique on the PostgreSQL
server instance where it is configured.
* `owner` - (Optional) The owner role of the database. If not specified the default is the user executing the command. To create a database owned by another role, you must be a direct or indirect member of that role, or be a superuser.
* `owner` - (Optional) The role name of the user who will own the database, or
`DEFAULT` to use the default (namely, the user executing the command). To
create a database owned by another role or to change the owner of an existing
database, you must be a direct or indirect member of the specified role, or
the username in the provider is a superuser.
* `tablespace_name` - (Optional) The name of the tablespace that will be
associated with the database, or `DEFAULT` to use the template database's
tablespace. This tablespace will be the default tablespace used for objects
created in this database.
* `connection_limit` - (Optional) How many concurrent connections can be made to
this database. `-1` (the default) means no limit.
* `allow_connections` - (Optional) If `false` then no one can connect to this
database. The default is `true`, allowing connections (except as restricted by
other mechanisms, such as `GRANT` or `REVOKE CONNECT`).
* `is_template` - (Optional) If `true`, then this database can be cloned by any
user with `CREATEDB` privileges; if `false` (the default), then only
superusers or the owner of the database can clone it.
* `template` - (Optional) The name of the template database from which to create
the database, or `DEFAULT` to use the default template (`template0`). NOTE:
the default in Terraform is `template0`, not `template1`. Changing this value
will force the creation of a new resource as this value can only be changed
when a database is created.
* `encoding` - (Optional) Character set encoding to use in the database.
Specify a string constant (e.g. `UTF8` or `SQL_ASCII`), or an integer encoding
number. If unset or set to an empty string the default encoding is set to
`UTF8`. If set to `DEFAULT` Terraform will use the same encoding as the
template database. Changing this value will force the creation of a new
resource as this value can only be changed when a database is created.
* `lc_collate` - (Optional) Collation order (`LC_COLLATE`) to use in the
database. This affects the sort order applied to strings, e.g. in queries
with `ORDER BY`, as well as the order used in indexes on text columns. If
unset or set to an empty string the default collation is set to `C`. If set
to `DEFAULT` Terraform will use the same collation order as the specified
`template` database. Changing this value will force the creation of a new
resource as this value can only be changed when a database is created.
* `lc_ctype` - (Optional) Character classification (`LC_CTYPE`) to use in the
database. This affects the categorization of characters, e.g. lower, upper and
digit. If unset or set to an empty string the default character classification
is set to `C`. If set to `DEFAULT` Terraform will use the character
classification of the specified `template` database. Changing this value will
force the creation of a new resource as this value can only be changed when a
database is created.
## Import Example
`postgresql_database` supports importing resources. Supposing the following
Terraform:
```
provider "postgresql" {
alias = "admindb"
}
resource "postgresql_database" "db1" {
provider = "postgresql.admindb"
name = "db1"
}
```
It is possible to import a `postgresql_database` resource with the following
command:
```
$ terraform import postgresql_database.testdb1 testdb1
```