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  }