Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code | Sign in
(558)

Delta Between Two Patch Sets: testservices/novaservice/service_http.go

Issue 6924043: Second phase of nova testing service: HTTP API. (Closed)
Left Patch Set: Second phase of nova testing service: HTTP aPI. Created 11 years, 3 months ago
Right Patch Set: Second phase of nova testing service: HTTP API. Created 11 years, 3 months ago
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments. Please Sign in to add in-line comments.
Jump to:
Left: Side by side diff | Download
Right: Side by side diff | Download
LEFTRIGHT
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
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 }
LEFTRIGHT

Powered by Google App Engine
RSS Feeds Recent Issues | This issue
This is Rietveld f62528b