Merge pull request #10682 from hashicorp/f-fixup-postgresql

Various changes to the PostgreSQL provider
This commit is contained in:
James Nugent 2016-12-12 15:22:53 -08:00 committed by GitHub
commit 7cda9e8c74
18 changed files with 1821 additions and 223 deletions

View File

@ -0,0 +1,28 @@
POSTGRES?=/opt/local/lib/postgresql96/bin/postgres
PSQL?=/opt/local/lib/postgresql96/bin/psql
PGDATA?=$(GOPATH)/src/github.com/hashicorp/terraform/builtin/providers/postgresql/data
initdb::
/opt/local/lib/postgresql96/bin/initdb --no-locale -U postgres -D $(PGDATA)
startdb::
2>&1 \
$(POSTGRES) \
-D $(PGDATA) \
-c log_connections=on \
-c log_disconnections=on \
-c log_duration=on \
-c log_statement=all \
| tee postgresql.log
cleandb::
rm -rf $(PGDATA)
freshdb:: cleandb initdb startdb
test::
2>&1 PGSSLMODE=disable PGHOST=/tmp PGUSER=postgres make -C ../../.. testacc TEST=./builtin/providers/postgresql | tee test.log
psql::
$(PSQL) -E postgres postgres

View File

@ -3,6 +3,7 @@ package postgresql
import ( import (
"database/sql" "database/sql"
"fmt" "fmt"
"log"
_ "github.com/lib/pq" //PostgreSQL db _ "github.com/lib/pq" //PostgreSQL db
) )
@ -11,10 +12,13 @@ import (
type Config struct { type Config struct {
Host string Host string
Port int Port int
Database string
Username string Username string
Password string Password string
SslMode string SSLMode string
ApplicationName string
Timeout int Timeout int
ConnectTimeoutSec int
} }
// Client struct holding connection string // Client struct holding connection string
@ -25,8 +29,14 @@ type Client struct {
// NewClient returns new client config // NewClient returns new client config
func (c *Config) NewClient() (*Client, error) { func (c *Config) NewClient() (*Client, error) {
connStr := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=postgres sslmode=%s connect_timeout=%d", c.Host, c.Port, c.Username, c.Password, c.SslMode, c.Timeout) // NOTE: dbname must come before user otherwise dbname will be set to
// user.
const dsnFmt = "host=%s port=%d dbname=%s user=%s password=%s sslmode=%s fallback_application_name=%s connect_timeout=%d"
logDSN := fmt.Sprintf(dsnFmt, c.Host, c.Port, c.Database, c.Username, "<redacted>", c.SSLMode, c.ApplicationName, c.ConnectTimeoutSec)
log.Printf("[INFO] PostgreSQL DSN: `%s`", logDSN)
connStr := fmt.Sprintf(dsnFmt, c.Host, c.Port, c.Database, c.Username, c.Password, c.SSLMode, c.ApplicationName, c.ConnectTimeoutSec)
client := Client{ client := Client{
connStr: connStr, connStr: connStr,
username: c.Username, username: c.Username,

View File

@ -0,0 +1,24 @@
package postgresql
import (
"fmt"
"strings"
)
// pqQuoteLiteral returns a string literal safe for inclusion in a PostgreSQL
// query as a parameter. The resulting string still needs to be wrapped in
// single quotes in SQL (i.e. fmt.Sprintf(`'%s'`, pqQuoteLiteral("str"))). See
// quote_literal_internal() in postgresql/backend/utils/adt/quote.c:77.
func pqQuoteLiteral(in string) string {
in = strings.Replace(in, `\`, `\\`, -1)
in = strings.Replace(in, `'`, `''`, -1)
return in
}
func validateConnLimit(v interface{}, key string) (warnings []string, errors []error) {
value := v.(int)
if value < -1 {
errors = append(errors, fmt.Errorf("%s can not be less than -1", key))
}
return
}

View File

@ -1,6 +1,9 @@
package postgresql package postgresql
import ( import (
"bytes"
"fmt"
"github.com/hashicorp/errwrap" "github.com/hashicorp/errwrap"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
@ -12,61 +15,89 @@ func Provider() terraform.ResourceProvider {
Schema: map[string]*schema.Schema{ Schema: map[string]*schema.Schema{
"host": { "host": {
Type: schema.TypeString, Type: schema.TypeString,
Required: true, Optional: true,
DefaultFunc: schema.MultiEnvDefaultFunc([]string{"PGHOST", "POSTGRESQL_HOST"}, nil), DefaultFunc: schema.EnvDefaultFunc("PGHOST", nil),
Description: "The PostgreSQL server address", Description: "Name of PostgreSQL server address to connect to",
}, },
"port": { "port": {
Type: schema.TypeInt, Type: schema.TypeInt,
Optional: true, Optional: true,
Default: 5432, DefaultFunc: schema.EnvDefaultFunc("PGPORT", 5432),
Description: "The PostgreSQL server port", Description: "The PostgreSQL port number to connect to at the server host, or socket file name extension for Unix-domain connections",
},
"database": {
Type: schema.TypeString,
Optional: true,
Description: "The name of the database to connect to in order to conenct to (defaults to `postgres`).",
DefaultFunc: schema.EnvDefaultFunc("PGDATABASE", "postgres"),
}, },
"username": { "username": {
Type: schema.TypeString, Type: schema.TypeString,
Required: true, Optional: true,
DefaultFunc: schema.MultiEnvDefaultFunc([]string{"PGUSER", "POSTGRESQL_USER"}, nil), DefaultFunc: schema.EnvDefaultFunc("PGUSER", "postgres"),
Description: "Username for PostgreSQL server connection", Description: "PostgreSQL user name to connect as",
}, },
"password": { "password": {
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
DefaultFunc: schema.MultiEnvDefaultFunc([]string{"PGPASSWORD", "POSTGRESQL_PASSWORD"}, nil), DefaultFunc: schema.EnvDefaultFunc("PGPASSWORD", nil),
Description: "Password for PostgreSQL server connection", Description: "Password to be used if the PostgreSQL server demands password authentication",
},
"sslmode": {
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("PGSSLMODE", nil),
Description: "This option determines whether or with what priority a secure SSL TCP/IP connection will be negotiated with the PostgreSQL server",
}, },
"ssl_mode": { "ssl_mode": {
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
DefaultFunc: schema.EnvDefaultFunc("PGSSLMODE", "require"), Deprecated: "Rename PostgreSQL provider `ssl_mode` attribute to `sslmode`",
Description: "Connection mode for PostgreSQL server",
}, },
"connect_timeout": { "connect_timeout": {
Type: schema.TypeInt, Type: schema.TypeInt,
Optional: true, Optional: true,
Default: 15, DefaultFunc: schema.EnvDefaultFunc("PGCONNECT_TIMEOUT", 180),
DefaultFunc: schema.EnvDefaultFunc("PGCONNECT_TIMEOUT", nil),
Description: "Maximum wait for connection, in seconds. Zero or not specified means wait indefinitely.", Description: "Maximum wait for connection, in seconds. Zero or not specified means wait indefinitely.",
ValidateFunc: validateConnTimeout,
}, },
}, },
ResourcesMap: map[string]*schema.Resource{ ResourcesMap: map[string]*schema.Resource{
"postgresql_database": resourcePostgreSQLDatabase(), "postgresql_database": resourcePostgreSQLDatabase(),
"postgresql_role": resourcePostgreSQLRole(),
"postgresql_extension": resourcePostgreSQLExtension(), "postgresql_extension": resourcePostgreSQLExtension(),
"postgresql_schema": resourcePostgreSQLSchema(),
"postgresql_role": resourcePostgreSQLRole(),
}, },
ConfigureFunc: providerConfigure, ConfigureFunc: providerConfigure,
} }
} }
func validateConnTimeout(v interface{}, key string) (warnings []string, errors []error) {
value := v.(int)
if value < 0 {
errors = append(errors, fmt.Errorf("%s can not be less than 0", key))
}
return
}
func providerConfigure(d *schema.ResourceData) (interface{}, error) { func providerConfigure(d *schema.ResourceData) (interface{}, error) {
var sslMode string
if sslModeRaw, ok := d.GetOk("sslmode"); ok {
sslMode = sslModeRaw.(string)
} else {
sslMode = d.Get("ssl_mode").(string)
}
config := Config{ config := Config{
Host: d.Get("host").(string), Host: d.Get("host").(string),
Port: d.Get("port").(int), Port: d.Get("port").(int),
Database: d.Get("database").(string),
Username: d.Get("username").(string), Username: d.Get("username").(string),
Password: d.Get("password").(string), Password: d.Get("password").(string),
SslMode: d.Get("ssl_mode").(string), SSLMode: sslMode,
Timeout: d.Get("connect_timeout").(int), ApplicationName: tfAppName(),
ConnectTimeoutSec: d.Get("connect_timeout").(int),
} }
client, err := config.NewClient() client, err := config.NewClient()
@ -76,3 +107,16 @@ func providerConfigure(d *schema.ResourceData) (interface{}, error) {
return client, nil return client, nil
} }
func tfAppName() string {
const VersionPrerelease = terraform.VersionPrerelease
var versionString bytes.Buffer
fmt.Fprintf(&versionString, "'Terraform v%s", terraform.Version)
if terraform.VersionPrerelease != "" {
fmt.Fprintf(&versionString, "-%s", terraform.VersionPrerelease)
}
fmt.Fprintf(&versionString, "'")
return versionString.String()
}

View File

@ -36,7 +36,4 @@ func testAccPreCheck(t *testing.T) {
if v := os.Getenv("PGUSER"); v == "" { if v := os.Getenv("PGUSER"); v == "" {
t.Fatal("PGUSER must be set for acceptance tests") t.Fatal("PGUSER must be set for acceptance tests")
} }
if v := os.Getenv("PGPASSWORD"); v == "" && host != "localhost" {
t.Fatal("PGPASSWORD must be set for acceptance tests if PGHOST is not localhost")
}
} }

View File

@ -1,8 +1,11 @@
package postgresql package postgresql
import ( import (
"bytes"
"database/sql" "database/sql"
"errors"
"fmt" "fmt"
"log"
"strings" "strings"
"github.com/hashicorp/errwrap" "github.com/hashicorp/errwrap"
@ -10,54 +13,170 @@ import (
"github.com/lib/pq" "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 { func resourcePostgreSQLDatabase() *schema.Resource {
return &schema.Resource{ return &schema.Resource{
Create: resourcePostgreSQLDatabaseCreate, Create: resourcePostgreSQLDatabaseCreate,
Read: resourcePostgreSQLDatabaseRead, Read: resourcePostgreSQLDatabaseRead,
Update: resourcePostgreSQLDatabaseUpdate, Update: resourcePostgreSQLDatabaseUpdate,
Delete: resourcePostgreSQLDatabaseDelete, Delete: resourcePostgreSQLDatabaseDelete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},
Schema: map[string]*schema.Schema{ Schema: map[string]*schema.Schema{
"name": { dbNameAttr: {
Type: schema.TypeString, Type: schema.TypeString,
Required: true, Required: true,
ForceNew: true, Description: "The PostgreSQL database name to connect to",
}, },
"owner": { dbOwnerAttr: {
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
Computed: true, Computed: true,
Description: "The role name of the user who will own the new database",
},
dbTemplateAttr: {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
Computed: true,
Description: "The name of the template from which to create the new database",
},
dbEncodingAttr: {
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
Description: "Character set encoding to use in the new database",
},
dbCollationAttr: {
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
Description: "Collation order (LC_COLLATE) to use in the new database",
},
dbCTypeAttr: {
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
Description: "Character classification (LC_CTYPE) to use in the new database",
},
dbTablespaceAttr: {
Type: schema.TypeString,
Optional: true,
Computed: true,
Description: "The name of the tablespace that will be associated with the new database",
},
dbConnLimitAttr: {
Type: schema.TypeInt,
Optional: true,
Computed: true,
Description: "How many concurrent connections can be made to this database",
ValidateFunc: validateConnLimit,
},
dbAllowConnsAttr: {
Type: schema.TypeBool,
Optional: true,
Default: true,
Description: "If false then no one can connect to this database",
},
dbIsTemplateAttr: {
Type: schema.TypeBool,
Optional: true,
Computed: true,
Description: "If true, then this database can be cloned by any user with CREATEDB privileges",
}, },
}, },
} }
} }
func resourcePostgreSQLDatabaseCreate(d *schema.ResourceData, meta interface{}) error { func resourcePostgreSQLDatabaseCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*Client) c := meta.(*Client)
conn, err := client.Connect() conn, err := c.Connect()
if err != nil { if err != nil {
return errwrap.Wrapf("Error connecting to PostgreSQL: {{err}}", err) return errwrap.Wrapf("Error connecting to PostgreSQL: {{err}}", err)
} }
defer conn.Close() defer conn.Close()
dbName := d.Get("name").(string) dbName := d.Get(dbNameAttr).(string)
dbOwner := d.Get("owner").(string) b := bytes.NewBufferString("CREATE DATABASE ")
connUsername := client.username fmt.Fprint(b, pq.QuoteIdentifier(dbName))
var dbOwnerCfg string // Handle each option individually and stream results into the query
if dbOwner != "" { // buffer.
dbOwnerCfg = fmt.Sprintf("WITH OWNER=%s", pq.QuoteIdentifier(dbOwner))
} else { switch v, ok := d.GetOk(dbOwnerAttr); {
dbOwnerCfg = "" case ok:
fmt.Fprint(b, " OWNER ", pq.QuoteIdentifier(v.(string)))
default:
// No owner specified in the config, default to using
// the connecting username.
fmt.Fprint(b, " OWNER ", pq.QuoteIdentifier(c.username))
} }
//needed in order to set the owner of the db if the connection user is not a superuser switch v, ok := d.GetOk(dbTemplateAttr); {
err = grantRoleMembership(conn, dbOwner, connUsername) case ok:
if err != nil { fmt.Fprint(b, " TEMPLATE ", pq.QuoteIdentifier(v.(string)))
return err case v.(string) == "", strings.ToUpper(v.(string)) != "DEFAULT":
fmt.Fprint(b, " TEMPLATE template0")
} }
query := fmt.Sprintf("CREATE DATABASE %s %s", pq.QuoteIdentifier(dbName), dbOwnerCfg) switch v, ok := d.GetOk(dbEncodingAttr); {
case ok:
fmt.Fprint(b, " ENCODING ", pq.QuoteIdentifier(v.(string)))
case v.(string) == "", strings.ToUpper(v.(string)) != "DEFAULT":
fmt.Fprint(b, ` ENCODING "UTF8"`)
}
switch v, ok := d.GetOk(dbCollationAttr); {
case ok:
fmt.Fprint(b, " LC_COLLATE ", pq.QuoteIdentifier(v.(string)))
case v.(string) == "", strings.ToUpper(v.(string)) != "DEFAULT":
fmt.Fprint(b, ` LC_COLLATE "C"`)
}
switch v, ok := d.GetOk(dbCTypeAttr); {
case ok:
fmt.Fprint(b, " LC_CTYPE ", pq.QuoteIdentifier(v.(string)))
case v.(string) == "", strings.ToUpper(v.(string)) != "DEFAULT":
fmt.Fprint(b, ` LC_CTYPE "C"`)
}
if v, ok := d.GetOk(dbTablespaceAttr); ok {
fmt.Fprint(b, " TABLESPACE ", pq.QuoteIdentifier(v.(string)))
}
{
val := d.Get(dbAllowConnsAttr).(bool)
fmt.Fprint(b, " ALLOW_CONNECTIONS ", val)
}
{
val := d.Get(dbConnLimitAttr).(int)
fmt.Fprint(b, " CONNECTION LIMIT ", val)
}
{
val := d.Get(dbIsTemplateAttr).(bool)
fmt.Fprint(b, " IS_TEMPLATE ", val)
}
query := b.String()
_, err = conn.Query(query) _, err = conn.Query(query)
if err != nil { if err != nil {
return errwrap.Wrapf(fmt.Sprintf("Error creating database %s: {{err}}", dbName), err) return errwrap.Wrapf(fmt.Sprintf("Error creating database %s: {{err}}", dbName), err)
@ -69,19 +188,24 @@ func resourcePostgreSQLDatabaseCreate(d *schema.ResourceData, meta interface{})
} }
func resourcePostgreSQLDatabaseDelete(d *schema.ResourceData, meta interface{}) error { func resourcePostgreSQLDatabaseDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*Client) c := meta.(*Client)
conn, err := client.Connect() conn, err := c.Connect()
if err != nil { if err != nil {
return errwrap.Wrapf("Error connecting to PostgreSQL: {{err}}", err) return errwrap.Wrapf("Error connecting to PostgreSQL: {{err}}", err)
} }
defer conn.Close() defer conn.Close()
dbName := d.Get("name").(string) dbName := d.Get(dbNameAttr).(string)
connUsername := client.username
dbOwner := d.Get("owner").(string) if isTemplate := d.Get(dbIsTemplateAttr).(bool); isTemplate {
//needed in order to set the owner of the db if the connection user is not a superuser // Template databases must have this attribute cleared before
err = grantRoleMembership(conn, dbOwner, connUsername) // they can be dropped.
if err != nil { if err := doSetDBIsTemplate(conn, dbName, false); err != nil {
return errwrap.Wrapf("Error updating database IS_TEMPLATE during DROP DATABASE: {{err}}", err)
}
}
if err := setDBIsTemplate(conn, d); err != nil {
return err return err
} }
@ -97,64 +221,204 @@ func resourcePostgreSQLDatabaseDelete(d *schema.ResourceData, meta interface{})
} }
func resourcePostgreSQLDatabaseRead(d *schema.ResourceData, meta interface{}) error { func resourcePostgreSQLDatabaseRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*Client) c := meta.(*Client)
conn, err := client.Connect() conn, err := c.Connect()
if err != nil { if err != nil {
return err return err
} }
defer conn.Close() defer conn.Close()
dbName := d.Get("name").(string) dbId := d.Id()
var dbName, ownerName string
var owner 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)
err = conn.QueryRow("SELECT pg_catalog.pg_get_userbyid(d.datdba) from pg_database d WHERE datname=$1", dbName).Scan(&owner)
switch { switch {
case err == sql.ErrNoRows: case err == sql.ErrNoRows:
log.Printf("[WARN] PostgreSQL database (%s) not found", dbId)
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", dbId)
d.SetId("") d.SetId("")
return nil return nil
case err != nil: case err != nil:
return errwrap.Wrapf("Error reading database: {{err}}", err) return errwrap.Wrapf("Error reading database: {{err}}", err)
default: 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)
return nil return nil
} }
} }
func resourcePostgreSQLDatabaseUpdate(d *schema.ResourceData, meta interface{}) error { func resourcePostgreSQLDatabaseUpdate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*Client) c := meta.(*Client)
conn, err := client.Connect() conn, err := c.Connect()
if err != nil { if err != nil {
return err return err
} }
defer conn.Close() defer conn.Close()
dbName := d.Get("name").(string) if err := setDBName(conn, d); err != nil {
return err
}
if d.HasChange("owner") { if err := setDBOwner(conn, d); err != nil {
owner := d.Get("owner").(string) return err
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 := 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) return resourcePostgreSQLDatabaseRead(d, meta)
} }
func grantRoleMembership(conn *sql.DB, dbOwner string, connUsername string) error { func setDBName(conn *sql.DB, d *schema.ResourceData) error {
if dbOwner != "" && dbOwner != connUsername { if !d.HasChange(dbNameAttr) {
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
if strings.Contains(err.Error(), "duplicate key value violates unique constraint") {
return nil return nil
} }
return errwrap.Wrapf("Error granting membership: {{err}}", err)
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
}
if err := doSetDBIsTemplate(conn, d.Get(dbNameAttr).(string), d.Get(dbIsTemplateAttr).(bool)); err != nil {
return errwrap.Wrapf("Error updating database IS_TEMPLATE: {{err}}", err)
}
return nil
}
func doSetDBIsTemplate(conn *sql.DB, dbName string, isTemplate bool) error {
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 return nil
} }

