From f9be94ea1fc4465ea824cb5b62cd8c49ef3c7af7 Mon Sep 17 00:00:00 2001 From: alexyaang <59073590+alexyaang@users.noreply.github.com> Date: Mon, 7 Aug 2023 12:23:32 -0400 Subject: [PATCH 1/6] adjusted scheduleRunReuqest to work with new registries tree --- src/commands/images/buildImage.ts | 160 +++---- src/commands/images/tagImage.ts | 194 ++++---- src/commands/registerCommands.ts | 3 +- .../azure/tasks/buildImageInAzure.ts | 59 +-- .../azure/tasks/scheduleRunRequest.ts | 443 +++++++++--------- .../Azure/AzureRegistryDataProvider.ts | 10 +- 6 files changed, 444 insertions(+), 425 deletions(-) diff --git a/src/commands/images/buildImage.ts b/src/commands/images/buildImage.ts index a51eb7a44b..db6660c458 100644 --- a/src/commands/images/buildImage.ts +++ b/src/commands/images/buildImage.ts @@ -1,80 +1,80 @@ -// /*--------------------------------------------------------------------------------------------- -// * Copyright (c) Microsoft Corporation. All rights reserved. -// * Licensed under the MIT License. See LICENSE.md in the project root for license information. -// *--------------------------------------------------------------------------------------------*/ - -// import { IActionContext, UserCancelledError } from "@microsoft/vscode-azext-utils"; -// import * as path from "path"; -// import * as vscode from "vscode"; -// import { ext } from "../../extensionVariables"; -// import { TaskCommandRunnerFactory } from "../../runtimes/runners/TaskCommandRunnerFactory"; -// import { getOfficialBuildTaskForDockerfile } from "../../tasks/TaskHelper"; -// import { getValidImageNameFromPath } from "../../utils/getValidImageName"; -// import { delay } from "../../utils/promiseUtils"; -// import { quickPickDockerFileItem } from "../../utils/quickPickFile"; -// import { quickPickWorkspaceFolder } from "../../utils/quickPickWorkspaceFolder"; -// import { selectBuildCommand } from "../selectCommandTemplate"; -// import { addImageTaggingTelemetry, getTagFromUserInput } from "./tagImage"; - -// const tagRegex: RegExp = /\$\{tag\}/i; - -// export async function buildImage(context: IActionContext, dockerFileUri: vscode.Uri | undefined): Promise { -// if (!vscode.workspace.isTrusted) { -// throw new UserCancelledError('enforceTrust'); -// } - -// const configOptions: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration('docker'); -// const defaultContextPath = configOptions.get('imageBuildContextPath', ''); - -// let rootFolder: vscode.WorkspaceFolder; -// if (dockerFileUri) { -// rootFolder = vscode.workspace.getWorkspaceFolder(dockerFileUri); -// } - -// rootFolder = rootFolder || await quickPickWorkspaceFolder(context, vscode.l10n.t('To build Docker files you must first open a folder or workspace in VS Code.')); - -// const dockerFileItem = await quickPickDockerFileItem(context, dockerFileUri, rootFolder); -// const task = await getOfficialBuildTaskForDockerfile(context, dockerFileItem.absoluteFilePath, rootFolder); - -// if (task) { -// await vscode.tasks.executeTask(task); -// } else { -// const contextPath: string = defaultContextPath || dockerFileItem.relativeFolderPath; - -// const terminalCommand = await selectBuildCommand( -// context, -// rootFolder, -// dockerFileItem.relativeFilePath, -// contextPath -// ); - -// // Replace '${tag}' if needed. Tag is a special case because we don't want to prompt unless necessary, so must manually replace it. -// if (tagRegex.test(terminalCommand.command)) { -// const absFilePath: string = path.join(rootFolder.uri.fsPath, dockerFileItem.relativeFilePath); -// const dockerFileKey = `buildTag_${absFilePath}`; -// const prevImageName: string | undefined = ext.context.workspaceState.get(dockerFileKey); - -// // Get imageName based previous entries, else on name of subfolder containing the Dockerfile -// const suggestedImageName = prevImageName || getValidImageNameFromPath(dockerFileItem.absoluteFolderPath, 'latest'); - -// // Temporary work-around for vscode bug where valueSelection can be messed up if a quick pick is followed by a showInputBox -// await delay(500); - -// addImageTaggingTelemetry(context, suggestedImageName, '.before'); -// const imageName: string = await getTagFromUserInput(context, suggestedImageName); -// addImageTaggingTelemetry(context, imageName, '.after'); - -// await ext.context.workspaceState.update(dockerFileKey, imageName); -// terminalCommand.command = terminalCommand.command.replace(tagRegex, imageName); -// } - -// const client = await ext.runtimeManager.getClient(); -// const taskCRF = new TaskCommandRunnerFactory({ -// taskName: client.displayName, -// workspaceFolder: rootFolder, -// focus: true, -// }); - -// await taskCRF.getCommandRunner()(terminalCommand); -// } -// } +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IActionContext, UserCancelledError } from "@microsoft/vscode-azext-utils"; +import * as path from "path"; +import * as vscode from "vscode"; +import { ext } from "../../extensionVariables"; +import { TaskCommandRunnerFactory } from "../../runtimes/runners/TaskCommandRunnerFactory"; +import { getOfficialBuildTaskForDockerfile } from "../../tasks/TaskHelper"; +import { getValidImageNameFromPath } from "../../utils/getValidImageName"; +import { delay } from "../../utils/promiseUtils"; +import { quickPickDockerFileItem } from "../../utils/quickPickFile"; +import { quickPickWorkspaceFolder } from "../../utils/quickPickWorkspaceFolder"; +import { selectBuildCommand } from "../selectCommandTemplate"; +import { addImageTaggingTelemetry, getTagFromUserInput } from "./tagImage"; + +const tagRegex: RegExp = /\$\{tag\}/i; + +export async function buildImage(context: IActionContext, dockerFileUri: vscode.Uri | undefined): Promise { + if (!vscode.workspace.isTrusted) { + throw new UserCancelledError('enforceTrust'); + } + + const configOptions: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration('docker'); + const defaultContextPath = configOptions.get('imageBuildContextPath', ''); + + let rootFolder: vscode.WorkspaceFolder; + if (dockerFileUri) { + rootFolder = vscode.workspace.getWorkspaceFolder(dockerFileUri); + } + + rootFolder = rootFolder || await quickPickWorkspaceFolder(context, vscode.l10n.t('To build Docker files you must first open a folder or workspace in VS Code.')); + + const dockerFileItem = await quickPickDockerFileItem(context, dockerFileUri, rootFolder); + const task = await getOfficialBuildTaskForDockerfile(context, dockerFileItem.absoluteFilePath, rootFolder); + + if (task) { + await vscode.tasks.executeTask(task); + } else { + const contextPath: string = defaultContextPath || dockerFileItem.relativeFolderPath; + + const terminalCommand = await selectBuildCommand( + context, + rootFolder, + dockerFileItem.relativeFilePath, + contextPath + ); + + // Replace '${tag}' if needed. Tag is a special case because we don't want to prompt unless necessary, so must manually replace it. + if (tagRegex.test(terminalCommand.command)) { + const absFilePath: string = path.join(rootFolder.uri.fsPath, dockerFileItem.relativeFilePath); + const dockerFileKey = `buildTag_${absFilePath}`; + const prevImageName: string | undefined = ext.context.workspaceState.get(dockerFileKey); + + // Get imageName based previous entries, else on name of subfolder containing the Dockerfile + const suggestedImageName = prevImageName || getValidImageNameFromPath(dockerFileItem.absoluteFolderPath, 'latest'); + + // Temporary work-around for vscode bug where valueSelection can be messed up if a quick pick is followed by a showInputBox + await delay(500); + + addImageTaggingTelemetry(context, suggestedImageName, '.before'); + const imageName: string = await getTagFromUserInput(context, suggestedImageName); + addImageTaggingTelemetry(context, imageName, '.after'); + + await ext.context.workspaceState.update(dockerFileKey, imageName); + terminalCommand.command = terminalCommand.command.replace(tagRegex, imageName); + } + + const client = await ext.runtimeManager.getClient(); + const taskCRF = new TaskCommandRunnerFactory({ + taskName: client.displayName, + workspaceFolder: rootFolder, + focus: true, + }); + + await taskCRF.getCommandRunner()(terminalCommand); + } +} diff --git a/src/commands/images/tagImage.ts b/src/commands/images/tagImage.ts index dda54f7e1c..aff407e5bd 100644 --- a/src/commands/images/tagImage.ts +++ b/src/commands/images/tagImage.ts @@ -1,97 +1,97 @@ -// /*--------------------------------------------------------------------------------------------- -// * Copyright (c) Microsoft Corporation. All rights reserved. -// * Licensed under the MIT License. See LICENSE.md in the project root for license information. -// *--------------------------------------------------------------------------------------------*/ - -// import { IActionContext, TelemetryProperties } from '@microsoft/vscode-azext-utils'; -// import * as vscode from 'vscode'; -// import { ext } from '../../extensionVariables'; -// import { ImageTreeItem } from '../../tree/images/ImageTreeItem'; -// import { UnifiedRegistryItem } from '../../tree/registries/UnifiedRegistryTreeDataProvider'; - -// 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, { -// ...context, -// noItemFoundErrorMessage: vscode.l10n.t('No images are available to tag') -// }); -// } - -// addImageTaggingTelemetry(context, node.fullTag, '.before'); -// const newTaggedName: string = await getTagFromUserInput(context, node.fullTag, ''); // TODO: review this later -// addImageTaggingTelemetry(context, newTaggedName, '.after'); - -// await ext.runWithDefaults(client => -// client.tagImage({ fromImageRef: node.imageId, toImageRef: newTaggedName }) -// ); - -// return newTaggedName; -// } - -// export async function getTagFromUserInput(context: IActionContext, fullTag: string, baseImagePath?: string): Promise { -// const opt: vscode.InputBoxOptions = { -// ignoreFocusOut: true, -// prompt: vscode.l10n.t('Tag image as...'), -// }; - -// if (fullTag.includes('/')) { -// opt.valueSelection = [0, fullTag.lastIndexOf('/')]; -// } else if (baseImagePath) { -// fullTag = `${baseImagePath}/${fullTag}`; -// opt.valueSelection = [0, fullTag.lastIndexOf('/')]; -// } - -// opt.value = fullTag; - -// return await context.ui.showInputBox(opt); -// } - -// const KnownRegistries: { type: string, regex: RegExp }[] = [ -// // Like username/path -// { type: 'dockerhub-namespace', regex: /^[^.:]+\/[^.:]+$/ }, - -// { type: 'dockerhub-dockerio', regex: /^docker.io.*\// }, -// { type: 'github', regex: /ghcr\.io.*\// }, -// { type: 'gitlab', regex: /gitlab.*\// }, -// { type: 'ACR', regex: /azurecr\.io.*\// }, -// { type: 'GCR', regex: /gcr\.io.*\// }, -// { type: 'ECR', regex: /\.ecr\..*\// }, -// { type: 'localhost', regex: /localhost:.*\// }, - -// // Has a port, probably a private registry -// { type: 'privateWithPort', regex: /:[0-9]+\// }, - -// // Match anything remaining -// { type: 'other', regex: /\// }, // has a slash -// { type: 'none', regex: /./ } // no slash -// ]; - -// export function addImageTaggingTelemetry(context: IActionContext, fullImageName: string, propertyPostfix: '.before' | '.after' | ''): void { -// try { -// const properties: TelemetryProperties = {}; - -// const [, repository, tag] = /^(.*):(.*)$/.exec(fullImageName) ?? [undefined, fullImageName, '']; - -// if (!!tag.match(/^[0-9.-]*(|alpha|beta|latest|edge|v|version)?[0-9.-]*$/)) { -// properties.safeTag = tag; -// } -// properties.hasTag = String(!!tag); -// properties.numSlashes = String(numberMatches(repository.match(/\//g))); - -// const knownRegistry = KnownRegistries.find(kr => !!repository.match(kr.regex)); -// if (knownRegistry) { -// properties.registryType = knownRegistry.type; -// } - -// for (const propertyName of Object.keys(properties)) { -// context.telemetry.properties[propertyName + propertyPostfix] = properties[propertyName]; -// } -// } catch (error) { -// console.error(error); -// } -// } - -// function numberMatches(matches: RegExpMatchArray | null): number { -// return matches ? matches.length : 0; -// } +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IActionContext, TelemetryProperties } from '@microsoft/vscode-azext-utils'; +import * as vscode from 'vscode'; +import { ext } from '../../extensionVariables'; +import { ImageTreeItem } from '../../tree/images/ImageTreeItem'; +import { UnifiedRegistryItem } from '../../tree/registries/UnifiedRegistryTreeDataProvider'; + +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, { + ...context, + noItemFoundErrorMessage: vscode.l10n.t('No images are available to tag') + }); + } + + addImageTaggingTelemetry(context, node.fullTag, '.before'); + const newTaggedName: string = await getTagFromUserInput(context, node.fullTag, ''); // TODO: review this later + addImageTaggingTelemetry(context, newTaggedName, '.after'); + + await ext.runWithDefaults(client => + client.tagImage({ fromImageRef: node.imageId, toImageRef: newTaggedName }) + ); + + return newTaggedName; +} + +export async function getTagFromUserInput(context: IActionContext, fullTag: string, baseImagePath?: string): Promise { + const opt: vscode.InputBoxOptions = { + ignoreFocusOut: true, + prompt: vscode.l10n.t('Tag image as...'), + }; + + if (fullTag.includes('/')) { + opt.valueSelection = [0, fullTag.lastIndexOf('/')]; + } else if (baseImagePath) { + fullTag = `${baseImagePath}/${fullTag}`; + opt.valueSelection = [0, fullTag.lastIndexOf('/')]; + } + + opt.value = fullTag; + + return await context.ui.showInputBox(opt); +} + +const KnownRegistries: { type: string, regex: RegExp }[] = [ + // Like username/path + { type: 'dockerhub-namespace', regex: /^[^.:]+\/[^.:]+$/ }, + + { type: 'dockerhub-dockerio', regex: /^docker.io.*\// }, + { type: 'github', regex: /ghcr\.io.*\// }, + { type: 'gitlab', regex: /gitlab.*\// }, + { type: 'ACR', regex: /azurecr\.io.*\// }, + { type: 'GCR', regex: /gcr\.io.*\// }, + { type: 'ECR', regex: /\.ecr\..*\// }, + { type: 'localhost', regex: /localhost:.*\// }, + + // Has a port, probably a private registry + { type: 'privateWithPort', regex: /:[0-9]+\// }, + + // Match anything remaining + { type: 'other', regex: /\// }, // has a slash + { type: 'none', regex: /./ } // no slash +]; + +export function addImageTaggingTelemetry(context: IActionContext, fullImageName: string, propertyPostfix: '.before' | '.after' | ''): void { + try { + const properties: TelemetryProperties = {}; + + const [, repository, tag] = /^(.*):(.*)$/.exec(fullImageName) ?? [undefined, fullImageName, '']; + + if (!!tag.match(/^[0-9.-]*(|alpha|beta|latest|edge|v|version)?[0-9.-]*$/)) { + properties.safeTag = tag; + } + properties.hasTag = String(!!tag); + properties.numSlashes = String(numberMatches(repository.match(/\//g))); + + const knownRegistry = KnownRegistries.find(kr => !!repository.match(kr.regex)); + if (knownRegistry) { + properties.registryType = knownRegistry.type; + } + + for (const propertyName of Object.keys(properties)) { + context.telemetry.properties[propertyName + propertyPostfix] = properties[propertyName]; + } + } catch (error) { + console.error(error); + } +} + +function numberMatches(matches: RegExpMatchArray | null): number { + return matches ? matches.length : 0; +} diff --git a/src/commands/registerCommands.ts b/src/commands/registerCommands.ts index ee3350847a..25d607ce72 100644 --- a/src/commands/registerCommands.ts +++ b/src/commands/registerCommands.ts @@ -68,6 +68,7 @@ import { connectRegistry } from "./registries/connectRegistry"; // import { logOutOfDockerCli } from "./registries/logOutOfDockerCli"; // import { pullImageFromRepository, pullRepository } from "./registries/pullImages"; // import { reconnectRegistry } from "./registries/reconnectRegistry"; +import { buildImageInAzure } from "./registries/azure/tasks/buildImageInAzure"; import { copyRemoteFullTag } from "./registries/copyRemoteFullTag"; import { copyRemoteImageDigest } from "./registries/copyRemoteImageDigest"; import { disconnectRegistry } from "./registries/disconnectRegistry"; @@ -182,7 +183,7 @@ export function registerCommands(): void { // registerCommand('vscode-docker.registries.dockerHub.openInBrowser', openDockerHubInBrowser); - // registerWorkspaceCommand('vscode-docker.registries.azure.buildImage', buildImageInAzure); + registerWorkspaceCommand('vscode-docker.registries.azure.buildImage', buildImageInAzure); registerCommand('vscode-docker.registries.azure.createRegistry', createAzureRegistry); // registerCommand('vscode-docker.registries.azure.deleteRegistry', deleteAzureRegistry); // registerCommand('vscode-docker.registries.azure.deleteRepository', deleteAzureRepository); diff --git a/src/commands/registries/azure/tasks/buildImageInAzure.ts b/src/commands/registries/azure/tasks/buildImageInAzure.ts index f2d25ba469..7d1cca1471 100644 --- a/src/commands/registries/azure/tasks/buildImageInAzure.ts +++ b/src/commands/registries/azure/tasks/buildImageInAzure.ts @@ -1,36 +1,37 @@ -// /*--------------------------------------------------------------------------------------------- -// * 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 type { Run } from '@azure/arm-containerregistry'; -// import { IActionContext, UserCancelledError } from '@microsoft/vscode-azext-utils'; -// import * as vscode from 'vscode'; -// import { getArmContainerRegistry } from '../../../../utils/lazyPackages'; -// import { delay } from '../../../../utils/promiseUtils'; +import type { Run } from '@azure/arm-containerregistry'; +import { IActionContext, UserCancelledError } from '@microsoft/vscode-azext-utils'; +import * as vscode from 'vscode'; +import { getArmContainerRegistry } from '../../../../utils/lazyPackages'; +import { delay } from '../../../../utils/promiseUtils'; +import { RootStrategy, scheduleRunRequest } from './scheduleRunRequest'; -// const WAIT_MS = 5000; +const WAIT_MS = 5000; -// export async function buildImageInAzure(context: IActionContext, uri?: vscode.Uri | undefined, rootStrategy?: RootStrategy | undefined): Promise { -// if (!vscode.workspace.isTrusted) { -// throw new UserCancelledError('enforceTrust'); -// } +export async function buildImageInAzure(context: IActionContext, uri?: vscode.Uri | undefined, rootStrategy?: RootStrategy | undefined): Promise { + if (!vscode.workspace.isTrusted) { + throw new UserCancelledError('enforceTrust'); + } -// const getRun = await scheduleRunRequest(context, "DockerBuildRequest", uri, rootStrategy); + const getRun = await scheduleRunRequest(context, "DockerBuildRequest", uri, rootStrategy); -// let run = await getRun(); -// const { KnownRunStatus } = await getArmContainerRegistry(); -// while ( -// run.status === KnownRunStatus.Started || -// run.status === KnownRunStatus.Queued || -// run.status === KnownRunStatus.Running -// ) { -// await delay(WAIT_MS); -// run = await getRun(); -// } + let run = await getRun(); + const { KnownRunStatus } = await getArmContainerRegistry(); + while ( + run.status === KnownRunStatus.Started || + run.status === KnownRunStatus.Queued || + run.status === KnownRunStatus.Running + ) { + await delay(WAIT_MS); + run = await getRun(); + } -// // we are returning the run so that other extensions can consume this with the vscode.commands.executeCommand -// // currently it is used by the ms-kubernetes-tools.aks-devx-tools extension (https://github.com/Azure/aks-devx-tools) -// return run; -// } + // we are returning the run so that other extensions can consume this with the vscode.commands.executeCommand + // currently it is used by the ms-kubernetes-tools.aks-devx-tools extension (https://github.com/Azure/aks-devx-tools) + return run; +} diff --git a/src/commands/registries/azure/tasks/scheduleRunRequest.ts b/src/commands/registries/azure/tasks/scheduleRunRequest.ts index e47f9d3460..dd8723479d 100644 --- a/src/commands/registries/azure/tasks/scheduleRunRequest.ts +++ b/src/commands/registries/azure/tasks/scheduleRunRequest.ts @@ -1,216 +1,227 @@ -// /*--------------------------------------------------------------------------------------------- -// * Copyright (c) Microsoft Corporation. All rights reserved. -// * Licensed under the MIT License. See LICENSE.md in the project root for license information. -// *--------------------------------------------------------------------------------------------*/ - -// import type { DockerBuildRequest as AcrDockerBuildRequest, FileTaskRunRequest as AcrFileTaskRunRequest, OS as AcrOS, Run as AcrRun, ContainerRegistryManagementClient } from "@azure/arm-containerregistry"; // These are only dev-time imports so don't need to be lazy -// import { IActionContext, IAzureQuickPickItem, nonNullProp } from '@microsoft/vscode-azext-utils'; -// import * as fse from 'fs-extra'; -// import * as os from 'os'; -// import * as path from 'path'; -// import * as readline from 'readline'; -// import * as tar from 'tar'; -// import * as vscode from 'vscode'; -// import { ext } from '../../../../extensionVariables'; -// import { getStorageBlob } from '../../../../utils/lazyPackages'; -// import { delay } from '../../../../utils/promiseUtils'; -// import { Item, quickPickDockerFileItem, quickPickYamlFileItem } from '../../../../utils/quickPickFile'; -// import { quickPickWorkspaceFolder } from '../../../../utils/quickPickWorkspaceFolder'; -// import { addImageTaggingTelemetry, getTagFromUserInput } from '../../../images/tagImage'; - -// const idPrecision = 6; -// const vcsIgnoreList = ['.git', '.gitignore', '.bzr', 'bzrignore', '.hg', '.hgignore', '.svn']; - -// // this is used by the ms-kubernetes-tools.aks-devx-tools extension (https://github.com/Azure/aks-devx-tools) -// export enum RootStrategy { -// Default = 'Default', -// DockerfileFolder = 'DockerfileFolder', -// } - -// export async function scheduleRunRequest(context: IActionContext, requestType: 'DockerBuildRequest' | 'FileTaskRunRequest', uri: vscode.Uri | undefined, rootStrategy?: RootStrategy | undefined): Promise<() => Promise> { -// // Acquire information. -// let rootFolder: vscode.WorkspaceFolder; -// let fileItem: Item; -// let imageName: string; -// if (requestType === 'DockerBuildRequest') { -// rootFolder = await quickPickWorkspaceFolder(context, vscode.l10n.t('To quick build Docker files you must first open a folder or workspace in VS Code.')); -// fileItem = await quickPickDockerFileItem(context, uri, rootFolder); -// imageName = await quickPickImageName(context, rootFolder, fileItem); -// } else if (requestType === 'FileTaskRunRequest') { -// rootFolder = await quickPickWorkspaceFolder(context, vscode.l10n.t('To run a task from a .yaml file you must first open a folder or workspace in VS Code.')); -// fileItem = await quickPickYamlFileItem(context, uri, rootFolder, vscode.l10n.t('To run a task from a .yaml file you must have yaml file in your VS Code workspace.')); -// } else { -// throw new Error(vscode.l10n.t('Run Request Type Currently not supported.')); -// } - -// const node = await ext.registriesTree.showTreeItemPicker(registryExpectedContextValues.azure.registry, context); - -// const osPick = ['Linux', 'Windows'].map(item => >{ label: item, data: item }); -// const osType: AcrOS = (await context.ui.showQuickPick(osPick, { placeHolder: vscode.l10n.t('Select image base OS') })).data; - -// const tarFilePath: string = getTempSourceArchivePath(); - -// try { -// // Prepare to run. -// ext.outputChannel.show(); - -// let rootUri = rootFolder.uri; -// if (rootStrategy === RootStrategy.DockerfileFolder) { -// // changes the root to the folder where the Dockerfile is -// // it is used by the ms-kubernetes-tools.aks-devx-tools extension (https://github.com/Azure/aks-devx-tools) -// rootUri = vscode.Uri.file(path.dirname(fileItem.absoluteFilePath)); -// } - -// const uploadedSourceLocation: string = await uploadSourceCode(await node.getClient(context), node.registryName, node.resourceGroup, rootUri, tarFilePath); -// ext.outputChannel.info(vscode.l10n.t('Uploaded source code from {0}', tarFilePath)); - -// let runRequest: AcrDockerBuildRequest | AcrFileTaskRunRequest; -// if (requestType === 'DockerBuildRequest') { -// runRequest = { -// type: requestType, -// imageNames: [imageName], -// isPushEnabled: true, -// sourceLocation: uploadedSourceLocation, -// platform: { os: osType }, -// dockerFilePath: path.relative(rootUri.fsPath, fileItem.absoluteFilePath) -// }; -// } else { -// runRequest = { -// type: 'FileTaskRunRequest', -// taskFilePath: path.relative(rootUri.fsPath, fileItem.absoluteFilePath), -// sourceLocation: uploadedSourceLocation, -// platform: { os: osType } -// }; -// } - -// // Schedule the run and Clean up. -// ext.outputChannel.info(vscode.l10n.t('Set up run request')); - -// const client = await node.getClient(context); -// const run = await client.registries.beginScheduleRunAndWait(node.resourceGroup, node.registryName, runRequest); -// ext.outputChannel.info(vscode.l10n.t('Scheduled run {0}', run.runId)); - -// void streamLogs(context, node, run); - -// // function returns the AcrRun info -// return async () => client.runs.get(node.resourceGroup, node.registryName, run.runId); -// } finally { -// if (await fse.pathExists(tarFilePath)) { -// await fse.unlink(tarFilePath); -// } -// } -// } - -// async function quickPickImageName(context: IActionContext, rootFolder: vscode.WorkspaceFolder, dockerFileItem: Item | undefined): Promise { -// const absFilePath: string = path.join(rootFolder.uri.fsPath, dockerFileItem.relativeFilePath); -// const dockerFileKey = `ACR_buildTag_${absFilePath}`; -// const prevImageName: string | undefined = ext.context.workspaceState.get(dockerFileKey); -// let suggestedImageName: string; - -// if (!prevImageName) { -// // Get imageName based on name of subfolder containing the Dockerfile, or else workspacefolder -// suggestedImageName = path.basename(dockerFileItem.relativeFolderPath).toLowerCase(); -// if (suggestedImageName === '.') { -// suggestedImageName = path.basename(rootFolder.uri.fsPath).toLowerCase().replace(/\s/g, ''); -// } - -// suggestedImageName += ":{{.Run.ID}}"; -// } else { -// suggestedImageName = prevImageName; -// } - -// // Temporary work-around for vscode bug where valueSelection can be messed up if a quick pick is followed by a showInputBox -// await delay(500); - -// addImageTaggingTelemetry(context, suggestedImageName, '.before'); -// const imageName: string = await getTagFromUserInput(context, suggestedImageName); -// addImageTaggingTelemetry(context, imageName, '.after'); - -// await ext.context.workspaceState.update(dockerFileKey, imageName); -// return imageName; -// } - -// async function uploadSourceCode(client: ContainerRegistryManagementClient, registryName: string, resourceGroupName: string, rootFolder: vscode.Uri, tarFilePath: string): Promise { -// ext.outputChannel.info(vscode.l10n.t(' Sending source code to temp file')); -// const source: string = rootFolder.fsPath; -// let items = await fse.readdir(source); -// items = items.filter(i => !(i in vcsIgnoreList)); -// // tslint:disable-next-line:no-unsafe-any -// tar.c({ cwd: source }, items).pipe(fse.createWriteStream(tarFilePath)); - -// ext.outputChannel.info(vscode.l10n.t(' Getting build source upload URL')); -// const sourceUploadLocation = await client.registries.getBuildSourceUploadUrl(resourceGroupName, registryName); -// const uploadUrl: string = sourceUploadLocation.uploadUrl; -// const relativePath: string = sourceUploadLocation.relativePath; - -// const storageBlob = await getStorageBlob(); -// const blobClient = new storageBlob.BlockBlobClient(uploadUrl); -// ext.outputChannel.info(vscode.l10n.t(' Creating block blob')); -// await blobClient.uploadFile(tarFilePath); - -// return relativePath; -// } - -// const blobCheckInterval = 1000; -// const maxBlobChecks = 30; -// async function streamLogs(context: IActionContext, node: AzureRegistryTreeItem, run: AcrRun): Promise { -// const result = await (await node.getClient(context)).runs.getLogSasUrl(node.resourceGroup, node.registryName, run.runId); - -// const storageBlob = await getStorageBlob(); -// const blobClient = new storageBlob.BlobClient(nonNullProp(result, 'logLink')); - -// // Start streaming the response to the output channel -// let byteOffset = 0; -// let totalChecks = 0; -// let exists = false; - -// await new Promise((resolve, reject) => { -// const timer = setInterval( -// async () => { -// try { -// if (!exists && !(exists = await blobClient.exists())) { -// totalChecks++; -// if (totalChecks >= maxBlobChecks) { -// clearInterval(timer); -// reject('Not found'); -// } -// } - -// const properties = await blobClient.getProperties(); -// if (properties.contentLength > byteOffset) { -// // New data available -// const response = await blobClient.download(byteOffset); -// byteOffset += response.contentLength; - -// const lineReader = readline.createInterface(response.readableStreamBody); -// for await (const line of lineReader) { -// const sanitizedLine = line -// // eslint-disable-next-line no-control-regex -// .replace(/[\x00-\x09\x0B-\x0C\x0E-\x1F]/g, ''); // Remove non-printing control characters -// ext.outputChannel.info(sanitizedLine); -// } -// } - -// if (properties.metadata?.complete) { -// clearInterval(timer); -// resolve(); -// } -// } catch (err) { -// clearInterval(timer); -// reject(err); -// } -// }, -// blobCheckInterval -// ); -// }); -// } - -// function getTempSourceArchivePath(): string { -// /* tslint:disable-next-line:insecure-random */ -// const id: number = Math.floor(Math.random() * Math.pow(10, idPrecision)); -// const archive = `sourceArchive${id}.tar.gz`; -// ext.outputChannel.info(vscode.l10n.t('Setting up temp file with \'{0}\'', archive)); -// const tarFilePath: string = path.join(os.tmpdir(), archive); -// return tarFilePath; -// } -// TODO: review this later +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type { DockerBuildRequest as AcrDockerBuildRequest, FileTaskRunRequest as AcrFileTaskRunRequest, OS as AcrOS, Run as AcrRun, ContainerRegistryManagementClient } from "@azure/arm-containerregistry"; // These are only dev-time imports so don't need to be lazy +import { AzureSubscription } from "@microsoft/vscode-azext-azureauth"; +import { getResourceGroupFromId } from "@microsoft/vscode-azext-azureutils"; +import { IActionContext, IAzureQuickPickItem, contextValueExperience, nonNullProp } from '@microsoft/vscode-azext-utils'; +import * as fse from 'fs-extra'; +import * as os from 'os'; +import * as path from 'path'; +import * as readline from 'readline'; +import * as tar from 'tar'; +import * as vscode from 'vscode'; +import { ext } from '../../../../extensionVariables'; +import { AzureRegistryItem } from "../../../../tree/registries/Azure/AzureRegistryDataProvider"; +import { UnifiedRegistryItem } from "../../../../tree/registries/UnifiedRegistryTreeDataProvider"; +import { getStorageBlob } from '../../../../utils/lazyPackages'; +import { delay } from '../../../../utils/promiseUtils'; +import { Item, quickPickDockerFileItem, quickPickYamlFileItem } from '../../../../utils/quickPickFile'; +import { quickPickWorkspaceFolder } from '../../../../utils/quickPickWorkspaceFolder'; +import { addImageTaggingTelemetry, getTagFromUserInput } from '../../../images/tagImage'; + +const idPrecision = 6; +const vcsIgnoreList = ['.git', '.gitignore', '.bzr', 'bzrignore', '.hg', '.hgignore', '.svn']; + +// this is used by the ms-kubernetes-tools.aks-devx-tools extension (https://github.com/Azure/aks-devx-tools) +export enum RootStrategy { + Default = 'Default', + DockerfileFolder = 'DockerfileFolder', +} + +export async function scheduleRunRequest(context: IActionContext, requestType: 'DockerBuildRequest' | 'FileTaskRunRequest', uri: vscode.Uri | undefined, rootStrategy?: RootStrategy | undefined): Promise<() => Promise> { + // Acquire information. + let rootFolder: vscode.WorkspaceFolder; + let fileItem: Item; + let imageName: string; + if (requestType === 'DockerBuildRequest') { + rootFolder = await quickPickWorkspaceFolder(context, vscode.l10n.t('To quick build Docker files you must first open a folder or workspace in VS Code.')); + fileItem = await quickPickDockerFileItem(context, uri, rootFolder); + imageName = await quickPickImageName(context, rootFolder, fileItem); + } else if (requestType === 'FileTaskRunRequest') { + rootFolder = await quickPickWorkspaceFolder(context, vscode.l10n.t('To run a task from a .yaml file you must first open a folder or workspace in VS Code.')); + fileItem = await quickPickYamlFileItem(context, uri, rootFolder, vscode.l10n.t('To run a task from a .yaml file you must have yaml file in your VS Code workspace.')); + } else { + throw new Error(vscode.l10n.t('Run Request Type Currently not supported.')); + } + + const node: UnifiedRegistryItem = await contextValueExperience(context, ext.registriesTree, { include: 'azureContainerRegistry' }); + const registry: AzureRegistryItem = node.wrappedItem; + const resourceGroup = getResourceGroupFromId(registry.id); + + const osPick = ['Linux', 'Windows'].map(item => >{ label: item, data: item }); + const osType: AcrOS = (await context.ui.showQuickPick(osPick, { placeHolder: vscode.l10n.t('Select image base OS') })).data; + + const tarFilePath: string = getTempSourceArchivePath(); + + try { + // Prepare to run. + ext.outputChannel.show(); + + let rootUri = rootFolder.uri; + if (rootStrategy === RootStrategy.DockerfileFolder) { + // changes the root to the folder where the Dockerfile is + // it is used by the ms-kubernetes-tools.aks-devx-tools extension (https://github.com/Azure/aks-devx-tools) + rootUri = vscode.Uri.file(path.dirname(fileItem.absoluteFilePath)); + } + + const azureRegistryClient = await createAzureClient(registry.subscription); + const uploadedSourceLocation: string = await uploadSourceCode(azureRegistryClient, registry.subscription.subscriptionId, resourceGroup, rootUri, tarFilePath); + ext.outputChannel.info(vscode.l10n.t('Uploaded source code from {0}', tarFilePath)); + + let runRequest: AcrDockerBuildRequest | AcrFileTaskRunRequest; + if (requestType === 'DockerBuildRequest') { + runRequest = { + type: requestType, + imageNames: [imageName], + isPushEnabled: true, + sourceLocation: uploadedSourceLocation, + platform: { os: osType }, + dockerFilePath: path.relative(rootUri.fsPath, fileItem.absoluteFilePath) + }; + } else { + runRequest = { + type: 'FileTaskRunRequest', + taskFilePath: path.relative(rootUri.fsPath, fileItem.absoluteFilePath), + sourceLocation: uploadedSourceLocation, + platform: { os: osType } + }; + } + + // Schedule the run and Clean up. + ext.outputChannel.info(vscode.l10n.t('Set up run request')); + + const run = await azureRegistryClient.registries.beginScheduleRunAndWait(resourceGroup, registry.label, runRequest); + ext.outputChannel.info(vscode.l10n.t('Scheduled run {0}', run.runId)); + + void streamLogs(context, node, run); + + // function returns the AcrRun info + return async () => azureRegistryClient.runs.get(resourceGroup, registry.label, run.runId); + } finally { + if (await fse.pathExists(tarFilePath)) { + await fse.unlink(tarFilePath); + } + } +} + +async function quickPickImageName(context: IActionContext, rootFolder: vscode.WorkspaceFolder, dockerFileItem: Item | undefined): Promise { + const absFilePath: string = path.join(rootFolder.uri.fsPath, dockerFileItem.relativeFilePath); + const dockerFileKey = `ACR_buildTag_${absFilePath}`; + const prevImageName: string | undefined = ext.context.workspaceState.get(dockerFileKey); + let suggestedImageName: string; + + if (!prevImageName) { + // Get imageName based on name of subfolder containing the Dockerfile, or else workspacefolder + suggestedImageName = path.basename(dockerFileItem.relativeFolderPath).toLowerCase(); + if (suggestedImageName === '.') { + suggestedImageName = path.basename(rootFolder.uri.fsPath).toLowerCase().replace(/\s/g, ''); + } + + suggestedImageName += ":{{.Run.ID}}"; + } else { + suggestedImageName = prevImageName; + } + + // Temporary work-around for vscode bug where valueSelection can be messed up if a quick pick is followed by a showInputBox + await delay(500); + + addImageTaggingTelemetry(context, suggestedImageName, '.before'); + const imageName: string = await getTagFromUserInput(context, suggestedImageName); + addImageTaggingTelemetry(context, imageName, '.after'); + + await ext.context.workspaceState.update(dockerFileKey, imageName); + return imageName; +} + +async function uploadSourceCode(client: ContainerRegistryManagementClient, registryName: string, resourceGroupName: string, rootFolder: vscode.Uri, tarFilePath: string): Promise { + ext.outputChannel.info(vscode.l10n.t(' Sending source code to temp file')); + const source: string = rootFolder.fsPath; + let items = await fse.readdir(source); + items = items.filter(i => !(i in vcsIgnoreList)); + // tslint:disable-next-line:no-unsafe-any + tar.c({ cwd: source }, items).pipe(fse.createWriteStream(tarFilePath)); + + ext.outputChannel.info(vscode.l10n.t(' Getting build source upload URL')); + const sourceUploadLocation = await client.registries.getBuildSourceUploadUrl(resourceGroupName, registryName); + const uploadUrl: string = sourceUploadLocation.uploadUrl; + const relativePath: string = sourceUploadLocation.relativePath; + + const storageBlob = await getStorageBlob(); + const blobClient = new storageBlob.BlockBlobClient(uploadUrl); + ext.outputChannel.info(vscode.l10n.t(' Creating block blob')); + await blobClient.uploadFile(tarFilePath); + + return relativePath; +} + +const blobCheckInterval = 1000; +const maxBlobChecks = 30; +async function streamLogs(context: IActionContext, node: UnifiedRegistryItem, run: AcrRun): Promise { + const azureRegistryClient = await createAzureClient(node.wrappedItem.subscription); + const resourceGroup = getResourceGroupFromId(node.wrappedItem.id); + const result = await azureRegistryClient.runs.getLogSasUrl(resourceGroup, node.wrappedItem.label, run.runId); + + const storageBlob = await getStorageBlob(); + const blobClient = new storageBlob.BlobClient(nonNullProp(result, 'logLink')); + + // Start streaming the response to the output channel + let byteOffset = 0; + let totalChecks = 0; + let exists = false; + + await new Promise((resolve, reject) => { + const timer = setInterval( + async () => { + try { + if (!exists && !(exists = await blobClient.exists())) { + totalChecks++; + if (totalChecks >= maxBlobChecks) { + clearInterval(timer); + reject('Not found'); + } + } + + const properties = await blobClient.getProperties(); + if (properties.contentLength > byteOffset) { + // New data available + const response = await blobClient.download(byteOffset); + byteOffset += response.contentLength; + + const lineReader = readline.createInterface(response.readableStreamBody); + for await (const line of lineReader) { + const sanitizedLine = line + // eslint-disable-next-line no-control-regex + .replace(/[\x00-\x09\x0B-\x0C\x0E-\x1F]/g, ''); // Remove non-printing control characters + ext.outputChannel.info(sanitizedLine); + } + } + + if (properties.metadata?.complete) { + clearInterval(timer); + resolve(); + } + } catch (err) { + clearInterval(timer); + reject(err); + } + }, + blobCheckInterval + ); + }); +} + +function getTempSourceArchivePath(): string { + /* tslint:disable-next-line:insecure-random */ + const id: number = Math.floor(Math.random() * Math.pow(10, idPrecision)); + const archive = `sourceArchive${id}.tar.gz`; + ext.outputChannel.info(vscode.l10n.t('Setting up temp file with \'{0}\'', archive)); + const tarFilePath: string = path.join(os.tmpdir(), archive); + return tarFilePath; +} + +async function createAzureClient(subscriptionItem: AzureSubscription): Promise { + return new (await import('@azure/arm-containerregistry')).ContainerRegistryManagementClient(subscriptionItem.credential, subscriptionItem.subscriptionId); +} diff --git a/src/tree/registries/Azure/AzureRegistryDataProvider.ts b/src/tree/registries/Azure/AzureRegistryDataProvider.ts index 3ec224bc34..df21ad7ff0 100644 --- a/src/tree/registries/Azure/AzureRegistryDataProvider.ts +++ b/src/tree/registries/Azure/AzureRegistryDataProvider.ts @@ -10,11 +10,12 @@ import { CommonRegistryItem, isRegistryRoot } from '@microsoft/vscode-docker-reg import * as vscode from 'vscode'; import { ACROAuthProvider } from './ACROAuthProvider'; -interface AzureRegistryItem extends V2RegistryItem { +export interface AzureRegistryItem extends V2RegistryItem { readonly subscription: AzureSubscription; + readonly id: string; } -interface AzureSubscriptionRegistryItem extends CommonRegistryItem { +export interface AzureSubscriptionRegistryItem extends CommonRegistryItem { readonly subscription: AzureSubscription; readonly type: 'azuresubscription'; } @@ -93,9 +94,13 @@ export class AzureRegistryDataProvider extends RegistryV2DataProvider implements parent: subscriptionItem, type: 'commonregistry', baseUrl: vscode.Uri.parse(`https://${registry.loginServer}`), + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion label: registry.name!, iconPath: vscode.Uri.joinPath(this.extensionContext.extensionUri, 'resources', 'azureRegistry.svg'), subscription: subscriptionItem.subscription, + additionalContextValues: ['azureContainerRegistry'], + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + id: registry.id! }; }); } @@ -121,6 +126,7 @@ export class AzureRegistryDataProvider extends RegistryV2DataProvider implements this.authenticationProviders.set(registryString, provider); } + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return this.authenticationProviders.get(registryString)!; } } From ea30d6d037bcea75983d6c6645463aa1fc21d70f Mon Sep 17 00:00:00 2001 From: alexyaang <59073590+alexyaang@users.noreply.github.com> Date: Mon, 7 Aug 2023 17:01:54 -0400 Subject: [PATCH 2/6] delete azure repository command implementation --- package.json | 2 +- src/commands/registerCommands.ts | 3 +- .../registries/azure/deleteAzureRepository.ts | 53 ++++++++++--------- .../azure/tasks/scheduleRunRequest.ts | 6 +-- .../Azure/AzureRegistryDataProvider.ts | 46 +++++++++++++++- .../getInformationFromRegistryItem.ts | 7 +++ 6 files changed, 83 insertions(+), 34 deletions(-) diff --git a/package.json b/package.json index 4490647747..62b64a5192 100644 --- a/package.json +++ b/package.json @@ -496,7 +496,7 @@ }, { "command": "vscode-docker.registries.azure.deleteRepository", - "when": "view == dockerRegistries && viewItem == azure;DockerV2;Repository;", + "when": "view == dockerRegistries && viewItem =~ /azureContainerRepository/i", "group": "regs_repo_2_destructive@1" }, { diff --git a/src/commands/registerCommands.ts b/src/commands/registerCommands.ts index 25d607ce72..6c34cbd481 100644 --- a/src/commands/registerCommands.ts +++ b/src/commands/registerCommands.ts @@ -68,6 +68,7 @@ import { connectRegistry } from "./registries/connectRegistry"; // import { logOutOfDockerCli } from "./registries/logOutOfDockerCli"; // import { pullImageFromRepository, pullRepository } from "./registries/pullImages"; // import { reconnectRegistry } from "./registries/reconnectRegistry"; +import { deleteAzureRepository } from "./registries/azure/deleteAzureRepository"; import { buildImageInAzure } from "./registries/azure/tasks/buildImageInAzure"; import { copyRemoteFullTag } from "./registries/copyRemoteFullTag"; import { copyRemoteImageDigest } from "./registries/copyRemoteImageDigest"; @@ -186,7 +187,7 @@ export function registerCommands(): void { registerWorkspaceCommand('vscode-docker.registries.azure.buildImage', buildImageInAzure); registerCommand('vscode-docker.registries.azure.createRegistry', createAzureRegistry); // registerCommand('vscode-docker.registries.azure.deleteRegistry', deleteAzureRegistry); - // registerCommand('vscode-docker.registries.azure.deleteRepository', deleteAzureRepository); + registerCommand('vscode-docker.registries.azure.deleteRepository', deleteAzureRepository); registerCommand('vscode-docker.registries.azure.openInPortal', openInAzurePortal); registerCommand('vscode-docker.registries.azure.runTask', runAzureTask); // registerWorkspaceCommand('vscode-docker.registries.azure.runFileAsTask', runFileAsAzureTask); diff --git a/src/commands/registries/azure/deleteAzureRepository.ts b/src/commands/registries/azure/deleteAzureRepository.ts index 57e8e4de49..542f5610bb 100644 --- a/src/commands/registries/azure/deleteAzureRepository.ts +++ b/src/commands/registries/azure/deleteAzureRepository.ts @@ -1,30 +1,33 @@ -// /*--------------------------------------------------------------------------------------------- -// * 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 type { AzureRepositoryTreeItem } from '../../../tree/registries/azure/AzureRepositoryTreeItem'; -// import { registryExpectedContextValues } from '../../../tree/registries/registryContextValues'; +import { DialogResponses, IActionContext, contextValueExperience } from '@microsoft/vscode-azext-utils'; +import { ProgressLocation, l10n, window } from 'vscode'; +import { ext } from '../../../extensionVariables'; +import { AzureRegistryDataProvider, AzureRepository } from '../../../tree/registries/Azure/AzureRegistryDataProvider'; +import { UnifiedRegistryItem } from '../../../tree/registries/UnifiedRegistryTreeDataProvider'; -// export async function deleteAzureRepository(context: IActionContext, node?: AzureRepositoryTreeItem): Promise { -// if (!node) { -// node = await ext.registriesTree.showTreeItemPicker(registryExpectedContextValues.azure.repository, { ...context, suppressCreatePick: true }); -// } +export async function deleteAzureRepository(context: IActionContext, node?: UnifiedRegistryItem): Promise { + if (!node) { + node = await contextValueExperience(context, ext.registriesTree, { include: 'azureContainerRepository' }); + } -// const confirmDelete = l10n.t('Are you sure you want to delete repository "{0}" and its associated images?', node.repoName); -// // no need to check result - cancel will throw a UserCancelledError -// await context.ui.showWarningMessage(confirmDelete, { modal: true }, DialogResponses.deleteResponse); + const confirmDelete = l10n.t('Are you sure you want to delete repository "{0}" and its associated images?', node.wrappedItem.label); + // no need to check result - cancel will throw a UserCancelledError + await context.ui.showWarningMessage(confirmDelete, { modal: true }, DialogResponses.deleteResponse); -// const deleting = l10n.t('Deleting repository "{0}"...', node.repoName); -// await window.withProgress({ location: ProgressLocation.Notification, title: deleting }, async () => { -// await node.deleteTreeItem(context); -// }); + const deleting = l10n.t('Deleting repository "{0}"...', node.wrappedItem.label); + await window.withProgress({ location: ProgressLocation.Notification, title: deleting }, async () => { + const azureDataProvider = node.provider as unknown as AzureRegistryDataProvider; + await azureDataProvider.deleteRepository(node.wrappedItem); + }); -// const deleteSucceeded = l10n.t('Successfully deleted repository "{0}".', node.repoName); -// // don't wait -// /* eslint-disable-next-line @typescript-eslint/no-floating-promises */ -// window.showInformationMessage(deleteSucceeded); -// } + ext.registriesTree.refresh(); + + const deleteSucceeded = l10n.t('Successfully deleted repository "{0}".', node.wrappedItem.label); + // don't wait + /* eslint-disable-next-line @typescript-eslint/no-floating-promises */ + window.showInformationMessage(deleteSucceeded); +} diff --git a/src/commands/registries/azure/tasks/scheduleRunRequest.ts b/src/commands/registries/azure/tasks/scheduleRunRequest.ts index dd8723479d..09af3b7850 100644 --- a/src/commands/registries/azure/tasks/scheduleRunRequest.ts +++ b/src/commands/registries/azure/tasks/scheduleRunRequest.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import type { DockerBuildRequest as AcrDockerBuildRequest, FileTaskRunRequest as AcrFileTaskRunRequest, OS as AcrOS, Run as AcrRun, ContainerRegistryManagementClient } from "@azure/arm-containerregistry"; // These are only dev-time imports so don't need to be lazy -import { AzureSubscription } from "@microsoft/vscode-azext-azureauth"; import { getResourceGroupFromId } from "@microsoft/vscode-azext-azureutils"; import { IActionContext, IAzureQuickPickItem, contextValueExperience, nonNullProp } from '@microsoft/vscode-azext-utils'; import * as fse from 'fs-extra'; @@ -16,6 +15,7 @@ import * as vscode from 'vscode'; import { ext } from '../../../../extensionVariables'; import { AzureRegistryItem } from "../../../../tree/registries/Azure/AzureRegistryDataProvider"; import { UnifiedRegistryItem } from "../../../../tree/registries/UnifiedRegistryTreeDataProvider"; +import { createAzureClient } from "../../../../tree/registries/getInformationFromRegistryItem"; import { getStorageBlob } from '../../../../utils/lazyPackages'; import { delay } from '../../../../utils/promiseUtils'; import { Item, quickPickDockerFileItem, quickPickYamlFileItem } from '../../../../utils/quickPickFile'; @@ -221,7 +221,3 @@ function getTempSourceArchivePath(): string { const tarFilePath: string = path.join(os.tmpdir(), archive); return tarFilePath; } - -async function createAzureClient(subscriptionItem: AzureSubscription): Promise { - return new (await import('@azure/arm-containerregistry')).ContainerRegistryManagementClient(subscriptionItem.credential, subscriptionItem.subscriptionId); -} diff --git a/src/tree/registries/Azure/AzureRegistryDataProvider.ts b/src/tree/registries/Azure/AzureRegistryDataProvider.ts index df21ad7ff0..1e11512939 100644 --- a/src/tree/registries/Azure/AzureRegistryDataProvider.ts +++ b/src/tree/registries/Azure/AzureRegistryDataProvider.ts @@ -5,11 +5,13 @@ import type { Registry as AcrRegistry } from '@azure/arm-containerregistry'; import { AzureSubscription, VSCodeAzureSubscriptionProvider } from '@microsoft/vscode-azext-azureauth'; -import { RegistryV2DataProvider, V2Registry, V2RegistryItem } from '@microsoft/vscode-docker-registries'; +import { RegistryV2DataProvider, V2Registry, V2RegistryItem, V2Repository, registryV2Request } from '@microsoft/vscode-docker-registries'; import { CommonRegistryItem, isRegistryRoot } from '@microsoft/vscode-docker-registries/lib/clients/Common/models'; import * as vscode from 'vscode'; import { ACROAuthProvider } from './ACROAuthProvider'; +export type AzureRepository = V2Repository; + export interface AzureRegistryItem extends V2RegistryItem { readonly subscription: AzureSubscription; readonly id: string; @@ -78,7 +80,7 @@ export class AzureRegistryDataProvider extends RegistryV2DataProvider implements this.subscriptionProvider.dispose(); } - public async getRegistries(subscriptionItem: CommonRegistryItem): Promise { + public override async getRegistries(subscriptionItem: CommonRegistryItem): Promise { subscriptionItem = subscriptionItem as AzureSubscriptionRegistryItem; // TODO: replace this with `createAzureClient` const acrClient = new (await import('@azure/arm-containerregistry')).ContainerRegistryManagementClient(subscriptionItem.subscription.credential, subscriptionItem.subscription.subscriptionId); @@ -105,6 +107,30 @@ export class AzureRegistryDataProvider extends RegistryV2DataProvider implements }); } + public override async getRepositories(registry: AzureRegistry): Promise { + const catalogResponse = await registryV2Request<{ repositories: string[] }>({ + authenticationProvider: this.getAuthenticationProvider(registry), + method: 'GET', + registryUri: registry.baseUrl, + path: ['v2', '_catalog'], + scopes: ['registry:catalog:*'], + }); + + const results: AzureRepository[] = []; + + for (const repository of catalogResponse.body?.repositories || []) { + results.push({ + parent: registry, + baseUrl: registry.baseUrl, + label: repository, + type: 'commonrepository', + additionalContextValues: ['azureContainerRepository'] + }); + } + + return results; + } + public override getTreeItem(element: CommonRegistryItem): Promise { if (isAzureSubscriptionRegistryItem(element)) { return Promise.resolve({ @@ -118,6 +144,22 @@ export class AzureRegistryDataProvider extends RegistryV2DataProvider implements } } + public async deleteRepository(item: AzureRepository): Promise { + + const authenticationProvider = this.getAuthenticationProvider(item.parent as unknown as AzureRegistryItem); + const reponse = await registryV2Request({ + method: 'DELETE', + registryUri: item.baseUrl, + path: ['v2', '_acr', `${item.label}`, 'repository'], + scopes: ['registry:catalog:*'], + authenticationProvider: authenticationProvider, + }); + } + + public async deleteRegistry(item: AzureRegistry): Promise { + throw new Error('Method not implemented.'); + } + protected override getAuthenticationProvider(item: AzureRegistryItem): ACROAuthProvider { const registryString = item.baseUrl.toString(); diff --git a/src/tree/registries/getInformationFromRegistryItem.ts b/src/tree/registries/getInformationFromRegistryItem.ts index eccab72abf..adb7bf44e2 100644 --- a/src/tree/registries/getInformationFromRegistryItem.ts +++ b/src/tree/registries/getInformationFromRegistryItem.ts @@ -3,6 +3,8 @@ * Licensed under the MIT License. See LICENSE.md in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { ContainerRegistryManagementClient } from "@azure/arm-containerregistry"; +import { AzureSubscription } from "@microsoft/vscode-azext-azureauth"; import { CommonRegistry, CommonRepository, CommonTag, isRegistry, isRepository, isTag } from "@microsoft/vscode-docker-registries"; import { UnifiedRegistryItem } from "./UnifiedRegistryTreeDataProvider"; @@ -34,3 +36,8 @@ export function getFullImageNameFromRegistryItem(node: UnifiedRegistryItem { + return new (await import('@azure/arm-containerregistry')).ContainerRegistryManagementClient(subscriptionItem.credential, subscriptionItem.subscriptionId); +} From d9402ce2bb98b997afcd23112ec235bb7122ef8e Mon Sep 17 00:00:00 2001 From: alexyaang <59073590+alexyaang@users.noreply.github.com> Date: Tue, 8 Aug 2023 09:12:27 -0400 Subject: [PATCH 3/6] actually added back delete azure repository --- src/tree/registries/Azure/AzureRegistryDataProvider.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/tree/registries/Azure/AzureRegistryDataProvider.ts b/src/tree/registries/Azure/AzureRegistryDataProvider.ts index 1e11512939..cdf98fe2e9 100644 --- a/src/tree/registries/Azure/AzureRegistryDataProvider.ts +++ b/src/tree/registries/Azure/AzureRegistryDataProvider.ts @@ -145,15 +145,19 @@ export class AzureRegistryDataProvider extends RegistryV2DataProvider implements } public async deleteRepository(item: AzureRepository): Promise { - const authenticationProvider = this.getAuthenticationProvider(item.parent as unknown as AzureRegistryItem); + const reponse = await registryV2Request({ method: 'DELETE', registryUri: item.baseUrl, path: ['v2', '_acr', `${item.label}`, 'repository'], - scopes: ['registry:catalog:*'], + scopes: [`repository:${item.label}:delete`], authenticationProvider: authenticationProvider, }); + + if (!reponse.succeeded) { + throw new Error(`Failed to delete repository: ${reponse.statusText}`); + } } public async deleteRegistry(item: AzureRegistry): Promise { From 08c05fc83a12c149412e93294bcb46a99d8a54c9 Mon Sep 17 00:00:00 2001 From: alexyaang <59073590+alexyaang@users.noreply.github.com> Date: Tue, 8 Aug 2023 14:19:35 -0400 Subject: [PATCH 4/6] added back delete Azure Registry command --- package.json | 2 +- src/commands/registerCommands.ts | 3 +- .../registries/azure/deleteAzureRegistry.ts | 55 ++++++++++--------- .../Azure/AzureRegistryDataProvider.ts | 8 ++- 4 files changed, 39 insertions(+), 29 deletions(-) diff --git a/package.json b/package.json index 62b64a5192..397272e299 100644 --- a/package.json +++ b/package.json @@ -486,7 +486,7 @@ }, { "command": "vscode-docker.registries.azure.deleteRegistry", - "when": "view == dockerRegistries && viewItem == azure;DockerV2;Registry;", + "when": "view == dockerRegistries && viewItem =~ /azureContainerRegistry/i", "group": "regs_reg_2_destructive@1" }, { diff --git a/src/commands/registerCommands.ts b/src/commands/registerCommands.ts index 6c34cbd481..15671deed9 100644 --- a/src/commands/registerCommands.ts +++ b/src/commands/registerCommands.ts @@ -68,6 +68,7 @@ import { connectRegistry } from "./registries/connectRegistry"; // import { logOutOfDockerCli } from "./registries/logOutOfDockerCli"; // import { pullImageFromRepository, pullRepository } from "./registries/pullImages"; // import { reconnectRegistry } from "./registries/reconnectRegistry"; +import { deleteAzureRegistry } from "./registries/azure/deleteAzureRegistry"; import { deleteAzureRepository } from "./registries/azure/deleteAzureRepository"; import { buildImageInAzure } from "./registries/azure/tasks/buildImageInAzure"; import { copyRemoteFullTag } from "./registries/copyRemoteFullTag"; @@ -186,7 +187,7 @@ export function registerCommands(): void { registerWorkspaceCommand('vscode-docker.registries.azure.buildImage', buildImageInAzure); registerCommand('vscode-docker.registries.azure.createRegistry', createAzureRegistry); - // registerCommand('vscode-docker.registries.azure.deleteRegistry', deleteAzureRegistry); + registerCommand('vscode-docker.registries.azure.deleteRegistry', deleteAzureRegistry); registerCommand('vscode-docker.registries.azure.deleteRepository', deleteAzureRepository); registerCommand('vscode-docker.registries.azure.openInPortal', openInAzurePortal); registerCommand('vscode-docker.registries.azure.runTask', runAzureTask); diff --git a/src/commands/registries/azure/deleteAzureRegistry.ts b/src/commands/registries/azure/deleteAzureRegistry.ts index bfac41cc95..febc0e4625 100644 --- a/src/commands/registries/azure/deleteAzureRegistry.ts +++ b/src/commands/registries/azure/deleteAzureRegistry.ts @@ -1,30 +1,35 @@ -// /*--------------------------------------------------------------------------------------------- -// * 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 type { AzureRegistryTreeItem } from '../../../tree/registries/azure/AzureRegistryTreeItem'; -// import { registryExpectedContextValues } from '../../../tree/registries/registryContextValues'; +import { DialogResponses, IActionContext, contextValueExperience } from '@microsoft/vscode-azext-utils'; +import { ProgressLocation, l10n, window } from 'vscode'; +import { ext } from '../../../extensionVariables'; +import { AzureRegistry, AzureRegistryDataProvider } from '../../../tree/registries/Azure/AzureRegistryDataProvider'; +import { UnifiedRegistryItem } from '../../../tree/registries/UnifiedRegistryTreeDataProvider'; -// export async function deleteAzureRegistry(context: IActionContext, node?: AzureRegistryTreeItem): Promise { -// if (!node) { -// node = await ext.registriesTree.showTreeItemPicker(registryExpectedContextValues.azure.registry, { ...context, suppressCreatePick: true }); -// } +export async function deleteAzureRegistry(context: IActionContext, node?: UnifiedRegistryItem): Promise { + if (!node) { + node = await contextValueExperience(context, ext.registriesTree, { include: 'azureContainerRegistry' }); + } -// const confirmDelete: string = l10n.t('Are you sure you want to delete registry "{0}" and its associated images?', node.registryName); -// // no need to check result - cancel will throw a UserCancelledError -// await context.ui.showWarningMessage(confirmDelete, { modal: true }, DialogResponses.deleteResponse); + const registryName = node.wrappedItem.label; -// const deleting = l10n.t('Deleting registry "{0}"...', node.registryName); -// await window.withProgress({ location: ProgressLocation.Notification, title: deleting }, async () => { -// await node.deleteTreeItem(context); -// }); + const confirmDelete: string = l10n.t('Are you sure you want to delete registry "{0}" and its associated images?', registryName); + // no need to check result - cancel will throw a UserCancelledError + await context.ui.showWarningMessage(confirmDelete, { modal: true }, DialogResponses.deleteResponse); -// const message = l10n.t('Successfully deleted registry "{0}".', node.registryName); -// // don't wait -// /* eslint-disable-next-line @typescript-eslint/no-floating-promises */ -// window.showInformationMessage(message); -// } + const deleting = l10n.t('Deleting registry "{0}"...', registryName); + await window.withProgress({ location: ProgressLocation.Notification, title: deleting }, async () => { + const azureRegistryDataProvider = node.provider as unknown as AzureRegistryDataProvider; + await azureRegistryDataProvider.deleteRegistry(node.wrappedItem); + }); + + ext.registriesTree.refresh(); + + const message = l10n.t('Successfully deleted registry "{0}".', registryName); + // don't wait + /* eslint-disable-next-line @typescript-eslint/no-floating-promises */ + window.showInformationMessage(message); +} diff --git a/src/tree/registries/Azure/AzureRegistryDataProvider.ts b/src/tree/registries/Azure/AzureRegistryDataProvider.ts index cdf98fe2e9..b2b2ae0831 100644 --- a/src/tree/registries/Azure/AzureRegistryDataProvider.ts +++ b/src/tree/registries/Azure/AzureRegistryDataProvider.ts @@ -8,6 +8,8 @@ import { AzureSubscription, VSCodeAzureSubscriptionProvider } from '@microsoft/v import { RegistryV2DataProvider, V2Registry, V2RegistryItem, V2Repository, registryV2Request } from '@microsoft/vscode-docker-registries'; import { CommonRegistryItem, isRegistryRoot } from '@microsoft/vscode-docker-registries/lib/clients/Common/models'; import * as vscode from 'vscode'; +import { getResourceGroupFromId } from '../../../utils/azureUtils'; +import { createAzureClient } from '../getInformationFromRegistryItem'; import { ACROAuthProvider } from './ACROAuthProvider'; export type AzureRepository = V2Repository; @@ -26,7 +28,7 @@ function isAzureSubscriptionRegistryItem(item: unknown): item is AzureSubscripti return !!item && typeof item === 'object' && (item as AzureSubscriptionRegistryItem).type === 'azuresubscription'; } -type AzureRegistry = V2Registry & AzureRegistryItem; +export type AzureRegistry = V2Registry & AzureRegistryItem; export class AzureRegistryDataProvider extends RegistryV2DataProvider implements vscode.Disposable { public readonly id = 'vscode-docker.azureContainerRegistry'; @@ -161,7 +163,9 @@ export class AzureRegistryDataProvider extends RegistryV2DataProvider implements } public async deleteRegistry(item: AzureRegistry): Promise { - throw new Error('Method not implemented.'); + const client = await createAzureClient(item.subscription); + const resourceGroup = getResourceGroupFromId(item.id); + await client.registries.beginDeleteAndWait(resourceGroup, item.label); } protected override getAuthenticationProvider(item: AzureRegistryItem): ACROAuthProvider { From d3bb8b83639869631f9af1aa9222c50ebe53a5d3 Mon Sep 17 00:00:00 2001 From: alexyaang <59073590+alexyaang@users.noreply.github.com> Date: Wed, 9 Aug 2023 09:29:46 -0400 Subject: [PATCH 5/6] changed registry tree util file name to be more general --- src/commands/registries/azure/tasks/scheduleRunRequest.ts | 2 +- src/commands/registries/copyRemoteFullTag.ts | 2 +- src/commands/registries/pullImages.ts | 2 +- src/tree/registries/Azure/AzureRegistryDataProvider.ts | 2 +- .../{getInformationFromRegistryItem.ts => registryTreeUtils.ts} | 1 - 5 files changed, 4 insertions(+), 5 deletions(-) rename src/tree/registries/{getInformationFromRegistryItem.ts => registryTreeUtils.ts} (98%) diff --git a/src/commands/registries/azure/tasks/scheduleRunRequest.ts b/src/commands/registries/azure/tasks/scheduleRunRequest.ts index 09af3b7850..cc6e7c3816 100644 --- a/src/commands/registries/azure/tasks/scheduleRunRequest.ts +++ b/src/commands/registries/azure/tasks/scheduleRunRequest.ts @@ -15,7 +15,7 @@ import * as vscode from 'vscode'; import { ext } from '../../../../extensionVariables'; import { AzureRegistryItem } from "../../../../tree/registries/Azure/AzureRegistryDataProvider"; import { UnifiedRegistryItem } from "../../../../tree/registries/UnifiedRegistryTreeDataProvider"; -import { createAzureClient } from "../../../../tree/registries/getInformationFromRegistryItem"; +import { createAzureClient } from "../../../../tree/registries/registryTreeUtils"; import { getStorageBlob } from '../../../../utils/lazyPackages'; import { delay } from '../../../../utils/promiseUtils'; import { Item, quickPickDockerFileItem, quickPickYamlFileItem } from '../../../../utils/quickPickFile'; diff --git a/src/commands/registries/copyRemoteFullTag.ts b/src/commands/registries/copyRemoteFullTag.ts index a85d67a329..4cf7ba996c 100644 --- a/src/commands/registries/copyRemoteFullTag.ts +++ b/src/commands/registries/copyRemoteFullTag.ts @@ -8,7 +8,7 @@ import { CommonTag } from '@microsoft/vscode-docker-registries'; import * as vscode from 'vscode'; import { ext } from '../../extensionVariables'; import { UnifiedRegistryItem } from '../../tree/registries/UnifiedRegistryTreeDataProvider'; -import { getFullImageNameFromRegistryItem } from '../../tree/registries/getInformationFromRegistryItem'; +import { getFullImageNameFromRegistryItem } from '../../tree/registries/registryTreeUtils'; export async function copyRemoteFullTag(context: IActionContext, node?: UnifiedRegistryItem): Promise { if (!node) { diff --git a/src/commands/registries/pullImages.ts b/src/commands/registries/pullImages.ts index d77620b4c5..738b05b1af 100644 --- a/src/commands/registries/pullImages.ts +++ b/src/commands/registries/pullImages.ts @@ -8,7 +8,7 @@ import { CommonRegistry, CommonRepository, CommonTag } from '@microsoft/vscode-d import { ext } from '../../extensionVariables'; import { TaskCommandRunnerFactory } from '../../runtimes/runners/TaskCommandRunnerFactory'; import { UnifiedRegistryItem } from '../../tree/registries/UnifiedRegistryTreeDataProvider'; -import { getImageNameFromRegistryItem } from '../../tree/registries/getInformationFromRegistryItem'; +import { getImageNameFromRegistryItem } from '../../tree/registries/registryTreeUtils'; import { logInToDockerCli } from './logInToDockerCli'; export async function pullRepository(context: IActionContext, node?: UnifiedRegistryItem): Promise { diff --git a/src/tree/registries/Azure/AzureRegistryDataProvider.ts b/src/tree/registries/Azure/AzureRegistryDataProvider.ts index b2b2ae0831..816f7ead53 100644 --- a/src/tree/registries/Azure/AzureRegistryDataProvider.ts +++ b/src/tree/registries/Azure/AzureRegistryDataProvider.ts @@ -9,7 +9,7 @@ import { RegistryV2DataProvider, V2Registry, V2RegistryItem, V2Repository, regis import { CommonRegistryItem, isRegistryRoot } from '@microsoft/vscode-docker-registries/lib/clients/Common/models'; import * as vscode from 'vscode'; import { getResourceGroupFromId } from '../../../utils/azureUtils'; -import { createAzureClient } from '../getInformationFromRegistryItem'; +import { createAzureClient } from '../registryTreeUtils'; import { ACROAuthProvider } from './ACROAuthProvider'; export type AzureRepository = V2Repository; diff --git a/src/tree/registries/getInformationFromRegistryItem.ts b/src/tree/registries/registryTreeUtils.ts similarity index 98% rename from src/tree/registries/getInformationFromRegistryItem.ts rename to src/tree/registries/registryTreeUtils.ts index adb7bf44e2..b51236f0ca 100644 --- a/src/tree/registries/getInformationFromRegistryItem.ts +++ b/src/tree/registries/registryTreeUtils.ts @@ -37,7 +37,6 @@ export function getFullImageNameFromRegistryItem(node: UnifiedRegistryItem { return new (await import('@azure/arm-containerregistry')).ContainerRegistryManagementClient(subscriptionItem.credential, subscriptionItem.subscriptionId); } From f96dc3e73c6dc9703ef83c18a7e49dcec7c53a30 Mon Sep 17 00:00:00 2001 From: alexyaang <59073590+alexyaang@users.noreply.github.com> Date: Wed, 9 Aug 2023 12:03:00 -0400 Subject: [PATCH 6/6] added openInAzurePortalCommand --- package.json | 2 +- .../registries/azure/openInAzurePortal.ts | 34 +++++++++++-------- .../Azure/AzureRegistryDataProvider.ts | 12 ++++--- 3 files changed, 28 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index 397272e299..659e06e9bf 100644 --- a/package.json +++ b/package.json @@ -561,7 +561,7 @@ }, { "command": "vscode-docker.registries.azure.openInPortal", - "when": "view == dockerRegistries && viewItem =~ /azure(Subscription|;DockerV2;Registry;)/", + "when": "view == dockerRegistries && viewItem =~ /(azuresubscription|azureContainerRegistry)/i", "group": "regs_zzz_common@1" }, { diff --git a/src/commands/registries/azure/openInAzurePortal.ts b/src/commands/registries/azure/openInAzurePortal.ts index 71c284f7d3..2dd09988a7 100644 --- a/src/commands/registries/azure/openInAzurePortal.ts +++ b/src/commands/registries/azure/openInAzurePortal.ts @@ -3,24 +3,28 @@ * Licensed under the MIT License. See LICENSE.md in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IActionContext } from '@microsoft/vscode-azext-utils'; +import { IActionContext, UserCancelledError, contextValueExperience } from '@microsoft/vscode-azext-utils'; +import * as vscode from 'vscode'; +import { ext } from '../../../extensionVariables'; +import { AzureRegistry, AzureSubscriptionRegistryItem, isAzureRegistryItem, isAzureSubscriptionRegistryItem } from '../../../tree/registries/Azure/AzureRegistryDataProvider'; import { UnifiedRegistryItem } from '../../../tree/registries/UnifiedRegistryTreeDataProvider'; -export async function openInAzurePortal(context: IActionContext, node?: UnifiedRegistryItem): Promise { - // if (!node) { - // node = await ext.registriesTree.showTreeItemPicker(registryExpectedContextValues.azure.registry, context); - // } +export async function openInAzurePortal(context: IActionContext, node?: UnifiedRegistryItem): Promise { + if (!node) { + node = await contextValueExperience(context, ext.registriesTree, { include: ['azuresubscription', 'azureContainerRegistry'] }); + } - // const azSubTreeItem = await getAzSubTreeItem(); - // const azExtAzureUtils = await getAzExtAzureUtils(); + const azureRegistryItem = node.wrappedItem; + const baseUrl = `${azureRegistryItem.subscription.environment.portalUrl}/#@${azureRegistryItem.subscription.tenantId}/resource`; + let url: string; - // if (node instanceof azSubTreeItem.SubscriptionTreeItem) { - // await azExtAzureUtils.openInPortal(node.subscription, node.subscription.subscriptionId); - // } else if (node instanceof AzureRegistryTreeItem) { - // await azExtAzureUtils.openInPortal(node.parent.subscription, node.registryId); - // } else { - // await azExtAzureUtils.openInPortal(node.parent.parent.subscription, `${node.parent.registryId}/repository`); - // } + if (isAzureSubscriptionRegistryItem(azureRegistryItem)) { + url = `${baseUrl}/subscriptions/${azureRegistryItem.subscription.subscriptionId}`; + } else if (isAzureRegistryItem(azureRegistryItem)) { + url = `${baseUrl}/${azureRegistryItem.id}`; + } else { + throw new UserCancelledError(); + } - // TODO: review this later + await vscode.env.openExternal(vscode.Uri.parse(url)); } diff --git a/src/tree/registries/Azure/AzureRegistryDataProvider.ts b/src/tree/registries/Azure/AzureRegistryDataProvider.ts index 816f7ead53..16957ca891 100644 --- a/src/tree/registries/Azure/AzureRegistryDataProvider.ts +++ b/src/tree/registries/Azure/AzureRegistryDataProvider.ts @@ -12,8 +12,6 @@ import { getResourceGroupFromId } from '../../../utils/azureUtils'; import { createAzureClient } from '../registryTreeUtils'; import { ACROAuthProvider } from './ACROAuthProvider'; -export type AzureRepository = V2Repository; - export interface AzureRegistryItem extends V2RegistryItem { readonly subscription: AzureSubscription; readonly id: string; @@ -24,11 +22,17 @@ export interface AzureSubscriptionRegistryItem extends CommonRegistryItem { readonly type: 'azuresubscription'; } -function isAzureSubscriptionRegistryItem(item: unknown): item is AzureSubscriptionRegistryItem { +export type AzureRegistry = V2Registry & AzureRegistryItem; + +export type AzureRepository = V2Repository; + +export function isAzureSubscriptionRegistryItem(item: unknown): item is AzureSubscriptionRegistryItem { return !!item && typeof item === 'object' && (item as AzureSubscriptionRegistryItem).type === 'azuresubscription'; } -export type AzureRegistry = V2Registry & AzureRegistryItem; +export function isAzureRegistryItem(item: unknown): item is AzureRegistry { + return !!item && typeof item === 'object' && (item as AzureRegistryItem).additionalContextValues?.includes('azureContainerRegistry'); +} export class AzureRegistryDataProvider extends RegistryV2DataProvider implements vscode.Disposable { public readonly id = 'vscode-docker.azureContainerRegistry';