Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 47 additions & 27 deletions src/spec-node/collectionCommonUtils/outdatedCommandImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,40 @@ import { CommonParams, ManifestContainer, getRef, getVersionsStrictSorted } from
import { DockerCLIParameters } from '../../spec-shutdown/dockerUtils';
import { request } from '../../spec-utils/httpRequest';
import { readDockerComposeConfig, getBuildInfoForService } from '../dockerCompose';
import { extractDockerfile, findImage } from '../dockerfileUtils';
import { extractDockerfile, findBaseImages } from '../dockerfileUtils';
import { ContainerFeatureInternalParams, userFeaturesToArray, getFeatureIdType, DEVCONTAINER_FEATURE_FILE_NAME, Feature } from '../../spec-configuration/containerFeaturesConfiguration';
import { readLockfile } from '../../spec-configuration/lockfile';

export interface OutdatedFeatures {
'features': {
[key: string]: {
'current': string;
'wanted': string;
'latest': string;
};
};
}

export interface OutdatedImages {
'images': {
[key: string]: {
'name': string;
'version': string;
'wantedVersion': string;
'current': string;
'wanted': string;
'currentImageValue': string;
'newImageValue': string;
'path': string;
};
};
}

export interface OutdatedResult {
'features': OutdatedFeatures;
'images': OutdatedImages;
}

