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