View File

@ -2,29 +2,90 @@ package postgresql
import ( import (
"database/sql" "database/sql"
"errors"
"fmt" "fmt"
"testing" "testing"
"errors"
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
) )
func TestAccPostgresqlDatabase_Basic(t *testing.T) { func TestAccPostgresqlDatabase_Basic(t *testing.T) {
resource.Test(t, resource.TestCase{ resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) }, PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders, Providers: testAccProviders,
CheckDestroy: testAccCheckPostgresqlDatabaseDestroy, CheckDestroy: testAccCheckPostgresqlDatabaseDestroy,
Steps: []resource.TestStep{ Steps: []resource.TestStep{
{ {
Config: testAccPostgresqlDatabaseConfig, Config: testAccPostgreSQLDatabaseConfig,
Check: resource.ComposeTestCheckFunc( Check: resource.ComposeTestCheckFunc(
testAccCheckPostgresqlDatabaseExists("postgresql_database.mydb"), testAccCheckPostgresqlDatabaseExists("postgresql_database.mydb"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"postgresql_database.mydb", "name", "mydb"), "postgresql_database.mydb", "name", "mydb"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"postgresql_database.mydb", "owner", "myrole"), "postgresql_database.mydb", "owner", "myrole"),
resource.TestCheckResourceAttr(
"postgresql_database.default_opts", "owner", "myrole"),
resource.TestCheckResourceAttr(
"postgresql_database.default_opts", "name", "default_opts_name"),
resource.TestCheckResourceAttr(
"postgresql_database.default_opts", "template", "template0"),
resource.TestCheckResourceAttr(
"postgresql_database.default_opts", "encoding", "UTF8"),
resource.TestCheckResourceAttr(
"postgresql_database.default_opts", "lc_collate", "C"),
resource.TestCheckResourceAttr(
"postgresql_database.default_opts", "lc_ctype", "C"),
resource.TestCheckResourceAttr(
"postgresql_database.default_opts", "tablespace_name", "pg_default"),
resource.TestCheckResourceAttr(
"postgresql_database.default_opts", "connection_limit", "-1"),
resource.TestCheckResourceAttr(
"postgresql_database.default_opts", "allow_connections", "true"),
resource.TestCheckResourceAttr(
"postgresql_database.default_opts", "is_template", "false"),
resource.TestCheckResourceAttr(
"postgresql_database.modified_opts", "owner", "myrole"),
resource.TestCheckResourceAttr(
"postgresql_database.modified_opts", "name", "custom_template_db"),
resource.TestCheckResourceAttr(
"postgresql_database.modified_opts", "template", "template0"),
resource.TestCheckResourceAttr(
"postgresql_database.modified_opts", "encoding", "UTF8"),
resource.TestCheckResourceAttr(
"postgresql_database.modified_opts", "lc_collate", "en_US.UTF-8"),
resource.TestCheckResourceAttr(
"postgresql_database.modified_opts", "lc_ctype", "en_US.UTF-8"),
resource.TestCheckResourceAttr(
"postgresql_database.modified_opts", "tablespace_name", "pg_default"),
resource.TestCheckResourceAttr(
"postgresql_database.modified_opts", "connection_limit", "10"),
resource.TestCheckResourceAttr(
"postgresql_database.modified_opts", "allow_connections", "false"),
resource.TestCheckResourceAttr(
"postgresql_database.modified_opts", "is_template", "true"),
resource.TestCheckResourceAttr(
"postgresql_database.pathological_opts", "owner", "myrole"),
resource.TestCheckResourceAttr(
"postgresql_database.pathological_opts", "name", "bad_template_db"),
resource.TestCheckResourceAttr(
"postgresql_database.pathological_opts", "template", "template0"),
resource.TestCheckResourceAttr(
"postgresql_database.pathological_opts", "encoding", "LATIN1"),
resource.TestCheckResourceAttr(
"postgresql_database.pathological_opts", "lc_collate", "C"),
resource.TestCheckResourceAttr(
"postgresql_database.pathological_opts", "lc_ctype", "C"),
resource.TestCheckResourceAttr(
"postgresql_database.pathological_opts", "tablespace_name", "pg_default"),
resource.TestCheckResourceAttr(
"postgresql_database.pathological_opts", "connection_limit", "0"),
resource.TestCheckResourceAttr(
"postgresql_database.pathological_opts", "allow_connections", "true"),
resource.TestCheckResourceAttr(
"postgresql_database.pathological_opts", "is_template", "true"),
), ),
}, },
}, },
@ -32,14 +93,13 @@ func TestAccPostgresqlDatabase_Basic(t *testing.T) {
} }
func TestAccPostgresqlDatabase_DefaultOwner(t *testing.T) { func TestAccPostgresqlDatabase_DefaultOwner(t *testing.T) {
resource.Test(t, resource.TestCase{ resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) }, PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders, Providers: testAccProviders,
CheckDestroy: testAccCheckPostgresqlDatabaseDestroy, CheckDestroy: testAccCheckPostgresqlDatabaseDestroy,
Steps: []resource.TestStep{ Steps: []resource.TestStep{
{ {
Config: testAccPostgresqlDatabaseConfig, Config: testAccPostgreSQLDatabaseConfig,
Check: resource.ComposeTestCheckFunc( Check: resource.ComposeTestCheckFunc(
testAccCheckPostgresqlDatabaseExists("postgresql_database.mydb_default_owner"), testAccCheckPostgresqlDatabaseExists("postgresql_database.mydb_default_owner"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
@ -119,7 +179,7 @@ func checkDatabaseExists(client *Client, dbName string) (bool, error) {
} }
} }
var testAccPostgresqlDatabaseConfig = ` var testAccPostgreSQLDatabaseConfig = `
resource "postgresql_role" "myrole" { resource "postgresql_role" "myrole" {
name = "myrole" name = "myrole"
login = true login = true
@ -135,6 +195,45 @@ resource "postgresql_database" "mydb2" {
owner = "${postgresql_role.myrole.name}" owner = "${postgresql_role.myrole.name}"
} }
resource "postgresql_database" "default_opts" {
name = "default_opts_name"
owner = "${postgresql_role.myrole.name}"
template = "template0"
encoding = "UTF8"
lc_collate = "C"
lc_ctype = "C"
tablespace_name = "pg_default"
connection_limit = -1
allow_connections = true
is_template = false
}
resource "postgresql_database" "modified_opts" {
name = "custom_template_db"
owner = "${postgresql_role.myrole.name}"
template = "template0"
encoding = "UTF8"
lc_collate = "en_US.UTF-8"
lc_ctype = "en_US.UTF-8"
tablespace_name = "pg_default"
connection_limit = 10
allow_connections = false
is_template = true
}
resource "postgresql_database" "pathological_opts" {
name = "bad_template_db"
owner = "${postgresql_role.myrole.name}"
template = "template0"
encoding = "LATIN1"
lc_collate = "C"
lc_ctype = "C"
tablespace_name = "pg_default"
connection_limit = 0
allow_connections = true
is_template = true
}
resource "postgresql_database" "mydb_default_owner" { resource "postgresql_database" "mydb_default_owner" {
name = "mydb_default_owner" name = "mydb_default_owner"
} }

View File

@ -1,86 +1,125 @@
package postgresql package postgresql
import ( import (
"bytes"
"database/sql" "database/sql"
"errors"
"fmt" "fmt"
"log"
"github.com/hashicorp/errwrap" "github.com/hashicorp/errwrap"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
"github.com/lib/pq" "github.com/lib/pq"
) )
const (
extNameAttr = "name"
extSchemaAttr = "schema"
extVersionAttr = "version"
)
func resourcePostgreSQLExtension() *schema.Resource { func resourcePostgreSQLExtension() *schema.Resource {
return &schema.Resource{ return &schema.Resource{
Create: resourcePostgreSQLExtensionCreate, Create: resourcePostgreSQLExtensionCreate,
Read: resourcePostgreSQLExtensionRead, Read: resourcePostgreSQLExtensionRead,
Update: resourcePostgreSQLExtensionUpdate,
Delete: resourcePostgreSQLExtensionDelete, Delete: resourcePostgreSQLExtensionDelete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},
Schema: map[string]*schema.Schema{ Schema: map[string]*schema.Schema{
"name": { extNameAttr: {
Type: schema.TypeString, Type: schema.TypeString,
Required: true, Required: true,
ForceNew: true, ForceNew: true,
}, },
extSchemaAttr: {
Type: schema.TypeString,
Optional: true,
Computed: true,
Description: "Sets the schema of an extension",
},
extVersionAttr: {
Type: schema.TypeString,
Optional: true,
Computed: true,
Description: "Sets the version number of the extension",
},
}, },
} }
} }
func resourcePostgreSQLExtensionCreate(d *schema.ResourceData, meta interface{}) error { func resourcePostgreSQLExtensionCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*Client) c := meta.(*Client)
conn, err := client.Connect() conn, err := c.Connect()
if err != nil { if err != nil {
return err return err
} }
defer conn.Close() defer conn.Close()
extensionName := d.Get("name").(string) extName := d.Get(extNameAttr).(string)
query := fmt.Sprintf("CREATE EXTENSION %s", pq.QuoteIdentifier(extensionName)) b := bytes.NewBufferString("CREATE EXTENSION ")
fmt.Fprintf(b, pq.QuoteIdentifier(extName))
if v, ok := d.GetOk(extSchemaAttr); ok {
fmt.Fprint(b, " SCHEMA ", pq.QuoteIdentifier(v.(string)))
}
if v, ok := d.GetOk(extVersionAttr); ok {
fmt.Fprint(b, " VERSION ", pq.QuoteIdentifier(v.(string)))
}
query := b.String()
_, err = conn.Query(query) _, err = conn.Query(query)
if err != nil { if err != nil {
return errwrap.Wrapf("Error creating extension: {{err}}", err) return errwrap.Wrapf("Error creating extension: {{err}}", err)
} }
d.SetId(extensionName) d.SetId(extName)
return resourcePostgreSQLExtensionRead(d, meta) return resourcePostgreSQLExtensionRead(d, meta)
} }
func resourcePostgreSQLExtensionRead(d *schema.ResourceData, meta interface{}) error { func resourcePostgreSQLExtensionRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*Client) c := meta.(*Client)
conn, err := client.Connect() conn, err := c.Connect()
if err != nil { if err != nil {
return err return err
} }
defer conn.Close() defer conn.Close()
extensionName := d.Get("name").(string) extID := d.Id()
var extName, extSchema, extVersion string
var hasExtension bool err = conn.QueryRow("SELECT e.extname, n.nspname, e.extversion FROM pg_catalog.pg_extension e, pg_catalog.pg_namespace n WHERE n.oid = e.extnamespace AND e.extname = $1", extID).Scan(&extName, &extSchema, &extVersion)
err = conn.QueryRow("SELECT 1 from pg_extension d WHERE extname=$1", extensionName).Scan(&hasExtension)
switch { switch {
case err == sql.ErrNoRows: case err == sql.ErrNoRows:
log.Printf("[WARN] PostgreSQL extension (%s) not found", d.Id())
d.SetId("") d.SetId("")
return nil return nil
case err != nil: case err != nil:
return errwrap.Wrapf("Error reading extension: {{err}}", err) return errwrap.Wrapf("Error reading extension: {{err}}", err)
default: default:
d.Set("extension", hasExtension) d.Set(extNameAttr, extName)
d.Set(extSchemaAttr, extSchema)
d.Set(extVersionAttr, extVersion)
d.SetId(extName)
return nil return nil
} }
} }
func resourcePostgreSQLExtensionDelete(d *schema.ResourceData, meta interface{}) error { func resourcePostgreSQLExtensionDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*Client) c := meta.(*Client)
conn, err := client.Connect() conn, err := c.Connect()
if err != nil { if err != nil {
return err return err
} }
defer conn.Close() defer conn.Close()
extensionName := d.Get("name").(string) extID := d.Id()
query := fmt.Sprintf("DROP EXTENSION %s", pq.QuoteIdentifier(extensionName)) query := fmt.Sprintf("DROP EXTENSION %s", pq.QuoteIdentifier(extID))
_, err = conn.Query(query) _, err = conn.Query(query)
if err != nil { if err != nil {
return errwrap.Wrapf("Error deleting extension: {{err}}", err) return errwrap.Wrapf("Error deleting extension: {{err}}", err)
@ -90,3 +129,68 @@ func resourcePostgreSQLExtensionDelete(d *schema.ResourceData, meta interface{})
return nil return nil
} }
func resourcePostgreSQLExtensionUpdate(d *schema.ResourceData, meta interface{}) error {
c := meta.(*Client)
conn, err := c.Connect()
if err != nil {
return err
}
defer conn.Close()
// Can't rename a schema
if err := setExtSchema(conn, d); err != nil {
return err
}
if err := setExtVersion(conn, d); err != nil {
return err
}
return resourcePostgreSQLExtensionRead(d, meta)
}
func setExtSchema(conn *sql.DB, d *schema.ResourceData) error {
if !d.HasChange(extSchemaAttr) {
return nil
}
extID := d.Id()
_, nraw := d.GetChange(extSchemaAttr)
n := nraw.(string)
if n == "" {
return errors.New("Error setting extension name to an empty string")
}
query := fmt.Sprintf("ALTER EXTENSION %s SET SCHEMA %s", pq.QuoteIdentifier(extID), pq.QuoteIdentifier(n))
if _, err := conn.Query(query); err != nil {
return errwrap.Wrapf("Error updating extension SCHEMA: {{err}}", err)
}
return nil
}
func setExtVersion(conn *sql.DB, d *schema.ResourceData) error {
if !d.HasChange(extVersionAttr) {
return nil
}
extID := d.Id()
b := bytes.NewBufferString("ALTER EXTENSION ")
fmt.Fprintf(b, "%s UPDATE", pq.QuoteIdentifier(extID))
_, nraw := d.GetChange(extVersionAttr)
n := nraw.(string)
if n != "" {
fmt.Fprintf(b, " TO %s", pq.QuoteIdentifier(n))
}
query := b.String()
if _, err := conn.Query(query); err != nil {
return errwrap.Wrapf("Error updating extension version: {{err}}", err)
}
return nil
}

View File

@ -10,7 +10,6 @@ import (
) )
func TestAccPostgresqlExtension_Basic(t *testing.T) { func TestAccPostgresqlExtension_Basic(t *testing.T) {
resource.Test(t, resource.TestCase{ resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) }, PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders, Providers: testAccProviders,
@ -22,6 +21,15 @@ func TestAccPostgresqlExtension_Basic(t *testing.T) {
testAccCheckPostgresqlExtensionExists("postgresql_extension.myextension"), testAccCheckPostgresqlExtensionExists("postgresql_extension.myextension"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"postgresql_extension.myextension", "name", "pg_trgm"), "postgresql_extension.myextension", "name", "pg_trgm"),
resource.TestCheckResourceAttr(
"postgresql_extension.myextension", "schema", "public"),
// NOTE(sean): Version 1.3 is what's
// shipped with PostgreSQL 9.6.1. This
// version number may drift in the
// future.
resource.TestCheckResourceAttr(
"postgresql_extension.myextension", "version", "1.3"),
), ),
}, },
}, },
@ -76,6 +84,42 @@ func testAccCheckPostgresqlExtensionExists(n string) resource.TestCheckFunc {
} }
} }
func TestAccPostgresqlExtension_SchemaRename(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckPostgresqlExtensionDestroy,
Steps: []resource.TestStep{
{
Config: testAccPostgresqlExtensionSchemaChange1,
Check: resource.ComposeTestCheckFunc(
testAccCheckPostgresqlExtensionExists("postgresql_extension.ext1trgm"),
resource.TestCheckResourceAttr(
"postgresql_schema.ext1foo", "name", "foo"),
resource.TestCheckResourceAttr(
"postgresql_extension.ext1trgm", "name", "pg_trgm"),
resource.TestCheckResourceAttr(
"postgresql_extension.ext1trgm", "name", "pg_trgm"),
resource.TestCheckResourceAttr(
"postgresql_extension.ext1trgm", "schema", "foo"),
),
},
{
Config: testAccPostgresqlExtensionSchemaChange2,
Check: resource.ComposeTestCheckFunc(
testAccCheckPostgresqlExtensionExists("postgresql_extension.ext1trgm"),
resource.TestCheckResourceAttr(
"postgresql_schema.ext1foo", "name", "bar"),
resource.TestCheckResourceAttr(
"postgresql_extension.ext1trgm", "name", "pg_trgm"),
resource.TestCheckResourceAttr(
"postgresql_extension.ext1trgm", "schema", "bar"),
),
},
},
})
}
func checkExtensionExists(client *Client, extensionName string) (bool, error) { func checkExtensionExists(client *Client, extensionName string) (bool, error) {
conn, err := client.Connect() conn, err := client.Connect()
if err != nil { if err != nil {
@ -83,8 +127,8 @@ func checkExtensionExists(client *Client, extensionName string) (bool, error) {
} }
defer conn.Close() defer conn.Close()
var _rez int var _rez bool
err = conn.QueryRow("SELECT 1 from pg_extension d WHERE extname=$1", extensionName).Scan(&_rez) err = conn.QueryRow("SELECT TRUE from pg_catalog.pg_extension d WHERE extname=$1", extensionName).Scan(&_rez)
switch { switch {
case err == sql.ErrNoRows: case err == sql.ErrNoRows:
return false, nil return false, nil
@ -100,3 +144,25 @@ resource "postgresql_extension" "myextension" {
name = "pg_trgm" name = "pg_trgm"
} }
` `
var testAccPostgresqlExtensionSchemaChange1 = `
resource "postgresql_schema" "ext1foo" {
name = "foo"
}
resource "postgresql_extension" "ext1trgm" {
name = "pg_trgm"
schema = "${postgresql_schema.ext1foo.name}"
}
`
var testAccPostgresqlExtensionSchemaChange2 = `
resource "postgresql_schema" "ext1foo" {
name = "bar"
}
resource "postgresql_extension" "ext1trgm" {
name = "pg_trgm"
schema = "${postgresql_schema.ext1foo.name}"
}
`

View File

@ -2,65 +2,229 @@ package postgresql
import ( import (
"database/sql" "database/sql"
"errors"
"fmt" "fmt"
"log"
"strings"
"github.com/hashicorp/errwrap" "github.com/hashicorp/errwrap"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
"github.com/lib/pq" "github.com/lib/pq"
) )
const (
roleBypassRLSAttr = "bypass_row_level_security"
roleConnLimitAttr = "connection_limit"
roleCreateDBAttr = "create_database"
roleCreateRoleAttr = "create_role"
roleEncryptedPassAttr = "encrypted_password"
roleInheritAttr = "inherit"
roleLoginAttr = "login"
roleNameAttr = "name"
rolePasswordAttr = "password"
roleReplicationAttr = "replication"
roleSuperuserAttr = "superuser"
roleValidUntilAttr = "valid_until"
// Deprecated options
roleDepEncryptedAttr = "encrypted"
)
func resourcePostgreSQLRole() *schema.Resource { func resourcePostgreSQLRole() *schema.Resource {
return &schema.Resource{ return &schema.Resource{
Create: resourcePostgreSQLRoleCreate, Create: resourcePostgreSQLRoleCreate,
Read: resourcePostgreSQLRoleRead, Read: resourcePostgreSQLRoleRead,
Update: resourcePostgreSQLRoleUpdate, Update: resourcePostgreSQLRoleUpdate,
Delete: resourcePostgreSQLRoleDelete, Delete: resourcePostgreSQLRoleDelete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},
Schema: map[string]*schema.Schema{ Schema: map[string]*schema.Schema{
"name": { roleNameAttr: {
Type: schema.TypeString, Type: schema.TypeString,
Required: true, Required: true,
ForceNew: true, Description: "The name of the role",
}, },
"login": { rolePasswordAttr: {
Type: schema.TypeBool,
Optional: true,
ForceNew: false,
Default: false,
},
"password": {
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
ForceNew: false, Computed: true,
Sensitive: true,
DefaultFunc: schema.EnvDefaultFunc("PGPASSWORD", nil),
Description: "Sets the role's password",
}, },
"encrypted": { roleDepEncryptedAttr: {
Type: schema.TypeString,
Optional: true,
Deprecated: fmt.Sprintf("Rename PostgreSQL role resource attribute %q to %q", roleDepEncryptedAttr, roleEncryptedPassAttr),
},
roleEncryptedPassAttr: {
Type: schema.TypeBool,
Optional: true,
Default: true,
Description: "Control whether the password is stored encrypted in the system catalogs",
},
roleValidUntilAttr: {
Type: schema.TypeString,
Optional: true,
Default: "infinity",
Description: "Sets a date and time after which the role's password is no longer valid",
},
roleConnLimitAttr: {
Type: schema.TypeInt,
Optional: true,
Computed: true,
Description: "How many concurrent connections can be made with this role",
ValidateFunc: validateConnLimit,
},
roleSuperuserAttr: {
Type: schema.TypeBool, Type: schema.TypeBool,
Optional: true, Optional: true,
ForceNew: false,
Default: false, Default: false,
Description: `Determine whether the new role is a "superuser"`,
},
roleCreateDBAttr: {
Type: schema.TypeBool,
Optional: true,
Default: false,
Description: "Define a role's ability to create databases",
},
roleCreateRoleAttr: {
Type: schema.TypeBool,
Optional: true,
Default: false,
Description: "Determine whether this role will be permitted to create new roles",
},
roleInheritAttr: {
Type: schema.TypeBool,
Optional: true,
Default: true,
Description: `Determine whether a role "inherits" the privileges of roles it is a member of`,
},
roleLoginAttr: {
Type: schema.TypeBool,
Optional: true,
Default: false,
Description: "Determine whether a role is allowed to log in",
},
roleReplicationAttr: {
Type: schema.TypeBool,
Optional: true,
Default: false,
Description: "Determine whether a role is allowed to initiate streaming replication or put the system in and out of backup mode",
},
roleBypassRLSAttr: {
Type: schema.TypeBool,
Optional: true,
Default: false,
Description: "Determine whether a role bypasses every row-level security (RLS) policy",
}, },
}, },
} }
} }
func resourcePostgreSQLRoleCreate(d *schema.ResourceData, meta interface{}) error { func resourcePostgreSQLRoleCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*Client) c := meta.(*Client)
conn, err := client.Connect() conn, err := c.Connect()
if err != nil { if err != nil {
return err return errwrap.Wrapf("Error connecting to PostgreSQL: {{err}}", err)
} }
defer conn.Close() defer conn.Close()
roleName := d.Get("name").(string) stringOpts := []struct {
loginAttr := getLoginStr(d.Get("login").(bool)) hclKey string
password := d.Get("password").(string) sqlKey string
}{
{rolePasswordAttr, "PASSWORD"},
{roleValidUntilAttr, "VALID UNTIL"},
}
intOpts := []struct {
hclKey string
sqlKey string
}{
{roleConnLimitAttr, "CONNECTION LIMIT"},
}
boolOpts := []struct {
hclKey string
sqlKeyEnable string
sqlKeyDisable string
}{
{roleSuperuserAttr, "CREATEDB", "NOCREATEDB"},
{roleCreateRoleAttr, "CREATEROLE", "NOCREATEROLE"},
{roleInheritAttr, "INHERIT", "NOINHERIT"},
{roleLoginAttr, "LOGIN", "NOLOGIN"},
{roleReplicationAttr, "REPLICATION", "NOREPLICATION"},
{roleBypassRLSAttr, "BYPASSRLS", "NOBYPASSRLS"},
encryptedCfg := getEncryptedStr(d.Get("encrypted").(bool)) // roleEncryptedPassAttr is used only when rolePasswordAttr is set.
// {roleEncryptedPassAttr, "ENCRYPTED", "UNENCRYPTED"},
}
query := fmt.Sprintf("CREATE ROLE %s %s %s PASSWORD '%s'", pq.QuoteIdentifier(roleName), loginAttr, encryptedCfg, password) createOpts := make([]string, 0, len(stringOpts)+len(intOpts)+len(boolOpts))
for _, opt := range stringOpts {
v, ok := d.GetOk(opt.hclKey)
if !ok {
continue
}
val := v.(string)
if val != "" {
switch {
case opt.hclKey == rolePasswordAttr:
if strings.ToUpper(v.(string)) == "NULL" {
createOpts = append(createOpts, "PASSWORD NULL")
} else {
if d.Get(roleEncryptedPassAttr).(bool) {
createOpts = append(createOpts, "ENCRYPTED")
} else {
createOpts = append(createOpts, "UNENCRYPTED")
}
createOpts = append(createOpts, fmt.Sprintf("%s '%s'", opt.sqlKey, pqQuoteLiteral(val)))
}
case opt.hclKey == roleValidUntilAttr:
switch {
case v.(string) == "", strings.ToLower(v.(string)) == "infinity":
createOpts = append(createOpts, fmt.Sprintf("%s '%s'", opt.sqlKey, "infinity"))
default:
createOpts = append(createOpts, fmt.Sprintf("%s %s", opt.sqlKey, pq.QuoteIdentifier(val)))
}
default:
createOpts = append(createOpts, fmt.Sprintf("%s %s", opt.sqlKey, pq.QuoteIdentifier(val)))
}
}
}
for _, opt := range intOpts {
val := d.Get(opt.hclKey).(int)
createOpts = append(createOpts, fmt.Sprintf("%s %d", opt.sqlKey, val))
}
for _, opt := range boolOpts {
if opt.hclKey == roleEncryptedPassAttr {
// This attribute is handled above in the stringOpts
// loop.
continue
}
val := d.Get(opt.hclKey).(bool)
valStr := opt.sqlKeyDisable
if val {
valStr = opt.sqlKeyEnable
}
createOpts = append(createOpts, valStr)
}
roleName := d.Get(roleNameAttr).(string)
createStr := strings.Join(createOpts, " ")
if len(createOpts) > 0 {
createStr = " WITH " + createStr
}
query := fmt.Sprintf("CREATE ROLE %s%s", pq.QuoteIdentifier(roleName), createStr)
_, err = conn.Query(query) _, err = conn.Query(query)
if err != nil { if err != nil {
return errwrap.Wrapf("Error creating role: {{err}}", err) return errwrap.Wrapf(fmt.Sprintf("Error creating role %s: {{err}}", roleName), err)
} }
d.SetId(roleName) d.SetId(roleName)
@ -76,8 +240,7 @@ func resourcePostgreSQLRoleDelete(d *schema.ResourceData, meta interface{}) erro
} }
defer conn.Close() defer conn.Close()
roleName := d.Get("name").(string) roleName := d.Get(roleNameAttr).(string)
query := fmt.Sprintf("DROP ROLE %s", pq.QuoteIdentifier(roleName)) query := fmt.Sprintf("DROP ROLE %s", pq.QuoteIdentifier(roleName))
_, err = conn.Query(query) _, err = conn.Query(query)
if err != nil { if err != nil {
@ -90,91 +253,296 @@ func resourcePostgreSQLRoleDelete(d *schema.ResourceData, meta interface{}) erro
} }
func resourcePostgreSQLRoleRead(d *schema.ResourceData, meta interface{}) error { func resourcePostgreSQLRoleRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*Client) c := meta.(*Client)
conn, err := client.Connect() conn, err := c.Connect()
if err != nil { if err != nil {
return err return err
} }
defer conn.Close() defer conn.Close()
roleName := d.Get("name").(string) roleId := d.Id()
var roleSuperuser, roleInherit, roleCreateRole, roleCreateDB, roleCanLogin, roleReplication, roleBypassRLS bool
var canLogin bool var roleConnLimit int
err = conn.QueryRow("SELECT rolcanlogin FROM pg_roles WHERE rolname=$1", roleName).Scan(&canLogin) var roleName, roleValidUntil string
err = conn.QueryRow("SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolconnlimit, COALESCE(rolvaliduntil::TEXT, 'infinity'), rolbypassrls FROM pg_catalog.pg_roles WHERE rolname=$1", roleId).Scan(&roleName, &roleSuperuser, &roleInherit, &roleCreateRole, &roleCreateDB, &roleCanLogin, &roleReplication, &roleConnLimit, &roleValidUntil, &roleBypassRLS)
switch { switch {
case err == sql.ErrNoRows: case err == sql.ErrNoRows:
log.Printf("[WARN] PostgreSQL role (%s) not found", roleId)
d.SetId("") d.SetId("")
return nil return nil
case err != nil: case err != nil:
return errwrap.Wrapf("Error reading role: {{err}}", err) return errwrap.Wrapf("Error reading role: {{err}}", err)
default: default:
d.Set("login", canLogin) d.Set(roleNameAttr, roleName)
d.Set(roleBypassRLSAttr, roleBypassRLS)
d.Set(roleConnLimitAttr, roleConnLimit)
d.Set(roleCreateDBAttr, roleCreateDB)
d.Set(roleCreateRoleAttr, roleCreateRole)
d.Set(roleEncryptedPassAttr, true)
d.Set(roleInheritAttr, roleInherit)
d.Set(roleLoginAttr, roleCanLogin)
d.Set(roleReplicationAttr, roleReplication)
d.Set(roleSuperuserAttr, roleSuperuser)
d.Set(roleValidUntilAttr, roleValidUntil)
d.SetId(roleName)
}
if !roleSuperuser {
// Return early if not superuser user
return nil
}
var rolePassword string
err = conn.QueryRow("SELECT COALESCE(passwd, '') FROM pg_catalog.pg_shadow AS s WHERE s.usename = $1", roleId).Scan(&rolePassword)
switch {
case err == sql.ErrNoRows:
return fmt.Errorf("PostgreSQL role (%s) not found in shadow database: {{err}}", roleId)
case err != nil:
return errwrap.Wrapf("Error reading role: {{err}}", err)
default:
d.Set(rolePasswordAttr, rolePassword)
return nil return nil
} }
} }
func resourcePostgreSQLRoleUpdate(d *schema.ResourceData, meta interface{}) error { func resourcePostgreSQLRoleUpdate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*Client) c := meta.(*Client)
conn, err := client.Connect() conn, err := c.Connect()
if err != nil { if err != nil {
return err return err
} }
defer conn.Close() defer conn.Close()
d.Partial(true) if err := setRoleName(conn, d); err != nil {
return err
roleName := d.Get("name").(string)
if d.HasChange("login") {
loginAttr := getLoginStr(d.Get("login").(bool))
query := fmt.Sprintf("ALTER ROLE %s %s", pq.QuoteIdentifier(roleName), pq.QuoteIdentifier(loginAttr))
_, err := conn.Query(query)
if err != nil {
return errwrap.Wrapf("Error updating login attribute for role: {{err}}", err)
} }
d.SetPartial("login") if err := setRoleBypassRLS(conn, d); err != nil {
return err
} }
password := d.Get("password").(string) if err := setRoleConnLimit(conn, d); err != nil {
if d.HasChange("password") { return err
encryptedCfg := getEncryptedStr(d.Get("encrypted").(bool))
query := fmt.Sprintf("ALTER ROLE %s %s PASSWORD '%s'", pq.QuoteIdentifier(roleName), encryptedCfg, password)
_, err := conn.Query(query)
if err != nil {
return errwrap.Wrapf("Error updating password attribute for role: {{err}}", err)
} }
d.SetPartial("password") if err := setRoleCreateDB(conn, d); err != nil {
return err
} }
if d.HasChange("encrypted") { if err := setRoleCreateRole(conn, d); err != nil {
encryptedCfg := getEncryptedStr(d.Get("encrypted").(bool)) return err
query := fmt.Sprintf("ALTER ROLE %s %s PASSWORD '%s'", pq.QuoteIdentifier(roleName), encryptedCfg, password)
_, err := conn.Query(query)
if err != nil {
return errwrap.Wrapf("Error updating encrypted attribute for role: {{err}}", err)
} }
d.SetPartial("encrypted") if err := setRoleInherit(conn, d); err != nil {
return err
}
if err := setRoleLogin(conn, d); err != nil {
return err
}
if err := setRoleReplication(conn, d); err != nil {
return err
}
if err := setRoleSuperuser(conn, d); err != nil {
return err
}
if err := setRoleValidUntil(conn, d); err != nil {
return err
} }
d.Partial(false)
return resourcePostgreSQLRoleRead(d, meta) return resourcePostgreSQLRoleRead(d, meta)
} }
func getLoginStr(canLogin bool) string { func setRoleName(conn *sql.DB, d *schema.ResourceData) error {
if canLogin { if !d.HasChange(roleNameAttr) {
return "login" return nil
}
return "nologin"
} }
func getEncryptedStr(isEncrypted bool) string { oraw, nraw := d.GetChange(roleNameAttr)
if isEncrypted { o := oraw.(string)
return "encrypted" n := nraw.(string)
if n == "" {
return errors.New("Error setting role name to an empty string")
} }
return "unencrypted"
query := fmt.Sprintf("ALTER ROLE %s RENAME TO %s", pq.QuoteIdentifier(o), pq.QuoteIdentifier(n))
if _, err := conn.Query(query); err != nil {
return errwrap.Wrapf("Error updating role NAME: {{err}}", err)
}
d.SetId(n)
return nil
}
func setRoleBypassRLS(conn *sql.DB, d *schema.ResourceData) error {
if !d.HasChange(roleBypassRLSAttr) {
return nil
}
bypassRLS := d.Get(roleBypassRLSAttr).(bool)
tok := "NOBYPASSRLS"
if bypassRLS {
tok = "BYPASSRLS"
}
roleName := d.Get(roleNameAttr).(string)
query := fmt.Sprintf("ALTER ROLE %s WITH %s", pq.QuoteIdentifier(roleName), tok)
if _, err := conn.Query(query); err != nil {
return errwrap.Wrapf("Error updating role BYPASSRLS: {{err}}", err)
}
return nil
}
func setRoleConnLimit(conn *sql.DB, d *schema.ResourceData) error {
if !d.HasChange(roleConnLimitAttr) {
return nil
}
connLimit := d.Get(roleConnLimitAttr).(int)
roleName := d.Get(roleNameAttr).(string)
query := fmt.Sprintf("ALTER ROLE %s CONNECTION LIMIT = %d", pq.QuoteIdentifier(roleName), connLimit)
if _, err := conn.Query(query); err != nil {
return errwrap.Wrapf("Error updating role CONNECTION LIMIT: {{err}}", err)
}
return nil
}
func setRoleCreateDB(conn *sql.DB, d *schema.ResourceData) error {
if !d.HasChange(roleCreateDBAttr) {
return nil
}
createDB := d.Get(roleCreateDBAttr).(bool)
tok := "NOCREATEDB"
if createDB {
tok = "CREATEDB"
}
roleName := d.Get(roleNameAttr).(string)
query := fmt.Sprintf("ALTER ROLE %s WITH %s", pq.QuoteIdentifier(roleName), tok)
if _, err := conn.Query(query); err != nil {
return errwrap.Wrapf("Error updating role CREATEDB: {{err}}", err)
}
return nil
}
func setRoleCreateRole(conn *sql.DB, d *schema.ResourceData) error {
if !d.HasChange(roleCreateRoleAttr) {
return nil
}
createRole := d.Get(roleCreateRoleAttr).(bool)
tok := "NOCREATEROLE"
if createRole {
tok = "CREATEROLE"
}
roleName := d.Get(roleNameAttr).(string)
query := fmt.Sprintf("ALTER ROLE %s WITH %s", pq.QuoteIdentifier(roleName), tok)
if _, err := conn.Query(query); err != nil {
return errwrap.Wrapf("Error updating role CREATEROLE: {{err}}", err)
}
return nil
}
func setRoleInherit(conn *sql.DB, d *schema.ResourceData) error {
if !d.HasChange(roleInheritAttr) {
return nil
}
inherit := d.Get(roleInheritAttr).(bool)
tok := "NOINHERIT"
if inherit {
tok = "INHERIT"
}
roleName := d.Get(roleNameAttr).(string)
query := fmt.Sprintf("ALTER ROLE %s WITH %s", pq.QuoteIdentifier(roleName), tok)
if _, err := conn.Query(query); err != nil {
return errwrap.Wrapf("Error updating role INHERIT: {{err}}", err)
}
return nil
}
func setRoleLogin(conn *sql.DB, d *schema.ResourceData) error {
if !d.HasChange(roleLoginAttr) {
return nil
}
login := d.Get(roleLoginAttr).(bool)
tok := "NOLOGIN"
if login {
tok = "LOGIN"
}
roleName := d.Get(roleNameAttr).(string)
query := fmt.Sprintf("ALTER ROLE %s WITH %s", pq.QuoteIdentifier(roleName), tok)
if _, err := conn.Query(query); err != nil {
return errwrap.Wrapf("Error updating role LOGIN: {{err}}", err)
}
return nil
}
func setRoleReplication(conn *sql.DB, d *schema.ResourceData) error {
if !d.HasChange(roleReplicationAttr) {
return nil
}
replication := d.Get(roleReplicationAttr).(bool)
tok := "NOREPLICATION"
if replication {
tok = "REPLICATION"
}
roleName := d.Get(roleNameAttr).(string)
query := fmt.Sprintf("ALTER ROLE %s WITH %s", pq.QuoteIdentifier(roleName), tok)
if _, err := conn.Query(query); err != nil {
return errwrap.Wrapf("Error updating role REPLICATION: {{err}}", err)
}
return nil
}
func setRoleSuperuser(conn *sql.DB, d *schema.ResourceData) error {
if !d.HasChange(roleSuperuserAttr) {
return nil
}
superuser := d.Get(roleSuperuserAttr).(bool)
tok := "NOSUPERUSER"
if superuser {
tok = "SUPERUSER"
}
roleName := d.Get(roleNameAttr).(string)
query := fmt.Sprintf("ALTER ROLE %s WITH %s", pq.QuoteIdentifier(roleName), tok)
if _, err := conn.Query(query); err != nil {
return errwrap.Wrapf("Error updating role SUPERUSER: {{err}}", err)
}
return nil
}
func setRoleValidUntil(conn *sql.DB, d *schema.ResourceData) error {
if !d.HasChange(roleValidUntilAttr) {
return nil
}
validUntil := d.Get(roleValidUntilAttr).(string)
if validUntil == "" {
return nil
} else if strings.ToLower(validUntil) == "infinity" {
validUntil = "infinity"
}
roleName := d.Get(roleNameAttr).(string)
query := fmt.Sprintf("ALTER ROLE %s VALID UNTIL '%s'", pq.QuoteIdentifier(roleName), pqQuoteLiteral(validUntil))
if _, err := conn.Query(query); err != nil {
return errwrap.Wrapf("Error updating role VALID UNTIL: {{err}}", err)
}
return nil
} }

