// Command config refuses the settings form's names in a module that has moved to config (novox/hq ADR 0348 // clauses 1 and 11, design 57 package K6, issue 665). // // "Config" replaces "settings": a manifest declares its values under `config`, a mergeable file names the keys it // takes under `takes-config`, and a file asks for a value with `${config:}`. A mergeable file naming no keys at // all is the settings form's last shape: it takes every key of its module's layer and declares none, so mesh-config // could set none of them (ADR 0348 §10, ADR 0326 §3); it is found here like an old name (issue 735). The old names — `settings`, // `takes-settings` and `${setting:}` — stay accepted by the controller as aliases until every module has // moved, and are then refused (ADR 0348 §1). The modules not yet moved are listed in the file `unmoved` beside this // check, one per line: the catalogue's list ADR 0348 §11 names. Every other module says no old name, in its // manifest or in a template file its manifest names; a module on the list that says none is taken off it; and a // line the trunk's copy (the merge base's, given with -base) does not hold is refused, so the list only shrinks; // while the trunk holds no copy, the modules in seed are the trunk's. // When it is empty, the controller refuses the old names itself (design 57 package R1). // // Prose is not judged: a comment in a file's text that says "settings" composes into the machine's file, and // rewording it would change what a node is sent. // // go run . [-base ] // // Exits 1 on a finding. package main import ( "bufio" "encoding/json" "flag" "fmt" "os" "path/filepath" "regexp" "sort" "strings" ) // The settings form's names (ADR 0348 §1). A key of a JSON object is a quoted string followed by a colon; a // string value is never followed by one. var ( settingRef = regexp.MustCompile(`\$\{setting:[a-z0-9][a-z0-9_.-]*\}`) takesSettingsKey = regexp.MustCompile(`"takes-settings"\s*:`) oldPlaceholder = "${setting:…}" oldTakes = "takes-settings" oldDeclaration = "settings" wholeLayer = "taking its whole layer" listFile = "unmoved" listPathInCatalog = "checks/config/" + listFile ) // Check answers, per module that says any, the settings form's names its manifest and its template files say. func Check(root string) (map[string][]string, error) { manifests, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json")) if err != nil { return nil, err } sort.Strings(manifests) out := map[string][]string{} for _, path := range manifests { raw, err := os.ReadFile(path) if err != nil { return nil, err } var m struct { Module string `json:"module"` Settings json.RawMessage `json:"settings"` Resources []map[string]any `json:"resources"` } if err := json.Unmarshal(raw, &m); err != nil { return nil, fmt.Errorf("%s: %w", path, err) } var said []string if len(m.Settings) > 0 && string(m.Settings) != "null" { said = append(said, oldDeclaration) } if takesSettingsKey.Match(raw) { said = append(said, oldTakes) } if settingRef.Match(raw) { said = append(said, oldPlaceholder) } for _, r := range m.Resources { // A mergeable file naming no keys takes every key of the layer (ADR 0326 §3), declaring none of them. if how, _ := r["merge"].(string); how != "" { _, oldList := r["takes-settings"] _, newList := r["takes-config"] if !oldList && !newList { said = append(said, fmt.Sprintf("%v %s", r["id"], wholeLayer)) } } name, _ := r["template"].(string) if name == "" { continue } text, err := os.ReadFile(filepath.Join(filepath.Dir(path), filepath.FromSlash(name))) if err != nil { return nil, fmt.Errorf("%s names the template %s: %w", m.Module, name, err) } if settingRef.Match(text) { said = append(said, oldPlaceholder+" in "+name) } } if len(said) > 0 { sort.Strings(said) out[m.Module] = said } } return out, nil } // seed is the list as it was first written, standing for the trunk's copy until the trunk holds one. // The modules taking a whole layer were found after the list was first written and before the trunk held a copy // (issue 735), so they stand in it too. var seed = map[string]bool{ "betterbird": true, "fail2ban": true, "influxdb": true, "keycloak": true, "mailu": true, "searxng": true, "baserow": true, "confluence": true, "gitea": true, "gitlab": true, "grafana": true, "home-assistant": true, "icecast": true, "jira": true, "letta": true, "matrix": true, "nodered": true, "supabase": true, "unifi": true, } // Unmoved is the modules a list names, one per line; a line starting with # and a blank line are not modules. func Unmoved(path string) (map[string]bool, error) { f, err := os.Open(path) if err != nil { return nil, err } defer f.Close() out := map[string]bool{} s := bufio.NewScanner(f) for s.Scan() { line := strings.TrimSpace(s.Text()) if line == "" || strings.HasPrefix(line, "#") { continue } out[line] = true } return out, s.Err() } // Judge answers the modules that say an old name and are not on the list (refused), the modules on the list that // say none (stale), and the modules on the list that the trunk's list does not hold (added). func Judge(found map[string][]string, unmoved, base map[string]bool) (refused, stale, added []string) { for m := range found { if !unmoved[m] { refused = append(refused, m) } } for m := range unmoved { if _, says := found[m]; !says { stale = append(stale, m) } if !base[m] { added = append(added, m) } } sort.Strings(refused) sort.Strings(stale) sort.Strings(added) return refused, stale, added } func main() { basePath := flag.String("base", "", "the trunk's copy of unmoved (the merge base's); seed when absent") flag.Parse() root := "." if flag.NArg() > 0 { root = flag.Arg(0) } found, err := Check(root) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } unmoved, err := Unmoved(listFile) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } base := seed if *basePath != "" { if base, err = Unmoved(*basePath); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } refused, stale, added := Judge(found, unmoved, base) for _, m := range refused { fmt.Printf("%s says %s, the settings form's names, and is not on %s: write config, takes-config and "+ "${config:} throughout, and name the keys each mergeable file takes (novox/hq ADR 0348 §1, §11)\n", m, strings.Join(found[m], ", "), listPathInCatalog) } for _, m := range stale { fmt.Printf("%s names %s, which says no old name: take it off the list\n", listPathInCatalog, m) } for _, m := range added { fmt.Printf("%s gains %s, which the trunk's copy does not hold: the list only shrinks — move the module to "+ "config instead (novox/hq ADR 0348 §11)\n", listPathInCatalog, m) } if len(refused) > 0 || len(stale) > 0 || len(added) > 0 { os.Exit(1) } fmt.Printf("config: no moved module says settings, takes-settings or ${setting:}, or takes a whole layer "+ "(%d not yet moved)\n", len(unmoved)) }