From 6377b1c6003c0b73e0171ea715e813d2ea773ec2 Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Wed, 28 Feb 2024 11:40:36 +0100 Subject: [PATCH 1/2] Add tags with build (devcontainers/ci#271) --- src/spec-node/containerFeatures.ts | 10 +++++++++- src/spec-node/devContainersSpecCLI.ts | 19 ++++++++++--------- src/spec-node/singleContainer.ts | 2 +- src/test/cli.build.test.ts | 6 ++++++ 4 files changed, 26 insertions(+), 11 deletions(-) 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 () => { From ef25df3cfbf9cec4183e99f2084af89ffbbdf08d Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Fri, 1 Mar 2024 20:09:34 +0100 Subject: [PATCH 2/2] 0.57.0 --- CHANGELOG.md | 6 ++++++ package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 511168b29..067481057 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ Notable changes. +## March 2024 + +### [0.57.0] +- Fix crash updating UID/GID when the image's platform is different from the native CPU arch (https://github.com/devcontainers/cli/pull/746) +- Add tags with build command (https://github.com/devcontainers/ci/issues/271) + ## February 2024 ### [0.56.2] diff --git a/package.json b/package.json index 05873ed32..e3e3aab65 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@devcontainers/cli", "description": "Dev Containers CLI", - "version": "0.56.2", + "version": "0.57.0", "bin": { "devcontainer": "devcontainer.js" },