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,18 +3,22 @@ package postgresql
import (
"database/sql"
"fmt"
"log"
_ "github.com/lib/pq" //PostgreSQL db
)
// Config - provider config
type Config struct {
Host string
Port int
Username string
Password string
SslMode string
Timeout int
Host string
Port int
Database string
Username string
Password string
SSLMode string
ApplicationName string
Timeout int
ConnectTimeoutSec int
}
// Client struct holding connection string
@ -25,8 +29,14 @@ type Client struct {
// NewClient returns new client config
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{
connStr: connStr,
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
import (
"bytes"
"fmt"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
@ -12,61 +15,89 @@ func Provider() terraform.ResourceProvider {
Schema: map[string]*schema.Schema{
"host": {
Type: schema.TypeString,
Required: true,
DefaultFunc: schema.MultiEnvDefaultFunc([]string{"PGHOST", "POSTGRESQL_HOST"}, nil),
Description: "The PostgreSQL server address",
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("PGHOST", nil),
Description: "Name of PostgreSQL server address to connect to",
},
"port": {
Type: schema.TypeInt,
Optional: true,
Default: 5432,
Description: "The PostgreSQL server port",
DefaultFunc: schema.EnvDefaultFunc("PGPORT", 5432),
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": {
Type: schema.TypeString,
Required: true,
DefaultFunc: schema.MultiEnvDefaultFunc([]string{"PGUSER", "POSTGRESQL_USER"}, nil),
Description: "Username for PostgreSQL server connection",
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("PGUSER", "postgres"),
Description: "PostgreSQL user name to connect as",
},
"password": {
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.MultiEnvDefaultFunc([]string{"PGPASSWORD", "POSTGRESQL_PASSWORD"}, nil),
Description: "Password for PostgreSQL server connection",
DefaultFunc: schema.EnvDefaultFunc("PGPASSWORD", nil),
Description: "Password to be used if the PostgreSQL server demands password authentication",
},
"ssl_mode": {
"sslmode": {
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("PGSSLMODE", "require"),
Description: "Connection mode for PostgreSQL server",
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": {
Type: schema.TypeString,
Optional: true,
Deprecated: "Rename PostgreSQL provider `ssl_mode` attribute to `sslmode`",
},
"connect_timeout": {
Type: schema.TypeInt,
Optional: true,
Default: 15,
DefaultFunc: schema.EnvDefaultFunc("PGCONNECT_TIMEOUT", nil),
Description: "Maximum wait for connection, in seconds. Zero or not specified means wait indefinitely.",
Type: schema.TypeInt,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("PGCONNECT_TIMEOUT", 180),
Description: "Maximum wait for connection, in seconds. Zero or not specified means wait indefinitely.",
ValidateFunc: validateConnTimeout,
},
},
ResourcesMap: map[string]*schema.Resource{
"postgresql_database": resourcePostgreSQLDatabase(),
"postgresql_role": resourcePostgreSQLRole(),
"postgresql_extension": resourcePostgreSQLExtension(),
"postgresql_schema": resourcePostgreSQLSchema(),
"postgresql_role": resourcePostgreSQLRole(),
},
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) {
var sslMode string
if sslModeRaw, ok := d.GetOk("sslmode"); ok {
sslMode = sslModeRaw.(string)
} else {
sslMode = d.Get("ssl_mode").(string)
}
config := Config{
Host: d.Get("host").(string),
Port: d.Get("port").(int),
Username: d.Get("username").(string),
Password: d.Get("password").(string),
SslMode: d.Get("ssl_mode").(string),
Timeout: d.Get("connect_timeout").(int),
Host: d.Get("host").(string),
Port: d.Get("port").(int),
Database: d.Get("database").(string),
Username: d.Get("username").(string),
Password: d.Get("password").(string),
SSLMode: sslMode,
ApplicationName: tfAppName(),
ConnectTimeoutSec: d.Get("connect_timeout").(int),
}
client, err := config.NewClient()
@ -76,3 +107,16 @@ func providerConfigure(d *schema.ResourceData) (interface{}, error) {
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 == "" {
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
import (
"bytes"
"database/sql"
"errors"
"fmt"
"log"
"strings"
"github.com/hashicorp/errwrap"
@ -10,54 +13,170 @@ 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": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
dbNameAttr: {
Type: schema.TypeString,
Required: true,
Description: "The PostgreSQL database name to connect to",
},
"owner": {
Type: schema.TypeString,
Optional: true,
Computed: true,
dbOwnerAttr: {
Type: schema.TypeString,
Optional: 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 {
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)
dbOwner := d.Get("owner").(string)
connUsername := client.username
dbName := d.Get(dbNameAttr).(string)
b := bytes.NewBufferString("CREATE DATABASE ")
fmt.Fprint(b, pq.QuoteIdentifier(dbName))
var dbOwnerCfg string
if dbOwner != "" {
dbOwnerCfg = fmt.Sprintf("WITH OWNER=%s", pq.QuoteIdentifier(dbOwner))
} else {
dbOwnerCfg = ""
// Handle each option individually and stream results into the query
// buffer.
switch v, ok := d.GetOk(dbOwnerAttr); {
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
err = grantRoleMembership(conn, dbOwner, connUsername)
if err != nil {
return err
switch v, ok := d.GetOk(dbTemplateAttr); {
case ok:
fmt.Fprint(b, " TEMPLATE ", pq.QuoteIdentifier(v.(string)))
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)
if err != nil {
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 {
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 {
dbName := d.Get(dbNameAttr).(string)
if isTemplate := d.Get(dbIsTemplateAttr).(bool); isTemplate {
// Template databases must have this attribute cleared before
// they can be dropped.
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
}
@ -97,64 +221,204 @@ 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", 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("")
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)
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)
}
func grantRoleMembership(conn *sql.DB, dbOwner string, connUsername string) error {
if dbOwner != "" && dbOwner != connUsername {
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 errwrap.Wrapf("Error granting membership: {{err}}", err)
}
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
}
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
}

View File

@ -2,29 +2,90 @@ package postgresql
import (
"database/sql"
"errors"
"fmt"
"testing"
"errors"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestAccPostgresqlDatabase_Basic(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckPostgresqlDatabaseDestroy,
Steps: []resource.TestStep{
{
Config: testAccPostgresqlDatabaseConfig,
Config: testAccPostgreSQLDatabaseConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckPostgresqlDatabaseExists("postgresql_database.mydb"),
resource.TestCheckResourceAttr(
"postgresql_database.mydb", "name", "mydb"),
resource.TestCheckResourceAttr(
"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) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckPostgresqlDatabaseDestroy,
Steps: []resource.TestStep{
{
Config: testAccPostgresqlDatabaseConfig,
Config: testAccPostgreSQLDatabaseConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckPostgresqlDatabaseExists("postgresql_database.mydb_default_owner"),
resource.TestCheckResourceAttr(
@ -119,7 +179,7 @@ func checkDatabaseExists(client *Client, dbName string) (bool, error) {
}
}
var testAccPostgresqlDatabaseConfig = `
var testAccPostgreSQLDatabaseConfig = `
resource "postgresql_role" "myrole" {
name = "myrole"
login = true
@ -135,6 +195,45 @@ resource "postgresql_database" "mydb2" {
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" {
name = "mydb_default_owner"
}

View File

@ -1,86 +1,125 @@
package postgresql
import (
"bytes"
"database/sql"
"errors"
"fmt"
"log"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/terraform/helper/schema"
"github.com/lib/pq"
)
const (
extNameAttr = "name"
extSchemaAttr = "schema"
extVersionAttr = "version"
)
func resourcePostgreSQLExtension() *schema.Resource {
return &schema.Resource{
Create: resourcePostgreSQLExtensionCreate,
Read: resourcePostgreSQLExtensionRead,
Update: resourcePostgreSQLExtensionUpdate,
Delete: resourcePostgreSQLExtensionDelete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},
Schema: map[string]*schema.Schema{
"name": {
extNameAttr: {
Type: schema.TypeString,
Required: 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 {
client := meta.(*Client)
conn, err := client.Connect()
c := meta.(*Client)
conn, err := c.Connect()
if err != nil {
return err
}
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)
if err != nil {
return errwrap.Wrapf("Error creating extension: {{err}}", err)
}
d.SetId(extensionName)
d.SetId(extName)
return resourcePostgreSQLExtensionRead(d, meta)
}
func resourcePostgreSQLExtensionRead(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()
extensionName := d.Get("name").(string)
var hasExtension bool
err = conn.QueryRow("SELECT 1 from pg_extension d WHERE extname=$1", extensionName).Scan(&hasExtension)
extID := d.Id()
var extName, extSchema, extVersion string
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)
switch {
case err == sql.ErrNoRows:
log.Printf("[WARN] PostgreSQL extension (%s) not found", d.Id())
d.SetId("")
return nil
case err != nil:
return errwrap.Wrapf("Error reading extension: {{err}}", err)
default:
d.Set("extension", hasExtension)
d.Set(extNameAttr, extName)
d.Set(extSchemaAttr, extSchema)
d.Set(extVersionAttr, extVersion)
d.SetId(extName)
return nil
}
}
func resourcePostgreSQLExtensionDelete(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()
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)
if err != nil {
return errwrap.Wrapf("Error deleting extension: {{err}}", err)
@ -90,3 +129,68 @@ func resourcePostgreSQLExtensionDelete(d *schema.ResourceData, meta interface{})
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) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
@ -22,6 +21,15 @@ func TestAccPostgresqlExtension_Basic(t *testing.T) {
testAccCheckPostgresqlExtensionExists("postgresql_extension.myextension"),
resource.TestCheckResourceAttr(
"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) {
conn, err := client.Connect()
if err != nil {
@ -83,8 +127,8 @@ func checkExtensionExists(client *Client, extensionName string) (bool, error) {
}
defer conn.Close()
var _rez int
err = conn.QueryRow("SELECT 1 from pg_extension d WHERE extname=$1", extensionName).Scan(&_rez)
var _rez bool
err = conn.QueryRow("SELECT TRUE from pg_catalog.pg_extension d WHERE extname=$1", extensionName).Scan(&_rez)
switch {
case err == sql.ErrNoRows:
return false, nil
@ -100,3 +144,25 @@ resource "postgresql_extension" "myextension" {
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 (
"database/sql"
"errors"
"fmt"
"log"
"strings"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/terraform/helper/schema"
"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 {
return &schema.Resource{
Create: resourcePostgreSQLRoleCreate,
Read: resourcePostgreSQLRoleRead,
Update: resourcePostgreSQLRoleUpdate,
Delete: resourcePostgreSQLRoleDelete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},
Schema: map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
roleNameAttr: {
Type: schema.TypeString,
Required: true,
Description: "The name of the role",
},
"login": {
Type: schema.TypeBool,
Optional: true,
ForceNew: false,
Default: false,
rolePasswordAttr: {
Type: schema.TypeString,
Optional: true,
Computed: true,
Sensitive: true,
DefaultFunc: schema.EnvDefaultFunc("PGPASSWORD", nil),
Description: "Sets the role's password",
},
"password": {
Type: schema.TypeString,
Optional: true,
ForceNew: false,
roleDepEncryptedAttr: {
Type: schema.TypeString,
Optional: true,
Deprecated: fmt.Sprintf("Rename PostgreSQL role resource attribute %q to %q", roleDepEncryptedAttr, roleEncryptedPassAttr),
},
"encrypted": {
Type: schema.TypeBool,
Optional: true,
ForceNew: false,
Default: false,
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,
Optional: true,
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 {
client := meta.(*Client)
conn, err := client.Connect()
c := meta.(*Client)
conn, err := c.Connect()
if err != nil {
return err
return errwrap.Wrapf("Error connecting to PostgreSQL: {{err}}", err)
}
defer conn.Close()
roleName := d.Get("name").(string)
loginAttr := getLoginStr(d.Get("login").(bool))
password := d.Get("password").(string)
stringOpts := []struct {
hclKey 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)
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)
@ -76,8 +240,7 @@ func resourcePostgreSQLRoleDelete(d *schema.ResourceData, meta interface{}) erro
}
defer conn.Close()
roleName := d.Get("name").(string)
roleName := d.Get(roleNameAttr).(string)
query := fmt.Sprintf("DROP ROLE %s", pq.QuoteIdentifier(roleName))
_, err = conn.Query(query)
if err != nil {
@ -90,91 +253,296 @@ func resourcePostgreSQLRoleDelete(d *schema.ResourceData, meta interface{}) erro
}
func resourcePostgreSQLRoleRead(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()
roleName := d.Get("name").(string)
var canLogin bool
err = conn.QueryRow("SELECT rolcanlogin FROM pg_roles WHERE rolname=$1", roleName).Scan(&canLogin)
roleId := d.Id()
var roleSuperuser, roleInherit, roleCreateRole, roleCreateDB, roleCanLogin, roleReplication, roleBypassRLS bool
var roleConnLimit int
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 {
case err == sql.ErrNoRows:
log.Printf("[WARN] PostgreSQL role (%s) not found", roleId)
d.SetId("")
return nil
case err != nil:
return errwrap.Wrapf("Error reading role: {{err}}", err)
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
}
}
func resourcePostgreSQLRoleUpdate(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()
d.Partial(true)
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 := setRoleName(conn, d); err != nil {
return err
}
password := d.Get("password").(string)
if d.HasChange("password") {
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 := setRoleBypassRLS(conn, d); err != nil {
return err
}
if d.HasChange("encrypted") {
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 encrypted attribute for role: {{err}}", err)
}
d.SetPartial("encrypted")
if err := setRoleConnLimit(conn, d); err != nil {
return err
}
if err := setRoleCreateDB(conn, d); err != nil {
return err
}
if err := setRoleCreateRole(conn, d); err != nil {
return err
}
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)
}
func getLoginStr(canLogin bool) string {
if canLogin {
return "login"
func setRoleName(conn *sql.DB, d *schema.ResourceData) error {
if !d.HasChange(roleNameAttr) {
return nil
}
return "nologin"
oraw, nraw := d.GetChange(roleNameAttr)
o := oraw.(string)
n := nraw.(string)
if n == "" {
return errors.New("Error setting role name to an empty string")
}
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 getEncryptedStr(isEncrypted bool) string {
if isEncrypted {
return "encrypted"
func setRoleBypassRLS(conn *sql.DB, d *schema.ResourceData) error {
if !d.HasChange(roleBypassRLSAttr) {
return nil
}
return "unencrypted"
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) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
@ -24,6 +23,29 @@ func TestAccPostgresqlRole_Basic(t *testing.T) {
"postgresql_role.myrole2", "name", "myrole2"),
resource.TestCheckResourceAttr(
"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" {
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_subnet_v2
### PostgreSQL
* postgresql_database
### Triton

View File

@ -18,9 +18,10 @@ Use the navigation to the left to read about the available resources.
provider "postgresql" {
host = "postgres_server_ip"
port = 5432
database = "postgres"
username = "postgres_user"
password = "postgres_password"
ssl_mode = "require"
sslmode = "require"
connect_timeout = 15
}
@ -61,9 +62,17 @@ The following arguments are supported:
* `host` - (Required) The address for the postgresql server connection.
* `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.
* `password` - (Optional) Password for the server connection.
* `ssl_mode` - (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`.
The default is `prefer`; the full set of options and their implications
can be seen [in the libpq SSL guide](http://www.postgresql.org/docs/9.4/static/libpq-ssl.html#LIBPQ-SSL-PROTECTION).
* `sslmode` - (Optional) Set the priority for an SSL connection to the server.
Valid values for `sslmode` are (note: `prefer` is not supported by Go's
[`lib/pq`](https://godoc.org/github.com/lib/pq)):
* 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
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,94 @@ 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
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"
sidebar_current: "docs-postgresql-resource-postgresql_role"
description: |-
Creates and manages a database on a PostgreSQL server.
Creates and manages a role on a PostgreSQL server.
---
# postgresql\_role
@ -19,19 +19,93 @@ resource "postgresql_role" "my_role" {
name = "my_role"
login = true
password = "mypass"
encrypted = true
}
resource "postgresql_role" "my_replication_role" {
name = "replication_role"
replication = true
login = true
connection_limit = 5
password = "md5c98cbfeb6a347a47eb8e96cfb4c4b890"
}
```
## Argument Reference
* `name` - (Required) The name of the role. Must be unique on the PostgreSQL server instance
where it is configured.
* `name` - (Required) The name of the role. Must be unique on the PostgreSQL
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
clauses in 'CREATE ROLE'. Default value is false.
* `superuser` - (Optional) Defines whether the role is a "superuser", and
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.