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 }