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"` } // 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:///…` 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 `@` (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 `@` 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, "/" 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 `@` // — 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 }