Steps 6 to 10, which turn a substrate into a mesh that can maintain itself
(novox/hq ADR 0067).
6 enrol a node record, a token, `mesh-host enrol`, and the host agent
running. Proved by the mesh having HEARD from the node, not by a
process existing: a host that cannot reach the broker looks exactly
like a successful install until the first push applies nothing.
7 registry the module that gives this mesh an image store, registered from a
--catalog checkout, assigned and pushed. Its image is upstream and
never built (04-ISSUES/029) — a placeholder digest there is refused.
Verified by asking `/v2/`, because a container that is up is not a
registry that serves.
8 publish the carried image pushed into that registry, which assigns it the
first manifest digest it has ever had. This is the hinge: without
it the mesh works and can never upgrade itself.
9 control the control plane registered as an ordinary module pinned to that
digest, with the substrate's own store connections delivered
through `secret accept` — read out of the bundle that made them,
because the mesh cannot invent a credential that predates it.
10 retire the temporary control plane dropped from the bundle and removed by
the host's ordinary removal pass.
Every step asks before it acts and reports "already done". No step leaves the
machine without a control plane: steps 9 and 10 overlap deliberately, and two
stateless control planes are untidy rather than broken.
mesh-control's `internal/builder`.PublishImage is mirrored rather than imported —
tier 0 depends on nothing that must be installed first — with one correction: the
digest is chosen from RepoDigests by repository instead of taken as element zero,
so an image pushed to two registries cannot silently pin this mesh to the wrong
one.
Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
166 lines
5.8 KiB
Go
166 lines
5.8 KiB
Go
package bootstrap
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-host/internal/declaration"
|
|
)
|
|
|
|
// Retirement is destruction by omission, which is the host's ordinary behaviour: it owns what it
|
|
// applied and removes what it owns and is no longer declared. These tests defend the bundle
|
|
// surgery that expresses it, because a bundle that came out of it unparseable would be found by
|
|
// the apply — after the file on the machine had already been replaced.
|
|
|
|
func produced(t *testing.T) []byte {
|
|
t.Helper()
|
|
out, err := Rewrite(theRealBundle(t), held)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return out.Bundle
|
|
}
|
|
|
|
// The temporary control plane leaves the bundle, everything else stays, and what is left parses.
|
|
func TestTheTemporaryControlPlaneLeavesTheBundleAndNothingElseDoes(t *testing.T) {
|
|
before, err := declaration.ParseFileTrusted(produced(t))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
shorter, err := removeResource(produced(t), ControlPlaneID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
after, err := declaration.ParseFileTrusted(shorter)
|
|
if err != nil {
|
|
t.Fatalf("the bundle without the control plane does not parse: %v\n%s", err, shorter)
|
|
}
|
|
if len(after.Resources) != len(before.Resources)-1 {
|
|
t.Fatalf("the bundle went from %d resources to %d, and one was removed",
|
|
len(before.Resources), len(after.Resources))
|
|
}
|
|
if _, err := controlPlaneIn(after); err == nil {
|
|
t.Error("the bundle still declares a control plane")
|
|
}
|
|
// The store and the broker are still exactly what they were. A retirement that took the
|
|
// substrate with it would leave the machine with a module and nothing under it.
|
|
for id, name := range containerNames(before) {
|
|
if id == ControlPlaneID {
|
|
continue
|
|
}
|
|
if containerNames(after)[id] != name {
|
|
t.Errorf("%s was lost or renamed by the retirement", id)
|
|
}
|
|
}
|
|
}
|
|
|
|
// **A `//` inside a string is not a comment.** The substrate's own bundle carries
|
|
// `postgres://…` several times, and a scanner that read the rest of those lines as a comment
|
|
// would lose braces and cut the wrong thing out — silently, because what it produced would still
|
|
// look like a file.
|
|
func TestASchemeInsideAStringIsNotReadAsAComment(t *testing.T) {
|
|
shorter, err := removeResource(produced(t), ControlPlaneID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
after, err := declaration.ParseFileTrusted(shorter)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// The migration action, which is the resource holding the most `://` of anything here, is
|
|
// still whole.
|
|
found := false
|
|
for _, r := range after.Resources {
|
|
if r.Identity() == "context-schemas" {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Error("the resource full of connection strings did not survive the removal")
|
|
}
|
|
}
|
|
|
|
// Removing the FIRST element takes the comma after it rather than the comma before it, because
|
|
// there is no comma before it. Getting this wrong produces a leading comma, which is JSON nothing
|
|
// parses — and the file would already have been written.
|
|
func TestRemovingTheFirstResourceLeavesAValidList(t *testing.T) {
|
|
shorter, err := removeResource(produced(t), "container-runtime")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
after, err := declaration.ParseFileTrusted(shorter)
|
|
if err != nil {
|
|
t.Fatalf("removing the first resource broke the bundle: %v\n%s", err, shorter)
|
|
}
|
|
for _, r := range after.Resources {
|
|
if r.Identity() == "container-runtime" {
|
|
t.Error("the first resource is still there")
|
|
}
|
|
}
|
|
}
|
|
|
|
// A bundle that declares no such resource is refused rather than silently returned unchanged. A
|
|
// removal that removed nothing and reported success would leave the apply below with nothing to
|
|
// do and the installer claiming a pivot it did not finish.
|
|
func TestRemovingSomethingThatIsNotThereIsRefused(t *testing.T) {
|
|
if _, err := removeResource(produced(t), "nothing-of-the-sort"); err == nil {
|
|
t.Fatal("a bundle was reported to have had a resource removed that it never declared")
|
|
}
|
|
}
|
|
|
|
// A re-run after the retirement finds a bundle with no control plane in it and says so, rather
|
|
// than failing. This is the idempotence of the last step, and it is the one a person is most
|
|
// likely to exercise: the pivot ends here, so a re-run to check ends here too.
|
|
func TestRetiringABundleThatAlreadyHasNoControlPlaneIsAlreadyDone(t *testing.T) {
|
|
shorter, err := removeResource(produced(t), ControlPlaneID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var said []string
|
|
out, err := RetireTheTemporaryControlPlane(context.Background(), Options{},
|
|
nil, shorter, nil, func(line string) { said = append(said, line) })
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !out.Already {
|
|
t.Error("a bundle with no control plane in it was not reported as already retired")
|
|
}
|
|
if !strings.Contains(strings.Join(said, "\n"), "already dropped") {
|
|
t.Errorf("the run does not say it was already done: %v", said)
|
|
}
|
|
}
|
|
|
|
// A container the runtime still knows about after the apply is a pivot that did not finish. Two
|
|
// control planes on one mesh are both correct and neither is wrong — but the temporary one was
|
|
// supposed to go, and saying it went when it did not is the fault this project keeps naming.
|
|
func TestAContainerStillThereAfterRemovalIsNotGone(t *testing.T) {
|
|
previous := answerEvery
|
|
answerEvery = time.Millisecond
|
|
defer func() { answerEvery = previous }()
|
|
|
|
stillThere := &asked{answer: func(_ string, _ []string) (string, error) {
|
|
return "true running\n", nil
|
|
}}
|
|
gone, err := isGone(context.Background(), stillThere.run, time.Second, 0, "temp-mesh-control")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if gone {
|
|
t.Error("a container the runtime still describes was reported gone")
|
|
}
|
|
|
|
removed := &asked{answer: func(_ string, _ []string) (string, error) {
|
|
return "", errors.New("No such object: temp-mesh-control")
|
|
}}
|
|
gone, err = isGone(context.Background(), removed.run, time.Second, 0, "temp-mesh-control")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !gone {
|
|
t.Error("a container the runtime does not know about was not reported gone")
|
|
}
|
|
}
|