apply: ensure a scheduled container's image is present at apply, without running it

A schedule: container (ADR 0053) is installed as present state and never run at apply — the
Scheduler fires it later on its cadence. But a service or run-once container only gets its
image as a side effect of docker run, so a scheduled step's image was not pulled until its
first scheduled fire: absent from the node right after a successful apply, so the first run
paid the whole pull latency and tooling that expects the image present after apply found it
missing.

applyContainer now probes the runtime and ensures the pinned image present for a scheduled
step before recording it. A new ensureImage helper inspects the image and pulls it only if
absent, then reads back (ADR 0018). Ensuring an image is not running it: no docker run fires
the container, so the no-run invariant of ADR 0053 holds. The runtime probe, previously
skipped for a schedule, now runs because a pull needs it — the schedule.go comment is updated
to match.

Tests: the install-does-not-run test is extended to allow the image-ensure while asserting no
fire and no needless pull; a new test applies a scheduled container whose image is absent and
asserts it is pulled and still not started. go build, go vet, go test ./... all pass.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
This commit is contained in:
2026-09-07 02:23:46 +02:00
parent a37acf77ae
commit e5af6bb58e
3 changed files with 120 additions and 16 deletions
+77 -7
View File
@@ -85,11 +85,25 @@ func scheduledContainer(t *testing.T, schedule string) *declaration.Container {
func TestInstallingAScheduleDoesNotRunItAndReportsCurrent(t *testing.T) {
// The deliberate inversion of run-once: the schedule is state that is present, so the apply is
// current as soon as it is recorded — nothing is run, and no runtime is even probed.
var calls []string
// current as soon as it is recorded. Installing it ensures the pinned image is present (a
// scheduled step is never run at apply, so nothing else pulls it), but it NEVER runs the
// container — no `docker run` fires it, which is the invariant that matters (novox/hq ADR 0053).
var fired bool // a `docker run` — the container was started
var pulled bool // an image reported present was pulled anyway
run := func(ctx context.Context, name string, args ...string) (string, error) {
calls = append(calls, name+" "+strings.Join(args, " "))
return "", errors.New("installing a schedule must not run any command")
switch args[0] {
case "info":
return "27.0\n", nil // a container runtime answers the apply's probe
case "image":
return "", nil // `image inspect`: the pinned image is already present
case "pull":
pulled = true
return "", nil
case "run":
fired = true
return "", errors.New("installing a schedule must not run the container")
}
return "", nil
}
d := parseTrusted(t, `{"declaration":1,"resources":[
{"id":"sync","type":"container","name":"sync","image":"`+pinned+`","schedule":"0 3 * * *"}
@@ -99,8 +113,11 @@ func TestInstallingAScheduleDoesNotRunItAndReportsCurrent(t *testing.T) {
if err != nil {
t.Fatalf("installing a schedule failed the apply: %v", err)
}
if len(calls) != 0 {
t.Errorf("installing a schedule ran commands, so it did more than record state: %v", calls)
if fired {
t.Error("installing a schedule ran the container — it installs state, it does not fire it")
}
if pulled {
t.Error("installing a schedule pulled an image it had just found present")
}
if report.Outcomes[0].Action != "created" {
t.Errorf("an installed schedule was not reported created: %+v", report.Outcomes[0])
@@ -114,7 +131,7 @@ func TestInstallingAScheduleDoesNotRunItAndReportsCurrent(t *testing.T) {
t.Error("an installed schedule recorded no marker, so a re-apply cannot tell it is unchanged")
}
// And a re-apply of the same declaration is unchanged and still runs nothing.
// And a re-apply of the same declaration is unchanged and still fires nothing.
report2, _, err := Apply(context.Background(), archHost(t), d, state, store.OriginCarried, run, nil, nil)
if err != nil {
t.Fatal(err)
@@ -122,6 +139,59 @@ func TestInstallingAScheduleDoesNotRunItAndReportsCurrent(t *testing.T) {
if report2.Changed() {
t.Errorf("re-installing the same schedule reported a change: %+v", report2.Outcomes)
}
if fired {
t.Error("re-installing a schedule ran the container")
}
}
func TestInstallingAScheduleEnsuresItsImageIsPresentWithoutStartingIt(t *testing.T) {
// A scheduled step is never run at apply, so `docker run` — which is what pulls a service's or a
// run-once step's image — never fetches it. Without an explicit pull the image is absent from the
// node until the first scheduled fire, which then pays the whole pull latency and, until it runs,
// leaves tooling that expects the image present after apply looking at a node without it. So the
// apply ensures the image present: when it is absent it is pulled, and still nothing is run.
imagePresent := false
var pulledImage string
var fired bool
run := func(ctx context.Context, name string, args ...string) (string, error) {
switch args[0] {
case "info":
return "27.0\n", nil
case "image": // `image inspect <image>`
if imagePresent {
return "", nil
}
return "", errors.New("no such image")
case "pull":
pulledImage = args[len(args)-1]
imagePresent = true // a real runtime leaves the image present after a pull
return "", nil
case "run":
fired = true
return "", nil
}
return "", nil
}
d := parseTrusted(t, `{"declaration":1,"resources":[
{"id":"sync","type":"container","name":"sync","image":"`+pinned+`","schedule":"0 3 * * *"}
]}`)
report, state, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, run, nil, nil)
if err != nil {
t.Fatalf("installing a schedule whose image was absent failed the apply: %v", err)
}
if pulledImage != pinned {
t.Errorf("installing a schedule did not pull its pinned image (pulled %q, want %q)", pulledImage, pinned)
}
if fired {
t.Error("installing a schedule ran the container — ensuring the image is present is not running it")
}
if report.Outcomes[0].Action != "created" {
t.Errorf("an installed schedule was not reported created: %+v", report.Outcomes[0])
}
if _, ok := state.Find("sync"); !ok {
t.Fatal("an installed schedule was not recorded as applied")
}
}
func TestAScheduledStepDoesNotGateWhatFollows(t *testing.T) {