New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

@devcontainers/cli

Package Overview
Dependencies
Maintainers
3
Versions
101
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@devcontainers/cli - npm Package Compare versions

Comparing version 0.1.0 to 0.2.0

12

dist/spec-configuration/containerFeaturesConfiguration.d.ts

@@ -101,3 +101,3 @@ /// <reference types="node" />

}
export declare function collapseFeaturesConfig(original: FeaturesConfig | undefined): CollapsedFeaturesConfig | undefined;
export declare function collapseFeaturesConfig(original: FeaturesConfig): CollapsedFeaturesConfig;
export declare const multiStageBuildExploration = false;

@@ -121,6 +121,12 @@ export declare function getContainerFeaturesFolder(_extensionPath: string | {

env: NodeJS.ProcessEnv;
}, dstFolder: string, config: DevContainerConfig, imageLabels: () => Promise<Record<string, string | undefined>>, getLocalFolder: (d: string) => string): Promise<FeaturesConfig | undefined>;
}, dstFolder: string, config: DevContainerConfig, imageLabelDetails: () => Promise<{
definition?: string;
version?: string;
}>, getLocalFolder: (d: string) => string): Promise<FeaturesConfig | undefined>;
export declare function doReadUserDeclaredFeatures(params: {
output: Log;
}, config: DevContainerConfig, featuresConfig: FeaturesConfig, imageLabels: () => Promise<Record<string, string | undefined>>): Promise<FeaturesConfig>;
}, config: DevContainerConfig, featuresConfig: FeaturesConfig, imageLabelDetails: () => Promise<{
definition?: string;
version?: string;
}>): Promise<FeaturesConfig>;
export declare function getFeatureMainProperty(feature: Feature): "version" | undefined;

@@ -127,0 +133,0 @@ export declare function getFeatureMainValue(feature: Feature): string | boolean | undefined;

@@ -37,5 +37,2 @@ "use strict";

