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 }