export async function outdated({
// 'user-data-folder': persistedFolder,
'workspace-folder': workspaceFolderArg,
Expand Down Expand Up @@ -69,8 +99,8 @@ export async function outdated({
throw new ContainerError({ description: `Dev container config (${uriToFsPath(configFile || getDefaultDevContainerConfigPath(cliHost, workspace!.configFolderPath), cliHost.platform)}) not found.` });
}

let outdatedFeatures: { features: { [key: string]: any } };
let outdatedImages: { images: { [key: string]: any } };
let outdatedFeatures: OutdatedFeatures;
let outdatedImages: OutdatedImages;

if (onlyFeatures || !(onlyImages || onlyFeatures)) {
const cacheFolder = await getCacheFolder(cliHost);
Expand Down Expand Up @@ -294,19 +324,14 @@ async function loadImageVersionInfo(params: CommonParams, config: DevContainerCo
const dockerfile = extractDockerfile(dockerfileText);

const resolvedImageInfo: Record<string, any> = {};
for (let i = 0; i < dockerfile.stages.length; i++) {
const stage = dockerfile.stages[i];
const currentImage = stage.from.image;
const previousStage = (i !== 0) ? dockerfile.stages[i - 1] : undefined;
const image = findImage(currentImage, dockerfile, config.build.args || {}, previousStage);
if (image === undefined) {
continue;
}
const images = findBaseImages(dockerfile, config.build.args || {});
for (const currentImage in images) {
if (images.hasOwnProperty(currentImage)) {
let imageInfo = await findImageVersionInfo(params, images[currentImage], dockerfilePath, currentImage);

let imageInfo = await findImageVersionInfo(params, image, dockerfilePath, currentImage);

if (imageInfo !== undefined) {
resolvedImageInfo[currentImage] = imageInfo;
if (imageInfo !== undefined) {
resolvedImageInfo[currentImage] = imageInfo;
}
}
}

Expand Down Expand Up @@ -350,19 +375,14 @@ async function loadImageVersionInfo(params: CommonParams, config: DevContainerCo
const dockerfileText = (await cliHost.readFile(resolvedDockerfilePath)).toString();
const dockerfile = extractDockerfile(dockerfileText);

for (let i = 0; i < dockerfile.stages.length; i++) {
const stage = dockerfile.stages[i];
const currentImage = stage.from.image;
const previousStage = (i !== 0) ? dockerfile.stages[i - 1] : undefined;
const image = findImage(currentImage, dockerfile, composeService.build?.args, previousStage);
if (image === undefined) {
continue;
}

let imageInfo = await findImageVersionInfo(params, image, resolvedDockerfilePath, currentImage);
const images = findBaseImages(dockerfile, composeService.build?.args || {});
for (const currentImage in images) {
if (images.hasOwnProperty(currentImage)) {
let imageInfo = await findImageVersionInfo(params, images[currentImage], resolvedDockerfilePath, currentImage);

if (imageInfo !== undefined) {
resolvedImageInfo[currentImage] = imageInfo;
if (imageInfo !== undefined) {
resolvedImageInfo[currentImage] = imageInfo;
}
}
}
}
Expand Down
55 changes: 10 additions & 45 deletions src/spec-node/dockerfileUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,53 +119,18 @@ export function findBaseImage(dockerfile: Dockerfile, buildArgs: Record<string,
return undefined;
}

export function findImage(image: string, dockerfile: Dockerfile, buildArgs: Record<string, string>, previousStage: Stage | undefined) {
const resolvedImage = replaceVariablesInImage(dockerfile, buildArgs, image, previousStage);
return resolvedImage;
}

function replaceVariablesInImage(dockerfile: Dockerfile, buildArgs: Record<string, string>, str: string, previousStage: Stage | undefined) {
return [...str.matchAll(argumentExpression)]
.map(match => {
const variable = match.groups!.variable;
const isVarExp = match.groups!.isVarExp ? true : false;
let value = findValueInImage(dockerfile, buildArgs, variable, previousStage) || '';
if (isVarExp) {
// Handle replacing variable expressions (${var:+word}) if they exist
const option = match.groups!.option;
const word = match.groups!.word;
const isSet = value !== '';
value = getExpressionValue(option, isSet, word, value);
}

return {
begin: match.index!,
end: match.index! + match[0].length,
value,
};
}).reverse()
.reduce((str, { begin, end, value }) => str.substring(0, begin) + value + str.substring(end), str);
}

function findValueInImage(dockerfile: Dockerfile, buildArgs: Record<string, string>, variable: string, previousStage: Stage | undefined) {
if (buildArgs !== undefined && variable in buildArgs) {
return buildArgs[variable];
}

if (previousStage !== undefined) {
const i = findLastIndex(previousStage.instructions, i => i.name === variable && (i.instruction === 'ENV' || i.instruction === 'ARG'));
if (i !== -1) {
return previousStage.instructions[i].value;
export function findBaseImages(dockerfile: Dockerfile, buildArgs: Record<string, string>) {
const resolvedBaseImages = {} as Record<string, string>;
for (let i = 0; i < dockerfile.stages.length; i++) {
const stage = dockerfile.stages[i];
const image = replaceVariables(dockerfile, buildArgs, /* not available in FROM instruction */ {}, stage.from.image, dockerfile.preamble, dockerfile.preamble.instructions.length);
const nextStage = dockerfile.stagesByLabel[image];
if (!nextStage) {
resolvedBaseImages[stage.from.image] = image;
}
}

const index = findLastIndex(dockerfile.preamble.instructions, i => i.name === variable && (i.instruction === 'ENV' || i.instruction === 'ARG'));
if (index !== -1) {
return dockerfile.preamble.instructions[index].value;
}

return undefined;
}
return resolvedBaseImages;
}

function extractDirectives(preambleStr: string) {
const map: Record<string, string> = {};
Expand Down
2 changes: 1 addition & 1 deletion src/test/cli.outdated.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ describe('Outdated', function () {
it('dockercompose-image', async () => {
const workspaceFolder = path.join(__dirname, 'configs/compose-image-with-features');

const res = await shellExec(`${cli} outdated --workspace-folder ${workspaceFolder} --only-images --output-format json`);
const res = await shellExec(`${cli} outdated --workspace-folder ${workspaceFolder} --only-images --output-format json`);
const response = JSON.parse(res.stdout);

assert.equal(response['features'], undefined);
Expand Down