Swaps three import paths for mesh-sdk's go/catalogue, go/overlay and go/validate (the same packages, copied from this repository at 6c2a8d62) and lets gofmt order the groups. The six swap commits compile together once the pin commit (go.mod, modules.txt) is on the branch; each alone does not, because a file on the SDK's types cannot call one still on the old package.
246 lines
10 KiB
Go
246 lines
10 KiB
Go
//go:build integration
|
|
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
"github.com/novox/mesh-controller/internal/chain"
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
"github.com/novox/mesh-controller/internal/lease"
|
|
"github.com/novox/mesh-controller/internal/link"
|
|
)
|
|
|
|
// **Acceptance 10 of design 47: one read of an assignment names the commit, the build and the rollout that put it there,
|
|
// and one read of the rollout lists the assignments it changed** (novox/hq ADR 0408, issue 1095). It enters by the live
|
|
// trigger — a merge heard, its batch cut into a plan when the window closed, its build answered through the build seat's
|
|
// path, the plan advanced — and writes no key and no row by hand. Only the machine's end of the send is the test's: the send
|
|
// is recorded as the controller records one it made, and the gate's facts are the store's.
|
|
func TestOneReadOfAnAssignmentNamesItsCommitBuildAndRolloutAndOneReadOfTheRolloutListsItsAssignments(t *testing.T) {
|
|
open := windowed(t)
|
|
everyHeadWaits(t)
|
|
asked := asksRecorded(t)
|
|
ctx := t.Context()
|
|
inv := open.inventory
|
|
if _, err := assign(ctx, open, "anchor", "notes"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var sent [][]string
|
|
wasSend := sendRollout
|
|
sendRollout = func(ctx context.Context, open *stores, names []string) ([]string, error) {
|
|
sent = append(sent, append([]string(nil), names...))
|
|
current, err := open.inventory.CurrentBuilds(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, node := range names {
|
|
digest := fmt.Sprintf("d-%s-%d", node, len(sent))
|
|
if err := open.inventory.RecordSent(ctx, nodeID(t, open, node), digest,
|
|
map[string]string{"notes": current["notes"].Commit}); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return names, nil
|
|
}
|
|
t.Cleanup(func() { sendRollout = wasSend })
|
|
wasGather := gatherGateFacts
|
|
gatherGateFacts = func(ctx context.Context, open *stores, component string) (gateFacts, error) {
|
|
f := gateFacts{now: time.Now(), reports: map[string]inventory.Reported{}, engines: map[string]string{},
|
|
rolledBack: map[string][]lease.Rollback{}, served: map[string]served{}}
|
|
reports, err := open.inventory.LastReports(ctx)
|
|
if err != nil {
|
|
return f, err
|
|
}
|
|
for _, r := range reports {
|
|
f.reports[r.Node] = r
|
|
}
|
|
return f, nil
|
|
}
|
|
t.Cleanup(func() { gatherGateFacts = wasGather })
|
|
|
|
// A merge of notes is heard, and its window closes: the batch is cut into the walk.
|
|
hear(t, open, catalogueMerge("c-notes", "notes", t0), t0.Add(time.Second))
|
|
cutAt(t, open, t0.Add(time.Second+90*time.Second))
|
|
ws, _ := walks(t, open)
|
|
if len(ws) != 1 {
|
|
t.Fatalf("%d walk(s) after the cut", len(ws))
|
|
}
|
|
rollout := ws[0].ID
|
|
|
|
// The walk asks its first tier (at the cut, or at its next step); the build seat answers, and the controller takes the
|
|
// outcome in as it does.
|
|
asking, err := inv.PlanByID(ctx, rollout)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for try := 0; try < 3 && (asking.Modules["notes"] == nil || asking.Modules["notes"].Build == ""); try++ {
|
|
advancePlans(ctx, open)
|
|
if asking, err = inv.PlanByID(ctx, rollout); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
buildID := asking.Modules["notes"].Build
|
|
if buildID == "" || len(*asked) != 1 || !strings.HasSuffix((*asked)[0], "#"+buildID) {
|
|
t.Fatalf("the walk asked %v, and notes' build is %q", *asked, buildID)
|
|
}
|
|
digest := "sha256:" + strings.Repeat("a", 64)
|
|
result := outcome(buildID, "modules/notes", "c-notes", "notes", "")
|
|
result.ReuseKey = "rk-notes"
|
|
result.Made = []link.MadeArtifact{{Name: "notes", Kind: "image", Reference: "artifact-store://notes/notes@" + digest}}
|
|
if err := (builds{inv: inv, open: open}).Built(ctx, result); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// The walk sends the tier to its first machine.
|
|
for try := 0; try < 3 && len(sent) == 0; try++ {
|
|
advancePlans(ctx, open)
|
|
}
|
|
if !slices.Equal(slices.Concat(sent...), []string{"anchor"}) {
|
|
t.Fatalf("sent %v", sent)
|
|
}
|
|
|
|
// One read of the assignment.
|
|
on, err := inv.AssignmentsOn(ctx, "anchor")
|
|
a, ok := assignmentOf(on, "notes")
|
|
if err != nil || !ok {
|
|
t.Fatalf("the assignments of anchor: %+v %v", on, err)
|
|
}
|
|
// Beside it a module pulled in by the assignment, which no send of this walk put anywhere.
|
|
if wireguard, pulled := assignmentOf(on, "mesh-wireguard"); pulled && (wireguard.Build != "" || wireguard.Rollout != "") {
|
|
t.Fatalf("a module this rollout did not send names a build or a rollout: %+v", wireguard)
|
|
}
|
|
if a.Module != "notes" || a.Commit != "c-notes" || a.Build != buildID || a.Rollout != rollout || a.Artifact != digest ||
|
|
a.Since.IsZero() || a.Assigned.IsZero() || !a.Stands() {
|
|
t.Fatalf("the assignment names %+v, not commit c-notes, build %s, artifact %s, rollout %s", a, buildID, digest, rollout)
|
|
}
|
|
// One read of the rollout.
|
|
p, err := inv.PlanByID(ctx, rollout)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wantChange := chain.Change{Node: "anchor", Module: "notes", After: chain.Ref{Build: buildID, Commit: "c-notes"}}
|
|
if !slices.Contains(p.Changed, wantChange) || !slices.Contains(p.Builds, buildID) || len(p.Steps) != 1 ||
|
|
p.Steps[0].Node != "anchor" || p.Steps[0].Sent == nil {
|
|
t.Fatalf("the rollout lists changed %+v, builds %v, steps %+v; want %+v", p.Changed, p.Builds, p.Steps, wantChange)
|
|
}
|
|
// And one read of the build names the same commit, with what it published and its reuse key.
|
|
built, found, err := inv.BuildByID(ctx, buildID)
|
|
if err != nil || !found || built.Commit != "c-notes" || built.Module != "notes" {
|
|
t.Fatalf("the build: %+v %v %v", built, found, err)
|
|
}
|
|
lately, err := inv.Builds(ctx, "notes", 1)
|
|
if err != nil || len(lately) != 1 || lately[0].ReuseKey != "rk-notes" || lately[0].Published == nil ||
|
|
lately[0].Published.Artifact != digest {
|
|
t.Fatalf("what the build published: %+v %v", lately, err)
|
|
}
|
|
// The verbs say the links.
|
|
if lines := changedLines(p); len(lines) != 2 || !strings.Contains(lines[1], "anchor/notes") ||
|
|
!strings.Contains(lines[1], buildID) {
|
|
t.Fatalf("`plans` says %v", lines)
|
|
}
|
|
if lines := runLines(on); len(lines) != 2 || !strings.Contains(lines[1], buildID) || !strings.Contains(lines[1], rollout) {
|
|
t.Fatalf("`node show` says %v", lines)
|
|
}
|
|
}
|
|
|
|
// A machine sent again what it runs says nothing new, and a push by hand with no rollout fills what is missing and blanks
|
|
// nothing (issue 1095).
|
|
func TestASendOfWhatAMachineRunsAlreadyChangesNoLinkOfItsAssignment(t *testing.T) {
|
|
open := aMesh(t)
|
|
ctx := t.Context()
|
|
inv := open.inventory
|
|
register(t, open, catalogue.Manifest{Module: "notes", Version: "1"})
|
|
if _, err := assign(ctx, open, "anchor", "notes"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.RecordBuild(ctx, inventory.Build{ID: "build-1", Module: "notes", Commit: "c1", Repository: "novox/notes"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
node := nodeID(t, open, "anchor")
|
|
inRollout := inventory.WithRollout(ctx, "plan-1", map[string]inventory.RolloutModule{"notes": {Commit: "c1", Build: "build-1"}})
|
|
if err := inv.RecordSent(inRollout, node, "d1", map[string]string{"notes": "c1"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Sent again by hand, with no rollout: the assignment still names the rollout that put the build there.
|
|
if err := inv.RecordSent(ctx, node, "d2", map[string]string{"notes": "c1"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
on, _ := inv.AssignmentsOn(ctx, "anchor")
|
|
if a, _ := assignmentOf(on, "notes"); a.Rollout != "plan-1" || a.Build != "build-1" || a.Commit != "c1" {
|
|
t.Fatalf("the assignment names %+v", on)
|
|
}
|
|
// A declaration sent by hand with no builds known makes what the machine runs unknown: a time on the key, no commit.
|
|
if err := inv.RecordSent(ctx, node, "d3", nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if on, _ = inv.AssignmentsOn(ctx, "anchor"); runOf(on, "notes").Commit != "" || runOf(on, "notes").SentUnknown.IsZero() {
|
|
t.Fatalf("a send of unknown builds left %+v", on)
|
|
}
|
|
// A module sent a commit no build is kept of is named by its commit alone.
|
|
if err := inv.RecordSent(ctx, node, "d4", map[string]string{"notes": "c2"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if on, _ = inv.AssignmentsOn(ctx, "anchor"); runOf(on, "notes").Commit != "c2" || runOf(on, "notes").Build != "" ||
|
|
runOf(on, "notes").Rollout != "" {
|
|
t.Fatalf("the assignment after a push of c2 by hand names %+v", on)
|
|
}
|
|
}
|
|
|
|
// assignmentOf is one module's assignment among those of a machine: assigning a module assigns what it requires beside it.
|
|
func assignmentOf(on []chain.Assignment, module string) (chain.Assignment, bool) {
|
|
for _, a := range on {
|
|
if a.Module == module {
|
|
return a, true
|
|
}
|
|
}
|
|
return chain.Assignment{}, false
|
|
}
|
|
|
|
func runOf(on []chain.Assignment, module string) chain.Assignment {
|
|
a, _ := assignmentOf(on, module)
|
|
return a
|
|
}
|
|
|
|
// **An assignment waiting for its build is one key of the assignments bucket** (ADR 0261, ADR 0408, issue 1094): entered
|
|
// through the `assign` verb for a module whose build is in flight, one read of `<node>.<module>` says state pending with the
|
|
// build it waits for, the build's outcome through the build seat's path makes the key stand with the wait carried as applied,
|
|
// and the table of pending assignments is read by nobody. No key is written by hand.
|
|
func TestAnAssignmentWaitingForItsBuildIsOneKeyOfTheAssignmentsBucket(t *testing.T) {
|
|
open := aCatalogueMesh(t)
|
|
ctx := t.Context()
|
|
inv := open.inventory
|
|
asksWithPaths(t)
|
|
mergeAdding(t, open, "modules/sensors", "3da80a4b00aa")
|
|
if _, err := assign(ctx, open, "laptop", "sensors"); err != nil {
|
|
t.Fatalf("an assignment while its build runs was refused: %v", err)
|
|
}
|
|
waiting, found, err := inv.AssignmentValue(ctx, "laptop", "sensors")
|
|
if err != nil || !found || waiting.State != chain.StatePending || waiting.Stands() || !waiting.Assigned.IsZero() ||
|
|
waiting.Pending == nil || waiting.Pending.State != chain.PendingWaiting || waiting.Pending.Build != "b-modules/sensors" ||
|
|
!strings.HasPrefix(waiting.Pending.ID, "p-") {
|
|
t.Fatalf("one read of the key says %+v (found %v, %v)", waiting, found, err)
|
|
}
|
|
if got := assignedTo(t, open, "laptop"); slices.Contains(got, "sensors") {
|
|
t.Fatalf("a module only waiting is assigned: %v", got)
|
|
}
|
|
|
|
if err := (builds{inv: inv, open: open}).Built(ctx, outcome("b-modules/sensors", "modules/sensors", "3da80a4b00aa", "sensors", "")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
made, found, err := inv.AssignmentValue(ctx, "laptop", "sensors")
|
|
if err != nil || !found || !made.Stands() || made.State != "" || made.Assigned.IsZero() || made.Pending == nil ||
|
|
made.Pending.State != chain.PendingApplied || made.Pending.ID != waiting.Pending.ID {
|
|
t.Fatalf("one read of the key after the build says %+v (found %v, %v)", made, found, err)
|
|
}
|
|
if !slices.Contains(assignedTo(t, open, "laptop"), "sensors") {
|
|
t.Fatal("the build registered sensors and it was not assigned")
|
|
}
|
|
}
|