A merge produces a tiered plan the mesh keeps (hq ADR 0162)
A module's dependencies are one relation in the catalogue — stands-on, packages, built-by, declared — answered by one call. A merge takes what moved and everything reachable from it, sorts the set into tiers (a code dependency in the same tier, a build dependency after its base is built, a runtime dependency after the build machine is built and running; the build machine's own base comes first, built by the one that runs), writes the plan to the store, asks the first tier and returns. Every outcome advances the plan; a ticker advances what outcomes cannot; a controller replaced mid-plan resumes it. status lists open plans and names one that has waited too long.
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
package inventory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/catalogue"
|
||||
)
|
||||
|
||||
// The kinds of edge in the catalogue's one dependency relation (novox/hq ADR 0162).
|
||||
const (
|
||||
// EdgeStandsOn: the module's artifact is built on the other's.
|
||||
EdgeStandsOn = "stands-on"
|
||||
// EdgePackages: the module's build reads the other's repository.
|
||||
EdgePackages = "packages"
|
||||
// EdgeBuiltBy: the module is built by the holder of the build-machine seat.
|
||||
EdgeBuiltBy = "built-by"
|
||||
// EdgeDeclared: the manifest's own `build.on`.
|
||||
EdgeDeclared = "declared"
|
||||
)
|
||||
|
||||
// Edge is one dependency: From depends on To, in the way Kind says.
|
||||
type Edge struct {
|
||||
From string `json:"from"`
|
||||
To string `json:"to"`
|
||||
Kind string `json:"kind"`
|
||||
}
|
||||
|
||||
// Dependencies is the catalogue's dependency relation, whole: every module the mesh holds, with
|
||||
// an edge to each module it depends on and the kind of dependency on the edge. One answer, so
|
||||
// nothing else computes an edge (novox/hq ADR 0162) — the merge handler, `build --on` and the
|
||||
// overview all read this.
|
||||
//
|
||||
// Four sources, one relation: a manifest's `build.on`; the artifacts the latest build was made
|
||||
// against (an `artifact-store://<module>/…` reference is an edge to that module); the repositories
|
||||
// the latest build read (an edge to the module whose source that is); and the build machine, which
|
||||
// every source-built module is built by.
|
||||
func (i *Inventory) Dependencies(ctx context.Context) ([]Edge, error) {
|
||||
entries, err := i.Catalogued(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
against, err := i.BuiltAgainst(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
read, err := i.ReadRepositories(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dependenciesOf(entries, against, read), nil
|
||||
}
|
||||
|
||||
// dependenciesOf is Dependencies over what was read, so a test can hand it a catalogue.
|
||||
func dependenciesOf(entries []Entry, against map[string][]string, read map[string][]ReadRepository) []Edge {
|
||||
known := map[string]bool{}
|
||||
byRepository := map[string][]string{}
|
||||
var builders []string
|
||||
for _, e := range entries {
|
||||
name := e.Manifest.Module
|
||||
known[name] = true
|
||||
if r := repositoryKey(e.Source.Repository); r != "" {
|
||||
byRepository[r] = append(byRepository[r], name)
|
||||
}
|
||||
if e.Manifest.ClaimsSeat("mesh-build-machine") {
|
||||
builders = append(builders, name)
|
||||
}
|
||||
}
|
||||
seen := map[Edge]bool{}
|
||||
var out []Edge
|
||||
add := func(from, to, kind string) {
|
||||
if from == to || !known[to] {
|
||||
return
|
||||
}
|
||||
e := Edge{From: from, To: to, Kind: kind}
|
||||
if !seen[e] {
|
||||
seen[e] = true
|
||||
out = append(out, e)
|
||||
}
|
||||
}
|
||||
for _, e := range entries {
|
||||
name := e.Manifest.Module
|
||||
if e.Manifest.Build != nil {
|
||||
for _, on := range e.Manifest.Build.On {
|
||||
if on.Module != "" {
|
||||
add(name, on.Module, EdgeDeclared)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, ref := range against[name] {
|
||||
if rest, ok := strings.CutPrefix(ref, catalogue.ArtifactStoreScheme); ok {
|
||||
if base, _, found := strings.Cut(rest, "/"); found {
|
||||
add(name, base, EdgeStandsOn)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, r := range read[name] {
|
||||
for _, other := range byRepository[repositoryKey(r.Repository)] {
|
||||
add(name, other, EdgePackages)
|
||||
}
|
||||
}
|
||||
if e.Source.Repository != "" {
|
||||
for _, b := range builders {
|
||||
add(name, b, EdgeBuiltBy)
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Slice(out, func(a, b int) bool {
|
||||
if out[a].From != out[b].From {
|
||||
return out[a].From < out[b].From
|
||||
}
|
||||
if out[a].To != out[b].To {
|
||||
return out[a].To < out[b].To
|
||||
}
|
||||
return out[a].Kind < out[b].Kind
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
// repositoryKey is a repository as compared: lower-cased, without a trailing `.git`.
|
||||
func repositoryKey(repository string) string {
|
||||
return strings.ToLower(strings.TrimSuffix(strings.TrimSpace(repository), ".git"))
|
||||
}
|
||||
Reference in New Issue
Block a user