SentBuilds answers from the keys of the node's assignments that name a commit, known when the node was sent anything and no key says its commit unknown. A send that names its builds writes the commit, build and rollout on each carried module's key (a module nothing assigns gets a standing key marked pulled-in, never a state) and clears what every other key of the node named; a send by hand writes sent-unknown, a time, on every key and clears their commits. A failed write of these fails the record of the send. node.sent_builds is no longer written. The plan pass ends a pulled-in key only when no send carries it.
249 lines
11 KiB
Go
249 lines
11 KiB
Go
//go:build integration
|
|
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-controller/internal/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")
|
|
}
|
|
if rows := inv.QueryForTest(t, `select count(*) from pending_assignment`); rows[0][0] != "0" {
|
|
t.Fatalf("the table of pending assignments took %s rows", rows[0][0])
|
|
}
|
|
}
|