Carry an adopted node's mode and taken modules in every declaration, from one marshaller (hq ADR 0100)
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/catalogue"
|
||||
"github.com/novox/mesh-controller/internal/inventory"
|
||||
)
|
||||
|
||||
// novox/hq ADR 0100: every declaration an adopted node is sent says it is adopted and which modules
|
||||
// were taken on it; a converged node's declaration is byte for byte what it was.
|
||||
|
||||
// helloWeb is a module the predecessor also runs: a page and a server under names it uses.
|
||||
func helloWeb() catalogue.Manifest {
|
||||
return catalogue.Manifest{Module: "hello-web", Version: "1",
|
||||
Resources: []map[string]any{
|
||||
{"id": "page", "type": "file", "path": "/var/lib/hello-web/index.html", "content": "hello"},
|
||||
{"id": "server", "type": "container", "name": "hello-web",
|
||||
"image": "registry.example/hello@sha256:" + strings.Repeat("a", 64)},
|
||||
{"id": "served", "type": "directory", "path": "/var/lib/hello-web"},
|
||||
}}
|
||||
}
|
||||
|
||||
// composed is what node would be sent now, as push composes it.
|
||||
func composed(t *testing.T, open *stores, node string) sendable {
|
||||
t.Helper()
|
||||
plan, settings, err := planFor(t.Context(), open, node)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
declared, err := declarationFor(t.Context(), open, node, plan, settings)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return declared
|
||||
}
|
||||
|
||||
func TestAConvergedDeclarationIsByteForByteWhatItWas(t *testing.T) {
|
||||
open := aMesh(t)
|
||||
register(t, open, helloWeb())
|
||||
if _, err := assign(t.Context(), open, "anchor", "hello-web"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
declared := composed(t, open, "anchor")
|
||||
if declared.Adoption != nil {
|
||||
t.Fatal("a converged node was given an adoption envelope")
|
||||
}
|
||||
body, err := declared.Body()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// The envelope exactly as every send site marshalled it before adoption existed.
|
||||
before, err := json.Marshal(map[string]any{"declaration": 1, "resources": declared.Resources})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(body, before) {
|
||||
t.Fatalf("a converged declaration changed:\n%s\n%s", body, before)
|
||||
}
|
||||
if bytes.Contains(body, []byte(`"adoption"`)) {
|
||||
t.Fatal("a converged declaration names adoption; an older host would refuse it")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnAdoptedDeclarationCarriesItsModeAndWhatWasTaken(t *testing.T) {
|
||||
open := aMesh(t)
|
||||
ctx := t.Context()
|
||||
register(t, open, helloWeb())
|
||||
if err := open.inventory.SetAdopted(ctx, "anchor", true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := assign(ctx, open, "anchor", "hello-web"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
declared := composed(t, open, "anchor")
|
||||
if declared.Adoption == nil {
|
||||
t.Fatal("an adopted node's declaration does not say it is adopted")
|
||||
}
|
||||
untaken := declared.Adoption.Untaken["hello-web"]
|
||||
if !reflect.DeepEqual(untaken, []string{"hello-web.page", "hello-web.server"}) {
|
||||
t.Fatalf("hello-web's files and containers are not named untaken: %v", declared.Adoption)
|
||||
}
|
||||
if len(declared.Adoption.Taken) != 0 {
|
||||
t.Fatalf("nothing was taken, and the declaration says %v", declared.Adoption.Taken)
|
||||
}
|
||||
|
||||
body, err := declared.Body()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var envelope map[string]any
|
||||
if err := json.Unmarshal(body, &envelope); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
adoption, _ := envelope["adoption"].(map[string]any)
|
||||
if _, ok := adoption["taken"].([]any); !ok {
|
||||
t.Fatalf("taken is not a list on the wire, even empty: %s", body)
|
||||
}
|
||||
|
||||
// The digest the mesh compares is the digest of what is sent: status and push agree.
|
||||
would, err := wouldSend(ctx, open, mustNodes(t, open))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if would["anchor"] != digestOf(body) {
|
||||
t.Fatal("the digest the mesh compares is not of the declaration push sends")
|
||||
}
|
||||
|
||||
// plan --json prints that same envelope.
|
||||
printed := stdoutOf(t, func() error { return planCommand(ctx, []string{"anchor", "--json"}) })
|
||||
var compact bytes.Buffer
|
||||
if err := json.Compact(&compact, []byte(printed)); err != nil {
|
||||
t.Fatalf("plan --json is not JSON: %v\n%s", err, printed)
|
||||
}
|
||||
if digestOf(compact.Bytes()) != digestOf(body) {
|
||||
t.Fatalf("plan --json shows something other than what push sends:\n%s", printed)
|
||||
}
|
||||
|
||||
// Taking the module moves its resources out of untaken.
|
||||
if err := open.inventory.Take(ctx, "anchor", "hello-web"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
declared = composed(t, open, "anchor")
|
||||
if _, still := declared.Adoption.Untaken["hello-web"]; still {
|
||||
t.Fatalf("a taken module is still untaken: %v", declared.Adoption)
|
||||
}
|
||||
if !reflect.DeepEqual(declared.Adoption.Taken, []string{"hello-web"}) {
|
||||
t.Fatalf("taken is %v", declared.Adoption.Taken)
|
||||
}
|
||||
}
|
||||
|
||||
func mustNodes(t *testing.T, open *stores) []inventory.Node {
|
||||
t.Helper()
|
||||
nodes, err := open.inventory.Nodes(t.Context())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return nodes
|
||||
}
|
||||
|
||||
// stdoutOf is what run printed.
|
||||
func stdoutOf(t *testing.T, run func() error) string {
|
||||
t.Helper()
|
||||
r, w, err := os.Pipe()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
saved := os.Stdout
|
||||
os.Stdout = w
|
||||
done := make(chan string)
|
||||
go func() {
|
||||
all, _ := io.ReadAll(r)
|
||||
done <- string(all)
|
||||
}()
|
||||
runErr := run()
|
||||
os.Stdout = saved
|
||||
w.Close()
|
||||
out := <-done
|
||||
if runErr != nil {
|
||||
t.Fatalf("%v\n%s", runErr, out)
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user