View File

@ -10,7 +10,6 @@ import (
) )
func TestAccPostgresqlRole_Basic(t *testing.T) { func TestAccPostgresqlRole_Basic(t *testing.T) {
resource.Test(t, resource.TestCase{ resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) }, PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders, Providers: testAccProviders,
@ -24,6 +23,29 @@ func TestAccPostgresqlRole_Basic(t *testing.T) {
"postgresql_role.myrole2", "name", "myrole2"), "postgresql_role.myrole2", "name", "myrole2"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"postgresql_role.myrole2", "login", "true"), "postgresql_role.myrole2", "login", "true"),
resource.TestCheckResourceAttr(
"postgresql_role.role_with_defaults", "name", "testing_role_with_defaults"),
resource.TestCheckResourceAttr(
"postgresql_role.role_with_defaults", "superuser", "false"),
resource.TestCheckResourceAttr(
"postgresql_role.role_with_defaults", "create_database", "false"),
resource.TestCheckResourceAttr(
"postgresql_role.role_with_defaults", "create_role", "false"),
resource.TestCheckResourceAttr(
"postgresql_role.role_with_defaults", "inherit", "false"),
resource.TestCheckResourceAttr(
"postgresql_role.role_with_defaults", "replication", "false"),
resource.TestCheckResourceAttr(
"postgresql_role.role_with_defaults", "bypass_row_level_security", "false"),
resource.TestCheckResourceAttr(
"postgresql_role.role_with_defaults", "connection_limit", "-1"),
resource.TestCheckResourceAttr(
"postgresql_role.role_with_defaults", "encrypted_password", "true"),
resource.TestCheckResourceAttr(
"postgresql_role.role_with_defaults", "password", ""),
resource.TestCheckResourceAttr(
"postgresql_role.role_with_defaults", "valid_until", "infinity"),
), ),
}, },
}, },
@ -129,4 +151,19 @@ resource "postgresql_role" "role_with_pwd_no_login" {
resource "postgresql_role" "role_simple" { resource "postgresql_role" "role_simple" {
name = "role_simple" name = "role_simple"
} }
resource "postgresql_role" "role_with_defaults" {
name = "testing_role_with_defaults"
superuser = false
create_database = false
create_role = false
inherit = false
login = false
replication = false
bypass_row_level_security = false
connection_limit = -1
encrypted_password = true
password = ""
valid_until = "infinity"
}
` `

View File

@ -0,0 +1,177 @@
package postgresql
import (
"bytes"
"database/sql"
"errors"
"fmt"
"log"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/terraform/helper/schema"
"github.com/lib/pq"
)
const (
schemaNameAttr = "name"
schemaAuthorizationAttr = "authorization"
)
func resourcePostgreSQLSchema() *schema.Resource {
return &schema.Resource{
Create: resourcePostgreSQLSchemaCreate,
Read: resourcePostgreSQLSchemaRead,
Update: resourcePostgreSQLSchemaUpdate,
Delete: resourcePostgreSQLSchemaDelete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},
Schema: map[string]*schema.Schema{
schemaNameAttr: {
Type: schema.TypeString,
Required: true,
Description: "The name of the schema",
},
schemaAuthorizationAttr: {
Type: schema.TypeString,
Optional: true,
Computed: true,
Description: "The role name of the owner of the schema",
},
},
}
}
func resourcePostgreSQLSchemaCreate(d *schema.ResourceData, meta interface{}) error {
c := meta.(*Client)
conn, err := c.Connect()
if err != nil {
return errwrap.Wrapf("Error connecting to PostgreSQL: {{err}}", err)
}
defer conn.Close()
schemaName := d.Get(schemaNameAttr).(string)
b := bytes.NewBufferString("CREATE SCHEMA ")
fmt.Fprintf(b, pq.QuoteIdentifier(schemaName))
if v, ok := d.GetOk(schemaAuthorizationAttr); ok {
fmt.Fprint(b, " AUTHORIZATION ", pq.QuoteIdentifier(v.(string)))
}
query := b.String()
_, err = conn.Query(query)
if err != nil {
return errwrap.Wrapf(fmt.Sprintf("Error creating schema %s: {{err}}", schemaName), err)
}
d.SetId(schemaName)
return resourcePostgreSQLSchemaRead(d, meta)
}
func resourcePostgreSQLSchemaDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*Client)
conn, err := client.Connect()
if err != nil {
return err
}
defer conn.Close()
schemaName := d.Get(schemaNameAttr).(string)
query := fmt.Sprintf("DROP SCHEMA %s", pq.QuoteIdentifier(schemaName))
_, err = conn.Query(query)
if err != nil {
return errwrap.Wrapf("Error deleting schema: {{err}}", err)
}
d.SetId("")
return nil
}
func resourcePostgreSQLSchemaRead(d *schema.ResourceData, meta interface{}) error {
c := meta.(*Client)
conn, err := c.Connect()
if err != nil {
return err
}
defer conn.Close()
schemaId := d.Id()
var schemaName, schemaAuthorization string
err = conn.QueryRow("SELECT nspname, pg_catalog.pg_get_userbyid(nspowner) FROM pg_catalog.pg_namespace WHERE nspname=$1", schemaId).Scan(&schemaName, &schemaAuthorization)
switch {
case err == sql.ErrNoRows:
log.Printf("[WARN] PostgreSQL schema (%s) not found", schemaId)
d.SetId("")
return nil
case err != nil:
return errwrap.Wrapf("Error reading schema: {{err}}", err)
default:
d.Set(schemaNameAttr, schemaName)
d.Set(schemaAuthorizationAttr, schemaAuthorization)
d.SetId(schemaName)
return nil
}
}
func resourcePostgreSQLSchemaUpdate(d *schema.ResourceData, meta interface{}) error {
c := meta.(*Client)
conn, err := c.Connect()
if err != nil {
return err
}
defer conn.Close()
if err := setSchemaName(conn, d); err != nil {
return err
}
if err := setSchemaAuthorization(conn, d); err != nil {
return err
}
return resourcePostgreSQLSchemaRead(d, meta)
}
func setSchemaName(conn *sql.DB, d *schema.ResourceData) error {
if !d.HasChange(schemaNameAttr) {
return nil
}
oraw, nraw := d.GetChange(schemaNameAttr)
o := oraw.(string)
n := nraw.(string)
if n == "" {
return errors.New("Error setting schema name to an empty string")
}
query := fmt.Sprintf("ALTER SCHEMA %s RENAME TO %s", pq.QuoteIdentifier(o), pq.QuoteIdentifier(n))
if _, err := conn.Query(query); err != nil {
return errwrap.Wrapf("Error updating schema NAME: {{err}}", err)
}
d.SetId(n)
return nil
}
func setSchemaAuthorization(conn *sql.DB, d *schema.ResourceData) error {
if !d.HasChange(schemaAuthorizationAttr) {
return nil
}
schemaAuthorization := d.Get(schemaAuthorizationAttr).(string)
if schemaAuthorization == "" {
return nil
}
schemaName := d.Get(schemaNameAttr).(string)
query := fmt.Sprintf("ALTER SCHEMA %s OWNER TO %s", pq.QuoteIdentifier(schemaName), pq.QuoteIdentifier(schemaAuthorization))
if _, err := conn.Query(query); err != nil {
return errwrap.Wrapf("Error updating schema AUTHORIZATION: {{err}}", err)
}
return nil
}

