Skip to content

Commit

Permalink
Use previous USER (#70)
Browse files Browse the repository at this point in the history
  • Loading branch information
chrmarti committed Jul 1, 2022
1 parent 60dd3bd commit c67f5c8
Show file tree
Hide file tree
Showing 5 changed files with 79 additions and 16 deletions.
8 changes: 4 additions & 4 deletions src/spec-node/containerFeatures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export async function extendImage(params: DockerResolverParameters, config: DevC
};
};

const imageUser = (await imageDetails()).Config.User || 'root';
const imageUser = async () => (await imageDetails()).Config.User || 'root';
const extendImageDetails = await getExtendImageBuildInfo(params, config, imageName, imageUser, imageLabelDetails);
if (!extendImageDetails || !extendImageDetails.featureBuildInfo) {
// no feature extensions - return
Expand Down Expand Up @@ -96,7 +96,7 @@ export async function extendImage(params: DockerResolverParameters, config: DevC
return { updatedImageName, collapsedFeaturesConfig, imageDetails };
}

export async function getExtendImageBuildInfo(params: DockerResolverParameters, config: DevContainerConfig, baseName: string, imageUser: string, imageLabelDetails: () => Promise<{ definition: string | undefined; version: string | undefined }>) {
export async function getExtendImageBuildInfo(params: DockerResolverParameters, config: DevContainerConfig, baseName: string, imageUser: () => Promise<string>, imageLabelDetails: () => Promise<{ definition: string | undefined; version: string | undefined }>) {

const featuresConfig = await generateFeaturesConfig(params.common, (await createFeaturesTempFolder(params.common)), config, imageLabelDetails, getContainerFeaturesFolder);
if (!featuresConfig) {
Expand Down Expand Up @@ -125,7 +125,7 @@ export function generateContainerEnvs(featuresConfig: FeaturesConfig) {
return result;
}

async function getContainerFeaturesBuildInfo(params: DockerResolverParameters, featuresConfig: FeaturesConfig, baseName: string, imageUser: string): Promise<{ dstFolder: string; dockerfileContent: string; dockerfilePrefixContent: string; buildArgs: Record<string, string>; buildKitContexts: Record<string, string> } | null> {
async function getContainerFeaturesBuildInfo(params: DockerResolverParameters, featuresConfig: FeaturesConfig, baseName: string, imageUser: () => Promise<string>): Promise<{ dstFolder: string; dockerfileContent: string; dockerfilePrefixContent: string; buildArgs: Record<string, string>; buildKitContexts: Record<string, string> } | null> {
const { common } = params;
const { cliHost, output } = common;
const { dstFolder } = featuresConfig;
Expand Down Expand Up @@ -264,7 +264,7 @@ ARG _DEV_CONTAINERS_BASE_IMAGE=placeholder
dockerfilePrefixContent,
buildArgs: {
_DEV_CONTAINERS_BASE_IMAGE: baseName,
_DEV_CONTAINERS_IMAGE_USER: imageUser,
_DEV_CONTAINERS_IMAGE_USER: await imageUser(),
_DEV_CONTAINERS_FEATURE_CONTENT_SOURCE: buildContentImageName,
},
buildKitContexts: useBuildKitBuildContexts ? { dev_containers_feature_content_source: dstFolder } : {},
Expand Down
11 changes: 6 additions & 5 deletions src/spec-node/dockerCompose.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import * as yaml from 'js-yaml';
import * as shellQuote from 'shell-quote';

import { createContainerProperties, startEventSeen, ResolverResult, getTunnelInformation, DockerResolverParameters, inspectDockerImage, ensureDockerfileHasFinalStageName } from './utils';
import { createContainerProperties, startEventSeen, ResolverResult, getTunnelInformation, DockerResolverParameters, inspectDockerImage, ensureDockerfileHasFinalStageName, getImageUser } from './utils';
import { ContainerProperties, setupInContainer, ResolverProgress } from '../spec-common/injectHeadless';
import { ContainerError } from '../spec-common/errors';
import { Workspace } from '../spec-utils/workspaces';
Expand Down Expand Up @@ -150,12 +150,13 @@ export async function buildAndExtendDockerCompose(config: DevContainerFromDocker

// determine base imageName for generated features build stage(s)
let baseName = 'dev_container_auto_added_stage_label';
let dockerfile = null;
let dockerfile: string;
let originalDockerfile: string;
const serviceInfo = getBuildInfoForService(composeService, cliHost.path, localComposeFiles);
if (serviceInfo.build) {
const { context, dockerfilePath, target } = serviceInfo.build;
const resolvedDockerfilePath = cliHost.path.isAbsolute(dockerfilePath) ? dockerfilePath : path.resolve(context, dockerfilePath);
dockerfile = (await cliHost.readFile(resolvedDockerfilePath)).toString();
dockerfile = originalDockerfile = (await cliHost.readFile(resolvedDockerfilePath)).toString();
if (target) {
// Explictly set build target for the dev container build features on that
baseName = target;
Expand All @@ -169,13 +170,13 @@ export async function buildAndExtendDockerCompose(config: DevContainerFromDocker
}
}
} else {
dockerfile = `FROM ${composeService.image} AS ${baseName}\n`;
dockerfile = originalDockerfile = `FROM ${composeService.image} AS ${baseName}\n`;
}

// determine whether we need to extend with features
const labelDetails = async () => { return { definition: undefined, version: undefined }; };
const noBuildKitParams = { ...params, buildKitVersion: null }; // skip BuildKit -> can't set additional build contexts with compose
const extendImageBuildInfo = await getExtendImageBuildInfo(noBuildKitParams, config, baseName, config.remoteUser ?? 'root', labelDetails);
const extendImageBuildInfo = await getExtendImageBuildInfo(noBuildKitParams, config, baseName, () => getImageUser(params, originalDockerfile), labelDetails);

let buildOverrideContent = null;
if (extendImageBuildInfo) {
Expand Down
5 changes: 3 additions & 2 deletions src/spec-node/singleContainer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/


import { createContainerProperties, startEventSeen, ResolverResult, getTunnelInformation, getDockerfilePath, getDockerContextPath, DockerResolverParameters, isDockerFileConfig, uriToWSLFsPath, WorkspaceConfiguration, getFolderImageName, inspectDockerImage, ensureDockerfileHasFinalStageName } from './utils';
import { createContainerProperties, startEventSeen, ResolverResult, getTunnelInformation, getDockerfilePath, getDockerContextPath, DockerResolverParameters, isDockerFileConfig, uriToWSLFsPath, WorkspaceConfiguration, getFolderImageName, inspectDockerImage, ensureDockerfileHasFinalStageName, getImageUser } from './utils';
import { ContainerProperties, setupInContainer, ResolverProgress } from '../spec-common/injectHeadless';
import { ContainerError, toErrorText } from '../spec-common/errors';
import { ContainerDetails, listContainers, DockerCLIParameters, inspectContainers, dockerCLI, dockerPtyCLI, toPtyExecParameters, ImageDetails, toExecParameters } from '../spec-shutdown/dockerUtils';
Expand Down Expand Up @@ -122,6 +122,7 @@ async function buildAndExtendImage(buildParams: DockerResolverParameters, config
}

let dockerfile = (await cliHost.readFile(dockerfilePath)).toString();
const originalDockerfile = dockerfile;
let baseName = 'dev_container_auto_added_stage_label';
if (config.build?.target) {
// Explictly set build target for the dev container build features on that
Expand All @@ -137,7 +138,7 @@ async function buildAndExtendImage(buildParams: DockerResolverParameters, config
}

const labelDetails = async () => { return { definition: undefined, version: undefined }; };
const extendImageBuildInfo = await getExtendImageBuildInfo(buildParams, config, baseName, config.remoteUser ?? 'root', labelDetails);
const extendImageBuildInfo = await getExtendImageBuildInfo(buildParams, config, baseName, () => getImageUser(buildParams, originalDockerfile), labelDetails);

let finalDockerfilePath = dockerfilePath;
const additionalBuildArgs: string[] = [];
Expand Down
33 changes: 30 additions & 3 deletions src/spec-node/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { ContainerProperties, getContainerProperties, ResolverParameters } from
import { Workspace } from '../spec-utils/workspaces';
import { URI } from 'vscode-uri';
import { ShellServer } from '../spec-common/shellServer';
import { inspectContainer, inspectImage, getEvents, ContainerDetails, DockerCLIParameters, dockerExecFunction, dockerPtyCLI, dockerPtyExecFunction, toDockerImageName, DockerComposeCLI } from '../spec-shutdown/dockerUtils';
import { inspectContainer, inspectImage, getEvents, ContainerDetails, DockerCLIParameters, dockerExecFunction, dockerPtyCLI, dockerPtyExecFunction, toDockerImageName, DockerComposeCLI, ImageDetails } from '../spec-shutdown/dockerUtils';
import { getRemoteWorkspaceFolder } from './dockerCompose';
import { findGitRootFolder } from '../spec-common/git';
import { parentURI, uriToFsPath } from '../spec-configuration/configurationCommonUtils';
Expand Down Expand Up @@ -332,17 +332,44 @@ export function getLocalCacheFolder(): string {
return path.join(os.tmpdir(), process.platform === 'linux' ? `vsch-${os.userInfo().username}` : 'vsch');
}

const findFromLines = new RegExp(/^(?<line>\s*FROM.*)/, 'gm');
const parseFromLine = /FROM\s+(?<platform>--platform=\S+\s+)?(?<image>\S+)(\s+[Aa][Ss]\s+(?<label>[^\s]+))?/;
const findUserLines = new RegExp(/^\s*USER\s+(?<user>\S+)/, 'gm');

export async function getImageUser(params: DockerResolverParameters, dockerfile: string) {
return internalGetImageUser(imageName => inspectDockerImage(params, imageName, true), dockerfile);
}

export async function internalGetImageUser(inspectDockerImage: (imageName: string) => Promise<ImageDetails>, dockerfile: string) {
// TODO: Other targets.
const userLines = [...dockerfile.matchAll(findUserLines)];
if (userLines.length) {
const user = userLines[userLines.length - 1].groups?.user;
if (user && user.indexOf('$') === -1) { // Ignore variables.
return user;
}
}
const fromLine = [...dockerfile.matchAll(findFromLines)][0];
const fromMatch = fromLine?.groups?.line?.match(parseFromLine);
const imageName = fromMatch?.groups?.image;
if (!imageName) {
return 'root';
}
const imageDetails = await inspectDockerImage(imageName);
return imageDetails.Config.User || 'root';
}

// not expected to be called externally (exposed for testing)
export function ensureDockerfileHasFinalStageName(dockerfile: string, defaultLastStageName: string): { lastStageName: string; modifiedDockerfile: string | undefined } {

// Find the last line that starts with "FROM" (possibly preceeded by white-space)
const fromLines = [...dockerfile.matchAll(new RegExp(/^(?<line>\s*FROM.*)/, 'gm'))];
const fromLines = [...dockerfile.matchAll(findFromLines)];
const lastFromLineMatch = fromLines[fromLines.length - 1];
const lastFromLine = lastFromLineMatch.groups?.line as string;

// 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]+))?/);
const fromMatch = lastFromLine.match(parseFromLine);
if (!fromMatch) {
throw new Error('Error parsing Dockerfile: failed to parse final FROM line');
}
Expand Down
38 changes: 36 additions & 2 deletions src/test/dockerfileUtils.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { assert } from 'chai';
import { ensureDockerfileHasFinalStageName } from '../spec-node/utils';
import { ensureDockerfileHasFinalStageName, internalGetImageUser } from '../spec-node/utils';
import { ImageDetails } from '../spec-shutdown/dockerUtils';

describe('ensureDockerfileHasFinalStageName', () => {

Expand Down Expand Up @@ -139,4 +140,37 @@ RUN another command
});
});
});
});
});

describe('getImageUser', () => {

it('for a simple FROM line', async () => {
const dockerfile = `FROM debian:latest as base
FROM ubuntu:latest as dev
`;
const details: ImageDetails = {
Id: '123',
Config: {
User: 'imageUser',
Env: null,
Labels: null,
Entrypoint: null,
Cmd: null
}
};
assert.strictEqual(await internalGetImageUser(async imageName => {
assert.strictEqual(imageName, 'debian:latest');
return details;
}, dockerfile), 'imageUser');
});

it('for a USER', async () => {
const dockerfile = `FROM ubuntu:latest as base
USER dockerfileUserA
USER dockerfileUserB
`;
assert.strictEqual(await internalGetImageUser(() => {
throw new Error('ooops');
}, dockerfile), 'dockerfileUserB');
});
});

0 comments on commit c67f5c8

Please sign in to comment.