Merge pull request 'A claim problem or a refused credential holds its module, not the whole pass (issue 1966, recovery)' (#219) from fix/1966-s1-partial-pass into main
This commit was merged in pull request #219.
This commit is contained in:
@@ -28,7 +28,7 @@ import (
|
|||||||
// writer folds them (issue 1126). Each module's Data says whether its manifest declares data. The compositions are unsigned
|
// writer folds them (issue 1126). Each module's Data says whether its manifest declares data. The compositions are unsigned
|
||||||
// (ADR 0593 §6) and not yet by digest (Composed.ByDigest).
|
// (ADR 0593 §6) and not yet by digest (Composed.ByDigest).
|
||||||
func Compositions(in Input, node string, fps map[string]string, mode Mode) (out map[string]engcomp.Composed, skipped []string, err error) {
|
func Compositions(in Input, node string, fps map[string]string, mode Mode) (out map[string]engcomp.Composed, skipped []string, err error) {
|
||||||
out, skipped, _, err = compositions(in, node, fps, mode, false)
|
out, skipped, _, _, err = compositions(in, node, fps, mode, false)
|
||||||
return out, skipped, err
|
return out, skipped, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ const BusUsersStandIn = "the bus's user list"
|
|||||||
// ErrStoodIn, naming each, so a stand-in is never applied as if it were the value; but for the bus's user list
|
// ErrStoodIn, naming each, so a stand-in is never applied as if it were the value; but for the bus's user list
|
||||||
// (BusUsersStandIn), which the bus node holds out of every apply of a composition it made itself.
|
// (BusUsersStandIn), which the bus node holds out of every apply of a composition it made itself.
|
||||||
func CompositionsApplied(in Input, node string, fps map[string]string) (map[string]engcomp.Composed, []string, error) {
|
func CompositionsApplied(in Input, node string, fps map[string]string) (map[string]engcomp.Composed, []string, error) {
|
||||||
out, skipped, _, err := compositions(in, node, fps, Reading, true)
|
out, skipped, _, _, err := compositions(in, node, fps, Reading, true)
|
||||||
return out, skipped, err
|
return out, skipped, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,13 +56,22 @@ func CompositionsApplied(in Input, node string, fps map[string]string) (map[stri
|
|||||||
// module the candidate adds, or one missing for a module the node runs. A module whose every value is held composes
|
// module the candidate adds, or one missing for a module the node runs. A module whose every value is held composes
|
||||||
// as CompositionsApplied composes it: the modes differ only in an own secret never issued.
|
// as CompositionsApplied composes it: the modes differ only in an own secret never issued.
|
||||||
func CompositionsShadow(in Input, node string, fps map[string]string) (out map[string]engcomp.Composed, skipped, stood []string, err error) {
|
func CompositionsShadow(in Input, node string, fps map[string]string) (out map[string]engcomp.Composed, skipped, stood []string, err error) {
|
||||||
return compositions(in, node, fps, Foreseeing, false)
|
out, skipped, stood, _, err = compositions(in, node, fps, Foreseeing, false)
|
||||||
|
return out, skipped, stood, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// CompositionsIn is the node's compositions in mode with what they stand in for answered (stood, sorted, the bus's user
|
// CompositionsIn is the node's compositions in mode with what they stand in for answered (stood, sorted, the bus's user
|
||||||
// list left out) rather than refused: CompositionsShadow in Foreseeing, and in Reading what CompositionsApplied refuses
|
// list left out) rather than refused: CompositionsShadow in Foreseeing, and in Reading what CompositionsApplied refuses
|
||||||
// before it is refused, so a caller that applies refuses each stand-in to the module it is for alone (issue 1126).
|
// before it is refused, so a caller that applies refuses each stand-in to the module it is for alone (issue 1126).
|
||||||
func CompositionsIn(in Input, node string, fps map[string]string, mode Mode) (out map[string]engcomp.Composed, skipped, stood []string, err error) {
|
func CompositionsIn(in Input, node string, fps map[string]string, mode Mode) (out map[string]engcomp.Composed, skipped, stood []string, err error) {
|
||||||
|
out, skipped, stood, _, err = compositions(in, node, fps, mode, false)
|
||||||
|
return out, skipped, stood, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// CompositionsHolding is CompositionsIn with the modules the node leaves out as they run, each with why: one whose claim cannot be
|
||||||
|
// held or whose credential was refused (issue 1966). Their compositions are not in out, so a caller that applies keeps what it last
|
||||||
|
// applied of each and says it held, as for any module refused alone.
|
||||||
|
func CompositionsHolding(in Input, node string, fps map[string]string, mode Mode) (out map[string]engcomp.Composed, skipped, stood []string, held map[string]string, err error) {
|
||||||
return compositions(in, node, fps, mode, false)
|
return compositions(in, node, fps, mode, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,7 +84,7 @@ func Folds(inOwn bool, fp string) bool {
|
|||||||
return inOwn && !strings.HasPrefix(fp, meshcatalogclient.SourcePrefix2)
|
return inOwn && !strings.HasPrefix(fp, meshcatalogclient.SourcePrefix2)
|
||||||
}
|
}
|
||||||
|
|
||||||
func compositions(in Input, node string, fps map[string]string, mode Mode, applied bool) (out map[string]engcomp.Composed, skipped, stood []string, err error) {
|
func compositions(in Input, node string, fps map[string]string, mode Mode, applied bool) (out map[string]engcomp.Composed, skipped, stood []string, heldBy map[string]string, err error) {
|
||||||
reading := in
|
reading := in
|
||||||
reading.Nodes = append([]Node(nil), in.Nodes...)
|
reading.Nodes = append([]Node(nil), in.Nodes...)
|
||||||
found := false
|
found := false
|
||||||
@@ -99,7 +108,7 @@ func compositions(in Input, node string, fps map[string]string, mode Mode, appli
|
|||||||
return module != "" && Folds(inOwn[module], fps[module])
|
return module != "" && Folds(inOwn[module], fps[module])
|
||||||
}
|
}
|
||||||
if !found {
|
if !found {
|
||||||
return nil, nil, nil, fmt.Errorf("compose: the mesh has no node %q", node)
|
return nil, nil, nil, nil, fmt.Errorf("compose: the mesh has no node %q", node)
|
||||||
}
|
}
|
||||||
// A folded module built by a build of the old form (Node.RunsOwn) is left to the node: composed from what runs of it
|
// A folded module built by a build of the old form (Node.RunsOwn) is left to the node: composed from what runs of it
|
||||||
// (Carried), so what it provides and requires still shapes the rest and what the others read of it (its published
|
// (Carried), so what it provides and requires still shapes the rest and what the others read of it (its published
|
||||||
@@ -167,10 +176,10 @@ func compositions(in Input, node string, fps map[string]string, mode Mode, appli
|
|||||||
}
|
}
|
||||||
d, problems, err := ComposeIn(reading, node, mode)
|
d, problems, err := ComposeIn(reading, node, mode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, nil, err
|
return nil, nil, nil, nil, err
|
||||||
}
|
}
|
||||||
if len(problems) > 0 {
|
if len(problems) > 0 {
|
||||||
return nil, nil, nil, fmt.Errorf("compose: %s's declaration is refused by the node-engine's validator: %v", node, problems)
|
return nil, nil, nil, nil, fmt.Errorf("compose: %s's declaration is refused by the node-engine's validator: %v", node, problems)
|
||||||
}
|
}
|
||||||
for _, x := range d.StoodIn {
|
for _, x := range d.StoodIn {
|
||||||
// The bus's user list is the bus node's own to follow, written alone and never from a composition the node
|
// The bus's user list is the bus node's own to follow, written alone and never from a composition the node
|
||||||
@@ -186,11 +195,11 @@ func compositions(in Input, node string, fps map[string]string, mode Mode, appli
|
|||||||
sort.Strings(stood)
|
sort.Strings(stood)
|
||||||
stood = slices.Compact(stood) // an own secret is both stood in for and foreseen
|
stood = slices.Compact(stood) // an own secret is both stood in for and foreseen
|
||||||
if applied && len(stood) > 0 {
|
if applied && len(stood) > 0 {
|
||||||
return nil, nil, nil, fmt.Errorf("%w on %s: %s", ErrStoodIn, node, strings.Join(stood, ", "))
|
return nil, nil, nil, nil, fmt.Errorf("%w on %s: %s", ErrStoodIn, node, strings.Join(stood, ", "))
|
||||||
}
|
}
|
||||||
body, err := d.Body()
|
body, err := d.Body()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, nil, err
|
return nil, nil, nil, nil, err
|
||||||
}
|
}
|
||||||
var envelope struct {
|
var envelope struct {
|
||||||
Resources []json.RawMessage `json:"resources"`
|
Resources []json.RawMessage `json:"resources"`
|
||||||
@@ -199,7 +208,7 @@ func compositions(in Input, node string, fps map[string]string, mode Mode, appli
|
|||||||
Provisioner string `json:"provisioner_account"`
|
Provisioner string `json:"provisioner_account"`
|
||||||
}
|
}
|
||||||
if err := json.Unmarshal(body, &envelope); err != nil {
|
if err := json.Unmarshal(body, &envelope); err != nil {
|
||||||
return nil, nil, nil, err
|
return nil, nil, nil, nil, err
|
||||||
}
|
}
|
||||||
var kept []json.RawMessage
|
var kept []json.RawMessage
|
||||||
keptPaths := map[string]bool{}
|
keptPaths := map[string]bool{}
|
||||||
@@ -213,7 +222,7 @@ func compositions(in Input, node string, fps map[string]string, mode Mode, appli
|
|||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
}
|
}
|
||||||
if err := json.Unmarshal(raw, &head); err != nil {
|
if err := json.Unmarshal(raw, &head); err != nil {
|
||||||
return nil, nil, nil, err
|
return nil, nil, nil, nil, err
|
||||||
}
|
}
|
||||||
if isCarried(head.Module, head.ID) {
|
if isCarried(head.Module, head.ID) {
|
||||||
continue // what the stub composed of it: the node's own carries what runs of it instead
|
continue // what the stub composed of it: the node's own carries what runs of it instead
|
||||||
@@ -223,11 +232,11 @@ func compositions(in Input, node string, fps map[string]string, mode Mode, appli
|
|||||||
// compositionsOf): the node's own composition is then the controller's, byte for byte.
|
// compositionsOf): the node's own composition is then the controller's, byte for byte.
|
||||||
var fields map[string]json.RawMessage
|
var fields map[string]json.RawMessage
|
||||||
if err := json.Unmarshal(raw, &fields); err != nil {
|
if err := json.Unmarshal(raw, &fields); err != nil {
|
||||||
return nil, nil, nil, err
|
return nil, nil, nil, nil, err
|
||||||
}
|
}
|
||||||
delete(fields, "module")
|
delete(fields, "module")
|
||||||
if raw, err = json.Marshal(fields); err != nil {
|
if raw, err = json.Marshal(fields); err != nil {
|
||||||
return nil, nil, nil, err
|
return nil, nil, nil, nil, err
|
||||||
}
|
}
|
||||||
if head.Type == string(declaration.TypeFile) {
|
if head.Type == string(declaration.TypeFile) {
|
||||||
foldedPaths[head.Path] = true
|
foldedPaths[head.Path] = true
|
||||||
@@ -248,7 +257,7 @@ func compositions(in Input, node string, fps map[string]string, mode Mode, appli
|
|||||||
if len(carried) > 0 {
|
if len(carried) > 0 {
|
||||||
var err error
|
var err error
|
||||||
if kept, foldedPaths, err = carry(kept, foldedPaths, runsOwn, isCarried); err != nil {
|
if kept, foldedPaths, err = carry(kept, foldedPaths, runsOwn, isCarried); err != nil {
|
||||||
return nil, nil, nil, err
|
return nil, nil, nil, nil, err
|
||||||
}
|
}
|
||||||
for _, p := range runsOwn.Filled {
|
for _, p := range runsOwn.Filled {
|
||||||
if foldedPaths[p] && !slices.Contains(envelope.Filled, p) {
|
if foldedPaths[p] && !slices.Contains(envelope.Filled, p) {
|
||||||
@@ -270,12 +279,12 @@ func compositions(in Input, node string, fps map[string]string, mode Mode, appli
|
|||||||
out, err = engcomp.Split(node, kept, fps, filled,
|
out, err = engcomp.Split(node, kept, fps, filled,
|
||||||
engcomp.NodeOwn{Adoption: envelope.Adoption, Provisioner: envelope.Provisioner})
|
engcomp.NodeOwn{Adoption: envelope.Adoption, Provisioner: envelope.Provisioner})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, nil, err
|
return nil, nil, nil, nil, err
|
||||||
}
|
}
|
||||||
if len(ownFilled) > 0 {
|
if len(ownFilled) > 0 {
|
||||||
own, err := engcomp.NodeKey(node)
|
own, err := engcomp.NodeKey(node)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, nil, err
|
return nil, nil, nil, nil, err
|
||||||
}
|
}
|
||||||
c := out[own]
|
c := out[own]
|
||||||
c.Filled = append(c.Filled, ownFilled...)
|
c.Filled = append(c.Filled, ownFilled...)
|
||||||
@@ -295,7 +304,7 @@ func compositions(in Input, node string, fps map[string]string, mode Mode, appli
|
|||||||
skipped = append(skipped, m)
|
skipped = append(skipped, m)
|
||||||
}
|
}
|
||||||
sort.Strings(skipped)
|
sort.Strings(skipped)
|
||||||
return out, skipped, stood, nil
|
return out, skipped, stood, d.Held, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RunsAnyOf says runs (a composition as it runs) holds a resource of module, by its module or the module its id names.
|
// RunsAnyOf says runs (a composition as it runs) holds a resource of module, by its module or the module its id names.
|
||||||
|
|||||||
@@ -23,12 +23,12 @@ type CredentialKey struct {
|
|||||||
// in neither list: the vault mints nothing for it (issue 1126).
|
// in neither list: the vault mints nothing for it (issue 1126).
|
||||||
func CredentialsNeeded(in Input, node string) (asConsumer, asProvider []CredentialKey, err error) {
|
func CredentialsNeeded(in Input, node string) (asConsumer, asProvider []CredentialKey, err error) {
|
||||||
c := newComposer(in)
|
c := newComposer(in)
|
||||||
resolved, err := c.resolution(node)
|
resolved, held, err := c.resolutionHolding(node)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
for _, n := range resolved.Needs {
|
for _, n := range resolved.Needs {
|
||||||
if n.ByRecord || n.From == "" || credentialless(n) {
|
if _, isHeld := held[n.For]; isHeld || n.ByRecord || n.From == "" || credentialless(n) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
asConsumer = append(asConsumer, CredentialKey{Provision: n.Name, Consumer: node, ConsumerModule: n.For,
|
asConsumer = append(asConsumer, CredentialKey{Provision: n.Name, Consumer: node, ConsumerModule: n.For,
|
||||||
|
|||||||
@@ -201,10 +201,7 @@ func TestAGrantIsCreatedForEachCredentialMadeForAConsumer(t *testing.T) {
|
|||||||
}
|
}
|
||||||
// A consumer whose set does not resolve is skipped, not fatal.
|
// A consumer whose set does not resolve is skipped, not fatal.
|
||||||
nodeOf(t, &in, "laptop").Assigned = append(nodeOf(t, &in, "laptop").Assigned, "files")
|
nodeOf(t, &in, "laptop").Assigned = append(nodeOf(t, &in, "laptop").Assigned, "files")
|
||||||
in.Catalogue["files"] = catalogue.Manifest{Module: "files", Version: "1", Requires: []string{"s3-bucket"},
|
in.Catalogue["files"] = catalogue.Manifest{Module: "files", Version: "1", Requires: []string{"s3-bucket", "nothing-gives-this"}}
|
||||||
Claims: []catalogue.Claim{{Name: "x", Scope: catalogue.ScopeNode}}}
|
|
||||||
in.Catalogue["twin"] = catalogue.Manifest{Module: "twin", Version: "1", Claims: []catalogue.Claim{{Name: "x", Scope: catalogue.ScopeNode}}}
|
|
||||||
nodeOf(t, &in, "laptop").Assigned = append(nodeOf(t, &in, "laptop").Assigned, "twin")
|
|
||||||
if got, err := Render(in, "anchor", Foreseeing); err != nil || len(got.Rendering.Grants) != 0 {
|
if got, err := Render(in, "anchor", Foreseeing); err != nil || len(got.Rendering.Grants) != 0 {
|
||||||
t.Errorf("a consumer that does not resolve: %v %+v", err, got.Rendering.Grants)
|
t.Errorf("a consumer that does not resolve: %v %+v", err, got.Rendering.Grants)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -337,7 +337,8 @@ func composeEach(in compose.Input, node string, fps map[string]string, applied b
|
|||||||
hold(m, failed[m], "")
|
hold(m, failed[m], "")
|
||||||
}
|
}
|
||||||
for {
|
for {
|
||||||
out, skipped, stood, err = compose.CompositionsIn(in, node, fps, mode)
|
var leftOut map[string]string
|
||||||
|
out, skipped, stood, leftOut, err = compose.CompositionsHolding(in, node, fps, mode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
module, leave := culprit(in, node, fps, mode, failed, err)
|
module, leave := culprit(in, node, fps, mode, failed, err)
|
||||||
if module == "" {
|
if module == "" {
|
||||||
@@ -346,6 +347,18 @@ func composeEach(in compose.Input, node string, fps map[string]string, applied b
|
|||||||
hold(module, err.Error(), leave)
|
hold(module, err.Error(), leave)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
// A module the node leaves out because its claim cannot be held or its credential was refused (issue 1966) is held with
|
||||||
|
// those words: its handed composition stays and the check names it refused; the rest composes.
|
||||||
|
heldNow := false
|
||||||
|
for _, m := range sortedKeys(leftOut) {
|
||||||
|
if _, done := failed[m]; !done {
|
||||||
|
hold(m, leftOut[m], "")
|
||||||
|
heldNow = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if heldNow {
|
||||||
|
continue
|
||||||
|
}
|
||||||
again := false
|
again := false
|
||||||
if applied && len(stood) > 0 {
|
if applied && len(stood) > 0 {
|
||||||
placed := map[string]bool{}
|
placed := map[string]bool{}
|
||||||
|
|||||||
@@ -300,3 +300,35 @@ func aHeldModuleOfItsOwnAccountNeverRefusesTheNode(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Issue 1966: two modules claiming one seat held once cost the second claimant, not the node: Compose holds it with the claim's
|
||||||
|
// words, composes the first, and (the node-engine keeping what it handed of a held module) leaves its composition out, whether or
|
||||||
|
// not it runs.
|
||||||
|
func TestASecondClaimantOfASeatIsHeldAndTheNodeComposesTheRest(t *testing.T) {
|
||||||
|
first := `{"module":"alpha","version":"1","claims":[{"name":"the-seat","scope":"node"}]}`
|
||||||
|
second := `{"module":"beta","version":"1","claims":[{"name":"the-seat","scope":"node"}]}`
|
||||||
|
for _, running := range []bool{false, true} {
|
||||||
|
b, fps := aNode(t, first, second)
|
||||||
|
scope := Scope{Node: "n1"}
|
||||||
|
if running {
|
||||||
|
scope.Running = []engcomp.Composed{{Node: "n1", Module: "beta", FP: fps["beta"], Resources: nil}}
|
||||||
|
}
|
||||||
|
got, _, held, err := Compose(context.Background(), b, Mesh{}, scope, fps)
|
||||||
|
if err != nil || len(held) != 1 || !strings.Contains(held["beta"], "alpha and beta both claim") {
|
||||||
|
t.Fatalf("running %v: err %v, held %v; want beta held with the claim's words", running, err, held)
|
||||||
|
}
|
||||||
|
for _, c := range got {
|
||||||
|
if c.Module == "beta" {
|
||||||
|
t.Errorf("running %v: the held claimant is composed: %+v", running, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// A check that adds the second claimant is refused for it, in the claim's words, and not judged to compose.
|
||||||
|
p := b.assignments["n1"]
|
||||||
|
ans := Answer(context.Background(), b, Mesh{}, Scope{Node: "n1", Assignments: map[string]*meshmoduleassigner.Assignments{"n1": &p}},
|
||||||
|
fps, scope.Running, "p-h1", 1)
|
||||||
|
if ans.Verdict != nodeengine.CandidateAnswerVerdictRefused || len(ans.Refusals) != 1 || ans.Refusals[0].Module != "beta" ||
|
||||||
|
!strings.Contains(ans.Refusals[0].Why, "both claim") {
|
||||||
|
t.Errorf("running %v: the answer is %+v, want beta refused with the claim's words", running, ans)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -283,7 +283,7 @@ func (c *composer) onTheNetworkNow() []Node {
|
|||||||
if len(n.Assigned) == 0 {
|
if len(n.Assigned) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
got, err := resolution.Resolve(c.in.Catalogue, n.Assigned,
|
got, _, err := resolution.ResolveHolding(c.in.Catalogue, n.Assigned,
|
||||||
catalogue.Node{Name: n.Name, Site: n.Place.Site, Capabilities: n.Capabilities},
|
catalogue.Node{Name: n.Name, Site: n.Place.Site, Capabilities: n.Capabilities},
|
||||||
catalogue.World{Unchecked: true, Holdings: c.in.Holdings})
|
catalogue.World{Unchecked: true, Holdings: c.in.Holdings})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -164,9 +164,8 @@ func TestANodeNotOnThePrivateNetworkGetsNoAddressAndNoNetwork(t *testing.T) {
|
|||||||
func TestARefusedNodeIsSaidWhenTheNetworkCannotBeComposed(t *testing.T) {
|
func TestARefusedNodeIsSaidWhenTheNetworkCannotBeComposed(t *testing.T) {
|
||||||
in := aMesh(t)
|
in := aMesh(t)
|
||||||
nodeOf(t, &in, "anchor").Place.Key = ""
|
nodeOf(t, &in, "anchor").Place.Key = ""
|
||||||
one := catalogue.Manifest{Module: "rival-one", Version: "1", Claims: []catalogue.Claim{{Name: "the-seat", Scope: catalogue.ScopeNode}}}
|
in.Catalogue["rival-one"] = catalogue.Manifest{Module: "rival-one", Version: "1", Resources: []map[string]any{{"id": "r", "type": "directory", "path": "/var/lib/the-same", "mode": "0700"}}}
|
||||||
two := catalogue.Manifest{Module: "rival-two", Version: "1", Claims: []catalogue.Claim{{Name: "the-seat", Scope: catalogue.ScopeNode}}}
|
in.Catalogue["rival-two"] = catalogue.Manifest{Module: "rival-two", Version: "1", Resources: []map[string]any{{"id": "r", "type": "directory", "path": "/var/lib/the-same", "mode": "0700"}}}
|
||||||
in.Catalogue["rival-one"], in.Catalogue["rival-two"] = one, two
|
|
||||||
hub := nodeOf(t, &in, "anchor")
|
hub := nodeOf(t, &in, "anchor")
|
||||||
hub.Assigned = append(hub.Assigned, "rival-one", "rival-two") // its own set is incoherent: the hub does not resolve
|
hub.Assigned = append(hub.Assigned, "rival-one", "rival-two") // its own set is incoherent: the hub does not resolve
|
||||||
_, err := Generators(in)
|
_, err := Generators(in)
|
||||||
@@ -293,14 +292,13 @@ func TestEachFieldOfTheInputReachesTheGenerator(t *testing.T) {
|
|||||||
}
|
}
|
||||||
laptop := nodeOf(t, &in, "laptop")
|
laptop := nodeOf(t, &in, "laptop")
|
||||||
laptop.Assigned = append(laptop.Assigned, "holder-one", "holder-two")
|
laptop.Assigned = append(laptop.Assigned, "holder-one", "holder-two")
|
||||||
// Without a key: a machine that does not resolve and holds a complete place keeps it on the network (issue 1833).
|
// Two claimants and nobody on record: the second is held and the machine stays on the network (issue 1966).
|
||||||
laptop.Place.Key = ""
|
if got, err := Resolve(in, "laptop"); err != nil || len(got.Held) != 1 || got.Held["holder-two"] == "" || addressesOf(in)["laptop"] != "10.77.0.2" {
|
||||||
if got := addressesOf(in); got["laptop"] != "" {
|
t.Fatalf("two claimants, none on record: held %v, %v", got.Held, err)
|
||||||
t.Fatalf("a machine whose set claims one seat twice is on the network: %v", got)
|
|
||||||
}
|
}
|
||||||
in.Holdings = []catalogue.Held{{Claim: "mesh-broker", Scope: catalogue.ScopeMesh, Node: "laptop", Module: "holder-one"}}
|
in.Holdings = []catalogue.Held{{Claim: "mesh-broker", Scope: catalogue.ScopeMesh, Node: "laptop", Module: "holder-one"}}
|
||||||
if got := addressesOf(in); got["laptop"] != "10.77.0.2" {
|
if got, err := Resolve(in, "laptop"); err != nil || len(got.Held) != 0 {
|
||||||
t.Errorf("a holder on record did not settle the seat: %v", got)
|
t.Errorf("a holder on record did not settle the seat: held %v, %v", got.Held, err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ type Declared struct {
|
|||||||
// ADR 0163, rule 6), sorted; LeftOutWhy is why each was.
|
// ADR 0163, rule 6), sorted; LeftOutWhy is why each was.
|
||||||
LeftOut []string
|
LeftOut []string
|
||||||
LeftOutWhy map[string]string
|
LeftOutWhy map[string]string
|
||||||
|
// Held is, of those, each module left out because its claim cannot be held or its credential was refused, and why (issue
|
||||||
|
// 1966): the node keeps what runs of it and the composer says it held. Not on the wire.
|
||||||
|
Held map[string]string
|
||||||
|
|
||||||
// Not on the wire:
|
// Not on the wire:
|
||||||
Received map[string]map[string][]catalogue.Contribution
|
Received map[string]map[string][]catalogue.Contribution
|
||||||
@@ -199,7 +202,7 @@ func (c *composer) declaration(nodeName string, mode Mode) (Declared, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return Declared{}, err
|
return Declared{}, err
|
||||||
}
|
}
|
||||||
out := Declared{Resources: composed.Resources, Adoption: adoptionFor(plan, node, composed),
|
out := Declared{Held: resolved.Held, Resources: composed.Resources, Adoption: adoptionFor(plan, node, composed),
|
||||||
Received: composed.Received, Mesh: with.Mesh, BusUsers: with.BusUsers,
|
Received: composed.Received, Mesh: with.Mesh, BusUsers: with.BusUsers,
|
||||||
LeftOut: sortedKeysOf(composed.LeftOut), LeftOutWhy: composed.LeftOut,
|
LeftOut: sortedKeysOf(composed.LeftOut), LeftOutWhy: composed.LeftOut,
|
||||||
Withheld: rendered.Withheld, Unbound: rendered.Unbound,
|
Withheld: rendered.Withheld, Unbound: rendered.Unbound,
|
||||||
|
|||||||
@@ -112,22 +112,6 @@ func TestAManifestTheNodeEngineRefusesComesBackAsProblemsAndNoError(t *testing.T
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1298/AC3: a node whose own set does not compose returns an error and no declaration, told apart from an unreadable Input.
|
|
||||||
func TestANodeWhoseSetDoesNotComposeReturnsAnErrorAndNoDeclaration(t *testing.T) {
|
|
||||||
in := aMesh(t)
|
|
||||||
in.Catalogue["rival-one"] = catalogue.Manifest{Module: "rival-one", Version: "1", Claims: []catalogue.Claim{{Name: "the-seat", Scope: catalogue.ScopeNode}}}
|
|
||||||
in.Catalogue["rival-two"] = catalogue.Manifest{Module: "rival-two", Version: "1", Claims: []catalogue.Claim{{Name: "the-seat", Scope: catalogue.ScopeNode}}}
|
|
||||||
laptop := nodeOf(t, &in, "laptop")
|
|
||||||
laptop.Assigned = append(laptop.Assigned, "rival-one", "rival-two")
|
|
||||||
d, problems, err := Compose(in, "laptop")
|
|
||||||
if err == nil || !IsUnresolvable(err) || d.Resources != nil || problems != nil {
|
|
||||||
t.Fatalf("a set that does not compose: %v %v %v", d.Resources, problems, err)
|
|
||||||
}
|
|
||||||
if _, _, err := Compose(in, "nowhere"); err == nil || IsUnresolvable(err) {
|
|
||||||
t.Fatalf("a node the Input does not have: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1298/AC4: composing the same Input twice gives byte-identical bodies, and the Input is unchanged.
|
// 1298/AC4: composing the same Input twice gives byte-identical bodies, and the Input is unchanged.
|
||||||
func TestComposingTwiceGivesTheSameBytesAndChangesNothing(t *testing.T) {
|
func TestComposingTwiceGivesTheSameBytesAndChangesNothing(t *testing.T) {
|
||||||
mk := func() Input {
|
mk := func() Input {
|
||||||
@@ -392,8 +376,8 @@ func overlayName() string { return overlay.Name }
|
|||||||
func TestANodeNotCountedAsOnTheNetworkIsSaidInTheResult(t *testing.T) {
|
func TestANodeNotCountedAsOnTheNetworkIsSaidInTheResult(t *testing.T) {
|
||||||
in := aMesh(t)
|
in := aMesh(t)
|
||||||
nodeOf(t, &in, "laptop").Place.Key = ""
|
nodeOf(t, &in, "laptop").Place.Key = ""
|
||||||
in.Catalogue["rival-one"] = catalogue.Manifest{Module: "rival-one", Version: "1", Claims: []catalogue.Claim{{Name: "the-seat", Scope: catalogue.ScopeNode}}}
|
in.Catalogue["rival-one"] = catalogue.Manifest{Module: "rival-one", Version: "1", Resources: []map[string]any{{"id": "r", "type": "directory", "path": "/var/lib/the-same", "mode": "0700"}}}
|
||||||
in.Catalogue["rival-two"] = catalogue.Manifest{Module: "rival-two", Version: "1", Claims: []catalogue.Claim{{Name: "the-seat", Scope: catalogue.ScopeNode}}}
|
in.Catalogue["rival-two"] = catalogue.Manifest{Module: "rival-two", Version: "1", Resources: []map[string]any{{"id": "r", "type": "directory", "path": "/var/lib/the-same", "mode": "0700"}}}
|
||||||
laptop := nodeOf(t, &in, "laptop")
|
laptop := nodeOf(t, &in, "laptop")
|
||||||
laptop.Assigned = append(laptop.Assigned, "rival-one", "rival-two")
|
laptop.Assigned = append(laptop.Assigned, "rival-one", "rival-two")
|
||||||
said := func(lines []string) bool {
|
said := func(lines []string) bool {
|
||||||
|
|||||||
@@ -147,8 +147,8 @@ func TestANetworkThatCannotBeComposedFromWhatRemainsStillFails(t *testing.T) {
|
|||||||
// among the sources "from the mesh" -- and another machine still composes.
|
// among the sources "from the mesh" -- and another machine still composes.
|
||||||
func TestAMachineThatDoesNotResolveKeepsItsNameAndItsAddressInTheMesh(t *testing.T) {
|
func TestAMachineThatDoesNotResolveKeepsItsNameAndItsAddressInTheMesh(t *testing.T) {
|
||||||
in := fourOnTheNetwork(t)
|
in := fourOnTheNetwork(t)
|
||||||
in.Catalogue["rival-one"] = catalogue.Manifest{Module: "rival-one", Version: "1", Claims: []catalogue.Claim{{Name: "the-seat", Scope: catalogue.ScopeNode}}}
|
in.Catalogue["rival-one"] = catalogue.Manifest{Module: "rival-one", Version: "1", Resources: []map[string]any{{"id": "r", "type": "directory", "path": "/var/lib/the-same", "mode": "0700"}}}
|
||||||
in.Catalogue["rival-two"] = catalogue.Manifest{Module: "rival-two", Version: "1", Claims: []catalogue.Claim{{Name: "the-seat", Scope: catalogue.ScopeNode}}}
|
in.Catalogue["rival-two"] = catalogue.Manifest{Module: "rival-two", Version: "1", Resources: []map[string]any{{"id": "r", "type": "directory", "path": "/var/lib/the-same", "mode": "0700"}}}
|
||||||
hub := nodeOf(t, &in, "anchor")
|
hub := nodeOf(t, &in, "anchor")
|
||||||
hub.Assigned = append(hub.Assigned, "rival-one", "rival-two")
|
hub.Assigned = append(hub.Assigned, "rival-one", "rival-two")
|
||||||
if _, err := Resolve(in, "anchor"); !IsUnresolvable(err) {
|
if _, err := Resolve(in, "anchor"); !IsUnresolvable(err) {
|
||||||
|
|||||||
@@ -114,6 +114,17 @@ func (c *composer) render(nodeName string, mode Mode) (Rendered, error) {
|
|||||||
out.CoreBoxesWanted = wanted
|
out.CoreBoxesWanted = wanted
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A module whose claim cannot be held, or whose credential was refused, is left out of the declaration and run as it runs:
|
||||||
|
// the machine composes the rest (issue 1966, ADR 0636 §3).
|
||||||
|
for module, why := range resolved.Held {
|
||||||
|
if r.NotPlaced == nil {
|
||||||
|
r.NotPlaced = map[string]string{}
|
||||||
|
}
|
||||||
|
if _, said := r.NotPlaced[module]; !said {
|
||||||
|
r.NotPlaced[module] = why
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// And a certificate for this machine's name inside the mesh, when anything on it asks.
|
// And a certificate for this machine's name inside the mesh, when anything on it asks.
|
||||||
for _, m := range plan.Modules {
|
for _, m := range plan.Modules {
|
||||||
if m.Certificate == nil {
|
if m.Certificate == nil {
|
||||||
|
|||||||
@@ -1253,6 +1253,11 @@ func Rules(r mcat.Resolution, with mcat.Rendering) ([]mcat.Rule, error) {
|
|||||||
// cannot be read for it, its listens open no wider than the mesh, never at the manifest's
|
// cannot be read for it, its listens open no wider than the mesh, never at the manifest's
|
||||||
// default, which could reopen a port an operator had narrowed.
|
// default, which could reopen a port an operator had narrowed.
|
||||||
left := LeftOut(r, with.Settings, with.Adopted)
|
left := LeftOut(r, with.Settings, with.Adopted)
|
||||||
|
for module, why := range with.NotPlaced {
|
||||||
|
if _, already := left[module]; !already {
|
||||||
|
left[module] = why
|
||||||
|
}
|
||||||
|
}
|
||||||
exposure := map[string]map[int]string{}
|
exposure := map[string]map[int]string{}
|
||||||
for _, m := range r.Modules {
|
for _, m := range r.Modules {
|
||||||
e, err := exposureOf(m, with.Settings[m.Module])
|
e, err := exposureOf(m, with.Settings[m.Module])
|
||||||
|
|||||||
@@ -37,6 +37,11 @@ func TestWithNoHolderOnRecordTheSoleClaimantHoldsAndTwoAreRefused(t *testing.T)
|
|||||||
if len(problems) != 1 || !strings.Contains(problems[0], "both claim") {
|
if len(problems) != 1 || !strings.Contains(problems[0], "both claim") {
|
||||||
t.Fatalf("two claimants with nothing on record were not refused: %v", problems)
|
t.Fatalf("two claimants with nothing on record were not refused: %v", problems)
|
||||||
}
|
}
|
||||||
|
// Held for the module that cannot hold, which then holds nothing; the other still does (issue 1966).
|
||||||
|
held, holding := holdClaims([]mcat.Manifest{oldBroker(), newBroker()}, node, nil, nil)
|
||||||
|
if len(holding) != 1 || !strings.Contains(holding["new-broker"], "both claim") || len(held) != 1 || held[0].Module != "old-broker" {
|
||||||
|
t.Fatalf("the second claimant is not the one held: held=%v holding=%v", held, holding)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// With a holder on record, the other eligible assignment is silent: not refused, and not holding.
|
// With a holder on record, the other eligible assignment is silent: not refused, and not holding.
|
||||||
|
|||||||
@@ -26,12 +26,17 @@ func (r *Refusal) Error() string {
|
|||||||
return "these assignments cannot be applied:\n - " + strings.Join(r.Problems, "\n - ")
|
return "these assignments cannot be applied:\n - " + strings.Join(r.Problems, "\n - ")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve works out everything a node runs, from what was assigned to it.
|
// ResolveHolding works out everything a node runs, from what was assigned to it. The catalogue is every module the mesh knows
|
||||||
//
|
// about; assigned is what a person put on this node. What comes back is the closure -- assigned modules plus everything they
|
||||||
// The catalogue is every module the mesh knows about; assigned is what a person put on this node.
|
// require -- or a refusal naming every reason it could not be closed. A claim problem costs the module that has it, not the
|
||||||
// What comes back is the closure — assigned modules plus everything they require — or a refusal
|
// machine (issue 1966, ADR 0636 §3: a refused claim blocks that claimant only). The modules that cannot hold what they claim come back with why, each still in
|
||||||
// naming every reason it could not be closed.
|
// the resolution's modules and none of them in its claims; the caller leaves them out of what it declares (LeftOut), so the
|
||||||
func Resolve(catalogue map[string]mcat.Manifest, assigned []string, node mcat.Node, world mcat.World) (mcat.Resolution, error) {
|
// machine composes everything else. Every other refusal is Resolve's.
|
||||||
|
func ResolveHolding(catalogue map[string]mcat.Manifest, assigned []string, node mcat.Node, world mcat.World) (mcat.Resolution, map[string]string, error) {
|
||||||
|
return resolve(catalogue, assigned, node, world)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolve(catalogue map[string]mcat.Manifest, assigned []string, node mcat.Node, world mcat.World) (mcat.Resolution, map[string]string, error) {
|
||||||
elsewhere := world.Held
|
elsewhere := world.Held
|
||||||
var problems []string
|
var problems []string
|
||||||
|
|
||||||
@@ -593,8 +598,7 @@ func Resolve(catalogue map[string]mcat.Manifest, assigned []string, node mcat.No
|
|||||||
}
|
}
|
||||||
|
|
||||||
problems = append(problems, checkCapabilities(resolution.Modules, node)...)
|
problems = append(problems, checkCapabilities(resolution.Modules, node)...)
|
||||||
claims, claimProblems := checkClaims(resolution.Modules, node, elsewhere, world.Holdings)
|
claims, held := holdClaims(resolution.Modules, node, elsewhere, world.Holdings)
|
||||||
problems = append(problems, claimProblems...)
|
|
||||||
problems = append(problems, checkResources(resolution.Modules)...)
|
problems = append(problems, checkResources(resolution.Modules)...)
|
||||||
resolution.Claims = claims
|
resolution.Claims = claims
|
||||||
|
|
||||||
@@ -615,10 +619,14 @@ func Resolve(catalogue map[string]mcat.Manifest, assigned []string, node mcat.No
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(problems) > 0 {
|
if len(problems) > 0 {
|
||||||
|
// A pass refused anyway says every reason at once, a held module's among them.
|
||||||
|
for _, why := range held {
|
||||||
|
problems = append(problems, why)
|
||||||
|
}
|
||||||
sort.Strings(problems)
|
sort.Strings(problems)
|
||||||
return mcat.Resolution{}, &Refusal{Problems: problems}
|
return mcat.Resolution{}, nil, &Refusal{Problems: problems}
|
||||||
}
|
}
|
||||||
return resolution, nil
|
return resolution, held, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// isModule reports whether a name is a module in its own right rather than only something
|
// isModule reports whether a name is a module in its own right rather than only something
|
||||||
@@ -672,12 +680,29 @@ func checkCapabilities(modules []mcat.Manifest, node mcat.Node) []string {
|
|||||||
return problems
|
return problems
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkClaims refuses two modules holding one singular thing.
|
// holdClaims refuses two modules holding one singular thing, within this node's own set and against what is held elsewhere for
|
||||||
//
|
// the wider scopes: the claims that hold, and for each module that cannot hold what it claims, why (every reason, joined).
|
||||||
// Within this node's own set, and against what is already held elsewhere for the wider scopes. A
|
func holdClaims(modules []mcat.Manifest, node mcat.Node, elsewhere []mcat.Held, holdings []mcat.Held) ([]mcat.Held, map[string]string) {
|
||||||
// claim at mesh scope is the same idea as the mesh's one hub, said once instead of hard-coded.
|
held, problems := claimsOf(modules, node, elsewhere, holdings)
|
||||||
func checkClaims(modules []mcat.Manifest, node mcat.Node, elsewhere []mcat.Held, holdings []mcat.Held) ([]mcat.Held, []string) {
|
var out map[string]string
|
||||||
var problems []string
|
for _, p := range problems {
|
||||||
|
if out == nil {
|
||||||
|
out = map[string]string{}
|
||||||
|
}
|
||||||
|
if prior, ok := out[p.module]; ok {
|
||||||
|
out[p.module] = prior + "; " + p.why
|
||||||
|
} else {
|
||||||
|
out[p.module] = p.why
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return held, out
|
||||||
|
}
|
||||||
|
|
||||||
|
// claimProblem is a refusal of one module's claim.
|
||||||
|
type claimProblem struct{ module, why string }
|
||||||
|
|
||||||
|
func claimsOf(modules []mcat.Manifest, node mcat.Node, elsewhere []mcat.Held, holdings []mcat.Held) ([]mcat.Held, []claimProblem) {
|
||||||
|
var problems []claimProblem
|
||||||
var held []mcat.Held
|
var held []mcat.Held
|
||||||
|
|
||||||
// onRecord is every recorded holder of a seat, if a handover ever named one: one for most seats,
|
// onRecord is every recorded holder of a seat, if a handover ever named one: one for most seats,
|
||||||
@@ -717,9 +742,9 @@ func checkClaims(modules []mcat.Manifest, node mcat.Node, elsewhere []mcat.Held,
|
|||||||
// disagrees with the mesh's definition of the role, and it is refused, named, rather
|
// disagrees with the mesh's definition of the role, and it is refused, named, rather
|
||||||
// than letting two machines answer for what the mesh has one of.
|
// than letting two machines answer for what the mesh has one of.
|
||||||
if s, known := mcat.SeatNamed(c.Name); known && !s.Replicated && len(recs) > 1 {
|
if s, known := mcat.SeatNamed(c.Name); known && !s.Replicated && len(recs) > 1 {
|
||||||
problems = append(problems, fmt.Sprintf(
|
problems = append(problems, claimProblem{m.Module, fmt.Sprintf(
|
||||||
"%q is on record as held by %d assignments, and it is held once per %s — "+
|
"%q is on record as held by %d assignments, and it is held once per %s — "+
|
||||||
"`seat %s --to <node>/<module>` records one", c.Name, len(recs), scope, c.Name))
|
"`seat %s --to <node>/<module>` records one", c.Name, len(recs), scope, c.Name)})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !recordedHere(c.Name, scope, m.Module) {
|
if !recordedHere(c.Name, scope, m.Module) {
|
||||||
@@ -734,9 +759,9 @@ func checkClaims(modules []mcat.Manifest, node mcat.Node, elsewhere []mcat.Held,
|
|||||||
// one seat, compared by the seat they resolve to and not by how each spelled it.
|
// one seat, compared by the seat they resolve to and not by how each spelled it.
|
||||||
seat := heldKey(c.Name, c.Kind)
|
seat := heldKey(c.Name, c.Kind)
|
||||||
if other, taken := byScope[scope][seat]; taken {
|
if other, taken := byScope[scope][seat]; taken {
|
||||||
problems = append(problems, fmt.Sprintf(
|
problems = append(problems, claimProblem{m.Module, fmt.Sprintf(
|
||||||
"%s and %s both claim %q, and only one thing may hold it per %s",
|
"%s and %s both claim %q, and only one thing may hold it per %s",
|
||||||
other, m.Module, seat, scope))
|
other, m.Module, seat, scope)})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
byScope[scope][seat] = m.Module
|
byScope[scope][seat] = m.Module
|
||||||
@@ -762,20 +787,34 @@ func checkClaims(modules []mcat.Manifest, node mcat.Node, elsewhere []mcat.Held,
|
|||||||
// Both claim and nobody is on record, or this refusal could not have happened.
|
// Both claim and nobody is on record, or this refusal could not have happened.
|
||||||
// The remedy is the handover that records the holder (novox/hq ADR 0131,
|
// The remedy is the handover that records the holder (novox/hq ADR 0131,
|
||||||
// 04-ISSUES/170), so it is named here rather than left to be found.
|
// 04-ISSUES/170), so it is named here rather than left to be found.
|
||||||
problems = append(problems, fmt.Sprintf(
|
problems = append(problems, claimProblem{h.Module, fmt.Sprintf(
|
||||||
"%s on %s claims %q, which %s on %s already holds — one per mesh. Nothing is "+
|
"%s on %s claims %q, which %s on %s already holds — one per mesh. Nothing is "+
|
||||||
"on record for it; `seat %s --to %s/%s` records the holder, and the other "+
|
"on record for it; `seat %s --to %s/%s` records the holder, and the other "+
|
||||||
"assignment then stands beside it, eligible and silent",
|
"assignment then stands beside it, eligible and silent",
|
||||||
h.Module, node.Name, h.Claim, e.Module, e.Node, h.Claim, e.Node, e.Module))
|
h.Module, node.Name, h.Claim, e.Module, e.Node, h.Claim, e.Node, e.Module)})
|
||||||
case mcat.ScopeSite:
|
case mcat.ScopeSite:
|
||||||
if node.Site != "" && node.Site == e.Site {
|
if node.Site != "" && node.Site == e.Site {
|
||||||
problems = append(problems, fmt.Sprintf(
|
problems = append(problems, claimProblem{h.Module, fmt.Sprintf(
|
||||||
"%s on %s claims %q, which %s on %s already holds at %s — one per site",
|
"%s on %s claims %q, which %s on %s already holds at %s — one per site",
|
||||||
h.Module, node.Name, h.Claim, e.Module, e.Node, node.Site))
|
h.Module, node.Name, h.Claim, e.Module, e.Node, node.Site)})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// A module refused here holds nothing of what it claims: its claims are not this node's holdings.
|
||||||
|
refused := map[string]bool{}
|
||||||
|
for _, p := range problems {
|
||||||
|
refused[p.module] = true
|
||||||
|
}
|
||||||
|
if len(refused) > 0 {
|
||||||
|
kept := held[:0]
|
||||||
|
for _, h := range held {
|
||||||
|
if !refused[h.Module] {
|
||||||
|
kept = append(kept, h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
held = kept
|
||||||
|
}
|
||||||
return held, problems
|
return held, problems
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package resolution
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
mcat "git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Resolve is ResolveHolding for a test that wants a claim problem as a refusal of the whole set, as before issue 1966.
|
||||||
|
func Resolve(catalogue map[string]mcat.Manifest, assigned []string, node mcat.Node, world mcat.World) (mcat.Resolution, error) {
|
||||||
|
got, held, err := ResolveHolding(catalogue, assigned, node, world)
|
||||||
|
if err != nil {
|
||||||
|
return mcat.Resolution{}, err
|
||||||
|
}
|
||||||
|
if len(held) > 0 {
|
||||||
|
var problems []string
|
||||||
|
for _, why := range held {
|
||||||
|
problems = append(problems, why)
|
||||||
|
}
|
||||||
|
sort.Strings(problems)
|
||||||
|
return mcat.Resolution{}, &Refusal{Problems: problems}
|
||||||
|
}
|
||||||
|
return got, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkClaims is holdClaims with the problems as a list.
|
||||||
|
func checkClaims(modules []mcat.Manifest, node mcat.Node, elsewhere []mcat.Held, holdings []mcat.Held) ([]mcat.Held, []string) {
|
||||||
|
held, why := holdClaims(modules, node, elsewhere, holdings)
|
||||||
|
var problems []string
|
||||||
|
for _, w := range why {
|
||||||
|
problems = append(problems, w)
|
||||||
|
}
|
||||||
|
sort.Strings(problems)
|
||||||
|
return held, problems
|
||||||
|
}
|
||||||
+23
-12
@@ -22,6 +22,10 @@ type Resolved struct {
|
|||||||
// NotGiven are the words of each credential only a person gives that nobody has: the consumer still resolves without it
|
// NotGiven are the words of each credential only a person gives that nobody has: the consumer still resolves without it
|
||||||
// (novox/hq ADR 0337 clause 4).
|
// (novox/hq ADR 0337 clause 4).
|
||||||
NotGiven []string
|
NotGiven []string
|
||||||
|
// Held are the modules this machine leaves out of its declaration and runs as they run, and why: one whose claim cannot be
|
||||||
|
// held (two claim a seat held once) or whose credential was refused (issue 1966, ADR 0636 §3). The rest of the machine
|
||||||
|
// composes; the declaration says each as left out (Declared.LeftOut), so a condition names it.
|
||||||
|
Held map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve works out everything a node should run, from what was assigned to it (the controller's planFor, plan.go): the
|
// Resolve works out everything a node should run, from what was assigned to it (the controller's planFor, plan.go): the
|
||||||
@@ -77,7 +81,7 @@ func (r Resolved) copied() Resolved {
|
|||||||
|
|
||||||
// planForNow is planFor worked out.
|
// planForNow is planFor worked out.
|
||||||
func (c *composer) planForNow(nodeName string) (Resolved, error) {
|
func (c *composer) planForNow(nodeName string) (Resolved, error) {
|
||||||
resolved, err := c.resolution(nodeName)
|
resolved, held, err := c.resolutionHolding(nodeName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Resolved{}, err
|
return Resolved{}, err
|
||||||
}
|
}
|
||||||
@@ -87,6 +91,7 @@ func (c *composer) planForNow(nodeName string) (Resolved, error) {
|
|||||||
}
|
}
|
||||||
shelf := c.shelfFor(node)
|
shelf := c.shelfFor(node)
|
||||||
var out Resolved
|
var out Resolved
|
||||||
|
out.Held = held
|
||||||
|
|
||||||
// The credential for each thing this node takes from elsewhere.
|
// The credential for each thing this node takes from elsewhere.
|
||||||
for i, n := range resolved.Needs {
|
for i, n := range resolved.Needs {
|
||||||
@@ -129,8 +134,13 @@ func (c *composer) planForNow(nodeName string) (Resolved, error) {
|
|||||||
case cred.State == CredentialRefused:
|
case cred.State == CredentialRefused:
|
||||||
// Said rather than skipped. A machine that resolves cleanly and receives no credential is one that will fail to
|
// Said rather than skipped. A machine that resolves cleanly and receives no credential is one that will fail to
|
||||||
// authenticate at some later, less obvious moment.
|
// authenticate at some later, less obvious moment.
|
||||||
return Resolved{}, fmt.Errorf("%s on %s needs %s from %s and no credential could be made for it: %s",
|
// Of that module only: it is held, the rest of the machine composes (issue 1966).
|
||||||
|
if out.Held == nil {
|
||||||
|
out.Held = map[string]string{}
|
||||||
|
}
|
||||||
|
out.Held[n.For] = fmt.Sprintf("%s on %s needs %s from %s and no credential could be made for it: %s",
|
||||||
n.For, nodeName, n.Name, n.From, cred.Why)
|
n.For, nodeName, n.Name, n.From, cred.Why)
|
||||||
|
continue
|
||||||
case cred.State == CredentialNotGiven, outside && !(held && cred.State == CredentialHeld):
|
case cred.State == CredentialNotGiven, outside && !(held && cred.State == CredentialHeld):
|
||||||
// Not given, and the rest of the consumer runs (ADR 0337 clause 4): no credential file, so the module can tell
|
// Not given, and the rest of the consumer runs (ADR 0337 clause 4): no credential file, so the module can tell
|
||||||
// it was not given; nothing the mesh made stands in for it.
|
// it was not given; nothing the mesh made stands in for it.
|
||||||
@@ -249,18 +259,19 @@ func movedProvider(m catalogue.Manifest) bool {
|
|||||||
return m.Moved || slices.ContainsFunc(m.Claims, func(c catalogue.Claim) bool { return c.Name == "mesh-vault" })
|
return m.Moved || slices.ContainsFunc(m.Claims, func(c catalogue.Claim) bool { return c.Name == "mesh-vault" })
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolution works out what one node should run, from what was assigned to it: the controller's planFor up to and
|
// resolutionHolding works out what one node should run, from what was assigned to it: the controller's planFor up to and
|
||||||
// including the resolution (plan.go planFor), and no further. A failure to compose the node's own modules is wrapped as
|
// including the resolution (plan.go planFor), and no further. A module whose claim cannot be held is returned beside the
|
||||||
// notResolvable; every other failure is returned as it is.
|
// resolution, with why, and not refused with the machine (issue 1966). A failure to compose the node's own modules is
|
||||||
func (c *composer) resolution(nodeName string) (catalogue.Resolution, error) {
|
// wrapped as notResolvable; every other failure is returned as it is.
|
||||||
|
func (c *composer) resolutionHolding(nodeName string) (catalogue.Resolution, map[string]string, error) {
|
||||||
node, err := c.in.node(nodeName)
|
node, err := c.in.node(nodeName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return catalogue.Resolution{}, err
|
return catalogue.Resolution{}, nil, err
|
||||||
}
|
}
|
||||||
shelf := c.shelfFor(node)
|
shelf := c.shelfFor(node)
|
||||||
world, _, left, err := c.theRestOfTheMesh(nodeName)
|
world, _, left, err := c.theRestOfTheMesh(nodeName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return catalogue.Resolution{}, err
|
return catalogue.Resolution{}, nil, err
|
||||||
}
|
}
|
||||||
world.Pinned = node.Pins
|
world.Pinned = node.Pins
|
||||||
// Where each of its consumers of a provision that keeps data was last sent (novox/hq ADR 0232).
|
// Where each of its consumers of a provision that keeps data was last sent (novox/hq ADR 0232).
|
||||||
@@ -268,14 +279,14 @@ func (c *composer) resolution(nodeName string) (catalogue.Resolution, error) {
|
|||||||
|
|
||||||
onNetwork, err := c.whereEveryoneIs()
|
onNetwork, err := c.whereEveryoneIs()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return catalogue.Resolution{}, err
|
return catalogue.Resolution{}, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// What this mesh can answer with a record rather than a machine, and which record each of this node's modules was
|
// What this mesh can answer with a record rather than a machine, and which record each of this node's modules was
|
||||||
// put on.
|
// put on.
|
||||||
world.Licences, world.Using = c.licencesFor(nodeName)
|
world.Licences, world.Using = c.licencesFor(nodeName)
|
||||||
|
|
||||||
resolved, err := resolution.Resolve(shelf, node.Assigned,
|
resolved, held, err := resolution.ResolveHolding(shelf, node.Assigned,
|
||||||
catalogue.Node{Name: nodeName, Site: node.Place.Site, Capabilities: node.Capabilities,
|
catalogue.Node{Name: nodeName, Site: node.Place.Site, Capabilities: node.Capabilities,
|
||||||
At: onNetwork[nodeName], PublicDomain: node.PublicDomain,
|
At: onNetwork[nodeName], PublicDomain: node.PublicDomain,
|
||||||
Account: node.Account, AccountHome: node.AccountHome,
|
Account: node.Account, AccountHome: node.AccountHome,
|
||||||
@@ -295,9 +306,9 @@ func (c *composer) resolution(nodeName string) (catalogue.Resolution, error) {
|
|||||||
err, strings.Join(c.unhostable, "; "))
|
err, strings.Join(c.unhostable, "; "))
|
||||||
}
|
}
|
||||||
// The node's own set does not compose: the only failure a mesh-wide gatherer may pass over.
|
// The node's own set does not compose: the only failure a mesh-wide gatherer may pass over.
|
||||||
return catalogue.Resolution{}, notResolvable{err}
|
return catalogue.Resolution{}, nil, notResolvable{err}
|
||||||
}
|
}
|
||||||
return resolved, nil
|
return resolved, held, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// shelfFor is the catalogue as one node composes its own set: the Input's, with each recorded or core module the node keeps at the
|
// shelfFor is the catalogue as one node composes its own set: the Input's, with each recorded or core module the node keeps at the
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package compose
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -223,21 +224,38 @@ func TestACredentialOnlyAPersonGivesIsNotMadeAndTheConsumerStillResolves(t *test
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1362/AC3: a credential the send would refuse, a given value sealed to a key a machine no longer has, refuses the resolution with
|
// 1362/AC3, issue 1966: a fault of one module's own, a credential the send would refuse (a given value sealed to a key a machine no
|
||||||
// today's words, and is not a node whose set does not compose.
|
// longer has) or a second claimant of a seat held once, costs that module and not the machine: it is held with the send's words, its
|
||||||
func TestACredentialTheSendWouldRefuseRefusesTheResolutionInTheSendsWords(t *testing.T) {
|
// declaration says it left out, and every other module of the machine composes. Not a node whose set does not compose.
|
||||||
|
func TestAModuleWhoseCredentialOrClaimFailsIsHeldAndTheMachineComposes(t *testing.T) {
|
||||||
why := "files's \"s3-bucket\" credential from anchor was accepted from a person, and a sealing key at one end has changed since. " +
|
why := "files's \"s3-bucket\" credential from anchor was accepted from a person, and a sealing key at one end has changed since. " +
|
||||||
"The mesh cannot re-seal a value it does not hold: accept it again with `secret accept laptop files s3-bucket --provider anchor`"
|
"The mesh cannot re-seal a value it does not hold: accept it again with `secret accept laptop files s3-bucket --provider anchor`"
|
||||||
in := theObjectsMesh(t, objects())
|
for _, c := range []struct{ name, module, said string }{
|
||||||
in.Credentials = []Credential{{Provision: "s3-bucket", Consumer: "laptop", ConsumerModule: "files", Provider: "anchor",
|
{"a refused credential", "files", "files on laptop needs s3-bucket from anchor and no credential could be made for it: " + why},
|
||||||
State: CredentialRefused, Why: why}}
|
{"a second claimant", "holder-two", "holder-one and holder-two both claim"},
|
||||||
_, err := Resolve(in, "laptop")
|
} {
|
||||||
want := "files on laptop needs s3-bucket from anchor and no credential could be made for it: " + why
|
t.Run(c.name, func(t *testing.T) {
|
||||||
if err == nil || err.Error() != want {
|
withSeat(t)
|
||||||
t.Fatalf("got %v, want %q", err, want)
|
in := theObjectsMesh(t, objects())
|
||||||
}
|
in.Credentials = []Credential{{Provision: "s3-bucket", Consumer: "laptop", ConsumerModule: "files", Provider: "anchor",
|
||||||
if IsUnresolvable(err) {
|
State: CredentialRefused, Why: why}}
|
||||||
t.Error("a refused credential is a node whose set does not compose")
|
if c.module == "holder-two" {
|
||||||
|
in = aMesh(t)
|
||||||
|
for _, name := range []string{"holder-one", "holder-two"} {
|
||||||
|
in.Catalogue[name] = aBrokerClaiming(name)
|
||||||
|
}
|
||||||
|
laptop := nodeOf(t, &in, "laptop")
|
||||||
|
laptop.Assigned = append(laptop.Assigned, "holder-one", "holder-two")
|
||||||
|
}
|
||||||
|
got, err := Resolve(in, "laptop")
|
||||||
|
if err != nil || IsUnresolvable(err) || len(got.Held) != 1 || !strings.Contains(got.Held[c.module], c.said) || len(got.Plan.Modules) == 0 {
|
||||||
|
t.Fatalf("held %v, %v; want %s held with %q", got.Held, err, c.module, c.said)
|
||||||
|
}
|
||||||
|
d, _, err := Compose(in, "laptop")
|
||||||
|
if err != nil || !slices.Contains(d.LeftOut, c.module) || !strings.Contains(d.LeftOutWhy[c.module], c.said) || len(d.Resources) == 0 {
|
||||||
|
t.Errorf("declared: left out %v, %d resources, %v", d.LeftOut, len(d.Resources), err)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,13 +295,19 @@ func TestARecordAnsweredNeedGetsItsKeyAndTheManagerItsPublicKey(t *testing.T) {
|
|||||||
// 1362/AC5: a node whose own set does not compose returns an error callers can tell from an unreadable Input.
|
// 1362/AC5: a node whose own set does not compose returns an error callers can tell from an unreadable Input.
|
||||||
func TestANodeWhoseOwnSetDoesNotComposeIsToldApartFromAnUnreadableInput(t *testing.T) {
|
func TestANodeWhoseOwnSetDoesNotComposeIsToldApartFromAnUnreadableInput(t *testing.T) {
|
||||||
in := aMesh(t)
|
in := aMesh(t)
|
||||||
in.Catalogue["rival-one"] = catalogue.Manifest{Module: "rival-one", Version: "1", Claims: []catalogue.Claim{{Name: "the-seat", Scope: catalogue.ScopeNode}}}
|
in.Catalogue["rival-one"] = catalogue.Manifest{Module: "rival-one", Version: "1", Requires: []string{"nothing-gives-this"}}
|
||||||
in.Catalogue["rival-two"] = catalogue.Manifest{Module: "rival-two", Version: "1", Claims: []catalogue.Claim{{Name: "the-seat", Scope: catalogue.ScopeNode}}}
|
|
||||||
laptop := nodeOf(t, &in, "laptop")
|
laptop := nodeOf(t, &in, "laptop")
|
||||||
laptop.Assigned = append(laptop.Assigned, "rival-one", "rival-two")
|
laptop.Assigned = append(laptop.Assigned, "rival-one")
|
||||||
if _, err := Resolve(in, "laptop"); err == nil || !IsUnresolvable(err) {
|
if _, err := Resolve(in, "laptop"); err == nil || !IsUnresolvable(err) {
|
||||||
t.Fatalf("a set that does not compose: %v", err)
|
t.Fatalf("a set that does not compose: %v", err)
|
||||||
}
|
}
|
||||||
|
// 1298/AC3: and its declaration is an error and nothing else.
|
||||||
|
if d, problems, err := Compose(in, "laptop"); err == nil || !IsUnresolvable(err) || d.Resources != nil || problems != nil {
|
||||||
|
t.Fatalf("a set that does not compose: %v %v %v", d.Resources, problems, err)
|
||||||
|
}
|
||||||
|
if _, _, err := Compose(in, "nowhere"); err == nil || IsUnresolvable(err) {
|
||||||
|
t.Fatalf("a node the Input does not have: %v", err)
|
||||||
|
}
|
||||||
if _, err := Resolve(in, "nowhere"); err == nil || IsUnresolvable(err) || !errors.Is(err, ErrNoSuchNode) {
|
if _, err := Resolve(in, "nowhere"); err == nil || IsUnresolvable(err) || !errors.Is(err, ErrNoSuchNode) {
|
||||||
t.Fatalf("a node the mesh does not have: %v", err)
|
t.Fatalf("a node the mesh does not have: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ func (c *composer) theRestOfTheMeshNow(exclude string) (catalogue.World, Passes,
|
|||||||
var firstHeld []catalogue.Held
|
var firstHeld []catalogue.Held
|
||||||
c.unhostable = nil
|
c.unhostable = nil
|
||||||
for _, o := range others {
|
for _, o := range others {
|
||||||
got, err := resolution.Resolve(shelf, o.assigned, o.node, catalogue.World{Unchecked: true, Holdings: holdings})
|
got, _, err := resolution.ResolveHolding(shelf, o.assigned, o.node, catalogue.World{Unchecked: true, Holdings: holdings})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Said, not skipped: a machine dropped here offers nothing and holds nothing.
|
// Said, not skipped: a machine dropped here offers nothing and holds nothing.
|
||||||
left = append(left, LeftOut{Node: o.node.Name, Err: err})
|
left = append(left, LeftOut{Node: o.node.Name, Err: err})
|
||||||
@@ -162,7 +162,7 @@ func (c *composer) theRestOfTheMeshNow(exclude string) (catalogue.World, Passes,
|
|||||||
// Each machine is resolved with its own pins, as its plan is.
|
// Each machine is resolved with its own pins, as its plan is.
|
||||||
theirs := world
|
theirs := world
|
||||||
theirs.Pinned = o.pins
|
theirs.Pinned = o.pins
|
||||||
got, err := resolution.Resolve(shelf, o.assigned, o.node, theirs)
|
got, _, err := resolution.ResolveHolding(shelf, o.assigned, o.node, theirs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Said only for the whole-mesh view: with one machine excluded, the view is partial by design.
|
// Said only for the whole-mesh view: with one machine excluded, the view is partial by design.
|
||||||
if exclude == "" {
|
if exclude == "" {
|
||||||
|
|||||||
@@ -89,8 +89,8 @@ func TestTheRestOfTheMeshOffersEveryOtherNodesProvisionsAndNoneOfTheExcluded(t *
|
|||||||
// 1361/AC2: a node whose assigned set does not resolve is left out and named with today's reason, and the others still offer.
|
// 1361/AC2: a node whose assigned set does not resolve is left out and named with today's reason, and the others still offer.
|
||||||
func TestANodeWhoseSetDoesNotResolveIsLeftOutAndTheOthersStillOffer(t *testing.T) {
|
func TestANodeWhoseSetDoesNotResolveIsLeftOutAndTheOthersStillOffer(t *testing.T) {
|
||||||
in := threeNodes(t)
|
in := threeNodes(t)
|
||||||
rivalOne := catalogue.Manifest{Module: "rival-one", Version: "1", Claims: []catalogue.Claim{{Name: "the-seat", Scope: catalogue.ScopeNode}}}
|
rivalOne := catalogue.Manifest{Module: "rival-one", Version: "1", Resources: []map[string]any{{"id": "r", "type": "directory", "path": "/var/lib/the-same", "mode": "0700"}}}
|
||||||
rivalTwo := catalogue.Manifest{Module: "rival-two", Version: "1", Claims: []catalogue.Claim{{Name: "the-seat", Scope: catalogue.ScopeNode}}}
|
rivalTwo := catalogue.Manifest{Module: "rival-two", Version: "1", Resources: []map[string]any{{"id": "r", "type": "directory", "path": "/var/lib/the-same", "mode": "0700"}}}
|
||||||
in.Catalogue["rival-one"], in.Catalogue["rival-two"] = rivalOne, rivalTwo
|
in.Catalogue["rival-one"], in.Catalogue["rival-two"] = rivalOne, rivalTwo
|
||||||
ace := nodeOf(t, &in, "ace")
|
ace := nodeOf(t, &in, "ace")
|
||||||
ace.Assigned = append(ace.Assigned, "rival-one", "rival-two")
|
ace.Assigned = append(ace.Assigned, "rival-one", "rival-two")
|
||||||
@@ -208,9 +208,10 @@ func TestAHolderOnRecordSettlesTheSeatInTheRestOfTheMesh(t *testing.T) {
|
|||||||
t.Fatalf("%v %v %+v", err, left, passes)
|
t.Fatalf("%v %v %+v", err, left, passes)
|
||||||
}
|
}
|
||||||
in.Holdings = nil
|
in.Holdings = nil
|
||||||
_, _, left, err = TheRestOfTheMesh(in, "ace")
|
// Nothing on record: the second claimant is held, not the node left out, and holds nothing in the view (issue 1966).
|
||||||
if err != nil || len(left) == 0 {
|
w, _, left, err := TheRestOfTheMesh(in, "ace")
|
||||||
t.Fatalf("two claimants with nothing on record were not refused: %v %v", err, left)
|
if err != nil || len(left) != 0 || len(w.Held) != 1 || w.Held[0].Module != "old-broker" {
|
||||||
|
t.Fatalf("two claimants with nothing on record: %v %v %+v", err, left, w.Held)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user