postgresql provider with "database" and "role" resources

This commit is contained in:
Adrian Chelaru 2015-10-27 12:04:19 +02:00 committed by Martin Atkins
parent 8e40b6b855
commit e1eef15646
15 changed files with 939 additions and 0 deletions

View File

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

View File

@ -0,0 +1 @@
package main

View File

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

View File

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

View File

@ -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")
}
}

View File

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

View File

@ -0,0 +1,144 @@
package postgresql
import (
"database/sql"
"fmt"
"testing"
"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{
resource.TestStep{
Config: testAccPostgresqlDatabaseConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckPostgresqlDatabaseExists("postgresql_database.mydb", "myrole"),
resource.TestCheckResourceAttr(
"postgresql_database.mydb", "name", "mydb"),
resource.TestCheckResourceAttr(
"postgresql_database.mydb", "owner", "myrole"),
),
},
},
})
}
func TestAccPostgresqlDatabase_DefaultOwner(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckPostgresqlDatabaseDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccPostgresqlDatabaseConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckPostgresqlDatabaseExists("postgresql_database.mydb_default_owner", ""),
resource.TestCheckResourceAttr(
"postgresql_database.mydb_default_owner", "name", "mydb_default_owner"),
),
},
},
})
}
func testAccCheckPostgresqlDatabaseDestroy(s *terraform.State) error {
client := testAccProvider.Meta().(*Client)
for _, rs := range s.RootModule().Resources {
if rs.Type != "postgresql_database" {
continue
}
exists, err := checkDatabaseExists(client, rs.Primary.ID)
if err != nil {
return fmt.Errorf("Error checking db %s", err)
}
if exists {
return fmt.Errorf("Db still exists after destroy")
}
}
return nil
}
func testAccCheckPostgresqlDatabaseExists(n string, owner 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")
}
actualOwner := rs.Primary.Attributes["owner"]
if actualOwner != owner {
return fmt.Errorf("Wrong owner for db expected %s got %s", owner, actualOwner)
}
client := testAccProvider.Meta().(*Client)
exists, err := checkDatabaseExists(client, rs.Primary.ID)
if err != nil {
return fmt.Errorf("Error checking db %s", err)
}
if !exists {
return fmt.Errorf("Db not found")
}
return nil
}
}
func checkDatabaseExists(client *Client, dbName string) (bool, error) {
conn, err := client.Connect()
if err != nil {
return false, err
}
defer conn.Close()
var _rez int
err = conn.QueryRow("SELECT 1 from pg_database d WHERE datname=$1", dbName).Scan(&_rez)
switch {
case err == sql.ErrNoRows:
return false, nil
case err != nil:
return false, fmt.Errorf("Error reading info about database: %s", err)
default:
return true, nil
}
}
var testAccPostgresqlDatabaseConfig = `
resource "postgresql_role" "myrole" {
name = "myrole"
login = true
}
resource "postgresql_database" "mydb" {
name = "mydb"
owner = "${postgresql_role.myrole.name}"
}
resource "postgresql_database" "mydb2" {
name = "mydb2"
owner = "${postgresql_role.myrole.name}"
}
resource "postgresql_database" "mydb_default_owner" {
name = "mydb_default_owner"
}
`

View File

