// 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 }