diff --git a/.github/workflows/dev-containers.yml b/.github/workflows/dev-containers.yml index 66288a6ab..3e5c392ff 100644 --- a/.github/workflows/dev-containers.yml +++ b/.github/workflows/dev-containers.yml @@ -69,7 +69,9 @@ jobs: registry-url: 'https://npm.pkg.github.com' scope: '@microsoft' - name: Install Dependencies - run: yarn install --frozen-lockfile + run: | + yarn install --frozen-lockfile + docker run --privileged --rm tonistiigi/binfmt --install all - name: Type-Check run: yarn type-check - name: Package diff --git a/src/spec-common/cliHost.ts b/src/spec-common/cliHost.ts index d74597f78..294f8be4a 100644 --- a/src/spec-common/cliHost.ts +++ b/src/spec-common/cliHost.ts @@ -1,5 +1,5 @@ /*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. + * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ diff --git a/src/spec-common/commonUtils.ts b/src/spec-common/commonUtils.ts index 0e23fafc7..c74b3c67f 100644 --- a/src/spec-common/commonUtils.ts +++ b/src/spec-common/commonUtils.ts @@ -41,6 +41,15 @@ export interface ExecFunction { (params: ExecParameters): Promise; } +export type GoOS = { [OS in NodeJS.Platform]: OS extends 'win32' ? 'windows' : OS; }[NodeJS.Platform]; +export type GoARCH = { [ARCH in NodeJS.Architecture]: ARCH extends 'x64' ? 'amd64' : ARCH; }[NodeJS.Architecture]; + +export interface PlatformInfo { + os: GoOS; + arch: GoARCH; + variant?: string; +} + export interface PtyExec { onData: Event; write?(data: string): void; diff --git a/src/spec-configuration/containerCollectionsOCI.ts b/src/spec-configuration/containerCollectionsOCI.ts index 9f69a633f..737789059 100644 --- a/src/spec-configuration/containerCollectionsOCI.ts +++ b/src/spec-configuration/containerCollectionsOCI.ts @@ -7,6 +7,7 @@ import * as crypto from 'crypto'; import { Log, LogLevel } from '../spec-utils/log'; import { isLocalFile, mkdirpLocal, readLocalFile, writeLocalFile } from '../spec-utils/pfs'; import { requestEnsureAuthenticated } from './httpOCIRegistry'; +import { GoARCH, GoOS, PlatformInfo } from '../spec-common/commonUtils'; export const DEVCONTAINER_MANIFEST_MEDIATYPE = 'application/vnd.devcontainers'; export const DEVCONTAINER_TAR_LAYER_MEDIATYPE = 'application/vnd.devcontainers.layer.v1+tar'; @@ -91,6 +92,7 @@ interface OCIImageIndexEntry { digest: string; platform: { architecture: string; + variant?: string; os: string; }; } @@ -116,7 +118,7 @@ const regexForVersionOrDigest = /^[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}$/; // https://go.dev/doc/install/source#environment // Expected by OCI Spec as seen here: https://github.com/opencontainers/image-spec/blob/main/image-index.md#image-index-property-descriptions -export function mapNodeArchitectureToGOARCH(arch: NodeJS.Architecture): string { +export function mapNodeArchitectureToGOARCH(arch: NodeJS.Architecture): GoARCH { switch (arch) { case 'x64': return 'amd64'; @@ -127,7 +129,7 @@ export function mapNodeArchitectureToGOARCH(arch: NodeJS.Architecture): string { // https://go.dev/doc/install/source#environment // Expected by OCI Spec as seen here: https://github.com/opencontainers/image-spec/blob/main/image-index.md#image-index-property-descriptions -export function mapNodeOSToGOOS(os: NodeJS.Platform): string { +export function mapNodeOSToGOOS(os: NodeJS.Platform): GoOS { switch (os) { case 'win32': return 'windows'; @@ -272,13 +274,13 @@ export function getCollectionRef(output: Log, registry: string, namespace: strin export async function fetchOCIManifestIfExists(params: CommonParams, ref: OCIRef | OCICollectionRef, manifestDigest?: string): Promise { const { output } = params; - // Simple mechanism to avoid making a DNS request for + // Simple mechanism to avoid making a DNS request for // something that is not a domain name. if (ref.registry.indexOf('.') < 0 && !ref.registry.startsWith('localhost')) { return; } - // TODO: Always use the manifest digest (the canonical digest) + // TODO: Always use the manifest digest (the canonical digest) // instead of the `ref.version` by referencing some lock file (if available). let reference = ref.version; if (manifestDigest) { @@ -338,7 +340,7 @@ export async function getManifest(params: CommonParams, url: string, ref: OCIRef } // https://github.com/opencontainers/image-spec/blob/main/manifest.md -export async function getImageIndexEntryForPlatform(params: CommonParams, url: string, ref: OCIRef | OCICollectionRef, platformInfo: { arch: NodeJS.Architecture; os: NodeJS.Platform }, mimeType?: string): Promise { +export async function getImageIndexEntryForPlatform(params: CommonParams, url: string, ref: OCIRef | OCICollectionRef, platformInfo: PlatformInfo, mimeType?: string): Promise { const { output } = params; const response = await getJsonWithMimeType(params, url, ref, mimeType || 'application/vnd.oci.image.index.v1+json'); if (!response) { @@ -351,15 +353,12 @@ export async function getImageIndexEntryForPlatform(params: CommonParams, url: s return undefined; } - const ociFriendlyPlatformInfo = { - arch: mapNodeArchitectureToGOARCH(platformInfo.arch), - os: mapNodeOSToGOOS(platformInfo.os), - }; - // Find a manifest for the current architecture and OS. return imageIndex.manifests.find(m => { - if (m.platform?.architecture === ociFriendlyPlatformInfo.arch && m.platform?.os === ociFriendlyPlatformInfo.os) { - return m; + if (m.platform?.architecture === platformInfo.arch && m.platform?.os === platformInfo.os) { + if (!platformInfo.variant || m.platform?.variant === platformInfo.variant) { + return m; + } } return undefined; }); @@ -595,4 +594,4 @@ export async function getBlob(params: CommonParams, url: string, ociCacheDir: st output.write(`Error getting blob: ${e}`, LogLevel.Error); return; } -} \ No newline at end of file +} diff --git a/src/spec-node/configContainer.ts b/src/spec-node/configContainer.ts index ea94fe4ea..ee6b93cb3 100644 --- a/src/spec-node/configContainer.ts +++ b/src/spec-node/configContainer.ts @@ -60,7 +60,7 @@ async function resolveWithLocalFolder(params: DockerResolverParameters, parsedAu const { dockerCLI, dockerComposeCLI } = params; const { env } = common; - const cliParams: DockerCLIParameters = { cliHost, dockerCLI, dockerComposeCLI, env, output }; + const cliParams: DockerCLIParameters = { cliHost, dockerCLI, dockerComposeCLI, env, output, platformInfo: params.platformInfo }; await ensureNoDisallowedFeatures(cliParams, config, additionalFeatures, idLabels); await runInitializeCommand({ ...params, common: { ...common, output: common.lifecycleHook.output } }, config.initializeCommand, common.lifecycleHook.onDidInput); diff --git a/src/spec-node/containerFeatures.ts b/src/spec-node/containerFeatures.ts index 8426feaf9..2c7c987be 100644 --- a/src/spec-node/containerFeatures.ts +++ b/src/spec-node/containerFeatures.ts @@ -22,7 +22,7 @@ import { ContainerError } from '../spec-common/errors'; // Environment variables must contain: // - alpha-numeric values, or // - the '_' character, and -// - a number cannot be the first character +// - a number cannot be the first character export const getSafeId = (str: string) => str .replace(/[^\w_]/g, '_') .replace(/^[\d_]+/g, '_') @@ -344,7 +344,7 @@ function getFeatureEnvVariables(f: Feature) { const values = getFeatureValueObject(f); const idSafe = getSafeId(f.id); const variables = []; - + if(f.internalVersion !== '2') { if (values) { @@ -365,7 +365,7 @@ function getFeatureEnvVariables(f: Feature) { variables.push(`${f.buildArg}=${getFeatureMainValue(f)}`); } return variables; - } + } } export async function getRemoteUserUIDUpdateDetails(params: DockerResolverParameters, mergedConfig: MergedDevContainerConfig, imageName: string, imageDetails: () => Promise, runArgsUser: string | undefined) { @@ -388,6 +388,7 @@ export async function getRemoteUserUIDUpdateDetails(params: DockerResolverParame imageName: fixedImageName, remoteUser, imageUser, + platform: [details.Os, details.Architecture, details.Variant].filter(Boolean).join('/') }; } @@ -399,7 +400,7 @@ export async function updateRemoteUserUID(params: DockerResolverParameters, merg if (!updateDetails) { return imageName; } - const { imageName: fixedImageName, remoteUser, imageUser } = updateDetails; + const { imageName: fixedImageName, remoteUser, imageUser, platform } = updateDetails; const dockerfileName = 'updateUID.Dockerfile'; const srcDockerfile = path.join(common.extensionPath, 'scripts', dockerfileName); @@ -415,6 +416,7 @@ export async function updateRemoteUserUID(params: DockerResolverParameters, merg 'build', '-f', destDockerfile, '-t', fixedImageName, + ...(platform ? ['--platform', platform] : []), '--build-arg', `BASE_IMAGE=${imageName}`, '--build-arg', `REMOTE_USER=${remoteUser}`, '--build-arg', `NEW_UID=${await cliHost.getuid!()}`, diff --git a/src/spec-node/devContainers.ts b/src/spec-node/devContainers.ts index b05d438a0..41b772892 100644 --- a/src/spec-node/devContainers.ts +++ b/src/spec-node/devContainers.ts @@ -7,9 +7,10 @@ import * as path from 'path'; import * as crypto from 'crypto'; import * as os from 'os'; +import { mapNodeOSToGOOS, mapNodeArchitectureToGOARCH } from '../spec-configuration/containerCollectionsOCI'; import { DockerResolverParameters, DevContainerAuthority, UpdateRemoteUserUIDDefault, BindMountConsistency, getCacheFolder } from './utils'; import { createNullLifecycleHook, finishBackgroundTasks, ResolverParameters, UserEnvProbe } from '../spec-common/injectHeadless'; -import { getCLIHost, loadNativeModule } from '../spec-common/commonUtils'; +import { GoARCH, GoOS, getCLIHost, loadNativeModule } from '../spec-common/commonUtils'; import { resolve } from './configContainer'; import { URI } from 'vscode-uri'; import { LogLevel, LogDimensions, toErrorText, createCombinedLog, createTerminalLog, Log, makeLog, LogFormat, createJSONLog, createPlainLog, LogHandler, replaceAllLog } from '../spec-utils/log'; @@ -163,12 +164,40 @@ export async function createDockerParams(options: ProvisionOptions, disposables: env: cliHost.env, output: common.output, }, dockerPath, dockerComposePath); + + const platformInfo = (() => { + if (common.buildxPlatform) { + const slash1 = common.buildxPlatform.indexOf('/'); + const slash2 = common.buildxPlatform.indexOf('/', slash1 + 1); + // `--platform linux/amd64/v3` `--platform linux/arm64/v8` + if (slash2 !== -1) { + return { + os: common.buildxPlatform.slice(0, slash1), + arch: common.buildxPlatform.slice(slash1 + 1, slash2), + variant: common.buildxPlatform.slice(slash2 + 1), + }; + } + // `--platform linux/amd64` and `--platform linux/arm64` + return { + os: common.buildxPlatform.slice(0, slash1), + arch: common.buildxPlatform.slice(slash1 + 1), + }; + } else { + // `--platform` omitted + return { + os: mapNodeOSToGOOS(cliHost.platform), + arch: mapNodeArchitectureToGOARCH(cliHost.arch), + }; + } + })(); + const buildKitVersion = options.useBuildKit === 'never' ? undefined : (await dockerBuildKitVersion({ cliHost, dockerCLI: dockerPath, dockerComposeCLI, env: cliHost.env, - output + output, + platformInfo })); return { common, @@ -195,6 +224,7 @@ export async function createDockerParams(options: ProvisionOptions, disposables: buildxPush: common.buildxPush, buildxOutput: common.buildxOutput, buildxCacheTo: common.buildxCacheTo, + platformInfo }; } diff --git a/src/spec-node/devContainersSpecCLI.ts b/src/spec-node/devContainersSpecCLI.ts index 2a45ba503..0b04f4fb0 100644 --- a/src/spec-node/devContainersSpecCLI.ts +++ b/src/spec-node/devContainersSpecCLI.ts @@ -41,6 +41,7 @@ import { featuresResolveDependenciesHandler, featuresResolveDependenciesOptions import { getFeatureIdWithoutVersion } from '../spec-configuration/containerFeaturesOCI'; import { featuresUpgradeHandler, featuresUpgradeOptions } from './upgradeCommand'; import { readFeaturesConfig } from './featureUtils'; +import { mapNodeOSToGOOS, mapNodeArchitectureToGOARCH } from '../spec-configuration/containerCollectionsOCI'; const defaultDefaultUserEnvProbe: UserEnvProbe = 'loginInteractiveShell'; @@ -601,7 +602,7 @@ async function doBuild({ throw new ContainerError({ description: '--push true cannot be used with --output.' }); } - const buildParams: DockerCLIParameters = { cliHost, dockerCLI, dockerComposeCLI, env, output }; + const buildParams: DockerCLIParameters = { cliHost, dockerCLI, dockerComposeCLI, env, output, platformInfo: params.platformInfo }; await ensureNoDisallowedFeatures(buildParams, config, additionalFeatures, undefined); // Support multiple use of `--image-name` @@ -1011,7 +1012,11 @@ async function readConfiguration({ dockerCLI, dockerComposeCLI, env: cliHost.env, - output + output, + platformInfo: { + os: mapNodeOSToGOOS(cliHost.platform), + arch: mapNodeArchitectureToGOARCH(cliHost.arch), + } }; const { container, idLabels } = await findContainerAndIdLabels(params, containerId, providedIdLabels, workspaceFolder, configPath?.fsPath); if (container) { diff --git a/src/spec-node/dockerCompose.ts b/src/spec-node/dockerCompose.ts index b51db8bdd..734b4379c 100644 --- a/src/spec-node/dockerCompose.ts +++ b/src/spec-node/dockerCompose.ts @@ -26,7 +26,7 @@ const serviceLabel = 'com.docker.compose.service'; export async function openDockerComposeDevContainer(params: DockerResolverParameters, workspace: Workspace, config: SubstitutedConfig, idLabels: string[], additionalFeatures: Record>): Promise { const { common, dockerCLI, dockerComposeCLI } = params; const { cliHost, env, output } = common; - const buildParams: DockerCLIParameters = { cliHost, dockerCLI, dockerComposeCLI, env, output }; + const buildParams: DockerCLIParameters = { cliHost, dockerCLI, dockerComposeCLI, env, output, platformInfo: params.platformInfo }; return _openDockerComposeDevContainer(params, buildParams, workspace, config, getRemoteWorkspaceFolder(config.config), idLabels, additionalFeatures); } @@ -150,7 +150,7 @@ export async function buildAndExtendDockerCompose(configWithRaw: SubstitutedConf const { cliHost, env, output } = common; const { config } = configWithRaw; - const cliParams: DockerCLIParameters = { cliHost, dockerCLI, dockerComposeCLI: dockerComposeCLIFunc, env, output }; + const cliParams: DockerCLIParameters = { cliHost, dockerCLI, dockerComposeCLI: dockerComposeCLIFunc, env, output, platformInfo: params.platformInfo }; const composeConfig = await readDockerComposeConfig(cliParams, localComposeFiles, envFile); const composeService = composeConfig.services[config.service]; @@ -406,7 +406,7 @@ async function startContainer(params: DockerResolverParameters, buildParams: Doc // Note: As a fallback, persistedFolder is set to the build's tmpDir() directory const additionalLabels = labels ? idLabels.concat(Object.keys(labels).map(key => `${key}=${labels[key]}`)) : idLabels; const overrideFilePath = await writeFeaturesComposeOverrideFile(updatedImageName, currentImageName, mergedConfig, config, versionPrefix, imageDetails, service, additionalLabels, params.additionalMounts, persistedFolder, featuresStartOverrideFilePrefix, buildCLIHost, params, output); - + if (overrideFilePath) { // Add file path to override file as parameter composeGlobalArgs.push('-f', overrideFilePath); @@ -714,7 +714,7 @@ export function dockerComposeCLIConfig(params: Omit { +export async function inspectImageInRegistry(output: Log, platformInfo: PlatformInfo, name: string): Promise { const resourceAndVersion = qualifyImageName(name); const params = { output, env: process.env }; const ref = getRef(output, resourceAndVersion); @@ -295,6 +295,9 @@ export async function inspectImageInRegistry(output: Log, platformInfo: { arch: return { Id: targetDigest, Config: obj.config, + Os: platformInfo.os, + Variant: platformInfo.variant, + Architecture: platformInfo.arch, }; } @@ -464,7 +467,7 @@ export async function runInitializeCommand(params: DockerResolverParameters, use infoOutput.raw(`\x1b[1mRunning the ${hookName} from devcontainer.json...\x1b[0m\r\n\r\n`); } - // If we have a command name then the command is running in parallel and + // If we have a command name then the command is running in parallel and // we need to hold output until the command is done so that the output // doesn't get interleaved with the output of other commands. const print = name ? 'end' : 'continuous'; diff --git a/src/spec-shutdown/dockerUtils.ts b/src/spec-shutdown/dockerUtils.ts index 7dc2a289f..d27bb1417 100644 --- a/src/spec-shutdown/dockerUtils.ts +++ b/src/spec-shutdown/dockerUtils.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CLIHost, runCommand, runCommandNoPty, ExecFunction, ExecParameters, Exec, PtyExecFunction, PtyExec, PtyExecParameters, plainExecAsPtyExec } from '../spec-common/commonUtils'; +import { CLIHost, runCommand, runCommandNoPty, ExecFunction, ExecParameters, Exec, PtyExecFunction, PtyExec, PtyExecParameters, plainExecAsPtyExec, PlatformInfo } from '../spec-common/commonUtils'; import { toErrorText } from '../spec-common/errors'; import * as ptyType from 'node-pty'; import { Log, makeLog } from '../spec-utils/log'; @@ -51,6 +51,7 @@ export interface DockerCLIParameters { dockerComposeCLI: () => Promise; env: NodeJS.ProcessEnv; output: Log; + platformInfo: PlatformInfo; } export interface PartialExecParameters { @@ -115,6 +116,9 @@ export async function inspectContainers(params: DockerCLIParameters | PartialExe export interface ImageDetails { Id: string; + Architecture: string; + Variant?: string; + Os: string; Config: { User: string; Env: string[] | null; @@ -386,4 +390,4 @@ export function toDockerImageName(name: string) { .toLowerCase() .replace(/[^a-z0-9\._-]+/g, '') .replace(/(\.[\._-]|_[\.-]|__[\._-]|-+[\._])[\._-]*/g, (_, a) => a.substr(0, a.length - 1)); -} \ No newline at end of file +} diff --git a/src/test/cli.build.test.ts b/src/test/cli.build.test.ts index f9206922e..446cc25a7 100644 --- a/src/test/cli.build.test.ts +++ b/src/test/cli.build.test.ts @@ -199,14 +199,17 @@ describe('Dev Containers CLI', function () { it('file ${os.tmpdir()}/output.tar should exist when using --output type=oci,dest=${os.tmpdir()/output.tar', async () => { const testFolder = `${__dirname}/configs/dockerfile-with-target`; const outputPath = `${os.tmpdir()}/output.tar`; - await shellExec('docker buildx create --name ocitest'); - await shellExec('docker buildx use ocitest'); - const res = await shellExec(`${cli} build --workspace-folder ${testFolder} --output 'type=oci,dest=${outputPath}'`); - await shellExec('docker buildx use default'); - await shellExec('docker buildx rm ocitest'); - const response = JSON.parse(res.stdout); - assert.equal(response.outcome, 'success'); - assert.equal(fs.existsSync(outputPath), true); + try { + await shellExec('docker buildx create --name ocitest'); + await shellExec('docker buildx use ocitest'); + const res = await shellExec(`${cli} build --workspace-folder ${testFolder} --output 'type=oci,dest=${outputPath}'`); + const response = JSON.parse(res.stdout); + assert.equal(response.outcome, 'success'); + assert.equal(fs.existsSync(outputPath), true); + } finally { + await shellExec('docker buildx use default'); + await shellExec('docker buildx rm ocitest'); + } }); it(`should execute successfully and export buildx cache with container builder`, async () => { diff --git a/src/test/configs/updateUIDamd64-platform-option/.devcontainer.json b/src/test/configs/updateUIDamd64-platform-option/.devcontainer.json new file mode 100644 index 000000000..d946d5485 --- /dev/null +++ b/src/test/configs/updateUIDamd64-platform-option/.devcontainer.json @@ -0,0 +1,14 @@ +{ + "build": { + "dockerfile": "Dockerfile", + "options": [ + "--platform", + "linux/amd64" + ] + }, + "remoteUser": "foo", + "runArgs": [ + "--platform", + "linux/amd64" + ] +} diff --git a/src/test/configs/updateUIDamd64-platform-option/Dockerfile b/src/test/configs/updateUIDamd64-platform-option/Dockerfile new file mode 100644 index 000000000..bbf53798e --- /dev/null +++ b/src/test/configs/updateUIDamd64-platform-option/Dockerfile @@ -0,0 +1,4 @@ +FROM debian:latest + +RUN addgroup --gid 4321 foo +RUN adduser --uid 1234 --gid 4321 foo diff --git a/src/test/configs/updateUIDamd64/.devcontainer.json b/src/test/configs/updateUIDamd64/.devcontainer.json new file mode 100644 index 000000000..432e69d7e --- /dev/null +++ b/src/test/configs/updateUIDamd64/.devcontainer.json @@ -0,0 +1,6 @@ +{ + "build": { + "dockerfile": "Dockerfile" + }, + "remoteUser": "foo" +} \ No newline at end of file diff --git a/src/test/configs/updateUIDamd64/Dockerfile b/src/test/configs/updateUIDamd64/Dockerfile new file mode 100644 index 000000000..8eb804451 --- /dev/null +++ b/src/test/configs/updateUIDamd64/Dockerfile @@ -0,0 +1,4 @@ +FROM --platform=linux/amd64 debian:latest + +RUN addgroup --gid 4321 foo +RUN adduser --uid 1234 --gid 4321 foo diff --git a/src/test/configs/updateUIDarm64-platform-option/.devcontainer.json b/src/test/configs/updateUIDarm64-platform-option/.devcontainer.json new file mode 100644 index 000000000..c9777414b --- /dev/null +++ b/src/test/configs/updateUIDarm64-platform-option/.devcontainer.json @@ -0,0 +1,14 @@ +{ + "build": { + "dockerfile": "Dockerfile", + "options": [ + "--platform", + "linux/arm64" + ] + }, + "remoteUser": "foo", + "runArgs": [ + "--platform", + "linux/arm64" + ] +} diff --git a/src/test/configs/updateUIDarm64-platform-option/Dockerfile b/src/test/configs/updateUIDarm64-platform-option/Dockerfile new file mode 100644 index 000000000..bbf53798e --- /dev/null +++ b/src/test/configs/updateUIDarm64-platform-option/Dockerfile @@ -0,0 +1,4 @@ +FROM debian:latest + +RUN addgroup --gid 4321 foo +RUN adduser --uid 1234 --gid 4321 foo diff --git a/src/test/configs/updateUIDarm64/.devcontainer.json b/src/test/configs/updateUIDarm64/.devcontainer.json new file mode 100644 index 000000000..432e69d7e --- /dev/null +++ b/src/test/configs/updateUIDarm64/.devcontainer.json @@ -0,0 +1,6 @@ +{ + "build": { + "dockerfile": "Dockerfile" + }, + "remoteUser": "foo" +} \ No newline at end of file diff --git a/src/test/configs/updateUIDarm64/Dockerfile b/src/test/configs/updateUIDarm64/Dockerfile new file mode 100644 index 000000000..ce1897a73 --- /dev/null +++ b/src/test/configs/updateUIDarm64/Dockerfile @@ -0,0 +1,4 @@ +FROM --platform=linux/arm64 debian:latest + +RUN addgroup --gid 4321 foo +RUN adduser --uid 1234 --gid 4321 foo diff --git a/src/test/configs/updateUIDarm64v8-platform-option/.devcontainer.json b/src/test/configs/updateUIDarm64v8-platform-option/.devcontainer.json new file mode 100644 index 000000000..41e35defd --- /dev/null +++ b/src/test/configs/updateUIDarm64v8-platform-option/.devcontainer.json @@ -0,0 +1,14 @@ +{ + "build": { + "dockerfile": "Dockerfile", + "options": [ + "--platform", + "linux/arm64/v8" + ] + }, + "remoteUser": "foo", + "runArgs": [ + "--platform", + "linux/arm64/v8" + ] +} diff --git a/src/test/configs/updateUIDarm64v8-platform-option/Dockerfile b/src/test/configs/updateUIDarm64v8-platform-option/Dockerfile new file mode 100644 index 000000000..bbf53798e --- /dev/null +++ b/src/test/configs/updateUIDarm64v8-platform-option/Dockerfile @@ -0,0 +1,4 @@ +FROM debian:latest + +RUN addgroup --gid 4321 foo +RUN adduser --uid 1234 --gid 4321 foo diff --git a/src/test/configs/updateUIDarm64v8/.devcontainer.json b/src/test/configs/updateUIDarm64v8/.devcontainer.json new file mode 100644 index 000000000..432e69d7e --- /dev/null +++ b/src/test/configs/updateUIDarm64v8/.devcontainer.json @@ -0,0 +1,6 @@ +{ + "build": { + "dockerfile": "Dockerfile" + }, + "remoteUser": "foo" +} \ No newline at end of file diff --git a/src/test/configs/updateUIDarm64v8/Dockerfile b/src/test/configs/updateUIDarm64v8/Dockerfile new file mode 100644 index 000000000..0f8c4714f --- /dev/null +++ b/src/test/configs/updateUIDarm64v8/Dockerfile @@ -0,0 +1,4 @@ +FROM --platform=linux/arm64/v8 debian:latest + +RUN addgroup --gid 4321 foo +RUN adduser --uid 1234 --gid 4321 foo diff --git a/src/test/dockerUtils.test.ts b/src/test/dockerUtils.test.ts index be2178e88..96e99610f 100644 --- a/src/test/dockerUtils.test.ts +++ b/src/test/dockerUtils.test.ts @@ -14,7 +14,7 @@ describe('Docker utils', function () { it('inspect image in docker.io', async () => { const imageName = 'docker.io/library/ubuntu:latest'; - const config = await inspectImageInRegistry(output, { arch: 'x64', os: 'linux' }, imageName); + const config = await inspectImageInRegistry(output, { arch: 'amd64', os: 'linux' }, imageName); assert.ok(config); assert.ok(config.Id); assert.ok(config.Config.Cmd); @@ -22,7 +22,7 @@ describe('Docker utils', function () { it('inspect image in mcr.microsoft.com', async () => { const imageName = 'mcr.microsoft.com/devcontainers/rust:1'; - const config = await inspectImageInRegistry(output, { arch: 'x64', os: 'linux' }, imageName); + const config = await inspectImageInRegistry(output, { arch: 'amd64', os: 'linux' }, imageName); assert.ok(config); assert.ok(config.Id); assert.ok(config.Config.Cmd); @@ -34,7 +34,7 @@ describe('Docker utils', function () { it('inspect image in ghcr.io', async () => { const imageName = 'ghcr.io/chrmarti/cache-from-test/images/test-cache:latest'; - const config = await inspectImageInRegistry(output, { arch: 'x64', os: 'linux' }, imageName); + const config = await inspectImageInRegistry(output, { arch: 'amd64', os: 'linux' }, imageName); assert.ok(config); assert.ok(config.Id); assert.ok(config.Config.Cmd); @@ -46,4 +46,4 @@ describe('Docker utils', function () { assert.strictEqual(qualifyImageName('random/image'), 'docker.io/random/image'); assert.strictEqual(qualifyImageName('foo/random/image'), 'foo/random/image'); }); -}); \ No newline at end of file +}); diff --git a/src/test/dockerfileUtils.test.ts b/src/test/dockerfileUtils.test.ts index 3bbe451c2..bd9489ddc 100644 --- a/src/test/dockerfileUtils.test.ts +++ b/src/test/dockerfileUtils.test.ts @@ -161,7 +161,9 @@ FROM ubuntu:latest as dev }, Entrypoint: null, Cmd: null - } + }, + Os: 'linux', + Architecture: 'amd64' }; const info = await internalGetImageBuildInfoFromDockerfile(async (imageName) => { assert.strictEqual(imageName, 'ubuntu:latest'); @@ -187,7 +189,9 @@ USER dockerfileUserB Labels: null, Entrypoint: null, Cmd: null - } + }, + Os: 'linux', + Architecture: 'amd64' }; const info = await internalGetImageBuildInfoFromDockerfile(async (imageName) => { assert.strictEqual(imageName, 'ubuntu:latest'); @@ -282,7 +286,7 @@ FROM \${cloud:+mcr.microsoft.com/}azure-cli:latest const image = findBaseImage(extracted, {}, undefined); assert.strictEqual(image, 'azure-cli:latest'); }); - + it('Negative variable expression with value specified', async () => { const dockerfile = ` ARG cloud @@ -295,7 +299,7 @@ FROM \${cloud:-mcr.microsoft.com/}azure-cli:latest }, undefined); assert.strictEqual(image, 'ghcr.io/azure-cli:latest'); }); - + it('Negative variable expression with no value specified', async () => { const dockerfile = ` ARG cloud @@ -620,7 +624,7 @@ user D const stage = extracted.stages[0]; assert.strictEqual(stage.from.image, 'E'); assert.strictEqual(stage.instructions.length, 3); - + const env = stage.instructions[0]; assert.strictEqual(env.instruction, 'ENV'); assert.strictEqual(env.name, 'A'); diff --git a/src/test/testUtils.ts b/src/test/testUtils.ts index ce2383b8d..ca9dad16a 100644 --- a/src/test/testUtils.ts +++ b/src/test/testUtils.ts @@ -9,6 +9,7 @@ import { SubstituteConfig } from '../spec-node/utils'; import { LogLevel, createPlainLog, makeLog, nullLog } from '../spec-utils/log'; import { dockerComposeCLIConfig } from '../spec-node/dockerCompose'; import { DockerCLIParameters } from '../spec-shutdown/dockerUtils'; +import { mapNodeArchitectureToGOARCH, mapNodeOSToGOOS } from '../spec-configuration/containerCollectionsOCI'; export interface BuildKitOption { text: string; @@ -159,6 +160,10 @@ export async function createCLIParams(hostPath: string) { dockerComposeCLI, env: {}, output, - }; + platformInfo: { + os: mapNodeOSToGOOS(cliHost.platform), + arch: mapNodeArchitectureToGOARCH(cliHost.arch), + } +}; return cliParams; } diff --git a/src/test/updateUID.test.ts b/src/test/updateUID.test.ts index c96bbb733..15d4f5dce 100644 --- a/src/test/updateUID.test.ts +++ b/src/test/updateUID.test.ts @@ -46,5 +46,65 @@ const pkg = require('../../package.json'); assert.strictEqual(gid.stdout.trim(), String(4321)); await devContainerDown({ containerId }); }); + + it('should update UID and GID when the platform is linux/amd64', async () => { + const testFolder = `${__dirname}/configs/updateUIDamd64`; + const containerId = (await devContainerUp(cli, testFolder)).containerId; + const uid = await shellExec(`${cli} exec --workspace-folder ${testFolder} id -u`); + assert.strictEqual(uid.stdout.trim(), String(process.getuid!())); + const gid = await shellExec(`${cli} exec --workspace-folder ${testFolder} id -g`); + assert.strictEqual(gid.stdout.trim(), String(process.getgid!())); + await devContainerDown({ containerId }); + }); + + it('should update UID and GID when --platform is linux/amd64', async () => { + const testFolder = `${__dirname}/configs/updateUIDamd64-platform-option`; + const containerId = (await devContainerUp(cli, testFolder)).containerId; + const uid = await shellExec(`${cli} exec --workspace-folder ${testFolder} id -u`); + assert.strictEqual(uid.stdout.trim(), String(process.getuid!())); + const gid = await shellExec(`${cli} exec --workspace-folder ${testFolder} id -g`); + assert.strictEqual(gid.stdout.trim(), String(process.getgid!())); + await devContainerDown({ containerId }); + }); + + it('should update UID and GID when the platform is linux/arm64', async () => { + const testFolder = `${__dirname}/configs/updateUIDarm64`; + const containerId = (await devContainerUp(cli, testFolder)).containerId; + const uid = await shellExec(`${cli} exec --workspace-folder ${testFolder} id -u`); + assert.strictEqual(uid.stdout.trim(), String(process.getuid!())); + const gid = await shellExec(`${cli} exec --workspace-folder ${testFolder} id -g`); + assert.strictEqual(gid.stdout.trim(), String(process.getgid!())); + await devContainerDown({ containerId }); + }); + + it('should update UID and GID when --platform is linux/arm64', async () => { + const testFolder = `${__dirname}/configs/updateUIDarm64-platform-option`; + const containerId = (await devContainerUp(cli, testFolder)).containerId; + const uid = await shellExec(`${cli} exec --workspace-folder ${testFolder} id -u`); + assert.strictEqual(uid.stdout.trim(), String(process.getuid!())); + const gid = await shellExec(`${cli} exec --workspace-folder ${testFolder} id -g`); + assert.strictEqual(gid.stdout.trim(), String(process.getgid!())); + await devContainerDown({ containerId }); + }); + + it('should update UID and GID when the platform is linux/arm64/v8', async () => { + const testFolder = `${__dirname}/configs/updateUIDarm64v8`; + const containerId = (await devContainerUp(cli, testFolder)).containerId; + const uid = await shellExec(`${cli} exec --workspace-folder ${testFolder} id -u`); + assert.strictEqual(uid.stdout.trim(), String(process.getuid!())); + const gid = await shellExec(`${cli} exec --workspace-folder ${testFolder} id -g`); + assert.strictEqual(gid.stdout.trim(), String(process.getgid!())); + await devContainerDown({ containerId }); + }); + + it('should update UID and GID when --platform is linux/arm64/v8', async () => { + const testFolder = `${__dirname}/configs/updateUIDarm64v8-platform-option`; + const containerId = (await devContainerUp(cli, testFolder)).containerId; + const uid = await shellExec(`${cli} exec --workspace-folder ${testFolder} id -u`); + assert.strictEqual(uid.stdout.trim(), String(process.getuid!())); + const gid = await shellExec(`${cli} exec --workspace-folder ${testFolder} id -g`); + assert.strictEqual(gid.stdout.trim(), String(process.getgid!())); + await devContainerDown({ containerId }); + }); }); });