diff --git a/modules/distribution/cmd/distribution-tools/main.go b/modules/distribution/cmd/distribution-tools/main.go index 4270a2adb..41c949a4a 100644 --- a/modules/distribution/cmd/distribution-tools/main.go +++ b/modules/distribution/cmd/distribution-tools/main.go @@ -12,7 +12,6 @@ import ( "encoding/json" "errors" "fmt" - builds "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" "net/url" "os" "strings" @@ -520,7 +519,7 @@ func tools(v *holder.Verbs, windows kept[holder.Window], callers func() (holder. func main() { base, _ := url.Parse("http://127.0.0.1:5001") store := &holder.OCI{Base: base, Upstream: &holder.Upstream{}} - h := &holder.Holder{Objects: parts.BusObjects(holder.IncomingObjects), Builder: builds.BuilderVersions{Ask: stdio.Ask}, Store: store, Say: say, Finding: finding, + h := &holder.Holder{Objects: parts.BusObjects(holder.IncomingObjects), Builder: holder.Reserved{Ask: stdio.Ask}, Store: store, Say: say, Finding: finding, Transfers: parts.BusTransfers[holder.Opening](holder.TransfersState), Cap: parts.BusCap(holder.IncomingObjects), Record: recordAct, Idle: parts.BusArm(holder.IdleState)} files := &holder.Files{Container: or(os.Getenv("MESH_REGISTRY_CONTAINER"), "mesh-registry"), Run: holder.ExecRunner, UID: os.Getuid()} v := &holder.Verbs{Holder: h, Catalog: store, Holds: kept[holder.Hold]{state: "holds", key: holder.Hold.Key}, Mirrors: mirrors{}, diff --git a/modules/distribution/go.mod b/modules/distribution/go.mod index 4a037fc1a..4fbf8a54a 100644 --- a/modules/distribution/go.mod +++ b/modules/distribution/go.mod @@ -8,7 +8,7 @@ require ( git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-delivery/contracts/go/clients/containerimageregistryclient v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-delivery/contracts/go/seats/containerimageregistry v0.0.0-00010101000000-000000000000 - git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-delivery/contracts/go/seats/meshbuilds v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-delivery/sdk v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-sdk/go v0.0.0-00010101000000-000000000000 github.com/nats-io/nats.go v1.54.0 diff --git a/modules/distribution/internal/holder/callers.go b/modules/distribution/internal/holder/callers.go index b3021139e..897c12152 100644 --- a/modules/distribution/internal/holder/callers.go +++ b/modules/distribution/internal/holder/callers.go @@ -2,7 +2,7 @@ // `oci-artifact-registry`, and of the old seat `mesh-artifact-store` until its window closes (novox/hq design 51 *The // four registry seats*, ADR 0605, ADR 0611, ADR 0617 §2). A build module publishes into a seat through `publish` and // `publish-part`: the parts land in distribution's own object store `incoming`, are hashed, checked against the -// builder's `builder_versions` and stored in the artifact store, which listens on its node's loopback; the store's port +// builder's reservation (`reserved`) and stored in the artifact store, which listens on its node's loopback; the store's port // is the program's, reads open, writes from its own process alone (front.go). package holder diff --git a/modules/distribution/internal/holder/holder_test.go b/modules/distribution/internal/holder/holder_test.go index d3cbe2bac..e0820d49f 100644 --- a/modules/distribution/internal/holder/holder_test.go +++ b/modules/distribution/internal/holder/holder_test.go @@ -286,12 +286,12 @@ func TestTheHolderOfTheTwoImageSeats(t *testing.T) { _, err := b.h.Publish(ArtifactSeat, builder, req("t")) return err }, "at their cap"}, - {"a version the builder did not number", func(b *bed) error { + {"a version the builder did not reserve", func(b *bed) error { b.h.Builder = fakeBuilder{ok: false} b.store.has[part.Digest] = true // held already: the publish ends at once _, err := b.h.Publish(ArtifactSeat, builder, req("t")) return err - }, "the builder did not number it"}, + }, "the builder did not reserve it"}, {"an image on the artifact seat", func(b *bed) error { r := req("t") r.Kind = KindImage @@ -843,9 +843,9 @@ func TestTheHolderOfTheTwoImageSeats(t *testing.T) { if err := json.Unmarshal(raw, &m); err != nil { t.Fatal(err) } - // The builder's question is its published package, taken by its series (ADR 0634 §1). - if m.Packages["git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"] != "0.1" { - t.Errorf("packages %v: want builds of series 0.1", m.Packages) + // The reservation is asked through the seat mesh-builds, so the manifest takes no package of the builder (design 47 §381). + if len(m.Packages) != 0 { + t.Errorf("packages %v: want none", m.Packages) } // E15: the provision artifact-store retires (ADR 0632 §3); the bootstrap keeps it as an alias on port 5100 while // the running node-engines still find the store by it (issue 1966), and nothing else is provided or served. diff --git a/modules/distribution/internal/holder/reserved.go b/modules/distribution/internal/holder/reserved.go new file mode 100644 index 000000000..89ebd9ae2 --- /dev/null +++ b/modules/distribution/internal/holder/reserved.go @@ -0,0 +1,42 @@ +package holder + +import ( + "encoding/json" + "fmt" + + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshbuilds" +) + +// Reserved is the holder's check of a publish against the builder's reservation (novox/hq design 47 §381, §478, §504): it asks the +// seat mesh-builds' verb reserved with the publish's name and version, a package's name in its ecosystem, and accepts the publish +// only when that version is the one reserved and an artifact of it has the publish's digest. It reads no catalog and a publish gains +// no field. It is the Builder the holder's Check asks. +type Reserved struct { + // Ask is the runtime's request and reply over the bus (mesh-sdk stdio.Ask). + Ask func(address string, body any) (json.RawMessage, error) +} + +// Address is the seat verb asked, as the manifest's `invokes` names it. +const Address = "seat:" + meshbuilds.Seat + "." + meshbuilds.VerbReserved + +// Recorded answers whether name@version with digest is reserved, and why not when it is not. An answer that cannot be read is an +// error, never a yes; a refusal by the builder (a name no entry carries, a version it did not reserve) is a no that names it. +func (r Reserved) Recorded(name, version, digest string) (bool, string, error) { + raw, err := r.Ask(Address, meshbuilds.ReservedInput{Name: name, Version: version}) + if err != nil { + return false, "", fmt.Errorf("%s did not answer for %s@%s: %w", Address, name, version, err) + } + var a meshbuilds.ReservedAnswer + if err := json.Unmarshal(raw, &a); err != nil || a.Version == "" { + return false, "", fmt.Errorf("%s answered no reservation for %s@%s: %s", Address, name, version, raw) + } + if a.Version != version { + return false, fmt.Sprintf("%s reserved %s for %s, not %s", a.Module, a.Version, name, version), nil + } + for _, art := range a.Artifacts { + if art.Digest == digest { + return true, "", nil + } + } + return false, fmt.Sprintf("%s reserved %s@%s for another digest than %s", a.Module, name, version, digest), nil +} diff --git a/modules/distribution/internal/holder/reserved_test.go b/modules/distribution/internal/holder/reserved_test.go new file mode 100644 index 000000000..312bf4afc --- /dev/null +++ b/modules/distribution/internal/holder/reserved_test.go @@ -0,0 +1,54 @@ +package holder + +import ( + "encoding/json" + "errors" + "strings" + "testing" +) + +// A publish is checked against the builder's reservation by the seat mesh-builds' verb reserved, asked with the package's name +// (design 47 §381, §478). +func TestReservedChecksAPublishAgainstTheReservation(t *testing.T) { + for _, c := range []struct { + name string + answer string + err error + ok bool + why string // part of the reason, or of the error + }{ + {name: "the reserved version and digest", answer: `{"module":"mesh-image","version":"0.1.7","artifacts":[{"artifact":"mesh-image:image","digest":"sha256:aa"}]}`, ok: true}, + {name: "another artifact of it carries the digest", answer: `{"module":"mesh-image","version":"0.1.7","artifacts":[{"artifact":"mesh-image:aux"},{"artifact":"mesh-image:image","digest":"sha256:aa"}]}`, ok: true}, + {name: "another digest", answer: `{"module":"mesh-image","version":"0.1.7","artifacts":[{"artifact":"mesh-image:image","digest":"sha256:bb"}]}`, why: "another digest than sha256:aa"}, + {name: "another version reserved", answer: `{"module":"mesh-image","version":"0.1.8","artifacts":[]}`, why: "reserved 0.1.8 for registry/mesh-image, not 0.1.7"}, + {name: "the builder refused the name", err: errors.New("no entry carries registry/mesh-image"), why: "no entry carries registry/mesh-image"}, + {name: "an answer that is none", answer: `{"error":"x"}`, why: "answered no reservation"}, + } { + asked := "" + r := Reserved{Ask: func(address string, body any) (json.RawMessage, error) { + b, _ := json.Marshal(body) + asked = address + " " + string(b) + return json.RawMessage(c.answer), c.err + }} + ok, why, err := r.Recorded("registry/mesh-image", "0.1.7", "sha256:aa") + if asked != `seat:mesh-builds.reserved {"name":"registry/mesh-image","version":"0.1.7"}` { + t.Fatalf("%s: asked %s", c.name, asked) + } + if c.ok { + if !ok || err != nil { + t.Errorf("%s: not accepted: %v %v", c.name, why, err) + } + continue + } + if ok || !strings.Contains(why+fmtErr(err), c.why) { + t.Errorf("%s: ok=%v why=%q err=%v, want %q", c.name, ok, why, err, c.why) + } + } +} + +func fmtErr(err error) string { + if err == nil { + return "" + } + return err.Error() +} diff --git a/modules/distribution/internal/holder/transfer.go b/modules/distribution/internal/holder/transfer.go index 6f2a4e5c3..e07812af9 100644 --- a/modules/distribution/internal/holder/transfer.go +++ b/modules/distribution/internal/holder/transfer.go @@ -129,7 +129,7 @@ type Objects = parts.Objects // TransferState is the state transfers. type TransferState = parts.Transfers[Opening] -// Builder is the builder's tool builder_versions, asked before anything is stored (ADR 0605 §4). +// Builder is the builder's reservation (Reserved), asked before anything is stored (ADR 0605 §4, design 47 §381). type Builder interface { Recorded(name, version, digest string) (ok bool, why string, err error) } @@ -196,17 +196,17 @@ func (h *Holder) receiver() *parts.Receiver[Opening] { ok, err := h.Store.Has(t.Shape.Name, digest) return err == nil && ok }, - // builder_versions is asked before anything is stored (ADR 0605 §4). + // The reservation is asked before anything is stored (ADR 0605 §4, design 47 §381). Check: func(t Transfer) error { if t.Shape.Version == "" { return nil } ok, why, err := h.Builder.Recorded(t.Shape.Name, t.Shape.Version, t.Shape.Digest) if err != nil { - return errors.New("builder_versions did not answer, so nothing is stored: " + err.Error()) + return errors.New("the reservation could not be read, so nothing is stored: " + err.Error()) } if !ok { - return errors.New("the builder did not number it: " + why) + return errors.New("the builder did not reserve it: " + why) } return nil }, diff --git a/modules/distribution/module.json b/modules/distribution/module.json index e834f13fc..04b974bbb 100644 --- a/modules/distribution/module.json +++ b/modules/distribution/module.json @@ -1,9 +1,6 @@ { "module": "distribution", "version": "2", - "packages": { - "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds": "0.1" - }, "provides": [ { "name": "artifact-store", @@ -63,7 +60,7 @@ ], "invokes": [ "distribution.registry_callers", - "mesh-delivery-builder.builder_versions", + "seat:mesh-builds.reserved", "mesh-delivery-view.node_haves", "seat:node-engine.manifest-reads", "seat:mesh-hand-acts.hand-act"