fleet-manager: checkpoint 1 — service skeleton + inventory store + admin API
First slice of the server-side fleet control plane (serving/fleet-manager/), the
backend for the admin SPA (monitor / authorise / lifecycle). See
docs/fleet-management-design.md + tpm-enrolment-design.md §5.
This checkpoint (stdlib-only):
- Inventory record (identity/binding/enrolled/version/health/usage/desired),
keyed by TPM cert serial = hostnqn key, per the design's 'inventory record born
at enrolment'. JSON-file Store (atomic writes, mutex) behind an interface so a
DB swap is a drop-in.
- Admin plane: mTLS server verified by the DEDICATED admin CA (never the fleet CA,
so a host cert can't reach admin) + a plaintext dev mode. JSON API:
GET /api/hosts, GET /api/hosts/{serial}, PUT /api/hosts/{serial}/desired
(desired-state audited to the admin client-cert CN).
- Dev seed (FLEET_MGR_SEED) so the SPA has data before real enrolment/agent flow.
Smoke-tested: list/get/desired-PUT (incl. version-drift + drain), 404s, atomic
persistence. go build + go vet + gofmt clean.
Agent gRPC plane, broker-fold-in authorise, InfluxDB metrics, and the themed SPA
follow in checkpoints 2-6.
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
// inventory.go — the fleet inventory record and the Store abstraction.
|
||||
//
|
||||
// The Host record is the join key the rest of the fleet-manager hangs off. It is
|
||||
// BORN at enrolment approval (identity/binding/enrolled), then FILLED by the
|
||||
// steady-state agent stream (version/health/usage) and by admin action (desired).
|
||||
// Schema mirrors docs/fleet-management-design.md "Inventory record" and
|
||||
// docs/tpm-enrolment-design.md "Inventory record born at enrolment".
|
||||
//
|
||||
// Keyed by the TPM cert SERIAL (lowercase hex) — identical to the value the NVMe
|
||||
// hostnqn derives from and the value the agent's mTLS leaf presents, so the cert
|
||||
// an agent shows IS its inventory key (no identity translation). See §5 identity
|
||||
// discussion.
|
||||
package main
|
||||
|
||||
import "time"
|
||||
|
||||
// Identity — minted at enrolment, immutable afterwards. CertSerial is the primary
|
||||
// key; Hostnqn = HOSTNQN_BASE + CertSerial (as the broker computes it on approve).
|
||||
type Identity struct {
|
||||
CertSerial string `json:"cert_serial"` // lowercase hex — PRIMARY KEY
|
||||
Hostnqn string `json:"hostnqn,omitempty"` // nqn...:host-<serial>
|
||||
MAC string `json:"mac,omitempty"` // advisory (DeviceContext at enrol)
|
||||
EKHash string `json:"ek_hash,omitempty"` // TPM EK hash (advisory)
|
||||
StarshipName string `json:"starship_name,omitempty"` // dnsmasq fleet-namer join
|
||||
}
|
||||
|
||||
// Binding — the physical↔crypto tie the human records during the OOB approval.
|
||||
type Binding struct {
|
||||
AssetTag string `json:"asset_tag,omitempty"`
|
||||
SwitchPort string `json:"switch_port,omitempty"`
|
||||
Location string `json:"location,omitempty"`
|
||||
}
|
||||
|
||||
// Enrolled — provenance of the approval that birthed this record.
|
||||
type Enrolled struct {
|
||||
Approver string `json:"approver,omitempty"` // admin CN from the approve mTLS
|
||||
At time.Time `json:"at,omitempty"` // approval timestamp
|
||||
Fingerprint string `json:"fingerprint,omitempty"` // the human-verified anchor
|
||||
}
|
||||
|
||||
// Version — what the host is actually running (agent-reported).
|
||||
type Version struct {
|
||||
ImageVersion string `json:"image_version,omitempty"`
|
||||
RootfsDigest string `json:"rootfs_digest,omitempty"`
|
||||
Kernel string `json:"kernel,omitempty"`
|
||||
}
|
||||
|
||||
// Health — the LATEST snapshot only. The full time-series lives in InfluxDB
|
||||
// (written by the manager as agent reports arrive); this cached last value is
|
||||
// what the fleet-overview list renders without a per-host Flux query.
|
||||
type Health struct {
|
||||
LoadAvg float64 `json:"load_avg,omitempty"`
|
||||
CPUPct float64 `json:"cpu_pct,omitempty"`
|
||||
TempC float64 `json:"temp_c,omitempty"`
|
||||
MemUsedPct float64 `json:"mem_used_pct,omitempty"`
|
||||
TmpfsUsedPct float64 `json:"tmpfs_used_pct,omitempty"` // overlay RAM pressure (ties to #17)
|
||||
NVMeRoot string `json:"nvme_root,omitempty"` // ok|degraded|down
|
||||
Auth string `json:"auth,omitempty"` // 802.1x/macsec: up|down
|
||||
SELinux string `json:"selinux,omitempty"` // enforcing|permissive
|
||||
Chrony string `json:"chrony,omitempty"` // synced|unsynced
|
||||
KioskActive bool `json:"kiosk_active"`
|
||||
}
|
||||
|
||||
// Usage — kiosk-session facts (agent-reported).
|
||||
type Usage struct {
|
||||
KioskURL string `json:"kiosk_url,omitempty"`
|
||||
SessionUptime string `json:"session_uptime,omitempty"`
|
||||
ChromiumCrashes int `json:"chromium_crashes,omitempty"`
|
||||
Display string `json:"display,omitempty"`
|
||||
}
|
||||
|
||||
// Desired — SERVER-AUTHORITATIVE target state. Set via the admin lifecycle API,
|
||||
// persisted here, and pushed to the host over its live agent stream (or delivered
|
||||
// on its next reconnect if offline). Inventory is the source of truth; the stream
|
||||
// is only the delivery optimisation.
|
||||
type Desired struct {
|
||||
TargetVersion string `json:"target_version,omitempty"`
|
||||
KioskURL string `json:"kiosk_url,omitempty"`
|
||||
RebootAt *time.Time `json:"reboot_at,omitempty"`
|
||||
Drain bool `json:"drain,omitempty"`
|
||||
UpdatedBy string `json:"updated_by,omitempty"` // admin CN
|
||||
UpdatedAt time.Time `json:"updated_at,omitempty"`
|
||||
}
|
||||
|
||||
// Host is the full inventory record.
|
||||
type Host struct {
|
||||
Identity Identity `json:"identity"`
|
||||
Binding Binding `json:"binding"`
|
||||
Enrolled Enrolled `json:"enrolled"`
|
||||
Version Version `json:"version"`
|
||||
Health Health `json:"health"`
|
||||
Usage Usage `json:"usage"`
|
||||
Desired Desired `json:"desired"`
|
||||
FirstSeen time.Time `json:"first_seen"`
|
||||
LastSeen time.Time `json:"last_seen"` // last agent report / stream activity
|
||||
Online bool `json:"online"` // agent stream currently connected
|
||||
}
|
||||
|
||||
// Store is the inventory persistence abstraction. The v1 implementation is a
|
||||
// JSON-file store (jsonStore); the interface keeps a DB swap a drop-in later.
|
||||
// All methods are safe for concurrent use.
|
||||
type Store interface {
|
||||
// List returns a snapshot of every host (copies — safe to mutate).
|
||||
List() []*Host
|
||||
// Get returns a copy of one host by cert serial.
|
||||
Get(serial string) (*Host, bool)
|
||||
// Upsert creates or replaces a host record (used by enrolment approval).
|
||||
Upsert(h *Host) error
|
||||
// SetDesired replaces the desired-state block and returns the updated host.
|
||||
SetDesired(serial string, d Desired) (*Host, bool, error)
|
||||
// ReportState applies an agent snapshot (version/health/usage + last-seen).
|
||||
ReportState(serial string, v Version, h Health, u Usage, at time.Time) (*Host, bool, error)
|
||||
// SetOnline flips the live-stream presence flag (and stamps last-seen on connect).
|
||||
SetOnline(serial string, online bool, at time.Time)
|
||||
}
|
||||
Reference in New Issue
Block a user