@ -0,0 +1,179 @@
package postgresql
import (
"database/sql"
"fmt"
"github.com/hashicorp/terraform/helper/schema"
"github.com/lib/pq"
)
func resourcePostgresqlRole() *schema.Resource {
return &schema.Resource{
Create: resourcePostgresqlRoleCreate,
Read: resourcePostgresqlRoleRead,
Update: resourcePostgresqlRoleUpdate,
Delete: resourcePostgresqlRoleDelete,
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"login": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
ForceNew: false,
Default: false,
},
"password": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: false,
},
"encrypted": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
ForceNew: false,
Default: false,
},
},
}
}
func resourcePostgresqlRoleCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*Client)
conn, err := client.Connect()
if err != nil {
return err
}
defer conn.Close()
roleName := d.Get("name").(string)
loginAttr := getLoginStr(d.Get("login").(bool))
password := d.Get("password").(string)
encryptedCfg := getEncryptedStr(d.Get("encrypted").(bool))
query := fmt.Sprintf("CREATE ROLE %s %s %s PASSWORD '%s'", pq.QuoteIdentifier(roleName), loginAttr, encryptedCfg, password)
_, err = conn.Query(query)
if err != nil {
return fmt.Errorf("Error creating role: %s", err)
}
d.SetId(roleName)
return nil
}
func resourcePostgresqlRoleDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*Client)
conn, err := client.Connect()
if err != nil {
return err
}
defer conn.Close()
roleName := d.Get("name").(string)
query := fmt.Sprintf("DROP ROLE %s", pq.QuoteIdentifier(roleName))
_, err = conn.Query(query)
if err != nil {
return err
}
d.SetId("")
return nil
}
func resourcePostgresqlRoleRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*Client)
conn, err := client.Connect()
if err != nil {
return err
}
defer conn.Close()
roleName := d.Get("name").(string)
var canLogin bool
err = conn.QueryRow("select rolcanlogin from pg_roles where rolname=$1", roleName).Scan(&canLogin)
switch {
case err == sql.ErrNoRows:
d.SetId("")
return nil
case err != nil:
return fmt.Errorf("Error reading info about role: %s", err)
default:
d.Set("login", canLogin)
return nil
}
}
func resourcePostgresqlRoleUpdate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*Client)
conn, err := client.Connect()
if err != nil {
return err
}
defer conn.Close()
d.Partial(true)
roleName := d.Get("name").(string)
if d.HasChange("login") {
loginAttr := getLoginStr(d.Get("login").(bool))
query := fmt.Sprintf("ALTER ROLE %s %s", pq.QuoteIdentifier(roleName), pq.QuoteIdentifier(loginAttr))
_, err := conn.Query(query)
if err != nil {
return fmt.Errorf("Error updating login attribute for role: %s", err)
}
d.SetPartial("login")
}
password := d.Get("password").(string)
if d.HasChange("password") {
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 fmt.Errorf("Error updating password attribute for role: %s", err)
}
d.SetPartial("password")
}
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 fmt.Errorf("Error updating encrypted attribute for role: %s", err)
}
d.SetPartial("encrypted")
}
d.Partial(false)
return resourcePostgresqlRoleRead(d, meta)
}
func getLoginStr(canLogin bool) string {
if canLogin {
return "login"
}
return "nologin"
}
func getEncryptedStr(isEncrypted bool) string {
if isEncrypted {
return "encrypted"
}
return "unencrypted"
}

View File

@ -0,0 +1,132 @@
package postgresql
import (
"database/sql"
"fmt"
"testing"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestAccPostgresqlRole_Basic(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckPostgresqlRoleDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccPostgresqlRoleConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckPostgresqlRoleExists("postgresql_role.myrole2", "true"),
resource.TestCheckResourceAttr(
"postgresql_role.myrole2", "name", "myrole2"),
resource.TestCheckResourceAttr(
"postgresql_role.myrole2", "login", "true"),
),
},
},
})
}
func testAccCheckPostgresqlRoleDestroy(s *terraform.State) error {
client := testAccProvider.Meta().(*Client)
for _, rs := range s.RootModule().Resources {
if rs.Type != "postgresql_role" {
continue
}
exists, err := checkRoleExists(client, rs.Primary.ID)
if err != nil {
return fmt.Errorf("Error checking role %s", err)
}
if exists {
return fmt.Errorf("Role still exists after destroy")
}
}
return nil
}
func testAccCheckPostgresqlRoleExists(n string, canLogin 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")
}
actualCanLogin := rs.Primary.Attributes["login"]
if actualCanLogin != canLogin {
return fmt.Errorf("Wrong value for login expected %s got %s", canLogin, actualCanLogin)
}
client := testAccProvider.Meta().(*Client)
exists, err := checkRoleExists(client, rs.Primary.ID)
if err != nil {
return fmt.Errorf("Error checking role %s", err)
}
if !exists {
return fmt.Errorf("Role not found")
}
return nil
}
}
func checkRoleExists(client *Client, roleName string) (bool, error) {
conn, err := client.Connect()
if err != nil {
return false, err
}
defer conn.Close()
var _rez int
err = conn.QueryRow("SELECT 1 from pg_roles d WHERE rolname=$1", roleName).Scan(&_rez)
switch {
case err == sql.ErrNoRows:
return false, nil
case err != nil:
return false, fmt.Errorf("Error reading info about role: %s", err)
default:
return true, nil
}
}
var testAccPostgresqlRoleConfig = `
resource "postgresql_role" "myrole2" {
name = "myrole2"
login = true
}
resource "postgresql_role" "role_with_pwd" {
name = "role_with_pwd"
login = true
password = "mypass"
}
resource "postgresql_role" "role_with_pwd_encr" {
name = "role_with_pwd_encr"
login = true
password = "mypass"
encrypted = true
}
resource "postgresql_role" "role_with_pwd_no_login" {
name = "role_with_pwd_no_login"
password = "mypass"
}
resource "postgresql_role" "role_simple" {
name = "role_simple"
}
`

