From 68df2b75da8bbebf046f5d00074e96e078766e8f Mon Sep 17 00:00:00 2001 From: "Brandon Waterloo [MSFT]" <36966225+bwateratmsft@users.noreply.github.com> Date: Tue, 16 Jun 2020 14:45:29 -0400 Subject: [PATCH 1/9] Check for and mark outdated images --- package.json | 5 + package.nls.json | 1 + src/constants.ts | 8 -- src/tree/images/ImageTreeItem.ts | 7 +- src/tree/images/ImagesTreeItem.ts | 5 + src/tree/images/LocalImageInfo.ts | 2 + src/tree/images/OutdatedImageChecker.ts | 152 ++++++++++++++++++++++++ 7 files changed, 171 insertions(+), 9 deletions(-) create mode 100644 src/tree/images/OutdatedImageChecker.ts diff --git a/package.json b/package.json index 5a51d6872a..ab7e3e4b2b 100644 --- a/package.json +++ b/package.json @@ -2057,6 +2057,11 @@ "type": "boolean", "default": true, "description": "%vscode-docker.config.docker.showRemoteWorkspaceWarning%" + }, + "docker.checkForOutdatedImages": { + "type": "boolean", + "default": true, + "description": "%vscode-docker.config.docker.checkForOutdatedImages%" } } }, diff --git a/package.nls.json b/package.nls.json index e70afba686..2ef56ea3e0 100644 --- a/package.nls.json +++ b/package.nls.json @@ -169,6 +169,7 @@ "vscode-docker.config.docker.dockerComposeBuild": "Set to true to include --build option when docker-compose command is invoked", "vscode-docker.config.docker.dockerComposeDetached": "Set to true to include --d (detached) option when docker-compose command is invoked", "vscode-docker.config.docker.showRemoteWorkspaceWarning": "Set to true to prompt to switch from \"UI\" extension mode to \"Workspace\" extension mode if an operation is not supported in UI mode.", + "vscode-docker.config.docker.checkForOutdatedImages": "Whether to check for outdated base images once per session", "vscode-docker.config.deprecated": "This setting has been deprecated and will be removed in a future release.", "vscode-docker.commands.api.configure": "Add Docker Files to Workspace (API)...", "vscode-docker.commands.compose.down": "Compose Down", diff --git a/src/constants.ts b/src/constants.ts index b93d1c173d..be32276c60 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -11,14 +11,6 @@ export const configPrefix: string = 'docker'; // Consider downloading multiple pages (images, tags, etc) export const PAGE_SIZE = 100; -export namespace keytarConstants { - export const serviceId: string = 'vscode-docker'; - - export const dockerHubTokenKey: string = 'dockerhub.token'; - export const dockerHubUserNameKey: string = 'dockerhub.username'; - export const dockerHubPasswordKey: string = 'dockerhub.password'; -} - export namespace configurationKeys { export const groupImagesBy = 'groupImagesBy'; } diff --git a/src/tree/images/ImageTreeItem.ts b/src/tree/images/ImageTreeItem.ts index 67abfa6f92..e0885a471e 100644 --- a/src/tree/images/ImageTreeItem.ts +++ b/src/tree/images/ImageTreeItem.ts @@ -6,6 +6,7 @@ import { Image } from 'dockerode'; import { AzExtParentTreeItem, AzExtTreeItem, IActionContext } from "vscode-azureextensionui"; import { ext } from '../../extensionVariables'; +import { localize } from '../../localize'; import { callDockerode, callDockerodeWithErrorHandling } from '../../utils/callDockerode'; import { getThemedIconPath, IconPath } from '../IconPath'; import { ILocalImageInfo } from './LocalImageInfo'; @@ -41,10 +42,14 @@ export class ImageTreeItem extends AzExtTreeItem { } public get description(): string | undefined { - return ext.imagesRoot.getTreeItemDescription(this._item); + return `${ext.imagesRoot.getTreeItemDescription(this._item)}${this._item.outdated ? localize('vscode-docker.tree.images.outdated', ' (Out of date)') : ''}`; } public get iconPath(): IconPath { + if (this._item.outdated) { + return getThemedIconPath('statusWarning'); + } + let icon: string; switch (ext.imagesRoot.labelSetting) { case 'Tag': diff --git a/src/tree/images/ImagesTreeItem.ts b/src/tree/images/ImagesTreeItem.ts index b468e0f9a0..70d129e0d4 100644 --- a/src/tree/images/ImagesTreeItem.ts +++ b/src/tree/images/ImagesTreeItem.ts @@ -14,8 +14,11 @@ import { ImageGroupTreeItem } from './ImageGroupTreeItem'; import { getImagePropertyValue, imageProperties, ImageProperty } from "./ImageProperties"; import { ImageTreeItem } from "./ImageTreeItem"; import { ILocalImageInfo, LocalImageInfo } from "./LocalImageInfo"; +import { OutdatedImageChecker } from "./OutdatedImageChecker"; export class ImagesTreeItem extends LocalRootTreeItemBase { + private readonly outdatedImageChecker: OutdatedImageChecker = new OutdatedImageChecker(); + public treePrefix: string = 'images'; public label: string = localize('vscode-docker.tree.images.label', 'Images'); public configureExplorerTitle: string = localize('vscode-docker.tree.images.configure', 'Configure images explorer'); @@ -61,6 +64,8 @@ export class ImagesTreeItem extends LocalRootTreeItemBase; + private readonly defaultRequestOptions: Lazy; + + public constructor() { + const dockerConfig = vscode.workspace.getConfiguration('docker'); + this.shouldLoad = dockerConfig.get('checkForOutdatedImages'); + + this.defaultRequestOptions = new Lazy(() => this.getRequestOptions()); + this.authContext = new AsyncLazy(async () => this.getAuthContext()); + } + + public async markOutdatedImages(images: ILocalImageInfo[]): Promise { + if (this.shouldLoad) { + this.shouldLoad = false; + try { + const imageCheckPromises = images + .filter(image => { + return /docker[.]io\/library/i.test(getImagePropertyValue(image, 'Registry')); + }) + .map(async (image) => { + if (await this.checkImage(image) === 'outdated') { + this.outdatedImageIds.push(image.imageId); + } + }); + + // Load the data for all images then force the tree to refresh + // By then, this.shouldLoad will be false so this path won't happen again + Promise.all(imageCheckPromises).then(() => { void ext.imagesRoot.refresh(); }, () => { }); + } catch { } + } + + for (const image of images) { + image.outdated = this.outdatedImageIds.some(i => i.toLowerCase() === image.imageId.toLowerCase()); + } + } + + private async checkImage(image: ILocalImageInfo): Promise<'latest' | 'outdated' | 'unknown'> { + try { + const [repo, tag] = image.fullTag.split(':'); + + // 1. Get an OAuth token to access the resource. No Authorization header is required for public scopes. + const token = await this.getToken(`repository:library/${repo}:pull`); + + // 2. Get the latest image ID from the manifest + const latestConfigImageId = await this.getLatestConfigImageId(repo, tag, token); + + // 3. Compare it with the current image's value + const dockerodeImage = ext.dockerode.getImage(image.fullTag); + const imageInspectInfo = await callDockerodeAsync(async () => dockerodeImage.inspect()); + + if (latestConfigImageId.toLowerCase() !== imageInspectInfo.Config.Image.toLowerCase()) { + return 'outdated'; + } + + return 'latest'; + } catch { // Errors are expected, e.g. all untagged local images are treated as if they are in docker.io/library, but will 404 when queried + return 'unknown'; + } + } + + private async getToken(scope: string): Promise { + const authContext = { + ...await this.authContext.getValue(), + scope: scope, + }; + + const authOptions: request.RequestPromiseOptions = { + ...this.defaultRequestOptions.value, + qs: { + service: authContext.service, + scope: authContext.scope, + }, + }; + + const tokenResponse = await request(authContext.realm.toString(), authOptions) as Response; + // eslint-disable-next-line @typescript-eslint/tslint/config + const token: string = tokenResponse?.body?.token; + + if (!token) { + throw new Error(localize('vscode-docker.outdatedImageChecker.noToken', 'Failed to acquire OAuth token for scope: \'{0}\'', scope)); + } + + return token; + } + + private async getLatestConfigImageId(repo: string, tag: string, oAuthToken: string): Promise { + const manifestOptions: request.RequestPromiseOptions = { + ...this.defaultRequestOptions.value, + auth: { + bearer: oAuthToken, + }, + }; + + const manifestResponse = await request(`https://registry-1.docker.io/v2/library/${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; + } + + private async getAuthContext(): Promise { + try { + const options = this.defaultRequestOptions.value; + await request('https://registry-1.docker.io/v2/', options); + } catch (err) { + const result = getWwwAuthenticateContext(err); + + if (!result) { + throw err; + } + + return result; + } + } + + private getRequestOptions(): request.RequestPromiseOptions { + const httpSettings = vscode.workspace.getConfiguration('http'); + const strictSSL = httpSettings.get('proxyStrictSSL', true); + return { + method: 'GET', + json: true, + resolveWithFullResponse: true, + strictSSL: strictSSL + }; + } +} From b70f4ded5f207abcf06a1c5c7b17db6ae211feff Mon Sep 17 00:00:00 2001 From: "Brandon Waterloo [MSFT]" <36966225+bwateratmsft@users.noreply.github.com> Date: Wed, 17 Jun 2020 09:34:53 -0400 Subject: [PATCH 2/9] Telemetry and more robustness --- .vscode/launch.json | 2 +- src/tree/images/OutdatedImageChecker.ts | 27 +++++++++++++++++++++---- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index a9dda3095f..1241b8f4e1 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -12,7 +12,7 @@ ], "env": { "AZCODE_DOCKER_IGNORE_BUNDLE": "1", - "DEBUGTELEMETRY": "1" + "DEBUGTELEMETRY": "v" }, "stopOnEntry": false, "sourceMaps": true, diff --git a/src/tree/images/OutdatedImageChecker.ts b/src/tree/images/OutdatedImageChecker.ts index ecdbdab066..2917b40ccc 100644 --- a/src/tree/images/OutdatedImageChecker.ts +++ b/src/tree/images/OutdatedImageChecker.ts @@ -6,6 +6,7 @@ import { Response } from 'request'; import * as request from 'request-promise-native'; import * as vscode from 'vscode'; +import { callWithTelemetryAndErrorHandling, IActionContext } from 'vscode-azureextensionui'; import { ext } from '../../extensionVariables'; import { localize } from '../../localize'; import { callDockerodeAsync } from '../../utils/callDockerode'; @@ -15,6 +16,8 @@ import { getWwwAuthenticateContext } from '../registries/auth/oAuthUtils'; import { getImagePropertyValue } from './ImageProperties'; import { ILocalImageInfo } from './LocalImageInfo'; +const noneRegex = //i; + export class OutdatedImageChecker { private shouldLoad: boolean; private readonly outdatedImageIds: string[] = []; @@ -32,9 +35,15 @@ export class OutdatedImageChecker { public async markOutdatedImages(images: ILocalImageInfo[]): Promise { if (this.shouldLoad) { this.shouldLoad = false; - try { + + // Don't wait + void callWithTelemetryAndErrorHandling('outdatedImageCheck', async (context: IActionContext) => { + context.errorHandling.suppressReportIssue = true; + context.errorHandling.suppressDisplay = true; + const imageCheckPromises = images .filter(image => { + // Only include images that are potentially in docker.io/library (no private or other public registries are supported) return /docker[.]io\/library/i.test(getImagePropertyValue(image, 'Registry')); }) .map(async (image) => { @@ -43,10 +52,16 @@ export class OutdatedImageChecker { } }); + context.telemetry.measurements.imagesChecked = imageCheckPromises.length; + // Load the data for all images then force the tree to refresh - // By then, this.shouldLoad will be false so this path won't happen again - Promise.all(imageCheckPromises).then(() => { void ext.imagesRoot.refresh(); }, () => { }); - } catch { } + await Promise.all(imageCheckPromises); + + context.telemetry.measurements.outdatedImages = this.outdatedImageIds.length; + + // Don't wait + void ext.imagesRoot.refresh(); + }); } for (const image of images) { @@ -58,6 +73,10 @@ export class OutdatedImageChecker { try { const [repo, tag] = image.fullTag.split(':'); + if (noneRegex.test(repo) || noneRegex.test(tag)) { + return 'outdated'; + } + // 1. Get an OAuth token to access the resource. No Authorization header is required for public scopes. const token = await this.getToken(`repository:library/${repo}:pull`); From 2fdc2fbaedb8ddc0b874ffa9a8b8ff2b8618f353 Mon Sep 17 00:00:00 2001 From: "Brandon Waterloo [MSFT]" <36966225+bwateratmsft@users.noreply.github.com> Date: Wed, 17 Jun 2020 09:36:39 -0400 Subject: [PATCH 3/9] Begone tabs --- .vscode/launch.json | 222 ++++++++++++++++++++++---------------------- 1 file changed, 111 insertions(+), 111 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 1241b8f4e1..08feff7e75 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,114 +1,114 @@ // A launch configuration that compiles the extension and then opens it inside a new window { - "version": "0.1.0", - "configurations": [ - { - "name": "Launch Extension", - "type": "extensionHost", - "request": "launch", - "runtimeExecutable": "${execPath}", - "args": [ - "--extensionDevelopmentPath=${workspaceFolder}" - ], - "env": { - "AZCODE_DOCKER_IGNORE_BUNDLE": "1", - "DEBUGTELEMETRY": "v" - }, - "stopOnEntry": false, - "sourceMaps": true, - "outFiles": [ - "${workspaceFolder}/out/**/*.js" - ], - "preLaunchTask": "${defaultBuildTask}" - }, - { - "name": "Launch Extension (webpack)", - "type": "extensionHost", - "request": "launch", - "runtimeExecutable": "${execPath}", - "args": [ - "--extensionDevelopmentPath=${workspaceFolder}" - ], - "env": { - "DEBUGTELEMETRY": "1" - }, - "stopOnEntry": false, - "sourceMaps": true, - "outFiles": [ - "${workspaceFolder}/out/**/*.js" - ], - "preLaunchTask": "npm: webpack" - }, - { - "name": "Launch Tests", - "type": "extensionHost", - "request": "launch", - "runtimeExecutable": "${execPath}", - "args": [ - "${workspaceFolder}/test/test.code-workspace", - "--extensionDevelopmentPath=${workspaceFolder}", - "--extensionTestsPath=${workspaceFolder}/out/test" - ], - "stopOnEntry": false, - "sourceMaps": true, - "outFiles": [ - "${workspaceFolder}/out/**/*.js" - ], - "preLaunchTask": "${defaultBuildTask}", - "env": { - "AZCODE_DOCKER_IGNORE_BUNDLE": "1", - "MOCHA_grep": "", // RegExp of tests to run (empty means all) - "MOCHA_enableTimeouts": "0", // Disable time-outs - "DEBUGTELEMETRY": "1", - "NODE_DEBUG": "" - } - }, - { - "name": "Launch Tests (unit)", - "type": "extensionHost", - "request": "launch", - "runtimeExecutable": "${execPath}", - "args": [ - "${workspaceFolder}/test/test.code-workspace", - "--extensionDevelopmentPath=${workspaceFolder}", - "--extensionTestsPath=${workspaceFolder}/out/test" - ], - "stopOnEntry": false, - "sourceMaps": true, - "outFiles": [ - "${workspaceFolder}/out/**/*.js" - ], - "preLaunchTask": "${defaultBuildTask}", - "env": { - "AZCODE_DOCKER_IGNORE_BUNDLE": "1", - "MOCHA_grep": "\\(unit\\)", // RegExp of tests to run (empty means all) - "MOCHA_enableTimeouts": "0", // Disable time-outs - "DEBUGTELEMETRY": "1", - "NODE_DEBUG": "" - } - }, - { - "name": "Launch Tests (webpack)", - "type": "extensionHost", - "request": "launch", - "runtimeExecutable": "${execPath}", - "args": [ - "${workspaceFolder}/test/test.code-workspace", - "--extensionDevelopmentPath=${workspaceFolder}", - "--extensionTestsPath=${workspaceFolder}/dist/test" - ], - "stopOnEntry": false, - "sourceMaps": true, - "outFiles": [ - "${workspaceFolder}/dist/**/*.js" - ], - "preLaunchTask": "npm: webpack", - "env": { - "MOCHA_grep": "", // RegExp of tests to run (empty means all) - "MOCHA_enableTimeouts": "0", // Disable time-outs - "DEBUGTELEMETRY": "1", - "NODE_DEBUG": "" - } - } - ] + "version": "0.1.0", + "configurations": [ + { + "name": "Launch Extension", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}" + ], + "env": { + "AZCODE_DOCKER_IGNORE_BUNDLE": "1", + "DEBUGTELEMETRY": "v" + }, + "stopOnEntry": false, + "sourceMaps": true, + "outFiles": [ + "${workspaceFolder}/out/**/*.js" + ], + "preLaunchTask": "${defaultBuildTask}" + }, + { + "name": "Launch Extension (webpack)", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}" + ], + "env": { + "DEBUGTELEMETRY": "1" + }, + "stopOnEntry": false, + "sourceMaps": true, + "outFiles": [ + "${workspaceFolder}/out/**/*.js" + ], + "preLaunchTask": "npm: webpack" + }, + { + "name": "Launch Tests", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "${workspaceFolder}/test/test.code-workspace", + "--extensionDevelopmentPath=${workspaceFolder}", + "--extensionTestsPath=${workspaceFolder}/out/test" + ], + "stopOnEntry": false, + "sourceMaps": true, + "outFiles": [ + "${workspaceFolder}/out/**/*.js" + ], + "preLaunchTask": "${defaultBuildTask}", + "env": { + "AZCODE_DOCKER_IGNORE_BUNDLE": "1", + "MOCHA_grep": "", // RegExp of tests to run (empty means all) + "MOCHA_enableTimeouts": "0", // Disable time-outs + "DEBUGTELEMETRY": "1", + "NODE_DEBUG": "" + } + }, + { + "name": "Launch Tests (unit)", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "${workspaceFolder}/test/test.code-workspace", + "--extensionDevelopmentPath=${workspaceFolder}", + "--extensionTestsPath=${workspaceFolder}/out/test" + ], + "stopOnEntry": false, + "sourceMaps": true, + "outFiles": [ + "${workspaceFolder}/out/**/*.js" + ], + "preLaunchTask": "${defaultBuildTask}", + "env": { + "AZCODE_DOCKER_IGNORE_BUNDLE": "1", + "MOCHA_grep": "\\(unit\\)", // RegExp of tests to run (empty means all) + "MOCHA_enableTimeouts": "0", // Disable time-outs + "DEBUGTELEMETRY": "1", + "NODE_DEBUG": "" + } + }, + { + "name": "Launch Tests (webpack)", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "${workspaceFolder}/test/test.code-workspace", + "--extensionDevelopmentPath=${workspaceFolder}", + "--extensionTestsPath=${workspaceFolder}/dist/test" + ], + "stopOnEntry": false, + "sourceMaps": true, + "outFiles": [ + "${workspaceFolder}/dist/**/*.js" + ], + "preLaunchTask": "npm: webpack", + "env": { + "MOCHA_grep": "", // RegExp of tests to run (empty means all) + "MOCHA_enableTimeouts": "0", // Disable time-outs + "DEBUGTELEMETRY": "1", + "NODE_DEBUG": "" + } + } + ] } From 11f0948c8e2ee4360b43f2b28e6be419f13dc10d Mon Sep 17 00:00:00 2001 From: "Brandon Waterloo [MSFT]" <36966225+bwateratmsft@users.noreply.github.com> Date: Wed, 17 Jun 2020 13:59:54 -0400 Subject: [PATCH 4/9] Make markOutdatedImages sync --- src/tree/images/ImagesTreeItem.ts | 2 +- src/tree/images/OutdatedImageChecker.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tree/images/ImagesTreeItem.ts b/src/tree/images/ImagesTreeItem.ts index 70d129e0d4..7aad066c06 100644 --- a/src/tree/images/ImagesTreeItem.ts +++ b/src/tree/images/ImagesTreeItem.ts @@ -64,7 +64,7 @@ export class ImagesTreeItem extends LocalRootTreeItemBase this.getAuthContext()); } - public async markOutdatedImages(images: ILocalImageInfo[]): Promise { + public markOutdatedImages(images: ILocalImageInfo[]): void { if (this.shouldLoad) { this.shouldLoad = false; From 11a190e679232753bd9c003b90a59b93be0b0f65 Mon Sep 17 00:00:00 2001 From: "Brandon Waterloo [MSFT]" <36966225+bwateratmsft@users.noreply.github.com> Date: Wed, 1 Jul 2020 15:13:12 -0400 Subject: [PATCH 5/9] Update outdated checker --- src/docker/Images.ts | 1 + src/tree/images/ImageGroupTreeItem.ts | 4 ++-- src/tree/images/ImageTreeItem.ts | 9 +++++---- src/tree/images/ImagesTreeItem.ts | 12 +++++++----- src/tree/images/OutdatedImageChecker.ts | 21 ++++++++++----------- 5 files changed, 25 insertions(+), 22 deletions(-) diff --git a/src/docker/Images.ts b/src/docker/Images.ts index a80b128d32..53680b9c45 100644 --- a/src/docker/Images.ts +++ b/src/docker/Images.ts @@ -12,6 +12,7 @@ export interface DockerImage extends DockerObject { export interface DockerImageInspection extends DockerObject { readonly Config?: { readonly ExposedPorts?: { readonly [portAndProtocol: string]: unknown; }; + readonly Image?: string; }; readonly Name: undefined; // Not defined for inspection diff --git a/src/tree/images/ImageGroupTreeItem.ts b/src/tree/images/ImageGroupTreeItem.ts index 97a3c78a23..421974edc8 100644 --- a/src/tree/images/ImageGroupTreeItem.ts +++ b/src/tree/images/ImageGroupTreeItem.ts @@ -3,12 +3,12 @@ * Licensed under the MIT License. See LICENSE.md in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { DockerImage } from "../../docker/Images"; import { IconPath } from "../IconPath"; import { LocalGroupTreeItemBase } from "../LocalGroupTreeItemBase"; import { getImageGroupIcon, ImageProperty } from "./ImageProperties"; +import { DatedDockerImage } from "./ImagesTreeItem"; -export class ImageGroupTreeItem extends LocalGroupTreeItemBase { +export class ImageGroupTreeItem extends LocalGroupTreeItemBase { public static readonly contextValue: string = 'imageGroup'; public readonly contextValue: string = ImageGroupTreeItem.contextValue; public childTypeLabel: string = 'image'; diff --git a/src/tree/images/ImageTreeItem.ts b/src/tree/images/ImageTreeItem.ts index 8424c39630..b477e2dfad 100644 --- a/src/tree/images/ImageTreeItem.ts +++ b/src/tree/images/ImageTreeItem.ts @@ -4,17 +4,18 @@ *--------------------------------------------------------------------------------------------*/ import { AzExtParentTreeItem, AzExtTreeItem, IActionContext } from "vscode-azureextensionui"; -import { DockerImage } from '../../docker/Images'; import { ext } from '../../extensionVariables'; +import { localize } from "../../localize"; import { getThemedIconPath, IconPath } from '../IconPath'; import { getTreeId } from "../LocalRootTreeItemBase"; +import { DatedDockerImage } from "./ImagesTreeItem"; export class ImageTreeItem extends AzExtTreeItem { public static contextValue: string = 'image'; public contextValue: string = ImageTreeItem.contextValue; - private readonly _item: DockerImage; + private readonly _item: DatedDockerImage; - public constructor(parent: AzExtParentTreeItem, itemInfo: DockerImage) { + public constructor(parent: AzExtParentTreeItem, itemInfo: DatedDockerImage) { super(parent); this._item = itemInfo; } @@ -44,7 +45,7 @@ export class ImageTreeItem extends AzExtTreeItem { } public get iconPath(): IconPath { - if (this._item.outdated) { + if (this._item.Outdated) { return getThemedIconPath('statusWarning'); } diff --git a/src/tree/images/ImagesTreeItem.ts b/src/tree/images/ImagesTreeItem.ts index 891ddec1c8..e0c20c4e49 100644 --- a/src/tree/images/ImagesTreeItem.ts +++ b/src/tree/images/ImagesTreeItem.ts @@ -15,15 +15,17 @@ import { getImagePropertyValue, imageProperties, ImageProperty } from "./ImagePr import { ImageTreeItem } from "./ImageTreeItem"; import { OutdatedImageChecker } from "./OutdatedImageChecker"; -export class ImagesTreeItem extends LocalRootTreeItemBase { +export type DatedDockerImage = DockerImage & { Outdated?: boolean }; + +export class ImagesTreeItem extends LocalRootTreeItemBase { private readonly outdatedImageChecker: OutdatedImageChecker = new OutdatedImageChecker(); public treePrefix: string = 'images'; public label: string = localize('vscode-docker.tree.images.label', 'Images'); public configureExplorerTitle: string = localize('vscode-docker.tree.images.configure', 'Configure images explorer'); - public childType: LocalChildType = ImageTreeItem; - public childGroupType: LocalChildGroupType = ImageGroupTreeItem; + public childType: LocalChildType = ImageTreeItem; + public childGroupType: LocalChildGroupType = ImageGroupTreeItem; public labelSettingInfo: ITreeSettingInfo = { properties: imageProperties, @@ -44,8 +46,8 @@ export class ImagesTreeItem extends LocalRootTreeItemBase { - const result = ext.dockerClient.getImages(context); + public async getItems(context: IActionContext): Promise { + const result = await ext.dockerClient.getImages(context); this.outdatedImageChecker.markOutdatedImages(result); return result; } diff --git a/src/tree/images/OutdatedImageChecker.ts b/src/tree/images/OutdatedImageChecker.ts index b292a36c20..56776a3575 100644 --- a/src/tree/images/OutdatedImageChecker.ts +++ b/src/tree/images/OutdatedImageChecker.ts @@ -7,14 +7,14 @@ import { Response } from 'request'; import * as request from 'request-promise-native'; import * as vscode from 'vscode'; import { callWithTelemetryAndErrorHandling, IActionContext } from 'vscode-azureextensionui'; +import { DockerImage } from '../../docker/Images'; import { ext } from '../../extensionVariables'; import { localize } from '../../localize'; -import { callDockerodeAsync } from '../../utils/callDockerode'; import { AsyncLazy, Lazy } from '../../utils/lazy'; import { IOAuthContext } from '../registries/auth/IAuthProvider'; import { getWwwAuthenticateContext } from '../registries/auth/oAuthUtils'; import { getImagePropertyValue } from './ImageProperties'; -import { ILocalImageInfo } from './LocalImageInfo'; +import { DatedDockerImage } from './ImagesTreeItem'; const noneRegex = //i; @@ -32,7 +32,7 @@ export class OutdatedImageChecker { this.authContext = new AsyncLazy(async () => this.getAuthContext()); } - public markOutdatedImages(images: ILocalImageInfo[]): void { + public markOutdatedImages(images: DatedDockerImage[]): void { if (this.shouldLoad) { this.shouldLoad = false; @@ -47,8 +47,8 @@ export class OutdatedImageChecker { return /docker[.]io\/library/i.test(getImagePropertyValue(image, 'Registry')); }) .map(async (image) => { - if (await this.checkImage(image) === 'outdated') { - this.outdatedImageIds.push(image.imageId); + if (await this.checkImage(context, image) === 'outdated') { + this.outdatedImageIds.push(image.Id); } }); @@ -65,13 +65,13 @@ export class OutdatedImageChecker { } for (const image of images) { - image.outdated = this.outdatedImageIds.some(i => i.toLowerCase() === image.imageId.toLowerCase()); + image.Outdated = this.outdatedImageIds.some(i => i.toLowerCase() === image.Id.toLowerCase()); } } - private async checkImage(image: ILocalImageInfo): Promise<'latest' | 'outdated' | 'unknown'> { + private async checkImage(context: IActionContext, image: DockerImage): Promise<'latest' | 'outdated' | 'unknown'> { try { - const [repo, tag] = image.fullTag.split(':'); + const [repo, tag] = image.Name.split(':'); if (noneRegex.test(repo) || noneRegex.test(tag)) { return 'outdated'; @@ -84,10 +84,9 @@ export class OutdatedImageChecker { const latestConfigImageId = await this.getLatestConfigImageId(repo, tag, token); // 3. Compare it with the current image's value - const dockerodeImage = ext.dockerode.getImage(image.fullTag); - const imageInspectInfo = await callDockerodeAsync(async () => dockerodeImage.inspect()); + const imageInspectInfo = await ext.dockerClient.inspectImage(context, image.Id); - if (latestConfigImageId.toLowerCase() !== imageInspectInfo.Config.Image.toLowerCase()) { + if (latestConfigImageId.toLowerCase() !== imageInspectInfo?.Config?.Image?.toLowerCase()) { return 'outdated'; } From 0e0755aea09a7e8349e85618b837f6b6a550eb0c Mon Sep 17 00:00:00 2001 From: "Brandon Waterloo [MSFT]" <36966225+bwateratmsft@users.noreply.github.com> Date: Tue, 7 Jul 2020 10:15:04 -0400 Subject: [PATCH 6/9] Move setting --- package.json | 10 +++++----- package.nls.json | 2 +- src/tree/images/OutdatedImageChecker.ts | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 8ff38a11be..de2c4674c6 100644 --- a/package.json +++ b/package.json @@ -1782,6 +1782,11 @@ "Label" ] }, + "docker.images.checkForOutdatedImages": { + "type": "boolean", + "default": true, + "description": "%vscode-docker.config.docker.images.checkForOutdatedImages%" + }, "docker.networks.groupBy": { "type": "string", "default": "None", @@ -2047,11 +2052,6 @@ "type": "boolean", "default": true, "description": "%vscode-docker.config.docker.showRemoteWorkspaceWarning%" - }, - "docker.checkForOutdatedImages": { - "type": "boolean", - "default": true, - "description": "%vscode-docker.config.docker.checkForOutdatedImages%" } } }, diff --git a/package.nls.json b/package.nls.json index f825a2480d..8e31e80db6 100644 --- a/package.nls.json +++ b/package.nls.json @@ -136,6 +136,7 @@ "vscode-docker.config.docker.images.description": "Any secondary properties to display for a image (an array). Possible elements include: CreatedTime, FullTag, ImageId, Registry, Repository, RepositoryName, RepositoryNameAndTag, and Tag", "vscode-docker.config.docker.images.label": "The primary property to display for a image: CreatedTime, FullTag, ImageId, Registry, Repository, RepositoryName, RepositoryNameAndTag, or Tag", "vscode-docker.config.docker.images.sortBy": "The property to use to sort images in Docker view: CreatedTime or Label", + "vscode-docker.config.docker.images.checkForOutdatedImages": "Whether to check for outdated base images once per session", "vscode-docker.config.docker.networks.groupBy": "The property to use to group networks in Docker view: CreatedTime, NetworkDriver, NetworkId, NetworkName, or None", "vscode-docker.config.docker.networks.description": "Any secondary properties to display for a Docker network (an array). Possible elements include CreatedTime, NetworkDriver, NetworkId, and NetworkName", "vscode-docker.config.docker.networks.label": "The primary property to display for a Docker network: CreatedTime, NetworkDriver, NetworkId, or NetworkName", @@ -167,7 +168,6 @@ "vscode-docker.config.docker.dockerComposeBuild": "Set to true to include --build option when docker-compose command is invoked", "vscode-docker.config.docker.dockerComposeDetached": "Set to true to include --d (detached) option when docker-compose command is invoked", "vscode-docker.config.docker.showRemoteWorkspaceWarning": "Set to true to prompt to switch from \"UI\" extension mode to \"Workspace\" extension mode if an operation is not supported in UI mode.", - "vscode-docker.config.docker.checkForOutdatedImages": "Whether to check for outdated base images once per session", "vscode-docker.config.deprecated": "This setting has been deprecated and will be removed in a future release.", "vscode-docker.commands.api.configure": "Add Docker Files to Workspace (API)...", "vscode-docker.commands.compose.down": "Compose Down", diff --git a/src/tree/images/OutdatedImageChecker.ts b/src/tree/images/OutdatedImageChecker.ts index 56776a3575..4a926ad205 100644 --- a/src/tree/images/OutdatedImageChecker.ts +++ b/src/tree/images/OutdatedImageChecker.ts @@ -26,7 +26,7 @@ export class OutdatedImageChecker { public constructor() { const dockerConfig = vscode.workspace.getConfiguration('docker'); - this.shouldLoad = dockerConfig.get('checkForOutdatedImages'); + this.shouldLoad = dockerConfig.get('images.checkForOutdatedImages'); this.defaultRequestOptions = new Lazy(() => this.getRequestOptions()); this.authContext = new AsyncLazy(async () => this.getAuthContext()); From ce4e70b1d27bf9c5755ef77fca02b4945be6a89e Mon Sep 17 00:00:00 2001 From: "Brandon Waterloo [MSFT]" <36966225+bwateratmsft@users.noreply.github.com> Date: Tue, 7 Jul 2020 10:17:36 -0400 Subject: [PATCH 7/9] Change to an interface --- src/tree/images/ImagesTreeItem.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/tree/images/ImagesTreeItem.ts b/src/tree/images/ImagesTreeItem.ts index e0c20c4e49..c41aad87dc 100644 --- a/src/tree/images/ImagesTreeItem.ts +++ b/src/tree/images/ImagesTreeItem.ts @@ -15,7 +15,9 @@ import { getImagePropertyValue, imageProperties, ImageProperty } from "./ImagePr import { ImageTreeItem } from "./ImageTreeItem"; import { OutdatedImageChecker } from "./OutdatedImageChecker"; -export type DatedDockerImage = DockerImage & { Outdated?: boolean }; +export interface DatedDockerImage extends DockerImage { + Outdated?: boolean; +} export class ImagesTreeItem extends LocalRootTreeItemBase { private readonly outdatedImageChecker: OutdatedImageChecker = new OutdatedImageChecker(); From f463889a9bd4f66d3e4169e667f5741db069140c Mon Sep 17 00:00:00 2001 From: "Brandon Waterloo [MSFT]" <36966225+bwateratmsft@users.noreply.github.com> Date: Tue, 7 Jul 2020 10:18:49 -0400 Subject: [PATCH 8/9] Update setting description --- package.nls.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.nls.json b/package.nls.json index 8e31e80db6..0b85acf926 100644 --- a/package.nls.json +++ b/package.nls.json @@ -136,7 +136,7 @@ "vscode-docker.config.docker.images.description": "Any secondary properties to display for a image (an array). Possible elements include: CreatedTime, FullTag, ImageId, Registry, Repository, RepositoryName, RepositoryNameAndTag, and Tag", "vscode-docker.config.docker.images.label": "The primary property to display for a image: CreatedTime, FullTag, ImageId, Registry, Repository, RepositoryName, RepositoryNameAndTag, or Tag", "vscode-docker.config.docker.images.sortBy": "The property to use to sort images in Docker view: CreatedTime or Label", - "vscode-docker.config.docker.images.checkForOutdatedImages": "Whether to check for outdated base images once per session", + "vscode-docker.config.docker.images.checkForOutdatedImages": "Check for outdated base images once per session", "vscode-docker.config.docker.networks.groupBy": "The property to use to group networks in Docker view: CreatedTime, NetworkDriver, NetworkId, NetworkName, or None", "vscode-docker.config.docker.networks.description": "Any secondary properties to display for a Docker network (an array). Possible elements include CreatedTime, NetworkDriver, NetworkId, and NetworkName", "vscode-docker.config.docker.networks.label": "The primary property to display for a Docker network: CreatedTime, NetworkDriver, NetworkId, or NetworkName", From a5b2a20886d2b179faa157d9eba68bfd2029a833 Mon Sep 17 00:00:00 2001 From: "Brandon Waterloo [MSFT]" <36966225+bwateratmsft@users.noreply.github.com> Date: Wed, 8 Jul 2020 10:39:20 -0400 Subject: [PATCH 9/9] Karol's feedback --- package.nls.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.nls.json b/package.nls.json index 0b85acf926..69024c37fc 100644 --- a/package.nls.json +++ b/package.nls.json @@ -136,7 +136,7 @@ "vscode-docker.config.docker.images.description": "Any secondary properties to display for a image (an array). Possible elements include: CreatedTime, FullTag, ImageId, Registry, Repository, RepositoryName, RepositoryNameAndTag, and Tag", "vscode-docker.config.docker.images.label": "The primary property to display for a image: CreatedTime, FullTag, ImageId, Registry, Repository, RepositoryName, RepositoryNameAndTag, or Tag", "vscode-docker.config.docker.images.sortBy": "The property to use to sort images in Docker view: CreatedTime or Label", - "vscode-docker.config.docker.images.checkForOutdatedImages": "Check for outdated base images once per session", + "vscode-docker.config.docker.images.checkForOutdatedImages": "Check for outdated base images once per Visual Studio Code session", "vscode-docker.config.docker.networks.groupBy": "The property to use to group networks in Docker view: CreatedTime, NetworkDriver, NetworkId, NetworkName, or None", "vscode-docker.config.docker.networks.description": "Any secondary properties to display for a Docker network (an array). Possible elements include CreatedTime, NetworkDriver, NetworkId, and NetworkName", "vscode-docker.config.docker.networks.label": "The primary property to display for a Docker network: CreatedTime, NetworkDriver, NetworkId, or NetworkName",