blob: d52e4c6778c076660acb1195e0096565e58ccb5b [file] [log] [blame]
Johan Euphrosine093044a2015-05-12 14:21:46 -07001// Copyright 2015 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5// Package kubernetes contains a minimal client for the Kubernetes API.
6package kubernetes
7
8import (
Evan Brown956434c2015-10-02 15:38:22 -07009 "bufio"
Johan Euphrosine093044a2015-05-12 14:21:46 -070010 "bytes"
Brad Fitzpatricka6dd6262018-03-06 22:22:17 +000011 "context"
Johan Euphrosine093044a2015-05-12 14:21:46 -070012 "encoding/json"
13 "fmt"
Brad Fitzpatrickb30f5062017-02-09 22:41:29 +000014 "io"
Johan Euphrosine093044a2015-05-12 14:21:46 -070015 "io/ioutil"
Brad Fitzpatrickc6ced0a2016-05-04 20:57:37 +000016 "log"
Johan Euphrosine093044a2015-05-12 14:21:46 -070017 "net/http"
18 "net/url"
Brad Fitzpatrickb30f5062017-02-09 22:41:29 +000019 "os"
Johan Euphrosine093044a2015-05-12 14:21:46 -070020 "strings"
21 "time"
22
Evan Brownc51d4e02015-09-08 15:56:15 -070023 "golang.org/x/build/kubernetes/api"
Evan Brown956434c2015-10-02 15:38:22 -070024 "golang.org/x/net/context/ctxhttp"
Johan Euphrosine093044a2015-05-12 14:21:46 -070025)
26
Johan Euphrosine093044a2015-05-12 14:21:46 -070027// Client is a client for the Kubernetes master.
28type Client struct {
Brad Fitzpatrickb30f5062017-02-09 22:41:29 +000029 httpClient *http.Client
30
31 // endPointURL is the Kubernetes master URL ending in
32 // "/api/v1".
Johan Euphrosine093044a2015-05-12 14:21:46 -070033 endpointURL string
Brad Fitzpatrickb30f5062017-02-09 22:41:29 +000034
35 namespace string // always in URL path-escaped form (for now)
Johan Euphrosine093044a2015-05-12 14:21:46 -070036}
37
38// NewClient returns a new Kubernetes client.
39// The provided host is an url (scheme://hostname[:port]) of a
40// Kubernetes master without any path.
41// The provided client is an authorized http.Client used to perform requests to the Kubernetes API master.
Heschi Kreinicka9d7de12021-09-02 15:09:37 -040042func NewClient(baseURL, namespace string, client *http.Client) (*Client, error) {
43 if namespace == "" {
44 return nil, fmt.Errorf("must specify Kubernetes namespace")
45 }
Johan Euphrosine093044a2015-05-12 14:21:46 -070046 validURL, err := url.Parse(baseURL)
47 if err != nil {
48 return nil, fmt.Errorf("failed to parse URL %q: %v", baseURL, err)
49 }
50 return &Client{
Brad Fitzpatrickb30f5062017-02-09 22:41:29 +000051 endpointURL: strings.TrimSuffix(validURL.String(), "/") + "/api/v1",
Johan Euphrosine093044a2015-05-12 14:21:46 -070052 httpClient: client,
Heschi Kreinicka9d7de12021-09-02 15:09:37 -040053 namespace: namespace,
Johan Euphrosine093044a2015-05-12 14:21:46 -070054 }, nil
55}
56
Brad Fitzpatrickadc161a2017-01-31 20:31:07 +000057// Close closes any idle HTTP connections still connected to the Kubernetes master.
58func (c *Client) Close() error {
59 if tr, ok := c.httpClient.Transport.(*http.Transport); ok {
60 tr.CloseIdleConnections()
61 }
62 return nil
63}
64
Brad Fitzpatrickb30f5062017-02-09 22:41:29 +000065// nsEndpoint returns the API endpoint root for this client.
66// (This has nothing to do with Service Endpoints.)
67func (c *Client) nsEndpoint() string {
68 return c.endpointURL + "/namespaces/" + c.namespace + "/"
69}
70
Brad Fitzpatrick4a3f4a82016-05-05 18:29:41 +000071// RunLongLivedPod creates a new pod resource in the default pod namespace with
72// the given pod API specification. It assumes the pod runs a
73// long-lived server (i.e. if the container exit quickly quickly, even
74// with success, then that is an error).
75//
Evan Brown956434c2015-10-02 15:38:22 -070076// It returns the pod status once it has entered the Running phase.
Evan Brown34ff1d92016-02-16 22:39:27 -080077// An error is returned if the pod can not be created, or if ctx.Done
Evan Brown956434c2015-10-02 15:38:22 -070078// is closed.
Brad Fitzpatrick4a3f4a82016-05-05 18:29:41 +000079func (c *Client) RunLongLivedPod(ctx context.Context, pod *api.Pod) (*api.PodStatus, error) {
Brad Fitzpatrick304492c2016-07-19 02:30:34 +000080 var podJSON bytes.Buffer
81 if err := json.NewEncoder(&podJSON).Encode(pod); err != nil {
82 return nil, fmt.Errorf("failed to encode pod in json: %v", err)
83 }
Brad Fitzpatrickb30f5062017-02-09 22:41:29 +000084 postURL := c.nsEndpoint() + "pods"
Brad Fitzpatrick304492c2016-07-19 02:30:34 +000085 req, err := http.NewRequest("POST", postURL, &podJSON)
86 if err != nil {
87 return nil, fmt.Errorf("failed to create request: POST %q : %v", postURL, err)
88 }
89 res, err := ctxhttp.Do(ctx, c.httpClient, req)
90 if err != nil {
91 return nil, fmt.Errorf("failed to make request: POST %q: %v", postURL, err)
92 }
93 body, err := ioutil.ReadAll(res.Body)
94 res.Body.Close()
95 if err != nil {
96 return nil, fmt.Errorf("failed to read request body for POST %q: %v", postURL, err)
97 }
98 if res.StatusCode != http.StatusCreated {
99 return nil, fmt.Errorf("http error: %d POST %q: %q: %v", res.StatusCode, postURL, string(body), err)
100 }
Johan Euphrosine093044a2015-05-12 14:21:46 -0700101 var podResult api.Pod
Brad Fitzpatrick304492c2016-07-19 02:30:34 +0000102 if err := json.Unmarshal(body, &podResult); err != nil {
103 return nil, fmt.Errorf("failed to decode pod resources: %v", err)
Johan Euphrosine093044a2015-05-12 14:21:46 -0700104 }
Evan Brown956434c2015-10-02 15:38:22 -0700105
Evan Brown34ff1d92016-02-16 22:39:27 -0800106 for {
Brad Fitzpatrick4a3f4a82016-05-05 18:29:41 +0000107 // TODO(bradfitz,evanbrown): pass podResult.ObjectMeta.ResourceVersion to PodStatus?
108 ps, err := c.PodStatus(ctx, podResult.Name)
Evan Brown34ff1d92016-02-16 22:39:27 -0800109 if err != nil {
Brad Fitzpatrick4a3f4a82016-05-05 18:29:41 +0000110 return nil, err
Evan Brown34ff1d92016-02-16 22:39:27 -0800111 }
Brad Fitzpatrick4a3f4a82016-05-05 18:29:41 +0000112 switch ps.Phase {
113 case api.PodPending:
114 // The main phase we're waiting on
115 break
116 case api.PodRunning:
117 return ps, nil
118 case api.PodSucceeded, api.PodFailed:
119 return nil, fmt.Errorf("pod entered phase %q", ps.Phase)
120 default:
121 log.Printf("RunLongLivedPod poll loop: pod %q in unexpected phase %q; sleeping", podResult.Name, ps.Phase)
122 }
123 select {
124 case <-time.After(5 * time.Second):
125 case <-ctx.Done():
126 // The pod did not leave the pending
127 // state. Try to clean it up.
128 go c.DeletePod(context.Background(), podResult.Name)
129 return nil, ctx.Err()
130 }
Johan Euphrosine093044a2015-05-12 14:21:46 -0700131 }
Evan Brown956434c2015-10-02 15:38:22 -0700132}
133
Brad Fitzpatrickb30f5062017-02-09 22:41:29 +0000134func (c *Client) do(ctx context.Context, method, urlStr string, dst interface{}) error {
135 req, err := http.NewRequest(method, urlStr, nil)
Brad Fitzpatrick304492c2016-07-19 02:30:34 +0000136 if err != nil {
Brad Fitzpatrickb30f5062017-02-09 22:41:29 +0000137 return err
Evan Brown83f97482015-10-17 20:35:55 -0700138 }
Brad Fitzpatrick304492c2016-07-19 02:30:34 +0000139 res, err := ctxhttp.Do(ctx, c.httpClient, req)
140 if err != nil {
Brad Fitzpatrickb30f5062017-02-09 22:41:29 +0000141 return err
Brad Fitzpatrick304492c2016-07-19 02:30:34 +0000142 }
Brad Fitzpatrickb30f5062017-02-09 22:41:29 +0000143 defer res.Body.Close()
Brad Fitzpatrick304492c2016-07-19 02:30:34 +0000144 if res.StatusCode != http.StatusOK {
Brad Fitzpatrickb30f5062017-02-09 22:41:29 +0000145 body, _ := ioutil.ReadAll(res.Body)
146 return fmt.Errorf("%v %s: %v, %s", method, urlStr, res.Status, body)
Brad Fitzpatrick304492c2016-07-19 02:30:34 +0000147 }
Brad Fitzpatrickb30f5062017-02-09 22:41:29 +0000148 if dst != nil {
149 var r io.Reader = res.Body
150 if false && strings.Contains(urlStr, "endpoints") { // for debugging
151 r = io.TeeReader(r, os.Stderr)
152 }
153 return json.NewDecoder(r).Decode(dst)
154 }
155 return nil
156}
Brad Fitzpatrick304492c2016-07-19 02:30:34 +0000157
Brad Fitzpatrick7449a822017-02-11 00:46:40 +0000158// GetServices returns all services in the cluster, regardless of status.
Brad Fitzpatrickb30f5062017-02-09 22:41:29 +0000159func (c *Client) GetServices(ctx context.Context) ([]api.Service, error) {
160 var list api.ServiceList
161 if err := c.do(ctx, "GET", c.nsEndpoint()+"services", &list); err != nil {
162 return nil, err
Brad Fitzpatrick304492c2016-07-19 02:30:34 +0000163 }
Brad Fitzpatrickb30f5062017-02-09 22:41:29 +0000164 return list.Items, nil
165}
166
167// Endpoint represents a service endpoint address.
168type Endpoint struct {
169 IP string
170 Port int
171 PortName string
172 Protocol string // "TCP" or "UDP"; never empty
173}
174
175// GetServiceEndpoints returns the endpoints for the named service.
Brad Fitzpatrick7449a822017-02-11 00:46:40 +0000176// If portName is non-empty, only endpoints matching that port name are returned.
Brad Fitzpatrickb30f5062017-02-09 22:41:29 +0000177func (c *Client) GetServiceEndpoints(ctx context.Context, serviceName, portName string) ([]Endpoint, error) {
178 var res api.Endpoints
179 // TODO: path escape serviceName?
180 if err := c.do(ctx, "GET", c.nsEndpoint()+"endpoints/"+serviceName, &res); err != nil {
181 return nil, err
182 }
183 var ep []Endpoint
184 for _, ss := range res.Subsets {
185 for _, port := range ss.Ports {
186 if portName != "" && port.Name != portName {
187 continue
188 }
189 for _, addr := range ss.Addresses {
190 proto := string(port.Protocol)
191 if proto == "" {
192 proto = "TCP"
193 }
194 ep = append(ep, Endpoint{
195 IP: addr.IP,
196 Port: port.Port,
197 PortName: port.Name,
198 Protocol: proto,
199 })
200 }
201 }
202 }
203 return ep, nil
204}
205
206// GetPods returns all pods in the cluster, regardless of status.
207func (c *Client) GetPods(ctx context.Context) ([]api.Pod, error) {
208 var list api.PodList
209 if err := c.do(ctx, "GET", c.nsEndpoint()+"pods", &list); err != nil {
210 return nil, err
211 }
212 return list.Items, nil
Evan Brown83f97482015-10-17 20:35:55 -0700213}
214
215// PodDelete deletes the specified Kubernetes pod.
216func (c *Client) DeletePod(ctx context.Context, podName string) error {
Brad Fitzpatrickb30f5062017-02-09 22:41:29 +0000217 url := c.nsEndpoint() + "pods/" + podName
Brad Fitzpatrick71265ac2016-11-23 05:58:31 +0000218 req, err := http.NewRequest("DELETE", url, strings.NewReader(`{"gracePeriodSeconds":0}`))
Brad Fitzpatrick304492c2016-07-19 02:30:34 +0000219 if err != nil {
220 return fmt.Errorf("failed to create request: DELETE %q : %v", url, err)
221 }
222 res, err := ctxhttp.Do(ctx, c.httpClient, req)
223 if err != nil {
224 return fmt.Errorf("failed to make request: DELETE %q: %v", url, err)
225 }
226 body, err := ioutil.ReadAll(res.Body)
227 res.Body.Close()
228 if err != nil {
Kevin Burkeb7a944e2017-02-12 01:18:14 -0800229 return fmt.Errorf("failed to read response body: DELETE %q: %v", url, err)
Brad Fitzpatrick304492c2016-07-19 02:30:34 +0000230 }
231 if res.StatusCode != http.StatusOK {
232 return fmt.Errorf("http error: %d DELETE %q: %q: %v", res.StatusCode, url, string(body), err)
233 }
234 return nil
Evan Brown83f97482015-10-17 20:35:55 -0700235}
236
Brad Fitzpatrick4a3f4a82016-05-05 18:29:41 +0000237// TODO(bradfitz): WatchPod is unreliable, so this is disabled.
238//
239// AwaitPodNotPending will return a pod's status in a
Evan Brown956434c2015-10-02 15:38:22 -0700240// podStatusResult when the pod is no longer in the pending
241// state.
242// The podResourceVersion is required to prevent a pod's entire
243// history from being retrieved when the watch is initiated.
244// If there is an error polling for the pod's status, or if
245// ctx.Done is closed, podStatusResult will contain an error.
Brad Fitzpatrick4a3f4a82016-05-05 18:29:41 +0000246func (c *Client) _AwaitPodNotPending(ctx context.Context, podName, podResourceVersion string) (*api.Pod, error) {
Evan Brown956434c2015-10-02 15:38:22 -0700247 if podResourceVersion == "" {
248 return nil, fmt.Errorf("resourceVersion for pod %v must be provided", podName)
249 }
250 ctx, cancel := context.WithCancel(ctx)
251 defer cancel()
252
Brad Fitzpatrick4a3f4a82016-05-05 18:29:41 +0000253 podStatusUpdates, err := c._WatchPod(ctx, podName, podResourceVersion)
Evan Brown956434c2015-10-02 15:38:22 -0700254 if err != nil {
255 return nil, err
256 }
Evan Brown956434c2015-10-02 15:38:22 -0700257 for {
258 select {
Brad Fitzpatrick90f71792016-04-08 19:03:58 +0000259 case <-ctx.Done():
260 return nil, ctx.Err()
261 case psr := <-podStatusUpdates:
Evan Brown956434c2015-10-02 15:38:22 -0700262 if psr.Err != nil {
Brad Fitzpatrick90f71792016-04-08 19:03:58 +0000263 // If the context is done, prefer its error:
264 select {
265 case <-ctx.Done():
266 return nil, ctx.Err()
267 default:
268 return nil, psr.Err
269 }
Evan Brown956434c2015-10-02 15:38:22 -0700270 }
Evan Brown2e452e12015-11-20 09:43:56 -0800271 if psr.Pod.Status.Phase != api.PodPending {
272 return psr.Pod, nil
Evan Brown956434c2015-10-02 15:38:22 -0700273 }
274 }
275 }
276}
277
Brad Fitzpatrick90f71792016-04-08 19:03:58 +0000278// PodStatusResult wraps an api.PodStatus and error.
Evan Brown956434c2015-10-02 15:38:22 -0700279type PodStatusResult struct {
Evan Brown2e452e12015-11-20 09:43:56 -0800280 Pod *api.Pod
281 Type string
282 Err error
Evan Brown956434c2015-10-02 15:38:22 -0700283}
284
285type watchPodStatus struct {
286 // The type of watch update contained in the message
287 Type string `json:"type"`
288 // Pod details
289 Object api.Pod `json:"object"`
290}
291
Brad Fitzpatrick4a3f4a82016-05-05 18:29:41 +0000292// TODO(bradfitz): WatchPod is unreliable and sometimes hangs forever
293// without closing and sometimes ends prematurely, so this API is
294// disabled.
295//
Evan Brown2e452e12015-11-20 09:43:56 -0800296// WatchPod long-polls the Kubernetes watch API to be notified
Evan Brown956434c2015-10-02 15:38:22 -0700297// of changes to the specified pod. Changes are sent on the returned
298// PodStatusResult channel as they are received.
299// The podResourceVersion is required to prevent a pod's entire
300// history from being retrieved when the watch is initiated.
301// The provided context must be canceled or timed out to stop the watch.
302// If any error occurs communicating with the Kubernetes API, the
303// error will be sent on the returned PodStatusResult channel and
304// it will be closed.
Brad Fitzpatrick4a3f4a82016-05-05 18:29:41 +0000305func (c *Client) _WatchPod(ctx context.Context, podName, podResourceVersion string) (<-chan PodStatusResult, error) {
Evan Brown956434c2015-10-02 15:38:22 -0700306 if podResourceVersion == "" {
307 return nil, fmt.Errorf("resourceVersion for pod %v must be provided", podName)
308 }
Brad Fitzpatrick90f71792016-04-08 19:03:58 +0000309 statusChan := make(chan PodStatusResult, 1)
Evan Brown956434c2015-10-02 15:38:22 -0700310
311 go func() {
312 defer close(statusChan)
Brad Fitzpatrickc6ced0a2016-05-04 20:57:37 +0000313 ctx, cancel := context.WithCancel(ctx)
314 defer cancel()
315
Evan Brown956434c2015-10-02 15:38:22 -0700316 // Make request to Kubernetes API
Brad Fitzpatrickb30f5062017-02-09 22:41:29 +0000317 getURL := c.endpointURL + "/watch/namespaces/" + c.namespace + "/pods/" + podName
Evan Brown956434c2015-10-02 15:38:22 -0700318 req, err := http.NewRequest("GET", getURL, nil)
319 req.URL.Query().Add("resourceVersion", podResourceVersion)
320 if err != nil {
321 statusChan <- PodStatusResult{Err: fmt.Errorf("failed to create request: GET %q : %v", getURL, err)}
322 return
323 }
324 res, err := ctxhttp.Do(ctx, c.httpClient, req)
325 if err != nil {
Brad Fitzpatrick90f71792016-04-08 19:03:58 +0000326 statusChan <- PodStatusResult{Err: err}
Evan Brown956434c2015-10-02 15:38:22 -0700327 return
328 }
Evan Brown34ff1d92016-02-16 22:39:27 -0800329 defer res.Body.Close()
Brad Fitzpatrickc6ced0a2016-05-04 20:57:37 +0000330 if res.StatusCode != 200 {
331 statusChan <- PodStatusResult{Err: fmt.Errorf("WatchPod status %v", res.Status)}
332 return
333 }
Evan Brown956434c2015-10-02 15:38:22 -0700334 reader := bufio.NewReader(res.Body)
Evan Brown83f97482015-10-17 20:35:55 -0700335
336 // bufio.Reader.ReadBytes is blocking, so we watch for
337 // context timeout or cancellation in a goroutine
338 // and close the response body when see see it. The
339 // response body is also closed via defer when the
340 // request is made, but closing twice is OK.
341 go func() {
342 <-ctx.Done()
343 res.Body.Close()
344 }()
345
Brad Fitzpatrickc6ced0a2016-05-04 20:57:37 +0000346 const backupPollDuration = 30 * time.Second
347 backupPoller := time.AfterFunc(backupPollDuration, func() {
348 log.Printf("kubernetes: backup poller in WatchPod checking on %q", podName)
349 st, err := c.PodStatus(ctx, podName)
350 log.Printf("kubernetes: backup poller in WatchPod PodStatus(%q) = %v, %v", podName, st, err)
351 if err != nil {
352 // Some error.
353 cancel()
354 }
355 })
356 defer backupPoller.Stop()
357
Evan Brown956434c2015-10-02 15:38:22 -0700358 for {
359 line, err := reader.ReadBytes('\n')
Brad Fitzpatrickc6ced0a2016-05-04 20:57:37 +0000360 log.Printf("kubernetes WatchPod status line of %q: %q, %v", podName, line, err)
361 backupPoller.Reset(backupPollDuration)
Evan Brown956434c2015-10-02 15:38:22 -0700362 if err != nil {
363 statusChan <- PodStatusResult{Err: fmt.Errorf("error reading streaming response body: %v", err)}
364 return
365 }
Brad Fitzpatrick90f71792016-04-08 19:03:58 +0000366 var wps watchPodStatus
Evan Brown956434c2015-10-02 15:38:22 -0700367 if err := json.Unmarshal(line, &wps); err != nil {
368 statusChan <- PodStatusResult{Err: fmt.Errorf("failed to decode watch pod status: %v", err)}
369 return
370 }
Evan Brown2e452e12015-11-20 09:43:56 -0800371 statusChan <- PodStatusResult{Pod: &wps.Object, Type: wps.Type}
Evan Brown956434c2015-10-02 15:38:22 -0700372 }
373 }()
374 return statusChan, nil
375}
376
377// Retrieve the status of a pod synchronously from the Kube
378// API server.
379func (c *Client) PodStatus(ctx context.Context, podName string) (*api.PodStatus, error) {
Brad Fitzpatrickb30f5062017-02-09 22:41:29 +0000380 getURL := c.nsEndpoint() + "pods/" + podName // TODO: escape podName?
Brad Fitzpatrick304492c2016-07-19 02:30:34 +0000381
382 // Make request to Kubernetes API
383 req, err := http.NewRequest("GET", getURL, nil)
384 if err != nil {
385 return nil, fmt.Errorf("failed to create request: GET %q : %v", getURL, err)
386 }
387 res, err := ctxhttp.Do(ctx, c.httpClient, req)
388 if err != nil {
389 return nil, fmt.Errorf("failed to make request: GET %q: %v", getURL, err)
390 }
391
392 body, err := ioutil.ReadAll(res.Body)
393 res.Body.Close()
394 if err != nil {
395 return nil, fmt.Errorf("failed to read request body for GET %q: %v", getURL, err)
396 }
397 if res.StatusCode != http.StatusOK {
398 return nil, fmt.Errorf("http error %d GET %q: %q: %v", res.StatusCode, getURL, string(body), err)
399 }
400
401 var pod *api.Pod
402 if err := json.Unmarshal(body, &pod); err != nil {
403 return nil, fmt.Errorf("failed to decode pod resources: %v", err)
Evan Brown956434c2015-10-02 15:38:22 -0700404 }
405 return &pod.Status, nil
406}
407
408// PodLog retrieves the container log for the first container
409// in the pod.
410func (c *Client) PodLog(ctx context.Context, podName string) (string, error) {
411 // TODO(evanbrown): support multiple containers
Brad Fitzpatrickb30f5062017-02-09 22:41:29 +0000412 url := c.nsEndpoint() + "pods/" + podName + "/log" // TODO: escape podName?
Brad Fitzpatrick304492c2016-07-19 02:30:34 +0000413 req, err := http.NewRequest("GET", url, nil)
414 if err != nil {
415 return "", fmt.Errorf("failed to create request: GET %q : %v", url, err)
Evan Brown956434c2015-10-02 15:38:22 -0700416 }
Brad Fitzpatrick304492c2016-07-19 02:30:34 +0000417 res, err := ctxhttp.Do(ctx, c.httpClient, req)
418 if err != nil {
419 return "", fmt.Errorf("failed to make request: GET %q: %v", url, err)
420 }
421 body, err := ioutil.ReadAll(res.Body)
422 res.Body.Close()
423 if err != nil {
424 return "", fmt.Errorf("failed to read response body: GET %q: %v", url, err)
425 }
426 if res.StatusCode != http.StatusOK {
427 return "", fmt.Errorf("http error %d GET %q: %q: %v", res.StatusCode, url, string(body), err)
428 }
429 return string(body), nil
Johan Euphrosine093044a2015-05-12 14:21:46 -0700430}
Evan Brown83f97482015-10-17 20:35:55 -0700431
432// PodNodes returns the list of nodes that comprise the Kubernetes cluster
433func (c *Client) GetNodes(ctx context.Context) ([]api.Node, error) {
Brad Fitzpatrickb30f5062017-02-09 22:41:29 +0000434 var list api.NodeList
Brad Fitzpatrickc5562d02017-02-17 21:58:52 +0000435 if err := c.do(ctx, "GET", c.endpointURL+"/nodes", &list); err != nil {
Brad Fitzpatrickb30f5062017-02-09 22:41:29 +0000436 return nil, err
Evan Brown83f97482015-10-17 20:35:55 -0700437 }
Brad Fitzpatrickb30f5062017-02-09 22:41:29 +0000438 return list.Items, nil
Evan Brown83f97482015-10-17 20:35:55 -0700439}