The pipeline was observable from a merge to an artifact and went dark where it touched a machine: a node's report is control traffic only the control plane reads, so nothing said which version a machine runs, or that it refused to (novox/hq ADR 0134). The control plane now states both under the seat it holds — a role's events belong to the role and keep their address when the holder is replaced — and only when the report is news, because a machine reconciles every minute and a fact per report would be a fact per minute per machine. Whether a report is news is the store's answer: it holds the previous one, so the listener returns it and the server states the fact. That also gives the catch-up replay a subject the controller may publish: it was published as a module's event from a module called "control-plane", which does not exist, so the controller's own account refused it and every catalogue that asked what it missed was answered with nothing.
535 lines
20 KiB
Go
535 lines
20 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-controller/internal/catalogue"
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
"github.com/novox/mesh-controller/internal/link"
|
|
"github.com/novox/mesh-controller/internal/overlay"
|
|
)
|
|
|
|
// novox/hq ADR 0100: taking a module is its cutover; converging a node is one act, previewed, and
|
|
// refused while a found container is held; returning to adopted keeps what was taken.
|
|
|
|
// anAdoptedAnchor is aMesh with the anchor adopted, running a served module the predecessor also
|
|
// runs and a module with only a file, and a filter module in the catalogue.
|
|
func anAdoptedAnchor(t *testing.T) (*stores, *[]string) {
|
|
t.Helper()
|
|
open := aMesh(t)
|
|
ctx := t.Context()
|
|
register(t, open, catalogue.Manifest{Module: "hello-web", Version: "1",
|
|
Listens: []catalogue.Listening{{Port: 8080, From: catalogue.FromEverywhere}},
|
|
Resources: []map[string]any{
|
|
{"id": "page", "type": "file", "path": "/var/lib/hello-web/index.html", "content": "hi"},
|
|
{"id": "server", "type": "container", "name": "hello-web", "ports": []any{"8080:80"},
|
|
"image": "registry.example/hello@sha256:" + strings.Repeat("a", 64)},
|
|
}})
|
|
register(t, open, catalogue.Manifest{Module: "notes", Version: "1",
|
|
Resources: []map[string]any{
|
|
{"id": "conf", "type": "file", "path": "/etc/notes.conf", "content": "x"},
|
|
}})
|
|
register(t, open, catalogue.Manifest{Module: "nftables", Version: "1",
|
|
Filtering: &catalogue.Filtering{Into: "/etc/nftables.conf"},
|
|
Resources: []map[string]any{{"id": "load", "type": "service", "unit": "mesh-filter.service",
|
|
"state": "running", "restart-on": []any{"filtering"}}}})
|
|
if err := open.inventory.SetAdopted(ctx, "anchor", true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, m := range []string{"hello-web", "notes"} {
|
|
if _, err := assign(ctx, open, "anchor", m); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
sent := &[]string{}
|
|
saved := sendNodes
|
|
sendNodes = func(_ context.Context, _ *stores, names []string) error {
|
|
*sent = append(*sent, names...)
|
|
return nil
|
|
}
|
|
t.Cleanup(func() { sendNodes = saved })
|
|
return open, sent
|
|
}
|
|
|
|
// reportsHolding has the anchor report, on what it was last sent, holding what is given.
|
|
func reportsHolding(t *testing.T, open *stores, held ...link.Held) {
|
|
t.Helper()
|
|
reportsReaching(t, open, []link.Reach{
|
|
{Protocol: "tcp", Address: "0.0.0.0", Port: 22, By: "sshd"},
|
|
{Protocol: "tcp", Address: "0.0.0.0", Port: 8080, By: "hello-web", Published: true,
|
|
ContainerPort: 80},
|
|
{Protocol: "tcp", Address: "0.0.0.0", Port: 5000, By: "predecessor-registry",
|
|
Published: true, ContainerPort: 5000},
|
|
{Protocol: "tcp", Address: "127.0.0.1", Port: 15672, By: "mesh-broker",
|
|
Published: true, ContainerPort: 15672},
|
|
}, held...)
|
|
}
|
|
|
|
// reportsReaching has the anchor report, on what it was last sent, what is reachable on it and
|
|
// holding what is given.
|
|
func reportsReaching(t *testing.T, open *stores, reachable []link.Reach, held ...link.Held) {
|
|
t.Helper()
|
|
ctx := t.Context()
|
|
body, err := composed(t, open, "anchor").Body()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
record, err := open.inventory.NodeByName(ctx, "anchor")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := open.inventory.RecordSent(ctx, record.ID, digestOf(body)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := (link.Enrolment{Inventory: open.inventory}).Heard(ctx, link.Report{
|
|
Node: "anchor", Applied: []string{"hello-web.x"}, Declared: digestOf(body),
|
|
Firewall: "ufw", Held: held, Reachable: reachable,
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
var (
|
|
heldContainer = link.Held{ID: "hello-web.server", Module: "hello-web", Kind: "container",
|
|
Target: "hello-web", Since: time.Now()}
|
|
heldFile = link.Held{ID: "notes.conf", Module: "notes", Kind: "file",
|
|
Target: "/etc/notes.conf", Since: time.Now(), Kept: "/var/lib/mesh-host/kept/abc-notes.conf"}
|
|
)
|
|
|
|
func TestTakingAModuleNotOnTheNodeIsRefused(t *testing.T) {
|
|
open, _ := anAdoptedAnchor(t)
|
|
if _, err := take(t.Context(), open, "anchor", "nftables"); !errors.Is(err, inventory.ErrNotAssigned) {
|
|
t.Fatalf("taking an unassigned module gave %v", err)
|
|
}
|
|
if _, err := take(t.Context(), open, "laptop", "network"); !errors.Is(err, inventory.ErrNotAdopted) {
|
|
t.Fatalf("taking on a converged node gave %v", err)
|
|
}
|
|
}
|
|
|
|
func TestConvergingIsRefusedOnAPreviewThatWouldBeStale(t *testing.T) {
|
|
open, sent := anAdoptedAnchor(t)
|
|
_, err := converge(t.Context(), open, "anchor", false, "", "")
|
|
if err == nil || !strings.Contains(err.Error(), "has not reported") {
|
|
t.Fatalf("a node that never reported was previewed: %v", err)
|
|
}
|
|
if len(*sent) != 0 {
|
|
t.Fatal("a refused converge sent something")
|
|
}
|
|
}
|
|
|
|
func TestTheFlipIsRefusedWhileAFoundContainerIsHeld(t *testing.T) {
|
|
open, sent := anAdoptedAnchor(t)
|
|
reportsHolding(t, open, heldContainer, heldFile)
|
|
_, err := converge(t.Context(), open, "anchor", true, "", "")
|
|
if err == nil || !strings.Contains(err.Error(), "take anchor hello-web once its data has moved") {
|
|
t.Fatalf("the flip was not refused while hello-web holds its found container: %v", err)
|
|
}
|
|
if n, _ := open.inventory.NodeByName(t.Context(), "anchor"); !n.Adopted || len(*sent) != 0 {
|
|
t.Fatal("a refused flip changed something")
|
|
}
|
|
}
|
|
|
|
func TestTakingNamesWhatItReplaces(t *testing.T) {
|
|
open, _ := anAdoptedAnchor(t)
|
|
reportsHolding(t, open, heldContainer, heldFile)
|
|
said, err := take(t.Context(), open, "anchor", "hello-web")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(said, "container hello-web (hello-web.server)") ||
|
|
!strings.Contains(said, "push anchor") {
|
|
t.Fatalf("taking did not say what it replaces and what to run:\n%s", said)
|
|
}
|
|
}
|
|
|
|
func TestConvergingPreviewsThenChangesAndAdoptingKeepsWhatWasTaken(t *testing.T) {
|
|
open, sent := anAdoptedAnchor(t)
|
|
ctx := t.Context()
|
|
if _, err := take(ctx, open, "anchor", "hello-web"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
reportsHolding(t, open, heldFile)
|
|
|
|
preview, err := converge(ctx, open, "anchor", false, "", "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, want := range []string{
|
|
"tcp/8080 hello-web (published, container port 80)",
|
|
"declared by hello-web (from anywhere)",
|
|
"WILL CLOSE — no module assigned here declares it",
|
|
// The anchor faces inward and is on the private network: the derived filter admits ssh
|
|
// from the mesh only.
|
|
"WILL CLOSE to everything outside the private network — ssh stays open from the mesh",
|
|
"notes\n replacing the found file /etc/notes.conf (notes.conf), original kept at",
|
|
"assigns nftables",
|
|
"the found firewall (ufw) is disabled, never flushed",
|
|
// What it routes is not a listener: said not to be previewed, and to be dropped.
|
|
"not previewed: traffic the machine routes that is not a published port",
|
|
"the derived filter drops it unless a module declares it",
|
|
} {
|
|
if !strings.Contains(preview, want) {
|
|
t.Errorf("the preview does not say %q:\n%s", want, preview)
|
|
}
|
|
}
|
|
if strings.Contains(preview, "15672") {
|
|
t.Errorf("a loopback listener is in the preview:\n%s", preview)
|
|
}
|
|
for _, line := range strings.Split(preview, "\n") {
|
|
if strings.Contains(line, "5000") && !strings.Contains(line, "WILL CLOSE") {
|
|
t.Errorf("an undeclared published port is not said to close: %s", line)
|
|
}
|
|
}
|
|
if n, _ := open.inventory.NodeByName(ctx, "anchor"); !n.Adopted || len(*sent) != 0 {
|
|
t.Fatal("the preview changed something")
|
|
}
|
|
|
|
if _, err := converge(ctx, open, "anchor", true, digestIn(t, preview), ""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
n, _ := open.inventory.NodeByName(ctx, "anchor")
|
|
if n.Adopted {
|
|
t.Fatal("converge --yes left the node adopted")
|
|
}
|
|
taken, _ := open.inventory.Taken(ctx, "anchor")
|
|
if !reflect.DeepEqual(taken, []string{"hello-web", overlay.Name, "nftables", "notes"}) {
|
|
t.Fatalf("the flip took %v", taken)
|
|
}
|
|
if !reflect.DeepEqual(*sent, []string{"anchor"}) {
|
|
t.Fatalf("the flip sent %v", *sent)
|
|
}
|
|
declared := composed(t, open, "anchor")
|
|
if declared.Adoption != nil {
|
|
t.Fatal("a converged node is still sent an adoption envelope")
|
|
}
|
|
if !hasID(declared.Resources, "nftables.filtering") {
|
|
t.Fatal("the converged node is not declared the mesh's filter")
|
|
}
|
|
|
|
if _, err := adopt(ctx, open, "anchor"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := adopt(ctx, open, "anchor"); err == nil {
|
|
t.Fatal("adopting an adopted node was not refused")
|
|
}
|
|
again, _ := open.inventory.Taken(ctx, "anchor")
|
|
if !reflect.DeepEqual(again, taken) {
|
|
t.Fatalf("returning to adopted lost what was taken: %v", again)
|
|
}
|
|
declared = composed(t, open, "anchor")
|
|
if declared.Adoption == nil || len(declared.Adoption.Untaken) != 0 {
|
|
t.Fatalf("returned to adopted, the envelope is %+v", declared.Adoption)
|
|
}
|
|
if hasID(declared.Resources, "nftables.filtering") || hasID(declared.Resources, "nftables.load") {
|
|
t.Fatal("returned to adopted, the mesh's filter is still declared")
|
|
}
|
|
}
|
|
|
|
func hasID(resources []map[string]any, id string) bool {
|
|
for _, r := range resources {
|
|
if r["id"] == id {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// The command API refuses a flip exactly as the command line does, in the same words.
|
|
func TestTheApiRefusesTheFlipInTheCommandLinesWords(t *testing.T) {
|
|
open, _ := anAdoptedAnchor(t)
|
|
reportsHolding(t, open, heldContainer)
|
|
_, direct := converge(t.Context(), open, "anchor", true, "", "")
|
|
if direct == nil {
|
|
t.Fatal("the flip was not refused")
|
|
}
|
|
got := asking(t, letIn{}, "POST", "/converge", `{"node":"anchor","yes":true}`)
|
|
if got.Code != http.StatusConflict {
|
|
t.Fatalf("got %d: %s", got.Code, got.Body.String())
|
|
}
|
|
var said map[string]any
|
|
if err := json.Unmarshal(got.Body.Bytes(), &said); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if said["refused"] != direct.Error() {
|
|
t.Fatalf("the API said %q and the command line %q", said["refused"], direct.Error())
|
|
}
|
|
if got := asking(t, letIn{}, "POST", "/take", `{"node":"anchor"}`); got.Code != http.StatusBadRequest {
|
|
t.Fatalf("a take naming no module got %d", got.Code)
|
|
}
|
|
}
|
|
|
|
// novox/hq ADR 0100: the preview says what the derived filter does, rendered as it is rendered. On
|
|
// a machine that faces inward, ssh is admitted from the private network only, and a port a module
|
|
// admits from the mesh only closes to everything outside it: both are said to close.
|
|
func TestThePreviewSaysWhatNarrowsToTheMeshCloses(t *testing.T) {
|
|
open, _ := anAdoptedAnchor(t)
|
|
ctx := t.Context()
|
|
register(t, open, catalogue.Manifest{Module: "store", Version: "1",
|
|
Listens: []catalogue.Listening{{Port: 5432, From: catalogue.FromMesh}}})
|
|
if _, err := assign(ctx, open, "anchor", "store"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
reportsReaching(t, open, []link.Reach{
|
|
{Protocol: "tcp", Address: "0.0.0.0", Port: 22, By: "sshd"},
|
|
{Protocol: "tcp", Address: "0.0.0.0", Port: 5432, By: "postgres"},
|
|
{Protocol: "tcp", Address: "10.77.0.1", Port: 5432, By: "postgres"},
|
|
{Protocol: "tcp", Address: "0.0.0.0", Port: 8080, By: "hello-web", Published: true,
|
|
ContainerPort: 80},
|
|
})
|
|
preview, err := converge(ctx, open, "anchor", false, "", "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
lines := map[string]string{}
|
|
for _, line := range strings.Split(preview, "\n") {
|
|
fields := strings.Fields(line)
|
|
if len(fields) > 1 && strings.HasPrefix(fields[0], "tcp/") {
|
|
lines[fields[0]+" "+fields[1]] += line + "\n"
|
|
}
|
|
}
|
|
if got := lines["tcp/22 sshd"]; !strings.Contains(got, "WILL CLOSE to everything outside "+
|
|
"the private network") {
|
|
t.Errorf("ssh on an inward machine is not said to close outside the mesh:\n%s", preview)
|
|
}
|
|
store := lines["tcp/5432 postgres"]
|
|
if strings.Count(store, "WILL CLOSE to everything outside the private network") != 1 ||
|
|
!strings.Contains(store, "declared by store (from mesh)") {
|
|
t.Errorf("the store's narrowing is not said to close, or its mesh address is:\n%s", preview)
|
|
}
|
|
if got := lines["tcp/8080 hello-web"]; !strings.Contains(got, "declared by hello-web (from anywhere)") {
|
|
t.Errorf("a port open to everywhere is not said to stay:\n%s", preview)
|
|
}
|
|
}
|
|
|
|
// digestIn is the digest a converge preview printed.
|
|
func digestIn(t *testing.T, preview string) string {
|
|
t.Helper()
|
|
for _, line := range strings.Split(preview, "\n") {
|
|
if fields := strings.Fields(line); len(fields) == 2 && fields[0] == "preview" {
|
|
return fields[1]
|
|
}
|
|
}
|
|
t.Fatalf("the preview printed no digest:\n%s", preview)
|
|
return ""
|
|
}
|
|
|
|
// The flip acts on the preview the operator saw: it names that preview's digest, and it is refused
|
|
// when the digest is missing, when anything the preview says has changed since, or when the node's
|
|
// account of itself is too old to be the machine as it is.
|
|
func TestTheFlipActsOnlyOnThePreviewTheOperatorSaw(t *testing.T) {
|
|
open, sent := anAdoptedAnchor(t)
|
|
ctx := t.Context()
|
|
if _, err := take(ctx, open, "anchor", "hello-web"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
reportsHolding(t, open, heldFile)
|
|
preview, err := converge(ctx, open, "anchor", false, "", "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
saw := digestIn(t, preview)
|
|
if !strings.Contains(preview, "converge anchor --yes "+saw) {
|
|
t.Fatalf("the preview does not say how to act on it:\n%s", preview)
|
|
}
|
|
unchanged := func() {
|
|
t.Helper()
|
|
if n, _ := open.inventory.NodeByName(ctx, "anchor"); !n.Adopted || len(*sent) != 0 {
|
|
t.Fatal("a refused flip changed something")
|
|
}
|
|
}
|
|
|
|
if _, err := converge(ctx, open, "anchor", true, "", ""); err == nil ||
|
|
!strings.Contains(err.Error(), "--yes "+saw) {
|
|
t.Fatalf("a flip naming no preview was not refused: %v", err)
|
|
}
|
|
unchanged()
|
|
|
|
// Something new is reachable: the preview the operator saw is not what would happen.
|
|
reportsReaching(t, open, []link.Reach{
|
|
{Protocol: "tcp", Address: "0.0.0.0", Port: 22, By: "sshd"},
|
|
{Protocol: "tcp", Address: "0.0.0.0", Port: 8080, By: "hello-web", Published: true,
|
|
ContainerPort: 80},
|
|
{Protocol: "tcp", Address: "0.0.0.0", Port: 6000, By: "something-new"},
|
|
}, heldFile)
|
|
_, err = converge(ctx, open, "anchor", true, saw, "")
|
|
if err == nil || !strings.Contains(err.Error(), "has changed since preview "+saw) {
|
|
t.Fatalf("a flip on a changed preview was not refused: %v", err)
|
|
}
|
|
unchanged()
|
|
|
|
// An account older than the flip trusts is refused, whatever digest is named.
|
|
again, err := converge(ctx, open, "anchor", false, "", "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
saved := reportFreshFor
|
|
reportFreshFor = time.Nanosecond
|
|
_, err = converge(ctx, open, "anchor", true, digestIn(t, again), "")
|
|
reportFreshFor = saved
|
|
if err == nil || !strings.Contains(err.Error(), "wait for its next report") {
|
|
t.Fatalf("a flip on an old account was not refused: %v", err)
|
|
}
|
|
unchanged()
|
|
|
|
if _, err := converge(ctx, open, "anchor", true, digestIn(t, again), ""); err != nil {
|
|
t.Fatalf("the flip on the preview just seen was refused: %v", err)
|
|
}
|
|
if n, _ := open.inventory.NodeByName(ctx, "anchor"); n.Adopted {
|
|
t.Fatal("the flip did not converge the node")
|
|
}
|
|
}
|
|
|
|
// The flip holds the node from its checks to its send, so a push composed meanwhile waits and is
|
|
// composed after it — never sent after it with the node still adopted. And the send inside the
|
|
// flip is not made to wait on the flip's own hold.
|
|
func TestTheFlipHoldsTheNodeWhileItSends(t *testing.T) {
|
|
open, _ := anAdoptedAnchor(t)
|
|
ctx := t.Context()
|
|
if _, err := take(ctx, open, "anchor", "hello-web"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
reportsHolding(t, open, heldFile)
|
|
preview, err := converge(ctx, open, "anchor", false, "", "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var heldElsewhere, heldHere error
|
|
sendNodes = func(inner context.Context, open *stores, names []string) error {
|
|
// Another caller cannot hold the node while the flip sends it.
|
|
waiting, cancel := context.WithTimeout(ctx, 300*time.Millisecond)
|
|
defer cancel()
|
|
if release, err := open.inventory.HoldNodes(waiting, names); err == nil {
|
|
release()
|
|
heldElsewhere = errors.New("another caller held the node while the flip sent it")
|
|
}
|
|
// The flip's own send holds it without waiting on itself.
|
|
_, release, err := holdNodes(inner, open, names)
|
|
if err != nil {
|
|
heldHere = err
|
|
return err
|
|
}
|
|
release()
|
|
return nil
|
|
}
|
|
if _, err := converge(ctx, open, "anchor", true, digestIn(t, preview), ""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if heldElsewhere != nil || heldHere != nil {
|
|
t.Fatalf("%v %v", heldElsewhere, heldHere)
|
|
}
|
|
// And given back once it is done.
|
|
release, err := open.inventory.HoldNodes(ctx, []string{"anchor"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
release()
|
|
}
|
|
|
|
// novox/hq ADR 0103: the preview names every kind of thing a module the flip takes holds as found,
|
|
// not only its files, and the digest changes when any of them does.
|
|
func TestThePreviewNamesEveryHeldKind(t *testing.T) {
|
|
open, _ := anAdoptedAnchor(t)
|
|
ctx := t.Context()
|
|
if _, err := take(ctx, open, "anchor", "hello-web"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
since := time.Now()
|
|
held := []link.Held{heldFile,
|
|
{ID: "notes.data", Module: "notes", Kind: "directory", Target: "/var/lib/notes", Since: since},
|
|
{ID: "notes.daemon", Module: "notes", Kind: "service", Target: "notes.service", Since: since},
|
|
{ID: "notes.seed", Module: "notes", Kind: "archive", Target: "/srv/notes", Since: since},
|
|
{ID: "notes.worker", Module: "notes", Kind: "process", Target: "notes-worker", Since: since},
|
|
{ID: "notes.account", Module: "notes", Kind: "user", Target: "notes", Since: since},
|
|
}
|
|
reportsHolding(t, open, held...)
|
|
preview, err := converge(ctx, open, "anchor", false, "", "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, want := range []string{
|
|
"replacing the found directory /var/lib/notes (notes.data)",
|
|
"replacing the found service notes.service (notes.daemon)",
|
|
"replacing the found archive /srv/notes (notes.seed)",
|
|
"replacing the found process notes-worker (notes.worker)",
|
|
"replacing the found user notes (notes.account)",
|
|
} {
|
|
if !strings.Contains(preview, want) {
|
|
t.Errorf("the preview does not say %q:\n%s", want, preview)
|
|
}
|
|
}
|
|
reportsHolding(t, open, held[:len(held)-1]...)
|
|
fewer, err := converge(ctx, open, "anchor", false, "", "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if digestIn(t, fewer) == digestIn(t, preview) {
|
|
t.Fatal("the digest does not change with what is held")
|
|
}
|
|
}
|
|
|
|
// novox/hq ADR 0100: the flip acts on what the node said is reachable, so an account naming nothing
|
|
// is refused. Every machine that is up answers on ssh; nothing reported means the host's collectors
|
|
// did not, and flipping would close ports the preview never named.
|
|
func TestTheFlipIsRefusedOnAnAccountNamingNothingReachable(t *testing.T) {
|
|
open, sent := anAdoptedAnchor(t)
|
|
ctx := t.Context()
|
|
if _, err := take(ctx, open, "anchor", "hello-web"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Only a loopback listener: nothing off the machine, which is the same silence.
|
|
reportsReaching(t, open, []link.Reach{
|
|
{Protocol: "tcp", Address: "127.0.0.1", Port: 15672, By: "mesh-broker"},
|
|
}, heldFile)
|
|
preview, err := converge(ctx, open, "anchor", false, "", "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(preview, "this account looks partial") {
|
|
t.Errorf("the preview does not mark a partial account:\n%s", preview)
|
|
}
|
|
_, err = converge(ctx, open, "anchor", true, digestIn(t, preview), "")
|
|
if err == nil || !strings.Contains(err.Error(), "says nothing is reachable on it") {
|
|
t.Fatalf("the flip was not refused on an account naming nothing: %v", err)
|
|
}
|
|
if n, _ := open.inventory.NodeByName(ctx, "anchor"); !n.Adopted || len(*sent) != 0 {
|
|
t.Fatal("a refused flip changed something")
|
|
}
|
|
}
|
|
|
|
// An assignment cannot land between a preview and the flip that takes every module: assigning
|
|
// holds the node, so it waits for whatever is converging it.
|
|
func TestAssigningWaitsForWhateverIsConvergingTheNode(t *testing.T) {
|
|
open, _ := anAdoptedAnchor(t)
|
|
ctx := t.Context()
|
|
if _, err := take(ctx, open, "anchor", "hello-web"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
reportsHolding(t, open, heldFile)
|
|
preview, err := converge(ctx, open, "anchor", false, "", "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
saved, savedPoll := inventory.HoldWaitFor, inventory.HoldPoll
|
|
inventory.HoldWaitFor, inventory.HoldPoll = time.Second, 50*time.Millisecond
|
|
defer func() { inventory.HoldWaitFor, inventory.HoldPoll = saved, savedPoll }()
|
|
|
|
var whileFlipping error
|
|
sendNodes = func(context.Context, *stores, []string) error {
|
|
_, whileFlipping = assign(ctx, open, "anchor", "notes")
|
|
return nil
|
|
}
|
|
if _, err := converge(ctx, open, "anchor", true, digestIn(t, preview), ""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !errors.Is(whileFlipping, inventory.ErrNodeBusy) {
|
|
t.Fatalf("an assignment landed while the node was being converged: %v", whileFlipping)
|
|
}
|
|
}
|