Skip to content

Commit

Permalink
Use HEAD request to get digest (microsoft#2691)
Browse files Browse the repository at this point in the history
* Use HEAD request to get digest

* Don't flag none-tagged
  • Loading branch information
bwateratmsft authored and Dmarch28 committed Mar 4, 2021
1 parent c26626d commit 67f5868
Show file tree
Hide file tree
Showing 3 changed files with 34 additions and 55 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1917,7 +1917,7 @@
},
"docker.images.checkForOutdatedImages": {
"type": "boolean",
"default": false,
"default": true,
"description": "%vscode-docker.config.docker.images.checkForOutdatedImages%"
},
"docker.networks.groupBy": {
Expand Down
3 changes: 2 additions & 1 deletion src/docker/Images.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ export interface DockerImage extends DockerObject {
export interface DockerImageInspection extends DockerObject {
readonly Config?: {
readonly ExposedPorts?: { readonly [portAndProtocol: string]: unknown; };
readonly Image?: string;
};

readonly RepoDigests?: string[];

readonly Os: string;
readonly Name: undefined; // Not defined for inspection
readonly Containers?: { // Not a real part of image inspection, but we add it because it's desperately needed
Expand Down
84 changes: 31 additions & 53 deletions src/tree/images/imageChecker/OutdatedImageChecker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,12 @@ import { callWithTelemetryAndErrorHandling, IActionContext } from 'vscode-azuree
import { ociClientId } from '../../../constants';
import { DockerImage } from '../../../docker/Images';
import { ext } from '../../../extensionVariables';
import { localize } from '../../../localize';
import { getImagePropertyValue } from '../ImageProperties';
import { DatedDockerImage } from '../ImagesTreeItem';
import { ImageRegistry, registries } from './registries';

const noneRegex = /<none>/i;

const lastLiveOutdatedCheckKey = 'vscode-docker.outdatedImageChecker.lastLiveCheck';
const outdatedImagesKey = 'vscode-docker.outdatedImageChecker.outdatedImages';

export class OutdatedImageChecker {
private shouldLoad: boolean;
private readonly outdatedImageIds: string[] = [];
Expand All @@ -32,12 +28,13 @@ export class OutdatedImageChecker {
const httpSettings = vscode.workspace.getConfiguration('http');
const strictSSL = httpSettings.get<boolean>('proxyStrictSSL', true);
this.defaultRequestOptions = {
method: 'GET',
method: 'HEAD',
json: true,
resolveWithFullResponse: true,
strictSSL: strictSSL,
headers: {
'X-Meta-Source-Client': ociClientId,
'Accept': 'application/vnd.docker.distribution.manifest.list.v2+json',
},
};
}
Expand All @@ -52,43 +49,33 @@ export class OutdatedImageChecker {
context.errorHandling.suppressReportIssue = true;
context.errorHandling.suppressDisplay = true;

const lastCheck = ext.context.globalState.get<number | undefined>(lastLiveOutdatedCheckKey, undefined);

if (lastCheck && Date.now() - lastCheck < 24 * 60 * 60 * 1000) {
// Use the cached data
context.telemetry.properties.checkSource = 'cache';
this.outdatedImageIds.push(...ext.context.globalState.get<string[]>(outdatedImagesKey, []));
} else {
// Do a live check
context.telemetry.properties.checkSource = 'live';
await ext.context.globalState.update(lastLiveOutdatedCheckKey, Date.now());

const imageCheckPromises: Promise<void>[] = [];

for (const image of images) {
const imageRegistry = getImagePropertyValue(image, 'Registry');
const matchingRegistry = registries.find(r => r.registryMatch.test(imageRegistry));

if (matchingRegistry) {
imageCheckPromises.push((async () => {
if (await this.checkImage(context, matchingRegistry, image) === 'outdated') {
this.outdatedImageIds.push(image.Id);
}
})());
}
// Do a live check
context.telemetry.properties.checkSource = 'live';

const imageCheckPromises: Promise<void>[] = [];

for (const image of images) {
const imageRegistry = getImagePropertyValue(image, 'Registry');
const matchingRegistry = registries.find(r => r.registryMatch.test(imageRegistry));

if (matchingRegistry) {
imageCheckPromises.push((async () => {
if (await this.checkImage(context, matchingRegistry, image) === 'outdated') {
this.outdatedImageIds.push(image.Id);
}
})());
}
}

context.telemetry.measurements.imagesChecked = imageCheckPromises.length;
context.telemetry.measurements.imagesChecked = imageCheckPromises.length;

// Load the data for all images then force the tree to refresh
await Promise.all(imageCheckPromises);
await ext.context.globalState.update(outdatedImagesKey, this.outdatedImageIds);
// Load the data for all images then force the tree to refresh
await Promise.all(imageCheckPromises);

context.telemetry.measurements.outdatedImages = this.outdatedImageIds.length;
context.telemetry.measurements.outdatedImages = this.outdatedImageIds.length;

// Don't wait
void ext.imagesRoot.refresh(context);
}
// Don't wait
void ext.imagesRoot.refresh(context);
});
}

Expand All @@ -104,23 +91,23 @@ export class OutdatedImageChecker {
const repo = registryAndRepo.replace(registry.registryMatch, '').replace(/^\/|\/$/, '');

if (noneRegex.test(repo) || noneRegex.test(tag)) {
return 'outdated';
return 'unknown';
}

let token: string | undefined;

// 1. Get an OAuth token to access the resource. No Authorization header is required for public scopes.
if (registry.getToken) {
token = await registry.getToken(this.defaultRequestOptions, `repository:library/${repo}:pull`);
token = await registry.getToken({ ...this.defaultRequestOptions, method: 'GET' }, `repository:library/${repo}:pull`);
}

// 2. Get the latest image ID from the manifest
const latestConfigImageId = await this.getLatestConfigImageId(registry, repo, tag, token);
// 2. Get the latest image digest ID from the manifest
const latestImageDigest = await this.getLatestImageDigest(registry, repo, tag, token);

// 3. Compare it with the current image's value
const imageInspectInfo = await ext.dockerClient.inspectImage(context, image.Id);

if (latestConfigImageId.toLowerCase() !== imageInspectInfo?.Config?.Image?.toLowerCase()) {
if (imageInspectInfo?.RepoDigests?.[0]?.toLowerCase()?.indexOf(latestImageDigest.toLowerCase()) < 0) {
return 'outdated';
}

Expand All @@ -130,7 +117,7 @@ export class OutdatedImageChecker {
}
}

private async getLatestConfigImageId(registry: ImageRegistry, repo: string, tag: string, oAuthToken: string | undefined): Promise<string> {
private async getLatestImageDigest(registry: ImageRegistry, repo: string, tag: string, oAuthToken: string | undefined): Promise<string> {
const manifestOptions: request.RequestPromiseOptions = {
...this.defaultRequestOptions,
auth: oAuthToken ? {
Expand All @@ -139,15 +126,6 @@ export class OutdatedImageChecker {
};

const manifestResponse = await request(`${registry.baseUrl}/${repo}/manifests/${tag}`, manifestOptions) as Response;
/* eslint-disable @typescript-eslint/tslint/config */
const firstHistory = JSON.parse(manifestResponse?.body?.history?.[0]?.v1Compatibility);
const latestConfigImageId: string = firstHistory?.config?.Image;
/* eslint-enable @typescript-eslint/tslint/config */

if (!latestConfigImageId) {
throw new Error(localize('vscode-docker.outdatedImageChecker.noManifest', 'Failed to acquire manifest token for image: \'{0}:{1}\'', repo, tag));
}

return latestConfigImageId;
return manifestResponse.headers['docker-content-digest'] as string;
}
}

0 comments on commit 67f5868

Please sign in to comment.