diff --git a/contracts/go/seats/meshartifactsweep/contract.go b/contracts/go/seats/meshartifactsweep/contract.go new file mode 100644 index 00000000..8aa87d21 --- /dev/null +++ b/contracts/go/seats/meshartifactsweep/contract.go @@ -0,0 +1,129 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// Package meshartifactsweep is the contract of the seat "mesh-artifact-sweep", defined whole (seats/mesh-artifact-sweep/seat.json): no description. +package meshartifactsweep + +import "time" + +// Seat is the seat's name. +const Seat = "mesh-artifact-sweep" + +const ( + VerbArtifacts = "artifacts" + VerbHeld = "held" + VerbCollect = "collect" + VerbMirrors = "mirrors" + VerbImages = "images" +) + +const ( + EventArtifactsGone = "artifacts-gone" +) + +// Verbs is every verb the seat serves, in its definition's order: a new copy each call. +func Verbs() []string { + return []string{VerbArtifacts, VerbHeld, VerbCollect, VerbMirrors, VerbImages} +} + +// Events is every event published on the seat's subject: a new copy each call. +func Events() []string { + return []string{EventArtifactsGone} +} + +// Family is where the seat's events are kept: the shared events stream, filtered to the seat's subjects. +const ( + Family = "mesh-artifact-sweep.events" + FamilyMaxAgeDays = 7 +) + +// SubjectArtifactsGone is where artifacts-gone is published. +const SubjectArtifactsGone = "mesh.seat.mesh-artifact-sweep.event.artifacts-gone" + +// ArtifactsGone is the body of artifacts-gone. +type ArtifactsGone struct { + // when the sweep found it + At *time.Time `json:"at,omitempty"` + // artifacts-gone + Event string `json:"event"` + // the key's fingerprint + Fingerprint string `json:"fingerprint"` + // every reference of the key the store does not hold, sorted + Gone []string `json:"gone"` + // the builder's done key + Key string `json:"key,omitempty"` + // the module whose builder's key names a gone artifact + Module string `json:"module"` +} + +// ArtifactsInput is what artifacts takes. +type ArtifactsInput struct { + // "true": list the collected references too (counted always) + Collected string `json:"collected,omitempty"` + // only this repository's references, as / (the counts stay whole) + Repository string `json:"repository,omitempty"` +} + +// The values of ArtifactsInput's collected. +const ( + ArtifactsInputCollectedTrue = "true" + ArtifactsInputCollectedFalse = "false" +) + +// ArtifactsAnswer is what artifacts answers. +type ArtifactsAnswer = map[string]any + +// HeldInput is what held takes. +type HeldInput struct { + // the artifacts' references, artifact-store:////blobs/sha256:: a list, or one text separated by commas + References []string `json:"references"` +} + +// HeldAnswer is what held answers. +type HeldAnswer struct { + // the references the store answered 404 for + Gone []string `json:"gone"` + // the references the store holds + Held []string `json:"held"` + // the store asked, host and port + Store string `json:"store,omitempty"` + // a reference the store could not be asked about, to why + Unknown map[string]string `json:"unknown,omitempty"` +} + +// CollectInput is what collect takes. +type CollectInput struct { + // "true": let go of what is eligible (needs why); without it nothing is done + Confirm string `json:"confirm,omitempty"` + // let go of at most this many (500 by default, at most 5000) + Most string `json:"most,omitempty"` + // with confirm: why, required and recorded + Why string `json:"why,omitempty"` +} + +// The values of CollectInput's confirm. +const ( + CollectInputConfirmTrue = "true" + CollectInputConfirmFalse = "false" +) + +// CollectAnswer is what collect answers. +type CollectAnswer = map[string]any + +// MirrorsInput is what mirrors takes. +type MirrorsInput struct { +} + +// MirrorsAnswer is what mirrors answers. +type MirrorsAnswer = map[string]any + +// ImagesInput is what images takes. +type ImagesInput struct { + // the machine's name + Node string `json:"node"` +} + +// ImagesAnswer is what images answers. +type ImagesAnswer = map[string]any + +// ArtifactsGoneEvent is the body of the event artifacts-gone. +type ArtifactsGoneEvent = ArtifactsGone diff --git a/contracts/go/seats/meshartifactsweep/contract_test.go b/contracts/go/seats/meshartifactsweep/contract_test.go new file mode 100644 index 00000000..ea7a5f1f --- /dev/null +++ b/contracts/go/seats/meshartifactsweep/contract_test.go @@ -0,0 +1,54 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +package meshartifactsweep + +import ( + "encoding/json" + "reflect" + "regexp" + "testing" +) + +// TestFixtures reads every fixture of seats/mesh-artifact-sweep/fixtures: each decodes into its type, a field the type does not +// know is dropped (ADR 0608 §6), and it reads back as its wire says; each subject named is the contract's. +func TestFixtures(t *testing.T) { + cases := []struct { + name, subject, given, wire string + decode func([]byte) (any, error) + subjectIs *regexp.Regexp + }{ + {name: "artifacts-gone on the seat's subject", subject: "mesh.seat.mesh-artifact-sweep.event.artifacts-gone", given: "{\"at\":\"2026-11-05T15:04:05Z\",\"event\":\"artifacts-gone\",\"fingerprint\":\"f1\",\"gone\":[\"artifact-store://ca-trust/archive/blobs/sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"artifact-store://ca-trust/archive/blobs/sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"key\":\"ca-trust.archive.f1\",\"module\":\"ca-trust\"}", wire: "{\"at\":\"2026-11-05T15:04:05Z\",\"event\":\"artifacts-gone\",\"fingerprint\":\"f1\",\"gone\":[\"artifact-store://ca-trust/archive/blobs/sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"artifact-store://ca-trust/archive/blobs/sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"key\":\"ca-trust.archive.f1\",\"module\":\"ca-trust\"}", + decode: func(b []byte) (any, error) { var v ArtifactsGoneEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-artifact-sweep\\.event\\.artifacts-gone$")}, + {name: "held asks the references", subject: "", given: "{\"references\":[\"artifact-store://ca-trust/archive/blobs/sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"artifact-store://ca-trust/archive/blobs/sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"artifact-store://ca-trust/archive/blobs/sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\"]}", wire: "{\"references\":[\"artifact-store://ca-trust/archive/blobs/sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"artifact-store://ca-trust/archive/blobs/sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"artifact-store://ca-trust/archive/blobs/sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\"]}", + decode: func(b []byte) (any, error) { var v HeldInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "held answers held, gone and unknown", subject: "", given: "{\"gone\":[\"artifact-store://ca-trust/archive/blobs/sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"held\":[\"artifact-store://ca-trust/archive/blobs/sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"],\"store\":\"store.test:5100\",\"unknown\":{\"artifact-store://ca-trust/archive/blobs/sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\":\"the store answered 503\"}}", wire: "{\"gone\":[\"artifact-store://ca-trust/archive/blobs/sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"held\":[\"artifact-store://ca-trust/archive/blobs/sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"],\"store\":\"store.test:5100\",\"unknown\":{\"artifact-store://ca-trust/archive/blobs/sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\":\"the store answered 503\"}}", + decode: func(b []byte) (any, error) { var v HeldAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "held answers with nothing unknown", subject: "", given: "{\"gone\":[],\"held\":[],\"store\":\"store.test:5100\"}", wire: "{\"gone\":[],\"held\":[],\"store\":\"store.test:5100\"}", + decode: func(b []byte) (any, error) { var v HeldAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + } + for _, c := range cases { + v, err := c.decode([]byte(c.given)) + if err != nil { + t.Errorf("%s: does not decode: %v", c.name, err) + continue + } + back, err := json.Marshal(v) + if err != nil { + t.Errorf("%s: does not encode: %v", c.name, err) + continue + } + var got, want any + if err := json.Unmarshal(back, &got); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal([]byte(c.wire), &want); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("%s: reads back as %s, want %s", c.name, back, c.wire) + } + if c.subjectIs != nil && !c.subjectIs.MatchString(c.subject) { + t.Errorf("%s: subject %q is not the contract's %s", c.name, c.subject, c.subjectIs) + } + } +} diff --git a/contracts/go/seats/meshartifactsweep/go.mod b/contracts/go/seats/meshartifactsweep/go.mod new file mode 100644 index 00000000..77f0649b --- /dev/null +++ b/contracts/go/seats/meshartifactsweep/go.mod @@ -0,0 +1,3 @@ +module git.novox.be/novox/mesh-delivery/contracts/go/seats/meshartifactsweep + +go 1.26.0 diff --git a/contracts/module.json b/contracts/module.json index d80c383c..09be597e 100644 --- a/contracts/module.json +++ b/contracts/module.json @@ -23,6 +23,16 @@ "series": "0.1", "generated-from": "." }, + { + "name": "go-meshartifactsweep", + "kind": "package", + "language": "go", + "paths": [ + "go/seats/meshartifactsweep" + ], + "series": "0.1", + "generated-from": "." + }, { "name": "go-meshcatalog", "kind": "package", @@ -186,6 +196,16 @@ "series": "0.1", "generated-from": "." }, + { + "name": "ts-meshartifactsweep", + "kind": "package", + "language": "typescript", + "paths": [ + "ts/seats/meshartifactsweep" + ], + "series": "0.1", + "generated-from": "." + }, { "name": "ts-meshcatalog", "kind": "package", diff --git a/contracts/seats/mesh-artifact-sweep/fixtures/events.json b/contracts/seats/mesh-artifact-sweep/fixtures/events.json new file mode 100644 index 00000000..3e39336a --- /dev/null +++ b/contracts/seats/mesh-artifact-sweep/fixtures/events.json @@ -0,0 +1,15 @@ +[ + { + "name": "artifacts-gone on the seat's subject", + "of": "event:artifacts-gone", + "subject": "mesh.seat.mesh-artifact-sweep.event.artifacts-gone", + "given": { + "event": "artifacts-gone", + "module": "ca-trust", + "key": "ca-trust.archive.f1", + "fingerprint": "f1", + "gone": ["artifact-store://ca-trust/archive/blobs/sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "artifact-store://ca-trust/archive/blobs/sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"], + "at": "2026-11-05T15:04:05Z" + } + } +] diff --git a/contracts/seats/mesh-artifact-sweep/fixtures/states.json b/contracts/seats/mesh-artifact-sweep/fixtures/states.json new file mode 100644 index 00000000..fe51488c --- /dev/null +++ b/contracts/seats/mesh-artifact-sweep/fixtures/states.json @@ -0,0 +1 @@ +[] diff --git a/contracts/seats/mesh-artifact-sweep/fixtures/verbs.json b/contracts/seats/mesh-artifact-sweep/fixtures/verbs.json new file mode 100644 index 00000000..b70970ee --- /dev/null +++ b/contracts/seats/mesh-artifact-sweep/fixtures/verbs.json @@ -0,0 +1,24 @@ +[ + { + "name": "held asks the references", + "of": "verb:held.input", + "why": "the references are the holder's own test values (the tool refuses a call without them through the console)", + "given": {"references": ["artifact-store://ca-trust/archive/blobs/sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "artifact-store://ca-trust/archive/blobs/sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "artifact-store://ca-trust/archive/blobs/sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"]} + }, + { + "name": "held answers held, gone and unknown", + "of": "verb:held.answer", + "why": "the answer of the holder's test (cmd/artifact-store-tools): every reference in one of the three", + "given": { + "store": "store.test:5100", + "held": ["artifact-store://ca-trust/archive/blobs/sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"], + "gone": ["artifact-store://ca-trust/archive/blobs/sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"], + "unknown": {"artifact-store://ca-trust/archive/blobs/sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc": "the store answered 503"} + } + }, + { + "name": "held answers with nothing unknown", + "of": "verb:held.answer", + "given": {"store": "store.test:5100", "held": [], "gone": []} + } +] diff --git a/contracts/seats/mesh-artifact-sweep/seat.json b/contracts/seats/mesh-artifact-sweep/seat.json index 2e5b673d..4763ce8d 100644 --- a/contracts/seats/mesh-artifact-sweep/seat.json +++ b/contracts/seats/mesh-artifact-sweep/seat.json @@ -2,7 +2,7 @@ "format": 1, "seat": "mesh-artifact-sweep", "scope": "mesh", - "grain": "seat-list", + "grain": "whole", "domain": "Delivery", "decision": "novox/hq ADR 0571", "verbs": [ @@ -25,6 +25,10 @@ } }, "type": "object" + }, + "answer": { + "type": "object", + "description": "what artifacts answers, as its description says" } }, { @@ -44,6 +48,41 @@ "references" ], "type": "object" + }, + "answer": { + "type": "object", + "properties": { + "store": { + "type": "string", + "description": "the store asked, host and port" + }, + "held": { + "type": "array", + "items": { + "type": "string" + }, + "description": "the references the store holds" + }, + "gone": { + "type": "array", + "items": { + "type": "string" + }, + "description": "the references the store answered 404 for" + }, + "unknown": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "a reference the store could not be asked about, to why" + } + }, + "required": [ + "held", + "gone" + ], + "description": "what held answers: a reference is in held, gone or unknown, never none of them" } }, { @@ -69,6 +108,10 @@ } }, "type": "object" + }, + "answer": { + "type": "object", + "description": "what collect answers, as its description says" } }, { @@ -77,6 +120,10 @@ "input": { "properties": {}, "type": "object" + }, + "answer": { + "type": "object", + "description": "what mirrors answers, as its description says" } }, { @@ -93,12 +140,65 @@ "node" ], "type": "object" + }, + "answer": { + "type": "object", + "description": "what images answers, as its description says" } } ], + "family": { + "name": "mesh-artifact-sweep.events", + "max-age-days": 7 + }, "events": [ { - "name": "artifacts-gone" + "name": "artifacts-gone", + "body": { + "$ref": "#/defs/ArtifactsGone" + } } - ] + ], + "defs": { + "ArtifactsGone": { + "type": "object", + "properties": { + "event": { + "type": "string", + "description": "artifacts-gone" + }, + "module": { + "type": "string", + "description": "the module whose builder's key names a gone artifact" + }, + "key": { + "type": "string", + "description": "the builder's done key" + }, + "fingerprint": { + "type": "string", + "description": "the key's fingerprint" + }, + "gone": { + "type": "array", + "items": { + "type": "string" + }, + "description": "every reference of the key the store does not hold, sorted" + }, + "at": { + "type": "string", + "format": "date-time", + "description": "when the sweep found it" + } + }, + "required": [ + "event", + "module", + "fingerprint", + "gone" + ], + "description": "the body of artifacts-gone" + } + } } diff --git a/contracts/ts/seats/meshartifactsweep/package-lock.json b/contracts/ts/seats/meshartifactsweep/package-lock.json new file mode 100644 index 00000000..2d3064d4 --- /dev/null +++ b/contracts/ts/seats/meshartifactsweep/package-lock.json @@ -0,0 +1,12 @@ +{ + "lockfileVersion": 3, + "name": "@novox/mesh-delivery-contracts-mesh-artifact-sweep", + "packages": { + "": { + "name": "@novox/mesh-delivery-contracts-mesh-artifact-sweep", + "version": "0.1.0" + } + }, + "requires": true, + "version": "0.1.0" +} diff --git a/contracts/ts/seats/meshartifactsweep/package.json b/contracts/ts/seats/meshartifactsweep/package.json new file mode 100644 index 00000000..14c46319 --- /dev/null +++ b/contracts/ts/seats/meshartifactsweep/package.json @@ -0,0 +1,13 @@ +{ + "description": "The contract of the seat mesh-artifact-sweep, generated from its definition.", + "exports": { + ".": "./src/index.ts" + }, + "files": [ + "src" + ], + "name": "@novox/mesh-delivery-contracts-mesh-artifact-sweep", + "type": "module", + "types": "./src/index.ts", + "version": "0.1.0" +} diff --git a/contracts/ts/seats/meshartifactsweep/src/index.ts b/contracts/ts/seats/meshartifactsweep/src/index.ts new file mode 100644 index 00000000..ad4d48a4 --- /dev/null +++ b/contracts/ts/seats/meshartifactsweep/src/index.ts @@ -0,0 +1,99 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// The contract of the seat "mesh-artifact-sweep", defined whole (seats/mesh-artifact-sweep/seat.json): no description. + +export const seat = "mesh-artifact-sweep"; + +export const verbs = { + artifacts: "artifacts", + held: "held", + collect: "collect", + mirrors: "mirrors", + images: "images", +} as const; + +export const events = { + artifactsGone: "artifacts-gone", +} as const; + +export const family = { name: "mesh-artifact-sweep.events", maxAgeDays: 7 } as const; + +export const artifactsGoneSubject = "mesh.seat.mesh-artifact-sweep.event.artifacts-gone"; + +/** the body of artifacts-gone */ +export interface ArtifactsGone { + /** when the sweep found it */ + "at"?: string; + /** artifacts-gone */ + "event": string; + /** the key's fingerprint */ + "fingerprint": string; + /** every reference of the key the store does not hold, sorted */ + "gone": string[]; + /** the builder's done key */ + "key"?: string; + /** the module whose builder's key names a gone artifact */ + "module": string; +} + +/** is what artifacts takes. */ +export interface ArtifactsInput { + /** "true": list the collected references too (counted always) */ + "collected"?: "true" | "false"; + /** only this repository's references, as / (the counts stay whole) */ + "repository"?: string; +} + +/** is what artifacts answers. */ +export type ArtifactsAnswer = Record; + +/** is what held takes. */ +export interface HeldInput { + /** the artifacts' references, artifact-store:////blobs/sha256:: a list, or one text separated by commas */ + "references": string[]; +} + +/** is what held answers. */ +export interface HeldAnswer { + /** the references the store answered 404 for */ + "gone": string[]; + /** the references the store holds */ + "held": string[]; + /** the store asked, host and port */ + "store"?: string; + /** a reference the store could not be asked about, to why */ + "unknown"?: Record; +} + +/** is what collect takes. */ +export interface CollectInput { + /** "true": let go of what is eligible (needs why); without it nothing is done */ + "confirm"?: "true" | "false"; + /** let go of at most this many (500 by default, at most 5000) */ + "most"?: string; + /** with confirm: why, required and recorded */ + "why"?: string; +} + +/** is what collect answers. */ +export type CollectAnswer = Record; + +/** is what mirrors takes. */ +export interface MirrorsInput { +} + +/** is what mirrors answers. */ +export type MirrorsAnswer = Record; + +/** is what images takes. */ +export interface ImagesInput { + /** the machine's name */ + "node": string; +} + +/** is what images answers. */ +export type ImagesAnswer = Record; + +/** is the body of the event artifacts-gone. */ +export type ArtifactsGoneEvent = ArtifactsGone; + diff --git a/contracts/ts/seats/meshartifactsweep/tsconfig.json b/contracts/ts/seats/meshartifactsweep/tsconfig.json new file mode 100644 index 00000000..60274c6a --- /dev/null +++ b/contracts/ts/seats/meshartifactsweep/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*.ts"] +} diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/gone_integration_test.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/gone_integration_test.go index bf59a418..ee342ee6 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/gone_integration_test.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/gone_integration_test.go @@ -12,6 +12,7 @@ import ( "github.com/nats-io/nats.go" "github.com/nats-io/nats.go/jetstream" + sweep "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshartifactsweep" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/delivery" ) @@ -35,7 +36,7 @@ func (h *heldVerb) serve(t *testing.T, b *bed) { References []string `json:"references"` } _ = json.Unmarshal(m.Data, &args) - a := delivery.HeldAnswer{Store: "store.test:5100", Held: []string{}, Gone: []string{}} + a := sweep.HeldAnswer{Store: "store.test:5100", Held: []string{}, Gone: []string{}} h.mu.Lock() for _, r := range args.References { h.asked = append(h.asked, r) @@ -69,7 +70,7 @@ func eventsConsumer(t *testing.T, b *bed, node string) { t.Fatal(err) } if _, err := b.js.CreateOrUpdateConsumer(b.ctx, eventsStream, jetstream.ConsumerConfig{Durable: node + "_mesh-delivery-builder", - AckPolicy: jetstream.AckExplicitPolicy, FilterSubjects: []string{delivery.ArtifactsGoneSubject}}); err != nil { + AckPolicy: jetstream.AckExplicitPolicy, FilterSubjects: []string{sweep.SubjectArtifactsGone}}); err != nil { t.Fatal(err) } } @@ -168,12 +169,13 @@ func TestArtifactsGoneLeadsTheBuilderToBuildTheKeyAgain(t *testing.T) { collected = true mu.Unlock() key, _ := delivery.BuilderBuildKey("ca-trust", fp) - body, err := delivery.EncodeArtifactsGone(delivery.ArtifactsGone{Module: "ca-trust", Key: key, Fingerprint: fp, - Gone: []string{ref}, At: time.Now().UTC()}) + at := time.Now().UTC() + body, err := json.Marshal(sweep.ArtifactsGone{Event: sweep.EventArtifactsGone, Module: "ca-trust", Key: key, Fingerprint: fp, + Gone: []string{ref}, At: &at}) if err != nil { t.Fatal(err) } - if _, err := b.js.Publish(b.ctx, delivery.ArtifactsGoneSubject, body); err != nil { + if _, err := b.js.Publish(b.ctx, sweep.SubjectArtifactsGone, body); err != nil { t.Fatal(err) } v := b.await(t, "ca-trust", func(v delivery.BuilderBuild) bool { diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/integration_test.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/integration_test.go index 795fd0c0..882a56a8 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/integration_test.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/integration_test.go @@ -28,6 +28,7 @@ import ( runner "git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep" "git.novox.be/novox/mesh-contracts/go/seats/gopackageregistry" "git.novox.be/novox/mesh-contracts/go/seats/ociartifactregistry" + sweep "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshartifactsweep" "git.novox.be/novox/mesh-sdk/go/buildstep" "git.novox.be/novox/mesh-sdk/go/buscompose" "git.novox.be/novox/mesh-sdk/go/delivery" diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/main.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/main.go index 927a8ad9..5559b78b 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/main.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/main.go @@ -26,6 +26,7 @@ import ( "github.com/nats-io/nats.go" "github.com/nats-io/nats.go/jetstream" + sweep "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshartifactsweep" "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/process" "git.novox.be/novox/mesh-sdk/go/views" @@ -352,7 +353,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro reader.Start(ctx, js, cred, process.AwaitBucket(ctx, js, conn, firstViewsBucket, "builder_first_views reads nothing and builder_first_view_again is not heard", logger.Printf)) // Every part has registered: the hub pulls the module's durable. Where the consumer is not there yet the builder says so // and binds it at a later change (heard.Writer; ADR 0615 §5). - _ = notHeard.Bind(eventsStream, delivery.ArtifactsGoneEvent+", what wakes a floor, the registry seats' words, the views and manifest-reads-changed are not heard until the builder starts again", func() error { + _ = notHeard.Bind(eventsStream, sweep.EventArtifactsGone+", what wakes a floor, the registry seats' words, the views and manifest-reads-changed are not heard until the builder starts again", func() error { return hub.Start(ctx, js, cred) }) go func() { <-ctx.Done(); hub.Stop() }() diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/refused_test.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/refused_test.go index 0e53e2f2..cc000338 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/refused_test.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/refused_test.go @@ -21,6 +21,7 @@ import ( "github.com/nats-io/nats.go/jetstream" "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + sweep "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshartifactsweep" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun" "git.novox.be/novox/mesh-delivery/sdk/enginereads" "git.novox.be/novox/mesh-delivery/sdk/forms" @@ -223,7 +224,7 @@ func aFloorsWakeIsHeard(t *testing.T) { if !f.heard(context.Background(), cleared) || !strings.Contains(logs.String(), "the floor of m/img is asked again: condition-cleared") { t.Fatalf("condition-cleared: log %q", logs.String()) } - if f.heard(context.Background(), &wakeMsg{subject: delivery.ArtifactsGoneSubject}) { + if f.heard(context.Background(), &wakeMsg{subject: sweep.SubjectArtifactsGone}) { t.Fatal("artifacts-gone was taken as a floor's wake") } // DLV-B4: a registry seat's outcome on the same consumer moves a version; a published wakes a name whose seat was diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/stored.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/stored.go index c5fc876a..7c1a023e 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/stored.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/stored.go @@ -9,8 +9,8 @@ import ( "github.com/nats-io/nats.go/jetstream" + sweep "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshartifactsweep" "git.novox.be/novox/mesh-sdk/go/catalogue" - "git.novox.be/novox/mesh-sdk/go/delivery" "mesh-delivery-builder/internal/build" ) @@ -35,7 +35,7 @@ func storedVia(call toolCaller) func(ctx context.Context, references []string) ( if err != nil { return nil, fmt.Errorf("asking held: %w", err) } - a, err := delivery.DecodeHeldAnswer(raw, references) + a, err := decodeHeld(raw, references) if err != nil { return nil, err } @@ -54,13 +54,62 @@ func storedVia(call toolCaller) func(ctx context.Context, references []string) ( // heardGone takes one artifacts-gone event in: acknowledged, then handed to the builder, which asks the store and builds // the key again. A body that is not one is said and acknowledged, so it is not handed over again. func heardGone(ctx context.Context, b *build.Builder, m jetstream.Msg, logf func(string, ...any)) { - e, err := delivery.DecodeArtifactsGone(m.Data()) + e, err := decodeGone(m.Data()) if ackErr := m.Ack(); ackErr != nil { logf("acknowledging %s: %v", m.Subject(), ackErr) } if err != nil { - logf("%s holds what is not %s, left: %v", m.Subject(), delivery.ArtifactsGoneEvent, err) + logf("%s holds what is not %s, left: %v", m.Subject(), sweep.EventArtifactsGone, err) return } b.ArtifactsGone(ctx, e) } + +// decodeHeld reads the seat's answer to held: the verb answers {output, ok, answer}, the answer being the seat's +// HeldAnswer. A reference asked that the answer names in none of held, gone and unknown is an error: an answer that +// leaves one out would let a key with a gone blob be reused. +func decodeHeld(raw []byte, asked []string) (sweep.HeldAnswer, error) { + var wrapped struct { + Output string `json:"output"` + OK bool `json:"ok"` + Answer json.RawMessage `json:"answer"` + } + if err := json.Unmarshal(raw, &wrapped); err != nil { + return sweep.HeldAnswer{}, fmt.Errorf("the answer of %s is not {output, ok, answer}: %w", SeatHeld, err) + } + if !wrapped.OK || len(wrapped.Answer) == 0 { + return sweep.HeldAnswer{}, fmt.Errorf("%s did not answer: %s", SeatHeld, wrapped.Output) + } + var a sweep.HeldAnswer + if err := json.Unmarshal(wrapped.Answer, &a); err != nil { + return sweep.HeldAnswer{}, fmt.Errorf("the answer of %s is not {held, gone}: %w", SeatHeld, err) + } + named := map[string]bool{} + for _, r := range a.Held { + named[r] = true + } + for _, r := range a.Gone { + named[r] = true + } + for r := range a.Unknown { + named[r] = true + } + for _, r := range asked { + if !named[r] { + return sweep.HeldAnswer{}, fmt.Errorf("the answer of %s names nothing of %s", SeatHeld, r) + } + } + return a, nil +} + +// decodeGone reads the event's body, refusing one that names no module, fingerprint or reference. +func decodeGone(raw []byte) (sweep.ArtifactsGone, error) { + var e sweep.ArtifactsGone + if err := json.Unmarshal(raw, &e); err != nil { + return sweep.ArtifactsGone{}, fmt.Errorf("%s is not one: %w", sweep.EventArtifactsGone, err) + } + if e.Event != sweep.EventArtifactsGone || e.Module == "" || e.Fingerprint == "" || len(e.Gone) == 0 { + return sweep.ArtifactsGone{}, fmt.Errorf("%s names no module, fingerprint or gone reference: %s", sweep.EventArtifactsGone, raw) + } + return e, nil +} diff --git a/modules/mesh-delivery-builder/go.mod b/modules/mesh-delivery-builder/go.mod index a493ed16..cfa50977 100644 --- a/modules/mesh-delivery-builder/go.mod +++ b/modules/mesh-delivery-builder/go.mod @@ -47,3 +47,7 @@ require ( ) replace git.novox.be/novox/mesh-delivery/sdk => ../../sdk + +require git.novox.be/novox/mesh-delivery/contracts/go/seats/meshartifactsweep v0.0.0-00010101000000-000000000000 + +replace git.novox.be/novox/mesh-delivery/contracts/go/seats/meshartifactsweep => ../../contracts/go/seats/meshartifactsweep diff --git a/modules/mesh-delivery-builder/internal/build/gone_test.go b/modules/mesh-delivery-builder/internal/build/gone_test.go index 499cdbc9..c114010a 100644 --- a/modules/mesh-delivery-builder/internal/build/gone_test.go +++ b/modules/mesh-delivery-builder/internal/build/gone_test.go @@ -9,6 +9,7 @@ import ( "sync" "testing" + sweep "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshartifactsweep" "git.novox.be/novox/mesh-sdk/go/delivery" ) @@ -184,14 +185,14 @@ func TestArtifactsGoneMakesTheBuilderAskAndBuildAgain(t *testing.T) { held.mu.Lock() held.gone[goneRef] = true // collected after the builder's look; the controller's take-in found it held.mu.Unlock() - r.b.ArtifactsGone(context.Background(), delivery.ArtifactsGone{Event: delivery.ArtifactsGoneEvent, Module: "m", + r.b.ArtifactsGone(context.Background(), sweep.ArtifactsGone{Event: sweep.EventArtifactsGone, Module: "m", Fingerprint: "src2:" + strings.Repeat("9", 64), Gone: []string{goneRef}}) r.b.Wait() if r.st.revs[r.key] != rev || !log.has("a fingerprint the module does not have now") { t.Fatalf("an event for another fingerprint moved the key (revision %d, was %d): %v", r.st.revs[r.key], rev, log.lines) } - r.b.ArtifactsGone(context.Background(), delivery.ArtifactsGone{Event: delivery.ArtifactsGoneEvent, Module: "m", + r.b.ArtifactsGone(context.Background(), sweep.ArtifactsGone{Event: sweep.EventArtifactsGone, Module: "m", Key: r.key, Fingerprint: fp1, Gone: []string{goneRef}}) r.b.Wait() v := valueOf(t, r.st, r.key) diff --git a/modules/mesh-delivery-builder/internal/build/run.go b/modules/mesh-delivery-builder/internal/build/run.go index fecf91c8..e651e436 100644 --- a/modules/mesh-delivery-builder/internal/build/run.go +++ b/modules/mesh-delivery-builder/internal/build/run.go @@ -13,6 +13,7 @@ import ( "sync" "time" + sweep "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshartifactsweep" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun" "git.novox.be/novox/mesh-delivery/sdk/forms" "git.novox.be/novox/mesh-sdk/go/buildstep" @@ -281,7 +282,7 @@ func (b *Builder) storedOnce(key string) func(ctx context.Context, references [] // that the store does not hold what the module's key names. The module is looked at again, which asks the store (ClaimOptions.Stored) and // builds the key again; an event for a fingerprint the module no longer has is said and left. The event only wakes the // builder: what is built rests on the store's answer, never on the event alone. -func (b *Builder) ArtifactsGone(ctx context.Context, e delivery.ArtifactsGone) { +func (b *Builder) ArtifactsGone(ctx context.Context, e sweep.ArtifactsGone) { b.init() b.mu.Lock() m, ok := b.known[e.Module] @@ -295,11 +296,11 @@ func (b *Builder) ArtifactsGone(ctx context.Context, e delivery.ArtifactsGone) { b.mu.Unlock() if !ok || m.FP2 != e.Fingerprint { b.logf("%s: heard %s for %s, a fingerprint the module does not have now: nothing to build", e.Module, - delivery.ArtifactsGoneEvent, e.Fingerprint) + sweep.EventArtifactsGone, e.Fingerprint) return } b.logf("%s: heard %s (%s): asking the artifact store and building the key again", e.Module, - delivery.ArtifactsGoneEvent, strings.Join(e.Gone, ", ")) + sweep.EventArtifactsGone, strings.Join(e.Gone, ", ")) b.consider(ctx, e.Module, m) }