Add support for OpenStack DBs
diff --git a/openstack/db/v1/databases/doc.go b/openstack/db/v1/databases/doc.go
new file mode 100644
index 0000000..18cbec7
--- /dev/null
+++ b/openstack/db/v1/databases/doc.go
@@ -0,0 +1 @@
+package databases
diff --git a/openstack/db/v1/databases/fixtures.go b/openstack/db/v1/databases/fixtures.go
new file mode 100644
index 0000000..d03466f
--- /dev/null
+++ b/openstack/db/v1/databases/fixtures.go
@@ -0,0 +1,76 @@
+package databases
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func HandleCreateDBSuccessfully(t *testing.T, instanceID string) {
+ th.Mux.HandleFunc("/instances/"+instanceID+"/databases", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestJSONRequest(t, r, `
+{
+ "databases": [
+ {
+ "character_set": "utf8",
+ "collate": "utf8_general_ci",
+ "name": "testingdb"
+ },
+ {
+ "name": "sampledb"
+ }
+ ]
+}
+`)
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
+
+func HandleListDBsSuccessfully(t *testing.T, instanceID string) {
+ th.Mux.HandleFunc("/instances/"+instanceID+"/databases", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "databases": [
+ {
+ "name": "anotherexampledb"
+ },
+ {
+ "name": "exampledb"
+ },
+ {
+ "name": "nextround"
+ },
+ {
+ "name": "sampledb"
+ },
+ {
+ "name": "testingdb"
+ }
+ ]
+}
+`)
+ })
+}
+
+func HandleDeleteDBSuccessfully(t *testing.T, instanceID, dbName string) {
+ th.Mux.HandleFunc("/instances/"+instanceID+"/databases/"+dbName, func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
diff --git a/openstack/db/v1/databases/requests.go b/openstack/db/v1/databases/requests.go
new file mode 100644
index 0000000..300e177
--- /dev/null
+++ b/openstack/db/v1/databases/requests.go
@@ -0,0 +1,105 @@
+package databases
+
+import (
+ "fmt"
+
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+type CreateOptsBuilder interface {
+ ToDBCreateMap() (map[string]interface{}, error)
+}
+
+// DatabaseOpts is the struct responsible for configuring a database; often in
+// the context of an instance.
+type CreateOpts struct {
+ // Specifies the name of the database. Optional.
+ Name string
+
+ // Set of symbols and encodings. Optional; the default character set is utf8.
+ CharSet string
+
+ // Set of rules for comparing characters in a character set. Optional; the
+ // default value for collate is utf8_general_ci.
+ Collate string
+}
+
+func (opts CreateOpts) ToMap() (map[string]string, error) {
+ if opts.Name == "" {
+ return nil, fmt.Errorf("Name is a required field")
+ }
+ if len(opts.Name) > 64 {
+ return nil, fmt.Errorf("Name must be less than 64 chars long")
+ }
+
+ db := map[string]string{"name": opts.Name}
+
+ if opts.CharSet != "" {
+ db["character_set"] = opts.CharSet
+ }
+ if opts.Collate != "" {
+ db["collate"] = opts.Collate
+ }
+ return db, nil
+}
+
+type BatchCreateOpts []CreateOpts
+
+func (opts BatchCreateOpts) ToDBCreateMap() (map[string]interface{}, error) {
+ var dbs []map[string]string
+ for _, db := range opts {
+ dbMap, err := db.ToMap()
+ if err != nil {
+ return nil, err
+ }
+ dbs = append(dbs, dbMap)
+ }
+ return map[string]interface{}{"databases": dbs}, nil
+}
+
+func Create(client *gophercloud.ServiceClient, instanceID string, opts CreateOptsBuilder) CreateResult {
+ var res CreateResult
+
+ reqBody, err := opts.ToDBCreateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ resp, err := perigee.Request("POST", baseURL(client, instanceID), perigee.Options{
+ MoreHeaders: client.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Body,
+ OkCodes: []int{202},
+ })
+
+ res.Header = resp.HttpResponse.Header
+ res.Err = err
+
+ return res
+}
+
+func List(client *gophercloud.ServiceClient, instanceID string) pagination.Pager {
+ createPageFn := func(r pagination.PageResult) pagination.Page {
+ return DBPage{pagination.LinkedPageBase{PageResult: r}}
+ }
+
+ return pagination.NewPager(client, baseURL(client, instanceID), createPageFn)
+}
+
+func Delete(client *gophercloud.ServiceClient, instanceID, dbName string) DeleteResult {
+ var res DeleteResult
+
+ resp, err := perigee.Request("DELETE", dbURL(client, instanceID, dbName), perigee.Options{
+ MoreHeaders: client.AuthenticatedHeaders(),
+ Results: &res.Body,
+ OkCodes: []int{202},
+ })
+
+ res.Header = resp.HttpResponse.Header
+ res.Err = err
+
+ return res
+}
diff --git a/openstack/db/v1/databases/requests_test.go b/openstack/db/v1/databases/requests_test.go
new file mode 100644
index 0000000..6f5edb6
--- /dev/null
+++ b/openstack/db/v1/databases/requests_test.go
@@ -0,0 +1,71 @@
+package databases
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+const instanceID = "{instanceID}"
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ HandleCreateDBSuccessfully(t, instanceID)
+
+ opts := BatchCreateOpts{
+ CreateOpts{Name: "testingdb", CharSet: "utf8", Collate: "utf8_general_ci"},
+ CreateOpts{Name: "sampledb"},
+ }
+
+ res := Create(fake.ServiceClient(), instanceID, opts)
+ th.AssertNoErr(t, res.Err)
+}
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ HandleListDBsSuccessfully(t, instanceID)
+
+ expectedDBs := []Database{
+ Database{Name: "anotherexampledb"},
+ Database{Name: "exampledb"},
+ Database{Name: "nextround"},
+ Database{Name: "sampledb"},
+ Database{Name: "testingdb"},
+ }
+
+ pages := 0
+ err := List(fake.ServiceClient(), instanceID).EachPage(func(page pagination.Page) (bool, error) {
+ pages++
+
+ actual, err := ExtractDBs(page)
+ if err != nil {
+ return false, err
+ }
+
+ th.CheckDeepEquals(t, expectedDBs, actual)
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+
+ if pages != 1 {
+ t.Errorf("Expected 1 page, saw %d", pages)
+ }
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ HandleDeleteDBSuccessfully(t, instanceID, "{dbName}")
+
+ err := Delete(fake.ServiceClient(), instanceID, "{dbName}").ExtractErr()
+ th.AssertNoErr(t, err)
+}
diff --git a/openstack/db/v1/databases/results.go b/openstack/db/v1/databases/results.go
new file mode 100644
index 0000000..85e3363
--- /dev/null
+++ b/openstack/db/v1/databases/results.go
@@ -0,0 +1,69 @@
+package databases
+
+import (
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+type Database struct {
+ // Specifies the name of the MySQL DB.
+ Name string
+
+ // Set of symbols and encodings. The default character set is utf8.
+ CharSet string
+
+ // Set of rules for comparing characters in a character set. The default
+ // value for collate is utf8_general_ci.
+ Collate string
+}
+
+type CreateResult struct {
+ gophercloud.Result
+}
+
+// DBPage represents a single page of a paginated DB collection.
+type DBPage struct {
+ pagination.LinkedPageBase
+}
+
+// IsEmpty checks to see whether the collection is empty.
+func (page DBPage) IsEmpty() (bool, error) {
+ dbs, err := ExtractDBs(page)
+ if err != nil {
+ return true, err
+ }
+ return len(dbs) == 0, nil
+}
+
+// NextPageURL will retrieve the next page URL.
+func (page DBPage) NextPageURL() (string, error) {
+ type resp struct {
+ Links []gophercloud.Link `mapstructure:"databases_links"`
+ }
+
+ var r resp
+ err := mapstructure.Decode(page.Body, &r)
+ if err != nil {
+ return "", err
+ }
+
+ return gophercloud.ExtractNextURL(r.Links)
+}
+
+// ExtractDBs will convert a generic pagination struct into a more
+// relevant slice of DB structs.
+func ExtractDBs(page pagination.Page) ([]Database, error) {
+ casted := page.(DBPage).Body
+
+ var response struct {
+ Databases []Database `mapstructure:"databases"`
+ }
+
+ err := mapstructure.Decode(casted, &response)
+ return response.Databases, err
+}
+
+type DeleteResult struct {
+ gophercloud.ErrResult
+}
diff --git a/openstack/db/v1/databases/urls.go b/openstack/db/v1/databases/urls.go
new file mode 100644
index 0000000..027ca58
--- /dev/null
+++ b/openstack/db/v1/databases/urls.go
@@ -0,0 +1,11 @@
+package databases
+
+import "github.com/rackspace/gophercloud"
+
+func baseURL(c *gophercloud.ServiceClient, instanceID string) string {
+ return c.ServiceURL("instances", instanceID, "databases")
+}
+
+func dbURL(c *gophercloud.ServiceClient, instanceID, dbName string) string {
+ return c.ServiceURL("instances", instanceID, "databases", dbName)
+}