github.com/dkerwin/nomad@v0.3.3-0.20160525181927-74554135514b/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  	"time"
    14  
    15  	"github.com/hashicorp/go-cleanhttp"
    16  )
    17  
    18  // QueryOptions are used to parameterize a query
    19  type QueryOptions struct {
    20  	// Providing a datacenter overwrites the region provided
    21  	// by the Config
    22  	Region string
    23  
    24  	// AllowStale allows any Nomad server (non-leader) to service
    25  	// a read. This allows for lower latency and higher throughput
    26  	AllowStale bool
    27  
    28  	// WaitIndex is used to enable a blocking query. Waits
    29  	// until the timeout or the next index is reached
    30  	WaitIndex uint64
    31  
    32  	// WaitTime is used to bound the duration of a wait.
    33  	// Defaults to that of the Config, but can be overridden.
    34  	WaitTime time.Duration
    35  
    36  	// If set, used as prefix for resource list searches
    37  	Prefix string
    38  }
    39  
    40  // WriteOptions are used to parameterize a write
    41  type WriteOptions struct {
    42  	// Providing a datacenter overwrites the region provided
    43  	// by the Config
    44  	Region string
    45  }
    46  
    47  // QueryMeta is used to return meta data about a query
    48  type QueryMeta struct {
    49  	// LastIndex. This can be used as a WaitIndex to perform
    50  	// a blocking query
    51  	LastIndex uint64
    52  
    53  	// Time of last contact from the leader for the
    54  	// server servicing the request
    55  	LastContact time.Duration
    56  
    57  	// Is there a known leader
    58  	KnownLeader bool
    59  
    60  	// How long did the request take
    61  	RequestTime time.Duration
    62  }
    63  
    64  // WriteMeta is used to return meta data about a write
    65  type WriteMeta struct {
    66  	// LastIndex. This can be used as a WaitIndex to perform
    67  	// a blocking query
    68  	LastIndex uint64
    69  
    70  	// How long did the request take
    71  	RequestTime time.Duration
    72  }
    73  
    74  // Config is used to configure the creation of a client
    75  type Config struct {
    76  	// Address is the address of the Nomad agent
    77  	Address string
    78  
    79  	// Region to use. If not provided, the default agent region is used.
    80  	Region string
    81  
    82  	// HttpClient is the client to use. Default will be
    83  	// used if not provided.
    84  	HttpClient *http.Client
    85  
    86  	// WaitTime limits how long a Watch will block. If not provided,
    87  	// the agent default values will be used.
    88  	WaitTime time.Duration
    89  }
    90  
    91  // DefaultConfig returns a default configuration for the client
    92  func DefaultConfig() *Config {
    93  	config := &Config{
    94  		Address:    "http://127.0.0.1:4646",
    95  		HttpClient: cleanhttp.DefaultClient(),
    96  	}
    97  	if addr := os.Getenv("NOMAD_ADDR"); addr != "" {
    98  		config.Address = addr
    99  	}
   100  	return config
   101  }
   102  
   103  // Client provides a client to the Nomad API
   104  type Client struct {
   105  	config Config
   106  }
   107  
   108  // NewClient returns a new client
   109  func NewClient(config *Config) (*Client, error) {
   110  	// bootstrap the config
   111  	defConfig := DefaultConfig()
   112  
   113  	if config.Address == "" {
   114  		config.Address = defConfig.Address
   115  	} else if _, err := url.Parse(config.Address); err != nil {
   116  		return nil, fmt.Errorf("invalid address '%s': %v", config.Address, err)
   117  	}
   118  
   119  	if config.HttpClient == nil {
   120  		config.HttpClient = defConfig.HttpClient
   121  	}
   122  
   123  	client := &Client{
   124  		config: *config,
   125  	}
   126  	return client, nil
   127  }
   128  
   129  // request is used to help build up a request
   130  type request struct {
   131  	config *Config
   132  	method string
   133  	url    *url.URL
   134  	params url.Values
   135  	body   io.Reader
   136  	obj    interface{}
   137  }
   138  
   139  // setQueryOptions is used to annotate the request with
   140  // additional query options
   141  func (r *request) setQueryOptions(q *QueryOptions) {
   142  	if q == nil {
   143  		return
   144  	}
   145  	if q.Region != "" {
   146  		r.params.Set("region", q.Region)
   147  	}
   148  	if q.AllowStale {
   149  		r.params.Set("stale", "")
   150  	}
   151  	if q.WaitIndex != 0 {
   152  		r.params.Set("index", strconv.FormatUint(q.WaitIndex, 10))
   153  	}
   154  	if q.WaitTime != 0 {
   155  		r.params.Set("wait", durToMsec(q.WaitTime))
   156  	}
   157  	if q.Prefix != "" {
   158  		r.params.Set("prefix", q.Prefix)
   159  	}
   160  }
   161  
   162  // durToMsec converts a duration to a millisecond specified string
   163  func durToMsec(dur time.Duration) string {
   164  	return fmt.Sprintf("%dms", dur/time.Millisecond)
   165  }
   166  
   167  // setWriteOptions is used to annotate the request with
   168  // additional write options
   169  func (r *request) setWriteOptions(q *WriteOptions) {
   170  	if q == nil {
   171  		return
   172  	}
   173  	if q.Region != "" {
   174  		r.params.Set("region", q.Region)
   175  	}
   176  }
   177  
   178  // toHTTP converts the request to an HTTP request
   179  func (r *request) toHTTP() (*http.Request, error) {
   180  	// Encode the query parameters
   181  	r.url.RawQuery = r.params.Encode()
   182  
   183  	// Check if we should encode the body
   184  	if r.body == nil && r.obj != nil {
   185  		if b, err := encodeBody(r.obj); err != nil {
   186  			return nil, err
   187  		} else {
   188  			r.body = b
   189  		}
   190  	}
   191  
   192  	// Create the HTTP request
   193  	req, err := http.NewRequest(r.method, r.url.RequestURI(), r.body)
   194  	if err != nil {
   195  		return nil, err
   196  	}
   197  
   198  	req.Header.Add("Accept-Encoding", "gzip")
   199  	req.URL.Host = r.url.Host
   200  	req.URL.Scheme = r.url.Scheme
   201  	req.Host = r.url.Host
   202  	return req, nil
   203  }
   204  
   205  // newRequest is used to create a new request
   206  func (c *Client) newRequest(method, path string) *request {
   207  	base, _ := url.Parse(c.config.Address)
   208  	u, _ := url.Parse(path)
   209  	r := &request{
   210  		config: &c.config,
   211  		method: method,
   212  		url: &url.URL{
   213  			Scheme: base.Scheme,
   214  			Host:   base.Host,
   215  			Path:   u.Path,
   216  		},
   217  		params: make(map[string][]string),
   218  	}
   219  	if c.config.Region != "" {
   220  		r.params.Set("region", c.config.Region)
   221  	}
   222  	if c.config.WaitTime != 0 {
   223  		r.params.Set("wait", durToMsec(r.config.WaitTime))
   224  	}
   225  
   226  	// Add in the query parameters, if any
   227  	for key, values := range u.Query() {
   228  		for _, value := range values {
   229  			r.params.Add(key, value)
   230  		}
   231  	}
   232  
   233  	return r
   234  }
   235  
   236  // multiCloser is to wrap a ReadCloser such that when close is called, multiple
   237  // Closes occur.
   238  type multiCloser struct {
   239  	reader       io.Reader
   240  	inorderClose []io.Closer
   241  }
   242  
   243  func (m *multiCloser) Close() error {
   244  	for _, c := range m.inorderClose {
   245  		if err := c.Close(); err != nil {
   246  			return err
   247  		}
   248  	}
   249  	return nil
   250  }
   251  
   252  func (m *multiCloser) Read(p []byte) (int, error) {
   253  	return m.reader.Read(p)
   254  }
   255  
   256  // doRequest runs a request with our client
   257  func (c *Client) doRequest(r *request) (time.Duration, *http.Response, error) {
   258  	req, err := r.toHTTP()
   259  	if err != nil {
   260  		return 0, nil, err
   261  	}
   262  	start := time.Now()
   263  	resp, err := c.config.HttpClient.Do(req)
   264  	diff := time.Now().Sub(start)
   265  
   266  	// If the response is compressed, we swap the body's reader.
   267  	if resp != nil && resp.Header != nil {
   268  		var reader io.ReadCloser
   269  		switch resp.Header.Get("Content-Encoding") {
   270  		case "gzip":
   271  			greader, err := gzip.NewReader(resp.Body)
   272  			if err != nil {
   273  				return 0, nil, err
   274  			}
   275  
   276  			// The gzip reader doesn't close the wrapped reader so we use
   277  			// multiCloser.
   278  			reader = &multiCloser{
   279  				reader:       greader,
   280  				inorderClose: []io.Closer{greader, resp.Body},
   281  			}
   282  		default:
   283  			reader = resp.Body
   284  		}
   285  		resp.Body = reader
   286  	}
   287  
   288  	return diff, resp, err
   289  }
   290  
   291  // Query is used to do a GET request against an endpoint
   292  // and deserialize the response into an interface using
   293  // standard Nomad conventions.
   294  func (c *Client) query(endpoint string, out interface{}, q *QueryOptions) (*QueryMeta, error) {
   295  	r := c.newRequest("GET", endpoint)
   296  	r.setQueryOptions(q)
   297  	rtt, resp, err := requireOK(c.doRequest(r))
   298  	if err != nil {
   299  		return nil, err
   300  	}
   301  	defer resp.Body.Close()
   302  
   303  	qm := &QueryMeta{}
   304  	parseQueryMeta(resp, qm)
   305  	qm.RequestTime = rtt
   306  
   307  	if err := decodeBody(resp, out); err != nil {
   308  		return nil, err
   309  	}
   310  	return qm, nil
   311  }
   312  
   313  // write is used to do a PUT request against an endpoint
   314  // and serialize/deserialized using the standard Nomad conventions.
   315  func (c *Client) write(endpoint string, in, out interface{}, q *WriteOptions) (*WriteMeta, error) {
   316  	r := c.newRequest("PUT", endpoint)
   317  	r.setWriteOptions(q)
   318  	r.obj = in
   319  	rtt, resp, err := requireOK(c.doRequest(r))
   320  	if err != nil {
   321  		return nil, err
   322  	}
   323  	defer resp.Body.Close()
   324  
   325  	wm := &WriteMeta{RequestTime: rtt}
   326  	parseWriteMeta(resp, wm)
   327  
   328  	if out != nil {
   329  		if err := decodeBody(resp, &out); err != nil {
   330  			return nil, err
   331  		}
   332  	}
   333  	return wm, nil
   334  }
   335  
   336  // write is used to do a PUT request against an endpoint
   337  // and serialize/deserialized using the standard Nomad conventions.
   338  func (c *Client) delete(endpoint string, out interface{}, q *WriteOptions) (*WriteMeta, error) {
   339  	r := c.newRequest("DELETE", endpoint)
   340  	r.setWriteOptions(q)
   341  	rtt, resp, err := requireOK(c.doRequest(r))
   342  	if err != nil {
   343  		return nil, err
   344  	}
   345  	defer resp.Body.Close()
   346  
   347  	wm := &WriteMeta{RequestTime: rtt}
   348  	parseWriteMeta(resp, wm)
   349  
   350  	if out != nil {
   351  		if err := decodeBody(resp, &out); err != nil {
   352  			return nil, err
   353  		}
   354  	}
   355  	return wm, nil
   356  }
   357  
   358  // parseQueryMeta is used to help parse query meta-data
   359  func parseQueryMeta(resp *http.Response, q *QueryMeta) error {
   360  	header := resp.Header
   361  
   362  	// Parse the X-Nomad-Index
   363  	index, err := strconv.ParseUint(header.Get("X-Nomad-Index"), 10, 64)
   364  	if err != nil {
   365  		return fmt.Errorf("Failed to parse X-Nomad-Index: %v", err)
   366  	}
   367  	q.LastIndex = index
   368  
   369  	// Parse the X-Nomad-LastContact
   370  	last, err := strconv.ParseUint(header.Get("X-Nomad-LastContact"), 10, 64)
   371  	if err != nil {
   372  		return fmt.Errorf("Failed to parse X-Nomad-LastContact: %v", err)
   373  	}
   374  	q.LastContact = time.Duration(last) * time.Millisecond
   375  
   376  	// Parse the X-Nomad-KnownLeader
   377  	switch header.Get("X-Nomad-KnownLeader") {
   378  	case "true":
   379  		q.KnownLeader = true
   380  	default:
   381  		q.KnownLeader = false
   382  	}
   383  	return nil
   384  }
   385  
   386  // parseWriteMeta is used to help parse write meta-data
   387  func parseWriteMeta(resp *http.Response, q *WriteMeta) error {
   388  	header := resp.Header
   389  
   390  	// Parse the X-Nomad-Index
   391  	index, err := strconv.ParseUint(header.Get("X-Nomad-Index"), 10, 64)
   392  	if err != nil {
   393  		return fmt.Errorf("Failed to parse X-Nomad-Index: %v", err)
   394  	}
   395  	q.LastIndex = index
   396  	return nil
   397  }
   398  
   399  // decodeBody is used to JSON decode a body
   400  func decodeBody(resp *http.Response, out interface{}) error {
   401  	dec := json.NewDecoder(resp.Body)
   402  	return dec.Decode(out)
   403  }
   404  
   405  // encodeBody is used to encode a request body
   406  func encodeBody(obj interface{}) (io.Reader, error) {
   407  	buf := bytes.NewBuffer(nil)
   408  	enc := json.NewEncoder(buf)
   409  	if err := enc.Encode(obj); err != nil {
   410  		return nil, err
   411  	}
   412  	return buf, nil
   413  }
   414  
   415  // requireOK is used to wrap doRequest and check for a 200
   416  func requireOK(d time.Duration, resp *http.Response, e error) (time.Duration, *http.Response, error) {
   417  	if e != nil {
   418  		if resp != nil {
   419  			resp.Body.Close()
   420  		}
   421  		return d, nil, e
   422  	}
   423  	if resp.StatusCode != 200 {
   424  		var buf bytes.Buffer
   425  		io.Copy(&buf, resp.Body)
   426  		resp.Body.Close()
   427  		return d, nil, fmt.Errorf("Unexpected response code: %d (%s)", resp.StatusCode, buf.Bytes())
   428  	}
   429  	return d, resp, nil
   430  }