package apply import ( "fmt" "os/exec" "strings" "testing" ) // fakeRuntime scripts the container runtime: it records every invocation and answers `inspect` // from a small in-memory model of which containers exist, whether they run, and their spec label. type fakeRuntime struct { calls []string exists map[string]bool running map[string]bool label map[string]string failOnRun bool } func newFakeRuntime() *fakeRuntime { return &fakeRuntime{ exists: map[string]bool{}, running: map[string]bool{}, label: map[string]string{}, } } func (f *fakeRuntime) run(name string, args ...string) (string, error) { f.calls = append(f.calls, name+" "+strings.Join(args, " ")) switch { case len(args) >= 1 && args[0] == "inspect": container := args[len(args)-1] if !f.exists[container] { return "", fmt.Errorf("Error: No such object: %s", container) } return fmt.Sprintf("%v|%s", f.running[container], f.label[container]), nil case len(args) >= 1 && args[0] == "run": if f.failOnRun { return "", fmt.Errorf("simulated run failure") } name, hash := parseRunNameAndLabel(args) f.exists[name] = true f.running[name] = true f.label[name] = hash return name, nil case len(args) >= 2 && args[0] == "rm": container := args[len(args)-1] delete(f.exists, container) delete(f.running, container) delete(f.label, container) return "", nil case len(args) >= 2 && args[0] == "network" && args[1] == "inspect": return "", fmt.Errorf("Error: No such network") case len(args) >= 2 && args[0] == "network" && args[1] == "create": return "", nil } return "", nil } func parseRunNameAndLabel(args []string) (name, hash string) { for i := 0; i < len(args)-1; i++ { switch args[i] { case "--name": name = args[i+1] case "--label": if v, ok := strings.CutPrefix(args[i+1], specLabel+"="); ok { hash = v } } } return name, hash } func containerResource(t *testing.T, name, image string) Resource { t.Helper() d := mustParse(t, `{"version":1,"resources":[ {"id":"`+name+`","type":"container","name":"`+name+`","image":"`+image+`", "network":"n","env":{"A":"1","B":"2"},"ports":["3000"], "volumes":["/services/x/data:/data"]}]}`) return d.Resources[0] } // A container that does not exist is created; applying the same declaration again finds it up to // date and starts nothing. func TestContainerApplyCreatesThenIsIdempotent(t *testing.T) { f := newFakeRuntime() c := containerApplier{run: f.run} r := containerResource(t, "gitea", "gitea/gitea@sha256:abc") created, err := c.Apply(r) if err != nil { t.Fatal(err) } if !created { t.Fatal("first apply did not report creating the container") } if countCalls(f.calls, "run") != 1 { t.Fatalf("expected exactly one run, got calls: %v", f.calls) } f.calls = nil created, err = c.Apply(r) if err != nil { t.Fatal(err) } if created { t.Fatal("second apply recreated an up-to-date container") } if countCalls(f.calls, "run") != 0 { t.Fatalf("idempotent apply still ran the container: %v", f.calls) } } // A container whose spec changed is recreated: the old one removed, a new one started. func TestContainerRecreatedWhenSpecChanges(t *testing.T) { f := newFakeRuntime() c := containerApplier{run: f.run} if _, err := c.Apply(containerResource(t, "gitea", "gitea/gitea@sha256:old")); err != nil { t.Fatal(err) } f.calls = nil // A new image is a new spec hash. if _, err := c.Apply(containerResource(t, "gitea", "gitea/gitea@sha256:new")); err != nil { t.Fatal(err) } if countCalls(f.calls, "rm") != 1 || countCalls(f.calls, "run") != 1 { t.Fatalf("a changed spec should remove and recreate; calls: %v", f.calls) } } // A foreign container by the same name — no spec label — is taken over, because a container holds // no state; its data is in bind mounts declared separately. func TestForeignContainerIsTakenOver(t *testing.T) { f := newFakeRuntime() f.exists["gitea"] = true f.running["gitea"] = true f.label["gitea"] = "" // started by something else, unlabelled c := containerApplier{run: f.run} created, err := c.Apply(containerResource(t, "gitea", "gitea/gitea@sha256:abc")) if err != nil { t.Fatal(err) } if !created { t.Fatal("taking over a foreign container should count as creating ours") } if countCalls(f.calls, "rm") != 1 { t.Fatalf("the foreign container should have been replaced: %v", f.calls) } } // runArgs carries every declared facet through to the invocation, env in sorted order. func TestRunArgsAreComplete(t *testing.T) { r := containerResource(t, "gitea", "img@sha256:abc") args := strings.Join(runArgs(r, "hash123"), " ") for _, want := range []string{ "run -d --name gitea --label mesh-host.spec=hash123", "--network n", "--env-file", "-e A=1 -e B=2", "-p 3000", "-v /services/x/data:/data", "img@sha256:abc", } { if want == "--env-file" { continue // this resource declares none; the others must all be present } if !strings.Contains(args, want) { t.Errorf("run args missing %q\n got: %s", want, args) } } } // A container resource that names no image is refused rather than started blank. func TestContainerWithoutImageIsRefused(t *testing.T) { // Built directly: the shape allows omitting image, but the applier must not. d := mustParse(t, `{"version":1,"resources":[ {"id":"x","type":"container","name":"x"}]}`) f := newFakeRuntime() if _, err := (containerApplier{run: f.run}).Apply(d.Resources[0]); err == nil { t.Fatal("a container with no image was started") } } func countCalls(calls []string, verb string) int { n := 0 for _, c := range calls { // docker ... — verb is the first arg after the runtime name. fields := strings.Fields(c) if len(fields) >= 2 && fields[1] == verb { n++ } } return n } // Smoke test against the real runtime: a container really comes up, is idempotent, and is // removed — the read-back path this whole design rests on, exercised for real. Skipped where the // runtime or its image is not available, never failed for the environment (novox/hq ADR 0034). func TestContainerAgainstRealRuntime(t *testing.T) { if _, err := exec.LookPath(containerRuntime); err != nil { t.Skipf("%s not installed", containerRuntime) } if out, err := exec.Command(containerRuntime, "run", "--rm", "alpine", "true").CombinedOutput(); err != nil { t.Skipf("cannot run a probe container (no image/daemon): %s", strings.TrimSpace(string(out))) } name := "mesh-host-apply-smoke" _ = exec.Command(containerRuntime, "rm", "-f", name).Run() t.Cleanup(func() { _ = exec.Command(containerRuntime, "rm", "-f", name).Run() }) r := mustParse(t, `{"version":1,"resources":[ {"id":"c","type":"container","name":"`+name+`","image":"alpine","args":["sleep","30"]}]}`).Resources[0] c := containerApplier{run: execRunner} created, err := c.Apply(r) if err != nil { t.Fatalf("real apply failed: %v", err) } if !created { t.Fatal("first real apply did not create the container") } // Idempotent: the running container is left alone. created, err = c.Apply(r) if err != nil { t.Fatalf("real re-apply failed: %v", err) } if created { t.Fatal("real re-apply recreated an up-to-date container") } // Removal really removes it. if err := c.Remove(Record{ID: "c", Type: "container", Ref: name, Created: true}); err != nil { t.Fatalf("real remove failed: %v", err) } if out, _ := exec.Command(containerRuntime, "inspect", name).CombinedOutput(); !strings.Contains(strings.ToLower(string(out)), "no such") { t.Fatalf("container still present after removal: %s", out) } }