github.com/emate/nomad@v0.8.2-wo-binpacking/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"
    11  	"net/http"
    12  	"net/url"
    13  	"os"
    14  	"strconv"
    15  	"strings"
    16  	"time"
    17  
    18  	"github.com/hashicorp/go-cleanhttp"
    19  	rootcerts "github.com/hashicorp/go-rootcerts"
    20  )
    21  
    22  var (
    23  	// ClientConnTimeout is the timeout applied when attempting to contact a
    24  	// client directly before switching to a connection through the Nomad
    25  	// server.
    26  	ClientConnTimeout = 1 * time.Second
    27  )
    28  
    29  // QueryOptions are used to parameterize a query
    30  type QueryOptions struct {
    31  	// Providing a datacenter overwrites the region provided
    32  	// by the Config
    33  	Region string
    34  
    35  	// Namespace is the target namespace for the query.
    36  	Namespace string
    37  
    38  	// AllowStale allows any Nomad server (non-leader) to service
    39  	// a read. This allows for lower latency and higher throughput
    40  	AllowStale bool
    41  
    42  	// WaitIndex is used to enable a blocking query. Waits
    43  	// until the timeout or the next index is reached
    44  	WaitIndex uint64
    45  
    46  	// WaitTime is used to bound the duration of a wait.
    47  	// Defaults to that of the Config, but can be overridden.
    48  	WaitTime time.Duration
    49  
    50  	// If set, used as prefix for resource list searches
    51  	Prefix string
    52  
    53  	// Set HTTP parameters on the query.
    54  	Params map[string]string
    55  
    56  	// AuthToken is the secret ID of an ACL token
    57  	AuthToken string
    58  }
    59  
    60  // WriteOptions are used to parameterize a write
    61  type WriteOptions struct {
    62  	// Providing a datacenter overwrites the region provided
    63  	// by the Config
    64  	Region string
    65  
    66  	// Namespace is the target namespace for the write.
    67  	Namespace string
    68  
    69  	// AuthToken is the secret ID of an ACL token
    70  	AuthToken string
    71  }
    72  
    73  // QueryMeta is used to return meta data about a query
    74  type QueryMeta struct {
    75  	// LastIndex. This can be used as a WaitIndex to perform
    76  	// a blocking query
    77  	LastIndex uint64
    78  
    79  	// Time of last contact from the leader for the
    80  	// server servicing the request
    81  	LastContact time.Duration
    82  
    83  	// Is there a known leader
    84  	KnownLeader bool
    85  
    86  	// How long did the request take
    87  	RequestTime time.Duration
    88  }
    89  
    90  // WriteMeta is used to return meta data about a write
    91  type WriteMeta struct {
    92  	// LastIndex. This can be used as a WaitIndex to perform
    93  	// a blocking query
    94  	LastIndex uint64
    95  
    96  	// How long did the request take
    97  	RequestTime time.Duration
    98  }
    99  
   100  // HttpBasicAuth is used to authenticate http client with HTTP Basic Authentication
   101  type HttpBasicAuth struct {
   102  	// Username to use for HTTP Basic Authentication
   103  	Username string
   104  
   105  	// Password to use for HTTP Basic Authentication
   106  	Password string
   107  }
   108  
   109  // Config is used to configure the creation of a client
   110  type Config struct {
   111  	// Address is the address of the Nomad agent
   112  	Address string
   113  
   114  	// Region to use. If not provided, the default agent region is used.
   115  	Region string
   116  
   117  	// SecretID to use. This can be overwritten per request.
   118  	SecretID string
   119  
   120  	// Namespace to use. If not provided the default namespace is used.
   121  	Namespace string
   122  
   123  	// httpClient is the client to use. Default will be used if not provided.
   124  	httpClient *http.Client
   125  
   126  	// HttpAuth is the auth info to use for http access.
   127  	HttpAuth *HttpBasicAuth
   128  
   129  	// WaitTime limits how long a Watch will block. If not provided,
   130  	// the agent default values will be used.
   131  	WaitTime time.Duration
   132  
   133  	// TLSConfig provides the various TLS related configurations for the http
   134  	// client
   135  	TLSConfig *TLSConfig
   136  }
   137  
   138  // ClientConfig copies the configuration with a new client address, region, and
   139  // whether the client has TLS enabled.
   140  func (c *Config) ClientConfig(region, address string, tlsEnabled bool) *Config {
   141  	scheme := "http"
   142  	if tlsEnabled {
   143  		scheme = "https"
   144  	}
   145  	defaultConfig := DefaultConfig()
   146  	config := &Config{
   147  		Address:    fmt.Sprintf("%s://%s", scheme, address),
   148  		Region:     region,
   149  		Namespace:  c.Namespace,
   150  		httpClient: defaultConfig.httpClient,
   151  		SecretID:   c.SecretID,
   152  		HttpAuth:   c.HttpAuth,
   153  		WaitTime:   c.WaitTime,
   154  		TLSConfig:  c.TLSConfig.Copy(),
   155  	}
   156  
   157  	// Update the tls server name for connecting to a client
   158  	if tlsEnabled && config.TLSConfig != nil {
   159  		config.TLSConfig.TLSServerName = fmt.Sprintf("client.%s.nomad", region)
   160  	}
   161  
   162  	return config
   163  }
   164  
   165  // TLSConfig contains the parameters needed to configure TLS on the HTTP client
   166  // used to communicate with Nomad.
   167  type TLSConfig struct {
   168  	// CACert is the path to a PEM-encoded CA cert file to use to verify the
   169  	// Nomad server SSL certificate.
   170  	CACert string
   171  
   172  	// CAPath is the path to a directory of PEM-encoded CA cert files to verify
   173  	// the Nomad server SSL certificate.
   174  	CAPath string
   175  
   176  	// ClientCert is the path to the certificate for Nomad communication
   177  	ClientCert string
   178  
   179  	// ClientKey is the path to the private key for Nomad communication
   180  	ClientKey string
   181  
   182  	// TLSServerName, if set, is used to set the SNI host when connecting via
   183  	// TLS.
   184  	TLSServerName string
   185  
   186  	// Insecure enables or disables SSL verification
   187  	Insecure bool
   188  }
   189  
   190  func (t *TLSConfig) Copy() *TLSConfig {
   191  	if t == nil {
   192  		return nil
   193  	}
   194  
   195  	nt := new(TLSConfig)
   196  	*nt = *t
   197  	return nt
   198  }
   199  
   200  // DefaultConfig returns a default configuration for the client
   201  func DefaultConfig() *Config {
   202  	config := &Config{
   203  		Address:    "http://127.0.0.1:4646",
   204  		httpClient: cleanhttp.DefaultClient(),
   205  		TLSConfig:  &TLSConfig{},
   206  	}
   207  	transport := config.httpClient.Transport.(*http.Transport)
   208  	transport.TLSHandshakeTimeout = 10 * time.Second
   209  	transport.TLSClientConfig = &tls.Config{
   210  		MinVersion: tls.VersionTLS12,
   211  	}
   212  
   213  	if addr := os.Getenv("NOMAD_ADDR"); addr != "" {
   214  		config.Address = addr
   215  	}
   216  	if v := os.Getenv("NOMAD_REGION"); v != "" {
   217  		config.Region = v
   218  	}
   219  	if v := os.Getenv("NOMAD_NAMESPACE"); v != "" {
   220  		config.Namespace = v
   221  	}
   222  	if auth := os.Getenv("NOMAD_HTTP_AUTH"); auth != "" {
   223  		var username, password string
   224  		if strings.Contains(auth, ":") {
   225  			split := strings.SplitN(auth, ":", 2)
   226  			username = split[0]
   227  			password = split[1]
   228  		} else {
   229  			username = auth
   230  		}
   231  
   232  		config.HttpAuth = &HttpBasicAuth{
   233  			Username: username,
   234  			Password: password,
   235  		}
   236  	}
   237  
   238  	// Read TLS specific env vars
   239  	if v := os.Getenv("NOMAD_CACERT"); v != "" {
   240  		config.TLSConfig.CACert = v
   241  	}
   242  	if v := os.Getenv("NOMAD_CAPATH"); v != "" {
   243  		config.TLSConfig.CAPath = v
   244  	}
   245  	if v := os.Getenv("NOMAD_CLIENT_CERT"); v != "" {
   246  		config.TLSConfig.ClientCert = v
   247  	}
   248  	if v := os.Getenv("NOMAD_CLIENT_KEY"); v != "" {
   249  		config.TLSConfig.ClientKey = v
   250  	}
   251  	if v := os.Getenv("NOMAD_SKIP_VERIFY"); v != "" {
   252  		if insecure, err := strconv.ParseBool(v); err == nil {
   253  			config.TLSConfig.Insecure = insecure
   254  		}
   255  	}
   256  	if v := os.Getenv("NOMAD_TOKEN"); v != "" {
   257  		config.SecretID = v
   258  	}
   259  	return config
   260  }
   261  
   262  // SetTimeout is used to place a timeout for connecting to Nomad. A negative
   263  // duration is ignored, a duration of zero means no timeout, and any other value
   264  // will add a timeout.
   265  func (c *Config) SetTimeout(t time.Duration) error {
   266  	if c == nil {
   267  		return fmt.Errorf("nil config")
   268  	} else if c.httpClient == nil {
   269  		return fmt.Errorf("nil HTTP client")
   270  	} else if c.httpClient.Transport == nil {
   271  		return fmt.Errorf("nil HTTP client transport")
   272  	}
   273  
   274  	// Apply a timeout.
   275  	if t.Nanoseconds() >= 0 {
   276  		transport, ok := c.httpClient.Transport.(*http.Transport)
   277  		if !ok {
   278  			return fmt.Errorf("unexpected HTTP transport: %T", c.httpClient.Transport)
   279  		}
   280  
   281  		transport.DialContext = (&net.Dialer{
   282  			Timeout:   t,
   283  			KeepAlive: 30 * time.Second,
   284  		}).DialContext
   285  	}
   286  
   287  	return nil
   288  }
   289  
   290  // ConfigureTLS applies a set of TLS configurations to the the HTTP client.
   291  func (c *Config) ConfigureTLS() error {
   292  	if c.TLSConfig == nil {
   293  		return nil
   294  	}
   295  	if c.httpClient == nil {
   296  		return fmt.Errorf("config HTTP Client must be set")
   297  	}
   298  
   299  	var clientCert tls.Certificate
   300  	foundClientCert := false
   301  	if c.TLSConfig.ClientCert != "" || c.TLSConfig.ClientKey != "" {
   302  		if c.TLSConfig.ClientCert != "" && c.TLSConfig.ClientKey != "" {
   303  			var err error
   304  			clientCert, err = tls.LoadX509KeyPair(c.TLSConfig.ClientCert, c.TLSConfig.ClientKey)
   305  			if err != nil {
   306  				return err
   307  			}
   308  			foundClientCert = true
   309  		} else {
   310  			return fmt.Errorf("Both client cert and client key must be provided")
   311  		}
   312  	}
   313  
   314  	clientTLSConfig := c.httpClient.Transport.(*http.Transport).TLSClientConfig
   315  	rootConfig := &rootcerts.Config{
   316  		CAFile: c.TLSConfig.CACert,
   317  		CAPath: c.TLSConfig.CAPath,
   318  	}
   319  	if err := rootcerts.ConfigureTLS(clientTLSConfig, rootConfig); err != nil {
   320  		return err
   321  	}
   322  
   323  	clientTLSConfig.InsecureSkipVerify = c.TLSConfig.Insecure
   324  
   325  	if foundClientCert {
   326  		clientTLSConfig.Certificates = []tls.Certificate{clientCert}
   327  	}
   328  	if c.TLSConfig.TLSServerName != "" {
   329  		clientTLSConfig.ServerName = c.TLSConfig.TLSServerName
   330  	}
   331  
   332  	return nil
   333  }
   334  
   335  // Client provides a client to the Nomad API
   336  type Client struct {
   337  	config Config
   338  }
   339  
   340  // NewClient returns a new client
   341  func NewClient(config *Config) (*Client, error) {
   342  	// bootstrap the config
   343  	defConfig := DefaultConfig()
   344  
   345  	if config.Address == "" {
   346  		config.Address = defConfig.Address
   347  	} else if _, err := url.Parse(config.Address); err != nil {
   348  		return nil, fmt.Errorf("invalid address '%s': %v", config.Address, err)
   349  	}
   350  
   351  	if config.httpClient == nil {
   352  		config.httpClient = defConfig.httpClient
   353  	}
   354  
   355  	// Configure the TLS configurations
   356  	if err := config.ConfigureTLS(); err != nil {
   357  		return nil, err
   358  	}
   359  
   360  	client := &Client{
   361  		config: *config,
   362  	}
   363  	return client, nil
   364  }
   365  
   366  // Address return the address of the Nomad agent
   367  func (c *Client) Address() string {
   368  	return c.config.Address
   369  }
   370  
   371  // SetRegion sets the region to forward API requests to.
   372  func (c *Client) SetRegion(region string) {
   373  	c.config.Region = region
   374  }
   375  
   376  // SetNamespace sets the namespace to forward API requests to.
   377  func (c *Client) SetNamespace(namespace string) {
   378  	c.config.Namespace = namespace
   379  }
   380  
   381  // GetNodeClient returns a new Client that will dial the specified node. If the
   382  // QueryOptions is set, its region will be used.
   383  func (c *Client) GetNodeClient(nodeID string, q *QueryOptions) (*Client, error) {
   384  	return c.getNodeClientImpl(nodeID, -1, q, c.Nodes().Info)
   385  }
   386  
   387  // GetNodeClientWithTimeout returns a new Client that will dial the specified
   388  // node using the specified timeout. If the QueryOptions is set, its region will
   389  // be used.
   390  func (c *Client) GetNodeClientWithTimeout(
   391  	nodeID string, timeout time.Duration, q *QueryOptions) (*Client, error) {
   392  	return c.getNodeClientImpl(nodeID, timeout, q, c.Nodes().Info)
   393  }
   394  
   395  // nodeLookup is the definition of a function used to lookup a node. This is
   396  // largely used to mock the lookup in tests.
   397  type nodeLookup func(nodeID string, q *QueryOptions) (*Node, *QueryMeta, error)
   398  
   399  // getNodeClientImpl is the implementation of creating a API client for
   400  // contacting a node. It takes a function to lookup the node such that it can be
   401  // mocked during tests.
   402  func (c *Client) getNodeClientImpl(nodeID string, timeout time.Duration, q *QueryOptions, lookup nodeLookup) (*Client, error) {
   403  	node, _, err := lookup(nodeID, q)
   404  	if err != nil {
   405  		return nil, err
   406  	}
   407  	if node.Status == "down" {
   408  		return nil, NodeDownErr
   409  	}
   410  	if node.HTTPAddr == "" {
   411  		return nil, fmt.Errorf("http addr of node %q (%s) is not advertised", node.Name, nodeID)
   412  	}
   413  
   414  	var region string
   415  	switch {
   416  	case q != nil && q.Region != "":
   417  		// Prefer the region set in the query parameter
   418  		region = q.Region
   419  	case c.config.Region != "":
   420  		// If the client is configured for a particular region use that
   421  		region = c.config.Region
   422  	default:
   423  		// No region information is given so use the default.
   424  		region = "global"
   425  	}
   426  
   427  	// Get an API client for the node
   428  	conf := c.config.ClientConfig(region, node.HTTPAddr, node.TLSEnabled)
   429  
   430  	// Set the timeout
   431  	conf.SetTimeout(timeout)
   432  
   433  	return NewClient(conf)
   434  }
   435  
   436  // SetSecretID sets the ACL token secret for API requests.
   437  func (c *Client) SetSecretID(secretID string) {
   438  	c.config.SecretID = secretID
   439  }
   440  
   441  // request is used to help build up a request
   442  type request struct {
   443  	config *Config
   444  	method string
   445  	url    *url.URL
   446  	params url.Values
   447  	token  string
   448  	body   io.Reader
   449  	obj    interface{}
   450  }
   451  
   452  // setQueryOptions is used to annotate the request with
   453  // additional query options
   454  func (r *request) setQueryOptions(q *QueryOptions) {
   455  	if q == nil {
   456  		return
   457  	}
   458  	if q.Region != "" {
   459  		r.params.Set("region", q.Region)
   460  	}
   461  	if q.Namespace != "" {
   462  		r.params.Set("namespace", q.Namespace)
   463  	}
   464  	if q.AuthToken != "" {
   465  		r.token = q.AuthToken
   466  	}
   467  	if q.AllowStale {
   468  		r.params.Set("stale", "")
   469  	}
   470  	if q.WaitIndex != 0 {
   471  		r.params.Set("index", strconv.FormatUint(q.WaitIndex, 10))
   472  	}
   473  	if q.WaitTime != 0 {
   474  		r.params.Set("wait", durToMsec(q.WaitTime))
   475  	}
   476  	if q.Prefix != "" {
   477  		r.params.Set("prefix", q.Prefix)
   478  	}
   479  	for k, v := range q.Params {
   480  		r.params.Set(k, v)
   481  	}
   482  }
   483  
   484  // durToMsec converts a duration to a millisecond specified string
   485  func durToMsec(dur time.Duration) string {
   486  	return fmt.Sprintf("%dms", dur/time.Millisecond)
   487  }
   488  
   489  // setWriteOptions is used to annotate the request with
   490  // additional write options
   491  func (r *request) setWriteOptions(q *WriteOptions) {
   492  	if q == nil {
   493  		return
   494  	}
   495  	if q.Region != "" {
   496  		r.params.Set("region", q.Region)
   497  	}
   498  	if q.Namespace != "" {
   499  		r.params.Set("namespace", q.Namespace)
   500  	}
   501  	if q.AuthToken != "" {
   502  		r.token = q.AuthToken
   503  	}
   504  }
   505  
   506  // toHTTP converts the request to an HTTP request
   507  func (r *request) toHTTP() (*http.Request, error) {
   508  	// Encode the query parameters
   509  	r.url.RawQuery = r.params.Encode()
   510  
   511  	// Check if we should encode the body
   512  	if r.body == nil && r.obj != nil {
   513  		if b, err := encodeBody(r.obj); err != nil {
   514  			return nil, err
   515  		} else {
   516  			r.body = b
   517  		}
   518  	}
   519  
   520  	// Create the HTTP request
   521  	req, err := http.NewRequest(r.method, r.url.RequestURI(), r.body)
   522  	if err != nil {
   523  		return nil, err
   524  	}
   525  
   526  	// Optionally configure HTTP basic authentication
   527  	if r.url.User != nil {
   528  		username := r.url.User.Username()
   529  		password, _ := r.url.User.Password()
   530  		req.SetBasicAuth(username, password)
   531  	} else if r.config.HttpAuth != nil {
   532  		req.SetBasicAuth(r.config.HttpAuth.Username, r.config.HttpAuth.Password)
   533  	}
   534  
   535  	req.Header.Add("Accept-Encoding", "gzip")
   536  	if r.token != "" {
   537  		req.Header.Set("X-Nomad-Token", r.token)
   538  	}
   539  
   540  	req.URL.Host = r.url.Host
   541  	req.URL.Scheme = r.url.Scheme
   542  	req.Host = r.url.Host
   543  	return req, nil
   544  }
   545  
   546  // newRequest is used to create a new request
   547  func (c *Client) newRequest(method, path string) (*request, error) {
   548  	base, _ := url.Parse(c.config.Address)
   549  	u, err := url.Parse(path)
   550  	if err != nil {
   551  		return nil, err
   552  	}
   553  	r := &request{
   554  		config: &c.config,
   555  		method: method,
   556  		url: &url.URL{
   557  			Scheme: base.Scheme,
   558  			User:   base.User,
   559  			Host:   base.Host,
   560  			Path:   u.Path,
   561  		},
   562  		params: make(map[string][]string),
   563  	}
   564  	if c.config.Region != "" {
   565  		r.params.Set("region", c.config.Region)
   566  	}
   567  	if c.config.Namespace != "" {
   568  		r.params.Set("namespace", c.config.Namespace)
   569  	}
   570  	if c.config.WaitTime != 0 {
   571  		r.params.Set("wait", durToMsec(r.config.WaitTime))
   572  	}
   573  	if c.config.SecretID != "" {
   574  		r.token = r.config.SecretID
   575  	}
   576  
   577  	// Add in the query parameters, if any
   578  	for key, values := range u.Query() {
   579  		for _, value := range values {
   580  			r.params.Add(key, value)
   581  		}
   582  	}
   583  
   584  	return r, nil
   585  }
   586  
   587  // multiCloser is to wrap a ReadCloser such that when close is called, multiple
   588  // Closes occur.
   589  type multiCloser struct {
   590  	reader       io.Reader
   591  	inorderClose []io.Closer
   592  }
   593  
   594  func (m *multiCloser) Close() error {
   595  	for _, c := range m.inorderClose {
   596  		if err := c.Close(); err != nil {
   597  			return err
   598  		}
   599  	}
   600  	return nil
   601  }
   602  
   603  func (m *multiCloser) Read(p []byte) (int, error) {
   604  	return m.reader.Read(p)
   605  }
   606  
   607  // doRequest runs a request with our client
   608  func (c *Client) doRequest(r *request) (time.Duration, *http.Response, error) {
   609  	req, err := r.toHTTP()
   610  	if err != nil {
   611  		return 0, nil, err
   612  	}
   613  	start := time.Now()
   614  	resp, err := c.config.httpClient.Do(req)
   615  	diff := time.Now().Sub(start)
   616  
   617  	// If the response is compressed, we swap the body's reader.
   618  	if resp != nil && resp.Header != nil {
   619  		var reader io.ReadCloser
   620  		switch resp.Header.Get("Content-Encoding") {
   621  		case "gzip":
   622  			greader, err := gzip.NewReader(resp.Body)
   623  			if err != nil {
   624  				return 0, nil, err
   625  			}
   626  
   627  			// The gzip reader doesn't close the wrapped reader so we use
   628  			// multiCloser.
   629  			reader = &multiCloser{
   630  				reader:       greader,
   631  				inorderClose: []io.Closer{greader, resp.Body},
   632  			}
   633  		default:
   634  			reader = resp.Body
   635  		}
   636  		resp.Body = reader
   637  	}
   638  
   639  	return diff, resp, err
   640  }
   641  
   642  // rawQuery makes a GET request to the specified endpoint but returns just the
   643  // response body.
   644  func (c *Client) rawQuery(endpoint string, q *QueryOptions) (io.ReadCloser, error) {
   645  	r, err := c.newRequest("GET", endpoint)
   646  	if err != nil {
   647  		return nil, err
   648  	}
   649  	r.setQueryOptions(q)
   650  	_, resp, err := requireOK(c.doRequest(r))
   651  	if err != nil {
   652  		return nil, err
   653  	}
   654  
   655  	return resp.Body, nil
   656  }
   657  
   658  // query is used to do a GET request against an endpoint
   659  // and deserialize the response into an interface using
   660  // standard Nomad conventions.
   661  func (c *Client) query(endpoint string, out interface{}, q *QueryOptions) (*QueryMeta, error) {
   662  	r, err := c.newRequest("GET", endpoint)
   663  	if err != nil {
   664  		return nil, err
   665  	}
   666  	r.setQueryOptions(q)
   667  	rtt, resp, err := requireOK(c.doRequest(r))
   668  	if err != nil {
   669  		return nil, err
   670  	}
   671  	defer resp.Body.Close()
   672  
   673  	qm := &QueryMeta{}
   674  	parseQueryMeta(resp, qm)
   675  	qm.RequestTime = rtt
   676  
   677  	if err := decodeBody(resp, out); err != nil {
   678  		return nil, err
   679  	}
   680  	return qm, nil
   681  }
   682  
   683  // putQuery is used to do a PUT request when doing a read against an endpoint
   684  // and deserialize the response into an interface using standard Nomad
   685  // conventions.
   686  func (c *Client) putQuery(endpoint string, in, out interface{}, q *QueryOptions) (*QueryMeta, error) {
   687  	r, err := c.newRequest("PUT", endpoint)
   688  	if err != nil {
   689  		return nil, err
   690  	}
   691  	r.setQueryOptions(q)
   692  	r.obj = in
   693  	rtt, resp, err := requireOK(c.doRequest(r))
   694  	if err != nil {
   695  		return nil, err
   696  	}
   697  	defer resp.Body.Close()
   698  
   699  	qm := &QueryMeta{}
   700  	parseQueryMeta(resp, qm)
   701  	qm.RequestTime = rtt
   702  
   703  	if err := decodeBody(resp, out); err != nil {
   704  		return nil, err
   705  	}
   706  	return qm, nil
   707  }
   708  
   709  // write is used to do a PUT request against an endpoint
   710  // and serialize/deserialized using the standard Nomad conventions.
   711  func (c *Client) write(endpoint string, in, out interface{}, q *WriteOptions) (*WriteMeta, error) {
   712  	r, err := c.newRequest("PUT", endpoint)
   713  	if err != nil {
   714  		return nil, err
   715  	}
   716  	r.setWriteOptions(q)
   717  	r.obj = in
   718  	rtt, resp, err := requireOK(c.doRequest(r))
   719  	if err != nil {
   720  		return nil, err
   721  	}
   722  	defer resp.Body.Close()
   723  
   724  	wm := &WriteMeta{RequestTime: rtt}
   725  	parseWriteMeta(resp, wm)
   726  
   727  	if out != nil {
   728  		if err := decodeBody(resp, &out); err != nil {
   729  			return nil, err
   730  		}
   731  	}
   732  	return wm, nil
   733  }
   734  
   735  // delete is used to do a DELETE request against an endpoint
   736  // and serialize/deserialized using the standard Nomad conventions.
   737  func (c *Client) delete(endpoint string, out interface{}, q *WriteOptions) (*WriteMeta, error) {
   738  	r, err := c.newRequest("DELETE", endpoint)
   739  	if err != nil {
   740  		return nil, err
   741  	}
   742  	r.setWriteOptions(q)
   743  	rtt, resp, err := requireOK(c.doRequest(r))
   744  	if err != nil {
   745  		return nil, err
   746  	}
   747  	defer resp.Body.Close()
   748  
   749  	wm := &WriteMeta{RequestTime: rtt}
   750  	parseWriteMeta(resp, wm)
   751  
   752  	if out != nil {
   753  		if err := decodeBody(resp, &out); err != nil {
   754  			return nil, err
   755  		}
   756  	}
   757  	return wm, nil
   758  }
   759  
   760  // parseQueryMeta is used to help parse query meta-data
   761  func parseQueryMeta(resp *http.Response, q *QueryMeta) error {
   762  	header := resp.Header
   763  
   764  	// Parse the X-Nomad-Index
   765  	index, err := strconv.ParseUint(header.Get("X-Nomad-Index"), 10, 64)
   766  	if err != nil {
   767  		return fmt.Errorf("Failed to parse X-Nomad-Index: %v", err)
   768  	}
   769  	q.LastIndex = index
   770  
   771  	// Parse the X-Nomad-LastContact
   772  	last, err := strconv.ParseUint(header.Get("X-Nomad-LastContact"), 10, 64)
   773  	if err != nil {
   774  		return fmt.Errorf("Failed to parse X-Nomad-LastContact: %v", err)
   775  	}
   776  	q.LastContact = time.Duration(last) * time.Millisecond
   777  
   778  	// Parse the X-Nomad-KnownLeader
   779  	switch header.Get("X-Nomad-KnownLeader") {
   780  	case "true":
   781  		q.KnownLeader = true
   782  	default:
   783  		q.KnownLeader = false
   784  	}
   785  	return nil
   786  }
   787  
   788  // parseWriteMeta is used to help parse write meta-data
   789  func parseWriteMeta(resp *http.Response, q *WriteMeta) error {
   790  	header := resp.Header
   791  
   792  	// Parse the X-Nomad-Index
   793  	index, err := strconv.ParseUint(header.Get("X-Nomad-Index"), 10, 64)
   794  	if err != nil {
   795  		return fmt.Errorf("Failed to parse X-Nomad-Index: %v", err)
   796  	}
   797  	q.LastIndex = index
   798  	return nil
   799  }
   800  
   801  // decodeBody is used to JSON decode a body
   802  func decodeBody(resp *http.Response, out interface{}) error {
   803  	dec := json.NewDecoder(resp.Body)
   804  	return dec.Decode(out)
   805  }
   806  
   807  // encodeBody is used to encode a request body
   808  func encodeBody(obj interface{}) (io.Reader, error) {
   809  	buf := bytes.NewBuffer(nil)
   810  	enc := json.NewEncoder(buf)
   811  	if err := enc.Encode(obj); err != nil {
   812  		return nil, err
   813  	}
   814  	return buf, nil
   815  }
   816  
   817  // requireOK is used to wrap doRequest and check for a 200
   818  func requireOK(d time.Duration, resp *http.Response, e error) (time.Duration, *http.Response, error) {
   819  	if e != nil {
   820  		if resp != nil {
   821  			resp.Body.Close()
   822  		}
   823  		return d, nil, e
   824  	}
   825  	if resp.StatusCode != 200 {
   826  		var buf bytes.Buffer
   827  		io.Copy(&buf, resp.Body)
   828  		resp.Body.Close()
   829  		return d, nil, fmt.Errorf("Unexpected response code: %d (%s)", resp.StatusCode, buf.Bytes())
   830  	}
   831  	return d, resp, nil
   832  }