From 57f3e21f0edf44230d918a0dbf0d2f50af05f1e5 Mon Sep 17 00:00:00 2001 From: Krzysztof Kwapisiewicz Date: Wed, 5 Aug 2015 13:34:44 +0200 Subject: [PATCH 01/10] Add keystone v3 projects basic CRUD --- openstack/identity/v3/projects/requests.go | 115 ++++++++ .../identity/v3/projects/requests_test.go | 274 ++++++++++++++++++ openstack/identity/v3/projects/results.go | 72 +++++ openstack/identity/v3/projects/urls.go | 11 + openstack/identity/v3/projects/urls_test.go | 23 ++ 5 files changed, 495 insertions(+) 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 create mode 100644 openstack/identity/v3/projects/urls_test.go diff --git a/openstack/identity/v3/projects/requests.go b/openstack/identity/v3/projects/requests.go new file mode 100644 index 00000000..b9182ebd --- /dev/null +++ b/openstack/identity/v3/projects/requests.go @@ -0,0 +1,115 @@ +package projects + +import ( + "github.com/racker/perigee" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +type project struct { + DomainID string `json:"domain_id"` + ParentID string `json:"parent_id"` + Enabled bool `json:"enabled"` + Name string `json:"name"` + Description string `json:"description"` +} + +type ProjectOpts struct { + DomainID string + ParentID string + Name string + Enabled bool + Description string +} + +func Create(client *gophercloud.ServiceClient, opts ProjectOpts) CreateResult { + type request struct { + Project project `json:"project"` + } + + reqBody := request{ + Project: project{ + DomainID: opts.DomainID, + ParentID: opts.ParentID, + Name: opts.Name, + Enabled: opts.Enabled, + Description: opts.Description, + }, + } + var result CreateResult + _, result.Err = perigee.Request("POST", listURL(client), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &result.Body, + OkCodes: []int{201}, + }) + return result +} + +type ListOpts struct { + DomainID string `q:"domain_id"` + ParentID string `q:"parent_id"` + Name string `q:"name"` + Enabled bool `q:"enabled"` + Page int `q:"page"` + PerPage int `q:"per_page"` +} + +func List(client *gophercloud.ServiceClient, opts ListOpts) pagination.Pager { + url := listURL(client) + query, err := gophercloud.BuildQueryString(opts) + if err != nil { + return pagination.Pager{Err: err} + } + + url += query.String() + createPage := func(r pagination.PageResult) pagination.Page { + return ProjectPage{pagination.LinkedPageBase{PageResult: r}} + } + + return pagination.NewPager(client, url, createPage) +} + +func Get(client *gophercloud.ServiceClient, projectID string) GetResult { + var result GetResult + _, result.Err = perigee.Request("GET", projectURL(client, projectID), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + Results: &result.Body, + OkCodes: []int{200}, + }) + return result +} + +func Update(client *gophercloud.ServiceClient, projectID string, opts ProjectOpts) UpdateResult { + type request struct { + Project project `json:"project"` + } + + reqBody := request{ + Project: project{ + DomainID: opts.DomainID, + ParentID: opts.ParentID, + Name: opts.Name, + Enabled: opts.Enabled, + Description: opts.Description, + }, + } + var result UpdateResult + _, result.Err = perigee.Request("PUT", projectURL(client, projectID), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + ReqBody: &reqBody, + Results: &result.Body, + OkCodes: []int{201}, + }) + return result +} + +func Delete(client *gophercloud.ServiceClient, projectID string) DeleteResult { + var result DeleteResult + _, result.Err = perigee.Request("DELETE", projectURL(client, projectID), perigee.Options{ + MoreHeaders: client.AuthenticatedHeaders(), + Results: &result.Body, + OkCodes: []int{204}, + }) + return result +} diff --git a/openstack/identity/v3/projects/requests_test.go b/openstack/identity/v3/projects/requests_test.go new file mode 100644 index 00000000..4762cb97 --- /dev/null +++ b/openstack/identity/v3/projects/requests_test.go @@ -0,0 +1,274 @@ +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": { + "domain_id": "1789d1", + "parent_id": "123c56", + "enabled": true, + "name": "Test Group", + "description": "My new project" + } + }`) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, `{ + "project": { + "domain_id": "1789d1", + "parent_id": "123c56", + "enabled": true, + "id": "263fd9", + "name": "Test Group", + "description": "My new project" + } + }`) + }) + + opts := ProjectOpts{ + DomainID: "1789d1", + ParentID: "123c56", + Enabled: true, + Name: "Test Group", + Description: "My new project", + } + result, err := Create(client.ServiceClient(), opts).Extract() + if err != nil { + t.Fatalf("Unexpected error from Create: %v", err) + } + + if result.ID != "263fd9" { + t.Errorf("Project id was unexpected [%s]", result.ID) + } + if result.DomainID != "1789d1" { + t.Errorf("Project domain_id was unexpected [%s]", result.DomainID) + } + if result.ParentID != "123c56" { + t.Errorf("Project parent_id was unexpected [%s]", result.ParentID) + } + if !result.Enabled { + t.Errorf("Project enabled was unexpected [%s]", result.Enabled) + } + if result.Name != "Test Group" { + t.Errorf("Project name was unexpected [%s]", result.Name) + } + if result.Description != "My new project" { + t.Errorf("Project description was unexpected [%s]", result.Description) + } +} + +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 + }, + "projects": [ + { + "domain_id": "1789d1", + "parent_id": "123c56", + "enabled": true, + "id": "263fd9", + "name": "Test Group" + }, + { + "domain_id": "1789d1", + "parent_id": "123c56", + "enabled": true, + "id": "50ef01", + "name": "Build Group" + } + ] + } + `) + }) + + 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{ + DomainID: "1789d1", + ParentID: "123c56", + Enabled: true, + ID: "263fd9", + Name: "Test Group", + }, + Project{ + DomainID: "1789d1", + ParentID: "123c56", + Enabled: true, + ID: "50ef01", + Name: "Build Group", + }, + } + + 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 TestGetSuccessful(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + + testhelper.Mux.HandleFunc("/projects/263fd9", 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, ` + { + "project": { + "domain_id": "1789d1", + "parent_id": "123c56", + "enabled": true, + "id": "263fd9", + "name": "Test Group", + "description": "My new project" + } + } + `) + }) + + result, err := Get(client.ServiceClient(), "263fd9").Extract() + if err != nil { + t.Fatalf("Error fetching service information: %v", err) + } + if result.ID != "263fd9" { + t.Errorf("Project id was unexpected [%s]", result.ID) + } + if result.DomainID != "1789d1" { + t.Errorf("Project domain_id was unexpected [%s]", result.DomainID) + } + if result.ParentID != "123c56" { + t.Errorf("Project parent_id was unexpected [%s]", result.ParentID) + } + if !result.Enabled { + t.Errorf("Project enabled was unexpected [%v]", result.Enabled) + } + if result.Name != "Test Group" { + t.Errorf("Project name was unexpected [%s]", result.Name) + } + if result.Description != "My new project" { + t.Errorf("Project description was unexpected [%s]", result.Description) + } +} + +func TestUpdateSuccessful(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + + testhelper.Mux.HandleFunc("/projects/263fd9", func(w http.ResponseWriter, r *http.Request) { + testhelper.TestMethod(t, r, "PUT") + testhelper.TestHeader(t, r, "X-Auth-Token", client.TokenID) + testhelper.TestJSONRequest(t, r, `{ + "project": { + "domain_id": "1789d1", + "parent_id": "123c56", + "enabled": true, + "name": "Test Group", + "description": "My new project" + } + }`) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, `{ + "project": { + "domain_id": "1789d1", + "parent_id": "123c56", + "enabled": true, + "id": "263fd9", + "name": "Test Group", + "description": "My new project" + } + }`) + }) + + opts := ProjectOpts{ + DomainID: "1789d1", + ParentID: "123c56", + Enabled: true, + Name: "Test Group", + Description: "My new project", + } + result, err := Update(client.ServiceClient(), "263fd9", opts).Extract() + if err != nil { + t.Fatalf("Unexpected error from Create: %v", err) + } + + if result.ID != "263fd9" { + t.Errorf("Project id was unexpected [%s]", result.ID) + } + if result.DomainID != "1789d1" { + t.Errorf("Project domain_id was unexpected [%s]", result.DomainID) + } + if result.ParentID != "123c56" { + t.Errorf("Project parent_id was unexpected [%s]", result.ParentID) + } + if !result.Enabled { + t.Errorf("Project enabled was unexpected [%s]", result.Enabled) + } + if result.Name != "Test Group" { + t.Errorf("Project name was unexpected [%s]", result.Name) + } + if result.Description != "My new project" { + t.Errorf("Project description was unexpected [%s]", result.Description) + } +} + +func TestDeleteSuccessful(t *testing.T) { + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + + testhelper.Mux.HandleFunc("/projects/263fd9", func(w http.ResponseWriter, r *http.Request) { + testhelper.TestMethod(t, r, "DELETE") + testhelper.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.WriteHeader(http.StatusNoContent) + }) + + res := Delete(client.ServiceClient(), "263fd9") + testhelper.AssertNoErr(t, res.Err) +} diff --git a/openstack/identity/v3/projects/results.go b/openstack/identity/v3/projects/results.go new file mode 100644 index 00000000..7c4b7102 --- /dev/null +++ b/openstack/identity/v3/projects/results.go @@ -0,0 +1,72 @@ +package projects + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +type commonResult struct { + gophercloud.Result +} + +func (r commonResult) Extract() (*Project, error) { + if r.Err != nil { + return nil, r.Err + } + + var response struct { + Project `json:"project"` + } + + err := mapstructure.Decode(r.Body, &response) + + return &response.Project, err +} + +type CreateResult struct { + commonResult +} + +type GetResult struct { + commonResult +} + +type UpdateResult struct { + commonResult +} + +type DeleteResult struct { + commonResult +} + +type Project struct { + DomainID string `mapstructure:"domain_id" json:"domain_id"` + ParentID string `mapstructure:"parent_id" json:"parent_id"` + Enabled bool `mapstructure:"enabled" json:"enabled"` + ID string `mapstructure:"id" json:"id"` + Name string `mapstructure:"name" json:"name"` + Description string `mapstructure:"description" json:"description"` +} + +type ProjectPage struct { + pagination.LinkedPageBase +} + +func (p ProjectPage) IsEmpty() (bool, error) { + projects, err := ExtractProjects(p) + if err != nil { + return true, err + } + return len(projects) == 0, nil +} + +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..51d3ab79 --- /dev/null +++ b/openstack/identity/v3/projects/urls.go @@ -0,0 +1,11 @@ +package projects + +import "github.com/rackspace/gophercloud" + +func listURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("projects") +} + +func projectURL(client *gophercloud.ServiceClient, projectID string) string { + return client.ServiceURL("projects", projectID) +} diff --git a/openstack/identity/v3/projects/urls_test.go b/openstack/identity/v3/projects/urls_test.go new file mode 100644 index 00000000..87d30517 --- /dev/null +++ b/openstack/identity/v3/projects/urls_test.go @@ -0,0 +1,23 @@ +package projects + +import ( + "testing" + + "github.com/rackspace/gophercloud" +) + +func TestListURL(t *testing.T) { + client := gophercloud.ServiceClient{Endpoint: "http://localhost:5000/v3/"} + url := listURL(&client) + if url != "http://localhost:5000/v3/projects" { + t.Errorf("Unexpected project URL generated: [%s]", url) + } +} + +func TestProjectURL(t *testing.T) { + client := gophercloud.ServiceClient{Endpoint: "http://localhost:5000/v3/"} + url := projectURL(&client, "12345") + if url != "http://localhost:5000/v3/projects/12345" { + t.Errorf("Unexpected project URL generated: [%s]", url) + } +} From dabf0ef0e6d27345e974129398e8094596778cba Mon Sep 17 00:00:00 2001 From: Krzysztof Kwapisiewicz Date: Wed, 5 Aug 2015 15:09:40 +0200 Subject: [PATCH 02/10] Add acceptance test --- .../openstack/identity/v3/project_test.go | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 acceptance/openstack/identity/v3/project_test.go diff --git a/acceptance/openstack/identity/v3/project_test.go b/acceptance/openstack/identity/v3/project_test.go new file mode 100644 index 00000000..9b21151c --- /dev/null +++ b/acceptance/openstack/identity/v3/project_test.go @@ -0,0 +1,71 @@ +// +build acceptance + +package v3 + +import ( + "testing" + + "github.com/rackspace/gophercloud/openstack/identity/v3/projects" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestProjectCRUDOperations(t *testing.T) { + serviceClient := createAuthenticatedClient(t) + if serviceClient == nil { + return + } + + // Create project + opts := projects.ProjectOpts{ + Enabled: true, + Name: "Test project", + Description: "This is test project", + } + project, err := projects.Create(serviceClient, opts).Extract() + th.AssertNoErr(t, err) + defer projects.Delete(serviceClient, project.ID) + th.AssertEquals(t, project.Enabled, true) + th.AssertEquals(t, project.Name, "Test project") + th.AssertEquals(t, project.Description, "This is test project") + + // List projects + pager := projects.List(serviceClient, projects.ListOpts{}) + err = pager.EachPage(func(page pagination.Page) (bool, error) { + t.Logf("--- Page ---") + + projectList, err := projects.ExtractProjects(page) + th.AssertNoErr(t, err) + + for _, p := range projectList { + t.Logf("Project: ID [%s] Name [%s] Is enabled? [%s]", + p.ID, p.Name, p.Enabled) + } + + return true, nil + }) + th.CheckNoErr(t, err) + projectID := project.ID + + // Get a project + if projectID == "" { + t.Fatalf("In order to retrieve a project, the ProjectID must be set") + } + project, err = projects.Get(serviceClient, projectID).Extract() + th.AssertNoErr(t, err) + th.AssertEquals(t, project.ID, projectID) + th.AssertEquals(t, project.DomainID, "") + th.AssertEquals(t, project.ParentID, "") + th.AssertEquals(t, project.Enabled, true) + th.AssertEquals(t, project.Name, "Test project") + th.AssertEquals(t, project.Description, "This is test project") + + // Update project + project, err = projects.Update(serviceClient, projectID, projects.ProjectOpts{Name: "New test project name"}).Extract() + th.AssertNoErr(t, err) + th.AssertEquals(t, project.Name, "New test project name") + + // Delete project + res := projects.Delete(serviceClient, projectID) + th.AssertNoErr(t, res.Err) +} From 188b1ed405b7b46c67c6c32a05059c79af9b8323 Mon Sep 17 00:00:00 2001 From: Krzysztof Kwapisiewicz Date: Wed, 5 Aug 2015 15:34:57 +0200 Subject: [PATCH 03/10] Go vet & lint messages fix --- openstack/identity/v3/projects/requests.go | 7 +++++++ openstack/identity/v3/projects/requests_test.go | 4 ++-- openstack/identity/v3/projects/results.go | 8 ++++++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/openstack/identity/v3/projects/requests.go b/openstack/identity/v3/projects/requests.go index b9182ebd..3a79db64 100644 --- a/openstack/identity/v3/projects/requests.go +++ b/openstack/identity/v3/projects/requests.go @@ -14,6 +14,7 @@ type project struct { Description string `json:"description"` } +// ProjectOpts Options for project create & update type ProjectOpts struct { DomainID string ParentID string @@ -22,6 +23,7 @@ type ProjectOpts struct { Description string } +// Create Creates project func Create(client *gophercloud.ServiceClient, opts ProjectOpts) CreateResult { type request struct { Project project `json:"project"` @@ -46,6 +48,7 @@ func Create(client *gophercloud.ServiceClient, opts ProjectOpts) CreateResult { return result } +// ListOpts Options for listing projects type ListOpts struct { DomainID string `q:"domain_id"` ParentID string `q:"parent_id"` @@ -55,6 +58,7 @@ type ListOpts struct { PerPage int `q:"per_page"` } +// List Lists projects func List(client *gophercloud.ServiceClient, opts ListOpts) pagination.Pager { url := listURL(client) query, err := gophercloud.BuildQueryString(opts) @@ -70,6 +74,7 @@ func List(client *gophercloud.ServiceClient, opts ListOpts) pagination.Pager { return pagination.NewPager(client, url, createPage) } +// Get Shows project details func Get(client *gophercloud.ServiceClient, projectID string) GetResult { var result GetResult _, result.Err = perigee.Request("GET", projectURL(client, projectID), perigee.Options{ @@ -80,6 +85,7 @@ func Get(client *gophercloud.ServiceClient, projectID string) GetResult { return result } +// Update Updates project information func Update(client *gophercloud.ServiceClient, projectID string, opts ProjectOpts) UpdateResult { type request struct { Project project `json:"project"` @@ -104,6 +110,7 @@ func Update(client *gophercloud.ServiceClient, projectID string, opts ProjectOpt return result } +// Delete Deletes project func Delete(client *gophercloud.ServiceClient, projectID string) DeleteResult { var result DeleteResult _, result.Err = perigee.Request("DELETE", projectURL(client, projectID), perigee.Options{ diff --git a/openstack/identity/v3/projects/requests_test.go b/openstack/identity/v3/projects/requests_test.go index 4762cb97..d33e1eb6 100644 --- a/openstack/identity/v3/projects/requests_test.go +++ b/openstack/identity/v3/projects/requests_test.go @@ -64,7 +64,7 @@ func TestCreateSuccessful(t *testing.T) { t.Errorf("Project parent_id was unexpected [%s]", result.ParentID) } if !result.Enabled { - t.Errorf("Project enabled was unexpected [%s]", result.Enabled) + t.Errorf("Project enabled was unexpected [%v]", result.Enabled) } if result.Name != "Test Group" { t.Errorf("Project name was unexpected [%s]", result.Name) @@ -248,7 +248,7 @@ func TestUpdateSuccessful(t *testing.T) { t.Errorf("Project parent_id was unexpected [%s]", result.ParentID) } if !result.Enabled { - t.Errorf("Project enabled was unexpected [%s]", result.Enabled) + t.Errorf("Project enabled was unexpected [%v]", result.Enabled) } if result.Name != "Test Group" { t.Errorf("Project name was unexpected [%s]", result.Name) diff --git a/openstack/identity/v3/projects/results.go b/openstack/identity/v3/projects/results.go index 7c4b7102..a792c7df 100644 --- a/openstack/identity/v3/projects/results.go +++ b/openstack/identity/v3/projects/results.go @@ -24,22 +24,27 @@ func (r commonResult) Extract() (*Project, error) { return &response.Project, err } +// CreateResult Project create result type CreateResult struct { commonResult } +// GetResult Project details result type GetResult struct { commonResult } +// UpdateResult Project update result type UpdateResult struct { commonResult } +// DeleteResult Project delete result type DeleteResult struct { commonResult } +// Project Project struct type Project struct { DomainID string `mapstructure:"domain_id" json:"domain_id"` ParentID string `mapstructure:"parent_id" json:"parent_id"` @@ -49,10 +54,12 @@ type Project struct { Description string `mapstructure:"description" json:"description"` } +// ProjectPage Page containing projects type ProjectPage struct { pagination.LinkedPageBase } +// IsEmpty checks if projects page is empty func (p ProjectPage) IsEmpty() (bool, error) { projects, err := ExtractProjects(p) if err != nil { @@ -61,6 +68,7 @@ func (p ProjectPage) IsEmpty() (bool, error) { return len(projects) == 0, nil } +// ExtractProjects extracts projects list from response func ExtractProjects(page pagination.Page) ([]Project, error) { var response struct { Projects []Project `mapstructure:"projects"` From b754a1aa66f355b4f13aa9241a2901cac8fce188 Mon Sep 17 00:00:00 2001 From: Krzysztof Kwapisiewicz Date: Thu, 6 Aug 2015 10:02:57 +0200 Subject: [PATCH 04/10] Update to fit master --- openstack/identity/v3/projects/requests.go | 27 ++++------------------ 1 file changed, 4 insertions(+), 23 deletions(-) diff --git a/openstack/identity/v3/projects/requests.go b/openstack/identity/v3/projects/requests.go index 3a79db64..d431f106 100644 --- a/openstack/identity/v3/projects/requests.go +++ b/openstack/identity/v3/projects/requests.go @@ -1,7 +1,6 @@ package projects import ( - "github.com/racker/perigee" "github.com/rackspace/gophercloud" "github.com/rackspace/gophercloud/pagination" ) @@ -39,12 +38,7 @@ func Create(client *gophercloud.ServiceClient, opts ProjectOpts) CreateResult { }, } var result CreateResult - _, result.Err = perigee.Request("POST", listURL(client), perigee.Options{ - MoreHeaders: client.AuthenticatedHeaders(), - ReqBody: &reqBody, - Results: &result.Body, - OkCodes: []int{201}, - }) + _, result.Err = client.Post(listURL(client), reqBody, &result.Body, nil) return result } @@ -77,11 +71,7 @@ func List(client *gophercloud.ServiceClient, opts ListOpts) pagination.Pager { // Get Shows project details func Get(client *gophercloud.ServiceClient, projectID string) GetResult { var result GetResult - _, result.Err = perigee.Request("GET", projectURL(client, projectID), perigee.Options{ - MoreHeaders: client.AuthenticatedHeaders(), - Results: &result.Body, - OkCodes: []int{200}, - }) + _, result.Err = client.Get(projectURL(client, projectID), &result.Body, nil) return result } @@ -101,22 +91,13 @@ func Update(client *gophercloud.ServiceClient, projectID string, opts ProjectOpt }, } var result UpdateResult - _, result.Err = perigee.Request("PUT", projectURL(client, projectID), perigee.Options{ - MoreHeaders: client.AuthenticatedHeaders(), - ReqBody: &reqBody, - Results: &result.Body, - OkCodes: []int{201}, - }) + _, result.Err = client.Put(projectURL(client, projectID), reqBody, &result.Body, nil) return result } // Delete Deletes project func Delete(client *gophercloud.ServiceClient, projectID string) DeleteResult { var result DeleteResult - _, result.Err = perigee.Request("DELETE", projectURL(client, projectID), perigee.Options{ - MoreHeaders: client.AuthenticatedHeaders(), - Results: &result.Body, - OkCodes: []int{204}, - }) + _, result.Err = client.Delete(projectURL(client, projectID), nil) return result } From 5c96166ec3aa1a7050a29909770f3bc8917a5e75 Mon Sep 17 00:00:00 2001 From: Krzysztof Kwapisiewicz Date: Tue, 17 Nov 2015 14:57:28 +0100 Subject: [PATCH 05/10] Apply @jrperritt suggestions --- openstack/identity/v3/projects/requests.go | 192 ++++++++++++++------- openstack/identity/v3/projects/results.go | 44 +++-- openstack/identity/v3/projects/urls.go | 26 ++- 3 files changed, 185 insertions(+), 77 deletions(-) diff --git a/openstack/identity/v3/projects/requests.go b/openstack/identity/v3/projects/requests.go index d431f106..358484f5 100644 --- a/openstack/identity/v3/projects/requests.go +++ b/openstack/identity/v3/projects/requests.go @@ -5,44 +5,77 @@ import ( "github.com/rackspace/gophercloud/pagination" ) -type project struct { - DomainID string `json:"domain_id"` - ParentID string `json:"parent_id"` - Enabled bool `json:"enabled"` - Name string `json:"name"` - Description string `json:"description"` -} - -// ProjectOpts Options for project create & update -type ProjectOpts struct { +type projectOpts struct { DomainID string ParentID string + Enabled *bool Name string - Enabled bool Description string } -// Create Creates project -func Create(client *gophercloud.ServiceClient, opts ProjectOpts) CreateResult { - type request struct { - Project project `json:"project"` +// CreateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Create operation in this package. Since many +// extensions decorate or modify the common logic, it is useful for them to +// satisfy a basic interface in order for them to be used. +type CreateOptsBuilder interface { + ToProjectCreateMap() (map[string]interface{}, error) +} + +// CreateOpts is the common options struct used in this package's Create +// operation. +type CreateOpts projectOpts + +// ToProjectCreateMap casts a CreateOpts struct to a map. +func (opts CreateOpts) ToProjectCreateMap() (map[string]interface{}, error) { + p := make(map[string]interface{}) + + if opts.DomainID != "" { + p["domain_id"] = opts.DomainID + } + if opts.ParentID != "" { + p["parent_id"] = opts.ParentID + } + if opts.Enabled != nil { + p["enabled"] = &opts.Enabled + } + if opts.Name != "" { + p["name"] = opts.Name + } + if opts.Description != "" { + p["description"] = opts.Description } - reqBody := request{ - Project: project{ - DomainID: opts.DomainID, - ParentID: opts.ParentID, - Name: opts.Name, - Enabled: opts.Enabled, - Description: opts.Description, - }, + return map[string]interface{}{"project": p}, nil +} + +// Create accepts a CreateOpts struct and creates a new project using the values +// provided. +// +// The tenant ID that is contained in the URI is the tenant that creates the +// network. An admin user, however, has the option of specifying another tenant +// ID in the CreateOpts struct. +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult { + var res CreateResult + + reqBody, err := opts.ToProjectCreateMap() + if err != nil { + res.Err = err + return res } - var result CreateResult - _, result.Err = client.Post(listURL(client), reqBody, &result.Body, nil) - return result + _, res.Err = client.Post(createURL(client), reqBody, &res.Body, nil) + return res } -// ListOpts Options for listing projects +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToProjectListQuery() (string, error) +} + +// ListOpts allows the filtering and of paginated collections through the API. +// Filtering is achieved by passing in struct field values that map to +// the project attributes you want to filter on. Page and PerPage are used for +// pagination. type ListOpts struct { DomainID string `q:"domain_id"` ParentID string `q:"parent_id"` @@ -52,52 +85,95 @@ type ListOpts struct { PerPage int `q:"per_page"` } -// List Lists projects -func List(client *gophercloud.ServiceClient, opts ListOpts) pagination.Pager { - url := listURL(client) - query, err := gophercloud.BuildQueryString(opts) +// ToProjectListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToProjectListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) if err != nil { - return pagination.Pager{Err: err} + return "", err } + return q.String(), nil +} - url += query.String() - createPage := func(r pagination.PageResult) pagination.Page { - return ProjectPage{pagination.LinkedPageBase{PageResult: r}} +// List returns a Pager which allows you to iterate over a collection of +// projects. It accepts a ListOpts struct, which allows you to filter the +// returned collection. +func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToProjectListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query } - return pagination.NewPager(client, url, createPage) + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return ProjectPage{pagination.LinkedPageBase{PageResult: r}} + }) } -// Get Shows project details -func Get(client *gophercloud.ServiceClient, projectID string) GetResult { - var result GetResult - _, result.Err = client.Get(projectURL(client, projectID), &result.Body, nil) - return result +// Get retrieves a specific project based on its unique ID. +func Get(client *gophercloud.ServiceClient, id string) GetResult { + var res GetResult + _, res.Err = client.Get(getURL(client, id), &res.Body, nil) + return res } -// Update Updates project information -func Update(client *gophercloud.ServiceClient, projectID string, opts ProjectOpts) UpdateResult { - type request struct { - Project project `json:"project"` +// UpdateOptsBuilder is the interface options structs have to satisfy in order +// to be used in the main Update operation in this package. Since many +// extensions decorate or modify the common logic, it is useful for them to +// satisfy a basic interface in order for them to be used. +type UpdateOptsBuilder interface { + ToProjectUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts is the common options struct used in this package's Update +// operation. +type UpdateOpts projectOpts + +// ToProjectUpdateMap casts a UpdateOpts struct to a map. +func (opts UpdateOpts) ToProjectUpdateMap() (map[string]interface{}, error) { + p := make(map[string]interface{}) + + if opts.DomainID != "" { + p["domain_id"] = opts.DomainID + } + if opts.ParentID != "" { + p["parent_id"] = opts.ParentID } + if opts.Enabled != nil { + p["enabled"] = &opts.Enabled + } + if opts.Name != "" { + p["name"] = opts.Name + } + if opts.Description != "" { + p["description"] = opts.Description + } + + return map[string]interface{}{"project": p}, nil +} - reqBody := request{ - Project: project{ - DomainID: opts.DomainID, - ParentID: opts.ParentID, - Name: opts.Name, - Enabled: opts.Enabled, - Description: opts.Description, - }, +// Update accepts a UpdateOpts struct and updates an existing project using the +// values provided. For more information, see the Create function. +func Update(client *gophercloud.ServiceClient, projectID string, opts UpdateOptsBuilder) UpdateResult { + var res UpdateResult + + reqBody, err := opts.ToProjectUpdateMap() + if err != nil { + res.Err = err + return res } - var result UpdateResult - _, result.Err = client.Put(projectURL(client, projectID), reqBody, &result.Body, nil) - return result + + _, res.Err = client.Patch(updateURL(client, projectID), reqBody, &res.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return res } -// Delete Deletes project +// Delete accepts a unique ID and deletes the project associated with it. func Delete(client *gophercloud.ServiceClient, projectID string) DeleteResult { var result DeleteResult - _, result.Err = client.Delete(projectURL(client, projectID), nil) + _, result.Err = client.Delete(deleteURL(client, projectID), nil) return result } diff --git a/openstack/identity/v3/projects/results.go b/openstack/identity/v3/projects/results.go index a792c7df..a1ae8486 100644 --- a/openstack/identity/v3/projects/results.go +++ b/openstack/identity/v3/projects/results.go @@ -6,6 +6,28 @@ import ( "github.com/rackspace/gophercloud/pagination" ) +// Project Project struct +type Project struct { + // The ID of the domain for the project. + DomainID string `mapstructure:"domain_id" json:"domain_id"` + + // The ID of the parent project. + ParentID string `mapstructure:"parent_id" json:"parent_id"` + + // Enables or disables a project. + // Set to true to enable the project or false to disable the project. Default is true. + Enabled bool `mapstructure:"enabled" json:"enabled"` + + // The ID for the project. + ID string `mapstructure:"id" json:"id"` + + // The project name. + Name string `mapstructure:"name" json:"name"` + + // The project description. + Description string `mapstructure:"description" json:"description"` +} + type commonResult struct { gophercloud.Result } @@ -16,7 +38,7 @@ func (r commonResult) Extract() (*Project, error) { } var response struct { - Project `json:"project"` + Project Project `json:"project"` } err := mapstructure.Decode(r.Body, &response) @@ -24,34 +46,24 @@ func (r commonResult) Extract() (*Project, error) { return &response.Project, err } -// CreateResult Project create result +// CreateResult represents the result of a create operation. type CreateResult struct { commonResult } -// GetResult Project details result +// GetResult represents the result of a get operation. type GetResult struct { commonResult } -// UpdateResult Project update result +// UpdateResult represents the result of a update operation. type UpdateResult struct { commonResult } -// DeleteResult Project delete result +// DeleteResult represents the result of a delete operation. type DeleteResult struct { - commonResult -} - -// Project Project struct -type Project struct { - DomainID string `mapstructure:"domain_id" json:"domain_id"` - ParentID string `mapstructure:"parent_id" json:"parent_id"` - Enabled bool `mapstructure:"enabled" json:"enabled"` - ID string `mapstructure:"id" json:"id"` - Name string `mapstructure:"name" json:"name"` - Description string `mapstructure:"description" json:"description"` + gophercloud.ErrResult } // ProjectPage Page containing projects diff --git a/openstack/identity/v3/projects/urls.go b/openstack/identity/v3/projects/urls.go index 51d3ab79..34767d0a 100644 --- a/openstack/identity/v3/projects/urls.go +++ b/openstack/identity/v3/projects/urls.go @@ -2,10 +2,30 @@ package projects import "github.com/rackspace/gophercloud" -func listURL(c *gophercloud.ServiceClient) string { +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("projects", id) +} + +func rootURL(c *gophercloud.ServiceClient) string { return c.ServiceURL("projects") } -func projectURL(client *gophercloud.ServiceClient, projectID string) string { - return client.ServiceURL("projects", projectID) +func getURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func listURL(c *gophercloud.ServiceClient) string { + return rootURL(c) +} + +func createURL(c *gophercloud.ServiceClient) string { + return rootURL(c) +} + +func updateURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func deleteURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) } From 9212326ec4c2acf3f3ee093905d2ae7af54b37af Mon Sep 17 00:00:00 2001 From: Krzysztof Kwapisiewicz Date: Tue, 17 Nov 2015 15:22:41 +0100 Subject: [PATCH 06/10] Fix acceptance tests --- acceptance/openstack/identity/v3/project_test.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/acceptance/openstack/identity/v3/project_test.go b/acceptance/openstack/identity/v3/project_test.go index 9b21151c..f944c689 100644 --- a/acceptance/openstack/identity/v3/project_test.go +++ b/acceptance/openstack/identity/v3/project_test.go @@ -16,9 +16,11 @@ func TestProjectCRUDOperations(t *testing.T) { return } + enabled := true + // Create project - opts := projects.ProjectOpts{ - Enabled: true, + opts := projects.CreateOpts{ + Enabled: &enabled, Name: "Test project", Description: "This is test project", } @@ -61,7 +63,7 @@ func TestProjectCRUDOperations(t *testing.T) { th.AssertEquals(t, project.Description, "This is test project") // Update project - project, err = projects.Update(serviceClient, projectID, projects.ProjectOpts{Name: "New test project name"}).Extract() + project, err = projects.Update(serviceClient, projectID, projects.UpdateOpts{Name: "New test project name"}).Extract() th.AssertNoErr(t, err) th.AssertEquals(t, project.Name, "New test project name") From 9d85a7d588648e0a4dfe108ce0421a42c1eb4583 Mon Sep 17 00:00:00 2001 From: Krzysztof Kwapisiewicz Date: Tue, 17 Nov 2015 15:57:22 +0100 Subject: [PATCH 07/10] Fix Unit Tests --- .../identity/v3/projects/requests_test.go | 16 ++++++----- openstack/identity/v3/projects/urls_test.go | 28 +++++++++++++++++-- 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/openstack/identity/v3/projects/requests_test.go b/openstack/identity/v3/projects/requests_test.go index d33e1eb6..94a3e770 100644 --- a/openstack/identity/v3/projects/requests_test.go +++ b/openstack/identity/v3/projects/requests_test.go @@ -42,10 +42,11 @@ func TestCreateSuccessful(t *testing.T) { }`) }) - opts := ProjectOpts{ + enabled := true + opts := CreateOpts{ DomainID: "1789d1", ParentID: "123c56", - Enabled: true, + Enabled: &enabled, Name: "Test Group", Description: "My new project", } @@ -200,7 +201,7 @@ func TestUpdateSuccessful(t *testing.T) { defer testhelper.TeardownHTTP() testhelper.Mux.HandleFunc("/projects/263fd9", func(w http.ResponseWriter, r *http.Request) { - testhelper.TestMethod(t, r, "PUT") + testhelper.TestMethod(t, r, "PATCH") testhelper.TestHeader(t, r, "X-Auth-Token", client.TokenID) testhelper.TestJSONRequest(t, r, `{ "project": { @@ -213,7 +214,7 @@ func TestUpdateSuccessful(t *testing.T) { }`) w.Header().Add("Content-Type", "application/json") - w.WriteHeader(http.StatusCreated) + w.WriteHeader(http.StatusOK) fmt.Fprintf(w, `{ "project": { "domain_id": "1789d1", @@ -226,16 +227,17 @@ func TestUpdateSuccessful(t *testing.T) { }`) }) - opts := ProjectOpts{ + enabled := true + opts := UpdateOpts{ DomainID: "1789d1", ParentID: "123c56", - Enabled: true, + Enabled: &enabled, Name: "Test Group", Description: "My new project", } result, err := Update(client.ServiceClient(), "263fd9", opts).Extract() if err != nil { - t.Fatalf("Unexpected error from Create: %v", err) + t.Fatalf("Unexpected error from Update: %v", err) } if result.ID != "263fd9" { diff --git a/openstack/identity/v3/projects/urls_test.go b/openstack/identity/v3/projects/urls_test.go index 87d30517..8ddd7e5b 100644 --- a/openstack/identity/v3/projects/urls_test.go +++ b/openstack/identity/v3/projects/urls_test.go @@ -6,6 +6,14 @@ import ( "github.com/rackspace/gophercloud" ) +func TestGetURL(t *testing.T) { + client := gophercloud.ServiceClient{Endpoint: "http://localhost:5000/v3/"} + url := getURL(&client, "12345") + if url != "http://localhost:5000/v3/projects/12345" { + t.Errorf("Unexpected project URL generated: [%s]", url) + } +} + func TestListURL(t *testing.T) { client := gophercloud.ServiceClient{Endpoint: "http://localhost:5000/v3/"} url := listURL(&client) @@ -14,9 +22,25 @@ func TestListURL(t *testing.T) { } } -func TestProjectURL(t *testing.T) { +func TestCreateURL(t *testing.T) { + client := gophercloud.ServiceClient{Endpoint: "http://localhost:5000/v3/"} + url := createURL(&client) + if url != "http://localhost:5000/v3/projects" { + t.Errorf("Unexpected project URL generated: [%s]", url) + } +} + +func TestUpdateURL(t *testing.T) { + client := gophercloud.ServiceClient{Endpoint: "http://localhost:5000/v3/"} + url := updateURL(&client, "12345") + if url != "http://localhost:5000/v3/projects/12345" { + t.Errorf("Unexpected project URL generated: [%s]", url) + } +} + +func TestDeleteURL(t *testing.T) { client := gophercloud.ServiceClient{Endpoint: "http://localhost:5000/v3/"} - url := projectURL(&client, "12345") + url := deleteURL(&client, "12345") if url != "http://localhost:5000/v3/projects/12345" { t.Errorf("Unexpected project URL generated: [%s]", url) } From 87dbae68ea62419507f685709d501aa5c8d492ba Mon Sep 17 00:00:00 2001 From: Krzysztof Kwapisiewicz Date: Wed, 3 Feb 2016 13:22:46 +0100 Subject: [PATCH 08/10] Add Roles and Project to Token --- openstack/identity/v3/tokens/results.go | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/openstack/identity/v3/tokens/results.go b/openstack/identity/v3/tokens/results.go index d134f7d4..3366800a 100644 --- a/openstack/identity/v3/tokens/results.go +++ b/openstack/identity/v3/tokens/results.go @@ -3,6 +3,7 @@ package tokens import ( "time" + "github.com/kwapik/gophercloud/openstack/identity/v3/projects" "github.com/mitchellh/mapstructure" "github.com/rackspace/gophercloud" ) @@ -120,7 +121,7 @@ func createErr(err error) CreateResult { // GetResult is the deferred response from a Get call. type GetResult struct { - commonResult + CreateResult } // RevokeResult is the deferred response from a Revoke call. @@ -128,6 +129,11 @@ type RevokeResult struct { commonResult } +type Role struct { + ID string `mapstructure:"id"` + Name string `mapstructure:"name"` +} + // Token is a string that grants a user access to a controlled set of services in an OpenStack provider. // Each Token is valid for a set length of time. type Token struct { @@ -136,4 +142,10 @@ type Token struct { // ExpiresAt is the timestamp at which this token will no longer be accepted. ExpiresAt time.Time + + // Project provides information about the project to which this token grants access. + Project projects.Project + + // Authorization need user info which can get from token authentication's response + Roles []Role } From 379e86ac8d255b86a57630c411470572e63e45ce Mon Sep 17 00:00:00 2001 From: Krzysztof Kwapisiewicz Date: Wed, 3 Feb 2016 16:09:59 +0100 Subject: [PATCH 09/10] Fix Project extraction from token --- openstack/identity/v3/tokens/results.go | 27 +++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/openstack/identity/v3/tokens/results.go b/openstack/identity/v3/tokens/results.go index 3366800a..3c5b14c4 100644 --- a/openstack/identity/v3/tokens/results.go +++ b/openstack/identity/v3/tokens/results.go @@ -3,7 +3,6 @@ package tokens import ( "time" - "github.com/kwapik/gophercloud/openstack/identity/v3/projects" "github.com/mitchellh/mapstructure" "github.com/rackspace/gophercloud" ) @@ -42,6 +41,18 @@ type CatalogEntry struct { Endpoints []Endpoint `mapstructure:"endpoints"` } +// Project provides information about the project to which this token grants access. +type Project struct { + ID string `mapstructure:"id"` + Name string `mapstructure:"name"` +} + +// Authorization need user info which can get from token authentication's response +type Role struct { + ID string `mapstructure:"id"` + Name string `mapstructure:"name"` +} + // ServiceCatalog provides a view into the service catalog from a previous, successful authentication. type ServiceCatalog struct { Entries []CatalogEntry @@ -66,7 +77,9 @@ func (r commonResult) ExtractToken() (*Token, error) { var response struct { Token struct { - ExpiresAt string `mapstructure:"expires_at"` + ExpiresAt string `mapstructure:"expires_at"` + Project Project `mapstructure:"project"` + Roles []Role `mapstructure:"roles"` } `mapstructure:"token"` } @@ -83,6 +96,9 @@ func (r commonResult) ExtractToken() (*Token, error) { // Attempt to parse the timestamp. token.ExpiresAt, err = time.Parse(gophercloud.RFC3339Milli, response.Token.ExpiresAt) + token.Project = response.Token.Project + token.Roles = response.Token.Roles + return &token, err } @@ -129,11 +145,6 @@ type RevokeResult struct { commonResult } -type Role struct { - ID string `mapstructure:"id"` - Name string `mapstructure:"name"` -} - // Token is a string that grants a user access to a controlled set of services in an OpenStack provider. // Each Token is valid for a set length of time. type Token struct { @@ -144,7 +155,7 @@ type Token struct { ExpiresAt time.Time // Project provides information about the project to which this token grants access. - Project projects.Project + Project Project // Authorization need user info which can get from token authentication's response Roles []Role From fb8ac8f5963a2f2ade0453daaeb20b1e2919eca8 Mon Sep 17 00:00:00 2001 From: Krzysztof Kwapisiewicz Date: Mon, 7 Nov 2016 14:18:26 +0100 Subject: [PATCH 10/10] Add ExtractUser --- openstack/identity/v3/tokens/results.go | 33 +++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/openstack/identity/v3/tokens/results.go b/openstack/identity/v3/tokens/results.go index 3c5b14c4..6b1499e5 100644 --- a/openstack/identity/v3/tokens/results.go +++ b/openstack/identity/v3/tokens/results.go @@ -47,6 +47,19 @@ type Project struct { Name string `mapstructure:"name"` } +// Domain provides information about the domain to which this token grants access. +type Domain struct { + ID string `mapstructure:"id"` + Name string `mapstructure:"name"` +} + +// User represents a user resource that exists on the API. +type User struct { + Domain Domain `mapstructure:"domain"` + ID string `mapstructure:"id"` + Name string `mapstructure:"name"` +} + // Authorization need user info which can get from token authentication's response type Role struct { ID string `mapstructure:"id"` @@ -122,6 +135,26 @@ func (result CreateResult) ExtractServiceCatalog() (*ServiceCatalog, error) { return &ServiceCatalog{Entries: response.Token.Entries}, nil } +// ExtractUser returns the User that was generated along with the user's Token. +func (result CreateResult) ExtractUser() (*User, error) { + if result.Err != nil { + return nil, result.Err + } + + var response struct { + Token struct { + User User `mapstructure:"user"` + } `mapstructure:"token"` + } + + err := mapstructure.Decode(result.Body, &response) + if err != nil { + return nil, err + } + + return &response.Token.User, nil +} + // CreateResult defers the interpretation of a created token. // Use ExtractToken() to interpret it as a Token, or ExtractServiceCatalog() to interpret it as a service catalog. type CreateResult struct {