mesh/delivery closed: a newer head of the same pull request
mesh/check failed: shanks: novox/mesh-catalog#505: checks/placement/go.mod names mesh-sdk f1d7c7377dc4, which is neither on mesh-sdk's trunk nor the g…
140 lines
4.6 KiB
Go
140 lines
4.6 KiB
Go
// 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/<node>.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 <checkout of mesh-catalog> < export.json
|
|
// placement-seed -catalogue <dir> -export export.json
|
|
//
|
|
// export.json is the verb's answer as it prints it ({"<node>": <placement>, ...}, or the verb's wrapping with it under
|
|
// "answer"). It opens no network connection, bus or store; it writes only <dir>/placement/<node>.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/<node>.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 <dir> [-export <file>]")
|
|
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 {<node>: <placement>}: %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
|
|
}
|