From 78949007671328714088d72fe95604dc2d90ca74 Mon Sep 17 00:00:00 2001 From: Christopher Parratto Date: Wed, 25 Jan 2017 16:56:09 -0500 Subject: [PATCH] Added support for more v3 compatible identity services. Including projects, roles and domains. --- openstack/identity/v3/domains/requests.go | 52 ++++++ .../identity/v3/domains/requests_test.go | 113 ++++++++++++ openstack/identity/v3/domains/results.go | 55 ++++++ openstack/identity/v3/domains/urls.go | 11 ++ openstack/identity/v3/groups/requests.go | 28 +++ openstack/identity/v3/groups/requests_test.go | 74 ++++++++ openstack/identity/v3/groups/results.go | 49 +++++ openstack/identity/v3/groups/urls.go | 7 + openstack/identity/v3/projects/requests.go | 81 ++++++++ .../identity/v3/projects/requests_test.go | 173 ++++++++++++++++++ openstack/identity/v3/projects/results.go | 79 ++++++++ openstack/identity/v3/projects/urls.go | 11 ++ openstack/identity/v3/roles/requests.go | 41 +++++ openstack/identity/v3/roles/requests_test.go | 116 +++++++++++- openstack/identity/v3/roles/results.go | 64 ++++++- openstack/identity/v3/roles/urls.go | 4 + 16 files changed, 956 insertions(+), 2 deletions(-) create mode 100644 openstack/identity/v3/domains/requests.go create mode 100644 openstack/identity/v3/domains/requests_test.go create mode 100644 openstack/identity/v3/domains/results.go create mode 100644 openstack/identity/v3/domains/urls.go create mode 100644 openstack/identity/v3/groups/requests.go create mode 100644 openstack/identity/v3/groups/requests_test.go create mode 100644 openstack/identity/v3/groups/results.go create mode 100644 openstack/identity/v3/groups/urls.go create mode 100644 openstack/identity/v3/projects/requests.go create mode 100644 openstack/identity/v3/projects/requests_test.go create mode 100644 openstack/identity/v3/projects/results.go create mode 100644 openstack/identity/v3/projects/urls.go diff --git a/openstack/identity/v3/domains/requests.go b/openstack/identity/v3/domains/requests.go new file mode 100644 index 00000000..6c76a830 --- /dev/null +++ b/openstack/identity/v3/domains/requests.go @@ -0,0 +1,52 @@ +package domains + +import ( + "errors" + "github.com/rackspace/gophercloud" + + "github.com/rackspace/gophercloud/pagination" +) + +// ListOpts allows you to query the List method. +type ListOpts struct { + Name string `q:"name"` + Enabled string `q:"enabled"` +} + +// List enumerates the services available to a specific user. +func List(client *gophercloud.ServiceClient, opts ListOpts) pagination.Pager { + u := listURL(client) + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return pagination.Pager{Err: err} + } + u += q.String() + createPage := func(r pagination.PageResult) pagination.Page { + return DomainPage{pagination.LinkedPageBase{PageResult: r}} + } + + return pagination.NewPager(client, u, createPage) +} + +// PairOpts allows you to pair roles, groups, on a domain +type PairOpts struct { + ID string `json:"id"` + GroupID string `json:"group_id"` + RoleID string `json:"role_id"` +} + +// Pair creates a relationship between a role, group, and domain +func Pair(client *gophercloud.ServiceClient, opts PairOpts) error { + if opts.ID == "" || opts.GroupID == "" || opts.RoleID == "" { + return errors.New("Domain, Role, and Group ids are required.") + } + + reqOpts := &gophercloud.RequestOpts{ + OkCodes: []int{204}, + MoreHeaders: map[string]string{"Content-Type": ""}, + } + + var result PairResult + _, result.Err = client.Put(pairDomainGroupAndRoleURL(client, opts.ID, opts.GroupID, opts.RoleID), nil, nil, reqOpts) + return result.Err +} diff --git a/openstack/identity/v3/domains/requests_test.go b/openstack/identity/v3/domains/requests_test.go new file mode 100644 index 00000000..1557cbd4 --- /dev/null +++ b/openstack/identity/v3/domains/requests_test.go @@ -0,0 +1,113 @@ +package domains + +import ( + "fmt" + "net/http" + "reflect" + "testing" + + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestListSinglePage(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + + testhelper.Mux.HandleFunc("/domains", func(w http.ResponseWriter, r *http.Request) { + testhelper.TestMethod(t, r, "GET") + testhelper.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ` + { + "domains": [ + { + "description": "Used for swift functional testing", + "enabled": true, + "id": "5a75994a383c449184053ff7270c4e91", + "links": { + "self": "http://example.com/identity/v3/domains/5a75994a383c449184053ff7270c4e91" + }, + "name": "swift_test" + }, + { + "description": "Owns users and tenants (i.e. projects) available on Identity API v2.", + "enabled": true, + "id": "default", + "links": { + "self": "http://example.com/identity/v3/domains/default" + }, + "name": "Default" + } + ], + "links": { + "next": null, + "previous": null, + "self": "http://example.com/identity/v3/domains" + } + } + `) + }) + + count := 0 + err := List(client.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractDomains(page) + if err != nil { + return false, err + } + + expected := []Domain{ + Domain{ + Description: "Used for swift functional testing", + ID: "5a75994a383c449184053ff7270c4e91", + Name: "swift_test", + Enabled: true, + Links: Link{Self: "http://example.com/identity/v3/domains/5a75994a383c449184053ff7270c4e91"}, + }, + Domain{ + Description: "Owns users and tenants (i.e. projects) available on Identity API v2.", + ID: "default", + Name: "Default", + Enabled: true, + Links: Link{Self: "http://example.com/identity/v3/domains/default"}, + }, + } + + if !reflect.DeepEqual(expected, actual) { + t.Errorf("Expected %#v, got %#v", expected, actual) + } + + return true, nil + }) + if err != nil { + t.Errorf("Unexpected error while paging: %v", err) + } + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestPairDomainGroupAndRole(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + + testhelper.Mux.HandleFunc("/domains/5a75994a383c449184053ff7270c4e91/groups/5a75994a383c449184053ff7270c4e92/roles/5a75994a383c449184053ff7270c4e93", func(w http.ResponseWriter, r *http.Request) { + testhelper.TestMethod(t, r, "PUT") + testhelper.TestHeader(t, r, "X-Auth-Token", client.TokenID) + w.WriteHeader(http.StatusNoContent) + }) + + pair := PairOpts{ + ID: "5a75994a383c449184053ff7270c4e91", + GroupID: "5a75994a383c449184053ff7270c4e92", + RoleID: "5a75994a383c449184053ff7270c4e93", + } + + err := Pair(client.ServiceClient(), pair) + if err != nil { + t.Fatalf("Unexpected error from Pair: %v", err) + } +} diff --git a/openstack/identity/v3/domains/results.go b/openstack/identity/v3/domains/results.go new file mode 100644 index 00000000..c52ba7dc --- /dev/null +++ b/openstack/identity/v3/domains/results.go @@ -0,0 +1,55 @@ +package domains + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + + "github.com/mitchellh/mapstructure" +) + +type commonResult struct { + gophercloud.Result +} + +// Link the object to hold a project link. +type Link struct { + Self string `json:"self,omitempty"` +} + +// Domain is main struct for holding domain attributes. +type Domain struct { + Description string `json:"description,omitempty"` + Enabled bool `json:"enabled"` + ID string `json:"id"` + Name string `json:"name"` + Links Link `json:"links"` +} + +// PairResult the object to error for failed pairs. +type PairResult struct { + commonResult +} + +// DomainPage is a single page of Domain results. +type DomainPage struct { + pagination.LinkedPageBase +} + +// IsEmpty returns true if the page contains no results. +func (p DomainPage) IsEmpty() (bool, error) { + domains, err := ExtractDomains(p) + if err != nil { + return true, err + } + return len(domains) == 0, nil +} + +// ExtractDomains extracts a slice of Domains from a Collection acquired from List. +func ExtractDomains(page pagination.Page) ([]Domain, error) { + var response struct { + Domains []Domain `mapstructure:"domains"` + } + + err := mapstructure.Decode(page.(DomainPage).Body, &response) + return response.Domains, err +} diff --git a/openstack/identity/v3/domains/urls.go b/openstack/identity/v3/domains/urls.go new file mode 100644 index 00000000..1523b0fb --- /dev/null +++ b/openstack/identity/v3/domains/urls.go @@ -0,0 +1,11 @@ +package domains + +import "github.com/rackspace/gophercloud" + +func listURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("domains") +} + +func pairDomainGroupAndRoleURL(client *gophercloud.ServiceClient, dID, gID, rID string) string { + return client.ServiceURL("domains/" + dID + "/groups/" + gID + "/roles/" + rID) +} diff --git a/openstack/identity/v3/groups/requests.go b/openstack/identity/v3/groups/requests.go new file mode 100644 index 00000000..5cf3e471 --- /dev/null +++ b/openstack/identity/v3/groups/requests.go @@ -0,0 +1,28 @@ +package groups + +import ( + "github.com/rackspace/gophercloud" + + "github.com/rackspace/gophercloud/pagination" +) + +// ListOpts allows you to query the List method. +type ListOpts struct { + Name string `q:"name"` + DomainID string `q:"domain_id"` +} + +// List enumerates the services available to a specific user. +func List(client *gophercloud.ServiceClient, opts ListOpts) pagination.Pager { + u := listURL(client) + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return pagination.Pager{Err: err} + } + u += q.String() + createPage := func(r pagination.PageResult) pagination.Page { + return GroupPage{pagination.LinkedPageBase{PageResult: r}} + } + + return pagination.NewPager(client, u, createPage) +} diff --git a/openstack/identity/v3/groups/requests_test.go b/openstack/identity/v3/groups/requests_test.go new file mode 100644 index 00000000..885365ee --- /dev/null +++ b/openstack/identity/v3/groups/requests_test.go @@ -0,0 +1,74 @@ +package groups + +import ( + "fmt" + "net/http" + "reflect" + "testing" + + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestListSinglePage(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + + testhelper.Mux.HandleFunc("/groups", func(w http.ResponseWriter, r *http.Request) { + testhelper.TestMethod(t, r, "GET") + testhelper.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ` + { + "links": { + "self": "http://example.com/identity/v3/groups", + "previous": null, + "next": null + }, + "groups": [ + { + "description": "non-admin group", + "id": "96372bbb152f475aa37e9a76a25a029c", + "links": { + "self": "http://example.com/identity/v3/groups/96372bbb152f475aa37e9a76a25a029c" + }, + "name": "nonadmins", + "domain_id": "default" + } + ] + } + `) + }) + + count := 0 + err := List(client.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractGroups(page) + if err != nil { + return false, err + } + + expected := []Group{ + Group{ + Description: "non-admin group", + Name: "nonadmins", + ID: "96372bbb152f475aa37e9a76a25a029c", + Links: Link{Self: "http://example.com/identity/v3/groups/96372bbb152f475aa37e9a76a25a029c"}, + }, + } + + if !reflect.DeepEqual(expected, actual) { + t.Errorf("Expected %#v, got %#v", expected, actual) + } + + return true, nil + }) + if err != nil { + t.Errorf("Unexpected error while paging: %v", err) + } + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} diff --git a/openstack/identity/v3/groups/results.go b/openstack/identity/v3/groups/results.go new file mode 100644 index 00000000..166a9628 --- /dev/null +++ b/openstack/identity/v3/groups/results.go @@ -0,0 +1,49 @@ +package groups + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + + "github.com/mitchellh/mapstructure" +) + +type commonResult struct { + gophercloud.Result +} + +// Link the object to hold a project link. +type Link struct { + Self string `json:"self,omitempty"` +} + +// Group is main struct for holding group attributes. +type Group struct { + Description string `json:"description"` + ID string `json:"id"` + Name string `json:"name"` + Links Link `json:"links"` +} + +// GroupPage is a single page of Group results. +type GroupPage struct { + pagination.LinkedPageBase +} + +// IsEmpty returns true if the page contains no results. +func (p GroupPage) IsEmpty() (bool, error) { + groups, err := ExtractGroups(p) + if err != nil { + return true, err + } + return len(groups) == 0, nil +} + +// ExtractGroups extracts a slice of Groups from a Collection acquired from List. +func ExtractGroups(page pagination.Page) ([]Group, error) { + var response struct { + Groups []Group `mapstructure:"groups"` + } + + err := mapstructure.Decode(page.(GroupPage).Body, &response) + return response.Groups, err +} diff --git a/openstack/identity/v3/groups/urls.go b/openstack/identity/v3/groups/urls.go new file mode 100644 index 00000000..d5547df0 --- /dev/null +++ b/openstack/identity/v3/groups/urls.go @@ -0,0 +1,7 @@ +package groups + +import "github.com/rackspace/gophercloud" + +func listURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("groups") +} diff --git a/openstack/identity/v3/projects/requests.go b/openstack/identity/v3/projects/requests.go new file mode 100644 index 00000000..389e58f4 --- /dev/null +++ b/openstack/identity/v3/projects/requests.go @@ -0,0 +1,81 @@ +package projects + +import ( + "errors" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +type response struct { + Project Project `json:"project"` +} + +// CreateOpts allows you to create a project +type CreateOpts struct { + IsDomain bool `json:"is_domain,omitempty"` + Description string `json:"description,omitempty"` + DomainID string `json:"domain_id,omitempty"` + Enabled bool `json:"enabled,omitempty"` + Name string `json:"name"` + ParentID string `json:"parent_id,omitempty"` +} + +// Create adds a new project using the provieded client. +func Create(client *gophercloud.ServiceClient, opts CreateOpts) CreateResult { + type request struct { + Project CreateOpts `json:"project"` + } + + req := request{Project: opts} + + var result CreateResult + _, result.Err = client.Post(listURL(client), req, &result.Body, nil) + return result +} + +// PairOpts allows you to pair roles, groups, on a project +type PairOpts struct { + ID string `json:"id"` + GroupID string `json:"group_id"` + RoleID string `json:"role_id"` +} + +// Pair creates a relationship between a role, group, and project +func Pair(client *gophercloud.ServiceClient, opts PairOpts) error { + if opts.ID == "" || opts.GroupID == "" || opts.RoleID == "" { + return errors.New("Project, Role, and Group ids are required.") + } + + reqOpts := &gophercloud.RequestOpts{ + OkCodes: []int{204}, + MoreHeaders: map[string]string{"Content-Type": ""}, + } + + var result PairResult + _, result.Err = client.Put(pairProjectGroupAndRoleURL(client, opts.ID, opts.GroupID, opts.RoleID), nil, nil, reqOpts) + return result.Err +} + +// ListOpts allows you to query the List method. +type ListOpts struct { + Name string `q:"name"` + DomainID string `q:"domain_id"` + ParentID string `q:"parent_id"` + Enabled bool `q:"enabled"` + IsDomain bool `q:"is_domain"` +} + +// List enumerates the projects available to a specific user. +func List(client *gophercloud.ServiceClient, opts ListOpts) pagination.Pager { + u := listURL(client) + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return pagination.Pager{Err: err} + } + u += q.String() + createPage := func(r pagination.PageResult) pagination.Page { + return ProjectPage{pagination.LinkedPageBase{PageResult: r}} + } + + return pagination.NewPager(client, u, createPage) +} diff --git a/openstack/identity/v3/projects/requests_test.go b/openstack/identity/v3/projects/requests_test.go new file mode 100644 index 00000000..a0a5a0ef --- /dev/null +++ b/openstack/identity/v3/projects/requests_test.go @@ -0,0 +1,173 @@ +package projects + +import ( + "fmt" + "net/http" + "reflect" + "testing" + + "github.com/rackspace/gophercloud/pagination" + "github.com/rackspace/gophercloud/testhelper" + "github.com/rackspace/gophercloud/testhelper/client" +) + +func TestCreateSuccessful(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + + testhelper.Mux.HandleFunc("/projects", func(w http.ResponseWriter, r *http.Request) { + testhelper.TestMethod(t, r, "POST") + testhelper.TestHeader(t, r, "X-Auth-Token", client.TokenID) + testhelper.TestJSONRequest(t, r, `{ + "project": { + "description": "My new project", + "domain_id": "default", + "enabled": true, + "is_domain": true, + "name": "myNewProject" + } + }`) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, `{ + "project": { + "description": "My new project", + "domain_id": "default", + "enabled": true, + "is_domain": false, + "name": "myNewProject", + "id": "1234567", + "links": { + "self": "http://os.test.com/v3/identity/projects/1234567" + } + } + }`) + }) + + project := CreateOpts{ + IsDomain: true, + Description: "My new project", + DomainID: "default", + Enabled: true, + Name: "myNewProject", + } + + result, err := Create(client.ServiceClient(), project).Extract() + if err != nil { + t.Fatalf("Unexpected error from Create: %v", err) + } + + if result.Description != "My new project" { + t.Errorf("Project description was unexpected [%s]", result.Description) + } +} + +func TestPairProjectGroupAndRole(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + + testhelper.Mux.HandleFunc("/projects/5a75994a383c449184053ff7270c4e91/groups/5a75994a383c449184053ff7270c4e92/roles/5a75994a383c449184053ff7270c4e93", func(w http.ResponseWriter, r *http.Request) { + testhelper.TestMethod(t, r, "PUT") + testhelper.TestHeader(t, r, "X-Auth-Token", client.TokenID) + w.WriteHeader(http.StatusNoContent) + }) + + pair := PairOpts{ + ID: "5a75994a383c449184053ff7270c4e91", + GroupID: "5a75994a383c449184053ff7270c4e92", + RoleID: "5a75994a383c449184053ff7270c4e93", + } + + err := Pair(client.ServiceClient(), pair) + if err != nil { + t.Fatalf("Unexpected error from Pair: %v", err) + } +} + +func TestListSinglePage(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + + testhelper.Mux.HandleFunc("/projects", func(w http.ResponseWriter, r *http.Request) { + testhelper.TestMethod(t, r, "GET") + testhelper.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ` + { + "links": { + "next": null, + "previous": null, + "self": "http://example.com/identity/v3/projects" + }, + "projects": [ + { + "is_domain": false, + "description": null, + "domain_id": "", + "enabled": true, + "id": "0c4e939acacf4376bdcd1129f1a054ad", + "links": { + "self": "http://example.com/identity/v3/projects/0c4e939acacf4376bdcd1129f1a054ad" + }, + "name": "admin", + "parent_id": null + }, + { + "is_domain": false, + "description": null, + "domain_id": "", + "enabled": true, + "id": "0cbd49cbf76d405d9c86562e1d579bd3", + "links": { + "self": "http://example.com/identity/v3/projects/0cbd49cbf76d405d9c86562e1d579bd3" + }, + "name": "demo", + "parent_id": null + } + ] + } + `) + }) + + count := 0 + err := List(client.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractProjects(page) + if err != nil { + return false, err + } + + expected := []Project{ + Project{ + ID: "0c4e939acacf4376bdcd1129f1a054ad", + IsDomain: false, + DomainID: "", + Enabled: true, + Name: "admin", + Links: Link{Self: "http://example.com/identity/v3/projects/0c4e939acacf4376bdcd1129f1a054ad"}, + }, + Project{ + ID: "0cbd49cbf76d405d9c86562e1d579bd3", + IsDomain: false, + DomainID: "", + Enabled: true, + Name: "demo", + Links: Link{Self: "http://example.com/identity/v3/projects/0cbd49cbf76d405d9c86562e1d579bd3"}, + }, + } + + if !reflect.DeepEqual(expected, actual) { + t.Errorf("Expected %#v, got %#v", expected, actual) + } + + return true, nil + }) + if err != nil { + t.Errorf("Unexpected error while paging: %v", err) + } + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} diff --git a/openstack/identity/v3/projects/results.go b/openstack/identity/v3/projects/results.go new file mode 100644 index 00000000..60445fc9 --- /dev/null +++ b/openstack/identity/v3/projects/results.go @@ -0,0 +1,79 @@ +package projects + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" + + "github.com/mitchellh/mapstructure" +) + +// Project the object to hold a project. +type Project struct { + ID string `json:"id"` + IsDomain bool `json:"is_domain"` + Description string `json:"description"` + DomainID string `json:"domain_id"` + Enabled bool `json:"enabled"` + Name string `json:"name"` + ParentID string `json:"parent_id"` + Links Link `json:"links"` +} + +// Link the object to hold a project link. +type Link struct { + Self string `json:"self"` +} + +type commonResult struct { + gophercloud.Result +} + +// Extract interprets a GetResult, CreateResult or UpdateResult as a concrete Service. +// An error is returned if the original call or the extraction failed. +func (r commonResult) Extract() (*Project, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + Project `json:"project"` + } + + err := mapstructure.Decode(r.Body, &res) + + return &res.Project, err +} + +// CreateResult the object to hold a project link. +type CreateResult struct { + commonResult +} + +// PairResult the object to error for failed pairs. +type PairResult struct { + commonResult +} + +// ProjectPage is a single page of Project results. +type ProjectPage struct { + pagination.LinkedPageBase +} + +// IsEmpty returns true if the page contains no results. +func (p ProjectPage) IsEmpty() (bool, error) { + projects, err := ExtractProjects(p) + if err != nil { + return true, err + } + return len(projects) == 0, nil +} + +// ExtractProjects extracts a slice of Projects from a Collection acquired from List. +func ExtractProjects(page pagination.Page) ([]Project, error) { + var response struct { + Projects []Project `mapstructure:"projects"` + } + + err := mapstructure.Decode(page.(ProjectPage).Body, &response) + return response.Projects, err +} diff --git a/openstack/identity/v3/projects/urls.go b/openstack/identity/v3/projects/urls.go new file mode 100644 index 00000000..d300ed0a --- /dev/null +++ b/openstack/identity/v3/projects/urls.go @@ -0,0 +1,11 @@ +package projects + +import "github.com/rackspace/gophercloud" + +func listURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("projects") +} + +func pairProjectGroupAndRoleURL(client *gophercloud.ServiceClient, dID, gID, rID string) string { + return client.ServiceURL("projects/" + dID + "/groups/" + gID + "/roles/" + rID) +} diff --git a/openstack/identity/v3/roles/requests.go b/openstack/identity/v3/roles/requests.go index d95c1e52..07c81bb7 100644 --- a/openstack/identity/v3/roles/requests.go +++ b/openstack/identity/v3/roles/requests.go @@ -48,3 +48,44 @@ func ListAssignments(client *gophercloud.ServiceClient, opts ListAssignmentsOpts return pagination.NewPager(client, url, createPage) } + +// CreateOpts allows you to create a role +type CreateOpts struct { + DomainID string `json:"domain_id"` + Name string `json:"name"` +} + +// Create adds a new role using the provieded client. +func Create(client *gophercloud.ServiceClient, opts CreateOpts) CreateResult { + type request struct { + Role CreateOpts `json:"role"` + } + + req := request{Role: opts} + + var result CreateResult + _, result.Err = client.Post(listURL(client), req, &result.Body, nil) + return result +} + +// ListOpts allows you to query the List method. +type ListOpts struct { + Name string `q:"name"` + DomainID string `q:"domain_id"` +} + +// List enumerates the roles available to a specific user. +func List(client *gophercloud.ServiceClient, opts ListOpts) pagination.Pager { + u := listURL(client) + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return pagination.Pager{Err: err} + } + u += q.String() + createPage := func(r pagination.PageResult) pagination.Page { + return RolePage{pagination.LinkedPageBase{PageResult: r}} + } + + return pagination.NewPager(client, u, createPage) +} + diff --git a/openstack/identity/v3/roles/requests_test.go b/openstack/identity/v3/roles/requests_test.go index d62dbff9..1ab16e54 100644 --- a/openstack/identity/v3/roles/requests_test.go +++ b/openstack/identity/v3/roles/requests_test.go @@ -11,7 +11,7 @@ import ( "github.com/rackspace/gophercloud/testhelper/client" ) -func TestListSinglePage(t *testing.T) { +func TestListSinglePageRA(t *testing.T) { testhelper.SetupHTTP() defer testhelper.TeardownHTTP() @@ -102,3 +102,117 @@ func TestListSinglePage(t *testing.T) { t.Errorf("Expected 1 page, got %d", count) } } + +func TestCreateSuccessful(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + + testhelper.Mux.HandleFunc("/roles", func(w http.ResponseWriter, r *http.Request) { + testhelper.TestMethod(t, r, "POST") + testhelper.TestHeader(t, r, "X-Auth-Token", client.TokenID) + testhelper.TestJSONRequest(t, r, `{ + "role": { + "domain_id": "92e782c4988642d783a95f4a87c3fdd7", + "name": "developer" + } + }`) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, `{ + "role": { + "domain_id": "92e782c4988642d783a95f4a87c3fdd7", + "id": "1e443fa8cee3482a8a2b6954dd5c8f12", + "links": { + "self": "http://example.com/identity/v3/roles/1e443fa8cee3482a8a2b6954dd5c8f12" + }, + "name": "developer" + } + }`) + }) + + role := CreateOpts{ + DomainID: "92e782c4988642d783a95f4a87c3fdd7", + Name: "developer", + } + + result, err := Create(client.ServiceClient(), role).Extract() + if err != nil { + t.Fatalf("Unexpected error from Create: %v", err) + } + + if result.Name != "developer" { + t.Errorf("Role name was unexpected [%s]", result.Name) + } +} + +func TestListSinglePage(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + + testhelper.Mux.HandleFunc("/roles", func(w http.ResponseWriter, r *http.Request) { + testhelper.TestMethod(t, r, "GET") + testhelper.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ` + { + "links": { + "next": null, + "previous": null, + "self": "http://example.com/identity/v3/roles" + }, + "roles": [ + { + "id": "5318e65d75574c17bf5339d3df33a5a3", + "links": { + "self": "http://example.com/identity/v3/roles/5318e65d75574c17bf5339d3df33a5a3" + }, + "name": "admin" + }, + { + "id": "9fe2ff9ee4384b1894a90878d3e92bab", + "links": { + "self": "http://example.com/identity/v3/roles/9fe2ff9ee4384b1894a90878d3e92bab" + }, + "name": "_member_" + } + ] + } + `) + }) + + count := 0 + err := List(client.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractRoles(page) + if err != nil { + return false, err + } + + expected := []Role{ + Role{ + ID: "5318e65d75574c17bf5339d3df33a5a3", + Name: "admin", + Links: Link{Self: "http://example.com/identity/v3/roles/5318e65d75574c17bf5339d3df33a5a3"}, + }, + Role{ + ID: "9fe2ff9ee4384b1894a90878d3e92bab", + Name: "_member_", + Links: Link{Self: "http://example.com/identity/v3/roles/9fe2ff9ee4384b1894a90878d3e92bab"}, + }, + } + + if !reflect.DeepEqual(expected, actual) { + t.Errorf("Expected %#v, got %#v", expected, actual) + } + + return true, nil + }) + if err != nil { + t.Errorf("Unexpected error while paging: %v", err) + } + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} diff --git a/openstack/identity/v3/roles/results.go b/openstack/identity/v3/roles/results.go index d25abd25..e40ee0ae 100644 --- a/openstack/identity/v3/roles/results.go +++ b/openstack/identity/v3/roles/results.go @@ -1,6 +1,7 @@ package roles import ( + "github.com/rackspace/gophercloud" "github.com/rackspace/gophercloud/pagination" "github.com/mitchellh/mapstructure" @@ -14,8 +15,16 @@ type RoleAssignment struct { Group Group `json:"group,omitempty"` } +// Link the object to hold a project link. +type Link struct { + Self string `json:"self,omitempty"` +} + type Role struct { - ID string `json:"id,omitempty"` + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + DomainID string `json:"domain_id,omitempty"` + Links Link `json:"links"` } type Scope struct { @@ -79,3 +88,56 @@ func ExtractRoleAssignments(page pagination.Page) ([]RoleAssignment, error) { err := mapstructure.Decode(page.(RoleAssignmentsPage).Body, &response) return response.RoleAssignments, err } + +type response struct { + Role Role `json:"role"` +} + +type commonResult struct { + gophercloud.Result +} + +// Extract interprets a CreateResult as a concrete Service. +// An error is returned if the original call or the extraction failed. +func (r commonResult) Extract() (*Role, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + Role `json:"role"` + } + + err := mapstructure.Decode(r.Body, &res) + + return &res.Role, err +} + +// CreateResult the object to hold a role link. +type CreateResult struct { + commonResult +} + +// RolePage is a single page of Role results. +type RolePage struct { + pagination.LinkedPageBase +} + +// IsEmpty returns true if the page contains no results. +func (p RolePage) IsEmpty() (bool, error) { + roles, err := ExtractRoles(p) + if err != nil { + return true, err + } + return len(roles) == 0, nil +} + +// ExtractRoles extracts a slice of Roles from a Collection acquired from List. +func ExtractRoles(page pagination.Page) ([]Role, error) { + var response struct { + Roles []Role `mapstructure:"roles"` + } + + err := mapstructure.Decode(page.(RolePage).Body, &response) + return response.Roles, err +} diff --git a/openstack/identity/v3/roles/urls.go b/openstack/identity/v3/roles/urls.go index b009340d..28480796 100644 --- a/openstack/identity/v3/roles/urls.go +++ b/openstack/identity/v3/roles/urls.go @@ -5,3 +5,7 @@ import "github.com/rackspace/gophercloud" func listAssignmentsURL(client *gophercloud.ServiceClient) string { return client.ServiceURL("role_assignments") } + +func listURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("roles") +}