diff --git a/src/spec-node/containerFeatures.ts b/src/spec-node/containerFeatures.ts index 2c7c987be..21649f10a 100644 --- a/src/spec-node/containerFeatures.ts +++ b/src/spec-node/containerFeatures.ts @@ -28,7 +28,7 @@ export const getSafeId = (str: string) => str .replace(/^[\d_]+/g, '_') .toUpperCase(); -export async function extendImage(params: DockerResolverParameters, config: SubstitutedConfig, imageName: string, additionalFeatures: Record>, canAddLabelsToContainer: boolean) { +export async function extendImage(params: DockerResolverParameters, config: SubstitutedConfig, imageName: string, additionalImageNames: string[], additionalFeatures: Record>, canAddLabelsToContainer: boolean) { const { common } = params; const { cliHost, output } = common; @@ -36,6 +36,13 @@ export async function extendImage(params: DockerResolverParameters, config: Subs const extendImageDetails = await getExtendImageBuildInfo(params, config, imageName, imageBuildInfo, undefined, additionalFeatures, canAddLabelsToContainer); if (!extendImageDetails?.featureBuildInfo) { // no feature extensions - return + if (additionalImageNames.length) { + if (params.isTTY) { + await Promise.all(additionalImageNames.map(name => dockerPtyCLI(params, 'tag', imageName, name))); + } else { + await Promise.all(additionalImageNames.map(name => dockerCLI(params, 'tag', imageName, name))); + } + } return { updatedImageName: [imageName], imageMetadata: getDevcontainerMetadata(imageBuildInfo.metadata, config, extendImageDetails?.featuresConfig), @@ -99,6 +106,7 @@ export async function extendImage(params: DockerResolverParameters, config: Subs args.push( '--target', featureBuildInfo.overrideTarget, '-t', updatedImageName, + ...additionalImageNames.map(name => ['-t', name]).flat(), '-f', dockerfilePath, emptyTempDir ); diff --git a/src/spec-node/devContainersSpecCLI.ts b/src/spec-node/devContainersSpecCLI.ts index 0b04f4fb0..701505b98 100644 --- a/src/spec-node/devContainersSpecCLI.ts +++ b/src/spec-node/devContainersSpecCLI.ts @@ -16,7 +16,7 @@ import { ContainerError } from '../spec-common/errors'; import { Log, LogDimensions, LogLevel, makeLog, mapLogLevel } from '../spec-utils/log'; import { probeRemoteEnv, runLifecycleHooks, runRemoteCommand, UserEnvProbe, setupInContainer } from '../spec-common/injectHeadless'; import { extendImage } from './containerFeatures'; -import { DockerCLIParameters, dockerPtyCLI, inspectContainer } from '../spec-shutdown/dockerUtils'; +import { dockerCLI, DockerCLIParameters, dockerPtyCLI, inspectContainer } from '../spec-shutdown/dockerUtils'; import { buildAndExtendDockerCompose, dockerComposeCLIConfig, getDefaultImageName, getProjectName, readDockerComposeConfig, readVersionPrefix } from './dockerCompose'; import { DevContainerFromDockerComposeConfig, DevContainerFromDockerfileConfig, getDockerComposeFilePaths } from '../spec-configuration/configuration'; import { workspaceFromPath } from '../spec-utils/workspaces'; @@ -583,7 +583,7 @@ async function doBuild({ omitSyntaxDirective, }, disposables); - const { common, dockerCLI, dockerComposeCLI } = params; + const { common, dockerComposeCLI } = params; const { cliHost, env, output } = common; const workspace = workspaceFromPath(cliHost.path, workspaceFolder); const configPath = configFile ? configFile : workspace @@ -602,7 +602,7 @@ async function doBuild({ throw new ContainerError({ description: '--push true cannot be used with --output.' }); } - const buildParams: DockerCLIParameters = { cliHost, dockerCLI, dockerComposeCLI, env, output, platformInfo: params.platformInfo }; + const buildParams: DockerCLIParameters = { cliHost, dockerCLI: params.dockerCLI, dockerComposeCLI, env, output, platformInfo: params.platformInfo }; await ensureNoDisallowedFeatures(buildParams, config, additionalFeatures, undefined); // Support multiple use of `--image-name` @@ -614,9 +614,6 @@ async function doBuild({ let { updatedImageName } = await buildNamedImageAndExtend(params, configWithRaw as SubstitutedConfig, additionalFeatures, false, imageNames); if (imageNames) { - if (!buildxPush && !buildxOutput) { - await Promise.all(imageNames.map(imageName => dockerPtyCLI(params, 'tag', updatedImageName[0], imageName))); - } imageNameResult = imageNames; } else { imageNameResult = updatedImageName; @@ -660,7 +657,12 @@ async function doBuild({ const originalImageName = overrideImageName || service.image || getDefaultImageName(await buildParams.dockerComposeCLI(), projectName, config.service); if (imageNames) { - await Promise.all(imageNames.map(imageName => dockerPtyCLI(params, 'tag', originalImageName, imageName))); + // Future improvement: Compose 2.6.0 (released 2022-05-30) added `tags` to the compose file. + if (params.isTTY) { + await Promise.all(imageNames.map(imageName => dockerPtyCLI(params, 'tag', originalImageName, imageName))); + } else { + await Promise.all(imageNames.map(imageName => dockerCLI(params, 'tag', originalImageName, imageName))); + } imageNameResult = imageNames; } else { imageNameResult = originalImageName; @@ -672,10 +674,9 @@ async function doBuild({ } await inspectDockerImage(params, config.image, true); - const { updatedImageName } = await extendImage(params, configWithRaw, config.image, additionalFeatures, false); + const { updatedImageName } = await extendImage(params, configWithRaw, config.image, imageNames || [], additionalFeatures, false); if (imageNames) { - await Promise.all(imageNames.map(imageName => dockerPtyCLI(params, 'tag', updatedImageName[0], imageName))); imageNameResult = imageNames; } else { imageNameResult = updatedImageName; diff --git a/src/spec-node/singleContainer.ts b/src/spec-node/singleContainer.ts index 5d35c3e06..0ff7e9a75 100644 --- a/src/spec-node/singleContainer.ts +++ b/src/spec-node/singleContainer.ts @@ -126,7 +126,7 @@ export async function buildNamedImageAndExtend(params: DockerResolverParameters, return await buildAndExtendImage(params, configWithRaw as SubstitutedConfig, imageNames, params.buildNoCache ?? false, additionalFeatures); } // image-based dev container - extend - return await extendImage(params, configWithRaw, imageNames[0], additionalFeatures, canAddLabelsToContainer); + return await extendImage(params, configWithRaw, imageNames[0], argImageNames || [], additionalFeatures, canAddLabelsToContainer); } async function buildAndExtendImage(buildParams: DockerResolverParameters, configWithRaw: SubstitutedConfig, baseImageNames: string[], noCache: boolean, additionalFeatures: Record>) { diff --git a/src/test/cli.build.test.ts b/src/test/cli.build.test.ts index 446cc25a7..d8df77e04 100644 --- a/src/test/cli.build.test.ts +++ b/src/test/cli.build.test.ts @@ -153,33 +153,39 @@ describe('Dev Containers CLI', function () { const testFolder = `${__dirname}/configs/dockerfile-with-features`; const image1 = 'image-1'; const image2 = 'image-2'; + await shellExec(`docker rmi -f ${image1} ${image2}`); const res = await shellExec(`${cli} build --workspace-folder ${testFolder} --image-name ${image1} --image-name ${image2}`); const response = JSON.parse(res.stdout); assert.equal(response.outcome, 'success'); assert.equal(response.imageName[0], image1); assert.equal(response.imageName[1], image2); + await shellExec(`docker inspect --type image ${image1} ${image2}`); }); it('should succeed with multiple --image-name parameters when dockerComposeFile is present', async () => { const testFolder = `${__dirname}/configs/compose-Dockerfile-alpine`; const image1 = 'image-1'; const image2 = 'image-2'; + await shellExec(`docker rmi -f ${image1} ${image2}`); const res = await shellExec(`${cli} build --workspace-folder ${testFolder} --image-name ${image1} --image-name ${image2}`); const response = JSON.parse(res.stdout); assert.equal(response.outcome, 'success'); assert.equal(response.imageName[0], image1); assert.equal(response.imageName[1], image2); + await shellExec(`docker inspect --type image ${image1} ${image2}`); }); it('should succeed with multiple --image-name parameters when image is present', async () => { const testFolder = `${__dirname}/configs/image`; const image1 = 'image-1'; const image2 = 'image-2'; + await shellExec(`docker rmi -f ${image1} ${image2}`); const res = await shellExec(`${cli} build --workspace-folder ${testFolder} --image-name ${image1} --image-name ${image2}`); const response = JSON.parse(res.stdout); assert.equal(response.outcome, 'success'); assert.equal(response.imageName[0], image1); assert.equal(response.imageName[1], image2); + await shellExec(`docker inspect --type image ${image1} ${image2}`); }); it('should fail with --push true and --output', async () => {