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:
@@ -139,9 +139,12 @@ func (s *Server) handleConfigList(w http.ResponseWriter, r *http.Request) {
|
||||
return entries[i].CustomerID < entries[j].CustomerID
|
||||
})
|
||||
|
||||
artifacts := s.store.GetArtifactManifest()
|
||||
|
||||
data := struct {
|
||||
Customers []customerListEntry
|
||||
GlobalFloor string
|
||||
Artifacts store.ArtifactManifest
|
||||
ActiveNav string
|
||||
Flash string
|
||||
CSRFToken string
|
||||
@@ -149,6 +152,7 @@ func (s *Server) handleConfigList(w http.ResponseWriter, r *http.Request) {
|
||||
}{
|
||||
Customers: entries,
|
||||
GlobalFloor: globalFloor,
|
||||
Artifacts: artifacts,
|
||||
ActiveNav: "configs",
|
||||
Flash: r.URL.Query().Get("flash"),
|
||||
CSRFToken: s.csrfToken(r),
|
||||
@@ -636,6 +640,58 @@ func (s *Server) handleSetGlobalFloor(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/configs?flash=floor_set", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// validSHA256 matches a lowercase 64-hex sha256 digest. Empty is also accepted by the artifact
|
||||
// handler (clears that artifact's checksum).
|
||||
var validSHA256 = regexp.MustCompile(`^[0-9a-f]{64}$`)
|
||||
|
||||
// normalizeSHA256 trims/lowercases and validates a sha256 submitted from the operator UI.
|
||||
// Returns (value, true) on a valid 64-hex digest or empty string; (_, false) otherwise.
|
||||
func normalizeSHA256(raw string) (string, bool) {
|
||||
v := strings.ToLower(strings.TrimSpace(raw))
|
||||
if v == "" {
|
||||
return "", true
|
||||
}
|
||||
if !validSHA256.MatchString(v) {
|
||||
return "", false
|
||||
}
|
||||
return v, true
|
||||
}
|
||||
|
||||
// handleSetArtifacts records the operator-vouched current artifact set (agent binary + golden
|
||||
// archive: version + sha256 each) into hub_settings. This is the checksum TRUST ROOT the
|
||||
// host-bootstrap script verifies fetched artifacts against. Versions are validated as bare semver
|
||||
// (reusing the floor validator); sha256s as 64-hex. Empty fields are allowed (clears that field).
|
||||
func (s *Server) handleSetArtifacts(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "Bad request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
agentVer, okAV := normalizeFloorInput(r.FormValue("agent_version"))
|
||||
agentSHA, okAS := normalizeSHA256(r.FormValue("agent_sha256"))
|
||||
goldenVer, okGV := normalizeFloorInput(r.FormValue("golden_version"))
|
||||
goldenSHA, okGS := normalizeSHA256(r.FormValue("golden_sha256"))
|
||||
if !okAV || !okGV {
|
||||
http.Redirect(w, r, "/configs?flash=artifact_ver_invalid", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
if !okAS || !okGS {
|
||||
http.Redirect(w, r, "/configs?flash=artifact_sha_invalid", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
if err := s.store.SetArtifactManifest(store.ArtifactManifest{
|
||||
AgentVersion: agentVer,
|
||||
AgentSHA256: agentSHA,
|
||||
GoldenVersion: goldenVer,
|
||||
GoldenSHA256: goldenSHA,
|
||||
}); err != nil {
|
||||
s.logger.Printf("[ERROR] Failed to set artifact manifest: %v", err)
|
||||
http.Error(w, "Internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
s.logger.Printf("[INFO] Artifact manifest set: agent=%s golden=%s", agentVer, goldenVer)
|
||||
http.Redirect(w, r, "/configs?flash=artifacts_set", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// handleSetCustomerFloor sets (or clears) a customer's per-customer controller-version floor
|
||||
// override. Empty clears the override (the customer then uses the global floor).
|
||||
func (s *Server) handleSetCustomerFloor(w http.ResponseWriter, r *http.Request, customerID string) {
|
||||
|
||||
Reference in New Issue
Block a user