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