function collapseFeaturesConfig(original) {
if (!original) {
return undefined;
}
const collapsed = {

@@ -78,11 +75,11 @@ allFeatures: original.featureSets

return `
ARG BASE_IMAGE=mcr.microsoft.com/vscode/devcontainers/base:buster
#{featureBuildStages}
FROM $BASE_IMAGE
#{nonBuildKitFeatureContentFallback}
FROM $_DEV_CONTAINERS_BASE_IMAGE
USER root
COPY . /tmp/build-features/
COPY --from=dev_containers_feature_content_source {contentSourceRootPath} /tmp/build-features/

@@ -95,4 +92,4 @@ #{featureLayer}

ARG IMAGE_USER=root
USER $IMAGE_USER
ARG _DEV_CONTAINERS_IMAGE_USER=root
USER $_DEV_CONTAINERS_IMAGE_USER
`;

@@ -506,3 +503,3 @@ }

// as well as downloading and merging in remote feature definitions.
async function generateFeaturesConfig(params, dstFolder, config, imageLabels, getLocalFolder) {
async function generateFeaturesConfig(params, dstFolder, config, imageLabelDetails, getLocalFolder) {
var _a;

@@ -536,3 +533,3 @@ const { output } = params;

// Run filtering and include user options into config.
featuresConfig = await doReadUserDeclaredFeatures(params, config, featuresConfig, imageLabels);
featuresConfig = await doReadUserDeclaredFeatures(params, config, featuresConfig, imageLabelDetails);
if (featuresConfig.featureSets.every(set => set.features.every(feature => feature.value === false))) {

@@ -546,8 +543,6 @@ return undefined;

// Given an existing featuresConfig, parse the user's features as they declared them in their devcontainer.
async function doReadUserDeclaredFeatures(params, config, featuresConfig, imageLabels) {
async function doReadUserDeclaredFeatures(params, config, featuresConfig, imageLabelDetails) {
var _a;
const { output } = params;
const labels = await imageLabels();
const definition = labels['com.visualstudio.code.devcontainers.id'];
const version = labels['version'];
const { definition, version } = await imageLabelDetails();
// Map user's declared features to its appropriate feature-set feature.

@@ -554,0 +549,0 @@ let configFeatures = config.features || {};

@@ -7,6 +7,23 @@ import { DevContainerConfig } from '../spec-configuration/configuration';

updatedImageName: string;
collapsedFeaturesConfig: import("../spec-configuration/containerFeaturesConfiguration").CollapsedFeaturesConfig | undefined;
collapsedFeaturesConfig: undefined;
imageDetails: () => Promise<ImageDetails>;
} | {
updatedImageName: string;
collapsedFeaturesConfig: import("../spec-configuration/containerFeaturesConfiguration").CollapsedFeaturesConfig;
imageDetails: () => Promise<ImageDetails>;
}>;
export declare function getExtendImageBuildInfo(params: DockerResolverParameters, config: DevContainerConfig, baseName: string, imageUser: string, imageLabelDetails: () => Promise<{
definition: string | undefined;
version: string | undefined;
}>): Promise<{
featureBuildInfo: {
dstFolder: string;
dockerfileContent: string;
dockerfilePrefixContent: string;
buildArgs: Record<string, string>;
buildKitContexts: Record<string, string>;
};
collapsedFeaturesConfig: import("../spec-configuration/containerFeaturesConfiguration").CollapsedFeaturesConfig;
} | null>;
export declare function generateContainerEnvs(featuresConfig: FeaturesConfig): string;
export declare function updateRemoteUserUID(params: DockerResolverParameters, config: DevContainerConfig, imageName: string, imageDetails: () => Promise<ImageDetails>, runArgsUser: string | undefined): Promise<string>;

@@ -26,3 +26,3 @@ "use strict";

Object.defineProperty(exports, "__esModule", { value: true });
exports.updateRemoteUserUID = exports.generateContainerEnvs = exports.extendImage = void 0;
exports.updateRemoteUserUID = exports.generateContainerEnvs = exports.getExtendImageBuildInfo = exports.extendImage = void 0;
const path = __importStar(require("path"));

@@ -39,9 +39,71 @@ const string_decoder_1 = require("string_decoder");

let cache;
const { common } = params;
const { cliHost, output } = common;
const imageDetails = () => cache || (cache = (0, utils_1.inspectDockerImage)(params, imageName, pullImageOnError));
const featuresConfig = await (0, containerFeaturesConfiguration_1.generateFeaturesConfig)(params.common, (await (0, utils_1.createFeaturesTempFolder)(params.common)), config, async () => (await imageDetails()).Config.Labels || {}, containerFeaturesConfiguration_1.getContainerFeaturesFolder);
const collapsedFeaturesConfig = (0, containerFeaturesConfiguration_1.collapseFeaturesConfig)(featuresConfig);
const updatedImageName = await addContainerFeatures(params, featuresConfig, imageName, imageDetails);
const imageLabelDetails = async () => {
const labels = (await imageDetails()).Config.Labels || {};
return {
definition: labels['com.visualstudio.code.devcontainers.id'],
version: labels['version'],
};
};
const imageUser = (await imageDetails()).Config.User || 'root';
const extendImageDetails = await getExtendImageBuildInfo(params, config, imageName, imageUser, imageLabelDetails);
if (!extendImageDetails || !extendImageDetails.featureBuildInfo) {
// no feature extensions - return
return {
updatedImageName: imageName,
collapsedFeaturesConfig: undefined,
imageDetails
};
}
const { featureBuildInfo, collapsedFeaturesConfig } = extendImageDetails;
// Got feature extensions -> build the image
const dockerfilePath = cliHost.path.join(featureBuildInfo.dstFolder, 'Dockerfile.extended');
await cliHost.writeFile(dockerfilePath, Buffer.from(featureBuildInfo.dockerfilePrefixContent + featureBuildInfo.dockerfileContent));
const folderImageName = (0, utils_1.getFolderImageName)(common);
const updatedImageName = `${imageName.startsWith(folderImageName) ? imageName : folderImageName}-features`;
const args = [];
if (params.useBuildKit) {
args.push('buildx', 'build', '--load');
for (const buildContext in featureBuildInfo.buildKitContexts) {
args.push('--build-context', `${buildContext}=${featureBuildInfo.buildKitContexts[buildContext]}`);
}
}
else {
args.push('build');
}
for (const buildArg in featureBuildInfo.buildArgs) {
args.push('--build-arg', `${buildArg}=${featureBuildInfo.buildArgs[buildArg]}`);
}
// Once this is step merged with the user Dockerfile (or working against the base image),
// the path will be the dev container context
// Set empty dir under temp path as the context for now to ensure we don't have dependencies on the features content
const emptyTempDir = cliHost.path.join(await cliHost.tmpdir(), '__dev-containers-build-empty');
cliHost.mkdirp(emptyTempDir);
args.push('-t', updatedImageName, '-f', dockerfilePath, emptyTempDir);
if (process.stdin.isTTY) {
const infoParams = { ...(0, dockerUtils_1.toPtyExecParameters)(params), output: (0, log_1.makeLog)(output, log_1.LogLevel.Info) };
await (0, dockerUtils_1.dockerPtyCLI)(infoParams, ...args);
}
else {
const infoParams = { ...(0, dockerUtils_1.toExecParameters)(params), output: (0, log_1.makeLog)(output, log_1.LogLevel.Info), print: 'continuous' };
await (0, dockerUtils_1.dockerCLI)(infoParams, ...args);
}
return { updatedImageName, collapsedFeaturesConfig, imageDetails };
}
exports.extendImage = extendImage;
async function getExtendImageBuildInfo(params, config, baseName, imageUser, imageLabelDetails) {
const featuresConfig = await (0, containerFeaturesConfiguration_1.generateFeaturesConfig)(params.common, (await (0, utils_1.createFeaturesTempFolder)(params.common)), config, imageLabelDetails, containerFeaturesConfiguration_1.getContainerFeaturesFolder);
if (!featuresConfig) {
return null;
}
const collapsedFeaturesConfig = (0, containerFeaturesConfiguration_1.collapseFeaturesConfig)(featuresConfig);
const featureBuildInfo = await getContainerFeaturesBuildInfo(params, featuresConfig, baseName, imageUser);
if (!featureBuildInfo) {
return null;
}
return { featureBuildInfo, collapsedFeaturesConfig };
}
exports.getExtendImageBuildInfo = getExtendImageBuildInfo;
// NOTE: only exported to enable testing. Not meant to be called outside file.

@@ -60,18 +122,12 @@ function generateContainerEnvs(featuresConfig) {

exports.generateContainerEnvs = generateContainerEnvs;
async function addContainerFeatures(params, featuresConfig, imageName, imageDetails) {
async function getContainerFeaturesBuildInfo(params, featuresConfig, baseName, imageUser) {
const { common } = params;
const { cliHost, output } = common;
if (!featuresConfig) {
return imageName;
}
const { dstFolder } = featuresConfig;
if (!dstFolder || dstFolder === '') {
output.write('dstFolder is undefined or empty in addContainerFeatures', log_1.LogLevel.Error);
return imageName;
return null;
}
// Calculate name of the build folder where localcache has been copied to.
const localCacheBuildFolderName = (0, containerFeaturesConfiguration_1.getSourceInfoString)({ type: 'local-cache' });
const imageUser = (await imageDetails()).Config.User || 'root';
const folderImageName = (0, utils_1.getFolderImageName)(common);
const updatedImageName = `${imageName.startsWith(folderImageName) ? imageName : folderImageName}-features`;
const srcFolder = (0, containerFeaturesConfiguration_1.getContainerFeaturesFolder)(common.extensionPath);

@@ -123,8 +179,21 @@ output.write(`local container features stored at: ${srcFolder}`);

}, Promise.resolve({})) : Promise.resolve({})));
// With Buildkit, we can supply an additional build context to provide access to
// the container-features content.
// For non-Buildkit, we build a temporary image to hold the container-features content in a way
// that is accessible from the docker build for non-BuiltKit builds
// TODO generate an image name that is specific to this dev container?
const buildContentImageName = 'dev_container_feature_content_temp';
// When copying via buildkit, the content is accessed via '.' (i.e. in the context root)
// When copying via temp image, the content is in '/tmp/build-features'
const contentSourceRootPath = params.useBuildKit ? '.' : '/tmp/build-features/';
const dockerfile = (0, containerFeaturesConfiguration_1.getContainerFeaturesBaseDockerFile)()
.replace('#{featureBuildStages}', getFeatureBuildStages(cliHost, featuresConfig, buildStageScripts))
.replace('#{nonBuildKitFeatureContentFallback}', params.useBuildKit ? '' : `FROM ${buildContentImageName} as dev_containers_feature_content_source`)
.replace('{contentSourceRootPath}', contentSourceRootPath)
.replace('#{featureBuildStages}', getFeatureBuildStages(featuresConfig, buildStageScripts, contentSourceRootPath))
.replace('#{featureLayer}', (0, containerFeaturesConfiguration_1.getFeatureLayers)(featuresConfig))
.replace('#{containerEnv}', generateContainerEnvs(featuresConfig))
.replace('#{copyFeatureBuildStages}', getCopyFeatureBuildStages(featuresConfig, buildStageScripts));
await cliHost.writeFile(cliHost.path.join(dstFolder, 'Dockerfile'), Buffer.from(dockerfile));
const dockerfilePrefixContent = `${params.useBuildKit ? '# syntax=docker/dockerfile:1.4' : ''}
ARG _DEV_CONTAINERS_BASE_IMAGE=mcr.microsoft.com/vscode/devcontainers/base:buster
`;
// Build devcontainer-features.env file(s) for each features source folder

@@ -151,14 +220,38 @@ await Promise.all([...featuresConfig.featureSets].map(async (featureSet, i) => {

}));
const args = [
'build',
'-t', updatedImageName,
'--build-arg', `BASE_IMAGE=${imageName}`,
'--build-arg', `IMAGE_USER=${imageUser}`,
// For non-BuildKit, build the temporary image for the container-features content
if (!params.useBuildKit) {
const buildContentDockerfile = `
FROM scratch
COPY . /tmp/build-features/
`;
const buildContentDockerfilePath = cliHost.path.join(dstFolder, 'Dockerfile.buildContent');
await cliHost.writeFile(buildContentDockerfilePath, Buffer.from(buildContentDockerfile));
const buildContentArgs = [
'build',
'-t', buildContentImageName,
'-f', buildContentDockerfilePath,
];
buildContentArgs.push(dstFolder);
if (process.stdin.isTTY) {
const buildContentInfoParams = { ...(0, dockerUtils_1.toPtyExecParameters)(params), output: (0, log_1.makeLog)(output, log_1.LogLevel.Info) };
await (0, dockerUtils_1.dockerPtyCLI)(buildContentInfoParams, ...buildContentArgs);
}
else {
const buildContentInfoParams = { ...(0, dockerUtils_1.toExecParameters)(params), output: (0, log_1.makeLog)(output, log_1.LogLevel.Info), print: 'continuous' };
await (0, dockerUtils_1.dockerCLI)(buildContentInfoParams, ...buildContentArgs);
}
}
return {
dstFolder,
];
const infoParams = { ...(0, dockerUtils_1.toPtyExecParameters)(params), output: (0, log_1.makeLog)(output, log_1.LogLevel.Info) };
await (0, dockerUtils_1.dockerPtyCLI)(infoParams, ...args);
return updatedImageName;
dockerfileContent: dockerfile,
dockerfilePrefixContent,
buildArgs: {
_DEV_CONTAINERS_BASE_IMAGE: baseName,
_DEV_CONTAINERS_IMAGE_USER: imageUser,
_DEV_CONTAINERS_FEATURE_CONTENT_SOURCE: buildContentImageName,
},
buildKitContexts: params.useBuildKit ? { dev_containers_feature_content_source: dstFolder } : {},
};
}
function getFeatureBuildStages(cliHost, featuresConfig, buildStageScripts) {
function getFeatureBuildStages(featuresConfig, buildStageScripts, contentSourceRootPath) {
return [].concat(...featuresConfig.featureSets

@@ -168,4 +261,4 @@ .map((featureSet, i) => featureSet.features

.map(f => `FROM mcr.microsoft.com/vscode/devcontainers/base:0-focal as ${(0, containerFeaturesConfiguration_1.getSourceInfoString)(featureSet.sourceInformation)}_${f.id}
COPY ${cliHost.path.join('.', (0, containerFeaturesConfiguration_1.getSourceInfoString)(featureSet.sourceInformation), 'features', f.id)} ${path.posix.join('/tmp/build-features', (0, containerFeaturesConfiguration_1.getSourceInfoString)(featureSet.sourceInformation), 'features', f.id)}
COPY ${cliHost.path.join('.', (0, containerFeaturesConfiguration_1.getSourceInfoString)(featureSet.sourceInformation), 'common')} ${path.posix.join('/tmp/build-features', (0, containerFeaturesConfiguration_1.getSourceInfoString)(featureSet.sourceInformation), 'common')}
COPY --from=dev_containers_feature_content_source ${path.posix.join(contentSourceRootPath, (0, containerFeaturesConfiguration_1.getSourceInfoString)(featureSet.sourceInformation), 'features', f.id)} ${path.posix.join('/tmp/build-features', (0, containerFeaturesConfiguration_1.getSourceInfoString)(featureSet.sourceInformation), 'features', f.id)}
COPY --from=dev_containers_feature_content_source ${path.posix.join(contentSourceRootPath, (0, containerFeaturesConfiguration_1.getSourceInfoString)(featureSet.sourceInformation), 'common')} ${path.posix.join('/tmp/build-features', (0, containerFeaturesConfiguration_1.getSourceInfoString)(featureSet.sourceInformation), 'common')}
RUN cd ${path.posix.join('/tmp/build-features', (0, containerFeaturesConfiguration_1.getSourceInfoString)(featureSet.sourceInformation), 'features', f.id)} && set -a && . ./devcontainer-features.env && set +a && ./bin/acquire`))).join('\n\n');

@@ -172,0 +265,0 @@ }

@@ -34,2 +34,3 @@ import { DockerResolverParameters, UpdateRemoteUserUIDDefault, BindMountConsistency } from './utils';

additionalCacheFroms: string[];
useBuildKit: 'auto' | 'never';
}

@@ -36,0 +37,0 @@ export declare function launch(options: ProvisionOptions, disposables: (() => Promise<unknown> | undefined)[]): Promise<{

@@ -36,2 +36,3 @@ "use strict";

const dockerCompose_1 = require("./dockerCompose");
const dockerUtils_1 = require("../spec-shutdown/dockerUtils");
async function launch(options, disposables) {

@@ -103,2 +104,14 @@ const params = await createDockerParams(options, disposables);

const dockerComposePath = options.dockerComposePath || 'docker-compose';
const dockerComposeCLI = (0, dockerCompose_1.dockerComposeCLIConfig)({
exec: cliHost.exec,
env: cliHost.env,
output: common.output,
}, dockerPath, dockerComposePath);
const useBuildKit = options.useBuildKit === 'never' ? false : (await (0, dockerUtils_1.dockerHasBuildKit)({
cliHost,
dockerCLI: dockerPath,
dockerComposeCLI,
env: cliHost.env,
output
}));
return {

@@ -108,7 +121,3 @@ common,

dockerCLI: dockerPath,
dockerComposeCLI: (0, dockerCompose_1.dockerComposeCLIConfig)({
exec: cliHost.exec,
env: cliHost.env,
output: common.output,
}, dockerPath, dockerComposePath),
dockerComposeCLI: dockerComposeCLI,
dockerEnv: cliHost.env,

@@ -126,2 +135,3 @@ workspaceMountConsistencyDefault: workspaceMountConsistency,

additionalCacheFroms: options.additionalCacheFroms,
useBuildKit,
};

@@ -128,0 +138,0 @@ }

@@ -100,2 +100,3 @@ "use strict";

'cache-from': { type: 'string', description: 'Additional image to use as potential layer cache during image building' },
'buildkit': { choices: ['auto', 'never'], default: 'auto', description: 'Control whether BuildKit should be used' },
})

@@ -127,3 +128,3 @@ .check(argv => {

}
async function provision({ 'user-data-folder': persistedFolder, 'docker-path': dockerPath, 'docker-compose-path': dockerComposePath, 'container-data-folder': containerDataFolder, 'container-system-data-folder': containerSystemDataFolder, 'workspace-folder': workspaceFolderArg, 'workspace-mount-consistency': workspaceMountConsistency, 'mount-workspace-git-root': mountWorkspaceGitRoot, 'id-label': idLabel, config, 'override-config': overrideConfig, 'log-level': logLevel, 'log-format': logFormat, 'terminal-rows': terminalRows, 'terminal-columns': terminalColumns, 'default-user-env-probe': defaultUserEnvProbe, 'update-remote-user-uid-default': updateRemoteUserUIDDefault, 'remove-existing-container': removeExistingContainer, 'build-no-cache': buildNoCache, 'expect-existing-container': expectExistingContainer, 'skip-post-create': skipPostCreate, 'skip-non-blocking-commands': skipNonBlocking, prebuild, mount, 'remote-env': addRemoteEnv, 'cache-from': addCacheFrom, }) {
async function provision({ 'user-data-folder': persistedFolder, 'docker-path': dockerPath, 'docker-compose-path': dockerComposePath, 'container-data-folder': containerDataFolder, 'container-system-data-folder': containerSystemDataFolder, 'workspace-folder': workspaceFolderArg, 'workspace-mount-consistency': workspaceMountConsistency, 'mount-workspace-git-root': mountWorkspaceGitRoot, 'id-label': idLabel, config, 'override-config': overrideConfig, 'log-level': logLevel, 'log-format': logFormat, 'terminal-rows': terminalRows, 'terminal-columns': terminalColumns, 'default-user-env-probe': defaultUserEnvProbe, 'update-remote-user-uid-default': updateRemoteUserUIDDefault, 'remove-existing-container': removeExistingContainer, 'build-no-cache': buildNoCache, 'expect-existing-container': expectExistingContainer, 'skip-post-create': skipPostCreate, 'skip-non-blocking-commands': skipNonBlocking, prebuild, mount, 'remote-env': addRemoteEnv, 'cache-from': addCacheFrom, 'buildkit': buildkit, }) {
const workspaceFolder = workspaceFolderArg ? path.resolve(process.cwd(), workspaceFolderArg) : undefined;

@@ -167,2 +168,3 @@ const addRemoteEnvs = addRemoteEnv ? (Array.isArray(addRemoteEnv) ? addRemoteEnv : [addRemoteEnv]) : [];

additionalCacheFroms: addCacheFroms,
useBuildKit: buildkit,
};

@@ -220,2 +222,3 @@ const result = await doProvision(options);

'cache-from': { type: 'string', description: 'Additional image to use as potential layer cache' },
'buildkit': { choices: ['auto', 'never'], default: 'auto', description: 'Control whether BuildKit should be used' },
});

@@ -233,3 +236,3 @@ }

}
async function doBuild({ 'user-data-folder': persistedFolder, 'docker-path': dockerPath, 'docker-compose-path': dockerComposePath, 'workspace-folder': workspaceFolderArg, 'log-level': logLevel, 'log-format': logFormat, 'no-cache': buildNoCache, 'image-name': argImageName, 'cache-from': addCacheFrom, }) {
async function doBuild({ 'user-data-folder': persistedFolder, 'docker-path': dockerPath, 'docker-compose-path': dockerComposePath, 'workspace-folder': workspaceFolderArg, 'log-level': logLevel, 'log-format': logFormat, 'no-cache': buildNoCache, 'image-name': argImageName, 'cache-from': addCacheFrom, 'buildkit': buildkit, }) {
const disposables = [];

@@ -270,2 +273,3 @@ const dispose = async () => {

additionalCacheFroms: addCacheFroms,
useBuildKit: buildkit
}, disposables);

@@ -440,2 +444,3 @@ const { common, dockerCLI, dockerComposeCLI } = params;

additionalCacheFroms: [],
useBuildKit: 'auto'
}, disposables);

