Lay out the controller's grants and the assigner's composition verdict under the checker's key, so the assigner's rule 6 and the checker's composition read one layout (issue 1297 subtask B1b, issue 1394; ADR 0528 clauses 2 and 5)
This commit is contained in:
@@ -285,3 +285,124 @@ func DecodeDifference(data []byte) (Difference, error) {
|
||||
|
||||
// SameDifference says whether two differences are the same value.
|
||||
func SameDifference(a, b Difference) bool { return reflect.DeepEqual(a, b) }
|
||||
|
||||
// The assigner's composition check (novox/hq ADR 0446 clauses 1 to 5, ADR 0528 clause 5, ADR 0482 clauses 3 and 9): for
|
||||
// each group the checker judges, the assigner composes the machines the change touches and writes its verdict under
|
||||
// the checker's key of that check, `<escaped group>.<heads hash>` (CheckerVerdictKey's form), in its bucket
|
||||
// compositions. The checker watches that key and reads it into its verdict's composition (Composition); a key not yet
|
||||
// written is a composition still running.
|
||||
const (
|
||||
// AssignerCompositionsBucket is the assigner's bucket of composition verdicts.
|
||||
AssignerCompositionsBucket = AssignerModule + "_compositions"
|
||||
// AssignerCompositionsHistory is the revisions the bucket keeps per key.
|
||||
AssignerCompositionsHistory = 5
|
||||
// CompositionWaits is the state of a composition that waits for what it composes from (ADR 0446 clause 3: the
|
||||
// indexer holds no candidates for the group yet), its Words saying what; it is no verdict.
|
||||
CompositionWaits = "waits"
|
||||
// CompositionRebuildWarning is the most modules a merge may rebuild before the composition warns (ADR 0446 clause 1).
|
||||
CompositionRebuildWarning = 12
|
||||
)
|
||||
|
||||
// CompositionKey is the key, in AssignerCompositionsBucket, of the composition of a group's check at its heads: the
|
||||
// checker's verdict key of the same check (CheckerVerdictKey).
|
||||
func CompositionKey(group, headsHash string) (string, error) {
|
||||
return CheckerVerdictKey(group, headsHash)
|
||||
}
|
||||
|
||||
// ParseCompositionKey is the inverse of CompositionKey; it refuses a want, held or difference key, naming it.
|
||||
func ParseCompositionKey(key string) (group, headsHash string, err error) {
|
||||
if group, headsHash, err = ParseCheckerVerdictKey(key); err != nil {
|
||||
return "", "", fmt.Errorf("delivery: %q is not a composition key, <group>.<heads hash>: %w", key, err)
|
||||
}
|
||||
return group, headsHash, nil
|
||||
}
|
||||
|
||||
// CompositionVerdict is the value of a composition key: the group and its heads hash, the State (pass, fail or waits),
|
||||
// the Words of a refusal or of what it waits for, the mesh-sdk commit it composed with (the assigner's own, or the
|
||||
// group's mesh-sdk member's head, ADR 0482 clause 3), the machines composed, the first machine that failed, the
|
||||
// warnings that fail nothing (more than twelve modules rebuilt; what was already broken before the change, ADR 0446
|
||||
// clause 1), and the time it took in integer milliseconds.
|
||||
type CompositionVerdict struct {
|
||||
V int `json:"v"`
|
||||
Group string `json:"group"`
|
||||
Heads string `json:"heads"`
|
||||
State string `json:"state"`
|
||||
Words string `json:"words,omitempty"`
|
||||
SDK string `json:"sdk,omitempty"`
|
||||
Machines []string `json:"machines,omitempty"`
|
||||
Machine string `json:"machine,omitempty"`
|
||||
Warnings []string `json:"warnings,omitempty"`
|
||||
Took int64 `json:"took,omitempty"`
|
||||
}
|
||||
|
||||
func (c CompositionVerdict) check() error {
|
||||
if c.Group == "" {
|
||||
return fmt.Errorf("delivery: a composition names no group")
|
||||
}
|
||||
if !isHex12(c.Heads) {
|
||||
return fmt.Errorf("delivery: the composition's heads hash %q is not 12 lowercase hex digits", c.Heads)
|
||||
}
|
||||
if c.Took < 0 {
|
||||
return fmt.Errorf("delivery: the composition's time is negative")
|
||||
}
|
||||
switch c.State {
|
||||
case CompositionPass:
|
||||
if c.SDK == "" {
|
||||
return fmt.Errorf("delivery: a composition that passed names no mesh-sdk commit")
|
||||
}
|
||||
if c.Machine != "" || c.Words != "" {
|
||||
return fmt.Errorf("delivery: a composition that passed names no failure")
|
||||
}
|
||||
case CompositionFail:
|
||||
if c.Words == "" {
|
||||
return fmt.Errorf("delivery: a composition that failed gives no words")
|
||||
}
|
||||
if c.SDK == "" {
|
||||
return fmt.Errorf("delivery: a composition that failed names no mesh-sdk commit")
|
||||
}
|
||||
case CompositionWaits:
|
||||
if c.Words == "" {
|
||||
return fmt.Errorf("delivery: a composition that waits does not say for what")
|
||||
}
|
||||
if c.Machine != "" || len(c.Machines) > 0 {
|
||||
return fmt.Errorf("delivery: a composition that waits composed no machine")
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("delivery: the composition state %q is not %s, %s or %s", c.State, CompositionPass, CompositionFail, CompositionWaits)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Composition is what the checker writes into its verdict from c: nil while c waits (the check is still running).
|
||||
func (c CompositionVerdict) Composition() *Composition {
|
||||
if c.State == CompositionWaits {
|
||||
return nil
|
||||
}
|
||||
return &Composition{State: c.State, Words: c.Words, SDK: c.SDK, Machines: c.Machines}
|
||||
}
|
||||
|
||||
// EncodeCompositionVerdict is c as JSON at Version; it refuses an unknown state, a pass or fail naming no mesh-sdk
|
||||
// commit, a fail without words, a pass naming a failure, and a wait that composed a machine.
|
||||
func EncodeCompositionVerdict(c CompositionVerdict) ([]byte, error) {
|
||||
if err := c.check(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.V = Version
|
||||
return json.Marshal(c)
|
||||
}
|
||||
|
||||
// DecodeCompositionVerdict reads a composition; it refuses an unknown version (ErrUnknownVersion) and what
|
||||
// EncodeCompositionVerdict refuses.
|
||||
func DecodeCompositionVerdict(data []byte) (CompositionVerdict, error) {
|
||||
var c CompositionVerdict
|
||||
if err := decode(data, &c); err != nil {
|
||||
return CompositionVerdict{}, err
|
||||
}
|
||||
if err := c.check(); err != nil {
|
||||
return CompositionVerdict{}, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// SameCompositionVerdict says whether two compositions are the same value.
|
||||
func SameCompositionVerdict(a, b CompositionVerdict) bool { return reflect.DeepEqual(a, b) }
|
||||
|
||||
@@ -155,3 +155,111 @@ func TestAssignerValuesRefused(t *testing.T) {
|
||||
t.Errorf("a difference naming no rule was read")
|
||||
}
|
||||
}
|
||||
|
||||
// 1297/B1b: a composition key is the checker's verdict key of the same check, and round-trips; a want, held or
|
||||
// difference key is refused, naming it.
|
||||
func TestCompositionKey(t *testing.T) {
|
||||
key, err := CompositionKey("feat/r4g", "0123456789ab")
|
||||
verdict, _ := CheckerVerdictKey("feat/r4g", "0123456789ab")
|
||||
if err != nil || key != verdict {
|
||||
t.Fatalf("CompositionKey = %q, %v; the checker's key is %q", key, err, verdict)
|
||||
}
|
||||
group, heads, err := ParseCompositionKey(key)
|
||||
if err != nil || group != "feat/r4g" || heads != "0123456789ab" {
|
||||
t.Errorf("ParseCompositionKey = %q, %q, %v", group, heads, err)
|
||||
}
|
||||
held, _ := HeldKey("mesh-mail", fp2of('a'))
|
||||
for _, bad := range []string{"want.ace.mesh-mail", held, "ace.mesh-mail", "g.0123456789AB", ""} {
|
||||
if _, _, err := ParseCompositionKey(bad); err == nil || !strings.Contains(err.Error(), "\""+bad+"\"") {
|
||||
t.Errorf("ParseCompositionKey(%q) = %v, want a refusal naming it", bad, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1297/B1b: a composition in each state round-trips; the checker's composition of a pass or fail carries its state,
|
||||
// words, mesh-sdk commit and machines, and a wait gives none.
|
||||
func TestCompositionVerdictRoundTrip(t *testing.T) {
|
||||
for _, c := range []CompositionVerdict{
|
||||
{Group: "g", Heads: "0123456789ab", State: CompositionPass, SDK: "7aacbc1", Machines: []string{"ace"}, Took: 120},
|
||||
{Group: "g", Heads: "0123456789ab", State: CompositionFail, SDK: "7aacbc1", Machines: []string{"ace", "g14"}, Machine: "g14",
|
||||
Words: "g14 runs mesh-mail, which the change removes from its source", Warnings: []string{"the merge rebuilds 13 modules"}},
|
||||
{Group: "g", Heads: "0123456789ab", State: CompositionWaits, Words: "the indexer holds no candidates for the group yet"},
|
||||
} {
|
||||
data, err := EncodeCompositionVerdict(c)
|
||||
if err != nil {
|
||||
t.Fatalf("EncodeCompositionVerdict(%+v): %v", c, err)
|
||||
}
|
||||
got, err := DecodeCompositionVerdict(data)
|
||||
c.V = Version
|
||||
if err != nil || !SameCompositionVerdict(got, c) {
|
||||
t.Errorf("composition %+v came back %+v, %v", c, got, err)
|
||||
}
|
||||
comp := got.Composition()
|
||||
if c.State == CompositionWaits {
|
||||
if comp != nil {
|
||||
t.Errorf("a wait gave the checker a composition %+v", comp)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if comp == nil || comp.State != c.State || comp.SDK != c.SDK || comp.Words != c.Words || len(comp.Machines) != len(c.Machines) {
|
||||
t.Errorf("the checker's composition of %+v is %+v", c, comp)
|
||||
}
|
||||
if err := comp.check(); err != nil {
|
||||
t.Errorf("the checker's composition of %+v is refused by the verdict: %v", c, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1297/B1b: what a composition may not say is refused.
|
||||
func TestCompositionVerdictRefused(t *testing.T) {
|
||||
for _, c := range []CompositionVerdict{
|
||||
{Heads: "0123456789ab", State: CompositionPass, SDK: "x"},
|
||||
{Group: "g", Heads: "0123", State: CompositionPass, SDK: "x"},
|
||||
{Group: "g", Heads: "0123456789ab", State: CompositionPass},
|
||||
{Group: "g", Heads: "0123456789ab", State: CompositionPass, SDK: "x", Machine: "ace"},
|
||||
{Group: "g", Heads: "0123456789ab", State: CompositionFail, SDK: "x"},
|
||||
{Group: "g", Heads: "0123456789ab", State: CompositionFail, Words: "w"},
|
||||
{Group: "g", Heads: "0123456789ab", State: CompositionWaits},
|
||||
{Group: "g", Heads: "0123456789ab", State: CompositionWaits, Words: "w", Machines: []string{"ace"}},
|
||||
{Group: "g", Heads: "0123456789ab", State: CompositionNotRun},
|
||||
{Group: "g", Heads: "0123456789ab", State: CompositionPass, SDK: "x", Took: -1},
|
||||
} {
|
||||
if _, err := EncodeCompositionVerdict(c); err == nil {
|
||||
t.Errorf("EncodeCompositionVerdict(%+v) accepted", c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1394/B1b: a grant key round-trips; a want or held key is refused, naming it; a grant round-trips and one naming an
|
||||
// empty account is refused.
|
||||
func TestControllerGrant(t *testing.T) {
|
||||
fp := fp2of('c')
|
||||
key, err := ControllerGrantKey("mesh-delivery-assigner", fp)
|
||||
if err != nil || key != "mesh-delivery-assigner."+strings.Repeat("c", 64) {
|
||||
t.Fatalf("ControllerGrantKey = %q, %v", key, err)
|
||||
}
|
||||
module, back, err := ParseControllerGrantKey(key)
|
||||
if err != nil || module != "mesh-delivery-assigner" || back != fp {
|
||||
t.Errorf("ParseControllerGrantKey = %q, %q, %v", module, back, err)
|
||||
}
|
||||
held, _ := HeldKey("mesh-mail", fp)
|
||||
for _, bad := range []string{"want.ace.mesh-mail", held, "ace.mesh-mail", ""} {
|
||||
if _, _, err := ParseControllerGrantKey(bad); err == nil || !strings.Contains(err.Error(), "\""+bad+"\"") {
|
||||
t.Errorf("ParseControllerGrantKey(%q) = %v, want a refusal naming it", bad, err)
|
||||
}
|
||||
}
|
||||
for _, g := range []ControllerGrant{{}, {Accounts: []string{"UABC", "UDEF"}}} {
|
||||
data, err := EncodeControllerGrant(g)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := DecodeControllerGrant(data)
|
||||
g.V = Version
|
||||
if err != nil || !SameControllerGrant(got, g) {
|
||||
t.Errorf("grant %+v came back %+v, %v", g, got, err)
|
||||
}
|
||||
}
|
||||
if _, err := EncodeControllerGrant(ControllerGrant{Accounts: []string{""}}); err == nil {
|
||||
t.Errorf("a grant naming an empty account was accepted")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package delivery
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// The controller's grants (novox/hq ADR 0528 clause 2, ADR 0429 §6 rule 6): when the controller has issued a module's
|
||||
// bus credential for a fingerprint, it writes `<module>.<fingerprint hex>` in its bucket grants. The assigner reads
|
||||
// it: a node's want moves to a fingerprint only once its grant exists. Only the controller writes the bucket.
|
||||
//
|
||||
// The names say Controller, since the catalogue's GrantKey (catalogue/streams.go) is a stream's grant pattern, unrelated.
|
||||
const (
|
||||
// ControllerGrantsBucket is the controller's bucket of issued credentials by fingerprint.
|
||||
ControllerGrantsBucket = "mesh-controller_grants"
|
||||
// ControllerGrantsHistory is the revisions the bucket keeps per key.
|
||||
ControllerGrantsHistory = 5
|
||||
)
|
||||
|
||||
// ControllerGrantKey is the key, in ControllerGrantsBucket, of a module's credential issued for a fingerprint:
|
||||
// `<module>.<hex>`, the hex being the fingerprint's key part (FingerprintKeyPart).
|
||||
func ControllerGrantKey(module, fingerprint string) (string, error) {
|
||||
return BuilderBuildKey(module, fingerprint)
|
||||
}
|
||||
|
||||
// ParseControllerGrantKey is the inverse of ControllerGrantKey; it refuses a want or held key, naming it.
|
||||
func ParseControllerGrantKey(key string) (module, fingerprint string, err error) {
|
||||
if module, fingerprint, err = ParseBuilderBuildKey(key); err != nil {
|
||||
return "", "", fmt.Errorf("delivery: %q is not a grant key, <module>.<fingerprint hex>: %w", key, err)
|
||||
}
|
||||
return module, fingerprint, nil
|
||||
}
|
||||
|
||||
// ControllerGrant is the value of a grant key: the bus accounts (their public keys) the module's processes were issued
|
||||
// for that fingerprint, none for a module whose grants the tool runner's account carries, so a reissue for another
|
||||
// account is a new revision. The entry's own time says when it was issued.
|
||||
type ControllerGrant struct {
|
||||
V int `json:"v"`
|
||||
Accounts []string `json:"accounts,omitempty"`
|
||||
}
|
||||
|
||||
func (g ControllerGrant) check() error {
|
||||
for _, a := range g.Accounts {
|
||||
if strings.TrimSpace(a) == "" || strings.ContainsAny(a, " \t\n") {
|
||||
return fmt.Errorf("delivery: a grant names an empty or spaced account %q", a)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// EncodeControllerGrant is g as JSON at Version; it refuses an empty or spaced account.
|
||||
func EncodeControllerGrant(g ControllerGrant) ([]byte, error) {
|
||||
if err := g.check(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
g.V = Version
|
||||
return json.Marshal(g)
|
||||
}
|
||||
|
||||
// DecodeControllerGrant reads a grant; it refuses an unknown version (ErrUnknownVersion) and an empty or spaced account.
|
||||
func DecodeControllerGrant(data []byte) (ControllerGrant, error) {
|
||||
var g ControllerGrant
|
||||
if err := decode(data, &g); err != nil {
|
||||
return ControllerGrant{}, err
|
||||
}
|
||||
if err := g.check(); err != nil {
|
||||
return ControllerGrant{}, err
|
||||
}
|
||||
return g, nil
|
||||
}
|
||||
|
||||
// SameControllerGrant says whether two grants are the same value.
|
||||
func SameControllerGrant(a, b ControllerGrant) bool { return reflect.DeepEqual(a, b) }
|
||||
Reference in New Issue
Block a user