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.