diff --git a/modules/mesh-delivery-builder/builds/builds.go b/modules/mesh-delivery-builder/builds/builds.go index 19299958..f282bb74 100644 --- a/modules/mesh-delivery-builder/builds/builds.go +++ b/modules/mesh-delivery-builder/builds/builds.go @@ -12,7 +12,7 @@ import ( "strings" "git.novox.be/novox/mesh-delivery/contracts/go/clients/containerimageregistryclient/reference" - "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogkey" "git.novox.be/novox/mesh-delivery/contracts/go/seats/containerimageregistry" "git.novox.be/novox/mesh-delivery/contracts/go/seats/ociartifactregistry" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds/forms" @@ -63,13 +63,13 @@ const ( ) // BuilderBuildKey is the key, in BuilderBuildsBucket, of a module's build at a fingerprint: `.`, the hex -// being the fingerprint's key part (meshcatalogclient.KeyPart), since a bus key may not hold ':'. A module holding a dot or a +// being the fingerprint's key part (meshcatalogkey.KeyPart), since a bus key may not hold ':'. A module holding a dot or a // character a key refuses, and a fingerprint not of the new form, are refused, naming them. func BuilderBuildKey(module, fp string) (string, error) { if err := keys.CheckName("module", module); err != nil { return "", err } - part, err := meshcatalogclient.KeyPart(fp) + part, err := meshcatalogkey.KeyPart(fp) if err != nil { return "", err } @@ -85,7 +85,7 @@ func ParseBuilderBuildKey(key string) (module, fp string, err error) { if err := keys.CheckName("module", module); err != nil { return "", "", fmt.Errorf("builds: %q is not a builder build key: %w", key, err) } - if fp, err = meshcatalogclient.ParseKeyPart(part); err != nil { + if fp, err = meshcatalogkey.ParseKeyPart(part); err != nil { return "", "", fmt.Errorf("builds: %q is not a builder build key: %w", key, err) } return module, fp, nil @@ -237,9 +237,16 @@ type BuilderBuild struct { // node-engine, beside the ones its manifest needs (novox/hq ADR 0618 §1). A reader that does not know the field // reads the build as needing none. Needs []string `json:"needs,omitempty"` + // KeptOn is, on a done key only, the build node that keeps the build instead of storing it through the seat + // oci-artifact-registry (design 47 G §671): every artifact of the build is kept there, each with its digest in + // Artifacts. A build not kept names none; the builder carries it onto the build's end (BuildLogEntry.KeptOn). + KeptOn string `json:"kept-on,omitempty"` } func (b BuilderBuild) check() error { + if b.KeptOn != "" && b.State != BuildDone { + return fmt.Errorf("builds: a build is kept on a done key alone, this one is %s", b.State) + } if err := trunk.CheckImports(b.Imports); err != nil { return err } diff --git a/modules/mesh-delivery-builder/builds/log.go b/modules/mesh-delivery-builder/builds/log.go index 0c59e696..455000d2 100644 --- a/modules/mesh-delivery-builder/builds/log.go +++ b/modules/mesh-delivery-builder/builds/log.go @@ -67,6 +67,9 @@ type BuildLogEntry struct { Manifest string `json:"manifest,omitempty"` Against []string `json:"against,omitempty"` Made []BuiltArtifact `json:"made,omitempty"` + // KeptOn is, at the end of a kept build, the build node that keeps it, once for the build (design 47 G §671): every + // artifact in Made is kept there, with its digest. A build not kept carries none. + KeptOn string `json:"kept-on,omitempty"` // Imports are, at a build's end, the mesh packages (import paths) the built programs import, their tests left out, // sorted and each once, as the build module answered them (BuildAnswer.Imports; novox/hq ADR 0593 §2): the indexer // keeps them on the module (KeptInputs2.Imports) when the build's fingerprint is the module's. diff --git a/modules/mesh-delivery-builder/internal/build/imports_test.go b/modules/mesh-delivery-builder/internal/build/imports_test.go index 20ee96e5..ac12fc0f 100644 --- a/modules/mesh-delivery-builder/internal/build/imports_test.go +++ b/modules/mesh-delivery-builder/internal/build/imports_test.go @@ -82,3 +82,31 @@ func outOfStepFilesABuildAnswersReachTheLog(t *testing.T) { }) } } + +// Design 47 G §671: a build the build node kept instead of storing it, which answers its digest and no reference, is done, +// and its end in the log names the node that keeps it once; a build not kept names none. +func aKeptBuildNamesItsNodeInItsEnd(t *testing.T) { + r := newRig(t, goBundle) + r.asks.Answer = func(a buildstep.StepAsk) buildstep.Step { + step := r.answer(a) + var answer buildstep.BuildAnswer + if err := json.Unmarshal(step.Result, &answer); err == nil && a.Language == "go" && len(answer.Artifacts) > 0 { + for i := range answer.Artifacts { + answer.Artifacts[i].Reference = "" + } + answer.Kept = strings.TrimPrefix(answer.Artifacts[0].Digest, "sha256:") + step.Result, _ = json.Marshal(answer) + } + return step + } + var logged []builds.BuildLogEntry + var mu sync.Mutex + r.b.Node, r.b.Log = "novox", func(e builds.BuildLogEntry) { mu.Lock(); logged = append(logged, e); mu.Unlock() } + v := r.consider(t, "modules/m") + if v.State != builds.BuildDone || v.KeptOn == "" { + t.Fatalf("done key %+v, want it done and kept on its node", v) + } + if len(logged) != 2 || logged[1].KeptOn != v.KeptOn || logged[0].KeptOn != "" { + t.Fatalf("log %+v, want kept-on %q on the end alone", logged, v.KeptOn) + } +} diff --git a/modules/mesh-delivery-builder/internal/build/run.go b/modules/mesh-delivery-builder/internal/build/run.go index baaf480c..e17baad3 100644 --- a/modules/mesh-delivery-builder/internal/build/run.go +++ b/modules/mesh-delivery-builder/internal/build/run.go @@ -651,6 +651,7 @@ type lane struct { digests map[string]string // artifact -> digest references map[string]string // artifact -> the reference an image was pushed to (a trunk build only) imports []string // the mesh packages the lane's programs import, as its build module answered them + kept string // the digest of the artifact the build node kept instead of storing it (BuildAnswer.Kept), else empty outOfStep []string // the generated files its build module's compare found out of step, passed or failed err error } @@ -763,7 +764,7 @@ func (b *Builder) build(ctx context.Context, module string, m meshcatalog.Module continue } r := l.references[t.Artifact] - if r == "" && stored(t.Kind) { + if r == "" && stored(t.Kind) && (l.kept == "" || strings.TrimPrefix(d, "sha256:") != l.kept) { // A bundle, an archive or an upstream copy is done only when its push was answered with where it is (ADR 0498 // clause 1); a candidate's included (clause 3). return failed("%s%s on %s: the build answered no reference, so its push to the artifact store was not answered", builds.NotStored, t.Artifact, l.node) @@ -796,6 +797,11 @@ func (b *Builder) build(ctx context.Context, module string, m meshcatalog.Module } done := builds.BuilderBuild{State: builds.BuildDone, By: b.Claimer.By, Manifest: m.Manifest, Step: step, Artifacts: artifacts, Toolchains: used, Imports: importsOf(lanes), Needs: forms.Current.Needs(artifacts)} // the forms it is written in (ADR 0618 §1) + for _, l := range lanes { + if l.kept != "" { + done.KeptOn = l.node // design 47 G §671: the build node that keeps the build + } + } if _, err := builds.EncodeBuilderBuild(done); err != nil { // A reference the build module answered that does not name the artifact and its digest is no reference. return failed("%s%v", builds.NotStored, err) @@ -1008,6 +1014,7 @@ func (b *Builder) runLane(ctx context.Context, l *lane, repository string, m mes return } l.imports = answer.Imports + l.kept = answer.Kept built := map[string]sdkstep.AskBuilt{} for _, a := range answer.Artifacts { built[a.Artifact] = a @@ -1092,6 +1099,7 @@ func LogEnded(module string, m meshcatalog.ModulesByModuleValue, ref, by string, e.Against = append([]string(nil), m.Inputs2.Bases...) } e.Made = append([]builds.BuiltArtifact(nil), result.Artifacts...) + e.KeptOn = result.KeptOn e.Imports = append([]string(nil), result.Imports...) // the indexer keeps them on the module (novox/hq ADR 0593 §2) e.Error, e.Step = result.Error, result.Step // a failed build's reason, not only its outcome return e diff --git a/modules/mesh-delivery-builder/internal/build/run_test.go b/modules/mesh-delivery-builder/internal/build/run_test.go index cfe21eaa..0339f41e 100644 --- a/modules/mesh-delivery-builder/internal/build/run_test.go +++ b/modules/mesh-delivery-builder/internal/build/run_test.go @@ -230,6 +230,7 @@ var ( func TestABundleAndAnArchiveAreBuiltOnOneNodeAndDone(t *testing.T) { // 1607/AC7, ADR 0478 clauses 1, 4 t.Run("the imports a build answers reach the done key and the log", importsABuildAnswersReachTheDoneKeyAndTheLog) + t.Run("a kept build names its node in its end", aKeptBuildNamesItsNodeInItsEnd) t.Run("the out-of-step files a build answers reach the log", outOfStepFilesABuildAnswersReachTheLog) t.Run("ADR 0624: a done key keeps each form a counted node reads", aDoneKeyKeepsEachFormACountedNodeReads) r := newRig(t, goBundle, archiveA, tsBundle)