github.com/hooklift/nomad@v0.5.7-0.20170407200202-db11e7dd7b55/api/api.go (about) 1 package api 2 3 import ( 4 "bytes" 5 "compress/gzip" 6 "crypto/tls" 7 "encoding/json" 8 "fmt" 9 "io" 10 "net/http" 11 "net/url" 12 "os" 13 "strconv" 14 "strings" 15 "time" 16 17 "github.com/hashicorp/go-cleanhttp" 18 rootcerts "github.com/hashicorp/go-rootcerts" 19 ) 20 21 // QueryOptions are used to parameterize a query 22 type QueryOptions struct { 23 // Providing a datacenter overwrites the region provided 24 // by the Config 25 Region string 26 27 // AllowStale allows any Nomad server (non-leader) to service 28 // a read. This allows for lower latency and higher throughput 29 AllowStale bool 30 31 // WaitIndex is used to enable a blocking query. Waits 32 // until the timeout or the next index is reached 33 WaitIndex uint64 34 35 // WaitTime is used to bound the duration of a wait. 36 // Defaults to that of the Config, but can be overridden. 37 WaitTime time.Duration 38 39 // If set, used as prefix for resource list searches 40 Prefix string 41 42 // Set HTTP parameters on the query. 43 Params map[string]string 44 } 45 46 // WriteOptions are used to parameterize a write 47 type WriteOptions struct { 48 // Providing a datacenter overwrites the region provided 49 // by the Config 50 Region string 51 } 52 53 // QueryMeta is used to return meta data about a query 54 type QueryMeta struct { 55 // LastIndex. This can be used as a WaitIndex to perform 56 // a blocking query 57 LastIndex uint64 58 59 // Time of last contact from the leader for the 60 // server servicing the request 61 LastContact time.Duration 62 63 // Is there a known leader 64 KnownLeader bool 65 66 // How long did the request take 67 RequestTime time.Duration 68 } 69 70 // WriteMeta is used to return meta data about a write 71 type WriteMeta struct { 72 // LastIndex. This can be used as a WaitIndex to perform 73 // a blocking query 74 LastIndex uint64 75 76 // How long did the request take 77 RequestTime time.Duration 78 } 79 80 // HttpBasicAuth is used to authenticate http client with HTTP Basic Authentication 81 type HttpBasicAuth struct { 82 // Username to use for HTTP Basic Authentication 83 Username string 84 85 // Password to use for HTTP Basic Authentication 86 Password string 87 } 88 89 // Config is used to configure the creation of a client 90 type Config struct { 91 // Address is the address of the Nomad agent 92 Address string 93 94 // Region to use. If not provided, the default agent region is used. 95 Region string 96 97 // HttpClient is the client to use. Default will be 98 // used if not provided. 99 HttpClient *http.Client 100 101 // HttpAuth is the auth info to use for http access. 102 HttpAuth *HttpBasicAuth 103 104 // WaitTime limits how long a Watch will block. If not provided, 105 // the agent default values will be used. 106 WaitTime time.Duration 107 108 // TLSConfig provides the various TLS related configurations for the http 109 // client 110 TLSConfig *TLSConfig 111 } 112 113 // CopyConfig copies the configuration with a new address 114 func (c *Config) CopyConfig(address string, tlsEnabled bool) *Config { 115 scheme := "http" 116 if tlsEnabled { 117 scheme = "https" 118 } 119 config := &Config{ 120 Address: fmt.Sprintf("%s://%s", scheme, address), 121 Region: c.Region, 122 HttpClient: c.HttpClient, 123 HttpAuth: c.HttpAuth, 124 WaitTime: c.WaitTime, 125 TLSConfig: c.TLSConfig, 126 } 127 128 return config 129 } 130 131 // TLSConfig contains the parameters needed to configure TLS on the HTTP client 132 // used to communicate with Nomad. 133 type TLSConfig struct { 134 // CACert is the path to a PEM-encoded CA cert file to use to verify the 135 // Nomad server SSL certificate. 136 CACert string 137 138 // CAPath is the path to a directory of PEM-encoded CA cert files to verify 139 // the Nomad server SSL certificate. 140 CAPath string 141 142 // ClientCert is the path to the certificate for Nomad communication 143 ClientCert string 144 145 // ClientKey is the path to the private key for Nomad communication 146 ClientKey string 147 148 // TLSServerName, if set, is used to set the SNI host when connecting via 149 // TLS. 150 TLSServerName string 151 152 // Insecure enables or disables SSL verification 153 Insecure bool 154 } 155 156 // DefaultConfig returns a default configuration for the client 157 func DefaultConfig() *Config { 158 config := &Config{ 159 Address: "http://127.0.0.1:4646", 160 HttpClient: cleanhttp.DefaultClient(), 161 TLSConfig: &TLSConfig{}, 162 } 163 transport := config.HttpClient.Transport.(*http.Transport) 164 transport.TLSHandshakeTimeout = 10 * time.Second 165 transport.TLSClientConfig = &tls.Config{ 166 MinVersion: tls.VersionTLS12, 167 } 168 169 if addr := os.Getenv("NOMAD_ADDR"); addr != "" { 170 config.Address = addr 171 } 172 if auth := os.Getenv("NOMAD_HTTP_AUTH"); auth != "" { 173 var username, password string 174 if strings.Contains(auth, ":") { 175 split := strings.SplitN(auth, ":", 2) 176 username = split[0] 177 password = split[1] 178 } else { 179 username = auth 180 } 181 182 config.HttpAuth = &HttpBasicAuth{ 183 Username: username, 184 Password: password, 185 } 186 } 187 188 // Read TLS specific env vars 189 if v := os.Getenv("NOMAD_CACERT"); v != "" { 190 config.TLSConfig.CACert = v 191 } 192 if v := os.Getenv("NOMAD_CAPATH"); v != "" { 193 config.TLSConfig.CAPath = v 194 } 195 if v := os.Getenv("NOMAD_CLIENT_CERT"); v != "" { 196 config.TLSConfig.ClientCert = v 197 } 198 if v := os.Getenv("NOMAD_CLIENT_KEY"); v != "" { 199 config.TLSConfig.ClientKey = v 200 } 201 if v := os.Getenv("NOMAD_SKIP_VERIFY"); v != "" { 202 if insecure, err := strconv.ParseBool(v); err == nil { 203 config.TLSConfig.Insecure = insecure 204 } 205 } 206 207 return config 208 } 209 210 // ConfigureTLS applies a set of TLS configurations to the the HTTP client. 211 func (c *Config) ConfigureTLS() error { 212 if c.HttpClient == nil { 213 return fmt.Errorf("config HTTP Client must be set") 214 } 215 216 var clientCert tls.Certificate 217 foundClientCert := false 218 if c.TLSConfig.ClientCert != "" || c.TLSConfig.ClientKey != "" { 219 if c.TLSConfig.ClientCert != "" && c.TLSConfig.ClientKey != "" { 220 var err error 221 clientCert, err = tls.LoadX509KeyPair(c.TLSConfig.ClientCert, c.TLSConfig.ClientKey) 222 if err != nil { 223 return err 224 } 225 foundClientCert = true 226 } else { 227 return fmt.Errorf("Both client cert and client key must be provided") 228 } 229 } 230 231 clientTLSConfig := c.HttpClient.Transport.(*http.Transport).TLSClientConfig 232 rootConfig := &rootcerts.Config{ 233 CAFile: c.TLSConfig.CACert, 234 CAPath: c.TLSConfig.CAPath, 235 } 236 if err := rootcerts.ConfigureTLS(clientTLSConfig, rootConfig); err != nil { 237 return err 238 } 239 240 clientTLSConfig.InsecureSkipVerify = c.TLSConfig.Insecure 241 242 if foundClientCert { 243 clientTLSConfig.Certificates = []tls.Certificate{clientCert} 244 } 245 if c.TLSConfig.TLSServerName != "" { 246 clientTLSConfig.ServerName = c.TLSConfig.TLSServerName 247 } 248 249 return nil 250 } 251 252 // Client provides a client to the Nomad API 253 type Client struct { 254 config Config 255 } 256 257 // NewClient returns a new client 258 func NewClient(config *Config) (*Client, error) { 259 // bootstrap the config 260 defConfig := DefaultConfig() 261 262 if config.Address == "" { 263 config.Address = defConfig.Address 264 } else if _, err := url.Parse(config.Address); err != nil { 265 return nil, fmt.Errorf("invalid address '%s': %v", config.Address, err) 266 } 267 268 if config.HttpClient == nil { 269 config.HttpClient = defConfig.HttpClient 270 } 271 272 // Configure the TLS cofigurations 273 if err := config.ConfigureTLS(); err != nil { 274 return nil, err 275 } 276 277 client := &Client{ 278 config: *config, 279 } 280 return client, nil 281 } 282 283 // SetRegion sets the region to forward API requests to. 284 func (c *Client) SetRegion(region string) { 285 c.config.Region = region 286 } 287 288 // request is used to help build up a request 289 type request struct { 290 config *Config 291 method string 292 url *url.URL 293 params url.Values 294 body io.Reader 295 obj interface{} 296 } 297 298 // setQueryOptions is used to annotate the request with 299 // additional query options 300 func (r *request) setQueryOptions(q *QueryOptions) { 301 if q == nil { 302 return 303 } 304 if q.Region != "" { 305 r.params.Set("region", q.Region) 306 } 307 if q.AllowStale { 308 r.params.Set("stale", "") 309 } 310 if q.WaitIndex != 0 { 311 r.params.Set("index", strconv.FormatUint(q.WaitIndex, 10)) 312 } 313 if q.WaitTime != 0 { 314 r.params.Set("wait", durToMsec(q.WaitTime)) 315 } 316 if q.Prefix != "" { 317 r.params.Set("prefix", q.Prefix) 318 } 319 for k, v := range q.Params { 320 r.params.Set(k, v) 321 } 322 } 323 324 // durToMsec converts a duration to a millisecond specified string 325 func durToMsec(dur time.Duration) string { 326 return fmt.Sprintf("%dms", dur/time.Millisecond) 327 } 328 329 // setWriteOptions is used to annotate the request with 330 // additional write options 331 func (r *request) setWriteOptions(q *WriteOptions) { 332 if q == nil { 333 return 334 } 335 if q.Region != "" { 336 r.params.Set("region", q.Region) 337 } 338 } 339 340 // toHTTP converts the request to an HTTP request 341 func (r *request) toHTTP() (*http.Request, error) { 342 // Encode the query parameters 343 r.url.RawQuery = r.params.Encode() 344 345 // Check if we should encode the body 346 if r.body == nil && r.obj != nil { 347 if b, err := encodeBody(r.obj); err != nil { 348 return nil, err 349 } else { 350 r.body = b 351 } 352 } 353 354 // Create the HTTP request 355 req, err := http.NewRequest(r.method, r.url.RequestURI(), r.body) 356 if err != nil { 357 return nil, err 358 } 359 360 // Optionally configure HTTP basic authentication 361 if r.url.User != nil { 362 username := r.url.User.Username() 363 password, _ := r.url.User.Password() 364 req.SetBasicAuth(username, password) 365 } else if r.config.HttpAuth != nil { 366 req.SetBasicAuth(r.config.HttpAuth.Username, r.config.HttpAuth.Password) 367 } 368 369 req.Header.Add("Accept-Encoding", "gzip") 370 req.URL.Host = r.url.Host 371 req.URL.Scheme = r.url.Scheme 372 req.Host = r.url.Host 373 return req, nil 374 } 375 376 // newRequest is used to create a new request 377 func (c *Client) newRequest(method, path string) (*request, error) { 378 base, _ := url.Parse(c.config.Address) 379 u, err := url.Parse(path) 380 if err != nil { 381 return nil, err 382 } 383 r := &request{ 384 config: &c.config, 385 method: method, 386 url: &url.URL{ 387 Scheme: base.Scheme, 388 User: base.User, 389 Host: base.Host, 390 Path: u.Path, 391 }, 392 params: make(map[string][]string), 393 } 394 if c.config.Region != "" { 395 r.params.Set("region", c.config.Region) 396 } 397 if c.config.WaitTime != 0 { 398 r.params.Set("wait", durToMsec(r.config.WaitTime)) 399 } 400 401 // Add in the query parameters, if any 402 for key, values := range u.Query() { 403 for _, value := range values { 404 r.params.Add(key, value) 405 } 406 } 407 408 return r, nil 409 } 410 411 // multiCloser is to wrap a ReadCloser such that when close is called, multiple 412 // Closes occur. 413 type multiCloser struct { 414 reader io.Reader 415 inorderClose []io.Closer 416 } 417 418 func (m *multiCloser) Close() error { 419 for _, c := range m.inorderClose { 420 if err := c.Close(); err != nil { 421 return err 422 } 423 } 424 return nil 425 } 426 427 func (m *multiCloser) Read(p []byte) (int, error) { 428 return m.reader.Read(p) 429 } 430 431 // doRequest runs a request with our client 432 func (c *Client) doRequest(r *request) (time.Duration, *http.Response, error) { 433 req, err := r.toHTTP() 434 if err != nil { 435 return 0, nil, err 436 } 437 start := time.Now() 438 resp, err := c.config.HttpClient.Do(req) 439 diff := time.Now().Sub(start) 440 441 // If the response is compressed, we swap the body's reader. 442 if resp != nil && resp.Header != nil { 443 var reader io.ReadCloser 444 switch resp.Header.Get("Content-Encoding") { 445 case "gzip": 446 greader, err := gzip.NewReader(resp.Body) 447 if err != nil { 448 return 0, nil, err 449 } 450 451 // The gzip reader doesn't close the wrapped reader so we use 452 // multiCloser. 453 reader = &multiCloser{ 454 reader: greader, 455 inorderClose: []io.Closer{greader, resp.Body}, 456 } 457 default: 458 reader = resp.Body 459 } 460 resp.Body = reader 461 } 462 463 return diff, resp, err 464 } 465 466 // rawQuery makes a GET request to the specified endpoint but returns just the 467 // response body. 468 func (c *Client) rawQuery(endpoint string, q *QueryOptions) (io.ReadCloser, error) { 469 r, err := c.newRequest("GET", endpoint) 470 if err != nil { 471 return nil, err 472 } 473 r.setQueryOptions(q) 474 _, resp, err := requireOK(c.doRequest(r)) 475 if err != nil { 476 return nil, err 477 } 478 479 return resp.Body, nil 480 } 481 482 // Query is used to do a GET request against an endpoint 483 // and deserialize the response into an interface using 484 // standard Nomad conventions. 485 func (c *Client) query(endpoint string, out interface{}, q *QueryOptions) (*QueryMeta, error) { 486 r, err := c.newRequest("GET", endpoint) 487 if err != nil { 488 return nil, err 489 } 490 r.setQueryOptions(q) 491 rtt, resp, err := requireOK(c.doRequest(r)) 492 if err != nil { 493 return nil, err 494 } 495 defer resp.Body.Close() 496 497 qm := &QueryMeta{} 498 parseQueryMeta(resp, qm) 499 qm.RequestTime = rtt 500 501 if err := decodeBody(resp, out); err != nil { 502 return nil, err 503 } 504 return qm, nil 505 } 506 507 // write is used to do a PUT request against an endpoint 508 // and serialize/deserialized using the standard Nomad conventions. 509 func (c *Client) write(endpoint string, in, out interface{}, q *WriteOptions) (*WriteMeta, error) { 510 r, err := c.newRequest("PUT", endpoint) 511 if err != nil { 512 return nil, err 513 } 514 r.setWriteOptions(q) 515 r.obj = in 516 rtt, resp, err := requireOK(c.doRequest(r)) 517 if err != nil { 518 return nil, err 519 } 520 defer resp.Body.Close() 521 522 wm := &WriteMeta{RequestTime: rtt} 523 parseWriteMeta(resp, wm) 524 525 if out != nil { 526 if err := decodeBody(resp, &out); err != nil { 527 return nil, err 528 } 529 } 530 return wm, nil 531 } 532 533 // delete is used to do a DELETE request against an endpoint 534 // and serialize/deserialized using the standard Nomad conventions. 535 func (c *Client) delete(endpoint string, out interface{}, q *WriteOptions) (*WriteMeta, error) { 536 r, err := c.newRequest("DELETE", endpoint) 537 if err != nil { 538 return nil, err 539 } 540 r.setWriteOptions(q) 541 rtt, resp, err := requireOK(c.doRequest(r)) 542 if err != nil { 543 return nil, err 544 } 545 defer resp.Body.Close() 546 547 wm := &WriteMeta{RequestTime: rtt} 548 parseWriteMeta(resp, wm) 549 550 if out != nil { 551 if err := decodeBody(resp, &out); err != nil { 552 return nil, err 553 } 554 } 555 return wm, nil 556 } 557 558 // parseQueryMeta is used to help parse query meta-data 559 func parseQueryMeta(resp *http.Response, q *QueryMeta) error { 560 header := resp.Header 561 562 // Parse the X-Nomad-Index 563 index, err := strconv.ParseUint(header.Get("X-Nomad-Index"), 10, 64) 564 if err != nil { 565 return fmt.Errorf("Failed to parse X-Nomad-Index: %v", err) 566 } 567 q.LastIndex = index 568 569 // Parse the X-Nomad-LastContact 570 last, err := strconv.ParseUint(header.Get("X-Nomad-LastContact"), 10, 64) 571 if err != nil { 572 return fmt.Errorf("Failed to parse X-Nomad-LastContact: %v", err) 573 } 574 q.LastContact = time.Duration(last) * time.Millisecond 575 576 // Parse the X-Nomad-KnownLeader 577 switch header.Get("X-Nomad-KnownLeader") { 578 case "true": 579 q.KnownLeader = true 580 default: 581 q.KnownLeader = false 582 } 583 return nil 584 } 585 586 // parseWriteMeta is used to help parse write meta-data 587 func parseWriteMeta(resp *http.Response, q *WriteMeta) error { 588 header := resp.Header 589 590 // Parse the X-Nomad-Index 591 index, err := strconv.ParseUint(header.Get("X-Nomad-Index"), 10, 64) 592 if err != nil { 593 return fmt.Errorf("Failed to parse X-Nomad-Index: %v", err) 594 } 595 q.LastIndex = index 596 return nil 597 } 598 599 // decodeBody is used to JSON decode a body 600 func decodeBody(resp *http.Response, out interface{}) error { 601 dec := json.NewDecoder(resp.Body) 602 return dec.Decode(out) 603 } 604 605 // encodeBody is used to encode a request body 606 func encodeBody(obj interface{}) (io.Reader, error) { 607 buf := bytes.NewBuffer(nil) 608 enc := json.NewEncoder(buf) 609 if err := enc.Encode(obj); err != nil { 610 return nil, err 611 } 612 return buf, nil 613 } 614 615 // requireOK is used to wrap doRequest and check for a 200 616 func requireOK(d time.Duration, resp *http.Response, e error) (time.Duration, *http.Response, error) { 617 if e != nil { 618 if resp != nil { 619 resp.Body.Close() 620 } 621 return d, nil, e 622 } 623 if resp.StatusCode != 200 { 624 var buf bytes.Buffer 625 io.Copy(&buf, resp.Body) 626 resp.Body.Close() 627 return d, nil, fmt.Errorf("Unexpected response code: %d (%s)", resp.StatusCode, buf.Bytes()) 628 } 629 return d, resp, nil 630 }