Skip to content
Merged
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
9 changes: 8 additions & 1 deletion .github/workflows/dev-containers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@devcontainers/cli",
"description": "Dev Containers CLI",
"version": "0.79.0",
"version": "0.80.0",
"bin": {
"devcontainer": "devcontainer.js"
},
Expand Down
7 changes: 3 additions & 4 deletions src/spec-configuration/containerFeaturesConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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} && \\
Expand All @@ -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} \\
Expand Down Expand Up @@ -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} \\
Expand Down
35 changes: 32 additions & 3 deletions src/spec-node/containerFeatures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -186,6 +190,7 @@ export interface ImageBuildOptions {
dockerfilePrefixContent: string;
buildArgs: Record<string, string>;
buildKitContexts: Record<string, string>;
securityOpts: string[];
}

function getImageBuildOptions(params: DockerResolverParameters, config: SubstitutedConfig<DevContainerConfig>, dstFolder: string, baseName: string, imageBuildInfo: ImageBuildInfo): ImageBuildOptions {
Expand All @@ -204,6 +209,7 @@ ${getDevcontainerMetadataLabel(getDevcontainerMetadata(imageBuildInfo.metadata,
_DEV_CONTAINERS_BASE_IMAGE: baseName,
} as Record<string, string>,
buildKitContexts: {} as Record<string, string>,
securityOpts: [],
};
}

Expand Down Expand Up @@ -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));
Expand All @@ -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))
Expand Down Expand Up @@ -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<boolean> {
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<ImageMetadataEntry[]>, composeServiceUser: string | undefined, imageUser: string) {
const reversed = imageMetadata.config.slice().reverse();
const containerUser = reversed.find(entry => entry.containerUser)?.containerUser || composeServiceUser || imageUser;
Expand Down
2 changes: 1 addition & 1 deletion src/spec-node/dockerCompose.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
4 changes: 4 additions & 0 deletions src/spec-node/singleContainer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [];
Expand Down
44 changes: 44 additions & 0 deletions src/test/cli.podman.test.ts
Original file line number Diff line number Diff line change
@@ -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}`);
});
});
});