runtimeclass-debugger/pkg/metaserver/client_test.go
lakshit verma 2d74853196
metaserver: retry TLS with --ca-file alone, not only with certs
The TLS retry was gated on hasCerts (cert-file and key-file both set),
so a TLS-only MetaServer with a CA-signed server certificate was never
tried when the caller passed only --ca-file. The tool then reported
AuthRequiredError and recommended the cert flags that would not help.

Run the TLS retry whenever a CA file or client certificates are
provided, and report a distinct reason when the handshake fails with a
CA-only setup.
2026-08-08 01:04:50 +05:30

307 lines
8.6 KiB
Go

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)
}
}