Perform what mesh-sdk's composition names foreseen, and compose a send over the SDK: perform, composeSend, composeQuestion, resolveNode (issues 1385, 1386)
A composition over the Input makes nothing; a send performs what it named (the pair credentials no row holds, the seats to pass, the ports and own secrets, the core credentials asked of the vault every send, the certificate), reads the stores again and composes again, and refuses a declaration that still stands in for a value anywhere in its body. A question (composeQuestion) never performs. The operation's Input is read once and kept in its context until a send changes what it read, per what the context keeps (recorded builds, the grants step, a settings walk). A core credential the vault did not answer for and no box is kept for refuses the send, where the controller sealed it itself (ADR 0396): the composition has no such fallback (reported). Callers switch in the next commits.
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
//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 := planFor(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: with the vault unreachable and no box of the credential kept, the send fails naming the module, and composes
|
||||
// nothing to send.
|
||||
func TestAVaultThatIsSilentRefusesTheSendWhereNoBoxIsKept(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 || !strings.Contains(err.Error(), "mesh-provisioner-client") {
|
||||
t.Fatalf("the send was not refused, naming the module: %v", err)
|
||||
}
|
||||
if len(declared.Resources) != 0 {
|
||||
t.Fatal("a declaration came with the refusal")
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user