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  }