Implemented confi snapshots
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net"
|
||||
@@ -105,6 +106,11 @@ func (h *Handler) Register(mux *http.ServeMux, prefix string) {
|
||||
mux.HandleFunc("GET "+prefix+"/synthetic/{name}", h.getSynthetic)
|
||||
mux.HandleFunc("PUT "+prefix+"/synthetic/{name}", h.updateSynthetic)
|
||||
mux.HandleFunc("DELETE "+prefix+"/synthetic/{name}", h.deleteSynthetic)
|
||||
// Synthetic signal git-style versioning (list / view / promote / fork).
|
||||
mux.HandleFunc("GET "+prefix+"/synthetic/{name}/versions", h.listSyntheticVersions)
|
||||
mux.HandleFunc("GET "+prefix+"/synthetic/{name}/versions/{version}", h.getSyntheticVersion)
|
||||
mux.HandleFunc("POST "+prefix+"/synthetic/{name}/versions/{version}/promote", h.promoteSyntheticVersion)
|
||||
mux.HandleFunc("POST "+prefix+"/synthetic/{name}/versions/{version}/fork", h.forkSyntheticVersion)
|
||||
// Server-side control logic CRUD (mutations are write-gated by the access
|
||||
// middleware; each mutation reloads the running engine).
|
||||
mux.HandleFunc("GET "+prefix+"/controllogic", h.listControlLogic)
|
||||
@@ -112,6 +118,11 @@ func (h *Handler) Register(mux *http.ServeMux, prefix string) {
|
||||
mux.HandleFunc("GET "+prefix+"/controllogic/{id}", h.getControlLogic)
|
||||
mux.HandleFunc("PUT "+prefix+"/controllogic/{id}", h.updateControlLogic)
|
||||
mux.HandleFunc("DELETE "+prefix+"/controllogic/{id}", h.deleteControlLogic)
|
||||
// Control-logic git-style versioning (list / view / promote / fork).
|
||||
mux.HandleFunc("GET "+prefix+"/controllogic/{id}/versions", h.listControlLogicVersions)
|
||||
mux.HandleFunc("GET "+prefix+"/controllogic/{id}/versions/{version}", h.getControlLogicVersion)
|
||||
mux.HandleFunc("POST "+prefix+"/controllogic/{id}/versions/{version}/promote", h.promoteControlLogicVersion)
|
||||
mux.HandleFunc("POST "+prefix+"/controllogic/{id}/versions/{version}/fork", h.forkControlLogicVersion)
|
||||
// Configuration manager — config sets (schemas) and instances (values),
|
||||
// both versioned git-style. Mutations are write-gated by the access
|
||||
// middleware; "apply" writes each value to its target signal.
|
||||
@@ -124,6 +135,7 @@ func (h *Handler) Register(mux *http.ServeMux, prefix string) {
|
||||
mux.HandleFunc("GET "+prefix+"/config/sets/{id}/versions/{version}", h.getConfigSetVersion)
|
||||
mux.HandleFunc("POST "+prefix+"/config/sets/{id}/versions/{version}/promote", h.promoteConfigSet)
|
||||
mux.HandleFunc("POST "+prefix+"/config/sets/{id}/versions/{version}/fork", h.forkConfigSet)
|
||||
mux.HandleFunc("POST "+prefix+"/config/sets/{id}/snapshot", h.snapshotConfigSet)
|
||||
mux.HandleFunc("GET "+prefix+"/config/sets/diff", h.diffConfigSets)
|
||||
mux.HandleFunc("GET "+prefix+"/config/instances", h.listConfigInstances)
|
||||
mux.HandleFunc("POST "+prefix+"/config/instances", h.createConfigInstance)
|
||||
@@ -135,7 +147,22 @@ func (h *Handler) Register(mux *http.ServeMux, prefix string) {
|
||||
mux.HandleFunc("POST "+prefix+"/config/instances/{id}/versions/{version}/promote", h.promoteConfigInstance)
|
||||
mux.HandleFunc("POST "+prefix+"/config/instances/{id}/versions/{version}/fork", h.forkConfigInstance)
|
||||
mux.HandleFunc("POST "+prefix+"/config/instances/{id}/apply", h.applyConfigInstance)
|
||||
mux.HandleFunc("POST "+prefix+"/config/instances/{id}/validate", h.validateConfigInstance)
|
||||
mux.HandleFunc("GET "+prefix+"/config/instances/{id}/livediff", h.diffConfigInstanceLive)
|
||||
mux.HandleFunc("GET "+prefix+"/config/instances/diff", h.diffConfigInstances)
|
||||
// Config rules — CUE validation/transformation logic bound to a set, run
|
||||
// when instances of that set are saved. Versioned git-style; "check"
|
||||
// evaluates an unsaved source for the live editor.
|
||||
mux.HandleFunc("GET "+prefix+"/config/rules", h.listConfigRules)
|
||||
mux.HandleFunc("POST "+prefix+"/config/rules", h.createConfigRule)
|
||||
mux.HandleFunc("POST "+prefix+"/config/rules/check", h.checkConfigRule)
|
||||
mux.HandleFunc("GET "+prefix+"/config/rules/{id}", h.getConfigRule)
|
||||
mux.HandleFunc("PUT "+prefix+"/config/rules/{id}", h.updateConfigRule)
|
||||
mux.HandleFunc("DELETE "+prefix+"/config/rules/{id}", h.deleteConfigRule)
|
||||
mux.HandleFunc("GET "+prefix+"/config/rules/{id}/versions", h.listConfigRuleVersions)
|
||||
mux.HandleFunc("GET "+prefix+"/config/rules/{id}/versions/{version}", h.getConfigRuleVersion)
|
||||
mux.HandleFunc("POST "+prefix+"/config/rules/{id}/versions/{version}/promote", h.promoteConfigRule)
|
||||
mux.HandleFunc("POST "+prefix+"/config/rules/{id}/versions/{version}/fork", h.forkConfigRule)
|
||||
}
|
||||
|
||||
// ── /me ─────────────────────────────────────────────────────────────────────
|
||||
@@ -1194,6 +1221,80 @@ func (h *Handler) deleteSynthetic(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (h *Handler) listSyntheticVersions(w http.ResponseWriter, r *http.Request) {
|
||||
if h.synthetic == nil {
|
||||
jsonError(w, http.StatusServiceUnavailable, "synthetic data source not enabled")
|
||||
return
|
||||
}
|
||||
name := r.PathValue("name")
|
||||
versions, err := h.synthetic.Versions(name)
|
||||
if err != nil {
|
||||
jsonError(w, http.StatusNotFound, "synthetic signal not found: "+name)
|
||||
return
|
||||
}
|
||||
jsonOK(w, versions)
|
||||
}
|
||||
|
||||
func (h *Handler) getSyntheticVersion(w http.ResponseWriter, r *http.Request) {
|
||||
if h.synthetic == nil {
|
||||
jsonError(w, http.StatusServiceUnavailable, "synthetic data source not enabled")
|
||||
return
|
||||
}
|
||||
name := r.PathValue("name")
|
||||
version, err := strconv.Atoi(r.PathValue("version"))
|
||||
if err != nil {
|
||||
jsonError(w, http.StatusBadRequest, "invalid version: "+r.PathValue("version"))
|
||||
return
|
||||
}
|
||||
def, err := h.synthetic.GetVersion(name, version)
|
||||
if err != nil {
|
||||
jsonError(w, http.StatusNotFound, "version not found")
|
||||
return
|
||||
}
|
||||
jsonOK(w, def)
|
||||
}
|
||||
|
||||
func (h *Handler) promoteSyntheticVersion(w http.ResponseWriter, r *http.Request) {
|
||||
if h.synthetic == nil {
|
||||
jsonError(w, http.StatusServiceUnavailable, "synthetic data source not enabled")
|
||||
return
|
||||
}
|
||||
name := r.PathValue("name")
|
||||
version, err := strconv.Atoi(r.PathValue("version"))
|
||||
if err != nil {
|
||||
jsonError(w, http.StatusBadRequest, "invalid version: "+r.PathValue("version"))
|
||||
return
|
||||
}
|
||||
def, err := h.synthetic.PromoteVersion(name, version)
|
||||
if err != nil {
|
||||
jsonError(w, http.StatusNotFound, "version not found")
|
||||
return
|
||||
}
|
||||
h.recordMutation(r, "synthetic.promote", fmt.Sprintf("%s v%d", name, version))
|
||||
jsonOK(w, def)
|
||||
}
|
||||
|
||||
func (h *Handler) forkSyntheticVersion(w http.ResponseWriter, r *http.Request) {
|
||||
if h.synthetic == nil {
|
||||
jsonError(w, http.StatusServiceUnavailable, "synthetic data source not enabled")
|
||||
return
|
||||
}
|
||||
name := r.PathValue("name")
|
||||
version, err := strconv.Atoi(r.PathValue("version"))
|
||||
if err != nil {
|
||||
jsonError(w, http.StatusBadRequest, "invalid version: "+r.PathValue("version"))
|
||||
return
|
||||
}
|
||||
def, err := h.synthetic.ForkVersion(name, version)
|
||||
if err != nil {
|
||||
jsonError(w, http.StatusNotFound, "version not found")
|
||||
return
|
||||
}
|
||||
h.recordMutation(r, "synthetic.fork", fmt.Sprintf("%s v%d -> %s", name, version, def.Name))
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
jsonOK(w, map[string]string{"id": def.Name})
|
||||
}
|
||||
|
||||
// ── Control logic ──────────────────────────────────────────────────────────────
|
||||
|
||||
func (h *Handler) listControlLogic(w http.ResponseWriter, _ *http.Request) {
|
||||
@@ -1294,6 +1395,88 @@ func (h *Handler) deleteControlLogic(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (h *Handler) listControlLogicVersions(w http.ResponseWriter, r *http.Request) {
|
||||
if h.ctrlLogic == nil {
|
||||
jsonError(w, http.StatusServiceUnavailable, "control logic not enabled")
|
||||
return
|
||||
}
|
||||
versions, err := h.ctrlLogic.Versions(r.PathValue("id"))
|
||||
if err != nil {
|
||||
jsonError(w, http.StatusNotFound, "control logic graph not found: "+r.PathValue("id"))
|
||||
return
|
||||
}
|
||||
jsonOK(w, versions)
|
||||
}
|
||||
|
||||
func (h *Handler) getControlLogicVersion(w http.ResponseWriter, r *http.Request) {
|
||||
if h.ctrlLogic == nil {
|
||||
jsonError(w, http.StatusServiceUnavailable, "control logic not enabled")
|
||||
return
|
||||
}
|
||||
version, err := strconv.Atoi(r.PathValue("version"))
|
||||
if err != nil {
|
||||
jsonError(w, http.StatusBadRequest, "invalid version: "+r.PathValue("version"))
|
||||
return
|
||||
}
|
||||
g, err := h.ctrlLogic.GetVersion(r.PathValue("id"), version)
|
||||
if err != nil {
|
||||
jsonError(w, http.StatusNotFound, "version not found")
|
||||
return
|
||||
}
|
||||
jsonOK(w, g)
|
||||
}
|
||||
|
||||
func (h *Handler) promoteControlLogicVersion(w http.ResponseWriter, r *http.Request) {
|
||||
if h.ctrlLogic == nil {
|
||||
jsonError(w, http.StatusServiceUnavailable, "control logic not enabled")
|
||||
return
|
||||
}
|
||||
if !h.policy.CanEditLogic(caller(r)) {
|
||||
jsonError(w, http.StatusForbidden, "you are not permitted to edit logic")
|
||||
return
|
||||
}
|
||||
id := r.PathValue("id")
|
||||
version, err := strconv.Atoi(r.PathValue("version"))
|
||||
if err != nil {
|
||||
jsonError(w, http.StatusBadRequest, "invalid version: "+r.PathValue("version"))
|
||||
return
|
||||
}
|
||||
g, err := h.ctrlLogic.Promote(id, version)
|
||||
if err != nil {
|
||||
jsonError(w, http.StatusNotFound, "version not found")
|
||||
return
|
||||
}
|
||||
h.ctrlEngine.Reload()
|
||||
h.recordMutation(r, "controllogic.promote", fmt.Sprintf("%s v%d", id, version))
|
||||
jsonOK(w, g)
|
||||
}
|
||||
|
||||
func (h *Handler) forkControlLogicVersion(w http.ResponseWriter, r *http.Request) {
|
||||
if h.ctrlLogic == nil {
|
||||
jsonError(w, http.StatusServiceUnavailable, "control logic not enabled")
|
||||
return
|
||||
}
|
||||
if !h.policy.CanEditLogic(caller(r)) {
|
||||
jsonError(w, http.StatusForbidden, "you are not permitted to edit logic")
|
||||
return
|
||||
}
|
||||
id := r.PathValue("id")
|
||||
version, err := strconv.Atoi(r.PathValue("version"))
|
||||
if err != nil {
|
||||
jsonError(w, http.StatusBadRequest, "invalid version: "+r.PathValue("version"))
|
||||
return
|
||||
}
|
||||
g, err := h.ctrlLogic.Fork(id, version)
|
||||
if err != nil {
|
||||
jsonError(w, http.StatusNotFound, "version not found")
|
||||
return
|
||||
}
|
||||
h.ctrlEngine.Reload()
|
||||
h.recordMutation(r, "controllogic.fork", fmt.Sprintf("%s v%d -> %s", id, version, g.ID))
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
jsonOK(w, map[string]string{"id": g.ID})
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
// extractLogicBlock returns the verbatim <logic>…</logic> section of an
|
||||
|
||||
@@ -52,13 +52,14 @@ func setup(t *testing.T) (*httptest.Server, func()) {
|
||||
if err != nil {
|
||||
t.Fatal("controllogic.NewStore:", err)
|
||||
}
|
||||
clEngine := controllogic.NewEngine(ctx, brk, clStore, audit.Nop(), log)
|
||||
|
||||
cfgStore, err := confmgr.New(dir)
|
||||
if err != nil {
|
||||
t.Fatal("confmgr.New:", err)
|
||||
}
|
||||
|
||||
clEngine := controllogic.NewEngine(ctx, brk, clStore, cfgStore, audit.Nop(), log)
|
||||
|
||||
mux := http.NewServeMux()
|
||||
api.New(brk, nil, store, cfgStore, access.New("", nil, nil, nil, nil), acl, clStore, clEngine, audit.Nop(), "", "", log).Register(mux, "/api/v1")
|
||||
|
||||
|
||||
@@ -6,8 +6,10 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/uopi/uopi/internal/broker"
|
||||
"github.com/uopi/uopi/internal/confmgr"
|
||||
)
|
||||
|
||||
@@ -444,3 +446,322 @@ func (h *Handler) resolveInstance(id, version string) (confmgr.ConfigInstance, e
|
||||
}
|
||||
return h.cfg.GetInstanceVersion(id, v)
|
||||
}
|
||||
|
||||
// validateConfigInstance evaluates the CUE rules bound to an instance's set
|
||||
// against its stored values, without persisting anything. The structured
|
||||
// RuleResult (violations + any transformed values) lets the UI surface
|
||||
// per-parameter failures.
|
||||
func (h *Handler) validateConfigInstance(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
|
||||
}
|
||||
res, err := h.cfg.ValidateInstanceRules(inst)
|
||||
if err != nil {
|
||||
jsonError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
jsonOK(w, res)
|
||||
}
|
||||
|
||||
// ── config snapshot / live diff ─────────────────────────────────────────────
|
||||
|
||||
// readResult is the per-signal outcome of a parallel live read.
|
||||
type readResult struct {
|
||||
val any
|
||||
err error
|
||||
}
|
||||
|
||||
// readSetSignals reads the current value of every distinct target signal of a
|
||||
// set in parallel (deduplicated by ds+signal), bounded by ctx. The returned map
|
||||
// is keyed by {ds, signal}.
|
||||
func (h *Handler) readSetSignals(ctx context.Context, set confmgr.ConfigSet) map[[2]string]readResult {
|
||||
jobs := make(map[[2]string]struct{}, len(set.Parameters))
|
||||
for _, p := range set.Parameters {
|
||||
jobs[[2]string{p.DS, p.Signal}] = struct{}{}
|
||||
}
|
||||
results := make(map[[2]string]readResult, len(jobs))
|
||||
var mu sync.Mutex
|
||||
var wg sync.WaitGroup
|
||||
for k := range jobs {
|
||||
wg.Add(1)
|
||||
go func(ds, signal string) {
|
||||
defer wg.Done()
|
||||
v, err := h.broker.ReadNow(ctx, broker.SignalRef{DS: ds, Name: signal})
|
||||
mu.Lock()
|
||||
results[[2]string{ds, signal}] = readResult{val: v.Data, err: err}
|
||||
mu.Unlock()
|
||||
}(k[0], k[1])
|
||||
}
|
||||
wg.Wait()
|
||||
return results
|
||||
}
|
||||
|
||||
// snapshotReader builds a confmgr.ReadFunc backed by a pre-read results map.
|
||||
func snapshotReader(results map[[2]string]readResult) confmgr.ReadFunc {
|
||||
return func(ds, signal string) (any, error) {
|
||||
r, ok := results[[2]string{ds, signal}]
|
||||
if !ok {
|
||||
return nil, errors.New("signal not read")
|
||||
}
|
||||
return r.val, r.err
|
||||
}
|
||||
}
|
||||
|
||||
// snapshotConfigSet captures the current value of every target signal of a set
|
||||
// and stores them as a new config instance. Body: optional {name}. Returns the
|
||||
// created instance plus the per-parameter capture result.
|
||||
func (h *Handler) snapshotConfigSet(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
|
||||
}
|
||||
var body struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
if r.Body != nil {
|
||||
_ = json.NewDecoder(r.Body).Decode(&body) // body is optional
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
||||
defer cancel()
|
||||
results := h.readSetSignals(ctx, set)
|
||||
snap := confmgr.Snapshot(set, snapshotReader(results))
|
||||
|
||||
name := body.Name
|
||||
if name == "" {
|
||||
name = set.Name + " snapshot " + time.Now().Format("2006-01-02 15:04:05")
|
||||
}
|
||||
inst := confmgr.ConfigInstance{
|
||||
Name: name,
|
||||
SetID: set.ID,
|
||||
Owner: caller(r),
|
||||
Values: snap.Values,
|
||||
}
|
||||
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.snapshot", out.ID+" "+out.Name+" captured="+strconv.Itoa(snap.Captured)+" failed="+strconv.Itoa(snap.Failed))
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
jsonOK(w, struct {
|
||||
Instance confmgr.ConfigInstance `json:"instance"`
|
||||
Snapshot confmgr.SnapshotResult `json:"snapshot"`
|
||||
}{out, snap})
|
||||
}
|
||||
|
||||
// diffConfigInstanceLive compares a stored instance against the current live
|
||||
// values of its set's target signals, so an operator can see how the saved
|
||||
// config differs from what the hardware currently holds. The stored side uses
|
||||
// resolved values (instance value or parameter default) so default-backed
|
||||
// parameters are not reported as spurious additions.
|
||||
func (h *Handler) diffConfigInstanceLive(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
|
||||
}
|
||||
set, err := h.cfg.SetForInstance(inst)
|
||||
if err != nil {
|
||||
jsonError(w, configStatus(err), "load set: "+err.Error())
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
||||
defer cancel()
|
||||
results := h.readSetSignals(ctx, set)
|
||||
snap := confmgr.Snapshot(set, snapshotReader(results))
|
||||
|
||||
left := confmgr.ConfigInstance{ID: inst.ID, Name: inst.Name, Values: map[string]any{}}
|
||||
for _, p := range set.Parameters {
|
||||
if v, ok := inst.Resolve(p); ok {
|
||||
left.Values[p.Key] = v
|
||||
}
|
||||
}
|
||||
current := confmgr.ConfigInstance{Name: "current", Values: snap.Values}
|
||||
jsonOK(w, confmgr.DiffInstances(left, current))
|
||||
}
|
||||
|
||||
// ── config rules (CUE validation/transformation) ────────────────────────────
|
||||
|
||||
func (h *Handler) listConfigRules(w http.ResponseWriter, _ *http.Request) {
|
||||
if !h.configEnabled(w) {
|
||||
return
|
||||
}
|
||||
rules, err := h.cfg.List(confmgr.KindRule)
|
||||
if err != nil {
|
||||
jsonError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
jsonOK(w, rules)
|
||||
}
|
||||
|
||||
func (h *Handler) getConfigRule(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.configEnabled(w) {
|
||||
return
|
||||
}
|
||||
rule, err := h.cfg.GetRule(r.PathValue("id"))
|
||||
if err != nil {
|
||||
jsonError(w, configStatus(err), err.Error())
|
||||
return
|
||||
}
|
||||
jsonOK(w, rule)
|
||||
}
|
||||
|
||||
func (h *Handler) createConfigRule(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.configEnabled(w) {
|
||||
return
|
||||
}
|
||||
var rule confmgr.ConfigRule
|
||||
if err := json.NewDecoder(r.Body).Decode(&rule); err != nil {
|
||||
jsonError(w, http.StatusBadRequest, "invalid JSON: "+err.Error())
|
||||
return
|
||||
}
|
||||
rule.ID = ""
|
||||
rule.Owner = caller(r)
|
||||
out, err := h.cfg.CreateRule(rule, r.URL.Query().Get("tag"))
|
||||
if err != nil {
|
||||
jsonError(w, configStatus(err), err.Error())
|
||||
return
|
||||
}
|
||||
h.recordMutation(r, "config.rule.create", out.ID+" "+out.Name)
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
jsonOK(w, out)
|
||||
}
|
||||
|
||||
func (h *Handler) updateConfigRule(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.configEnabled(w) {
|
||||
return
|
||||
}
|
||||
id := r.PathValue("id")
|
||||
var rule confmgr.ConfigRule
|
||||
if err := json.NewDecoder(r.Body).Decode(&rule); err != nil {
|
||||
jsonError(w, http.StatusBadRequest, "invalid JSON: "+err.Error())
|
||||
return
|
||||
}
|
||||
out, err := h.cfg.UpdateRule(id, rule, r.URL.Query().Get("tag"))
|
||||
if err != nil {
|
||||
jsonError(w, configStatus(err), err.Error())
|
||||
return
|
||||
}
|
||||
h.recordMutation(r, "config.rule.update", out.ID+" v"+strconv.Itoa(out.Version))
|
||||
jsonOK(w, out)
|
||||
}
|
||||
|
||||
func (h *Handler) deleteConfigRule(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.configEnabled(w) {
|
||||
return
|
||||
}
|
||||
id := r.PathValue("id")
|
||||
if err := h.cfg.Delete(confmgr.KindRule, id); err != nil {
|
||||
jsonError(w, configStatus(err), err.Error())
|
||||
return
|
||||
}
|
||||
h.recordMutation(r, "config.rule.delete", id)
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (h *Handler) listConfigRuleVersions(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.configEnabled(w) {
|
||||
return
|
||||
}
|
||||
versions, err := h.cfg.Versions(confmgr.KindRule, r.PathValue("id"))
|
||||
if err != nil {
|
||||
jsonError(w, configStatus(err), err.Error())
|
||||
return
|
||||
}
|
||||
jsonOK(w, versions)
|
||||
}
|
||||
|
||||
func (h *Handler) getConfigRuleVersion(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
|
||||
}
|
||||
rule, err := h.cfg.GetRuleVersion(r.PathValue("id"), version)
|
||||
if err != nil {
|
||||
jsonError(w, configStatus(err), err.Error())
|
||||
return
|
||||
}
|
||||
jsonOK(w, rule)
|
||||
}
|
||||
|
||||
func (h *Handler) promoteConfigRule(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.KindRule, id, version); err != nil {
|
||||
jsonError(w, configStatus(err), err.Error())
|
||||
return
|
||||
}
|
||||
h.recordMutation(r, "config.rule.promote", id+" v"+strconv.Itoa(version))
|
||||
rule, err := h.cfg.GetRule(id)
|
||||
if err != nil {
|
||||
jsonError(w, configStatus(err), err.Error())
|
||||
return
|
||||
}
|
||||
jsonOK(w, rule)
|
||||
}
|
||||
|
||||
func (h *Handler) forkConfigRule(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.KindRule, id, version)
|
||||
if err != nil {
|
||||
jsonError(w, configStatus(err), err.Error())
|
||||
return
|
||||
}
|
||||
h.recordMutation(r, "config.rule.fork", id+" v"+strconv.Itoa(version)+" -> "+newID)
|
||||
rule, err := h.cfg.GetRule(newID)
|
||||
if err != nil {
|
||||
jsonError(w, configStatus(err), err.Error())
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
jsonOK(w, rule)
|
||||
}
|
||||
|
||||
// checkConfigRule compiles a (possibly unsaved) CUE source and, when sample
|
||||
// values are supplied, evaluates it against them. It powers the editor's live
|
||||
// validation panel; nothing is persisted.
|
||||
func (h *Handler) checkConfigRule(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.configEnabled(w) {
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
Source string `json:"source"`
|
||||
Values map[string]any `json:"values"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
jsonError(w, http.StatusBadRequest, "invalid JSON: "+err.Error())
|
||||
return
|
||||
}
|
||||
jsonOK(w, confmgr.EvaluateRule(body.Source, body.Values))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user