golang.org/x/build@v0.0.0-20240506185731-218518f32b70/kubernetes/client.go (about) 1 // 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. 6 package kubernetes 7 8 import ( 9 "bufio" 10 "bytes" 11 "context" 12 "encoding/json" 13 "fmt" 14 "io" 15 "log" 16 "net/http" 17 "net/url" 18 "os" 19 "strings" 20 "time" 21 22 "golang.org/x/build/kubernetes/api" 23 "golang.org/x/net/context/ctxhttp" 24 ) 25 26 // Client is a client for the Kubernetes master. 27 type Client struct { 28 httpClient *http.Client 29 30 // endPointURL is the Kubernetes master URL ending in 31 // "/api/v1". 32 endpointURL string 33 34 namespace string // always in URL path-escaped form (for now) 35 } 36 37 // NewClient returns a new Kubernetes client. 38 // The provided host is an url (scheme://hostname[:port]) of a 39 // Kubernetes master without any path. 40 // The provided client is an authorized http.Client used to perform requests to the Kubernetes API master. 41 func NewClient(baseURL, namespace string, client *http.Client) (*Client, error) { 42 if namespace == "" { 43 return nil, fmt.Errorf("must specify Kubernetes namespace") 44 } 45 validURL, err := url.Parse(baseURL) 46 if err != nil { 47 return nil, fmt.Errorf("failed to parse URL %q: %v", baseURL, err) 48 } 49 return &Client{ 50 endpointURL: strings.TrimSuffix(validURL.String(), "/") + "/api/v1", 51 httpClient: client, 52 namespace: namespace, 53 }, nil 54 } 55 56 // Close closes any idle HTTP connections still connected to the Kubernetes master. 57 func (c *Client) Close() error { 58 if tr, ok := c.httpClient.Transport.(*http.Transport); ok { 59 tr.CloseIdleConnections() 60 } 61 return nil 62 } 63 64 // nsEndpoint returns the API endpoint root for this client. 65 // (This has nothing to do with Service Endpoints.) 66 func (c *Client) nsEndpoint() string { 67 return c.endpointURL + "/namespaces/" + c.namespace + "/" 68 } 69 70 // RunLongLivedPod creates a new pod resource in the default pod namespace with 71 // the given pod API specification. It assumes the pod runs a 72 // long-lived server (i.e. if the container exit quickly, even 73 // with success, then that is an error). 74 // 75 // It returns the pod status once it has entered the Running phase. 76 // An error is returned if the pod can not be created, or if ctx.Done 77 // is closed. 78 func (c *Client) RunLongLivedPod(ctx context.Context, pod *api.Pod) (*api.PodStatus, error) { 79 var podJSON bytes.Buffer 80 if err := json.NewEncoder(&podJSON).Encode(pod); err != nil { 81 return nil, fmt.Errorf("failed to encode pod in json: %v", err) 82 } 83 postURL := c.nsEndpoint() + "pods" 84 req, err := http.NewRequest("POST", postURL, &podJSON) 85 if err != nil { 86 return nil, fmt.Errorf("failed to create request: POST %q : %v", postURL, err) 87 } 88 res, err := ctxhttp.Do(ctx, c.httpClient, req) 89 if err != nil { 90 return nil, fmt.Errorf("failed to make request: POST %q: %v", postURL, err) 91 } 92 body, err := io.ReadAll(res.Body) 93 res.Body.Close() 94 if err != nil { 95 return nil, fmt.Errorf("failed to read request body for POST %q: %v", postURL, err) 96 } 97 if res.StatusCode != http.StatusCreated { 98 return nil, fmt.Errorf("http error: %d POST %q: %q: %v", res.StatusCode, postURL, string(body), err) 99 } 100 var podResult api.Pod 101 if err := json.Unmarshal(body, &podResult); err != nil { 102 return nil, fmt.Errorf("failed to decode pod resources: %v", err) 103 } 104 105 for { 106 // TODO(bradfitz,evanbrown): pass podResult.ObjectMeta.ResourceVersion to PodStatus? 107 ps, err := c.PodStatus(ctx, podResult.Name) 108 if err != nil { 109 return nil, err 110 } 111 switch ps.Phase { 112 case api.PodPending: 113 // The main phase we're waiting on 114 break 115 case api.PodRunning: 116 return ps, nil 117 case api.PodSucceeded, api.PodFailed: 118 return nil, fmt.Errorf("pod entered phase %q", ps.Phase) 119 default: 120 log.Printf("RunLongLivedPod poll loop: pod %q in unexpected phase %q; sleeping", podResult.Name, ps.Phase) 121 } 122 select { 123 case <-time.After(5 * time.Second): 124 case <-ctx.Done(): 125 // The pod did not leave the pending 126 // state. Try to clean it up. 127 go c.DeletePod(context.Background(), podResult.Name) 128 return nil, ctx.Err() 129 } 130 } 131 } 132 133 func (c *Client) do(ctx context.Context, method, urlStr string, dst interface{}) error { 134 req, err := http.NewRequest(method, urlStr, nil) 135 if err != nil { 136 return err 137 } 138 res, err := ctxhttp.Do(ctx, c.httpClient, req) 139 if err != nil { 140 return err 141 } 142 defer res.Body.Close() 143 if res.StatusCode != http.StatusOK { 144 body, _ := io.ReadAll(res.Body) 145 return fmt.Errorf("%v %s: %v, %s", method, urlStr, res.Status, body) 146 } 147 if dst != nil { 148 var r io.Reader = res.Body 149 if false && strings.Contains(urlStr, "endpoints") { // for debugging 150 r = io.TeeReader(r, os.Stderr) 151 } 152 return json.NewDecoder(r).Decode(dst) 153 } 154 return nil 155 } 156 157 // GetServices returns all services in the cluster, regardless of status. 158 func (c *Client) GetServices(ctx context.Context) ([]api.Service, error) { 159 var list api.ServiceList 160 if err := c.do(ctx, "GET", c.nsEndpoint()+"services", &list); err != nil { 161 return nil, err 162 } 163 return list.Items, nil 164 } 165 166 // Endpoint represents a service endpoint address. 167 type Endpoint struct { 168 IP string 169 Port int 170 PortName string 171 Protocol string // "TCP" or "UDP"; never empty 172 } 173 174 // GetServiceEndpoints returns the endpoints for the named service. 175 // If portName is non-empty, only endpoints matching that port name are returned. 176 func (c *Client) GetServiceEndpoints(ctx context.Context, serviceName, portName string) ([]Endpoint, error) { 177 var res api.Endpoints 178 // TODO: path escape serviceName? 179 if err := c.do(ctx, "GET", c.nsEndpoint()+"endpoints/"+serviceName, &res); err != nil { 180 return nil, err 181 } 182 var ep []Endpoint 183 for _, ss := range res.Subsets { 184 for _, port := range ss.Ports { 185 if portName != "" && port.Name != portName { 186 continue 187 } 188 for _, addr := range ss.Addresses { 189 proto := string(port.Protocol) 190 if proto == "" { 191 proto = "TCP" 192 } 193 ep = append(ep, Endpoint{ 194 IP: addr.IP, 195 Port: port.Port, 196 PortName: port.Name, 197 Protocol: proto, 198 }) 199 } 200 } 201 } 202 return ep, nil 203 } 204 205 // GetPods returns all pods in the cluster, regardless of status. 206 func (c *Client) GetPods(ctx context.Context) ([]api.Pod, error) { 207 var list api.PodList 208 if err := c.do(ctx, "GET", c.nsEndpoint()+"pods", &list); err != nil { 209 return nil, err 210 } 211 return list.Items, nil 212 } 213 214 // DeletePod deletes the specified Kubernetes pod. 215 func (c *Client) DeletePod(ctx context.Context, podName string) error { 216 url := c.nsEndpoint() + "pods/" + podName 217 req, err := http.NewRequest("DELETE", url, strings.NewReader(`{"gracePeriodSeconds":0}`)) 218 if err != nil { 219 return fmt.Errorf("failed to create request: DELETE %q : %v", url, err) 220 } 221 res, err := ctxhttp.Do(ctx, c.httpClient, req) 222 if err != nil { 223 return fmt.Errorf("failed to make request: DELETE %q: %v", url, err) 224 } 225 body, err := io.ReadAll(res.Body) 226 res.Body.Close() 227 if err != nil { 228 return fmt.Errorf("failed to read response body: DELETE %q: %v", url, err) 229 } 230 if res.StatusCode != http.StatusOK { 231 return fmt.Errorf("http error: %d DELETE %q: %q: %v", res.StatusCode, url, string(body), err) 232 } 233 return nil 234 } 235 236 // TODO(bradfitz): WatchPod is unreliable, so this is disabled. 237 // 238 // AwaitPodNotPending will return a pod's status in a 239 // podStatusResult when the pod is no longer in the pending 240 // state. 241 // The podResourceVersion is required to prevent a pod's entire 242 // history from being retrieved when the watch is initiated. 243 // If there is an error polling for the pod's status, or if 244 // ctx.Done is closed, podStatusResult will contain an error. 245 func (c *Client) _AwaitPodNotPending(ctx context.Context, podName, podResourceVersion string) (*api.Pod, error) { 246 if podResourceVersion == "" { 247 return nil, fmt.Errorf("resourceVersion for pod %v must be provided", podName) 248 } 249 ctx, cancel := context.WithCancel(ctx) 250 defer cancel() 251 252 podStatusUpdates, err := c._WatchPod(ctx, podName, podResourceVersion) 253 if err != nil { 254 return nil, err 255 } 256 for { 257 select { 258 case <-ctx.Done(): 259 return nil, ctx.Err() 260 case psr := <-podStatusUpdates: 261 if psr.Err != nil { 262 // If the context is done, prefer its error: 263 select { 264 case <-ctx.Done(): 265 return nil, ctx.Err() 266 default: 267 return nil, psr.Err 268 } 269 } 270 if psr.Pod.Status.Phase != api.PodPending { 271 return psr.Pod, nil 272 } 273 } 274 } 275 } 276 277 // PodStatusResult wraps an api.PodStatus and error. 278 type PodStatusResult struct { 279 Pod *api.Pod 280 Type string 281 Err error 282 } 283 284 type watchPodStatus struct { 285 // The type of watch update contained in the message 286 Type string `json:"type"` 287 // Pod details 288 Object api.Pod `json:"object"` 289 } 290 291 // TODO(bradfitz): WatchPod is unreliable and sometimes hangs forever 292 // without closing and sometimes ends prematurely, so this API is 293 // disabled. 294 // 295 // WatchPod long-polls the Kubernetes watch API to be notified 296 // of changes to the specified pod. Changes are sent on the returned 297 // PodStatusResult channel as they are received. 298 // The podResourceVersion is required to prevent a pod's entire 299 // history from being retrieved when the watch is initiated. 300 // The provided context must be canceled or timed out to stop the watch. 301 // If any error occurs communicating with the Kubernetes API, the 302 // error will be sent on the returned PodStatusResult channel and 303 // it will be closed. 304 func (c *Client) _WatchPod(ctx context.Context, podName, podResourceVersion string) (<-chan PodStatusResult, error) { 305 if podResourceVersion == "" { 306 return nil, fmt.Errorf("resourceVersion for pod %v must be provided", podName) 307 } 308 statusChan := make(chan PodStatusResult, 1) 309 310 go func() { 311 defer close(statusChan) 312 ctx, cancel := context.WithCancel(ctx) 313 defer cancel() 314 315 // Make request to Kubernetes API 316 getURL := c.endpointURL + "/watch/namespaces/" + c.namespace + "/pods/" + podName 317 req, err := http.NewRequest("GET", getURL, nil) 318 req.URL.Query().Add("resourceVersion", podResourceVersion) 319 if err != nil { 320 statusChan <- PodStatusResult{Err: fmt.Errorf("failed to create request: GET %q : %v", getURL, err)} 321 return 322 } 323 res, err := ctxhttp.Do(ctx, c.httpClient, req) 324 if err != nil { 325 statusChan <- PodStatusResult{Err: err} 326 return 327 } 328 defer res.Body.Close() 329 if res.StatusCode != 200 { 330 statusChan <- PodStatusResult{Err: fmt.Errorf("WatchPod status %v", res.Status)} 331 return 332 } 333 reader := bufio.NewReader(res.Body) 334 335 // bufio.Reader.ReadBytes is blocking, so we watch for 336 // context timeout or cancellation in a goroutine 337 // and close the response body when see it. The 338 // response body is also closed via defer when the 339 // request is made, but closing twice is OK. 340 go func() { 341 <-ctx.Done() 342 res.Body.Close() 343 }() 344 345 const backupPollDuration = 30 * time.Second 346 backupPoller := time.AfterFunc(backupPollDuration, func() { 347 log.Printf("kubernetes: backup poller in WatchPod checking on %q", podName) 348 st, err := c.PodStatus(ctx, podName) 349 log.Printf("kubernetes: backup poller in WatchPod PodStatus(%q) = %v, %v", podName, st, err) 350 if err != nil { 351 // Some error. 352 cancel() 353 } 354 }) 355 defer backupPoller.Stop() 356 357 for { 358 line, err := reader.ReadBytes('\n') 359 log.Printf("kubernetes WatchPod status line of %q: %q, %v", podName, line, err) 360 backupPoller.Reset(backupPollDuration) 361 if err != nil { 362 statusChan <- PodStatusResult{Err: fmt.Errorf("error reading streaming response body: %v", err)} 363 return 364 } 365 var wps watchPodStatus 366 if err := json.Unmarshal(line, &wps); err != nil { 367 statusChan <- PodStatusResult{Err: fmt.Errorf("failed to decode watch pod status: %v", err)} 368 return 369 } 370 statusChan <- PodStatusResult{Pod: &wps.Object, Type: wps.Type} 371 } 372 }() 373 return statusChan, nil 374 } 375 376 // Retrieve the status of a pod synchronously from the Kube 377 // API server. 378 func (c *Client) PodStatus(ctx context.Context, podName string) (*api.PodStatus, error) { 379 getURL := c.nsEndpoint() + "pods/" + podName // TODO: escape podName? 380 381 // Make request to Kubernetes API 382 req, err := http.NewRequest("GET", getURL, nil) 383 if err != nil { 384 return nil, fmt.Errorf("failed to create request: GET %q : %v", getURL, err) 385 } 386 res, err := ctxhttp.Do(ctx, c.httpClient, req) 387 if err != nil { 388 return nil, fmt.Errorf("failed to make request: GET %q: %v", getURL, err) 389 } 390 391 body, err := io.ReadAll(res.Body) 392 res.Body.Close() 393 if err != nil { 394 return nil, fmt.Errorf("failed to read request body for GET %q: %v", getURL, err) 395 } 396 if res.StatusCode != http.StatusOK { 397 return nil, fmt.Errorf("http error %d GET %q: %q: %v", res.StatusCode, getURL, string(body), err) 398 } 399 400 var pod *api.Pod 401 if err := json.Unmarshal(body, &pod); err != nil { 402 return nil, fmt.Errorf("failed to decode pod resources: %v", err) 403 } 404 return &pod.Status, nil 405 } 406 407 // PodLog retrieves the container log for the first container 408 // in the pod. 409 func (c *Client) PodLog(ctx context.Context, podName string) (string, error) { 410 // TODO(evanbrown): support multiple containers 411 url := c.nsEndpoint() + "pods/" + podName + "/log" // TODO: escape podName? 412 req, err := http.NewRequest("GET", url, nil) 413 if err != nil { 414 return "", fmt.Errorf("failed to create request: GET %q : %v", url, err) 415 } 416 res, err := ctxhttp.Do(ctx, c.httpClient, req) 417 if err != nil { 418 return "", fmt.Errorf("failed to make request: GET %q: %v", url, err) 419 } 420 body, err := io.ReadAll(res.Body) 421 res.Body.Close() 422 if err != nil { 423 return "", fmt.Errorf("failed to read response body: GET %q: %v", url, err) 424 } 425 if res.StatusCode != http.StatusOK { 426 return "", fmt.Errorf("http error %d GET %q: %q: %v", res.StatusCode, url, string(body), err) 427 } 428 return string(body), nil 429 } 430 431 // GetNodes returns the list of nodes that comprise the Kubernetes cluster 432 func (c *Client) GetNodes(ctx context.Context) ([]api.Node, error) { 433 var list api.NodeList 434 if err := c.do(ctx, "GET", c.endpointURL+"/nodes", &list); err != nil { 435 return nil, err 436 } 437 return list.Items, nil 438 }