Left: | ||
Right: |
LEFT | RIGHT |
---|---|
1 // Nova double testing service - HTTP API implementation | 1 // Nova double testing service - HTTP API implementation |
2 | 2 |
3 package novaservice | 3 package novaservice |
4 | 4 |
5 import ( | 5 import ( |
6 "encoding/json" | 6 "encoding/json" |
7 "io/ioutil" | 7 "io/ioutil" |
8 "launchpad.net/goose/nova" | 8 "launchpad.net/goose/nova" |
9 "net/http" | 9 "net/http" |
10 "strconv" | 10 "strconv" |
11 "strings" | 11 "strings" |
12 ) | 12 ) |
13 | 13 |
14 const authToken = "X-Auth-Token" | 14 const authToken = "X-Auth-Token" |
15 | 15 |
16 // response defines a single HTTP response. | 16 // response defines a single HTTP response. |
17 type response struct { | 17 type response struct { |
18 code int | 18 code int |
19 body string | 19 body string |
20 contentType string | 20 contentType string |
21 errorText string | 21 errorText string |
22 } | 22 } |
23 | 23 |
24 // verbatim real Nova responses | 24 // verbatim real Nova responses |
fwereade
2012/12/12 15:18:44
I suspect we'll want multiple versions of some of
dimitern
2012/12/12 17:32:24
In a follow up, I'll remove all that we don't real
| |
25 var ( | 25 var ( |
26 unauthorizedResponse = response{ | 26 unauthorizedResponse = response{ |
27 http.StatusUnauthorized, | 27 http.StatusUnauthorized, |
28 `401 Unauthorized | 28 `401 Unauthorized |
29 | 29 |
30 This server could not verify that you are authorized to access the ` + | 30 This server could not verify that you are authorized to access the ` + |
rog
2012/12/12 15:02:33
if you're going to use + to concatenate parts of t
dimitern
2012/12/12 17:32:24
Not sure how to include the NLs inside - it will b
| |
31 `document you requested. Either you supplied the wrong ` + | 31 `document you requested. Either you supplied the wrong ` + |
32 `credentials (e.g., bad password), or your browser does ` + | 32 `credentials (e.g., bad password), or your browser does ` + |
33 `not understand how to supply the credentials required. | 33 `not understand how to supply the credentials required. |
34 | 34 |
35 Authentication required | 35 Authentication required |
36 `, | 36 `, |
37 "text/plain; charset=UTF-8", | 37 "text/plain; charset=UTF-8", |
38 "", | 38 "", |
39 } | 39 } |
40 forbiddenResponse = response{ | 40 forbiddenResponse = response{ |
(...skipping 64 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
105 `"application/vnd.sun.wadl+xml", "rel": "describedby"}]} }`, | 105 `"application/vnd.sun.wadl+xml", "rel": "describedby"}]} }`, |
106 "application/json", | 106 "application/json", |
107 "", | 107 "", |
108 } | 108 } |
109 createdResponse = response{ | 109 createdResponse = response{ |
110 http.StatusCreated, | 110 http.StatusCreated, |
111 "201 Created", | 111 "201 Created", |
112 "text/plain; charset=UTF-8", | 112 "text/plain; charset=UTF-8", |
113 "", | 113 "", |
114 } | 114 } |
115 noContentResponse = response{ | |
116 http.StatusNoContent, | |
117 "", | |
118 "text/plain; charset=UTF-8", | |
119 "", | |
120 } | |
115 errorResponse = response{ | 121 errorResponse = response{ |
116 http.StatusInternalServerError, | 122 http.StatusInternalServerError, |
117 `{"internalServerError":{"message":"$ERROR$",code:500}}`, | 123 `{"internalServerError":{"message":"$ERROR$",code:500}}`, |
118 "application/json", | 124 "application/json", |
119 "", // set by sendError() | 125 "", // set by sendError() |
120 } | 126 } |
121 ) | 127 ) |
122 | 128 |
123 // endpoint returns the current testing server's endpoint URL. | 129 // endpoint returns the current testing server's endpoint URL. |
124 func endpoint() string { | 130 func endpoint() string { |
125 » return hostname + baseURL + "/" | 131 » return hostname + versionPath + "/" |
126 } | 132 } |
127 | 133 |
128 // replaceVars replaces $ENDPOINT$, $URL$, and $ERROR$ in the response body | 134 // replaceVars replaces $ENDPOINT$, $URL$, and $ERROR$ in the response body |
129 // with their values, taking the original requset into account, and | 135 // with their values, taking the original requset into account, and |
130 // returns the result as a []byte. | 136 // returns the result as a []byte. |
131 func (resp response) replaceVars(r *http.Request) []byte { | 137 func (resp response) replaceVars(r *http.Request) []byte { |
132 url := strings.TrimLeft(r.URL.Path, "/") | 138 url := strings.TrimLeft(r.URL.Path, "/") |
133 body := resp.body | 139 body := resp.body |
134 » body = strings.Replace(body, "$ENDPOINT$", endpoint(), 1) | 140 » body = strings.Replace(body, "$ENDPOINT$", endpoint(), -1) |
fwereade
2012/12/12 15:18:44
Not sure it's a big benefit to replace only once -
dimitern
2012/12/12 17:32:24
Fair point, I'll make them global.
| |
135 » body = strings.Replace(body, "$URL$", url, 1) | 141 » body = strings.Replace(body, "$URL$", url, -1) |
136 if resp.errorText != "" { | 142 if resp.errorText != "" { |
137 » » body = strings.Replace(body, "$ERROR$", resp.errorText, 1) | 143 » » body = strings.Replace(body, "$ERROR$", resp.errorText, -1) |
138 } | 144 } |
139 return []byte(body) | 145 return []byte(body) |
140 } | 146 } |
141 | 147 |
142 // send serializes the response as needed and sends it. | 148 // send serializes the response as needed and sends it. |
143 func (resp response) send(w http.ResponseWriter, r *http.Request) { | 149 func (resp response) send(w http.ResponseWriter, r *http.Request) { |
144 if resp.contentType != "" { | 150 if resp.contentType != "" { |
145 w.Header().Set("Content-Type", resp.contentType) | 151 w.Header().Set("Content-Type", resp.contentType) |
146 } | 152 } |
147 var body []byte | 153 var body []byte |
148 if resp.body != "" { | 154 if resp.body != "" { |
fwereade
2012/12/12 15:18:44
Do we need this condition?
dimitern
2012/12/12 17:32:24
Yes, because I need to know the actual Content-Len
| |
149 body = resp.replaceVars(r) | 155 body = resp.replaceVars(r) |
150 } | 156 } |
151 // workaround for https://code.google.com/p/go/issues/detail?id=4454 | 157 // workaround for https://code.google.com/p/go/issues/detail?id=4454 |
152 w.Header().Set("Content-Length", strconv.Itoa(len(body))) | 158 w.Header().Set("Content-Length", strconv.Itoa(len(body))) |
153 if resp.code != 0 { | 159 if resp.code != 0 { |
154 w.WriteHeader(resp.code) | 160 w.WriteHeader(resp.code) |
155 } | 161 } |
156 if len(body) > 0 { | 162 if len(body) > 0 { |
157 w.Write(body) | 163 w.Write(body) |
158 } | 164 } |
159 } | 165 } |
160 | 166 |
161 // sendError responds with the given error to the given http request. | 167 // sendError responds with the given error to the given http request. |
162 func sendError(err error, w http.ResponseWriter, r *http.Request) { | 168 func sendError(err error, w http.ResponseWriter, r *http.Request) { |
163 eresp := errorResponse | 169 eresp := errorResponse |
fwereade
2012/12/12 15:18:44
Nice
dimitern
2012/12/12 17:32:24
Yeah!
| |
164 eresp.errorText = err.Error() | 170 eresp.errorText = err.Error() |
165 eresp.send(w, r) | 171 eresp.send(w, r) |
166 } | 172 } |
167 | 173 |
168 // sendJSON sends the specified response serialized as JSON. | 174 // sendJSON sends the specified response serialized as JSON. |
169 func sendJSON(code int, resp interface{}, w http.ResponseWriter, r *http.Request ) { | 175 func sendJSON(code int, resp interface{}, w http.ResponseWriter, r *http.Request ) { |
170 var data []byte | 176 var data []byte |
171 if resp != nil { | 177 if resp != nil { |
172 var err error | 178 var err error |
173 data, err = json.Marshal(resp) | 179 data, err = json.Marshal(resp) |
174 if err != nil { | 180 if err != nil { |
175 sendError(err, w, r) | 181 sendError(err, w, r) |
fwereade
2012/12/12 15:18:44
return?
dimitern
2012/12/12 17:32:24
(facepalm) of course!
| |
176 » » } | 182 » » » return |
177 » } | 183 » » } |
178 » if len(data) == 0 { | 184 » } |
rog
2012/12/12 15:02:33
why not just always set content length, as above?
dimitern
2012/12/12 17:32:24
Done.
| |
179 » » // workaround for https://code.google.com/p/go/issues/detail?id= 4454 | 185 » // workaround for https://code.google.com/p/go/issues/detail?id=4454 |
180 » » w.Header().Set("Content-Length", "0") | 186 » w.Header().Set("Content-Length", strconv.Itoa(len(data))) |
181 » } | |
182 w.WriteHeader(code) | 187 w.WriteHeader(code) |
183 w.Write(data) | 188 w.Write(data) |
184 } | 189 } |
185 | 190 |
186 // handleUnauthorizedNotFound is called for each request to check for | 191 // handleUnauthorizedNotFound is called for each request to check for |
187 // common errors (X-Auth-Token and trailing slash in URL). Returns | 192 // common errors (X-Auth-Token and trailing slash in URL). Returns |
188 // true if handled, false if no errors found. | 193 // true if it's OK, false if a response was sent. |
fwereade
2012/12/12 15:18:44
Maybe invert the bool's meaning? Conventions are a
dimitern
2012/12/12 17:32:24
Done.
| |
189 func (n *Nova) handleUnauthorizedNotFound(w http.ResponseWriter, r *http.Request ) bool { | 194 func (n *Nova) handleUnauthorizedNotFound(w http.ResponseWriter, r *http.Request ) bool { |
190 path := r.URL.Path | 195 path := r.URL.Path |
191 if r.Header.Get(authToken) != n.token { | 196 if r.Header.Get(authToken) != n.token { |
192 unauthorizedResponse.send(w, r) | 197 unauthorizedResponse.send(w, r) |
193 » » return true | 198 » » return false |
194 } | 199 } |
195 if strings.HasSuffix(path, "/") && path != "/" { | 200 if strings.HasSuffix(path, "/") && path != "/" { |
196 notFoundResponse.send(w, r) | 201 notFoundResponse.send(w, r) |
197 » » return true | 202 » » return false |
198 » } | 203 » } |
199 » return false | 204 » return true |
200 } | 205 } |
201 | 206 |
202 // handle registers the given Nova handler method for the URL prefix. | 207 // handle registers the given Nova handler method for the URL prefix. |
203 func (n *Nova) handle(prefix string, handler func(*Nova, http.ResponseWriter, *h ttp.Request)) http.Handler { | 208 func (n *Nova) handle(prefix string, handler func(*Nova, http.ResponseWriter, *h ttp.Request)) http.Handler { |
204 h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | 209 h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
205 » » if !n.handleUnauthorizedNotFound(w, r) { | 210 » » if n.handleUnauthorizedNotFound(w, r) { |
206 handler(n, w, r) | 211 handler(n, w, r) |
207 } | 212 } |
208 }) | 213 }) |
209 return http.StripPrefix(prefix, h) | 214 return http.StripPrefix(prefix, h) |
210 } | 215 } |
211 | 216 |
212 // respond returns an http Handler sending the given response. | 217 // respond returns an http Handler sending the given response. |
213 func (n *Nova) respond(resp response) http.Handler { | 218 func (n *Nova) respond(resp response) http.Handler { |
214 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | 219 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
215 » » if !n.handleUnauthorizedNotFound(w, r) { | 220 » » if n.handleUnauthorizedNotFound(w, r) { |
216 resp.send(w, r) | 221 resp.send(w, r) |
217 } | 222 } |
218 }) | 223 }) |
219 } | 224 } |
220 | 225 |
221 // handleFlavors handles the flavors HTTP API. | 226 // handleFlavors handles the flavors HTTP API. |
222 func (n *Nova) handleFlavors(w http.ResponseWriter, r *http.Request) { | 227 func (n *Nova) handleFlavors(w http.ResponseWriter, r *http.Request) { |
223 hasSlash := strings.Index(r.URL.Path, "/") != -1 | |
224 switch r.Method { | 228 switch r.Method { |
225 case "GET": | 229 case "GET": |
226 if hasSlash { | |
rog
2012/12/12 15:02:33
i don't think this can ever happen, because the ha
dimitern
2012/12/12 17:32:24
Fair enough, I checked and it's true. I'll remove
| |
227 notFoundJSONResponse.send(w, r) | |
228 return | |
229 } | |
230 entities := n.allFlavorsAsEntities() | 230 entities := n.allFlavorsAsEntities() |
231 if len(entities) == 0 { | 231 if len(entities) == 0 { |
232 sendJSON(http.StatusNoContent, nil, w, r) | 232 sendJSON(http.StatusNoContent, nil, w, r) |
233 return | 233 return |
234 } | 234 } |
235 var resp struct { | 235 var resp struct { |
236 Flavors []nova.Entity `json:"flavors"` | 236 Flavors []nova.Entity `json:"flavors"` |
237 } | 237 } |
238 resp.Flavors = entities | 238 resp.Flavors = entities |
fwereade
2012/12/12 15:18:44
resp := struct {
Flavors []nova.Entity
dimitern
2012/12/12 17:32:24
Only when unmarshaling the case does not matter. w
rog
2012/12/12 18:12:13
nope. json names are marshalled as is. it's only u
| |
239 sendJSON(http.StatusOK, resp, w, r) | 239 sendJSON(http.StatusOK, resp, w, r) |
240 case "POST": | 240 case "POST": |
241 if hasSlash { | |
242 notFoundResponse.send(w, r) | |
243 return | |
244 } | |
245 body, err := ioutil.ReadAll(r.Body) | 241 body, err := ioutil.ReadAll(r.Body) |
246 r.Body.Close() | 242 r.Body.Close() |
247 if err != nil { | 243 if err != nil { |
248 sendError(err, w, r) | 244 sendError(err, w, r) |
249 return | 245 return |
250 } | 246 } |
251 if len(body) == 0 { | 247 if len(body) == 0 { |
252 badRequest2Response.send(w, r) | 248 badRequest2Response.send(w, r) |
253 return | 249 return |
254 } | 250 } |
255 var flavor struct { | 251 var flavor struct { |
256 Flavor nova.FlavorDetail | 252 Flavor nova.FlavorDetail |
257 } | 253 } |
258 err = json.Unmarshal(body, &flavor) | 254 err = json.Unmarshal(body, &flavor) |
259 if err != nil { | 255 if err != nil { |
260 sendError(err, w, r) | 256 sendError(err, w, r) |
261 return | 257 return |
262 } | 258 } |
263 n.buildFlavorLinks(&flavor.Flavor) | 259 n.buildFlavorLinks(&flavor.Flavor) |
264 err = n.addFlavor(flavor.Flavor) | 260 err = n.addFlavor(flavor.Flavor) |
265 if err != nil { | 261 if err != nil { |
266 sendError(err, w, r) | 262 sendError(err, w, r) |
267 return | 263 return |
268 } | 264 } |
269 createdResponse.send(w, r) | 265 createdResponse.send(w, r) |
270 » case "PUT": | 266 » case "PUT", "DELETE": |
271 » » fallthrough | |
272 » case "DELETE": | |
fwereade
2012/12/12 15:18:44
case "PUT", "DELETE":
dimitern
2012/12/12 17:32:24
Done.
| |
273 notFoundResponse.send(w, r) | 267 notFoundResponse.send(w, r) |
274 default: | 268 default: |
275 panic("unknown request method: " + r.Method) | 269 panic("unknown request method: " + r.Method) |
276 } | 270 } |
277 } | 271 } |
278 | 272 |
279 // handleFlavorsDetail handles the flavors/detail HTTP API. | 273 // handleFlavorsDetail handles the flavors/detail HTTP API. |
280 func (n *Nova) handleFlavorsDetail(w http.ResponseWriter, r *http.Request) { | 274 func (n *Nova) handleFlavorsDetail(w http.ResponseWriter, r *http.Request) { |
281 hasSlash := strings.Count(r.URL.Path, "/") > 1 | |
282 switch r.Method { | 275 switch r.Method { |
283 case "GET": | 276 case "GET": |
284 if hasSlash { | |
285 notFoundJSONResponse.send(w, r) | |
286 return | |
287 } | |
288 flavors := n.allFlavors() | 277 flavors := n.allFlavors() |
289 if len(flavors) == 0 { | 278 if len(flavors) == 0 { |
290 sendJSON(http.StatusNoContent, nil, w, r) | 279 sendJSON(http.StatusNoContent, nil, w, r) |
291 return | 280 return |
292 } | 281 } |
293 var resp struct { | 282 var resp struct { |
294 Flavors []nova.FlavorDetail `json:"flavors"` | 283 Flavors []nova.FlavorDetail `json:"flavors"` |
295 } | 284 } |
296 resp.Flavors = flavors | 285 resp.Flavors = flavors |
fwereade
2012/12/12 15:18:44
This can be compressed as above.
dimitern
2012/12/12 17:32:24
See my comment above (if it's about the json tag).
| |
297 sendJSON(http.StatusOK, resp, w, r) | 286 sendJSON(http.StatusOK, resp, w, r) |
298 case "POST": | 287 case "POST": |
299 notFoundResponse.send(w, r) | 288 notFoundResponse.send(w, r) |
300 case "PUT": | 289 case "PUT": |
301 notFoundJSONResponse.send(w, r) | 290 notFoundJSONResponse.send(w, r) |
302 case "DELETE": | 291 case "DELETE": |
303 forbiddenResponse.send(w, r) | 292 forbiddenResponse.send(w, r) |
304 default: | 293 default: |
305 panic("unknown request method: " + r.Method) | 294 panic("unknown request method: " + r.Method) |
306 } | 295 } |
307 } | 296 } |
308 | 297 |
309 // setupHTTP attaches all the needed handlers to provide the HTTP API. | 298 // setupHTTP attaches all the needed handlers to provide the HTTP API. |
310 func (n *Nova) setupHTTP(mux *http.ServeMux) { | 299 func (n *Nova) setupHTTP(mux *http.ServeMux) { |
311 mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { | 300 mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { |
312 » » if n.handleUnauthorizedNotFound(w, r) { | 301 » » if !n.handleUnauthorizedNotFound(w, r) { |
313 return | 302 return |
314 } | 303 } |
315 if r.URL.Path == "/" { | 304 if r.URL.Path == "/" { |
316 noVersionResponse.send(w, r) | 305 noVersionResponse.send(w, r) |
317 } else { | 306 } else { |
318 multipleChoicesResponse.send(w, r) | 307 multipleChoicesResponse.send(w, r) |
319 } | 308 } |
320 }) | 309 }) |
321 » urlVersion := "/" + n.baseURL + "/" | 310 » urlVersion := "/" + n.versionPath + "/" |
fwereade
2012/12/12 15:18:44
This would be less surprising if the field were "b
dimitern
2012/12/12 17:32:24
Done.
| |
322 urlTenant := urlVersion + n.tenantId + "/" | 311 urlTenant := urlVersion + n.tenantId + "/" |
323 mux.Handle(urlVersion, n.respond(badRequestResponse)) | 312 mux.Handle(urlVersion, n.respond(badRequestResponse)) |
324 mux.HandleFunc(urlTenant, func(w http.ResponseWriter, r *http.Request) { | 313 mux.HandleFunc(urlTenant, func(w http.ResponseWriter, r *http.Request) { |
325 » » if n.handleUnauthorizedNotFound(w, r) { | 314 » » if !n.handleUnauthorizedNotFound(w, r) { |
326 return | 315 return |
327 } | 316 } |
328 // any unknown path | 317 // any unknown path |
329 notFoundResponse.send(w, r) | 318 notFoundResponse.send(w, r) |
330 }) | 319 }) |
331 mux.Handle(urlTenant+"flavors", n.handle(urlTenant, (*Nova).handleFlavor s)) | 320 mux.Handle(urlTenant+"flavors", n.handle(urlTenant, (*Nova).handleFlavor s)) |
332 mux.Handle(urlTenant+"flavors/detail", n.handle(urlTenant, (*Nova).handl eFlavorsDetail)) | 321 mux.Handle(urlTenant+"flavors/detail", n.handle(urlTenant, (*Nova).handl eFlavorsDetail)) |
333 } | 322 } |
LEFT | RIGHT |