A tool check alone is refused, so the store test's c2 never registered; the fixtures are now parsed by a test that needs no store.
199 lines
8.5 KiB
Go
199 lines
8.5 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-controller/internal/catalogue"
|
|
"github.com/novox/mesh-controller/internal/identity"
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
"github.com/novox/mesh-controller/internal/link"
|
|
)
|
|
|
|
// The grants step on a store (novox/hq issue 490): the machine holding the bus is sent its new user list,
|
|
// and nothing else — not the new build of a module it runs, not a module newly assigned there.
|
|
//
|
|
// The restic shape: anchor holds the bus and runs restic at c1, as does laptop; restic's c2 gives it a
|
|
// health tool. The grants step's send to anchor composes restic at c1, carries the user list that grants
|
|
// laptop's node-engine c2's tool, and records that anchor still runs c1. A module newly assigned to anchor
|
|
// makes the step fail, said, rather than install it unjudged.
|
|
func TestTheGrantsStepSendsTheUserListAndNothingElse(t *testing.T) {
|
|
open := aMesh(t)
|
|
ctx := t.Context()
|
|
inv := open.inventory
|
|
start := time.Now().Add(-time.Hour)
|
|
build := func(module, commit string, at time.Time, manifest map[string]any) link.BuildResult {
|
|
manifest["module"], manifest["version"] = module, "1"
|
|
raw, _ := json.Marshal(manifest)
|
|
return link.BuildResult{ID: link.NewBuildID(at), Repository: "novox/mesh-catalog", Path: "modules/" + module,
|
|
On: "anchor", Module: module, Commit: commit, Manifest: raw,
|
|
Source: &link.SourceOnSeat{Seat: "git", Repository: "novox/mesh-catalog"}}
|
|
}
|
|
for _, b := range []link.BuildResult{
|
|
build("nats", "n1", start, natsFixture()),
|
|
build("restic", "c1", start.Add(time.Second), resticFixture(false)),
|
|
build("wallpaper", "w1", start.Add(2*time.Second), wallpaperFixture()),
|
|
} {
|
|
if _, _, err := takeIn(ctx, inv, asTheOperator(t, inv, b)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
for _, a := range [][2]string{{"anchor", "nats"}, {"anchor", "restic"}, {"laptop", "restic"}} {
|
|
if _, err := inv.Assign(ctx, a[0], a[1]); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
for _, n := range []string{"anchor", "laptop"} {
|
|
if _, err := inv.MintBusPassword(ctx, inventory.BusUser{Username: "node." + n, Kind: inventory.BusNode, Node: n}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
wasEpoch := epochForActs
|
|
epochForActs = func(context.Context) (uint64, error) { return 7, nil }
|
|
t.Cleanup(func() { epochForActs = wasEpoch })
|
|
|
|
// A send to anchor composed as sendToEach composes it: numbered, planned, declared, stamped.
|
|
compose := func(under context.Context) (catalogue.Resolution, sendable) {
|
|
t.Helper()
|
|
numbered, err := allot(under, inv, "anchor")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
plan, settings, err := planFor(under, open, "anchor")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
gens, err := generators(under, open)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
declared, err := declarationWith(under, open, "anchor", plan, settings, gens, Allocating)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
numbered.stamp(&declared)
|
|
return plan, declared
|
|
}
|
|
record := func(declared sendable) {
|
|
t.Helper()
|
|
body, err := declared.Body()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := recordSent(ctx, inv, "anchor", body, declared.Builds, declared.Epoch,
|
|
sentRecordOf(ctx, declared)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
// Kept for anchor alone. laptop is never recorded as sent, on purpose: composing anchor resolves laptop
|
|
// too (who is on the private network), and a step that kept every machine's builds refused anchor's
|
|
// composition for want of laptop's last send (repo-check of #230 at 0f853e93).
|
|
kept := keepingEveryBuild(keepingRecorded(ctx), "anchor")
|
|
|
|
// What anchor was last sent: everything at the builds of before the merge, as an ordinary send sends it.
|
|
_, before := compose(keepingRecorded(ctx))
|
|
record(before)
|
|
plan, declared := compose(kept)
|
|
if only, err := grantsOnlyIn(ctx, open, "anchor", plan, declared); err != nil || only != "" {
|
|
t.Fatalf("with nothing changed, the step reads %q, %v", only, err)
|
|
}
|
|
|
|
// The merge: restic's c2 gives it a health tool.
|
|
if _, _, err := takeIn(ctx, inv, build("restic", "c2", start.Add(time.Minute),
|
|
resticFixture(true))); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// The step's send to anchor: restic as anchor runs it, c1, and the user list granting c2's tool.
|
|
generation, err := inv.AssignmentGeneration(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
plan, declared = compose(kept)
|
|
for _, m := range plan.Modules {
|
|
if m.Module == "restic" && len(m.Tools) > 0 {
|
|
t.Fatalf("the grants step composed restic's new build on anchor: tools %v", m.Tools)
|
|
}
|
|
}
|
|
if declared.Builds["restic"] != "c1" {
|
|
t.Fatalf("the step's send records it carries restic %q; want c1, which anchor runs", declared.Builds["restic"])
|
|
}
|
|
if !strings.Contains(declared.BusUsers, "mesh.mod.restic.tool.backup_health.laptop") {
|
|
t.Errorf("the step's declaration does not grant laptop's node-engine restic's new tool:\n%s", declared.BusUsers)
|
|
}
|
|
// The generation it carries is the current one (novox/hq issue 234), and the step does not raise it.
|
|
if declared.composedFrom != generation {
|
|
t.Errorf("the step's send was composed from generation %d; the mesh is at %d", declared.composedFrom, generation)
|
|
}
|
|
// The guard, on the very declaration the send sends: only the user list differs from the last send.
|
|
if only, err := grantsOnlyIn(ctx, open, "anchor", plan, declared); err != nil || only != "" {
|
|
t.Fatalf("the step reads as changing more than the user list: %q, %v", only, err)
|
|
}
|
|
// And the gate's refusal still reads the step's send: it moves nothing there.
|
|
if names, err := ungatedIn(kept, open, []string{"anchor"}, ""); err != nil || strings.Join(names, ",") != "anchor" {
|
|
t.Fatalf("the step's send is refused as a move: %v, %v", names, err)
|
|
}
|
|
// Recorded as the send records it: anchor still runs restic c1, its gate still to come there.
|
|
record(declared)
|
|
if sent, _, err := inv.SentBuilds(ctx, "anchor"); err != nil || sent["restic"] != "c1" {
|
|
t.Fatalf("after the step anchor reads as sent restic %q (%v); want c1", sent["restic"], err)
|
|
}
|
|
if after, err := inv.AssignmentGeneration(ctx); err != nil || after != generation {
|
|
t.Fatalf("the step moved the assignment generation from %d to %d (%v)", generation, after, err)
|
|
}
|
|
|
|
// A module newly assigned to anchor: the step would install it, unjudged, so it is refused unsent.
|
|
if _, err := inv.Assign(ctx, "anchor", "wallpaper"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
plan, declared = compose(kept)
|
|
only, err := grantsOnlyIn(ctx, open, "anchor", plan, declared)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(only, "wallpaper newly assigned") || !strings.Contains(only, "+wallpaper") {
|
|
t.Fatalf("a new assignment on the bus's machine reads %q; want the step refused, naming it", only)
|
|
}
|
|
// And the send itself refuses it, unsent: judged where it is composed, before the bus is dialled.
|
|
identity.ForTest(t)
|
|
if _, err := sendToEach(kept, open, []string{"anchor"}); !errors.Is(err, errNotGrantsOnly) ||
|
|
!strings.Contains(err.Error(), "wallpaper") {
|
|
t.Fatalf("the grants step's send of a new assignment was not refused by the send: %v", err)
|
|
}
|
|
}
|
|
|
|
// fixtureImage is the image every fixture container of the grants step's tests runs.
|
|
var fixtureImage = "registry.invalid:5000/restic/backup@sha256:" + strings.Repeat("b", 64)
|
|
|
|
// resticFixture is restic's manifest in the shape of mesh-catalog #210: at c2 (withTool) its backup judged by
|
|
// its new tool `backup_health`, beside a check of another kind it does not run itself — a tool check alone
|
|
// is refused (ADR 0227 rule 8, ADR 0240 rule 2) — and at c1 neither.
|
|
func resticFixture(withTool bool) map[string]any {
|
|
backup := map[string]any{"id": "backup", "type": "container", "name": "restic-backup", "image": fixtureImage}
|
|
measure := map[string]any{"id": "measure", "type": "container", "name": "restic-measure", "image": fixtureImage}
|
|
m := map[string]any{"resources": []any{backup, measure}}
|
|
if withTool {
|
|
backup["health"] = map[string]any{"kind": catalogue.HealthTool, "tool": "backup_health"}
|
|
measure["health"] = map[string]any{"kind": "runtime"}
|
|
m["tools"] = []string{"backup_health"}
|
|
}
|
|
return m
|
|
}
|
|
|
|
// natsFixture is the bus's module: it holds mesh-broker and is sent the user list.
|
|
func natsFixture() map[string]any {
|
|
return map[string]any{"bus-users": "/var/lib/nats-module/conf/accounts.conf",
|
|
"claims": []any{map[string]any{"name": catalogue.BrokerSeat, "scope": catalogue.ScopeMesh}}}
|
|
}
|
|
|
|
// wallpaperFixture is a module the bus's machine is newly assigned.
|
|
func wallpaperFixture() map[string]any {
|
|
return map[string]any{"resources": []any{
|
|
map[string]any{"id": "paper", "type": "container", "name": "wallpaper", "image": fixtureImage}}}
|
|
}
|