Skip to content

feat(pod): select the engine through the sdk resolvers and stage images on W3.7 (#41) - #42

Merged
github-actions[bot] merged 1 commit into
mainfrom
feat/pod-resolver-migration
Oct 23, 2026
Merged

github-actions[bot] merged 1 commit into
mainfrom
feat/pod-resolver-migration

Conversation

@aitrawog

Copy link
Copy Markdown
Contributor

Summary

Migrates plugin-deploy-pod — the largest consumer left — onto the sdk's ONE selection model and the W3.7 image store. Every site that read rt.RunEngine, re-derived "deploy entry else runtime" by hand, or carried an engine fallback now calls deploykit.ResolvePodEngine + deploykit.ResolveContainerdTarget; image staging probes deploykit.NewImageStore instead of container.LocalImageExists.

Pins: github.com/opencharly/spec v0.2026296.1036, github.com/opencharly/sdk v0.2026296.1207 (W3.1 resolvers + W3.7 ImageStore/ResolveImage).

Behaviour this changes (each pinned by a test that fails on the pre-change tree):

  1. A retired/unknown engine word is now a hard error. Before, the deploy entry's word was returned verbatim and kit.EngineBinary fed it to container.EngineCapabilityFor, which resolves ANY unknown word to the default engine — a nerdctl deploy ran podman silently.
  2. A per-deploy namespace: now reaches the engine argv/unit/store. Before, engineGlobalArgs emitted the capability's default namespace only.
  3. A per-deploy rootful: now reaches the target (and the refusal predicate reads it). Before, the predicate read rt.Rootful only.
  4. Staging reads the RESOLVED engine's store and surfaces daemon failures; absent is concluded only when the daemon answered.

Not in scope (unchanged): the W3.6 overlay build engine (overlay.go:51, #36) and the W3.8 systemd-container init work.

Per-site migration (old → new)

Site (pre-change origin/main a2d8546) Old New
resolve.go:47,212 rootfulRefusal(rt.RunEngine, rt.RunMode), rootfulUnsupported(rt) rootfulRefusal(sel.Engine, …), rootfulUnsupported(sel.Target, rt.RunMode)
resolve.go:60,95,313 boxEngineForDeploy(…, rt.RunEngine) resolvePodSelectionFromConfig(dc, box, instance, rt)
resolve.go:158-161 detectDevices(…, rt.RunEngine); engine := rt.RunEngine; meta.Engine override detectDevices(…, string(sel.Engine)); sel.Engine (override deleted)
resolve.go:178,181 ensureImagePresent(…, rt.RunEngine), ExtractMetadata(rt.RunEngine, …) ensureImagePresent(…, sel.Target), ExtractMetadata(string(sel.Engine), …)
resolve.go:293 podRootful(rt) sel.Target.Rootful
config_setup.go:98,191,197,258,340,345,350,382,553 rt.RunEngine at each sel.Engine / sel.Target (resolved once per config run)
config_setup_helpers.go:524,526 runConfigDirect rootful/engine from rt from sel
config_setup_helpers.go:586,600 dataEngine from the entry by hand; ensureImagePresent(rt.RunEngine) resolvePodSelectionFromConfig; sel.Target
config_setup_helpers.go:704,708,724,762,789 updateAllDeployedQuadlets used the global rt.RunEngine per-entry resolvePodSelectionFromConfig
config_remove.go:40,42,44 engineStopArgv(rt.RunEngine, …), engineRemoveArgv(rt.RunEngine, …) resolved sel.Target
enc_tunnel_resolve.go:96 engine := rt.RunEngine resolved sel.Engine
engine_ops.go:52 engineRunArgv(engine, rootful, …) engineRunArgv(target, …) (Rootful + globals from the target)
engine_ops.go:142-151 capability default address+namespace target.Address / target.Namespace
engine_ops.go:168-176 stop/rm from the capability row from the resolved target
engine_ops.go:216,231 podRootful(rt), rootfulUnsupported(rt) target fields (both helpers' rt-only forms deleted)
image_ensure.go:36-64 container.LocalImageExists(engine, ref) + resolveRunEngine fallback NewImageStore(ex, target) + ResolveImage step 1 + build:ensure + re-probe
lifecycle.go:334 ExtractMetadata(rt.RunEngine, …) resolved selection
resolve_f12.go:154,285,304 boxEngineForDeploy(…, rt.RunEngine) resolved selection; shell/exec/logs argv carry the target's global flags
resolve_ref.go:44 currentRunEngine() resolved selection
volume_root.go:94 engine word from the caller caller passes sel.Engine

Deleted in the same cut: boxEngineForDeploy, currentRunEngine, resolveRunEngine, podRootful, the meta.Engine override, and image_ensure_test.go (the test of the retired probe).

How tested

Gate derived from .github/workflows/ (this repo ships only tag-on-merge; the merge gate is the org-wide charly/pr-validator, and the repo's AGENTS build/validate/test surface is the local gate). Every command's OWN exit code, on the committed tree:

# head 80b9f28fb07a0c55806d862a2ba6cd79f425c96d (git rev-parse HEAD)
$ git status --short
(empty)
$ go build ./...                       # rc=0
$ go vet ./...                         # rc=0
$ go test -count=1 ./...               # rc=0
ok  github.com/opencharly/plugin-deploy-pod/candy/plugin-deploy-pod  1.027s
$ charly box validate                  # rc=0
charly box validate: OK — checked 1 candy, 3 deploys, 7 distros, 7 builders; 0 warnings, 0 errors
$ go mod tidy                          # rc=0, go.mod/go.sum byte-identical (no-op)

Pin bump verified against the producers: sdk v0.2026296.1207 requires spec v0.2026296.1036 (its own go.mod), and the downstream module builds, vets and tests at both tags — no assumption that a bumped producer compiles.

Failing-without controls

The new tests were copied onto a throwaway worktree of the PRE-CHANGE origin/main (a2d8546) and run there:

$ go test -run 'TestResolvePodSelection|TestDeployNamespace|TestDeployRootful|TestEngineRunArgv_UsesResolvedTarget|TestEnsureImagePresent' ./...
./selection_test.go:23:54: undefined: deploykit.Selection
./selection_test.go:26:14: undefined: resolvePodSelection
./selection_test.go:34:59: undefined: deploykit.ContainerdTarget
FAIL  github.com/opencharly/plugin-deploy-pod/candy/plugin-deploy-pod [build failed]

The retired behaviour itself was measured on that same pre-change tree (one-off control, not shipped):

pre-change engineGlobalArgs(containerd) = "--address /run/containerd/containerd.sock --namespace default"
pre-change: retired word nerdctl -> "podman"; deploy-level rootful is not an input at all

Each migrated site's arm: TestResolvePodSelection_RejectsRetiredEngineWord (site 1), TestDeployNamespaceReachesEngineArgv + TestEngineRunArgv_UsesResolvedTarget (site 2 and the argv builders), TestDeployRootfulReachesTarget (site 3), TestEnsureImagePresent_ProbesResolvedStore (the store boundary, including the post-build:ensure re-probe).

Live result — visible skip (mechanism named)

The migrated plugin was built from this head (cmd/serve) and served as a baked provider (CHARLY_PLUGIN_DIR + plugin-deploy-pod.providers = deploy:pod) into an isolated HOME/XDG_*, so the changed runtime path really ran:

$ charly config localhost/check-crabbox-cli:2026.296.1116   # isolated XDG, quadlet mode
plugin plugin-deploy-pod: served from baked binary /tmp/dp-plugins/plugin-deploy-pod (build sha256:9db261…)
charly: error: deploy:pod config: … image not found in local storage: check-crabbox-cli:2026.296.1116:
  ensure-image "check-crabbox-cli:2026.296.1116": not present locally, pull failed, and no buildable
  short-name match in charly.yml

That failure is the NEW store probe correctly reporting absence (the local image is localhost/check-crabbox-cli:…, and the short ref is a different storage key); the resolver + NewImageStore.Has + build:ensure path executed live.

A full pod deploy on a disposable bed was NOT achieved, for two mechanisms measured on this host, not faked:

  1. The dev charly binary (source and binary both 2026.293.1809) resolves deploy add's substrate from the compiled-in/project-scan path only: target "pod" is a known substrate but its deploy provider is not connected (… not compiled-in or failed to load) — even with the same baked deploy:pod binary the config path connects. This is the deploy-add provider-resolution path, not this plugin's code.
  2. xt_multiport is absent from this kernel (modinfo xt_multiport → ERROR: Module xt_multiport not found; no /sys/module/xt_multiport), so a containerd lease cannot acquire here.

The R10 pod beds (check-pod-overlay, check-sidecar-pod) were therefore not run; the live evidence above is the changed config path. The tier says so.

Rulebook compliance

  • R1: no failure was filed away. The two live blockers are named above with their measured mechanism; neither is a defect in this diff. No new issue was filed — the W3.6 residue already has #36.
  • R2: one selection implementation (selection.go); the per-call-site lookups, the unset-engine fallback and the rt-only rootful predicate are deleted, not kept as siblings.
  • R3: no sleep, retry, magic number or ad-hoc container command; the store drives the engine through the capability row.
  • R4: no documented command changed; no prose routed around a defect.
  • R5: claim-keyed grep pasted below; the one remaining engine literal is the W3.6 overlay builder already owned by #36.
  • R6: edit happened in a session worktree off fresh origin/main; no submodule tracked checkout was modified; no force-push.
  • R7: the new tests fail on the pre-change tree (pasted above); the store boundary is asserted after build:ensure, not just at call time.
  • R8: no service was mocked; the live engine path ran against the real podman store (and reported its real absence).
  • R9: the plugin binary was built from this tree; charly box validate ran on the committed tree with the dev binary named above.
  • R10: the change-class gate is the surface above; the pod beds are a visible skip with the two mechanisms named.
  • SDD: no schema field was needed — the migration consumes existing spec fields (Deploy.Engine/Namespace/Rootful, containerd.namespace); no CUE edit, so no cue-gen run was owed.
$ git grep -nE 'boxEngineForDeploy|currentRunEngine|resolveRunEngine|podRootful' -- . ':!CHANGELOG'
# rc=1 (no hits)
$ git grep -n 'rt.RunEngine' -- candy/plugin-deploy-pod ':!*_test.go'
candy/plugin-deploy-pod/selection.go:14:// rt.RunEngine, re-derives the deploy-entry-over-runtime precedence by hand, or carries an engine
candy/plugin-deploy-pod/selection.go:20:// rt.RunEngine / rt.Namespace (hostenv.ResolveRuntime owns it). `auto` is a selector the resolver
# comments describing the resolver's INPUT, not reads
$ git grep -nE '"(podman|docker|containerd|nerdctl)"' -- candy/plugin-deploy-pod ':!*_test.go'
candy/plugin-deploy-pod/overlay.go:48:// "podman" — charly/runOverlayBuild never threaded the deploy node's engine onto the overlay
candy/plugin-deploy-pod/overlay.go:51:const engineBin = "podman"
# the W3.6 overlay builder, filed as opencharly/plugin-deploy-pod#36

Change classification

Runtime consumer migration in the out-of-tree deploy:pod plugin: Go source + tests + go.mod/go.sum pins. No schema, no CLI surface, no image definition.

Filed issues

None filed by this PR. The one out-of-scope residue (W3.6 overlay build engine) is the pre-existing opencharly/plugin-deploy-pod#36.

Closes #41

Agent: containerd-crabbox-program/w31d · session 8b1c01cf-7f79-4a75-bff8-240415a06208
Assisted-by: Claude Code deepseek-v4.1-flash:cloud (analysed on a live system)

…es on W3.7

Every site in the plugin that read the run engine directly now consumes the ONE
sdk selection model, and image staging moves onto the W3.7 image store:

- ONE resolvePodSelection (selection.go) over deploykit.ResolvePodEngine +
  ResolveContainerdTarget; the hand-rolled deploy-entry-else-runtime lookup,
  the unset-engine fallback and the rt-only rootful predicate are deleted.
- The resolved target carries the namespace and rootful mode, so a per-deploy
  namespace: / rootful: now reaches the engine argv, the unit and the store
  (the capability default was the only namespace source before).
- A retired/unknown engine word is rejected instead of silently resolving to
  the default engine (the old lookup fed the capability table's fallback).
- ensureImagePresent probes deploykit.NewImageStore built from the resolved
  target, then runs build:ensure (W3.6) and re-probes the same store. The
  engine-specific container.LocalImageExists probe is gone; a daemon failure
  is no longer read as absent.
- Pins: spec v0.2026296.1036 + sdk v0.2026296.1207 (build, vet, test and
  charly box validate pass on the bump; go mod tidy is a no-op).

Not in scope: the overlay build engine (#36, W3.6)
and the systemd-container init work (W3.8).

Refs #41

Assisted-by: Claude Code deepseek-v4.1-flash:cloud (analysed on a live system)
@aitrawog

Copy link
Copy Markdown
Contributor Author

STATUS — the plugin-deploy-pod W3.1/W3.7 consumer leg is in review.

  • Issue: opencharly/plugin-deploy-pod#41
  • PR: opencharly/plugin-deploy-pod#42
  • Head: 80b9f28fb07a0c55806d862a2ba6cd79f425c96d (tree 1a80f89b11389ea28ecbd165146caafa48e41f9c)
  • Pins: spec v0.2026296.1036 + sdk v0.2026296.1207

Done: every engine read in the plugin is on ResolvePodEngine + ResolveContainerdTarget; image staging on NewImageStore + ResolveImage; the hand-rolled lookup, the unset-engine fallback and the rt-only rootful predicate are deleted.

Gate on the committed tree: git status empty; go build rc=0; go vet rc=0; go test -count=1 ./... rc=0; charly box validate rc=0 (0 warnings); go mod tidy no-op.

Live: the changed config path ran live through the baked plugin (isolated XDG) and reached the new store probe; the full pod bed is a visible skip — the dev binary's deploy add path does not connect a baked deploy:pod provider, and this kernel has no xt_multiport for a containerd lease. Both mechanisms are named in the PR body; the tier is analysed on a live system.

Next: read the fresh charly/pr-validator verdict in full; fix or rebut every finding. No agent merges.

Agent: containerd-crabbox-program/w31d · session 8b1c01cf-7f79-4a75-bff8-240415a06208
Assisted-by: Claude Code deepseek-v4.1-flash:cloud (analysed on a live system)

@github-actions

Copy link
Copy Markdown

Review — PASS

I re-derived the PR against the complete current state (21 files, head 80b9f28…, no comments).

Correctness vs. body claims. The per-site migration table matches the diff:

  • rt.RunEngine reads are replaced by sel.Engine/sel.Target in config_setup.go, config_setup_helpers.go, config_remove.go, enc_tunnel_resolve.go, resolve.go, resolve_f12.go, resolve_ref.go, volume_root.go.
  • boxEngineForDeploy, currentRunEngine, resolveRunEngine, podRootful, and the meta.Engine override are deleted with no dangling references; image_ensure_test.go is removed and superseded by selection_test.go.
  • The argv/unit/store path now threads a single podSelection (selection.go) through engineRunArgv, engineStopArgv, engineRemoveArgv, engineGlobalArgs, buildStartArgs, buildShellArgs, buildExecArgs, directRunArgs, writeSystemdUnitConfig. Signature changes are propagated to every caller and test I can see (hostname_parity_test.go, volume_root_test.go, resolve_f12_test.go, engine_ops_test.go).
  • The three behavioral claims (retired-word hard error, deploy namespace: reaching argv, deploy rootful: reaching the target/refusal) are each exercised by selection_test.go and consistent with the deploykit.ResolvePodEngine/ResolveContainerdTarget usage.

Tests. TestResolvePodSelection_RejectsRetiredEngineWord, TestDeployNamespaceReachesEngineArgv, TestDeployRootfulReachesTarget, TestEngineRunArgv_UsesResolvedTarget, and TestEnsureImagePresent_ProbesResolvedStore (including the post-build:ensure re-probe) are present and pin the new behavior. The failing-without control is a build failure on the pre-change tree (undefined deploykit.Selection/resolvePodSelection), which is a weak but valid negative control; the retired-behavior measurements are additionally reported.

Security. No secrets introduced. The reverseDeployExecutor shim exposes only RunCapture and fails loudly on all other methods. Presence-probe scripts are built by deploykit and namespace values are shell-quoted (asserted in the test), so no new injection surface.

Honesty / scope. File count (21) and head SHA match. The rt.RunEngine grep shows only doc comments in selection.go; the remaining engine literal is the out-of-scope W3.6 overlay builder already tracked by #36. No schema/CLI change, consistent with "no CUE edit owed."

Non-blocking observations. deploySelection calls dc.Lookup without an explicit dc != nil guard while sibling sites guard dc before other deploykit calls; this is safe only if deploykit.DeployConfig.Lookup is nil-receiver-safe. The TestRunPodConfigRemove_DirectModeAllowed arm (nil executor) exercises this path and the reported green test run is consistent with a nil-safe Lookup, so I treat it as acceptable rather than a defect. Ordering of the rootful refusal relative to loadDeploy in resolvePodStartSystemdUnit also shifts slightly (a bad engine word now surfaces before the rootful message), but this is cosmetic.

Comments. The comment thread is empty; nothing to disposition.

Verdict: PASS


ollama-cloud/deepseek-v4.1-flash — action-review.

View action run

@github-actions
github-actions Bot enabled auto-merge (squash) October 23, 2026 12:51
@github-actions
github-actions Bot merged commit 9b015b1 into main Oct 23, 2026
3 checks passed
charly-auto-merge Bot added a commit that referenced this pull request Oct 23, 2026
@aitrawog

Copy link
Copy Markdown
Contributor Author

RESOLVED — the plugin-deploy-pod W3.1/W3.7 consumer leg has landed.

  • Issue: opencharly/plugin-deploy-pod#41 (closed explicitly; Closes #41 did not auto-close)
  • PR: opencharly/plugin-deploy-pod#42 (validator PASS on head 80b9f28fb07a0c55806d862a2ba6cd79f425c96d)
  • Merge: 9b015b1c47135555830f04ddb690aac2082f7788 (squash), 2026-10-23T12:51:49Z
  • Tag: v2026.296.1251 at the merge commit; CHANGELOG 2026.296.1251.md (1f322ea)
  • Pins: spec v0.2026296.1036 + sdk v0.2026296.1207

Settled: every engine read in the plugin is on ResolvePodEngine + ResolveContainerdTarget; image staging on NewImageStore + ResolveImage; the hand-rolled deploy-entry-else-runtime lookup, the unset-engine fallback and the rt-only rootful predicate are deleted; a per-deploy namespace:/rootful: now reaches the argv/unit/store; a retired engine word is a hard error.

Validator observations, dispositioned: deploykit.DeployConfig.Lookup IS nil-receiver-safe (spec/deploy_config.go:46), so the unguarded call is correct; the rootful-refusal ordering shift is cosmetic and accepted.

Gate on the committed tree: git status empty; build/vet/test rc=0; charly box validate rc=0 with 0 warnings; go mod tidy no-op. Live: the changed config path ran live through the baked plugin; the full pod bed is a visible skip with both mechanisms named in the PR body (dev binary does not connect a baked deploy:pod for deploy add; kernel lacks xt_multiport). Tier: analysed on a live system.

Not in scope, unchanged: W3.6 overlay build engine (#36), W3.8 systemd-container init.

Agent: containerd-crabbox-program/w31d · session 8b1c01cf-7f79-4a75-bff8-240415a06208
Assisted-by: Claude Code deepseek-v4.1-flash:cloud (analysed on a live system)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(pod): migrate engine selection onto sdk/deploykit resolvers and the W3.7 image store

2 participants