github.com/skyscape-cloud-services/terraform@v0.9.2-0.20170609144644-7ece028a1747/builtin/providers/postgresql/resource_postgresql_database.go (about)

     1  package postgresql
     2  
     3  import (
     4  	"bytes"
     5  	"database/sql"
     6  	"errors"
     7  	"fmt"
     8  	"log"
     9  	"strings"
    10  
    11  	"github.com/hashicorp/errwrap"
    12  	"github.com/hashicorp/terraform/helper/schema"
    13  	"github.com/lib/pq"
    14  )
    15  
    16  const (
    17  	dbAllowConnsAttr = "allow_connections"
    18  	dbCTypeAttr      = "lc_ctype"
    19  	dbCollationAttr  = "lc_collate"
    20  	dbConnLimitAttr  = "connection_limit"
    21  	dbEncodingAttr   = "encoding"
    22  	dbIsTemplateAttr = "is_template"
    23  	dbNameAttr       = "name"
    24  	dbOwnerAttr      = "owner"
    25  	dbTablespaceAttr = "tablespace_name"
    26  	dbTemplateAttr   = "template"
    27  )
    28  
    29  func resourcePostgreSQLDatabase() *schema.Resource {
    30  	return &schema.Resource{
    31  		Create: resourcePostgreSQLDatabaseCreate,
    32  		Read:   resourcePostgreSQLDatabaseRead,
    33  		Update: resourcePostgreSQLDatabaseUpdate,
    34  		Delete: resourcePostgreSQLDatabaseDelete,
    35  		Exists: resourcePostgreSQLDatabaseExists,
    36  		Importer: &schema.ResourceImporter{
    37  			State: schema.ImportStatePassthrough,
    38  		},
    39  
    40  		Schema: map[string]*schema.Schema{
    41  			dbNameAttr: {
    42  				Type:        schema.TypeString,
    43  				Required:    true,
    44  				Description: "The PostgreSQL database name to connect to",
    45  			},
    46  			dbOwnerAttr: {
    47  				Type:        schema.TypeString,
    48  				Optional:    true,
    49  				Computed:    true,
    50  				Description: "The ROLE which owns the database",
    51  			},
    52  			dbTemplateAttr: {
    53  				Type:        schema.TypeString,
    54  				Optional:    true,
    55  				ForceNew:    true,
    56  				Computed:    true,
    57  				Description: "The name of the template from which to create the new database",
    58  			},
    59  			dbEncodingAttr: {
    60  				Type:        schema.TypeString,
    61  				Optional:    true,
    62  				Computed:    true,
    63  				ForceNew:    true,
    64  				Description: "Character set encoding to use in the new database",
    65  			},
    66  			dbCollationAttr: {
    67  				Type:        schema.TypeString,
    68  				Optional:    true,
    69  				Computed:    true,
    70  				ForceNew:    true,
    71  				Description: "Collation order (LC_COLLATE) to use in the new database",
    72  			},
    73  			dbCTypeAttr: {
    74  				Type:        schema.TypeString,
    75  				Optional:    true,
    76  				Computed:    true,
    77  				ForceNew:    true,
    78  				Description: "Character classification (LC_CTYPE) to use in the new database",
    79  			},
    80  			dbTablespaceAttr: {
    81  				Type:        schema.TypeString,
    82  				Optional:    true,
    83  				Computed:    true,
    84  				Description: "The name of the tablespace that will be associated with the new database",
    85  			},
    86  			dbConnLimitAttr: {
    87  				Type:         schema.TypeInt,
    88  				Optional:     true,
    89  				Default:      -1,
    90  				Description:  "How many concurrent connections can be made to this database",
    91  				ValidateFunc: validateConnLimit,
    92  			},
    93  			dbAllowConnsAttr: {
    94  				Type:        schema.TypeBool,
    95  				Optional:    true,
    96  				Default:     true,
    97  				Description: "If false then no one can connect to this database",
    98  			},
    99  			dbIsTemplateAttr: {
   100  				Type:        schema.TypeBool,
   101  				Optional:    true,
   102  				Computed:    true,
   103  				Description: "If true, then this database can be cloned by any user with CREATEDB privileges",
   104  			},
   105  		},
   106  	}
   107  }
   108  
   109  func resourcePostgreSQLDatabaseCreate(d *schema.ResourceData, meta interface{}) error {
   110  	c := meta.(*Client)
   111  
   112  	c.catalogLock.Lock()
   113  	defer c.catalogLock.Unlock()
   114  
   115  	conn, err := c.Connect()
   116  	if err != nil {
   117  		return errwrap.Wrapf("Error connecting to PostgreSQL: {{err}}", err)
   118  	}
   119  	defer conn.Close()
   120  
   121  	dbName := d.Get(dbNameAttr).(string)
   122  	b := bytes.NewBufferString("CREATE DATABASE ")
   123  	fmt.Fprint(b, pq.QuoteIdentifier(dbName))
   124  
   125  	//needed in order to set the owner of the db if the connection user is not a superuser
   126  	err = grantRoleMembership(conn, d.Get(dbOwnerAttr).(string), c.username)
   127  	if err != nil {
   128  		return errwrap.Wrapf(fmt.Sprintf("Error adding connection user (%q) to ROLE %q: {{err}}", c.username, d.Get(dbOwnerAttr).(string)), err)
   129  	}
   130  	defer func() {
   131  		//undo the grant if the connection user is not a superuser
   132  		err = revokeRoleMembership(conn, d.Get(dbOwnerAttr).(string), c.username)
   133  		if err != nil {
   134  			err = errwrap.Wrapf(fmt.Sprintf("Error removing connection user (%q) from ROLE %q: {{err}}", c.username, d.Get(dbOwnerAttr).(string)), err)
   135  		}
   136  	}()
   137  
   138  	// Handle each option individually and stream results into the query
   139  	// buffer.
   140  
   141  	switch v, ok := d.GetOk(dbOwnerAttr); {
   142  	case ok:
   143  		fmt.Fprint(b, " OWNER ", pq.QuoteIdentifier(v.(string)))
   144  	default:
   145  		// No owner specified in the config, default to using
   146  		// the connecting username.
   147  		fmt.Fprint(b, " OWNER ", pq.QuoteIdentifier(c.username))
   148  	}
   149  
   150  	switch v, ok := d.GetOk(dbTemplateAttr); {
   151  	case ok:
   152  		fmt.Fprint(b, " TEMPLATE ", pq.QuoteIdentifier(v.(string)))
   153  	case v.(string) == "", strings.ToUpper(v.(string)) != "DEFAULT":
   154  		fmt.Fprint(b, " TEMPLATE template0")
   155  	}
   156  
   157  	switch v, ok := d.GetOk(dbEncodingAttr); {
   158  	case ok:
   159  		fmt.Fprint(b, " ENCODING ", pq.QuoteIdentifier(v.(string)))
   160  	case v.(string) == "", strings.ToUpper(v.(string)) != "DEFAULT":
   161  		fmt.Fprint(b, ` ENCODING "UTF8"`)
   162  	}
   163  
   164  	switch v, ok := d.GetOk(dbCollationAttr); {
   165  	case ok:
   166  		fmt.Fprint(b, " LC_COLLATE ", pq.QuoteIdentifier(v.(string)))
   167  	case v.(string) == "", strings.ToUpper(v.(string)) != "DEFAULT":
   168  		fmt.Fprint(b, ` LC_COLLATE "C"`)
   169  	}
   170  
   171  	switch v, ok := d.GetOk(dbCTypeAttr); {
   172  	case ok:
   173  		fmt.Fprint(b, " LC_CTYPE ", pq.QuoteIdentifier(v.(string)))
   174  	case v.(string) == "", strings.ToUpper(v.(string)) != "DEFAULT":
   175  		fmt.Fprint(b, ` LC_CTYPE "C"`)
   176  	}
   177  
   178  	if v, ok := d.GetOk(dbTablespaceAttr); ok {
   179  		fmt.Fprint(b, " TABLESPACE ", pq.QuoteIdentifier(v.(string)))
   180  	}
   181  
   182  	{
   183  		val := d.Get(dbAllowConnsAttr).(bool)
   184  		fmt.Fprint(b, " ALLOW_CONNECTIONS ", val)
   185  	}
   186  
   187  	{
   188  		val := d.Get(dbConnLimitAttr).(int)
   189  		fmt.Fprint(b, " CONNECTION LIMIT ", val)
   190  	}
   191  
   192  	{
   193  		val := d.Get(dbIsTemplateAttr).(bool)
   194  		fmt.Fprint(b, " IS_TEMPLATE ", val)
   195  	}
   196  
   197  	query := b.String()
   198  	_, err = conn.Query(query)
   199  	if err != nil {
   200  		return errwrap.Wrapf(fmt.Sprintf("Error creating database %q: {{err}}", dbName), err)
   201  	}
   202  
   203  	d.SetId(dbName)
   204  
   205  	// Set err outside of the return so that the deferred revoke can override err
   206  	// if necessary.
   207  	err = resourcePostgreSQLDatabaseReadImpl(d, meta)
   208  	return err
   209  }
   210  
   211  func resourcePostgreSQLDatabaseDelete(d *schema.ResourceData, meta interface{}) error {
   212  	c := meta.(*Client)
   213  	c.catalogLock.Lock()
   214  	defer c.catalogLock.Unlock()
   215  
   216  	conn, err := c.Connect()
   217  	if err != nil {
   218  		return errwrap.Wrapf("Error connecting to PostgreSQL: {{err}}", err)
   219  	}
   220  	defer conn.Close()
   221  
   222  	dbName := d.Get(dbNameAttr).(string)
   223  
   224  	if isTemplate := d.Get(dbIsTemplateAttr).(bool); isTemplate {
   225  		// Template databases must have this attribute cleared before
   226  		// they can be dropped.
   227  		if err := doSetDBIsTemplate(conn, dbName, false); err != nil {
   228  			return errwrap.Wrapf("Error updating database IS_TEMPLATE during DROP DATABASE: {{err}}", err)
   229  		}
   230  	}
   231  
   232  	if err := setDBIsTemplate(conn, d); err != nil {
   233  		return err
   234  	}
   235  
   236  	query := fmt.Sprintf("DROP DATABASE %s", pq.QuoteIdentifier(dbName))
   237  	_, err = conn.Query(query)
   238  	if err != nil {
   239  		return errwrap.Wrapf("Error dropping database: {{err}}", err)
   240  	}
   241  
   242  	d.SetId("")
   243  
   244  	return nil
   245  }
   246  
   247  func resourcePostgreSQLDatabaseExists(d *schema.ResourceData, meta interface{}) (bool, error) {
   248  	c := meta.(*Client)
   249  	c.catalogLock.RLock()
   250  	defer c.catalogLock.RUnlock()
   251  
   252  	conn, err := c.Connect()
   253  	if err != nil {
   254  		return false, err
   255  	}
   256  	defer conn.Close()
   257  
   258  	var dbName string
   259  	err = conn.QueryRow("SELECT d.datname from pg_database d WHERE datname=$1", d.Id()).Scan(&dbName)
   260  	switch {
   261  	case err == sql.ErrNoRows:
   262  		return false, nil
   263  	case err != nil:
   264  		return false, err
   265  	}
   266  
   267  	return true, nil
   268  }
   269  
   270  func resourcePostgreSQLDatabaseRead(d *schema.ResourceData, meta interface{}) error {
   271  	c := meta.(*Client)
   272  	c.catalogLock.RLock()
   273  	defer c.catalogLock.RUnlock()
   274  
   275  	return resourcePostgreSQLDatabaseReadImpl(d, meta)
   276  }
   277  
   278  func resourcePostgreSQLDatabaseReadImpl(d *schema.ResourceData, meta interface{}) error {
   279  	c := meta.(*Client)
   280  	conn, err := c.Connect()
   281  	if err != nil {
   282  		return err
   283  	}
   284  	defer conn.Close()
   285  
   286  	dbId := d.Id()
   287  	var dbName, ownerName string
   288  	err = conn.QueryRow("SELECT d.datname, pg_catalog.pg_get_userbyid(d.datdba) from pg_database d WHERE datname=$1", dbId).Scan(&dbName, &ownerName)
   289  	switch {
   290  	case err == sql.ErrNoRows:
   291  		log.Printf("[WARN] PostgreSQL database (%q) not found", dbId)
   292  		d.SetId("")
   293  		return nil
   294  	case err != nil:
   295  		return errwrap.Wrapf("Error reading database: {{err}}", err)
   296  	}
   297  
   298  	var dbEncoding, dbCollation, dbCType, dbTablespaceName string
   299  	var dbConnLimit int
   300  	var dbAllowConns, dbIsTemplate bool
   301  	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).
   302  		Scan(
   303  			&dbEncoding, &dbCollation, &dbCType, &dbTablespaceName,
   304  			&dbConnLimit, &dbAllowConns, &dbIsTemplate,
   305  		)
   306  	switch {
   307  	case err == sql.ErrNoRows:
   308  		log.Printf("[WARN] PostgreSQL database (%q) not found", dbId)
   309  		d.SetId("")
   310  		return nil
   311  	case err != nil:
   312  		return errwrap.Wrapf("Error reading database: {{err}}", err)
   313  	default:
   314  		d.Set(dbNameAttr, dbName)
   315  		d.Set(dbOwnerAttr, ownerName)
   316  		d.Set(dbEncodingAttr, dbEncoding)
   317  		d.Set(dbCollationAttr, dbCollation)
   318  		d.Set(dbCTypeAttr, dbCType)
   319  		d.Set(dbTablespaceAttr, dbTablespaceName)
   320  		d.Set(dbConnLimitAttr, dbConnLimit)
   321  		d.Set(dbAllowConnsAttr, dbAllowConns)
   322  		d.Set(dbIsTemplateAttr, dbIsTemplate)
   323  		dbTemplate := d.Get(dbTemplateAttr).(string)
   324  		if dbTemplate == "" {
   325  			dbTemplate = "template0"
   326  		}
   327  		d.Set(dbTemplateAttr, dbTemplate)
   328  		return nil
   329  	}
   330  }
   331  
   332  func resourcePostgreSQLDatabaseUpdate(d *schema.ResourceData, meta interface{}) error {
   333  	c := meta.(*Client)
   334  	c.catalogLock.Lock()
   335  	defer c.catalogLock.Unlock()
   336  
   337  	conn, err := c.Connect()
   338  	if err != nil {
   339  		return err
   340  	}
   341  	defer conn.Close()
   342  
   343  	if err := setDBName(conn, d); err != nil {
   344  		return err
   345  	}
   346  
   347  	if err := setDBOwner(c, conn, d); err != nil {
   348  		return err
   349  	}
   350  
   351  	if err := setDBTablespace(conn, d); err != nil {
   352  		return err
   353  	}
   354  
   355  	if err := setDBConnLimit(conn, d); err != nil {
   356  		return err
   357  	}
   358  
   359  	if err := setDBAllowConns(conn, d); err != nil {
   360  		return err
   361  	}
   362  
   363  	if err := setDBIsTemplate(conn, d); err != nil {
   364  		return err
   365  	}
   366  
   367  	// Empty values: ALTER DATABASE name RESET configuration_parameter;
   368  
   369  	return resourcePostgreSQLDatabaseReadImpl(d, meta)
   370  }
   371  
   372  func setDBName(conn *sql.DB, d *schema.ResourceData) error {
   373  	if !d.HasChange(dbNameAttr) {
   374  		return nil
   375  	}
   376  
   377  	oraw, nraw := d.GetChange(dbNameAttr)
   378  	o := oraw.(string)
   379  	n := nraw.(string)
   380  	if n == "" {
   381  		return errors.New("Error setting database name to an empty string")
   382  	}
   383  
   384  	query := fmt.Sprintf("ALTER DATABASE %s RENAME TO %s", pq.QuoteIdentifier(o), pq.QuoteIdentifier(n))
   385  	if _, err := conn.Query(query); err != nil {
   386  		return errwrap.Wrapf("Error updating database name: {{err}}", err)
   387  	}
   388  	d.SetId(n)
   389  
   390  	return nil
   391  }
   392  
   393  func setDBOwner(c *Client, conn *sql.DB, d *schema.ResourceData) error {
   394  	if !d.HasChange(dbOwnerAttr) {
   395  		return nil
   396  	}
   397  
   398  	owner := d.Get(dbOwnerAttr).(string)
   399  	if owner == "" {
   400  		return nil
   401  	}
   402  
   403  	//needed in order to set the owner of the db if the connection user is not a superuser
   404  	err := grantRoleMembership(conn, d.Get(dbOwnerAttr).(string), c.username)
   405  	if err != nil {
   406  		return errwrap.Wrapf(fmt.Sprintf("Error adding connection user (%q) to ROLE %q: {{err}}", c.username, d.Get(dbOwnerAttr).(string)), err)
   407  	}
   408  	defer func() {
   409  		// undo the grant if the connection user is not a superuser
   410  		err = revokeRoleMembership(conn, d.Get(dbOwnerAttr).(string), c.username)
   411  		if err != nil {
   412  			err = errwrap.Wrapf(fmt.Sprintf("Error removing connection user (%q) from ROLE %q: {{err}}", c.username, d.Get(dbOwnerAttr).(string)), err)
   413  		}
   414  	}()
   415  
   416  	dbName := d.Get(dbNameAttr).(string)
   417  	query := fmt.Sprintf("ALTER DATABASE %s OWNER TO %s", pq.QuoteIdentifier(dbName), pq.QuoteIdentifier(owner))
   418  	if _, err := conn.Query(query); err != nil {
   419  		return errwrap.Wrapf("Error updating database OWNER: {{err}}", err)
   420  	}
   421  
   422  	return err
   423  }
   424  
   425  func setDBTablespace(conn *sql.DB, d *schema.ResourceData) error {
   426  	if !d.HasChange(dbTablespaceAttr) {
   427  		return nil
   428  	}
   429  
   430  	tbspName := d.Get(dbTablespaceAttr).(string)
   431  	dbName := d.Get(dbNameAttr).(string)
   432  	var query string
   433  	if tbspName == "" || strings.ToUpper(tbspName) == "DEFAULT" {
   434  		query = fmt.Sprintf("ALTER DATABASE %s RESET TABLESPACE", pq.QuoteIdentifier(dbName))
   435  	} else {
   436  		query = fmt.Sprintf("ALTER DATABASE %s SET TABLESPACE %s", pq.QuoteIdentifier(dbName), pq.QuoteIdentifier(tbspName))
   437  	}
   438  
   439  	if _, err := conn.Query(query); err != nil {
   440  		return errwrap.Wrapf("Error updating database TABLESPACE: {{err}}", err)
   441  	}
   442  
   443  	return nil
   444  }
   445  
   446  func setDBConnLimit(conn *sql.DB, d *schema.ResourceData) error {
   447  	if !d.HasChange(dbConnLimitAttr) {
   448  		return nil
   449  	}
   450  
   451  	connLimit := d.Get(dbConnLimitAttr).(int)
   452  	dbName := d.Get(dbNameAttr).(string)
   453  	query := fmt.Sprintf("ALTER DATABASE %s CONNECTION LIMIT = %d", pq.QuoteIdentifier(dbName), connLimit)
   454  	if _, err := conn.Query(query); err != nil {
   455  		return errwrap.Wrapf("Error updating database CONNECTION LIMIT: {{err}}", err)
   456  	}
   457  
   458  	return nil
   459  }
   460  
   461  func setDBAllowConns(conn *sql.DB, d *schema.ResourceData) error {
   462  	if !d.HasChange(dbAllowConnsAttr) {
   463  		return nil
   464  	}
   465  
   466  	allowConns := d.Get(dbAllowConnsAttr).(bool)
   467  	dbName := d.Get(dbNameAttr).(string)
   468  	query := fmt.Sprintf("ALTER DATABASE %s ALLOW_CONNECTIONS %t", pq.QuoteIdentifier(dbName), allowConns)
   469  	if _, err := conn.Query(query); err != nil {
   470  		return errwrap.Wrapf("Error updating database ALLOW_CONNECTIONS: {{err}}", err)
   471  	}
   472  
   473  	return nil
   474  }
   475  
   476  func setDBIsTemplate(conn *sql.DB, d *schema.ResourceData) error {
   477  	if !d.HasChange(dbIsTemplateAttr) {
   478  		return nil
   479  	}
   480  
   481  	if err := doSetDBIsTemplate(conn, d.Get(dbNameAttr).(string), d.Get(dbIsTemplateAttr).(bool)); err != nil {
   482  		return errwrap.Wrapf("Error updating database IS_TEMPLATE: {{err}}", err)
   483  	}
   484  
   485  	return nil
   486  }
   487  
   488  func doSetDBIsTemplate(conn *sql.DB, dbName string, isTemplate bool) error {
   489  	query := fmt.Sprintf("ALTER DATABASE %s IS_TEMPLATE %t", pq.QuoteIdentifier(dbName), isTemplate)
   490  	if _, err := conn.Query(query); err != nil {
   491  		return errwrap.Wrapf("Error updating database IS_TEMPLATE: {{err}}", err)
   492  	}
   493  
   494  	return nil
   495  }
   496  
   497  func grantRoleMembership(conn *sql.DB, dbOwner string, connUsername string) error {
   498  	if dbOwner != "" && dbOwner != connUsername {
   499  		query := fmt.Sprintf("GRANT %s TO %s", pq.QuoteIdentifier(dbOwner), pq.QuoteIdentifier(connUsername))
   500  		_, err := conn.Query(query)
   501  		if err != nil {
   502  			// is already member or role
   503  			if strings.Contains(err.Error(), "duplicate key value violates unique constraint") {
   504  				return nil
   505  			}
   506  			return errwrap.Wrapf("Error granting membership: {{err}}", err)
   507  		}
   508  	}
   509  	return nil
   510  }
   511  
   512  func revokeRoleMembership(conn *sql.DB, dbOwner string, connUsername string) error {
   513  	if dbOwner != "" && dbOwner != connUsername {
   514  		query := fmt.Sprintf("REVOKE %s FROM %s", pq.QuoteIdentifier(dbOwner), pq.QuoteIdentifier(connUsername))
   515  		_, err := conn.Query(query)
   516  		if err != nil {
   517  			return errwrap.Wrapf("Error revoking membership: {{err}}", err)
   518  		}
   519  	}
   520  	return nil
   521  }