github.com/maier/nomad@v0.4.1-0.20161110003312-a9e3d0b8549d/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 if c.TLSConfig.ClientCert != "" || c.TLSConfig.ClientKey != "" {
   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 {
   378  	base, _ := url.Parse(c.config.Address)
   379  	u, _ := url.Parse(path)
   380  	r := &request{
   381  		config: &c.config,
   382  		method: method,
   383  		url: &url.URL{
   384  			Scheme: base.Scheme,
   385  			User:   base.User,
   386  			Host:   base.Host,
   387  			Path:   u.Path,
   388  		},
   389  		params: make(map[string][]string),
   390  	}
   391  	if c.config.Region != "" {
   392  		r.params.Set("region", c.config.Region)
   393  	}
   394  	if c.config.WaitTime != 0 {
   395  		r.params.Set("wait", durToMsec(r.config.WaitTime))
   396  	}
   397  
   398  	// Add in the query parameters, if any
   399  	for key, values := range u.Query() {
   400  		for _, value := range values {
   401  			r.params.Add(key, value)
   402  		}
   403  	}
   404  
   405  	return r
   406  }
   407  
   408  // multiCloser is to wrap a ReadCloser such that when close is called, multiple
   409  // Closes occur.
   410  type multiCloser struct {
   411  	reader       io.Reader
   412  	inorderClose []io.Closer
   413  }
   414  
   415  func (m *multiCloser) Close() error {
   416  	for _, c := range m.inorderClose {
   417  		if err := c.Close(); err != nil {
   418  			return err
   419  		}
   420  	}
   421  	return nil
   422  }
   423  
   424  func (m *multiCloser) Read(p []byte) (int, error) {
   425  	return m.reader.Read(p)
   426  }
   427  
   428  // doRequest runs a request with our client
   429  func (c *Client) doRequest(r *request) (time.Duration, *http.Response, error) {
   430  	req, err := r.toHTTP()
   431  	if err != nil {
   432  		return 0, nil, err
   433  	}
   434  	start := time.Now()
   435  	resp, err := c.config.HttpClient.Do(req)
   436  	diff := time.Now().Sub(start)
   437  
   438  	// If the response is compressed, we swap the body's reader.
   439  	if resp != nil && resp.Header != nil {
   440  		var reader io.ReadCloser
   441  		switch resp.Header.Get("Content-Encoding") {
   442  		case "gzip":
   443  			greader, err := gzip.NewReader(resp.Body)
   444  			if err != nil {
   445  				return 0, nil, err
   446  			}
   447  
   448  			// The gzip reader doesn't close the wrapped reader so we use
   449  			// multiCloser.
   450  			reader = &multiCloser{
   451  				reader:       greader,
   452  				inorderClose: []io.Closer{greader, resp.Body},
   453  			}
   454  		default:
   455  			reader = resp.Body
   456  		}
   457  		resp.Body = reader
   458  	}
   459  
   460  	return diff, resp, err
   461  }
   462  
   463  // rawQuery makes a GET request to the specified endpoint but returns just the
   464  // response body.
   465  func (c *Client) rawQuery(endpoint string, q *QueryOptions) (io.ReadCloser, error) {
   466  	r := c.newRequest("GET", endpoint)
   467  	r.setQueryOptions(q)
   468  	_, resp, err := requireOK(c.doRequest(r))
   469  	if err != nil {
   470  		return nil, err
   471  	}
   472  
   473  	return resp.Body, nil
   474  }
   475  
   476  // Query is used to do a GET request against an endpoint
   477  // and deserialize the response into an interface using
   478  // standard Nomad conventions.
   479  func (c *Client) query(endpoint string, out interface{}, q *QueryOptions) (*QueryMeta, error) {
   480  	r := c.newRequest("GET", endpoint)
   481  	r.setQueryOptions(q)
   482  	rtt, resp, err := requireOK(c.doRequest(r))
   483  	if err != nil {
   484  		return nil, err
   485  	}
   486  	defer resp.Body.Close()
   487  
   488  	qm := &QueryMeta{}
   489  	parseQueryMeta(resp, qm)
   490  	qm.RequestTime = rtt
   491  
   492  	if err := decodeBody(resp, out); err != nil {
   493  		return nil, err
   494  	}
   495  	return qm, nil
   496  }
   497  
   498  // write is used to do a PUT request against an endpoint
   499  // and serialize/deserialized using the standard Nomad conventions.
   500  func (c *Client) write(endpoint string, in, out interface{}, q *WriteOptions) (*WriteMeta, error) {
   501  	r := c.newRequest("PUT", endpoint)
   502  	r.setWriteOptions(q)
   503  	r.obj = in
   504  	rtt, resp, err := requireOK(c.doRequest(r))
   505  	if err != nil {
   506  		return nil, err
   507  	}
   508  	defer resp.Body.Close()
   509  
   510  	wm := &WriteMeta{RequestTime: rtt}
   511  	parseWriteMeta(resp, wm)
   512  
   513  	if out != nil {
   514  		if err := decodeBody(resp, &out); err != nil {
   515  			return nil, err
   516  		}
   517  	}
   518  	return wm, nil
   519  }
   520  
   521  // write is used to do a PUT request against an endpoint
   522  // and serialize/deserialized using the standard Nomad conventions.
   523  func (c *Client) delete(endpoint string, out interface{}, q *WriteOptions) (*WriteMeta, error) {
   524  	r := c.newRequest("DELETE", endpoint)
   525  	r.setWriteOptions(q)
   526  	rtt, resp, err := requireOK(c.doRequest(r))
   527  	if err != nil {
   528  		return nil, err
   529  	}
   530  	defer resp.Body.Close()
   531  
   532  	wm := &WriteMeta{RequestTime: rtt}
   533  	parseWriteMeta(resp, wm)
   534  
   535  	if out != nil {
   536  		if err := decodeBody(resp, &out); err != nil {
   537  			return nil, err
   538  		}
   539  	}
   540  	return wm, nil
   541  }
   542  
   543  // parseQueryMeta is used to help parse query meta-data
   544  func parseQueryMeta(resp *http.Response, q *QueryMeta) error {
   545  	header := resp.Header
   546  
   547  	// Parse the X-Nomad-Index
   548  	index, err := strconv.ParseUint(header.Get("X-Nomad-Index"), 10, 64)
   549  	if err != nil {
   550  		return fmt.Errorf("Failed to parse X-Nomad-Index: %v", err)
   551  	}
   552  	q.LastIndex = index
   553  
   554  	// Parse the X-Nomad-LastContact
   555  	last, err := strconv.ParseUint(header.Get("X-Nomad-LastContact"), 10, 64)
   556  	if err != nil {
   557  		return fmt.Errorf("Failed to parse X-Nomad-LastContact: %v", err)
   558  	}
   559  	q.LastContact = time.Duration(last) * time.Millisecond
   560  
   561  	// Parse the X-Nomad-KnownLeader
   562  	switch header.Get("X-Nomad-KnownLeader") {
   563  	case "true":
   564  		q.KnownLeader = true
   565  	default:
   566  		q.KnownLeader = false
   567  	}
   568  	return nil
   569  }
   570  
   571  // parseWriteMeta is used to help parse write meta-data
   572  func parseWriteMeta(resp *http.Response, q *WriteMeta) error {
   573  	header := resp.Header
   574  
   575  	// Parse the X-Nomad-Index
   576  	index, err := strconv.ParseUint(header.Get("X-Nomad-Index"), 10, 64)
   577  	if err != nil {
   578  		return fmt.Errorf("Failed to parse X-Nomad-Index: %v", err)
   579  	}
   580  	q.LastIndex = index
   581  	return nil
   582  }
   583  
   584  // decodeBody is used to JSON decode a body
   585  func decodeBody(resp *http.Response, out interface{}) error {
   586  	dec := json.NewDecoder(resp.Body)
   587  	return dec.Decode(out)
   588  }
   589  
   590  // encodeBody is used to encode a request body
   591  func encodeBody(obj interface{}) (io.Reader, error) {
   592  	buf := bytes.NewBuffer(nil)
   593  	enc := json.NewEncoder(buf)
   594  	if err := enc.Encode(obj); err != nil {
   595  		return nil, err
   596  	}
   597  	return buf, nil
   598  }
   599  
   600  // requireOK is used to wrap doRequest and check for a 200
   601  func requireOK(d time.Duration, resp *http.Response, e error) (time.Duration, *http.Response, error) {
   602  	if e != nil {
   603  		if resp != nil {
   604  			resp.Body.Close()
   605  		}
   606  		return d, nil, e
   607  	}
   608  	if resp.StatusCode != 200 {
   609  		var buf bytes.Buffer
   610  		io.Copy(&buf, resp.Body)
   611  		resp.Body.Close()
   612  		return d, nil, fmt.Errorf("Unexpected response code: %d (%s)", resp.StatusCode, buf.Bytes())
   613  	}
   614  	return d, resp, nil
   615  }