A build records the bases it was handed, and the mesh reads its edges from builds #113

Merged
mesh-admin merged 1 commits from feat/build-edges-are-recorded into main 2026-09-28 01:51:17 +00:00
6 changed files with 198 additions and 70 deletions
+19 -13
View File
@@ -42,23 +42,21 @@ func buildOn(ctx context.Context, base string, wait time.Duration) error {
if err != nil { if err != nil {
return err return err
} }
against, err := open.inventory.BuiltAgainst(ctx)
if err != nil {
return err
}
var on []inventory.Entry var on []inventory.Entry
for _, e := range held { for _, e := range held {
if e.Manifest.Build == nil { if standsOnModule(e, base, against) {
continue on = append(on, e)
}
for _, b := range e.Manifest.Build.On {
if standsOnModule(b, base) {
on = append(on, e)
break
}
} }
} }
if len(on) == 0 { if len(on) == 0 {
fmt.Printf("nothing the mesh holds stands on %s\n", base) fmt.Printf("nothing the mesh holds stands on %s\n", base)
return nil return nil
} }
on = orderByBases(on) on = orderByBases(on, against)
fmt.Printf("%d module(s) stand on %s:\n", len(on), base) fmt.Printf("%d module(s) stand on %s:\n", len(on), base)
var failed []string var failed []string
for _, e := range on { for _, e := range on {
@@ -134,8 +132,9 @@ func buildCommand(ctx context.Context, args []string) error {
// //
// **By digest and path, never by where it was pushed** (novox/hq 04-ISSUES/102). The builder // **By digest and path, never by where it was pushed** (novox/hq 04-ISSUES/102). The builder
// says `<registry>:<port>/<module>/<artifact>@sha256:…`; the mesh records the artifact-store // says `<registry>:<port>/<module>/<artifact>@sha256:…`; the mesh records the artifact-store
// reference and composes the store's address back in where a reference is used. `against` is kept // reference and composes the store's address back in where a reference is used. `against` — what
// as announced: it is what the build stood on as the builder saw it, and the catalogue's edge. // the build stood on, the catalogue's edge — is recorded the same way, so an edge names a module's
// artifact and not the machine it was pulled from.
func buildFrom(result link.BuildResult) inventory.Build { func buildFrom(result link.BuildResult) inventory.Build {
kept := inventory.Build{ kept := inventory.Build{
ID: result.ID, Repository: result.Repository, Ref: result.Ref, ID: result.ID, Repository: result.Repository, Ref: result.Ref,
@@ -145,7 +144,10 @@ func buildFrom(result link.BuildResult) inventory.Build {
// edges, and it is not always listening when a build happens — on a fresh mesh it cannot // edges, and it is not always listening when a build happens — on a fresh mesh it cannot
// be, for exactly the modules it needs most. Keeping them is what makes a replay able to // be, for exactly the modules it needs most. Keeping them is what makes a replay able to
// rebuild the graph rather than a list of names. // rebuild the graph rather than a list of names.
Path: result.Path, Against: result.Against, Path: result.Path,
}
for _, ref := range result.Against {
kept.Against = append(kept.Against, catalogue.Recorded(ref))
} }
var announced []inventory.Artifact var announced []inventory.Artifact
for _, made := range result.Made { for _, made := range result.Made {
@@ -344,7 +346,11 @@ func buildBehind(ctx context.Context, wait time.Duration) error {
// Bases first: a module built before the module it stands on is built against the old one // Bases first: a module built before the module it stands on is built against the old one
// and reports success (novox/hq 04-ISSUES/131). // and reports success (novox/hq 04-ISSUES/131).
stale = orderByBases(stale) against, err := inv.BuiltAgainst(ctx)
if err != nil {
return err
}
stale = orderByBases(stale, against)
var failed []string var failed []string
for _, e := range stale { for _, e := range stale {
+37 -8
View File
@@ -1,6 +1,7 @@
package main package main
import ( import (
"strings"
"testing" "testing"
"github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/catalogue"
@@ -8,19 +9,28 @@ import (
"github.com/novox/mesh-controller/internal/link" "github.com/novox/mesh-controller/internal/link"
) )
func entry(module string, on ...string) inventory.Entry { // entry is a module as the catalogue holds it: built, so its manifest carries no `build` any more.
b := &catalogue.Build{} func entry(module string, _ ...string) inventory.Entry {
for _, o := range on { return inventory.Entry{Manifest: catalogue.Manifest{Module: module}}
b.On = append(b.On, catalogue.BuildsOn{Arg: "X", Module: o, Artifact: "runtime"}) }
// stoodOn is what each module's newest build recorded it was handed.
func stoodOn(edges map[string][]string) map[string][]string {
out := map[string][]string{}
for module, bases := range edges {
for _, b := range bases {
out[module] = append(out[module], catalogue.ArtifactStoreScheme+b+"/runtime@sha256:"+strings.Repeat("0", 64))
}
} }
return inventory.Entry{Manifest: catalogue.Manifest{Module: module, Build: b}} return out
} }
// A module built before the module it stands on is built against the old one and reports success // A module built before the module it stands on is built against the old one and reports success
// (novox/hq 04-ISSUES/131). So bases come first, however the set arrived. // (novox/hq 04-ISSUES/131). So bases come first, however the set arrived.
func TestBasesAreBuiltBeforeWhatStandsOnThem(t *testing.T) { func TestBasesAreBuiltBeforeWhatStandsOnThem(t *testing.T) {
in := []inventory.Entry{entry("app", "runtime"), entry("runtime", "base"), entry("other"), entry("base")} in := []inventory.Entry{entry("app"), entry("runtime"), entry("other"), entry("base")}
got := orderByBases(in) edges := stoodOn(map[string][]string{"app": {"runtime"}, "runtime": {"base"}})
got := orderByBases(in, edges)
pos := map[string]int{} pos := map[string]int{}
for i, e := range got { for i, e := range got {
pos[e.Manifest.Module] = i pos[e.Manifest.Module] = i
@@ -32,12 +42,31 @@ func TestBasesAreBuiltBeforeWhatStandsOnThem(t *testing.T) {
t.Fatalf("an entry was lost or doubled: %d", len(got)) t.Fatalf("an entry was lost or doubled: %d", len(got))
} }
// A base outside the set is not waited for: it is not being rebuilt. // A base outside the set is not waited for: it is not being rebuilt.
got = orderByBases([]inventory.Entry{entry("app", "elsewhere")}) got = orderByBases([]inventory.Entry{entry("app")}, stoodOn(map[string][]string{"app": {"elsewhere"}}))
if len(got) != 1 { if len(got) != 1 {
t.Fatalf("a dependency outside the set changed the set: %v", got) t.Fatalf("a dependency outside the set changed the set: %v", got)
} }
} }
// A module registered from its manifest and never built still names its bases there; once built,
// the recorded edge is what says so. Both are read, and a module never stands on itself.
func TestWhatStandsOnAModuleIsReadFromItsBuildOrItsManifest(t *testing.T) {
built := entry("gitea")
edges := stoodOn(map[string][]string{"gitea": {"mesh-tools"}})
if !standsOnModule(built, "mesh-tools", edges) {
t.Fatal("a recorded edge was not read")
}
if standsOnModule(built, "gitea", edges) || standsOnModule(built, "postgres", edges) {
t.Fatal("an edge was invented")
}
fresh := inventory.Entry{Manifest: catalogue.Manifest{Module: "plex", Build: &catalogue.Build{
On: []catalogue.BuildsOn{{Arg: "RUNTIME_BASE", Module: "mesh-tools", Artifact: "runtime"}},
}}}
if !standsOnModule(fresh, "mesh-tools", nil) {
t.Fatal("a manifest's own base was not read")
}
}
// A merge names a repository the way the forge does; a source is recorded the way a build was // A merge names a repository the way the forge does; a source is recorded the way a build was
// asked for. The two meet on owner/repo and branch, whichever form the record took. // asked for. The two meet on owner/repo and branch, whichever form the record took.
func TestAMergeMatchesTheSourcesBuiltFromIt(t *testing.T) { func TestAMergeMatchesTheSourcesBuiltFromIt(t *testing.T) {
+39 -28
View File
@@ -250,7 +250,11 @@ func (f following) SourceMoved(ctx context.Context, m link.SourceMoved) error {
m.Owner, m.Repo, m.Base, m.Commit) m.Owner, m.Repo, m.Base, m.Commit)
return nil return nil
} }
ordered := orderByBases(moved) against, err := inv.BuiltAgainst(ctx)
if err != nil {
return notNow(err)
}
ordered := orderByBases(moved, against)
names := make([]string, 0, len(ordered)) names := make([]string, 0, len(ordered))
for _, e := range ordered { for _, e := range ordered {
names = append(names, e.Manifest.Module) names = append(names, e.Manifest.Module)
@@ -265,7 +269,7 @@ func (f following) SourceMoved(ctx context.Context, m link.SourceMoved) error {
failed = append(failed, e.Manifest.Module) failed = append(failed, e.Manifest.Module)
// A base that failed is a reason to stop: what stands on it would be built against // A base that failed is a reason to stop: what stands on it would be built against
// the old one, and report success (novox/hq 04-ISSUES/131). // the old one, and report success (novox/hq 04-ISSUES/131).
if standsOn(ordered, e.Manifest.Module) { if standsOn(ordered, e.Manifest.Module, against) {
fmt.Printf(" stopping: %s is a base of what was still to build\n", e.Manifest.Module) fmt.Printf(" stopping: %s is a base of what was still to build\n", e.Manifest.Module)
break break
} }
@@ -292,10 +296,13 @@ func sourceIs(s inventory.Source, m link.SourceMoved) bool {
return s.Ref == "" || s.Ref == m.Base return s.Ref == "" || s.Ref == m.Base
} }
// orderByBases is the entries with every base before what stands on it: a module whose build names // orderByBases is the entries with every base before what stands on it: a module whose build stood
// another's artifact under build.on comes after that module. Entries outside the set are not // on another's artifact comes after that module. Entries outside the set are not waited for — they
// waited for — they are not being rebuilt. Stable for what has no order between it. // are not being rebuilt. Stable for what has no order between it.
func orderByBases(entries []inventory.Entry) []inventory.Entry { //
// `against` is what each module's newest build stood on (inventory.BuiltAgainst): the edges are
// derived from builds, not declared, because a recorded manifest no longer carries `build.on`.
func orderByBases(entries []inventory.Entry, against map[string][]string) []inventory.Entry {
inSet := map[string]bool{} inSet := map[string]bool{}
for _, e := range entries { for _, e := range entries {
inSet[e.Manifest.Module] = true inSet[e.Manifest.Module] = true
@@ -309,13 +316,9 @@ func orderByBases(entries []inventory.Entry) []inventory.Entry {
return return
} }
seen[name] = true seen[name] = true
if e.Manifest.Build != nil { for _, base := range entries {
for _, on := range e.Manifest.Build.On { if base.Manifest.Module != name && inSet[base.Manifest.Module] && standsOnModule(e, base.Manifest.Module, against) {
for _, base := range entries { place(base, seen)
if base.Manifest.Module != name && inSet[base.Manifest.Module] && standsOnModule(on, base.Manifest.Module) {
place(base, seen)
}
}
} }
} }
placed[name] = true placed[name] = true
@@ -328,26 +331,34 @@ func orderByBases(entries []inventory.Entry) []inventory.Entry {
} }
// standsOn is whether anything in the set is built on the named module's artifacts. // standsOn is whether anything in the set is built on the named module's artifacts.
func standsOn(entries []inventory.Entry, module string) bool { func standsOn(entries []inventory.Entry, module string, against map[string][]string) bool {
for _, e := range entries { for _, e := range entries {
if e.Manifest.Build == nil { if standsOnModule(e, module, against) {
continue return true
}
for _, on := range e.Manifest.Build.On {
if standsOnModule(on, module) {
return true
}
} }
} }
return false return false
} }
// standsOnModule is whether a base names the module: as written in a manifest (`module`), or as // standsOnModule is whether an entry's build stood on the named module: by what its newest build
// recorded after a build, when the mesh has replaced it with the artifact it resolved to // recorded it was handed (`artifact-store://<module>/<artifact>@…`, the module's own artifact), or
// (`artifact-store://<module>/<artifact>@…`). A recorded manifest is what the catalogue holds. // — for a module registered from a manifest and not yet built — by the base its manifest names.
func standsOnModule(on catalogue.BuildsOn, module string) bool { func standsOnModule(e inventory.Entry, module string, against map[string][]string) bool {
if on.Module == module { if e.Manifest.Module == module {
return true return false
} }
return strings.HasPrefix(on.Image, "artifact-store://"+module+"/") if e.Manifest.Build != nil {
for _, on := range e.Manifest.Build.On {
if on.Module == module {
return true
}
}
}
prefix := catalogue.ArtifactStoreScheme + module + "/"
for _, ref := range against[e.Manifest.Module] {
if strings.HasPrefix(ref, prefix) {
return true
}
}
return false
} }
+34 -15
View File
@@ -169,6 +169,8 @@ func Build(ctx context.Context, run Runner, publish Publisher,
} }
var built []catalogue.Built var built []catalogue.Built
// stoodOn is every base the build was handed, as resolved — the edges the catalogue derives.
var stoodOn []string
if manifest.Build != nil { if manifest.Build != nil {
// What this module said it stands on, answered with what this mesh actually holds. Done // What this module said it stands on, answered with what this mesh actually holds. Done
// before anything is built, so a missing base is refused in front of the person who can // before anything is built, so a missing base is refused in front of the person who can
@@ -186,11 +188,12 @@ func Build(ctx context.Context, run Runner, publish Publisher,
} }
return from, nil return from, nil
} }
args, err := standingOn(ctx, manifest, held, mirror) args, bases, err := standingOn(ctx, manifest, held, mirror)
if err != nil { if err != nil {
say("bases", "UNMET: %v", err) say("bases", "UNMET: %v", err)
return Result{}, err return Result{}, err
} }
stoodOn = bases
if len(args) > 0 { if len(args) > 0 {
say("bases", "%d resolved from what the mesh holds", len(args)/2) say("bases", "%d resolved from what the mesh holds", len(args)/2)
} }
@@ -217,7 +220,7 @@ func Build(ctx context.Context, run Runner, publish Publisher,
} }
say("done", "%s at %s — %d artifact(s) pinned", manifest.Module, short(commit), len(built)) say("done", "%s at %s — %d artifact(s) pinned", manifest.Module, short(commit), len(built))
return Result{Manifest: resolved, Commit: commit, Built: built, return Result{Manifest: resolved, Commit: commit, Built: built,
Against: against(within, manifest)}, nil Against: against(within, manifest, stoodOn)}, nil
} }
// Log is where a build says what it is doing, step by step. Nil is silent — the tests pass none, // Log is where a build says what it is doing, step by step. Nil is silent — the tests pass none,
@@ -339,14 +342,27 @@ func describe(path string) string {
// allowed to name — a tag is something somebody else can move under you. // allowed to name — a tag is something somebody else can move under you.
var pinnedImage = regexp.MustCompile(`[A-Za-z0-9][A-Za-z0-9._/:-]*@sha256:[0-9a-f]{64}`) var pinnedImage = regexp.MustCompile(`[A-Za-z0-9][A-Za-z0-9._/:-]*@sha256:[0-9a-f]{64}`)
// against reads what this module's image artifacts are built on top of, out of the files that // against is what this module's image artifacts are built on top of: every base the mesh resolved
// build them. Nothing is guessed: a reference that is not written down is not reported. // and handed the recipe as a build argument (`build.on`), and any image a recipe pins by digest
func against(within string, manifest catalogue.Manifest) []string { // itself. Nothing is guessed: a reference that was neither resolved nor written down is not
// reported.
//
// **The resolved bases are the edges.** A recipe reads its base from an argument (`FROM
// ${RUNTIME_BASE}`), so the digest is never in the file, and a derivation that read files alone
// recorded no edge for any module on the mesh — which is why nothing knew what a changed base
// meant to rebuild (novox/hq 04-ISSUES/131).
func against(within string, manifest catalogue.Manifest, resolved []string) []string {
if manifest.Build == nil { if manifest.Build == nil {
return nil return nil
} }
seen := map[string]bool{} seen := map[string]bool{}
var out []string var out []string
for _, r := range resolved {
if r != "" && !seen[r] {
seen[r] = true
out = append(out, r)
}
}
for _, a := range manifest.Build.Artifacts { for _, a := range manifest.Build.Artifacts {
if a.Kind != catalogue.ArtifactImage || a.From == "" { if a.Kind != catalogue.ArtifactImage || a.From == "" {
continue continue
@@ -704,40 +720,42 @@ var _ io.Writer = (*stringWriter)(nil)
// built cannot be built here yet, and the useful sentence names which module is missing — not the // built cannot be built here yet, and the useful sentence names which module is missing — not the
// one a container runtime produces when a recipe's first line refers to an image nobody has. // one a container runtime produces when a recipe's first line refers to an image nobody has.
// //
// The order is fixed so two builds of one commit invoke the same command. // The order is fixed so two builds of one commit invoke the same command. Returned alongside the
// arguments is every reference they resolved to, which is what the build stood on.
func standingOn(ctx context.Context, manifest catalogue.Manifest, held map[string]string, func standingOn(ctx context.Context, manifest catalogue.Manifest, held map[string]string,
mirror func(ctx context.Context, from, repository string) (string, error)) ([]string, error) { mirror func(ctx context.Context, from, repository string) (string, error)) ([]string, []string, error) {
if manifest.Build == nil || len(manifest.Build.On) == 0 { if manifest.Build == nil || len(manifest.Build.On) == 0 {
return nil, nil return nil, nil, nil
} }
on := append([]catalogue.BuildsOn{}, manifest.Build.On...) on := append([]catalogue.BuildsOn{}, manifest.Build.On...)
sort.Slice(on, func(i, j int) bool { return on[i].Arg < on[j].Arg }) sort.Slice(on, func(i, j int) bool { return on[i].Arg < on[j].Arg })
var args []string var args, resolved []string
for _, base := range on { for _, base := range on {
if base.Image != "" { if base.Image != "" {
// A vendor's image, declared (novox/hq 04-ISSUES/064, ADR 0097). Pinned, because a tag // A vendor's image, declared (novox/hq 04-ISSUES/064, ADR 0097). Pinned, because a tag
// is what somebody else can move; copied into the mesh's registry, because a build // is what somebody else can move; copied into the mesh's registry, because a build
// that reaches a public registry on its own is a build that works sometimes. // that reaches a public registry on its own is a build that works sometimes.
if base.Arg == "" || base.Module != "" || base.Artifact != "" { if base.Arg == "" || base.Module != "" || base.Artifact != "" {
return nil, fmt.Errorf( return nil, nil, fmt.Errorf(
"%s stands on the image %s, and a base is either a module's artifact or an "+ "%s stands on the image %s, and a base is either a module's artifact or an "+
"image — never both — read from one build argument", manifest.Module, base.Image) "image — never both — read from one build argument", manifest.Module, base.Image)
} }
if !strings.Contains(base.Image, "@sha256:") { if !strings.Contains(base.Image, "@sha256:") {
return nil, fmt.Errorf( return nil, nil, fmt.Errorf(
"%s stands on the image %q, which is not pinned by digest. A tag is what "+ "%s stands on the image %q, which is not pinned by digest. A tag is what "+
"somebody else can move; name it as <image>@sha256:…", manifest.Module, base.Image) "somebody else can move; name it as <image>@sha256:…", manifest.Module, base.Image)
} }
reference, err := mirror(ctx, base.Image, manifest.Module+"/on-"+strings.ToLower(base.Arg)) reference, err := mirror(ctx, base.Image, manifest.Module+"/on-"+strings.ToLower(base.Arg))
if err != nil { if err != nil {
return nil, fmt.Errorf("%s stands on %s: %w", manifest.Module, base.Image, err) return nil, nil, fmt.Errorf("%s stands on %s: %w", manifest.Module, base.Image, err)
} }
args = append(args, "--build-arg", base.Arg+"="+reference) args = append(args, "--build-arg", base.Arg+"="+reference)
resolved = append(resolved, reference)
continue continue
} }
if base.Arg == "" || base.Module == "" || base.Artifact == "" { if base.Arg == "" || base.Module == "" || base.Artifact == "" {
return nil, fmt.Errorf( return nil, nil, fmt.Errorf(
"%s says its build stands on something, and does not say all of what: a base "+ "%s says its build stands on something, and does not say all of what: a base "+
"needs the module, the artifact, and the build argument the recipe reads it "+ "needs the module, the artifact, and the build argument the recipe reads it "+
"from", manifest.Module) "from", manifest.Module)
@@ -745,14 +763,15 @@ func standingOn(ctx context.Context, manifest catalogue.Manifest, held map[strin
key := base.Module + "/" + base.Artifact key := base.Module + "/" + base.Artifact
reference, has := held[key] reference, has := held[key]
if !has { if !has {
return nil, fmt.Errorf( return nil, nil, fmt.Errorf(
"%s builds on %s, and this mesh has not built it. Build %s first — every module "+ "%s builds on %s, and this mesh has not built it. Build %s first — every module "+
"in this toolchain stands on it, so it is the thing to have before anything "+ "in this toolchain stands on it, so it is the thing to have before anything "+
"else", manifest.Module, key, base.Module) "else", manifest.Module, key, base.Module)
} }
args = append(args, "--build-arg", base.Arg+"="+reference) args = append(args, "--build-arg", base.Arg+"="+reference)
resolved = append(resolved, reference)
} }
return args, nil return args, resolved, nil
} }
// compile runs a module's own code through its toolchain, and says where the result is. // compile runs a module's own code through its toolchain, and says where the result is.
+34 -6
View File
@@ -22,7 +22,7 @@ func TestABaseTheMeshHasNotBuiltIsRefused(t *testing.T) {
On: []catalogue.BuildsOn{{Arg: "RUNTIME_BASE", Module: "mesh-tools", Artifact: "runtime"}}, On: []catalogue.BuildsOn{{Arg: "RUNTIME_BASE", Module: "mesh-tools", Artifact: "runtime"}},
}, },
} }
_, err := standingOn(context.Background(), manifest, map[string]string{}, noMirror) _, _, err := standingOn(context.Background(), manifest, map[string]string{}, noMirror)
if err == nil { if err == nil {
t.Fatal("a base nothing has built was accepted; the build would have failed on its first line") t.Fatal("a base nothing has built was accepted; the build would have failed on its first line")
} }
@@ -42,7 +42,7 @@ func TestABaseTheMeshHoldsBecomesABuildArgument(t *testing.T) {
}, },
} }
held := map[string]string{"mesh-tools/runtime": "127.0.0.1:5000/mesh-tools/runtime@sha256:" + strings.Repeat("a", 64)} held := map[string]string{"mesh-tools/runtime": "127.0.0.1:5000/mesh-tools/runtime@sha256:" + strings.Repeat("a", 64)}
args, err := standingOn(context.Background(), manifest, held, noMirror) args, _, err := standingOn(context.Background(), manifest, held, noMirror)
if err != nil { if err != nil {
t.Fatalf("a base this mesh holds was refused: %v", err) t.Fatalf("a base this mesh holds was refused: %v", err)
} }
@@ -54,7 +54,7 @@ func TestABaseTheMeshHoldsBecomesABuildArgument(t *testing.T) {
// A module naming no base asks for nothing, which is most modules. // A module naming no base asks for nothing, which is most modules.
func TestAModuleNamingNoBaseAddsNoArguments(t *testing.T) { func TestAModuleNamingNoBaseAddsNoArguments(t *testing.T) {
args, err := standingOn(context.Background(), catalogue.Manifest{Module: "hello-web", Build: &catalogue.Build{}}, nil, noMirror) args, _, err := standingOn(context.Background(), catalogue.Manifest{Module: "hello-web", Build: &catalogue.Build{}}, nil, noMirror)
if err != nil || args != nil { if err != nil || args != nil {
t.Fatalf("a module naming no base produced %v, %v", args, err) t.Fatalf("a module naming no base produced %v, %v", args, err)
} }
@@ -66,7 +66,7 @@ func TestAnIncompleteBaseIsRefused(t *testing.T) {
Module: "postgres", Module: "postgres",
Build: &catalogue.Build{On: []catalogue.BuildsOn{{Module: "mesh-tools", Artifact: "runtime"}}}, Build: &catalogue.Build{On: []catalogue.BuildsOn{{Module: "mesh-tools", Artifact: "runtime"}}},
} }
if _, err := standingOn(context.Background(), manifest, map[string]string{"mesh-tools/runtime": "x"}, noMirror); err == nil { if _, _, err := standingOn(context.Background(), manifest, map[string]string{"mesh-tools/runtime": "x"}, noMirror); err == nil {
t.Fatal("a base with no build argument was accepted; nothing would have read it") t.Fatal("a base with no build argument was accepted; nothing would have read it")
} }
} }
@@ -86,7 +86,7 @@ func TestADeclaredVendorImageIsCopiedInAndHandedToTheRecipe(t *testing.T) {
}, },
} }
var asked []string var asked []string
args, err := standingOn(context.Background(), manifest, nil, func(_ context.Context, from, repository string) (string, error) { args, _, err := standingOn(context.Background(), manifest, nil, func(_ context.Context, from, repository string) (string, error) {
asked = append(asked, from+" -> "+repository) asked = append(asked, from+" -> "+repository)
return "127.0.0.1:5000/" + repository + "@sha256:" + strings.Repeat("d", 64), nil return "127.0.0.1:5000/" + repository + "@sha256:" + strings.Repeat("d", 64), nil
}) })
@@ -101,7 +101,7 @@ func TestADeclaredVendorImageIsCopiedInAndHandedToTheRecipe(t *testing.T) {
} }
// Unpinned, it is refused: a tag is what somebody else can move. // Unpinned, it is refused: a tag is what somebody else can move.
manifest.Build.On[0].Image = "quay.io/minio/mc:latest" manifest.Build.On[0].Image = "quay.io/minio/mc:latest"
if _, err := standingOn(context.Background(), manifest, nil, noMirror); err == nil || !strings.Contains(err.Error(), "not pinned") { if _, _, err := standingOn(context.Background(), manifest, nil, noMirror); err == nil || !strings.Contains(err.Error(), "not pinned") {
t.Fatalf("an unpinned vendor image was accepted: %v", err) t.Fatalf("an unpinned vendor image was accepted: %v", err)
} }
} }
@@ -151,3 +151,31 @@ func TestARecipeIsReadAsInstructions(t *testing.T) {
t.Fatalf("a heredoc line or a continued stage was read as a base: %v", bases) t.Fatalf("a heredoc line or a continued stage was read as a base: %v", bases)
} }
} }
// What a build was handed as its bases is what it stood on — recorded, so a changed base knows what
// to rebuild (novox/hq 04-ISSUES/131). A recipe reads the base from an argument, so nothing else
// could know.
func TestTheBasesABuildWasHandedAreWhatItStoodOn(t *testing.T) {
manifest := catalogue.Manifest{
Module: "gitea",
Build: &catalogue.Build{
On: []catalogue.BuildsOn{
{Arg: "RUNTIME_BASE", Module: "mesh-tools", Artifact: "runtime"},
{Arg: "BUILD_BASE", Module: "mesh-tools", Artifact: "build"},
},
Artifacts: []catalogue.Artifact{{Name: "runtime", Kind: catalogue.ArtifactImage, From: "Dockerfile"}},
},
}
held := map[string]string{
"mesh-tools/runtime": "127.0.0.1:5000/mesh-tools/runtime@sha256:" + strings.Repeat("a", 64),
"mesh-tools/build": "127.0.0.1:5000/mesh-tools/build@sha256:" + strings.Repeat("b", 64),
}
_, resolved, err := standingOn(context.Background(), manifest, held, noMirror)
if err != nil {
t.Fatal(err)
}
got := against(t.TempDir(), manifest, resolved)
if len(got) != 2 || got[0] != held["mesh-tools/build"] || got[1] != held["mesh-tools/runtime"] {
t.Fatalf("the bases the build was handed were not what it stood on: %v", got)
}
}
+35
View File
@@ -164,6 +164,41 @@ func (i *Inventory) Held(ctx context.Context) (map[string]string, error) {
return held, rows.Err() return held, rows.Err()
} }
// BuiltAgainst is what each module's newest successful build stood on, as recorded — the build
// edges (ADR 0009). A module whose last build recorded no bases is absent, which is also what a
// module standing on nothing looks like: an edge the mesh has not derived is not an edge.
func (i *Inventory) BuiltAgainst(ctx context.Context) (map[string][]string, error) {
rows, err := i.store.Pool().Query(ctx,
`select distinct on (module) module, built_against
from build
where module is not null and module <> '' and failed = ''
order by module, at desc`)
if err != nil {
return nil, err
}
defer rows.Close()
against := map[string][]string{}
for rows.Next() {
var module string
var raw []byte
if err := rows.Scan(&module, &raw); err != nil {
return nil, err
}
if len(raw) == 0 {
continue
}
var refs []string
if err := json.Unmarshal(raw, &refs); err != nil {
continue
}
if len(refs) > 0 {
against[module] = refs
}
}
return against, rows.Err()
}
// manifestOrNil keeps the difference between "declared nothing" and "predates this being kept". // manifestOrNil keeps the difference between "declared nothing" and "predates this being kept".
// //
// A build recorded before the mesh kept manifests has no manifest, and that is not the same as one // A build recorded before the mesh kept manifests has no manifest, and that is not the same as one