c5f91174f6
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
72 lines
2.4 KiB
Go
72 lines
2.4 KiB
Go
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)
|
|
}
|