feat(hub): host-report client + collector + first daemon loop (slice 3, v0.3.0)
internal/hub: the agent's first daemon — a periodic read-only host-report POSTed to the hub (the heartbeat; no separate ping). - HostReport wire contract (shared field-for-field with the hub ingest): host metrics, guests (vmid + spec), cloudflared status; storage/backups/restore-tests/ pbs/audit collections DEFINED but emitted empty (slices 5/6 fill). - Collector over a read-only proxmoxReader (adapted to the real proxmox surface; no proxmox changes) + a CloudflaredProber. Partial-failure: NodeStatus fail = hard (skip POST); per-guest GuestConfig fail = status "unknown", still report. - Client: Bearer-auth POST, standard TLS (system roots / optional ca_file), typed TransportError/HTTPError, token never in errors. - Loop: immediate first report, adopt hub poll_interval (clamp [60,3600]), resilient to collect/report errors, clean ctx-cancel shutdown. - ControlEnvelope: only poll_interval_seconds acted on; blocked/desired_generation/ has_signed_ops parsed-but-ignored (slice 4). - config: HubConfig + FELHOM_AGENT_HUB_* overlay + mode-aware HubConfig.Validate + WithDefaults + hub-key redaction; example config updated. - main: no-selftest mode is now the daemon; added --selftest=hub. Version -> 0.3.0. Tests: report serialization, client (incl. token-redaction), collector partial- failure, loop continuation+interval adoption, config. internal/proxmox + internal/ authz untouched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
package hub
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-agent/internal/proxmox"
|
||||
)
|
||||
|
||||
func quietLogger() *slog.Logger { return slog.New(slog.NewTextHandler(io.Discard, nil)) }
|
||||
|
||||
// roundTripFunc is a mock http.RoundTripper.
|
||||
type roundTripFunc func(*http.Request) (*http.Response, error)
|
||||
|
||||
func (f roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) }
|
||||
|
||||
// testClient builds a hub Client over a mock transport (no network).
|
||||
func testClient(rt roundTripFunc) *Client {
|
||||
return newClient("https://hub.example.test", "super-secret-bearer-key", &http.Client{Transport: rt}, quietLogger())
|
||||
}
|
||||
|
||||
func httpResp(code int, body string) *http.Response {
|
||||
return &http.Response{
|
||||
StatusCode: code,
|
||||
Body: io.NopCloser(strings.NewReader(body)),
|
||||
Header: http.Header{"Content-Type": []string{"application/json"}},
|
||||
}
|
||||
}
|
||||
|
||||
// fakePx is a fake proxmoxReader.
|
||||
type fakePx struct {
|
||||
node string
|
||||
ns proxmox.NodeStatus
|
||||
nsErr error
|
||||
lxc []proxmox.Guest
|
||||
lxcErr error
|
||||
cfg map[int]proxmox.GuestConfig
|
||||
cfgErr map[int]error
|
||||
}
|
||||
|
||||
func (f *fakePx) Node() string { return f.node }
|
||||
func (f *fakePx) NodeStatus(ctx context.Context) (proxmox.NodeStatus, error) {
|
||||
return f.ns, f.nsErr
|
||||
}
|
||||
func (f *fakePx) ListLXC(ctx context.Context) ([]proxmox.Guest, error) { return f.lxc, f.lxcErr }
|
||||
func (f *fakePx) GuestConfig(ctx context.Context, vmid int) (proxmox.GuestConfig, error) {
|
||||
if e := f.cfgErr[vmid]; e != nil {
|
||||
return proxmox.GuestConfig{}, e
|
||||
}
|
||||
return f.cfg[vmid], nil
|
||||
}
|
||||
|
||||
// fakeProber is a fake CloudflaredProber.
|
||||
type fakeProber struct {
|
||||
status string
|
||||
err error
|
||||
}
|
||||
|
||||
func (p fakeProber) Status(ctx context.Context) (string, error) { return p.status, p.err }
|
||||
Reference in New Issue
Block a user