-
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 8 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,130 @@ | ||
| /*--------------------------------------------------------------------------------------------- | ||
| * 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, InspectContainersItemMount, InspectContainersItemNetwork } from '../../contracts/ContainerClient'; | ||
| import { dateStringWithFallbackSchema } from '../../contracts/ZodTransforms'; | ||
| import { parseDockerLikeImageName } from '../../utils/parseDockerLikeImageName'; | ||
| import { parseDockerLikeEnvironmentVariables } from '../DockerClientBase/parseDockerLikeEnvironmentVariables'; | ||
| import { AppleContainerPublishedPortSchema, normalizeAppleContainerPublishedPorts } from './AppleContainerPublishedPort'; | ||
|
|
||
| const AppleContainerStatusNetworkSchema = z.object({ | ||
| network: z.optional(z.string()), | ||
| ipv4Address: z.optional(z.string()), | ||
| ipv4Gateway: z.optional(z.string()), | ||
| macAddress: z.optional(z.string()), | ||
| }); | ||
|
|
||
| /** | ||
| * `configuration.mounts[]` -- a volume mount reports its volume name under | ||
| * `type.volume.name` (the sibling top-level `source` is the host-side backing file, not usable | ||
| * as a `--mount source=` value); anything else (confirmed: a bind mount reports `type.virtiofs: | ||
| * {}`) is treated as a bind mount using the top-level `source` path. `readOnly` isn't a | ||
| * dedicated field -- a `readonly` mount adds `"ro"` to `options` (confirmed against a real | ||
| * `--mount ...,readonly` bind mount). | ||
| */ | ||
| const AppleContainerMountSchema = z.object({ | ||
| destination: z.optional(z.string()), | ||
| source: z.optional(z.string()), | ||
| options: z.optional(z.array(z.string())), | ||
| type: z.optional(z.object({ | ||
| volume: z.optional(z.object({ | ||
| name: z.optional(z.string()), | ||
| })), | ||
| })), | ||
| }); | ||
|
|
||
| function normalizeAppleContainerMounts(mounts: Array<z.infer<typeof AppleContainerMountSchema>> | undefined): Array<InspectContainersItemMount> { | ||
| return (mounts ?? []) | ||
| .filter((mount): mount is typeof mount & { destination: string } => !!mount.destination) | ||
| .map((mount) => { | ||
| const readOnly = (mount.options ?? []).includes('ro'); | ||
| const volumeName = mount.type?.volume?.name; | ||
|
|
||
| return volumeName | ||
| ? { type: 'volume' as const, source: volumeName, destination: mount.destination, readOnly } | ||
| : { type: 'bind' as const, source: mount.source ?? '', destination: mount.destination, readOnly }; | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * `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())), | ||
| mounts: z.optional(z.array(AppleContainerMountSchema)), | ||
| publishedPorts: z.optional(z.array(AppleContainerPublishedPortSchema)), | ||
| }), | ||
| 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', | ||
| ports: normalizeAppleContainerPublishedPorts(container.configuration.publishedPorts), | ||
| mounts: normalizeAppleContainerMounts(container.configuration.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,102 @@ | ||
| /*--------------------------------------------------------------------------------------------- | ||
| * 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; | ||
| const nameInfo = parseDockerLikeImageName(image.configuration.name); | ||
| const digest = image.configuration.descriptor?.digest; | ||
| // Matches the `repository@sha256:...` form SharedInspectImageRecord uses, rather than a | ||
| // bare digest. | ||
| const repository = nameInfo.registry ? `${nameInfo.registry}/${nameInfo.image}` : nameInfo.image; | ||
|
|
||
| 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: nameInfo, | ||
| repoDigests: repository && digest ? [`${repository}@${digest}`] : [], | ||
| // `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,93 @@ | ||
| /*--------------------------------------------------------------------------------------------- | ||
| * 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'; | ||
| import { AppleContainerPublishedPortSchema, normalizeAppleContainerPublishedPorts } from './AppleContainerPublishedPort'; | ||
|
|
||
| const AppleContainerNetworkAttachmentSchema = z.object({ | ||
| network: z.optional(z.string()), | ||
| }); | ||
|
|
||
| /** | ||
| * Only the `type.volume.name` sliver is needed here (to match the `volumes` list filter) -- | ||
| * see `AppleContainerInspectContainerRecord.ts` for the full mount shape used by `inspect`. | ||
| */ | ||
| const AppleContainerMountVolumeRefSchema = z.object({ | ||
| type: z.optional(z.object({ | ||
| volume: z.optional(z.object({ | ||
| name: 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()), | ||
| descriptor: z.optional(z.object({ | ||
| digest: z.optional(z.string()), | ||
| })), | ||
| }), | ||
| labels: z.optional(z.record(z.string(), z.string())), | ||
| networks: z.optional(z.array(AppleContainerNetworkAttachmentSchema)), | ||
| mounts: z.optional(z.array(AppleContainerMountVolumeRefSchema)), | ||
| publishedPorts: z.optional(z.array(AppleContainerPublishedPortSchema)), | ||
| }), | ||
| 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), | ||
| ports: normalizeAppleContainerPublishedPorts(container.configuration.publishedPorts), | ||
| 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, | ||
| }; | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.