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.
178 lines
5 KiB
Go
178 lines
5 KiB
Go
// Package metaserver queries the edge node MetaServer HTTP API.
|
|
package metaserver
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
nodev1 "k8s.io/api/node/v1"
|
|
)
|
|
|
|
const runtimeClassPath = "/apis/node.k8s.io/v1/runtimeclasses"
|
|
|
|
// AuthRequiredError indicates the MetaServer requires authentication the
|
|
// caller did not provide.
|
|
type AuthRequiredError struct {
|
|
Reason string
|
|
}
|
|
|
|
func (e *AuthRequiredError) Error() string { return e.Reason }
|
|
|
|
// UnreachableError indicates the MetaServer could not be reached at all.
|
|
type UnreachableError struct {
|
|
Reason string
|
|
}
|
|
|
|
func (e *UnreachableError) Error() string { return e.Reason }
|
|
|
|
// Client queries a MetaServer.
|
|
type Client struct {
|
|
BaseURL string
|
|
Timeout time.Duration
|
|
CertFile string
|
|
KeyFile string
|
|
CAFile string
|
|
}
|
|
|
|
// ListRuntimeClasses fetches the RuntimeClass objects served by the
|
|
// MetaServer. Auth is detected and handled in three modes: plain HTTP when
|
|
// no auth is configured, a TLS retry when a CA file or client certificates
|
|
// are provided, and an AuthRequiredError when the server demands
|
|
// certificates that were not provided.
|
|
func (c *Client) ListRuntimeClasses(ctx context.Context) ([]nodev1.RuntimeClass, error) {
|
|
endpoint := c.endpoint()
|
|
useTLS := strings.HasPrefix(c.BaseURL, "https://")
|
|
|
|
body, code, err := c.get(ctx, c.httpClient(useTLS), endpoint)
|
|
if err == nil {
|
|
return c.handleStatus(body, code)
|
|
}
|
|
|
|
if c.hasCerts() || c.CAFile != "" {
|
|
body, code, retryErr := c.get(ctx, c.httpClient(true), endpoint)
|
|
if retryErr == nil {
|
|
return c.handleStatus(body, code)
|
|
}
|
|
err = retryErr
|
|
}
|
|
|
|
if c.speaksTLS(endpoint) {
|
|
switch {
|
|
case c.hasCerts():
|
|
return nil, &AuthRequiredError{Reason: fmt.Sprintf("MetaServer rejected client certificates: %v", err)}
|
|
case c.CAFile != "":
|
|
return nil, &AuthRequiredError{Reason: fmt.Sprintf("MetaServer TLS handshake failed: %v; pass --cert-file/--key-file if the server requires client certificates", err)}
|
|
default:
|
|
return nil, &AuthRequiredError{Reason: "MetaServer requires TLS with client certificates; pass --cert-file/--key-file/--ca-file"}
|
|
}
|
|
}
|
|
return nil, &UnreachableError{Reason: fmt.Sprintf("MetaServer not reachable at %s: %v", c.BaseURL, err)}
|
|
}
|
|
|
|
func (c *Client) endpoint() string {
|
|
base := strings.TrimRight(c.BaseURL, "/")
|
|
if !strings.HasPrefix(base, "http://") && !strings.HasPrefix(base, "https://") {
|
|
base = "http://" + base
|
|
}
|
|
return base + runtimeClassPath
|
|
}
|
|
|
|
func (c *Client) get(ctx context.Context, client *http.Client, endpoint string) ([]byte, int, error) {
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
req.Header.Set("Accept", "application/json")
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, resp.StatusCode, err
|
|
}
|
|
return body, resp.StatusCode, nil
|
|
}
|
|
|
|
func (c *Client) handleStatus(body []byte, code int) ([]nodev1.RuntimeClass, error) {
|
|
switch code {
|
|
case http.StatusOK:
|
|
return parseList(body)
|
|
case http.StatusUnauthorized, http.StatusForbidden:
|
|
return nil, &AuthRequiredError{Reason: fmt.Sprintf("MetaServer requires authentication (HTTP %d); pass --cert-file/--key-file/--ca-file", code)}
|
|
default:
|
|
return nil, fmt.Errorf("MetaServer returned HTTP %d: %.200s", code, strings.TrimSpace(string(body)))
|
|
}
|
|
}
|
|
|
|
func parseList(body []byte) ([]nodev1.RuntimeClass, error) {
|
|
var list nodev1.RuntimeClassList
|
|
if err := json.Unmarshal(body, &list); err != nil {
|
|
return nil, fmt.Errorf("unexpected MetaServer response: %v", err)
|
|
}
|
|
sort.Slice(list.Items, func(i, j int) bool { return list.Items[i].Name < list.Items[j].Name })
|
|
return list.Items, nil
|
|
}
|
|
|
|
func (c *Client) hasCerts() bool {
|
|
return c.CertFile != "" && c.KeyFile != ""
|
|
}
|
|
|
|
func (c *Client) httpClient(useTLS bool) *http.Client {
|
|
if !useTLS {
|
|
return &http.Client{Timeout: c.Timeout}
|
|
}
|
|
tlsConfig := &tls.Config{MinVersion: tls.VersionTLS12}
|
|
if c.CAFile != "" {
|
|
pool, err := x509.SystemCertPool()
|
|
if err != nil {
|
|
pool = x509.NewCertPool()
|
|
}
|
|
if ca, err := os.ReadFile(c.CAFile); err == nil {
|
|
pool.AppendCertsFromPEM(ca)
|
|
}
|
|
tlsConfig.RootCAs = pool
|
|
}
|
|
if c.hasCerts() {
|
|
if cert, err := tls.LoadX509KeyPair(c.CertFile, c.KeyFile); err == nil {
|
|
tlsConfig.Certificates = []tls.Certificate{cert}
|
|
}
|
|
}
|
|
return &http.Client{
|
|
Timeout: c.Timeout,
|
|
Transport: &http.Transport{
|
|
TLSClientConfig: tlsConfig,
|
|
},
|
|
}
|
|
}
|
|
|
|
// speaksTLS probes whether the endpoint answers TLS handshakes, which
|
|
// indicates a MetaServer that requires client certificates.
|
|
func (c *Client) speaksTLS(endpoint string) bool {
|
|
u, err := url.Parse(endpoint)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
dialer := &net.Dialer{Timeout: c.Timeout}
|
|
conn, err := tls.DialWithDialer(dialer, "tcp", u.Host, &tls.Config{
|
|
InsecureSkipVerify: true,
|
|
MinVersion: tls.VersionTLS12,
|
|
})
|
|
if err != nil {
|
|
return false
|
|
}
|
|
_ = conn.Close()
|
|
return true
|
|
}
|