From 9713ceb84d0aa8711335716afad35954723ac92e Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 22 Oct 2026 21:06:33 +0200 Subject: [PATCH] Ask build-agent for itself and its toolchain images, never a builder key (ADR 0520, issue 1851) The builder writes no key of build-agent or of the toolchain bases its builds stand on, so a walk moving one would hold its tier for ever. Until R4g they are build-agent's own, as the builder's module is; the bases are read from the toolchains, not copied. --- cmd/mesh-controller/builders_own_test.go | 45 ++++++++++++++++++++++++ cmd/mesh-controller/plan_keys.go | 23 +++++++++--- internal/builder/toolchain.go | 18 ++++++++++ 3 files changed, 81 insertions(+), 5 deletions(-) diff --git a/cmd/mesh-controller/builders_own_test.go b/cmd/mesh-controller/builders_own_test.go index 3c2d717b..05b7589c 100644 --- a/cmd/mesh-controller/builders_own_test.go +++ b/cmd/mesh-controller/builders_own_test.go @@ -59,3 +59,48 @@ func TestAWalkAsksBuildAgentForTheBuildersOwnModuleAlone(t *testing.T) { t.Fatalf("the other module is not taken from its key: %+v", s) } } + +// ADR 0520 (issue 1851): until R4g build-agent and the toolchain images its builds stand on are build-agent's own, as the +// builder's module is: the builder writes no key of them, so a walk asks build-agent for them and reads no key; any other +// module of the same tier is still taken from its key. +func TestAWalkAsksBuildAgentForItselfAndItsToolchainsAndReadsNoKeyOfThem(t *testing.T) { + own := []string{"build-agent", "mesh-tools", "mesh-tools-go", "mesh-tools-python"} + wasModule := askedModuleOf + askedModuleOf = func(_ context.Context, _ buildSource, path string) (string, error) { + return strings.TrimPrefix(path, "modules/"), nil + } + t.Cleanup(func() { askedModuleOf = wasModule }) + for _, m := range own { + if err := onlyTheBuildersOwn(t.Context(), buildSource{Repository: "novox/x", Seat: "git"}, "modules/"+m); err != nil { + t.Fatalf("an ask of %s is refused: %v", m, err) + } + } + var asked []string + was := askABuild + askABuild = func(_ context.Context, _ buildSource, path, _ string) (string, error) { + asked = append(asked, path) + return "build-" + strings.TrimPrefix(path, "modules/"), nil + } + t.Cleanup(func() { askABuild = was }) + p, byName := aWalkOfOne("sudo") + p.Tiers = [][]string{append(append([]string{}, own...), "sudo")} + for _, m := range own { + e := byName["sudo"] + e.Source.Path = "modules/" + m + byName[m] = e + } + for _, m := range p.Tiers[0] { + askModule(t.Context(), p, m, byName) + } + if strings.Join(asked, " ") != "modules/build-agent modules/mesh-tools modules/mesh-tools-go modules/mesh-tools-python" { + t.Fatalf("build-agent was asked for %v", asked) + } + for _, m := range own { + if s := p.Modules[m]; s.Key || s.Build != "build-"+m || s.State != "asked" { + t.Fatalf("%s is not asked of build-agent, or is read from a key: %+v", m, s) + } + } + if s := p.Modules["sudo"]; !s.Key || s.Build != "" { + t.Fatalf("the other module is not taken from its key: %+v", s) + } +} diff --git a/cmd/mesh-controller/plan_keys.go b/cmd/mesh-controller/plan_keys.go index c3677e6e..cf8f6a30 100644 --- a/cmd/mesh-controller/plan_keys.go +++ b/cmd/mesh-controller/plan_keys.go @@ -8,6 +8,8 @@ import ( "time" sdkdelivery "git.novox.be/novox/mesh-sdk/go/delivery" + + "github.com/novox/mesh-controller/internal/builder" "github.com/novox/mesh-controller/internal/builderkeys" "github.com/novox/mesh-controller/internal/inventory" ) @@ -22,13 +24,24 @@ import ( // yet, or waiting for a reference, holds the tier and says so; a failed key fails the walk in the builder's own words. // **No timer**: the builder's and the indexer's writes wake the look (builderkeys.Watch, watchNewModules). -// isBuildersOwn says a module is the builder's own: the one module build-agent still builds, asked on a merge, until R4g -// (ADR 0453 §5, ADR 0498 §7). +// isBuildersOwn says a module is build-agent's own: asked of build-agent on a merge and taken from its build record, never +// from the builder's key, until R4g (ADR 0453 §5, ADR 0498 §7, ADR 0520). They are the builder's module, build-agent itself +// (its image's context is another repository's) and the toolchain images build-agent's builds stand on +// (builder.Bases): the builder writes no key of any of them, and a walk waiting for one would hold its tier for ever. func isBuildersOwn(module string) bool { return buildersOwn(module) } -// buildersOwn is isBuildersOwn's rule: a variable only so a test of the one ask left (its bus waits, its plan key) may -// name modules of its own as the builder's. -var buildersOwn = func(module string) bool { return module == sdkdelivery.BuilderModule } +// buildAgentsOwn are those modules: the builder's, build-agent's and every toolchain's base, read from the toolchains. +var buildAgentsOwn = func() map[string]bool { + own := map[string]bool{sdkdelivery.BuilderModule: true, builder.AgentModule: true} + for _, b := range builder.Bases() { + own[b] = true + } + return own +}() + +// buildersOwn is isBuildersOwn's rule: a variable only so a test of the asks left (their bus waits, their plan key) may +// name modules of its own as build-agent's. +var buildersOwn = func(module string) bool { return buildAgentsOwn[module] } // planKeyReads is what a walk reads of the indexer and the builder (builderkeys.Reader, or a test's). type planKeyReads interface { diff --git a/internal/builder/toolchain.go b/internal/builder/toolchain.go index f5a304da..1cb0a381 100644 --- a/internal/builder/toolchain.go +++ b/internal/builder/toolchain.go @@ -234,6 +234,24 @@ func spoken() string { } // Languages is every language the mesh can build, for anything that wants to say so. +// AgentModule is build-agent's own module: the machine's builder, whose image is built from another repository's context. +const AgentModule = "build-agent" + +// Bases are the modules whose artifacts provide the toolchains' compilers (Toolchain.Base), each once, sorted: images +// build-agent's builds stand on. +func Bases() []string { + seen := map[string]bool{} + var out []string + for _, t := range toolchains { + if t.Base != "" && !seen[t.Base] { + seen[t.Base] = true + out = append(out, t.Base) + } + } + sort.Strings(out) + return out +} + func Languages() []string { names := make([]string, 0, len(toolchains)) for _, t := range toolchains {