mirror of
https://github.com/vee1e/runtimeclass-debugger.git
synced 2026-09-01 18:27:58 +00:00
golangci-lint v2 with its default linters flags ten unchecked Close returns (errcheck) that v1.64 did not report: the database and rows handles in the SQLite store, the MetaServer response body and probe connection, and the test fixtures.
307 lines
8.6 KiB
Go
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)
|
|
}
|
|
}
|