github.com/grafana/pyroscope@v1.18.0/pkg/metastore/discovery/kuberesolver/kubernetes.go (about)

     1  // nolint
     2  package kuberesolver
     3  
     4  import (
     5  	"context"
     6  	"crypto/tls"
     7  	"crypto/x509"
     8  	"encoding/json"
     9  	"fmt"
    10  	"net"
    11  	"net/http"
    12  	"net/url"
    13  	"os"
    14  	"strings"
    15  	"sync"
    16  	"time"
    17  
    18  	"github.com/fsnotify/fsnotify"
    19  )
    20  
    21  const (
    22  	serviceAccountToken     = "/var/run/secrets/kubernetes.io/serviceaccount/token"
    23  	serviceAccountCACert    = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
    24  	kubernetesNamespaceFile = "/var/run/secrets/kubernetes.io/serviceaccount/namespace"
    25  	defaultNamespace        = "default"
    26  )
    27  
    28  // K8sClient is minimal kubernetes client interface
    29  type K8sClient interface {
    30  	Do(req *http.Request) (*http.Response, error)
    31  	GetRequest(url string) (*http.Request, error)
    32  	Host() string
    33  }
    34  
    35  type k8sClient struct {
    36  	host       string
    37  	token      string
    38  	tokenLck   sync.RWMutex
    39  	httpClient *http.Client
    40  }
    41  
    42  func (kc *k8sClient) GetRequest(url string) (*http.Request, error) {
    43  	if !strings.HasPrefix(url, kc.host) {
    44  		url = fmt.Sprintf("%s/%s", kc.host, url)
    45  	}
    46  	req, err := http.NewRequest("GET", url, nil)
    47  	if err != nil {
    48  		return nil, err
    49  	}
    50  	kc.tokenLck.RLock()
    51  	defer kc.tokenLck.RUnlock()
    52  	if len(kc.token) > 0 {
    53  		req.Header.Set("Authorization", "Bearer "+kc.token)
    54  	}
    55  	return req, nil
    56  }
    57  
    58  func (kc *k8sClient) Do(req *http.Request) (*http.Response, error) {
    59  	return kc.httpClient.Do(req)
    60  }
    61  
    62  func (kc *k8sClient) Host() string {
    63  	return kc.host
    64  }
    65  
    66  func (kc *k8sClient) setToken(token string) {
    67  	kc.tokenLck.Lock()
    68  	defer kc.tokenLck.Unlock()
    69  	kc.token = token
    70  }
    71  
    72  // NewInClusterK8sClient creates K8sClient if it is inside Kubernetes
    73  func NewInClusterK8sClient() (K8sClient, error) {
    74  	host, port := os.Getenv("KUBERNETES_SERVICE_HOST"), os.Getenv("KUBERNETES_SERVICE_PORT")
    75  	if len(host) == 0 || len(port) == 0 {
    76  		return nil, fmt.Errorf("unable to load in-cluster configuration, KUBERNETES_SERVICE_HOST and KUBERNETES_SERVICE_PORT must be defined")
    77  	}
    78  	token, err := os.ReadFile(serviceAccountToken)
    79  	if err != nil {
    80  		return nil, err
    81  	}
    82  	ca, err := os.ReadFile(serviceAccountCACert)
    83  	if err != nil {
    84  		return nil, err
    85  	}
    86  	certPool := x509.NewCertPool()
    87  	certPool.AppendCertsFromPEM(ca)
    88  	transport := &http.Transport{TLSClientConfig: &tls.Config{
    89  		MinVersion: tls.VersionTLS12,
    90  		RootCAs:    certPool,
    91  	}}
    92  	httpClient := &http.Client{Transport: transport, Timeout: time.Nanosecond * 0}
    93  
    94  	client := &k8sClient{
    95  		host:       "https://" + net.JoinHostPort(host, port),
    96  		token:      string(token),
    97  		httpClient: httpClient,
    98  	}
    99  
   100  	// Create a new file watcher to listen for new Service Account tokens
   101  	watcher, err := fsnotify.NewWatcher()
   102  	if err != nil {
   103  		return nil, err
   104  	}
   105  
   106  	go func() {
   107  		for {
   108  			select {
   109  			case event, ok := <-watcher.Events:
   110  				if !ok {
   111  					return
   112  				}
   113  				// k8s configmaps uses symlinks, we need this workaround.
   114  				// original configmap file is removed
   115  				if event.Op.Has(fsnotify.Remove) || event.Op.Has(fsnotify.Chmod) {
   116  					// remove watcher since the file is removed
   117  					watcher.Remove(event.Name)
   118  					// add a new watcher pointing to the new symlink/file
   119  					watcher.Add(serviceAccountToken)
   120  					token, err := os.ReadFile(serviceAccountToken)
   121  					if err == nil {
   122  						client.setToken(string(token))
   123  					}
   124  				}
   125  				if event.Has(fsnotify.Write) {
   126  					token, err := os.ReadFile(serviceAccountToken)
   127  					if err == nil {
   128  						client.setToken(string(token))
   129  					}
   130  				}
   131  			case _, ok := <-watcher.Errors:
   132  				if !ok {
   133  					return
   134  				}
   135  			}
   136  		}
   137  	}()
   138  
   139  	err = watcher.Add(serviceAccountToken)
   140  	if err != nil {
   141  		return nil, err
   142  	}
   143  
   144  	return client, nil
   145  }
   146  
   147  // NewInsecureK8sClient creates an insecure k8s client which is suitable
   148  // to connect kubernetes api behind proxy
   149  func NewInsecureK8sClient(apiURL string) K8sClient {
   150  	return &k8sClient{
   151  		host:       apiURL,
   152  		httpClient: http.DefaultClient,
   153  	}
   154  }
   155  
   156  func getEndpoints(client K8sClient, namespace, targetName string) (Endpoints, error) {
   157  	u, err := url.Parse(fmt.Sprintf("%s/api/v1/namespaces/%s/endpoints/%s",
   158  		client.Host(), namespace, targetName))
   159  	if err != nil {
   160  		return Endpoints{}, err
   161  	}
   162  	req, err := client.GetRequest(u.String())
   163  	if err != nil {
   164  		return Endpoints{}, err
   165  	}
   166  	resp, err := client.Do(req)
   167  	if err != nil {
   168  		return Endpoints{}, err
   169  	}
   170  	defer resp.Body.Close()
   171  	if resp.StatusCode != http.StatusOK {
   172  		return Endpoints{}, fmt.Errorf("invalid response code %d for service %s in namespace %s", resp.StatusCode, targetName, namespace)
   173  	}
   174  	result := Endpoints{}
   175  	err = json.NewDecoder(resp.Body).Decode(&result)
   176  	return result, err
   177  }
   178  
   179  func watchEndpoints(ctx context.Context, client K8sClient, namespace, targetName string) (watchInterface, error) {
   180  	u, err := url.Parse(fmt.Sprintf("%s/api/v1/watch/namespaces/%s/endpoints/%s",
   181  		client.Host(), namespace, targetName))
   182  	if err != nil {
   183  		return nil, err
   184  	}
   185  	req, err := client.GetRequest(u.String())
   186  	if err != nil {
   187  		return nil, err
   188  	}
   189  	req = req.WithContext(ctx)
   190  	resp, err := client.Do(req)
   191  	if err != nil {
   192  		return nil, err
   193  	}
   194  	if resp.StatusCode != http.StatusOK {
   195  		defer resp.Body.Close()
   196  		return nil, fmt.Errorf("invalid response code %d for service %s in namespace %s", resp.StatusCode, targetName, namespace)
   197  	}
   198  	return newStreamWatcher(resp.Body), nil
   199  }
   200  
   201  func getCurrentNamespaceOrDefault() string {
   202  	ns, err := os.ReadFile(kubernetesNamespaceFile)
   203  	if err != nil {
   204  		return defaultNamespace
   205  	}
   206  	return string(ns)
   207  }