Skip to content

Commit 0b69f2f

Browse files
carolynvsjrperritt
authored andcommitted
Support deleting Magnum bays (#38)
* Fix variable name to match standards * Support deleting a Magnum bay * Extract the error message from requests that fail with 400 The problem may not actually be due to a bad request and actually is caused by the bay state, so display a more appropriate error message. * Move delete error handling into errors.go * Enable special handling of 409 Conflict errors * Attempt to unwrap custom error messages from the server Fallback to the default error message for the status code if a custom error message isn't present * Unwrap errors from create and get too * Preserve the original UnexpectedResponseCode This lets consumers get a nice message but still inspect the failed response code, body, etc. * Remove redundant else statements * Use standard DeleteResult instead of DeleteHeader
1 parent 37991bb commit 0b69f2f

File tree

8 files changed

+279
-10
lines changed

8 files changed

+279
-10
lines changed

acceptance/openstack/containerorchestration/v1/bay_test.go

+1-2
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,7 @@ func TestBayCRUDOperations(t *testing.T) {
1919
bayModelID := "5b793604-fc76-4886-a834-ed522812cdcb"
2020
b, err := bays.Create(Client, bays.CreateOpts{BayModelID: bayModelID}).Extract()
2121
th.AssertNoErr(t, err)
22-
// TODO: cleanup after ourselves once DELETE is implemented
23-
// defer bays.Delete(Client, b.ID)
22+
defer bays.Delete(Client, b.ID)
2423
th.AssertEquals(t, b.Status, "CREATE_IN_PROGRESS")
2524
th.AssertEquals(t, b.BayModelID, bayModelID)
2625
th.AssertEquals(t, b.Masters, 1)

errors.go

+14
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,11 @@ type ErrDefault408 struct {
8787
ErrUnexpectedResponseCode
8888
}
8989

90+
// ErrDefault409 is the default error type returned on a 409 HTTP response code.
91+
type ErrDefault409 struct {
92+
ErrUnexpectedResponseCode
93+
}
94+
9095
// ErrDefault429 is the default error type returned on a 429 HTTP response code.
9196
type ErrDefault429 struct {
9297
ErrUnexpectedResponseCode
@@ -117,6 +122,9 @@ func (e ErrDefault405) Error() string {
117122
func (e ErrDefault408) Error() string {
118123
return "The server timed out waiting for the request"
119124
}
125+
func (e ErrDefault409) Error() string {
126+
return "Request failed due to a conflict, such as duplicate data or an edit conflict between multiple simultaneous updates."
127+
}
120128
func (e ErrDefault429) Error() string {
121129
return "Too many requests have been sent in a given amount of time. Pause" +
122130
" requests, wait up to one minute, and try again."
@@ -159,6 +167,12 @@ type Err408er interface {
159167
Error408(ErrUnexpectedResponseCode) error
160168
}
161169

170+
// Err409er is the interface resource error types implement to override the error message
171+
// from a 409 error.
172+
type Err409er interface {
173+
Error409(ErrUnexpectedResponseCode) error
174+
}
175+
162176
// Err429er is the interface resource error types implement to override the error message
163177
// from a 429 error.
164178
type Err429er interface {

openstack/containerorchestration/v1/bays/requests.go

+13-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package bays
22

33
import (
44
"github.com/gophercloud/gophercloud"
5+
"github.com/gophercloud/gophercloud/openstack/containerorchestration/v1/common"
56
"github.com/gophercloud/gophercloud/pagination"
67
)
78

@@ -47,7 +48,8 @@ func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
4748

4849
// Get retrieves a specific bay based on its unique ID.
4950
func Get(c *gophercloud.ServiceClient, id string) (r GetResult) {
50-
_, r.Err = c.Get(getURL(c, id), &r.Body, nil)
51+
ro := &gophercloud.RequestOpts{ErrorContext: &common.ErrorResponse{}}
52+
_, r.Err = c.Get(getURL(c, id), &r.Body, ro)
5153
return
5254
}
5355

@@ -81,6 +83,15 @@ func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResul
8183
r.Err = err
8284
return
8385
}
84-
_, r.Err = c.Post(createURL(c), b, &r.Body, nil)
86+
87+
ro := &gophercloud.RequestOpts{ErrorContext: &common.ErrorResponse{}}
88+
_, r.Err = c.Post(createURL(c), b, &r.Body, ro)
89+
return
90+
}
91+
92+
// Delete accepts a unique ID and deletes the bay associated with it.
93+
func Delete(c *gophercloud.ServiceClient, bayID string) (r DeleteResult) {
94+
ro := &gophercloud.RequestOpts{ErrorContext: &common.ErrorResponse{}}
95+
_, r.Err = c.Delete(deleteURL(c, bayID), ro)
8596
return
8697
}

openstack/containerorchestration/v1/bays/results.go

+5
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ type GetResult struct {
2626
commonResult
2727
}
2828

29+
// DeleteResult represents the result of a delete operation.
30+
type DeleteResult struct {
31+
gophercloud.ErrResult
32+
}
33+
2934
// Represents a Container Orchestration Engine Bay, i.e. a cluster
3035
type Bay struct {
3136
// UUID for the bay

openstack/containerorchestration/v1/bays/testing/requests_test.go

+115
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,39 @@ func TestGet(t *testing.T) {
141141
th.AssertEquals(t, b.ID, "a56a6cd8-0779-461b-b1eb-26cec904284a")
142142
}
143143

144+
func TestGetFailed(t *testing.T) {
145+
th.SetupHTTP()
146+
defer th.TeardownHTTP()
147+
148+
th.Mux.HandleFunc("/v1/bays/duplicatename", func(w http.ResponseWriter, r *http.Request) {
149+
th.TestMethod(t, r, "GET")
150+
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
151+
w.WriteHeader(http.StatusConflict)
152+
fmt.Fprintf(w, `
153+
{
154+
"errors": [
155+
{
156+
"status": 409,
157+
"code": "client",
158+
"links": [],
159+
"title": "Multiple bays exist with same name",
160+
"detail": "Multiple bays exist with same name. Please use the bay uuid instead.",
161+
"request_id": ""
162+
}
163+
]
164+
}
165+
`)
166+
})
167+
168+
res := bays.Get(fake.ServiceClient(), "duplicatename")
169+
170+
th.AssertEquals(t, "Multiple bays exist with same name. Please use the bay uuid instead.", res.Err.Error())
171+
172+
er, ok := res.Err.(*fake.ErrorResponse)
173+
th.AssertEquals(t, true, ok)
174+
th.AssertEquals(t, http.StatusConflict, er.Actual)
175+
}
176+
144177
func TestCreate(t *testing.T) {
145178
th.SetupHTTP()
146179
defer th.TeardownHTTP()
@@ -203,3 +236,85 @@ func TestCreate(t *testing.T) {
203236
th.AssertEquals(t, b.Masters, 1)
204237
th.AssertEquals(t, b.Nodes, 2)
205238
}
239+
240+
func TestCreateFailed(t *testing.T) {
241+
th.SetupHTTP()
242+
defer th.TeardownHTTP()
243+
244+
th.Mux.HandleFunc("/v1/bays", func(w http.ResponseWriter, r *http.Request) {
245+
th.TestMethod(t, r, "POST")
246+
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
247+
w.WriteHeader(http.StatusInternalServerError)
248+
fmt.Fprintf(w, `
249+
{
250+
"errors": [
251+
{
252+
"status": 500,
253+
"code": "client",
254+
"links": [],
255+
"title": "Nova is down",
256+
"detail": "Nova is down. Try again later.",
257+
"request_id": ""
258+
}
259+
]
260+
}
261+
`)
262+
})
263+
264+
options := bays.CreateOpts{Name: "mycluster", Nodes: 2, BayModelID: "5b793604-fc76-4886-a834-ed522812cdcb"}
265+
266+
res := bays.Create(fake.ServiceClient(), options)
267+
268+
th.AssertEquals(t, "Nova is down. Try again later.", res.Err.Error())
269+
270+
er, ok := res.Err.(*fake.ErrorResponse)
271+
th.AssertEquals(t, true, ok)
272+
th.AssertEquals(t, http.StatusInternalServerError, er.Actual)
273+
}
274+
275+
func TestDelete(t *testing.T) {
276+
th.SetupHTTP()
277+
defer th.TeardownHTTP()
278+
279+
th.Mux.HandleFunc("/v1/bays/a56a6cd8-0779-461b-b1eb-26cec904284a", func(w http.ResponseWriter, r *http.Request) {
280+
th.TestMethod(t, r, "DELETE")
281+
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
282+
w.WriteHeader(http.StatusNoContent)
283+
})
284+
285+
res := bays.Delete(fake.ServiceClient(), "a56a6cd8-0779-461b-b1eb-26cec904284a")
286+
th.AssertNoErr(t, res.Err)
287+
}
288+
289+
func TestDeleteFailed(t *testing.T) {
290+
th.SetupHTTP()
291+
defer th.TeardownHTTP()
292+
293+
th.Mux.HandleFunc("/v1/bays/a56a6cd8-0779-461b-b1eb-26cec904284a", func(w http.ResponseWriter, r *http.Request) {
294+
th.TestMethod(t, r, "DELETE")
295+
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
296+
w.WriteHeader(http.StatusBadRequest)
297+
fmt.Fprintf(w, `
298+
{
299+
"errors": [
300+
{
301+
"status": 400,
302+
"code": "client",
303+
"links": [],
304+
"title": "Bay k8sbay already has an operation in progress",
305+
"detail": "Bay k8sbay already has an operation in progress.",
306+
"request_id": ""
307+
}
308+
]
309+
}
310+
`)
311+
})
312+
313+
res := bays.Delete(fake.ServiceClient(), "a56a6cd8-0779-461b-b1eb-26cec904284a")
314+
315+
th.AssertEquals(t, "Bay k8sbay already has an operation in progress.", res.Err.Error())
316+
317+
er, ok := res.Err.(*fake.ErrorResponse)
318+
th.AssertEquals(t, true, ok)
319+
th.AssertEquals(t, http.StatusBadRequest, er.Actual)
320+
}

openstack/containerorchestration/v1/bays/urls.go

+10-6
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,18 @@ package bays
22

33
import "github.com/gophercloud/gophercloud"
44

5-
func createURL(client *gophercloud.ServiceClient) string {
6-
return client.ServiceURL("bays")
5+
func createURL(c *gophercloud.ServiceClient) string {
6+
return c.ServiceURL("bays")
77
}
88

9-
func listURL(client *gophercloud.ServiceClient) string {
10-
return client.ServiceURL("bays")
9+
func listURL(c *gophercloud.ServiceClient) string {
10+
return c.ServiceURL("bays")
1111
}
1212

13-
func getURL(client *gophercloud.ServiceClient, id string) string {
14-
return client.ServiceURL("bays", id)
13+
func getURL(c *gophercloud.ServiceClient, id string) string {
14+
return c.ServiceURL("bays", id)
15+
}
16+
17+
func deleteURL(c *gophercloud.ServiceClient, id string) string {
18+
return c.ServiceURL("bays", id)
1519
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package common
2+
3+
import (
4+
"encoding/json"
5+
"github.com/gophercloud/gophercloud"
6+
"strings"
7+
)
8+
9+
// ErrorResponse represents failed response
10+
type ErrorResponse struct {
11+
gophercloud.ErrUnexpectedResponseCode
12+
Errors []Error `json:"errors" required:"true"`
13+
}
14+
15+
// Error represents a Magnum API error message
16+
type Error struct {
17+
Status int `json:"status"`
18+
Code string `json:"code"`
19+
Title string `json:"title"`
20+
Detail string `json:"detail"`
21+
RequestID string `json:"request_id,omitempty"`
22+
}
23+
24+
// Error returns the error message details from the server
25+
func (e *ErrorResponse) Error() string {
26+
errors := []string{}
27+
for _, e := range e.Errors {
28+
errors = append(errors, e.Detail)
29+
}
30+
return strings.Join(errors, "\n")
31+
}
32+
33+
func (e *ErrorResponse) unwrapError(r gophercloud.ErrUnexpectedResponseCode) bool {
34+
e.ErrUnexpectedResponseCode = r
35+
err := json.Unmarshal(r.Body, &e)
36+
return err == nil
37+
}
38+
39+
// Error400 extracts the actual error message from the body of the response
40+
func (e *ErrorResponse) Error400(r gophercloud.ErrUnexpectedResponseCode) error {
41+
if e.unwrapError(r) {
42+
return e
43+
}
44+
45+
return gophercloud.ErrDefault400{ErrUnexpectedResponseCode: r}
46+
}
47+
48+
// Error401 extracts the actual error message from the body of the response
49+
func (e *ErrorResponse) Error401(r gophercloud.ErrUnexpectedResponseCode) error {
50+
if e.unwrapError(r) {
51+
return e
52+
}
53+
return gophercloud.ErrDefault401{ErrUnexpectedResponseCode: r}
54+
}
55+
56+
// Error404 extracts the actual error message from the body of the response
57+
func (e *ErrorResponse) Error404(r gophercloud.ErrUnexpectedResponseCode) error {
58+
if e.unwrapError(r) {
59+
return e
60+
}
61+
return gophercloud.ErrDefault404{ErrUnexpectedResponseCode: r}
62+
}
63+
64+
// Error405 extracts the actual error message from the body of the response
65+
func (e *ErrorResponse) Error405(r gophercloud.ErrUnexpectedResponseCode) error {
66+
if e.unwrapError(r) {
67+
return e
68+
}
69+
70+
return gophercloud.ErrDefault405{ErrUnexpectedResponseCode: r}
71+
}
72+
73+
// Error408 extracts the actual error message from the body of the response
74+
func (e *ErrorResponse) Error408(r gophercloud.ErrUnexpectedResponseCode) error {
75+
if e.unwrapError(r) {
76+
return e
77+
}
78+
79+
return gophercloud.ErrDefault408{ErrUnexpectedResponseCode: r}
80+
}
81+
82+
// Error409 extracts the actual error message from the body of the response
83+
func (e *ErrorResponse) Error409(r gophercloud.ErrUnexpectedResponseCode) error {
84+
if e.unwrapError(r) {
85+
return e
86+
}
87+
88+
return gophercloud.ErrDefault409{ErrUnexpectedResponseCode: r}
89+
}
90+
91+
// Error429 extracts the actual error message from the body of the response
92+
func (e *ErrorResponse) Error429(r gophercloud.ErrUnexpectedResponseCode) error {
93+
if e.unwrapError(r) {
94+
return e
95+
}
96+
97+
return gophercloud.ErrDefault429{ErrUnexpectedResponseCode: r}
98+
}
99+
100+
// Error500 extracts the actual error message from the body of the response
101+
func (e *ErrorResponse) Error500(r gophercloud.ErrUnexpectedResponseCode) error {
102+
if e.unwrapError(r) {
103+
return e
104+
}
105+
106+
return gophercloud.ErrDefault500{ErrUnexpectedResponseCode: r}
107+
}
108+
109+
// Error503 extracts the actual error message from the body of the response
110+
func (e *ErrorResponse) Error503(r gophercloud.ErrUnexpectedResponseCode) error {
111+
if e.unwrapError(r) {
112+
return e
113+
}
114+
115+
return gophercloud.ErrDefault503{ErrUnexpectedResponseCode: r}
116+
}

provider_client.go

+5
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,11 @@ func (client *ProviderClient) Request(method, url string, options *RequestOpts)
254254
if error408er, ok := errType.(Err408er); ok {
255255
err = error408er.Error408(respErr)
256256
}
257+
case http.StatusConflict:
258+
err = ErrDefault409{respErr}
259+
if error409er, ok := errType.(Err409er); ok {
260+
err = error409er.Error409(respErr)
261+
}
257262
case 429:
258263
err = ErrDefault429{respErr}
259264
if error429er, ok := errType.(Err429er); ok {

0 commit comments

Comments
 (0)