1be7cf2b7f
CI / test (push) Successful in 1m41s
Implements the narrowed Stage 1 scope from ROADMAP.md, against the bootstrap-flow fix from DESIGN.md §4.1/§6 (the earlier self-registration flow couldn't work unauthenticated against terdut-server's real AuthMiddleware -- see that commit for the full trace). - api/v1alpha1: TerdutServer with spec.endpoint + spec.credentialsSecretRef + spec.allowedTeams (image/replicas/networking/database deferred to Stage 5, per DESIGN.md's own narrowing). SecretKeyRef has no namespace field -- always the operator's own, by construction. - internal/controller: TerdutServerReconciler implements exactly the bring-your-own path -- adopt spec.credentialsSecretRef if the Secret exists and has data under the given key, probe GET /api/version as a reachability check, set Ready/Bootstrapped conditions accordingly. Self-registration (the /api/bootstrap race) is not implemented; unset spec.credentialsSecretRef reports Ready: False, reason: CredentialsSecretRefRequired, not an attempt at a flow that would fail unauthenticated anyway. No finalizer: this stage creates nothing server-side and adopts rather than generates its Secret, so there's nothing to clean up on delete yet. - internal/tdclient: minimal terdut-server API client (Version only, the one call this stage needs), styled after terdut-tui's own internal/api/client.go per terdut/CLAUDE.md's mirroring convention. - Tests: envtest suite covering all four not-ready paths plus the happy path (fake terdut-server via httptest.Server, per DESIGN.md §11), and a focused unit suite for tdclient. 75.6%/82.4% coverage. - Event recording uses the new events.k8s.io/v1 recorder API (mgr.GetEventRecorder), not the deprecated GetEventRecorderFor -- caught by golangci-lint's staticcheck before it shipped. Verified locally: make fmt lint test build all clean, 0 lint issues, all specs pass.
116 lines
3.4 KiB
Go
116 lines
3.4 KiB
Go
// Package tdclient is a minimal terdut-server API client for the operator's
|
|
// own controllers. It mirrors the shape of terdut-tui's
|
|
// internal/api/client.go (baseURL/httpClient fields, a shared do/statusError
|
|
// helper, per terdut/CLAUDE.md's "any change to a server endpoint or JSON
|
|
// shape must be mirrored" convention) but authorizes with a Bearer API key
|
|
// rather than a session cookie — the operator never signs in as a human
|
|
// (DESIGN.md §6).
|
|
//
|
|
// Deliberately minimal for Stage 1 (ROADMAP.md): only Version, the
|
|
// reachability probe TerdutServer's controller needs. Bootstrap and the
|
|
// service-account endpoints land here once self-registration does (Stage 5).
|
|
package tdclient
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Client talks to one terdut-server install, optionally as a service
|
|
// account. A zero-value token works for endpoints that don't need one
|
|
// (Version).
|
|
type Client struct {
|
|
baseURL string
|
|
httpClient *http.Client
|
|
token string
|
|
}
|
|
|
|
// New creates a Client against baseURL, with no credential set.
|
|
func New(baseURL string) *Client {
|
|
return &Client{
|
|
baseURL: strings.TrimRight(baseURL, "/"),
|
|
httpClient: &http.Client{Timeout: 10 * time.Second},
|
|
}
|
|
}
|
|
|
|
// WithToken returns a copy of c that authorizes every request as a Bearer
|
|
// credential — a user's own API key or a service-account key
|
|
// (SERVICE-ACCOUNTS.md), the server resolves either the same way.
|
|
func (c *Client) WithToken(token string) *Client {
|
|
cp := *c
|
|
cp.token = token
|
|
return &cp
|
|
}
|
|
|
|
// StatusError is a non-2xx response — the server's {"error": "..."} body
|
|
// decoded into Message, same shape terdut-tui's client uses.
|
|
type StatusError struct {
|
|
Code int
|
|
Message string
|
|
}
|
|
|
|
func (e *StatusError) Error() string {
|
|
if e.Message != "" {
|
|
return fmt.Sprintf("server returned %d: %s", e.Code, e.Message)
|
|
}
|
|
return fmt.Sprintf("server returned %d", e.Code)
|
|
}
|
|
|
|
func statusError(resp *http.Response) error {
|
|
var e struct {
|
|
Error string `json:"error"`
|
|
}
|
|
_ = json.NewDecoder(resp.Body).Decode(&e)
|
|
return &StatusError{Code: resp.StatusCode, Message: e.Error}
|
|
}
|
|
|
|
func (c *Client) newRequest(ctx context.Context, method, path string) (*http.Request, error) {
|
|
req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Accept", "application/json")
|
|
if c.token != "" {
|
|
req.Header.Set("Authorization", "Bearer "+c.token)
|
|
}
|
|
return req, nil
|
|
}
|
|
|
|
func (c *Client) do(req *http.Request, out any) error {
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
if resp.StatusCode >= 400 {
|
|
return statusError(resp)
|
|
}
|
|
if out != nil {
|
|
return json.NewDecoder(resp.Body).Decode(out)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Version calls GET /api/version — unauthenticated, per terdut-server's own
|
|
// router.go comment ("a client deciding whether it can talk to this server —
|
|
// terdut-tui, terdut-operator — needs to ask before it holds a credential
|
|
// for it"). Used here purely as a reachability probe: a bad endpoint fails
|
|
// here, clearly, rather than on whatever the controller tries first.
|
|
func (c *Client) Version(ctx context.Context) (string, error) {
|
|
req, err := c.newRequest(ctx, http.MethodGet, "/api/version")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
var v struct {
|
|
Version string `json:"version"`
|
|
}
|
|
if err := c.do(req, &v); err != nil {
|
|
return "", err
|
|
}
|
|
return v.Version, nil
|
|
}
|