@@ -644,2 +649,3 @@ const { common } = params;

additionalCacheFroms: [],
useBuildKit: 'auto'
}, disposables);

@@ -646,0 +652,0 @@ const { common } = params;

@@ -13,3 +13,6 @@ import { ResolverResult, DockerResolverParameters, WorkspaceConfiguration } from './utils';

}>;
export declare function buildNamedImage(params: DockerResolverParameters, config: DevContainerFromDockerfileConfig | DevContainerFromImageConfig): Promise<string>;
export declare function ensureDockerfileHasFinalStageName(dockerfile: string, defaultLastStageName: string): {
lastStageName: string;
modifiedDockerfile: string | undefined;
};
export declare function findUserArg(runArgs?: string[]): string | undefined;

@@ -16,0 +19,0 @@ export declare function findExistingContainer(params: DockerResolverParameters, labels: string[]): Promise<ContainerDetails | undefined>;

@@ -6,4 +6,23 @@ "use strict";

*--------------------------------------------------------------------------------------------*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.bailOut = exports.spawnDevContainer = exports.findDevContainer = exports.findExistingContainer = exports.findUserArg = exports.buildNamedImage = exports.buildNamedImageAndExtend = exports.openDockerfileDevContainer = exports.hostFolderLabel = void 0;
exports.bailOut = exports.spawnDevContainer = exports.findDevContainer = exports.findExistingContainer = exports.findUserArg = exports.ensureDockerfileHasFinalStageName = exports.buildNamedImageAndExtend = exports.openDockerfileDevContainer = exports.hostFolderLabel = void 0;
const utils_1 = require("./utils");

