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 }