From d3781fd840d6a720e9e04085790b0530f353ccd2 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 11 Nov 2026 14:35:53 +0100 Subject: [PATCH] W12.5: the three build modules answer kept-artifact with the domain SDK's judgement (issue 1966) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Design 47 lines 1888-1906 at b229f5c4 (test 60): the verb sits in every build seat's definition. Its judgement (what may be kept, the kept builds a node holds, the three refusals) is shared code of one domain's modules, so it is the domain SDK's (sdk/buildstep KeptArtifact, ADR 0634 §2): mesh-build-go keeps its store, sweep and tools and drops its own copy of the judgement and its table; mesh-build-node and mesh-build-image keep no build and answer over none, so a module claiming no oci-artifact-registry is refused and the holder is answered not kept. Their manifests serve the verb and invoke seat:mesh-catalog.module, which the judgement asks. Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ (Joined from 42d364fe.) Builds: design 47 G §271, §464 (BLD9) Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ --- .../cmd/mesh-build-go/archive.go | 5 +- .../mesh-build-go/cmd/mesh-build-go/build.go | 5 +- .../cmd/mesh-build-go/keptbuilds.go | 110 +++--------------- .../cmd/mesh-build-go/keptbuilds_test.go | 41 +++---- .../cmd/mesh-build-image/asks.go | 2 +- .../cmd/mesh-build-image/asks_test.go | 28 ++++- .../cmd/mesh-build-image/compose_test.go | 2 +- .../cmd/mesh-build-image/keptartifact.go | 42 +++++++ .../cmd/mesh-build-image/manifest_test.go | 2 +- modules/mesh-build-image/module.json | 5 +- .../cmd/mesh-build-node/asks.go | 2 +- .../cmd/mesh-build-node/asks_test.go | 27 ++++- .../cmd/mesh-build-node/keptartifact.go | 45 +++++++ .../cmd/mesh-build-node/manifest_test.go | 2 +- modules/mesh-build-node/module.json | 5 +- 15 files changed, 183 insertions(+), 140 deletions(-) create mode 100644 modules/mesh-build-image/cmd/mesh-build-image/keptartifact.go create mode 100644 modules/mesh-build-node/cmd/mesh-build-node/keptartifact.go diff --git a/modules/mesh-build-go/cmd/mesh-build-go/archive.go b/modules/mesh-build-go/cmd/mesh-build-go/archive.go index f7073418e..819b6b839 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/archive.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/archive.go @@ -24,6 +24,7 @@ import ( "time" "git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep" + sdkstep "git.novox.be/novox/mesh-delivery/sdk/buildstep" "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/therootcompany/xz" @@ -45,7 +46,7 @@ func (m *Module) httpClient() *http.Client { // buildArchive packs one archive artifact of the module in dir: a pinned file when it names a url, else the directory // `from` of the module's tree. -func (m *Module) buildArchive(ctx context.Context, workspace, module, name, dir string, art catalogue.Artifact, keep *keptFor) (any, error) { +func (m *Module) buildArchive(ctx context.Context, workspace, module, name, dir string, art catalogue.Artifact, keep *sdkstep.KeptFor) (any, error) { if art.URL != "" { return m.pinnedArchive(ctx, workspace, module, name, art, keep) } @@ -84,7 +85,7 @@ func (m *Module) buildArchive(ctx context.Context, workspace, module, name, dir // pinnedArchive fetches the file an artifact is pinned to, checks it against the pin, lays it out as the artifact says // (kept whole as `file`, or unpacked from `within`) and packs it. -func (m *Module) pinnedArchive(ctx context.Context, workspace, module, name string, art catalogue.Artifact, keep *keptFor) (any, error) { +func (m *Module) pinnedArchive(ctx context.Context, workspace, module, name string, art catalogue.Artifact, keep *sdkstep.KeptFor) (any, error) { file, err := m.pinnedFile(ctx, workspace, art) if err != nil { return nil, fmt.Errorf("%s: %w", art.Name, err) diff --git a/modules/mesh-build-go/cmd/mesh-build-go/build.go b/modules/mesh-build-go/cmd/mesh-build-go/build.go index 7ad77cc7c..d42fd7dbf 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/build.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/build.go @@ -16,6 +16,7 @@ import ( "git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep" artifactref "git.novox.be/novox/mesh-delivery/contracts/go/clients/containerimageregistryclient/reference" builds "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" + sdkstep "git.novox.be/novox/mesh-delivery/sdk/buildstep" "git.novox.be/novox/mesh-sdk/go/catalogue" ) @@ -120,7 +121,7 @@ func (m *Module) build(ctx context.Context, args map[string]any) (any, error) { // The version the build ask carries is the build's argument MESH_VERSION for every artifact (design 47 G §370). version, _ := args["version"].(string) fp, _ := args["fp"].(string) - keep := keptOf(publish, manifest, fp) + keep := sdkstep.KeepOf(publish, manifest, fp) if art.Kind == catalogue.ArtifactPackage && art.Language == "go" { seat, _ := args["seat"].(string) return m.buildGoPackage(ctx, workspace, dir, *art, version, seat) @@ -183,7 +184,7 @@ func (m *Module) build(ctx context.Context, args map[string]any) (any, error) { // answers the reference; a publish the seat refuses fails the step `not stored:`, unless keep names the build of the // artifact store's holder, which this node then keeps and answers as kept (keptbuilds.go). The packed bytes never stay on // the node: they are held in this process until the publish answered (the lead's decision L9). -func (m *Module) packInto(ctx context.Context, workspace, module, name, artifact, kind, root string, keep *keptFor) (BuildResult, error) { +func (m *Module) packInto(ctx context.Context, workspace, module, name, artifact, kind, root string, keep *sdkstep.KeptFor) (BuildResult, error) { archive, digest, err := buildstep.Pack(root) if err != nil { return BuildResult{}, fmt.Errorf("packing %s: %w", artifact, err) diff --git a/modules/mesh-build-go/cmd/mesh-build-go/keptbuilds.go b/modules/mesh-build-go/cmd/mesh-build-go/keptbuilds.go index 581bb18be..bdb9800d9 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/keptbuilds.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/keptbuilds.go @@ -12,14 +12,13 @@ package main // §20): this module checks no caller of its own. import ( - "encoding/json" "fmt" "slices" - "strings" "time" seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/nodebuildgo" ociseat "git.novox.be/novox/mesh-delivery/contracts/go/seats/ociartifactregistry" + sdkstep "git.novox.be/novox/mesh-delivery/sdk/buildstep" stdio "git.novox.be/novox/mesh-sdk/go" "git.novox.be/novox/mesh-sdk/go/buscalls" "git.novox.be/novox/mesh-sdk/go/catalogue" @@ -38,37 +37,16 @@ type KeptObjects interface { Delete(name string) error } -// keptFor is what a build keeps a refused artifact for: the module, which claims the store's seat, and its build's -// fingerprint, which kept-artifact is asked by. -type keptFor struct{ Module, Fp string } - -// keptOf is the keep of a build, or nil when nothing may be kept: only a trunk build (publish) of a module whose -// manifest at the built commit claims oci-artifact-registry, named by its fingerprint. -func keptOf(publish bool, manifest catalogue.Manifest, fp string) *keptFor { - if !publish || fp == "" || !manifest.ClaimsSeat(ociseat.Seat) { - return nil - } - return &keptFor{Module: manifest.Module, Fp: fp} -} - -// KeptBuild is one kept build: the object's description names its module, fingerprint and the node that kept it. -type KeptBuild struct { - Module string `json:"module"` - Fp string `json:"fp"` - Node string `json:"node"` - Digest string `json:"digest"` - Bytes uint64 `json:"bytes"` - LastUsed time.Time `json:"last-used"` -} +// KeptBuild is one kept build, the domain SDK's form (sdk/buildstep KeptBuild). +type KeptBuild = sdkstep.KeptBuild // keepBuild keeps the archive of digest ("sha256:") for k on this node; the object is named by the hex digest. -func (m *Module) keepBuild(k keptFor, digest string, archive []byte) error { +func (m *Module) keepBuild(k sdkstep.KeptFor, digest string, archive []byte) error { if m.KeptBuilds == nil { return fmt.Errorf("this module keeps no builds (no %s store)", KeptBuildsStore) } - hex := strings.TrimPrefix(digest, "sha256:") - desc, _ := json.Marshal(KeptBuild{Module: k.Module, Fp: k.Fp, Node: m.Node}) - if _, err := m.KeptBuilds.Put(hex, archive, string(desc)); err != nil { + hex, desc := sdkstep.KeptObject(k, m.Node, digest) + if _, err := m.KeptBuilds.Put(hex, archive, desc); err != nil { return fmt.Errorf("keeping the build of %s on %s: %w", k.Module, KeptBuildsStore, err) } if m.Say != nil { @@ -86,17 +64,7 @@ func (m *Module) keptBuilds() ([]KeptBuild, error) { if err != nil { return nil, err } - out := []KeptBuild{} - for _, i := range infos { - var k KeptBuild - if i.Deleted || json.Unmarshal([]byte(i.Description), &k) != nil || k.Node != m.Node { - continue - } - k.Digest, k.Bytes, k.LastUsed = i.Name, i.Size, i.Modified.UTC() - out = append(out, k) - } - slices.SortFunc(out, func(a, b KeptBuild) int { return strings.Compare(a.Digest, b.Digest) }) - return out, nil + return sdkstep.KeptBuildsOf(infos, m.Node), nil } // dropKeptBuild removes the kept build of the hex digest this node keeps; one it does not keep is no error. @@ -130,70 +98,24 @@ func (m *Module) claimsStore(module string) (bool, error) { if ask == nil { ask = stdio.Ask } - raw, err := ask(CatalogModuleVerb, map[string]any{"module": module}) + raw, err := ask(sdkstep.CatalogModuleVerb, map[string]any{"module": module}) if err != nil { return false, err } - var a struct { - Module struct { - Manifest json.RawMessage `json:"manifest"` - } `json:"module"` - } - if err := json.Unmarshal(raw, &a); err != nil { - return false, fmt.Errorf("%s answered what is no module: %w", CatalogModuleVerb, err) - } - if len(a.Module.Manifest) == 0 { - return false, nil - } - man, err := catalogue.Decode(a.Module.Manifest) - if err != nil { - return false, fmt.Errorf("the manifest of %s: %w", module, err) - } - return man.ClaimsSeat(ociseat.Seat), nil + return sdkstep.ClaimsStoreOf(module, raw) } -// CatalogModuleVerb is the seat mesh-catalog's verb that answers one module's manifest, as the manifest invokes it. -const CatalogModuleVerb = "seat:mesh-catalog.module" - -// keptArtifact is the verb kept-artifact's answer for in. -func (m *Module) keptArtifact(in seat.KeptArtifactInput) seat.KeptArtifactAnswer { - a := seat.KeptArtifactAnswer{Module: in.Module, Fp: in.Fp} +// keptArtifact is the verb kept-artifact's answer for in, the domain SDK's judgement over the builds this node keeps. +func (m *Module) keptArtifact(in seat.KeptArtifactInput) sdkstep.KeptArtifactAnswer { kept, err := m.keptBuilds() if err != nil { - a.Error = "reading the kept builds: " + err.Error() - return a + return sdkstep.KeptArtifactAnswer{Module: in.Module, Fp: in.Fp, Error: "reading the kept builds: " + err.Error()} } - var other *KeptBuild - for i, k := range kept { - if k.Module != in.Module || k.Fp != in.Fp { - continue - } - if k.Digest == in.Digest { - a.Digest, a.Size, a.Object = k.Digest, int64(k.Bytes), k.Digest - if m.Self == "" { - a.Error = "this module runs with no name of its own (MESH_MODULE), so its store's name on the bus is unknown" - return a - } - a.Store = catalogue.ObjectsBucket(m.Self, KeptBuildsStore) - return a - } - other = &kept[i] + store := "" + if m.Self != "" { + store = catalogue.ObjectsBucket(m.Self, KeptBuildsStore) } - claims, err := m.claimsStore(in.Module) - switch { - case err != nil: - a.Error = "asking whether " + in.Module + " claims " + ociseat.Seat + ": " + err.Error() - case !claims: - a.Refused = seat.KeptArtifactAnswerRefusedClaimsNoOciArtifactRegistry - a.Words = fmt.Sprintf("%s claims no %s: a build node keeps builds for that seat's holder alone", in.Module, ociseat.Seat) - case other != nil: - a.Refused, a.Digest = seat.KeptArtifactAnswerRefusedDigestDiffers, other.Digest - a.Words = fmt.Sprintf("this node keeps the build of %s at %s under the digest %s, not %s", in.Module, in.Fp, other.Digest, in.Digest) - default: - a.Refused = seat.KeptArtifactAnswerRefusedNotKept - a.Words = fmt.Sprintf("this node keeps no build of %s at %s (never kept, or swept)", in.Module, in.Fp) - } - return a + return sdkstep.KeptArtifact(in.Module, in.Fp, in.Digest, kept, store, m.claimsStore) } // keptArtifactVerb serves `.kept-artifact`. diff --git a/modules/mesh-build-go/cmd/mesh-build-go/keptbuilds_test.go b/modules/mesh-build-go/cmd/mesh-build-go/keptbuilds_test.go index e6de5202b..aff556c19 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/keptbuilds_test.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/keptbuilds_test.go @@ -75,35 +75,26 @@ func TestTheStoreHoldersRefusedBuildIsKeptAndServedByKeptArtifact(t *testing.T) continue } hex := strings.TrimPrefix(res.Kept, "sha256:") - m.AskInput = func(key string, body any) (json.RawMessage, error) { - if key != CatalogModuleVerb { - return nil, errors.New("asked " + key) - } - mod, _ := body.(map[string]any)["module"].(string) - man := `{"module":"` + mod + `"}` - if mod == "demo" || mod == "lost" { - man = `{"module":"` + mod + `","claims":[{"name":"oci-artifact-registry","scope":"mesh"}]}` - } - return json.RawMessage(`{"module":{"module":"` + mod + `","manifest":` + man + `}}`), nil - } + // The answer's judgement is the domain SDK's (sdk/buildstep KeptArtifact, its own table); here only that the verb + // answers this node's kept build from this module's store, asking the seat mesh-catalog nothing for it. + m.AskInput = func(key string, _ any) (json.RawMessage, error) { return nil, errors.New("asked " + key) } fp := args["fp"].(string) - for _, a := range []struct { - name string - in seat.KeptArtifactInput - refused, store string - }{ - {"the kept build", seat.KeptArtifactInput{Module: "demo", Fp: fp, Digest: hex}, "", "mesh-build-go_kept-builds"}, - {"another digest", seat.KeptArtifactInput{Module: "demo", Fp: fp, Digest: strings.Repeat("b", 64)}, seat.KeptArtifactAnswerRefusedDigestDiffers, ""}, - {"a module claiming no store", seat.KeptArtifactInput{Module: "other", Fp: fp, Digest: hex}, seat.KeptArtifactAnswerRefusedClaimsNoOciArtifactRegistry, ""}, - {"a holder not kept here", seat.KeptArtifactInput{Module: "lost", Fp: fp, Digest: hex}, seat.KeptArtifactAnswerRefusedNotKept, ""}, - } { - got := m.keptArtifact(a.in) - if got.Refused != a.refused || got.Store != a.store || (a.refused == "" && (got.Object != hex || got.Size == 0)) || - (a.refused != "" && !strings.Contains(got.Words, a.in.Module)) { - t.Errorf("%s: %+v", a.name, got) + var verb func(map[string]any) (any, error) + for _, tool := range m.SeatTools(nil) { + if tool.Name == Seat+"."+seat.VerbKeptArtifact { + verb = tool.Run } } + out, _ := verb(map[string]any{"module": "demo", "fp": fp, "digest": hex}) + raw, _ := json.Marshal(out) + var got seat.KeptArtifactAnswer + if err := json.Unmarshal(raw, &got); err != nil || got.Refused != "" || got.Error != "" || got.Store != "mesh-build-go_kept-builds" || got.Object != hex || got.Size == 0 { + t.Errorf("kept-artifact of the kept build: %s", raw) + } // Another node's build in the shared store is not this node's to answer. + m.AskInput = func(string, any) (json.RawMessage, error) { + return json.RawMessage(`{"module":{"module":"demo","manifest":` + holder + `}}`), nil + } m.Node = "g14" if got := m.keptArtifact(seat.KeptArtifactInput{Module: "demo", Fp: fp, Digest: hex}); got.Refused != seat.KeptArtifactAnswerRefusedNotKept { t.Errorf("another node's kept build answered %+v", got) diff --git a/modules/mesh-build-image/cmd/mesh-build-image/asks.go b/modules/mesh-build-image/cmd/mesh-build-image/asks.go index ca33045ad..38a1bda81 100644 --- a/modules/mesh-build-image/cmd/mesh-build-image/asks.go +++ b/modules/mesh-build-image/cmd/mesh-build-image/asks.go @@ -77,7 +77,7 @@ func (m *Module) Puller(steps []StepTool, source buildstep.AskSource) *buildstep // SeatTools are the seat's verbs on this node (ADR 0525 §10). func (m *Module) SeatTools(p *buildstep.Puller) []stdio.Tool { - return append(buildstep.SeatTools(Seat, "image", p), stepsTool()) + return append(buildstep.SeatTools(Seat, "image", p), stepsTool(), m.keptArtifactVerb()) } // StepEndTool ends one step left running (issue 1890); the module's steps state is the one it reads and writes. diff --git a/modules/mesh-build-image/cmd/mesh-build-image/asks_test.go b/modules/mesh-build-image/cmd/mesh-build-image/asks_test.go index 4ff1a2d4a..ebdece6a1 100644 --- a/modules/mesh-build-image/cmd/mesh-build-image/asks_test.go +++ b/modules/mesh-build-image/cmd/mesh-build-image/asks_test.go @@ -2,12 +2,14 @@ package main import ( "context" + "encoding/json" "strings" "testing" "time" "git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep" sdkstep "git.novox.be/novox/mesh-delivery/sdk/buildstep" + stdio "git.novox.be/novox/mesh-sdk/go" ) const askCommit = "0123456789abcdef0123456789abcdef01234567" @@ -62,15 +64,33 @@ func TestAPulledImageAskCopiesThenBuilds(t *testing.T) { if got := strings.Join(calls, ","); got != "build_image_copy,build_image_fetch,build_image_build" { t.Errorf("calls = %s", got) } - served := map[string]bool{} + served := map[string]stdio.Tool{} for _, tool := range m.SeatTools(p) { - served[tool.Name] = true + served[tool.Name] = tool } - for _, verb := range []string{"current", "pause", "resume"} { - if !served[Seat+"."+verb] { + for _, verb := range []string{"current", "pause", "resume", "kept-artifact"} { + if _, ok := served[Seat+"."+verb]; !ok { t.Errorf("%s.%s is not served", Seat, verb) } } + // Design 47 lines 1888-1906 (test 60): kept-artifact is the domain SDK's judgement over no kept build here: the + // store's holder is not kept, any other module claims no oci-artifact-registry. + m.Call = func(key string, body any) (json.RawMessage, error) { + mod, _ := body.(map[string]any)["module"].(string) + man := `{"module":"` + mod + `"}` + if mod == "holder" { + man = `{"module":"holder","claims":[{"name":"oci-artifact-registry","scope":"mesh"}]}` + } + return json.RawMessage(`{"module":{"module":"` + mod + `","manifest":` + man + `}}`), nil + } + for mod, refused := range map[string]string{"holder": "not kept", "web": "claims no oci-artifact-registry"} { + if v, ok := served[Seat+".kept-artifact"]; ok { + out, _ := v.Run(map[string]any{"module": mod, "fp": "fp1", "digest": strings.Repeat("a", 64)}) + if a, _ := out.(sdkstep.KeptArtifactAnswer); a.Refused != refused || !strings.Contains(a.Words, mod) { + t.Errorf("kept-artifact of %s answered %+v, want refused %q", mod, out, refused) + } + } + } // A node without /dev/net/tun (a build node, 2026-10-28) takes no ask: its capacity is 0 and says why, so the ask goes to a node // that can build it instead of failing in buildah's network setup; once it is there again, the capacity is back. tun := m.Tun diff --git a/modules/mesh-build-image/cmd/mesh-build-image/compose_test.go b/modules/mesh-build-image/cmd/mesh-build-image/compose_test.go index 8a09744da..e952b09b5 100644 --- a/modules/mesh-build-image/cmd/mesh-build-image/compose_test.go +++ b/modules/mesh-build-image/cmd/mesh-build-image/compose_test.go @@ -24,7 +24,7 @@ func TestTheModuleInvokesTheImageSeatAndHoldsNoRegistry(t *testing.T) { t.Fatal(err) } if want := []string{"seat:container-image-registry.publish", "seat:container-image-registry.publish-part", - "seat:container-image-registry.mirror", "seat:mesh-module-assigner.assignments"}; !slices.Equal(m.Invokes, want) { + "seat:container-image-registry.mirror", "seat:mesh-module-assigner.assignments", "seat:mesh-catalog.module"}; !slices.Equal(m.Invokes, want) { t.Errorf("invokes = %v, want %v", m.Invokes, want) } // The seat's outcomes a copy waits on; the version comes in the ask, so version.numbered is not heard (design 47 G §413). diff --git a/modules/mesh-build-image/cmd/mesh-build-image/keptartifact.go b/modules/mesh-build-image/cmd/mesh-build-image/keptartifact.go new file mode 100644 index 000000000..27dd10a30 --- /dev/null +++ b/modules/mesh-build-image/cmd/mesh-build-image/keptartifact.go @@ -0,0 +1,42 @@ +package main + +// **The artifact store's holder takes its own new build even when its seat is the one broken** (novox/hq design 47, +// lines 1888-1906 at b229f5c4; test 60; milestone 47/G W12.5, issue 1966): the verb kept-artifact sits in every build +// seat's definition, so this module serves it too. It keeps no build of its own: its answer is the domain SDK's +// judgement (sdk/buildstep KeptArtifact, ADR 0634 §2) over no kept build, so a module that claims no +// oci-artifact-registry is refused as such and the store's holder is answered not kept here. Who may call it is the +// seat definition's (the node-engines alone), granted by the grants part (ADR 0666 §20): this module checks no caller. + +import ( + seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/nodebuildimage" + sdkstep "git.novox.be/novox/mesh-delivery/sdk/buildstep" + stdio "git.novox.be/novox/mesh-sdk/go" +) + +// keptArtifact is the verb kept-artifact's answer for in: this node keeps no build. +func (m *Module) keptArtifact(in seat.KeptArtifactInput) sdkstep.KeptArtifactAnswer { + return sdkstep.KeptArtifact(in.Module, in.Fp, in.Digest, nil, "", func(module string) (bool, error) { + call := m.call() + raw, err := call(sdkstep.CatalogModuleVerb, map[string]any{"module": module}) + if err != nil { + return false, err + } + return sdkstep.ClaimsStoreOf(module, raw) + }) +} + +// keptArtifactVerb serves `.kept-artifact`. +func (m *Module) keptArtifactVerb() stdio.Tool { + return stdio.Tool{Name: Seat + "." + seat.VerbKeptArtifact, + Description: "Where the build this node kept for the artifact store's holder (the holder of " + sdkstep.StoreSeat + + ") is. This build module keeps none: it refuses a module that claims no " + sdkstep.StoreSeat + + " and answers the holder's build not kept here, naming the module.", + Input: map[string]any{"type": "object", "required": []string{"module", "fp", "digest"}, "properties": map[string]any{ + "module": map[string]any{"type": "string", "description": "the module, the holder of " + sdkstep.StoreSeat}, + "fp": map[string]any{"type": "string", "description": "its build's fingerprint"}, + "digest": map[string]any{"type": "string", "description": "the artifact's sha256, 64 lowercase hex digits"}}}, + Run: func(args map[string]any) (any, error) { + str := func(k string) string { s, _ := args[k].(string); return s } + return m.keptArtifact(seat.KeptArtifactInput{Module: str("module"), Fp: str("fp"), Digest: str("digest")}), nil + }} +} diff --git a/modules/mesh-build-image/cmd/mesh-build-image/manifest_test.go b/modules/mesh-build-image/cmd/mesh-build-image/manifest_test.go index 8419a7a08..e4b6b3d4c 100644 --- a/modules/mesh-build-image/cmd/mesh-build-image/manifest_test.go +++ b/modules/mesh-build-image/cmd/mesh-build-image/manifest_test.go @@ -125,7 +125,7 @@ func TestManifestKeepsItsStatesClaimsItsBuildSeatAndDeclaresItsHealthTool(t *tes if want := []string{"keys", MirrorWaitsState, "pauses", "steps"}; !slices.Equal(states, want) { t.Errorf("state = %v, want %v", states, want) } - if len(m.Claims) != 1 || m.Claims[0].Name != Seat || !slices.Equal(m.Claims[0].Serves, []string{"current", "drop", "pause", "resume", "steps"}) { + if len(m.Claims) != 1 || m.Claims[0].Name != Seat || !slices.Equal(m.Claims[0].Serves, []string{"current", "drop", "pause", "resume", "steps", "kept-artifact"}) { t.Errorf("claims = %v, want the seat %s alone", m.Claims, Seat) } raw, _ := os.ReadFile(filepath.Join(moduleDir, "module.json")) diff --git a/modules/mesh-build-image/module.json b/modules/mesh-build-image/module.json index be406b135..9190b8be7 100644 --- a/modules/mesh-build-image/module.json +++ b/modules/mesh-build-image/module.json @@ -21,7 +21,7 @@ { "name": "node-build-image", "scope": "node", - "serves": ["current", "drop", "pause", "resume", "steps"] + "serves": ["current", "drop", "pause", "resume", "steps", "kept-artifact"] } ], "config": { @@ -65,7 +65,8 @@ "seat:container-image-registry.publish", "seat:container-image-registry.publish-part", "seat:container-image-registry.mirror", - "seat:mesh-module-assigner.assignments" + "seat:mesh-module-assigner.assignments", + "seat:mesh-catalog.module" ], "consumes": [ "container-image-registry.published", diff --git a/modules/mesh-build-node/cmd/mesh-build-node/asks.go b/modules/mesh-build-node/cmd/mesh-build-node/asks.go index 5161802e3..da739e18a 100644 --- a/modules/mesh-build-node/cmd/mesh-build-node/asks.go +++ b/modules/mesh-build-node/cmd/mesh-build-node/asks.go @@ -87,7 +87,7 @@ func (m *Module) Puller(steps []StepTool, source buildstep.AskSource) *buildstep // SeatTools are the seat's verbs on this node (ADR 0525 §10). func (m *Module) SeatTools(p *buildstep.Puller) []stdio.Tool { - return append(buildstep.SeatTools(Seat, "Node", p), m.stepsTool()) + return append(buildstep.SeatTools(Seat, "Node", p), m.stepsTool(), m.keptArtifactVerb()) } // StepEndTool ends one step left running (issue 1890); the module's steps state is the one it reads and writes. diff --git a/modules/mesh-build-node/cmd/mesh-build-node/asks_test.go b/modules/mesh-build-node/cmd/mesh-build-node/asks_test.go index 3b02e0b23..56546c5be 100644 --- a/modules/mesh-build-node/cmd/mesh-build-node/asks_test.go +++ b/modules/mesh-build-node/cmd/mesh-build-node/asks_test.go @@ -12,6 +12,7 @@ import ( "git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep" sdkstep "git.novox.be/novox/mesh-delivery/sdk/buildstep" + stdio "git.novox.be/novox/mesh-sdk/go" ) const askCommit = "0123456789abcdef0123456789abcdef01234567" @@ -123,13 +124,31 @@ func TestManifestTakesTheBuildCapacity(t *testing.T) { t.Error("no settings file") } m0 := &Module{Node: "ace", Capacity: func() int { return 0 }} - served := map[string]bool{} + served := map[string]stdio.Tool{} for _, tool := range m0.SeatTools(m0.Puller(nil, buildstep.NewFakeAsks())) { - served[tool.Name] = true + served[tool.Name] = tool } - for _, verb := range []string{"current", "pause", "resume"} { - if !served[Seat+"."+verb] { + for _, verb := range []string{"current", "pause", "resume", "kept-artifact"} { + if _, ok := served[Seat+"."+verb]; !ok { t.Errorf("%s.%s is not served", Seat, verb) } } + // Design 47 lines 1888-1906 (test 60): kept-artifact is the domain SDK's judgement over no kept build here: the + // store's holder is not kept, any other module claims no oci-artifact-registry. + m0.Call = func(key string, body any) (json.RawMessage, error) { + mod, _ := body.(map[string]any)["module"].(string) + man := `{"module":"` + mod + `"}` + if mod == "holder" { + man = `{"module":"holder","claims":[{"name":"oci-artifact-registry","scope":"mesh"}]}` + } + return json.RawMessage(`{"module":{"module":"` + mod + `","manifest":` + man + `}}`), nil + } + for mod, refused := range map[string]string{"holder": "not kept", "web": "claims no oci-artifact-registry"} { + if v, ok := served[Seat+".kept-artifact"]; ok { + out, _ := v.Run(map[string]any{"module": mod, "fp": "fp1", "digest": strings.Repeat("a", 64)}) + if a, _ := out.(sdkstep.KeptArtifactAnswer); a.Refused != refused || !strings.Contains(a.Words, mod) { + t.Errorf("kept-artifact of %s answered %+v, want refused %q", mod, out, refused) + } + } + } } diff --git a/modules/mesh-build-node/cmd/mesh-build-node/keptartifact.go b/modules/mesh-build-node/cmd/mesh-build-node/keptartifact.go new file mode 100644 index 000000000..30dd76998 --- /dev/null +++ b/modules/mesh-build-node/cmd/mesh-build-node/keptartifact.go @@ -0,0 +1,45 @@ +package main + +// **The artifact store's holder takes its own new build even when its seat is the one broken** (novox/hq design 47, +// lines 1888-1906 at b229f5c4; test 60; milestone 47/G W12.5, issue 1966): the verb kept-artifact sits in every build +// seat's definition, so this module serves it too. It keeps no build of its own: its answer is the domain SDK's +// judgement (sdk/buildstep KeptArtifact, ADR 0634 §2) over no kept build, so a module that claims no +// oci-artifact-registry is refused as such and the store's holder is answered not kept here. Who may call it is the +// seat definition's (the node-engines alone), granted by the grants part (ADR 0666 §20): this module checks no caller. + +import ( + seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/nodebuildnode" + sdkstep "git.novox.be/novox/mesh-delivery/sdk/buildstep" + stdio "git.novox.be/novox/mesh-sdk/go" +) + +// keptArtifact is the verb kept-artifact's answer for in: this node keeps no build. +func (m *Module) keptArtifact(in seat.KeptArtifactInput) sdkstep.KeptArtifactAnswer { + return sdkstep.KeptArtifact(in.Module, in.Fp, in.Digest, nil, "", func(module string) (bool, error) { + call := m.Call + if call == nil { + call = stdio.Ask + } + raw, err := call(sdkstep.CatalogModuleVerb, map[string]any{"module": module}) + if err != nil { + return false, err + } + return sdkstep.ClaimsStoreOf(module, raw) + }) +} + +// keptArtifactVerb serves `.kept-artifact`. +func (m *Module) keptArtifactVerb() stdio.Tool { + return stdio.Tool{Name: Seat + "." + seat.VerbKeptArtifact, + Description: "Where the build this node kept for the artifact store's holder (the holder of " + sdkstep.StoreSeat + + ") is. This build module keeps none: it refuses a module that claims no " + sdkstep.StoreSeat + + " and answers the holder's build not kept here, naming the module.", + Input: map[string]any{"type": "object", "required": []string{"module", "fp", "digest"}, "properties": map[string]any{ + "module": map[string]any{"type": "string", "description": "the module, the holder of " + sdkstep.StoreSeat}, + "fp": map[string]any{"type": "string", "description": "its build's fingerprint"}, + "digest": map[string]any{"type": "string", "description": "the artifact's sha256, 64 lowercase hex digits"}}}, + Run: func(args map[string]any) (any, error) { + str := func(k string) string { s, _ := args[k].(string); return s } + return m.keptArtifact(seat.KeptArtifactInput{Module: str("module"), Fp: str("fp"), Digest: str("digest")}), nil + }} +} diff --git a/modules/mesh-build-node/cmd/mesh-build-node/manifest_test.go b/modules/mesh-build-node/cmd/mesh-build-node/manifest_test.go index 131d1c15c..288922bf5 100644 --- a/modules/mesh-build-node/cmd/mesh-build-node/manifest_test.go +++ b/modules/mesh-build-node/cmd/mesh-build-node/manifest_test.go @@ -117,7 +117,7 @@ func TestManifestKeepsItsStatesClaimsItsBuildSeatAndDeclaresItsHealthTool(t *tes if want := []string{"keys", "pauses", "steps", "toolchains"}; !slices.Equal(states, want) { t.Errorf("state = %v, want %v", states, want) } - if len(m.Claims) != 1 || m.Claims[0].Name != Seat || !slices.Equal(m.Claims[0].Serves, []string{"current", "drop", "pause", "resume", "steps"}) { + if len(m.Claims) != 1 || m.Claims[0].Name != Seat || !slices.Equal(m.Claims[0].Serves, []string{"current", "drop", "pause", "resume", "steps", "kept-artifact"}) { t.Errorf("claims = %v, want the seat %s alone", m.Claims, Seat) } if !slices.Contains(m.Tools, HealthTool) { diff --git a/modules/mesh-build-node/module.json b/modules/mesh-build-node/module.json index 4d9a5936b..e1cf7a539 100644 --- a/modules/mesh-build-node/module.json +++ b/modules/mesh-build-node/module.json @@ -21,7 +21,7 @@ { "name": "node-build-node", "scope": "node", - "serves": ["current", "drop", "pause", "resume", "steps"] + "serves": ["current", "drop", "pause", "resume", "steps", "kept-artifact"] } ], "config": { @@ -79,7 +79,8 @@ "seat:oci-artifact-registry.publish-part", "seat:npm-package-registry.publish", "seat:npm-package-registry.publish-part", - "seat:mesh-module-assigner.assignments" + "seat:mesh-module-assigner.assignments", + "seat:mesh-catalog.module" ], "own-secrets": { "broker": "${dir:state}/broker"