OpenStack DB users
diff --git a/openstack/db/v1/users/doc.go b/openstack/db/v1/users/doc.go
new file mode 100644
index 0000000..82abcb9
--- /dev/null
+++ b/openstack/db/v1/users/doc.go
@@ -0,0 +1 @@
+package users
diff --git a/openstack/db/v1/users/fixtures.go b/openstack/db/v1/users/fixtures.go
new file mode 100644
index 0000000..6bada41
--- /dev/null
+++ b/openstack/db/v1/users/fixtures.go
@@ -0,0 +1,91 @@
+package users
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func HandleCreateUserSuccessfully(t *testing.T, instanceID string) {
+ th.Mux.HandleFunc("/instances/"+instanceID+"/users", 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, `
+{
+ "users": [
+ {
+ "databases": [
+ {
+ "name": "databaseA"
+ }
+ ],
+ "name": "dbuser3",
+ "password": "secretsecret"
+ },
+ {
+ "databases": [
+ {
+ "name": "databaseB"
+ },
+ {
+ "name": "databaseC"
+ }
+ ],
+ "name": "dbuser4",
+ "password": "secretsecret"
+ }
+ ]
+}
+`)
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
+
+func HandleListUsersSuccessfully(t *testing.T, instanceID string) {
+ th.Mux.HandleFunc("/instances/"+instanceID+"/users", 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, `
+{
+ "users": [
+ {
+ "databases": [
+ {
+ "name": "databaseA"
+ }
+ ],
+ "name": "dbuser3"
+ },
+ {
+ "databases": [
+ {
+ "name": "databaseB"
+ },
+ {
+ "name": "databaseC"
+ }
+ ],
+ "name": "dbuser4"
+ }
+ ]
+}
+`)
+ })
+}
+
+func HandleDeleteUserSuccessfully(t *testing.T, instanceID, dbName string) {
+ th.Mux.HandleFunc("/instances/"+instanceID+"/users/"+dbName, func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
diff --git a/openstack/db/v1/users/requests.go b/openstack/db/v1/users/requests.go
new file mode 100644
index 0000000..529f43f
--- /dev/null
+++ b/openstack/db/v1/users/requests.go
@@ -0,0 +1,112 @@
+package users
+
+import (
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+ db "github.com/rackspace/gophercloud/openstack/db/v1/databases"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+type CreateOptsBuilder interface {
+ ToUserCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts is the struct responsible for configuring a new user; often in the
+// context of an instance.
+type CreateOpts struct {
+ // Specifies a name for the user.
+ Name string
+
+ // Specifies a password for the user.
+ Password string
+
+ // An array of databases that this user will connect to. The `name` field is
+ // the only requirement for each option.
+ Databases db.BatchCreateOpts
+
+ // Specifies the host from which a user is allowed to connect to the database.
+ // Possible values are a string containing an IPv4 address or "%" to allow
+ // connecting from any host. Optional; the default is "%".
+ Host string
+}
+
+func (opts CreateOpts) ToMap() (map[string]interface{}, error) {
+ user := map[string]interface{}{}
+
+ if opts.Name != "" {
+ user["name"] = opts.Name
+ }
+ if opts.Password != "" {
+ user["password"] = opts.Password
+ }
+ if opts.Host != "" {
+ user["host"] = opts.Host
+ }
+
+ var dbs []map[string]string
+ for _, db := range opts.Databases {
+ dbs = append(dbs, map[string]string{"name": db.Name})
+ }
+ if len(dbs) > 0 {
+ user["databases"] = dbs
+ }
+
+ return user, nil
+}
+
+type BatchCreateOpts []CreateOpts
+
+func (opts BatchCreateOpts) ToUserCreateMap() (map[string]interface{}, error) {
+ var users []map[string]interface{}
+ for _, opt := range opts {
+ user, err := opt.ToMap()
+ if err != nil {
+ return nil, err
+ }
+ users = append(users, user)
+ }
+ return map[string]interface{}{"users": users}, nil
+}
+
+func Create(client *gophercloud.ServiceClient, instanceID string, opts CreateOptsBuilder) CreateResult {
+ var res CreateResult
+
+ reqBody, err := opts.ToUserCreateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ resp, err := perigee.Request("POST", baseURL(client, instanceID), perigee.Options{
+ MoreHeaders: client.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ 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 UserPage{pagination.LinkedPageBase{PageResult: r}}
+ }
+
+ return pagination.NewPager(client, baseURL(client, instanceID), createPageFn)
+}
+
+func Delete(client *gophercloud.ServiceClient, instanceID, userName string) DeleteResult {
+ var res DeleteResult
+
+ resp, err := perigee.Request("DELETE", userURL(client, instanceID, userName), perigee.Options{
+ MoreHeaders: client.AuthenticatedHeaders(),
+ OkCodes: []int{202},
+ })
+
+ res.Header = resp.HttpResponse.Header
+ res.Err = err
+
+ return res
+}
diff --git a/openstack/db/v1/users/requests_test.go b/openstack/db/v1/users/requests_test.go
new file mode 100644
index 0000000..00c4e58
--- /dev/null
+++ b/openstack/db/v1/users/requests_test.go
@@ -0,0 +1,93 @@
+package users
+
+import (
+ "testing"
+
+ db "github.com/rackspace/gophercloud/openstack/db/v1/databases"
+ "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()
+
+ HandleCreateUserSuccessfully(t, instanceID)
+
+ opts := BatchCreateOpts{
+ CreateOpts{
+ Databases: db.BatchCreateOpts{
+ db.CreateOpts{Name: "databaseA"},
+ },
+ Name: "dbuser3",
+ Password: "secretsecret",
+ },
+ CreateOpts{
+ Databases: db.BatchCreateOpts{
+ db.CreateOpts{Name: "databaseB"},
+ db.CreateOpts{Name: "databaseC"},
+ },
+ Name: "dbuser4",
+ Password: "secretsecret",
+ },
+ }
+
+ res := Create(fake.ServiceClient(), instanceID, opts)
+ th.AssertNoErr(t, res.Err)
+}
+
+func TestUserList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ HandleListUsersSuccessfully(t, instanceID)
+
+ expectedUsers := []User{
+ User{
+ Databases: []db.Database{
+ db.Database{Name: "databaseA"},
+ },
+ Name: "dbuser3",
+ },
+ User{
+ Databases: []db.Database{
+ db.Database{Name: "databaseB"},
+ db.Database{Name: "databaseC"},
+ },
+ Name: "dbuser4",
+ },
+ }
+
+ pages := 0
+ err := List(fake.ServiceClient(), instanceID).EachPage(func(page pagination.Page) (bool, error) {
+ pages++
+
+ actual, err := ExtractUsers(page)
+ if err != nil {
+ return false, err
+ }
+
+ th.CheckDeepEquals(t, expectedUsers, actual)
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+
+ if pages != 1 {
+ t.Errorf("Expected 1 page, saw %d", pages)
+ }
+}
+
+func TestDeleteInstance(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ HandleDeleteUserSuccessfully(t, instanceID, "{dbName}")
+
+ res := Delete(fake.ServiceClient(), instanceID, "{dbName}")
+ th.AssertNoErr(t, res.Err)
+}
diff --git a/openstack/db/v1/users/results.go b/openstack/db/v1/users/results.go
new file mode 100644
index 0000000..ce07e76
--- /dev/null
+++ b/openstack/db/v1/users/results.go
@@ -0,0 +1,71 @@
+package users
+
+import (
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ db "github.com/rackspace/gophercloud/openstack/db/v1/databases"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// User represents a database user
+type User struct {
+ // The user name
+ Name string
+
+ // The user password
+ Password string
+
+ // The databases associated with this user
+ Databases []db.Database
+}
+
+type CreateResult struct {
+ gophercloud.ErrResult
+}
+
+// UserPage represents a single page of a paginated user collection.
+type UserPage struct {
+ pagination.LinkedPageBase
+}
+
+// IsEmpty checks to see whether the collection is empty.
+func (page UserPage) IsEmpty() (bool, error) {
+ users, err := ExtractUsers(page)
+ if err != nil {
+ return true, err
+ }
+ return len(users) == 0, nil
+}
+
+// NextPageURL will retrieve the next page URL.
+func (page UserPage) NextPageURL() (string, error) {
+ type resp struct {
+ Links []gophercloud.Link `mapstructure:"users_links"`
+ }
+
+ var r resp
+ err := mapstructure.Decode(page.Body, &r)
+ if err != nil {
+ return "", err
+ }
+
+ return gophercloud.ExtractNextURL(r.Links)
+}
+
+// ExtractUsers will convert a generic pagination struct into a more
+// relevant slice of User structs.
+func ExtractUsers(page pagination.Page) ([]User, error) {
+ casted := page.(UserPage).Body
+
+ var response struct {
+ Users []User `mapstructure:"users"`
+ }
+
+ err := mapstructure.Decode(casted, &response)
+
+ return response.Users, err
+}
+
+type DeleteResult struct {
+ gophercloud.ErrResult
+}
diff --git a/openstack/db/v1/users/urls.go b/openstack/db/v1/users/urls.go
new file mode 100644
index 0000000..2a3cacd
--- /dev/null
+++ b/openstack/db/v1/users/urls.go
@@ -0,0 +1,11 @@
+package users
+
+import "github.com/rackspace/gophercloud"
+
+func baseURL(c *gophercloud.ServiceClient, instanceID string) string {
+ return c.ServiceURL("instances", instanceID, "users")
+}
+
+func userURL(c *gophercloud.ServiceClient, instanceID, userName string) string {
+ return c.ServiceURL("instances", instanceID, "users", userName)
+}