Every held thing carries what a take compares: for a found container its image and the image's date, the networks it is on and the other containers on each, its mounts and published ports, beside the declared image (and its date once pulled), ports and volumes, with the downgrade decided when both dates are known; for a found file whether the declared content differs and how, as lines lost and lines new. A resource whose target moved keeps the former target on record as an orphan, so the next apply removes the container or file the host wrote under the old name (issue 097). Every apply reports the strays: containers the mesh neither wrote nor holds.
98 lines
4.3 KiB
Go
98 lines
4.3 KiB
Go
package apply
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/novox/mesh-host/internal/store"
|
|
)
|
|
|
|
// A take is a comparison (novox/hq ADR 0163): while a module's container is held, the host reports
|
|
// the found image and its age beside the declared one, the networks and who else is on them, the
|
|
// mounts and the ports — and says when the declared image is the older.
|
|
func TestAHeldContainerCarriesTheFactsATakeCompares(t *testing.T) {
|
|
dir, page, m := predecessor(t)
|
|
m.containers["hello-web"].image = "web:1.27"
|
|
m.containers["hello-web"].imageID = "sha256:found"
|
|
m.containers["hello-web"].networks = []string{"predecessor_default"}
|
|
m.containers["hello-web"].mounts = []string{"/srv/web:/data"}
|
|
m.containers["hello-web"].ports = []string{"80/tcp>0.0.0.0:8080"}
|
|
m.images = map[string]string{"sha256:found": "2026-09-17T10:00:00Z", pinned: "2026-08-20T10:00:00Z"}
|
|
m.members = map[string][]string{"predecessor_default": {"hello-web", "office", "db"}}
|
|
|
|
_, state := applyAdopted(t, adopted(t, untakenWeb, webResources(page)), store.State{}, m, dir)
|
|
h, ok := state.HeldAt("hello-web.server")
|
|
if !ok || h.Facts == nil {
|
|
t.Fatalf("a held container carries no facts: %+v", h)
|
|
}
|
|
f := h.Facts
|
|
if f.Image != "web:1.27" || f.ImageCreated != "2026-09-17T10:00:00Z" {
|
|
t.Errorf("the found image and its age: %+v", f)
|
|
}
|
|
if f.DeclaredImage != pinned || f.DeclaredImageCreated != "2026-08-20T10:00:00Z" || !f.Downgrade {
|
|
t.Errorf("the declared image, its age, and that it is a downgrade: %+v", f)
|
|
}
|
|
if got := f.Networks["predecessor_default"]; len(got) != 2 || got[0] != "db" || got[1] != "office" {
|
|
t.Errorf("the neighbours on the found network, without the container itself: %v", f.Networks)
|
|
}
|
|
if len(f.Mounts) != 1 || f.Mounts[0] != "/srv/web:/data" || len(f.Ports) != 1 || f.Ports[0] != "80/tcp>0.0.0.0:8080" {
|
|
t.Errorf("mounts and ports as found: %+v", f)
|
|
}
|
|
// And the held file carries how the declared content differs from what was found.
|
|
p, ok := state.HeldAt("hello-web.page")
|
|
if !ok || p.Facts == nil || !p.Facts.Differs {
|
|
t.Fatalf("a held file that differs from the declared content does not say so: %+v", p)
|
|
}
|
|
joined := strings.Join(p.Facts.Difference, "\n")
|
|
if !strings.Contains(joined, "- the predecessor's page") || !strings.Contains(joined, "+ the mesh's page") {
|
|
t.Errorf("the difference does not show what is lost and what is new: %q", joined)
|
|
}
|
|
_ = os.Remove(filepath.Join(dir, "unused"))
|
|
}
|
|
|
|
// A declared image not yet on the machine leaves its age unknown and the comparison undecided.
|
|
func TestAnImageNotYetPulledLeavesTheDowngradeUndecided(t *testing.T) {
|
|
dir, page, m := predecessor(t)
|
|
m.containers["hello-web"].image = "web:1.27"
|
|
m.containers["hello-web"].imageID = "sha256:found"
|
|
m.images = map[string]string{"sha256:found": "2026-09-17T10:00:00Z"}
|
|
_, state := applyAdopted(t, adopted(t, untakenWeb, webResources(page)), store.State{}, m, dir)
|
|
h, _ := state.HeldAt("hello-web.server")
|
|
if h.Facts == nil || h.Facts.DeclaredImageCreated != "" || h.Facts.Downgrade {
|
|
t.Fatalf("an unknown declared age decided a downgrade: %+v", h.Facts)
|
|
}
|
|
}
|
|
|
|
func TestTheDifferenceIsWhatIsLostAndWhatIsNew(t *testing.T) {
|
|
differs, lines := differenceOf("a\nprivate scope: local\nb\n", "a\nb\nupstream: public\n")
|
|
if !differs || len(lines) != 2 || lines[0] != "- private scope: local" || lines[1] != "+ upstream: public" {
|
|
t.Fatalf("got %v %v", differs, lines)
|
|
}
|
|
if differs, lines := differenceOf("same\n", "same\n"); differs || lines != nil {
|
|
t.Fatalf("identical content differs: %v %v", differs, lines)
|
|
}
|
|
}
|
|
|
|
// What runs on the machine that the mesh neither wrote nor holds is reported (ADR 0163).
|
|
func TestStraysAreWhatRunsHereThatNobodyAsked(t *testing.T) {
|
|
m := &machine{containers: map[string]*fakeContainer{
|
|
"hello-web": {id: "ours", running: true, image: "web:1"},
|
|
"gitea-old": {id: "left-behind", running: true, image: "gitea:1.22"},
|
|
"held-thing": {id: "found", running: true, image: "x:1"},
|
|
}}
|
|
known := store.State{
|
|
Resources: []store.Applied{{ID: "hello-web.server", Type: "container", Target: "hello-web"}},
|
|
Held: []store.Held{{ID: "other.server", Kind: "container", Target: "held-thing"}},
|
|
}
|
|
strays, err := Strays(context.Background(), m.run, known)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(strays) != 1 || strays[0].Name != "gitea-old" || !strings.Contains(strays[0].Detail, "gitea:1.22") {
|
|
t.Fatalf("strays: %+v", strays)
|
|
}
|
|
}
|