2015-10-27 11:04:19 +01:00
package postgresql
import (
"database/sql"
2016-11-06 09:57:59 +01:00
"errors"
2015-10-27 11:04:19 +01:00
"fmt"
2016-11-06 09:57:59 +01:00
"log"
2015-10-27 11:04:19 +01:00
"strings"
2016-09-05 23:46:40 +02:00
"github.com/hashicorp/errwrap"
2015-10-27 11:04:19 +01:00
"github.com/hashicorp/terraform/helper/schema"
"github.com/lib/pq"
)
2016-11-06 09:57:59 +01:00
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"
)
2016-09-06 00:04:48 +02:00
func resourcePostgreSQLDatabase ( ) * schema . Resource {
2015-10-27 11:04:19 +01:00
return & schema . Resource {
2016-09-06 00:04:48 +02:00
Create : resourcePostgreSQLDatabaseCreate ,
Read : resourcePostgreSQLDatabaseRead ,
Update : resourcePostgreSQLDatabaseUpdate ,
Delete : resourcePostgreSQLDatabaseDelete ,
2016-11-06 09:57:59 +01:00
Importer : & schema . ResourceImporter {
State : schema . ImportStatePassthrough ,
} ,
2015-10-27 11:04:19 +01:00
Schema : map [ string ] * schema . Schema {
2016-11-06 09:57:59 +01:00
dbNameAttr : {
2016-09-06 08:37:39 +02:00
Type : schema . TypeString ,
Required : true ,
Description : "The PostgreSQL database name to connect to" ,
2015-10-27 11:04:19 +01:00
} ,
2016-11-06 09:57:59 +01:00
dbOwnerAttr : {
2016-09-06 08:37:39 +02:00
Type : schema . TypeString ,
Optional : true ,
2016-09-06 08:40:13 +02:00
Computed : true ,
2016-09-06 08:37:39 +02:00
Description : "The role name of the user who will own the new database" ,
2015-10-27 11:04:19 +01:00
} ,
2016-11-06 09:57:59 +01:00
dbTemplateAttr : {
2016-09-06 03:40:35 +02:00
Type : schema . TypeString ,
Optional : true ,
2016-11-06 09:57:59 +01:00
ForceNew : true ,
Computed : true ,
2016-09-06 08:40:13 +02:00
Description : "The name of the template from which to create the new database" ,
2016-09-06 03:40:35 +02:00
} ,
2016-11-06 09:57:59 +01:00
dbEncodingAttr : {
2016-09-06 03:40:35 +02:00
Type : schema . TypeString ,
Optional : true ,
2016-09-06 08:40:13 +02:00
Computed : true ,
2016-09-06 08:40:28 +02:00
ForceNew : true ,
2016-09-06 08:40:13 +02:00
Description : "Character set encoding to use in the new database" ,
2016-09-06 03:40:35 +02:00
} ,
2016-11-06 09:57:59 +01:00
dbCollationAttr : {
2016-09-06 03:40:35 +02:00
Type : schema . TypeString ,
Optional : true ,
2016-09-06 08:40:13 +02:00
Computed : true ,
2016-09-06 08:40:28 +02:00
ForceNew : true ,
2016-09-06 08:40:13 +02:00
Description : "Collation order (LC_COLLATE) to use in the new database" ,
2016-09-06 03:40:35 +02:00
} ,
2016-11-06 09:57:59 +01:00
dbCTypeAttr : {
2016-09-06 03:40:35 +02:00
Type : schema . TypeString ,
Optional : true ,
2016-09-06 08:40:13 +02:00
Computed : true ,
2016-09-06 08:40:28 +02:00
ForceNew : true ,
2016-09-06 08:40:13 +02:00
Description : "Character classification (LC_CTYPE) to use in the new database" ,
2016-09-06 03:40:35 +02:00
} ,
2016-11-06 09:57:59 +01:00
dbTablespaceAttr : {
2016-09-06 03:40:35 +02:00
Type : schema . TypeString ,
Optional : true ,
2016-09-06 08:40:13 +02:00
Computed : true ,
Description : "The name of the tablespace that will be associated with the new database" ,
2016-09-06 03:40:35 +02:00
} ,
2016-11-06 09:57:59 +01:00
dbConnLimitAttr : {
2016-09-06 08:39:22 +02:00
Type : schema . TypeInt ,
Optional : true ,
Computed : true ,
Description : "How many concurrent connections can be made to this database" ,
ValidateFunc : validateConnLimit ,
2016-09-06 03:40:35 +02:00
} ,
2016-11-06 09:57:59 +01:00
dbAllowConnsAttr : {
2016-09-06 03:40:35 +02:00
Type : schema . TypeBool ,
Optional : true ,
2016-11-06 17:46:40 +01:00
Default : true ,
2016-09-06 08:40:13 +02:00
Description : "If false then no one can connect to this database" ,
2016-09-06 03:40:35 +02:00
} ,
2016-11-06 09:57:59 +01:00
dbIsTemplateAttr : {
2016-09-06 03:40:35 +02:00
Type : schema . TypeBool ,
Optional : true ,
2016-09-06 08:40:13 +02:00
Computed : true ,
Description : "If true, then this database can be cloned by any user with CREATEDB privileges" ,
2016-09-06 03:40:35 +02:00
} ,
2015-10-27 11:04:19 +01:00
} ,
}
}
2016-09-06 08:39:22 +02:00
func validateConnLimit ( v interface { } , key string ) ( warnings [ ] string , errors [ ] error ) {
value := v . ( int )
if value < - 1 {
errors = append ( errors , fmt . Errorf ( "%d can not be less than -1" , key ) )
}
return
}
2016-09-06 00:04:48 +02:00
func resourcePostgreSQLDatabaseCreate ( d * schema . ResourceData , meta interface { } ) error {
2016-11-06 09:57:59 +01:00
c := meta . ( * Client )
conn , err := c . Connect ( )
2015-10-27 11:04:19 +01:00
if err != nil {
2016-09-05 23:46:40 +02:00
return errwrap . Wrapf ( "Error connecting to PostgreSQL: {{err}}" , err )
2015-10-27 11:04:19 +01:00
}
defer conn . Close ( )
2016-09-06 03:40:35 +02:00
stringOpts := [ ] struct {
hclKey string
sqlKey string
} {
2016-11-06 09:57:59 +01:00
{ dbOwnerAttr , "OWNER" } ,
{ dbTemplateAttr , "TEMPLATE" } ,
{ dbEncodingAttr , "ENCODING" } ,
{ dbCollationAttr , "LC_COLLATE" } ,
{ dbCTypeAttr , "LC_CTYPE" } ,
{ dbTablespaceAttr , "TABLESPACE" } ,
}
intOpts := [ ] struct {
hclKey string
sqlKey string
} {
{ dbConnLimitAttr , "CONNECTION LIMIT" } ,
}
boolOpts := [ ] struct {
hclKey string
sqlKey string
} {
{ dbAllowConnsAttr , "ALLOW_CONNECTIONS" } ,
{ dbIsTemplateAttr , "IS_TEMPLATE" } ,
2015-10-27 11:04:19 +01:00
}
2016-11-06 09:57:59 +01:00
createOpts := make ( [ ] string , 0 , len ( stringOpts ) + len ( intOpts ) + len ( boolOpts ) )
2016-09-06 03:40:35 +02:00
for _ , opt := range stringOpts {
v , ok := d . GetOk ( opt . hclKey )
var val string
if ! ok {
2016-11-06 09:57:59 +01:00
switch {
case opt . hclKey == dbOwnerAttr && v . ( string ) == "" :
// No owner specified in the config, default to using
// the connecting username.
val = c . username
case strings . ToUpper ( v . ( string ) ) == "DEFAULT" &&
( opt . hclKey == dbTemplateAttr ||
opt . hclKey == dbEncodingAttr ||
opt . hclKey == dbCollationAttr ||
opt . hclKey == dbCTypeAttr ) :
// Use the defaults from the template database
// as opposed to best practices.
fallthrough
default :
2016-09-06 03:40:35 +02:00
continue
}
}
2015-10-27 11:04:19 +01:00
2016-09-06 03:40:35 +02:00
val = v . ( string )
2016-11-06 09:57:59 +01:00
switch {
case opt . hclKey == dbOwnerAttr && ( val == "" || strings . ToUpper ( val ) == "DEFAULT" ) :
// Owner was blank/DEFAULT, default to using the connecting username.
val = c . username
d . Set ( dbOwnerAttr , val )
case opt . hclKey == dbTablespaceAttr && ( val == "" || strings . ToUpper ( val ) == "DEFAULT" ) :
val = "pg_default"
d . Set ( dbTablespaceAttr , val )
case opt . hclKey == dbTemplateAttr :
if val == "" {
val = "template0"
d . Set ( dbTemplateAttr , val )
} else if strings . ToUpper ( val ) == "DEFAULT" {
val = ""
}
case opt . hclKey == dbEncodingAttr :
if val == "" {
val = "UTF8"
d . Set ( dbEncodingAttr , val )
} else if strings . ToUpper ( val ) == "DEFAULT" {
val = ""
}
case opt . hclKey == dbCollationAttr :
if val == "" {
val = "C"
d . Set ( dbCollationAttr , val )
} else if strings . ToUpper ( val ) == "DEFAULT" {
val = ""
}
case opt . hclKey == dbCTypeAttr :
if val == "" {
val = "C"
d . Set ( dbCTypeAttr , val )
} else if strings . ToUpper ( val ) == "DEFAULT" {
val = ""
}
2016-09-06 03:40:35 +02:00
}
if val != "" {
createOpts = append ( createOpts , fmt . Sprintf ( "%s=%s" , opt . sqlKey , pq . QuoteIdentifier ( val ) ) )
}
}
for _ , opt := range intOpts {
v , ok := d . GetOk ( opt . hclKey )
if ! ok {
continue
}
val := v . ( int )
createOpts = append ( createOpts , fmt . Sprintf ( "%s=%d" , opt . sqlKey , val ) )
2015-10-27 11:04:19 +01:00
}
2016-09-06 03:40:35 +02:00
for _ , opt := range boolOpts {
2016-11-06 17:46:40 +01:00
v := d . Get ( opt . hclKey )
2016-09-06 03:40:35 +02:00
2016-09-06 08:41:24 +02:00
valStr := "FALSE"
if val := v . ( bool ) ; val {
valStr = "TRUE"
}
createOpts = append ( createOpts , fmt . Sprintf ( "%s=%s" , opt . sqlKey , valStr ) )
2016-09-06 03:40:35 +02:00
}
2016-11-06 09:57:59 +01:00
dbName := d . Get ( dbNameAttr ) . ( string )
2016-09-06 03:40:35 +02:00
createStr := strings . Join ( createOpts , " " )
if len ( createOpts ) > 0 {
createStr = " WITH " + createStr
}
query := fmt . Sprintf ( "CREATE DATABASE %s%s" , pq . QuoteIdentifier ( dbName ) , createStr )
2015-10-27 11:04:19 +01:00
_ , err = conn . Query ( query )
if err != nil {
2016-09-06 00:04:48 +02:00
return errwrap . Wrapf ( fmt . Sprintf ( "Error creating database %s: {{err}}" , dbName ) , err )
2015-10-27 11:04:19 +01:00
}
d . SetId ( dbName )
2016-09-06 00:04:48 +02:00
return resourcePostgreSQLDatabaseRead ( d , meta )
2015-10-27 11:04:19 +01:00
}
2016-09-06 00:04:48 +02:00
func resourcePostgreSQLDatabaseDelete ( d * schema . ResourceData , meta interface { } ) error {
2016-11-06 09:57:59 +01:00
c := meta . ( * Client )
conn , err := c . Connect ( )
2015-10-27 11:04:19 +01:00
if err != nil {
2016-09-05 23:46:40 +02:00
return errwrap . Wrapf ( "Error connecting to PostgreSQL: {{err}}" , err )
2015-10-27 11:04:19 +01:00
}
defer conn . Close ( )
2016-11-06 09:57:59 +01:00
dbName := d . Get ( dbNameAttr ) . ( string )
2016-11-06 17:46:40 +01:00
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
}
2015-10-27 11:04:19 +01:00
query := fmt . Sprintf ( "DROP DATABASE %s" , pq . QuoteIdentifier ( dbName ) )
_ , err = conn . Query ( query )
if err != nil {
2016-09-05 23:46:40 +02:00
return errwrap . Wrapf ( "Error dropping database: {{err}}" , err )
2015-10-27 11:04:19 +01:00
}
d . SetId ( "" )
return nil
}
2016-09-06 00:04:48 +02:00
func resourcePostgreSQLDatabaseRead ( d * schema . ResourceData , meta interface { } ) error {
2016-11-06 09:57:59 +01:00
c := meta . ( * Client )
conn , err := c . Connect ( )
2015-10-27 11:04:19 +01:00
if err != nil {
return err
}
defer conn . Close ( )
2016-11-06 09:57:59 +01:00
dbId := d . Id ( )
var dbName , ownerName string
err = conn . QueryRow ( "SELECT d.datname, pg_catalog.pg_get_userbyid(d.datdba) from pg_database d WHERE datname=$1" , dbId ) . Scan ( & dbName , & ownerName )
switch {
case err == sql . ErrNoRows :
log . Printf ( "[WARN] PostgreSQL database (%s) not found" , d . Id ( ) )
d . SetId ( "" )
return nil
case err != nil :
return errwrap . Wrapf ( "Error reading database: {{err}}" , err )
}
2015-10-27 11:04:19 +01:00
2016-11-06 09:57:59 +01:00
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 ,
)
2015-10-27 11:04:19 +01:00
switch {
case err == sql . ErrNoRows :
2016-11-06 09:57:59 +01:00
log . Printf ( "[WARN] PostgreSQL database (%s) not found" , d . Id ( ) )
2015-10-27 11:04:19 +01:00
d . SetId ( "" )
return nil
case err != nil :
2016-09-06 00:04:48 +02:00
return errwrap . Wrapf ( "Error reading database: {{err}}" , err )
2015-10-27 11:04:19 +01:00
default :
2016-11-06 09:57:59 +01:00
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 )
2015-10-27 11:04:19 +01:00
return nil
}
}
2016-09-06 00:04:48 +02:00
func resourcePostgreSQLDatabaseUpdate ( d * schema . ResourceData , meta interface { } ) error {
2016-11-06 09:57:59 +01:00
c := meta . ( * Client )
conn , err := c . Connect ( )
2015-10-27 11:04:19 +01:00
if err != nil {
return err
}
defer conn . Close ( )
2016-11-06 09:57:59 +01:00
if err := setDBName ( conn , d ) ; err != nil {
return err
}
2015-10-27 11:04:19 +01:00
2016-11-06 09:57:59 +01:00
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
2015-10-27 11:04:19 +01:00
}
2016-11-06 09:57:59 +01:00
if err := setDBIsTemplate ( conn , d ) ; err != nil {
return err
}
// Empty values: ALTER DATABASE name RESET configuration_parameter;
2016-09-06 00:04:48 +02:00
return resourcePostgreSQLDatabaseRead ( d , meta )
2015-10-27 11:04:19 +01:00
}
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 {
2016-11-06 09:57:59 +01:00
// is already member or role
2015-10-27 11:04:19 +01:00
if strings . Contains ( err . Error ( ) , "duplicate key value violates unique constraint" ) {
return nil
}
2016-09-05 23:46:40 +02:00
return errwrap . Wrapf ( "Error granting membership: {{err}}" , err )
2015-10-27 11:04:19 +01:00
}
}
return nil
}
2016-11-06 09:57:59 +01:00
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
}
2016-11-06 17:46:40 +01:00
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 {
2016-11-06 09:57:59 +01:00
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
}