github.com/jmitchell/nomad@v0.1.3-0.20151007230021-7ab84c2862d8/api/api.go (about)

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