// Command predefined refuses a change to predefined.json that removes a key a module still names (novox/hq ADR // 0348 clause 7, design 57 package K2, issue 661). // // Mesh config and node config hold values only for the keys on the catalogue's list of predefined keys, at the // root of the repository; a module's config value may fall back to one, `"predefined": ""`. A key removed // while a manifest still names it would leave that module falling back to nothing: the controller refuses its // build then, at the next merge that touches it. So the removal is refused here, in the pull request that makes // it, naming each module that names the key. // // **What it cannot know.** Whether a key still holds a value at the mesh or a node is mesh-config's to say (design // 57 package K3): it holds the values, and the catalogue's check reads no store. Until mesh-config refuses that // itself, removing a key nobody names is let through here, and said. // // The list it compares with is the trunk's at the merge base, given with -base; without one, nothing was removed. // The controller the mesh runs reads the list itself (type, validation, every manifest against it) in the // module check; this check reads only its keys. // // go run . [-base ] // // Exits 1 on a finding. package main import ( "encoding/json" "errors" "flag" "fmt" "io/fs" "os" "path/filepath" "sort" "strings" ) // File is the list's file at the root of the catalogue. const File = "predefined.json" // Keys is the keys of a list, read as a JSON object; an absent file has none. func Keys(path string) (map[string]bool, error) { raw, err := os.ReadFile(path) if errors.Is(err, fs.ErrNotExist) { return map[string]bool{}, nil } if err != nil { return nil, err } var keys map[string]json.RawMessage if err := json.Unmarshal(raw, &keys); err != nil { return nil, fmt.Errorf("%s is not a JSON object of keys: %w", path, err) } out := map[string]bool{} for k := range keys { out[k] = true } return out, nil } // Named is, per predefined key, the modules whose manifests name it: under `config`, or its alias `settings`. func Named(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) named := 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"` Config map[string]json.RawMessage `json:"config"` Settings map[string]json.RawMessage `json:"settings"` } if err := json.Unmarshal(raw, &m); err != nil { return nil, fmt.Errorf("%s: %w", path, err) } for _, declared := range []map[string]json.RawMessage{m.Config, m.Settings} { for _, d := range declared { var value struct { Predefined string `json:"predefined"` } if json.Unmarshal(d, &value) == nil && value.Predefined != "" { named[value.Predefined] = append(named[value.Predefined], m.Module) } } } } return named, nil } // Removed is each key the base list holds and the tree's does not, with the modules still naming it; a removed // key nobody names is answered with none. func Removed(base, tree map[string]bool, named map[string][]string) map[string][]string { out := map[string][]string{} for k := range base { if !tree[k] { out[k] = named[k] } } return out } func main() { basePath := flag.String("base", "", "the trunk's predefined.json at the merge base") flag.Parse() if flag.NArg() != 1 { fmt.Fprintln(os.Stderr, "usage: predefined [-base ] ") os.Exit(2) } root := flag.Arg(0) tree, err := Keys(filepath.Join(root, File)) if err != nil { fmt.Println(err) os.Exit(1) } if *basePath == "" { fmt.Printf("%s: %d key(s); no trunk list given, so none was removed\n", File, len(tree)) return } base, err := Keys(*basePath) if err != nil { fmt.Println(err) os.Exit(1) } named, err := Named(root) if err != nil { fmt.Println(err) os.Exit(1) } removed := Removed(base, tree, named) keys := make([]string, 0, len(removed)) for k := range removed { keys = append(keys, k) } sort.Strings(keys) failed := false for _, k := range keys { if modules := removed[k]; len(modules) > 0 { failed = true fmt.Printf("%s: the key %q is removed and still named by %s: move each off it first (novox/hq ADR 0348 "+ "clause 7)\n", File, k, strings.Join(modules, ", ")) } else { fmt.Printf("%s: the key %q is removed and no module names it; whether the mesh or a node still holds a "+ "value for it is mesh-config's to refuse, not this check's\n", File, k) } } if failed { os.Exit(1) } fmt.Printf("%s: %d key(s), none removed that a module names\n", File, len(tree)) }