View File

@ -0,0 +1,155 @@
package postgresql
import (
"database/sql"
"fmt"
"testing"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestAccPostgresqlSchema_Basic(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckPostgresqlSchemaDestroy,
Steps: []resource.TestStep{
{
Config: testAccPostgresqlSchemaConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckPostgresqlSchemaExists("postgresql_schema.test1", "foo"),
resource.TestCheckResourceAttr(
"postgresql_role.myrole3", "name", "myrole3"),
resource.TestCheckResourceAttr(
"postgresql_role.myrole3", "login", "true"),
resource.TestCheckResourceAttr(
"postgresql_schema.test1", "name", "foo"),
// `postgres` is a calculated value
// based on the username used in the
// provider
resource.TestCheckResourceAttr(
"postgresql_schema.test1", "authorization", "postgres"),
),
},
},
})
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckPostgresqlSchemaDestroy,
Steps: []resource.TestStep{
{
Config: testAccPostgresqlSchemaAuthConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckPostgresqlSchemaExists("postgresql_schema.test2", "foo2"),
resource.TestCheckResourceAttr(
"postgresql_role.myrole4", "name", "myrole4"),
resource.TestCheckResourceAttr(
"postgresql_role.myrole4", "login", "true"),
resource.TestCheckResourceAttr(
"postgresql_schema.test2", "name", "foo2"),
resource.TestCheckResourceAttr(
"postgresql_schema.test2", "authorization", "myrole4"),
),
},
},
})
}
func testAccCheckPostgresqlSchemaDestroy(s *terraform.State) error {
client := testAccProvider.Meta().(*Client)
for _, rs := range s.RootModule().Resources {
if rs.Type != "postgresql_schema" {
continue
}
exists, err := checkSchemaExists(client, rs.Primary.ID)
if err != nil {
return fmt.Errorf("Error checking schema %s", err)
}
if exists {
return fmt.Errorf("Schema still exists after destroy")
}
}
return nil
}
func testAccCheckPostgresqlSchemaExists(n string, schemaName string) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[n]
if !ok {
return fmt.Errorf("Resource not found: %s", n)
}
if rs.Primary.ID == "" {
return fmt.Errorf("No ID is set")
}
actualSchemaName := rs.Primary.Attributes["name"]
if actualSchemaName != schemaName {
return fmt.Errorf("Wrong value for schema name expected %s got %s", schemaName, actualSchemaName)
}
client := testAccProvider.Meta().(*Client)
exists, err := checkSchemaExists(client, rs.Primary.ID)
if err != nil {
return fmt.Errorf("Error checking schema %s", err)
}
if !exists {
return fmt.Errorf("Schema not found")
}
return nil
}
}
func checkSchemaExists(client *Client, schemaName string) (bool, error) {
conn, err := client.Connect()
if err != nil {
return false, err
}
defer conn.Close()
var _rez string
err = conn.QueryRow("SELECT nspname FROM pg_catalog.pg_namespace WHERE nspname=$1", schemaName).Scan(&_rez)
switch {
case err == sql.ErrNoRows:
return false, nil
case err != nil:
return false, fmt.Errorf("Error reading info about schema: %s", err)
default:
return true, nil
}
}
var testAccPostgresqlSchemaConfig = `
resource "postgresql_role" "myrole3" {
name = "myrole3"
login = true
}
resource "postgresql_schema" "test1" {
name = "foo"
}
`
var testAccPostgresqlSchemaAuthConfig = `
resource "postgresql_role" "myrole4" {
name = "myrole4"
login = true
}
resource "postgresql_schema" "test2" {
name = "foo2"
authorization = "${postgresql_role.myrole4.name}"
}
`

