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  }