package api import ( "context" "encoding/json" "errors" "net/http" "strconv" "time" "github.com/uopi/uopi/internal/confmgr" ) // configEnabled guards every config-manager handler; the store is always // constructed today, but the nil check keeps the handlers safe if it is ever // made optional. func (h *Handler) configEnabled(w http.ResponseWriter) bool { if h.cfg == nil { jsonError(w, http.StatusServiceUnavailable, "configuration manager not enabled") return false } return true } func pathVersion(r *http.Request) (int, error) { return strconv.Atoi(r.PathValue("version")) } func configStatus(err error) int { if errors.Is(err, confmgr.ErrNotFound) { return http.StatusNotFound } return http.StatusBadRequest } // ── config sets ───────────────────────────────────────────────────────────── func (h *Handler) listConfigSets(w http.ResponseWriter, _ *http.Request) { if !h.configEnabled(w) { return } sets, err := h.cfg.List(confmgr.KindSet) if err != nil { jsonError(w, http.StatusInternalServerError, err.Error()) return } jsonOK(w, sets) } func (h *Handler) getConfigSet(w http.ResponseWriter, r *http.Request) { if !h.configEnabled(w) { return } set, err := h.cfg.GetSet(r.PathValue("id")) if err != nil { jsonError(w, configStatus(err), err.Error()) return } jsonOK(w, set) } func (h *Handler) createConfigSet(w http.ResponseWriter, r *http.Request) { if !h.configEnabled(w) { return } var set confmgr.ConfigSet if err := json.NewDecoder(r.Body).Decode(&set); err != nil { jsonError(w, http.StatusBadRequest, "invalid JSON: "+err.Error()) return } set.ID = "" set.Owner = caller(r) out, err := h.cfg.CreateSet(set, r.URL.Query().Get("tag")) if err != nil { jsonError(w, configStatus(err), err.Error()) return } h.recordMutation(r, "config.set.create", out.ID+" "+out.Name) w.WriteHeader(http.StatusCreated) jsonOK(w, out) } func (h *Handler) updateConfigSet(w http.ResponseWriter, r *http.Request) { if !h.configEnabled(w) { return } id := r.PathValue("id") var set confmgr.ConfigSet if err := json.NewDecoder(r.Body).Decode(&set); err != nil { jsonError(w, http.StatusBadRequest, "invalid JSON: "+err.Error()) return } out, err := h.cfg.UpdateSet(id, set, r.URL.Query().Get("tag")) if err != nil { jsonError(w, configStatus(err), err.Error()) return } h.recordMutation(r, "config.set.update", out.ID+" v"+strconv.Itoa(out.Version)) jsonOK(w, out) } func (h *Handler) deleteConfigSet(w http.ResponseWriter, r *http.Request) { if !h.configEnabled(w) { return } id := r.PathValue("id") if err := h.cfg.Delete(confmgr.KindSet, id); err != nil { jsonError(w, configStatus(err), err.Error()) return } h.recordMutation(r, "config.set.delete", id) w.WriteHeader(http.StatusNoContent) } func (h *Handler) listConfigSetVersions(w http.ResponseWriter, r *http.Request) { if !h.configEnabled(w) { return } versions, err := h.cfg.Versions(confmgr.KindSet, r.PathValue("id")) if err != nil { jsonError(w, configStatus(err), err.Error()) return } jsonOK(w, versions) } func (h *Handler) getConfigSetVersion(w http.ResponseWriter, r *http.Request) { if !h.configEnabled(w) { return } version, err := pathVersion(r) if err != nil { jsonError(w, http.StatusBadRequest, "invalid version") return } set, err := h.cfg.GetSetVersion(r.PathValue("id"), version) if err != nil { jsonError(w, configStatus(err), err.Error()) return } jsonOK(w, set) } func (h *Handler) promoteConfigSet(w http.ResponseWriter, r *http.Request) { if !h.configEnabled(w) { return } version, err := pathVersion(r) if err != nil { jsonError(w, http.StatusBadRequest, "invalid version") return } id := r.PathValue("id") if err := h.cfg.Promote(confmgr.KindSet, id, version); err != nil { jsonError(w, configStatus(err), err.Error()) return } h.recordMutation(r, "config.set.promote", id+" v"+strconv.Itoa(version)) set, err := h.cfg.GetSet(id) if err != nil { jsonError(w, configStatus(err), err.Error()) return } jsonOK(w, set) } func (h *Handler) forkConfigSet(w http.ResponseWriter, r *http.Request) { if !h.configEnabled(w) { return } version, err := pathVersion(r) if err != nil { jsonError(w, http.StatusBadRequest, "invalid version") return } id := r.PathValue("id") newID, err := h.cfg.Fork(confmgr.KindSet, id, version) if err != nil { jsonError(w, configStatus(err), err.Error()) return } h.recordMutation(r, "config.set.fork", id+" v"+strconv.Itoa(version)+" -> "+newID) set, err := h.cfg.GetSet(newID) if err != nil { jsonError(w, configStatus(err), err.Error()) return } w.WriteHeader(http.StatusCreated) jsonOK(w, set) } // diffConfigSets compares two set revisions. Query params: a, b (ids); // optional av, bv (versions, default current). func (h *Handler) diffConfigSets(w http.ResponseWriter, r *http.Request) { if !h.configEnabled(w) { return } q := r.URL.Query() left, err := h.resolveSet(q.Get("a"), q.Get("av")) if err != nil { jsonError(w, configStatus(err), "left set: "+err.Error()) return } right, err := h.resolveSet(q.Get("b"), q.Get("bv")) if err != nil { jsonError(w, configStatus(err), "right set: "+err.Error()) return } jsonOK(w, confmgr.DiffSets(left, right)) } func (h *Handler) resolveSet(id, version string) (confmgr.ConfigSet, error) { if id == "" { return confmgr.ConfigSet{}, errors.New("missing set id") } if version == "" { return h.cfg.GetSet(id) } v, err := strconv.Atoi(version) if err != nil { return confmgr.ConfigSet{}, errors.New("invalid version") } return h.cfg.GetSetVersion(id, v) } // ── config instances ──────────────────────────────────────────────────────── func (h *Handler) listConfigInstances(w http.ResponseWriter, _ *http.Request) { if !h.configEnabled(w) { return } insts, err := h.cfg.List(confmgr.KindInstance) if err != nil { jsonError(w, http.StatusInternalServerError, err.Error()) return } jsonOK(w, insts) } func (h *Handler) getConfigInstance(w http.ResponseWriter, r *http.Request) { if !h.configEnabled(w) { return } inst, err := h.cfg.GetInstance(r.PathValue("id")) if err != nil { jsonError(w, configStatus(err), err.Error()) return } jsonOK(w, inst) } func (h *Handler) createConfigInstance(w http.ResponseWriter, r *http.Request) { if !h.configEnabled(w) { return } var inst confmgr.ConfigInstance if err := json.NewDecoder(r.Body).Decode(&inst); err != nil { jsonError(w, http.StatusBadRequest, "invalid JSON: "+err.Error()) return } inst.ID = "" inst.Owner = caller(r) out, err := h.cfg.CreateInstance(inst, r.URL.Query().Get("tag")) if err != nil { jsonError(w, configStatus(err), err.Error()) return } h.recordMutation(r, "config.instance.create", out.ID+" "+out.Name) w.WriteHeader(http.StatusCreated) jsonOK(w, out) } func (h *Handler) updateConfigInstance(w http.ResponseWriter, r *http.Request) { if !h.configEnabled(w) { return } id := r.PathValue("id") var inst confmgr.ConfigInstance if err := json.NewDecoder(r.Body).Decode(&inst); err != nil { jsonError(w, http.StatusBadRequest, "invalid JSON: "+err.Error()) return } out, err := h.cfg.UpdateInstance(id, inst, r.URL.Query().Get("tag")) if err != nil { jsonError(w, configStatus(err), err.Error()) return } h.recordMutation(r, "config.instance.update", out.ID+" v"+strconv.Itoa(out.Version)) jsonOK(w, out) } func (h *Handler) deleteConfigInstance(w http.ResponseWriter, r *http.Request) { if !h.configEnabled(w) { return } id := r.PathValue("id") if err := h.cfg.Delete(confmgr.KindInstance, id); err != nil { jsonError(w, configStatus(err), err.Error()) return } h.recordMutation(r, "config.instance.delete", id) w.WriteHeader(http.StatusNoContent) } func (h *Handler) listConfigInstanceVersions(w http.ResponseWriter, r *http.Request) { if !h.configEnabled(w) { return } versions, err := h.cfg.Versions(confmgr.KindInstance, r.PathValue("id")) if err != nil { jsonError(w, configStatus(err), err.Error()) return } jsonOK(w, versions) } func (h *Handler) getConfigInstanceVersion(w http.ResponseWriter, r *http.Request) { if !h.configEnabled(w) { return } version, err := pathVersion(r) if err != nil { jsonError(w, http.StatusBadRequest, "invalid version") return } inst, err := h.cfg.GetInstanceVersion(r.PathValue("id"), version) if err != nil { jsonError(w, configStatus(err), err.Error()) return } jsonOK(w, inst) } func (h *Handler) promoteConfigInstance(w http.ResponseWriter, r *http.Request) { if !h.configEnabled(w) { return } version, err := pathVersion(r) if err != nil { jsonError(w, http.StatusBadRequest, "invalid version") return } id := r.PathValue("id") if err := h.cfg.Promote(confmgr.KindInstance, id, version); err != nil { jsonError(w, configStatus(err), err.Error()) return } h.recordMutation(r, "config.instance.promote", id+" v"+strconv.Itoa(version)) inst, err := h.cfg.GetInstance(id) if err != nil { jsonError(w, configStatus(err), err.Error()) return } jsonOK(w, inst) } func (h *Handler) forkConfigInstance(w http.ResponseWriter, r *http.Request) { if !h.configEnabled(w) { return } version, err := pathVersion(r) if err != nil { jsonError(w, http.StatusBadRequest, "invalid version") return } id := r.PathValue("id") newID, err := h.cfg.Fork(confmgr.KindInstance, id, version) if err != nil { jsonError(w, configStatus(err), err.Error()) return } h.recordMutation(r, "config.instance.fork", id+" v"+strconv.Itoa(version)+" -> "+newID) inst, err := h.cfg.GetInstance(newID) if err != nil { jsonError(w, configStatus(err), err.Error()) return } w.WriteHeader(http.StatusCreated) jsonOK(w, inst) } // applyConfigInstance writes every resolvable parameter value of an instance to // its target signal via the broker. Per-parameter outcomes are returned so a // partial apply is reported faithfully. func (h *Handler) applyConfigInstance(w http.ResponseWriter, r *http.Request) { if !h.configEnabled(w) { return } id := r.PathValue("id") inst, err := h.cfg.GetInstance(id) if err != nil { jsonError(w, configStatus(err), err.Error()) return } set, err := h.cfg.SetForInstance(inst) if err != nil { jsonError(w, configStatus(err), "load set: "+err.Error()) return } ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second) defer cancel() write := func(ds, signal string, value any) error { src, ok := h.broker.Source(ds) if !ok { return errors.New("unknown data source: " + ds) } return src.Write(ctx, signal, value) } res := confmgr.Apply(set, inst, write) h.recordMutation(r, "config.instance.apply", id+" applied="+strconv.Itoa(res.Applied)+" failed="+strconv.Itoa(res.Failed)) jsonOK(w, res) } // diffConfigInstances compares two instance revisions. Query params: a, b // (ids); optional av, bv (versions, default current). func (h *Handler) diffConfigInstances(w http.ResponseWriter, r *http.Request) { if !h.configEnabled(w) { return } q := r.URL.Query() left, err := h.resolveInstance(q.Get("a"), q.Get("av")) if err != nil { jsonError(w, configStatus(err), "left instance: "+err.Error()) return } right, err := h.resolveInstance(q.Get("b"), q.Get("bv")) if err != nil { jsonError(w, configStatus(err), "right instance: "+err.Error()) return } jsonOK(w, confmgr.DiffInstances(left, right)) } func (h *Handler) resolveInstance(id, version string) (confmgr.ConfigInstance, error) { if id == "" { return confmgr.ConfigInstance{}, errors.New("missing instance id") } if version == "" { return h.cfg.GetInstance(id) } v, err := strconv.Atoi(version) if err != nil { return confmgr.ConfigInstance{}, errors.New("invalid version") } return h.cfg.GetInstanceVersion(id, v) }