Skip to content

Commit 4e35efe

Browse files
authored
merge conflict;
2 parents c000b10 + 4705707 commit 4e35efe

10 files changed

Lines changed: 120 additions & 40 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22

33
Notable changes.
44

5+
## February 2023
6+
7+
### [0.30.0]
8+
9+
- Fix: Merge metadata logic for containerEnv for `devcontainer build`. (https://github.com/devcontainers/cli/pull/392)
10+
- Support querying registries that Accept application/vnd.oci.image.index.v1+json. (https://github.com/devcontainers/cli/pull/393)
11+
- Updates Features cache logic - Incrementally copy features near the layer they're installed. (https://github.com/devcontainers/cli/pull/382)
12+
513
## January 2023
614

715
### [0.29.0]

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@devcontainers/cli",
33
"description": "Dev Containers CLI",
4-
"version": "0.29.0",
4+
"version": "0.30.0",
55
"bin": {
66
"devcontainer": "devcontainer.js"
77
},

src/spec-configuration/containerFeaturesConfiguration.ts

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -239,22 +239,22 @@ export function getSourceInfoString(srcInfo: SourceInformation): string {
239239
}
240240

241241
// TODO: Move to node layer.
242-
export function getContainerFeaturesBaseDockerFile() {
242+
export function getContainerFeaturesBaseDockerFile(destBasePath = '/opt/build-features/') {
243243
return `
244244
#{featureBuildStages}
245245
246246
#{nonBuildKitFeatureContentFallback}
247247
248248
FROM $_DEV_CONTAINERS_BASE_IMAGE AS dev_containers_feature_content_normalize
249249
USER root
250-
COPY --from=dev_containers_feature_content_source {contentSourceRootPath} /opt/build-features/
251-
RUN chmod -R 0777 /opt/build-features
250+
COPY --from=dev_containers_feature_content_source {contentSourceRootPath}/devcontainer-features.builtin.env ${destBasePath}
251+
RUN chmod -R 0777 ${destBasePath}
252252
253253
FROM $_DEV_CONTAINERS_BASE_IMAGE AS dev_containers_target_stage
254254
255255
USER root
256256
257-
COPY --from=dev_containers_feature_content_normalize /opt/build-features /opt/build-features
257+
COPY --from=dev_containers_feature_content_normalize {contentSourceRootPath} ${destBasePath}
258258
259259
#{featureLayer}
260260
@@ -333,7 +333,7 @@ function escapeQuotesForShell(input: string) {
333333
return input.replace(new RegExp(`'`, 'g'), `'\\''`);
334334
}
335335

336-
export function getFeatureLayers(featuresConfig: FeaturesConfig, containerUser: string, remoteUser: string) {
336+
export function getFeatureLayers(featuresConfig: FeaturesConfig, containerUser: string, remoteUser: string, useBuildKitBuildContexts = false, contentSourceRootPath = '/tmp/build-features/', destBasePath = '/opt/build-features/') {
337337
let result = `RUN \\
338338
echo "_CONTAINER_USER_HOME=$(getent passwd ${containerUser} | cut -d: -f6)" >> /opt/build-features/devcontainer-features.builtin.env && \\
339339
echo "_REMOTE_USER_HOME=$(getent passwd ${remoteUser} | cut -d: -f6)" >> /opt/build-features/devcontainer-features.builtin.env
@@ -343,22 +343,53 @@ echo "_REMOTE_USER_HOME=$(getent passwd ${remoteUser} | cut -d: -f6)" >> /opt/bu
343343
// Features version 1
344344
const folders = (featuresConfig.featureSets || []).filter(y => y.internalVersion !== '2').map(x => x.features[0].consecutiveId);
345345
folders.forEach(folder => {
346-
result += `RUN cd /opt/build-features/${folder} \\
346+
const source = path.posix.join(contentSourceRootPath, folder!);
347+
const dest = path.posix.join(destBasePath, folder!);
348+
if (!useBuildKitBuildContexts) {
349+
result += `COPY --chown=root:root --from=dev_containers_feature_content_source ${source} ${dest}
350+
RUN chmod -R 0777 ${dest} \\
351+
&& cd ${dest} \\
347352
&& chmod +x ./install.sh \\
348353
&& ./install.sh
349354
350355
`;
356+
} else {
357+
result += `RUN --mount=type=bind,from=dev_containers_feature_content_source,source=${source},target=/tmp/build-features-src/${folder} \\
358+
cp -ar /tmp/build-features-src/${folder} ${dest} \\
359+
&& chmod -R 0777 ${dest} \\
360+
&& cd ${dest} \\
361+
&& chmod +x ./install.sh \\
362+
&& ./install.sh
363+
364+
`;
365+
}
351366
});
352367
// Features version 2
353368
featuresConfig.featureSets.filter(y => y.internalVersion === '2').forEach(featureSet => {
354369
featureSet.features.forEach(feature => {
355370
result += generateContainerEnvs(feature);
356-
result += `
357-
RUN cd /opt/build-features/${feature.consecutiveId} \\
371+
const source = path.posix.join(contentSourceRootPath, feature.consecutiveId!);
372+
const dest = path.posix.join(destBasePath, feature.consecutiveId!);
373+
if (!useBuildKitBuildContexts) {
374+
result += `
375+
COPY --chown=root:root --from=dev_containers_feature_content_source ${source} ${dest}
376+
RUN chmod -R 0777 ${dest} \\
377+
&& cd ${dest} \\
358378
&& chmod +x ./devcontainer-features-install.sh \\
359379
&& ./devcontainer-features-install.sh
360380
361381
`;
382+
} else {
383+
result += `
384+
RUN --mount=type=bind,from=dev_containers_feature_content_source,source=${source},target=/tmp/build-features-src/${feature.consecutiveId} \\
385+
cp -ar /tmp/build-features-src/${feature.consecutiveId} ${destBasePath} \\
386+
&& chmod -R 0777 ${dest} \\
387+
&& cd ${dest} \\
388+
&& chmod +x ./devcontainer-features-install.sh \\
389+
&& ./devcontainer-features-install.sh
390+
391+
`;
392+
}
362393
});
363394
});
364395
return result;

src/spec-node/configContainer.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import * as jsonc from 'jsonc-parser';
99

1010
import { openDockerfileDevContainer } from './singleContainer';
1111
import { openDockerComposeDevContainer } from './dockerCompose';
12-
import { ResolverResult, DockerResolverParameters, isDockerFileConfig, runUserCommand, createDocuments, getWorkspaceConfiguration, BindMountConsistency, uriToFsPath, DevContainerAuthority, isDevContainerAuthority, SubstituteConfig, SubstitutedConfig, addSubstitution, envListToObj } from './utils';
12+
import { ResolverResult, DockerResolverParameters, isDockerFileConfig, runUserCommand, createDocuments, getWorkspaceConfiguration, BindMountConsistency, uriToFsPath, DevContainerAuthority, isDevContainerAuthority, SubstituteConfig, SubstitutedConfig, addSubstitution, envListToObj, findContainerAndIdLabels } from './utils';
1313
import { beforeContainerSubstitute, substitute } from '../spec-common/variableSubstitution';
1414
import { ContainerError } from '../spec-common/errors';
1515
import { Workspace, workspaceFromPath, isWorkspacePath } from '../spec-utils/workspaces';
@@ -21,19 +21,19 @@ import { DevContainerConfig, DevContainerFromDockerComposeConfig, DevContainerFr
2121

2222
export { getWellKnownDevContainerPaths as getPossibleDevContainerPaths } from '../spec-configuration/configurationCommonUtils';
2323

24-
export async function resolve(params: DockerResolverParameters, configFile: URI | undefined, overrideConfigFile: URI | undefined, idLabels: string[], additionalFeatures: Record<string, string | boolean | Record<string, string | boolean>>): Promise<ResolverResult> {
24+
export async function resolve(params: DockerResolverParameters, configFile: URI | undefined, overrideConfigFile: URI | undefined, providedIdLabels: string[] | undefined, additionalFeatures: Record<string, string | boolean | Record<string, string | boolean>>): Promise<ResolverResult> {
2525
if (configFile && !/\/\.?devcontainer\.json$/.test(configFile.path)) {
2626
throw new Error(`Filename must be devcontainer.json or .devcontainer.json (${uriToFsPath(configFile, params.common.cliHost.platform)}).`);
2727
}
2828
const parsedAuthority = params.parsedAuthority;
2929
if (!parsedAuthority || isDevContainerAuthority(parsedAuthority)) {
30-
return resolveWithLocalFolder(params, parsedAuthority, configFile, overrideConfigFile, idLabels, additionalFeatures);
30+
return resolveWithLocalFolder(params, parsedAuthority, configFile, overrideConfigFile, providedIdLabels, additionalFeatures);
3131
} else {
3232
throw new Error(`Unexpected authority: ${JSON.stringify(parsedAuthority)}`);
3333
}
3434
}
3535

36-
async function resolveWithLocalFolder(params: DockerResolverParameters, parsedAuthority: DevContainerAuthority | undefined, configFile: URI | undefined, overrideConfigFile: URI | undefined, idLabels: string[], additionalFeatures: Record<string, string | boolean | Record<string, string | boolean>>): Promise<ResolverResult> {
36+
async function resolveWithLocalFolder(params: DockerResolverParameters, parsedAuthority: DevContainerAuthority | undefined, configFile: URI | undefined, overrideConfigFile: URI | undefined, providedIdLabels: string[] | undefined, additionalFeatures: Record<string, string | boolean | Record<string, string | boolean>>): Promise<ResolverResult> {
3737
const { common, workspaceMountConsistencyDefault } = params;
3838
const { cliHost, output } = common;
3939

@@ -52,6 +52,7 @@ async function resolveWithLocalFolder(params: DockerResolverParameters, parsedAu
5252
throw new ContainerError({ description: `No dev container config and no workspace found.` });
5353
}
5454
}
55+
const idLabels = providedIdLabels || (await findContainerAndIdLabels(params, undefined, providedIdLabels, workspace?.rootFolderPath, configPath?.fsPath, params.removeOnStartup)).idLabels;
5556
const configWithRaw = addSubstitution(configs.config, config => beforeContainerSubstitute(envListToObj(idLabels), config));
5657
const { config } = configWithRaw;
5758

src/spec-node/containerFeatures.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,7 @@ async function getFeaturesBuildOptions(params: DockerResolverParameters, devCont
289289
.replace('#{nonBuildKitFeatureContentFallback}', useBuildKitBuildContexts ? '' : `FROM ${buildContentImageName} as dev_containers_feature_content_source`)
290290
.replace('{contentSourceRootPath}', contentSourceRootPath)
291291
.replace('#{featureBuildStages}', getFeatureBuildStages(featuresConfig, buildStageScripts, contentSourceRootPath))
292-
.replace('#{featureLayer}', getFeatureLayers(featuresConfig, containerUser, remoteUser))
292+
.replace('#{featureLayer}', getFeatureLayers(featuresConfig, containerUser, remoteUser, useBuildKitBuildContexts, contentSourceRootPath))
293293
.replace('#{containerEnv}', generateContainerEnvs(featuresConfig))
294294
.replace('#{copyFeatureBuildStages}', getCopyFeatureBuildStages(featuresConfig, buildStageScripts))
295295
.replace('#{devcontainerMetadata}', getDevcontainerMetadataLabel(imageMetadata, common.experimentalImageMetadata))

src/spec-node/devContainers.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,13 @@ export interface ProvisionOptions {
6565
};
6666
}
6767

68-
export async function launch(options: ProvisionOptions, idLabels: string[], disposables: (() => Promise<unknown> | undefined)[]) {
68+
export async function launch(options: ProvisionOptions, providedIdLabels: string[] | undefined, disposables: (() => Promise<unknown> | undefined)[]) {
6969
const params = await createDockerParams(options, disposables);
7070
const output = params.common.output;
7171
const text = 'Resolving Remote';
7272
const start = output.start(text);
7373

74-
const result = await resolve(params, options.configFile, options.overrideConfigFile, idLabels, options.additionalFeatures ?? {});
74+
const result = await resolve(params, options.configFile, options.overrideConfigFile, providedIdLabels, options.additionalFeatures ?? {});
7575
output.stop(text, start);
7676
const { dockerContainerId, composeProjectName } = result;
7777
return {

src/spec-node/devContainersSpecCLI.ts

Lines changed: 13 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ import yargs, { Argv } from 'yargs';
99
import * as jsonc from 'jsonc-parser';
1010

1111
import { createDockerParams, createLog, experimentalImageMetadataDefault, launch, ProvisionOptions } from './devContainers';
12-
import { SubstitutedConfig, createContainerProperties, createFeaturesTempFolder, envListToObj, inspectDockerImage, isDockerFileConfig, SubstituteConfig, addSubstitution } from './utils';
12+
import { SubstitutedConfig, createContainerProperties, createFeaturesTempFolder, envListToObj, inspectDockerImage, isDockerFileConfig, SubstituteConfig, addSubstitution, findContainerAndIdLabels } from './utils';
1313
import { URI } from 'vscode-uri';
1414
import { ContainerError } from '../spec-common/errors';
1515
import { Log, LogLevel, makeLog, mapLogLevel } from '../spec-utils/log';
16-
import { probeRemoteEnv, runLifecycleHooks, runRemoteCommand, UserEnvProbe, setupInContainer } from '../spec-common/injectHeadless';
17-
import { bailOut, buildNamedImageAndExtend, findDevContainer, hostFolderLabel } from './singleContainer';
16+
import { probeRemoteEnv, runRemoteCommand, UserEnvProbe, setupInContainer, runLifecycleHooks } from '../spec-common/injectHeadless';
17+
import { bailOut, buildNamedImageAndExtend } from './singleContainer';
1818
import { extendImage } from './containerFeatures';
1919
import { DockerCLIParameters, dockerPtyCLI, inspectContainer } from '../spec-shutdown/dockerUtils';
2020
import { buildAndExtendDockerCompose, dockerComposeCLIConfig, getDefaultImageName, getProjectName, readDockerComposeConfig, readVersionPrefix } from './dockerCompose';
@@ -193,7 +193,7 @@ async function provision({
193193
const addRemoteEnvs = addRemoteEnv ? (Array.isArray(addRemoteEnv) ? addRemoteEnv as string[] : [addRemoteEnv]) : [];
194194
const addCacheFroms = addCacheFrom ? (Array.isArray(addCacheFrom) ? addCacheFrom as string[] : [addCacheFrom]) : [];
195195
const additionalFeatures = additionalFeaturesJson ? jsonc.parse(additionalFeaturesJson) as Record<string, string | boolean | Record<string, string | boolean>> : {};
196-
const idLabels = idLabel ? (Array.isArray(idLabel) ? idLabel as string[] : [idLabel]) : getDefaultIdLabels(workspaceFolder!);
196+
const providedIdLabels = idLabel ? Array.isArray(idLabel) ? idLabel as string[] : [idLabel] : undefined;
197197
const options: ProvisionOptions = {
198198
dockerPath,
199199
dockerComposePath,
@@ -245,7 +245,7 @@ async function provision({
245245
skipPersistingCustomizationsFromFeatures: false,
246246
};
247247

248-
const result = await doProvision(options, idLabels);
248+
const result = await doProvision(options, providedIdLabels);
249249
const exitCode = result.outcome === 'error' ? 1 : 0;
250250
console.log(JSON.stringify(result));
251251
if (result.outcome === 'success') {
@@ -255,13 +255,13 @@ async function provision({
255255
process.exit(exitCode);
256256
}
257257

258-
async function doProvision(options: ProvisionOptions, idLabels: string[]) {
258+
async function doProvision(options: ProvisionOptions, providedIdLabels: string[] | undefined) {
259259
const disposables: (() => Promise<unknown> | undefined)[] = [];
260260
const dispose = async () => {
261261
await Promise.all(disposables.map(d => d()));
262262
};
263263
try {
264-
const result = await launch(options, idLabels, disposables);
264+
const result = await launch(options, providedIdLabels, disposables);
265265
return {
266266
outcome: 'success' as 'success',
267267
dispose,
@@ -758,8 +758,7 @@ async function doRunUserCommands({
758758
};
759759
try {
760760
const workspaceFolder = workspaceFolderArg ? path.resolve(process.cwd(), workspaceFolderArg) : undefined;
761-
const idLabels = idLabel ? (Array.isArray(idLabel) ? idLabel as string[] : [idLabel]) :
762-
workspaceFolder ? getDefaultIdLabels(workspaceFolder) : undefined;
761+
const providedIdLabels = idLabel ? Array.isArray(idLabel) ? idLabel as string[] : [idLabel] : undefined;
763762
const addRemoteEnvs = addRemoteEnv ? (Array.isArray(addRemoteEnv) ? addRemoteEnv as string[] : [addRemoteEnv]) : [];
764763
const configFile = configParam ? URI.file(path.resolve(process.cwd(), configParam)) : undefined;
765764
const overrideConfigFile = overrideConfig ? URI.file(path.resolve(process.cwd(), overrideConfig)) : undefined;
@@ -822,7 +821,7 @@ async function doRunUserCommands({
822821
substitute: value => substitute({ platform: cliHost.platform, env: cliHost.env }, value)
823822
};
824823

825-
const container = containerId ? await inspectContainer(params, containerId) : await findDevContainer(params, idLabels!);
824+
const { container, idLabels } = await findContainerAndIdLabels(params, containerId, providedIdLabels, workspaceFolder, configPath?.fsPath);
826825
if (!container) {
827826
bailOut(common.output, 'Dev container not found.');
828827
}
@@ -926,8 +925,7 @@ async function readConfiguration({
926925
let output: Log | undefined;
927926
try {
928927
const workspaceFolder = workspaceFolderArg ? path.resolve(process.cwd(), workspaceFolderArg) : undefined;
929-
const idLabels = idLabel ? (Array.isArray(idLabel) ? idLabel as string[] : [idLabel]) :
930-
workspaceFolder ? getDefaultIdLabels(workspaceFolder) : undefined;
928+
const providedIdLabels = idLabel ? Array.isArray(idLabel) ? idLabel as string[] : [idLabel] : undefined;
931929
const configFile = configParam ? URI.file(path.resolve(process.cwd(), configParam)) : undefined;
932930
const overrideConfigFile = overrideConfig ? URI.file(path.resolve(process.cwd(), overrideConfig)) : undefined;
933931
const cwd = workspaceFolder || process.cwd();
@@ -971,7 +969,7 @@ async function readConfiguration({
971969
env: cliHost.env,
972970
output
973971
};
974-
const container = containerId ? await inspectContainer(params, containerId) : await findDevContainer(params, idLabels!);
972+
const { container, idLabels } = await findContainerAndIdLabels(params, containerId, providedIdLabels, workspaceFolder, configPath?.fsPath);
975973
if (container) {
976974
configuration = addSubstitution(configuration, config => beforeContainerSubstitute(envListToObj(idLabels), config));
977975
configuration = addSubstitution(configuration, config => containerSubstitute(cliHost.platform, configuration.config.configFilePath, envListToObj(container.Config.Env), config));
@@ -1111,8 +1109,7 @@ export async function doExec({
11111109
};
11121110
try {
11131111
const workspaceFolder = workspaceFolderArg ? path.resolve(process.cwd(), workspaceFolderArg) : undefined;
1114-
const idLabels = idLabel ? (Array.isArray(idLabel) ? idLabel as string[] : [idLabel]) :
1115-
workspaceFolder ? getDefaultIdLabels(workspaceFolder) : undefined;
1112+
const providedIdLabels = idLabel ? Array.isArray(idLabel) ? idLabel as string[] : [idLabel] : undefined;
11161113
const addRemoteEnvs = addRemoteEnv ? (Array.isArray(addRemoteEnv) ? addRemoteEnv as string[] : [addRemoteEnv]) : [];
11171114
const configFile = configParam ? URI.file(path.resolve(process.cwd(), configParam)) : undefined;
11181115
const overrideConfigFile = overrideConfig ? URI.file(path.resolve(process.cwd(), overrideConfig)) : undefined;
@@ -1171,7 +1168,7 @@ export async function doExec({
11711168
substitute: value => substitute({ platform: cliHost.platform, env: cliHost.env }, value)
11721169
};
11731170

1174-
const container = containerId ? await inspectContainer(params, containerId) : await findDevContainer(params, idLabels!);
1171+
const { container, idLabels } = await findContainerAndIdLabels(params, containerId, providedIdLabels, workspaceFolder, configPath?.fsPath);
11751172
if (!container) {
11761173
bailOut(common.output, 'Dev container not found.');
11771174
}
@@ -1207,7 +1204,3 @@ export async function doExec({
12071204
};
12081205
}
12091206
}
1210-
1211-
function getDefaultIdLabels(workspaceFolder: string) {
1212-
return [`${hostFolderLabel}=${workspaceFolder}`];
1213-
}

src/spec-node/singleContainer.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { getDevcontainerMetadata, getImageBuildInfoFromDockerfile, getImageMetad
1515
import { ensureDockerfileHasFinalStageName } from './dockerfileUtils';
1616

1717
export const hostFolderLabel = 'devcontainer.local_folder'; // used to label containers created from a workspace/folder
18+
export const configFileLabel = 'devcontainer.config_file';
1819

1920
export async function openDockerfileDevContainer(params: DockerResolverParameters, configWithRaw: SubstitutedConfig<DevContainerFromDockerfileConfig | DevContainerFromImageConfig>, workspaceConfig: WorkspaceConfiguration, idLabels: string[], additionalFeatures: Record<string, string | boolean | Record<string, string | boolean>>): Promise<ResolverResult> {
2021
const { common } = params;

0 commit comments

Comments
 (0)