diff --git a/cmd/mesh-controller/held.go b/cmd/mesh-controller/held.go new file mode 100644 index 00000000..20e8bb97 --- /dev/null +++ b/cmd/mesh-controller/held.go @@ -0,0 +1,128 @@ +package main + +import ( + "context" + "errors" + "fmt" + "slices" + "strings" + "time" + + "git.novox.be/novox/mesh-sdk/go/catalogue" + sdkdelivery "git.novox.be/novox/mesh-sdk/go/delivery" + + "github.com/novox/mesh-controller/internal/artifacts" + "github.com/novox/mesh-controller/internal/builderkeys" + "github.com/novox/mesh-controller/internal/link" +) + +// The verb `held` and the event `artifacts-gone` (novox/hq issue 1937). +// +// **A done key can name a blob the store no longer holds.** A build module pushes a bundle or an archive as a bare blob +// (ADR 0498 §1); the store's nightly collector deletes every blob no manifest names; and only a take-in or a sweep holds +// one by its manifest. A key the controller has not taken in by the next collection names a blob that is gone. On +// 2026-10-24 that was node-tools', mesh-host's and the forge bridge's. Since issue 1933 the take-in refuses such a key; +// these two let the builder build it again: `held` before it reuses a done key, `artifacts-gone` when a take-in found it. + +// heldMost is the most references one ask of held judges: a builder asks for one key's artifacts at a time. +const heldMost = 64 + +// heldAsk asks the store whether it holds one reference: nil, artifacts.Gone or another error. A variable so a test +// needs no store. +var heldAsk = func(ctx context.Context, address, reference string) error { + return artifacts.Store{Address: address}.Holds(ctx, reference) +} + +// heldAddress is the artifact store's address the serving controller asks. A variable so a test needs no records. +var heldAddress = func(ctx context.Context) (string, error) { + d := doctorFrom + if d == nil || d.open == nil || d.open.inventory == nil { + return "", errors.New("this controller is not serving, so it asks the artifact store nothing: ask again, and " + + "the serving controller answers") + } + shelf, err := d.open.inventory.Catalogue(ctx) + if err != nil { + return "", err + } + address, err := artifactStoreAddress(ctx, d.open.inventory, shelf, "") + if err != nil { + return "", err + } + if address == "" { + return "", errors.New("the artifact store is on no network the controller reaches") + } + return address, nil +} + +// heldAnswer is the verb `held`: each reference given, held, gone (the store answered 404) or unknown with why. It +// holds and writes nothing: a reference the store holds and no record names is left for the take-in to hold. +func heldAnswer(ctx context.Context, given string) (any, error) { + var refs []string + for _, r := range strings.Split(given, ",") { + if r = strings.TrimSpace(r); r != "" && !slices.Contains(refs, r) { + refs = append(refs, r) + } + } + if len(refs) == 0 { + return nil, errors.New("held judges the references given, and none was") + } + if len(refs) > heldMost { + return nil, fmt.Errorf("held judges at most %d references at once, and %d were given", heldMost, len(refs)) + } + for _, r := range refs { + if _, ours := catalogue.InArtifactStore(r); !ours { + return nil, fmt.Errorf("%s is not a reference into the mesh's artifact store (artifact-store://...): nothing was asked", r) + } + } + address, err := heldAddress(ctx) + if err != nil { + return nil, err + } + asking, stop := context.WithTimeout(ctx, 20*time.Second) + defer stop() + a := sdkdelivery.HeldAnswer{Store: address, Held: []string{}, Gone: []string{}} + for _, r := range refs { + switch err := heldAsk(asking, address, r); { + case err == nil: + a.Held = append(a.Held, r) + case errors.Is(err, artifacts.Gone): + a.Gone = append(a.Gone, r) + default: + if a.Unknown == nil { + a.Unknown = map[string]string{} + } + a.Unknown[r] = err.Error() + } + } + out := fmt.Sprintf("the artifact store at %s holds %d of the %d reference(s) asked", address, len(a.Held), len(refs)) + if len(a.Gone) > 0 { + out += "; gone: " + strings.Join(a.Gone, ", ") + } + if len(a.Unknown) > 0 { + out += fmt.Sprintf("; %d could not be asked", len(a.Unknown)) + } + return verbAnswer{Output: out + "\n", OK: true, Answer: a}, nil +} + +// sayArtifactsGone says `artifacts-gone` for a builder's key the take-in refused because the store does not hold what +// it names (issue 1937): the builder hears it and builds the key again. Said from the serving controller only, and never +// in the take-in's way: a bus that cannot carry it is said in the journal. +var sayArtifactsGone = func(ctx context.Context, k builderkeys.Key, module string, gone []string) { + bus := checkEvents + if bus == nil || len(gone) == 0 { + return + } + key, _ := sdkdelivery.BuilderBuildKey(module, k.Fingerprint) + raw, err := sdkdelivery.EncodeArtifactsGone(sdkdelivery.ArtifactsGone{Module: module, Key: key, + Fingerprint: k.Fingerprint, Gone: gone, At: time.Now().UTC()}) + if err != nil { + fmt.Printf("%s: its key names archives the store does not hold, and that could not be said: %v\n", module, err) + return + } + saying, stop := context.WithTimeout(context.WithoutCancel(ctx), 10*time.Second) + defer stop() + if err := bus.PublishSeatEvent(saying, link.MeshControllerSeat, link.KeyArtifactsGone, raw); err != nil { + fmt.Printf("%s: its key names archives the store does not hold, and %s could not be said: %v\n", + module, link.KeyArtifactsGone, err) + } +} diff --git a/cmd/mesh-controller/held_test.go b/cmd/mesh-controller/held_test.go new file mode 100644 index 00000000..68f129f9 --- /dev/null +++ b/cmd/mesh-controller/held_test.go @@ -0,0 +1,132 @@ +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) + } +} diff --git a/cmd/mesh-controller/key_takein.go b/cmd/mesh-controller/key_takein.go index c958d6b7..3f00bbc2 100644 --- a/cmd/mesh-controller/key_takein.go +++ b/cmd/mesh-controller/key_takein.go @@ -351,6 +351,9 @@ func buildOfKey(ctx context.Context, keys keyReads, t keyTake, kc keyContext) ( // archive the store does not hold is sent, and every machine assigned the module fails its apply on the 404. An // archive is held by its manifest, so the store's own collector keeps it too. if kc.hold != nil { + // **Every gone archive named, and said** (issue 1937): the builder hears `artifacts-gone` and builds the key + // again, so the refusal is the last word on this key, not a plan that fails at every retry. + var gone, goneNames []string for _, a := range k.Build.Artifacts { reference := catalogue.Recorded(a.Reference) if _, ours := catalogue.InArtifactStore(reference); !ours { @@ -360,13 +363,21 @@ func buildOfKey(ctx context.Context, keys keyReads, t keyTake, kc keyContext) ( switch { case err == nil, errors.Is(err, artifacts.ErrNotOurs): case errors.Is(err, artifacts.Gone): - return inventory.Build{}, catalogue.Manifest{}, fmt.Errorf("the builder's key of %s names %s for its "+ - "artifact %q, which is not in the artifact store: nothing was recorded or registered, and the key "+ - "must be built again", t.Module, reference, a.Name) + gone = append(gone, reference) + goneNames = append(goneNames, fmt.Sprintf("%s for its artifact %q", reference, a.Name)) default: return waits("the artifact store could not hold %s's artifact %q (%s): %v", t.Module, a.Name, reference, err) } } + if len(gone) > 0 { + if t.Hand == nil { + sayArtifactsGone(ctx, k, t.Module, gone) + } + return inventory.Build{}, catalogue.Manifest{}, fmt.Errorf("the builder's key of %s names %s, not in "+ + "the artifact store: nothing was recorded or registered, and the key must be built "+ + "again (said as %s, which the builder builds it again on)", t.Module, strings.Join(goneNames, " and "), + link.KeyArtifactsGone) + } } built, err := builtOfKey(ctx, kc.archive, manifest, k) if err != nil { diff --git a/cmd/mesh-controller/seatverbs.go b/cmd/mesh-controller/seatverbs.go index a4bf2f30..87350b89 100644 --- a/cmd/mesh-controller/seatverbs.go +++ b/cmd/mesh-controller/seatverbs.go @@ -318,6 +318,8 @@ func (a *verbArguments) commandLine() ([]string, error) { return nil, errors.New("dead-letters is answered by the serving controller, on its own connection, not by a command") case "root-free": return nil, errors.New("root-free is judged by the serving controller, on its own connection, not by a command") + case "held": + return nil, errors.New("held is asked by the serving controller, of the artifact store, not by a command") case "status": return []string{"status", "--json"}, nil case "nodes": @@ -1253,6 +1255,9 @@ func seatToolHandlers() (map[string]link.ToolHandler, []string, error) { if verb == "dead-letters" { return deadLettersAnswer(ctx, a) } + if verb == "held" { + return heldAnswer(ctx, a.given["references"]) + } if verb == "root-free" { return rootFreeAnswer(ctx, a.given["machines"], rootClock()) } @@ -1376,7 +1381,10 @@ var inProcess = map[string]bool{"tools": true, "calls": true, "doctor": true, "dead-letters": true, // Whether a machine is root-free, judged live on the serving controller's store and connection (novox/hq ADR // 0259 §8): the router asks it before an approval. - "root-free": true} + "root-free": true, + // Whether the artifact store holds what a builder's key names, asked by the builder before it reuses the key (novox/hq + // issue 1937). + "held": true} // pushFileVar names the file the controller's push settings are written to (module.json, `push-wait`). const pushFileVar = "MESH_PUSH_FILE" diff --git a/go.mod b/go.mod index f8f3f062..4e0a9bd9 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/novox/mesh-controller go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261023150321-a2c813a6b468 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261024020722-34f210e5eba1 github.com/jackc/pgx/v5 v5.10.0 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 diff --git a/go.sum b/go.sum index 5ffa1878..6b35a21b 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ git.novox.be/novox/mesh-host v0.0.0-20261018171808-9d9e3a46c4f5 h1:E5stfIeCN6uYg git.novox.be/novox/mesh-host v0.0.0-20261018171808-9d9e3a46c4f5/go.mod h1:4wyoCr6IIIIarEUuROThWDj7DAvJvIidqMjWcpSnOwE= git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261023150321-a2c813a6b468 h1:UcMee+IV2ii5E4dRZdQUaSaeh9IedTFYumuQbBiB0k0= git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261023150321-a2c813a6b468/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261024020722-34f210e5eba1 h1:fzOkrz9CSp3mEjeV7NfZx8yoivI1sD1AOtIPtyyF8RI= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261024020722-34f210e5eba1/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/internal/broker/nats.go b/internal/broker/nats.go index 01cf591d..e55ba60f 100644 --- a/internal/broker/nats.go +++ b/internal/broker/nats.go @@ -406,6 +406,12 @@ var DegradableGrants = []DegradableGrant{ Reason: "carried only at genesis: the controller composes its own permissions and pushes them to the bus at its " + "first push, and keeps the packages each node declares only after that, holding the lease (ADR 0507 §5, issue 1840)", }, + { + Subject: "mesh.seat." + ControllerSeat + ".event.artifacts-gone", + Reason: "refused: the take-in is refused all the same and says why; only the builder's rebuild waits for the " + + "controller's first push, and the builder also asks held when a module's value or its own start comes (novox/hq " + + "issue 1937)", + }, { Subject: "mesh.seat." + ControllerSeat + ".event.sent", Reason: "refused: the controller says a tier sent to a machine only for outsiders, and a send that could not " + diff --git a/internal/broker/streams.go b/internal/broker/streams.go index f694c83f..d3eec6d9 100644 --- a/internal/broker/streams.go +++ b/internal/broker/streams.go @@ -379,7 +379,9 @@ var ControllerStates = []string{"applied", "refused", "built-before", "sent", // And a key of a record mirror written or removed (novox/hq ADR 0289, design 54 §3): the assignments and // each registered module's declared config (design 57 F3). - "assignment-changed", "declared-config-changed", "predefined-changed"} + "assignment-changed", "declared-config-changed", "predefined-changed", + // And a builder's key not taken in because the artifact store does not hold what it names (novox/hq issue 1937). + "artifacts-gone"} // BusAdvisories are what the bus server says about the mesh's own account that the controller // reads (novox/hq to-be 45 §3, S9): a durable consumer that handed a message over as often as it diff --git a/internal/broker/testdata/composed.conf b/internal/broker/testdata/composed.conf index 5d4ce16e..a6a80775 100644 --- a/internal/broker/testdata/composed.conf +++ b/internal/broker/testdata/composed.conf @@ -29,7 +29,7 @@ accounts { ] users = [ { user: "controller", password: "$2a$11$cccccccccccccccccccccc", permissions: { - publish: { allow: ["$JS.ACK.CONTROL.controller.>", "$JS.ACK.DEAD_LETTER_NOTICES.controller.>", "$JS.ACK.EVENTS.controller-merges.>", "$JS.ACK.EVENTS.controller.>", "$JS.API.>", "$KV.mesh-controller_asked.>", "$KV.mesh-controller_assignments.>", "$KV.mesh-controller_builds.>", "$KV.mesh-controller_calls.>", "$KV.mesh-controller_condition-history.>", "$KV.mesh-controller_conditions.>", "$KV.mesh-controller_declared-config.>", "$KV.mesh-controller_declared-packages.>", "$KV.mesh-controller_delivery-merges.>", "$KV.mesh-controller_grants.>", "$KV.mesh-controller_hand-acts.>", "$KV.mesh-controller_lease.>", "$KV.mesh-controller_merge-window.>", "$KV.mesh-controller_predefined.>", "$KV.mesh-controller_rollout-silent.>", "$KV.mesh-controller_rollouts.>", "$KV.mesh-controller_states-announced.>", "$SRV.INFO", "_INBOX.enrol.>", "mesh.again.>", "mesh.assignment.>", "mesh.events.dead.>", "mesh.mod.*.tool.>", "mesh.mod.mesh-controller.event.states", "mesh.node.>", "mesh.seat.mesh-controller.event.applied", "mesh.seat.mesh-controller.event.assignment-changed", "mesh.seat.mesh-controller.event.built-before", "mesh.seat.mesh-controller.event.checked", "mesh.seat.mesh-controller.event.condition-changed", "mesh.seat.mesh-controller.event.condition-cleared", "mesh.seat.mesh-controller.event.condition-raised", "mesh.seat.mesh-controller.event.declared-config-changed", "mesh.seat.mesh-controller.event.doctor-heartbeat", "mesh.seat.mesh-controller.event.healer-acted", "mesh.seat.mesh-controller.event.plan-moved", "mesh.seat.mesh-controller.event.predefined-changed", "mesh.seat.mesh-controller.event.refused", "mesh.seat.mesh-controller.event.rolled-back", "mesh.seat.mesh-controller.event.secret-replaced", "mesh.seat.mesh-controller.event.sent", "mesh.seat.mesh-controller.tool.plans", "mesh.seat.mesh-controller.tool.push", "mesh.seat.mesh-controller.tool.upgrade", "mesh.seat.mesh-delivery.tool.close", "mesh.seat.mesh-delivery.tool.groups", "mesh.seat.mesh-delivery.tool.release", "mesh.seat.mesh-delivery.tool.stalled", "mesh.seat.mesh-delivery.tool.stop", "mesh.seat.mesh-delivery.tool.times", "mesh.seat.node-backup.tool.backed-up.*", "mesh.seat.node-backup.tool.now.*", "mesh.seat.node-intrusion-prevention.tool.banned.*", "mesh.seat.node-launcher.tool.secret.*", "mesh.seat.node-service-manager.tool.restart.*", "mesh.seat.operator-channel.tool.tell"] } + publish: { allow: ["$JS.ACK.CONTROL.controller.>", "$JS.ACK.DEAD_LETTER_NOTICES.controller.>", "$JS.ACK.EVENTS.controller-merges.>", "$JS.ACK.EVENTS.controller.>", "$JS.API.>", "$KV.mesh-controller_asked.>", "$KV.mesh-controller_assignments.>", "$KV.mesh-controller_builds.>", "$KV.mesh-controller_calls.>", "$KV.mesh-controller_condition-history.>", "$KV.mesh-controller_conditions.>", "$KV.mesh-controller_declared-config.>", "$KV.mesh-controller_declared-packages.>", "$KV.mesh-controller_delivery-merges.>", "$KV.mesh-controller_grants.>", "$KV.mesh-controller_hand-acts.>", "$KV.mesh-controller_lease.>", "$KV.mesh-controller_merge-window.>", "$KV.mesh-controller_predefined.>", "$KV.mesh-controller_rollout-silent.>", "$KV.mesh-controller_rollouts.>", "$KV.mesh-controller_states-announced.>", "$SRV.INFO", "_INBOX.enrol.>", "mesh.again.>", "mesh.assignment.>", "mesh.events.dead.>", "mesh.mod.*.tool.>", "mesh.mod.mesh-controller.event.states", "mesh.node.>", "mesh.seat.mesh-controller.event.applied", "mesh.seat.mesh-controller.event.artifacts-gone", "mesh.seat.mesh-controller.event.assignment-changed", "mesh.seat.mesh-controller.event.built-before", "mesh.seat.mesh-controller.event.checked", "mesh.seat.mesh-controller.event.condition-changed", "mesh.seat.mesh-controller.event.condition-cleared", "mesh.seat.mesh-controller.event.condition-raised", "mesh.seat.mesh-controller.event.declared-config-changed", "mesh.seat.mesh-controller.event.doctor-heartbeat", "mesh.seat.mesh-controller.event.healer-acted", "mesh.seat.mesh-controller.event.plan-moved", "mesh.seat.mesh-controller.event.predefined-changed", "mesh.seat.mesh-controller.event.refused", "mesh.seat.mesh-controller.event.rolled-back", "mesh.seat.mesh-controller.event.secret-replaced", "mesh.seat.mesh-controller.event.sent", "mesh.seat.mesh-controller.tool.plans", "mesh.seat.mesh-controller.tool.push", "mesh.seat.mesh-controller.tool.upgrade", "mesh.seat.mesh-delivery.tool.close", "mesh.seat.mesh-delivery.tool.groups", "mesh.seat.mesh-delivery.tool.release", "mesh.seat.mesh-delivery.tool.stalled", "mesh.seat.mesh-delivery.tool.stop", "mesh.seat.mesh-delivery.tool.times", "mesh.seat.node-backup.tool.backed-up.*", "mesh.seat.node-backup.tool.now.*", "mesh.seat.node-intrusion-prevention.tool.banned.*", "mesh.seat.node-launcher.tool.secret.*", "mesh.seat.node-service-manager.tool.restart.*", "mesh.seat.operator-channel.tool.tell"] } subscribe: { allow: ["$JS.API.>", "$JS.EVENT.ADVISORY.CONSUMER.DELETED.>", "$JS.EVENT.ADVISORY.CONSUMER.MAX_DELIVERIES.>", "$SRV.INFO", "$SRV.INFO.mesh-controller", "$SRV.INFO.mesh-controller.>", "$SRV.PING", "$SRV.PING.mesh-controller", "$SRV.PING.mesh-controller.>", "$SRV.STATS", "$SRV.STATS.mesh-controller", "$SRV.STATS.mesh-controller.>", "_DELIVER.controller", "_DELIVER.controller.>", "_INBOX.controller.>", "mesh.control.>", "mesh.mod.*.event.provisioner.failing", "mesh.mod.*.event.provisioner.recovered", "mesh.mod.*.event.provisioner.retirement", "mesh.mod.gitea.event.pull.merged", "mesh.mod.gitea.event.pull.updated", "mesh.mod.mesh-catalog.event.catching-up", "mesh.mod.mesh-catalog.event.upgraded", "mesh.seat.mesh-build-machine.event.built", "mesh.seat.mesh-controller.tool.>", "mesh.seat.node-build-agent.event.built", "mesh.seat.operator-channel.event.decided.mesh-controller"] } allow_responses: { max: 1, ttl: "1m" } } } diff --git a/internal/link/events.go b/internal/link/events.go index eb86e4c6..551a9039 100644 --- a/internal/link/events.go +++ b/internal/link/events.go @@ -89,6 +89,9 @@ const ( KeyDeclaredConfigChanged = "declared-config-changed" // KeyPredefinedChanged: the predefined-keys mirror's one key was written (design 57 K3): a newer list kept. KeyPredefinedChanged = "predefined-changed" + // KeyArtifactsGone: a builder's key was not taken in because the artifact store does not hold an artifact it names + // (novox/hq issue 1937): the module, the key, its fingerprint and every gone reference. The builder builds the key again. + KeyArtifactsGone = "artifacts-gone" ) // Applied is what a machine now runs, as the mesh states it. diff --git a/module.json b/module.json index 169d08cf..d41ce99e 100644 --- a/module.json +++ b/module.json @@ -102,6 +102,7 @@ "data", "build", "artifacts", + "held", "collect", "images", "mirrors", diff --git a/testdata/beside/CAPTURED b/testdata/beside/CAPTURED index e8220a86..c4cff530 100644 --- a/testdata/beside/CAPTURED +++ b/testdata/beside/CAPTURED @@ -9,7 +9,7 @@ a second place to clean besides the catalogue itself. mesh-catalog 2246ccc492e59579a140752e6f574b2121d296af modules/*/module.json, modules/*/files, modules/*/templates, predefined.json mesh-host 9d9e3a46c4f5943cfffb413d962ce48b75e2fd39 examples/foundation-first-node.lock -mesh-sdk a2c813a6b468899bb5a95666b8f6f305be52f200 conformance/events/module-event.json +mesh-sdk 34f210e5eba1d16abe06b526de1026c282692b83 conformance/events/module-event.json To move them, from this repository's root, with the two repositories checked out beside it: