diff --git a/cmd/mesh-controller/batches.go b/cmd/mesh-controller/batches.go index 9f030a6c..2f5505e9 100644 --- a/cmd/mesh-controller/batches.go +++ b/cmd/mesh-controller/batches.go @@ -1109,6 +1109,16 @@ const resolverSeat = "mesh-dns-resolver" // classOf is a walk's class (novox/hq ADR 0282 decision 1): core when it walks a module on the controller's own // path or one holding the mesh's resolver, leaf otherwise. func classOf(walk inventory.Plan, entries []inventory.Entry) string { + modules := make([]string, 0, len(walk.Modules)) + for m := range walk.Modules { + modules = append(modules, m) + } + return classOfModules(modules, entries) +} + +// classOfModules is the class of modules moved together: core when one is on the controller's own path or holds +// the mesh's resolver, leaf otherwise (ADR 0282 decision 1). +func classOfModules(modules []string, entries []inventory.Entry) string { resolvers := map[string]bool{} for _, e := range entries { for _, c := range e.Manifest.Claims { @@ -1117,7 +1127,7 @@ func classOf(walk inventory.Plan, entries []inventory.Entry) string { } } } - for m := range walk.Modules { + for _, m := range modules { if _, own := onTheControllersPath[m]; own || resolvers[m] { return inventory.ClassCore } diff --git a/cmd/mesh-controller/changeplan.go b/cmd/mesh-controller/changeplan.go index 9c84cd19..5d3bfb08 100644 --- a/cmd/mesh-controller/changeplan.go +++ b/cmd/mesh-controller/changeplan.go @@ -105,9 +105,23 @@ func changePlanOf(repository, base, head string, r mergeReach, entries []invento 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, a module it adds a directory for included — and none when it moves nothing. +func changeClassOf(p link.ChangePlan, entries []inventory.Entry) string { + if len(p.Moved) == 0 && len(p.Dependents) == 0 && len(p.New) == 0 { + return classNone + } + return classOfModules(append(append([]string{}, p.Moved...), p.Dependents...), 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" diff --git a/cmd/mesh-controller/fixclass_test.go b/cmd/mesh-controller/fixclass_test.go new file mode 100644 index 00000000..a6e828c2 --- /dev/null +++ b/cmd/mesh-controller/fixclass_test.go @@ -0,0 +1,51 @@ +package main + +import ( + "testing" + + "github.com/novox/mesh-controller/internal/catalogue" + "github.com/novox/mesh-controller/internal/inventory" + "github.com/novox/mesh-controller/internal/link" +) + +// **A change plan says its class** (novox/hq ADR 0313 §2): the issue tracker holds a fix to the budget of the +// slowest-budgeted module the fix moves, and a fix that moves no module has no fix budget. The plan says core, +// leaf or none, so mesh-delivery can pass it on with the fix's delivery. +func TestAChangePlanSaysTheClassOfWhatItMoves(t *testing.T) { + const repo = "http://forge.internal:20000/novox/mesh-catalog.git" + entry := func(name, path string) inventory.Entry { + e := fromRepo(name, repo, path) + e.Source.BuiltFrom = "old" + e.On = []string{"anchor"} + return e + } + gitea := entry("gitea", "modules/gitea") + resolver := entry("dnsmasq", "modules/dnsmasq") + resolver.Manifest.Claims = []catalogue.Claim{{Name: resolverSeat}} + delivery := entry(catalogue.DeliverySeat, "modules/mesh-delivery") + webApp := entry("photos", "modules/photos") + entries := []inventory.Entry{gitea, resolver, delivery, webApp} + // photos stands on gitea: a change to gitea moves photos as its dependent. + edges := []inventory.Edge{{From: "photos", To: "gitea", Kind: inventory.EdgeStandsOn}} + policy := func(string) (inventory.Upgrade, bool) { return inventory.Upgrade{RollOut: true}, true } + plan := func(paths ...string) link.ChangePlan { + m := link.SourceMoved{Owner: "novox", Repo: "mesh-catalog", Base: "main", Commit: "head", Paths: paths} + return changePlanOf("novox/mesh-catalog", "main", "head", reachOfMerge(m, entries, nil, edges), entries, policy) + } + + for _, c := range []struct { + why string + paths []string + want string + }{ + {"a leaf module and its dependent", []string{"modules/gitea/index.ts"}, inventory.ClassLeaf}, + {"the delivery's owner, on the controller's own path", []string{"modules/mesh-delivery/cmd/x.go"}, inventory.ClassCore}, + {"the mesh's resolver", []string{"modules/dnsmasq/module.json"}, inventory.ClassCore}, + {"a leaf and a core module together: the slowest-budgeted", []string{"modules/gitea/index.ts", "modules/dnsmasq/module.json"}, inventory.ClassCore}, + {"nothing any module's build reads", []string{"README.md"}, "none"}, + } { + if got := plan(c.paths...).Class; got != c.want { + t.Errorf("%s: class %q, want %q", c.why, got, c.want) + } + } +} diff --git a/internal/link/events.go b/internal/link/events.go index eab227d9..60715315 100644 --- a/internal/link/events.go +++ b/internal/link/events.go @@ -311,6 +311,9 @@ type ChangePlan struct { Steps []string `json:"steps,omitempty"` // Summary is the plan in one line, for a commit status. Summary string `json:"summary"` + // Class is the class of the slowest-budgeted module the change moves (novox/hq ADR 0282 §1, ADR 0313 §2): + // core, leaf, or none when it moves no module at all — a fix that moves nothing has no fix budget. + Class string `json:"class,omitempty"` } // MachinePlan is one machine's part of a change plan.