From f31ebff10e1c2d6b6c0ab132b1d47833da8fee66 Mon Sep 17 00:00:00 2001 From: Sean Chittenden Date: Mon, 5 Sep 2016 16:19:19 -0700 Subject: [PATCH 01/46] Change the PostgreSQL PGSSLMODE option to sslmode to match PostgreSQL idioms. Also don't specify the default and rely on github.com/lib/pq (which uses "required" and is different than what libpq(3) uses, which is "preferred" and unsupported by github.com/lib/pq). --- builtin/providers/postgresql/provider.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/builtin/providers/postgresql/provider.go b/builtin/providers/postgresql/provider.go index e90b01038..cbdc4a4e5 100644 --- a/builtin/providers/postgresql/provider.go +++ b/builtin/providers/postgresql/provider.go @@ -34,10 +34,10 @@ func Provider() terraform.ResourceProvider { DefaultFunc: schema.MultiEnvDefaultFunc([]string{"PGPASSWORD", "POSTGRESQL_PASSWORD"}, nil), Description: "Password for PostgreSQL server connection", }, - "ssl_mode": { + "sslmode": { Type: schema.TypeString, Optional: true, - DefaultFunc: schema.EnvDefaultFunc("PGSSLMODE", "require"), + DefaultFunc: schema.EnvDefaultFunc("PGSSLMODE", nil), Description: "Connection mode for PostgreSQL server", }, "connect_timeout": { From b68ef2c40bb8e2f205e1f3bcb5559deef7096337 Mon Sep 17 00:00:00 2001 From: Sean Chittenden Date: Mon, 5 Sep 2016 16:23:23 -0700 Subject: [PATCH 02/46] Fall through to using the defaults from github.com/lib/pq --- builtin/providers/postgresql/provider.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/builtin/providers/postgresql/provider.go b/builtin/providers/postgresql/provider.go index cbdc4a4e5..d644aa2a2 100644 --- a/builtin/providers/postgresql/provider.go +++ b/builtin/providers/postgresql/provider.go @@ -12,19 +12,19 @@ func Provider() terraform.ResourceProvider { Schema: map[string]*schema.Schema{ "host": { Type: schema.TypeString, - Required: true, + Optional: true, DefaultFunc: schema.MultiEnvDefaultFunc([]string{"PGHOST", "POSTGRESQL_HOST"}, nil), Description: "The PostgreSQL server address", }, "port": { Type: schema.TypeInt, Optional: true, - Default: 5432, + DefaultFunc: schema.MultiEnvDefaultFunc([]string{"PGPORT"}, nil), Description: "The PostgreSQL server port", }, "username": { Type: schema.TypeString, - Required: true, + Optional: true, DefaultFunc: schema.MultiEnvDefaultFunc([]string{"PGUSER", "POSTGRESQL_USER"}, nil), Description: "Username for PostgreSQL server connection", }, From ec130d538c3319ffb12884c87fe80146295478bd Mon Sep 17 00:00:00 2001 From: Sean Chittenden Date: Mon, 5 Sep 2016 16:24:26 -0700 Subject: [PATCH 03/46] Add comments requesting that the testing-specific environment variables can be removed in Terraform 0.8.0 --- builtin/providers/postgresql/provider.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/builtin/providers/postgresql/provider.go b/builtin/providers/postgresql/provider.go index d644aa2a2..3e2efcbcf 100644 --- a/builtin/providers/postgresql/provider.go +++ b/builtin/providers/postgresql/provider.go @@ -11,8 +11,9 @@ func Provider() terraform.ResourceProvider { return &schema.Provider{ Schema: map[string]*schema.Schema{ "host": { - Type: schema.TypeString, + Type: schema.TypeString, Optional: true, + // FIXME: Remove POSTGRESQL_HOST in 0.8 DefaultFunc: schema.MultiEnvDefaultFunc([]string{"PGHOST", "POSTGRESQL_HOST"}, nil), Description: "The PostgreSQL server address", }, @@ -23,14 +24,16 @@ func Provider() terraform.ResourceProvider { Description: "The PostgreSQL server port", }, "username": { - Type: schema.TypeString, + Type: schema.TypeString, Optional: true, + // FIXME: Remove POSTGRESQL_USER in 0.8 DefaultFunc: schema.MultiEnvDefaultFunc([]string{"PGUSER", "POSTGRESQL_USER"}, nil), Description: "Username for PostgreSQL server connection", }, "password": { - Type: schema.TypeString, - Optional: true, + Type: schema.TypeString, + Optional: true, + // FIXME: Remove POSTGRESQL_PASSWORD in 0.8 DefaultFunc: schema.MultiEnvDefaultFunc([]string{"PGPASSWORD", "POSTGRESQL_PASSWORD"}, nil), Description: "Password for PostgreSQL server connection", }, From 655617ed6a0d5c88c0c31ae1766fcfa8330b8bdd Mon Sep 17 00:00:00 2001 From: Sean Chittenden Date: Mon, 5 Sep 2016 17:09:11 -0700 Subject: [PATCH 04/46] Remove the PGPASSWORD requirement for tests. If they're required and the value is missing, the test will fail. There's no need to enforce that in the test itself. --- builtin/providers/postgresql/provider_test.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/builtin/providers/postgresql/provider_test.go b/builtin/providers/postgresql/provider_test.go index 9596a0089..697c83329 100644 --- a/builtin/providers/postgresql/provider_test.go +++ b/builtin/providers/postgresql/provider_test.go @@ -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") - } } From 7e5ccc089b1348eac961488c6ecba357ada4f010 Mon Sep 17 00:00:00 2001 From: Sean Chittenden Date: Mon, 5 Sep 2016 17:10:15 -0700 Subject: [PATCH 05/46] Spell `ssl_mode` like `sslmode` --- builtin/providers/postgresql/provider.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builtin/providers/postgresql/provider.go b/builtin/providers/postgresql/provider.go index 3e2efcbcf..64029e21f 100644 --- a/builtin/providers/postgresql/provider.go +++ b/builtin/providers/postgresql/provider.go @@ -68,8 +68,8 @@ func providerConfigure(d *schema.ResourceData) (interface{}, error) { 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), + SslMode: d.Get("sslmode").(string), } client, err := config.NewClient() From 2aee081e4b6e888db9e7b3952b9af5f593e4e5f9 Mon Sep 17 00:00:00 2001 From: Sean Chittenden Date: Mon, 5 Sep 2016 17:14:26 -0700 Subject: [PATCH 06/46] Eh, specify default values lib/pq zero initializes itself --- builtin/providers/postgresql/provider.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/builtin/providers/postgresql/provider.go b/builtin/providers/postgresql/provider.go index 64029e21f..0b14b7466 100644 --- a/builtin/providers/postgresql/provider.go +++ b/builtin/providers/postgresql/provider.go @@ -20,14 +20,14 @@ func Provider() terraform.ResourceProvider { "port": { Type: schema.TypeInt, Optional: true, - DefaultFunc: schema.MultiEnvDefaultFunc([]string{"PGPORT"}, nil), + DefaultFunc: schema.EnvDefaultFunc("PGPORT", 5432), Description: "The PostgreSQL server port", }, "username": { Type: schema.TypeString, Optional: true, // FIXME: Remove POSTGRESQL_USER in 0.8 - DefaultFunc: schema.MultiEnvDefaultFunc([]string{"PGUSER", "POSTGRESQL_USER"}, nil), + DefaultFunc: schema.MultiEnvDefaultFunc([]string{"PGUSER", "POSTGRESQL_USER"}, "postgres"), Description: "Username for PostgreSQL server connection", }, "password": { From f3add9e7efbb3066a743d88a86cf8c8f61cc4c0a Mon Sep 17 00:00:00 2001 From: Sean Chittenden Date: Mon, 5 Sep 2016 18:40:35 -0700 Subject: [PATCH 07/46] Flesh out the CREATE DATABASE for PostgreSQL. --- .../resource_postgresql_database.go | 135 ++++++++++++++++-- .../resource_postgresql_database_test.go | 22 ++- 2 files changed, 139 insertions(+), 18 deletions(-) diff --git a/builtin/providers/postgresql/resource_postgresql_database.go b/builtin/providers/postgresql/resource_postgresql_database.go index 0cffcaa98..352493691 100644 --- a/builtin/providers/postgresql/resource_postgresql_database.go +++ b/builtin/providers/postgresql/resource_postgresql_database.go @@ -21,13 +21,61 @@ func resourcePostgreSQLDatabase() *schema.Resource { "name": { Type: schema.TypeString, Required: true, - ForceNew: true, }, "owner": { Type: schema.TypeString, Optional: true, Computed: true, }, + "template": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the template from which to create the new database.", + }, + "encoding": { + Type: schema.TypeString, + Optional: true, + Description: "Character set encoding to use in the new database.", + }, + "lc_collate": { + Type: schema.TypeString, + Optional: true, + Description: "Collation order (LC_COLLATE) to use in the new database.", + }, + "lc_ctype": { + Type: schema.TypeString, + Optional: true, + Description: "Character classification (LC_CTYPE) to use in the new database.", + }, + "tablespace_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the tablespace that will be associated with the new database.", + }, + "connection_limit": { + Type: schema.TypeInt, + Optional: true, + Description: "How many concurrent connections can be made to this database.", + ValidateFunc: func(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 + }, + }, + "allow_connections": { + Type: schema.TypeBool, + Optional: true, + Default: true, + Description: "If false then no one can connect to this database.", + }, + "is_template": { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: "If true, then this database can be cloned by any user with CREATEDB privileges.", + }, }, } } @@ -40,24 +88,85 @@ func resourcePostgreSQLDatabaseCreate(d *schema.ResourceData, meta interface{}) } defer conn.Close() - dbName := d.Get("name").(string) - dbOwner := d.Get("owner").(string) connUsername := client.username - var dbOwnerCfg string - if dbOwner != "" { - dbOwnerCfg = fmt.Sprintf("WITH OWNER=%s", pq.QuoteIdentifier(dbOwner)) - } else { - dbOwnerCfg = "" + const numOptions = 9 + createOpts := make([]string, 0, numOptions) + + stringOpts := []struct { + hclKey string + sqlKey string + }{ + {"owner", "OWNER"}, + {"template", "TEMPLATE"}, + {"encoding", "ENCODING"}, + {"lc_collate", "LC_COLLATE"}, + {"lc_ctype", "LC_CTYPE"}, + {"tablespace_name", "TABLESPACE"}, + } + for _, opt := range stringOpts { + v, ok := d.GetOk(opt.hclKey) + var val string + if !ok { + // Set the owner to the connection username + if opt.hclKey == "owner" && v.(string) == "" { + val = connUsername + } else { + continue + } + } + + val = v.(string) + + // Set the owner to the connection username + if opt.hclKey == "owner" && val == "" { + val = connUsername + } + + if val != "" { + createOpts = append(createOpts, fmt.Sprintf("%s=%s", opt.sqlKey, pq.QuoteIdentifier(val))) + } } - //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 + intOpts := []struct { + hclKey string + sqlKey string + }{ + {"connection_limit", "CONNECTION LIMIT"}, + } + 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)) } - query := fmt.Sprintf("CREATE DATABASE %s %s", pq.QuoteIdentifier(dbName), dbOwnerCfg) + boolOpts := []struct { + hclKey string + sqlKey string + }{ + {"allow_connections", "ALLOW_CONNECTIONS"}, + {"is_template", "IS_TEMPLATE"}, + } + for _, opt := range boolOpts { + v, ok := d.GetOk(opt.hclKey) + if !ok { + continue + } + + val := v.(bool) + createOpts = append(createOpts, fmt.Sprintf("%s=%t", opt.sqlKey, val)) + } + + dbName := d.Get("name").(string) + createStr := strings.Join(createOpts, " ") + if len(createOpts) > 0 { + createStr = " WITH " + createStr + } + query := fmt.Sprintf("CREATE DATABASE %s%s", pq.QuoteIdentifier(dbName), createStr) _, err = conn.Query(query) if err != nil { return errwrap.Wrapf(fmt.Sprintf("Error creating database %s: {{err}}", dbName), err) diff --git a/builtin/providers/postgresql/resource_postgresql_database_test.go b/builtin/providers/postgresql/resource_postgresql_database_test.go index 4fb20e918..a6a0dc4ef 100644 --- a/builtin/providers/postgresql/resource_postgresql_database_test.go +++ b/builtin/providers/postgresql/resource_postgresql_database_test.go @@ -6,19 +6,19 @@ import ( "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( @@ -32,14 +32,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 +118,7 @@ func checkDatabaseExists(client *Client, dbName string) (bool, error) { } } -var testAccPostgresqlDatabaseConfig = ` +var testAccPostgreSQLDatabaseConfig = ` resource "postgresql_role" "myrole" { name = "myrole" login = true @@ -135,6 +134,19 @@ resource "postgresql_database" "mydb2" { owner = "${postgresql_role.myrole.name}" } +resource "postgresql_database" "mydb3" { + name = "mydb3" + owner = "${postgresql_role.myrole.name}" + template = "template1" + encoding = "SQL_ASCII" + lc_collate = "C" + lc_ctype = "C" + tablespace_name = "pg_default" + connection_limit = -1 + allow_connections = false + is_template = false +} + resource "postgresql_database" "mydb_default_owner" { name = "mydb_default_owner" } From 68cadd3c2a604a83f626af92278818b283c2d34b Mon Sep 17 00:00:00 2001 From: Sean Chittenden Date: Mon, 5 Sep 2016 23:01:55 -0700 Subject: [PATCH 08/46] Update the FIXME/TODO style to match Google style guides --- builtin/providers/postgresql/provider.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/builtin/providers/postgresql/provider.go b/builtin/providers/postgresql/provider.go index 0b14b7466..03e1bbdcd 100644 --- a/builtin/providers/postgresql/provider.go +++ b/builtin/providers/postgresql/provider.go @@ -13,7 +13,7 @@ func Provider() terraform.ResourceProvider { "host": { Type: schema.TypeString, Optional: true, - // FIXME: Remove POSTGRESQL_HOST in 0.8 + // TODO(sean@): Remove POSTGRESQL_HOST in 0.8 DefaultFunc: schema.MultiEnvDefaultFunc([]string{"PGHOST", "POSTGRESQL_HOST"}, nil), Description: "The PostgreSQL server address", }, @@ -26,14 +26,14 @@ func Provider() terraform.ResourceProvider { "username": { Type: schema.TypeString, Optional: true, - // FIXME: Remove POSTGRESQL_USER in 0.8 + // TODO(sean@): Remove POSTGRESQL_USER in 0.8 DefaultFunc: schema.MultiEnvDefaultFunc([]string{"PGUSER", "POSTGRESQL_USER"}, "postgres"), Description: "Username for PostgreSQL server connection", }, "password": { Type: schema.TypeString, Optional: true, - // FIXME: Remove POSTGRESQL_PASSWORD in 0.8 + // TODO(sean@): Remove POSTGRESQL_PASSWORD in 0.8 DefaultFunc: schema.MultiEnvDefaultFunc([]string{"PGPASSWORD", "POSTGRESQL_PASSWORD"}, nil), Description: "Password for PostgreSQL server connection", }, From deb91f61ce89588fc2951a80aa1ce823127affa0 Mon Sep 17 00:00:00 2001 From: Sean Chittenden Date: Mon, 5 Sep 2016 23:11:10 -0700 Subject: [PATCH 09/46] Update the descriptions to mostly match the official PostgreSQL docs. --- builtin/providers/postgresql/provider.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/builtin/providers/postgresql/provider.go b/builtin/providers/postgresql/provider.go index 03e1bbdcd..c0be6f08d 100644 --- a/builtin/providers/postgresql/provider.go +++ b/builtin/providers/postgresql/provider.go @@ -15,33 +15,33 @@ func Provider() terraform.ResourceProvider { Optional: true, // TODO(sean@): Remove POSTGRESQL_HOST in 0.8 DefaultFunc: schema.MultiEnvDefaultFunc([]string{"PGHOST", "POSTGRESQL_HOST"}, nil), - Description: "The PostgreSQL server address", + Description: "Name of PostgreSQL server address to connect to", }, "port": { Type: schema.TypeInt, Optional: true, DefaultFunc: schema.EnvDefaultFunc("PGPORT", 5432), - Description: "The PostgreSQL server port", + Description: "The PostgreSQL port number to connect to at the server host, or socket file name extension for Unix-domain connections", }, "username": { Type: schema.TypeString, Optional: true, // TODO(sean@): Remove POSTGRESQL_USER in 0.8 DefaultFunc: schema.MultiEnvDefaultFunc([]string{"PGUSER", "POSTGRESQL_USER"}, "postgres"), - Description: "Username for PostgreSQL server connection", + Description: "PostgreSQL user name to connect as", }, "password": { Type: schema.TypeString, Optional: true, // TODO(sean@): Remove POSTGRESQL_PASSWORD in 0.8 DefaultFunc: schema.MultiEnvDefaultFunc([]string{"PGPASSWORD", "POSTGRESQL_PASSWORD"}, nil), - Description: "Password for PostgreSQL server connection", + Description: "Password to be used if the PostgreSQL server demands password authentication", }, "sslmode": { Type: schema.TypeString, Optional: true, DefaultFunc: schema.EnvDefaultFunc("PGSSLMODE", nil), - Description: "Connection mode for PostgreSQL server", + Description: "This option determines whether or with what priority a secure SSL TCP/IP connection will be negotiated with the PostgreSQL server", }, "connect_timeout": { Type: schema.TypeInt, From 02dea2edd9eebe71920a9e8d80675ba3f037e609 Mon Sep 17 00:00:00 2001 From: Sean Chittenden Date: Mon, 5 Sep 2016 23:37:39 -0700 Subject: [PATCH 10/46] Add missing descriptions to owner and name --- .../postgresql/resource_postgresql_database.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/builtin/providers/postgresql/resource_postgresql_database.go b/builtin/providers/postgresql/resource_postgresql_database.go index 352493691..4a9ea97f4 100644 --- a/builtin/providers/postgresql/resource_postgresql_database.go +++ b/builtin/providers/postgresql/resource_postgresql_database.go @@ -19,13 +19,14 @@ func resourcePostgreSQLDatabase() *schema.Resource { Schema: map[string]*schema.Schema{ "name": { - Type: schema.TypeString, - Required: true, + Type: schema.TypeString, + Required: true, + Description: "The PostgreSQL database name to connect to", }, "owner": { - Type: schema.TypeString, - Optional: true, - Computed: true, + Type: schema.TypeString, + Optional: true, + Description: "The role name of the user who will own the new database", }, "template": { Type: schema.TypeString, From 242405bdf10b4d0754917ebb9ad8cb476ef3a79e Mon Sep 17 00:00:00 2001 From: Sean Chittenden Date: Mon, 5 Sep 2016 23:39:22 -0700 Subject: [PATCH 11/46] Factor out the validate function for connection limits --- .../resource_postgresql_database.go | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/builtin/providers/postgresql/resource_postgresql_database.go b/builtin/providers/postgresql/resource_postgresql_database.go index 4a9ea97f4..58b72ead9 100644 --- a/builtin/providers/postgresql/resource_postgresql_database.go +++ b/builtin/providers/postgresql/resource_postgresql_database.go @@ -54,16 +54,11 @@ func resourcePostgreSQLDatabase() *schema.Resource { Description: "The name of the tablespace that will be associated with the new database.", }, "connection_limit": { - Type: schema.TypeInt, - Optional: true, - Description: "How many concurrent connections can be made to this database.", - ValidateFunc: func(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 - }, + Type: schema.TypeInt, + Optional: true, + Computed: true, + Description: "How many concurrent connections can be made to this database", + ValidateFunc: validateConnLimit, }, "allow_connections": { Type: schema.TypeBool, @@ -81,6 +76,14 @@ func resourcePostgreSQLDatabase() *schema.Resource { } } +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 +} + func resourcePostgreSQLDatabaseCreate(d *schema.ResourceData, meta interface{}) error { client := meta.(*Client) conn, err := client.Connect() From 547dcf27e14d949847987356cc3202ee6ee60dab Mon Sep 17 00:00:00 2001 From: Sean Chittenden Date: Mon, 5 Sep 2016 23:40:13 -0700 Subject: [PATCH 12/46] Decorate the computed attribute where appropriate --- .../resource_postgresql_database.go | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/builtin/providers/postgresql/resource_postgresql_database.go b/builtin/providers/postgresql/resource_postgresql_database.go index 58b72ead9..5cb77dcc4 100644 --- a/builtin/providers/postgresql/resource_postgresql_database.go +++ b/builtin/providers/postgresql/resource_postgresql_database.go @@ -26,32 +26,37 @@ func resourcePostgreSQLDatabase() *schema.Resource { "owner": { Type: schema.TypeString, Optional: true, + Computed: true, Description: "The role name of the user who will own the new database", }, "template": { Type: schema.TypeString, Optional: true, - Description: "The name of the template from which to create the new database.", + Description: "The name of the template from which to create the new database", }, "encoding": { Type: schema.TypeString, Optional: true, - Description: "Character set encoding to use in the new database.", + Computed: true, + Description: "Character set encoding to use in the new database", }, "lc_collate": { Type: schema.TypeString, Optional: true, - Description: "Collation order (LC_COLLATE) to use in the new database.", + Computed: true, + Description: "Collation order (LC_COLLATE) to use in the new database", }, "lc_ctype": { Type: schema.TypeString, Optional: true, - Description: "Character classification (LC_CTYPE) to use in the new database.", + Computed: true, + Description: "Character classification (LC_CTYPE) to use in the new database", }, "tablespace_name": { Type: schema.TypeString, Optional: true, - Description: "The name of the tablespace that will be associated with the new database.", + Computed: true, + Description: "The name of the tablespace that will be associated with the new database", }, "connection_limit": { Type: schema.TypeInt, @@ -63,14 +68,14 @@ func resourcePostgreSQLDatabase() *schema.Resource { "allow_connections": { Type: schema.TypeBool, Optional: true, - Default: true, - Description: "If false then no one can connect to this database.", + Computed: true, + Description: "If false then no one can connect to this database", }, "is_template": { Type: schema.TypeBool, Optional: true, - Default: false, - Description: "If true, then this database can be cloned by any user with CREATEDB privileges.", + Computed: true, + Description: "If true, then this database can be cloned by any user with CREATEDB privileges", }, }, } From 5b66bf07459cb56f7d48d85ed04a7551a68769c3 Mon Sep 17 00:00:00 2001 From: Sean Chittenden Date: Mon, 5 Sep 2016 23:40:28 -0700 Subject: [PATCH 13/46] ForceNew is required when changing the locale, ctype, and encoding. This will cause someone some grief. TODO: Figure out how to prevent someone from blowing off their foot if they twiddle this after the fact. --- builtin/providers/postgresql/resource_postgresql_database.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/builtin/providers/postgresql/resource_postgresql_database.go b/builtin/providers/postgresql/resource_postgresql_database.go index 5cb77dcc4..560285766 100644 --- a/builtin/providers/postgresql/resource_postgresql_database.go +++ b/builtin/providers/postgresql/resource_postgresql_database.go @@ -38,18 +38,21 @@ func resourcePostgreSQLDatabase() *schema.Resource { Type: schema.TypeString, Optional: true, Computed: true, + ForceNew: true, Description: "Character set encoding to use in the new database", }, "lc_collate": { Type: schema.TypeString, Optional: true, Computed: true, + ForceNew: true, Description: "Collation order (LC_COLLATE) to use in the new database", }, "lc_ctype": { Type: schema.TypeString, Optional: true, Computed: true, + ForceNew: true, Description: "Character classification (LC_CTYPE) to use in the new database", }, "tablespace_name": { From 3779dfffa9a13e3061414c26871e8714f5fa2c5e Mon Sep 17 00:00:00 2001 From: Sean Chittenden Date: Mon, 5 Sep 2016 23:41:24 -0700 Subject: [PATCH 14/46] Use a string instead of the `%t` modifier for printing a bool in SQL --- .../providers/postgresql/resource_postgresql_database.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/builtin/providers/postgresql/resource_postgresql_database.go b/builtin/providers/postgresql/resource_postgresql_database.go index 560285766..48d05cfd4 100644 --- a/builtin/providers/postgresql/resource_postgresql_database.go +++ b/builtin/providers/postgresql/resource_postgresql_database.go @@ -169,8 +169,11 @@ func resourcePostgreSQLDatabaseCreate(d *schema.ResourceData, meta interface{}) continue } - val := v.(bool) - createOpts = append(createOpts, fmt.Sprintf("%s=%t", opt.sqlKey, val)) + valStr := "FALSE" + if val := v.(bool); val { + valStr = "TRUE" + } + createOpts = append(createOpts, fmt.Sprintf("%s=%s", opt.sqlKey, valStr)) } dbName := d.Get("name").(string) From 55061d190a369c2314a7944b97243f4f48bf6dbf Mon Sep 17 00:00:00 2001 From: Sean Chittenden Date: Sat, 5 Nov 2016 08:50:40 -0700 Subject: [PATCH 15/46] Prefer PGHOSTADDR over PGHOST, if set. --- builtin/providers/postgresql/provider.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builtin/providers/postgresql/provider.go b/builtin/providers/postgresql/provider.go index c0be6f08d..cb4a5a578 100644 --- a/builtin/providers/postgresql/provider.go +++ b/builtin/providers/postgresql/provider.go @@ -14,7 +14,7 @@ func Provider() terraform.ResourceProvider { Type: schema.TypeString, Optional: true, // TODO(sean@): Remove POSTGRESQL_HOST in 0.8 - DefaultFunc: schema.MultiEnvDefaultFunc([]string{"PGHOST", "POSTGRESQL_HOST"}, nil), + DefaultFunc: schema.MultiEnvDefaultFunc([]string{"PGHOSTADDR", "PGHOST", "POSTGRESQL_HOST"}, nil), Description: "Name of PostgreSQL server address to connect to", }, "port": { From 44d907a3de207cc79040b77deabc150c349a2688 Mon Sep 17 00:00:00 2001 From: Sean Chittenden Date: Sat, 5 Nov 2016 08:57:35 -0700 Subject: [PATCH 16/46] Revert "Prefer PGHOSTADDR over PGHOST, if set." lib/pq doesn't support PGHOSTADDR. https://github.com/lib/pq/blob/d8eeeb8bae8896dd8e1b7e514ab0d396c4f12a1b/conn.go#L1851 This reverts commit 26a2a974f2b2f2cefd41b426b8469fa3ea72b33e. --- builtin/providers/postgresql/provider.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builtin/providers/postgresql/provider.go b/builtin/providers/postgresql/provider.go index cb4a5a578..c0be6f08d 100644 --- a/builtin/providers/postgresql/provider.go +++ b/builtin/providers/postgresql/provider.go @@ -14,7 +14,7 @@ func Provider() terraform.ResourceProvider { Type: schema.TypeString, Optional: true, // TODO(sean@): Remove POSTGRESQL_HOST in 0.8 - DefaultFunc: schema.MultiEnvDefaultFunc([]string{"PGHOSTADDR", "PGHOST", "POSTGRESQL_HOST"}, nil), + DefaultFunc: schema.MultiEnvDefaultFunc([]string{"PGHOST", "POSTGRESQL_HOST"}, nil), Description: "Name of PostgreSQL server address to connect to", }, "port": { From 602a908d7b31c1ed4c5c3c32899b3cc94613077e Mon Sep 17 00:00:00 2001 From: Sean Chittenden Date: Sun, 6 Nov 2016 01:23:33 -0700 Subject: [PATCH 17/46] Add a `fallback_application_name` to the PostgreSQL DSN. --- builtin/providers/postgresql/config.go | 20 ++++++++++------ builtin/providers/postgresql/provider.go | 29 +++++++++++++++++++----- 2 files changed, 36 insertions(+), 13 deletions(-) diff --git a/builtin/providers/postgresql/config.go b/builtin/providers/postgresql/config.go index c096eb6e2..1021e7f28 100644 --- a/builtin/providers/postgresql/config.go +++ b/builtin/providers/postgresql/config.go @@ -3,18 +3,20 @@ 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 + Username string + Password string + SslMode string + Timeout int + ApplicationName string } // Client struct holding connection string @@ -25,8 +27,12 @@ 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) + const dsnFmt = "host=%s port=%d user=%s password=%s sslmode=%s fallback_application_name=%s connect_timeout=%d" + logDSN := fmt.Sprintf(dsnFmt, c.Host, c.Port, c.Username, "", c.SSLMode, c.ApplicationName) + log.Printf("[INFO] PostgreSQL DSN: `%s`", logDSN) + + connStr := fmt.Sprintf(dsnFmt, c.Host, c.Port, c.Username, c.Password, c.SSLMode, c.ApplicationName, c.Timeout) client := Client{ connStr: connStr, username: c.Username, diff --git a/builtin/providers/postgresql/provider.go b/builtin/providers/postgresql/provider.go index c0be6f08d..7046793c1 100644 --- a/builtin/providers/postgresql/provider.go +++ b/builtin/providers/postgresql/provider.go @@ -1,6 +1,9 @@ package postgresql import ( + "bytes" + "fmt" + "github.com/hashicorp/errwrap" "github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/terraform" @@ -64,12 +67,13 @@ func Provider() terraform.ResourceProvider { func providerConfigure(d *schema.ResourceData) (interface{}, error) { config := Config{ - Host: d.Get("host").(string), - Port: d.Get("port").(int), - Username: d.Get("username").(string), - Password: d.Get("password").(string), - Timeout: d.Get("connect_timeout").(int), - SslMode: d.Get("sslmode").(string), + Host: d.Get("host").(string), + Port: d.Get("port").(int), + Username: d.Get("username").(string), + Password: d.Get("password").(string), + Timeout: d.Get("connect_timeout").(int), + SslMode: d.Get("sslmode").(string), + ApplicationName: tfAppName(), } client, err := config.NewClient() @@ -79,3 +83,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() +} From a200899d935f82faf80ea557266e5a5d7d714574 Mon Sep 17 00:00:00 2001 From: Sean Chittenden Date: Sun, 6 Nov 2016 01:27:51 -0700 Subject: [PATCH 18/46] Allow the PostgreSQL provider to talk to a non-default database. --- builtin/providers/postgresql/config.go | 9 ++++++--- builtin/providers/postgresql/provider.go | 7 +++++++ .../source/docs/providers/postgresql/index.html.markdown | 4 +++- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/builtin/providers/postgresql/config.go b/builtin/providers/postgresql/config.go index 1021e7f28..81ad07583 100644 --- a/builtin/providers/postgresql/config.go +++ b/builtin/providers/postgresql/config.go @@ -12,6 +12,7 @@ import ( type Config struct { Host string Port int + Database string Username string Password string SslMode string @@ -27,12 +28,14 @@ type Client struct { // NewClient returns new client config func (c *Config) NewClient() (*Client, error) { - const dsnFmt = "host=%s port=%d user=%s password=%s sslmode=%s fallback_application_name=%s connect_timeout=%d" + // 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.Username, "", c.SSLMode, c.ApplicationName) + logDSN := fmt.Sprintf(dsnFmt, c.Host, c.Port, c.Database, c.Username, "", c.SSLMode, c.ApplicationName) log.Printf("[INFO] PostgreSQL DSN: `%s`", logDSN) - connStr := fmt.Sprintf(dsnFmt, c.Host, c.Port, c.Username, c.Password, c.SSLMode, c.ApplicationName, c.Timeout) + connStr := fmt.Sprintf(dsnFmt, c.Host, c.Port, c.Database, c.Username, c.Password, c.SSLMode, c.ApplicationName, c.Timeout) client := Client{ connStr: connStr, username: c.Username, diff --git a/builtin/providers/postgresql/provider.go b/builtin/providers/postgresql/provider.go index 7046793c1..62a12bcea 100644 --- a/builtin/providers/postgresql/provider.go +++ b/builtin/providers/postgresql/provider.go @@ -26,6 +26,12 @@ func Provider() terraform.ResourceProvider { 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, Optional: true, @@ -69,6 +75,7 @@ func providerConfigure(d *schema.ResourceData) (interface{}, error) { config := Config{ 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), Timeout: d.Get("connect_timeout").(int), diff --git a/website/source/docs/providers/postgresql/index.html.markdown b/website/source/docs/providers/postgresql/index.html.markdown index f4643f7cc..f5757efc4 100644 --- a/website/source/docs/providers/postgresql/index.html.markdown +++ b/website/source/docs/providers/postgresql/index.html.markdown @@ -18,6 +18,7 @@ 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" @@ -61,9 +62,10 @@ 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. +* `sslmode` - (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). From 3750bf7af220d91acbce3dde5ce4fba714c2db3b Mon Sep 17 00:00:00 2001 From: Sean Chittenden Date: Sun, 6 Nov 2016 01:44:57 -0700 Subject: [PATCH 19/46] Depreciate the PostgreSQL provider's `ssl_mode` option in favor of `sslmode`. Both libpq(3) and github.com/lib/pq both use `sslmode`. Prefer this vs the non-standard `ssl_mode`. `ssl_mode` is supported for compatibility but should be removed in the future. Changelog: yes --- builtin/providers/postgresql/config.go | 2 +- builtin/providers/postgresql/provider.go | 12 +++++++++++- .../docs/providers/postgresql/index.html.markdown | 10 +++++++++- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/builtin/providers/postgresql/config.go b/builtin/providers/postgresql/config.go index 81ad07583..2d14baba8 100644 --- a/builtin/providers/postgresql/config.go +++ b/builtin/providers/postgresql/config.go @@ -15,7 +15,7 @@ type Config struct { Database string Username string Password string - SslMode string + SSLMode string Timeout int ApplicationName string } diff --git a/builtin/providers/postgresql/provider.go b/builtin/providers/postgresql/provider.go index 62a12bcea..9c6dc949c 100644 --- a/builtin/providers/postgresql/provider.go +++ b/builtin/providers/postgresql/provider.go @@ -59,6 +59,11 @@ func Provider() terraform.ResourceProvider { DefaultFunc: schema.EnvDefaultFunc("PGCONNECT_TIMEOUT", nil), Description: "Maximum wait for connection, in seconds. Zero or not specified means wait indefinitely.", }, + "ssl_mode": { + Type: schema.TypeString, + Optional: true, + Deprecated: "Rename PostgreSQL provider `ssl_mode` attribute to `sslmode`", + }, }, ResourcesMap: map[string]*schema.Resource{ @@ -72,14 +77,19 @@ func Provider() terraform.ResourceProvider { } func providerConfigure(d *schema.ResourceData) (interface{}, error) { + var sslMode string + var ok bool + if sslMode, ok = d.GetOk("sslmode").(string); !ok { + sslMode = d.Get("ssl_mode").(string) + } config := Config{ 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, Timeout: d.Get("connect_timeout").(int), - SslMode: d.Get("sslmode").(string), ApplicationName: tfAppName(), } diff --git a/website/source/docs/providers/postgresql/index.html.markdown b/website/source/docs/providers/postgresql/index.html.markdown index f5757efc4..3f30d6574 100644 --- a/website/source/docs/providers/postgresql/index.html.markdown +++ b/website/source/docs/providers/postgresql/index.html.markdown @@ -21,7 +21,7 @@ provider "postgresql" { database = "postgres" username = "postgres_user" password = "postgres_password" - ssl_mode = "require" + sslmode = "require" connect_timeout = 15 } @@ -66,6 +66,14 @@ The following arguments are supported: * `username` - (Required) Username for the server connection. * `password` - (Optional) Password for the server connection. * `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. 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). From 300ef2bc976eab21dbd33720311d12590ebd5ed3 Mon Sep 17 00:00:00 2001 From: Sean Chittenden Date: Sun, 6 Nov 2016 01:49:37 -0700 Subject: [PATCH 20/46] Add `connect_timeout` support to the PostgreSQL provider. --- builtin/providers/postgresql/config.go | 21 +++++++------ builtin/providers/postgresql/provider.go | 31 ++++++++++++++----- .../providers/postgresql/index.html.markdown | 5 ++- 3 files changed, 36 insertions(+), 21 deletions(-) diff --git a/builtin/providers/postgresql/config.go b/builtin/providers/postgresql/config.go index 2d14baba8..f7e82ad82 100644 --- a/builtin/providers/postgresql/config.go +++ b/builtin/providers/postgresql/config.go @@ -10,14 +10,15 @@ import ( // Config - provider config type Config struct { - Host string - Port int - Database string - Username string - Password string - SSLMode string - Timeout int - ApplicationName string + Host string + Port int + Database string + Username string + Password string + SSLMode string + ApplicationName string + Timeout int + ConnectTimeoutSec int } // Client struct holding connection string @@ -32,10 +33,10 @@ func (c *Config) NewClient() (*Client, error) { // 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, "", c.SSLMode, c.ApplicationName) + logDSN := fmt.Sprintf(dsnFmt, c.Host, c.Port, c.Database, c.Username, "", 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.Timeout) + 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, diff --git a/builtin/providers/postgresql/provider.go b/builtin/providers/postgresql/provider.go index 9c6dc949c..65aa0ba7c 100644 --- a/builtin/providers/postgresql/provider.go +++ b/builtin/providers/postgresql/provider.go @@ -64,6 +64,13 @@ func Provider() terraform.ResourceProvider { Optional: true, Deprecated: "Rename PostgreSQL provider `ssl_mode` attribute to `sslmode`", }, + "connect_timeout": { + 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{ @@ -76,6 +83,14 @@ func Provider() terraform.ResourceProvider { } } +func validateConnTimeout(v interface{}, key string) (warnings []string, errors []error) { + value := v.(int) + if value < 0 { + errors = append(errors, fmt.Errorf("%d can not be less than 0", key)) + } + return +} + func providerConfigure(d *schema.ResourceData) (interface{}, error) { var sslMode string var ok bool @@ -83,14 +98,14 @@ func providerConfigure(d *schema.ResourceData) (interface{}, error) { sslMode = d.Get("ssl_mode").(string) } config := Config{ - 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, - Timeout: d.Get("connect_timeout").(int), - ApplicationName: tfAppName(), + 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() diff --git a/website/source/docs/providers/postgresql/index.html.markdown b/website/source/docs/providers/postgresql/index.html.markdown index 3f30d6574..3cd9aee8f 100644 --- a/website/source/docs/providers/postgresql/index.html.markdown +++ b/website/source/docs/providers/postgresql/index.html.markdown @@ -74,6 +74,5 @@ The following arguments are supported: * 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. 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). +* `connect_timeout` - (Optional) Maximum wait for connection, in seconds. The + default is `180s`. Zero or not specified means wait indefinitely. From 59f4ad6fd158a642d6662da9e577059753fec11f Mon Sep 17 00:00:00 2001 From: Sean Chittenden Date: Sun, 6 Nov 2016 01:50:28 -0700 Subject: [PATCH 21/46] Alpha sort the resources in the PostgreSQL map. --- builtin/providers/postgresql/provider.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builtin/providers/postgresql/provider.go b/builtin/providers/postgresql/provider.go index 65aa0ba7c..9ca012d85 100644 --- a/builtin/providers/postgresql/provider.go +++ b/builtin/providers/postgresql/provider.go @@ -75,8 +75,8 @@ func Provider() terraform.ResourceProvider { ResourcesMap: map[string]*schema.Resource{ "postgresql_database": resourcePostgreSQLDatabase(), - "postgresql_role": resourcePostgreSQLRole(), "postgresql_extension": resourcePostgreSQLExtension(), + "postgresql_role": resourcePostgreSQLRole(), }, ConfigureFunc: providerConfigure, From 5280c37bea94923e8666b0f6777f5504db65aef4 Mon Sep 17 00:00:00 2001 From: Sean Chittenden Date: Sun, 6 Nov 2016 01:57:59 -0700 Subject: [PATCH 22/46] `postgresql_database` resource provider should now be feature complete. * Add support to import databases. See docs. * Add support for renaming databases * Add support for all known PostgreSQL database attributes, including: * "allow_connections" * "lc_ctype" * "lc_collate" * "connection_limit" * "encoding" * "is_template" * "owner" * "tablespace_name" * "template" --- .../resource_postgresql_database.go | 356 ++++++++++++++---- .../resource_postgresql_database_test.go | 28 +- .../source/docs/import/importability.html.md | 3 + .../r/postgresql_database.html.markdown | 87 ++++- 4 files changed, 389 insertions(+), 85 deletions(-) diff --git a/builtin/providers/postgresql/resource_postgresql_database.go b/builtin/providers/postgresql/resource_postgresql_database.go index 48d05cfd4..04129afce 100644 --- a/builtin/providers/postgresql/resource_postgresql_database.go +++ b/builtin/providers/postgresql/resource_postgresql_database.go @@ -2,7 +2,9 @@ package postgresql import ( "database/sql" + "errors" "fmt" + "log" "strings" "github.com/hashicorp/errwrap" @@ -10,71 +12,89 @@ 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": { + dbNameAttr: { Type: schema.TypeString, Required: true, Description: "The PostgreSQL database name to connect to", }, - "owner": { + dbOwnerAttr: { Type: schema.TypeString, Optional: true, Computed: true, Description: "The role name of the user who will own the new database", }, - "template": { + dbTemplateAttr: { Type: schema.TypeString, Optional: true, + ForceNew: true, + Computed: true, Description: "The name of the template from which to create the new database", }, - "encoding": { + dbEncodingAttr: { Type: schema.TypeString, Optional: true, Computed: true, ForceNew: true, Description: "Character set encoding to use in the new database", }, - "lc_collate": { + dbCollationAttr: { Type: schema.TypeString, Optional: true, Computed: true, ForceNew: true, Description: "Collation order (LC_COLLATE) to use in the new database", }, - "lc_ctype": { + dbCTypeAttr: { Type: schema.TypeString, Optional: true, Computed: true, ForceNew: true, Description: "Character classification (LC_CTYPE) to use in the new database", }, - "tablespace_name": { + dbTablespaceAttr: { Type: schema.TypeString, Optional: true, Computed: true, Description: "The name of the tablespace that will be associated with the new database", }, - "connection_limit": { + dbConnLimitAttr: { Type: schema.TypeInt, Optional: true, Computed: true, Description: "How many concurrent connections can be made to this database", ValidateFunc: validateConnLimit, }, - "allow_connections": { + dbAllowConnsAttr: { Type: schema.TypeBool, Optional: true, Computed: true, Description: "If false then no one can connect to this database", }, - "is_template": { + dbIsTemplateAttr: { Type: schema.TypeBool, Optional: true, Computed: true, @@ -93,46 +113,101 @@ func validateConnLimit(v interface{}, key string) (warnings []string, errors []e } 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() - connUsername := client.username - - const numOptions = 9 - createOpts := make([]string, 0, numOptions) - stringOpts := []struct { hclKey string sqlKey string }{ - {"owner", "OWNER"}, - {"template", "TEMPLATE"}, - {"encoding", "ENCODING"}, - {"lc_collate", "LC_COLLATE"}, - {"lc_ctype", "LC_CTYPE"}, - {"tablespace_name", "TABLESPACE"}, + {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"}, + } + + createOpts := make([]string, 0, len(stringOpts)+len(intOpts)+len(boolOpts)) + for _, opt := range stringOpts { v, ok := d.GetOk(opt.hclKey) var val string if !ok { - // Set the owner to the connection username - if opt.hclKey == "owner" && v.(string) == "" { - val = connUsername - } else { + 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: continue } } val = v.(string) - // Set the owner to the connection username - if opt.hclKey == "owner" && val == "" { - val = connUsername + 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 = "" + } } if val != "" { @@ -140,12 +215,6 @@ func resourcePostgreSQLDatabaseCreate(d *schema.ResourceData, meta interface{}) } } - intOpts := []struct { - hclKey string - sqlKey string - }{ - {"connection_limit", "CONNECTION LIMIT"}, - } for _, opt := range intOpts { v, ok := d.GetOk(opt.hclKey) if !ok { @@ -156,13 +225,6 @@ func resourcePostgreSQLDatabaseCreate(d *schema.ResourceData, meta interface{}) createOpts = append(createOpts, fmt.Sprintf("%s=%d", opt.sqlKey, val)) } - boolOpts := []struct { - hclKey string - sqlKey string - }{ - {"allow_connections", "ALLOW_CONNECTIONS"}, - {"is_template", "IS_TEMPLATE"}, - } for _, opt := range boolOpts { v, ok := d.GetOk(opt.hclKey) if !ok { @@ -176,7 +238,7 @@ func resourcePostgreSQLDatabaseCreate(d *schema.ResourceData, meta interface{}) createOpts = append(createOpts, fmt.Sprintf("%s=%s", opt.sqlKey, valStr)) } - dbName := d.Get("name").(string) + dbName := d.Get(dbNameAttr).(string) createStr := strings.Join(createOpts, " ") if len(createOpts) > 0 { createStr = " WITH " + createStr @@ -193,22 +255,14 @@ 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 { - return err - } - + dbName := d.Get(dbNameAttr).(string) query := fmt.Sprintf("DROP DATABASE %s", pq.QuoteIdentifier(dbName)) _, err = conn.Query(query) if err != nil { @@ -221,50 +275,94 @@ 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", d.Id()) + 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", d.Id()) 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) + d.SetId(dbName) 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) } @@ -273,7 +371,7 @@ func grantRoleMembership(conn *sql.DB, dbOwner string, connUsername string) erro 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 + // is already member or role if strings.Contains(err.Error(), "duplicate key value violates unique constraint") { return nil } @@ -282,3 +380,109 @@ func grantRoleMembership(conn *sql.DB, dbOwner string, connUsername string) erro } return nil } + +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 + } + + isTemplate := d.Get(dbIsTemplateAttr).(bool) + dbName := d.Get(dbNameAttr).(string) + 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 +} diff --git a/builtin/providers/postgresql/resource_postgresql_database_test.go b/builtin/providers/postgresql/resource_postgresql_database_test.go index a6a0dc4ef..13a491a7e 100644 --- a/builtin/providers/postgresql/resource_postgresql_database_test.go +++ b/builtin/providers/postgresql/resource_postgresql_database_test.go @@ -25,6 +25,26 @@ func TestAccPostgresqlDatabase_Basic(t *testing.T) { "postgresql_database.mydb", "name", "mydb"), resource.TestCheckResourceAttr( "postgresql_database.mydb", "owner", "myrole"), + resource.TestCheckResourceAttr( + "postgresql_database.all_opts", "owner", "myrole"), + resource.TestCheckResourceAttr( + "postgresql_database.all_opts", "name", "all_opts_name"), + resource.TestCheckResourceAttr( + "postgresql_database.all_opts", "template", "template0"), + resource.TestCheckResourceAttr( + "postgresql_database.all_opts", "encoding", "UTF8"), + resource.TestCheckResourceAttr( + "postgresql_database.all_opts", "lc_collate", "C"), + resource.TestCheckResourceAttr( + "postgresql_database.all_opts", "lc_ctype", "C"), + resource.TestCheckResourceAttr( + "postgresql_database.all_opts", "tablespace_name", "pg_default"), + resource.TestCheckResourceAttr( + "postgresql_database.all_opts", "connection_limit", "-1"), + resource.TestCheckResourceAttr( + "postgresql_database.all_opts", "allow_connections", "false"), + resource.TestCheckResourceAttr( + "postgresql_database.all_opts", "is_template", "false"), ), }, }, @@ -134,11 +154,11 @@ resource "postgresql_database" "mydb2" { owner = "${postgresql_role.myrole.name}" } -resource "postgresql_database" "mydb3" { - name = "mydb3" +resource "postgresql_database" "all_opts" { + name = "all_opts_name" owner = "${postgresql_role.myrole.name}" - template = "template1" - encoding = "SQL_ASCII" + template = "template0" + encoding = "UTF8" lc_collate = "C" lc_ctype = "C" tablespace_name = "pg_default" diff --git a/website/source/docs/import/importability.html.md b/website/source/docs/import/importability.html.md index a3b305738..8ce15281d 100644 --- a/website/source/docs/import/importability.html.md +++ b/website/source/docs/import/importability.html.md @@ -159,6 +159,9 @@ To make a resource importable, please see the * openstack_networking_secgroup_v2 * openstack_networking_subnet_v2 +### PostgreSQL + +* postgresql_database ### Triton diff --git a/website/source/docs/providers/postgresql/r/postgresql_database.html.markdown b/website/source/docs/providers/postgresql/r/postgresql_database.html.markdown index be37ed9bc..6d765f8ef 100644 --- a/website/source/docs/providers/postgresql/r/postgresql_database.html.markdown +++ b/website/source/docs/providers/postgresql/r/postgresql_database.html.markdown @@ -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,90 @@ 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 made 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 = "db1" +} +``` + +It is possible to import a `postgresql_database` resource with the following +command: + +``` +$ terraform import postgresql_database.testdb1 testdb1 +``` From f253fc9eff87fb63c089a99250df7af4298936f3 Mon Sep 17 00:00:00 2001 From: Sean Chittenden Date: Sun, 6 Nov 2016 01:21:45 -0800 Subject: [PATCH 23/46] Commit miss earlier, fix syntax from unstaged commit. --- builtin/providers/postgresql/provider.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/builtin/providers/postgresql/provider.go b/builtin/providers/postgresql/provider.go index 9ca012d85..966375ca8 100644 --- a/builtin/providers/postgresql/provider.go +++ b/builtin/providers/postgresql/provider.go @@ -93,8 +93,9 @@ func validateConnTimeout(v interface{}, key string) (warnings []string, errors [ func providerConfigure(d *schema.ResourceData) (interface{}, error) { var sslMode string - var ok bool - if sslMode, ok = d.GetOk("sslmode").(string); !ok { + if sslModeRaw, ok := d.GetOk("sslmode"); ok { + sslMode = sslModeRaw.(string) + } else { sslMode = d.Get("ssl_mode").(string) } config := Config{ From e9b2b382882dbe1aa2b067ae623ff35157d63a4b Mon Sep 17 00:00:00 2001 From: Sean Chittenden Date: Sun, 6 Nov 2016 08:28:03 -0800 Subject: [PATCH 24/46] Remove SetId() call from *Read(), this isn't required for import to work. --- builtin/providers/postgresql/resource_postgresql_database.go | 1 - 1 file changed, 1 deletion(-) diff --git a/builtin/providers/postgresql/resource_postgresql_database.go b/builtin/providers/postgresql/resource_postgresql_database.go index 04129afce..104b8c7fc 100644 --- a/builtin/providers/postgresql/resource_postgresql_database.go +++ b/builtin/providers/postgresql/resource_postgresql_database.go @@ -324,7 +324,6 @@ func resourcePostgreSQLDatabaseRead(d *schema.ResourceData, meta interface{}) er dbTemplate = "template0" } d.Set(dbTemplateAttr, dbTemplate) - d.SetId(dbName) return nil } } From 37fdc958b34b90bb59b5561efb78bb16ab33125a Mon Sep 17 00:00:00 2001 From: Sean Chittenden Date: Sun, 6 Nov 2016 08:46:40 -0800 Subject: [PATCH 25/46] Always remove the IS_TEMPLATE attribute before dropping a database. --- .../resource_postgresql_database.go | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/builtin/providers/postgresql/resource_postgresql_database.go b/builtin/providers/postgresql/resource_postgresql_database.go index 104b8c7fc..ca3522f1f 100644 --- a/builtin/providers/postgresql/resource_postgresql_database.go +++ b/builtin/providers/postgresql/resource_postgresql_database.go @@ -91,7 +91,7 @@ func resourcePostgreSQLDatabase() *schema.Resource { dbAllowConnsAttr: { Type: schema.TypeBool, Optional: true, - Computed: true, + Default: true, Description: "If false then no one can connect to this database", }, dbIsTemplateAttr: { @@ -226,10 +226,7 @@ func resourcePostgreSQLDatabaseCreate(d *schema.ResourceData, meta interface{}) } for _, opt := range boolOpts { - v, ok := d.GetOk(opt.hclKey) - if !ok { - continue - } + v := d.Get(opt.hclKey) valStr := "FALSE" if val := v.(bool); val { @@ -263,6 +260,19 @@ func resourcePostgreSQLDatabaseDelete(d *schema.ResourceData, meta interface{}) defer conn.Close() 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 + } + query := fmt.Sprintf("DROP DATABASE %s", pq.QuoteIdentifier(dbName)) _, err = conn.Query(query) if err != nil { @@ -476,8 +486,14 @@ func setDBIsTemplate(conn *sql.DB, d *schema.ResourceData) error { return nil } - isTemplate := d.Get(dbIsTemplateAttr).(bool) - dbName := d.Get(dbNameAttr).(string) + 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) From 6b540ecb553bb950ea2c3882ffdb2f1f911d457a Mon Sep 17 00:00:00 2001 From: Sean Chittenden Date: Sun, 6 Nov 2016 09:46:14 -0800 Subject: [PATCH 26/46] Don't use d.GetOk() when the zero value is a required attribute. Add "pathological" test. --- .../resource_postgresql_database.go | 35 ++++--- .../resource_postgresql_database_test.go | 94 ++++++++++++++++--- 2 files changed, 102 insertions(+), 27 deletions(-) diff --git a/builtin/providers/postgresql/resource_postgresql_database.go b/builtin/providers/postgresql/resource_postgresql_database.go index ca3522f1f..7503d2ec8 100644 --- a/builtin/providers/postgresql/resource_postgresql_database.go +++ b/builtin/providers/postgresql/resource_postgresql_database.go @@ -181,32 +181,44 @@ func resourcePostgreSQLDatabaseCreate(d *schema.ResourceData, meta interface{}) val = "pg_default" d.Set(dbTablespaceAttr, val) case opt.hclKey == dbTemplateAttr: - if val == "" { + switch { + case val == "": val = "template0" d.Set(dbTemplateAttr, val) - } else if strings.ToUpper(val) == "DEFAULT" { + case strings.ToUpper(val) == "DEFAULT": val = "" + default: + d.Set(dbTemplateAttr, val) } case opt.hclKey == dbEncodingAttr: - if val == "" { + switch { + case val == "": val = "UTF8" d.Set(dbEncodingAttr, val) - } else if strings.ToUpper(val) == "DEFAULT" { + case strings.ToUpper(val) == "DEFAULT": val = "" + default: + d.Set(dbEncodingAttr, val) } case opt.hclKey == dbCollationAttr: - if val == "" { + switch { + case val == "": val = "C" d.Set(dbCollationAttr, val) - } else if strings.ToUpper(val) == "DEFAULT" { + case strings.ToUpper(val) == "DEFAULT": val = "" + default: + d.Set(dbCollationAttr, val) } case opt.hclKey == dbCTypeAttr: - if val == "" { + switch { + case val == "": val = "C" d.Set(dbCTypeAttr, val) - } else if strings.ToUpper(val) == "DEFAULT" { + case strings.ToUpper(val) == "DEFAULT": val = "" + default: + d.Set(dbCTypeAttr, val) } } @@ -216,12 +228,7 @@ func resourcePostgreSQLDatabaseCreate(d *schema.ResourceData, meta interface{}) } for _, opt := range intOpts { - v, ok := d.GetOk(opt.hclKey) - if !ok { - continue - } - - val := v.(int) + val := d.Get(opt.hclKey).(int) createOpts = append(createOpts, fmt.Sprintf("%s=%d", opt.sqlKey, val)) } diff --git a/builtin/providers/postgresql/resource_postgresql_database_test.go b/builtin/providers/postgresql/resource_postgresql_database_test.go index 13a491a7e..a53587ae6 100644 --- a/builtin/providers/postgresql/resource_postgresql_database_test.go +++ b/builtin/providers/postgresql/resource_postgresql_database_test.go @@ -26,25 +26,67 @@ func TestAccPostgresqlDatabase_Basic(t *testing.T) { resource.TestCheckResourceAttr( "postgresql_database.mydb", "owner", "myrole"), resource.TestCheckResourceAttr( - "postgresql_database.all_opts", "owner", "myrole"), + "postgresql_database.default_opts", "owner", "myrole"), resource.TestCheckResourceAttr( - "postgresql_database.all_opts", "name", "all_opts_name"), + "postgresql_database.default_opts", "name", "default_opts_name"), resource.TestCheckResourceAttr( - "postgresql_database.all_opts", "template", "template0"), + "postgresql_database.default_opts", "template", "template0"), resource.TestCheckResourceAttr( - "postgresql_database.all_opts", "encoding", "UTF8"), + "postgresql_database.default_opts", "encoding", "UTF8"), resource.TestCheckResourceAttr( - "postgresql_database.all_opts", "lc_collate", "C"), + "postgresql_database.default_opts", "lc_collate", "C"), resource.TestCheckResourceAttr( - "postgresql_database.all_opts", "lc_ctype", "C"), + "postgresql_database.default_opts", "lc_ctype", "C"), resource.TestCheckResourceAttr( - "postgresql_database.all_opts", "tablespace_name", "pg_default"), + "postgresql_database.default_opts", "tablespace_name", "pg_default"), resource.TestCheckResourceAttr( - "postgresql_database.all_opts", "connection_limit", "-1"), + "postgresql_database.default_opts", "connection_limit", "-1"), resource.TestCheckResourceAttr( - "postgresql_database.all_opts", "allow_connections", "false"), + "postgresql_database.default_opts", "allow_connections", "true"), resource.TestCheckResourceAttr( - "postgresql_database.all_opts", "is_template", "false"), + "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"), ), }, }, @@ -154,8 +196,8 @@ resource "postgresql_database" "mydb2" { owner = "${postgresql_role.myrole.name}" } -resource "postgresql_database" "all_opts" { - name = "all_opts_name" +resource "postgresql_database" "default_opts" { + name = "default_opts_name" owner = "${postgresql_role.myrole.name}" template = "template0" encoding = "UTF8" @@ -163,10 +205,36 @@ resource "postgresql_database" "all_opts" { lc_ctype = "C" tablespace_name = "pg_default" connection_limit = -1 - allow_connections = false + 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" } From 2e529146a5e92251081b3c75fba6ecabbac37c3d Mon Sep 17 00:00:00 2001 From: Sean Chittenden Date: Sun, 6 Nov 2016 09:49:05 -0800 Subject: [PATCH 27/46] Remove unused code. --- .../postgresql/resource_postgresql_database.go | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/builtin/providers/postgresql/resource_postgresql_database.go b/builtin/providers/postgresql/resource_postgresql_database.go index 7503d2ec8..c85b44d0b 100644 --- a/builtin/providers/postgresql/resource_postgresql_database.go +++ b/builtin/providers/postgresql/resource_postgresql_database.go @@ -382,21 +382,6 @@ func resourcePostgreSQLDatabaseUpdate(d *schema.ResourceData, meta interface{}) 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) - } - } - return nil -} - func setDBName(conn *sql.DB, d *schema.ResourceData) error { if !d.HasChange(dbNameAttr) { return nil From bfc2a2d42fed4905f1850f259acbdb490fd1b2bb Mon Sep 17 00:00:00 2001 From: Sean Chittenden Date: Sun, 6 Nov 2016 13:16:12 -0800 Subject: [PATCH 28/46] Commit WIP re: updated postgresql_role provider. *Read() and *Update() still need to be updated. --- .../resource_postgresql_database.go | 8 - .../postgresql/resource_postgresql_role.go | 257 +++++++++++++++--- .../resource_postgresql_role_test.go | 38 +++ builtin/providers/postgresql/validators.go | 11 + .../r/postgresql_database.html.markdown | 12 +- .../r/postgresql_role.html.markdown | 88 +++++- 6 files changed, 354 insertions(+), 60 deletions(-) create mode 100644 builtin/providers/postgresql/validators.go diff --git a/builtin/providers/postgresql/resource_postgresql_database.go b/builtin/providers/postgresql/resource_postgresql_database.go index c85b44d0b..6dcd0c009 100644 --- a/builtin/providers/postgresql/resource_postgresql_database.go +++ b/builtin/providers/postgresql/resource_postgresql_database.go @@ -104,14 +104,6 @@ func resourcePostgreSQLDatabase() *schema.Resource { } } -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 -} - func resourcePostgreSQLDatabaseCreate(d *schema.ResourceData, meta interface{}) error { c := meta.(*Client) conn, err := c.Connect() diff --git a/builtin/providers/postgresql/resource_postgresql_role.go b/builtin/providers/postgresql/resource_postgresql_role.go index 89137fab8..dc4191871 100644 --- a/builtin/providers/postgresql/resource_postgresql_role.go +++ b/builtin/providers/postgresql/resource_postgresql_role.go @@ -3,64 +3,232 @@ package postgresql import ( "database/sql" "fmt" + "log" + "strconv" + "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, + 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: false, + 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") + } + escapedPassword := strconv.Quote(val) + escapedPassword = strings.TrimLeft(escapedPassword, `"`) + escapedPassword = strings.TrimRight(escapedPassword, `"`) + createOpts = append(createOpts, fmt.Sprintf("%s '%s'", opt.sqlKey, escapedPassword)) + } + case opt.hclKey == roleValidUntilAttr: + switch { + case v.(string) == "", strings.ToUpper(v.(string)) == "NULL": + 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,7 +244,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) @@ -90,25 +258,32 @@ 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) + roleName := d.Get(roleNameAttr).(string) + if roleName == "" { + roleName = d.Id() + } - var canLogin bool - err = conn.QueryRow("SELECT rolcanlogin FROM pg_roles WHERE rolname=$1", roleName).Scan(&canLogin) + var roleCanLogin bool + err = conn.QueryRow("SELECT rolcanlogin FROM pg_roles WHERE rolname=$1", roleName).Scan(&roleCanLogin) 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 role: {{err}}", err) default: - d.Set("login", canLogin) + d.Set(roleNameAttr, roleName) + d.Set(roleLoginAttr, roleCanLogin) + d.Set("encrypted", true) + d.SetId(roleName) return nil } } @@ -123,21 +298,21 @@ func resourcePostgreSQLRoleUpdate(d *schema.ResourceData, meta interface{}) erro d.Partial(true) - roleName := d.Get("name").(string) + roleName := d.Get(roleNameAttr).(string) - if d.HasChange("login") { - loginAttr := getLoginStr(d.Get("login").(bool)) + if d.HasChange(roleLoginAttr) { + loginAttr := getLoginStr(d.Get(roleLoginAttr).(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") + d.SetPartial(roleLoginAttr) } - password := d.Get("password").(string) - if d.HasChange("password") { + password := d.Get(rolePasswordAttr).(string) + if d.HasChange(rolePasswordAttr) { encryptedCfg := getEncryptedStr(d.Get("encrypted").(bool)) query := fmt.Sprintf("ALTER ROLE %s %s PASSWORD '%s'", pq.QuoteIdentifier(roleName), encryptedCfg, password) @@ -146,7 +321,7 @@ func resourcePostgreSQLRoleUpdate(d *schema.ResourceData, meta interface{}) erro return errwrap.Wrapf("Error updating password attribute for role: {{err}}", err) } - d.SetPartial("password") + d.SetPartial(rolePasswordAttr) } if d.HasChange("encrypted") { diff --git a/builtin/providers/postgresql/resource_postgresql_role_test.go b/builtin/providers/postgresql/resource_postgresql_role_test.go index 2188a1ef7..31d0b0ebe 100644 --- a/builtin/providers/postgresql/resource_postgresql_role_test.go +++ b/builtin/providers/postgresql/resource_postgresql_role_test.go @@ -24,6 +24,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", "NULL"), ), }, }, @@ -129,4 +152,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 = "NULL" +} ` diff --git a/builtin/providers/postgresql/validators.go b/builtin/providers/postgresql/validators.go new file mode 100644 index 000000000..8bc75209e --- /dev/null +++ b/builtin/providers/postgresql/validators.go @@ -0,0 +1,11 @@ +package postgresql + +import "fmt" + +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 +} diff --git a/website/source/docs/providers/postgresql/r/postgresql_database.html.markdown b/website/source/docs/providers/postgresql/r/postgresql_database.html.markdown index 6d765f8ef..35d5b5815 100644 --- a/website/source/docs/providers/postgresql/r/postgresql_database.html.markdown +++ b/website/source/docs/providers/postgresql/r/postgresql_database.html.markdown @@ -42,8 +42,8 @@ resource "postgresql_database" "my_db" { tablespace. This tablespace will be the default tablespace used for objects created in this database. -* `connection_limit` - (Optional) How many concurrent connections can be made to - this database. `-1` (the default) means no limit. +* `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 @@ -95,7 +95,7 @@ provider "postgresql" { resource "postgresql_database" "db1" { provider = "postgresql.admindb" - name = "db1" + name = "testdb1" } ``` @@ -103,5 +103,9 @@ It is possible to import a `postgresql_database` resource with the following command: ``` -$ terraform import postgresql_database.testdb1 testdb1 +$ 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. diff --git a/website/source/docs/providers/postgresql/r/postgresql_role.html.markdown b/website/source/docs/providers/postgresql/r/postgresql_role.html.markdown index a5d5c17d8..e2818dc80 100644 --- a/website/source/docs/providers/postgresql/r/postgresql_role.html.markdown +++ b/website/source/docs/providers/postgresql/r/postgresql_role.html.markdown @@ -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. \ No newline at end of file +* `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 `false`. + +* `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. From db5d7b0438660a193b0d7837317be9cc9a664f04 Mon Sep 17 00:00:00 2001 From: Sean Chittenden Date: Sun, 6 Nov 2016 13:17:54 -0800 Subject: [PATCH 29/46] Style nit --- builtin/providers/postgresql/resource_postgresql_database.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/builtin/providers/postgresql/resource_postgresql_database.go b/builtin/providers/postgresql/resource_postgresql_database.go index 6dcd0c009..4d3fe297c 100644 --- a/builtin/providers/postgresql/resource_postgresql_database.go +++ b/builtin/providers/postgresql/resource_postgresql_database.go @@ -225,10 +225,10 @@ func resourcePostgreSQLDatabaseCreate(d *schema.ResourceData, meta interface{}) } for _, opt := range boolOpts { - v := d.Get(opt.hclKey) + val := d.Get(opt.hclKey).(bool) valStr := "FALSE" - if val := v.(bool); val { + if val { valStr = "TRUE" } createOpts = append(createOpts, fmt.Sprintf("%s=%s", opt.sqlKey, valStr)) From e2448473cb3f992d49c3b66e74f6e4b1b5bd42f9 Mon Sep 17 00:00:00 2001 From: Sean Chittenden Date: Mon, 7 Nov 2016 10:32:51 -0800 Subject: [PATCH 30/46] Clean up the DatabaseCreate call. --- builtin/providers/postgresql/helpers.go | 24 +++ .../resource_postgresql_database.go | 193 ++++++------------ builtin/providers/postgresql/validators.go | 11 - 3 files changed, 89 insertions(+), 139 deletions(-) create mode 100644 builtin/providers/postgresql/helpers.go delete mode 100644 builtin/providers/postgresql/validators.go diff --git a/builtin/providers/postgresql/helpers.go b/builtin/providers/postgresql/helpers.go new file mode 100644 index 000000000..db61713dc --- /dev/null +++ b/builtin/providers/postgresql/helpers.go @@ -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("%d can not be less than -1", key)) + } + return +} diff --git a/builtin/providers/postgresql/resource_postgresql_database.go b/builtin/providers/postgresql/resource_postgresql_database.go index 4d3fe297c..0f0f1c363 100644 --- a/builtin/providers/postgresql/resource_postgresql_database.go +++ b/builtin/providers/postgresql/resource_postgresql_database.go @@ -1,6 +1,7 @@ package postgresql import ( + "bytes" "database/sql" "errors" "fmt" @@ -112,134 +113,70 @@ func resourcePostgreSQLDatabaseCreate(d *schema.ResourceData, meta interface{}) } defer conn.Close() - stringOpts := []struct { - hclKey string - sqlKey string - }{ - {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"}, - } - - createOpts := make([]string, 0, len(stringOpts)+len(intOpts)+len(boolOpts)) - - for _, opt := range stringOpts { - v, ok := d.GetOk(opt.hclKey) - var val string - if !ok { - 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: - continue - } - } - - val = v.(string) - - 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: - switch { - case val == "": - val = "template0" - d.Set(dbTemplateAttr, val) - case strings.ToUpper(val) == "DEFAULT": - val = "" - default: - d.Set(dbTemplateAttr, val) - } - case opt.hclKey == dbEncodingAttr: - switch { - case val == "": - val = "UTF8" - d.Set(dbEncodingAttr, val) - case strings.ToUpper(val) == "DEFAULT": - val = "" - default: - d.Set(dbEncodingAttr, val) - } - case opt.hclKey == dbCollationAttr: - switch { - case val == "": - val = "C" - d.Set(dbCollationAttr, val) - case strings.ToUpper(val) == "DEFAULT": - val = "" - default: - d.Set(dbCollationAttr, val) - } - case opt.hclKey == dbCTypeAttr: - switch { - case val == "": - val = "C" - d.Set(dbCTypeAttr, val) - case strings.ToUpper(val) == "DEFAULT": - val = "" - default: - d.Set(dbCTypeAttr, val) - } - } - - if val != "" { - 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 { - val := d.Get(opt.hclKey).(bool) - - valStr := "FALSE" - if val { - valStr = "TRUE" - } - createOpts = append(createOpts, fmt.Sprintf("%s=%s", opt.sqlKey, valStr)) - } - dbName := d.Get(dbNameAttr).(string) - createStr := strings.Join(createOpts, " ") - if len(createOpts) > 0 { - createStr = " WITH " + createStr + b := bytes.NewBufferString("CREATE DATABASE ") + fmt.Fprint(b, pq.QuoteIdentifier(dbName)) + + // 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)) } - query := fmt.Sprintf("CREATE DATABASE %s%s", pq.QuoteIdentifier(dbName), createStr) + + 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") + } + + 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) @@ -296,7 +233,7 @@ func resourcePostgreSQLDatabaseRead(d *schema.ResourceData, meta interface{}) er 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()) + log.Printf("[WARN] PostgreSQL database (%s) not found", dbId) d.SetId("") return nil case err != nil: @@ -313,7 +250,7 @@ func resourcePostgreSQLDatabaseRead(d *schema.ResourceData, meta interface{}) er ) switch { case err == sql.ErrNoRows: - log.Printf("[WARN] PostgreSQL database (%s) not found", d.Id()) + log.Printf("[WARN] PostgreSQL database (%s) not found", dbId) d.SetId("") return nil case err != nil: diff --git a/builtin/providers/postgresql/validators.go b/builtin/providers/postgresql/validators.go deleted file mode 100644 index 8bc75209e..000000000 --- a/builtin/providers/postgresql/validators.go +++ /dev/null @@ -1,11 +0,0 @@ -package postgresql - -import "fmt" - -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 -} From 15cd542392ae4335e5f523f7fe59940b70dc0713 Mon Sep 17 00:00:00 2001 From: Sean Chittenden Date: Mon, 12 Dec 2016 00:53:06 -0800 Subject: [PATCH 31/46] Remove a duplicate `connect_timeout` from a rebase+stash. --- builtin/providers/postgresql/provider.go | 7 ------- 1 file changed, 7 deletions(-) diff --git a/builtin/providers/postgresql/provider.go b/builtin/providers/postgresql/provider.go index 966375ca8..56852c597 100644 --- a/builtin/providers/postgresql/provider.go +++ b/builtin/providers/postgresql/provider.go @@ -52,13 +52,6 @@ func Provider() terraform.ResourceProvider { 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", }, - "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.", - }, "ssl_mode": { Type: schema.TypeString, Optional: true, From b576a3eda1a31137205683fbbe203b912bd3d4c7 Mon Sep 17 00:00:00 2001 From: Sean Chittenden Date: Mon, 12 Dec 2016 01:07:26 -0800 Subject: [PATCH 32/46] Rename variable from `roleSuperUserAttr` to `roleSuperuserAttr`. --- builtin/providers/postgresql/resource_postgresql_role.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/builtin/providers/postgresql/resource_postgresql_role.go b/builtin/providers/postgresql/resource_postgresql_role.go index dc4191871..0c3811108 100644 --- a/builtin/providers/postgresql/resource_postgresql_role.go +++ b/builtin/providers/postgresql/resource_postgresql_role.go @@ -23,7 +23,7 @@ const ( roleNameAttr = "name" rolePasswordAttr = "password" roleReplicationAttr = "replication" - roleSuperUserAttr = "superuser" + roleSuperuserAttr = "superuser" roleValidUntilAttr = "valid_until" // Deprecated options @@ -78,7 +78,7 @@ func resourcePostgreSQLRole() *schema.Resource { Description: "How many concurrent connections can be made with this role", ValidateFunc: validateConnLimit, }, - roleSuperUserAttr: { + roleSuperuserAttr: { Type: schema.TypeBool, Optional: true, Default: false, @@ -150,7 +150,7 @@ func resourcePostgreSQLRoleCreate(d *schema.ResourceData, meta interface{}) erro sqlKeyEnable string sqlKeyDisable string }{ - {roleSuperUserAttr, "CREATEDB", "NOCREATEDB"}, + {roleSuperuserAttr, "CREATEDB", "NOCREATEDB"}, {roleCreateRoleAttr, "CREATEROLE", "NOCREATEROLE"}, {roleInheritAttr, "INHERIT", "NOINHERIT"}, {roleLoginAttr, "LOGIN", "NOLOGIN"}, From e9dc92c18d6d69e1a25a07d22dfc32359779b019 Mon Sep 17 00:00:00 2001 From: Sean Chittenden Date: Mon, 12 Dec 2016 01:11:07 -0800 Subject: [PATCH 33/46] Change the default for `inherit` from `false` to `true` to match PostgreSQL. --- builtin/providers/postgresql/resource_postgresql_role.go | 4 ++-- .../docs/providers/postgresql/r/postgresql_role.html.markdown | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/builtin/providers/postgresql/resource_postgresql_role.go b/builtin/providers/postgresql/resource_postgresql_role.go index 0c3811108..cf74d6c78 100644 --- a/builtin/providers/postgresql/resource_postgresql_role.go +++ b/builtin/providers/postgresql/resource_postgresql_role.go @@ -2,9 +2,9 @@ package postgresql import ( "database/sql" + "errors" "fmt" "log" - "strconv" "strings" "github.com/hashicorp/errwrap" @@ -99,7 +99,7 @@ func resourcePostgreSQLRole() *schema.Resource { roleInheritAttr: { Type: schema.TypeBool, Optional: true, - Default: false, + Default: true, Description: `Determine whether a role "inherits" the privileges of roles it is a member of`, }, roleLoginAttr: { diff --git a/website/source/docs/providers/postgresql/r/postgresql_role.html.markdown b/website/source/docs/providers/postgresql/r/postgresql_role.html.markdown index e2818dc80..cc36790e6 100644 --- a/website/source/docs/providers/postgresql/r/postgresql_role.html.markdown +++ b/website/source/docs/providers/postgresql/r/postgresql_role.html.markdown @@ -47,7 +47,7 @@ resource "postgresql_role" "my_replication_role" { is `false`. * `inherit` - (Optional) Defines whether a role "inherits" the privileges of - roles it is a member of. Default value is `false`. + 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 From e36827c5fef1dac1a62dd01c5a8f1e26c42dd0dd Mon Sep 17 00:00:00 2001 From: Sean Chittenden Date: Mon, 12 Dec 2016 01:18:13 -0800 Subject: [PATCH 34/46] Change the default for `valid_until` to `infinity` to match the default. --- builtin/providers/postgresql/resource_postgresql_role.go | 6 +++--- .../providers/postgresql/resource_postgresql_role_test.go | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/builtin/providers/postgresql/resource_postgresql_role.go b/builtin/providers/postgresql/resource_postgresql_role.go index cf74d6c78..767d9c849 100644 --- a/builtin/providers/postgresql/resource_postgresql_role.go +++ b/builtin/providers/postgresql/resource_postgresql_role.go @@ -65,10 +65,10 @@ func resourcePostgreSQLRole() *schema.Resource { 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: { @@ -188,8 +188,8 @@ func resourcePostgreSQLRoleCreate(d *schema.ResourceData, meta interface{}) erro } case opt.hclKey == roleValidUntilAttr: switch { - case v.(string) == "", strings.ToUpper(v.(string)) == "NULL": - createOpts = append(createOpts, fmt.Sprintf("%s %s", opt.sqlKey, "'infinity'")) + 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))) } diff --git a/builtin/providers/postgresql/resource_postgresql_role_test.go b/builtin/providers/postgresql/resource_postgresql_role_test.go index 31d0b0ebe..c6ff0dec6 100644 --- a/builtin/providers/postgresql/resource_postgresql_role_test.go +++ b/builtin/providers/postgresql/resource_postgresql_role_test.go @@ -46,7 +46,7 @@ func TestAccPostgresqlRole_Basic(t *testing.T) { resource.TestCheckResourceAttr( "postgresql_role.role_with_defaults", "password", ""), resource.TestCheckResourceAttr( - "postgresql_role.role_with_defaults", "valid_until", "NULL"), + "postgresql_role.role_with_defaults", "valid_until", "infinity"), ), }, }, @@ -165,6 +165,6 @@ resource "postgresql_role" "role_with_defaults" { connection_limit = -1 encrypted_password = true password = "" - valid_until = "NULL" + valid_until = "infinity" } ` From 6deb61b0cf535d08a7e0204f286e0356560f7599 Mon Sep 17 00:00:00 2001 From: Sean Chittenden Date: Mon, 12 Dec 2016 01:33:02 -0800 Subject: [PATCH 35/46] Enable import support for PostgreSQL's extensions. --- .../resource_postgresql_extension.go | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/builtin/providers/postgresql/resource_postgresql_extension.go b/builtin/providers/postgresql/resource_postgresql_extension.go index d4e990b56..d0cb83cc7 100644 --- a/builtin/providers/postgresql/resource_postgresql_extension.go +++ b/builtin/providers/postgresql/resource_postgresql_extension.go @@ -3,6 +3,7 @@ package postgresql import ( "database/sql" "fmt" + "log" "github.com/hashicorp/errwrap" "github.com/hashicorp/terraform/helper/schema" @@ -14,6 +15,9 @@ func resourcePostgreSQLExtension() *schema.Resource { Create: resourcePostgreSQLExtensionCreate, Read: resourcePostgreSQLExtensionRead, Delete: resourcePostgreSQLExtensionDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, Schema: map[string]*schema.Schema{ "name": { @@ -26,8 +30,8 @@ func resourcePostgreSQLExtension() *schema.Resource { } 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 } @@ -47,32 +51,35 @@ func resourcePostgreSQLExtensionCreate(d *schema.ResourceData, meta interface{}) } 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() + dbId := d.Id() extensionName := d.Get("name").(string) var hasExtension bool - err = conn.QueryRow("SELECT 1 from pg_extension d WHERE extname=$1", extensionName).Scan(&hasExtension) + err = conn.QueryRow("SELECT TRUE from pg_catalog.pg_extension d WHERE extname=$1", dbId).Scan(&hasExtension) 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.SetId(extensionName) 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 } From 4c6c52ee8de8309c61c2a7dc97cb6991e8dc7cf9 Mon Sep 17 00:00:00 2001 From: Sean Chittenden Date: Mon, 12 Dec 2016 01:36:14 -0800 Subject: [PATCH 36/46] Nuke some whitespace. --- .../providers/postgresql/resource_postgresql_extension_test.go | 1 - builtin/providers/postgresql/resource_postgresql_role_test.go | 1 - 2 files changed, 2 deletions(-) diff --git a/builtin/providers/postgresql/resource_postgresql_extension_test.go b/builtin/providers/postgresql/resource_postgresql_extension_test.go index 78b666e8e..d74a2c188 100644 --- a/builtin/providers/postgresql/resource_postgresql_extension_test.go +++ b/builtin/providers/postgresql/resource_postgresql_extension_test.go @@ -10,7 +10,6 @@ import ( ) func TestAccPostgresqlExtension_Basic(t *testing.T) { - resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, diff --git a/builtin/providers/postgresql/resource_postgresql_role_test.go b/builtin/providers/postgresql/resource_postgresql_role_test.go index c6ff0dec6..5ff3a880d 100644 --- a/builtin/providers/postgresql/resource_postgresql_role_test.go +++ b/builtin/providers/postgresql/resource_postgresql_role_test.go @@ -10,7 +10,6 @@ import ( ) func TestAccPostgresqlRole_Basic(t *testing.T) { - resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, From 1a93309e5048cb4e68fb202242da3e4dce30775a Mon Sep 17 00:00:00 2001 From: Sean Chittenden Date: Mon, 12 Dec 2016 01:37:00 -0800 Subject: [PATCH 37/46] Expand postgresql_role support to include all known options. --- .../postgresql/resource_postgresql_role.go | 313 ++++++++++++++---- 1 file changed, 253 insertions(+), 60 deletions(-) diff --git a/builtin/providers/postgresql/resource_postgresql_role.go b/builtin/providers/postgresql/resource_postgresql_role.go index 767d9c849..99b2fe23a 100644 --- a/builtin/providers/postgresql/resource_postgresql_role.go +++ b/builtin/providers/postgresql/resource_postgresql_role.go @@ -181,10 +181,7 @@ func resourcePostgreSQLRoleCreate(d *schema.ResourceData, meta interface{}) erro } else { createOpts = append(createOpts, "UNENCRYPTED") } - escapedPassword := strconv.Quote(val) - escapedPassword = strings.TrimLeft(escapedPassword, `"`) - escapedPassword = strings.TrimRight(escapedPassword, `"`) - createOpts = append(createOpts, fmt.Sprintf("%s '%s'", opt.sqlKey, escapedPassword)) + createOpts = append(createOpts, fmt.Sprintf("%s '%s'", opt.sqlKey, pqQuoteLiteral(val))) } case opt.hclKey == roleValidUntilAttr: switch { @@ -211,7 +208,6 @@ func resourcePostgreSQLRoleCreate(d *schema.ResourceData, meta interface{}) erro continue } val := d.Get(opt.hclKey).(bool) - valStr := opt.sqlKeyDisable if val { valStr = opt.sqlKeyEnable @@ -245,7 +241,6 @@ func resourcePostgreSQLRoleDelete(d *schema.ResourceData, meta interface{}) erro defer conn.Close() roleName := d.Get(roleNameAttr).(string) - query := fmt.Sprintf("DROP ROLE %s", pq.QuoteIdentifier(roleName)) _, err = conn.Query(query) if err != nil { @@ -265,91 +260,289 @@ func resourcePostgreSQLRoleRead(d *schema.ResourceData, meta interface{}) error } defer conn.Close() - roleName := d.Get(roleNameAttr).(string) - if roleName == "" { - roleName = d.Id() - } - - var roleCanLogin bool - err = conn.QueryRow("SELECT rolcanlogin FROM pg_roles WHERE rolname=$1", roleName).Scan(&roleCanLogin) + 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 database (%s) not found", d.Id()) + 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(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("encrypted", true) + 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(roleNameAttr).(string) - - if d.HasChange(roleLoginAttr) { - loginAttr := getLoginStr(d.Get(roleLoginAttr).(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(roleLoginAttr) + if err := setRoleName(conn, d); err != nil { + return err } - password := d.Get(rolePasswordAttr).(string) - if d.HasChange(rolePasswordAttr) { - 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(rolePasswordAttr) + 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 } From d1c9ebb6c2065c20579503333f1c5d5f88650ef7 Mon Sep 17 00:00:00 2001 From: Sean Chittenden Date: Mon, 12 Dec 2016 07:35:41 -0800 Subject: [PATCH 38/46] Add PostgreSQL schema support --- builtin/providers/postgresql/provider.go | 1 + .../postgresql/resource_postgresql_schema.go | 177 ++++++++++++++++++ .../resource_postgresql_schema_test.go | 155 +++++++++++++++ .../r/postgresql_schema.html.markdown | 58 ++++++ 4 files changed, 391 insertions(+) create mode 100644 builtin/providers/postgresql/resource_postgresql_schema.go create mode 100644 builtin/providers/postgresql/resource_postgresql_schema_test.go create mode 100644 website/source/docs/providers/postgresql/r/postgresql_schema.html.markdown diff --git a/builtin/providers/postgresql/provider.go b/builtin/providers/postgresql/provider.go index 56852c597..0c6aec01f 100644 --- a/builtin/providers/postgresql/provider.go +++ b/builtin/providers/postgresql/provider.go @@ -69,6 +69,7 @@ func Provider() terraform.ResourceProvider { ResourcesMap: map[string]*schema.Resource{ "postgresql_database": resourcePostgreSQLDatabase(), "postgresql_extension": resourcePostgreSQLExtension(), + "postgresql_schema": resourcePostgreSQLSchema(), "postgresql_role": resourcePostgreSQLRole(), }, diff --git a/builtin/providers/postgresql/resource_postgresql_schema.go b/builtin/providers/postgresql/resource_postgresql_schema.go new file mode 100644 index 000000000..10c847060 --- /dev/null +++ b/builtin/providers/postgresql/resource_postgresql_schema.go @@ -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 +} diff --git a/builtin/providers/postgresql/resource_postgresql_schema_test.go b/builtin/providers/postgresql/resource_postgresql_schema_test.go new file mode 100644 index 000000000..5a5eac059 --- /dev/null +++ b/builtin/providers/postgresql/resource_postgresql_schema_test.go @@ -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}" +} +` diff --git a/website/source/docs/providers/postgresql/r/postgresql_schema.html.markdown b/website/source/docs/providers/postgresql/r/postgresql_schema.html.markdown new file mode 100644 index 000000000..ba7b9c081 --- /dev/null +++ b/website/source/docs/providers/postgresql/r/postgresql_schema.html.markdown @@ -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. From 201d9b9dfdf20e01600b8a3d113ebae6d35ddf87 Mon Sep 17 00:00:00 2001 From: Sean Chittenden Date: Mon, 12 Dec 2016 13:14:06 -0800 Subject: [PATCH 39/46] Fix the description for the postgresql_role. --- .../docs/providers/postgresql/r/postgresql_role.html.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/source/docs/providers/postgresql/r/postgresql_role.html.markdown b/website/source/docs/providers/postgresql/r/postgresql_role.html.markdown index cc36790e6..d27d60035 100644 --- a/website/source/docs/providers/postgresql/r/postgresql_role.html.markdown +++ b/website/source/docs/providers/postgresql/r/postgresql_role.html.markdown @@ -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 From daa951434f56465c82818407c2fae46fdf50578b Mon Sep 17 00:00:00 2001 From: Sean Chittenden Date: Mon, 12 Dec 2016 13:15:57 -0800 Subject: [PATCH 40/46] Teach postgresql_extension about schemas. --- .../resource_postgresql_extension.go | 82 ++++++++++++++++--- .../resource_postgresql_extension_test.go | 58 +++++++++++++ 2 files changed, 128 insertions(+), 12 deletions(-) diff --git a/builtin/providers/postgresql/resource_postgresql_extension.go b/builtin/providers/postgresql/resource_postgresql_extension.go index d0cb83cc7..1040ef0f0 100644 --- a/builtin/providers/postgresql/resource_postgresql_extension.go +++ b/builtin/providers/postgresql/resource_postgresql_extension.go @@ -1,7 +1,9 @@ package postgresql import ( + "bytes" "database/sql" + "errors" "fmt" "log" @@ -10,21 +12,33 @@ import ( "github.com/lib/pq" ) +const ( + extNameAttr = "name" + extSchemaAttr = "schema" +) + 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", + }, }, } } @@ -37,15 +51,22 @@ func resourcePostgreSQLExtensionCreate(d *schema.ResourceData, meta interface{}) } 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))) + } + + 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) } @@ -58,11 +79,10 @@ func resourcePostgreSQLExtensionRead(d *schema.ResourceData, meta interface{}) e } defer conn.Close() - dbId := d.Id() - extensionName := d.Get("name").(string) + extID := d.Get(extNameAttr).(string) - var hasExtension bool - err = conn.QueryRow("SELECT TRUE from pg_catalog.pg_extension d WHERE extname=$1", dbId).Scan(&hasExtension) + var extName, extSchema string + err = conn.QueryRow("SELECT e.extname, n.nspname FROM pg_catalog.pg_extension e, pg_catalog.pg_namespace n WHERE n.oid = e.extnamespace AND e.extname = $1", extID).Scan(&extName, &extSchema) switch { case err == sql.ErrNoRows: log.Printf("[WARN] PostgreSQL extension (%s) not found", d.Id()) @@ -71,8 +91,9 @@ func resourcePostgreSQLExtensionRead(d *schema.ResourceData, meta interface{}) e case err != nil: return errwrap.Wrapf("Error reading extension: {{err}}", err) default: - d.Set("extension", hasExtension) - d.SetId(extensionName) + d.Set(extNameAttr, extName) + d.Set(extSchemaAttr, extSchema) + d.SetId(extName) return nil } } @@ -85,9 +106,9 @@ func resourcePostgreSQLExtensionDelete(d *schema.ResourceData, meta interface{}) } defer conn.Close() - extensionName := d.Get("name").(string) + extName := d.Get(extNameAttr).(string) - query := fmt.Sprintf("DROP EXTENSION %s", pq.QuoteIdentifier(extensionName)) + query := fmt.Sprintf("DROP EXTENSION %s", pq.QuoteIdentifier(extName)) _, err = conn.Query(query) if err != nil { return errwrap.Wrapf("Error deleting extension: {{err}}", err) @@ -97,3 +118,40 @@ 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 + } + + return resourcePostgreSQLExtensionRead(d, meta) +} + +func setExtSchema(conn *sql.DB, d *schema.ResourceData) error { + if !d.HasChange(extSchemaAttr) { + return nil + } + + oraw, nraw := d.GetChange(extSchemaAttr) + o := oraw.(string) + 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(o), pq.QuoteIdentifier(n)) + if _, err := conn.Query(query); err != nil { + return errwrap.Wrapf("Error updating extension SCHEMA: {{err}}", err) + } + + return nil +} diff --git a/builtin/providers/postgresql/resource_postgresql_extension_test.go b/builtin/providers/postgresql/resource_postgresql_extension_test.go index d74a2c188..b474f3299 100644 --- a/builtin/providers/postgresql/resource_postgresql_extension_test.go +++ b/builtin/providers/postgresql/resource_postgresql_extension_test.go @@ -75,6 +75,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 { @@ -99,3 +135,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}" +} +` From c602f024f4089e4c5b9b6aaeafb51380a9395338 Mon Sep 17 00:00:00 2001 From: Sean Chittenden Date: Mon, 12 Dec 2016 13:40:49 -0800 Subject: [PATCH 41/46] Fix the remaining `postgresql_extension` unit test. --- .../postgresql/resource_postgresql_extension.go | 13 ++++++------- .../resource_postgresql_extension_test.go | 4 ++-- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/builtin/providers/postgresql/resource_postgresql_extension.go b/builtin/providers/postgresql/resource_postgresql_extension.go index 1040ef0f0..37df80918 100644 --- a/builtin/providers/postgresql/resource_postgresql_extension.go +++ b/builtin/providers/postgresql/resource_postgresql_extension.go @@ -79,8 +79,7 @@ func resourcePostgreSQLExtensionRead(d *schema.ResourceData, meta interface{}) e } defer conn.Close() - extID := d.Get(extNameAttr).(string) - + extID := d.Id() var extName, extSchema string err = conn.QueryRow("SELECT e.extname, n.nspname FROM pg_catalog.pg_extension e, pg_catalog.pg_namespace n WHERE n.oid = e.extnamespace AND e.extname = $1", extID).Scan(&extName, &extSchema) switch { @@ -106,9 +105,9 @@ func resourcePostgreSQLExtensionDelete(d *schema.ResourceData, meta interface{}) } defer conn.Close() - extName := d.Get(extNameAttr).(string) + extID := d.Id() - query := fmt.Sprintf("DROP EXTENSION %s", pq.QuoteIdentifier(extName)) + query := fmt.Sprintf("DROP EXTENSION %s", pq.QuoteIdentifier(extID)) _, err = conn.Query(query) if err != nil { return errwrap.Wrapf("Error deleting extension: {{err}}", err) @@ -141,14 +140,14 @@ func setExtSchema(conn *sql.DB, d *schema.ResourceData) error { return nil } - oraw, nraw := d.GetChange(extSchemaAttr) - o := oraw.(string) + 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(o), pq.QuoteIdentifier(n)) + 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) } diff --git a/builtin/providers/postgresql/resource_postgresql_extension_test.go b/builtin/providers/postgresql/resource_postgresql_extension_test.go index b474f3299..a41a3496b 100644 --- a/builtin/providers/postgresql/resource_postgresql_extension_test.go +++ b/builtin/providers/postgresql/resource_postgresql_extension_test.go @@ -118,8 +118,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 From a4965c01af7ffd0e1ff913623eeda6b889ba6ce0 Mon Sep 17 00:00:00 2001 From: Sean Chittenden Date: Mon, 12 Dec 2016 14:30:25 -0800 Subject: [PATCH 42/46] Fix up helpers. Pointed out by: @stack72 --- builtin/providers/postgresql/helpers.go | 2 +- builtin/providers/postgresql/provider.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/builtin/providers/postgresql/helpers.go b/builtin/providers/postgresql/helpers.go index db61713dc..2be57bf39 100644 --- a/builtin/providers/postgresql/helpers.go +++ b/builtin/providers/postgresql/helpers.go @@ -18,7 +18,7 @@ func pqQuoteLiteral(in string) string { 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)) + errors = append(errors, fmt.Errorf("%s can not be less than -1", key)) } return } diff --git a/builtin/providers/postgresql/provider.go b/builtin/providers/postgresql/provider.go index 0c6aec01f..d692ba2fb 100644 --- a/builtin/providers/postgresql/provider.go +++ b/builtin/providers/postgresql/provider.go @@ -80,7 +80,7 @@ func Provider() terraform.ResourceProvider { func validateConnTimeout(v interface{}, key string) (warnings []string, errors []error) { value := v.(int) if value < 0 { - errors = append(errors, fmt.Errorf("%d can not be less than 0", key)) + errors = append(errors, fmt.Errorf("%s can not be less than 0", key)) } return } From 8c41f0859bba9ed70ea6458113c8de7ad85d2ed5 Mon Sep 17 00:00:00 2001 From: Sean Chittenden Date: Mon, 12 Dec 2016 14:37:40 -0800 Subject: [PATCH 43/46] Add the version attribute to postgresql_extension. --- .../resource_postgresql_extension.go | 48 +++++++++++++++++-- .../resource_postgresql_extension_test.go | 9 ++++ 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/builtin/providers/postgresql/resource_postgresql_extension.go b/builtin/providers/postgresql/resource_postgresql_extension.go index 37df80918..240de3eb3 100644 --- a/builtin/providers/postgresql/resource_postgresql_extension.go +++ b/builtin/providers/postgresql/resource_postgresql_extension.go @@ -13,8 +13,9 @@ import ( ) const ( - extNameAttr = "name" - extSchemaAttr = "schema" + extNameAttr = "name" + extSchemaAttr = "schema" + extVersionAttr = "version" ) func resourcePostgreSQLExtension() *schema.Resource { @@ -39,6 +40,12 @@ func resourcePostgreSQLExtension() *schema.Resource { 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", + }, }, } } @@ -60,6 +67,10 @@ func resourcePostgreSQLExtensionCreate(d *schema.ResourceData, meta interface{}) 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 { @@ -80,8 +91,8 @@ func resourcePostgreSQLExtensionRead(d *schema.ResourceData, meta interface{}) e defer conn.Close() extID := d.Id() - var extName, extSchema string - err = conn.QueryRow("SELECT e.extname, n.nspname FROM pg_catalog.pg_extension e, pg_catalog.pg_namespace n WHERE n.oid = e.extnamespace AND e.extname = $1", extID).Scan(&extName, &extSchema) + 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()) @@ -92,6 +103,7 @@ func resourcePostgreSQLExtensionRead(d *schema.ResourceData, meta interface{}) e default: d.Set(extNameAttr, extName) d.Set(extSchemaAttr, extSchema) + d.Set(extVersionAttr, extVersion) d.SetId(extName) return nil } @@ -132,6 +144,10 @@ func resourcePostgreSQLExtensionUpdate(d *schema.ResourceData, meta interface{}) return err } + if err := setExtVersion(conn, d); err != nil { + return err + } + return resourcePostgreSQLExtensionRead(d, meta) } @@ -154,3 +170,27 @@ func setExtSchema(conn *sql.DB, d *schema.ResourceData) error { 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 +} diff --git a/builtin/providers/postgresql/resource_postgresql_extension_test.go b/builtin/providers/postgresql/resource_postgresql_extension_test.go index a41a3496b..7deb71a39 100644 --- a/builtin/providers/postgresql/resource_postgresql_extension_test.go +++ b/builtin/providers/postgresql/resource_postgresql_extension_test.go @@ -21,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"), ), }, }, From 6ed37770c31b18b9aef469859a3051c091ebb674 Mon Sep 17 00:00:00 2001 From: Sean Chittenden Date: Mon, 12 Dec 2016 14:44:46 -0800 Subject: [PATCH 44/46] Add the testing Makefile that I'm using for testing the provider locally. --- builtin/providers/postgresql/GNUmakefile | 28 ++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 builtin/providers/postgresql/GNUmakefile diff --git a/builtin/providers/postgresql/GNUmakefile b/builtin/providers/postgresql/GNUmakefile new file mode 100644 index 000000000..926b843f9 --- /dev/null +++ b/builtin/providers/postgresql/GNUmakefile @@ -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 From 2ecd42c0be7f641ca1d4ba81ea9b2c4c4a1de838 Mon Sep 17 00:00:00 2001 From: Sean Chittenden Date: Mon, 12 Dec 2016 15:21:00 -0800 Subject: [PATCH 45/46] Remove non-standard environment variables in prep for 0.8. --- builtin/providers/postgresql/provider.go | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/builtin/providers/postgresql/provider.go b/builtin/providers/postgresql/provider.go index d692ba2fb..2f7064447 100644 --- a/builtin/providers/postgresql/provider.go +++ b/builtin/providers/postgresql/provider.go @@ -14,10 +14,9 @@ func Provider() terraform.ResourceProvider { return &schema.Provider{ Schema: map[string]*schema.Schema{ "host": { - Type: schema.TypeString, - Optional: true, - // TODO(sean@): Remove POSTGRESQL_HOST in 0.8 - DefaultFunc: schema.MultiEnvDefaultFunc([]string{"PGHOST", "POSTGRESQL_HOST"}, nil), + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("PGHOST", nil), Description: "Name of PostgreSQL server address to connect to", }, "port": { @@ -33,17 +32,15 @@ func Provider() terraform.ResourceProvider { DefaultFunc: schema.EnvDefaultFunc("PGDATABASE", "postgres"), }, "username": { - Type: schema.TypeString, - Optional: true, - // TODO(sean@): Remove POSTGRESQL_USER in 0.8 - DefaultFunc: schema.MultiEnvDefaultFunc([]string{"PGUSER", "POSTGRESQL_USER"}, "postgres"), + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("PGUSER", "postgres"), Description: "PostgreSQL user name to connect as", }, "password": { - Type: schema.TypeString, - Optional: true, - // TODO(sean@): Remove POSTGRESQL_PASSWORD in 0.8 - DefaultFunc: schema.MultiEnvDefaultFunc([]string{"PGPASSWORD", "POSTGRESQL_PASSWORD"}, nil), + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("PGPASSWORD", nil), Description: "Password to be used if the PostgreSQL server demands password authentication", }, "sslmode": { From ce60c4fedeb487a27d67d3371b26782917c7c01a Mon Sep 17 00:00:00 2001 From: Sean Chittenden Date: Mon, 12 Dec 2016 15:21:20 -0800 Subject: [PATCH 46/46] gofmt cleanup on imports. --- .../providers/postgresql/resource_postgresql_database_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/builtin/providers/postgresql/resource_postgresql_database_test.go b/builtin/providers/postgresql/resource_postgresql_database_test.go index a53587ae6..581c18e60 100644 --- a/builtin/providers/postgresql/resource_postgresql_database_test.go +++ b/builtin/providers/postgresql/resource_postgresql_database_test.go @@ -2,11 +2,10 @@ package postgresql import ( "database/sql" + "errors" "fmt" "testing" - "errors" - "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" )