diff --git a/internal/acceptance/openstack/identity/v3/projects_test.go b/internal/acceptance/openstack/identity/v3/projects_test.go index f19861aab5..528a30b2a7 100644 --- a/internal/acceptance/openstack/identity/v3/projects_test.go +++ b/internal/acceptance/openstack/identity/v3/projects_test.go @@ -364,3 +364,32 @@ func TestProjectsTags(t *testing.T) { tools.PrintResource(t, updatedProject) th.AssertEquals(t, len(updatedProject.Tags), 0) } + +func TestProjectsTagsCRUD(t *testing.T) { + clients.RequireAdmin(t) + + client, err := clients.NewIdentityV3Client() + th.AssertNoErr(t, err) + + createOpts := projects.CreateOpts{ + Tags: []string{"Tag1", "Tag2"}, + } + + projectMain, err := CreateProject(t, client, &createOpts) + th.AssertNoErr(t, err) + defer DeleteProject(t, client, projectMain.ID) + + projectTagsList, err := projects.ListTags(context.TODO(), client, projectMain.ID).Extract() + tools.PrintResource(t, projectTagsList) + th.AssertNoErr(t, err) + + modifyOpts := projects.ModifyTagsOpts{ + Tags: []string{"foo", "bar"}, + } + projectTags, err := projects.ModifyTags(context.TODO(), client, projectMain.ID, modifyOpts).Extract() + tools.PrintResource(t, projectTags) + th.AssertNoErr(t, err) + + err = projects.DeleteTags(context.TODO(), client, projectMain.ID).ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/openstack/identity/v3/projects/doc.go b/openstack/identity/v3/projects/doc.go index bee8760c34..6aea466a51 100644 --- a/openstack/identity/v3/projects/doc.go +++ b/openstack/identity/v3/projects/doc.go @@ -64,5 +64,30 @@ Example to Delete a Project if err != nil { panic(err) } + +Example to List all tags of a Project + + projectID := "966b3c7d36a24facaf20b7e458bf2192" + err := projects.ListTags(context.TODO(), identityClient, projectID).Extract() + if err != nil { + panic(err) + } + +Example to modify all tags of a Project + + projectID := "966b3c7d36a24facaf20b7e458bf2192" + tags := ["foo", "bar"] + projects, err := projects.ModifyTags(context.TODO(), identityClient, projectID, tags).Extract() + if err != nil { + panic(err) + } + +Example to Delete all tags of a Project + + projectID := "966b3c7d36a24facaf20b7e458bf2192" + err := projects.DeleteTags(context.TODO(), identityClient, projectID).ExtractErr() + if err != nil { + panic(err) + } */ package projects diff --git a/openstack/identity/v3/projects/requests.go b/openstack/identity/v3/projects/requests.go index 2d9d5eaa39..568d12a719 100644 --- a/openstack/identity/v3/projects/requests.go +++ b/openstack/identity/v3/projects/requests.go @@ -243,3 +243,58 @@ func Update(ctx context.Context, client *gophercloud.ServiceClient, id string, o _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) return } + +// CheckTags lists tags for a project. +func ListTags(ctx context.Context, client *gophercloud.ServiceClient, projectID string) (r ListTagsResult) { + resp, err := client.Get(ctx, listTagsURL(client, projectID), &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Tags represents a list of Tags object. +type ModifyTagsOpts struct { + // Tags is the list of tags associated with the project. + Tags []string `json:"tags,omitempty"` +} + +// ModifyTagsOptsBuilder allows extensions to add additional parameters to +// the Modify request. +type ModifyTagsOptsBuilder interface { + ToModifyTagsCreateMap() (map[string]interface{}, error) +} + +// ToModifyTagsCreateMap formats a ModifyTagsOpts into a Modify tags request. +func (opts ModifyTagsOpts) ToModifyTagsCreateMap() (map[string]interface{}, error) { + b, err := gophercloud.BuildRequestBody(opts, "") + + if err != nil { + return nil, err + } + return b, nil +} + +// ModifyTags deletes all tags of a project and adds new ones. +func ModifyTags(ctx context.Context, client *gophercloud.ServiceClient, projectID string, opts ModifyTagsOpts) (r ModifyTagsResult) { + + b, err := opts.ToModifyTagsCreateMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Put(ctx, modifyTagsURL(client, projectID), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// DeleteTag deletes a tag from a project. +func DeleteTags(ctx context.Context, client *gophercloud.ServiceClient, projectID string) (r DeleteTagsResult) { + resp, err := client.Delete(ctx, deleteTagsURL(client, projectID), &gophercloud.RequestOpts{ + OkCodes: []int{204}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/openstack/identity/v3/projects/results.go b/openstack/identity/v3/projects/results.go index 23f482f14a..f571334313 100644 --- a/openstack/identity/v3/projects/results.go +++ b/openstack/identity/v3/projects/results.go @@ -154,3 +154,49 @@ func (r projectResult) Extract() (*Project, error) { err := r.ExtractInto(&s) return s.Project, err } + +// Tags represents a list of Tags object. +type Tags struct { + // Tags is the list of tags associated with the project. + Tags []string `json:"tags,omitempty"` +} + +// ListTagsResult is the result of a List Tags request. Call its Extract method to +// interpret it as a list of tags. +type ListTagsResult struct { + gophercloud.Result +} + +// Extract interprets any ListTagsResult as a Tags Object. +func (r ListTagsResult) Extract() (*Tags, error) { + var s = &Tags{} + err := r.ExtractInto(&s) + return s, err +} + +// ProjectTags represents a list of Tags object. +type ProjectTags struct { + // Tags is the list of tags associated with the project. + Projects []Project `json:"projects,omitempty"` + // Links contains referencing links to the implied_role. + Links map[string]interface{} `json:"links"` +} + +// ModifyTagsResLinksult is the result of a Tags request. Call its Extract method to +// interpret it as a project of tags. +type ModifyTagsResult struct { + gophercloud.Result +} + +// Extract interprets any ModifyTags as a Tags Object. +func (r ModifyTagsResult) Extract() (*ProjectTags, error) { + var s = &ProjectTags{} + err := r.ExtractInto(&s) + return s, err +} + +// DeleteTagsResult is the result of a Delete Tags request. Call its ExtractErr method to +// determine if the request succeeded or failed. +type DeleteTagsResult struct { + gophercloud.ErrResult +} diff --git a/openstack/identity/v3/projects/testing/fixtures_test.go b/openstack/identity/v3/projects/testing/fixtures_test.go index f78d2660ef..9d07b66f24 100644 --- a/openstack/identity/v3/projects/testing/fixtures_test.go +++ b/openstack/identity/v3/projects/testing/fixtures_test.go @@ -138,6 +138,44 @@ const UpdateOutput = ` } ` +// ListTagsOutput provides the output to a ListTags request. +const ListTagsOutput = ` +{ + "tags": ["foo", "bar"] +} +` + +// ModifyProjectTagsRequest provides the input to a ModifyTags request. +const ModifyProjectTagsRequest = ` +{ + "tags": ["foo", "bar"] +} +` + +// ModifyProjectTagsOutput provides the output to a ModifyTags request. +const ModifyProjectTagsOutput = ` +{ + "links": { + "next": null, + "previous": null, + "self": "http://identity:5000/v3/projects" + }, + "projects": [ + { + "description": "Test Project", + "domain_id": "default", + "enabled": true, + "id": "3d4c2c82bd5948f0bcab0cf3a7c9b48c", + "links": { + "self": "http://identity:5000/v3/projects/3d4c2c82bd5948f0bcab0cf3a7c9b48c" + }, + "name": "demo", + "tags": ["foo", "bar"] + } + ] +} +` + // FirstProject is a Project fixture. var FirstProject = projects.Project{ Description: "my first project", @@ -212,6 +250,31 @@ var ExpectedAvailableProjectsSlice = []projects.Project{FirstProject, SecondProj // ExpectedProjectSlice is the slice of projects expected to be returned from ListOutput. var ExpectedProjectSlice = []projects.Project{RedTeam, BlueTeam} +var ExpectedTags = projects.Tags{ + Tags: []string{"foo", "bar"}, +} + +var ExpectedProjects = projects.ProjectTags{ + Projects: []projects.Project{ + { + Description: "Test Project", + DomainID: "default", + Enabled: true, + ID: "3d4c2c82bd5948f0bcab0cf3a7c9b48c", + Extra: map[string]interface{}{"links": map[string]interface{}{ + "self": "http://identity:5000/v3/projects/3d4c2c82bd5948f0bcab0cf3a7c9b48c", + }}, + Name: "demo", + Tags: []string{"foo", "bar"}, + }, + }, + Links: map[string]interface{}{ + "next": nil, + "previous": nil, + "self": "http://identity:5000/v3/projects", + }, +} + // HandleListAvailableProjectsSuccessfully creates an HTTP handler at `/auth/projects` // on the test handler mux that responds with a list of two tenants. func HandleListAvailableProjectsSuccessfully(t *testing.T) { @@ -290,3 +353,32 @@ func HandleUpdateProjectSuccessfully(t *testing.T) { fmt.Fprintf(w, UpdateOutput) }) } + +func HandleListProjectTagsSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/projects/966b3c7d36a24facaf20b7e458bf2192/tags", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ListTagsOutput) + }) +} + +func HandleModifyProjectTagsSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/projects/966b3c7d36a24facaf20b7e458bf2192/tags", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, ModifyProjectTagsRequest) + + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ModifyProjectTagsOutput) + }) +} +func HandleDeleteProjectTagsSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/projects/966b3c7d36a24facaf20b7e458bf2192/tags", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.WriteHeader(http.StatusNoContent) + }) +} diff --git a/openstack/identity/v3/projects/testing/requests_test.go b/openstack/identity/v3/projects/testing/requests_test.go index 3ca4898123..6ff4efc7bc 100644 --- a/openstack/identity/v3/projects/testing/requests_test.go +++ b/openstack/identity/v3/projects/testing/requests_test.go @@ -134,4 +134,37 @@ func TestUpdateProject(t *testing.T) { actual, err := projects.Update(context.TODO(), client.ServiceClient(), "1234", updateOpts).Extract() th.AssertNoErr(t, err) th.CheckDeepEquals(t, UpdatedRedTeam, *actual) + t.Log(projects.Update(context.TODO(), client.ServiceClient(), "1234", updateOpts)) +} + +func TestListProjectTags(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListProjectTagsSuccessfully(t) + + actual, err := projects.ListTags(context.TODO(), client.ServiceClient(), "966b3c7d36a24facaf20b7e458bf2192").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, ExpectedTags, *actual) +} + +func TestModifyProjectTags(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleModifyProjectTagsSuccessfully(t) + + modifyOpts := projects.ModifyTagsOpts{ + Tags: []string{"foo", "bar"}, + } + actual, err := projects.ModifyTags(context.TODO(), client.ServiceClient(), "966b3c7d36a24facaf20b7e458bf2192", modifyOpts).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, ExpectedProjects, *actual) +} + +func TestDeleteTags(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleDeleteProjectTagsSuccessfully(t) + + err := projects.DeleteTags(context.TODO(), client.ServiceClient(), "966b3c7d36a24facaf20b7e458bf2192").ExtractErr() + th.AssertNoErr(t, err) } diff --git a/openstack/identity/v3/projects/urls.go b/openstack/identity/v3/projects/urls.go index fc22c02dc5..27cd6c2019 100644 --- a/openstack/identity/v3/projects/urls.go +++ b/openstack/identity/v3/projects/urls.go @@ -25,3 +25,15 @@ func deleteURL(client *gophercloud.ServiceClient, projectID string) string { func updateURL(client *gophercloud.ServiceClient, projectID string) string { return client.ServiceURL("projects", projectID) } + +func listTagsURL(client *gophercloud.ServiceClient, projectID string) string { + return client.ServiceURL("projects", projectID, "tags") +} + +func modifyTagsURL(client *gophercloud.ServiceClient, projectID string) string { + return client.ServiceURL("projects", projectID, "tags") +} + +func deleteTagsURL(client *gophercloud.ServiceClient, projectID string) string { + return client.ServiceURL("projects", projectID, "tags") +}