metropolis/test/e2e: add self-test image for networking

We don't have any networking tests in our E2E tests. This adds an image
which de-facto implements one. Or at least, will implement one once we
move to split workers/controllers and contacting a Kubernetes apiserver
from a pod will mean we're actually testing cross-node traffic.

Change-Id: I3d7be3824ac041d72e1c19cd468d30dbcb71fa03
Reviewed-on: https://review.monogon.dev/c/monogon/+/1481
Reviewed-by: Lorenz Brun <lorenz@monogon.tech>
Tested-by: Jenkins CI
diff --git a/metropolis/test/e2e/selftest/BUILD.bazel b/metropolis/test/e2e/selftest/BUILD.bazel
new file mode 100644
index 0000000..2aa8029
--- /dev/null
+++ b/metropolis/test/e2e/selftest/BUILD.bazel
@@ -0,0 +1,16 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+load("@io_bazel_rules_docker//go:image.bzl", "go_image")
+
+go_library(
+    name = "selftest",
+    srcs = ["main.go"],
+    importpath = "source.monogon.dev/metropolis/test/e2e/selftest",
+    visibility = ["//visibility:private"],
+)
+
+go_image(
+    name = "selftest_image",
+    embed = [":selftest"],
+    pure = "on",
+    visibility = ["//metropolis/node:__pkg__"],
+)
diff --git a/metropolis/test/e2e/selftest/README.md b/metropolis/test/e2e/selftest/README.md
new file mode 100644
index 0000000..b001f38
--- /dev/null
+++ b/metropolis/test/e2e/selftest/README.md
@@ -0,0 +1,8 @@
+self-test image
+===
+
+This image is used by the Metropolis E2E tests to perform some cluster-internal
+tests. See //metropolis/test/e2e:main_test.go for usage.
+
+The image should be run as a Kubernetes Job, and should return 0 if all tests
+have passed. If the job fails, its last log line will be printed.
\ No newline at end of file
diff --git a/metropolis/test/e2e/selftest/main.go b/metropolis/test/e2e/selftest/main.go
new file mode 100644
index 0000000..2603eae
--- /dev/null
+++ b/metropolis/test/e2e/selftest/main.go
@@ -0,0 +1,84 @@
+package main
+
+import (
+	"context"
+	"crypto/tls"
+	"crypto/x509"
+	"encoding/json"
+	"fmt"
+	"log"
+	"net/http"
+	"os"
+	"time"
+)
+
+// test1InClusterKubernetes exercises connectivity to the cluster-local
+// Kubernetes API server. It expects to be able to connect to the APIserver using
+// the ServiceAccount and cluster CA injected by the Kubelet.
+//
+// The entire functionality is reimplemented without relying on Kubernetes
+// client code to make the expected behaviour clear.
+func test1InClusterKubernetes(ctx context.Context) error {
+	token, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/token")
+	if err != nil {
+		return fmt.Errorf("failed to read serviceaccount token: %w", err)
+	}
+
+	cacert, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/ca.crt")
+	if err != nil {
+		return fmt.Errorf("failed to read cluster CA certificate: %w", err)
+	}
+	pool := x509.NewCertPool()
+	pool.AppendCertsFromPEM(cacert)
+
+	client := &http.Client{
+		Transport: &http.Transport{
+			TLSClientConfig: &tls.Config{
+				RootCAs: pool,
+			},
+		},
+	}
+
+	req, err := http.NewRequestWithContext(ctx, "GET", "https://kubernetes.default.svc.cluster.local/api", nil)
+	if err != nil {
+		return fmt.Errorf("creating request failed: %w", err)
+	}
+	req.Header.Set("Authorization", "Bearer "+string(token))
+
+	res, err := client.Do(req)
+	if err != nil {
+		return fmt.Errorf("request failed: %w", err)
+	}
+	defer res.Body.Close()
+
+	j := struct {
+		Kind    string `json:"kind"`
+		Message string `json:"message"`
+	}{}
+	if err := json.NewDecoder(res.Body).Decode(&j); err != nil {
+		return fmt.Errorf("json parse error: %w", err)
+	}
+
+	if j.Kind == "Status" {
+		return fmt.Errorf("API server responded with error: %q", j.Message)
+	}
+	if j.Kind != "APIVersions" {
+		return fmt.Errorf("unexpected response from server (kind: %q)", j.Kind)
+	}
+
+	return nil
+}
+
+func main() {
+	log.Printf("Metropolis Kubernetes self-test starting...")
+	ctx, ctxC := context.WithTimeout(context.Background(), 10*time.Second)
+	defer ctxC()
+
+	log.Printf("1. In-cluster Kubernetes client...")
+	if err := test1InClusterKubernetes(ctx); err != nil {
+		fmt.Println(err.Error())
+		os.Exit(1)
+	}
+
+	log.Printf("All tests passed.")
+}