initial config manager
This commit is contained in:
@@ -0,0 +1,446 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user