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