Files
mesh-controller/cmd/mesh-controller/delivery_edges.go
T

171 lines
5.5 KiB
Go

package main
import (
"fmt"
"slices"
"sort"
"strings"
"github.com/novox/mesh-controller/internal/inventory"
)
// The order of a plan's modules is read from the module graph and from what the change says of the manifests
// (novox/hq ADR 0419), per module and never per repository: no `after:` line is read, and no member of a group
// is ordered against another. Pure, so a test needs no store.
// The rules that draw an edge of a plan's order, in the words the plan shows.
const (
ruleVerbBeforeCaller = "verb before caller"
ruleVersionSkew = "version skew"
ruleEngineFirst = "engine before controller"
)
// The two modules the rollout rules name.
const (
theController = "mesh-controller"
theEngine = "mesh-host"
)
// moduleChange is what a change says of one module's manifest: the seat verbs ("<seat>.<verb>") it adds to what the
// module serves, the seat verbs it adds to what the module invokes, and whether the manifest changes at all.
type moduleChange struct {
Serves []string
Invokes []string
Manifest bool
}
// planOrder is the edges the rules draw among a plan's modules, and the cycle they make when they make one.
type planOrder struct {
Edges []inventory.Edge
// Cycle is every edge of a cycle among the order's edges and the graph's, the module graph's included.
Cycle []inventory.Edge
}
// CycleSaid names every module and edge of the cycle, for a check to fail with (ADR 0419 clause 6).
func (o planOrder) CycleSaid() string {
if len(o.Cycle) == 0 {
return ""
}
var parts []string
for _, e := range o.Cycle {
parts = append(parts, edgeSaid(e))
}
return "the modules of the plan are ordered in a cycle: " + strings.Join(parts, "; ")
}
// edgeSaid is an edge as a person reads it: the module after, the module before, and why.
func edgeSaid(e inventory.Edge) string {
why := e.Why
if why == "" {
why = e.Kind
}
return fmt.Sprintf("%s after %s (%s)", e.From, e.To, why)
}
// orders is whether an edge of the module graph orders a plan's tiers (tiersOf).
func orders(e inventory.Edge) bool {
return e.Kind != inventory.EdgePackages && e.Kind != inventory.EdgeBuiltBy && e.From != e.To
}
// orderEdges draws the edges of a plan's order among its moved modules (ADR 0419 clause 3, 4): a module whose change
// adds a verb to a seat goes before one whose change adds a call of that verb; the controller, when the plan moves
// it, goes before every module whose manifest the plan changes, the node-engine excepted; and the node-engine goes
// before the controller by default, when the plan moves both and no edge orders them either way, transitively. An
// edge is "From after To". graph is the module graph, whose edges order too and are never drawn again.
func orderEdges(moved []string, changes map[string]moduleChange, graph []inventory.Edge) planOrder {
var out planOrder
sorted := append([]string(nil), moved...)
sort.Strings(sorted)
for _, server := range sorted {
for _, caller := range sorted {
if server == caller {
continue
}
for _, verb := range changes[server].Serves {
if slices.Contains(changes[caller].Invokes, verb) {
out.Edges = append(out.Edges, inventory.Edge{From: caller, To: server, Kind: edgeChangeOrder,
Why: ruleVerbBeforeCaller + ": " + verb})
}
}
}
}
if slices.Contains(sorted, theController) {
for _, m := range sorted {
if m != theController && m != theEngine && changes[m].Manifest {
out.Edges = append(out.Edges, inventory.Edge{From: m, To: theController, Kind: edgeChangeOrder,
Why: ruleVersionSkew})
}
}
}
if slices.Contains(sorted, theController) && slices.Contains(sorted, theEngine) {
all := orderingEdges(graph, out.Edges)
switch {
case pathBetween(all, theController, theEngine) != nil:
// An edge already puts the controller after the engine: the default says the same.
case pathBetween(all, theEngine, theController) != nil:
// An edge puts the engine after the controller: it overrules the default, and says so.
path := pathBetween(all, theEngine, theController)
for i := range out.Edges {
if out.Edges[i] == path[0] {
out.Edges[i].Why += " (overrules " + ruleEngineFirst + ")"
break
}
}
default:
out.Edges = append(out.Edges, inventory.Edge{From: theController, To: theEngine, Kind: edgeChangeOrder,
Why: ruleEngineFirst})
}
}
all := orderingEdges(graph, out.Edges)
for _, e := range out.Edges {
if back := pathBetween(all, e.To, e.From); back != nil {
out.Cycle = append(out.Cycle, e)
out.Cycle = append(out.Cycle, back...)
break
}
}
return out
}
// orderingEdges is the graph's edges that order and the edges drawn, together.
func orderingEdges(graph, drawn []inventory.Edge) []inventory.Edge {
var out []inventory.Edge
for _, e := range graph {
if orders(e) {
out = append(out, e)
}
}
return append(out, drawn...)
}
// pathBetween is the edges that lead from a module to another along "depends on" (From to To), the shortest, or nil
// when the first does not depend on the second.
func pathBetween(edges []inventory.Edge, from, to string) []inventory.Edge {
if from == to {
return nil
}
via := map[string]inventory.Edge{}
seen := map[string]bool{from: true}
queue := []string{from}
for len(queue) > 0 {
at := queue[0]
queue = queue[1:]
for _, e := range edges {
if e.From != at || seen[e.To] {
continue
}
seen[e.To] = true
via[e.To] = e
if e.To == to {
var path []inventory.Edge
for m := to; m != from; m = via[m].From {
path = append([]inventory.Edge{via[m]}, path...)
}
return path
}
queue = append(queue, e.To)
}
}
return nil
}