Skip to content

Commit

Permalink
Add multi build support (#24)
Browse files Browse the repository at this point in the history
  • Loading branch information
juzuluag authored Jun 21, 2022
1 parent f19cbec commit 208ebb3
Show file tree
Hide file tree
Showing 6 changed files with 119 additions and 11 deletions.
2 changes: 2 additions & 0 deletions src/spec-common/injectHeadless.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ export interface ResolverParameters {
backgroundTasks: (Promise<void> | (() => Promise<void>))[];
persistedFolder: string; // A path where config can be persisted and restored at a later time. Should default to tmpdir() folder if not provided.
remoteEnv: Record<string, string>;
buildxPlatform: string | undefined;
buildxPush: boolean;
}

export interface PostCreate {
Expand Down
6 changes: 6 additions & 0 deletions src/spec-node/devContainers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ export interface ProvisionOptions {
remoteEnv: Record<string, string>;
additionalCacheFroms: string[];
useBuildKit: 'auto' | 'never';
buildxPlatform: string | undefined;
buildxPush: boolean;
}

export async function launch(options: ProvisionOptions, disposables: (() => Promise<unknown> | undefined)[]) {
Expand Down Expand Up @@ -113,6 +115,8 @@ export async function createDockerParams(options: ProvisionOptions, disposables:
backgroundTasks: [],
persistedFolder: persistedFolder || await cliHost.tmpdir(), // Fallback to tmpDir(), even though that isn't 'persistent'
remoteEnv,
buildxPlatform: options.buildxPlatform,
buildxPush: options.buildxPush,
};

const dockerPath = options.dockerPath || 'docker';
Expand Down Expand Up @@ -148,6 +152,8 @@ export async function createDockerParams(options: ProvisionOptions, disposables:
additionalCacheFroms: options.additionalCacheFroms,
buildKitVersion,
isTTY: process.stdin.isTTY || options.logFormat === 'json',
buildxPlatform: common.buildxPlatform,
buildxPush: common.buildxPush,
};
}

Expand Down
31 changes: 26 additions & 5 deletions src/spec-node/devContainersSpecCLI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,8 @@ async function provision({
remoteEnv: keyValuesToRecord(addRemoteEnvs),
additionalCacheFroms: addCacheFroms,
useBuildKit: buildkit,
buildxPlatform: undefined,
buildxPush: false,
};

const result = await doProvision(options);
Expand Down Expand Up @@ -241,6 +243,8 @@ function buildOptions(y: Argv) {
'image-name': { type: 'string', description: 'Image name.' },
'cache-from': { type: 'string', description: 'Additional image to use as potential layer cache' },
'buildkit': { choices: ['auto' as 'auto', 'never' as 'never'], default: 'auto' as 'auto', description: 'Control whether BuildKit should be used' },
'platform': { type: 'string', description: 'Set target platforms.' },
'push': { type: 'boolean', default: false, description: 'Push to a container registry.' },
});
}

Expand Down Expand Up @@ -269,6 +273,8 @@ async function doBuild({
'image-name': argImageName,
'cache-from': addCacheFrom,
'buildkit': buildkit,
'platform': buildxPlatform,
'push': buildxPush,
}: BuildArgs) {
const disposables: (() => Promise<unknown> | undefined)[] = [];
const dispose = async () => {
Expand Down Expand Up @@ -305,7 +311,9 @@ async function doBuild({
updateRemoteUserUIDDefault: 'never',
remoteEnv: {},
additionalCacheFroms: addCacheFroms,
useBuildKit: buildkit
useBuildKit: buildkit,
buildxPlatform,
buildxPush,
}, disposables);

const { common, dockerCLI, dockerComposeCLI } = params;
Expand All @@ -325,16 +333,22 @@ async function doBuild({
if (isDockerFileConfig(config)) {

// Build the base image and extend with features etc.
const { updatedImageName } = await buildNamedImageAndExtend(params, config);
const { updatedImageName } = await buildNamedImageAndExtend(params, config, argImageName);

if (argImageName) {
await dockerPtyCLI(params, 'tag', updatedImageName, argImageName);
if (!buildxPush) {
await dockerPtyCLI(params, 'tag', updatedImageName, argImageName);
}
imageNameResult = argImageName;
} else {
imageNameResult = updatedImageName;
}
} else if ('dockerComposeFile' in config) {

if (buildxPlatform || buildxPush) {
throw new ContainerError({ description: '--platform or --push not supported.' });
}

const cwdEnvFile = cliHost.path.join(cliHost.cwd, '.env');
const envFile = Array.isArray(config.dockerComposeFile) && config.dockerComposeFile.length === 0 && await cliHost.isFile(cwdEnvFile) ? cwdEnvFile : undefined;
const composeFiles = await getDockerComposeFilePaths(cliHost, config, cliHost.env, workspaceFolder);
Expand Down Expand Up @@ -371,6 +385,9 @@ async function doBuild({
await dockerPtyCLI(params, 'pull', config.image);
const { updatedImageName } = await extendImage(params, config, config.image, 'image' in config);

if (buildxPlatform || buildxPush) {
throw new ContainerError({ description: '--platform or --push require dockerfilePath.' });
}
if (argImageName) {
await dockerPtyCLI(params, 'tag', updatedImageName, argImageName);
imageNameResult = argImageName;
Expand Down Expand Up @@ -509,7 +526,9 @@ async function doRunUserCommands({
updateRemoteUserUIDDefault: 'never',
remoteEnv: keyValuesToRecord(addRemoteEnvs),
additionalCacheFroms: [],
useBuildKit: 'auto'
useBuildKit: 'auto',
buildxPlatform: undefined,
buildxPush: false,
}, disposables);

const { common } = params;
Expand Down Expand Up @@ -753,7 +772,9 @@ async function doExec({
updateRemoteUserUIDDefault: 'never',
remoteEnv: keyValuesToRecord(addRemoteEnvs),
additionalCacheFroms: [],
useBuildKit: 'auto'
useBuildKit: 'auto',
buildxPlatform: undefined,
buildxPush: false,
}, disposables);

const { common } = params;
Expand Down
27 changes: 21 additions & 6 deletions src/spec-node/singleContainer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,15 +100,19 @@ async function setupContainer(container: ContainerDetails, params: DockerResolve
};
}

export async function buildNamedImageAndExtend(params: DockerResolverParameters, config: DevContainerFromDockerfileConfig | DevContainerFromImageConfig) {
const imageName = 'image' in config ? config.image : getFolderImageName(params.common);
function getDefaultName(config: DevContainerFromDockerfileConfig | DevContainerFromImageConfig, params: DockerResolverParameters) {
return 'image' in config ? config.image : getFolderImageName(params.common);
}
export async function buildNamedImageAndExtend(params: DockerResolverParameters, config: DevContainerFromDockerfileConfig | DevContainerFromImageConfig, argImageName?: string) {
const imageName = argImageName ?? getDefaultName(config, params);
params.common.progress(ResolverProgress.BuildingImage);
if (isDockerFileConfig(config)) {
return await buildAndExtendImage(params, config, imageName, params.buildNoCache ?? false);
}
// image-based dev container - extend
return await extendImage(params, config, imageName, 'image' in config);
}

async function buildAndExtendImage(buildParams: DockerResolverParameters, config: DevContainerFromDockerfileConfig, baseImageName: string, noCache: boolean) {
const { cliHost, output } = buildParams.common;
const dockerfileUri = getDockerfilePath(cliHost, config);
Expand Down Expand Up @@ -158,15 +162,26 @@ async function buildAndExtendImage(buildParams: DockerResolverParameters, config
}

const args: string[] = [];
if (!buildParams.buildKitVersion &&
(buildParams.buildxPlatform || buildParams.buildxPush)) {
throw new ContainerError({ description: '--platform or --push require BuildKit enabled.', data: { fileWithError: dockerfilePath } });
}
if (buildParams.buildKitVersion) {
args.push('buildx', 'build',
'--load', // (short for --output=docker, i.e. load into normal 'docker images' collection)
'--build-arg', 'BUILDKIT_INLINE_CACHE=1', // ensure cache manifest is included in the image
);
args.push('buildx', 'build');
if (buildParams.buildxPlatform) {
args.push('--platform', buildParams.buildxPlatform);
}
if (buildParams.buildxPush) {
args.push('--push');
} else {
args.push('--load'); // (short for --output=docker, i.e. load into normal 'docker images' collection)
}
args.push('--build-arg', 'BUILDKIT_INLINE_CACHE=1');
} else {
args.push('build');
}
args.push('-f', finalDockerfilePath, '-t', baseImageName);

const target = config.build?.target;
if (target) {
args.push('--target', target);
Expand Down
2 changes: 2 additions & 0 deletions src/spec-node/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ export interface DockerResolverParameters {
additionalCacheFroms: string[];
buildKitVersion: string | null;
isTTY: boolean;
buildxPlatform: string | undefined;
buildxPush: boolean;
}

export interface ResolverResult {
Expand Down
62 changes: 62 additions & 0 deletions src/test/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,69 @@ describe('Dev Containers CLI', function () {
assert.equal(success, false, 'expect non-successful call');
});

it('should succeed with supported --platform', async () => {
const testFolder = `${__dirname}/configs/dockerfile-with-target`;
const res = await shellExec(`${cli} build --workspace-folder ${testFolder} --platform linux/amd64`);
const response = JSON.parse(res.stdout);
assert.equal(response.outcome, 'success');
});

it('should fail --platform without dockerfile', async () => {
let success = false;
const testFolder = `${__dirname}/configs/image`;
try {
await shellExec(`${cli} build --workspace-folder ${testFolder} --platform linux/amd64`);
} catch (error) {
assert.equal(error.error.code, 1, 'Should fail with exit code 1');
const res = JSON.parse(error.stdout);
assert.equal(res.outcome, 'error');
assert.match(res.message, /require dockerfilePath/);
}
assert.equal(success, false, 'expect non-successful call');
});

it('should fail with unsupported --platform', async () => {
let success = false;
const testFolder = `${__dirname}/configs/dockerfile-with-target`;
try {
await shellExec(`${cli} build --workspace-folder ${testFolder} --platform fake/platform`);
success = true;
} catch (error) {
assert.equal(error.error.code, 1, 'Should fail with exit code 1');
const res = JSON.parse(error.stdout);
assert.equal(res.outcome, 'error');
assert.match(res.message, /Command failed/);
}
assert.equal(success, false, 'expect non-successful call');
});

it('should fail with BuildKit never and --platform', async () => {
let success = false;
const testFolder = `${__dirname}/configs/dockerfile-with-target`;
try {
await shellExec(`${cli} build --workspace-folder ${testFolder} --buildkit=never --platform linux/amd64`);
} catch (error) {
assert.equal(error.error.code, 1, 'Should fail with exit code 1');
const res = JSON.parse(error.stdout);
assert.equal(res.outcome, 'error');
assert.match(res.message, /require BuildKit enabled/);
}
assert.equal(success, false, 'expect non-successful call');
});

it('should fail with docker-compose and --platform not supported', async () => {
let success = false;
const testFolder = `${__dirname}/configs/compose-image-with-features`;
try {
await shellExec(`${cli} build --workspace-folder ${testFolder} --platform linux/amd64`);
} catch (error) {
assert.equal(error.error.code, 1, 'Should fail with exit code 1');
const res = JSON.parse(error.stdout);
assert.equal(res.outcome, 'error');
assert.match(res.message, /not supported/);
}
assert.equal(success, false, 'expect non-successful call');
});
});

describe('Command up', () => {
Expand Down

0 comments on commit 208ebb3

Please sign in to comment.