One name per thing, per the HQ glossary: the module/container/image/binary/repo becomes mesh-controller, the seat the-controller, and the store+broker pair the foundation (embedded base bundles, default template and example lock renamed with their go:embed directives). No behaviour change — a pure vocabulary rename. Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
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
|
|
// foundation 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 foundation'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-controller")
|
|
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-controller")
|
|
}}
|
|
gone, err = isGone(context.Background(), removed.run, time.Second, 0, "temp-mesh-controller")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !gone {
|
|
t.Error("a container the runtime does not know about was not reported gone")
|
|
}
|
|
}
|