171 lines
5.5 KiB
Go
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
|
|
}
|