View File

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

View File

@ -18,9 +18,10 @@ Use the navigation to the left to read about the available resources.
provider "postgresql" { provider "postgresql" {
host = "postgres_server_ip" host = "postgres_server_ip"
port = 5432 port = 5432
database = "postgres"
username = "postgres_user" username = "postgres_user"
password = "postgres_password" password = "postgres_password"
ssl_mode = "require" sslmode = "require"
connect_timeout = 15 connect_timeout = 15
} }
@ -61,9 +62,17 @@ The following arguments are supported:
* `host` - (Required) The address for the postgresql server connection. * `host` - (Required) The address for the postgresql server connection.
* `port` - (Optional) The port for the postgresql server connection. The default is `5432`. * `port` - (Optional) The port for the postgresql server connection. The default is `5432`.
* `database` - (Optional) Database to connect to. The default is `postgres`.
* `username` - (Required) Username for the server connection. * `username` - (Required) Username for the server connection.
* `password` - (Optional) Password for the server connection. * `password` - (Optional) Password for the server connection.
* `ssl_mode` - (Optional) Set the priority for an SSL connection to the server. * `sslmode` - (Optional) Set the priority for an SSL connection to the server.
* `connect_timeout` - (Optional) Maximum wait for connection, in seconds. Zero means wait indefinitely, the default is `15`. Valid values for `sslmode` are (note: `prefer` is not supported by Go's
The default is `prefer`; the full set of options and their implications [`lib/pq`](https://godoc.org/github.com/lib/pq)):
can be seen [in the libpq SSL guide](http://www.postgresql.org/docs/9.4/static/libpq-ssl.html#LIBPQ-SSL-PROTECTION). * disable - No SSL
* require - Always SSL (the default, also skip verification)
* verify-ca - Always SSL (verify that the certificate presented by the server was signed by a trusted CA)
* verify-full - Always SSL (verify that the certification presented by the server was signed by a trusted CA and the server host name matches the one in the certificate)
Additional information on the options and their implications can be seen
[in the `libpq(3)` SSL guide](http://www.postgresql.org/docs/current/static/libpq-ssl.html#LIBPQ-SSL-PROTECTION).
* `connect_timeout` - (Optional) Maximum wait for connection, in seconds. The
default is `180s`. Zero or not specified means wait indefinitely.

View File

@ -8,8 +8,8 @@ description: |-
# postgresql\_database # postgresql\_database
The ``postgresql_database`` resource creates and manages a database on a PostgreSQL The ``postgresql_database`` resource creates and manages a database instance on
server. a PostgreSQL server.
## Usage ## Usage
@ -18,13 +18,94 @@ server.
resource "postgresql_database" "my_db" { resource "postgresql_database" "my_db" {
name = "my_db" name = "my_db"
owner = "my_role" owner = "my_role"
template = "template0"
collation = "C"
connection_limit = -1
allow_connections = true
} }
``` ```
## Argument Reference ## Argument Reference
* `name` - (Required) The name of the database. Must be unique on the PostgreSQL server instance * `name` - (Required) The name of the database. Must be unique on the PostgreSQL
where it is configured. 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
established 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 = "testdb1"
}
```
It is possible to import a `postgresql_database` resource with the following
command:
```
$ terraform import postgresql_database.db1 testdb1
```
Where `testdb1` is the name of the database to import and
`postgresql_database.db1` is the name of the resource whose state will be
populated as a result of the command.

View File

@ -3,7 +3,7 @@ layout: "postgresql"
page_title: "PostgreSQL: postgresql_role" page_title: "PostgreSQL: postgresql_role"
sidebar_current: "docs-postgresql-resource-postgresql_role" sidebar_current: "docs-postgresql-resource-postgresql_role"
description: |- description: |-
Creates and manages a database on a PostgreSQL server. Creates and manages a role on a PostgreSQL server.
--- ---
# postgresql\_role # postgresql\_role
@ -19,19 +19,93 @@ resource "postgresql_role" "my_role" {
name = "my_role" name = "my_role"
login = true login = true
password = "mypass" password = "mypass"
encrypted = true
} }
resource "postgresql_role" "my_replication_role" {
name = "replication_role"
replication = true
login = true
connection_limit = 5
password = "md5c98cbfeb6a347a47eb8e96cfb4c4b890"
}
``` ```
## Argument Reference ## Argument Reference
* `name` - (Required) The name of the role. Must be unique on the PostgreSQL server instance * `name` - (Required) The name of the role. Must be unique on the PostgreSQL
where it is configured. server instance where it is configured.
* `login` - (Optional) Configures whether a role is allowed to log in; that is, whether the role can be given as the initial session authorization name during client connection. Corresponds to the LOGIN/NOLOGIN * `superuser` - (Optional) Defines whether the role is a "superuser", and
clauses in 'CREATE ROLE'. Default value is false. therefore can override all access restrictions within the database. Default
value is `false`.
* `password` - (Optional) Sets the role's password. (A password is only of use for roles having the LOGIN attribute, but you can nonetheless define one for roles without it.) If you do not plan to use password authentication you can omit this option. If no password is specified, the password will be set to null and password authentication will always fail for that user. * `create_database` - (Optional) Defines a role's ability to execute `CREATE
DATABASE`. Default value is `false`.
* `encrypted` - (Optional) Corresponds to ENCRYPTED, UNENCRYPTED in PostgreSQL. This controls whether the password is stored encrypted in the system catalogs. Default is false. * `create_role` - (Optional) Defines a role's ability to execute `CREATE ROLE`.
A role with this privilege can also alter and drop other roles. Default value
is `false`.
* `inherit` - (Optional) Defines whether a role "inherits" the privileges of
roles it is a member of. Default value is `true`.
* `login` - (Optional) Defines whether role is allowed to log in. Roles without
this attribute are useful for managing database privileges, but are not users
in the usual sense of the word. Default value is `false`.
* `replication` - (Optional) Defines whether a role is allowed to initiate
streaming replication or put the system in and out of backup mode. Default
value is `false`
* `bypass_row_level_security` - (Optional) Defines whether a role bypasses every
row-level security (RLS) policy. Default value is `false`.
* `connection_limit` - (Optional) If this role can log in, this specifies how
many concurrent connections the role can establish. `-1` (the default) means no
limit.
* `encrypted_password` - (Optional) Defines whether the password is stored
encrypted in the system catalogs. Default value is `true`. NOTE: this value
is always set (to the conservative and safe value), but may interfere with the
behavior of
[PostgreSQL's `password_encryption` setting](https://www.postgresql.org/docs/current/static/runtime-config-connection.html#GUC-PASSWORD-ENCRYPTION).
* `password` - (Optional) Sets the role's password. (A password is only of use
for roles having the `login` attribute set to true, but you can nonetheless
define one for roles without it.) Roles without a password explicitly set are
left alone. If the password is set to the magic value `NULL`, the password
will be always be cleared.
* `valid_until` - (Optional) Defines the date and time after which the role's
password is no longer valid. Established connections past this `valid_time`
will have to be manually terminated. This value corresponds to a PostgreSQL
datetime. If omitted or the magic value `NULL` is used, `valid_until` will be
set to `infinity`. Default is `NULL`, therefore `infinity`.
## Import Example
`postgresql_role` supports importing resources. Supposing the following
Terraform:
```
provider "postgresql" {
alias = "admindb"
}
resource "postgresql_role" "replication_role" {
provider = "postgresql.admindb"
name = "replication_name"
}
```
It is possible to import a `postgresql_role` resource with the following
command:
```
$ terraform import postgresql_role.replication_role replication_name
```
Where `replication_name` is the name of the role to import and
`postgresql_role.replication_role` is the name of the resource whose state will
be populated as a result of the command.

View File

@ -0,0 +1,58 @@
---
layout: "postgresql"
page_title: "PostgreSQL: postgresql_schema"
sidebar_current: "docs-postgresql-resource-postgresql_schema"
description: |-
Creates and manages a schema within a PostgreSQL database.
---
# postgresql\_schema
The ``postgresql_schema`` resource creates and manages a schema within a
PostgreSQL database.
## Usage
```
resource "postgresql_schema" "my_schema" {
name = "my_schema"
authorization = "my_role"
}
```
## Argument Reference
* `name` - (Required) The name of the schema. Must be unique in the PostgreSQL
database instance where it is configured.
* `authorization` - (Optional) The owner of the schema. Defaults to the
username configured in the schema's provider.
## Import Example
`postgresql_schema` supports importing resources. Supposing the following
Terraform:
```
provider "postgresql" {
alias = "admindb"
}
resource "postgresql_schema" "schema_foo" {
provider = "postgresql.admindb"
name = "my_schema"
}
```
It is possible to import a `postgresql_schema` resource with the following
command:
```
$ terraform import postgresql_schema.schema_foo my_schema
```
Where `my_schema` is the name of the schema in the PostgreSQL database and
`postgresql_schema.schema_foo` is the name of the resource whose state will be
populated as a result of the command.