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 }