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