Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions extensions/vscode-containers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2594,15 +2594,17 @@
"com.microsoft.visualstudio.containers.podman",
"com.microsoft.visualstudio.containers.nerdctl",
"com.microsoft.visualstudio.containers.finch",
"com.microsoft.visualstudio.containers.wslc"
"com.microsoft.visualstudio.containers.wslc",
"com.microsoft.visualstudio.containers.applecontainer"
],
"enumItemLabels": [
"Default",
"Docker",
"Podman",
"Nerdctl",
"Finch",
"WSLC (Windows only, preview)"
"WSLC (Windows only, preview)",
"Container (macOS only, preview)"
Comment thread
bwateratmsft marked this conversation as resolved.
Outdated
]
},
"containers.orchestratorClient": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
* Licensed under the MIT License. See LICENSE.md in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { DockerClient, DockerComposeClient, FinchClient, FinchComposeClient, IContainerOrchestratorClient, IContainersClient, NerdctlClient, NerdctlComposeClient, PodmanClient, PodmanComposeClient, WslcClient } from '@microsoft/vscode-container-client';
import { isWindows } from '../utils/osUtils';
import { AppleContainerClient, DockerClient, DockerComposeClient, FinchClient, FinchComposeClient, IContainerOrchestratorClient, IContainersClient, NerdctlClient, NerdctlComposeClient, PodmanClient, PodmanComposeClient, WslcClient } from '@microsoft/vscode-container-client';
import { isArm64, isMac, isWindows } from '../utils/osUtils';

/**
* A client class that can be instantiated with no arguments and exposes its well-known id as a
Expand Down Expand Up @@ -43,6 +43,8 @@ export const officialRuntimeRegistrations: readonly OfficialRuntimeRegistration[
{ containerClient: FinchClient, orchestratorClient: FinchComposeClient },
// The WSL Container CLI is Windows-only and has no compose counterpart.
{ containerClient: WslcClient, isSupported: isWindows },
// The Apple container CLI is Apple Silicon Mac only and has no compose counterpart.
{ containerClient: AppleContainerClient, isSupported: () => isMac() && isArm64() },
];

/**
Expand Down
Comment thread
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: [],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as other file. these are there under variations[].config.config.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per Claude:

I verified against redis:7.2-alpine (which has real EXPOSE/VOLUME in its build history) that Apple's image inspect genuinely omits ExposedPorts/Volumes from the OCI config. The current empty-array code is correct as written.

// 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,
};
}
Loading