github.com/ffrizzo/terraform@v0.8.2-0.20161219200057-992e12335f3d/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 Importer: &schema.ResourceImporter{ 36 State: schema.ImportStatePassthrough, 37 }, 38 39 Schema: map[string]*schema.Schema{ 40 dbNameAttr: { 41 Type: schema.TypeString, 42 Required: true, 43 Description: "The PostgreSQL database name to connect to", 44 }, 45 dbOwnerAttr: { 46 Type: schema.TypeString, 47 Optional: true, 48 Computed: true, 49 Description: "The role name of the user who will own the new database", 50 }, 51 dbTemplateAttr: { 52 Type: schema.TypeString, 53 Optional: true, 54 ForceNew: true, 55 Computed: true, 56 Description: "The name of the template from which to create the new database", 57 }, 58 dbEncodingAttr: { 59 Type: schema.TypeString, 60 Optional: true, 61 Computed: true, 62 ForceNew: true, 63 Description: "Character set encoding to use in the new database", 64 }, 65 dbCollationAttr: { 66 Type: schema.TypeString, 67 Optional: true, 68 Computed: true, 69 ForceNew: true, 70 Description: "Collation order (LC_COLLATE) to use in the new database", 71 }, 72 dbCTypeAttr: { 73 Type: schema.TypeString, 74 Optional: true, 75 Computed: true, 76 ForceNew: true, 77 Description: "Character classification (LC_CTYPE) to use in the new database", 78 }, 79 dbTablespaceAttr: { 80 Type: schema.TypeString, 81 Optional: true, 82 Computed: true, 83 Description: "The name of the tablespace that will be associated with the new database", 84 }, 85 dbConnLimitAttr: { 86 Type: schema.TypeInt, 87 Optional: true, 88 Computed: true, 89 Description: "How many concurrent connections can be made to this database", 90 ValidateFunc: validateConnLimit, 91 }, 92 dbAllowConnsAttr: { 93 Type: schema.TypeBool, 94 Optional: true, 95 Default: true, 96 Description: "If false then no one can connect to this database", 97 }, 98 dbIsTemplateAttr: { 99 Type: schema.TypeBool, 100 Optional: true, 101 Computed: true, 102 Description: "If true, then this database can be cloned by any user with CREATEDB privileges", 103 }, 104 }, 105 } 106 } 107 108 func resourcePostgreSQLDatabaseCreate(d *schema.ResourceData, meta interface{}) error { 109 c := meta.(*Client) 110 conn, err := c.Connect() 111 if err != nil { 112 return errwrap.Wrapf("Error connecting to PostgreSQL: {{err}}", err) 113 } 114 defer conn.Close() 115 116 dbName := d.Get(dbNameAttr).(string) 117 b := bytes.NewBufferString("CREATE DATABASE ") 118 fmt.Fprint(b, pq.QuoteIdentifier(dbName)) 119 120 // Handle each option individually and stream results into the query 121 // buffer. 122 123 switch v, ok := d.GetOk(dbOwnerAttr); { 124 case ok: 125 fmt.Fprint(b, " OWNER ", pq.QuoteIdentifier(v.(string))) 126 default: 127 // No owner specified in the config, default to using 128 // the connecting username. 129 fmt.Fprint(b, " OWNER ", pq.QuoteIdentifier(c.username)) 130 } 131 132 switch v, ok := d.GetOk(dbTemplateAttr); { 133 case ok: 134 fmt.Fprint(b, " TEMPLATE ", pq.QuoteIdentifier(v.(string))) 135 case v.(string) == "", strings.ToUpper(v.(string)) != "DEFAULT": 136 fmt.Fprint(b, " TEMPLATE template0") 137 } 138 139 switch v, ok := d.GetOk(dbEncodingAttr); { 140 case ok: 141 fmt.Fprint(b, " ENCODING ", pq.QuoteIdentifier(v.(string))) 142 case v.(string) == "", strings.ToUpper(v.(string)) != "DEFAULT": 143 fmt.Fprint(b, ` ENCODING "UTF8"`) 144 } 145 146 switch v, ok := d.GetOk(dbCollationAttr); { 147 case ok: 148 fmt.Fprint(b, " LC_COLLATE ", pq.QuoteIdentifier(v.(string))) 149 case v.(string) == "", strings.ToUpper(v.(string)) != "DEFAULT": 150 fmt.Fprint(b, ` LC_COLLATE "C"`) 151 } 152 153 switch v, ok := d.GetOk(dbCTypeAttr); { 154 case ok: 155 fmt.Fprint(b, " LC_CTYPE ", pq.QuoteIdentifier(v.(string))) 156 case v.(string) == "", strings.ToUpper(v.(string)) != "DEFAULT": 157 fmt.Fprint(b, ` LC_CTYPE "C"`) 158 } 159 160 if v, ok := d.GetOk(dbTablespaceAttr); ok { 161 fmt.Fprint(b, " TABLESPACE ", pq.QuoteIdentifier(v.(string))) 162 } 163 164 { 165 val := d.Get(dbAllowConnsAttr).(bool) 166 fmt.Fprint(b, " ALLOW_CONNECTIONS ", val) 167 } 168 169 { 170 val := d.Get(dbConnLimitAttr).(int) 171 fmt.Fprint(b, " CONNECTION LIMIT ", val) 172 } 173 174 { 175 val := d.Get(dbIsTemplateAttr).(bool) 176 fmt.Fprint(b, " IS_TEMPLATE ", val) 177 } 178 179 query := b.String() 180 _, err = conn.Query(query) 181 if err != nil { 182 return errwrap.Wrapf(fmt.Sprintf("Error creating database %s: {{err}}", dbName), err) 183 } 184 185 d.SetId(dbName) 186 187 return resourcePostgreSQLDatabaseRead(d, meta) 188 } 189 190 func resourcePostgreSQLDatabaseDelete(d *schema.ResourceData, meta interface{}) error { 191 c := meta.(*Client) 192 conn, err := c.Connect() 193 if err != nil { 194 return errwrap.Wrapf("Error connecting to PostgreSQL: {{err}}", err) 195 } 196 defer conn.Close() 197 198 dbName := d.Get(dbNameAttr).(string) 199 200 if isTemplate := d.Get(dbIsTemplateAttr).(bool); isTemplate { 201 // Template databases must have this attribute cleared before 202 // they can be dropped. 203 if err := doSetDBIsTemplate(conn, dbName, false); err != nil { 204 return errwrap.Wrapf("Error updating database IS_TEMPLATE during DROP DATABASE: {{err}}", err) 205 } 206 } 207 208 if err := setDBIsTemplate(conn, d); err != nil { 209 return err 210 } 211 212 query := fmt.Sprintf("DROP DATABASE %s", pq.QuoteIdentifier(dbName)) 213 _, err = conn.Query(query) 214 if err != nil { 215 return errwrap.Wrapf("Error dropping database: {{err}}", err) 216 } 217 218 d.SetId("") 219 220 return nil 221 } 222 223 func resourcePostgreSQLDatabaseRead(d *schema.ResourceData, meta interface{}) error { 224 c := meta.(*Client) 225 conn, err := c.Connect() 226 if err != nil { 227 return err 228 } 229 defer conn.Close() 230 231 dbId := d.Id() 232 var dbName, ownerName string 233 err = conn.QueryRow("SELECT d.datname, pg_catalog.pg_get_userbyid(d.datdba) from pg_database d WHERE datname=$1", dbId).Scan(&dbName, &ownerName) 234 switch { 235 case err == sql.ErrNoRows: 236 log.Printf("[WARN] PostgreSQL database (%s) not found", dbId) 237 d.SetId("") 238 return nil 239 case err != nil: 240 return errwrap.Wrapf("Error reading database: {{err}}", err) 241 } 242 243 var dbEncoding, dbCollation, dbCType, dbTablespaceName string 244 var dbConnLimit int 245 var dbAllowConns, dbIsTemplate bool 246 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). 247 Scan( 248 &dbEncoding, &dbCollation, &dbCType, &dbTablespaceName, 249 &dbConnLimit, &dbAllowConns, &dbIsTemplate, 250 ) 251 switch { 252 case err == sql.ErrNoRows: 253 log.Printf("[WARN] PostgreSQL database (%s) not found", dbId) 254 d.SetId("") 255 return nil 256 case err != nil: 257 return errwrap.Wrapf("Error reading database: {{err}}", err) 258 default: 259 d.Set(dbNameAttr, dbName) 260 d.Set(dbOwnerAttr, ownerName) 261 d.Set(dbEncodingAttr, dbEncoding) 262 d.Set(dbCollationAttr, dbCollation) 263 d.Set(dbCTypeAttr, dbCType) 264 d.Set(dbTablespaceAttr, dbTablespaceName) 265 d.Set(dbConnLimitAttr, dbConnLimit) 266 d.Set(dbAllowConnsAttr, dbAllowConns) 267 d.Set(dbIsTemplateAttr, dbIsTemplate) 268 dbTemplate := d.Get(dbTemplateAttr).(string) 269 if dbTemplate == "" { 270 dbTemplate = "template0" 271 } 272 d.Set(dbTemplateAttr, dbTemplate) 273 return nil 274 } 275 } 276 277 func resourcePostgreSQLDatabaseUpdate(d *schema.ResourceData, meta interface{}) error { 278 c := meta.(*Client) 279 conn, err := c.Connect() 280 if err != nil { 281 return err 282 } 283 defer conn.Close() 284 285 if err := setDBName(conn, d); err != nil { 286 return err 287 } 288 289 if err := setDBOwner(conn, d); err != nil { 290 return err 291 } 292 293 if err := setDBTablespace(conn, d); err != nil { 294 return err 295 } 296 297 if err := setDBConnLimit(conn, d); err != nil { 298 return err 299 } 300 301 if err := setDBAllowConns(conn, d); err != nil { 302 return err 303 } 304 305 if err := setDBIsTemplate(conn, d); err != nil { 306 return err 307 } 308 309 // Empty values: ALTER DATABASE name RESET configuration_parameter; 310 311 return resourcePostgreSQLDatabaseRead(d, meta) 312 } 313 314 func setDBName(conn *sql.DB, d *schema.ResourceData) error { 315 if !d.HasChange(dbNameAttr) { 316 return nil 317 } 318 319 oraw, nraw := d.GetChange(dbNameAttr) 320 o := oraw.(string) 321 n := nraw.(string) 322 if n == "" { 323 return errors.New("Error setting database name to an empty string") 324 } 325 326 query := fmt.Sprintf("ALTER DATABASE %s RENAME TO %s", pq.QuoteIdentifier(o), pq.QuoteIdentifier(n)) 327 if _, err := conn.Query(query); err != nil { 328 return errwrap.Wrapf("Error updating database name: {{err}}", err) 329 } 330 d.SetId(n) 331 332 return nil 333 } 334 335 func setDBOwner(conn *sql.DB, d *schema.ResourceData) error { 336 if !d.HasChange(dbOwnerAttr) { 337 return nil 338 } 339 340 owner := d.Get(dbOwnerAttr).(string) 341 if owner == "" { 342 return nil 343 } 344 345 dbName := d.Get(dbNameAttr).(string) 346 query := fmt.Sprintf("ALTER DATABASE %s OWNER TO %s", pq.QuoteIdentifier(dbName), pq.QuoteIdentifier(owner)) 347 if _, err := conn.Query(query); err != nil { 348 return errwrap.Wrapf("Error updating database OWNER: {{err}}", err) 349 } 350 351 return nil 352 } 353 354 func setDBTablespace(conn *sql.DB, d *schema.ResourceData) error { 355 if !d.HasChange(dbTablespaceAttr) { 356 return nil 357 } 358 359 tbspName := d.Get(dbTablespaceAttr).(string) 360 dbName := d.Get(dbNameAttr).(string) 361 var query string 362 if tbspName == "" || strings.ToUpper(tbspName) == "DEFAULT" { 363 query = fmt.Sprintf("ALTER DATABASE %s RESET TABLESPACE", pq.QuoteIdentifier(dbName)) 364 } else { 365 query = fmt.Sprintf("ALTER DATABASE %s SET TABLESPACE %s", pq.QuoteIdentifier(dbName), pq.QuoteIdentifier(tbspName)) 366 } 367 368 if _, err := conn.Query(query); err != nil { 369 return errwrap.Wrapf("Error updating database TABLESPACE: {{err}}", err) 370 } 371 372 return nil 373 } 374 375 func setDBConnLimit(conn *sql.DB, d *schema.ResourceData) error { 376 if !d.HasChange(dbConnLimitAttr) { 377 return nil 378 } 379 380 connLimit := d.Get(dbConnLimitAttr).(int) 381 dbName := d.Get(dbNameAttr).(string) 382 query := fmt.Sprintf("ALTER DATABASE %s CONNECTION LIMIT = %d", pq.QuoteIdentifier(dbName), connLimit) 383 if _, err := conn.Query(query); err != nil { 384 return errwrap.Wrapf("Error updating database CONNECTION LIMIT: {{err}}", err) 385 } 386 387 return nil 388 } 389 390 func setDBAllowConns(conn *sql.DB, d *schema.ResourceData) error { 391 if !d.HasChange(dbAllowConnsAttr) { 392 return nil 393 } 394 395 allowConns := d.Get(dbAllowConnsAttr).(bool) 396 dbName := d.Get(dbNameAttr).(string) 397 query := fmt.Sprintf("ALTER DATABASE %s ALLOW_CONNECTIONS %t", pq.QuoteIdentifier(dbName), allowConns) 398 if _, err := conn.Query(query); err != nil { 399 return errwrap.Wrapf("Error updating database ALLOW_CONNECTIONS: {{err}}", err) 400 } 401 402 return nil 403 } 404 405 func setDBIsTemplate(conn *sql.DB, d *schema.ResourceData) error { 406 if !d.HasChange(dbIsTemplateAttr) { 407 return nil 408 } 409 410 if err := doSetDBIsTemplate(conn, d.Get(dbNameAttr).(string), d.Get(dbIsTemplateAttr).(bool)); err != nil { 411 return errwrap.Wrapf("Error updating database IS_TEMPLATE: {{err}}", err) 412 } 413 414 return nil 415 } 416 417 func doSetDBIsTemplate(conn *sql.DB, dbName string, isTemplate bool) error { 418 query := fmt.Sprintf("ALTER DATABASE %s IS_TEMPLATE %t", pq.QuoteIdentifier(dbName), isTemplate) 419 if _, err := conn.Query(query); err != nil { 420 return errwrap.Wrapf("Error updating database IS_TEMPLATE: {{err}}", err) 421 } 422 423 return nil 424 }