Add support for Apple's container CLI runtime - #577
Add support for Apple's container CLI runtime#577Victor Manuel Puga Ruiz (VictorPuga) wants to merge 12 commits into
container CLI runtime#577Conversation
Adds a container-client for Apple's `container` CLI (macOS 26+, Apple Silicon only), gated on isMac() && isArm64() in officialRuntimeRegistrations.ts and exposed via the containers.containerClient setting. Extends DockerClientBase but overrides most command builders, since the CLI's surface diverges from Docker's beyond what Finch/Podman needed: container-object verbs are top-level (`run`/`list`/`stop`/ `delete`, not `container run`-style), `list`/`image list` accept no `--filter` flag at all (filtering is done client-side instead), and `list`/`image list` JSON is a nested, non-Docker-shaped record, so it gets its own schema/normalizer files. `image pull` is pinned to `--arch arm64` since it otherwise fetches every platform in a multi-arch manifest by default. `events`/`restart` are rejected since the CLI has no equivalent subcommand. All behavior was verified against a real CLI 1.2.0 install rather than assumed from docs, including a stdout/stderr split test confirming progress output never pollutes stdout, so the base class's stdout-only output parsing for run/stop/remove needed no override. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
configuration.name is a sibling of configuration.descriptor in real `container image list --format json` output, not nested inside it. The schema had it nested, so zod/mini silently dropped the field instead of erroring, and every image parsed as <none>:<none> -- visible in the Images tree as a <none> parent with <none> children. Also exclude platform.architecture "unknown" variants from the size sum. Each real platform in a multi-arch pull is paired with a ~86KB attestation/provenance blob reported as its own "unknown/unknown" variant; summing those in inflated the reported image size. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
`image inspect` and `inspect` accept no --format flag (confirmed: "Unknown option '--format'"); JSON is their only output. `inspect` (containers) is also a bare verb like run/list/stop/delete, not `container inspect`. Both were left un-overridden and inherited DockerClientBase's --format-based defaults, breaking "Run Interactive" and the image/container hover tooltips entirely. Adds AppleContainerInspectImageRecord.ts and AppleContainerInspectContainerRecord.ts, following the same real-JSON-shape-first approach as the list records. Also fixes ListImagesItem.id/InspectImagesItem.id: `container` has no ID-based image addressing at all -- image inspect/rm/run reject a bare digest, a sha256:-prefixed digest, and even name@sha256:digest, only a name:tag reference resolves. id was set to the manifest digest, which this CLI can never look up, breaking every downstream call that reuses it (tooltip inspection, image-ancestor container filtering). id is now the name:tag reference itself, the only value actually usable as a CLI argument for this runtime. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds 34 unit tests covering every overridden command: checkInstall, version, info, pullImage, listImages, inspectImages, runContainer, listContainers, stopContainers, removeContainers, inspectContainers, and the unsupported getEventStream/restartContainers rejections. Fixtures are trimmed from real `container` CLI 1.2.0 output captured on Apple Silicon hardware (see apple-container-poc-plan.md), not hand-guessed shapes, and assert the specific quirks already found and fixed: configuration.name vs configuration.descriptor.name, the "unknown" attestation variant excluded from image size, id being the name:tag reference rather than the digest, bare verbs (run/list/stop/ delete/inspect) instead of container-prefixed ones, and --mount using target= instead of destination=. Full package suite (269 tests) and lint pass with this change. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
State-icon bug: getContainerStateIcon (extension-side) only recognizes Docker's state vocabulary (exited, dead, created, etc.). Apple's own "stopped" string was passed through unmapped and fell into that switch's default arm, which renders the *running* icon -- so every container looked like it needed "start" regardless of actual state. Confirmed container's vocabulary is just running/stopped, even for a created-but-never-started container (no separate "created" state). Maps "stopped" to Docker's "exited" so state-dependent UI reads correctly. Start-command bug: getStartContainersCommandArgs was never overridden, so it inherited "container container start <id>" -- wrong noun prefix, same class of bug already fixed for run/list/stop/delete/ inspect. Also discovered `container start` only accepts one container ID at a time (confirmed: a second ID errors and neither container starts, unlike stop/delete which accept multiple), so a multi-select "Start" request now throws CommandNotSupportedError explicitly rather than silently starting only the first container or none. Both verified against the real CLI and covered by 3 new unit tests (272 passing, lint clean). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
getExecContainerCommandArgs / getLogsForContainerCommandArgs were unoverridden and inherited container container exec / container container logs from DockerClientBase -- same wrong-noun-prefix bug already fixed for run/list/stop/start/delete/inspect. Real command is bare exec/logs. logs also needed a flag fix: container logs has no --tail (tailing is -n <count> instead), and no --timestamps/--since/--until support at all. Those now throw CommandNotSupportedError when explicitly requested rather than being silently dropped or erroring at the CLI layer. Both verified against the real CLI. 8 new unit tests (280 passing total across the package, lint clean). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
container CLI runtime
|
@microsoft-github-policy-service agree |
Fixes 3 issues flagged in PR microsoft#577 review: - ListImageRecord: a legitimate 0-byte size no longer collapses to undefined; only an empty (non-"unknown") variant list does. - InspectContainerRecord: network attachments with no name are dropped instead of surfacing as an empty-string network name. - InspectContainerRecord: imageId keeps its sha256: prefix so downstream slicing (ImageTreeItem, ContainerTreeItem, askCopilot) still works.
| * - No `events`, `restart`, or `info` subcommand exists. | ||
| * - `--version`/`-v` only accepts the long form; the short form errors. | ||
| */ | ||
| export class AppleContainerClient extends DockerClientBase implements IContainersClient { |
There was a problem hiding this comment.
It's on me to look into this (or Copilot lol), but we should see to what degree we can inherit from the base client. I imagine not a ton since Apple Containers seem to be substantially different from Docker/Podman/Nerdctl/Finch...more like WSLC.
…/login/ports/mounts Addresses the remaining review feedback on PR microsoft#577 (Apple container CLI runtime client), all re-verified against a real CLI 1.2.0 install: - Add full Volume and Network command support (list/inspect/create/remove/prune), with new Zod schemas built from real captured JSON -- these were entirely unimplemented before. - Fix prune: the base class emitted `container container prune` (duplicate noun) and unconditionally passed `--force`, which none of container/image/volume/ network prune actually support. Each of the four has a distinct real output format, now parsed correctly instead of guessed. - Fix login/logout: route through `registry login`/`registry logout`, the real command path, instead of a nonexistent top-level `login`/`logout`. - Wire up real ports/mounts in container list/inspect from configuration.publishedPorts/.mounts, including port-range expansion and volume-vs-bind mount detection (previously hardcoded empty). - Wire up real imageAncestors/volumes/networks list-filters using raw record fields confirmed to be present (previously left unfiltered). - Fix repoDigests to the repository@sha256:... form used elsewhere. - Drop the now-unnecessary --mount destination= override (confirmed the real CLI accepts it directly via the shared withDockerMountsArg helper). - Remove stray references to the AI-assisted planning doc from code comments. - Add an AppleContainerCanary integration test (23 assertions, gated on CONTAINER_CLIENT_TYPE=applecontainer) so a future CLI release that adds any of the capabilities worked around here gets caught automatically. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Buddy Maness (TwistedCrafts)
left a comment
There was a problem hiding this comment.
hope this is a fix.
|
Hi, Patrick Verbrugge (@patverb). The code review comments were addressed. |
| * - No `events`, `restart`, or `info` subcommand exists. | ||
| * - `--version`/`-v` only accepts the long form; the short form errors. | ||
| */ | ||
| export class AppleContainerClient extends DockerClientBase implements IContainersClient { |
There was a problem hiding this comment.
This client should be wired in to the Container Client E2E tests, and those should be run to ensure that coverage is maximized.
There was a problem hiding this comment.
Done
| } | ||
|
|
||
| // There is no `restart` subcommand. | ||
| public override restartContainers(options: RestartContainersCommandOptions): Promise<PromiseCommandResponse<Array<string>>> { |
There was a problem hiding this comment.
🤖 While you're overriding the inherited container-noun commands here, statsContainers needs the same treatment — it's currently broken as inherited. The base builds container stats (Docker's docker container stats), which for this client becomes container container stats and fails with Error: Plugin 'container-container' not found. On top of that, container stats has no --all flag at all — it shows all running containers by default and errors with Unknown option '--all'.
It needs a getStatsContainersCommandArgs override that returns a bare stats with no --all. Verified against CLI 1.2.2.
There was a problem hiding this comment.
Fixed
| // `image prune` accepts `--all` but not `--force` (confirmed: errors with "Unknown option | ||
| // '--force'"), unlike the base which always passes `--force`. Real output: a "Reclaimed X | ||
| // in disk space" summary line, then one `deleted <digest>` line per removed image. | ||
| protected override getPruneImagesCommandArgs(options: PruneImagesCommandOptions): CommandLineArgs { |
There was a problem hiding this comment.
🤖 buildImage is also broken as inherited. The base builds image build, but Apple's container has no image build subcommand — build is a top-level verb, so container image build ... errors (Unknown option '-t', and it just routes to the image help without building anything).
This needs a getBuildImageCommandArgs override that uses a bare build, and drops --iidfile and --disable-content-trust (neither exists on container build). Everything else it accepts maps over: --file/--target/--tag/--label/--platform/--build-arg/--pull plus the positional context dir. Verified against CLI 1.2.2 — a real container build --tag x . succeeds where container image build fails.
There was a problem hiding this comment.
Fixed
| strict: boolean, | ||
| ): Promise<Array<InspectNetworksItem>> { | ||
| return this.parseInspectJson(output, strict, (item) => | ||
| normalizeAppleContainerInspectNetworkRecord(AppleContainerListNetworkRecordSchema.parse(item), JSON.stringify(item))); |
There was a problem hiding this comment.
🤖 readFile and writeFile are also broken as inherited, and need overrides at the end of the class (a new File Commands region).
The base's readFile streams via cp CONTAINER:PATH -, and writeFile streams via cp - CONTAINER:DIR. But Apple's container cp supports neither stdin nor stdout - — a literal - is just treated as a local filename (verified: container cp c:/etc/hostname - writes a file named - in the cwd and emits nothing; the stdin write form silently doesn't write the piped content).
Suggested fix, mirroring what WslcClient does for reads:
getReadFileCommandArgs→exec <container> tar -cf - -C <dir> <file>, so the caller still receives the single-entry tarball stream it expects (requirestarin the image; Linux containers only).getWriteFileCommandArgs→ for the streamed case (noinputFile),exec -i <container> tar -xf - -C <path>to extract the incoming tar; when a hostinputFileis given, fall back to Apple's normal workingcp <inputFile> <container>:<path>.
Note this diverges from WslcClient.getWriteFileCommandArgs, which can still use cp - CONTAINER:DIR because wslc's cp does accept stdin -; Apple's does not, so the streamed write has to go through exec. All verified against CLI 1.2.2.
There was a problem hiding this comment.
Fixed
…s overrides Both commands were broken as inherited from DockerClientBase. buildImage built `image build`, but Apple's CLI has `build` as a top-level verb with no `image build` subcommand; it also drops --iidfile/--disable-content-trust, which don't exist on `container build`. statsContainers built `container stats`, which becomes `container container stats` here and fails to route; the real verb is bare `stats` with no --all flag at all. Addresses review feedback from bwateratmsft on PR microsoft#577. Verified against real CLI 1.2.0 output on Apple Silicon per the repo's AppleContainerClient verification convention. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Both were broken as inherited from DockerClientBase. `container cp` doesn't accept a stdin or stdout `-`: `container cp CONTAINER:PATH -` just writes a local file literally named `-` and streams nothing, and the equivalent stdin write silently drops the piped content. readFile now tars the target file inside the container via `exec ... tar -cf - -C <dir> <file>`, mirroring the same gap in WslcClient. writeFile extracts a streamed tar via `exec -i ... tar -xf -` when no host inputFile is given, and falls back to the base's plain `cp <file> CONTAINER:DIR` (which works fine for host-to-container copies) when one is. Addresses review feedback from bwateratmsft on PR microsoft#577. Verified against a real running container on Apple Silicon per the repo's AppleContainerClient verification convention. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…t E2E suite
Adds 'applecontainer' as a ClientType and instantiates AppleContainerClient in
ContainersClientE2E.test.ts, skipping paths the CLI genuinely lacks (restart,
events, --expose/--publish-all, and the whole orchestrator suite, since there
is no Compose equivalent).
Running the suite against real hardware surfaced three runtime differences
that only show up under live execution, not static review:
- Network names must be lowercase (confirmed: `container network create
testNetworkCamel` errors with "invalid network name"). The shared
`testContainerNetworkName` fixture had mixed case, breaking container setup
for every runtime that reached it; renamed to an all-lowercase name.
- `container build` always auto-tags with a random UUID even without --tag,
so it never produces a dangling image the way Docker's untagged builds do.
Bare `image prune` reclaims nothing as a result; PruneImagesCommand now
passes `{ all: true }` for this client so the test still proves prune works.
- The first `container run` in a session fetches a per-machine kernel + init
VM image (~20s cold, ~1s once cached), which blew the suite's 10s default
timeout. Bumped to 60s for the Containers `before` hook, Apple-only.
Verified: 38 passing / 8 pending against the real `container` CLI, and
re-ran the same suite against docker (44 passing / 2 pending) to confirm the
shared network-name fixture fix didn't regress other runtimes.
Addresses review feedback from bwateratmsft on PR microsoft#577.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Relates to #143.
Summary
Adds
AppleContainerClient, anIContainersClientimplementation for Apple'scontainerCLI (macOS 26+, Apple Silicon only). ExtendsDockerClientBasebut overrides nearly every command builder, since the CLI's surface (bare verbs likerun/list/stop, no--filter, non-Docker JSON shapes) isn't Docker-compatible enough to inherit as-is. Registered behindisMac() && isArm64(), no compose counterpart (none exists), same aswslc.All behavior below was verified against a real CLI 1.2.0 install, not inferred from docs.
Supported
checkInstall,version,info(synthesized),run,list,start,stop,delete,exec,logs,inspectContainers,stats,build,prune(container/image/volume/network),login/logout,readFile/writeFile(viaexec+tar, sincecphas no stdin/stdout support),inspectImages,image pull(pinned to--arch arm64),image list, full volume and network support (list/inspect/create/remove/prune).Notable gotchas handled
name:tagresolves, soListImagesItem.idis the reference, not the digest.running/stopped; mapped to Docker'srunning/exitedso tree icons and start/stop visibility read correctly.container startonly accepts one ID at a time — multi-select throws instead of silently starting one.logshas no--timestamps/--since/--until, tails via-nnot--tail.cpdoesn't support-for stdin/stdout, soreadFile/writeFilego throughexec ... tarinstead (mirrors the same gap inWslcClient).buildandstatsare top-level verbs (build,stats), notimage build/container stats; the latter would double the noun prefix and fail to route.pruneneeded per-resource handling: the base class'scontainer container prunedouble-noun bug, plus an unconditional--forcethat none of container/image/volume/network prune actually accept.login/logoutroute throughregistry login/registry logout, not a nonexistent top-levellogin/logout.events/restartdon't exist and are rejected explicitly.Out of scope
containers.commands.*in package.json) that bypassesIContainersCliententirely, with no per-runtime override. Confirmed broken for this runtime; workaround is a customcontainers.commands.logssetting. Real fix is a separate, bigger change.wslc).Testing
330 unit tests passing (full package suite), lint clean. E2E suite (
ContainersClientE2E.test.ts) now wired up for this client: 38 passing / 8 pending against the realcontainerCLI on Apple Silicon, and the same suite re-run against Docker (44 passing / 2 pending) to confirm the shared fixture changes didn't regress other runtimes. Manually verified end-to-end on real hardware (pull/run/list/stop/start/inspect/exec/logs/remove/build/volumes/networks) plus the Images/Containers tree views and tooltips in the running extension.