View File

@ -21,6 +21,7 @@ body.layout-heroku,
body.layout-mailgun,
body.layout-openstack,
body.layout-packet,
body.layout-postgresql,
body.layout-rundeck,
body.layout-statuscake,
body.layout-template,

View File

@ -0,0 +1,63 @@
---
layout: "postgresql"
page_title: "Provider: PostgreSQL"
sidebar_current: "docs-postgresql-index"
description: |-
A provider for PostgreSQL Server.
---
# PostgreSQL Provider
The PostgreSQL provider gives the ability to deploy and configure resources in a PostgreSQL server.
Use the navigation to the left to read about the available resources.
## Usage
```
provider "postgresql" {
host = "postgres_server_ip"
port = 5432
username = "postgres_user"
password = "postgres_password"
}
```
Configuring multiple servers can be done by specifying the alias option.
```
provider "postgresql" {
alias = "pg1"
host = "postgres_server_ip1"
username = "postgres_user1"
password = "postgres_password1"
}
provider "postgresql" {
alias = "pg2"
host = "postgres_server_ip2"
username = "postgres_user2"
password = "postgres_password2"
}
resource "postgresql_database" "my_db1" {
provider = "postgresql.pg1"
name = "my_db1"
}
resource "postgresql_database" "my_db2" {
provider = "postgresql.pg2"
name = "my_db2"
}
```
## Argument Reference
The following arguments are supported:
* `host` - (Required) The address for the postgresql server connection.
* `port` - (Optional) The port for the postgresql server connection. (Default 5432)
* `username` - (Required) Username for the server connection.
* `password` - (Optional) Password for the server connection.

View File

@ -0,0 +1,30 @@
---
layout: "postgresql"
page_title: "PostgreSQL: postgresql_database"
sidebar_current: "docs-postgresql-resource-postgresql_database"
description: |-
Creates and manages a database on a PostgreSQL server.
---
# postgresql\_database
The ``postgresql_database`` resource creates and manages a database on a PostgreSQL
server.
## Usage
```
resource "postgresql_database" "my_db" {
name = "my_db"
owner = "my_role
}
```
## Argument Reference
* `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.

View File

@ -0,0 +1,37 @@
---
layout: "postgresql"
page_title: "PostgreSQL: postgresql_role"
sidebar_current: "docs-postgresql-resource-postgresql_role"
description: |-
Creates and manages a database on a PostgreSQL server.
---
# postgresql\_role
The ``postgresql_role`` resource creates and manages a role on a PostgreSQL
server.
## Usage
```
resource "postgresql_role" "my_role" {
name = "my_role"
login = true
password = "mypass"
encrypted = true
}
```
## Argument Reference
* `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.
* `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.
* `encrypted` - (Optional) Corresponds to ENCRYPTED, UNENCRYPTED in PostgreSQL. This controls whether the password is stored encrypted in the system catalogs. Default is false.

View File

@ -181,6 +181,10 @@
<a href="/docs/providers/packet/index.html">Packet</a>
</li>
<li<%= sidebar_current("docs-providers-postgresql") %>>
<a href="/docs/providers/postgresql/index.html">PostgreSQL</a>
</li>
<li<%= sidebar_current("docs-providers-rundeck") %>>
<a href="/docs/providers/rundeck/index.html">Rundeck</a>
</li>

View File

@ -0,0 +1,29 @@
<% wrap_layout :inner do %>
<% content_for :sidebar do %>
<div class="docs-sidebar hidden-print affix-top" role="complementary">
<ul class="nav docs-sidenav">
<li<%= sidebar_current("docs-home") %>>
<a href="/docs/providers/index.html">&laquo; Documentation Home</a>
</li>
<li<%= sidebar_current("docs-postgresql-index") %>>
<a href="/docs/providers/postgresql/index.html">PostgreSQL Provider</a>
</li>
<li<%= sidebar_current(/^docs-postgresql-resource/) %>>
<a href="#">Resources</a>
<ul class="nav nav-visible">
<li<%= sidebar_current("docs-postgresql-resource-postgresql_database") %>>
<a href="/docs/providers/postgresql/r/postgresql_database.html">postgresql_database</a>
</li>
<li<%= sidebar_current("docs-postgresql-resource-postgresql_role") %>>
<a href="/docs/providers/postgresql/r/postgresql_role.html">postgresql_role</a>
</li>
</ul>
</li>
</ul>
</div>
<% end %>
<%= yield %>
<% end %>