From 902e3c9e96863c93499bf4634a3a39fe718d1ec3 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 28 Oct 2026 18:09:38 +0100 Subject: [PATCH] =?UTF-8?q?Refuse=20placement=20files=20on=20a=20forge=20m?= =?UTF-8?q?essage=20and=20delete=20placement-seed:=20no=20repository=20hol?= =?UTF-8?q?ds=20placement=20(ADR=200567=20=C2=A711,=20issue=201126)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go/cmd/placement-seed/main.go | 139 ----------------------------- go/cmd/placement-seed/main_test.go | 61 ------------- go/delivery/forge_inputs_test.go | 23 ++--- go/delivery/source.go | 27 ++---- 4 files changed, 15 insertions(+), 235 deletions(-) delete mode 100644 go/cmd/placement-seed/main.go delete mode 100644 go/cmd/placement-seed/main_test.go diff --git a/go/cmd/placement-seed/main.go b/go/cmd/placement-seed/main.go deleted file mode 100644 index 93fae61..0000000 --- a/go/cmd/placement-seed/main.go +++ /dev/null @@ -1,139 +0,0 @@ -// placement-seed writes the catalogue's first placement files (novox/hq ADR 0549 §4, ADR 0555 §6, issue 1126) from the -// answer of the controller's one read-only verb `placement-export`: one `placement/.json` per node, each the -// node's placement as the controller's own code read it from its own store, checked as the catalogue's check checks it -// (delivery.CheckPlacements) before anything is written. The pull request it makes merges only when its composition check -// composes every node equal to what it runs. -// -// placement-seed -catalogue < export.json -// placement-seed -catalogue -export export.json -// -// export.json is the verb's answer as it prints it ({"": , ...}, or the verb's wrapping with it under -// "answer"). It opens no network connection, bus or store; it writes only /placement/.json, and refuses to -// write over a placement file already there. Exit codes: 0 written; 1 the export cannot be read, is refused by the -// check, or a file is there; 2 the flags are wrong. -package main - -import ( - "encoding/json" - "errors" - "flag" - "fmt" - "io" - "os" - "path/filepath" - "sort" - - "git.novox.be/novox/mesh-sdk/go/delivery" -) - -func main() { - catalogue := flag.String("catalogue", "", "the checkout of the catalogue to write placement/.json into") - export := flag.String("export", "", "the verb's answer; standard input when absent") - flag.Parse() - if *catalogue == "" || flag.NArg() > 0 { - fmt.Fprintln(os.Stderr, "usage: placement-seed -catalogue [-export ]") - os.Exit(2) - } - in := io.Reader(os.Stdin) - if *export != "" { - f, err := os.Open(*export) - if err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } - defer f.Close() - in = f - } - written, err := Seed(in, *catalogue) - for _, path := range written { - fmt.Println(path) - } - if err != nil { - fmt.Fprintln(os.Stderr, "placement-seed:", err) - os.Exit(1) - } -} - -// Seed reads the export and writes each node's placement file under root, answering the paths written. Nothing is -// written when the export does not read, the check refuses it, or a placement file is already there. -func Seed(export io.Reader, root string) ([]string, error) { - raw, err := io.ReadAll(export) - if err != nil { - return nil, err - } - placements, err := read(raw) - if err != nil { - return nil, err - } - all := func(string) bool { return true } - if refused := delivery.CheckPlacements(placements, all, all); len(refused) > 0 { - return nil, fmt.Errorf("the export is refused by the catalogue's check: %v", refused) - } - nodes := make([]string, 0, len(placements)) - for node := range placements { - nodes = append(nodes, node) - } - sort.Strings(nodes) - files := map[string][]byte{} - for _, node := range nodes { - p := placements[node] - p.Revision = "" // only the indexer writes a revision, into the key - if _, err := delivery.EncodePlacement(p); err != nil { - return nil, fmt.Errorf("%s: %w", node, err) - } - p.V = delivery.Version - body, err := json.MarshalIndent(p, "", " ") - if err != nil { - return nil, err - } - path := filepath.Join(root, filepath.FromSlash(delivery.PlacementFile(node))) - if _, err := os.Stat(path); err == nil { - return nil, fmt.Errorf("%s is already there; the seed is written once (ADR 0549 §4)", path) - } else if !errors.Is(err, os.ErrNotExist) { - return nil, err - } - files[path] = append(body, '\n') - } - var written []string - for _, node := range nodes { - path := filepath.Join(root, filepath.FromSlash(delivery.PlacementFile(node))) - if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { - return written, err - } - if err := os.WriteFile(path, files[path], 0o644); err != nil { - return written, err - } - written = append(written, path) - } - return written, nil -} - -// read is the export as the verb prints it, or wrapped as a verb's answer (the controller's verbAnswer: its JSON under -// "answer"). -func read(raw []byte) (map[string]delivery.Placement, error) { - var wrapped struct { - Answer json.RawMessage `json:"answer"` - } - if json.Unmarshal(raw, &wrapped) == nil && len(wrapped.Answer) > 0 { - raw = wrapped.Answer - } - var byNode map[string]json.RawMessage - if err := json.Unmarshal(raw, &byNode); err != nil { - return nil, fmt.Errorf("the export is not {: }: %w", err) - } - if len(byNode) == 0 { - return nil, errors.New("the export names no node") - } - out := map[string]delivery.Placement{} - for node, body := range byNode { - p, err := delivery.DecodePlacement(body) - if err != nil { - return nil, fmt.Errorf("%s: %w", node, err) - } - if p.Node != node { - return nil, fmt.Errorf("the export's placement of %s names the node %q", node, p.Node) - } - out[node] = p - } - return out, nil -} diff --git a/go/cmd/placement-seed/main_test.go b/go/cmd/placement-seed/main_test.go deleted file mode 100644 index 156b4f4..0000000 --- a/go/cmd/placement-seed/main_test.go +++ /dev/null @@ -1,61 +0,0 @@ -package main - -import ( - "encoding/json" - "os" - "path/filepath" - "strings" - "testing" - - "git.novox.be/novox/mesh-sdk/go/compose" - "git.novox.be/novox/mesh-sdk/go/compose/frombus" - "git.novox.be/novox/mesh-sdk/go/delivery" -) - -// Issue 1126 (ADR 0549 §4, ADR 0555 §6): the seed writes one placement file per node of the export, as the catalogue's -// check and the indexer read it, from the verb's answer bare or wrapped; it writes nothing for an export the check -// refuses, and never over a file already there. -func TestTheSeedWritesEachNodesPlacementFileOnce(t *testing.T) { - export := `{"answer":{"ace":{"v":1,"node":"ace","revision":"x","decided":{"address":"10.77.0.2"},"modules":[{"module":"plex","ports":{"32400":32400}}]},` + - `"novox":{"v":1,"node":"novox","decided":{"hub":true,"address":"10.77.0.1"},"modules":[]}}}` - root := t.TempDir() - written, err := Seed(strings.NewReader(export), root) - if err != nil || len(written) != 2 { - t.Fatalf("written %v, %v", written, err) - } - raw, err := os.ReadFile(filepath.Join(root, "placement", "ace.json")) - if err != nil { - t.Fatal(err) - } - p, err := delivery.DecodePlacement(raw) - if err != nil || p.Node != "ace" || p.Revision != "" || p.HostPorts()["plex"][32400] != 32400 { - t.Errorf("ace's file reads %+v, %v", p, err) - } - if _, err := Seed(strings.NewReader(export), root); err == nil || !strings.Contains(err.Error(), "already there") { - t.Errorf("a second seed over the files: %v", err) - } - clash := `{"ace":{"v":1,"node":"ace","decided":{"address":"10.77.0.1"},"modules":[]},"novox":{"v":1,"node":"novox","decided":{"address":"10.77.0.1"},"modules":[]}}` - other := t.TempDir() - if _, err := Seed(strings.NewReader(clash), other); err == nil || !strings.Contains(err.Error(), "both have the address") { - t.Errorf("an export the check refuses: %v", err) - } - if entries, _ := os.ReadDir(filepath.Join(other, "placement")); len(entries) != 0 { - t.Errorf("a refused export wrote %v", entries) - } - // What the controller's placement-export prints, frombus.Export per node, is what the seed takes: a node with no - // module placed included. - in := compose.Input{Nodes: []compose.Node{{Name: "solo", Place: compose.Place{Hub: true, Address: "10.77.0.9"}}}} - exported, err := frombus.Export(in, "solo") - if err != nil { - t.Fatal(err) - } - answer, _ := json.Marshal(map[string]any{"answer": map[string]delivery.Placement{"solo": exported}}) - if written, err := Seed(strings.NewReader(string(answer)), t.TempDir()); err != nil || len(written) != 1 { - t.Errorf("Export's answer %s was not seeded: %v, %v", answer, written, err) - } - for _, bad := range []string{`[]`, `{}`, `{"ace":{"v":1,"node":"novox","modules":[]}}`} { - if _, err := Seed(strings.NewReader(bad), t.TempDir()); err == nil { - t.Errorf("%s was seeded", bad) - } - } -} diff --git a/go/delivery/forge_inputs_test.go b/go/delivery/forge_inputs_test.go index 633913b..5238339 100644 --- a/go/delivery/forge_inputs_test.go +++ b/go/delivery/forge_inputs_test.go @@ -9,10 +9,9 @@ const oddManifest = "{\n\t\"module\": \"a\",\n \"z\": 1, \"a\": [ \"x\",\"y\" func mergedWithInputs() Forge { return Forge{Kind: ForgeMerged, Repository: "novox/mesh-sdk", Number: 36, Head: "aaaa", Merge: "cccc", Root: "t0", - Trees: map[string]string{"a": "t1", "b": "t2"}, - Manifests: map[string]string{"a": oddManifest}, - Locks: map[string]map[string]string{"a": {"go.sum": strings.Repeat("ab", 32)}}, - Placements: map[string]string{"ace": `{"v":1,"node":"ace","modules":[]}`}, PlacementsRead: true} + Trees: map[string]string{"a": "t1", "b": "t2"}, + Manifests: map[string]string{"a": oddManifest}, + Locks: map[string]map[string]string{"a": {"go.sum": strings.Repeat("ab", 32)}}} } // 1442/AC1 @@ -22,7 +21,7 @@ func TestForgeMergedCarriesManifestsAndLocks(t *testing.T) { if err != nil { t.Fatal(err) } - for _, frag := range []string{`"manifests":{"a":`, `"locks":{"a":{"go.sum":`, `"placements":{"ace":`, `"placements-read":true`} { + for _, frag := range []string{`"manifests":{"a":`, `"locks":{"a":{"go.sum":`} { if !strings.Contains(string(data), frag) { t.Errorf("the encoding lacks %s: %s", frag, data) } @@ -48,9 +47,9 @@ func TestForgeMergedRefusesBadInputs(t *testing.T) { "locks outside trees": {func(f *Forge) { f.Locks["yy"] = map[string]string{"go.sum": "h"} }, `"yy"`}, "empty manifest": {func(f *Forge) { f.Manifests["b"] = "" }, `"b"`}, "empty lock hash": {func(f *Forge) { f.Locks["a"]["go.sum"] = "" }, `"a"`}, - "placements unsaid": {func(f *Forge) { f.PlacementsRead = false }, "placements-read"}, - "placement of no node": {func(f *Forge) { f.Placements["a.b"] = "{}" }, "names no node"}, - "placements on a pull": {func(f *Forge) { f.Kind, f.Action, f.Merge, f.Root, f.Trees = ForgePull, PullOpened, "", "", nil }, "only a merged message"}, + // No repository holds placement (ADR 0567 §11): a message carrying placement files is refused, on a merge or a pull. + "placement files": {func(f *Forge) { f.RetiredPlacements = []byte(`{"anchor":"{}"}`) }, "mesh-placement"}, + "placements read": {func(f *Forge) { f.RetiredRead = true }, "mesh-placement"}, } { f := mergedWithInputs() tc.mut(&f) @@ -86,14 +85,6 @@ func TestForgePullCarriesManifestsAndLocks(t *testing.T) { if _, err := EncodeForge(f); err == nil || !strings.Contains(err.Error(), `"c"`) { t.Errorf("locks outside Dirs: %v", err) } - // A pull message of the catalogue carries its head's placement files whole (placement candidates, issue 1126). - f = Forge{Kind: ForgePull, Repository: CatalogueRepository, Number: 2, Head: "h", Action: PullUpdated, - Placements: map[string]string{"ace": `{"v":1,"node":"ace","modules":[]}`}, PlacementsRead: true} - if data, err := EncodeForge(f); err != nil { - t.Errorf("a catalogue pull with placement files: %v", err) - } else if got, err := DecodeForge(data); err != nil || !SameForge(got, withV(f)) { - t.Errorf("a catalogue pull's placement files changed on the way: %+v, %v", got, err) - } for name, f := range map[string]Forge{ "refused manifests": {Kind: ForgeRefused, Repository: "a/b", Act: "x", Words: "no", Manifests: map[string]string{"a": "{}"}}, "refused locks": {Kind: ForgeRefused, Repository: "a/b", Act: "x", Words: "no", Locks: map[string]map[string]string{"a": {"go.sum": "h"}}}, diff --git a/go/delivery/source.go b/go/delivery/source.go index ebff8b3..6b0d25c 100644 --- a/go/delivery/source.go +++ b/go/delivery/source.go @@ -537,13 +537,11 @@ type Forge struct { Act string `json:"act,omitempty"` Words string `json:"words,omitempty"` Open []OpenPull `json:"open,omitempty"` - // Placements are, on a merged message of the catalogue repository, every placement file of the merge commit, the - // bytes exact, by node (PlacementFile; novox/hq ADR 0549 §2), and PlacementsRead says they were read, so a catalogue - // whose last placement file was removed is told from a message that read none (older writers, another repository). - // On a pull message of the catalogue (CatalogueRepository) they are every placement file at the pull request's head, - // read whole or not at all, which the indexer writes as placement candidates (PlacementCandidate). - Placements map[string]string `json:"placements,omitempty"` - PlacementsRead bool `json:"placements-read,omitempty"` + // RetiredPlacements is the placement files a bridge before ADR 0567 carried (ADR 0549 §2, ADR 0559 §2): placement is + // the seat mesh-placement's state now and no repository holds it (ADR 0567 §11), so a message carrying them is + // refused, loudly, rather than read with them dropped. + RetiredPlacements json.RawMessage `json:"placements,omitempty"` + RetiredRead bool `json:"placements-read,omitempty"` } func (f Forge) check() error { @@ -556,18 +554,9 @@ func (f Forge) check() error { if len(f.Open) > 0 { return fmt.Errorf("delivery: only an open message names open pull requests") } - // A pull message of the catalogue carries its head's placement files, so its change of placement is composed and - // answered by the nodes it places before it merges (placement candidates, issue 1126; ADR 0549 §4). - if (len(f.Placements) > 0 || f.PlacementsRead) && f.Kind != ForgeMerged && (f.Kind != ForgePull || f.Repository != CatalogueRepository) { - return fmt.Errorf("delivery: only a merged message, or a pull message of the catalogue, carries placement files") - } - if len(f.Placements) > 0 && !f.PlacementsRead { - return fmt.Errorf("delivery: a message with placement files must say it read them (placements-read)") - } - for node := range f.Placements { - if err := checkName("node", node); err != nil { - return fmt.Errorf("delivery: a placement file names no node: %w", err) - } + if len(f.RetiredPlacements) > 0 || f.RetiredRead { + return fmt.Errorf("delivery: a forge message carries placement files, which no repository holds since ADR 0567: " + + "placement is the seat mesh-placement's state") } if err := checkRepository(f.Repository); err != nil { return err