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 }