Skip to content

Commit

Permalink
Merge #3653: PostgreSQL provider
Browse files Browse the repository at this point in the history
  • Loading branch information
apparentlymart committed Dec 4, 2015
2 parents 8e40b6b + e1eef15 commit 29d4266
Show file tree
Hide file tree
Showing 16 changed files with 940 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

FEATURES:
* **New provider: `vcd` - VMware vCloud Director** [GH-3785]
* **New provider: `postgresql` - Create PostgreSQL databases and roles** [GH-3653]
* **New resource: `google_pubsub_topic`** [GH-3671]
* **New resource: `google_pubsub_subscription`** [GH-3671]

Expand Down
12 changes: 12 additions & 0 deletions builtin/bins/provider-postgresql/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package main

import (
"github.com/hashicorp/terraform/builtin/providers/postgresql"
"github.com/hashicorp/terraform/plugin"
)

func main() {
plugin.Serve(&plugin.ServeOpts{
ProviderFunc: postgresql.Provider,
})
}
1 change: 1 addition & 0 deletions builtin/bins/provider-postgresql/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package main
43 changes: 43 additions & 0 deletions builtin/providers/postgresql/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package postgresql

import (
"database/sql"
"fmt"
_ "github.com/lib/pq" //PostgreSQL db
)

// Config - provider config
type Config struct {
Host string
Port int
Username string
Password string
}

// Client struct holding connection string
type Client struct {
username string
connStr string
}

//NewClient returns new client config
func (c *Config) NewClient() (*Client, error) {
connStr := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=postgres", c.Host, c.Port, c.Username, c.Password)

client := Client{
connStr: connStr,
username: c.Username,
}

return &client, nil
}

//Connect will manually connect/diconnect to prevent a large number or db connections being made
func (c *Client) Connect() (*sql.DB, error) {
db, err := sql.Open("postgres", c.connStr)
if err != nil {
return nil, fmt.Errorf("Error connecting to postgresql server: %s", err)
}

return db, nil
}
63 changes: 63 additions & 0 deletions builtin/providers/postgresql/provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package postgresql

import (
"fmt"

"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
)

// Provider returns a terraform.ResourceProvider.
func Provider() terraform.ResourceProvider {
return &schema.Provider{
Schema: map[string]*schema.Schema{
"host": &schema.Schema{
Type: schema.TypeString,
Required: true,
DefaultFunc: schema.EnvDefaultFunc("POSTGRESQL_HOST", nil),
Description: "The postgresql server address",
},
"port": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Default: 5432,
Description: "The postgresql server port",
},
"username": &schema.Schema{
Type: schema.TypeString,
Required: true,
DefaultFunc: schema.EnvDefaultFunc("POSTGRESQL_USERNAME", nil),
Description: "Username for postgresql server connection",
},
"password": &schema.Schema{
Type: schema.TypeString,
Required: true,
DefaultFunc: schema.EnvDefaultFunc("POSTGRESQL_PASSWORD", nil),
Description: "Password for postgresql server connection",
},
},

ResourcesMap: map[string]*schema.Resource{
"postgresql_database": resourcePostgresqlDatabase(),
"postgresql_role": resourcePostgresqlRole(),
},

ConfigureFunc: providerConfigure,
}
}

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),
}

client, err := config.NewClient()
if err != nil {
return nil, fmt.Errorf("Error initializing Postgresql client: %s", err)
}

return client, nil
}
41 changes: 41 additions & 0 deletions builtin/providers/postgresql/provider_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package postgresql

import (
"os"
"testing"

"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
)

var testAccProviders map[string]terraform.ResourceProvider
var testAccProvider *schema.Provider

func init() {
testAccProvider = Provider().(*schema.Provider)
testAccProviders = map[string]terraform.ResourceProvider{
"postgresql": testAccProvider,
}
}

func TestProvider(t *testing.T) {
if err := Provider().(*schema.Provider).InternalValidate(); err != nil {
t.Fatalf("err: %s", err)
}
}

func TestProvider_impl(t *testing.T) {
var _ terraform.ResourceProvider = Provider()
}

func testAccPreCheck(t *testing.T) {
if v := os.Getenv("POSTGRESQL_HOST"); v == "" {
t.Fatal("POSTGRESQL_HOST must be set for acceptance tests")
}
if v := os.Getenv("POSTGRESQL_USERNAME"); v == "" {
t.Fatal("POSTGRESQL_USERNAME must be set for acceptance tests")
}
if v := os.Getenv("POSTGRESQL_PASSWORD"); v == "" {
t.Fatal("POSTGRESQL_PASSWORD must be set for acceptance tests")
}
}
160 changes: 160 additions & 0 deletions builtin/providers/postgresql/resource_postgresql_database.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package postgresql

import (
"database/sql"
"fmt"
"strings"

"github.com/hashicorp/terraform/helper/schema"
"github.com/lib/pq"
)

func resourcePostgresqlDatabase() *schema.Resource {
return &schema.Resource{
Create: resourcePostgresqlDatabaseCreate,
Read: resourcePostgresqlDatabaseRead,
Update: resourcePostgresqlDatabaseUpdate,
Delete: resourcePostgresqlDatabaseDelete,

Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"owner": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: false,
Computed: true,
},
},
}
}

func resourcePostgresqlDatabaseCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*Client)
conn, err := client.Connect()
if err != nil {
return err
}
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 = ""
}

//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
}

query := fmt.Sprintf("CREATE DATABASE %s %s", pq.QuoteIdentifier(dbName), dbOwnerCfg)
_, err = conn.Query(query)
if err != nil {
return fmt.Errorf("Error creating postgresql database %s: %s", dbName, err)
}

d.SetId(dbName)

return nil
}

func resourcePostgresqlDatabaseDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*Client)
conn, err := client.Connect()
if err != nil {
return 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
}

query := fmt.Sprintf("DROP DATABASE %s", pq.QuoteIdentifier(dbName))
_, err = conn.Query(query)
if err != nil {
return err
}

d.SetId("")

return nil
}

func resourcePostgresqlDatabaseRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*Client)
conn, err := client.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)
switch {
case err == sql.ErrNoRows:
d.SetId("")
return nil
case err != nil:
return fmt.Errorf("Error reading info about database: %s", err)
default:
d.Set("owner", owner)
return nil
}
}

func resourcePostgresqlDatabaseUpdate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*Client)
conn, err := client.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 fmt.Errorf("Error updating owner for database: %s", err)
}
}
}

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 fmt.Errorf("Error granting membership: %s", err)
}
}
return nil
}
Loading

0 comments on commit 29d4266

Please sign in to comment.