diff --git a/modules/mesh-build-go/buildstep/go.mod b/modules/mesh-build-go/buildstep/go.mod index e9d32952a..77b28476c 100644 --- a/modules/mesh-build-go/buildstep/go.mod +++ b/modules/mesh-build-go/buildstep/go.mod @@ -3,6 +3,7 @@ module git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep go 1.26.0 require ( + git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner 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/clients/gopackageregistryclient v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-delivery/contracts/go/clients/npmpackageregistryclient v0.0.0-00010101000000-000000000000 diff --git a/modules/mesh-build-go/buildstep/publish.go b/modules/mesh-build-go/buildstep/publish.go index 06e080ae8..a83de94df 100644 --- a/modules/mesh-build-go/buildstep/publish.go +++ b/modules/mesh-build-go/buildstep/publish.go @@ -15,6 +15,7 @@ import ( "os" "strings" + assigner "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner" imageclient "git.novox.be/novox/mesh-delivery/contracts/go/clients/containerimageregistryclient" goclient "git.novox.be/novox/mesh-delivery/contracts/go/clients/gopackageregistryclient" npmclient "git.novox.be/novox/mesh-delivery/contracts/go/clients/npmpackageregistryclient" @@ -23,6 +24,7 @@ import ( goseat "git.novox.be/novox/mesh-delivery/contracts/go/seats/gopackageregistry" npmseat "git.novox.be/novox/mesh-delivery/contracts/go/seats/npmpackageregistry" ociseat "git.novox.be/novox/mesh-delivery/contracts/go/seats/ociartifactregistry" + sdkstep "git.novox.be/novox/mesh-delivery/sdk/buildstep" "git.novox.be/novox/mesh-delivery/sdk/parts" ) @@ -76,6 +78,41 @@ func SentHow(n int) func(parts.Sent) string { // PublishParts publishes in with ps as its parts on seat, through that seat's client: in's fields are the seat's own // (the four registry seats share the publish verb's input, design 51), the parts are written by the client. func PublishParts(ctx context.Context, call SeatCall, seat string, in reg.PublishInput, ps []parts.Part) (parts.Sent, error) { + sent, err := publishParts(ctx, call, seat, in, ps) + return sent, HolderSilentOr(call, seat, err) +} + +// AssignerHolders is the verb that tells who holds a seat: `mesh-module-assigner.assignments` with seats +// (the state seat-holders). The build modules' manifests `invokes` it. +const AssignerHolders = "seat:" + assigner.Seat + "." + assigner.VerbAssignments + +// HolderSilentOr is err, or when it is a seat verb's "no responders" and the assigner names a holder of seat, a +// sdkstep.HolderSilent (a Passing: the holder's tool runner is being replaced and answers a moment later, ADR 0664 §1). The +// bus says "no responders" for a seat nothing holds too, so the words alone are no transport fault: with no holder named, or +// when the holders cannot be asked, the error stays as it is, said with why, and fails at once. +func HolderSilentOr(call SeatCall, seat string, err error) error { + if err == nil || !strings.Contains(strings.ToLower(err.Error()), "no responders") { + return err + } + raw, aerr := call(AssignerHolders, map[string]any{"seats": assigner.AssignmentsInputSeatsTrue}) + if aerr != nil { + return fmt.Errorf("%w (who holds %s could not be asked: %v)", err, seat, aerr) + } + var answer struct { + Seats []assigner.SeatHolders `json:"seats"` + } + if jerr := json.Unmarshal(raw, &answer); jerr != nil { + return fmt.Errorf("%w (who holds %s was answered in what is not read: %v)", err, seat, jerr) + } + for _, sh := range answer.Seats { + if sh.Seat == seat && len(sh.Holders) > 0 { + return fmt.Errorf("%w: %w", err, &sdkstep.HolderSilent{Seat: seat, Holder: sh.Holders[0].Module, Node: sh.Holders[0].Node}) + } + } + return fmt.Errorf("%w (nothing holds %s)", err, seat) +} + +func publishParts(ctx context.Context, call SeatCall, seat string, in reg.PublishInput, ps []parts.Part) (parts.Sent, error) { switch seat { case ImageSeat: s, err := imageclient.Publish(ctx, imageclient.Call(call), in, ps) 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 cb4eab097..466fae221 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/build.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/build.go @@ -178,7 +178,7 @@ func (m *Module) packInto(ctx context.Context, workspace, module, name, artifact return BuildResult{}, fmt.Errorf("packing %s: %w", artifact, err) } if err := m.publishArchive(ctx, name, artifact, kind, digest, archive); err != nil { - return BuildResult{}, fmt.Errorf("%s%s: %v", builds.NotStored, artifact, err) + return BuildResult{}, fmt.Errorf("%s%s: %w", builds.NotStored, artifact, err) } return BuildResult{Artifact: artifact, Digest: digest, Size: len(archive), Reference: artifactref.SeatReference(ArtifactSeat, name, artifact, digest)}, nil } diff --git a/modules/mesh-build-go/cmd/mesh-build-go/gopackage.go b/modules/mesh-build-go/cmd/mesh-build-go/gopackage.go index dbeaf4bee..18af8cb04 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/gopackage.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/gopackage.go @@ -82,7 +82,7 @@ func (m *Module) buildGoPackage(ctx context.Context, workspace, dir string, art return res, nil } if err := m.publishOn(ctx, seat, path, "package", version, digest, transferName(path, version, digest), body); err != nil { - return BuildResult{}, fmt.Errorf("%s%s: %v", builds.NotStored, art.Name, err) + return BuildResult{}, fmt.Errorf("%s%s: %w", builds.NotStored, art.Name, err) } res.Reference = path + "@" + version return res, nil diff --git a/modules/mesh-build-go/cmd/mesh-build-go/publish_test.go b/modules/mesh-build-go/cmd/mesh-build-go/publish_test.go index 4f5a587d9..7a37ace2b 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/publish_test.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/publish_test.go @@ -14,6 +14,7 @@ import ( "sync" "testing" + "git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep" registry "git.novox.be/novox/mesh-delivery/contracts/go/seats/containerimageregistry" builds "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" sdkstep "git.novox.be/novox/mesh-delivery/sdk/buildstep" @@ -139,6 +140,27 @@ func TestARefusedPushFailsTheStepNotStored(t *testing.T) { !strings.Contains(st.Reason, "oci-artifact-registry") { t.Errorf("no holder: the step ended %s: %q", st.State, st.Reason) } + // ADR 0664 §1 (issue 1748): "no responders" is the same words for a seat nothing holds and for a holder whose tool runner is + // being replaced; the assigner says which. A named holder makes it a transport fault (the bus gives the ask again), none + // fails at once naming it. + for _, c := range []struct { + name, holders string + passing bool + words string + }{ + {"a holder named", `{"seats":[{"seat":"oci-artifact-registry","holders":[{"module":"mesh-registry","node":"novox"}]}],"holds":1}`, true, "the holder is silent: seat oci-artifact-registry, held by mesh-registry on novox"}, + {"nothing holds it", `{"seats":[{"seat":"go-package-registry","holders":[{"module":"mesh-registry","node":"novox"}]}],"holds":1}`, false, "nothing holds oci-artifact-registry"}, + } { + m.Call = func(key string, _ any) (json.RawMessage, error) { + if key == buildstep.AssignerHolders { + return json.RawMessage(c.holders), nil + } + return nil, errors.New("nats: no responders available for request") + } + if _, st := runBuild(t, m, states, "tools"); st.State != sdkstep.StepFailed || st.Passing != c.passing || !strings.Contains(st.Reason, c.words) { + t.Errorf("%s: the step ended %s: %q (passing %v)", c.name, st.State, st.Reason, st.Passing) + } + } } // 1587-S4 (the lead's decision L9): the packed file is not left on the node once its push answered, a candidate's diff --git a/modules/mesh-build-go/go.mod b/modules/mesh-build-go/go.mod index 13f373b82..87a97efea 100644 --- a/modules/mesh-build-go/go.mod +++ b/modules/mesh-build-go/go.mod @@ -102,6 +102,7 @@ replace git.novox.be/novox/mesh-catalog/modules/ssh-client/sshkeys => ../ssh-cli require ( git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions v0.0.0-00010101000000-000000000000 // indirect + git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000 // indirect git.novox.be/novox/mesh-delivery/contracts/go/clients/gopackageregistryclient v0.0.0-00010101000000-000000000000 // indirect git.novox.be/novox/mesh-delivery/contracts/go/clients/npmpackageregistryclient v0.0.0-00010101000000-000000000000 // indirect git.novox.be/novox/mesh-delivery/contracts/go/clients/ociartifactregistryclient v0.0.0-00010101000000-000000000000 // indirect diff --git a/modules/mesh-build-go/module.json b/modules/mesh-build-go/module.json index 850792328..d630360ce 100644 --- a/modules/mesh-build-go/module.json +++ b/modules/mesh-build-go/module.json @@ -35,7 +35,8 @@ "seat:oci-artifact-registry.publish-part", "seat:go-package-registry.publish", "seat:go-package-registry.publish-part", - "seat:mesh-delivery-checker.input" + "seat:mesh-delivery-checker.input", + "seat:mesh-module-assigner.assignments" ], "config": { "capacity": { 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 b24aa8138..ff7acedf9 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"}; !slices.Equal(m.Invokes, want) { + "seat:container-image-registry.mirror", "seat:mesh-module-assigner.assignments"}; !slices.Equal(m.Invokes, want) { t.Errorf("invokes = %v, want %v", m.Invokes, want) } // The builder's version.numbered for the late publish (ADR 0617 §1), and the seat's outcomes a copy waits on. diff --git a/modules/mesh-build-image/cmd/mesh-build-image/publish.go b/modules/mesh-build-image/cmd/mesh-build-image/publish.go index 4af9d4eb7..7295fc11f 100644 --- a/modules/mesh-build-image/cmd/mesh-build-image/publish.go +++ b/modules/mesh-build-image/cmd/mesh-build-image/publish.go @@ -136,7 +136,7 @@ func (m *Module) publish(ctx context.Context, workspace, module, local string, r Transfer: buildstep.TransferName(name, q, img.Digest)} sent, err := buildstep.PublishFiles(ctx, m.call(), buildstep.ImageSeat, in, img.Files) if err != nil { - return nil, fmt.Errorf("%s%s: %v", builds.NotStored, res.Artifact, err) + return nil, fmt.Errorf("%s%s: %w", builds.NotStored, res.Artifact, err) } m.say("published %s on %s: %d of its %d parts sent, the rest held", name, buildstep.ImageSeat, len(sent.Parts), len(img.Files)) res.Digest, res.Pushed = img.Digest, true diff --git a/modules/mesh-build-image/go.mod b/modules/mesh-build-image/go.mod index 9795b1b99..06309053b 100644 --- a/modules/mesh-build-image/go.mod +++ b/modules/mesh-build-image/go.mod @@ -34,6 +34,7 @@ require git.novox.be/novox/mesh-catalog/modules/ssh-client/sshkeys v0.0.0-000101 replace git.novox.be/novox/mesh-catalog/modules/ssh-client/sshkeys => ../ssh-client/sshkeys require ( + git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000 // indirect git.novox.be/novox/mesh-delivery/contracts/go/clients/gopackageregistryclient v0.0.0-00010101000000-000000000000 // indirect git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient v0.0.0-00010101000000-000000000000 // indirect git.novox.be/novox/mesh-delivery/contracts/go/clients/npmpackageregistryclient v0.0.0-00010101000000-000000000000 // indirect diff --git a/modules/mesh-build-image/module.json b/modules/mesh-build-image/module.json index b9013f5bc..fa94ba713 100644 --- a/modules/mesh-build-image/module.json +++ b/modules/mesh-build-image/module.json @@ -63,7 +63,8 @@ "invokes": [ "seat:container-image-registry.publish", "seat:container-image-registry.publish-part", - "seat:container-image-registry.mirror" + "seat:container-image-registry.mirror", + "seat:mesh-module-assigner.assignments" ], "consumes": [ "mesh-delivery-builder.version.numbered", diff --git a/modules/mesh-build-node/cmd/mesh-build-node/build.go b/modules/mesh-build-node/cmd/mesh-build-node/build.go index 00a966817..4c7bb7da0 100644 --- a/modules/mesh-build-node/cmd/mesh-build-node/build.go +++ b/modules/mesh-build-node/cmd/mesh-build-node/build.go @@ -168,7 +168,7 @@ func (m *Module) build(ctx context.Context, args map[string]any) (any, error) { // Published from this process once the build ended, a candidate's as a trunk build's (ADR 0498 §1 §2; CAT-BN3): the // step answers the seat's reference, or fails not stored. The packed bytes are never written to the node (L9). if err := m.publishOn(ctx, buildstep.ArtifactSeat, a.module+"/"+a.Name, "bundle", "", digest, archive); err != nil { - return nil, fmt.Errorf("%s%s: %v", builds.NotStored, a.Name, err) + return nil, fmt.Errorf("%s%s: %w", builds.NotStored, a.Name, err) } files := []string{} _ = filepath.WalkDir(bundleDir, func(p string, d os.DirEntry, err error) error { diff --git a/modules/mesh-build-node/cmd/mesh-build-node/package.go b/modules/mesh-build-node/cmd/mesh-build-node/package.go index bb2f51bcc..ab61e2e1b 100644 --- a/modules/mesh-build-node/cmd/mesh-build-node/package.go +++ b/modules/mesh-build-node/cmd/mesh-build-node/package.go @@ -551,7 +551,7 @@ func (m *Module) buildPackage(ctx context.Context, workspace, module, root, dir, return res, nil } if err := m.publishOn(ctx, seat, p.Name, "package", p.Version, p.Digest, p.tgz); err != nil { - return nil, fmt.Errorf("%s%s: %v", builds.NotStored, a.Name, err) + return nil, fmt.Errorf("%s%s: %w", builds.NotStored, a.Name, err) } res.Reference = p.Name + "@" + p.Version return res, nil diff --git a/modules/mesh-build-node/cmd/mesh-build-node/push_test.go b/modules/mesh-build-node/cmd/mesh-build-node/push_test.go index 4854a59e3..4e526e139 100644 --- a/modules/mesh-build-node/cmd/mesh-build-node/push_test.go +++ b/modules/mesh-build-node/cmd/mesh-build-node/push_test.go @@ -17,6 +17,7 @@ import ( "git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep" builds "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" + sdkstep "git.novox.be/novox/mesh-delivery/sdk/buildstep" ) // pushed are the blobs the fake holder took, by digest, for the tests that read an archive back. @@ -130,6 +131,18 @@ func TestARefusedPushFailsNotStoredAndNoPackedFileStays(t *testing.T) { if c.words != "" && (err == nil || !strings.HasPrefix(err.Error(), builds.NotStored+"tools: ") || !strings.Contains(err.Error(), c.words)) { t.Errorf("a refused publish answered %v", err) } + if c.noSeat { + // ADR 0664 §1: with a holder named by the assigner the same words are a transport fault, the bus gives the ask again. + m.Call = func(key string, _ any) (json.RawMessage, error) { + if key == buildstep.AssignerHolders { + return json.RawMessage(`{"seats":[{"seat":"oci-artifact-registry","holders":[{"module":"mesh-registry","node":"novox"}]}],"holds":1}`), nil + } + return nil, errors.New("nats: no responders available for request") + } + if _, err := m.build(context.Background(), buildArgs()); err == nil || !sdkstep.IsPassing(err) || !strings.Contains(err.Error(), "the holder is silent") { + t.Errorf("no responders with a holder named answered %v, want a transport fault", err) + } + } var archives []string filepath.WalkDir(work, func(p string, d os.DirEntry, err error) error { if err == nil && strings.HasSuffix(p, ".tar.gz") { diff --git a/modules/mesh-build-node/go.mod b/modules/mesh-build-node/go.mod index 6d3d86384..dc59b88f2 100644 --- a/modules/mesh-build-node/go.mod +++ b/modules/mesh-build-node/go.mod @@ -94,6 +94,7 @@ require git.novox.be/novox/mesh-catalog/modules/ssh-client/sshkeys v0.0.0-000101 replace git.novox.be/novox/mesh-catalog/modules/ssh-client/sshkeys => ../ssh-client/sshkeys require ( + git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000 // indirect git.novox.be/novox/mesh-delivery/contracts/go/clients/gopackageregistryclient v0.0.0-00010101000000-000000000000 // indirect git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient v0.0.0-00010101000000-000000000000 // indirect git.novox.be/novox/mesh-delivery/contracts/go/clients/npmpackageregistryclient v0.0.0-00010101000000-000000000000 // indirect diff --git a/modules/mesh-build-node/module.json b/modules/mesh-build-node/module.json index c73dd5954..9d48c9104 100644 --- a/modules/mesh-build-node/module.json +++ b/modules/mesh-build-node/module.json @@ -76,7 +76,8 @@ "seat:oci-artifact-registry.publish", "seat:oci-artifact-registry.publish-part", "seat:npm-package-registry.publish", - "seat:npm-package-registry.publish-part" + "seat:npm-package-registry.publish-part", + "seat:mesh-module-assigner.assignments" ], "own-secrets": { "broker": "${dir:state}/broker"