hub v0.16.0 + host-install v1.1.0: Day-0 artifact manifest + self-install the agent (BUNDLE slice)
Hub (v0.16.0):
- store: ArtifactManifest{agent,golden version+sha256} in hub_settings; Get/SetArtifactManifest.
- handler: GET /api/v1/artifacts/{id} (passphrase auth, mirrors config-retrieve). Unset => 200 empty.
- web: operator UI "Day-0 artifacts" card (POST /configs/artifacts), semver + 64-hex validation.
- artifact_test.go: returned-verbatim / unset-empty / 401 / 404 / store round-trip.
host-install (v1.1.0):
- new step 5/8 agent-install: manifest + git token (config-retrieve) -> fetch binary from Gitea ->
verify sha256 vs hub manifest (abort on mismatch) -> install non-root felhom-agent user + binary +
sudoers (visudo -cf) + canonical unit. Idempotent.
- new step 7/8 golden: local fallback else fetch+verify+import from Gitea (--force-gitea-golden).
- agent now runs non-root (privileged.mode sudo), config chowned to the service user.
- README prerequisites trimmed to: install PVE + create customer.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -868,6 +868,75 @@ func (s *Store) SetGlobalMinControllerVersion(version string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// ArtifactManifest is the operator-vouched current artifact set (agent binary + golden archive)
|
||||
// served to the host-bootstrap script so it can verify-before-install. The hub is the TRUST ROOT
|
||||
// for these checksums (a different root than Gitea, which only STORES the bytes): the script fetches
|
||||
// each artifact from Gitea with the config-retrieve git token, then checks its sha256 against the
|
||||
// value recorded here before installing/using it. Empty fields = nothing published yet (the script
|
||||
// then falls back to the local golden / fails clearly on a missing binary).
|
||||
type ArtifactManifest struct {
|
||||
AgentVersion string `json:"agent_version"`
|
||||
AgentSHA256 string `json:"agent_sha256"`
|
||||
GoldenVersion string `json:"golden_version"`
|
||||
GoldenSHA256 string `json:"golden_sha256"`
|
||||
}
|
||||
|
||||
// hub_settings keys for the artifact manifest (BUNDLE slice). Stored as discrete key/value rows in
|
||||
// the existing hub_settings table — same mechanism as the controller-version floor, so it survives
|
||||
// restarts and needs no schema change.
|
||||
const (
|
||||
settingArtifactAgentVersion = "artifact_agent_version"
|
||||
settingArtifactAgentSHA256 = "artifact_agent_sha256"
|
||||
settingArtifactGoldenVersion = "artifact_golden_version"
|
||||
settingArtifactGoldenSHA256 = "artifact_golden_sha256"
|
||||
)
|
||||
|
||||
// getSetting reads a single hub_settings value ("" if the row is absent).
|
||||
func (s *Store) getSetting(key string) string {
|
||||
var v string
|
||||
if err := s.db.QueryRow(`SELECT value FROM hub_settings WHERE key = ?`, key).Scan(&v); err != nil {
|
||||
return ""
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// setSetting upserts a single hub_settings value.
|
||||
func (s *Store) setSetting(key, value string) error {
|
||||
_, err := s.db.Exec(`
|
||||
INSERT INTO hub_settings (key, value, updated_at)
|
||||
VALUES (?, ?, datetime('now'))
|
||||
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = datetime('now')`,
|
||||
key, value,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetArtifactManifest returns the operator-recorded current artifact set. All-empty when nothing
|
||||
// has been published yet.
|
||||
func (s *Store) GetArtifactManifest() ArtifactManifest {
|
||||
return ArtifactManifest{
|
||||
AgentVersion: s.getSetting(settingArtifactAgentVersion),
|
||||
AgentSHA256: s.getSetting(settingArtifactAgentSHA256),
|
||||
GoldenVersion: s.getSetting(settingArtifactGoldenVersion),
|
||||
GoldenSHA256: s.getSetting(settingArtifactGoldenSHA256),
|
||||
}
|
||||
}
|
||||
|
||||
// SetArtifactManifest persists the operator-recorded current artifact set (all four fields). Each
|
||||
// field is stored independently so a partial form submission (e.g. agent only) still round-trips.
|
||||
func (s *Store) SetArtifactManifest(m ArtifactManifest) error {
|
||||
if err := s.setSetting(settingArtifactAgentVersion, m.AgentVersion); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.setSetting(settingArtifactAgentSHA256, m.AgentSHA256); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.setSetting(settingArtifactGoldenVersion, m.GoldenVersion); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.setSetting(settingArtifactGoldenSHA256, m.GoldenSHA256)
|
||||
}
|
||||
|
||||
// EffectiveMinControllerVersion resolves the floor that actually applies to a customer: the
|
||||
// per-customer override when set (non-empty), otherwise the global floor (hub_settings → config/env
|
||||
// default). Returns "" when no floor applies at all (Phase 2 inert for that customer).
|
||||
|
||||
Reference in New Issue
Block a user