Merge pull request 'The merge gate composes each machine with its engine's contract, so moved modules are handed there as on the mesh (issue 969)' (#365) from fix/969-gate-composes-handing into main
This commit was merged in pull request #365.
This commit is contained in:
@@ -287,6 +287,12 @@ func gatherFacts(ctx context.Context, open *stores, busVersion string) (snapshot
|
||||
return snapshot.Facts{}, err
|
||||
}
|
||||
m.Architecture, m.Kernel = reported.Architecture, reported.Kernel
|
||||
// Whether its node-engine takes handed files is its contract (issue 969).
|
||||
if stated, had, err := inv.HealthOf(ctx, n.Name); err != nil {
|
||||
return snapshot.Facts{}, err
|
||||
} else if had {
|
||||
m.Contract = stated.Contract
|
||||
}
|
||||
outward, err := inv.OutwardLinksOf(ctx, n.Name)
|
||||
if err != nil {
|
||||
return snapshot.Facts{}, err
|
||||
|
||||
@@ -1017,6 +1017,13 @@ func raiseFromFacts(ctx context.Context, open *stores, f snapshot.Facts, shelf m
|
||||
"kernel": m.Kernel, "capabilities": caps}); err != nil {
|
||||
return notes, err
|
||||
}
|
||||
// What its node-engine takes, as the mesh composes it today (issue 969): without the statement the gate
|
||||
// composes a moved module the old way on a machine the mesh hands it to.
|
||||
if stated, ok := standInHealth(m, f.Taken); ok {
|
||||
if _, err := inv.RecordHealth(ctx, stated); err != nil {
|
||||
return notes, err
|
||||
}
|
||||
}
|
||||
for _, record := range []func(context.Context, string, string) error{inv.RecordSealingKey, inv.RecordOverlayKey} {
|
||||
key, err := standInKey()
|
||||
if err != nil {
|
||||
@@ -1355,3 +1362,12 @@ func firstOr(items, otherwise []string) string {
|
||||
}
|
||||
return "it says nothing more"
|
||||
}
|
||||
|
||||
// standInHealth is the statement the gate gives a machine of the snapshot so that it composes as the mesh does:
|
||||
// only its contract, which says whether it takes handed files (issue 969). False for a snapshot that carries none.
|
||||
func standInHealth(m snapshot.Machine, taken time.Time) (inventory.NodeHealth, bool) {
|
||||
if m.Contract == 0 {
|
||||
return inventory.NodeHealth{}, false
|
||||
}
|
||||
return inventory.NodeHealth{Node: m.Name, Contract: m.Contract, SaidAt: taken, HeardAt: taken}, true
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/catalogue"
|
||||
snapshot "github.com/novox/mesh-controller/internal/facts"
|
||||
"github.com/novox/mesh-controller/internal/inventory"
|
||||
"github.com/novox/mesh-controller/internal/link"
|
||||
)
|
||||
|
||||
// Issue 969: the gate composes a machine as the mesh does, so a moved module is handed where the machine takes
|
||||
// handed files and composed the old way where it does not.
|
||||
func TestIssue969TheGateGivesAMachineItsContractSoItTakesHandedFiles(t *testing.T) {
|
||||
taken := time.Date(2026, 10, 16, 12, 0, 0, 0, time.UTC)
|
||||
|
||||
stated, ok := standInHealth(snapshot.Machine{Name: "f35", Contract: link.HandedContract}, taken)
|
||||
if !ok || !takesHanded(stated, ok) {
|
||||
t.Fatalf("a machine whose engine takes handed files must be given that in the gate: %+v, %v", stated, ok)
|
||||
}
|
||||
old, ok := standInHealth(snapshot.Machine{Name: "e12", Contract: link.HandedContract - 1}, taken)
|
||||
if !ok || takesHanded(old, ok) {
|
||||
t.Fatalf("a machine whose engine is older must not take handed files: %+v, %v", old, ok)
|
||||
}
|
||||
none, ok := standInHealth(snapshot.Machine{Name: "d01"}, taken)
|
||||
if ok || takesHanded(none, ok) {
|
||||
t.Fatalf("a snapshot that carries no contract states nothing: %+v, %v", none, ok)
|
||||
}
|
||||
}
|
||||
|
||||
// Issue 969, through the store: a machine whose engine stated the handed contract is in the snapshot with it, the
|
||||
// gate raises it as taking handed files, and one whose statement is below it is raised as not taking them. Run in
|
||||
// the merge check with the other tests that need a store; the composition itself, handed here and refused there, is
|
||||
// the catalogue's TestAHandedModuleNamingARunTimeBoundKeyComposesOnlyWhereItIsHanded.
|
||||
func TestIssue969TheGateRaisesEachMachineAsItsEngineStatedItself(t *testing.T) {
|
||||
open := aMesh(t)
|
||||
ctx := t.Context()
|
||||
said := time.Date(2026, 10, 16, 12, 0, 0, 0, time.UTC)
|
||||
for node, contract := range map[string]int{"anchor": link.HandedContract, "laptop": link.HandedContract - 1} {
|
||||
if _, err := open.inventory.RecordHealth(ctx, inventory.NodeHealth{Node: node, Contract: contract,
|
||||
SaidAt: said, HeardAt: said}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
f, err := gatherFacts(ctx, open, "2.11.17")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(f.Machines) != 2 {
|
||||
t.Fatalf("the snapshot holds %d machines", len(f.Machines))
|
||||
}
|
||||
want := map[string]bool{}
|
||||
for _, m := range f.Machines {
|
||||
if m.Contract == 0 {
|
||||
t.Fatalf("%s: the snapshot carries no contract", m.Name)
|
||||
}
|
||||
want[m.Name] = m.Contract >= link.HandedContract
|
||||
}
|
||||
handing := 0
|
||||
for _, hands := range want {
|
||||
if hands {
|
||||
handing++
|
||||
}
|
||||
}
|
||||
if handing != 1 {
|
||||
t.Fatalf("one machine takes handed files in the snapshot, got %v", want)
|
||||
}
|
||||
|
||||
raised, drop, err := throwawayStores(ctx, os.Getenv("MESH_TEST_POSTGRES"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer drop()
|
||||
if _, err := raiseFromFacts(ctx, raised, f, map[string]catalogue.Manifest{}, map[string]inventory.Source{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for name, hands := range want {
|
||||
got, err := engineTakesHanded(ctx, raised.inventory, name)
|
||||
if err != nil || got != hands {
|
||||
t.Errorf("%s: the gate raises it as taking handed files = %v (%v), the mesh says %v", name, got, err, hands)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1126,7 +1126,13 @@ func engineTakesHanded(ctx context.Context, inv *inventory.Inventory, node strin
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return had && stated.Contract >= link.HandedContract, nil
|
||||
return takesHanded(stated, had), nil
|
||||
}
|
||||
|
||||
// takesHanded is whether a machine's newest statement (had: it ever stated one) is of an engine that writes a
|
||||
// filled file on the provisioner client's word alone.
|
||||
func takesHanded(stated inventory.NodeHealth, had bool) bool {
|
||||
return had && stated.Contract >= link.HandedContract
|
||||
}
|
||||
|
||||
// provisionerAccount is the account the provisioner's client runs as on this machine (novox/hq ADR 0349 §4): the user
|
||||
|
||||
@@ -123,3 +123,37 @@ func TestAHandedModulesLocalCredentialsOfTheVaultAreNamedByLocalInItsPlan(t *tes
|
||||
t.Fatalf("the plan names %v, want admin and internal-token", names)
|
||||
}
|
||||
}
|
||||
|
||||
// **Issue 969: a key its provider serves only at run time.** A moved module's file may name `${bound:…}` of a
|
||||
// provision whose provider tells it at run time (ssh-client's known_host of the forge). Where the machine takes
|
||||
// handed files the client fills it and the module composes; where it does not the controller must fill it from what
|
||||
// the provider serves statically, cannot, and refuses it with the very words an unmarked module is refused with
|
||||
// (ADR 0349 §17: the old way, byte for byte).
|
||||
func TestAHandedModuleNamingARunTimeBoundKeyComposesOnlyWhereItIsHanded(t *testing.T) {
|
||||
text := "Host ${bound:postgres-database:at}\n KnownHost ${bound:postgres-database:known_host}\n"
|
||||
moved := strings.Replace(botMoved(), `"own-secrets":{"token":"${dir:state}/token"}`,
|
||||
`"requires":["postgres-database"],"own-secrets":{"token":"${dir:state}/token"}`, 1)
|
||||
unmoved := strings.Replace(moved, `"moved":true,`, ``, 1)
|
||||
if unmoved == moved {
|
||||
t.Fatal("the twin is still marked moved")
|
||||
}
|
||||
|
||||
c, got := composeWith(t, withDatabase(t, moved, text, true), handingTo("store:5000"))
|
||||
if strings.Contains(got, "known_host") || len(c.Filled) == 0 {
|
||||
t.Fatalf("handed, the module is filled by the client and the controller names nothing: %v %s", c.Filled, got)
|
||||
}
|
||||
|
||||
for name, with := range map[string]Rendering{
|
||||
"an engine that does not take a filled file alone": {Provisioner: "mesh-provisioner-client", ArtifactStore: "store:5000"},
|
||||
"no provisioner client on the machine": {Hands: true, ArtifactStore: "store:5000"},
|
||||
} {
|
||||
_, err := withDatabase(t, moved, text, true).Compose(sealedForBot(with))
|
||||
if err == nil || !strings.Contains(err.Error(), `asks its postgres-database binding for "known_host"`) {
|
||||
t.Fatalf("%s: the key is not refused: %v", name, err)
|
||||
}
|
||||
_, before := withDatabase(t, unmoved, text, false).Compose(sealedForBot(with))
|
||||
if before == nil || before.Error() != err.Error() {
|
||||
t.Fatalf("%s: refused otherwise than the unmarked module: %v / %v", name, err, before)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,6 +112,10 @@ type Machine struct {
|
||||
// NodeEngine and NodeTools are the builds it runs.
|
||||
NodeEngine string `json:"node-engine,omitempty"`
|
||||
NodeTools string `json:"node-tools,omitempty"`
|
||||
// Contract is the contract of the node-engine's newest health statement (0 when it never stated one): what
|
||||
// says whether the machine takes handed files (novox/hq issue 969). A check that composed it without would
|
||||
// judge a moved module the old way where the mesh hands it.
|
||||
Contract int `json:"contract,omitempty"`
|
||||
// Capabilities are what it reported it can do; the detail kept only where it is a version.
|
||||
Capabilities []Capability `json:"capabilities,omitempty"`
|
||||
// Site, Hub, Public: where it is on the private network — its site (a pseudonym), whether it is the
|
||||
|
||||
Vendored
+1
@@ -8,6 +8,7 @@
|
||||
./cmd/mesh-controller TestAStopWithoutAMergeLeavesItOutOfABatch
|
||||
./cmd/mesh-controller TestSavingPlansAndMovingMergesIsOneAct
|
||||
./cmd/mesh-controller TestAPlanFollowedFromTheStoreAnswersWhenItMoves
|
||||
./cmd/mesh-controller TestIssue969TheGateRaisesEachMachineAsItsEngineStatedItself
|
||||
./internal/inventory TestTheLastSendSaysWhatTheMachineAnswered
|
||||
./internal/inventory TestAReportIsTheLastSendsAnswerOnlyWhenItIsAboutThatSend
|
||||
./cmd/mesh-controller TestNodeShowAndStatusReadTheLastSendFromTheStore
|
||||
|
||||
Reference in New Issue
Block a user