Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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,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
Comment thread
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] : [],
Comment thread
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: [],

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,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: [],
Comment thread
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,
};
}
Loading