Skip to content
Open
Show file tree
Hide file tree
Changes from all 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 @@ -2595,15 +2595,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": [
"%vscode-containers.config.containers.containerClient.default%",
"%vscode-containers.config.containers.containerClient.docker%",
"%vscode-containers.config.containers.containerClient.podman%",
"%vscode-containers.config.containers.containerClient.nerdctl%",
"%vscode-containers.config.containers.containerClient.finch%",
"%vscode-containers.config.containers.containerClient.wslc%"
"%vscode-containers.config.containers.containerClient.wslc%",
"%vscode-containers.config.containers.containerClient.applecontainer%"
]
},
"containers.orchestratorClient": {
Expand Down
1 change: 1 addition & 0 deletions extensions/vscode-containers/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@
"vscode-containers.config.containers.containerClient.nerdctl": "Nerdctl",
"vscode-containers.config.containers.containerClient.finch": "Finch",
"vscode-containers.config.containers.containerClient.wslc": "WSLC (Windows only, preview)",
"vscode-containers.config.containers.containerClient.applecontainer": "Container (macOS only, preview)",
"vscode-containers.config.containers.orchestratorClient": "Which container orchestrator client to use. If not specified, Docker Compose will be used. Changing requires a restart to take effect.",
"vscode-containers.config.containers.orchestratorClient.default": "Default",
"vscode-containers.config.containers.orchestratorClient.dockerCompose": "Docker Compose",
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