|  | // Package metricsproxy implements an authenticating proxy in front of the K8s | 
|  | // controller-manager and scheduler providing unauthenticated access to the | 
|  | // metrics via local ports | 
|  | package metricsproxy | 
|  |  | 
|  | import ( | 
|  | "context" | 
|  | "crypto/tls" | 
|  | "crypto/x509" | 
|  | "fmt" | 
|  | "io" | 
|  | "net" | 
|  | "net/http" | 
|  |  | 
|  | "k8s.io/kubernetes/cmd/kubeadm/app/constants" | 
|  |  | 
|  | "source.monogon.dev/metropolis/node" | 
|  | "source.monogon.dev/metropolis/node/kubernetes/pki" | 
|  | "source.monogon.dev/metropolis/pkg/supervisor" | 
|  | ) | 
|  |  | 
|  | type Service struct { | 
|  | // KPKI is a reference to the Kubernetes PKI | 
|  | KPKI *pki.PKI | 
|  | } | 
|  |  | 
|  | type kubernetesExporter struct { | 
|  | Name string | 
|  | // TargetPort on which this exporter is running. | 
|  | TargetPort node.Port | 
|  | // TargetPort on which the unauthenticated exporter should run. | 
|  | ListenPort node.Port | 
|  | // ServerName used to verify the tls connection. | 
|  | ServerName string | 
|  | } | 
|  |  | 
|  | // services are the kubernetes services which are exposed via this proxy. | 
|  | var services = []*kubernetesExporter{ | 
|  | { | 
|  | Name:       "kubernetes-scheduler", | 
|  | TargetPort: constants.KubeSchedulerPort, | 
|  | ListenPort: node.MetricsKubeSchedulerListenerPort, | 
|  | ServerName: "kube-scheduler.local", | 
|  | }, | 
|  | { | 
|  | Name:       "kubernetes-controller-manager", | 
|  | TargetPort: constants.KubeControllerManagerPort, | 
|  | ListenPort: node.MetricsKubeControllerManagerListenerPort, | 
|  | ServerName: "kube-controller-manager.local", | 
|  | }, | 
|  | { | 
|  | Name:       "kubernetes-apiserver", | 
|  | TargetPort: node.KubernetesAPIPort, | 
|  | ListenPort: node.MetricsKubeAPIServerListenerPort, | 
|  | ServerName: "kubernetes", | 
|  | }, | 
|  | } | 
|  |  | 
|  | type metricsService struct { | 
|  | *kubernetesExporter | 
|  | transport *http.Transport | 
|  | } | 
|  |  | 
|  | func (s *metricsService) ServeHTTP(w http.ResponseWriter, r *http.Request) { | 
|  | if r.Method != http.MethodGet { | 
|  | http.Error(w, fmt.Sprintf("method %q not allowed", r.Method), http.StatusMethodNotAllowed) | 
|  | return | 
|  | } | 
|  |  | 
|  | ctx := r.Context() | 
|  |  | 
|  | // We are supplying the http.Server with a BaseContext that contains the | 
|  | // context from our runnable which contains the logger | 
|  | logger := supervisor.Logger(ctx) | 
|  |  | 
|  | url := "https://127.0.0.1:" + s.TargetPort.PortString() + "/metrics" | 
|  | outReq, err := http.NewRequestWithContext(ctx, "GET", url, nil) | 
|  | if err != nil { | 
|  | logger.Errorf("%s: forwarding to %q failed: %v", r.RemoteAddr, s.Name, err) | 
|  | http.Error(w, "internal server error", http.StatusInternalServerError) | 
|  | return | 
|  | } | 
|  |  | 
|  | res, err := s.transport.RoundTrip(outReq) | 
|  | if err != nil { | 
|  | logger.Errorf("%s: forwarding to %q failed: %v", r.RemoteAddr, s.Name, err) | 
|  | http.Error(w, "could not reach exporter", http.StatusBadGateway) | 
|  | return | 
|  | } | 
|  | defer res.Body.Close() | 
|  |  | 
|  | copyHeader(w.Header(), res.Header) | 
|  | w.WriteHeader(res.StatusCode) | 
|  |  | 
|  | if _, err := io.Copy(w, res.Body); err != nil { | 
|  | logger.Errorf("%s: copying response from %q failed: %v", r.RemoteAddr, s.Name, err) | 
|  | return | 
|  | } | 
|  | } | 
|  |  | 
|  | func (s *metricsService) Run(ctx context.Context) error { | 
|  | supervisor.Signal(ctx, supervisor.SignalHealthy) | 
|  |  | 
|  | srv := http.Server{ | 
|  | BaseContext: func(_ net.Listener) context.Context { | 
|  | return ctx | 
|  | }, | 
|  | Addr:    net.JoinHostPort("127.0.0.1", s.ListenPort.PortString()), | 
|  | Handler: s, | 
|  | } | 
|  |  | 
|  | go func() { | 
|  | <-ctx.Done() | 
|  | srv.Close() | 
|  | }() | 
|  |  | 
|  | err := srv.ListenAndServe() | 
|  | if err != nil && ctx.Err() != nil { | 
|  | return ctx.Err() | 
|  | } | 
|  | return fmt.Errorf("ListenAndServe: %w", err) | 
|  | } | 
|  |  | 
|  | func (s *Service) Run(ctx context.Context) error { | 
|  | // TODO(q3k): move this to IssueCertificates and replace with dedicated certificate | 
|  | cert, key, err := s.KPKI.Certificate(ctx, pki.Master) | 
|  | if err != nil { | 
|  | return fmt.Errorf("could not load certificate %q from PKI: %w", pki.Master, err) | 
|  | } | 
|  | parsedKey, err := x509.ParsePKCS8PrivateKey(key) | 
|  | if err != nil { | 
|  | return fmt.Errorf("failed to parse key for cert %q: %w", pki.Master, err) | 
|  | } | 
|  |  | 
|  | caCert, _, err := s.KPKI.Certificate(ctx, pki.IdCA) | 
|  | if err != nil { | 
|  | return fmt.Errorf("could not load certificate %q from PKI: %w", pki.IdCA, err) | 
|  | } | 
|  | parsedCACert, err := x509.ParseCertificate(caCert) | 
|  | if err != nil { | 
|  | return fmt.Errorf("failed to parse cert %q: %w", pki.IdCA, err) | 
|  | } | 
|  |  | 
|  | rootCAs := x509.NewCertPool() | 
|  | rootCAs.AddCert(parsedCACert) | 
|  |  | 
|  | tlsConfig := &tls.Config{ | 
|  | RootCAs: rootCAs, | 
|  | Certificates: []tls.Certificate{{ | 
|  | Certificate: [][]byte{cert}, | 
|  | PrivateKey:  parsedKey, | 
|  | }}, | 
|  | } | 
|  |  | 
|  | for _, svc := range services { | 
|  | tlsConfig := tlsConfig.Clone() | 
|  | tlsConfig.ServerName = svc.ServerName | 
|  |  | 
|  | transport := http.DefaultTransport.(*http.Transport).Clone() | 
|  | transport.TLSClientConfig = tlsConfig | 
|  | transport.TLSClientConfig.ServerName = svc.ServerName | 
|  |  | 
|  | err := supervisor.Run(ctx, svc.Name, (&metricsService{ | 
|  | kubernetesExporter: svc, | 
|  | transport:          transport, | 
|  | }).Run) | 
|  | if err != nil { | 
|  | return fmt.Errorf("could not run sub-service %q: %w", svc.Name, err) | 
|  | } | 
|  | } | 
|  |  | 
|  | supervisor.Signal(ctx, supervisor.SignalHealthy) | 
|  |  | 
|  | <-ctx.Done() | 
|  | return ctx.Err() | 
|  | } | 
|  |  | 
|  | func copyHeader(dst, src http.Header) { | 
|  | for k, vv := range src { | 
|  | for _, v := range vv { | 
|  | dst.Add(k, v) | 
|  | } | 
|  | } | 
|  | } |