diff --git a/src/spec-node/containerFeatures.ts b/src/spec-node/containerFeatures.ts index dc750b045..8d1c0f10f 100644 --- a/src/spec-node/containerFeatures.ts +++ b/src/spec-node/containerFeatures.ts @@ -98,6 +98,9 @@ export async function extendImage(params: DockerResolverParameters, config: Subs 'build', ); } + if (params.buildNoCache) { + args.push('--no-cache'); + } for (const buildArg in featureBuildInfo.buildArgs) { args.push('--build-arg', `${buildArg}=${featureBuildInfo.buildArgs[buildArg]}`); } diff --git a/src/test/cli.build.test.ts b/src/test/cli.build.test.ts index 633eba897..c22e3913f 100644 --- a/src/test/cli.build.test.ts +++ b/src/test/cli.build.test.ts @@ -64,6 +64,73 @@ describe('Dev Containers CLI', function () { }); }); + it('should not use docker cache for features when `--no-cache` flag is passed', async () => { + // Arrange + const testFolder = `${__dirname}/configs/image-with-features`; + const devContainerJson = `${testFolder}/.devcontainer.json`; + + const devContainerFileContents = JSON.parse(fs.readFileSync(devContainerJson, 'utf8')); + const baseImage = devContainerFileContents.image; + + const originalImageName = 'feature-cache-test-original-image'; + const cachedImageName = 'feature-cache-test-rerun-image'; + const nonCachedImageName = 'feature-cache-test-no-cache-image'; + + const commandBase = `${cli} build --workspace-folder ${testFolder}`; + const buildCommand = `${commandBase} --image-name ${originalImageName}`; + const cachedBuildCommand = `${commandBase} --image-name ${cachedImageName}`; + const buildWithoutCacheCommand = `${commandBase} --image-name ${nonCachedImageName} --no-cache`; + + function arrayStartsWithArray(subject: string[], startsWith: string[]) { + if (subject.length < startsWith.length) { + return false; + } + for (let i = 0; i < startsWith.length; i++) { + if (subject[i] !== startsWith[i]) { + return false; + } + } + return true; + } + + function haveCommonEntries(arr1: string[], arr2: string[]) { + return arr1.every(item => arr2.includes(item)); + } + + // Act + await shellExec(`docker pull ${baseImage}`); // pull base image so we can inspect it later + await shellExec(buildCommand); + await shellExec(cachedBuildCommand); + await shellExec(buildWithoutCacheCommand); + + // Assert + const baseImageInspectCommandResult = await shellExec(`docker inspect ${baseImage}`); + const originalImageInspectCommandResult = await shellExec(`docker inspect ${originalImageName}`); + const cachedImageInspectCommandResult = await shellExec(`docker inspect ${cachedImageName}`); + const noCacheImageInspectCommandResult = await shellExec(`docker inspect ${nonCachedImageName}`); + + const baseImageDetails = JSON.parse(baseImageInspectCommandResult.stdout); + const originalImageDetails = JSON.parse(originalImageInspectCommandResult.stdout); + const cachedImageDetails = JSON.parse(cachedImageInspectCommandResult.stdout); + const noCacheImageDetails = JSON.parse(noCacheImageInspectCommandResult.stdout); + + const baseImageLayers: string[] = baseImageDetails[0].RootFS.Layers; + const originalImageLayers: string[] = originalImageDetails[0].RootFS.Layers; + const cachedImageLayers: string[] = cachedImageDetails[0].RootFS.Layers; + const nonCachedImageLayers: string[] = noCacheImageDetails[0].RootFS.Layers; + + assert.equal(arrayStartsWithArray(originalImageLayers, baseImageLayers), true, 'because the image is made up of feature layers on top of the base image'); + assert.equal(arrayStartsWithArray(cachedImageLayers, baseImageLayers), true, 'because the image is made up of feature layers on top of the base image'); + assert.equal(arrayStartsWithArray(nonCachedImageLayers, baseImageLayers), true, 'because the image is made up of feature layers on top of the base image'); + + const originalImageWithoutBaseImageLayers = originalImageLayers.slice(baseImageLayers.length); + const cachedImageWithoutBaseImageLayers = cachedImageLayers.slice(baseImageLayers.length); + const nonCachedImageWithoutBaseImageLayers = nonCachedImageLayers.slice(baseImageLayers.length); + + assert.deepEqual(originalImageWithoutBaseImageLayers, cachedImageWithoutBaseImageLayers, 'because they are the same image built sequentially therefore the second should have used caching'); + assert.equal(haveCommonEntries(cachedImageWithoutBaseImageLayers, nonCachedImageWithoutBaseImageLayers), false, 'because we passed the --no-cache argument which disables the use of the cache, therefore the non-base image layers should have nothin in common'); + }); + it('should fail with "not found" error when config is not found', async () => { let success = false; try {