| package metrics | 
 |  | 
 | import ( | 
 | 	"context" | 
 | 	"crypto/tls" | 
 | 	"crypto/x509" | 
 | 	"fmt" | 
 | 	"net" | 
 | 	"net/http" | 
 | 	"os/exec" | 
 |  | 
 | 	"source.monogon.dev/metropolis/node" | 
 | 	"source.monogon.dev/metropolis/node/core/identity" | 
 | 	"source.monogon.dev/metropolis/pkg/supervisor" | 
 | ) | 
 |  | 
 | // Service is the Metropolis Metrics Service. | 
 | // | 
 | // Currently, metrics means Prometheus metrics. | 
 | // | 
 | // It runs a forwarding proxy from a public HTTPS listener to a number of | 
 | // locally-running exporters, themselves listening over HTTP. The listener uses | 
 | // the main cluster CA and the node's main certificate, authenticating incoming | 
 | // connections with the same CA. | 
 | // | 
 | // Each exporter is exposed on a separate path, /metrics/<name>, where <name> is | 
 | // the name of the exporter. | 
 | // | 
 | // The HTTPS listener is bound to node.MetricsPort. | 
 | type Service struct { | 
 | 	// Credentials used to run the TLS/HTTPS listener and verify incoming | 
 | 	// connections. | 
 | 	Credentials *identity.NodeCredentials | 
 | 	// List of Exporters to run and to forward HTTP requests to. If not set, defaults | 
 | 	// to DefaultExporters. | 
 | 	Exporters []Exporter | 
 |  | 
 | 	// enableDynamicAddr enables listening on a dynamically chosen TCP port. This is | 
 | 	// used by tests to make sure we don't fail due to the default port being already | 
 | 	// in use. | 
 | 	enableDynamicAddr bool | 
 | 	// dynamicAddr will contain the picked dynamic listen address after the service | 
 | 	// starts, if enableDynamicAddr is set. | 
 | 	dynamicAddr chan string | 
 | } | 
 |  | 
 | // listen starts the public TLS listener for the service. | 
 | func (s *Service) listen() (net.Listener, error) { | 
 | 	cert := s.Credentials.TLSCredentials() | 
 |  | 
 | 	pool := x509.NewCertPool() | 
 | 	pool.AddCert(s.Credentials.ClusterCA()) | 
 |  | 
 | 	tlsc := tls.Config{ | 
 | 		Certificates: []tls.Certificate{ | 
 | 			cert, | 
 | 		}, | 
 | 		ClientAuth: tls.RequireAndVerifyClientCert, | 
 | 		ClientCAs:  pool, | 
 | 		// TODO(q3k): use VerifyPeerCertificate/VerifyConnection to check that the | 
 | 		// incoming client is allowed to access metrics. Currently we allow | 
 | 		// anyone/anything with a valid cluster certificate to access them. | 
 | 	} | 
 |  | 
 | 	addr := net.JoinHostPort("", node.MetricsPort.PortString()) | 
 | 	if s.enableDynamicAddr { | 
 | 		addr = "" | 
 | 	} | 
 | 	return tls.Listen("tcp", addr, &tlsc) | 
 | } | 
 |  | 
 | func (s *Service) Run(ctx context.Context) error { | 
 | 	lis, err := s.listen() | 
 | 	if err != nil { | 
 | 		return fmt.Errorf("listen failed: %w", err) | 
 | 	} | 
 | 	if s.enableDynamicAddr { | 
 | 		s.dynamicAddr <- lis.Addr().String() | 
 | 	} | 
 |  | 
 | 	if s.Exporters == nil { | 
 | 		s.Exporters = DefaultExporters | 
 | 	} | 
 |  | 
 | 	// First, make sure we don't have duplicate exporters. | 
 | 	seenNames := make(map[string]bool) | 
 | 	for _, exporter := range s.Exporters { | 
 | 		if seenNames[exporter.Name] { | 
 | 			return fmt.Errorf("duplicate exporter name: %q", exporter.Name) | 
 | 		} | 
 | 		seenNames[exporter.Name] = true | 
 | 	} | 
 |  | 
 | 	// Start all exporters as sub-runnables. | 
 | 	for _, exporter := range s.Exporters { | 
 | 		cmd := exec.CommandContext(ctx, exporter.Executable, exporter.Arguments...) | 
 | 		err := supervisor.Run(ctx, exporter.Name, func(ctx context.Context) error { | 
 | 			return supervisor.RunCommand(ctx, cmd) | 
 | 		}) | 
 | 		if err != nil { | 
 | 			return fmt.Errorf("running %s failed: %w", exporter.Name, err) | 
 | 		} | 
 |  | 
 | 	} | 
 |  | 
 | 	// And register all exporter forwarding functions on a mux. | 
 | 	mux := http.NewServeMux() | 
 | 	logger := supervisor.Logger(ctx) | 
 | 	for _, exporter := range s.Exporters { | 
 | 		exporter := exporter | 
 |  | 
 | 		mux.HandleFunc(exporter.externalPath(), func(w http.ResponseWriter, r *http.Request) { | 
 | 			exporter.forward(logger, w, r) | 
 | 		}) | 
 |  | 
 | 		logger.Infof("Registered exporter %q", exporter.Name) | 
 | 	} | 
 |  | 
 | 	supervisor.Signal(ctx, supervisor.SignalHealthy) | 
 |  | 
 | 	// Start forwarding server. | 
 | 	srv := http.Server{ | 
 | 		Handler: mux, | 
 | 		BaseContext: func(_ net.Listener) context.Context { | 
 | 			return ctx | 
 | 		}, | 
 | 	} | 
 |  | 
 | 	go func() { | 
 | 		<-ctx.Done() | 
 | 		srv.Close() | 
 | 	}() | 
 |  | 
 | 	err = srv.Serve(lis) | 
 | 	if err != nil && ctx.Err() != nil { | 
 | 		return ctx.Err() | 
 | 	} | 
 | 	return fmt.Errorf("Serve: %w", err) | 
 | } | 
 |  | 
 | type sdTarget struct { | 
 | 	Targets []string `json:"target"` | 
 | } |