package cli import ( "bytes" "encoding/json" "errors" "fmt" "io" "os" "os/exec" "path/filepath" "strconv" "strings" "time" "github.com/spf13/cobra" "gitea.mynodes.xyz/ilker/llmgw/internal/config" "gitea.mynodes.xyz/ilker/llmgw/internal/control/adminapi" "gitea.mynodes.xyz/ilker/llmgw/internal/settings" ) func newConfigCmd(client func() *ctlClient, asJSON *bool) *cobra.Command { var version int64 c := &cobra.Command{ Use: "config", Short: "The cluster's settings: show, change, and go back", Long: "Everything in server.yaml that is not one machine's own — guardrails,\n" + "limits, pricing, routing, engines, notifications, the mirror's policy —\n" + "is the cluster's, kept in the store and the same on every control plane.\n" + "A change is checked, kept as a new version, and in force on every control\n" + "plane within seconds: live where it can be (guard), otherwise each control\n" + "plane starts again on it, standbys first and the leader last.\n\n" + "With no subcommand, prints the settings in force.\n\n" + "A machine's own keys (" + strings.Join(config.LocalKeys(), ", ") + ")\n" + "stay in its /etc/llmgw/server.yaml; a change there is noticed and applied\n" + "on that machine the same way.", Example: " llmgw ctl config\n" + " llmgw ctl config get router.retries\n" + " llmgw ctl config set router.retries 3 --note 'flaky upstream'\n" + " llmgw ctl config set firewall_networks '[10.20.0.0/16]'\n" + " llmgw ctl config edit\n" + " llmgw ctl config history\n" + " llmgw ctl config rollback 4", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, _ []string) error { path := "/api/v1/settings" if version > 0 { path += "?version=" + strconv.FormatInt(version, 10) } var v adminapi.SettingsView if err := client().get(cmd.Context(), path, &v); err != nil { return err } if *asJSON { return emitJSON(v) } fmt.Fprintf(cmd.ErrOrStderr(), "# version %d, %s by %s%s\n", v.Version, v.At.Local().Format(time.DateTime), orUnknown(v.By), noteSuffix(v.Note)) fmt.Fprint(cmd.OutOrStdout(), v.Body) return nil }, } c.Flags().Int64Var(&version, "version", 0, "an earlier version") c.AddCommand(newConfigGetCmd(client), newConfigSetCmd(client), newConfigUnsetCmd(client), newConfigEditCmd(client), newConfigHistoryCmd(client, asJSON), newConfigRollbackCmd(client)) return c } func orUnknown(s string) string { if s == "" { return "unknown" } return s } func noteSuffix(n string) string { if n == "" { return "" } return ": " + n } // keyPath reads a dotted key, refusing one machine's own. func keyPath(key string) ([]string, error) { path := strings.Split(key, ".") if config.IsLocal(path) { return nil, fmt.Errorf("%s is each machine's own: change it in that machine's /etc/llmgw/server.yaml, "+ "where a change is applied by itself too", key) } return path, nil } func newConfigGetCmd(client func() *ctlClient) *cobra.Command { return &cobra.Command{ Use: "get KEY", Short: "One key of the cluster's settings, as YAML", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { path, err := keyPath(args[0]) if err != nil { return err } var v adminapi.SettingsView if err := client().get(cmd.Context(), "/api/v1/settings", &v); err != nil { return err } doc, err := settings.FromBytes("settings", []byte(v.Body)) if err != nil { return err } n := doc.Node(path) if n == nil { return fmt.Errorf("%s is not set in the cluster's settings; its default applies (docs/reference.md)", args[0]) } sub, err := settings.FromBytes("value", nil) if err != nil { return err } if err := sub.SetNode([]string{"v"}, n); err != nil { return err } b, err := sub.Bytes() if err != nil { return err } out := strings.TrimPrefix(string(b), "v:") fmt.Fprintln(cmd.OutOrStdout(), strings.TrimSpace(dedent(out))) return nil }, } } // dedent takes off the two spaces a block nested under one key carries. func dedent(s string) string { lines := strings.Split(s, "\n") for i, l := range lines { lines[i] = strings.TrimPrefix(l, " ") } return strings.Join(lines, "\n") } // changeSettings reads the newest settings, changes them, and puts the // result in force, saying what that does. func changeSettings(cmd *cobra.Command, client func() *ctlClient, note string, change func(doc *settings.Doc) error, ) error { ctx := cmd.Context() var cur adminapi.SettingsView if err := client().get(ctx, "/api/v1/settings", &cur); err != nil { return err } doc, err := settings.FromBytes("settings", []byte(cur.Body)) if err != nil { return err } if err := change(doc); err != nil { return err } b, err := doc.Bytes() if err != nil { return err } return putSettings(cmd, client, cur, b, note) } func putSettings(cmd *cobra.Command, client func() *ctlClient, cur adminapi.SettingsView, body []byte, note string) error { before, _ := config.Parse([]byte(cur.Body)) after, err := config.Parse(body) if err != nil { return err } changed := config.Changed(before, after) if len(changed) == 0 { fmt.Fprintln(cmd.OutOrStdout(), "nothing changed") return nil } req, _ := json.Marshal(adminapi.SettingsRequest{Base: cur.Version, Body: string(body), Note: note}) var v adminapi.SettingsView if err := client().put(cmd.Context(), "/api/v1/settings", req, &v); err != nil { return err } fmt.Fprintf(cmd.OutOrStdout(), "version %d: %s\n", v.Version, strings.Join(changed, ", ")) if onlyLive(changed) { fmt.Fprintln(cmd.OutOrStdout(), "in force on every control plane within seconds") } else { fmt.Fprintln(cmd.OutOrStdout(), "every control plane starts again on it within seconds, standbys first, "+ "the leader half a minute later; engines keep serving") } return nil } func newConfigSetCmd(client func() *ctlClient) *cobra.Command { var note string c := &cobra.Command{ Use: "set KEY VALUE", Short: "Change one key of the cluster's settings; VALUE is YAML", Example: " llmgw ctl config set router.retries 3\n" + " llmgw ctl config set guard.default '[baseline, finance]'\n" + " llmgw ctl config set notify.webhooks '[{url: https://hooks.example/llmgw}]'", Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { path, err := keyPath(args[0]) if err != nil { return err } return changeSettings(cmd, client, note, func(doc *settings.Doc) error { return doc.SetYAML(path, args[1]) }) }, } c.Flags().StringVar(¬e, "note", "", "why, kept with the version") return c } func newConfigUnsetCmd(client func() *ctlClient) *cobra.Command { var note string c := &cobra.Command{ Use: "unset KEY", Short: "Remove one key from the cluster's settings, so its default applies", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { path, err := keyPath(args[0]) if err != nil { return err } return changeSettings(cmd, client, note, func(doc *settings.Doc) error { if doc.Node(path) == nil { return fmt.Errorf("%s is not set", args[0]) } doc.Unset(path) return nil }) }, } c.Flags().StringVar(¬e, "note", "", "why, kept with the version") return c } func newConfigEditCmd(client func() *ctlClient) *cobra.Command { var note string c := &cobra.Command{ Use: "edit", Short: "Edit the cluster's settings in $EDITOR", Long: "Opens the settings in force in $EDITOR (vi when unset). Saved, they are\n" + "checked and kept as a new version; a document that does not load is\n" + "opened again with the reason, until it does or is left unchanged.", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, _ []string) error { var cur adminapi.SettingsView if err := client().get(cmd.Context(), "/api/v1/settings", &cur); err != nil { return err } dir, err := os.MkdirTemp("", "llmgw-settings") if err != nil { return err } defer os.RemoveAll(dir) file := filepath.Join(dir, "settings.yaml") body := []byte(cur.Body) problem := "" for { text := body if problem != "" { text = append([]byte("# NOT SAVED: "+strings.ReplaceAll(problem, "\n", " ")+"\n"), stripProblem(body)...) } if err := os.WriteFile(file, text, 0o600); err != nil { return err } if err := runEditor(file, cmd.InOrStdin(), cmd.OutOrStdout(), cmd.ErrOrStderr()); err != nil { return err } edited, err := os.ReadFile(file) //nolint:gosec // written above if err != nil { return err } edited = stripProblem(edited) if bytes.Equal(bytes.TrimSpace(edited), bytes.TrimSpace([]byte(cur.Body))) { fmt.Fprintln(cmd.OutOrStdout(), "nothing changed") return nil } err = putSettings(cmd, client, cur, edited, note) if err == nil { return nil } if strings.Contains(err.Error(), "changed since they were read") { // Somebody else's change came first: opening the editor // again on the old base would only be refused again. return fmt.Errorf("%w; your edit is not saved", err) } body, problem = edited, err.Error() } }, } c.Flags().StringVar(¬e, "note", "", "why, kept with the version") return c } func stripProblem(b []byte) []byte { if bytes.HasPrefix(b, []byte("# NOT SAVED: ")) { if i := bytes.IndexByte(b, '\n'); i >= 0 { return b[i+1:] } } return b } func runEditor(file string, in io.Reader, out, errOut io.Writer) error { editor := os.Getenv("VISUAL") if editor == "" { editor = os.Getenv("EDITOR") } if editor == "" { editor = "vi" } parts := strings.Fields(editor) c := exec.Command(parts[0], append(parts[1:], file)...) //nolint:gosec // the operator's own editor c.Stdin, c.Stdout, c.Stderr = in, out, errOut if err := c.Run(); err != nil { return fmt.Errorf("%s: %w", editor, err) } return nil } func newConfigHistoryCmd(client func() *ctlClient, asJSON *bool) *cobra.Command { var limit int c := &cobra.Command{ Use: "history", Short: "Versions of the cluster's settings, newest first", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, _ []string) error { var list []adminapi.SettingsView if err := client().get(cmd.Context(), "/api/v1/settings/history?limit="+strconv.Itoa(limit), &list); err != nil { return err } if *asJSON { return emitJSON(list) } w := tabTo(cmd.OutOrStdout()) fmt.Fprintln(w, "VERSION\tWHEN\tBY\tNOTE") for _, v := range list { fmt.Fprintf(w, "%d\t%s\t%s\t%s\n", v.Version, v.At.Local().Format(time.DateTime), orUnknown(v.By), v.Note) } return w.Flush() }, } c.Flags().IntVar(&limit, "limit", 20, "how many") return c } func newConfigRollbackCmd(client func() *ctlClient) *cobra.Command { var note string c := &cobra.Command{ Use: "rollback VERSION", Short: "Put an earlier version of the cluster's settings back in force, as a new version", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { n, err := strconv.ParseInt(args[0], 10, 64) if err != nil || n < 1 { return errors.New("VERSION is a number from 'llmgw ctl config history'") } var cur, old adminapi.SettingsView if err := client().get(cmd.Context(), "/api/v1/settings", &cur); err != nil { return err } if err := client().get(cmd.Context(), "/api/v1/settings?version="+args[0], &old); err != nil { return err } if note == "" { note = "back to version " + args[0] } return putSettings(cmd, client, cur, []byte(old.Body), note) }, } c.Flags().StringVar(¬e, "note", "", "why, kept with the version") return c }