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) }