···11+package api
22+33+// AgentMember represents a cluster member known to the agent
44+type AgentMember struct {
55+ Name string
66+ Addr string
77+ Port uint16
88+ Tags map[string]string
99+ // Status of the Member which corresponds to github.com/hashicorp/serf/serf.MemberStatus
1010+ // Value is one of:
1111+ //
1212+ // AgentMemberNone = 0
1313+ // AgentMemberAlive = 1
1414+ // AgentMemberLeaving = 2
1515+ // AgentMemberLeft = 3
1616+ // AgentMemberFailed = 4
1717+ Status int
1818+ ProtocolMin uint8
1919+ ProtocolMax uint8
2020+ ProtocolCur uint8
2121+ DelegateMin uint8
2222+ DelegateMax uint8
2323+ DelegateCur uint8
2424+}
2525+2626+// XXX: this may be needed for consul compatability, but I'm leaving it
2727+// out for now in case it isn't.
2828+// const AllSegments = "_all"
2929+3030+// Agent can be used to query the Agent endpoints
3131+type Agent struct {
3232+ c *Client
3333+3434+ // cache the node name
3535+ nodeName string
3636+}
3737+3838+// Agent returns a handle to the agent endpoints
3939+func (c *Client) Agent() *Agent {
4040+ return &Agent{c: c}
4141+}
4242+4343+// Self is used to query the agent we are speaking to for
4444+// information about itself
4545+func (a *Agent) Self() (map[string]map[string]interface{}, error) {
4646+ r := a.c.newRequest("GET", "/v1/agent/self")
4747+ _, resp, err := requireOK(a.c.doRequest(r))
4848+ if err != nil {
4949+ return nil, err
5050+ }
5151+ defer closeResponseBody(resp)
5252+5353+ var out map[string]map[string]interface{}
5454+ if err := decodeBody(resp, &out); err != nil {
5555+ return nil, err
5656+ }
5757+ return out, nil
5858+}
5959+6060+// Host is used to retrieve information about the host the
6161+// agent is running on such as CPU, memory, and disk. Requires
6262+// a operator:read ACL token.
6363+func (a *Agent) Host() (map[string]interface{}, error) {
6464+ r := a.c.newRequest("GET", "/v1/agent/host")
6565+ _, resp, err := requireOK(a.c.doRequest(r))
6666+ if err != nil {
6767+ return nil, err
6868+ }
6969+ defer closeResponseBody(resp)
7070+7171+ var out map[string]interface{}
7272+ if err := decodeBody(resp, &out); err != nil {
7373+ return nil, err
7474+ }
7575+ return out, nil
7676+}
7777+7878+// NodeName is used to get the node name of the agent
7979+func (a *Agent) NodeName() (string, error) {
8080+ if a.nodeName != "" {
8181+ return a.nodeName, nil
8282+ }
8383+ info, err := a.Self()
8484+ if err != nil {
8585+ return "", err
8686+ }
8787+ name := info["Config"]["NodeName"].(string)
8888+ a.nodeName = name
8989+ return name, nil
9090+}
9191+9292+// Members returns the known serf gossip members
9393+func (a *Agent) Members() ([]*AgentMember, error) {
9494+ r := a.c.newRequest("GET", "/v1/agent/members")
9595+ _, resp, err := requireOK(a.c.doRequest(r))
9696+ if err != nil {
9797+ return nil, err
9898+ }
9999+ defer closeResponseBody(resp)
100100+101101+ var out []*AgentMember
102102+ if err := decodeBody(resp, &out); err != nil {
103103+ return nil, err
104104+ }
105105+ return out, nil
106106+}
+483
api/api.go
···11+package api
22+33+import (
44+ "bytes"
55+ "context"
66+ "encoding/json"
77+ "fmt"
88+ "io"
99+ "io/ioutil"
1010+ "net"
1111+ "net/http"
1212+ "net/url"
1313+ "strconv"
1414+ "strings"
1515+ "sync"
1616+ "time"
1717+1818+ "git.j3s.sh/cascade/lib/cleanhttp"
1919+)
2020+2121+// Config is used to configure the creation of a client
2222+type Config struct {
2323+ // Address is the address of the Consul server
2424+ Address string
2525+2626+ // Scheme is the URI scheme for the Consul server
2727+ Scheme string
2828+2929+ // Transport is the Transport to use for the http client.
3030+ Transport *http.Transport
3131+3232+ // HttpClient is the client to use. Default will be
3333+ // used if not provided.
3434+ HttpClient *http.Client
3535+3636+ // WaitTime limits how long a Watch will block. If not provided,
3737+ // the agent default values will be used.
3838+ WaitTime time.Duration
3939+}
4040+4141+// DefaultConfig returns a default configuration for the client. By default this
4242+// will pool and reuse idle connections to cascade. If you have a long-lived
4343+// client object, this is the desired behavior and should make the most efficient
4444+// use of the connections to cascade. If you don't reuse a client object, which
4545+// is not recommended, then you may notice idle connections building up over
4646+// time. To avoid this, use the DefaultNonPooledConfig() instead.
4747+func DefaultConfig() *Config {
4848+ return defaultConfig(cleanhttp.DefaultPooledTransport)
4949+}
5050+5151+// DefaultNonPooledConfig returns a default configuration for the client which
5252+// does not pool connections. This isn't a recommended configuration because it
5353+// will reconnect to cascade on every request, but this is useful to avoid the
5454+// accumulation of idle connections if you make many client objects during the
5555+// lifetime of your application.
5656+func DefaultNonPooledConfig() *Config {
5757+ return defaultConfig(cleanhttp.DefaultTransport)
5858+}
5959+6060+// defaultConfig returns the default configuration for the client, using the
6161+// given function to make the transport.
6262+func defaultConfig(transportFn func() *http.Transport) *Config {
6363+ config := &Config{
6464+ Address: "127.0.0.1:8500",
6565+ Scheme: "http",
6666+ Transport: transportFn(),
6767+ }
6868+6969+ return config
7070+}
7171+7272+// Client provides a client to the Consul API
7373+type Client struct {
7474+ modifyLock sync.RWMutex
7575+ headers http.Header
7676+7777+ config Config
7878+}
7979+8080+// Headers gets the current set of headers used for requests. This returns a
8181+// copy; to modify it call AddHeader or SetHeaders.
8282+func (c *Client) Headers() http.Header {
8383+ c.modifyLock.RLock()
8484+ defer c.modifyLock.RUnlock()
8585+8686+ if c.headers == nil {
8787+ return nil
8888+ }
8989+9090+ ret := make(http.Header)
9191+ for k, v := range c.headers {
9292+ for _, val := range v {
9393+ ret[k] = append(ret[k], val)
9494+ }
9595+ }
9696+9797+ return ret
9898+}
9999+100100+// AddHeader allows a single header key/value pair to be added
101101+// in a race-safe fashion.
102102+func (c *Client) AddHeader(key, value string) {
103103+ c.modifyLock.Lock()
104104+ defer c.modifyLock.Unlock()
105105+ c.headers.Add(key, value)
106106+}
107107+108108+// SetHeaders clears all previous headers and uses only the given
109109+// ones going forward.
110110+func (c *Client) SetHeaders(headers http.Header) {
111111+ c.modifyLock.Lock()
112112+ defer c.modifyLock.Unlock()
113113+ c.headers = headers
114114+}
115115+116116+// NewClient returns a new client
117117+func NewClient(config *Config) (*Client, error) {
118118+ // bootstrap the config
119119+ defConfig := DefaultConfig()
120120+121121+ if config.Address == "" {
122122+ config.Address = defConfig.Address
123123+ }
124124+125125+ if config.Scheme == "" {
126126+ config.Scheme = defConfig.Scheme
127127+ }
128128+129129+ if config.Transport == nil {
130130+ config.Transport = defConfig.Transport
131131+ }
132132+133133+ if config.HttpClient == nil {
134134+ config.HttpClient = NewHttpClient(config.Transport)
135135+ }
136136+137137+ parts := strings.SplitN(config.Address, "://", 2)
138138+ if len(parts) == 2 {
139139+ switch parts[0] {
140140+ case "http":
141141+ config.Scheme = "http"
142142+ // TODO: unix socket support? do i care? no???
143143+ default:
144144+ return nil, fmt.Errorf("Unknown protocol scheme: %s", parts[0])
145145+ }
146146+ config.Address = parts[1]
147147+ }
148148+149149+ return &Client{config: *config, headers: make(http.Header)}, nil
150150+}
151151+152152+// NewHttpClient returns an http client configured with the given Transport
153153+func NewHttpClient(transport *http.Transport) *http.Client {
154154+ client := &http.Client{
155155+ Transport: transport,
156156+ }
157157+ return client
158158+}
159159+160160+// request is used to help build up a request
161161+type request struct {
162162+ config *Config
163163+ method string
164164+ url *url.URL
165165+ params url.Values
166166+ body io.Reader
167167+ header http.Header
168168+ obj interface{}
169169+ ctx context.Context
170170+}
171171+172172+// QueryMeta is used to return meta data about a query
173173+type QueryMeta struct {
174174+ // LastIndex. This can be used as a WaitIndex to perform
175175+ // a blocking query
176176+ LastIndex uint64
177177+178178+ // LastContentHash. This can be used as a WaitHash to perform a blocking query
179179+ // for endpoints that support hash-based blocking. Endpoints that do not
180180+ // support it will return an empty hash.
181181+ LastContentHash string
182182+183183+ // How long did the request take
184184+ RequestTime time.Duration
185185+186186+ // Is address translation enabled for HTTP responses on this agent
187187+ AddressTranslationEnabled bool
188188+}
189189+190190+// QueryOptions are used to parameterize a query
191191+type QueryOptions struct {
192192+ // WaitIndex is used to enable a blocking query. Waits
193193+ // until the timeout or the next index is reached
194194+ WaitIndex uint64
195195+196196+ // WaitHash is used by some endpoints instead of WaitIndex to perform blocking
197197+ // on state based on a hash of the response rather than a monotonic index.
198198+ // This is required when the state being blocked on is not stored in Raft, for
199199+ // example agent-local proxy configuration.
200200+ WaitHash string
201201+202202+ // WaitTime is used to bound the duration of a wait.
203203+ // Defaults to that of the Config, but can be overridden.
204204+ WaitTime time.Duration
205205+206206+ // Near is used to provide a node name that will sort the results
207207+ // in ascending order based on the estimated round trip time from
208208+ // that node. Setting this to "_agent" will use the agent's node
209209+ // for the sort.
210210+ Near string
211211+212212+ // NodeMeta is used to filter results by nodes with the given
213213+ // metadata key/value pairs. Currently, only one key/value pair can
214214+ // be provided for filtering.
215215+ NodeMeta map[string]string
216216+217217+ // ctx is an optional context pass through to the underlying HTTP
218218+ // request layer. Use Context() and WithContext() to manage this.
219219+ ctx context.Context
220220+221221+ // Filter requests filtering data prior to it being returned. The string
222222+ // is a go-bexpr compatible expression.
223223+ Filter string
224224+}
225225+226226+// setQueryOptions is used to annotate the request with
227227+// additional query options
228228+func (r *request) setQueryOptions(q *QueryOptions) {
229229+ if q == nil {
230230+ return
231231+ }
232232+ if q.WaitIndex != 0 {
233233+ r.params.Set("index", strconv.FormatUint(q.WaitIndex, 10))
234234+ }
235235+ if q.WaitTime != 0 {
236236+ r.params.Set("wait", durToMsec(q.WaitTime))
237237+ }
238238+ if q.WaitHash != "" {
239239+ r.params.Set("hash", q.WaitHash)
240240+ }
241241+ if q.Near != "" {
242242+ r.params.Set("near", q.Near)
243243+ }
244244+ if q.Filter != "" {
245245+ r.params.Set("filter", q.Filter)
246246+ }
247247+ if len(q.NodeMeta) > 0 {
248248+ for key, value := range q.NodeMeta {
249249+ r.params.Add("node-meta", key+":"+value)
250250+ }
251251+ }
252252+253253+ r.ctx = q.ctx
254254+}
255255+256256+// durToMsec converts a duration to a millisecond specified string. If the
257257+// user selected a positive value that rounds to 0 ms, then we will use 1 ms
258258+// so they get a short delay, otherwise Consul will translate the 0 ms into
259259+// a huge default delay.
260260+func durToMsec(dur time.Duration) string {
261261+ ms := dur / time.Millisecond
262262+ if dur > 0 && ms == 0 {
263263+ ms = 1
264264+ }
265265+ return fmt.Sprintf("%dms", ms)
266266+}
267267+268268+// serverError is a string we look for to detect 500 errors.
269269+const serverError = "Unexpected response code: 500"
270270+271271+// IsRetryableError returns true for 500 errors from the Consul servers, and
272272+// network connection errors. These are usually retryable at a later time.
273273+// This applies to reads but NOT to writes. This may return true for errors
274274+// on writes that may have still gone through, so do not use this to retry
275275+// any write operations.
276276+func IsRetryableError(err error) bool {
277277+ if err == nil {
278278+ return false
279279+ }
280280+281281+ if _, ok := err.(net.Error); ok {
282282+ return true
283283+ }
284284+285285+ // TODO (slackpad) - Make a real error type here instead of using
286286+ // a string check.
287287+ return strings.Contains(err.Error(), serverError)
288288+}
289289+290290+// toHTTP converts the request to an HTTP request
291291+func (r *request) toHTTP() (*http.Request, error) {
292292+ // Encode the query parameters
293293+ r.url.RawQuery = r.params.Encode()
294294+295295+ // Check if we should encode the body
296296+ if r.body == nil && r.obj != nil {
297297+ b, err := encodeBody(r.obj)
298298+ if err != nil {
299299+ return nil, err
300300+ }
301301+ r.body = b
302302+ }
303303+304304+ // Create the HTTP request
305305+ req, err := http.NewRequest(r.method, r.url.RequestURI(), r.body)
306306+ if err != nil {
307307+ return nil, err
308308+ }
309309+310310+ req.URL.Host = r.url.Host
311311+ req.URL.Scheme = r.url.Scheme
312312+ req.Host = r.url.Host
313313+ req.Header = r.header
314314+315315+ // Content-Type must always be set when a body is present
316316+ // See https://github.com/hashicorp/consul/issues/10011
317317+ if req.Body != nil && req.Header.Get("Content-Type") == "" {
318318+ req.Header.Set("Content-Type", "application/json")
319319+ }
320320+321321+ if r.ctx != nil {
322322+ return req.WithContext(r.ctx), nil
323323+ }
324324+325325+ return req, nil
326326+}
327327+328328+// newRequest is used to create a new request
329329+func (c *Client) newRequest(method, path string) *request {
330330+ r := &request{
331331+ config: &c.config,
332332+ method: method,
333333+ url: &url.URL{
334334+ Scheme: c.config.Scheme,
335335+ Host: c.config.Address,
336336+ Path: path,
337337+ },
338338+ params: make(map[string][]string),
339339+ header: c.Headers(),
340340+ }
341341+342342+ if c.config.WaitTime != 0 {
343343+ r.params.Set("wait", durToMsec(r.config.WaitTime))
344344+ }
345345+346346+ return r
347347+}
348348+349349+// doRequest runs a request with our client
350350+func (c *Client) doRequest(r *request) (time.Duration, *http.Response, error) {
351351+ req, err := r.toHTTP()
352352+ if err != nil {
353353+ return 0, nil, err
354354+ }
355355+ start := time.Now()
356356+ resp, err := c.config.HttpClient.Do(req)
357357+ diff := time.Since(start)
358358+ return diff, resp, err
359359+}
360360+361361+// Query is used to do a GET request against an endpoint
362362+// and deserialize the response into an interface using
363363+// standard cascade conventions.
364364+func (c *Client) query(endpoint string, out interface{}, q *QueryOptions) (*QueryMeta, error) {
365365+ r := c.newRequest("GET", endpoint)
366366+ r.setQueryOptions(q)
367367+ rtt, resp, err := c.doRequest(r)
368368+ if err != nil {
369369+ return nil, err
370370+ }
371371+ defer closeResponseBody(resp)
372372+373373+ qm := &QueryMeta{}
374374+ err = parseQueryMeta(resp, qm)
375375+ if err != nil {
376376+ return nil, err
377377+ }
378378+ qm.RequestTime = rtt
379379+380380+ if err := decodeBody(resp, out); err != nil {
381381+ return nil, err
382382+ }
383383+ return qm, nil
384384+}
385385+386386+// parseQueryMeta is used to help parse query meta-data
387387+func parseQueryMeta(resp *http.Response, q *QueryMeta) error {
388388+ header := resp.Header
389389+390390+ // Parse the X-Consul-Index (if it's set - hash based blocking queries don't
391391+ // set this)
392392+ // if indexStr := header.Get("X-Consul-Index"); indexStr != "" {
393393+ // index, err := strconv.ParseUint(indexStr, 10, 64)
394394+ // if err != nil {
395395+ // return fmt.Errorf("Failed to parse X-Consul-Index: %v", err)
396396+ // }
397397+ // q.LastIndex = index
398398+ // }
399399+ // q.LastContentHash = header.Get("X-Consul-ContentHash")
400400+401401+ // Parse X-Consul-Translate-Addresses
402402+ switch header.Get("X-Consul-Translate-Addresses") {
403403+ case "true":
404404+ q.AddressTranslationEnabled = true
405405+ default:
406406+ q.AddressTranslationEnabled = false
407407+ }
408408+409409+ return nil
410410+}
411411+412412+// decodeBody is used to JSON decode a body
413413+func decodeBody(resp *http.Response, out interface{}) error {
414414+ dec := json.NewDecoder(resp.Body)
415415+ return dec.Decode(out)
416416+}
417417+418418+// encodeBody is used to encode a request body
419419+func encodeBody(obj interface{}) (io.Reader, error) {
420420+ buf := bytes.NewBuffer(nil)
421421+ enc := json.NewEncoder(buf)
422422+ if err := enc.Encode(obj); err != nil {
423423+ return nil, err
424424+ }
425425+ return buf, nil
426426+}
427427+428428+// requireOK is used to wrap doRequest and check for a 200
429429+func requireOK(d time.Duration, resp *http.Response, e error) (time.Duration, *http.Response, error) {
430430+ if e != nil {
431431+ if resp != nil {
432432+ closeResponseBody(resp)
433433+ }
434434+ return d, nil, e
435435+ }
436436+ if resp.StatusCode != 200 {
437437+ return d, nil, generateUnexpectedResponseCodeError(resp)
438438+ }
439439+ return d, resp, nil
440440+}
441441+442442+// closeResponseBody reads resp.Body until EOF, and then closes it. The read
443443+// is necessary to ensure that the http.Client's underlying RoundTripper is able
444444+// to re-use the TCP connection. See godoc on net/http.Client.Do.
445445+func closeResponseBody(resp *http.Response) error {
446446+ _, _ = io.Copy(ioutil.Discard, resp.Body)
447447+ return resp.Body.Close()
448448+}
449449+450450+func (req *request) filterQuery(filter string) {
451451+ if filter == "" {
452452+ return
453453+ }
454454+455455+ req.params.Set("filter", filter)
456456+}
457457+458458+// generateUnexpectedResponseCodeError consumes the rest of the body, closes
459459+// the body stream and generates an error indicating the status code was
460460+// unexpected.
461461+func generateUnexpectedResponseCodeError(resp *http.Response) error {
462462+ var buf bytes.Buffer
463463+ io.Copy(&buf, resp.Body)
464464+ closeResponseBody(resp)
465465+ return fmt.Errorf("Unexpected response code: %d (%s)", resp.StatusCode, buf.Bytes())
466466+}
467467+468468+func requireNotFoundOrOK(d time.Duration, resp *http.Response, e error) (bool, time.Duration, *http.Response, error) {
469469+ if e != nil {
470470+ if resp != nil {
471471+ closeResponseBody(resp)
472472+ }
473473+ return false, d, nil, e
474474+ }
475475+ switch resp.StatusCode {
476476+ case 200:
477477+ return true, d, resp, nil
478478+ case 404:
479479+ return false, d, resp, nil
480480+ default:
481481+ return false, d, nil, generateUnexpectedResponseCodeError(resp)
482482+ }
483483+}
+60
lib/cleanhttp/cleanhttp.go
···11+// Copyright (c) HashiCorp, Inc.
22+// SPDX-License-Identifier: MPL-2.0
33+44+package cleanhttp
55+66+import (
77+ "net"
88+ "net/http"
99+ "runtime"
1010+ "time"
1111+)
1212+1313+// DefaultTransport returns a new http.Transport with similar default values to
1414+// http.DefaultTransport, but with idle connections and keepalives disabled.
1515+func DefaultTransport() *http.Transport {
1616+ transport := DefaultPooledTransport()
1717+ transport.DisableKeepAlives = true
1818+ transport.MaxIdleConnsPerHost = -1
1919+ return transport
2020+}
2121+2222+// DefaultPooledTransport returns a new http.Transport with similar default
2323+// values to http.DefaultTransport. Do not use this for transient transports as
2424+// it can leak file descriptors over time. Only use this for transports that
2525+// will be re-used for the same host(s).
2626+func DefaultPooledTransport() *http.Transport {
2727+ transport := &http.Transport{
2828+ Proxy: http.ProxyFromEnvironment,
2929+ DialContext: (&net.Dialer{
3030+ Timeout: 30 * time.Second,
3131+ KeepAlive: 30 * time.Second,
3232+ DualStack: true,
3333+ }).DialContext,
3434+ MaxIdleConns: 100,
3535+ IdleConnTimeout: 90 * time.Second,
3636+ ExpectContinueTimeout: 1 * time.Second,
3737+ ForceAttemptHTTP2: true,
3838+ MaxIdleConnsPerHost: runtime.GOMAXPROCS(0) + 1,
3939+ }
4040+ return transport
4141+}
4242+4343+// DefaultClient returns a new http.Client with similar default values to
4444+// http.Client, but with a non-shared Transport, idle connections disabled, and
4545+// keepalives disabled.
4646+func DefaultClient() *http.Client {
4747+ return &http.Client{
4848+ Transport: DefaultTransport(),
4949+ }
5050+}
5151+5252+// DefaultPooledClient returns a new http.Client with similar default values to
5353+// http.Client, but with a shared Transport. Do not use this function for
5454+// transient clients as it can leak file descriptors over time. Only use this
5555+// for clients that will be re-used for the same host(s).
5656+func DefaultPooledClient() *http.Client {
5757+ return &http.Client{
5858+ Transport: DefaultPooledTransport(),
5959+ }
6060+}