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,60 @@
|
||||
package hub
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHostReport_FieldNamesAndEmptyCollections(t *testing.T) {
|
||||
r := &HostReport{
|
||||
HostID: "demo-host-01",
|
||||
ReportedAt: "2026-06-08T12:00:00Z",
|
||||
AgentVersion: "0.3.0",
|
||||
Host: HostMetrics{
|
||||
Node: "demo-felhom", CPUPercent: 3.2,
|
||||
MemoryTotalBytes: 16777216000, MemoryUsedBytes: 4194304000, MemoryPercent: 25.0,
|
||||
DiskTotalBytes: 152000000000, DiskUsedBytes: 30000000000, DiskPercent: 19.7,
|
||||
LoadAvg: []string{"0.10", "0.20", "0.15"}, UptimeSeconds: 86400,
|
||||
},
|
||||
Guests: []Guest{{
|
||||
VMID: 100, Name: "felhom-cust-acme", Status: "running", ControllerVersion: "",
|
||||
Spec: &GuestSpec{Cores: 2, MemoryBytes: 2147483648, DiskBytes: 21474836480},
|
||||
}},
|
||||
StorageTargets: []StorageTarget{},
|
||||
Backups: []Backup{},
|
||||
RestoreTests: []RestoreTest{},
|
||||
PBSSnapshots: []PBSSnapshot{},
|
||||
AuditTail: []AuditEntry{},
|
||||
Cloudflared: Cloudflared{Status: "active"},
|
||||
}
|
||||
b, err := json.Marshal(r)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := string(b)
|
||||
|
||||
for _, field := range []string{
|
||||
`"host_id":"demo-host-01"`, `"reported_at":`, `"agent_version":"0.3.0"`,
|
||||
`"cpu_percent":3.2`, `"memory_total_bytes":16777216000`, `"loadavg":["0.10","0.20","0.15"]`,
|
||||
`"disk_percent":19.7`, `"uptime_seconds":86400`,
|
||||
`"vmid":100`, `"controller_version":""`, `"memory_bytes":2147483648`,
|
||||
`"cloudflared":{"status":"active"}`,
|
||||
// empty collections must be [] not null
|
||||
`"storage_targets":[]`, `"backups":[]`, `"restore_tests":[]`, `"pbs_snapshots":[]`, `"audit_tail":[]`,
|
||||
} {
|
||||
if !strings.Contains(got, field) {
|
||||
t.Errorf("report JSON missing %s\n got: %s", field, got)
|
||||
}
|
||||
}
|
||||
if strings.Contains(got, "null") {
|
||||
t.Errorf("report JSON must not contain null (empty collections should be []): %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGuest_SpecOmittedWhenUnknown(t *testing.T) {
|
||||
b, _ := json.Marshal(Guest{VMID: 9, Name: "g", Status: "unknown"})
|
||||
if strings.Contains(string(b), "spec") {
|
||||
t.Errorf("unknown guest should omit spec, got %s", b)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user