mesh/delivery-group group fix/1937-done-key-blobs rejected: composed together: the checker's verdict at the members' heads (c20fdcb8c623): integration: github.…
mesh/delivery superseded: a newer head of the same pull request
mesh/check failed: integration: github.com/novox/mesh-controller/cmd/mesh-controller.TestAWarrantForRetryRetriesAFailedPlanInTheStoreAndEndsItsConditi…
A done key the controller never took in holds no manifest, and the store's nightly collector deleted its bare blob: node-tools', mesh-host's and the forge bridge's keys name blobs that answer 404. Since issue 1933 the take-in refuses such a key, and nothing built it again. held (in the serving process, HEADs only) lets the builder ask before it reuses a key; a refused take-in now names every gone archive and says artifacts-gone with the module, key, fingerprint and references.
133 lines
4.7 KiB
Go
133 lines
4.7 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"slices"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
sdkdelivery "git.novox.be/novox/mesh-sdk/go/delivery"
|
|
|
|
"github.com/novox/mesh-controller/internal/artifacts"
|
|
"github.com/novox/mesh-controller/internal/link"
|
|
)
|
|
|
|
// 1937/AC1: held answers each reference held, gone (the store's 404) or unknown, asks only HEADs, and its answer is what
|
|
// the builder reads (mesh-sdk delivery.DecodeHeldAnswer).
|
|
func TestHeldAnswersEachReferenceHeldOrGone(t *testing.T) {
|
|
present := "artifact-store://node-tools/runtime/blobs/" + shaOf("held")
|
|
missing := "artifact-store://node-tools/runtime/blobs/" + shaOf("gone")
|
|
broken := "artifact-store://mesh-host/host-arch/blobs/" + shaOf("broken")
|
|
address, ask := heldAddress, heldAsk
|
|
t.Cleanup(func() { heldAddress, heldAsk = address, ask })
|
|
heldAddress = func(context.Context) (string, error) { return "store.test:5100", nil }
|
|
var asked []string
|
|
var mu sync.Mutex
|
|
heldAsk = func(_ context.Context, at, reference string) error {
|
|
mu.Lock()
|
|
asked = append(asked, at+" "+reference)
|
|
mu.Unlock()
|
|
switch reference {
|
|
case missing:
|
|
return artifacts.Gone
|
|
case broken:
|
|
return errors.New("the artifact store answered 500")
|
|
}
|
|
return nil
|
|
}
|
|
got, err := heldAnswer(t.Context(), present+", "+missing+","+broken+","+present)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
raw, err := json.Marshal(got)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
a, err := sdkdelivery.DecodeHeldAnswer(raw, []string{present, missing, broken})
|
|
if err != nil {
|
|
t.Fatalf("the builder cannot read held's answer: %v\n%s", err, raw)
|
|
}
|
|
if !slices.Equal(a.Held, []string{present}) || !slices.Equal(a.Gone, []string{missing}) || a.Unknown[broken] == "" {
|
|
t.Errorf("held answered %+v", a)
|
|
}
|
|
if len(asked) != 3 {
|
|
t.Errorf("held asked the store %d times for three distinct references: %v", len(asked), asked)
|
|
}
|
|
if !strings.Contains(got.(verbAnswer).Output, missing) {
|
|
t.Errorf("held's words do not name the gone reference: %s", got.(verbAnswer).Output)
|
|
}
|
|
|
|
for _, refused := range []string{"", "registry.example/x@sha256:00"} {
|
|
if _, err := heldAnswer(t.Context(), refused); err == nil {
|
|
t.Errorf("held took %q", refused)
|
|
}
|
|
}
|
|
// A verb answered in the serving process, never by a command line.
|
|
if !inProcess["held"] {
|
|
t.Error("held is not answered by the serving controller")
|
|
}
|
|
if _, err := argvFor("held", map[string]any{"references": present}); err == nil {
|
|
t.Error("held composed a command line")
|
|
}
|
|
}
|
|
|
|
// saidGone keeps the artifacts-gone events said.
|
|
type saidGone struct {
|
|
link.Bus // only PublishSeatEvent is said on
|
|
mu sync.Mutex
|
|
said []sdkdelivery.ArtifactsGone
|
|
}
|
|
|
|
func (s *saidGone) PublishSeatEvent(_ context.Context, seat, event string, body []byte) error {
|
|
if seat != link.MeshControllerSeat || event != link.KeyArtifactsGone {
|
|
return errors.New("said under the wrong seat or name: " + seat + " " + event)
|
|
}
|
|
e, err := sdkdelivery.DecodeArtifactsGone(body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.said = append(s.said, e)
|
|
return nil
|
|
}
|
|
|
|
// 1937/AC2: a take-in refused for a gone archive names every gone one and says artifacts-gone with the module, the key,
|
|
// its fingerprint and the gone references, so the builder builds the key again; a hand registration says nothing.
|
|
func TestARefusedTakeInSaysArtifactsGone(t *testing.T) {
|
|
program := "artifact-store://x/tools/blobs/" + shaOf("t")
|
|
image := "artifact-store://x/img@" + shaOf("i")
|
|
keys, trunk := aKeyOf(tsBundle, 3,
|
|
sdkdelivery.BuiltArtifact{Name: "tools", Kind: "bundle", Digest: shaOf("t"), Reference: program},
|
|
sdkdelivery.BuiltArtifact{Name: "img", Kind: "image", Digest: shaOf("i"), Reference: image})
|
|
bus := &saidGone{}
|
|
was := checkEvents
|
|
checkEvents = bus
|
|
t.Cleanup(func() { checkEvents = was })
|
|
gone := func(context.Context, string) error { return artifacts.Gone }
|
|
|
|
_, _, err := buildOfKey(t.Context(), keys, keyTake{Module: "x", Trunk: trunk}, keyContext{hold: gone})
|
|
if err == nil || !strings.Contains(err.Error(), program) || !strings.Contains(err.Error(), image) ||
|
|
!strings.Contains(err.Error(), "not in the artifact store") {
|
|
t.Fatalf("a take-in of a key whose archives are gone: %v", err)
|
|
}
|
|
if len(bus.said) != 1 {
|
|
t.Fatalf("said %d artifacts-gone events; want one", len(bus.said))
|
|
}
|
|
e := bus.said[0]
|
|
wantKey, _ := sdkdelivery.BuilderBuildKey("x", keyFP)
|
|
if e.Module != "x" || e.Fingerprint != keyFP || e.Key != wantKey || !slices.Equal(e.Gone, []string{image, program}) {
|
|
t.Errorf("said %+v", e)
|
|
}
|
|
|
|
bus.said = nil
|
|
_, _, _ = buildOfKey(t.Context(), keys, keyTake{Module: "x", Trunk: trunk, Hand: map[string]string{"tools": program}},
|
|
keyContext{hold: gone})
|
|
if len(bus.said) != 0 {
|
|
t.Errorf("a registration by hand said %v: no builder's key was refused", bus.said)
|
|
}
|
|
}
|