diff --git a/package.json b/package.json index e4095da173..81e0b98a27 100644 --- a/package.json +++ b/package.json @@ -1917,7 +1917,7 @@ }, "docker.images.checkForOutdatedImages": { "type": "boolean", - "default": false, + "default": true, "description": "%vscode-docker.config.docker.images.checkForOutdatedImages%" }, "docker.networks.groupBy": { diff --git a/src/docker/Images.ts b/src/docker/Images.ts index a5fa9cb822..8f294037bc 100644 --- a/src/docker/Images.ts +++ b/src/docker/Images.ts @@ -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 diff --git a/src/tree/images/imageChecker/OutdatedImageChecker.ts b/src/tree/images/imageChecker/OutdatedImageChecker.ts index f20dbcc881..83ea96e072 100644 --- a/src/tree/images/imageChecker/OutdatedImageChecker.ts +++ b/src/tree/images/imageChecker/OutdatedImageChecker.ts @@ -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 = //i; -const lastLiveOutdatedCheckKey = 'vscode-docker.outdatedImageChecker.lastLiveCheck'; -const outdatedImagesKey = 'vscode-docker.outdatedImageChecker.outdatedImages'; - export class OutdatedImageChecker { private shouldLoad: boolean; private readonly outdatedImageIds: string[] = []; @@ -32,12 +28,13 @@ export class OutdatedImageChecker { const httpSettings = vscode.workspace.getConfiguration('http'); const strictSSL = httpSettings.get('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', }, }; } @@ -52,43 +49,33 @@ export class OutdatedImageChecker { context.errorHandling.suppressReportIssue = true; context.errorHandling.suppressDisplay = true; - const lastCheck = ext.context.globalState.get(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(outdatedImagesKey, [])); - } else { - // Do a live check - context.telemetry.properties.checkSource = 'live'; - await ext.context.globalState.update(lastLiveOutdatedCheckKey, Date.now()); - - const imageCheckPromises: Promise[] = []; - - 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[] = []; + + 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); }); } @@ -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'; } @@ -130,7 +117,7 @@ export class OutdatedImageChecker { } } - private async getLatestConfigImageId(registry: ImageRegistry, repo: string, tag: string, oAuthToken: string | undefined): Promise { + private async getLatestImageDigest(registry: ImageRegistry, repo: string, tag: string, oAuthToken: string | undefined): Promise { const manifestOptions: request.RequestPromiseOptions = { ...this.defaultRequestOptions, auth: oAuthToken ? { @@ -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; } }