hub v0.130.0: OS updates, guest fast lane — rings, per-box switch, OS releases approved from ring 0, os-report, os_update desired block
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-hub/internal/osupdates"
|
||||
)
|
||||
|
||||
// OSUpdateService is the hub half of the guest fast lane (`11` §8 step 2; hub v0.130.0).
|
||||
type OSUpdateService interface {
|
||||
Ingest(hostID string, r osupdates.Report) error
|
||||
DesiredBlock(hostID string) osupdates.Block
|
||||
}
|
||||
|
||||
// SetOSUpdateService wires the OS-update service. nil → the report endpoint answers 503 and no block is merged.
|
||||
func (h *Handler) SetOSUpdateService(s OSUpdateService) { h.osUpdates = s }
|
||||
|
||||
// handleOSReport: POST /api/v1/hosts/{id}/os-report — the agent's report after every OS-leg run. Per-host key,
|
||||
// SELF-SCOPED (a host reports only for itself).
|
||||
func (h *Handler) handleOSReport(w http.ResponseWriter, r *http.Request, pathHostID string) {
|
||||
authHostID, _, isGlobal, ok := h.checkAuthHost(r)
|
||||
if !ok {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
if pathHostID == "" || (!isGlobal && authHostID != pathHostID) {
|
||||
http.Error(w, "Forbidden: host_id mismatch", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if h.osUpdates == nil {
|
||||
http.Error(w, "os updates not configured", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
body, err := io.ReadAll(io.LimitReader(r.Body, 4<<20))
|
||||
if err != nil {
|
||||
http.Error(w, "read error", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
var rep osupdates.Report
|
||||
if err := json.Unmarshal(body, &rep); err != nil || rep.RunID == "" {
|
||||
http.Error(w, "body must be an os report with run_id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := h.osUpdates.Ingest(pathHostID, rep); err != nil {
|
||||
h.logger.Printf("[WARN] os-report from %s: %v", pathHostID, err)
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"ok": true})
|
||||
}
|
||||
|
||||
// mergeOSUpdate adds the hub-OWNED `os_update` block to a host's desired state at read time (like
|
||||
// mergeWireguard): the stored operator blob is never modified. No service → pass-through unchanged.
|
||||
func (h *Handler) mergeOSUpdate(hostID, desired string) string {
|
||||
if h.osUpdates == nil {
|
||||
return desired
|
||||
}
|
||||
var doc map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(desired), &doc); err != nil {
|
||||
h.logger.Printf("[ERROR] os_update merge %s: stored desired_json unparsable: %v (serving unmerged)", hostID, err)
|
||||
return desired
|
||||
}
|
||||
doc["os_update"] = h.osUpdates.DesiredBlock(hostID)
|
||||
out, err := json.Marshal(doc)
|
||||
if err != nil {
|
||||
return desired
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
Reference in New Issue
Block a user