Unify trunk on main: initialization → main #3

Merged
jschoubben merged 58 commits from initialization into main 2026-09-05 01:13:33 +00:00
7 changed files with 181 additions and 53 deletions
Showing only changes of commit fa48b5825e - Show all commits
+6 -3
View File
@@ -331,7 +331,7 @@ func runApply(ctx context.Context, opts options, d *declaration.Declaration, sou
return err
}
report, updated, applyErr := apply.Apply(ctx, sys, d, known, apply.ExecRunner, func(line string) {
report, updated, applyErr := apply.Apply(ctx, sys, d, known, store.OriginCarried, apply.ExecRunner, func(line string) {
if !opts.json {
fmt.Println(line)
}
@@ -538,7 +538,7 @@ func runLink(ctx context.Context, opts options) error {
Fingerprint: mine.Membership.Fingerprint,
Password: mine.Membership.Password,
Signer: mine.Membership.Signer,
}, apply, opts.timeout)
}, apply, func(line string) { fmt.Println(line) }, opts.timeout)
}
// applyDeclared applies a declaration that has already been proved to come from the mesh.
@@ -569,7 +569,10 @@ func applyDeclared(ctx context.Context, opts options, raw []byte) link.Report {
return link.Report{Refused: err.Error()}
}
outcome, updated, applyErr := apply.Apply(ctx, built, declared, known, apply.ExecRunner, nil)
// Declared, not carried. A declaration from the mesh removes only what the mesh previously
// declared — never what this machine raised for itself from its bundle (04-ISSUES/010).
outcome, updated, applyErr := apply.Apply(ctx, built, declared, known, store.OriginDeclared,
apply.ExecRunner, nil)
// Saved whichever way it went. Recording only on success would lose the footprint of a
// failed apply, and that footprint is on the machine either way.
+2 -2
View File
@@ -74,7 +74,7 @@
"command": ["docker", "run", "--rm", "--network", "container:mesh-store",
"-e", "MESH_STORE_INVENTORY=postgres://postgres:bootstrap@127.0.0.1:5432/inventory?sslmode=disable",
"-e", "MESH_STORE_IDENTITY=postgres://postgres:bootstrap@127.0.0.1:5432/identity?sslmode=disable",
"192.0.2.250:5000/mesh-control@sha256:1dfcf6a879e16e671d4d6459271fd2630e30560afa1211508ab3994494786893",
"192.0.2.250:5000/mesh-control@sha256:b14e0a9765445b56f21aa8cf25be6a614b46974de710ddad92ee577074065858",
"migrate"],
"verify": ["sh", "-c", "docker exec mesh-store psql -U postgres -d inventory -tAc \"select to_regclass('public.node')\" | grep -qx node && docker exec mesh-store psql -U postgres -d identity -tAc \"select to_regclass('public.signing_key')\" | grep -qx signing_key"]
},
@@ -108,7 +108,7 @@
"id": "control-plane",
"type": "container",
"name": "mesh-control",
"image": "192.0.2.250:5000/mesh-control@sha256:1dfcf6a879e16e671d4d6459271fd2630e30560afa1211508ab3994494786893",
"image": "192.0.2.250:5000/mesh-control@sha256:b14e0a9765445b56f21aa8cf25be6a614b46974de710ddad92ee577074065858",
"network": "host",
"args": ["serve"],
"volumes": ["mesh-broker-tls:/broker-tls:ro"],
+4 -2
View File
@@ -85,6 +85,7 @@ func Apply(
sys system.System,
d *declaration.Declaration,
known store.State,
origin string,
run Runner,
log func(string),
) (Report, store.State, error) {
@@ -98,7 +99,7 @@ func Apply(
declared[r.Identity()] = true
}
for _, orphan := range known.Orphans(declared) {
for _, orphan := range known.Orphans(declared, origin) {
action, detail, err := remove(ctx, sys, orphan, run)
if err != nil {
return report, known, &Error{Resource: orphan.ID, Err: err, Done: report}
@@ -119,7 +120,8 @@ func Apply(
// Only now. The record follows the fact, never leads it.
known.Record(store.Applied{
ID: resource.Identity(), Type: string(resource.Kind()),
Origin: origin,
ID: resource.Identity(), Type: string(resource.Kind()),
Target: outcome.Target, AppliedAt: time.Now().UTC(),
})
report.Outcomes = append(report.Outcomes, outcome)
+36 -36
View File
@@ -39,7 +39,7 @@ func TestApplyingTwiceChangesNothingTheSecondTime(t *testing.T) {
{"id":"f","type":"file","path":"`+dir+`/etc/a.conf","content":"hello\n","mode":"0640"}
]}`)
first, state, err := Apply(context.Background(), archHost(t), d, store.State{}, noServices, nil)
first, state, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, noServices, nil)
if err != nil {
t.Fatal(err)
}
@@ -47,7 +47,7 @@ func TestApplyingTwiceChangesNothingTheSecondTime(t *testing.T) {
t.Fatal("the first apply on an empty machine changed nothing")
}
second, _, err := Apply(context.Background(), archHost(t), d, state, noServices, nil)
second, _, err := Apply(context.Background(), archHost(t), d, state, store.OriginCarried, noServices, nil)
if err != nil {
t.Fatal(err)
}
@@ -65,7 +65,7 @@ func TestADriftedMachineIsReturned(t *testing.T) {
{"id":"f","type":"file","path":"`+path+`","content":"correct\n","mode":"0644"}
]}`)
_, state, err := Apply(context.Background(), archHost(t), d, store.State{}, noServices, nil)
_, state, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, noServices, nil)
if err != nil {
t.Fatal(err)
}
@@ -73,7 +73,7 @@ func TestADriftedMachineIsReturned(t *testing.T) {
t.Fatal(err)
}
report, _, err := Apply(context.Background(), archHost(t), d, state, noServices, nil)
report, _, err := Apply(context.Background(), archHost(t), d, state, store.OriginCarried, noServices, nil)
if err != nil {
t.Fatal(err)
}
@@ -97,7 +97,7 @@ func TestADroppedResourceIsRemoved(t *testing.T) {
{"id":"keep","type":"file","path":"`+keep+`","content":"a\n"},
{"id":"drop","type":"file","path":"`+drop+`","content":"b\n"}
]}`)
_, state, err := Apply(context.Background(), archHost(t), both, store.State{}, noServices, nil)
_, state, err := Apply(context.Background(), archHost(t), both, store.State{}, store.OriginCarried, noServices, nil)
if err != nil {
t.Fatal(err)
}
@@ -105,7 +105,7 @@ func TestADroppedResourceIsRemoved(t *testing.T) {
one := parse(t, `{"declaration":1,"resources":[
{"id":"keep","type":"file","path":"`+keep+`","content":"a\n"}
]}`)
report, state, err := Apply(context.Background(), archHost(t), one, state, noServices, nil)
report, state, err := Apply(context.Background(), archHost(t), one, state, store.OriginCarried, noServices, nil)
if err != nil {
t.Fatal(err)
}
@@ -137,7 +137,7 @@ func TestNothingTheHostDidNotCreateIsTouched(t *testing.T) {
d := parse(t, `{"declaration":1,"resources":[
{"id":"ours","type":"file","path":"`+filepath.Join(dir, "ours.conf")+`","content":"a\n"}
]}`)
if _, _, err := Apply(context.Background(), archHost(t), d, store.State{}, noServices, nil); err != nil {
if _, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, noServices, nil); err != nil {
t.Fatal(err)
}
@@ -156,7 +156,7 @@ func TestARenameToTheSamePathDoesNotDeleteTheNewFile(t *testing.T) {
before := parse(t, `{"declaration":1,"resources":[
{"id":"old","type":"file","path":"`+path+`","content":"old\n"}
]}`)
_, state, err := Apply(context.Background(), archHost(t), before, store.State{}, noServices, nil)
_, state, err := Apply(context.Background(), archHost(t), before, store.State{}, store.OriginCarried, noServices, nil)
if err != nil {
t.Fatal(err)
}
@@ -164,7 +164,7 @@ func TestARenameToTheSamePathDoesNotDeleteTheNewFile(t *testing.T) {
after := parse(t, `{"declaration":1,"resources":[
{"id":"new","type":"file","path":"`+path+`","content":"new\n"}
]}`)
if _, _, err := Apply(context.Background(), archHost(t), after, state, noServices, nil); err != nil {
if _, _, err := Apply(context.Background(), archHost(t), after, state, store.OriginCarried, noServices, nil); err != nil {
t.Fatal(err)
}
@@ -192,7 +192,7 @@ func TestAFailedStepFailsTheApply(t *testing.T) {
{"id":"never","type":"file","path":"`+filepath.Join(dir, "never.conf")+`","content":"b\n"}
]}`)
_, _, err := Apply(context.Background(), archHost(t), d, store.State{}, noServices, nil)
_, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, noServices, nil)
if err == nil {
t.Fatal("an impossible resource did not fail the apply")
}
@@ -225,7 +225,7 @@ func TestNothingIsRecordedUntilItWorked(t *testing.T) {
{"id":"doomed","type":"directory","path":"`+blocker+`"}
]}`)
_, state, err := Apply(context.Background(), archHost(t), d, store.State{}, noServices, nil)
_, state, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, noServices, nil)
if err == nil {
t.Fatal("expected a failure")
}
@@ -244,7 +244,7 @@ func TestAModeIsMaintainedNotJustSet(t *testing.T) {
{"id":"f","type":"file","path":"`+path+`","content":"s\n","mode":"0600"}
]}`)
_, state, err := Apply(context.Background(), archHost(t), d, store.State{}, noServices, nil)
_, state, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, noServices, nil)
if err != nil {
t.Fatal(err)
}
@@ -252,7 +252,7 @@ func TestAModeIsMaintainedNotJustSet(t *testing.T) {
t.Fatal(err)
}
report, _, err := Apply(context.Background(), archHost(t), d, state, noServices, nil)
report, _, err := Apply(context.Background(), archHost(t), d, state, store.OriginCarried, noServices, nil)
if err != nil {
t.Fatal(err)
}
@@ -283,7 +283,7 @@ func TestAServiceIsReadBackNotAssumed(t *testing.T) {
d := parse(t, `{"declaration":1,"resources":[
{"id":"s","type":"service","unit":"doomed.service","state":"running"}
]}`)
_, _, err := Apply(context.Background(), archHost(t), d, store.State{}, run, nil)
_, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, run, nil)
if err == nil {
t.Fatal("a service that died immediately was reported as running")
}
@@ -299,7 +299,7 @@ func TestAnUnknownServiceStateIsRefusedNotGuessed(t *testing.T) {
d := parse(t, `{"declaration":1,"resources":[
{"id":"s","type":"service","unit":"odd.service","state":"running"}
]}`)
_, _, err := Apply(context.Background(), archHost(t), d, store.State{}, run, nil)
_, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, run, nil)
if err == nil || !strings.Contains(err.Error(), "neither running nor stopped") {
t.Errorf("an unrecognised service state was not refused: %v", err)
}
@@ -323,7 +323,7 @@ func TestADroppedServiceIsStoppedNotDeleted(t *testing.T) {
{"id":"other","type":"file","path":"`+filepath.Join(t.TempDir(), "a")+`","content":"a\n"}
]}`)
if _, _, err := Apply(context.Background(), archHost(t), d, state, run, nil); err != nil {
if _, _, err := Apply(context.Background(), archHost(t), d, state, store.OriginCarried, run, nil); err != nil {
t.Fatal(err)
}
joined := strings.Join(commands, "; ")
@@ -349,7 +349,7 @@ func TestAUnitThatDoesNotExistIsNotStopped(t *testing.T) {
{"id":"s","type":"service","unit":"never-installed.service","state":"stopped"}
]}`)
_, state, err := Apply(context.Background(), archHost(t), d, store.State{}, absent, nil)
_, state, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, absent, nil)
if err == nil {
t.Fatal("a unit that does not exist was reported as satisfactorily stopped")
}
@@ -370,7 +370,7 @@ func TestAMaskedUnitIsRefused(t *testing.T) {
d := parse(t, `{"declaration":1,"resources":[
{"id":"s","type":"service","unit":"masked.service","state":"running"}
]}`)
if _, _, err := Apply(context.Background(), archHost(t), d, store.State{}, masked, nil); err == nil {
if _, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, masked, nil); err == nil {
t.Fatal("a masked unit was accepted")
}
}
@@ -398,7 +398,7 @@ func TestForgettingAUnitThatIsGoneDoesNotStrandTheNode(t *testing.T) {
{"id":"f","type":"file","path":"`+filepath.Join(t.TempDir(), "a")+`","content":"a\n"}
]}`)
report, state, err := Apply(context.Background(), archHost(t), d, known, run, nil)
report, state, err := Apply(context.Background(), archHost(t), d, known, store.OriginCarried, run, nil)
if err != nil {
t.Fatalf("a vanished unit stranded the apply: %v", err)
}
@@ -442,7 +442,7 @@ func TestABrokenPackageDatabaseIsNotReadAsNotInstalled(t *testing.T) {
{"id":"rt","type":"package","package":"docker"}
]}`)
_, _, err := Apply(context.Background(), archHost(t), d, store.State{}, run, nil)
_, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, run, nil)
if err == nil {
t.Fatal("a broken package database was read as 'not installed'")
}
@@ -463,7 +463,7 @@ func TestAnInstalledPackageIsNotReinstalled(t *testing.T) {
{"id":"rt","type":"package","package":"docker"}
]}`)
report, _, err := Apply(context.Background(), archHost(t), d, store.State{}, run, nil)
report, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, run, nil)
if err != nil {
t.Fatalf("apply failed: %v", err)
}
@@ -493,7 +493,7 @@ func TestAPackageIsNeverUninstalled(t *testing.T) {
{"id":"f","type":"file","path":"`+filepath.Join(t.TempDir(), "a")+`","content":"a\n"}
]}`)
report, state, err := Apply(context.Background(), archHost(t), d, known, run, nil)
report, state, err := Apply(context.Background(), archHost(t), d, known, store.OriginCarried, run, nil)
if err != nil {
t.Fatalf("dropping a package stranded the apply: %v", err)
}
@@ -523,7 +523,7 @@ func TestAnActionThatIsAlreadyTrueDoesNotRun(t *testing.T) {
{"id":"db","type":"action","command":["create-db","mesh"],"verify":["has-db","mesh"]}
]}`)
report, _, err := Apply(context.Background(), archHost(t), d, store.State{}, run, nil)
report, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, run, nil)
if err != nil {
t.Fatalf("apply failed: %v", err)
}
@@ -549,7 +549,7 @@ func TestAnActionThatSucceedsAndDoesNothingFails(t *testing.T) {
{"id":"db","type":"action","command":["create-db","mesh"],"verify":["has-db","mesh"]}
]}`)
_, state, err := Apply(context.Background(), archHost(t), d, store.State{}, run, nil)
_, state, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, run, nil)
if err == nil {
t.Fatal("an action that reported success and did nothing was accepted")
}
@@ -576,7 +576,7 @@ func TestAnActionRunsInsideTheContainerItNames(t *testing.T) {
{"id":"db","type":"action","in":"store","command":["createdb","mesh"],"verify":["psql","-lqt"]}
]}`)
if _, _, err := Apply(context.Background(), archHost(t), d, store.State{}, run, nil); err != nil {
if _, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, run, nil); err != nil {
t.Fatalf("apply failed: %v", err)
}
if !sawExec {
@@ -603,7 +603,7 @@ func TestAContainerThatExitsImmediatelyFailsTheApply(t *testing.T) {
{"id":"store","type":"container","name":"store","image":"`+pinned+`"}
]}`)
_, state, err := Apply(context.Background(), archHost(t), d, store.State{}, run, nil)
_, state, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, run, nil)
if err == nil {
t.Fatal("a container that exited immediately was reported as applied")
}
@@ -645,7 +645,7 @@ func TestAContainerWhoseDeclarationChangedIsReplaced(t *testing.T) {
return "", nil
}
report, _, err := Apply(context.Background(), archHost(t), d, store.State{}, run, nil)
report, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, run, nil)
if err != nil {
t.Fatalf("apply failed: %v", err)
}
@@ -675,7 +675,7 @@ func TestAContainerThatMatchesIsLeftAlone(t *testing.T) {
return "", nil
}
report, _, err := Apply(context.Background(), archHost(t), d, store.State{}, run, nil)
report, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, run, nil)
if err != nil {
t.Fatalf("apply failed: %v", err)
}
@@ -734,7 +734,7 @@ func TestAServiceIsEnabledAtBootWhenAsked(t *testing.T) {
{"id":"rt","type":"service","unit":"docker.service","state":"running","boot":"enabled"}
]}`)
report, _, err := Apply(context.Background(), archHost(t), d, store.State{},
report, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried,
systemctlStub(t, "loaded", "inactive", "disabled", &verbs), nil)
if err != nil {
t.Fatalf("apply failed: %v", err)
@@ -754,7 +754,7 @@ func TestBootIsEnabledBeforeTheUnitIsStarted(t *testing.T) {
d := parseTrusted(t, `{"declaration":1,"resources":[
{"id":"rt","type":"service","unit":"docker.service","state":"running","boot":"enabled"}
]}`)
if _, _, err := Apply(context.Background(), archHost(t), d, store.State{},
if _, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried,
systemctlStub(t, "loaded", "inactive", "disabled", &verbs), nil); err != nil {
t.Fatal(err)
}
@@ -769,7 +769,7 @@ func TestAlreadyEnabledAndRunningIsUnchanged(t *testing.T) {
{"id":"rt","type":"service","unit":"docker.service","state":"running","boot":"enabled"}
]}`)
report, _, err := Apply(context.Background(), archHost(t), d, store.State{},
report, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried,
systemctlStub(t, "loaded", "active", "enabled", &verbs), nil)
if err != nil {
t.Fatalf("apply failed: %v", err)
@@ -789,7 +789,7 @@ func TestOmittingBootLeavesItAlone(t *testing.T) {
d := parseTrusted(t, `{"declaration":1,"resources":[
{"id":"rt","type":"service","unit":"docker.service","state":"running"}
]}`)
if _, _, err := Apply(context.Background(), archHost(t), d, store.State{},
if _, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried,
systemctlStub(t, "loaded", "inactive", "enabled", &verbs), nil); err != nil {
t.Fatal(err)
}
@@ -809,7 +809,7 @@ func TestAStaticUnitCannotBeEnabled(t *testing.T) {
{"id":"rt","type":"service","unit":"dbus.socket","state":"running","boot":"enabled"}
]}`)
_, _, err := Apply(context.Background(), archHost(t), d, store.State{},
_, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried,
systemctlStub(t, "loaded", "active", "static", &verbs), nil)
if err == nil {
t.Fatal("a static unit was accepted as enable-able")
@@ -824,7 +824,7 @@ func TestAnUnknownBootStateIsRefusedNotGuessed(t *testing.T) {
d := parseTrusted(t, `{"declaration":1,"resources":[
{"id":"rt","type":"service","unit":"x.service","state":"running","boot":"enabled"}
]}`)
_, _, err := Apply(context.Background(), archHost(t), d, store.State{},
_, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried,
systemctlStub(t, "loaded", "active", "indirect", &verbs), nil)
if err == nil {
t.Fatal("an unrecognised boot state was guessed at instead of refused")
@@ -900,7 +900,7 @@ func TestAContainerUsesTheRuntimeTheMachineHas(t *testing.T) {
// It will fail at read-back — the stub never reports it running — and what matters is
// WHICH binary it used getting there.
_, _, _ = Apply(context.Background(), archHost(t), d, store.State{}, run, nil)
_, _, _ = Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, run, nil)
for _, c := range calledWith {
if c != "podman" {
@@ -922,7 +922,7 @@ func TestNoRuntimeIsSaidPlainly(t *testing.T) {
{"id":"store","type":"container","name":"store","image":"`+pinned+`"}
]}`)
_, _, err := Apply(context.Background(), archHost(t), d, store.State{}, run, nil)
_, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, run, nil)
if err == nil {
t.Fatal("a machine with no container runtime applied a container")
}
+39 -5
View File
@@ -31,13 +31,20 @@ type Membership struct {
// Applier is what the host does with a declaration that has been proved to come from the mesh.
type Applier func(ctx context.Context, declaration []byte) Report
// Announce is how the link says what is happening, so a node running unattended leaves an
// account of it. Nil is allowed and means say nothing.
type Announce func(string)
// Run holds the link open, applying what arrives and reporting what happened.
//
// Outbound only, and nothing listens on this machine. The connection is the node's presence in
// the mesh: while it is up the node is enrolled, and while it is down the node is disconnected —
// which is an ordinary situation and not a failure, so this returns rather than panicking and
// leaves restarting to whatever supervises it.
func Run(ctx context.Context, m Membership, apply Applier, timeout time.Duration) error {
func Run(ctx context.Context, m Membership, apply Applier, say Announce, timeout time.Duration) error {
if say == nil {
say = func(string) {}
}
config, err := PinnedConfig(m.Fingerprint)
if err != nil {
return err
@@ -84,6 +91,18 @@ func Run(ctx context.Context, m Membership, apply Applier, timeout time.Duration
}
closed := conn.NotifyClose(make(chan *amqp.Error, 1))
// Published mandatory, so the broker hands back anything it cannot route rather than
// dropping it. Without this a report goes to an exchange with no matching binding, the
// publisher is told nothing, and the mesh believes this node never answered while the node
// believes it did — which is what happened when `report` was left unbound on the other side.
returned := channel.NotifyReturn(make(chan amqp.Return, 4))
go func() {
for r := range returned {
say(fmt.Sprintf("the broker could not route this node's %s: %s (%d %s)",
r.RoutingKey, r.Exchange, r.ReplyCode, r.ReplyText))
}
}()
for {
select {
case <-ctx.Done():
@@ -95,7 +114,15 @@ func Run(ctx context.Context, m Membership, apply Applier, timeout time.Duration
return errors.New("the broker stopped delivering")
}
report := handle(ctx, m, apply, delivery)
publishReport(ctx, channel, m, report, timeout)
switch {
case report.Refused != "":
say("refused a declaration: " + report.Refused)
case len(report.Failed) > 0:
say(fmt.Sprintf("applied %d and failed: %v", len(report.Applied), report.Failed))
default:
say(fmt.Sprintf("applied %d resource(s)", len(report.Applied)))
}
publishReport(ctx, channel, m, report, say, timeout)
// Acknowledged after the report is published. A node that dies between applying and
// reporting leaves the declaration on the broker and applies it again on return,
// which is safe because applying is reconciliation — it converges rather than
@@ -127,14 +154,21 @@ func handleBody(ctx context.Context, m Membership, body []byte, apply Applier) R
}
func publishReport(ctx context.Context, channel *amqp.Channel, m Membership, report Report,
timeout time.Duration) {
say Announce, timeout time.Duration) {
report.Node = m.Node
body, err := json.Marshal(report)
if err != nil {
say("cannot encode this node's own report: " + err.Error())
return
}
publish, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
_ = channel.PublishWithContext(publish, Exchange, KeyReport, false, false,
amqp.Publishing{ContentType: "application/json", Body: body})
// Said rather than swallowed. A report that fails to publish leaves the mesh believing this
// node never answered, while the node believes it did — and the two would go on disagreeing
// with nothing anywhere saying so. That shape of fault is the one this project keeps finding.
if err := channel.PublishWithContext(publish, Exchange, KeyReport, true, false,
amqp.Publishing{ContentType: "application/json", Body: body}); err != nil {
say(fmt.Sprintf("applied, and could not tell the mesh: %v", err))
}
}
+40 -3
View File
@@ -33,6 +33,16 @@ const DefaultPath = "/var/lib/mesh-host/state.json"
type Applied struct {
ID string `json:"id"`
Type string `json:"type"`
// Origin is who asked for this: the bundle this host carries, or the mesh.
//
// Recorded because the two must not remove each other. A node raises its own substrate from
// the bundle before any mesh exists, then enrols and is sent declarations — and a
// declaration naming two resources would otherwise remove the store, the broker and the
// control plane, which is 04-ISSUES/010 and happened on the first end-to-end run.
//
// Empty means carried, for state written before this field existed: everything a host had
// applied at that point came from its bundle.
Origin string `json:"origin,omitempty"`
// Target is what was changed — a path, a unit — so removal knows what to undo without
// re-reading a declaration that may no longer exist.
Target string `json:"target"`
@@ -165,12 +175,39 @@ func (s *State) Forget(id string) {
// Reverse order because undoing in the order things were made undoes a directory before the
// file inside it. Reversing is the only ordering the host can derive without deciding
// anything, which is the line novox/hq ADR 0005 draws.
func (s State) Orphans(declared map[string]bool) []Applied {
func (s State) Orphans(declared map[string]bool, origin string) []Applied {
var out []Applied
for i := len(s.Resources) - 1; i >= 0; i-- {
if !declared[s.Resources[i].ID] {
out = append(out, s.Resources[i])
r := s.Resources[i]
// Only this origin's own. A mesh declaration says nothing about what the bundle raised,
// and a bundle says nothing about what the mesh assigned — so neither may remove the
// other's by omission, which is the only way either could express removal.
if originOf(r) != origin {
continue
}
if !declared[r.ID] {
out = append(out, r)
}
}
return out
}
// Origins a resource can have.
const (
// Carried is the bundle this host was built with.
OriginCarried = "carried"
// Declared is the mesh, over the link.
OriginDeclared = "declared"
)
// originOf reads a record's origin, treating absence as carried.
//
// State written before origins existed was all bundle-applied: a host had no other way to be
// told anything. Guessing wrong in the other direction would have a first upgrade remove the
// substrate, which is the fault this field exists to prevent.
func originOf(r Applied) string {
if r.Origin == "" {
return OriginCarried
}
return r.Origin
}
+54 -2
View File
@@ -116,7 +116,7 @@ func TestOrphansAreWhatWasAppliedAndIsNoLongerDeclared(t *testing.T) {
{ID: "file", Type: "file", Target: "/etc/mesh/a.conf"},
{ID: "kept", Type: "file", Target: "/etc/mesh/b.conf"},
}}
orphans := s.Orphans(map[string]bool{"kept": true})
orphans := s.Orphans(map[string]bool{"kept": true}, OriginCarried)
if len(orphans) != 2 {
t.Fatalf("expected two orphans, got %d: %+v", len(orphans), orphans)
@@ -130,7 +130,59 @@ func TestOrphansAreWhatWasAppliedAndIsNoLongerDeclared(t *testing.T) {
func TestNothingIsAnOrphanWhenEverythingIsDeclared(t *testing.T) {
s := State{Resources: []Applied{{ID: "a", Type: "file", Target: "/a"}}}
if got := s.Orphans(map[string]bool{"a": true}); len(got) != 0 {
if got := s.Orphans(map[string]bool{"a": true}, OriginCarried); len(got) != 0 {
t.Errorf("a declared resource was treated as an orphan: %+v", got)
}
}
func TestADeclarationDoesNotOrphanWhatTheBundleRaised(t *testing.T) {
// 04-ISSUES/010. A first node raises its substrate from the bundle it carries, then enrols
// and is sent a declaration naming two resources. Before origins, that removed the store, the
// broker and the control plane that had sent it — the mesh deleting itself over the link the
// message arrived on, in under a second, on the first end-to-end run.
s := State{Resources: []Applied{
{ID: "store", Type: "container", Target: "mesh-store", Origin: OriginCarried},
{ID: "broker", Type: "container", Target: "mesh-broker", Origin: OriginCarried},
{ID: "greeting", Type: "directory", Target: "/var/lib/demo", Origin: OriginDeclared},
}}
// The mesh declares nothing at all. Everything it previously declared is an orphan; nothing
// the bundle raised is.
orphans := s.Orphans(map[string]bool{}, OriginDeclared)
if len(orphans) != 1 || orphans[0].ID != "greeting" {
var got []string
for _, o := range orphans {
got = append(got, o.ID)
}
t.Fatalf("a declaration would remove %v; it may only remove what the mesh declared", got)
}
}
func TestTheBundleDoesNotOrphanWhatTheMeshDeclared(t *testing.T) {
// The same rule the other way. A host reconciling its carried bundle must not remove what the
// mesh assigned to this node, or every restart would undo the node's actual work.
s := State{Resources: []Applied{
{ID: "store", Type: "container", Target: "mesh-store", Origin: OriginCarried},
{ID: "workload", Type: "container", Target: "some-app", Origin: OriginDeclared},
}}
orphans := s.Orphans(map[string]bool{"store": true}, OriginCarried)
if len(orphans) != 0 {
t.Errorf("reconciling the bundle would remove %s, which the mesh declared", orphans[0].ID)
}
}
func TestStateWrittenBeforeOriginsExistedIsTreatedAsCarried(t *testing.T) {
// Every resource a host had applied before this field existed came from its bundle, because
// there was no other way to tell it anything. Guessing the other way would have the first
// declaration remove the substrate — which is the fault this exists to prevent, arriving
// through the upgrade that fixes it.
s := State{Resources: []Applied{{ID: "store", Type: "container", Target: "mesh-store"}}}
if got := s.Orphans(map[string]bool{}, OriginDeclared); len(got) != 0 {
t.Errorf("a declaration would remove %s, recorded before origins existed", got[0].ID)
}
if got := s.Orphans(map[string]bool{}, OriginCarried); len(got) != 1 {
t.Error("the bundle cannot remove its own resource, so nothing could ever remove it")
}
}