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.
125 lines
3.6 KiB
Go
125 lines
3.6 KiB
Go
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"))
|
|
}
|