github.com/dkerwin/nomad@v0.3.3-0.20160525181927-74554135514b/api/api.go (about) 1 package api 2 3 import ( 4 "bytes" 5 "compress/gzip" 6 "encoding/json" 7 "fmt" 8 "io" 9 "net/http" 10 "net/url" 11 "os" 12 "strconv" 13 "time" 14 15 "github.com/hashicorp/go-cleanhttp" 16 ) 17 18 // QueryOptions are used to parameterize a query 19 type QueryOptions struct { 20 // Providing a datacenter overwrites the region provided 21 // by the Config 22 Region string 23 24 // AllowStale allows any Nomad server (non-leader) to service 25 // a read. This allows for lower latency and higher throughput 26 AllowStale bool 27 28 // WaitIndex is used to enable a blocking query. Waits 29 // until the timeout or the next index is reached 30 WaitIndex uint64 31 32 // WaitTime is used to bound the duration of a wait. 33 // Defaults to that of the Config, but can be overridden. 34 WaitTime time.Duration 35 36 // If set, used as prefix for resource list searches 37 Prefix string 38 } 39 40 // WriteOptions are used to parameterize a write 41 type WriteOptions struct { 42 // Providing a datacenter overwrites the region provided 43 // by the Config 44 Region string 45 } 46 47 // QueryMeta is used to return meta data about a query 48 type QueryMeta struct { 49 // LastIndex. This can be used as a WaitIndex to perform 50 // a blocking query 51 LastIndex uint64 52 53 // Time of last contact from the leader for the 54 // server servicing the request 55 LastContact time.Duration 56 57 // Is there a known leader 58 KnownLeader bool 59 60 // How long did the request take 61 RequestTime time.Duration 62 } 63 64 // WriteMeta is used to return meta data about a write 65 type WriteMeta struct { 66 // LastIndex. This can be used as a WaitIndex to perform 67 // a blocking query 68 LastIndex uint64 69 70 // How long did the request take 71 RequestTime time.Duration 72 } 73 74 // Config is used to configure the creation of a client 75 type Config struct { 76 // Address is the address of the Nomad agent 77 Address string 78 79 // Region to use. If not provided, the default agent region is used. 80 Region string 81 82 // HttpClient is the client to use. Default will be 83 // used if not provided. 84 HttpClient *http.Client 85 86 // WaitTime limits how long a Watch will block. If not provided, 87 // the agent default values will be used. 88 WaitTime time.Duration 89 } 90 91 // DefaultConfig returns a default configuration for the client 92 func DefaultConfig() *Config { 93 config := &Config{ 94 Address: "http://127.0.0.1:4646", 95 HttpClient: cleanhttp.DefaultClient(), 96 } 97 if addr := os.Getenv("NOMAD_ADDR"); addr != "" { 98 config.Address = addr 99 } 100 return config 101 } 102 103 // Client provides a client to the Nomad API 104 type Client struct { 105 config Config 106 } 107 108 // NewClient returns a new client 109 func NewClient(config *Config) (*Client, error) { 110 // bootstrap the config 111 defConfig := DefaultConfig() 112 113 if config.Address == "" { 114 config.Address = defConfig.Address 115 } else if _, err := url.Parse(config.Address); err != nil { 116 return nil, fmt.Errorf("invalid address '%s': %v", config.Address, err) 117 } 118 119 if config.HttpClient == nil { 120 config.HttpClient = defConfig.HttpClient 121 } 122 123 client := &Client{ 124 config: *config, 125 } 126 return client, nil 127 } 128 129 // request is used to help build up a request 130 type request struct { 131 config *Config 132 method string 133 url *url.URL 134 params url.Values 135 body io.Reader 136 obj interface{} 137 } 138 139 // setQueryOptions is used to annotate the request with 140 // additional query options 141 func (r *request) setQueryOptions(q *QueryOptions) { 142 if q == nil { 143 return 144 } 145 if q.Region != "" { 146 r.params.Set("region", q.Region) 147 } 148 if q.AllowStale { 149 r.params.Set("stale", "") 150 } 151 if q.WaitIndex != 0 { 152 r.params.Set("index", strconv.FormatUint(q.WaitIndex, 10)) 153 } 154 if q.WaitTime != 0 { 155 r.params.Set("wait", durToMsec(q.WaitTime)) 156 } 157 if q.Prefix != "" { 158 r.params.Set("prefix", q.Prefix) 159 } 160 } 161 162 // durToMsec converts a duration to a millisecond specified string 163 func durToMsec(dur time.Duration) string { 164 return fmt.Sprintf("%dms", dur/time.Millisecond) 165 } 166 167 // setWriteOptions is used to annotate the request with 168 // additional write options 169 func (r *request) setWriteOptions(q *WriteOptions) { 170 if q == nil { 171 return 172 } 173 if q.Region != "" { 174 r.params.Set("region", q.Region) 175 } 176 } 177 178 // toHTTP converts the request to an HTTP request 179 func (r *request) toHTTP() (*http.Request, error) { 180 // Encode the query parameters 181 r.url.RawQuery = r.params.Encode() 182 183 // Check if we should encode the body 184 if r.body == nil && r.obj != nil { 185 if b, err := encodeBody(r.obj); err != nil { 186 return nil, err 187 } else { 188 r.body = b 189 } 190 } 191 192 // Create the HTTP request 193 req, err := http.NewRequest(r.method, r.url.RequestURI(), r.body) 194 if err != nil { 195 return nil, err 196 } 197 198 req.Header.Add("Accept-Encoding", "gzip") 199 req.URL.Host = r.url.Host 200 req.URL.Scheme = r.url.Scheme 201 req.Host = r.url.Host 202 return req, nil 203 } 204 205 // newRequest is used to create a new request 206 func (c *Client) newRequest(method, path string) *request { 207 base, _ := url.Parse(c.config.Address) 208 u, _ := url.Parse(path) 209 r := &request{ 210 config: &c.config, 211 method: method, 212 url: &url.URL{ 213 Scheme: base.Scheme, 214 Host: base.Host, 215 Path: u.Path, 216 }, 217 params: make(map[string][]string), 218 } 219 if c.config.Region != "" { 220 r.params.Set("region", c.config.Region) 221 } 222 if c.config.WaitTime != 0 { 223 r.params.Set("wait", durToMsec(r.config.WaitTime)) 224 } 225 226 // Add in the query parameters, if any 227 for key, values := range u.Query() { 228 for _, value := range values { 229 r.params.Add(key, value) 230 } 231 } 232 233 return r 234 } 235 236 // multiCloser is to wrap a ReadCloser such that when close is called, multiple 237 // Closes occur. 238 type multiCloser struct { 239 reader io.Reader 240 inorderClose []io.Closer 241 } 242 243 func (m *multiCloser) Close() error { 244 for _, c := range m.inorderClose { 245 if err := c.Close(); err != nil { 246 return err 247 } 248 } 249 return nil 250 } 251 252 func (m *multiCloser) Read(p []byte) (int, error) { 253 return m.reader.Read(p) 254 } 255 256 // doRequest runs a request with our client 257 func (c *Client) doRequest(r *request) (time.Duration, *http.Response, error) { 258 req, err := r.toHTTP() 259 if err != nil { 260 return 0, nil, err 261 } 262 start := time.Now() 263 resp, err := c.config.HttpClient.Do(req) 264 diff := time.Now().Sub(start) 265 266 // If the response is compressed, we swap the body's reader. 267 if resp != nil && resp.Header != nil { 268 var reader io.ReadCloser 269 switch resp.Header.Get("Content-Encoding") { 270 case "gzip": 271 greader, err := gzip.NewReader(resp.Body) 272 if err != nil { 273 return 0, nil, err 274 } 275 276 // The gzip reader doesn't close the wrapped reader so we use 277 // multiCloser. 278 reader = &multiCloser{ 279 reader: greader, 280 inorderClose: []io.Closer{greader, resp.Body}, 281 } 282 default: 283 reader = resp.Body 284 } 285 resp.Body = reader 286 } 287 288 return diff, resp, err 289 } 290 291 // Query is used to do a GET request against an endpoint 292 // and deserialize the response into an interface using 293 // standard Nomad conventions. 294 func (c *Client) query(endpoint string, out interface{}, q *QueryOptions) (*QueryMeta, error) { 295 r := c.newRequest("GET", endpoint) 296 r.setQueryOptions(q) 297 rtt, resp, err := requireOK(c.doRequest(r)) 298 if err != nil { 299 return nil, err 300 } 301 defer resp.Body.Close() 302 303 qm := &QueryMeta{} 304 parseQueryMeta(resp, qm) 305 qm.RequestTime = rtt 306 307 if err := decodeBody(resp, out); err != nil { 308 return nil, err 309 } 310 return qm, nil 311 } 312 313 // write is used to do a PUT request against an endpoint 314 // and serialize/deserialized using the standard Nomad conventions. 315 func (c *Client) write(endpoint string, in, out interface{}, q *WriteOptions) (*WriteMeta, error) { 316 r := c.newRequest("PUT", endpoint) 317 r.setWriteOptions(q) 318 r.obj = in 319 rtt, resp, err := requireOK(c.doRequest(r)) 320 if err != nil { 321 return nil, err 322 } 323 defer resp.Body.Close() 324 325 wm := &WriteMeta{RequestTime: rtt} 326 parseWriteMeta(resp, wm) 327 328 if out != nil { 329 if err := decodeBody(resp, &out); err != nil { 330 return nil, err 331 } 332 } 333 return wm, nil 334 } 335 336 // write is used to do a PUT request against an endpoint 337 // and serialize/deserialized using the standard Nomad conventions. 338 func (c *Client) delete(endpoint string, out interface{}, q *WriteOptions) (*WriteMeta, error) { 339 r := c.newRequest("DELETE", endpoint) 340 r.setWriteOptions(q) 341 rtt, resp, err := requireOK(c.doRequest(r)) 342 if err != nil { 343 return nil, err 344 } 345 defer resp.Body.Close() 346 347 wm := &WriteMeta{RequestTime: rtt} 348 parseWriteMeta(resp, wm) 349 350 if out != nil { 351 if err := decodeBody(resp, &out); err != nil { 352 return nil, err 353 } 354 } 355 return wm, nil 356 } 357 358 // parseQueryMeta is used to help parse query meta-data 359 func parseQueryMeta(resp *http.Response, q *QueryMeta) error { 360 header := resp.Header 361 362 // Parse the X-Nomad-Index 363 index, err := strconv.ParseUint(header.Get("X-Nomad-Index"), 10, 64) 364 if err != nil { 365 return fmt.Errorf("Failed to parse X-Nomad-Index: %v", err) 366 } 367 q.LastIndex = index 368 369 // Parse the X-Nomad-LastContact 370 last, err := strconv.ParseUint(header.Get("X-Nomad-LastContact"), 10, 64) 371 if err != nil { 372 return fmt.Errorf("Failed to parse X-Nomad-LastContact: %v", err) 373 } 374 q.LastContact = time.Duration(last) * time.Millisecond 375 376 // Parse the X-Nomad-KnownLeader 377 switch header.Get("X-Nomad-KnownLeader") { 378 case "true": 379 q.KnownLeader = true 380 default: 381 q.KnownLeader = false 382 } 383 return nil 384 } 385 386 // parseWriteMeta is used to help parse write meta-data 387 func parseWriteMeta(resp *http.Response, q *WriteMeta) error { 388 header := resp.Header 389 390 // Parse the X-Nomad-Index 391 index, err := strconv.ParseUint(header.Get("X-Nomad-Index"), 10, 64) 392 if err != nil { 393 return fmt.Errorf("Failed to parse X-Nomad-Index: %v", err) 394 } 395 q.LastIndex = index 396 return nil 397 } 398 399 // decodeBody is used to JSON decode a body 400 func decodeBody(resp *http.Response, out interface{}) error { 401 dec := json.NewDecoder(resp.Body) 402 return dec.Decode(out) 403 } 404 405 // encodeBody is used to encode a request body 406 func encodeBody(obj interface{}) (io.Reader, error) { 407 buf := bytes.NewBuffer(nil) 408 enc := json.NewEncoder(buf) 409 if err := enc.Encode(obj); err != nil { 410 return nil, err 411 } 412 return buf, nil 413 } 414 415 // requireOK is used to wrap doRequest and check for a 200 416 func requireOK(d time.Duration, resp *http.Response, e error) (time.Duration, *http.Response, error) { 417 if e != nil { 418 if resp != nil { 419 resp.Body.Close() 420 } 421 return d, nil, e 422 } 423 if resp.StatusCode != 200 { 424 var buf bytes.Buffer 425 io.Copy(&buf, resp.Body) 426 resp.Body.Close() 427 return d, nil, fmt.Errorf("Unexpected response code: %d (%s)", resp.StatusCode, buf.Bytes()) 428 } 429 return d, resp, nil 430 }