@@ -16,2 +35,3 @@ const injectHeadless_1 = require("../spec-common/injectHeadless");

const product_1 = require("../spec-utils/product");
const path = __importStar(require("path"));
exports.hostFolderLabel = 'devcontainer.local_folder'; // used to label containers created from a workspace/folder

@@ -95,16 +115,143 @@ async function openDockerfileDevContainer(params, config, workspaceConfig, idLabels) {

async function buildNamedImageAndExtend(params, config) {
const baseImageName = await buildNamedImage(params, config);
return await (0, containerFeatures_1.extendImage)(params, config, baseImageName, 'image' in config);
}
exports.buildNamedImageAndExtend = buildNamedImageAndExtend;
async function buildNamedImage(params, config) {
var _a;
const imageName = 'image' in config ? config.image : (0, utils_1.getFolderImageName)(params.common);
params.common.progress(injectHeadless_1.ResolverProgress.BuildingImage);
if ((0, utils_1.isDockerFileConfig)(config)) {
params.common.progress(injectHeadless_1.ResolverProgress.BuildingImage);
await buildImage(params, config, imageName, (_a = params.buildNoCache) !== null && _a !== void 0 ? _a : false);
return await buildAndExtendImage(params, config, imageName, (_a = params.buildNoCache) !== null && _a !== void 0 ? _a : false);
}
return imageName;
// image-based dev container - extend
return await (0, containerFeatures_1.extendImage)(params, config, imageName, 'image' in config);
}
exports.buildNamedImage = buildNamedImage;
exports.buildNamedImageAndExtend = buildNamedImageAndExtend;
async function buildAndExtendImage(buildParams, config, baseImageName, noCache) {
var _a, _b, _c, _d;
const { cliHost, output } = buildParams.common;
const dockerfileUri = (0, utils_1.getDockerfilePath)(cliHost, config);
const dockerfilePath = await (0, utils_1.uriToWSLFsPath)(dockerfileUri, cliHost);
if (!cliHost.isFile(dockerfilePath)) {
throw new errors_1.ContainerError({ description: `Dockerfile (${dockerfilePath}) not found.` });
}
let dockerfile = (await cliHost.readFile(dockerfilePath)).toString();
let baseName = 'dev_container_auto_added_stage_label';
if ((_a = config.build) === null || _a === void 0 ? void 0 : _a.target) {
// Explictly set build target for the dev container build features on that
baseName = config.build.target;
}
else {
// Use the last stage in the Dockerfile
// Find the last line that starts with "FROM" (possibly preceeded by white-space)
const { lastStageName, modifiedDockerfile } = ensureDockerfileHasFinalStageName(dockerfile, baseImageName);
baseName = lastStageName;
if (modifiedDockerfile) {
dockerfile = modifiedDockerfile;
}
}
const labelDetails = async () => { return { definition: undefined, version: undefined }; };
const extendImageBuildInfo = await (0, containerFeatures_1.getExtendImageBuildInfo)(buildParams, config, baseName, (_b = config.remoteUser) !== null && _b !== void 0 ? _b : 'root', labelDetails);
let finalDockerfilePath = dockerfilePath;
const additionalBuildArgs = [];
if (extendImageBuildInfo) {
const { featureBuildInfo } = extendImageBuildInfo;
// We add a '# syntax' line at the start, so strip out any existing line
const syntaxMatch = dockerfile.match(/^\s*#\s*syntax\s*=.*[\r\n]/g);
if (syntaxMatch) {
dockerfile = dockerfile.slice(syntaxMatch[0].length);
}
let finalDockerfileContent = `${featureBuildInfo.dockerfilePrefixContent}${dockerfile}\n${featureBuildInfo === null || featureBuildInfo === void 0 ? void 0 : featureBuildInfo.dockerfileContent}`;
finalDockerfilePath = path.posix.join(featureBuildInfo === null || featureBuildInfo === void 0 ? void 0 : featureBuildInfo.dstFolder, 'Dockerfile-with-features');
await cliHost.writeFile(finalDockerfilePath, Buffer.from(finalDockerfileContent));
// track additional build args to include below
for (const buildContext in featureBuildInfo.buildKitContexts) {
additionalBuildArgs.push('--build-context', `${buildContext}=${featureBuildInfo.buildKitContexts[buildContext]}`);
}
for (const buildArg in featureBuildInfo.buildArgs) {
additionalBuildArgs.push('--build-arg', `${buildArg}=${featureBuildInfo.buildArgs[buildArg]}`);
}
}
const args = [];
if (buildParams.useBuildKit) {
args.push('buildx', 'build', '--load', // (short for --output=docker, i.e. load into normal 'docker images' collection)
'--build-arg', 'BUILDKIT_INLINE_CACHE=1');
}
else {
args.push('build');
}
args.push('-f', finalDockerfilePath, '-t', baseImageName);
const target = (_c = config.build) === null || _c === void 0 ? void 0 : _c.target;
if (target) {
args.push('--target', target);
}
if (noCache) {
args.push('--no-cache', '--pull');
}
else if (config.build && config.build.cacheFrom) {
buildParams.additionalCacheFroms.forEach(cacheFrom => args.push('--cache-from', cacheFrom));
if (typeof config.build.cacheFrom === 'string') {
args.push('--cache-from', config.build.cacheFrom);
}
else {
for (let index = 0; index < config.build.cacheFrom.length; index++) {
const cacheFrom = config.build.cacheFrom[index];
args.push('--cache-from', cacheFrom);
}
}
}
const buildArgs = (_d = config.build) === null || _d === void 0 ? void 0 : _d.args;
if (buildArgs) {
for (const key in buildArgs) {
args.push('--build-arg', `${key}=${buildArgs[key]}`);
}
}
args.push(...additionalBuildArgs);
args.push(await (0, utils_1.uriToWSLFsPath)((0, utils_1.getDockerContextPath)(cliHost, config), cliHost));
try {
if (process.stdin.isTTY) {
const infoParams = { ...(0, dockerUtils_1.toPtyExecParameters)(buildParams), output: (0, log_1.makeLog)(output, log_1.LogLevel.Info) };
await (0, dockerUtils_1.dockerPtyCLI)(infoParams, ...args);
}
else {
const infoParams = { ...(0, dockerUtils_1.toExecParameters)(buildParams), output: (0, log_1.makeLog)(output, log_1.LogLevel.Info), print: 'continuous' };
await (0, dockerUtils_1.dockerCLI)(infoParams, ...args);
}
}
catch (err) {
throw new errors_1.ContainerError({ description: 'An error occurred building the image.', originalError: err, data: { fileWithError: dockerfilePath } });
}
const imageDetails = () => (0, utils_1.inspectDockerImage)(buildParams, baseImageName, false);
return {
updatedImageName: baseImageName,
collapsedFeaturesConfig: extendImageBuildInfo === null || extendImageBuildInfo === void 0 ? void 0 : extendImageBuildInfo.collapsedFeaturesConfig,
imageDetails
};
}
// not expected to be called externally (exposed for testing)
function ensureDockerfileHasFinalStageName(dockerfile, defaultLastStageName) {
var _a, _b;
// Find the last line that starts with "FROM" (possibly preceeded by white-space)
const fromLines = [...dockerfile.matchAll(new RegExp(/^(?<line>\s*FROM.*)/, 'gm'))];
const lastFromLineMatch = fromLines[fromLines.length - 1];
const lastFromLine = (_a = lastFromLineMatch.groups) === null || _a === void 0 ? void 0 : _a.line;
// Test for "FROM [--platform=someplat] base [as label]"
// That is, match against optional platform and label
const fromMatch = lastFromLine.match(/FROM\s+(?<platform>--platform=\S+\s+)?\S+(\s+[Aa][Ss]\s+(?<label>[^\s]+))?/);
if (!fromMatch) {
throw new Error('Error parsing Dockerfile: failed to parse final FROM line');
}
if ((_b = fromMatch.groups) === null || _b === void 0 ? void 0 : _b.label) {
return {
lastStageName: fromMatch.groups.label,
modifiedDockerfile: undefined,
};
}
// Last stage doesn't have a name, so modify the Dockerfile to set the name to defaultLastStageName
const lastLineStartIndex = lastFromLineMatch.index + fromMatch.index;
const lastLineEndIndex = lastLineStartIndex + lastFromLine.length;
const matchedFromText = fromMatch[0];
let modifiedDockerfile = dockerfile.slice(0, lastLineStartIndex + matchedFromText.length);
modifiedDockerfile += ` AS ${defaultLastStageName}`;
const remainingFromLineLength = lastFromLine.length - matchedFromText.length;
modifiedDockerfile += dockerfile.slice(lastLineEndIndex - remainingFromLineLength);
return { lastStageName: defaultLastStageName, modifiedDockerfile: modifiedDockerfile };
}
exports.ensureDockerfileHasFinalStageName = ensureDockerfileHasFinalStageName;
function findUserArg(runArgs = []) {

@@ -160,45 +307,2 @@ for (let i = runArgs.length - 1; i >= 0; i--) {

exports.findDevContainer = findDevContainer;
async function buildImage(buildParams, config, baseImageName, noCache) {
var _a, _b;
const { cliHost, output } = buildParams.common;
const dockerfileUri = (0, utils_1.getDockerfilePath)(cliHost, config);
const dockerfilePath = await (0, utils_1.uriToWSLFsPath)(dockerfileUri, cliHost);
if (!cliHost.isFile(dockerfilePath)) {
throw new errors_1.ContainerError({ description: `Dockerfile (${dockerfilePath}) not found.` });
}
const args = ['build', '-f', dockerfilePath, '-t', baseImageName];
const target = (_a = config.build) === null || _a === void 0 ? void 0 : _a.target;
if (target) {
args.push('--target', target);
}
if (noCache) {
args.push('--no-cache', '--pull');
}
else if (config.build && config.build.cacheFrom) {
if (typeof config.build.cacheFrom === 'string') {
args.push('--cache-from', config.build.cacheFrom);
}
else {
for (let index = 0; index < config.build.cacheFrom.length; index++) {
const cacheFrom = config.build.cacheFrom[index];
args.push('--cache-from', cacheFrom);
}
}
}
buildParams.additionalCacheFroms.forEach(cacheFrom => args.push('--cache-from', cacheFrom));
const buildArgs = (_b = config.build) === null || _b === void 0 ? void 0 : _b.args;
if (buildArgs) {
for (const key in buildArgs) {
args.push('--build-arg', `${key}=${buildArgs[key]}`);
}
}
args.push(await (0, utils_1.uriToWSLFsPath)((0, utils_1.getDockerContextPath)(cliHost, config), cliHost));
try {
const infoParams = { ...(0, dockerUtils_1.toPtyExecParameters)(buildParams), output: (0, log_1.makeLog)(output, log_1.LogLevel.Info) };
await (0, dockerUtils_1.dockerPtyCLI)(infoParams, ...args);
}
catch (err) {
throw new errors_1.ContainerError({ description: 'An error occurred building the image.', originalError: err, data: { fileWithError: dockerfilePath } });
}
}
async function spawnDevContainer(params, config, collapsedFeaturesConfig, imageName, labels, workspaceMount, imageDetails) {

@@ -205,0 +309,0 @@ const { common } = params;

@@ -38,2 +38,3 @@ /// <reference types="node" />

additionalCacheFroms: string[];
useBuildKit: boolean;
}

@@ -40,0 +41,0 @@ export interface ResolverResult {

@@ -52,3 +52,3 @@ /// <reference types="node" />

output: Log;
print?: boolean;
print?: boolean | 'continuous';
}

@@ -101,2 +101,3 @@ export interface PartialPtyExecParameters {

export declare function getEvents(params: DockerCLIParameters | DockerResolverParameters, filters?: Record<string, string[]>): Promise<Exec>;
export declare function dockerHasBuildKit(params: DockerCLIParameters | PartialExecParameters | DockerResolverParameters): Promise<boolean>;
export declare function dockerCLI(params: DockerCLIParameters | PartialExecParameters | DockerResolverParameters, ...args: string[]): Promise<{

@@ -103,0 +104,0 @@ stdout: Buffer;

@@ -7,3 +7,3 @@ "use strict";

Object.defineProperty(exports, "__esModule", { value: true });
exports.toDockerImageName = exports.toPtyExecParameters = exports.toExecParameters = exports.dockerPtyExecFunction = exports.dockerExecFunction = exports.dockerComposePtyCLI = exports.dockerComposeCLI = exports.dockerPtyCLI = exports.isPodman = exports.dockerContext = exports.dockerCLI = exports.getEvents = exports.createVolume = exports.listVolumes = exports.listContainers = exports.inspectVolumes = exports.inspectVolume = exports.inspectImage = exports.inspectContainers = exports.inspectContainer = void 0;
exports.toDockerImageName = exports.toPtyExecParameters = exports.toExecParameters = exports.dockerPtyExecFunction = exports.dockerExecFunction = exports.dockerComposePtyCLI = exports.dockerComposeCLI = exports.dockerPtyCLI = exports.isPodman = exports.dockerContext = exports.dockerCLI = exports.dockerHasBuildKit = exports.getEvents = exports.createVolume = exports.listVolumes = exports.listContainers = exports.inspectVolumes = exports.inspectVolume = exports.inspectImage = exports.inspectContainers = exports.inspectContainer = void 0;
const commonUtils_1 = require("../spec-common/commonUtils");

@@ -135,2 +135,12 @@ const errors_1 = require("../spec-common/errors");

exports.getEvents = getEvents;
async function dockerHasBuildKit(params) {
try {
await dockerCLI(params, 'buildx', 'version');
return true;
}
catch {
return false;
}
}
exports.dockerHasBuildKit = dockerHasBuildKit;
async function dockerCLI(params, ...args) {

@@ -137,0 +147,0 @@ const partial = toExecParameters(params);

{
"name": "@devcontainers/cli",
"description": "Dev Containers CLI",
"version": "0.1.0",
"version": "0.2.0",
"bin": {

@@ -6,0 +6,0 @@ "devcontainer": "devcontainer.js"

@@ -23,17 +23,15 @@ # Dev Container CLI

You can try out the CLI in just a few steps. This repository has a [dev container configuration](https://github.com/devcontainers/cli/tree/main/.devcontainer), which you can use to ensure you have the right dependencies installed.
We'd love for you to try out the dev container CLI and let us know what you think. You can quickly try it out in just a few simple steps, either by installing its npm package or building the CLI repo from sources (see "[Build from sources](#build-from-sources)").
Compile the CLI with yarn:
```sh
yarn
yarn compile
To install the npm package you will need Python and C/C++ installed to build one of the dependencies (see, e.g., [here](https://github.com/microsoft/vscode/wiki/How-to-Contribute) for instructions).
### npm install
```bash
npm install -g @devcontainers/cli
```
Verify you can run the CLI and see its help text:
```sh
node devcontainer.js --help
```
Outputs:
```
```bash
devcontainer <command>

@@ -53,6 +51,11 @@

You can try out the dev container CLI with a sample project, like this [Rust sample](https://github.com/microsoft/vscode-remote-try-rust). Clone the Rust sample to the repo's parent folder and start a dev container:
```sh
git clone https://github.com/microsoft/vscode-remote-try-rust ../vscode-remote-try-rust
node devcontainer.js up --workspace-folder ../vscode-remote-try-rust
### Try out the CLI
Once you have the CLI, you can try it out with a sample project, like this [Rust sample](https://github.com/microsoft/vscode-remote-try-rust).
Clone the Rust sample to your machine, and start a dev container with the CLI's `up` command:
```bash
git clone https://github.com/microsoft/vscode-remote-try-rust
devcontainer up --workspace-folder <path-to-vscode-remote-try-rust>
```

@@ -62,6 +65,6 @@

```
```bash
[88 ms] dev-containers-cli 0.1.0.
[165 ms] Start: Run: docker build -f /home/node/vscode-remote-try-rust/.devcontainer/Dockerfile -t vsc-vscode-remote-try-rust-89420ad7399ba74f55921e49cc3ecfd2 --build-arg VARIANT=bullseye /home/node/vscode-remote-try-rust/.devcontainer
[+] Building 0.5s (5/5) FINISHED
[+] Building 0.5s (5/5) FINISHED
=> [internal] load build definition from Dockerfile 0.0s

@@ -82,11 +85,11 @@ => => transferring dockerfile: 38B 0.0s

You can then run some command in this dev container:
You can then run commands in this dev container:
```sh
node devcontainer.js exec --workspace-folder ../vscode-remote-try-rust cargo run
```bash
devcontainer exec --workspace-folder <path-to-vscode-remote-try-rust> cargo run
```
This will compile and run the Rust sample:
This will compile and run the Rust sample, outputting:
```
```bash
[33 ms] dev-containers-cli 0.1.0.

@@ -100,2 +103,19 @@ Compiling hello_remote_world v0.1.0 (/workspaces/vscode-remote-try-rust)

Congrats, you've just run the dev container CLI and seen it in action!
## Build from sources
This repository has a [dev container configuration](https://github.com/devcontainers/cli/tree/main/.devcontainer), which you can use to ensure you have the right dependencies installed.
Compile the CLI with yarn:
```sh
yarn
yarn compile
```
Verify you can run the CLI and see its help text:
```sh
node devcontainer.js --help
```
## Specification

@@ -102,0 +122,0 @@

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc