package adminapi import ( "context" "encoding/json" "errors" "net/http" "strconv" "time" "gitea.mynodes.xyz/ilker/llmgw/internal/store" ) // SettingsStore keeps the cluster's settings. type SettingsStore interface { ClusterSettings(ctx context.Context) (store.SettingsVersion, error) ClusterSettingsAt(ctx context.Context, version int64) (store.SettingsVersion, error) ClusterSettingsHistory(ctx context.Context, limit int) ([]store.SettingsVersion, error) PutClusterSettings(ctx context.Context, base int64, body, by, note string) (int64, error) } // Settings is what the settings endpoints need: where they are kept, and a // check that a document would load — laid over this control plane's own // server.yaml, validated as it would be at start. type Settings struct { Store SettingsStore Check func(body []byte) error // LocalKeys are those each machine's server.yaml keeps, which the // document cannot set. LocalKeys []string } // SetSettings enables the settings endpoints. func (a *API) SetSettings(s *Settings) { a.settings = s } // SettingsView is one version of the cluster's settings. type SettingsView struct { Version int64 `json:"version"` Body string `json:"body"` Note string `json:"note,omitempty"` At time.Time `json:"at"` By string `json:"by,omitempty"` LocalKeys []string `json:"local_keys,omitempty"` } // SettingsRequest is a new version of the cluster's settings, made from the // version Base, which must still be the newest. type SettingsRequest struct { Base int64 `json:"base"` Body string `json:"body"` Note string `json:"note,omitempty"` } // SettingsRollbackRequest puts an earlier version's settings back in force, // as a new version. type SettingsRollbackRequest struct { Version int64 `json:"version"` Note string `json:"note,omitempty"` } func settingsView(v store.SettingsVersion) SettingsView { return SettingsView{Version: v.Version, Body: v.Body, Note: v.Note, At: v.At, By: v.By} } func (a *API) registerSettings(mux Mux) { need := func(w http.ResponseWriter) bool { if a.settings == nil { writeError(w, http.StatusNotImplemented, "this control plane has no store to keep the cluster's settings in") return false } return true } mux.HandleFunc("GET /api/v1/settings", func(w http.ResponseWriter, r *http.Request) { if !need(w) { return } var v store.SettingsVersion var err error if q := r.URL.Query().Get("version"); q != "" { n, perr := strconv.ParseInt(q, 10, 64) if perr != nil { writeError(w, http.StatusBadRequest, "version is a number") return } v, err = a.settings.Store.ClusterSettingsAt(r.Context(), n) if err == nil && v.Version == 0 { writeError(w, http.StatusNotFound, "there is no such version") return } } else { v, err = a.settings.Store.ClusterSettings(r.Context()) } if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } out := settingsView(v) out.LocalKeys = a.settings.LocalKeys writeJSON(w, http.StatusOK, out) }) mux.HandleFunc("GET /api/v1/settings/history", func(w http.ResponseWriter, r *http.Request) { if !need(w) { return } limit := 20 if q := r.URL.Query().Get("limit"); q != "" { if n, err := strconv.Atoi(q); err == nil && n > 0 && n <= 500 { limit = n } } list, err := a.settings.Store.ClusterSettingsHistory(r.Context(), limit) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } out := make([]SettingsView, len(list)) for i, v := range list { out[i] = settingsView(v) } writeJSON(w, http.StatusOK, out) }) put := func(w http.ResponseWriter, r *http.Request, base int64, body, note string) { if err := a.settings.Check([]byte(body)); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } n, err := a.settings.Store.PutClusterSettings(r.Context(), base, body, actorOf(r), note) switch { case errors.Is(err, store.ErrSettingsChanged): writeError(w, http.StatusConflict, err.Error()) return case err != nil: writeError(w, http.StatusInternalServerError, err.Error()) return } v, err := a.settings.Store.ClusterSettingsAt(r.Context(), n) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, settingsView(v)) } mux.HandleFunc("PUT /api/v1/settings", func(w http.ResponseWriter, r *http.Request) { if !need(w) { return } var req SettingsRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } put(w, r, req.Base, req.Body, req.Note) }) mux.HandleFunc("POST /api/v1/settings/rollback", func(w http.ResponseWriter, r *http.Request) { if !need(w) { return } var req SettingsRollbackRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } old, err := a.settings.Store.ClusterSettingsAt(r.Context(), req.Version) if err == nil && old.Version == 0 { writeError(w, http.StatusNotFound, "there is no such version") return } cur, err2 := a.settings.Store.ClusterSettings(r.Context()) if err = errors.Join(err, err2); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } note := req.Note if note == "" { note = "back to version " + strconv.FormatInt(req.Version, 10) } put(w, r, cur.Version, old.Body, note) }) }