202 lines
7.1 KiB
Go
202 lines
7.1 KiB
Go
// 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:<key>}`. 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:<key>}` — 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 <the trunk's unmoved>] <catalogue root>
|
|
//
|
|
// 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:<key>} 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:<key>}, or takes a whole layer "+
|
|
"(%d not yet moved)\n",
|
|
len(unmoved))
|
|
}
|