-
Notifications
You must be signed in to change notification settings - Fork 84
Add support for Apple's container CLI runtime
#577
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 7 commits
43e3c46
c66052f
b5d9602
62b24e3
60f9d81
c4cb4c8
471ff4d
1b5d5fb
2b5847c
d24acf0
c35ed70
b0b689c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
|
bwateratmsft marked this conversation as resolved.
|
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| /*--------------------------------------------------------------------------------------------- | ||
| * Copyright (c) Microsoft Corporation. All rights reserved. | ||
| * Licensed under the MIT License. See LICENSE in the project root for license information. | ||
| *--------------------------------------------------------------------------------------------*/ | ||
|
|
||
| import * as z from 'zod/mini'; | ||
| import type { InspectContainersItem, InspectContainersItemNetwork } from '../../contracts/ContainerClient'; | ||
| import { dateStringWithFallbackSchema } from '../../contracts/ZodTransforms'; | ||
| import { parseDockerLikeImageName } from '../../utils/parseDockerLikeImageName'; | ||
| import { parseDockerLikeEnvironmentVariables } from '../DockerClientBase/parseDockerLikeEnvironmentVariables'; | ||
|
|
||
| const AppleContainerStatusNetworkSchema = z.object({ | ||
| network: z.optional(z.string()), | ||
| ipv4Address: z.optional(z.string()), | ||
| ipv4Gateway: z.optional(z.string()), | ||
| macAddress: z.optional(z.string()), | ||
| }); | ||
|
|
||
| /** | ||
| * `container inspect <id>` emits the same nested shape as `container list` (see | ||
| * `AppleContainerListContainerRecord.ts`), with the full `initProcess` and richer | ||
| * `status.networks` entries added. No `--format` flag exists for this command -- confirmed: | ||
| * `container inspect --format json <id>` errors with "Unknown option '--format'"; JSON is the | ||
| * only output this command produces. The verb is also bare `inspect`, not `container inspect`. | ||
| */ | ||
| export const AppleContainerInspectContainerRecordSchema = z.object({ | ||
| id: z.string(), | ||
| configuration: z.object({ | ||
| creationDate: z.optional(dateStringWithFallbackSchema), | ||
| image: z.object({ | ||
| descriptor: z.optional(z.object({ | ||
| digest: z.optional(z.string()), | ||
| })), | ||
| reference: z.optional(z.string()), | ||
| }), | ||
| initProcess: z.optional(z.object({ | ||
| executable: z.optional(z.string()), | ||
| arguments: z.optional(z.array(z.string())), | ||
| environment: z.optional(z.array(z.string())), | ||
| workingDirectory: z.optional(z.string()), | ||
| })), | ||
| labels: z.optional(z.record(z.string(), z.string())), | ||
| }), | ||
| status: z.object({ | ||
| startedDate: z.optional(dateStringWithFallbackSchema), | ||
| networks: z.optional(z.array(AppleContainerStatusNetworkSchema)), | ||
| }), | ||
| }); | ||
|
|
||
| export type AppleContainerInspectContainerRecord = z.infer<typeof AppleContainerInspectContainerRecordSchema>; | ||
|
|
||
| /** | ||
| * Normalize a parsed {@link AppleContainerInspectContainerRecord} to the common | ||
| * {@link InspectContainersItem}. | ||
| */ | ||
| export function normalizeAppleContainerInspectContainerRecord(container: AppleContainerInspectContainerRecord, raw: string): InspectContainersItem { | ||
| const initProcess = container.configuration.initProcess; | ||
| const networks: InspectContainersItemNetwork[] = (container.status.networks ?? []) | ||
| .filter((network): network is typeof network & { network: string } => !!network.network) | ||
| .map((network) => ({ | ||
| name: network.network, | ||
| gateway: network.ipv4Gateway, | ||
| ipAddress: network.ipv4Address, | ||
| macAddress: network.macAddress, | ||
| })); | ||
|
|
||
| return { | ||
| id: container.id, | ||
| // The `container` CLI has no name distinct from the container's ID; see the same note | ||
| // in AppleContainerListContainerRecord.ts. | ||
| name: container.id, | ||
| // Kept in the same `sha256:<digest>` form as SharedInspectContainerRecord -- consumers | ||
| // (ImageTreeItem, ContainerTreeItem, askCopilot) slice this assuming that prefix. | ||
| imageId: container.configuration.image.descriptor?.digest ?? '', | ||
| image: parseDockerLikeImageName(container.configuration.image.reference), | ||
| isolation: undefined, | ||
| status: undefined, | ||
| environmentVariables: parseDockerLikeEnvironmentVariables(initProcess?.environment ?? []), | ||
| networks, | ||
| ipAddress: networks[0]?.ipAddress, | ||
| operatingSystem: 'linux', | ||
| // `configuration.publishedPorts`/`.mounts` shapes haven't been captured against real | ||
|
bwateratmsft marked this conversation as resolved.
Outdated
|
||
| // `--publish`/`--mount` runs yet; left empty rather than guessing field names. | ||
| ports: [], | ||
| mounts: [], | ||
| labels: container.configuration.labels ?? {}, | ||
| // Apple Container has no separate entrypoint/cmd split in inspect output -- only the | ||
| // fully resolved init process (executable + arguments) is reported. | ||
| entrypoint: [], | ||
| command: initProcess?.executable ? [initProcess.executable, ...(initProcess.arguments ?? [])] : [], | ||
| currentDirectory: initProcess?.workingDirectory, | ||
| createdAt: container.configuration.creationDate ?? new Date(0), | ||
| startedAt: container.status.startedDate, | ||
| finishedAt: undefined, | ||
| raw, | ||
| }; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| /*--------------------------------------------------------------------------------------------- | ||
| * Copyright (c) Microsoft Corporation. All rights reserved. | ||
| * Licensed under the MIT License. See LICENSE in the project root for license information. | ||
| *--------------------------------------------------------------------------------------------*/ | ||
|
|
||
| import * as z from 'zod/mini'; | ||
| import type { InspectImagesItem } from '../../contracts/ContainerClient'; | ||
| import { architectureStringSchema, dateStringWithFallbackSchema, osTypeStringSchema } from '../../contracts/ZodTransforms'; | ||
| import { parseDockerLikeImageName } from '../../utils/parseDockerLikeImageName'; | ||
| import { parseDockerLikeEnvironmentVariables } from '../DockerClientBase/parseDockerLikeEnvironmentVariables'; | ||
|
|
||
| const AppleContainerImageVariantConfigSchema = z.object({ | ||
| Cmd: z.optional(z.array(z.string())), | ||
| Entrypoint: z.optional(z.array(z.string())), | ||
| Env: z.optional(z.array(z.string())), | ||
| WorkingDir: z.optional(z.string()), | ||
| Labels: z.optional(z.record(z.string(), z.string())), | ||
| User: z.optional(z.string()), | ||
| }); | ||
|
|
||
| const AppleContainerInspectImageVariantSchema = z.object({ | ||
| platform: z.optional(z.object({ | ||
| architecture: z.optional(z.string()), | ||
| os: z.optional(z.string()), | ||
| })), | ||
| config: z.optional(z.object({ | ||
| config: z.optional(AppleContainerImageVariantConfigSchema), | ||
| })), | ||
| }); | ||
|
|
||
| /** | ||
| * `container image inspect <ref>` emits the same manifest-list-oriented shape as `image list` | ||
| * (see `AppleContainerListImageRecord.ts`), just with the full OCI image config nested under | ||
| * `variants[].config.config` instead of only `variants[].size`. No `--format` flag exists for | ||
| * this command -- confirmed: `container image inspect --format json <ref>` errors with | ||
| * "Unknown option '--format'"; JSON is the only output this command produces. | ||
| */ | ||
| export const AppleContainerInspectImageRecordSchema = z.object({ | ||
| id: z.string(), | ||
| configuration: z.object({ | ||
| creationDate: z.optional(dateStringWithFallbackSchema), | ||
| descriptor: z.optional(z.object({ | ||
| digest: z.optional(z.string()), | ||
| })), | ||
| name: z.optional(z.string()), | ||
| }), | ||
| variants: z.optional(z.array(AppleContainerInspectImageVariantSchema)), | ||
| }); | ||
|
|
||
| export type AppleContainerInspectImageRecord = z.infer<typeof AppleContainerInspectImageRecordSchema>; | ||
|
|
||
| /** | ||
| * A multi-platform image reports one variant per platform, plus an unrelated | ||
| * `platform.architecture: "unknown"` attestation blob per real platform (see | ||
| * `AppleContainerListImageRecord.ts`). Prefer the arm64/linux variant, since this client only | ||
| * ever runs on Apple Silicon; fall back to the first non-attestation variant otherwise. | ||
| */ | ||
| function selectPrimaryVariant(variants: AppleContainerInspectImageRecord['variants']) { | ||
| const usable = (variants ?? []).filter((variant) => variant.platform?.architecture !== 'unknown'); | ||
| return usable.find((variant) => variant.platform?.architecture === 'arm64' && variant.platform?.os === 'linux') ?? usable[0]; | ||
| } | ||
|
|
||
| /** | ||
| * Normalize a parsed {@link AppleContainerInspectImageRecord} to the common | ||
| * {@link InspectImagesItem}. | ||
| */ | ||
| export function normalizeAppleContainerInspectImageRecord(image: AppleContainerInspectImageRecord, raw: string): InspectImagesItem { | ||
| const variant = selectPrimaryVariant(image.variants); | ||
| const config = variant?.config?.config; | ||
|
|
||
| return { | ||
| // `container` has no ID-based image addressing (see the note in | ||
| // AppleContainerListImageRecord.ts); mirror that file's `id` choice so this stays a | ||
| // usable CLI reference rather than an inert digest. | ||
| id: image.configuration.name ?? image.id, | ||
| image: parseDockerLikeImageName(image.configuration.name), | ||
| repoDigests: image.configuration.descriptor?.digest ? [image.configuration.descriptor.digest] : [], | ||
|
bwateratmsft marked this conversation as resolved.
Outdated
|
||
| // `image inspect` doesn't distinguish local-only images from ones pulled from a | ||
| // registry; every inspectable image is on-disk, so this is always true. | ||
| isLocalImage: true, | ||
| environmentVariables: parseDockerLikeEnvironmentVariables(config?.Env ?? []), | ||
| // No ExposedPorts-equivalent field observed in the OCI image config `image inspect` | ||
| // emits. | ||
| ports: [], | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as other file. these are there under variations[].config.config.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Per Claude:
|
||
| // No Volumes-equivalent field observed. | ||
| volumes: [], | ||
| labels: config?.Labels ?? {}, | ||
| entrypoint: config?.Entrypoint ?? [], | ||
| command: config?.Cmd ?? [], | ||
| currentDirectory: config?.WorkingDir, | ||
| architecture: architectureStringSchema.parse(variant?.platform?.architecture ?? ''), | ||
| operatingSystem: osTypeStringSchema.parse(variant?.platform?.os ?? ''), | ||
| createdAt: image.configuration.creationDate, | ||
| user: config?.User, | ||
| raw, | ||
| }; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| /*--------------------------------------------------------------------------------------------- | ||
| * Copyright (c) Microsoft Corporation. All rights reserved. | ||
| * Licensed under the MIT License. See LICENSE in the project root for license information. | ||
| *--------------------------------------------------------------------------------------------*/ | ||
|
|
||
| import * as z from 'zod/mini'; | ||
| import type { ListContainersItem } from '../../contracts/ContainerClient'; | ||
| import { dateStringWithFallbackSchema } from '../../contracts/ZodTransforms'; | ||
| import { parseDockerLikeImageName } from '../../utils/parseDockerLikeImageName'; | ||
|
|
||
| const AppleContainerNetworkAttachmentSchema = z.object({ | ||
| network: z.optional(z.string()), | ||
| }); | ||
|
|
||
| /** | ||
| * `container list --format json` emits a nested, non-Docker-like shape (captured against | ||
| * real CLI 1.2.0 output): `{configuration: {id, image: {reference}, labels, networks, ...}, | ||
| * id, status: {state, networks, startedDate}}`. There is no flat `Names`/`Image`/`Ports` | ||
| * record to reuse from `SharedListContainerRecordSchema`, so this keeps its own module. | ||
| */ | ||
| export const AppleContainerListContainerRecordSchema = z.object({ | ||
| id: z.string(), | ||
| configuration: z.object({ | ||
| creationDate: z.optional(dateStringWithFallbackSchema), | ||
| image: z.object({ | ||
| reference: z.optional(z.string()), | ||
| }), | ||
| labels: z.optional(z.record(z.string(), z.string())), | ||
| networks: z.optional(z.array(AppleContainerNetworkAttachmentSchema)), | ||
| }), | ||
| status: z.object({ | ||
| state: z.optional(z.string()), | ||
| }), | ||
| }); | ||
|
|
||
| export type AppleContainerListContainerRecord = z.infer<typeof AppleContainerListContainerRecordSchema>; | ||
|
|
||
| /** | ||
| * `container` only ever reports `status.state` as `"running"` or `"stopped"` -- confirmed for | ||
| * a running container, a container stopped after running, and a `container create`d-but-never- | ||
| * started container (all three produce one of those two strings; there is no separate | ||
| * "created" state, matching the CLI having no `pause`/`unpause` and hence no "paused" state | ||
| * either). The rest of the extension keys off Docker's vocabulary instead (see | ||
| * `getContainerStateIcon` in `ContainerProperties.ts`, whose switch has no `"stopped"` case) -- | ||
| * passing `"stopped"` through unmapped landed in that switch's `default:` arm, which is the | ||
| * *running*-icon case, so a stopped container rendered with the running/start icon. Map onto | ||
| * Docker's `"exited"` instead so state-dependent UI (icons, context-menu start/stop visibility) | ||
| * reads correctly. | ||
| */ | ||
| function mapAppleContainerState(state: string | undefined): string { | ||
| return state === 'running' ? 'running' : 'exited'; | ||
| } | ||
|
|
||
| /** | ||
| * Normalize a parsed {@link AppleContainerListContainerRecord} to the common | ||
| * {@link ListContainersItem}. | ||
| */ | ||
| export function normalizeAppleContainerListContainerRecord(container: AppleContainerListContainerRecord): ListContainersItem { | ||
| return { | ||
| id: container.id, | ||
| // The `container` CLI has no name distinct from the container's ID -- `--name` (or | ||
| // the auto-generated ID) is the same value in both places. | ||
| name: container.id, | ||
| labels: container.configuration.labels ?? {}, | ||
| image: parseDockerLikeImageName(container.configuration.image.reference), | ||
| // `configuration.publishedPorts` shape hasn't been captured against a real `--publish` | ||
| // run yet; left empty rather than guessing field names. Fill in once verified. | ||
| ports: [], | ||
|
bwateratmsft marked this conversation as resolved.
Outdated
|
||
| networks: (container.configuration.networks ?? []) | ||
| .map((attachment) => attachment.network) | ||
| .filter((name): name is string => !!name), | ||
| createdAt: container.configuration.creationDate ?? new Date(0), | ||
| state: mapAppleContainerState(container.status.state), | ||
| // No human-readable status string (e.g. Docker's "Up 5 minutes") is emitted. | ||
| status: undefined, | ||
| }; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| /*--------------------------------------------------------------------------------------------- | ||
| * Copyright (c) Microsoft Corporation. All rights reserved. | ||
| * Licensed under the MIT License. See LICENSE in the project root for license information. | ||
| *--------------------------------------------------------------------------------------------*/ | ||
|
|
||
| import * as z from 'zod/mini'; | ||
| import type { ListImagesItem } from '../../contracts/ContainerClient'; | ||
| import { dateStringWithFallbackSchema } from '../../contracts/ZodTransforms'; | ||
| import { parseDockerLikeImageName } from '../../utils/parseDockerLikeImageName'; | ||
|
|
||
| const AppleContainerImageVariantSchema = z.object({ | ||
| size: z.optional(z.number()), | ||
| platform: z.optional(z.object({ | ||
| architecture: z.optional(z.string()), | ||
| })), | ||
| }); | ||
|
|
||
| /** | ||
| * `container image list --format json` emits a manifest-list-oriented shape (captured | ||
| * against real CLI 1.2.0 output), not Docker's flat `Repository`/`Tag`/`ID`/`Size`: the | ||
| * repository/tag live in `configuration.name` (a sibling of `configuration.descriptor`, e.g. | ||
| * `"docker.io/library/alpine:latest"`), and per-platform blobs live under `variants[]`. | ||
| * `image pull` defaults to fetching every platform in a multi-arch manifest -- `variants` | ||
| * reflects what's actually present locally (confirmed: an image pulled with `--arch arm64` | ||
| * has exactly one variant), so summing `variants[].size` gives the real on-disk size rather | ||
| * than double-counting undownloaded platforms. Each real platform variant is paired with a | ||
| * same-sized-ish `platform.architecture: "unknown"` attestation/provenance blob (~86KB, | ||
| * confirmed present for every real platform in a multi-arch pull); those are excluded from | ||
| * the size sum since they aren't part of the image itself. | ||
| * | ||
| * Unlike every other client this extension supports, `container` has no ID-based image | ||
| * addressing at all: `image inspect`/`image rm`/`run` reject a bare digest, a `sha256:`- | ||
| * prefixed digest, and even `name@sha256:digest` (all confirmed to fail with "image not | ||
| * found"); only a `name:tag` reference resolves. `ListImagesItem.id` round-trips into those | ||
| * commands elsewhere in the extension (tooltip inspection, image-ancestor container filtering), | ||
| * so it's set to the name:tag reference here rather than the manifest digest -- the only value | ||
| * that's actually usable as a CLI argument for this runtime. | ||
| */ | ||
| export const AppleContainerListImageRecordSchema = z.object({ | ||
| id: z.string(), | ||
| configuration: z.object({ | ||
| creationDate: z.optional(dateStringWithFallbackSchema), | ||
| name: z.optional(z.string()), | ||
| }), | ||
| variants: z.optional(z.array(AppleContainerImageVariantSchema)), | ||
| }); | ||
|
|
||
| export type AppleContainerListImageRecord = z.infer<typeof AppleContainerListImageRecordSchema>; | ||
|
|
||
| /** | ||
| * Normalize a parsed {@link AppleContainerListImageRecord} to the common | ||
| * {@link ListImagesItem}. | ||
| */ | ||
| export function normalizeAppleContainerListImageRecord(image: AppleContainerListImageRecord): ListImagesItem { | ||
| const realVariants = (image.variants ?? []) | ||
| .filter((variant) => variant.platform?.architecture !== 'unknown'); | ||
|
|
||
| return { | ||
| // Falls back to the (functionally unusable) digest only for images with no name -- | ||
| // ListImagesItem.id must be a non-empty string, and such images can't be individually | ||
| // referenced by this CLI at all regardless of what string is put here. | ||
| id: image.configuration.name ?? image.id, | ||
| image: parseDockerLikeImageName(image.configuration.name), | ||
| createdAt: image.configuration.creationDate ?? new Date(0), | ||
| // Only undefined when no real (non-"unknown") variants were reported -- a real variant | ||
| // summing to 0 bytes is a legitimate size, not an "unknown" sentinel. | ||
| size: realVariants.length > 0 ? realVariants.reduce((total, variant) => total + (variant.size ?? 0), 0) : undefined, | ||
| }; | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.