Files
llmgw/internal/settings/settings.go
T
Ilker ManapandClaude Opus 5.5 9ab93a0aa6 Watch the cluster and edit every setting from the console
The console gains live views of nodes, GPUs, models, replicas, usage and
agent logs, and a settings tree over every key of server.yaml (agent.yaml on
a node), generated from the configuration structs: values with their
defaults and doc comments, edited in dialogs, saved only once the service's
own loader accepts them, comments kept. The generator now tells blocks apart
by package, which brings the TLS, engine and supervisor settings back into
the reference.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-03 22:54:09 +02:00

514 lines
13 KiB
Go

// Package settings edits llmgw's configuration files key by key, for the
// settings screens: what each key is comes from the generated reference, so
// every setting the code reads is there without anyone listing it, and the
// file is edited as YAML nodes, so its comments and order survive.
package settings
import (
"bytes"
"fmt"
"os"
"reflect"
"strconv"
"strings"
"time"
"go.yaml.in/yaml/v3"
"gitea.mynodes.xyz/ilker/llmgw/internal/docgen"
)
// Kind is how a key is shown and edited.
type Kind int
// The kinds of key.
const (
Text Kind = iota
Int
Float
Bool
Duration
TextList // []string, []int: one value a line
TextMap // map[string]string and the like: key = value lines
Block // a nested block: a submenu
BlockList // a list of blocks: a submenu of entries
BlockMap // a map of blocks: a submenu of named entries
)
// Key is one key of a block, as the screens need it.
type Key struct {
docgen.Field
Kind Kind
// Block is the block it opens, for Block, BlockList and BlockMap.
Block *docgen.Block
}
// Blocks finds a block by ID in the generated reference.
func Find(id string) (*docgen.Block, bool) {
for i := range docgen.Reference {
if docgen.Reference[i].ID() == id {
return &docgen.Reference[i], true
}
}
return nil, false
}
// Keys are a block's keys with their kinds.
func Keys(b *docgen.Block) []Key {
out := make([]Key, 0, len(b.Fields))
for _, f := range b.Fields {
k := Key{Field: f}
k.Kind, k.Block = kindOf(b.Package, f)
out = append(out, k)
}
return out
}
func kindOf(pkg string, f docgen.Field) (Kind, *docgen.Block) {
if nb, ok := Find(f.Nested); ok {
return Block, nb
}
t := strings.TrimPrefix(f.Type, "*")
switch t {
case "string":
return Text, nil
case "int", "int64", "uint", "uint32":
return Int, nil
case "float64", "float32":
return Float, nil
case "bool":
return Bool, nil
case "time.Duration":
return Duration, nil
}
if elem, ok := strings.CutPrefix(t, "[]"); ok {
if b, ok := Find(qualify(pkg, elem)); ok {
return BlockList, b
}
return TextList, nil
}
if strings.HasPrefix(t, "map[") {
elem := t[strings.Index(t, "]")+1:]
if b, ok := Find(qualify(pkg, elem)); ok {
return BlockMap, b
}
return TextMap, nil
}
return Text, nil
}
// qualify is a type as a block ID: Endpoint in config is config.Endpoint.
func qualify(pkg, typ string) string {
if strings.Contains(typ, ".") {
return typ
}
return pkg + "." + typ
}
// Root is the block a file is: server.yaml is config.Server, agent.yaml the
// agent's Config.
func Root(file string) (*docgen.Block, error) {
id := "config.Server"
if strings.Contains(file, "agent") {
id = "agent.Config"
}
b, ok := Find(id)
if !ok {
return nil, fmt.Errorf("no block %s in the reference", id)
}
return b, nil
}
// Doc is a configuration file, open for editing.
type Doc struct {
Path string
root *yaml.Node // the document's top mapping
doc yaml.Node
// Changed counts edits not yet saved.
Changed int
}
// Open reads a file; a missing one is an empty document.
func Open(path string) (*Doc, error) {
d := &Doc{Path: path}
b, err := os.ReadFile(path)
if err != nil && !os.IsNotExist(err) {
return nil, err
}
if len(bytes.TrimSpace(b)) > 0 {
if err := yaml.Unmarshal(b, &d.doc); err != nil {
return nil, fmt.Errorf("%s: %w", path, err)
}
}
if d.doc.Kind == 0 {
d.doc = yaml.Node{Kind: yaml.DocumentNode, Content: []*yaml.Node{{Kind: yaml.MappingNode, Tag: "!!map"}}}
}
d.root = d.doc.Content[0]
if d.root.Kind != yaml.MappingNode {
return nil, fmt.Errorf("%s is not a mapping at the top", path)
}
return d, nil
}
// Bytes is the document as it would be written.
func (d *Doc) Bytes() ([]byte, error) {
var b bytes.Buffer
enc := yaml.NewEncoder(&b)
enc.SetIndent(2)
if err := enc.Encode(&d.doc); err != nil {
return nil, err
}
return b.Bytes(), enc.Close()
}
// Node is the node at a path of keys and list indexes, or nil.
func (d *Doc) Node(path []string) *yaml.Node {
n := d.root
for _, p := range path {
n = child(n, p)
if n == nil {
return nil
}
}
return n
}
func child(n *yaml.Node, key string) *yaml.Node {
switch n.Kind {
case yaml.MappingNode:
for i := 0; i+1 < len(n.Content); i += 2 {
if n.Content[i].Value == key {
return n.Content[i+1]
}
}
case yaml.SequenceNode:
if i, err := strconv.Atoi(key); err == nil && i >= 0 && i < len(n.Content) {
return n.Content[i]
}
}
return nil
}
// ensure is the node at a path, made as mappings where there is none.
func (d *Doc) ensure(path []string) *yaml.Node {
n := d.root
for _, p := range path {
c := child(n, p)
if c == nil {
if n.Kind != yaml.MappingNode {
return nil
}
c = &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"}
n.Content = append(n.Content, &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: p}, c)
}
n = c
}
return n
}
// Set writes one key's value: a scalar, a list of lines or a map of
// "key = value" lines, by its kind. It checks the value first.
func (d *Doc) Set(path []string, k Kind, value string) error {
if len(path) == 0 {
return fmt.Errorf("no key")
}
n, err := encode(k, value)
if err != nil {
return err
}
parent := d.ensure(path[:len(path)-1])
if parent == nil {
return fmt.Errorf("%s is not a block", strings.Join(path[:len(path)-1], "."))
}
key := path[len(path)-1]
if parent.Kind == yaml.SequenceNode {
i, _ := strconv.Atoi(key)
parent.Content[i] = n
} else if c := child(parent, key); c != nil {
// Kept in place, with whatever comment it had.
n.HeadComment, n.LineComment, n.FootComment = c.HeadComment, c.LineComment, c.FootComment
*c = *n
} else {
parent.Content = append(parent.Content, &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: key}, n)
}
d.Changed++
return nil
}
// Unset removes a key, so its default applies.
func (d *Doc) Unset(path []string) {
if len(path) == 0 {
return
}
parent := d.Node(path[:len(path)-1])
if parent == nil {
return
}
key := path[len(path)-1]
switch parent.Kind {
case yaml.MappingNode:
for i := 0; i+1 < len(parent.Content); i += 2 {
if parent.Content[i].Value == key {
parent.Content = append(parent.Content[:i], parent.Content[i+2:]...)
d.Changed++
return
}
}
case yaml.SequenceNode:
if i, err := strconv.Atoi(key); err == nil && i < len(parent.Content) {
parent.Content = append(parent.Content[:i], parent.Content[i+1:]...)
d.Changed++
}
}
}
// Append adds an empty block to the list at path, and returns its index.
func (d *Doc) Append(path []string) int {
parent := d.ensure(path[:len(path)-1])
key := path[len(path)-1]
seq := child(parent, key)
if seq == nil || seq.Kind != yaml.SequenceNode {
seq = &yaml.Node{Kind: yaml.SequenceNode, Tag: "!!seq"}
if c := child(parent, key); c != nil {
*c = *seq
seq = c
} else {
parent.Content = append(parent.Content, &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: key}, seq)
}
}
seq.Content = append(seq.Content, &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"})
d.Changed++
return len(seq.Content) - 1
}
// AddEntry adds an empty block under a name to the map at path.
func (d *Doc) AddEntry(path []string, name string) error {
m := d.ensure(path)
if m == nil || m.Kind != yaml.MappingNode {
return fmt.Errorf("%s is not a map", strings.Join(path, "."))
}
if child(m, name) != nil {
return fmt.Errorf("%s is there already", name)
}
m.Content = append(m.Content, &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: name},
&yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"})
d.Changed++
return nil
}
// Len is how many entries a list or map at path has.
func (d *Doc) Len(path []string) int {
n := d.Node(path)
switch {
case n == nil:
return 0
case n.Kind == yaml.SequenceNode:
return len(n.Content)
case n.Kind == yaml.MappingNode:
return len(n.Content) / 2
}
return 0
}
// MapKeys are a map's keys, in the file's order.
func (d *Doc) MapKeys(path []string) []string {
n := d.Node(path)
if n == nil || n.Kind != yaml.MappingNode {
return nil
}
var out []string
for i := 0; i+1 < len(n.Content); i += 2 {
out = append(out, n.Content[i].Value)
}
return out
}
// Text is a key's value as it is edited: a scalar as written, a list one
// item a line, a map "key = value" a line. ok is false for a key the file
// does not set.
func (d *Doc) Text(path []string) (string, bool) {
n := d.Node(path)
if n == nil {
return "", false
}
switch n.Kind {
case yaml.ScalarNode:
return n.Value, true
case yaml.SequenceNode:
var l []string
for _, c := range n.Content {
l = append(l, c.Value)
}
return strings.Join(l, "\n"), true
case yaml.MappingNode:
var l []string
for i := 0; i+1 < len(n.Content); i += 2 {
l = append(l, n.Content[i].Value+" = "+n.Content[i+1].Value)
}
return strings.Join(l, "\n"), true
}
return "", true
}
func encode(k Kind, value string) (*yaml.Node, error) {
v := strings.TrimSpace(value)
scalar := func(tag string) *yaml.Node { return &yaml.Node{Kind: yaml.ScalarNode, Tag: tag, Value: v} }
switch k {
case Int:
if _, err := strconv.ParseInt(v, 10, 64); err != nil {
return nil, fmt.Errorf("%q is not a whole number", v)
}
return scalar("!!int"), nil
case Float:
if _, err := strconv.ParseFloat(v, 64); err != nil {
return nil, fmt.Errorf("%q is not a number", v)
}
return scalar("!!float"), nil
case Bool:
b, err := strconv.ParseBool(v)
if err != nil {
return nil, fmt.Errorf("%q is not true or false", v)
}
return &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!bool", Value: strconv.FormatBool(b)}, nil
case Duration:
if _, err := time.ParseDuration(v); err != nil {
return nil, fmt.Errorf("%q is not a duration, like 30s, 5m or 2h", v)
}
return scalar("!!str"), nil
case TextList:
seq := &yaml.Node{Kind: yaml.SequenceNode, Tag: "!!seq"}
for _, l := range splitLines(value) {
seq.Content = append(seq.Content, &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: l})
}
return seq, nil
case TextMap:
m := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"}
for _, l := range splitLines(value) {
k, val, ok := strings.Cut(l, "=")
if !ok {
return nil, fmt.Errorf("%q is not key = value", l)
}
m.Content = append(m.Content,
&yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: strings.TrimSpace(k)},
&yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: strings.TrimSpace(val)})
}
return m, nil
case Block, BlockList, BlockMap:
return nil, fmt.Errorf("a block is edited key by key")
}
return scalar("!!str"), nil
}
func splitLines(s string) []string {
var out []string
for _, l := range strings.FieldsFunc(s, func(r rune) bool { return r == '\n' || r == ',' }) {
if l = strings.TrimSpace(l); l != "" {
out = append(out, l)
}
}
return out
}
// Save checks the document with check — a loader that reads and validates
// a file — and writes it, the old file kept beside it. Nothing is written
// when the check fails.
func (d *Doc) Save(check func(path string) error) (string, error) {
b, err := d.Bytes()
if err != nil {
return "", err
}
tmp := d.Path + ".check"
if err := os.WriteFile(tmp, b, 0o600); err != nil {
return "", err
}
defer os.Remove(tmp)
if check != nil {
if err := check(tmp); err != nil {
return "", err
}
}
mode := os.FileMode(0o644)
backup := ""
if info, err := os.Stat(d.Path); err == nil {
mode = info.Mode().Perm()
old, err := os.ReadFile(d.Path)
if err != nil {
return "", err
}
backup = fmt.Sprintf("%s.bak-%s", d.Path, time.Now().Format("20060102-150405"))
if err := os.WriteFile(backup, old, mode); err != nil {
return "", err
}
}
if err := os.WriteFile(d.Path+".new", b, mode); err != nil {
return "", err
}
if err := os.Rename(d.Path+".new", d.Path); err != nil {
return "", err
}
d.Changed = 0
return backup, nil
}
// Effective is the value a loaded configuration has at a path of koanf
// keys — what applies, default included — as text.
func Effective(cfg any, path []string) (string, bool) {
v := reflect.ValueOf(cfg)
for _, p := range path {
for v.Kind() == reflect.Pointer {
if v.IsNil() {
return "", false
}
v = v.Elem()
}
switch v.Kind() {
case reflect.Struct:
found := false
t := v.Type()
for i := 0; i < t.NumField(); i++ {
if tag, _, _ := strings.Cut(t.Field(i).Tag.Get("koanf"), ","); tag == p {
v, found = v.Field(i), true
break
}
}
if !found {
return "", false
}
case reflect.Slice:
i, err := strconv.Atoi(p)
if err != nil || i >= v.Len() {
return "", false
}
v = v.Index(i)
case reflect.Map:
mv := v.MapIndex(reflect.ValueOf(p))
if !mv.IsValid() {
return "", false
}
v = mv
default:
return "", false
}
}
for v.Kind() == reflect.Pointer {
if v.IsNil() {
return "", true
}
v = v.Elem()
}
switch v.Kind() {
case reflect.Slice:
var l []string
for i := 0; i < v.Len(); i++ {
l = append(l, fmt.Sprint(v.Index(i).Interface()))
}
return strings.Join(l, ", "), true
case reflect.Struct, reflect.Map:
return "", true
}
if d, ok := v.Interface().(time.Duration); ok {
return d.String(), true
}
return fmt.Sprint(v.Interface()), true
}