232 lines
11 KiB
Go
232 lines
11 KiB
Go
package compose
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
"git.novox.be/novox/mesh-sdk/go/overlay"
|
|
)
|
|
|
|
// objects and files are the provider and consumer of mesh-controller's facts_test.go aMeshWithSecrets (cmd/mesh-controller at
|
|
// 6c2a8d62): a module providing s3-bucket at mesh scope with an identity bound, and one requiring it.
|
|
func objects() catalogue.Manifest {
|
|
return catalogue.Manifest{Module: "objects", Version: "1",
|
|
Provides: []catalogue.Offer{{Name: "s3-bucket", Scope: catalogue.ScopeMesh,
|
|
Identity: &catalogue.OfferIdentity{Max: 20, In: "an S3 access key"}}},
|
|
Receives: map[string]string{"s3-bucket": "/var/lib/mesh/objects/mesh.json"}}
|
|
}
|
|
|
|
func files() catalogue.Manifest {
|
|
return catalogue.Manifest{Module: "files", Version: "1", Requires: []string{"s3-bucket"},
|
|
Resources: []map[string]any{{"id": "config", "type": "file", "path": "/etc/files/config.json",
|
|
"mode": "0600", "content": "{}", "merge": "json"}}}
|
|
}
|
|
|
|
// threeNodes is aMesh and a third node `ace` running a database, so the mesh has three nodes that each offer something:
|
|
// anchor objects, ace a database, laptop a consumer of the first.
|
|
func threeNodes(t *testing.T) Input {
|
|
t.Helper()
|
|
in := aMesh(t)
|
|
db := catalogue.Manifest{Module: "pg", Version: "1", Provides: catalogue.FromAnywhere("postgres-database")}
|
|
in.Catalogue["objects"], in.Catalogue["files"], in.Catalogue["pg"] = objects(), files(), db
|
|
anchor, laptop := nodeOf(t, &in, "anchor"), nodeOf(t, &in, "laptop")
|
|
anchor.Assigned = append(anchor.Assigned, "objects")
|
|
laptop.Assigned = append(laptop.Assigned, "files")
|
|
in.Nodes = append(in.Nodes, Node{Name: "ace", Place: Place{Site: "there", Address: "10.77.0.3"},
|
|
Assigned: []string{"pg"}, Capabilities: map[string]bool{}})
|
|
return in
|
|
}
|
|
|
|
func providers(w catalogue.World, provision string) []string {
|
|
var out []string
|
|
for _, p := range w.Offered[provision] {
|
|
out = append(out, p.Node+"/"+p.Module)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// 1361/AC1: three nodes each offering a provision; the rest of the mesh, for one of them, offers every other node's and none of its
|
|
// own, and holds what they hold.
|
|
func TestTheRestOfTheMeshOffersEveryOtherNodesProvisionsAndNoneOfTheExcluded(t *testing.T) {
|
|
in := threeNodes(t)
|
|
w, passes, left, err := TheRestOfTheMesh(in, "laptop")
|
|
if err != nil || len(left) != 0 || len(passes.Passes) != 0 {
|
|
t.Fatalf("%v %v %+v", err, left, passes)
|
|
}
|
|
if got := providers(w, "s3-bucket"); len(got) != 1 || got[0] != "anchor/objects" {
|
|
t.Errorf("s3-bucket is offered by %v", got)
|
|
}
|
|
if got := providers(w, "postgres-database"); len(got) != 1 || got[0] != "ace/pg" {
|
|
t.Errorf("postgres-database is offered by %v", got)
|
|
}
|
|
if p := w.Offered["s3-bucket"][0]; p.At != "anchor.internal" {
|
|
t.Errorf("anchor is at %q on the private network", p.At)
|
|
}
|
|
// Excluding anchor takes its offer away and keeps the others'.
|
|
w, _, _, err = TheRestOfTheMesh(in, "anchor")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := providers(w, "s3-bucket"); len(got) != 0 {
|
|
t.Errorf("the excluded node's offer is in the world: %v", got)
|
|
}
|
|
if got := providers(w, "postgres-database"); len(got) != 1 {
|
|
t.Errorf("another node's offer is missing: %v", got)
|
|
}
|
|
// With nobody excluded, every offer is there, and everyone's claims are held.
|
|
w, _, _, err = TheRestOfTheMesh(in, "")
|
|
if err != nil || len(providers(w, "s3-bucket")) != 1 || len(providers(w, "postgres-database")) != 1 {
|
|
t.Fatalf("%v %v", providers(w, "s3-bucket"), err)
|
|
}
|
|
// Each machine is resolved with its own pins, as its plan is: the world is the same for any pin of the excluded node.
|
|
nodeOf(t, &in, "laptop").Pins = map[string]catalogue.Chosen{"s3-bucket": {Node: "anchor", Module: "objects"}}
|
|
if _, _, _, err := TheRestOfTheMesh(in, "laptop"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// 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) {
|
|
in := threeNodes(t)
|
|
rivalOne := catalogue.Manifest{Module: "rival-one", Version: "1", Claims: []catalogue.Claim{{Name: "the-seat", Scope: catalogue.ScopeNode}}}
|
|
rivalTwo := catalogue.Manifest{Module: "rival-two", Version: "1", Claims: []catalogue.Claim{{Name: "the-seat", Scope: catalogue.ScopeNode}}}
|
|
in.Catalogue["rival-one"], in.Catalogue["rival-two"] = rivalOne, rivalTwo
|
|
ace := nodeOf(t, &in, "ace")
|
|
ace.Assigned = append(ace.Assigned, "rival-one", "rival-two")
|
|
w, _, left, err := TheRestOfTheMesh(in, "laptop")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(left) == 0 {
|
|
t.Fatal("a node whose set does not resolve was not left out")
|
|
}
|
|
for _, l := range left {
|
|
if l.Node != "ace" || !strings.Contains(l.String(), "ace is left out of the rest of the mesh: it does not resolve: ") {
|
|
t.Errorf("left out %q", l.String())
|
|
}
|
|
}
|
|
if got := providers(w, "s3-bucket"); len(got) != 1 {
|
|
t.Errorf("the others do not offer: %v", got)
|
|
}
|
|
if got := providers(w, "postgres-database"); len(got) != 0 {
|
|
t.Errorf("the node left out offers: %v", got)
|
|
}
|
|
// Said for the whole-mesh view in both passes (as the controller says it), and for a view with one machine excluded in the first only.
|
|
_, _, whole, _ := TheRestOfTheMesh(in, "")
|
|
if len(whole) != 2 {
|
|
t.Errorf("the whole-mesh view says %d line(s), want the two passes'", len(whole))
|
|
}
|
|
if len(left) != 1 {
|
|
t.Errorf("a view with one excluded says %d line(s), want the first pass's", len(left))
|
|
}
|
|
}
|
|
|
|
// 1361/AC3 and AC4 (mesh-controller seat_pass_test.go TestACompositionPassesTheSeatAndWritesOneHandActByTheController, in
|
|
// the part that is not a write): a seat held once whose holder on record is assigned nowhere and one claimant: the seat
|
|
// passes to that claimant in the World, and is returned as a pass; Input's holdings are unchanged.
|
|
func TestASeatWhoseHolderIsAssignedNowhereGoesToItsOnlyClaimant(t *testing.T) {
|
|
withSeat(t)
|
|
mk := func() Input {
|
|
return Input{
|
|
Catalogue: map[string]catalogue.Manifest{"gate": aBrokerClaiming("gate")},
|
|
Nodes: []Node{{Name: "novox", Place: Place{Site: "here"}, Assigned: []string{"gate"}}},
|
|
Holdings: []catalogue.Held{{Claim: "mesh-broker", Scope: catalogue.ScopeMesh, Node: "novox", Module: "old-broker", Site: "here"}},
|
|
}
|
|
}
|
|
in := mk()
|
|
w, passes, _, err := TheRestOfTheMesh(in, "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(w.Holdings) != 1 || w.Holdings[0].Module != "gate" || w.Holdings[0].Node != "novox" {
|
|
t.Fatalf("the holdings after the composition do not name the gate: %+v", w.Holdings)
|
|
}
|
|
if len(passes.Passes) != 1 {
|
|
t.Fatalf("passes=%d, want one", len(passes.Passes))
|
|
}
|
|
p := passes.Passes[0]
|
|
if p.Seat.Name != "mesh-broker" || p.From.Module != "old-broker" || p.To.Module != "gate" || p.To.Node != "novox" {
|
|
t.Errorf("the pass is %+v", p)
|
|
}
|
|
unchanged(t, mk, func(in Input) {
|
|
if _, _, _, err := TheRestOfTheMesh(in, ""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
}
|
|
|
|
// 1317/AC2 (seat_pass_test.go TestACompositionWithTwoClaimantsRecordsNoneAndSaysSo): two claimants, no pass, the refusal said with
|
|
// both and the remedy. 1317/AC3 and AC4 (TestACompositionChangesNothingWhileTheHolderIsAssignedOrNobodyClaims): nothing changes while
|
|
// the holder is assigned, nor with no claimant.
|
|
func TestNoSeatIsPassedForTwoClaimantsAnAssignedHolderOrNoClaimant(t *testing.T) {
|
|
withSeat(t)
|
|
held := []catalogue.Held{{Claim: "mesh-broker", Scope: catalogue.ScopeMesh, Node: "novox", Module: "old-broker"}}
|
|
_, passes := passSeatsOn(held, []catalogue.Assignment{
|
|
{Node: "novox", Manifest: aBrokerClaiming("gate")}, {Node: "ace", Manifest: aBrokerClaiming("other")}})
|
|
said := strings.Join(passes.Said, "\n")
|
|
if len(passes.Passes) != 0 || !strings.Contains(said, "gate") || !strings.Contains(said, "other") ||
|
|
!strings.Contains(said, "seat mesh-broker --to") {
|
|
t.Fatalf("passes %+v said %q", passes.Passes, said)
|
|
}
|
|
got, passes := passSeatsOn(held, []catalogue.Assignment{
|
|
{Node: "novox", Manifest: aBrokerClaiming("old-broker")}, {Node: "novox", Manifest: aBrokerClaiming("gate")}})
|
|
if len(passes.Passes) != 0 || len(got) != 1 || got[0].Module != "old-broker" {
|
|
t.Fatalf("holder assigned: %+v %+v", got, passes)
|
|
}
|
|
got, passes = passSeatsOn(held, nil)
|
|
if len(passes.Passes) != 0 || len(got) != 1 || got[0].Module != "old-broker" {
|
|
t.Fatalf("no claimant: %+v %+v", got, passes)
|
|
}
|
|
}
|
|
|
|
// seat_pass_test.go TestAHolderAssignedButNotOnTheShelfIsNotTakenForGone.
|
|
func TestAHolderAssignedButNotOnTheShelfIsNotTakenForGone(t *testing.T) {
|
|
withSeat(t)
|
|
as := assignmentsOf(map[string]catalogue.Manifest{"gate": aBrokerClaiming("gate")},
|
|
map[string][]string{"novox": {"gate", "old-broker"}})
|
|
passes, _ := catalogue.SeatsToPassOn(
|
|
[]catalogue.Held{{Claim: "mesh-broker", Scope: catalogue.ScopeMesh, Node: "novox", Module: "old-broker"}}, as)
|
|
if len(passes) != 0 {
|
|
t.Fatalf("a holder missing from the shelf lost its seat: %+v", passes)
|
|
}
|
|
}
|
|
|
|
// The not-resolving node of a view with a holder on record: holdings reach the first pass (novox/hq ADR 0131), so a node running
|
|
// the next holder beside the current one is not refused as a second holder.
|
|
func TestAHolderOnRecordSettlesTheSeatInTheRestOfTheMesh(t *testing.T) {
|
|
withSeat(t)
|
|
in := Input{
|
|
Catalogue: map[string]catalogue.Manifest{"old-broker": aBrokerClaiming("old-broker"), "gate": aBrokerClaiming("gate")},
|
|
Nodes: []Node{
|
|
{Name: "novox", Assigned: []string{"old-broker", "gate"}},
|
|
{Name: "ace", Assigned: []string{}}},
|
|
Holdings: []catalogue.Held{{Claim: "mesh-broker", Scope: catalogue.ScopeMesh, Node: "novox", Module: "old-broker"}},
|
|
}
|
|
_, passes, left, err := TheRestOfTheMesh(in, "ace")
|
|
if err != nil || len(left) != 0 || len(passes.Passes) != 0 {
|
|
t.Fatalf("%v %v %+v", err, left, passes)
|
|
}
|
|
in.Holdings = nil
|
|
_, _, left, err = TheRestOfTheMesh(in, "ace")
|
|
if err != nil || len(left) == 0 {
|
|
t.Fatalf("two claimants with nothing on record were not refused: %v %v", err, left)
|
|
}
|
|
}
|
|
|
|
// Each machine is resolved in the second pass with its own pins, as its plan is (novox/hq issue 188): a pin to a provider that
|
|
// does not answer is its own refusal, so the machine is left out for it; the same machine without the pin is not.
|
|
func TestEachNodeIsResolvedWithItsOwnPinsInTheRestOfTheMesh(t *testing.T) {
|
|
in := threeNodes(t)
|
|
in.Nodes = append(in.Nodes, Node{Name: "newt", Place: Place{Site: "there", Address: "10.77.0.4"}, Assigned: []string{overlay.Name, "files"}, Capabilities: map[string]bool{}})
|
|
_, _, left, err := TheRestOfTheMesh(in, "")
|
|
if err != nil || len(left) != 0 {
|
|
t.Fatalf("without a pin the consumer is left out: %v %v", left, err)
|
|
}
|
|
nodeOf(t, &in, "newt").Pins = map[string]catalogue.Chosen{"s3-bucket": {Node: "ace", Module: "nothing"}}
|
|
_, _, left, err = TheRestOfTheMesh(in, "")
|
|
if err != nil || len(left) != 1 || left[0].Node != "newt" || !strings.Contains(left[0].String(), "was told to get \"s3-bucket\" from") {
|
|
t.Fatalf("a pin to a provider that does not answer did not leave the consumer out: %v %v", left, err)
|
|
}
|
|
}
|