216 lines
8.3 KiB
Go
216 lines
8.3 KiB
Go
package inventory
|
|
|
|
import (
|
|
"context"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/novox/mesh-controller/internal/builder"
|
|
"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 read the other's repository. **No longer drawn** (novox/hq ADR 0267
|
|
// rule 4): sharing a repository is no dependency, and a changed file moves every module whose build
|
|
// source holds it, directly. Kept so an edge recorded or snapshotted before is read, and then neither
|
|
// widens nor orders a plan.
|
|
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"
|
|
// EdgeWorkerOf: the module holds the build seat, whose worker the control plane defines
|
|
// (novox/hq issue 206). The one place a *running* order enters the graph: a build machine rolled
|
|
// before the controller that redefines its worker cannot bind it, and nothing can then build the
|
|
// controller that would end that — so the holder of the build seat follows the controller, and
|
|
// the controller is built by whichever build machine is running, as it always was.
|
|
EdgeWorkerOf = "worker-of"
|
|
)
|
|
|
|
// TheControlPlane is the module that defines every seat's worker on the bus.
|
|
const TheControlPlane = "mesh-controller"
|
|
|
|
// 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"`
|
|
// Why is the rule and the fact that drew an edge of a plan's order, in the words the plan shows (novox/hq ADR
|
|
// 0419 clause 5): "verb before caller: <seat>.<verb>". Empty on the catalogue's own edges, which Kind names.
|
|
Why string `json:"why,omitempty"`
|
|
}
|
|
|
|
// 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.
|
|
//
|
|
// Three 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); and the build machine,
|
|
// which every source-built module is built by. The repositories a build read are no edge (novox/hq ADR
|
|
// 0267 rule 4).
|
|
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
|
|
}
|
|
held, err := i.Held(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return dependenciesWith(entries, against, held), nil
|
|
}
|
|
|
|
// packagesHeld is, by package name, the module whose artifact the mesh holds as that package: a
|
|
// `package` artifact's reference is `<name>@<version>` (the builder's publishPackage), never an
|
|
// artifact-store reference, so a build that stood on it recorded the package's name and version.
|
|
func packagesHeld(held map[string]string) map[string]string {
|
|
out := map[string]string{}
|
|
for key, reference := range held {
|
|
module, _, ok := strings.Cut(key, "/")
|
|
if !ok {
|
|
continue
|
|
}
|
|
if name := packageName(reference); name != "" {
|
|
out[name] = module
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// packageName is the name of a `<name>@<version>` package reference, or "" for anything else: a
|
|
// kept or pinned image (`…@sha256:…`), a URL, or a reference with no version.
|
|
func packageName(reference string) string {
|
|
if strings.HasPrefix(reference, catalogue.ArtifactStoreScheme) || strings.Contains(reference, "@sha256:") ||
|
|
strings.Contains(reference, "://") {
|
|
return ""
|
|
}
|
|
at := strings.LastIndex(reference, "@")
|
|
if at <= 0 || at == len(reference)-1 {
|
|
return ""
|
|
}
|
|
return reference[:at]
|
|
}
|
|
|
|
// dependenciesOf is Dependencies over what was read, so a test can hand it a catalogue.
|
|
//
|
|
// `read` draws no edge (novox/hq ADR 0267 rule 4): what a build read moves it through its build source, in
|
|
// the planner, never through the relation.
|
|
func dependenciesOf(entries []Entry, against map[string][]string, _ map[string][]ReadRepository) []Edge {
|
|
return dependenciesWith(entries, against, nil)
|
|
}
|
|
|
|
// dependenciesWith is the relation with the artifacts the mesh holds, "<module>/<artifact>" to its
|
|
// reference (Held), so a package a build stood on is read as its module's.
|
|
//
|
|
// **A built manifest carries no `build`** (catalogue.Resolve drops it), so what a module declared it
|
|
// is built on is read from what is kept of it once it is built (novox/hq issue 614): the bases its
|
|
// newest build was handed (`against`) — an artifact-store reference, or a package's `<name>@<version>`
|
|
// — and the language of every bundle it compiled (`Bundles`). Read only from records the mesh already
|
|
// keeps, so it holds for every module built before it, and a controller rolled back to one without it
|
|
// reads the same records as before.
|
|
func dependenciesWith(entries []Entry, against map[string][]string, held map[string]string) []Edge {
|
|
packages := packagesHeld(held)
|
|
known := map[string]bool{}
|
|
var builders []string
|
|
for _, e := range entries {
|
|
name := e.Manifest.Module
|
|
known[name] = true
|
|
if e.Manifest.ClaimsSeat("node-build-agent") || 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)
|
|
}
|
|
}
|
|
// **A bundle stands on the toolchain it is compiled in** (novox/hq 04-ISSUES/211). A
|
|
// manifest names its toolchain by language, not in `build.on`, so the edge was implicit
|
|
// and a merge that moved the toolchain and a bundle together built both in one tier —
|
|
// the bundle against the toolchain as it was, recorded as built from the new commit. Read
|
|
// from the manifest, so it holds before any build has recorded what it stood on; and a
|
|
// toolchain that moves rebuilds every bundle compiled in it, which is what a toolchain
|
|
// carrying a bundle's dependencies requires.
|
|
for _, a := range e.Manifest.Build.Artifacts {
|
|
if a.Kind != catalogue.ArtifactBundle {
|
|
continue
|
|
}
|
|
if chain, err := builder.ToolchainFor(a.Language); err == nil {
|
|
add(name, chain.Base, EdgeStandsOn)
|
|
}
|
|
}
|
|
}
|
|
// **And once it is built**, from the bundles its resolved manifest keeps (novox/hq issue 614): the
|
|
// `build` it was declared with is gone by then, and a toolchain that moved rebuilt none of them.
|
|
for _, b := range e.Manifest.Bundles {
|
|
if chain, err := builder.ToolchainFor(b.Language); err == nil {
|
|
add(name, chain.Base, EdgeStandsOn)
|
|
}
|
|
}
|
|
for _, ref := range against[name] {
|
|
if rest, ok := strings.CutPrefix(ref, catalogue.ArtifactStoreScheme); ok {
|
|
// A copy of an upstream image is no module's artifact (novox/hq ADR 0257): its
|
|
// repository is named for the image, and its first segment names no module.
|
|
if strings.HasPrefix(rest, builder.MirrorPrefix) {
|
|
continue
|
|
}
|
|
if base, _, found := strings.Cut(rest, "/"); found {
|
|
add(name, base, EdgeStandsOn)
|
|
}
|
|
continue
|
|
}
|
|
// A package base (`build.on` naming a module's `package` artifact) is recorded as the
|
|
// package's name and version (novox/hq issue 614): the module that publishes that package
|
|
// is what it stood on, whichever version the mesh holds now.
|
|
if base, ok := packages[packageName(ref)]; ok {
|
|
add(name, base, EdgeStandsOn)
|
|
}
|
|
}
|
|
if e.Source.Repository != "" {
|
|
for _, b := range builders {
|
|
add(name, b, EdgeBuiltBy)
|
|
}
|
|
}
|
|
}
|
|
if known[TheControlPlane] {
|
|
for _, b := range builders {
|
|
if b != TheControlPlane {
|
|
add(b, TheControlPlane, EdgeWorkerOf)
|
|
}
|
|
}
|
|
}
|
|
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
|
|
}
|