mesh-wants is the Delivery domain's seat, defined in the delivery contracts; the indexer knows no Placement domain (issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
module git.novox.be/novox/mesh-delivery/contracts/go/clients/meshwantsclient
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-sdk/go v0.0.0-00010101000000-000000000000
|
||||
)
|
||||
@@ -0,0 +1,326 @@
|
||||
// Package meshwantsclient is the client of the seat mesh-wants: the keys of its state wants, and the check and the codec
|
||||
// of a want, a node's own want and a hold, which every module that reads or writes wants shares (the assigner that writes
|
||||
// them, the node-engine, the provisioner and the artifact store that read them). The layouts are the seat's contract; this
|
||||
// file writes the version and refuses what the earlier codec refused (novox/hq ADR 0528, ADR 0539, ADR 0546, ADR 0634 §4).
|
||||
package meshwantsclient
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants"
|
||||
"git.novox.be/novox/mesh-sdk/go/keys"
|
||||
)
|
||||
|
||||
// The three values of the state wants, in the seat's types.
|
||||
type (
|
||||
// Want is a node's want for a module: the fingerprint it should run and the digest of the one composition it allows,
|
||||
// or Removed, or Self when the node composes it itself; Newest and Rule name what holds the node back.
|
||||
Want = seat.WantsByWantNodeModuleValue
|
||||
// NodeWant is a node's own want: the digest of the node's own composition it may apply.
|
||||
NodeWant = seat.WantsByWantNodeNodeValue
|
||||
// Held is a fingerprint the assigner holds: the node whose health check failed on it, and the check's words.
|
||||
Held = seat.WantsByHeldModuleFingerprintValue
|
||||
)
|
||||
|
||||
// The rules of ADR 0429 §6 and ADR 0539 clause 6, by number, as a want or a difference names the one that holds a node back.
|
||||
const (
|
||||
RuleBuildDone = 1 // the build of F is done
|
||||
RuleNotHeld = 2 // F is not held
|
||||
RuleCanary = 3 // the node is the module's canary, or the canary runs F healthy
|
||||
RuleStep = 4 // at most step other nodes are ahead (step 0: no limit, ADR 0528 §3)
|
||||
RuleProviders = 5 // every provider the module requires runs healthy on that node, or at its canary
|
||||
RuleGrant = 6 // the module's credential for F exists (grants.<module>.<fp>)
|
||||
RuleComposed = 7 // the module's composition for that node at F exists (ADR 0539 §6)
|
||||
// RuleProvisions: every provision the module requires on that node is provided (novox/hq ADR 0570 §4).
|
||||
RuleProvisions = 8
|
||||
)
|
||||
|
||||
// ruleRead is the highest rule a reader takes (novox/hq ADR 0614 §2, §4): 9 and 10 are kept for the verb wait and the
|
||||
// capability hold of a later release.
|
||||
const ruleRead = 10
|
||||
|
||||
// The answers a want carries to a failed backup before an apply (ADR 0539 clause 16).
|
||||
const (
|
||||
// BackupAgain is the answer to back the module up again before the apply.
|
||||
BackupAgain = seat.WantsByWantNodeModuleValueBackupAgain
|
||||
// BackupSkip is the answer to go on without a backup.
|
||||
BackupSkip = seat.WantsByWantNodeModuleValueBackupSkip
|
||||
)
|
||||
|
||||
// NodeWord is the last part of a node's own key.
|
||||
const NodeWord = "node"
|
||||
|
||||
const (
|
||||
wantWord = "want"
|
||||
heldWord = "held"
|
||||
srcWord = "src2:"
|
||||
)
|
||||
|
||||
var (
|
||||
moduleNameOK = regexp.MustCompile(`^[-/_a-zA-Z0-9]+(\.[-/_a-zA-Z0-9]+)*$`)
|
||||
digestForm = regexp.MustCompile(`^sha256:[0-9a-f]{64}$`)
|
||||
)
|
||||
|
||||
func moduleKeyPart(module string) (string, error) {
|
||||
if module == "" {
|
||||
return "", fmt.Errorf("wants: the module name is empty")
|
||||
}
|
||||
if !moduleNameOK.MatchString(module) {
|
||||
return "", fmt.Errorf("wants: the module name %q holds a wildcard, whitespace, '=', an empty part between dots or a character a key refuses", module)
|
||||
}
|
||||
return keys.EscapeName(module), nil
|
||||
}
|
||||
|
||||
func parseModuleKeyPart(part string) (string, error) {
|
||||
if err := keys.CheckName("module", part); err != nil {
|
||||
return "", err
|
||||
}
|
||||
module, err := keys.UnescapeName(part)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if back, err := moduleKeyPart(module); err != nil || back != part {
|
||||
return "", fmt.Errorf("wants: the module part %q is not a module name as a key writes it", part)
|
||||
}
|
||||
return module, nil
|
||||
}
|
||||
|
||||
func checkFingerprint(fp, of string) error {
|
||||
if part, ok := strings.CutPrefix(fp, srcWord); !ok || !keys.IsHex64(part) {
|
||||
return fmt.Errorf("wants: %s: %q is not a fingerprint of the new form, %s and 64 lowercase hex digits", of, fp, srcWord)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkDigest(digest, of string) error {
|
||||
if !digestForm.MatchString(digest) {
|
||||
return fmt.Errorf("wants: %s %q is not sha256:<64 lowercase hex digits>", of, digest)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkRule(rule int64, of string) error {
|
||||
if rule < RuleBuildDone || rule > ruleRead {
|
||||
return fmt.Errorf("wants: %s names the rule %d, which is not one of 1 to %d", of, rule, ruleRead)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WantKey is the key, in the state wants, of a node's want for a module: `want.<node>.<module>`. A module named `node` is
|
||||
// refused, since `want.<node>.node` is the node's own key.
|
||||
func WantKey(node, module string) (string, error) {
|
||||
if err := keys.CheckName("node", node); err != nil {
|
||||
return "", err
|
||||
}
|
||||
part, err := moduleKeyPart(module)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if module == NodeWord {
|
||||
return "", fmt.Errorf(`wants: the module name "node" is refused: want.%s.node is the node's own key`, node)
|
||||
}
|
||||
return seat.WantsByWantNodeModuleKey(node, part), nil
|
||||
}
|
||||
|
||||
// WantNodeKey is the key of a node's own want: `want.<node>.node`.
|
||||
func WantNodeKey(node string) (string, error) {
|
||||
if err := keys.CheckName("node", node); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return seat.WantsByWantNodeNodeKey(node), nil
|
||||
}
|
||||
|
||||
// ParseWantNodeKey is the inverse of WantNodeKey; it refuses a module's want key and any key of another form.
|
||||
func ParseWantNodeKey(key string) (node string, err error) {
|
||||
node, ok := seat.ParseWantsByWantNodeNodeKey(key)
|
||||
if !ok || keys.CheckName("node", node) != nil {
|
||||
return "", fmt.Errorf("wants: %q is not a node's own want key, want.<node>.node", key)
|
||||
}
|
||||
return node, nil
|
||||
}
|
||||
|
||||
// ParseWantKey is the inverse of WantKey; it refuses a held key, the node's own want key and any key of another form.
|
||||
func ParseWantKey(key string) (node, module string, err error) {
|
||||
refused := fmt.Errorf("wants: %q is not a want key, want.<node>.<module>", key)
|
||||
node, part, ok := seat.ParseWantsByWantNodeModuleKey(key)
|
||||
if !ok || part == NodeWord || keys.CheckName("node", node) != nil {
|
||||
return "", "", refused
|
||||
}
|
||||
if module, err = parseModuleKeyPart(part); err != nil {
|
||||
return "", "", refused
|
||||
}
|
||||
return node, module, nil
|
||||
}
|
||||
|
||||
// HeldKey is the key of a module's fingerprint the assigner holds: `held.<module>.<hex>`, the hex being the
|
||||
// fingerprint's key part.
|
||||
func HeldKey(module, fingerprint string) (string, error) {
|
||||
if err := keys.CheckName("module", module); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := checkFingerprint(fingerprint, "a held key's fingerprint"); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return seat.WantsByHeldModuleFingerprintKey(module, strings.TrimPrefix(fingerprint, srcWord)), nil
|
||||
}
|
||||
|
||||
// ParseHeldKey is the inverse of HeldKey: the module and the fingerprint (`src2:` and the hex).
|
||||
func ParseHeldKey(key string) (module, fingerprint string, err error) {
|
||||
module, part, ok := seat.ParseWantsByHeldModuleFingerprintKey(key)
|
||||
if !ok || keys.CheckName("module", module) != nil || !keys.IsHex64(part) {
|
||||
return "", "", fmt.Errorf("wants: %q is not a held key, held.<module>.<fingerprint hex>", key)
|
||||
}
|
||||
return module, srcWord + part, nil
|
||||
}
|
||||
|
||||
func checkWant(w Want) error {
|
||||
if w.Removed {
|
||||
if w.Fp != "" || w.Composed != "" || w.Newest != "" || w.Rule != 0 || w.Backup != "" || w.Put || w.Hold || w.Self || w.Yes != "" || w.Release != "" {
|
||||
return fmt.Errorf("wants: a removed want names no fingerprint, composition, rule, backup answer or put")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if w.Hold && !w.Put {
|
||||
return fmt.Errorf("wants: only a put holds a want")
|
||||
}
|
||||
if w.Put && (w.Newest != "" || w.Rule != 0) {
|
||||
return fmt.Errorf("wants: a put's want is held back by no rule")
|
||||
}
|
||||
if err := checkFingerprint(w.Fp, "a want's fingerprint"); err != nil {
|
||||
return err
|
||||
}
|
||||
// A node the catalogue places composes its own modules (ADR 0547 §2): its want is Self and names the build alone. Any
|
||||
// other want names the one composition it allows (ADR 0539 clause 1).
|
||||
switch {
|
||||
case w.Self && w.Composed != "":
|
||||
return fmt.Errorf("wants: the want of %s says the node composes it, and names a composition", w.Fp)
|
||||
case !w.Self && w.Composed == "":
|
||||
return fmt.Errorf("wants: the want of %s names no composition: a want names the digest of the one composition it allows (ADR 0539 clause 1), unless the node composes it itself", w.Fp)
|
||||
case w.Composed != "":
|
||||
if err := checkDigest(w.Composed, "a want's composition digest"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if w.Yes != "" {
|
||||
if err := checkDigest(w.Yes, "the digest a want's yes allows"); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if w.Release != "" {
|
||||
return fmt.Errorf("wants: the want of %s names the release %s and no yes it gave", w.Fp, w.Release)
|
||||
}
|
||||
switch w.Backup {
|
||||
case "", BackupAgain, BackupSkip:
|
||||
default:
|
||||
return fmt.Errorf("wants: the backup answer %q is not %s or %s", w.Backup, BackupAgain, BackupSkip)
|
||||
}
|
||||
if w.Newest == "" && w.Rule == 0 {
|
||||
return nil
|
||||
}
|
||||
if err := checkFingerprint(w.Newest, "a want's newest fingerprint"); err != nil {
|
||||
return err
|
||||
}
|
||||
if w.Newest == w.Fp {
|
||||
return fmt.Errorf("wants: a want at its newest fingerprint names no rule holding it back")
|
||||
}
|
||||
return checkRule(w.Rule, "a want held back")
|
||||
}
|
||||
|
||||
// CheckWant refuses a want the codec refuses.
|
||||
func CheckWant(w Want) error { return checkWant(w) }
|
||||
|
||||
// EncodeWant is w as JSON at the kept values' version; it refuses a fingerprint not of the new form, a removed want naming
|
||||
// one, and a want that names no composition unless the node composes it itself.
|
||||
func EncodeWant(w Want) ([]byte, error) {
|
||||
if err := checkWant(w); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
w.V = keys.Version
|
||||
return json.Marshal(w)
|
||||
}
|
||||
|
||||
// DecodeWant reads a want; it refuses an unknown version (keys.ErrUnknownVersion), a field the contract does not know and
|
||||
// what EncodeWant refuses.
|
||||
func DecodeWant(data []byte) (Want, error) {
|
||||
if err := keys.CheckVersion(data); err != nil {
|
||||
return Want{}, err
|
||||
}
|
||||
w, err := seat.DecodeWantsByWantNodeModuleValue(data)
|
||||
if err != nil {
|
||||
return Want{}, fmt.Errorf("wants: value does not fit its shape: %w", err)
|
||||
}
|
||||
if err := checkWant(w); err != nil {
|
||||
return Want{}, err
|
||||
}
|
||||
return w, nil
|
||||
}
|
||||
|
||||
// SameWant says whether two wants are the same value, so the assigner writes only when they differ.
|
||||
func SameWant(a, b Want) bool { return a == b }
|
||||
|
||||
func checkNodeWant(w NodeWant) error {
|
||||
switch {
|
||||
case w.Self && w.Composed != "":
|
||||
return fmt.Errorf("wants: the node's own want says the node composes it, and names a composition")
|
||||
case w.Self:
|
||||
return nil
|
||||
}
|
||||
return checkDigest(w.Composed, "the node's own want's composition digest")
|
||||
}
|
||||
|
||||
// EncodeNodeWant is w as JSON; it refuses a digest not of the sha256 form, and a want that names no composition unless it
|
||||
// says the node composes its own (Self, ADR 0547 §2).
|
||||
func EncodeNodeWant(w NodeWant) ([]byte, error) {
|
||||
if err := checkNodeWant(w); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
w.V = keys.Version
|
||||
return json.Marshal(w)
|
||||
}
|
||||
|
||||
// DecodeNodeWant reads a node's own want; it refuses an unknown version, a field a node's own want does not have (a
|
||||
// fingerprint among them), and what EncodeNodeWant refuses.
|
||||
func DecodeNodeWant(data []byte) (NodeWant, error) {
|
||||
if err := keys.CheckVersion(data); err != nil {
|
||||
return NodeWant{}, err
|
||||
}
|
||||
w, err := seat.DecodeWantsByWantNodeNodeValue(data)
|
||||
if err != nil {
|
||||
return NodeWant{}, fmt.Errorf("wants: a node's own want does not fit its shape: %w", err)
|
||||
}
|
||||
if err := checkNodeWant(w); err != nil {
|
||||
return NodeWant{}, err
|
||||
}
|
||||
return w, nil
|
||||
}
|
||||
|
||||
func checkHeld(h Held) error { return keys.CheckName("node", h.Node) }
|
||||
|
||||
// EncodeHeld is h as JSON; it refuses a node name a key refuses.
|
||||
func EncodeHeld(h Held) ([]byte, error) {
|
||||
if err := checkHeld(h); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h.V = keys.Version
|
||||
return json.Marshal(h)
|
||||
}
|
||||
|
||||
// DecodeHeld reads a hold; it refuses an unknown version and what EncodeHeld refuses.
|
||||
func DecodeHeld(data []byte) (Held, error) {
|
||||
if err := keys.CheckVersion(data); err != nil {
|
||||
return Held{}, err
|
||||
}
|
||||
h, err := seat.DecodeWantsByHeldModuleFingerprintValue(data)
|
||||
if err != nil {
|
||||
return Held{}, fmt.Errorf("wants: value does not fit its shape: %w", err)
|
||||
}
|
||||
if err := checkHeld(h); err != nil {
|
||||
return Held{}, err
|
||||
}
|
||||
return h, nil
|
||||
}
|
||||
|
||||
// SameHeld says whether two holds are the same value.
|
||||
func SameHeld(a, b Held) bool { return a == b }
|
||||
@@ -0,0 +1,162 @@
|
||||
package meshwantsclient
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/keys"
|
||||
)
|
||||
|
||||
func rep(c string) string { return strings.Repeat(c, 64) }
|
||||
|
||||
// TestWantsAreWhatTheEarlierCodecWrote holds the values and keys equal to what go/delivery's codec wrote for the same
|
||||
// values (the golden texts are its output; the field order is the contract's now, so values are compared decoded), and the
|
||||
// refusals it made.
|
||||
func TestWantsAreWhatTheEarlierCodecWrote(t *testing.T) {
|
||||
a, b, c, d := "src2:"+rep("a"), "src2:"+rep("b"), "sha256:"+rep("c"), "sha256:"+rep("d")
|
||||
sameJSON := func(t *testing.T, got []byte, want string) {
|
||||
t.Helper()
|
||||
var g, w any
|
||||
if err := json.Unmarshal(got, &g); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := json.Unmarshal([]byte(want), &w); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(g, w) {
|
||||
t.Fatalf("wrote %s, the earlier codec wrote %s", got, want)
|
||||
}
|
||||
}
|
||||
t.Run("wants", func(t *testing.T) {
|
||||
for name, row := range map[string]struct {
|
||||
w Want
|
||||
golden string
|
||||
}{
|
||||
"composed": {Want{Fp: a, Composed: c, Words: "w"}, `{"v":1,"fp":"` + a + `","composed":"` + c + `","words":"w"}`},
|
||||
"held back": {Want{Fp: a, Self: true, Newest: b, Rule: RuleGrant, Backup: BackupAgain, Words: "held"}, `{"v":1,"fp":"` + a + `","newest":"` + b + `","rule":6,"backup":"again","words":"held","self":true}`},
|
||||
"put": {Want{Fp: a, Self: true, Put: true, Hold: true, Yes: d, Release: "r1"}, `{"v":1,"fp":"` + a + `","put":true,"hold":true,"yes":"` + d + `","release":"r1","self":true}`},
|
||||
"removed": {Want{Removed: true, Words: "gone"}, `{"v":1,"removed":true,"words":"gone"}`},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
raw, err := EncodeWant(row.w)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sameJSON(t, raw, row.golden)
|
||||
back, err := DecodeWant(raw)
|
||||
if err != nil || !SameWant(back, func() Want { w := row.w; w.V = keys.Version; return w }()) {
|
||||
t.Fatalf("read back %+v, %v", back, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
t.Run("refused", func(t *testing.T) {
|
||||
for name, w := range map[string]Want{
|
||||
"old fingerprint": {Fp: "src1:abc", Composed: c},
|
||||
"no composition": {Fp: a},
|
||||
"self and composition": {Fp: a, Self: true, Composed: c},
|
||||
"hold without put": {Fp: a, Self: true, Hold: true},
|
||||
"put held back": {Fp: a, Self: true, Put: true, Newest: b, Rule: RuleStep},
|
||||
"removed names a rule": {Removed: true, Rule: 1},
|
||||
"release without yes": {Fp: a, Self: true, Release: "r"},
|
||||
"backup answer unknown": {Fp: a, Self: true, Backup: "later"},
|
||||
"newest equals fp": {Fp: a, Self: true, Newest: a, Rule: RuleStep},
|
||||
"rule 11": {Fp: a, Self: true, Newest: b, Rule: 11},
|
||||
"newest without a rule": {Fp: a, Self: true, Newest: b},
|
||||
"composition not sha256": {Fp: a, Composed: "sha256:xyz"},
|
||||
} {
|
||||
if _, err := EncodeWant(w); err == nil {
|
||||
t.Errorf("%s: written", name)
|
||||
}
|
||||
}
|
||||
if _, err := EncodeWant(Want{Fp: a, Self: true, Newest: b, Rule: 10}); err != nil {
|
||||
t.Errorf("rule 10 is read: %v", err)
|
||||
}
|
||||
if _, err := DecodeWant([]byte(`{"fp":"` + a + `","self":true}`)); !errors.Is(err, keys.ErrUnknownVersion) {
|
||||
t.Errorf("no version: %v", err)
|
||||
}
|
||||
if _, err := DecodeWant([]byte(`{"v":1,"fp":"` + a + `","self":true,"extra":1}`)); err == nil {
|
||||
t.Error("an unknown field is read")
|
||||
}
|
||||
})
|
||||
t.Run("node want and hold", func(t *testing.T) {
|
||||
raw, err := EncodeNodeWant(NodeWant{Composed: c, Words: "n"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sameJSON(t, raw, `{"v":1,"composed":"`+c+`","words":"n"}`)
|
||||
raw, err = EncodeNodeWant(NodeWant{Self: true})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sameJSON(t, raw, `{"v":1,"composed":"","self":true}`)
|
||||
if back, err := DecodeNodeWant(raw); err != nil || !back.Self {
|
||||
t.Fatalf("%+v %v", back, err)
|
||||
}
|
||||
for name, w := range map[string]NodeWant{"none": {}, "both": {Self: true, Composed: c}, "bad": {Composed: "x"}} {
|
||||
if _, err := EncodeNodeWant(w); err == nil {
|
||||
t.Errorf("%s: written", name)
|
||||
}
|
||||
}
|
||||
if _, err := DecodeNodeWant([]byte(`{"v":1,"composed":"` + c + `","fp":"` + a + `"}`)); err == nil {
|
||||
t.Error("a fingerprint in a node's own want is read")
|
||||
}
|
||||
raw, err = EncodeHeld(Held{Node: "ace", Words: "unhealthy"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sameJSON(t, raw, `{"v":1,"node":"ace","words":"unhealthy"}`)
|
||||
if h, err := DecodeHeld(raw); err != nil || !SameHeld(h, Held{V: 1, Node: "ace", Words: "unhealthy"}) {
|
||||
t.Fatalf("%+v %v", h, err)
|
||||
}
|
||||
if _, err := EncodeHeld(Held{Node: "a.b"}); err == nil {
|
||||
t.Error("a node with a dot is held")
|
||||
}
|
||||
})
|
||||
t.Run("keys", func(t *testing.T) {
|
||||
for _, row := range []struct{ module, want string }{
|
||||
{"mesh-delivery-assigner", "want.ace.mesh-delivery-assigner"},
|
||||
{"novox.be", "want.ace.novox=2ebe"},
|
||||
} {
|
||||
k, err := WantKey("ace", row.module)
|
||||
if err != nil || k != row.want {
|
||||
t.Fatalf("WantKey %s = %q, %v", row.module, k, err)
|
||||
}
|
||||
if n, m, err := ParseWantKey(k); err != nil || n != "ace" || m != row.module {
|
||||
t.Fatalf("ParseWantKey %q = %q %q %v", k, n, m, err)
|
||||
}
|
||||
}
|
||||
if k, err := HeldKey("mesh-delivery-assigner", a); err != nil || k != "held.mesh-delivery-assigner."+rep("a") {
|
||||
t.Fatalf("HeldKey = %q %v", k, err)
|
||||
}
|
||||
if m, fp, err := ParseHeldKey("held.mesh-delivery-assigner." + rep("a")); err != nil || m != "mesh-delivery-assigner" || fp != a {
|
||||
t.Fatalf("ParseHeldKey = %q %q %v", m, fp, err)
|
||||
}
|
||||
if k, err := WantNodeKey("ace"); err != nil || k != "want.ace.node" {
|
||||
t.Fatalf("WantNodeKey = %q %v", k, err)
|
||||
}
|
||||
if n, err := ParseWantNodeKey("want.ace.node"); err != nil || n != "ace" {
|
||||
t.Fatalf("ParseWantNodeKey = %q %v", n, err)
|
||||
}
|
||||
if _, err := WantKey("ace", "node"); err == nil {
|
||||
t.Error("a module named node has a want key")
|
||||
}
|
||||
if _, err := HeldKey("novox.be", a); err == nil {
|
||||
t.Error("a module with a dot has a held key")
|
||||
}
|
||||
for _, key := range []string{"want.ace.node", "held.m." + rep("a"), "ace.m", "want.ace", "want..m"} {
|
||||
if _, _, err := ParseWantKey(key); err == nil {
|
||||
t.Errorf("%q is a want key", key)
|
||||
}
|
||||
}
|
||||
if _, err := ParseWantNodeKey("want.ace.m"); err == nil {
|
||||
t.Error("a module's want is a node's own")
|
||||
}
|
||||
if _, _, err := ParseHeldKey("want.ace.m"); err == nil {
|
||||
t.Error("a want key is a held key")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,337 @@
|
||||
// Code generated by seatgen from the seat definitions; DO NOT EDIT.
|
||||
|
||||
// Package meshwants is the contract of the seat "mesh-wants", defined whole (seats/mesh-wants/seat.json): The seat that says what each node should run and whether each module's new builds wait for a person (ADR 0565, ADR 0539).
|
||||
package meshwants
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Seat is the seat's name.
|
||||
const Seat = "mesh-wants"
|
||||
|
||||
const (
|
||||
VerbUpgrade = "upgrade"
|
||||
VerbBus = "bus"
|
||||
)
|
||||
|
||||
const (
|
||||
EventPolicyChanged = "policy-changed"
|
||||
)
|
||||
|
||||
// Verbs is every verb the seat serves, in its definition's order: a new copy each call.
|
||||
func Verbs() []string {
|
||||
return []string{VerbUpgrade, VerbBus}
|
||||
}
|
||||
|
||||
// Events is every event published on the seat's subject: a new copy each call.
|
||||
func Events() []string {
|
||||
return []string{EventPolicyChanged}
|
||||
}
|
||||
|
||||
// Family is where the seat's events are kept: the shared events stream, filtered to the seat's subjects.
|
||||
const (
|
||||
Family = "mesh-wants.events"
|
||||
FamilyMaxAgeDays = 7
|
||||
)
|
||||
|
||||
// SubjectPolicyChanged is where policy-changed is published.
|
||||
const SubjectPolicyChanged = "mesh.seat.mesh-wants.event.policy-changed"
|
||||
|
||||
const (
|
||||
StateWants = "wants"
|
||||
StateDifferences = "differences"
|
||||
StateCompositions = "compositions"
|
||||
)
|
||||
|
||||
// WantsByWantNodeModuleKey is the key of one value of the state wants under the form "want.<node>.<module>".
|
||||
func WantsByWantNodeModuleKey(node, module string) string { return "want." + node + "." + module }
|
||||
|
||||
// ParseWantsByWantNodeModuleKey reads the parts of a key of the form "want.<node>.<module>", in the order of the form; the last result is false for a key of another form.
|
||||
func ParseWantsByWantNodeModuleKey(key string) (string, string, bool) {
|
||||
rest := key
|
||||
if !strings.HasPrefix(rest, "want.") {
|
||||
return "", "", false
|
||||
}
|
||||
rest = rest[5:]
|
||||
v1, after1, found1 := strings.Cut(rest, ".")
|
||||
if !found1 || v1 == "" {
|
||||
return "", "", false
|
||||
}
|
||||
rest = after1
|
||||
if rest == "" {
|
||||
return "", "", false
|
||||
}
|
||||
v3 := rest
|
||||
rest = ""
|
||||
if rest != "" {
|
||||
return "", "", false
|
||||
}
|
||||
return v1, v3, true
|
||||
}
|
||||
|
||||
// DecodeWantsByWantNodeModuleValue reads one value of the state wants under the form "want.<node>.<module>", refusing a field WantsByWantNodeModuleValue does not know and any text after the value.
|
||||
func DecodeWantsByWantNodeModuleValue(raw []byte) (WantsByWantNodeModuleValue, error) {
|
||||
var v WantsByWantNodeModuleValue
|
||||
dec := json.NewDecoder(bytes.NewReader(raw))
|
||||
dec.DisallowUnknownFields()
|
||||
if err := dec.Decode(&v); err != nil {
|
||||
return v, err
|
||||
}
|
||||
if dec.More() {
|
||||
return v, errors.New("text after the value")
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// WantsByWantNodeNodeKey is the key of one value of the state wants under the form "want.<node>.node".
|
||||
func WantsByWantNodeNodeKey(node string) string { return "want." + node + ".node" }
|
||||
|
||||
// ParseWantsByWantNodeNodeKey reads the parts of a key of the form "want.<node>.node", in the order of the form; the last result is false for a key of another form.
|
||||
func ParseWantsByWantNodeNodeKey(key string) (string, bool) {
|
||||
rest := key
|
||||
if !strings.HasPrefix(rest, "want.") {
|
||||
return "", false
|
||||
}
|
||||
rest = rest[5:]
|
||||
v1, after1, found1 := strings.Cut(rest, ".node")
|
||||
if !found1 || v1 == "" {
|
||||
return "", false
|
||||
}
|
||||
rest = after1
|
||||
if rest != "" {
|
||||
return "", false
|
||||
}
|
||||
return v1, true
|
||||
}
|
||||
|
||||
// DecodeWantsByWantNodeNodeValue reads one value of the state wants under the form "want.<node>.node", refusing a field WantsByWantNodeNodeValue does not know and any text after the value.
|
||||
func DecodeWantsByWantNodeNodeValue(raw []byte) (WantsByWantNodeNodeValue, error) {
|
||||
var v WantsByWantNodeNodeValue
|
||||
dec := json.NewDecoder(bytes.NewReader(raw))
|
||||
dec.DisallowUnknownFields()
|
||||
if err := dec.Decode(&v); err != nil {
|
||||
return v, err
|
||||
}
|
||||
if dec.More() {
|
||||
return v, errors.New("text after the value")
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// WantsByHeldModuleFingerprintKey is the key of one value of the state wants under the form "held.<module>.<fingerprint>".
|
||||
func WantsByHeldModuleFingerprintKey(module, fingerprint string) string {
|
||||
return "held." + module + "." + fingerprint
|
||||
}
|
||||
|
||||
// ParseWantsByHeldModuleFingerprintKey reads the parts of a key of the form "held.<module>.<fingerprint>", in the order of the form; the last result is false for a key of another form.
|
||||
func ParseWantsByHeldModuleFingerprintKey(key string) (string, string, bool) {
|
||||
rest := key
|
||||
if !strings.HasPrefix(rest, "held.") {
|
||||
return "", "", false
|
||||
}
|
||||
rest = rest[5:]
|
||||
v1, after1, found1 := strings.Cut(rest, ".")
|
||||
if !found1 || v1 == "" {
|
||||
return "", "", false
|
||||
}
|
||||
rest = after1
|
||||
if rest == "" {
|
||||
return "", "", false
|
||||
}
|
||||
v3 := rest
|
||||
rest = ""
|
||||
if rest != "" {
|
||||
return "", "", false
|
||||
}
|
||||
return v1, v3, true
|
||||
}
|
||||
|
||||
// DecodeWantsByHeldModuleFingerprintValue reads one value of the state wants under the form "held.<module>.<fingerprint>", refusing a field WantsByHeldModuleFingerprintValue does not know and any text after the value.
|
||||
func DecodeWantsByHeldModuleFingerprintValue(raw []byte) (WantsByHeldModuleFingerprintValue, error) {
|
||||
var v WantsByHeldModuleFingerprintValue
|
||||
dec := json.NewDecoder(bytes.NewReader(raw))
|
||||
dec.DisallowUnknownFields()
|
||||
if err := dec.Decode(&v); err != nil {
|
||||
return v, err
|
||||
}
|
||||
if dec.More() {
|
||||
return v, errors.New("text after the value")
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// DifferencesByNodeModuleKey is the key of one value of the state differences under the form "<node>.<module>".
|
||||
func DifferencesByNodeModuleKey(node, module string) string { return node + "." + module }
|
||||
|
||||
// ParseDifferencesByNodeModuleKey reads the parts of a key of the form "<node>.<module>", in the order of the form; the last result is false for a key of another form.
|
||||
func ParseDifferencesByNodeModuleKey(key string) (string, string, bool) {
|
||||
rest := key
|
||||
v0, after0, found0 := strings.Cut(rest, ".")
|
||||
if !found0 || v0 == "" {
|
||||
return "", "", false
|
||||
}
|
||||
rest = after0
|
||||
if rest == "" {
|
||||
return "", "", false
|
||||
}
|
||||
v2 := rest
|
||||
rest = ""
|
||||
if rest != "" {
|
||||
return "", "", false
|
||||
}
|
||||
return v0, v2, true
|
||||
}
|
||||
|
||||
// DecodeDifferencesByNodeModuleValue reads one value of the state differences under the form "<node>.<module>", refusing a field DifferencesByNodeModuleValue does not know and any text after the value.
|
||||
func DecodeDifferencesByNodeModuleValue(raw []byte) (DifferencesByNodeModuleValue, error) {
|
||||
var v DifferencesByNodeModuleValue
|
||||
dec := json.NewDecoder(bytes.NewReader(raw))
|
||||
dec.DisallowUnknownFields()
|
||||
if err := dec.Decode(&v); err != nil {
|
||||
return v, err
|
||||
}
|
||||
if dec.More() {
|
||||
return v, errors.New("text after the value")
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// CompositionsByGroupHeadsKey is the key of one value of the state compositions under the form "<group>.<heads>".
|
||||
func CompositionsByGroupHeadsKey(group, heads string) string { return group + "." + heads }
|
||||
|
||||
// ParseCompositionsByGroupHeadsKey reads the parts of a key of the form "<group>.<heads>", in the order of the form; the last result is false for a key of another form.
|
||||
func ParseCompositionsByGroupHeadsKey(key string) (string, string, bool) {
|
||||
rest := key
|
||||
v0, after0, found0 := strings.Cut(rest, ".")
|
||||
if !found0 || v0 == "" {
|
||||
return "", "", false
|
||||
}
|
||||
rest = after0
|
||||
if rest == "" {
|
||||
return "", "", false
|
||||
}
|
||||
v2 := rest
|
||||
rest = ""
|
||||
if rest != "" {
|
||||
return "", "", false
|
||||
}
|
||||
return v0, v2, true
|
||||
}
|
||||
|
||||
// DecodeCompositionsByGroupHeadsValue reads one value of the state compositions under the form "<group>.<heads>", refusing a field CompositionsByGroupHeadsValue does not know and any text after the value.
|
||||
func DecodeCompositionsByGroupHeadsValue(raw []byte) (CompositionsByGroupHeadsValue, error) {
|
||||
var v CompositionsByGroupHeadsValue
|
||||
dec := json.NewDecoder(bytes.NewReader(raw))
|
||||
dec.DisallowUnknownFields()
|
||||
if err := dec.Decode(&v); err != nil {
|
||||
return v, err
|
||||
}
|
||||
if dec.More() {
|
||||
return v, errors.New("text after the value")
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// UpgradeInput is what upgrade takes.
|
||||
type UpgradeInput struct {
|
||||
// "true": each node's own composition, by its digest, and the assigner's findings
|
||||
Compare string `json:"compare,omitempty"`
|
||||
// one module
|
||||
Module string `json:"module,omitempty"`
|
||||
// with module: roll, together, record, or default to take a person's choice back
|
||||
Policy string `json:"policy,omitempty"`
|
||||
// with policy: why, required and kept
|
||||
Why string `json:"why,omitempty"`
|
||||
}
|
||||
|
||||
// The values of UpgradeInput's compare.
|
||||
const (
|
||||
UpgradeInputCompareTrue = "true"
|
||||
UpgradeInputCompareFalse = "false"
|
||||
)
|
||||
|
||||
// UpgradeAnswer is what upgrade answers.
|
||||
type UpgradeAnswer = map[string]any
|
||||
|
||||
// BusInput is what bus takes.
|
||||
type BusInput struct {
|
||||
}
|
||||
|
||||
// BusAnswer is what bus answers.
|
||||
type BusAnswer = map[string]any
|
||||
|
||||
// PolicyChangedEvent is the body of the event policy-changed.
|
||||
type PolicyChangedEvent = map[string]any
|
||||
|
||||
// WantsByWantNodeModuleValue is one value of the state wants under the key want.<node>.<module>.
|
||||
type WantsByWantNodeModuleValue struct {
|
||||
// again or skip
|
||||
Backup string `json:"backup,omitempty"`
|
||||
Composed string `json:"composed,omitempty"`
|
||||
Fp string `json:"fp,omitempty"`
|
||||
Hold bool `json:"hold,omitempty"`
|
||||
Newest string `json:"newest,omitempty"`
|
||||
Put bool `json:"put,omitempty"`
|
||||
Release string `json:"release,omitempty"`
|
||||
Removed bool `json:"removed,omitempty"`
|
||||
// the rule that holds the node back, 1 to 10
|
||||
Rule int64 `json:"rule,omitempty"`
|
||||
Self bool `json:"self,omitempty"`
|
||||
V int64 `json:"v,omitempty"`
|
||||
Words string `json:"words,omitempty"`
|
||||
Yes string `json:"yes,omitempty"`
|
||||
}
|
||||
|
||||
// The values of WantsByWantNodeModuleValue's backup.
|
||||
const (
|
||||
WantsByWantNodeModuleValueBackupAgain = "again"
|
||||
WantsByWantNodeModuleValueBackupSkip = "skip"
|
||||
)
|
||||
|
||||
// WantsByWantNodeNodeValue is one value of the state wants under the key want.<node>.node.
|
||||
type WantsByWantNodeNodeValue struct {
|
||||
Composed string `json:"composed"`
|
||||
Self bool `json:"self,omitempty"`
|
||||
V int64 `json:"v,omitempty"`
|
||||
Words string `json:"words,omitempty"`
|
||||
}
|
||||
|
||||
// WantsByHeldModuleFingerprintValue is one value of the state wants under the key held.<module>.<fingerprint>.
|
||||
type WantsByHeldModuleFingerprintValue struct {
|
||||
Node string `json:"node"`
|
||||
V int64 `json:"v,omitempty"`
|
||||
Words string `json:"words,omitempty"`
|
||||
}
|
||||
|
||||
// DifferencesByNodeModuleValue is one value of the state differences under the key <node>.<module>.
|
||||
type DifferencesByNodeModuleValue struct {
|
||||
Rule int64 `json:"rule,omitempty"`
|
||||
Sent string `json:"sent,omitempty"`
|
||||
SentBuild string `json:"sent-build,omitempty"`
|
||||
Unexplained bool `json:"unexplained,omitempty"`
|
||||
V int64 `json:"v,omitempty"`
|
||||
Want string `json:"want,omitempty"`
|
||||
WantRemoved bool `json:"want-removed,omitempty"`
|
||||
Words string `json:"words,omitempty"`
|
||||
}
|
||||
|
||||
// CompositionsByGroupHeadsValue is one value of the state compositions under the key <group>.<heads>.
|
||||
type CompositionsByGroupHeadsValue struct {
|
||||
Group string `json:"group"`
|
||||
// 12 lowercase hex digits
|
||||
Heads string `json:"heads"`
|
||||
Machine string `json:"machine,omitempty"`
|
||||
Machines []string `json:"machines,omitempty"`
|
||||
Sdk string `json:"sdk,omitempty"`
|
||||
// pass, fail or waits
|
||||
State string `json:"state"`
|
||||
// milliseconds
|
||||
Took int64 `json:"took,omitempty"`
|
||||
V int64 `json:"v,omitempty"`
|
||||
Warnings []string `json:"warnings,omitempty"`
|
||||
Words string `json:"words,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// Code generated by seatgen from the seat definitions; DO NOT EDIT.
|
||||
|
||||
package meshwants
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestFixtures reads every fixture of seats/mesh-wants/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: "a want", subject: "", given: "{\"fp\":\"ab12\",\"rule\":3,\"v\":1}", wire: "{\"fp\":\"ab12\",\"rule\":3,\"v\":1}",
|
||||
decode: func(b []byte) (any, error) {
|
||||
var v WantsByWantNodeModuleValue
|
||||
err := json.Unmarshal(b, &v)
|
||||
return v, err
|
||||
}, subjectIs: nil},
|
||||
{name: "a node's own want", subject: "", given: "{\"composed\":\"sha256:aa\",\"self\":true,\"v\":1}", wire: "{\"composed\":\"sha256:aa\",\"self\":true,\"v\":1}",
|
||||
decode: func(b []byte) (any, error) {
|
||||
var v WantsByWantNodeNodeValue
|
||||
err := json.Unmarshal(b, &v)
|
||||
return v, err
|
||||
}, subjectIs: nil},
|
||||
{name: "a held fingerprint", subject: "", given: "{\"node\":\"ace\",\"v\":1}", wire: "{\"node\":\"ace\",\"v\":1}",
|
||||
decode: func(b []byte) (any, error) {
|
||||
var v WantsByHeldModuleFingerprintValue
|
||||
err := json.Unmarshal(b, &v)
|
||||
return v, err
|
||||
}, subjectIs: nil},
|
||||
{name: "a difference", subject: "", given: "{\"unexplained\":true,\"v\":1,\"want\":\"ab12\"}", wire: "{\"unexplained\":true,\"v\":1,\"want\":\"ab12\"}",
|
||||
decode: func(b []byte) (any, error) {
|
||||
var v DifferencesByNodeModuleValue
|
||||
err := json.Unmarshal(b, &v)
|
||||
return v, err
|
||||
}, subjectIs: nil},
|
||||
{name: "a composition", subject: "", given: "{\"group\":\"g1\",\"heads\":\"0123456789ab\",\"state\":\"pass\",\"v\":1}", wire: "{\"group\":\"g1\",\"heads\":\"0123456789ab\",\"state\":\"pass\",\"v\":1}",
|
||||
decode: func(b []byte) (any, error) {
|
||||
var v CompositionsByGroupHeadsValue
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
module git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants
|
||||
|
||||
go 1.26.0
|
||||
@@ -285,6 +285,35 @@
|
||||
],
|
||||
"series": "0.1",
|
||||
"generated-from": "."
|
||||
},
|
||||
{
|
||||
"name": "go-meshwants",
|
||||
"kind": "package",
|
||||
"language": "go",
|
||||
"paths": [
|
||||
"go/seats/meshwants"
|
||||
],
|
||||
"series": "0.1",
|
||||
"generated-from": "."
|
||||
},
|
||||
{
|
||||
"name": "ts-meshwants",
|
||||
"kind": "package",
|
||||
"language": "typescript",
|
||||
"paths": [
|
||||
"ts/seats/meshwants"
|
||||
],
|
||||
"series": "0.1",
|
||||
"generated-from": "."
|
||||
},
|
||||
{
|
||||
"name": "go-meshwantsclient",
|
||||
"kind": "package",
|
||||
"language": "go",
|
||||
"paths": [
|
||||
"go/clients/meshwantsclient"
|
||||
],
|
||||
"series": "0.1"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
[
|
||||
{
|
||||
"name": "a want",
|
||||
"of": "state:wants/want.<node>.<module>",
|
||||
"given": {
|
||||
"v": 1,
|
||||
"fp": "ab12",
|
||||
"rule": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "a node's own want",
|
||||
"of": "state:wants/want.<node>.node",
|
||||
"given": {
|
||||
"v": 1,
|
||||
"composed": "sha256:aa",
|
||||
"self": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "a held fingerprint",
|
||||
"of": "state:wants/held.<module>.<fingerprint>",
|
||||
"given": {
|
||||
"v": 1,
|
||||
"node": "ace"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "a difference",
|
||||
"of": "state:differences/<node>.<module>",
|
||||
"given": {
|
||||
"v": 1,
|
||||
"want": "ab12",
|
||||
"unexplained": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "a composition",
|
||||
"of": "state:compositions/<group>.<heads>",
|
||||
"given": {
|
||||
"v": 1,
|
||||
"group": "g1",
|
||||
"heads": "0123456789ab",
|
||||
"state": "pass"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,284 @@
|
||||
{
|
||||
"format": 1,
|
||||
"seat": "mesh-wants",
|
||||
"scope": "mesh",
|
||||
"grain": "whole",
|
||||
"domain": "Delivery",
|
||||
"decision": "novox/hq ADR 0565, ADR 0634, ADR 0635",
|
||||
"description": "The seat that says what each node should run and whether each module's new builds wait for a person (ADR 0565, ADR 0539).",
|
||||
"verbs": [
|
||||
{
|
||||
"name": "upgrade",
|
||||
"description": "Whether each module's new builds wait for a person: its policy (roll, together or record) and where it comes from (a person, the module, the bus, its irreplaceable data, the default); the bus is never rolled. With module, that one; with module, policy and why, a person's choice for it, recorded as a hand act; with policy default, the choice taken back. With compare, each node's own composition and the assigner's findings.",
|
||||
"input": {
|
||||
"properties": {
|
||||
"compare": {
|
||||
"description": "\"true\": each node's own composition, by its digest, and the assigner's findings",
|
||||
"enum": [
|
||||
"true",
|
||||
"false"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"module": {
|
||||
"description": "one module",
|
||||
"type": "string"
|
||||
},
|
||||
"policy": {
|
||||
"description": "with module: roll, together, record, or default to take a person's choice back",
|
||||
"type": "string"
|
||||
},
|
||||
"why": {
|
||||
"description": "with policy: why, required and kept",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"answer": {
|
||||
"type": "object",
|
||||
"description": "each module's policy, or the one chosen, or the comparison"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "bus",
|
||||
"description": "What a bus upgrade would do: the fingerprint of the bus module each node runs against the indexer's newest, and the last bus steps from the hand-act log and the assigner's puts of the bus. Read-only: the step itself is the put on the operator's warrant.",
|
||||
"input": {
|
||||
"properties": {},
|
||||
"type": "object"
|
||||
},
|
||||
"answer": {
|
||||
"type": "object",
|
||||
"description": "what a bus upgrade would do, read only"
|
||||
}
|
||||
}
|
||||
],
|
||||
"events": [
|
||||
{
|
||||
"name": "policy-changed",
|
||||
"body": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
],
|
||||
"family": {
|
||||
"name": "mesh-wants.events",
|
||||
"max-age-days": 7
|
||||
},
|
||||
"states": [
|
||||
{
|
||||
"name": "wants",
|
||||
"template": "state",
|
||||
"key-forms": [
|
||||
{
|
||||
"key": "want.<node>.<module>",
|
||||
"value": {
|
||||
"type": "object",
|
||||
"description": "a node's want for a module",
|
||||
"properties": {
|
||||
"v": {
|
||||
"type": "integer"
|
||||
},
|
||||
"fp": {
|
||||
"type": "string"
|
||||
},
|
||||
"composed": {
|
||||
"type": "string"
|
||||
},
|
||||
"removed": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"newest": {
|
||||
"type": "string"
|
||||
},
|
||||
"rule": {
|
||||
"type": "integer",
|
||||
"description": "the rule that holds the node back, 1 to 10"
|
||||
},
|
||||
"backup": {
|
||||
"type": "string",
|
||||
"description": "again or skip",
|
||||
"enum": [
|
||||
"again",
|
||||
"skip"
|
||||
]
|
||||
},
|
||||
"words": {
|
||||
"type": "string"
|
||||
},
|
||||
"put": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"hold": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"yes": {
|
||||
"type": "string"
|
||||
},
|
||||
"release": {
|
||||
"type": "string"
|
||||
},
|
||||
"self": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "a node's want for a module"
|
||||
},
|
||||
{
|
||||
"key": "want.<node>.node",
|
||||
"value": {
|
||||
"type": "object",
|
||||
"description": "a node's own want",
|
||||
"properties": {
|
||||
"v": {
|
||||
"type": "integer"
|
||||
},
|
||||
"composed": {
|
||||
"type": "string"
|
||||
},
|
||||
"words": {
|
||||
"type": "string"
|
||||
},
|
||||
"self": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"composed"
|
||||
]
|
||||
},
|
||||
"description": "a node's own want, naming the digest of its own composition"
|
||||
},
|
||||
{
|
||||
"key": "held.<module>.<fingerprint>",
|
||||
"value": {
|
||||
"type": "object",
|
||||
"description": "a fingerprint the assigner holds",
|
||||
"properties": {
|
||||
"v": {
|
||||
"type": "integer"
|
||||
},
|
||||
"node": {
|
||||
"type": "string"
|
||||
},
|
||||
"words": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"description": "a module's fingerprint the assigner holds"
|
||||
}
|
||||
],
|
||||
"history": 20,
|
||||
"description": "what each node should run, and the fingerprints held back"
|
||||
},
|
||||
{
|
||||
"name": "differences",
|
||||
"template": "state",
|
||||
"key-forms": [
|
||||
{
|
||||
"key": "<node>.<module>",
|
||||
"value": {
|
||||
"type": "object",
|
||||
"description": "the difference between a want and what is sent",
|
||||
"properties": {
|
||||
"v": {
|
||||
"type": "integer"
|
||||
},
|
||||
"want": {
|
||||
"type": "string"
|
||||
},
|
||||
"want-removed": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"sent": {
|
||||
"type": "string"
|
||||
},
|
||||
"sent-build": {
|
||||
"type": "string"
|
||||
},
|
||||
"rule": {
|
||||
"type": "integer"
|
||||
},
|
||||
"unexplained": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"words": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "a module on a node"
|
||||
}
|
||||
],
|
||||
"history": 64,
|
||||
"description": "differences between a want and what the node-engine is sent"
|
||||
},
|
||||
{
|
||||
"name": "compositions",
|
||||
"template": "state",
|
||||
"key-forms": [
|
||||
{
|
||||
"key": "<group>.<heads>",
|
||||
"value": {
|
||||
"type": "object",
|
||||
"description": "the composition verdict of a group's check at its heads",
|
||||
"properties": {
|
||||
"v": {
|
||||
"type": "integer"
|
||||
},
|
||||
"group": {
|
||||
"type": "string"
|
||||
},
|
||||
"heads": {
|
||||
"type": "string",
|
||||
"description": "12 lowercase hex digits"
|
||||
},
|
||||
"state": {
|
||||
"type": "string",
|
||||
"description": "pass, fail or waits"
|
||||
},
|
||||
"words": {
|
||||
"type": "string"
|
||||
},
|
||||
"sdk": {
|
||||
"type": "string"
|
||||
},
|
||||
"machines": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"machine": {
|
||||
"type": "string"
|
||||
},
|
||||
"warnings": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"took": {
|
||||
"type": "integer",
|
||||
"description": "milliseconds"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"group",
|
||||
"heads",
|
||||
"state"
|
||||
]
|
||||
},
|
||||
"description": "a group's check at its heads"
|
||||
}
|
||||
],
|
||||
"history": 5,
|
||||
"description": "composition verdicts of a group's check; the checker reads them"
|
||||
}
|
||||
]
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"lockfileVersion": 3,
|
||||
"name": "@novox/mesh-delivery-contracts-mesh-wants",
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@novox/mesh-delivery-contracts-mesh-wants",
|
||||
"version": "0.1.0"
|
||||
}
|
||||
},
|
||||
"requires": true,
|
||||
"version": "0.1.0"
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "The contract of the seat mesh-wants, generated from its definition.",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"files": [
|
||||
"src"
|
||||
],
|
||||
"name": "@novox/mesh-delivery-contracts-mesh-wants",
|
||||
"type": "module",
|
||||
"types": "./src/index.ts",
|
||||
"version": "0.1.0"
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
// Code generated by seatgen from the seat definitions; DO NOT EDIT.
|
||||
|
||||
// The contract of the seat "mesh-wants", defined whole (seats/mesh-wants/seat.json): The seat that says what each node should run and whether each module's new builds wait for a person (ADR 0565, ADR 0539).
|
||||
|
||||
export const seat = "mesh-wants";
|
||||
|
||||
export const verbs = {
|
||||
upgrade: "upgrade",
|
||||
bus: "bus",
|
||||
} as const;
|
||||
|
||||
export const events = {
|
||||
policyChanged: "policy-changed",
|
||||
} as const;
|
||||
|
||||
export const family = { name: "mesh-wants.events", maxAgeDays: 7 } as const;
|
||||
|
||||
export const policyChangedSubject = "mesh.seat.mesh-wants.event.policy-changed";
|
||||
|
||||
export const states = {
|
||||
wants: "wants",
|
||||
differences: "differences",
|
||||
compositions: "compositions",
|
||||
} as const;
|
||||
|
||||
/** The key of one value of the state wants under the form "want.<node>.<module>". */
|
||||
export function wantsByWantNodeModuleKey(node: string, module: string): string {
|
||||
return "want." + node + "." + module;
|
||||
}
|
||||
|
||||
/** The parts of a key of the form "want.<node>.<module>", or undefined for a key of another form. */
|
||||
export function parseWantsByWantNodeModuleKey(_key: string): { node: string; module: string } | undefined {
|
||||
let _rest = _key;
|
||||
if (!_rest.startsWith("want.")) return undefined;
|
||||
_rest = _rest.slice(5);
|
||||
const _at1 = _rest.indexOf(".");
|
||||
if (_at1 <= 0) return undefined;
|
||||
const node = _rest.slice(0, _at1);
|
||||
_rest = _rest.slice(_at1 + 1);
|
||||
if (_rest === "") return undefined;
|
||||
const module = _rest;
|
||||
_rest = "";
|
||||
if (_rest !== "") return undefined;
|
||||
return { node, module };
|
||||
}
|
||||
|
||||
/** The key of one value of the state wants under the form "want.<node>.node". */
|
||||
export function wantsByWantNodeNodeKey(node: string): string {
|
||||
return "want." + node + ".node";
|
||||
}
|
||||
|
||||
/** The parts of a key of the form "want.<node>.node", or undefined for a key of another form. */
|
||||
export function parseWantsByWantNodeNodeKey(_key: string): { node: string } | undefined {
|
||||
let _rest = _key;
|
||||
if (!_rest.startsWith("want.")) return undefined;
|
||||
_rest = _rest.slice(5);
|
||||
const _at1 = _rest.indexOf(".node");
|
||||
if (_at1 <= 0) return undefined;
|
||||
const node = _rest.slice(0, _at1);
|
||||
_rest = _rest.slice(_at1 + 5);
|
||||
if (_rest !== "") return undefined;
|
||||
return { node };
|
||||
}
|
||||
|
||||
/** The key of one value of the state wants under the form "held.<module>.<fingerprint>". */
|
||||
export function wantsByHeldModuleFingerprintKey(module: string, fingerprint: string): string {
|
||||
return "held." + module + "." + fingerprint;
|
||||
}
|
||||
|
||||
/** The parts of a key of the form "held.<module>.<fingerprint>", or undefined for a key of another form. */
|
||||
export function parseWantsByHeldModuleFingerprintKey(_key: string): { module: string; fingerprint: string } | undefined {
|
||||
let _rest = _key;
|
||||
if (!_rest.startsWith("held.")) return undefined;
|
||||
_rest = _rest.slice(5);
|
||||
const _at1 = _rest.indexOf(".");
|
||||
if (_at1 <= 0) return undefined;
|
||||
const module = _rest.slice(0, _at1);
|
||||
_rest = _rest.slice(_at1 + 1);
|
||||
if (_rest === "") return undefined;
|
||||
const fingerprint = _rest;
|
||||
_rest = "";
|
||||
if (_rest !== "") return undefined;
|
||||
return { module, fingerprint };
|
||||
}
|
||||
|
||||
/** The key of one value of the state differences under the form "<node>.<module>". */
|
||||
export function differencesByNodeModuleKey(node: string, module: string): string {
|
||||
return node + "." + module;
|
||||
}
|
||||
|
||||
/** The parts of a key of the form "<node>.<module>", or undefined for a key of another form. */
|
||||
export function parseDifferencesByNodeModuleKey(_key: string): { node: string; module: string } | undefined {
|
||||
let _rest = _key;
|
||||
const _at0 = _rest.indexOf(".");
|
||||
if (_at0 <= 0) return undefined;
|
||||
const node = _rest.slice(0, _at0);
|
||||
_rest = _rest.slice(_at0 + 1);
|
||||
if (_rest === "") return undefined;
|
||||
const module = _rest;
|
||||
_rest = "";
|
||||
if (_rest !== "") return undefined;
|
||||
return { node, module };
|
||||
}
|
||||
|
||||
/** The key of one value of the state compositions under the form "<group>.<heads>". */
|
||||
export function compositionsByGroupHeadsKey(group: string, heads: string): string {
|
||||
return group + "." + heads;
|
||||
}
|
||||
|
||||
/** The parts of a key of the form "<group>.<heads>", or undefined for a key of another form. */
|
||||
export function parseCompositionsByGroupHeadsKey(_key: string): { group: string; heads: string } | undefined {
|
||||
let _rest = _key;
|
||||
const _at0 = _rest.indexOf(".");
|
||||
if (_at0 <= 0) return undefined;
|
||||
const group = _rest.slice(0, _at0);
|
||||
_rest = _rest.slice(_at0 + 1);
|
||||
if (_rest === "") return undefined;
|
||||
const heads = _rest;
|
||||
_rest = "";
|
||||
if (_rest !== "") return undefined;
|
||||
return { group, heads };
|
||||
}
|
||||
|
||||
/** is what upgrade takes. */
|
||||
export interface UpgradeInput {
|
||||
/** "true": each node's own composition, by its digest, and the assigner's findings */
|
||||
"compare"?: "true" | "false";
|
||||
/** one module */
|
||||
"module"?: string;
|
||||
/** with module: roll, together, record, or default to take a person's choice back */
|
||||
"policy"?: string;
|
||||
/** with policy: why, required and kept */
|
||||
"why"?: string;
|
||||
}
|
||||
|
||||
/** is what upgrade answers. */
|
||||
export type UpgradeAnswer = Record<string, unknown>;
|
||||
|
||||
/** is what bus takes. */
|
||||
export interface BusInput {
|
||||
}
|
||||
|
||||
/** is what bus answers. */
|
||||
export type BusAnswer = Record<string, unknown>;
|
||||
|
||||
/** is the body of the event policy-changed. */
|
||||
export type PolicyChangedEvent = Record<string, unknown>;
|
||||
|
||||
/** is one value of the state wants under the key want.<node>.<module>. */
|
||||
export interface WantsByWantNodeModuleValue {
|
||||
/** again or skip */
|
||||
"backup"?: "again" | "skip";
|
||||
"composed"?: string;
|
||||
"fp"?: string;
|
||||
"hold"?: boolean;
|
||||
"newest"?: string;
|
||||
"put"?: boolean;
|
||||
"release"?: string;
|
||||
"removed"?: boolean;
|
||||
/** the rule that holds the node back, 1 to 10 */
|
||||
"rule"?: number;
|
||||
"self"?: boolean;
|
||||
"v"?: number;
|
||||
"words"?: string;
|
||||
"yes"?: string;
|
||||
}
|
||||
|
||||
/** is one value of the state wants under the key want.<node>.node. */
|
||||
export interface WantsByWantNodeNodeValue {
|
||||
"composed": string;
|
||||
"self"?: boolean;
|
||||
"v"?: number;
|
||||
"words"?: string;
|
||||
}
|
||||
|
||||
/** is one value of the state wants under the key held.<module>.<fingerprint>. */
|
||||
export interface WantsByHeldModuleFingerprintValue {
|
||||
"node": string;
|
||||
"v"?: number;
|
||||
"words"?: string;
|
||||
}
|
||||
|
||||
/** is one value of the state differences under the key <node>.<module>. */
|
||||
export interface DifferencesByNodeModuleValue {
|
||||
"rule"?: number;
|
||||
"sent"?: string;
|
||||
"sent-build"?: string;
|
||||
"unexplained"?: boolean;
|
||||
"v"?: number;
|
||||
"want"?: string;
|
||||
"want-removed"?: boolean;
|
||||
"words"?: string;
|
||||
}
|
||||
|
||||
/** is one value of the state compositions under the key <group>.<heads>. */
|
||||
export interface CompositionsByGroupHeadsValue {
|
||||
"group": string;
|
||||
/** 12 lowercase hex digits */
|
||||
"heads": string;
|
||||
"machine"?: string;
|
||||
"machines"?: string[];
|
||||
"sdk"?: string;
|
||||
/** pass, fail or waits */
|
||||
"state": string;
|
||||
/** milliseconds */
|
||||
"took"?: number;
|
||||
"v"?: number;
|
||||
"warnings"?: string[];
|
||||
"words"?: string;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
+7
-7
@@ -348,12 +348,12 @@ func TestAMeshSDKMergeMovesOnlyTheModulesImportingAChangedPackage(t *testing.T)
|
||||
}
|
||||
|
||||
// ADR 0636 §5, D-I3, D-I5: a seat of the mesh's own contracts is taken only from its domain's contracts module, and a move
|
||||
// there is a handover in either merge order of the two trunks. `wants` (Placement) and `vault` (Identity and access) are
|
||||
// there is a handover in either merge order of the two trunks. `wants` (Provisioning) and `vault` (Identity and access) are
|
||||
// held by mesh-contracts as an older indexer kept them.
|
||||
seat := func(name, domain string) string {
|
||||
return `{"format":1,"seat":"` + name + `","scope":"mesh","grain":"seat-list","decision":"d","domain":"` + domain + `"}`
|
||||
}
|
||||
for name, domain := range map[string]string{"wants": "Placement", "vault": "Identity and access"} {
|
||||
for name, domain := range map[string]string{"wants": "Provisioning", "vault": "Identity and access"} {
|
||||
var d seatdef.Definition
|
||||
if err := json.Unmarshal([]byte(seat(name, domain)), &d); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -365,18 +365,18 @@ func TestAMeshSDKMergeMovesOnlyTheModulesImportingAChangedPackage(t *testing.T)
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
const dropped, placement, identity = "8888888888888888888888888888888888888888", "9999999999999999999999999999999999999999", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
const dropped, provisioningDef, identity = "8888888888888888888888888888888888888888", "9999999999999999999999999999999999999999", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
seatFiles[dropped] = map[string]string{"seats/git/seat.json": git} // mesh-contracts without wants and vault
|
||||
seatFiles[placement] = map[string]string{"seats/wants/seat.json": seat("wants", "Placement")}
|
||||
seatFiles[provisioningDef] = map[string]string{"seats/wants/seat.json": seat("wants", "Provisioning")}
|
||||
seatFiles[identity] = map[string]string{"seats/vault/seat.json": seat("vault", "Identity and access")}
|
||||
domainModule := func(name string) string {
|
||||
return strings.Replace(contracts, `"mesh-contracts"`, `"`+name+`"`, 1)
|
||||
}
|
||||
// Order 1: the domain's module merges first (its own repository) and takes wants over.
|
||||
merged("novox/mesh-placement", 90, placement, forgemsg.Forge{Root: "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
|
||||
Trees: map[string]string{"": "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"}, Manifests: map[string]string{"": domainModule("mesh-placement-contracts")}})
|
||||
merged("novox/mesh-provisioning", 90, provisioningDef, forgemsg.Forge{Root: "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
|
||||
Trees: map[string]string{"": "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"}, Manifests: map[string]string{"": domainModule("mesh-provisioning-contracts")}})
|
||||
entry("wants.trunk", func(e index.SeatDefinitionEntry) bool {
|
||||
return e.Module == "mesh-placement-contracts" && e.Commit == placement
|
||||
return e.Module == "mesh-provisioning-contracts" && e.Commit == provisioningDef
|
||||
})
|
||||
// mesh-contracts then drops both: wants stays the domain module's; vault, whose module has not merged yet, is kept.
|
||||
merged("novox/mesh-contracts", 91, dropped, forgemsg.Forge{Root: "dddddddddddddddddddddddddddddddddddddddd",
|
||||
|
||||
@@ -318,13 +318,13 @@ func TestTheControlRecords(t *testing.T) {
|
||||
|
||||
// ADR 0636 §5, D-I3, D-I5: a seat of the mesh's own contracts is taken only from its domain's contracts module; a
|
||||
// move there is a handover in either order of the two trunks; a second definer is refused with a finding naming both.
|
||||
// The seats `wants` (Placement) and `vault` (Identity and access) are held by mesh-contracts as an older indexer
|
||||
// The seats `wants` (Provisioning) and `vault` (Identity and access) are held by mesh-contracts as an older indexer
|
||||
// kept them; their definitions are listed from the module's `seats/` directory with no index.
|
||||
seat := func(name, domain string) string {
|
||||
return `{"format":1,"seat":"` + name + `","scope":"mesh","grain":"seat-list","decision":"d","domain":"` + domain + `"}`
|
||||
}
|
||||
for _, name := range []string{"wants", "vault"} {
|
||||
dom := map[string]string{"wants": "Placement", "vault": "Identity and access"}[name]
|
||||
dom := map[string]string{"wants": "Provisioning", "vault": "Identity and access"}[name]
|
||||
var d seatdef.Definition
|
||||
if err := json.Unmarshal([]byte(seat(name, dom)), &d); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -334,10 +334,10 @@ func TestTheControlRecords(t *testing.T) {
|
||||
Commit: "old", Digest: digest, Definition: raw})
|
||||
defs.vals[name+".trunk"] = data
|
||||
}
|
||||
files["c1"] = map[string]string{"seats/wants/seat.json": seat("wants", "Placement"), "seats/vault/seat.json": seat("vault", "Identity and access"),
|
||||
files["c1"] = map[string]string{"seats/wants/seat.json": seat("wants", "Provisioning"), "seats/vault/seat.json": seat("vault", "Identity and access"),
|
||||
"seats/lone/seat.json": seat("lone", "Core")}
|
||||
files["c2"] = map[string]string{"seats/lone/seat.json": seat("lone", "Core")}
|
||||
files["p1"] = map[string]string{"seats/wants/seat.json": seat("wants", "Placement")}
|
||||
files["p1"] = map[string]string{"seats/wants/seat.json": seat("wants", "Provisioning")}
|
||||
files["i1"] = map[string]string{"seats/vault/seat.json": seat("vault", "Identity and access")}
|
||||
domainManifest := func(module string) map[string]string {
|
||||
return map[string]string{meshcatalogclient.RootPath: strings.Replace(manifest, `"mesh-contracts"`, `"`+module+`"`, 1)}
|
||||
@@ -354,7 +354,7 @@ func TestTheControlRecords(t *testing.T) {
|
||||
// mesh-contracts defines wants and vault, seats of other domains: refused with a finding naming both modules, the
|
||||
// held entries stand, and lone (Core) is taken.
|
||||
mergeAt(repo, "c1", map[string]string{meshcatalogclient.RootPath: manifest})
|
||||
if v, ok := findings.vals[outside]; !ok || !strings.Contains(string(v), "mesh-placement-contracts") || !strings.Contains(string(v), "mesh-identity-and-access-contracts") ||
|
||||
if v, ok := findings.vals[outside]; !ok || !strings.Contains(string(v), "mesh-provisioning-contracts") || !strings.Contains(string(v), "mesh-identity-and-access-contracts") ||
|
||||
!strings.Contains(string(v), "mesh-contracts defines it") {
|
||||
t.Errorf("finding %s = %s, want it naming both modules", outside, v)
|
||||
}
|
||||
@@ -366,12 +366,12 @@ func TestTheControlRecords(t *testing.T) {
|
||||
}
|
||||
// Order 1: the domain's module merges first (another repository) and takes wants over; mesh-contracts then drops it and
|
||||
// the entry stays the domain module's, nothing removed.
|
||||
mergeAt("novox/mesh-placement", "p1", domainManifest("mesh-placement-contracts"))
|
||||
if w, _ := entry("wants.trunk"); w.Module != "mesh-placement-contracts" || w.Commit != "p1" {
|
||||
mergeAt("novox/mesh-provisioning", "p1", domainManifest("mesh-provisioning-contracts"))
|
||||
if w, _ := entry("wants.trunk"); w.Module != "mesh-provisioning-contracts" || w.Commit != "p1" {
|
||||
t.Errorf("the domain's module did not take wants over: %+v", w)
|
||||
}
|
||||
mergeAt(repo, "c2", map[string]string{meshcatalogclient.RootPath: manifest})
|
||||
if w, _ := entry("wants.trunk"); w.Module != "mesh-placement-contracts" {
|
||||
if w, _ := entry("wants.trunk"); w.Module != "mesh-provisioning-contracts" {
|
||||
t.Errorf("mesh-contracts dropping wants removed or replaced the domain module's entry: %+v", w)
|
||||
}
|
||||
if _, ok := findings.vals[outside]; ok {
|
||||
@@ -391,14 +391,14 @@ func TestTheControlRecords(t *testing.T) {
|
||||
t.Errorf("said %+v: a handover removes nothing", c)
|
||||
}
|
||||
}
|
||||
// A pull request of mesh-contracts defining a Placement seat fails its check as a definition that does not parse.
|
||||
files["h2"] = map[string]string{"seats/wants/seat.json": seat("wants", "Placement")}
|
||||
// A pull request of mesh-contracts defining a Provisioning seat fails its check as a definition that does not parse.
|
||||
files["h2"] = map[string]string{"seats/wants/seat.json": seat("wants", "Provisioning")}
|
||||
ix.ReadFile = func(_ context.Context, _, sha, p string) ([]byte, error) { return []byte(files[sha][p]), nil }
|
||||
if err := ix.ApplySeatDefinitions(ctx, forgemsg.Forge{Kind: forgemsg.ForgePull, Action: forgemsg.PullUpdated, Repository: repo, Number: 11, Head: "h2",
|
||||
Dirs: []string{meshcatalogclient.RootPath}, Manifests: map[string]string{meshcatalogclient.RootPath: manifest}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if e, _ := entry(SeatDefinitionKey("wants", SourcePull, repo, 11)); !strings.Contains(e.Unparsed, "mesh-placement-contracts") || e.Definition != nil {
|
||||
if e, _ := entry(SeatDefinitionKey("wants", SourcePull, repo, 11)); !strings.Contains(e.Unparsed, "mesh-provisioning-contracts") || e.Definition != nil {
|
||||
t.Errorf("a pull request's wants from mesh-contracts = %+v, want it refused naming the domain's module", e)
|
||||
}
|
||||
// The verb seat-definitions: no seat lists every trunk seat with its digest (never a pull request's); a seat answers it whole.
|
||||
|
||||
@@ -19,9 +19,9 @@ import (
|
||||
// fallbackContracts is the mesh's contracts module for a domain with no contracts module of its own.
|
||||
const fallbackContracts = "mesh-contracts"
|
||||
|
||||
// domainsWithContracts are the domains whose seats have a contracts module of their own (ADR 0634 §7): the seven
|
||||
// domainsWithContracts are the domains whose seats have a contracts module of their own (ADR 0634 §7): the six
|
||||
// the milestone creates. Every other domain's seats are defined in fallbackContracts.
|
||||
var domainsWithContracts = []string{"Delivery", "Placement", "Provisioning", "Identity and access", "Health and repair",
|
||||
var domainsWithContracts = []string{"Delivery", "Provisioning", "Identity and access", "Health and repair",
|
||||
"Connectivity", "Operator and conversation"}
|
||||
|
||||
// domainContracts is the module that defines a domain's seats: mesh-<domain>-contracts, the heading lowercased with
|
||||
|
||||
Reference in New Issue
Block a user