The mesh knows where a module came from, and whether it is behind
Delivery is a comparison, not a pipeline: the control plane holds what source
exists and what has been built from it, and the difference is the work. Both
halves are written down now, so "is this current" is a question about two
columns rather than something you find out by building.
`status` answers "did my change go out?", which ADR 0010 names as the real risk
of replacing a pipeline with a comparison -- it is answerable today by opening
a pipeline, and something had to replace that.
zsh holds 4f2a9c1e, source has 9e3b7d2a
running on laptop
The machines are the point. A module being out of date is a fact about the
catalogue; which machines are running last week's version is the thing with
consequences.
Three things this had to get right.
A module with no source is never behind -- it was handed over directly, which
is how a one-off arrives, and saying "out of date" about it would be inventing
a comparison against nothing.
A source nobody has checked is not behind either. Reporting it as behind would
put every module on the list the moment provenance was recorded, which makes
the list say nothing. Fault injection found this: my first test passed with the
guard removed, because both halves were empty strings and compared equal. The
case that actually needed it -- a known commit and an unknown head -- was
untested.
And handing over a manifest by hand does not erase where the module normally
comes from. Fixing something in a hurry is legitimate; silently forgetting its
origin is not, because that record is the only thing that would say afterwards
that a machine is running something nobody can rebuild.
Also fixed the flag parsing, which stopped at the first positional argument and
silently ignored every flag after it -- so `module add thing.json --source x`
recorded no source at all and said it had succeeded. The host's own parser
documents this exact footgun and I wrote it again anyway.
This commit is contained in:
@@ -22,7 +22,7 @@ func TestAModuleRoundTripsWholeAndUnshredded(t *testing.T) {
|
||||
Claims: []catalogue.Claim{{Name: "the-seat", Scope: catalogue.ScopeNode}},
|
||||
Resources: []map[string]any{{"id": "conf", "type": "file", "path": "/etc/X11/x.conf"}},
|
||||
}
|
||||
if err := inv.RegisterModule(t.Context(), m); err != nil {
|
||||
if err := inv.RegisterModule(t.Context(), m, Source{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -46,10 +46,10 @@ func TestRegisteringAgainReplacesTheManifest(t *testing.T) {
|
||||
// A manifest changing is the ordinary case — a module gains a requirement, a claim, a
|
||||
// resource. What matters is that the change is what the next resolution sees.
|
||||
inv := fresh(t)
|
||||
if err := inv.RegisterModule(t.Context(), manifest("thing", nil, nil)); err != nil {
|
||||
if err := inv.RegisterModule(t.Context(), manifest("thing", nil, nil), Source{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := inv.RegisterModule(t.Context(), manifest("thing", []string{"a-thing"}, nil)); err != nil {
|
||||
if err := inv.RegisterModule(t.Context(), manifest("thing", []string{"a-thing"}, nil), Source{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ func TestAModuleAMachineIsRunningCannotBeForgotten(t *testing.T) {
|
||||
if _, err := inv.AddNode(t.Context(), "laptop"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := inv.RegisterModule(t.Context(), manifest("thing", nil, nil)); err != nil {
|
||||
if err := inv.RegisterModule(t.Context(), manifest("thing", nil, nil), Source{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := inv.Assign(t.Context(), "laptop", "thing"); err != nil {
|
||||
@@ -99,7 +99,7 @@ func TestRemovingANodeTakesItsAssignments(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := inv.RegisterModule(t.Context(), manifest("thing", nil, nil)); err != nil {
|
||||
if err := inv.RegisterModule(t.Context(), manifest("thing", nil, nil), Source{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := inv.Assign(t.Context(), "laptop", "thing"); err != nil {
|
||||
@@ -146,7 +146,7 @@ func TestAssigningTwiceIsNotAnError(t *testing.T) {
|
||||
if _, err := inv.AddNode(t.Context(), "laptop"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := inv.RegisterModule(t.Context(), manifest("thing", nil, nil)); err != nil {
|
||||
if err := inv.RegisterModule(t.Context(), manifest("thing", nil, nil), Source{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for i := 0; i < 3; i++ {
|
||||
@@ -210,3 +210,157 @@ func TestOnlyPresentCapabilitiesCount(t *testing.T) {
|
||||
t.Error("a capability the node reported as ABSENT was counted as present")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAModuleWithNoSourceIsNeverBehind(t *testing.T) {
|
||||
// It was handed over directly, which is how a one-off arrives and how every module got here
|
||||
// before provenance existed. Saying "out of date" about it would be inventing a comparison
|
||||
// against nothing.
|
||||
inv := fresh(t)
|
||||
if err := inv.RegisterModule(t.Context(), manifest("thing", nil, nil), Source{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
from, err := inv.SourceOf(t.Context(), "thing")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !from.Current() {
|
||||
t.Error("a module with no source was reported as behind")
|
||||
}
|
||||
behind, err := inv.Behind(t.Context())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(behind) != 0 {
|
||||
t.Errorf("a module with no source is in the behind list: %v", behind)
|
||||
}
|
||||
}
|
||||
|
||||
func TestASourceThatMovedMakesTheModuleBehind(t *testing.T) {
|
||||
inv := fresh(t)
|
||||
if err := inv.RegisterModule(t.Context(), manifest("thing", nil, nil),
|
||||
Source{Repository: "novox/thing", Ref: "main", BuiltFrom: "aaaa1111"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
from, err := inv.SourceOf(t.Context(), "thing")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !from.Current() {
|
||||
t.Fatal("a module built from the only commit its source has is behind")
|
||||
}
|
||||
|
||||
if err := inv.SourceMoved(t.Context(), "thing", "bbbb2222"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
from, err = inv.SourceOf(t.Context(), "thing")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if from.Current() {
|
||||
t.Error("the source moved and the module still reports as current")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBeingBehindNamesTheMachinesRunningTheOldOne(t *testing.T) {
|
||||
// The question somebody actually has. A module being out of date is a fact about the
|
||||
// catalogue; machines running last week's version is the thing with consequences.
|
||||
inv := fresh(t)
|
||||
for _, n := range []string{"laptop", "workstation"} {
|
||||
if _, err := inv.AddNode(t.Context(), n); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := inv.RegisterModule(t.Context(), manifest("thing", nil, nil),
|
||||
Source{Repository: "novox/thing", BuiltFrom: "aaaa1111"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, n := range []string{"laptop", "workstation"} {
|
||||
if err := inv.Assign(t.Context(), n, "thing"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := inv.SourceMoved(t.Context(), "thing", "bbbb2222"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
behind, err := inv.Behind(t.Context())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(behind["thing"]) != 2 {
|
||||
t.Errorf("running on %v; both machines have the old one", behind["thing"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestRebuildingCatchesUp(t *testing.T) {
|
||||
inv := fresh(t)
|
||||
if err := inv.RegisterModule(t.Context(), manifest("thing", nil, nil),
|
||||
Source{Repository: "novox/thing", BuiltFrom: "aaaa1111"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := inv.SourceMoved(t.Context(), "thing", "bbbb2222"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := inv.RegisterModule(t.Context(), manifest("thing", []string{"a-thing"}, nil),
|
||||
Source{Repository: "novox/thing", BuiltFrom: "bbbb2222"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
from, err := inv.SourceOf(t.Context(), "thing")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !from.Current() {
|
||||
t.Errorf("built from the commit the source has and still behind: %+v", from)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandingOverAManifestDoesNotEraseWhereItComesFrom(t *testing.T) {
|
||||
// Fixing something in a hurry is legitimate. Silently forgetting where the module normally
|
||||
// comes from is not: it is the only thing that would say, afterwards, that a machine is
|
||||
// running something nobody can rebuild.
|
||||
inv := fresh(t)
|
||||
if err := inv.RegisterModule(t.Context(), manifest("thing", nil, nil),
|
||||
Source{Repository: "novox/thing", Ref: "main", BuiltFrom: "aaaa1111"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := inv.RegisterModule(t.Context(), manifest("thing", []string{"patched"}, nil),
|
||||
Source{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
from, err := inv.SourceOf(t.Context(), "thing")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if from.Repository != "novox/thing" {
|
||||
t.Errorf("handing over a manifest erased the source: %+v", from)
|
||||
}
|
||||
}
|
||||
|
||||
func TestASourceNobodyHasCheckedIsNotBehind(t *testing.T) {
|
||||
// A module built from a commit, where nothing has yet told the mesh whether that source has
|
||||
// moved. It is not behind — nobody has looked. Reporting it as behind would put every module
|
||||
// on the list the moment provenance was recorded, which makes the list say nothing.
|
||||
inv := fresh(t)
|
||||
if err := inv.RegisterModule(t.Context(), manifest("thing", nil, nil),
|
||||
Source{Repository: "novox/thing", BuiltFrom: "aaaa1111"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
from, err := inv.SourceOf(t.Context(), "thing")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Registering sets the head to what was built, so the two agree until something says
|
||||
// otherwise. Either way it must not read as behind.
|
||||
if !from.Current() {
|
||||
t.Errorf("a source nobody has checked reports as behind: %+v", from)
|
||||
}
|
||||
|
||||
// And with the head genuinely unknown, which is what a module registered before provenance
|
||||
// existed looks like after somebody adds a source to it.
|
||||
if (Source{Repository: "novox/thing", BuiltFrom: "aaaa1111"}).Current() == false {
|
||||
t.Error("a module with no known head reports as behind")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user