From 0b3b67fedfd9d695c110563b494a1e03bf483620 Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Thu, 10 Jul 2025 11:08:33 +0200 Subject: [PATCH] Use label=disable instead of z flag (microsoft/vscode-remote-release#10585) --- .github/workflows/dev-containers.yml | 9 +++- CHANGELOG.md | 5 +++ package.json | 2 +- .../containerFeaturesConfiguration.ts | 7 ++- src/spec-node/containerFeatures.ts | 35 +++++++++++++-- src/spec-node/dockerCompose.ts | 2 +- src/spec-node/singleContainer.ts | 4 ++ src/test/cli.podman.test.ts | 44 +++++++++++++++++++ 8 files changed, 98 insertions(+), 10 deletions(-) create mode 100644 src/test/cli.podman.test.ts diff --git a/.github/workflows/dev-containers.yml b/.github/workflows/dev-containers.yml index 26b2933e4..fca4f56b4 100644 --- a/.github/workflows/dev-containers.yml +++ b/.github/workflows/dev-containers.yml @@ -52,12 +52,13 @@ jobs: "src/test/cli.exec.buildKit.2.test.ts", "src/test/cli.exec.nonBuildKit.1.test.ts", "src/test/cli.exec.nonBuildKit.2.test.ts", + "src/test/cli.podman.test.ts", "src/test/cli.test.ts", "src/test/cli.up.test.ts", "src/test/imageMetadata.test.ts", "src/test/container-features/containerFeaturesOCIPush.test.ts", # Run all except the above: - "--exclude src/test/container-features/containerFeaturesOrder.test.ts --exclude src/test/container-features/registryCompatibilityOCI.test.ts --exclude src/test/container-features/containerFeaturesOCIPush.test.ts --exclude src/test/container-features/e2e.test.ts --exclude src/test/container-features/featuresCLICommands.test.ts --exclude src/test/cli.build.test.ts --exclude src/test/cli.exec.buildKit.1.test.ts --exclude src/test/cli.exec.buildKit.2.test.ts --exclude src/test/cli.exec.nonBuildKit.1.test.ts --exclude src/test/cli.exec.nonBuildKit.2.test.ts --exclude src/test/cli.test.ts --exclude src/test/cli.up.test.ts --exclude src/test/imageMetadata.test.ts 'src/test/**/*.test.ts'", + "--exclude src/test/container-features/containerFeaturesOrder.test.ts --exclude src/test/container-features/registryCompatibilityOCI.test.ts --exclude src/test/container-features/containerFeaturesOCIPush.test.ts --exclude src/test/container-features/e2e.test.ts --exclude src/test/container-features/featuresCLICommands.test.ts --exclude src/test/cli.build.test.ts --exclude src/test/cli.exec.buildKit.1.test.ts --exclude src/test/cli.exec.buildKit.2.test.ts --exclude src/test/cli.exec.nonBuildKit.1.test.ts --exclude src/test/cli.exec.nonBuildKit.2.test.ts --exclude src/test/cli.podman.test.ts --exclude src/test/cli.test.ts --exclude src/test/cli.up.test.ts --exclude src/test/imageMetadata.test.ts 'src/test/**/*.test.ts'", ] steps: - name: Checkout @@ -68,6 +69,12 @@ jobs: node-version: '18.x' registry-url: 'https://npm.pkg.github.com' scope: '@microsoft' + - name: Tools Info + run: | + docker info + docker buildx version + podman info + podman buildx version - name: Install Dependencies run: | yarn install --frozen-lockfile diff --git a/CHANGELOG.md b/CHANGELOG.md index 198705387..5f9dafcd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ Notable changes. +## July 2025 + +### [0.80.0] +- Podman: Use label=disable instead of z flag (https://github.com/microsoft/vscode-remote-release/issues/10585) + ## June 2025 ### [0.79.0] diff --git a/package.json b/package.json index a5ba12624..736fb3a10 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@devcontainers/cli", "description": "Dev Containers CLI", - "version": "0.79.0", + "version": "0.80.0", "bin": { "devcontainer": "devcontainer.js" }, diff --git a/src/spec-configuration/containerFeaturesConfiguration.ts b/src/spec-configuration/containerFeaturesConfiguration.ts index 8309fb6ab..662ed3b65 100644 --- a/src/spec-configuration/containerFeaturesConfiguration.ts +++ b/src/spec-configuration/containerFeaturesConfiguration.ts @@ -289,9 +289,8 @@ function escapeQuotesForShell(input: string) { return input.replace(new RegExp(`'`, 'g'), `'\\''`); } -export function getFeatureLayers(featuresConfig: FeaturesConfig, containerUser: string, remoteUser: string, isBuildah = false, useBuildKitBuildContexts = false, contentSourceRootPath = '/tmp/build-features') { +export function getFeatureLayers(featuresConfig: FeaturesConfig, containerUser: string, remoteUser: string, useBuildKitBuildContexts = false, contentSourceRootPath = '/tmp/build-features') { - const useSELinuxLabel = process.platform === 'linux' && isBuildah; const builtinsEnvFile = `${path.posix.join(FEATURES_CONTAINER_TEMP_DEST_FOLDER, 'devcontainer-features.builtin.env')}`; let result = `RUN \\ echo "_CONTAINER_USER_HOME=$(${getEntPasswdShellCommand(containerUser)} | cut -d: -f6)" >> ${builtinsEnvFile} && \\ @@ -313,7 +312,7 @@ RUN chmod -R 0755 ${dest} \\ `; } else { - result += `RUN --mount=type=bind,from=dev_containers_feature_content_source,source=${source},target=/tmp/build-features-src/${folder}${useSELinuxLabel ? ',z' : ''} \\ + result += `RUN --mount=type=bind,from=dev_containers_feature_content_source,source=${source},target=/tmp/build-features-src/${folder} \\ cp -ar /tmp/build-features-src/${folder} ${FEATURES_CONTAINER_TEMP_DEST_FOLDER} \\ && chmod -R 0755 ${dest} \\ && cd ${dest} \\ @@ -341,7 +340,7 @@ RUN chmod -R 0755 ${dest} \\ `; } else { result += ` -RUN --mount=type=bind,from=dev_containers_feature_content_source,source=${source},target=/tmp/build-features-src/${feature.consecutiveId}${useSELinuxLabel ? ',z' : ''} \\ +RUN --mount=type=bind,from=dev_containers_feature_content_source,source=${source},target=/tmp/build-features-src/${feature.consecutiveId} \\ cp -ar /tmp/build-features-src/${feature.consecutiveId} ${FEATURES_CONTAINER_TEMP_DEST_FOLDER} \\ && chmod -R 0755 ${dest} \\ && cd ${dest} \\ diff --git a/src/spec-node/containerFeatures.ts b/src/spec-node/containerFeatures.ts index 85835557b..c7e42a56a 100644 --- a/src/spec-node/containerFeatures.ts +++ b/src/spec-node/containerFeatures.ts @@ -12,7 +12,7 @@ import { FeaturesConfig, getContainerFeaturesBaseDockerFile, getFeatureInstallWr import { readLocalFile } from '../spec-utils/pfs'; import { includeAllConfiguredFeatures } from '../spec-utils/product'; import { createFeaturesTempFolder, DockerResolverParameters, getCacheFolder, getFolderImageName, getEmptyContextFolder, SubstitutedConfig } from './utils'; -import { isEarlierVersion, parseVersion } from '../spec-common/commonUtils'; +import { isEarlierVersion, parseVersion, runCommandNoPty } from '../spec-common/commonUtils'; import { getDevcontainerMetadata, getDevcontainerMetadataLabel, getImageBuildInfoFromImage, ImageBuildInfo, ImageMetadataEntry, imageMetadataLabel, MergedDevContainerConfig } from './imageMetadata'; import { supportsBuildContexts } from './dockerfileUtils'; import { ContainerError } from '../spec-common/errors'; @@ -92,6 +92,10 @@ export async function extendImage(params: DockerResolverParameters, config: Subs for (const buildContext in featureBuildInfo.buildKitContexts) { args.push('--build-context', `${buildContext}=${featureBuildInfo.buildKitContexts[buildContext]}`); } + + for (const securityOpt of featureBuildInfo.securityOpts) { + args.push('--security-opt', securityOpt); + } } else { // Not using buildx args.push( @@ -186,6 +190,7 @@ export interface ImageBuildOptions { dockerfilePrefixContent: string; buildArgs: Record; buildKitContexts: Record; + securityOpts: string[]; } function getImageBuildOptions(params: DockerResolverParameters, config: SubstitutedConfig, dstFolder: string, baseName: string, imageBuildInfo: ImageBuildInfo): ImageBuildOptions { @@ -204,6 +209,7 @@ ${getDevcontainerMetadataLabel(getDevcontainerMetadata(imageBuildInfo.metadata, _DEV_CONTAINERS_BASE_IMAGE: baseName, } as Record, buildKitContexts: {} as Record, + securityOpts: [], }; } @@ -234,7 +240,7 @@ async function getFeaturesBuildOptions(params: DockerResolverParameters, devCont const minRequiredVersion = [0, 8, 0]; const useBuildKitBuildContexts = buildKitVersionParsed ? !isEarlierVersion(buildKitVersionParsed, minRequiredVersion) : false; const buildContentImageName = 'dev_container_feature_content_temp'; - const isBuildah = !!params.buildKitVersion?.versionString.toLowerCase().includes('buildah'); + const disableSELinuxLabels = useBuildKitBuildContexts && await isUsingSELinuxLabels(params); const omitPropertyOverride = params.common.skipPersistingCustomizationsFromFeatures ? ['customizations'] : []; const imageMetadata = getDevcontainerMetadata(imageBuildInfo.metadata, devContainerConfig, featuresConfig, omitPropertyOverride, getOmitDevcontainerPropertyOverride(params.common)); @@ -251,7 +257,7 @@ async function getFeaturesBuildOptions(params: DockerResolverParameters, devCont const contentSourceRootPath = useBuildKitBuildContexts ? '.' : '/tmp/build-features/'; const dockerfile = getContainerFeaturesBaseDockerFile(contentSourceRootPath) .replace('#{nonBuildKitFeatureContentFallback}', useBuildKitBuildContexts ? '' : `FROM ${buildContentImageName} as dev_containers_feature_content_source`) - .replace('#{featureLayer}', getFeatureLayers(featuresConfig, containerUser, remoteUser, isBuildah, useBuildKitBuildContexts, contentSourceRootPath)) + .replace('#{featureLayer}', getFeatureLayers(featuresConfig, containerUser, remoteUser, useBuildKitBuildContexts, contentSourceRootPath)) .replace('#{containerEnv}', generateContainerEnvsV1(featuresConfig)) .replace('#{devcontainerMetadata}', getDevcontainerMetadataLabel(imageMetadata)) .replace('#{containerEnvMetadata}', generateContainerEnvs(devContainerConfig.config.containerEnv, true)) @@ -343,9 +349,32 @@ ARG _DEV_CONTAINERS_BASE_IMAGE=placeholder _DEV_CONTAINERS_FEATURE_CONTENT_SOURCE: buildContentImageName, }, buildKitContexts: useBuildKitBuildContexts ? { dev_containers_feature_content_source: dstFolder } : {}, + securityOpts: disableSELinuxLabels ? ['label=disable'] : [], }; } +async function isUsingSELinuxLabels(params: DockerResolverParameters): Promise { + try { + const { common } = params; + const { cliHost, output } = common; + return params.isPodman && cliHost.platform === 'linux' + && (await runCommandNoPty({ + exec: cliHost.exec, + cmd: 'getenforce', + output, + print: true, + })).stdout.toString().trim() !== 'Disabled' + && (await dockerCLI({ + ...toExecParameters(params), + print: true, + }, 'info', '-f', '{{.Host.Security.SELinuxEnabled}}')).stdout.toString().trim() === 'true'; + } catch { + // If we can't run the commands, assume SELinux is not enabled. + return false; + + } +} + export function findContainerUsers(imageMetadata: SubstitutedConfig, composeServiceUser: string | undefined, imageUser: string) { const reversed = imageMetadata.config.slice().reverse(); const containerUser = reversed.find(entry => entry.containerUser)?.containerUser || composeServiceUser || imageUser; diff --git a/src/spec-node/dockerCompose.ts b/src/spec-node/dockerCompose.ts index 07f5ec436..4c20ebd68 100644 --- a/src/spec-node/dockerCompose.ts +++ b/src/spec-node/dockerCompose.ts @@ -188,7 +188,7 @@ export async function buildAndExtendDockerCompose(configWithRaw: SubstitutedConf // determine whether we need to extend with features const version = parseVersion((await params.dockerComposeCLI()).version); - const supportsAdditionalBuildContexts = version && !isEarlierVersion(version, [2, 17, 0]); + const supportsAdditionalBuildContexts = !params.isPodman && version && !isEarlierVersion(version, [2, 17, 0]); const optionalBuildKitParams = supportsAdditionalBuildContexts ? params : { ...params, buildKitVersion: undefined }; const extendImageBuildInfo = await getExtendImageBuildInfo(optionalBuildKitParams, configWithRaw, baseName, imageBuildInfo, composeService.user, additionalFeatures, canAddLabelsToContainer); diff --git a/src/spec-node/singleContainer.ts b/src/spec-node/singleContainer.ts index 4bb159d4a..07d111cc2 100644 --- a/src/spec-node/singleContainer.ts +++ b/src/spec-node/singleContainer.ts @@ -180,6 +180,10 @@ async function buildAndExtendImage(buildParams: DockerResolverParameters, config for (const buildArg in featureBuildInfo.buildArgs) { additionalBuildArgs.push('--build-arg', `${buildArg}=${featureBuildInfo.buildArgs[buildArg]}`); } + + for (const securityOpt of featureBuildInfo.securityOpts) { + additionalBuildArgs.push('--security-opt', securityOpt); + } } const args: string[] = []; diff --git a/src/test/cli.podman.test.ts b/src/test/cli.podman.test.ts new file mode 100644 index 000000000..932d9a358 --- /dev/null +++ b/src/test/cli.podman.test.ts @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import * as path from 'path'; +import { shellExec } from './testUtils'; + +const pkg = require('../../package.json'); + +describe('Dev Containers CLI using Podman', function () { + this.timeout('240s'); + + const tmp = path.relative(process.cwd(), path.join(__dirname, 'tmp')); + const cli = `npx --prefix ${tmp} devcontainer`; + + before('Install', async () => { + await shellExec(`rm -rf ${tmp}/node_modules`); + await shellExec(`mkdir -p ${tmp}`); + await shellExec(`npm --prefix ${tmp} install devcontainers-cli-${pkg.version}.tgz`); + }); + + describe('Command up using Podman', () => { + + it('should execute successfully with valid config with features', async () => { + const res = await shellExec(`${cli} up --docker-path podman --workspace-folder ${__dirname}/configs/image-with-features`); + const response = JSON.parse(res.stdout); + assert.equal(response.outcome, 'success'); + const containerId: string = response.containerId; + assert.ok(containerId, 'Container id not found.'); + await shellExec(`podman rm -f ${containerId}`); + }); + + it('should execute successfully with valid config with features', async () => { + const res = await shellExec(`${cli} up --docker-path podman --workspace-folder ${__dirname}/configs/dockerfile-with-features`); + const response = JSON.parse(res.stdout); + assert.equal(response.outcome, 'success'); + const containerId: string = response.containerId; + assert.ok(containerId, 'Container id not found.'); + await shellExec(`podman rm -f ${containerId}`); + }); + }); +}); \ No newline at end of file