package main import ( "errors" "fmt" "io" "os" "path/filepath" "sort" "strings" "time" "github.com/novox/mesh-controller/internal/catalogue" ) // moduleCheck judges manifests where they are written, with no mesh (novox/hq ADR 0037, issue 148). // // **The same functions registration runs, and nothing the command line adds** (ADR 0035): the strict // parse with every per-manifest problem, then the rules no single manifest can be judged against, // over exactly the manifests given. Somebody describing their own application in their own // repository runs this before pushing and finds out there, rather than when a running mesh refuses // the registration or, later, when a machine applies something that resolved and should not have. // // **What it cannot know without a store, it says.** The mesh's own seat set is the store's (ADR // 0122); this binary carries a compiled copy that the store overrides when loaded, so a claim on a // mesh seat is judged fully only at registration. A seat another module declares is unknown unless // that module's manifest is passed too. Both are printed as a note, not as a problem — a check that // refused what it could not see would teach people to ignore it. // // **And every identity against every bound it meets** (novox/hq ADR 0225, issue 263): each module's // identity, on a machine whose name is `longestMachine` characters, against the bound of every // provision it wants that a manifest given here offers. An overflow is refused in the pull request // that introduces it — a new requirement, a lowered bound, a longer slug — instead of on the // provider's machine when a real machine's name first meets the module's. // SomeManifestsVar, set by the merge gate, says the manifests given are only some of their repository's. const SomeManifestsVar = "MESH_MODULE_CHECK_SOME" func moduleCheck(paths []string, out io.Writer) error { return moduleCheckFor(paths, catalogue.DefaultLongestMachine, out) } func moduleCheckFor(paths []string, longestMachine int, out io.Writer) error { if len(paths) == 0 { return errors.New("module check ... — one file per module; pass every " + "manifest of a repository together so the rules between them are checked too") } shelf := catalogue.Shelf{} faulted := map[string]bool{} failed := 0 for _, path := range paths { raw, err := os.ReadFile(path) if err != nil { fmt.Fprintf(out, "%s: %v\n", path, err) failed++ continue } m, err := catalogue.ParseManifest(raw) if err != nil { fmt.Fprintf(out, "%s: %v\n", path, err) failed++ continue } if first, twice := shelf[m.Module]; twice { _ = first fmt.Fprintf(out, "%s: %s was already given; two manifests name one module\n", path, m.Module) failed++ continue } // A definition names no installation (novox/hq ADR 0112, ADR 0155): judged here, in the // catalogue-wide test, and at registration, which refuses in the same words. if wrong := catalogue.TrustProblems(m); len(wrong) > 0 { for _, p := range wrong { fmt.Fprintf(out, "%s: %s\n", path, p) } failed += len(wrong) faulted[m.Module] = true } if named := catalogue.InstallationProblems(m); len(named) > 0 { for _, p := range named { fmt.Fprintf(out, "%s: %s\n", path, p) } failed += len(named) faulted[m.Module] = true } shelf[m.Module] = m } // Between the manifests: a seat declared twice, a use of a seat nothing declares, a claim on // a seat that does not exist. Run only over what parsed, because a problem inside one manifest // has already been said and would be said again here in a worse form. // // **Over some of a repository's manifests, a seat none of them declares is a note** (novox/hq issue 364), as // this command's own word says above: the merge gate passes only the manifests a change touches, and says so // with SomeManifestsVar, so a module that uses or claims a seat another module declares (the operator // channel's, a channel bench) was refused there for a manifest it was not given. Given every manifest — the // catalogue's own check, and registration — it stays a refusal. some := os.Getenv(SomeManifestsVar) != "" problems := catalogue.CatalogueProblems(shelf) sort.Strings(problems) for _, p := range problems { if some && catalogue.IsUndeclaredSeat(p) { fmt.Fprintf(out, "note: %s among the manifests given; registration judges it against the whole catalogue, "+ "and passing the declaring module's manifest too judges it here\n", p) continue } fmt.Fprintln(out, p) failed++ } // Between the manifests too: an identity against the bounds of the provisions it wants, which // only the provider's manifest states. identities := catalogue.IdentityProblems(shelf, longestMachine) sort.Strings(identities) for _, p := range identities { fmt.Fprintln(out, p) } failed += len(identities) // And the data each module keeps (novox/hq ADR 0233): a provider that grants says what it keeps for // its consumers, a directory a container writes is declared, and no backup line is written by hand. data := catalogue.DataProblems(shelf) sort.Strings(data) for _, p := range data { fmt.Fprintln(out, p) } failed += len(data) var names []string for name := range shelf { names = append(names, name) } sort.Strings(names) // **Every long-running resource says how it is ready** (novox/hq ADR 0240 rule 8): warned until the // date, refused from it. The count is the catalogue's: its merge check keeps the number and lets a // change lower it, never raise it. undeclared := 0 required := !checkNow().Before(catalogue.HealthRequiredFrom) for _, name := range names { missing := catalogue.Undeclared(shelf[name]) undeclared += len(missing) if len(missing) == 0 { continue } if required { for _, id := range missing { fmt.Fprintf(out, "%s: %s stays up and does not say how it is ready: a long-running resource declares "+ "health since %s (novox/hq ADR 0240 rule 8)\n", name, id, catalogue.HealthRequiredFrom.Format("2006-01-02")) } failed += len(missing) faulted[name] = true } } // **A file that asks for a setting and does not say whether it is trusted counts as trusted** (novox/hq issue // 339): listed and counted, never refused, so an author can opt out a file nothing trusts. unsaid := 0 for _, name := range names { unsaid += len(catalogue.UnsaidTrust(shelf[name])) } for _, name := range names { m := shelf[name] if faulted[name] { continue } fmt.Fprintf(out, "%s: ok", name) if n := len(m.Tools); n > 0 { fmt.Fprintf(out, ", %d tool(s)", n) } if len(m.Invokes) > 0 { fmt.Fprintf(out, ", invokes %s", joinInvokes(m.Invokes)) } // The state it keeps and reads (novox/hq ADR 0201), so a reviewer sees what lands on the bus. if len(m.State) > 0 { kept := make([]string, 0, len(m.State)) for _, s := range m.State { kept = append(kept, s.Name) } fmt.Fprintf(out, ", keeps state %s", strings.Join(kept, ", ")) } // And the logs it keeps, with their caps (novox/hq ADR 0297). if len(m.Logs) > 0 { kept := make([]string, 0, len(m.Logs)) for _, l := range m.Logs { kept = append(kept, fmt.Sprintf("%s (%d MiB)", l.Name, l.Cap())) } fmt.Fprintf(out, ", keeps log %s", strings.Join(kept, ", ")) } if len(m.Reads) > 0 { fmt.Fprintf(out, ", reads %s", strings.Join(m.Reads, ", ")) } // The data it keeps, by class, so a reviewer sees what the mesh will protect and how. if items := m.DataItems(); len(items) > 0 { kept := make([]string, 0, len(items)) for _, it := range items { kept = append(kept, it.ID+" ("+it.Class+")") } fmt.Fprintf(out, ", keeps %s", strings.Join(kept, ", ")) } // How what it runs is ready (ADR 0240): each declared check, and what is judged by liveness alone. var checks []string for _, r := range m.Resources { if h, has, _ := catalogue.ReadHealth(r); has { checks = append(checks, fmt.Sprintf("%v by %s", r["id"], catalogue.HealthWords(h))) } } if len(checks) > 0 { fmt.Fprintf(out, ", ready: %s", strings.Join(checks, "; ")) } if missing := catalogue.UnsaidTrust(m); len(missing) > 0 { fmt.Fprintf(out, "; WARNING: %s ask(s) for a setting and do(es) not say whether it is trusted, so it counts as "+ "trusted: set at the terminal alone; say %q false where nothing trusts it (novox/hq issue 339)", strings.Join(missing, ", "), catalogue.TrustedField) } if missing := catalogue.Undeclared(m); len(missing) > 0 { fmt.Fprintf(out, "; WARNING: %s stay(s) up and say(s) not how it is ready — judged by liveness alone, "+ "refused from %s (ADR 0240 rule 8)", strings.Join(missing, ", "), catalogue.HealthRequiredFrom.Format("2006-01-02")) } fmt.Fprintln(out) } // The count the catalogue keeps (ADR 0240 rule 8), in a line its merge check reads. fmt.Fprintf(out, "%s %d\n", UndeclaredHealthLine, undeclared) fmt.Fprintf(out, "%s %d\n", UnsaidTrustLine, unsaid) if failed > 0 { return fmt.Errorf("%d problem(s) in %d manifest(s)", failed, len(paths)) } fmt.Fprintf(out, "%d manifest(s) checked. Judged against the seats this binary carries; a claim on "+ "one of the mesh's own seats is judged fully at registration, and a seat declared by a "+ "module not given here reads as unknown. Identities judged on a %d-character machine name, "+ "against the bounds of the providers given here\n", len(paths), longestMachine) return nil } // UndeclaredHealthLine starts the line `module check` says the count of long-running resources without // `health` in, over the manifests given: the catalogue's merge check compares it with the number it keeps. const UndeclaredHealthLine = "long-running resources without health:" // UnsaidTrustLine starts the line `module check` says the count of files that ask for a setting and do not say // whether it is trusted, and so count as trusted (novox/hq issue 339). const UnsaidTrustLine = "files asking for a setting without saying whether it is trusted:" // checkNow is the clock `module check` judges the date by; a test sets it. var checkNow = time.Now func joinInvokes(invokes []string) string { if len(invokes) == 1 && invokes[0] == "*" { return "every tool" } s := "" for i, t := range invokes { if i > 0 { s += ", " } s += t } return s } // manifestsUnder lists every module.json below a directory, for `module check `. func manifestsUnder(dir string) ([]string, error) { var found []string err := filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error { if err != nil { return err } if d.IsDir() && (d.Name() == "node_modules" || d.Name() == ".git" || d.Name() == "dist") { return filepath.SkipDir } if !d.IsDir() && d.Name() == "module.json" { found = append(found, path) } return nil }) sort.Strings(found) return found, err }