package metaserver import ( "context" "crypto/rand" "crypto/rsa" "crypto/tls" "crypto/x509" "crypto/x509/pkix" "encoding/json" "encoding/pem" "errors" "math/big" "net" "net/http" "net/http/httptest" "os" "path/filepath" "testing" "time" ) func runtimeClassList(names ...string) []byte { items := []map[string]interface{}{} for _, n := range names { items = append(items, map[string]interface{}{ "apiVersion": "node.k8s.io/v1", "kind": "RuntimeClass", "metadata": map[string]interface{}{"name": n}, "handler": n, }) } b, _ := json.Marshal(map[string]interface{}{ "apiVersion": "node.k8s.io/v1", "kind": "RuntimeClassList", "metadata": map[string]interface{}{}, "items": items, }) return b } func newClient(serverURL string) *Client { return &Client{BaseURL: serverURL, Timeout: 5 * time.Second} } func TestListRuntimeClasses(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/apis/node.k8s.io/v1/runtimeclasses" { w.WriteHeader(http.StatusNotFound) return } _, _ = w.Write(runtimeClassList("runsc", "kata")) })) defer server.Close() classes, err := newClient(server.URL).ListRuntimeClasses(context.Background()) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(classes) != 2 || classes[0].Name != "kata" || classes[1].Name != "runsc" { t.Fatalf("expected sorted [kata runsc], got %+v", classes) } } func TestListEmptyList(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write(runtimeClassList()) })) defer server.Close() classes, err := newClient(server.URL).ListRuntimeClasses(context.Background()) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(classes) != 0 { t.Fatalf("expected no classes, got %+v", classes) } } func TestServerErrorNotAuthNotUnreachable(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) _, _ = w.Write([]byte("boom")) })) defer server.Close() _, err := newClient(server.URL).ListRuntimeClasses(context.Background()) if err == nil { t.Fatal("expected error, got nil") } var authErr *AuthRequiredError var unreachErr *UnreachableError if errors.As(err, &authErr) || errors.As(err, &unreachErr) { t.Fatalf("expected plain error, got %T: %v", err, err) } } func TestAuthRequiredOnHTTPStatus(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusForbidden) })) defer server.Close() _, err := newClient(server.URL).ListRuntimeClasses(context.Background()) var authErr *AuthRequiredError if !errors.As(err, &authErr) { t.Fatalf("expected AuthRequiredError, got %v", err) } } func TestUnreachable(t *testing.T) { l, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } addr := l.Addr().String() _ = l.Close() _, err = newClient("http://" + addr).ListRuntimeClasses(context.Background()) var unreachErr *UnreachableError if !errors.As(err, &unreachErr) { t.Fatalf("expected UnreachableError, got %v", err) } } func TestMalformedResponse(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte("not json")) })) defer server.Close() _, err := newClient(server.URL).ListRuntimeClasses(context.Background()) if err == nil { t.Fatal("expected error, got nil") } } type testPKI struct { caPath string clientCertPath string clientKeyPath string serverCert tls.Certificate } func generateTestPKI(t *testing.T) *testPKI { t.Helper() dir := t.TempDir() now := time.Now() caKey, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { t.Fatal(err) } caTmpl := &x509.Certificate{ SerialNumber: big.NewInt(1), Subject: pkix.Name{CommonName: "runtimeclass-debugger-test-ca"}, NotBefore: now.Add(-time.Hour), NotAfter: now.Add(24 * time.Hour), IsCA: true, KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature, BasicConstraintsValid: true, } caDER, err := x509.CreateCertificate(rand.Reader, caTmpl, caTmpl, &caKey.PublicKey, caKey) if err != nil { t.Fatal(err) } caCert, err := x509.ParseCertificate(caDER) if err != nil { t.Fatal(err) } caPath := filepath.Join(dir, "ca.crt") if err := os.WriteFile(caPath, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: caDER}), 0o600); err != nil { t.Fatal(err) } signLeaf := func(cn string, ip net.IP, usage []x509.ExtKeyUsage) (tls.Certificate, string, string) { key, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { t.Fatal(err) } tmpl := &x509.Certificate{ SerialNumber: big.NewInt(2), Subject: pkix.Name{CommonName: cn}, NotBefore: now.Add(-time.Hour), NotAfter: now.Add(24 * time.Hour), KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, ExtKeyUsage: usage, } if ip != nil { tmpl.IPAddresses = []net.IP{ip} } der, err := x509.CreateCertificate(rand.Reader, tmpl, caCert, &key.PublicKey, caKey) if err != nil { t.Fatal(err) } certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}) keyDER, err := x509.MarshalPKCS8PrivateKey(key) if err != nil { t.Fatal(err) } keyPEM := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyDER}) certPath := filepath.Join(dir, cn+".crt") keyPath := filepath.Join(dir, cn+".key") if err := os.WriteFile(certPath, certPEM, 0o600); err != nil { t.Fatal(err) } if err := os.WriteFile(keyPath, keyPEM, 0o600); err != nil { t.Fatal(err) } cert, err := tls.X509KeyPair(certPEM, keyPEM) if err != nil { t.Fatal(err) } return cert, certPath, keyPath } serverCert, _, _ := signLeaf("metaserver", net.ParseIP("127.0.0.1"), []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}) _, clientCertPath, clientKeyPath := signLeaf("client", nil, []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}) return &testPKI{caPath: caPath, clientCertPath: clientCertPath, clientKeyPath: clientKeyPath, serverCert: serverCert} } func newTLSMetaServer(t *testing.T, pki *testPKI) *httptest.Server { t.Helper() caPEM, err := os.ReadFile(pki.caPath) if err != nil { t.Fatal(err) } pool := x509.NewCertPool() pool.AppendCertsFromPEM(caPEM) handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if len(r.TLS.PeerCertificates) == 0 { w.WriteHeader(http.StatusUnauthorized) return } _, _ = w.Write(runtimeClassList("kata")) }) server := httptest.NewUnstartedServer(handler) server.TLS = &tls.Config{ Certificates: []tls.Certificate{pki.serverCert}, ClientCAs: pool, ClientAuth: tls.VerifyClientCertIfGiven, MinVersion: tls.VersionTLS12, } server.StartTLS() return server } func TestAuthTLSWithoutCertsWarns(t *testing.T) { server := newTLSMetaServer(t, generateTestPKI(t)) defer server.Close() _, err := newClient(server.URL).ListRuntimeClasses(context.Background()) var authErr *AuthRequiredError if !errors.As(err, &authErr) { t.Fatalf("expected AuthRequiredError, got %v", err) } } func TestAuthTLSWithCertsSucceeds(t *testing.T) { pki := generateTestPKI(t) server := newTLSMetaServer(t, pki) defer server.Close() client := &Client{ BaseURL: server.URL, Timeout: 5 * time.Second, CertFile: pki.clientCertPath, KeyFile: pki.clientKeyPath, CAFile: pki.caPath, } classes, err := client.ListRuntimeClasses(context.Background()) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(classes) != 1 || classes[0].Name != "kata" { t.Fatalf("expected [kata], got %+v", classes) } } // TestTLSWithCAOnlySucceeds covers a TLS-only MetaServer whose certificate // is signed by a CA the caller trusts: no client certificate is needed, and // passing --ca-file alone must trigger the TLS retry. func TestTLSWithCAOnlySucceeds(t *testing.T) { pki := generateTestPKI(t) server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write(runtimeClassList("kata")) })) server.TLS = &tls.Config{ Certificates: []tls.Certificate{pki.serverCert}, MinVersion: tls.VersionTLS12, } server.StartTLS() defer server.Close() client := &Client{ BaseURL: server.URL, Timeout: 5 * time.Second, CAFile: pki.caPath, } classes, err := client.ListRuntimeClasses(context.Background()) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(classes) != 1 || classes[0].Name != "kata" { t.Fatalf("expected [kata], got %+v", classes) } }