diff --git a/package-lock.json b/package-lock.json index 955306476e..69a751f00d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -824,7 +824,7 @@ "node_modules/@microsoft/vscode-docker-registries": { "version": "0.0.1-alpha", "resolved": "file:../vscode-docker-extensibility/packages/vscode-docker-registries/microsoft-vscode-docker-registries-0.0.1-alpha.tgz", - "integrity": "sha512-AUa6YyRJgGzc96gJylSiI+EGm0B5s28LS7Hi8yHH4aIMz8vU/yK5O9PWrt/GxIxm2AHP4oFqQ59lWy2mk2xunA==", + "integrity": "sha512-SA18suNyHYMsmrfWRN4fkgAHXHG29inYpy5vfhHv7TNs7ha7HRZp93NXpebD1DuPYxFPpmFx3rjdLyADc6DdNA==", "license": "See LICENSE in the project root for license information.", "dependencies": { "dayjs": "^1.11.7", diff --git a/package.json b/package.json index f3c6b6149d..f6ac326631 100644 --- a/package.json +++ b/package.json @@ -511,7 +511,7 @@ }, { "command": "vscode-docker.registries.copyImageDigest", - "when": "view == dockerRegistries && viewItem =~ /registryV2Tag/", + "when": "view == dockerRegistries && viewItem =~ /(dockerHubTag|registryV2Tag|azureContainerTag|githubRegistryTag)/", "group": "regs_tag_1_general@3" }, { @@ -531,7 +531,7 @@ }, { "command": "vscode-docker.registries.deleteImage", - "when": "view == dockerRegistries && viewItem =~ /DockerV2;Tag;/", + "when": "view == dockerRegistries && viewItem =~ /(genericRegistryV2Tag|azureContainerTag|githubRegistryTag)/i", "group": "regs_tag_2_destructive@2" }, { diff --git a/src/commands/images/pushImage.ts b/src/commands/images/pushImage.ts index 3626f9ce36..4a15304cac 100644 --- a/src/commands/images/pushImage.ts +++ b/src/commands/images/pushImage.ts @@ -59,8 +59,8 @@ export async function pushImage(context: IActionContext, node: ImageTreeItem | u // Give the user a chance to modify the tag however they want const finalTag = await tagImage(context, node, connectedRegistry); - - if (connectedRegistry && finalTag.startsWith(getBaseImagePathFromRegistryItem(connectedRegistry.wrappedItem))) { + const baseImagePath = getBaseImagePathFromRegistryItem(connectedRegistry.wrappedItem); + if (connectedRegistry && finalTag.startsWith(baseImagePath)) { // If a registry was found/chosen and is still the same as the final tag's registry, try logging in await vscode.commands.executeCommand('vscode-docker.registries.logInToDockerCli', connectedRegistry); } diff --git a/src/commands/images/tagImage.ts b/src/commands/images/tagImage.ts index ebc8554e61..f5bfd8f97b 100644 --- a/src/commands/images/tagImage.ts +++ b/src/commands/images/tagImage.ts @@ -11,7 +11,7 @@ import { ImageTreeItem } from '../../tree/images/ImageTreeItem'; import { UnifiedRegistryItem } from '../../tree/registries/UnifiedRegistryTreeDataProvider'; import { getBaseImagePathFromRegistryItem } from '../../tree/registries/registryTreeUtils'; -export async function tagImage(context: IActionContext, node?: ImageTreeItem, registry?: UnifiedRegistryItem): Promise { +export async function tagImage(context: IActionContext, node?: ImageTreeItem, registry?: UnifiedRegistryItem): Promise { if (!node) { await ext.imagesTree.refresh(context); node = await ext.imagesTree.showTreeItemPicker(ImageTreeItem.contextValue, { @@ -21,7 +21,7 @@ export async function tagImage(context: IActionContext, node?: ImageTreeItem, re } addImageTaggingTelemetry(context, node.fullTag, '.before'); - const baseImagePath = isRegistry(registry) ? getBaseImagePathFromRegistryItem(registry.wrappedItem as CommonRegistry) : undefined; + const baseImagePath = isRegistry(registry.wrappedItem) ? getBaseImagePathFromRegistryItem(registry.wrappedItem) : undefined; const newTaggedName: string = await getTagFromUserInput(context, node.fullTag, baseImagePath); addImageTaggingTelemetry(context, newTaggedName, '.after'); diff --git a/src/commands/registerCommands.ts b/src/commands/registerCommands.ts index c7403f0be3..2e8a1481c9 100644 --- a/src/commands/registerCommands.ts +++ b/src/commands/registerCommands.ts @@ -64,6 +64,7 @@ import { viewAzureProperties } from "./registries/azure/viewAzureProperties"; import { connectRegistry } from "./registries/connectRegistry"; import { copyRemoteFullTag } from "./registries/copyRemoteFullTag"; import { copyRemoteImageDigest } from "./registries/copyRemoteImageDigest"; +import { deleteRemoteImage } from "./registries/deleteRemoteImage"; import { disconnectRegistry } from "./registries/disconnectRegistry"; import { openDockerHubInBrowser } from "./registries/dockerHub/openDockerHubInBrowser"; import { logInToDockerCli } from "./registries/logInToDockerCli"; @@ -165,7 +166,7 @@ export function registerCommands(): void { registerCommand('vscode-docker.registries.connectRegistry', connectRegistry); registerCommand('vscode-docker.registries.copyImageDigest', copyRemoteImageDigest); registerCommand('vscode-docker.registries.copyRemoteFullTag', copyRemoteFullTag); - // registerCommand('vscode-docker.registries.deleteImage', deleteRemoteImage); + registerCommand('vscode-docker.registries.deleteImage', deleteRemoteImage); registerCommand('vscode-docker.registries.deployImageToAzure', deployImageToAzure); registerCommand('vscode-docker.registries.deployImageToAca', deployImageToAca); registerCommand('vscode-docker.registries.disconnectRegistry', disconnectRegistry); diff --git a/src/commands/registries/copyRemoteImageDigest.ts b/src/commands/registries/copyRemoteImageDigest.ts index 1e022c75cf..62f9164d6c 100644 --- a/src/commands/registries/copyRemoteImageDigest.ts +++ b/src/commands/registries/copyRemoteImageDigest.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IActionContext, contextValueExperience } from "@microsoft/vscode-azext-utils"; -import { CommonTag, RegistryV2DataProvider, V2Tag } from "@microsoft/vscode-docker-registries"; +import { CommonRegistryDataProvider, CommonTag } from "@microsoft/vscode-docker-registries"; import * as vscode from "vscode"; import { ext } from "../../extensionVariables"; import { UnifiedRegistryItem } from "../../tree/registries/UnifiedRegistryTreeDataProvider"; @@ -14,8 +14,8 @@ export async function copyRemoteImageDigest(context: IActionContext, node?: Unif node = await contextValueExperience(context, ext.registriesTree, { include: ['registryV2Tag'] }); } - const v2DataProvider = node.provider as unknown as RegistryV2DataProvider; - const digest = await v2DataProvider.getImageDigest(node.wrappedItem as V2Tag); + const v2DataProvider = node.provider as unknown as CommonRegistryDataProvider; + const digest = await v2DataProvider.getImageDigest?.(node.wrappedItem as CommonTag); /* eslint-disable-next-line @typescript-eslint/no-floating-promises */ vscode.env.clipboard.writeText(digest); diff --git a/src/commands/registries/deleteRemoteImage.ts b/src/commands/registries/deleteRemoteImage.ts index 0495177910..6a16de34cd 100644 --- a/src/commands/registries/deleteRemoteImage.ts +++ b/src/commands/registries/deleteRemoteImage.ts @@ -1,37 +1,50 @@ -// /*--------------------------------------------------------------------------------------------- -// * Copyright (c) Microsoft Corporation. All rights reserved. -// * Licensed under the MIT License. See LICENSE.md in the project root for license information. -// *--------------------------------------------------------------------------------------------*/ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ -// import { DialogResponses, IActionContext } from '@microsoft/vscode-azext-utils'; -// import { l10n, ProgressLocation, window } from 'vscode'; -// import { ext } from '../../extensionVariables'; -// import { DockerV2TagTreeItem } from '../../tree/registries/dockerV2/DockerV2TagTreeItem'; -// import { registryExpectedContextValues } from '../../tree/registries/registryContextValues'; +import { DialogResponses, IActionContext, UserCancelledError, parseError } from '@microsoft/vscode-azext-utils'; +import { CommonTag, GenericRegistryV2DataProvider } from '@microsoft/vscode-docker-registries'; +import { ProgressLocation, l10n, window } from 'vscode'; +import { ext } from '../../extensionVariables'; +import { UnifiedRegistryItem } from '../../tree/registries/UnifiedRegistryTreeDataProvider'; +import { getImageNameFromRegistryTagItem } from '../../tree/registries/registryTreeUtils'; +import { registryExperience } from '../../utils/registryExperience'; -// export async function deleteRemoteImage(context: IActionContext, node?: DockerV2TagTreeItem): Promise { -// if (!node) { -// node = await ext.registriesTree.showTreeItemPicker(registryExpectedContextValues.dockerV2.tag, { -// ...context, -// suppressCreatePick: true, -// noItemFoundErrorMessage: l10n.t('No remote images are available to delete') -// }); -// } +export async function deleteRemoteImage(context: IActionContext, node?: UnifiedRegistryItem): Promise { + if (!node) { + node = await registryExperience(context, ext.registriesTree, { include: ['genericRegistryV2Tag', 'azureContainerTag', 'githubRegistryTag'] }, false); + } -// const confirmDelete = l10n.t('Are you sure you want to delete image "{0}"? This will delete all images that have the same digest.', node.repoNameAndTag); -// // no need to check result - cancel will throw a UserCancelledError -// await context.ui.showWarningMessage(confirmDelete, { modal: true }, DialogResponses.deleteResponse); + const tagName = getImageNameFromRegistryTagItem(node.wrappedItem); + const confirmDelete = l10n.t('Are you sure you want to delete image "{0}"? This will delete all images that have the same digest.', tagName); + // no need to check result - cancel will throw a UserCancelledError + await context.ui.showWarningMessage(confirmDelete, { modal: true }, DialogResponses.deleteResponse); -// const repoTI = node.parent; -// const deleting = l10n.t('Deleting image "{0}"...', node.repoNameAndTag); -// await window.withProgress({ location: ProgressLocation.Notification, title: deleting }, async () => { -// await node.deleteTreeItem(context); -// }); + const deleting = l10n.t('Deleting image "{0}"...', tagName); + await window.withProgress({ location: ProgressLocation.Notification, title: deleting }, async () => { + const provider = node.provider as unknown as GenericRegistryV2DataProvider; -// // Other tags that also matched the image may have been deleted, so refresh the whole repository -// await repoTI.refresh(context); -// const message = l10n.t('Successfully deleted image "{0}".', node.repoNameAndTag); -// // don't wait -// /* eslint-disable-next-line @typescript-eslint/no-floating-promises */ -// window.showInformationMessage(message); -// } + try { + await provider.deleteTag(node.wrappedItem); + } catch (error) { + const errorType: string = parseError(error).errorType.toLowerCase(); + if (errorType === '405' || errorType === 'unsupported') { + // Don't wait + // eslint-disable-next-line @typescript-eslint/no-floating-promises + context.ui.showWarningMessage('Deleting remote images is not supported on this registry. It may need to be enabled.', { learnMoreLink: 'https://aka.ms/AA7jsql' }); + throw new UserCancelledError(); + } else { + throw error; + } + } + }); + + // TODO: investigate if we can do this for GitHub + + // Other tags that also matched the image may have been deleted, so refresh the whole repository + // don't wait + void ext.registriesTree.refresh(); + /* eslint-disable-next-line @typescript-eslint/no-floating-promises */ + window.showInformationMessage(l10n.t('Successfully deleted image "{0}".', tagName)); +} diff --git a/src/tree/registries/Azure/AzureRegistryDataProvider.ts b/src/tree/registries/Azure/AzureRegistryDataProvider.ts index 2da1ca14f1..f3ac530d4d 100644 --- a/src/tree/registries/Azure/AzureRegistryDataProvider.ts +++ b/src/tree/registries/Azure/AzureRegistryDataProvider.ts @@ -181,7 +181,7 @@ export class AzureRegistryDataProvider extends RegistryV2DataProvider implements await client.registries.beginDeleteAndWait(resourceGroup, item.label); } - public async deleteTag(item: AzureTag): Promise { + public override async deleteTag(item: AzureTag): Promise { const authenticationProvider = this.getAuthenticationProvider(item.parent.parent as unknown as AzureRegistryItem); const reponse = await registryV2Request({ @@ -194,7 +194,6 @@ export class AzureRegistryDataProvider extends RegistryV2DataProvider implements if (!reponse.succeeded) { throw new Error(`Failed to delete tag: ${reponse.statusText}`); - } } diff --git a/src/tree/registries/registryTreeUtils.ts b/src/tree/registries/registryTreeUtils.ts index 6c8fc98acc..b07b6ad88a 100644 --- a/src/tree/registries/registryTreeUtils.ts +++ b/src/tree/registries/registryTreeUtils.ts @@ -25,9 +25,12 @@ export function getBaseImagePathFromRegistryItem(registry: CommonRegistry): stri switch (registry.additionalContextValues?.[0] ?? '') { case 'azureContainerRegistry': - case 'genericRegistryV2': { + case 'genericRegistryV2Registry': { return registry.baseUrl.authority.toLowerCase(); } + case 'githubRegistry': { + return `${registry.baseUrl.authority.toLowerCase()}/${registry.label}`; + } case 'dockerHubRegistry': default: return `${registry.label}`; diff --git a/src/utils/registryExperience.ts b/src/utils/registryExperience.ts index 25a8d5d5dc..e4a7eb0700 100644 --- a/src/utils/registryExperience.ts +++ b/src/utils/registryExperience.ts @@ -6,13 +6,13 @@ import { AzureWizardPromptStep, ContextValueFilter, IActionContext, QuickPickWizardContext, RecursiveQuickPickStep, runQuickPickWizard } from '@microsoft/vscode-azext-utils'; import * as vscode from 'vscode'; -export async function registryExperience(context: IActionContext, tdp: vscode.TreeDataProvider, contextValueFilter: ContextValueFilter): Promise { +export async function registryExperience(context: IActionContext, tdp: vscode.TreeDataProvider, contextValueFilter: ContextValueFilter, skipIfOne: boolean = true): Promise { const promptSteps: AzureWizardPromptStep[] = [ new RecursiveQuickPickStep( tdp, { contextValueFilter: contextValueFilter, - skipIfOne: true + skipIfOne: skipIfOne } ) ];