metropolis: implement Metrics Service

This is the first pass at a Metrics Service. It currently consists of an
HTTP reverse proxy which authenticates incoming connections using the
Cluster CA and certificates, and passes these connections over to a
locally running node_exporter.

In the future more exporters will be added, and we will likely also run
our own exporter for Metropolis-specific metrics.

Change-Id: Ibab52aa303965dd7d975f5035f411d1c56ad73e6
Reviewed-on: https://review.monogon.dev/c/monogon/+/1816
Tested-by: Jenkins CI
Reviewed-by: Leopold Schabel <leo@monogon.tech>
diff --git a/metropolis/test/e2e/main_test.go b/metropolis/test/e2e/main_test.go
index 4a54d2f..1e51332 100644
--- a/metropolis/test/e2e/main_test.go
+++ b/metropolis/test/e2e/main_test.go
@@ -18,6 +18,8 @@
 
 import (
 	"context"
+	"crypto/tls"
+	"crypto/x509"
 	"errors"
 	"fmt"
 	"io"
@@ -25,6 +27,7 @@
 	"net/http"
 	_ "net/http"
 	_ "net/http/pprof"
+	"net/url"
 	"os"
 	"strings"
 	"testing"
@@ -357,6 +360,44 @@
 				}
 				return fmt.Errorf("job still running")
 			})
+			util.TestEventual(t, "Prometheus node metrics retrieved", ctx, smallTestTimeout, func(ctx context.Context) error {
+				pool := x509.NewCertPool()
+				pool.AddCert(cluster.CACertificate)
+				cl := http.Client{
+					Transport: &http.Transport{
+						TLSClientConfig: &tls.Config{
+							Certificates: []tls.Certificate{cluster.Owner},
+							RootCAs:      pool,
+						},
+						DialContext: func(ctx context.Context, _, addr string) (net.Conn, error) {
+							return cluster.DialNode(ctx, addr)
+						},
+					},
+				}
+				u := url.URL{
+					Scheme: "https",
+					Host:   net.JoinHostPort(cluster.NodeIDs[0], common.MetricsPort.PortString()),
+					Path:   "/metrics/node",
+				}
+				res, err := cl.Get(u.String())
+				if err != nil {
+					return err
+				}
+				defer res.Body.Close()
+				if res.StatusCode != 200 {
+					return fmt.Errorf("status code %d", res.StatusCode)
+				}
+
+				body, err := io.ReadAll(res.Body)
+				if err != nil {
+					return err
+				}
+				needle := "node_uname_info"
+				if !strings.Contains(string(body), needle) {
+					return util.Permanent(fmt.Errorf("could not find %q in returned response", needle))
+				}
+				return nil
+			})
 			if os.Getenv("HAVE_NESTED_KVM") != "" {
 				util.TestEventual(t, "Pod for KVM/QEMU smoke test", ctx, smallTestTimeout, func(ctx context.Context) error {
 					runcRuntimeClass := "runc"