236 lines
7.9 KiB
Go
236 lines
7.9 KiB
Go
//go:build integration
|
|
|
|
package main
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
"git.novox.be/novox/mesh-sdk/go/compose"
|
|
"github.com/novox/mesh-controller/internal/link"
|
|
)
|
|
|
|
func aWebModule() catalogue.Manifest {
|
|
return catalogue.Manifest{Module: "web", Version: "1",
|
|
Listens: []catalogue.Listening{{Port: 8080, From: catalogue.FromEverywhere}},
|
|
OwnSecrets: catalogue.OwnSecrets{"admin": {Path: "/etc/web/admin"}},
|
|
Resources: []map[string]any{
|
|
{"id": "server", "type": "container", "name": "web", "ports": []any{"8080"},
|
|
"image": "registry.example/web@sha256:" + strings.Repeat("a", 64)},
|
|
}}
|
|
}
|
|
|
|
// 1385/AC1: a port the composition names foreseen is allocated with the inventory's PortFor for that module and port, a second
|
|
// composition names it no more, and the number is the one the controller's own Allocating composition gives in the same store.
|
|
func TestPerformAllocatesAForeseenPortAndTheNumberIsTheControllers(t *testing.T) {
|
|
open := aMesh(t)
|
|
ctx := t.Context()
|
|
register(t, open, aWebModule())
|
|
if _, err := assign(ctx, open, "laptop", "web"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
in, err := readComposeInput(ctx, open)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
d, _, err := compose.ComposeIn(in.Input, "laptop", compose.Foreseeing)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(d.ForeseenPorts) != 1 || d.ForeseenPorts[0] != "web:8080" {
|
|
t.Fatalf("the composition names the ports %v foreseen, want web:8080", d.ForeseenPorts)
|
|
}
|
|
did, err := perform(ctx, open, in, "laptop", d)
|
|
if err != nil || !did {
|
|
t.Fatalf("perform did %v: %v", did, err)
|
|
}
|
|
held, err := open.inventory.PortsFor(ctx, "laptop")
|
|
if err != nil || len(held) != 1 || held[0].Module != "web" || held[0].Wanted != 8080 {
|
|
t.Fatalf("the port perform allocated reads %+v: %v", held, err)
|
|
}
|
|
in, err = readComposeInput(ctx, open)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if again, _, err := compose.ComposeIn(in.Input, "laptop", compose.Foreseeing); err != nil || len(again.ForeseenPorts) != 0 {
|
|
t.Fatalf("a second composition names %v foreseen: %v", again.ForeseenPorts, err)
|
|
}
|
|
// The number is the one the controller allocates in the same store.
|
|
plan, settings, err := resolveNode(ctx, open, "laptop")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
old, err := declarationFor(ctx, open, "laptop", plan, settings)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
after, _, err := compose.ComposeIn(in.Input, "laptop", compose.Foreseeing)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got, want := portsOf(after.Resources), portsOf(old.Resources); got != want {
|
|
t.Fatalf("the declaration publishes %s; the controller's allocation publishes %s", got, want)
|
|
}
|
|
}
|
|
|
|
func portsOf(resources []map[string]any) string {
|
|
var out []string
|
|
for _, r := range resources {
|
|
if r["type"] == "container" {
|
|
out = append(out, fmt.Sprint(r["name"], r["ports"]))
|
|
}
|
|
}
|
|
return strings.Join(out, ";")
|
|
}
|
|
|
|
// 1385/AC2: an own secret named foreseen is made with SecretForModule, and a pair credential the Input names would-make is
|
|
// made as planFor chose; both exist after the send's composition and a second composition names neither.
|
|
func TestPerformMakesTheOwnSecretsAndThePairCredentialsTheCompositionNamed(t *testing.T) {
|
|
open, _ := aMeshWithSecrets(t)
|
|
ctx := t.Context()
|
|
inv := open.inventory
|
|
register(t, open, aWebModule())
|
|
register(t, open, aConsumerOfTheBucket("reports"))
|
|
for _, m := range []string{"web", "reports"} {
|
|
if _, err := assign(ctx, open, "laptop", m); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
before, err := madeIn(ctx, open)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
declared, err := composeSend(ctx, open, "laptop")
|
|
if err != nil {
|
|
t.Fatalf("the send is refused: %v", err)
|
|
}
|
|
if len(declared.foreseen) != 0 {
|
|
t.Fatalf("the declaration still names %v foreseen", declared.foreseen)
|
|
}
|
|
after, err := madeIn(ctx, open)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
madeNow := func(have, was []string) []string {
|
|
seen := map[string]bool{}
|
|
for _, s := range was {
|
|
seen[s] = true
|
|
}
|
|
var out []string
|
|
for _, s := range have {
|
|
if !seen[s] {
|
|
out = append(out, s)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
if got := madeNow(after.OwnSecrets, before.OwnSecrets); len(got) != 1 || !strings.Contains(got[0], "web/admin") {
|
|
t.Errorf("the own secrets made are %v, want laptop web/admin", got)
|
|
}
|
|
if got := madeNow(after.Pairs, before.Pairs); len(got) != 1 || !strings.Contains(got[0], "reports") {
|
|
t.Errorf("the pair credentials made are %v, want the one of reports", got)
|
|
}
|
|
// And the credential is the one a second composition reads as held.
|
|
in, err := readComposeInput(ctx, open)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(in.wouldMake) != 0 {
|
|
t.Errorf("after the send the next send would still make %v", in.wouldMake)
|
|
}
|
|
if _, err := inv.SecretsFrom(ctx, "anchor"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// 1385/AC3 (the lead's answer 1): with the vault unreachable and no box of the credential kept, the send carries the
|
|
// controller's own seal for it (ADR 0396), says so as a condition, and keeps no box.
|
|
func TestAVaultThatIsSilentSealsTheSendByTheControllerWhereNoBoxIsKept(t *testing.T) {
|
|
open, vault := aCoreMesh(t, link.VaultBoxContract)
|
|
vault.silent = errors.New("no responders")
|
|
declared, err := composeSend(t.Context(), open, "laptop")
|
|
if err != nil {
|
|
t.Fatalf("the send is refused: %v", err)
|
|
}
|
|
f := clientsBroker(declared.Resources)
|
|
if f == nil || f["sealed"] == nil || f[catalogue.VaultSealedField] != nil {
|
|
t.Fatalf("the core credential is composed as %v: the controller's own seal, until the vault holds it", f)
|
|
}
|
|
if len(declared.foreseen) != 0 {
|
|
t.Fatalf("the declaration still names %v foreseen", declared.foreseen)
|
|
}
|
|
if c, found := vaultSilence(t, "laptop"); !found || !strings.Contains(c.Summary, "sealed by the controller until the vault holds it") {
|
|
t.Fatalf("no condition says why: %+v", c)
|
|
}
|
|
kept, err := open.inventory.KeptCoreBoxes(t.Context(), "laptop")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(kept["mesh-provisioner-client"]) != 0 {
|
|
t.Fatalf("a box was kept for a credential the vault never boxed: %v", kept)
|
|
}
|
|
}
|
|
|
|
// 1385/AC6: perform is idempotent: a second send's composition over the stores it left makes and records nothing.
|
|
func TestAFinishedSendMakesNothingTheSecondTime(t *testing.T) {
|
|
open, _ := aMeshWithSecrets(t)
|
|
ctx := t.Context()
|
|
register(t, open, aWebModule())
|
|
if _, err := assign(ctx, open, "laptop", "web"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := composeSend(ctx, open, "laptop"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
before := tableState(t, open.inventory)
|
|
if _, err := composeSend(ctx, open, "laptop"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for k, v := range tableState(t, open.inventory) {
|
|
if before[k] != v {
|
|
t.Errorf("a second send of the same machine moved %s", k)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Review round 1, finding 1: a machine that rejoined with a new sealing key keeps its made rows sealed to the old key. A
|
|
// send re-makes them for the new key, as SecretForModule did on main, and the file naming the secret is not refused.
|
|
func TestASendReMakesAnOwnSecretSealedToAKeyTheMachineNoLongerHas(t *testing.T) {
|
|
open := aMesh(t)
|
|
ctx := t.Context()
|
|
inv := open.inventory
|
|
register(t, open, aBot("bot", false))
|
|
if _, err := assign(ctx, open, "laptop", "bot"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := composeSend(ctx, open, "laptop"); err != nil {
|
|
t.Fatalf("the first send is refused: %v", err)
|
|
}
|
|
if err := inv.RecordSealingKey(ctx, nodeID(t, open, "laptop"), aPublicKey(t)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := composeSend(ctx, open, "laptop"); err != nil {
|
|
t.Fatalf("a machine that rejoined cannot be sent to: %v", err)
|
|
}
|
|
in, err := readComposeInput(ctx, open)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
n, _ := nodeOf(in.Input, "laptop")
|
|
found := false
|
|
for _, r := range in.OwnSecrets {
|
|
if r.Node == "laptop" && r.Module == "bot" && r.Name == "token" {
|
|
found = true
|
|
if r.SealedTo != n.SealingKey {
|
|
t.Fatalf("the own secret is still sealed to a key the machine no longer has")
|
|
}
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatal("the own secret has no row")
|
|
}
|
|
}
|