package main import ( "context" "flag" "fmt" "sort" "strings" "time" "github.com/novox/mesh-control/internal/inventory" ) // is anything broken, is anything not answering, is anything out of date. // // Split out of main.go, which had reached 2,769 lines because appending was always the // cheapest next step. That is how novox/hq ADR 0001 records `hal/sdk` reaching 34,636: // nothing in it was wrong, and no one edit was the one that should have been a new file. // short is a commit as a person refers to it. func short(commit string) string { if len(commit) > 8 { return commit[:8] } return commit } // statusCommand answers "did my change go out?". // // novox/hq ADR 0010 names losing that question as the real risk of replacing a pipeline with a // comparison: it is answerable today by opening a pipeline, and something has to replace that or // this is worse to live with whatever its other properties. // // The answer is not "a job succeeded". It is which modules the mesh has not built from what their // source now has, and which machines are running the old one. func statusCommand(ctx context.Context, args []string) error { set := flag.NewFlagSet("status", flag.ContinueOnError) asJSON := set.Bool("json", false, "the same answers, for something other than a person") if _, err := parseAround(set, args); err != nil { return err } inv, err := openInventory(ctx) if err != nil { return err } defer inv.Close() asked, err := theThreeQuestions(ctx, inv) if err != nil { return err } wrong, nodes, quiet := asked.wrong, asked.nodes, asked.quiet behind, sources := asked.behind, asked.sources if *asJSON { body, err := statusAsJSON(wrong, nodes, quiet, behind, sources, asked.waiting) if err != nil { return err } fmt.Println(string(body)) return nil } if len(wrong) > 0 { fmt.Printf("%d machine(s) are not doing what they were told:\n\n", len(wrong)) for _, d := range wrong { fmt.Printf(" %-18s %-9s %s\n", d.Node, d.Outcome, d.At.Local().Format("2006-01-02 15:04")) if d.Refused != "" { // The host's own words. It says exactly what it could not accept, and nothing // written here would say it better. fmt.Printf(" %-18s %s\n", "", firstLine(d.Refused)) } for _, f := range d.Failed { fmt.Printf(" %-18s %s: %s\n", "", f.ID, firstLine(f.Error)) } } fmt.Println() } if len(quiet) > 0 { var said []string for _, n := range quiet { said = append(said, n.Name+" ("+heardFrom(n)+")") } fmt.Printf("%d machine(s) not heard from lately:\n %s\n\n", len(quiet), strings.Join(said, "\n ")) } if len(behind) > 0 { var names []string for m := range behind { names = append(names, m) } sort.Strings(names) fmt.Printf("%d module(s) behind their source:\n\n", len(behind)) for _, m := range names { from := sources[m] fmt.Printf(" %-18s holds %s, source has %s\n", m, short(from.BuiltFrom), short(from.Head)) if on := behind[m]; len(on) > 0 { // The part somebody actually wants. A module being out of date is a fact about // the catalogue; machines running the old one is the thing with consequences. fmt.Printf(" %-18s running on %s\n", "", strings.Join(on, ", ")) } else { fmt.Printf(" %-18s assigned to nothing\n", "") } } // The remedy, beside the problem. A status that says what is wrong and not what to do // about it makes somebody go and find the command, and the command is the whole point of // having noticed. fmt.Printf("\n `build --behind` builds them; `push --behind` sends them on\n") fmt.Println() } if len(asked.waiting) > 0 { // The other half of "is anything out of date": a module behind its source says the // catalogue is old, and this says a machine is — and only this one has somebody's change // waiting inside it. var told, never []string for _, m := range asked.waiting { if m.Never { never = append(never, m.Node) continue } told = append(told, m.Node) } if len(told) > 0 { fmt.Printf("%d machine(s) are not running what the mesh would send them:\n %s\n", len(told), strings.Join(told, ", ")) } if len(never) > 0 { // Never told is not out of date. The remedy is the same push and the situation is // not the same at all: nobody has ever asked this machine to be anything. fmt.Printf("%d machine(s) have never been sent anything:\n %s\n", len(never), strings.Join(never, ", ")) } fmt.Printf("\n `push --behind` sends them\n\n") } if len(wrong) == 0 && len(quiet) == 0 && len(behind) == 0 && len(asked.waiting) == 0 { // Said plainly. "Nothing to report" and "nothing was checked" must never look the same, // and getting here means every question was asked and answered. fmt.Printf("%d machine(s), all doing what they were told, all heard from, running what "+ "the mesh would send them, and every module current with its source\n", len(nodes)) } return nil } // firstLine is as much of a failure as belongs in a list. func firstLine(s string) string { if cut := strings.IndexByte(s, '\n'); cut >= 0 { return strings.TrimSpace(s[:cut]) } return strings.TrimSpace(s) } // builds keeps what a builder said, for the serving control plane. // // A type of its own rather than a method on the enrolment, because they are unrelated things // arriving on one queue and an implementation of one should not have to say anything about the // other. type builds struct{ inv *inventory.Inventory } // theThreeQuestions reads what anything answering "is the mesh alright" needs. // // **One reading, for every way of saying it** (novox/hq 03-DESIGN/01-to-be/11-a-board.md). There // are three now — a person's status, its JSON, and a page — and three implementations of "which // machine is not doing what it was told" would be three chances to disagree about it. // // The order is the design and not a convenience: is anything broken, is anything not answering, is // anything out of date. The first has consequences now, the second may, the third is a plan for // later — and anything that led with the third would bury the first. func theThreeQuestions(ctx context.Context, inv *inventory.Inventory) (answers, error) { var out answers var err error out.wrong, err = inv.NotDoingWhatTheyWereTold(ctx) if err != nil { return answers{}, err } out.nodes, err = inv.Nodes(ctx) if err != nil { return answers{}, err } for _, n := range out.nodes { // Never heard from, or not lately. Different from failing: a machine that says nothing // may be new, switched off, or unreachable, and none of those is a machine that tried // and could not. if n.LastSeen.IsZero() || time.Since(n.LastSeen) > time.Hour { out.quiet = append(out.quiet, n) } } out.behind, err = inv.Behind(ctx) if err != nil { return answers{}, err } // And which machines are not running what the mesh would send them. The same question as a // module being behind its source, one level down: that one says the catalogue is out of date, // this one says a machine is — and only the second has anybody's change waiting in it. would, err := wouldSend(ctx, inv, out.nodes) if err != nil { return answers{}, err } out.waiting, err = inv.Waiting(ctx, would) if err != nil { return answers{}, err } out.sources = map[string]inventory.Source{} for module := range out.behind { from, err := inv.SourceOf(ctx, module) if err != nil { return answers{}, err } out.sources[module] = from } return out, nil }