package main import ( "fmt" "path" "slices" "sort" "strings" "github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" ) // The change plan (novox/hq ADR 0238): **one commit, one plan** — what a change does to the mesh, computed // from its diffset (a repository, the branch it merges into, the commit at hand) by the planner, never by // a mapping of its own. reachOfMerge answers what moves and what follows it; this adds where each module // goes — the deploy plan, machine by machine in the build plan's order — and what is not an ordinary // send. A pull request's check posts it with its verdict; the release a merge makes follows the same // planner, so the two can be compared. // policyOf is a module's upgrade policy, as the controller holds it; false when it cannot be read. type policyOf func(module string) (inventory.Upgrade, bool) // changePlanOf is the change plan of a reach: pure, so it is tested without a store. func changePlanOf(repository, base, head string, r mergeReach, entries []inventory.Entry, policy policyOf) link.ChangePlan { p := link.ChangePlan{Repository: repository, Base: base, Head: head, Moved: r.Moved(), Dependents: r.Dependents(), New: r.Added, Unread: r.Unread, Tiers: r.Plan.Tiers} byName := map[string]inventory.Entry{} for _, e := range entries { byName[e.Manifest.Module] = e } // The machine holding the bus, whose declaration carries every module's grants (novox/hq issue 490). holder := "" for _, e := range entries { if e.Manifest.BusUsers != "" && e.Manifest.ClaimsSeat(catalogue.BrokerSeat) && len(e.On) > 0 { holder = e.On[0] } } var granting []string machines := map[string]*link.MachinePlan{} machine := func(name string) *link.MachinePlan { if machines[name] == nil { machines[name] = &link.MachinePlan{Machine: name} } return machines[name] } for _, tier := range r.Plan.Tiers { for _, name := range tier { e, held := byName[name] if !held { continue } u, known := policy(name) waits := known && !u.RollOut for _, on := range e.On { if waits { machine(on).Waits = append(machine(on).Waits, name) } else { machine(on).Receives = append(machine(on).Receives, name) } } if !waits && holder != "" && !(len(e.On) == 1 && e.On[0] == holder) && len(e.On) > 0 && !slices.Contains(granting, name) { granting = append(granting, name) } switch { case (name == "nats" || catalogue.ProvidesBus(e.Manifest)) && len(e.On) > 0: // Said before the merge (novox/hq issue 336): a new bus build holds every send to the bus's machine // until a person takes the step, so merging it is a promise to take it. p.Steps = append(p.Steps, fmt.Sprintf("%s: %s is never sent by an ordinary send, and until %s runs, "+ "nothing else is sent to %s either — unless the new build turns out the same as the one running there "+ "(issue 280)", busUpgradeNeeded, name, busUpgradeVerb, strings.Join(e.On, ", "))) case name == "nats" || catalogue.ProvidesBus(e.Manifest): p.Steps = append(p.Steps, "a planned bus step: the bus is upgraded by `bus upgrade`, never by an ordinary send") case waits && len(e.On) > 0 && (u.From == catalogue.FromModule || u.From == catalogue.FromData): // Judged per machine by the walk, against the build each was last sent (novox/hq ADR 0377). p.Steps = append(p.Steps, fmt.Sprintf("%s waits for a person: its policy records (%s) — unless on a machine "+ "its move recreates none of its containers and moves none of its data, which the walk judges and then sends", name, orNone(u.From))) case waits && len(e.On) > 0: p.Steps = append(p.Steps, fmt.Sprintf("%s waits for a person: its policy records (%s)", name, orNone(u.From))) } if len(e.Manifest.Provides) > 0 && len(e.On) > 0 { var offers []string for _, o := range e.Manifest.Provides { offers = append(offers, o.Name) } p.Steps = append(p.Steps, fmt.Sprintf("%s provides %s: its consumers are sent again after it", name, strings.Join(offers, ", "))) } if e.Manifest.Data != nil && len(e.On) > 0 { p.Steps = append(p.Steps, fmt.Sprintf("%s keeps data (ADR 0233): what it holds is backed up before it moves", name)) } } } if len(granting) > 0 { // Said before the merge (novox/hq issue 490): what the walk does when the change alters the bus's // user list. Whether it does is known only once the builds are registered, so it is said as a rule. p.Steps = append(p.Steps, fmt.Sprintf("%s: if this changes what the bus's user list says (a new tool, "+ "subject or user), %s is sent that list alone, every build there kept, before %s is judged on its first "+ "machine", grantsStepWord, holder, strings.Join(granting, ", "))) } var names []string for name := range machines { names = append(names, name) } sort.Strings(names) for _, name := range names { p.Machines = append(p.Machines, *machines[name]) } p.Summary = summaryOf(p) p.Class = changeClassOf(p, entries) return p } // classNone is the class of a change that moves no module: nothing for a walk to move (novox/hq ADR 0313 §2). const classNone = "none" // changeClassOf is a change plan's class (novox/hq ADR 0313 §2): that of the slowest-budgeted module it moves, // itself or as a dependent — core when one is on the controller's own path or holds the mesh's resolver, leaf // otherwise — and none when it moves nothing. A module it adds a directory for counts by its own class: the // catalogue names a module's directory by the module, so the directory's last element is its name. The graph holds // no manifest of it yet, so a new module is core by its name alone (on the controller's own path), never by a // resolver claim its manifest makes. func changeClassOf(p link.ChangePlan, entries []inventory.Entry) string { if len(p.Moved) == 0 && len(p.Dependents) == 0 && len(p.New) == 0 { return classNone } modules := append(append([]string{}, p.Moved...), p.Dependents...) for _, dir := range p.New { if name := path.Base(strings.Trim(dir, "/")); name != "." && name != "/" && name != "" { modules = append(modules, name) } } return classOfModules(modules, entries) } // grantsStepWord names the grants step (novox/hq issue 490): the bus's user list sent alone to the machine // holding the bus ahead of a walk's gate. Not the bus step, which replaces the bus itself. const grantsStepWord = "grants step" // busUpgradeNeeded is how a change plan says that merging it holds the bus's machine for a person's step. const busUpgradeNeeded = "merging this needs a person's bus upgrade" // summaryOf is a change plan in one line: what it builds, where it goes, and whether the bus moves. func summaryOf(p link.ChangePlan) string { if len(p.Moved) == 0 && len(p.New) == 0 { return "builds nothing: the change touches no module of the mesh's graph" } var parts []string what := strings.Join(p.Moved, ", ") if len(p.Dependents) > 0 { what += fmt.Sprintf(" (+%d dependent(s))", len(p.Dependents)) } if len(p.New) > 0 { if what != "" { what += ", " } what += "new: " + strings.Join(p.New, ", ") } var to []string for _, m := range p.Machines { if len(m.Receives) > 0 { to = append(to, m.Machine) } } if len(to) > 0 { parts = append(parts, "builds "+what+" → "+strings.Join(to, ", ")) } else { parts = append(parts, "builds "+what+", sent nowhere") } bus := "no bus step" for _, s := range p.Steps { switch { case strings.HasPrefix(s, busUpgradeNeeded): bus = busUpgradeNeeded case strings.HasPrefix(s, "a planned bus step") && bus == "no bus step": bus = "a bus step" } } parts = append(parts, bus) waiting := 0 for _, m := range p.Machines { waiting += len(m.Waits) } if waiting > 0 { parts = append(parts, fmt.Sprintf("%d wait(s) for a person", waiting)) } return strings.Join(parts, "; ") } // planText is a change plan as a person reads it, for the pull request's comment. func planText(p link.ChangePlan) string { var b strings.Builder fmt.Fprintf(&b, "change plan of %s at %.8s into %s: %s\n", p.Repository, p.Head, p.Base, p.Summary) for i, tier := range p.Tiers { fmt.Fprintf(&b, " tier %d: %s\n", i, strings.Join(tier, ", ")) } for _, m := range p.Machines { line := " " + m.Machine + ": " if len(m.Receives) > 0 { line += "receives " + strings.Join(m.Receives, ", ") } if len(m.Waits) > 0 { if len(m.Receives) > 0 { line += "; " } line += "waits for a person: " + strings.Join(m.Waits, ", ") } b.WriteString(line + "\n") } for _, s := range p.Steps { b.WriteString(" - " + s + "\n") } if len(p.Unread) > 0 { fmt.Fprintf(&b, " read by no module's build: %s\n", strings.Join(p.Unread, ", ")) } return b.String() }