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
189 lines
7.0 KiB
Go
189 lines
7.0 KiB
Go
package store
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestAFreshMachineHasAnEmptyStateNotAnError(t *testing.T) {
|
|
// The first apply on a machine that has never been touched is the ordinary case, not a
|
|
// failure. A host that errored here could never bootstrap anything.
|
|
s, err := Load(filepath.Join(t.TempDir(), "nothing-here.json"))
|
|
if err != nil {
|
|
t.Fatalf("a fresh machine produced an error: %v", err)
|
|
}
|
|
if len(s.Resources) != 0 {
|
|
t.Errorf("a fresh machine claims to own %d resources", len(s.Resources))
|
|
}
|
|
}
|
|
|
|
func TestAnUnreadableStateIsRefusedNotIgnored(t *testing.T) {
|
|
// The dangerous one. Starting empty would make the host believe it owns nothing, so it
|
|
// would remove nothing it should and re-apply everything it need not — silently.
|
|
path := filepath.Join(t.TempDir(), "state.json")
|
|
if err := os.WriteFile(path, []byte("{this is not json"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, err := Load(path)
|
|
if err == nil {
|
|
t.Fatal("a corrupt state was read as an empty one")
|
|
}
|
|
if !strings.Contains(err.Error(), "believes it owns nothing") {
|
|
t.Errorf("the error does not say why this matters: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestWhatIsSavedIsWhatIsLoaded(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "state.json")
|
|
want := State{Resources: []Applied{
|
|
{ID: "etc", Type: "directory", Target: "/etc/mesh", AppliedAt: time.Now().UTC().Truncate(time.Second)},
|
|
{ID: "conf", Type: "file", Target: "/etc/mesh/host.conf", AppliedAt: time.Now().UTC().Truncate(time.Second)},
|
|
}}
|
|
if err := Save(path, want); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, err := Load(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(got.Resources) != 2 || got.Resources[0].ID != "etc" || got.Resources[1].ID != "conf" {
|
|
t.Fatalf("order or content was lost: %+v", got.Resources)
|
|
}
|
|
if got.UpdatedAt.IsZero() {
|
|
t.Error("the state does not say when it was written")
|
|
}
|
|
}
|
|
|
|
func TestTheStateIsNotWorldReadable(t *testing.T) {
|
|
// It records what is on the machine and where. Not secret, and not everyone's business.
|
|
path := filepath.Join(t.TempDir(), "state.json")
|
|
if err := Save(path, State{Resources: []Applied{{ID: "a", Type: "file", Target: "/a"}}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if mode := info.Mode().Perm(); mode&0o077 != 0 {
|
|
t.Errorf("the state is readable by others: %o", mode)
|
|
}
|
|
}
|
|
|
|
func TestSavingLeavesNoDebrisBehind(t *testing.T) {
|
|
// The write is atomic through a temporary file. A run that left those behind would fill a
|
|
// directory with near-copies of the truth, and the next reader would have to guess.
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "state.json")
|
|
for i := 0; i < 3; i++ {
|
|
if err := Save(path, State{Resources: []Applied{{ID: "a", Type: "file", Target: "/a"}}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
entries, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(entries) != 1 {
|
|
names := []string{}
|
|
for _, e := range entries {
|
|
names = append(names, e.Name())
|
|
}
|
|
t.Errorf("expected only the state file, found: %v", names)
|
|
}
|
|
}
|
|
|
|
func TestRecordReplacesRatherThanDuplicating(t *testing.T) {
|
|
s := State{}
|
|
s.Record(Applied{ID: "a", Type: "file", Target: "/old"})
|
|
s.Record(Applied{ID: "a", Type: "file", Target: "/new"})
|
|
|
|
if len(s.Resources) != 1 {
|
|
t.Fatalf("one identity produced %d records", len(s.Resources))
|
|
}
|
|
if s.Resources[0].Target != "/new" {
|
|
t.Errorf("the record was not updated: %+v", s.Resources[0])
|
|
}
|
|
}
|
|
|
|
func TestOrphansAreWhatWasAppliedAndIsNoLongerDeclared(t *testing.T) {
|
|
// The whole reason the store arrives at stage 2 rather than stage 3: removal is impossible
|
|
// without knowing what was applied.
|
|
s := State{Resources: []Applied{
|
|
{ID: "dir", Type: "directory", Target: "/etc/mesh"},
|
|
{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}, OriginCarried)
|
|
|
|
if len(orphans) != 2 {
|
|
t.Fatalf("expected two orphans, got %d: %+v", len(orphans), orphans)
|
|
}
|
|
// Reverse order: undoing in the order things were made would remove a directory before the
|
|
// file inside it.
|
|
if orphans[0].ID != "file" || orphans[1].ID != "dir" {
|
|
t.Errorf("orphans are not in reverse order: %s then %s", orphans[0].ID, orphans[1].ID)
|
|
}
|
|
}
|
|
|
|
func TestNothingIsAnOrphanWhenEverythingIsDeclared(t *testing.T) {
|
|
s := State{Resources: []Applied{{ID: "a", Type: "file", Target: "/a"}}}
|
|
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 foundation 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 foundation — 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")
|
|
}
|
|
}
|