diff --git a/package.json b/package.json index 659e06e9bf..e4aa4d60eb 100644 --- a/package.json +++ b/package.json @@ -481,7 +481,7 @@ }, { "command": "vscode-docker.registries.azure.createRegistry", - "when": "view == dockerRegistries && viewItem == azureextensionui.azureSubscription", + "when": "view == dockerRegistries && viewItem =~ /azuresubscription/i", "group": "regs_1_general@1" }, { diff --git a/src/commands/registries/azure/createAzureRegistry.ts b/src/commands/registries/azure/createAzureRegistry.ts index a569cd5833..30d4efd305 100644 --- a/src/commands/registries/azure/createAzureRegistry.ts +++ b/src/commands/registries/azure/createAzureRegistry.ts @@ -3,18 +3,54 @@ * Licensed under the MIT License. See LICENSE.md in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IActionContext } from '@microsoft/vscode-azext-utils'; -// import type { SubscriptionTreeItem } from '../../../tree/registries/azure/SubscriptionTreeItem'; // These are only dev-time imports so don't need to be lazy +import { AzureWizard, IActionContext, contextValueExperience, createSubscriptionContext, nonNullProp } from '@microsoft/vscode-azext-utils'; +import { l10n, window } from 'vscode'; +import { ext } from '../../../extensionVariables'; +import { AzureSubscriptionRegistryItem } from '../../../tree/registries/Azure/AzureRegistryDataProvider'; +import { AzureRegistryCreateStep } from '../../../tree/registries/Azure/createWizard/AzureRegistryCreateStep'; +import { AzureRegistryNameStep } from '../../../tree/registries/Azure/createWizard/AzureRegistryNameStep'; +import { AzureRegistrySkuStep } from '../../../tree/registries/Azure/createWizard/AzureRegistrySkuStep'; +import { IAzureRegistryWizardContext } from '../../../tree/registries/Azure/createWizard/IAzureRegistryWizardContext'; import { UnifiedRegistryItem } from '../../../tree/registries/UnifiedRegistryTreeDataProvider'; -// import { getAzSubTreeItem } from '../../../utils/lazyPackages'; +import { getAzExtAzureUtils } from '../../../utils/lazyPackages'; -export async function createAzureRegistry(context: IActionContext, node?: UnifiedRegistryItem): Promise { - // const azSubTreeItem = await getAzSubTreeItem(); +export async function createAzureRegistry(context: IActionContext, node?: UnifiedRegistryItem): Promise { if (!node) { - // node = await ext.registriesTree.showTreeItemPicker(azSubTreeItem.SubscriptionTreeItem.contextValue, context); + node = await contextValueExperience(context, ext.registriesTree, { include: 'azuresubscription' }); } - // await node.createChild(context); - // TODO: review this later + const subscriptionContext = createSubscriptionContext(node.wrappedItem.subscription); + const wizardContext: IAzureRegistryWizardContext = { + ...context, + ...subscriptionContext, + azureSubscription: node.wrappedItem.subscription, + }; + const azExtAzureUtils = await getAzExtAzureUtils(); + + const promptSteps = [ + new AzureRegistryNameStep(), + new AzureRegistrySkuStep(), + new azExtAzureUtils.ResourceGroupListStep(), + ]; + azExtAzureUtils.LocationListStep.addStep(wizardContext, promptSteps); + + const wizard = new AzureWizard( + wizardContext, + { + promptSteps, + executeSteps: [ + new AzureRegistryCreateStep() + ], + title: l10n.t('Create new Azure Container Registry') + } + ); + + await wizard.prompt(); + const newRegistryName: string = nonNullProp(wizardContext, 'newRegistryName'); + await wizard.execute(); + + /* eslint-disable-next-line @typescript-eslint/no-floating-promises */ + window.showInformationMessage(`Successfully created registry "${newRegistryName}".`); + void ext.registriesTree.refresh(); } diff --git a/src/commands/registries/azure/deleteAzureRegistry.ts b/src/commands/registries/azure/deleteAzureRegistry.ts index febc0e4625..e6f059b123 100644 --- a/src/commands/registries/azure/deleteAzureRegistry.ts +++ b/src/commands/registries/azure/deleteAzureRegistry.ts @@ -26,7 +26,7 @@ export async function deleteAzureRegistry(context: IActionContext, node?: Unifie await azureRegistryDataProvider.deleteRegistry(node.wrappedItem); }); - ext.registriesTree.refresh(); + void ext.registriesTree.refresh(); const message = l10n.t('Successfully deleted registry "{0}".', registryName); // don't wait diff --git a/src/commands/registries/azure/deleteAzureRepository.ts b/src/commands/registries/azure/deleteAzureRepository.ts index 542f5610bb..627b347a7d 100644 --- a/src/commands/registries/azure/deleteAzureRepository.ts +++ b/src/commands/registries/azure/deleteAzureRepository.ts @@ -24,7 +24,7 @@ export async function deleteAzureRepository(context: IActionContext, node?: Unif await azureDataProvider.deleteRepository(node.wrappedItem); }); - ext.registriesTree.refresh(); + void ext.registriesTree.refresh(); const deleteSucceeded = l10n.t('Successfully deleted repository "{0}".', node.wrappedItem.label); // don't wait diff --git a/src/commands/registries/azure/tasks/scheduleRunRequest.ts b/src/commands/registries/azure/tasks/scheduleRunRequest.ts index cc6e7c3816..d0cf24e22d 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/registryTreeUtils"; +import { createAzureContainerRegistryClient } from "../../../../utils/azureUtils"; import { getStorageBlob } from '../../../../utils/lazyPackages'; import { delay } from '../../../../utils/promiseUtils'; import { Item, quickPickDockerFileItem, quickPickYamlFileItem } from '../../../../utils/quickPickFile'; @@ -67,7 +67,7 @@ export async function scheduleRunRequest(context: IActionContext, requestType: ' rootUri = vscode.Uri.file(path.dirname(fileItem.absoluteFilePath)); } - const azureRegistryClient = await createAzureClient(registry.subscription); + const azureRegistryClient = await createAzureContainerRegistryClient(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)); @@ -160,7 +160,7 @@ async function uploadSourceCode(client: ContainerRegistryManagementClient, regis const blobCheckInterval = 1000; const maxBlobChecks = 30; async function streamLogs(context: IActionContext, node: UnifiedRegistryItem, run: AcrRun): Promise { - const azureRegistryClient = await createAzureClient(node.wrappedItem.subscription); + const azureRegistryClient = await createAzureContainerRegistryClient(node.wrappedItem.subscription); const resourceGroup = getResourceGroupFromId(node.wrappedItem.id); const result = await azureRegistryClient.runs.getLogSasUrl(resourceGroup, node.wrappedItem.label, run.runId); diff --git a/src/tree/registries/Azure/AzureRegistryDataProvider.ts b/src/tree/registries/Azure/AzureRegistryDataProvider.ts index 16957ca891..89a185cb66 100644 --- a/src/tree/registries/Azure/AzureRegistryDataProvider.ts +++ b/src/tree/registries/Azure/AzureRegistryDataProvider.ts @@ -8,8 +8,7 @@ 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 '../registryTreeUtils'; +import { createAzureContainerRegistryClient, getResourceGroupFromId } from '../../../utils/azureUtils'; import { ACROAuthProvider } from './ACROAuthProvider'; export interface AzureRegistryItem extends V2RegistryItem { @@ -114,27 +113,13 @@ 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'] - }); - } + const repositories = await super.getRepositories(registry); + const repositoriesWithAdditionalContext = repositories.map(repository => ({ + ...repository, + additionalContextValues: ['azureContainerRepository'] + })); - return results; + return repositoriesWithAdditionalContext; } public override getTreeItem(element: CommonRegistryItem): Promise { @@ -167,7 +152,7 @@ export class AzureRegistryDataProvider extends RegistryV2DataProvider implements } public async deleteRegistry(item: AzureRegistry): Promise { - const client = await createAzureClient(item.subscription); + const client = await createAzureContainerRegistryClient(item.subscription); const resourceGroup = getResourceGroupFromId(item.id); await client.registries.beginDeleteAndWait(resourceGroup, item.label); } diff --git a/src/tree/registries/Azure/createWizard/AzureRegistryCreateStep.ts b/src/tree/registries/Azure/createWizard/AzureRegistryCreateStep.ts new file mode 100644 index 0000000000..43a330c51d --- /dev/null +++ b/src/tree/registries/Azure/createWizard/AzureRegistryCreateStep.ts @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type { AzExtLocation } from '@microsoft/vscode-azext-azureutils'; +import { AzureWizardExecuteStep, nonNullProp, parseError } from '@microsoft/vscode-azext-utils'; +import { Progress, l10n } from 'vscode'; +import { ext } from '../../../../extensionVariables'; +import { createAzureContainerRegistryClient } from '../../../../utils/azureUtils'; +import { getAzExtAzureUtils } from '../../../../utils/lazyPackages'; +import { IAzureRegistryWizardContext } from './IAzureRegistryWizardContext'; + +export class AzureRegistryCreateStep extends AzureWizardExecuteStep { + public priority: number = 130; + + public async execute(context: IAzureRegistryWizardContext, progress: Progress<{ message?: string; increment?: number }>): Promise { + const newRegistryName = nonNullProp(context, 'newRegistryName'); + + const client = await createAzureContainerRegistryClient(context.azureSubscription); + + const azExtAzureUtils = await getAzExtAzureUtils(); + const creating: string = l10n.t('Creating registry "{0}"...', newRegistryName); + ext.outputChannel.info(creating); + progress.report({ message: creating }); + + const location: AzExtLocation = await azExtAzureUtils.LocationListStep.getLocation(context); + const locationName: string = nonNullProp(location, 'name'); + const resourceGroup = nonNullProp(context, 'resourceGroup'); + try { + context.registry = await client.registries.beginCreateAndWait( + nonNullProp(resourceGroup, 'name'), + newRegistryName, + { + sku: { + name: nonNullProp(context, 'newRegistrySku') + }, + location: locationName + } + ); + } + catch (err) { + const parsedError = parseError(err); + if (parsedError.errorType === 'MissingSubscriptionRegistration') { + context.errorHandling.suppressReportIssue = true; + } + + throw err; + } + + const created = l10n.t('Successfully created registry "{0}".', newRegistryName); + ext.outputChannel.info(created); + } + + public shouldExecute(context: IAzureRegistryWizardContext): boolean { + return !context.registry; + } +} diff --git a/src/tree/registries/Azure/createWizard/AzureRegistryNameStep.ts b/src/tree/registries/Azure/createWizard/AzureRegistryNameStep.ts new file mode 100644 index 0000000000..14843f6b4b --- /dev/null +++ b/src/tree/registries/Azure/createWizard/AzureRegistryNameStep.ts @@ -0,0 +1,50 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type { ContainerRegistryManagementClient } from '@azure/arm-containerregistry'; // These are only dev-time imports so don't need to be lazy +import { AzureNameStep } from '@microsoft/vscode-azext-utils'; +import { l10n } from 'vscode'; +import { getArmContainerRegistry, getAzExtAzureUtils } from '../../../../utils/lazyPackages'; +import { IAzureRegistryWizardContext } from './IAzureRegistryWizardContext'; + +export class AzureRegistryNameStep extends AzureNameStep { + protected async isRelatedNameAvailable(context: IAzureRegistryWizardContext, name: string): Promise { + const azExtAzureUtils = await getAzExtAzureUtils(); + return await azExtAzureUtils.ResourceGroupListStep.isNameAvailable(context, name); + } + + public async prompt(context: IAzureRegistryWizardContext): Promise { + const azExtAzureUtils = await getAzExtAzureUtils(); + const armContainerRegistry = await getArmContainerRegistry(); + const client = azExtAzureUtils.createAzureClient(context, armContainerRegistry.ContainerRegistryManagementClient); + context.newRegistryName = (await context.ui.showInputBox({ + placeHolder: l10n.t('Registry name'), + prompt: l10n.t('Provide a registry name'), + /* eslint-disable-next-line @typescript-eslint/promise-function-async */ + validateInput: (name: string) => validateRegistryName(name, client) + })).trim(); + + context.relatedNameTask = this.generateRelatedName(context, context.newRegistryName, azExtAzureUtils.resourceGroupNamingRules); + } + + public shouldPrompt(context: IAzureRegistryWizardContext): boolean { + return !context.newRegistryName; + } +} + +async function validateRegistryName(name: string, client: ContainerRegistryManagementClient): Promise { + name = name ? name.trim() : ''; + + const min = 5; + const max = 50; + if (name.length < min || name.length > max) { + return l10n.t('The name must be between {0} and {1} characters.', min, max); + } else if (name.match(/[^a-z0-9]/i)) { + return l10n.t('The name can only contain alphanumeric characters.'); + } else { + const nameStatus = await client.registries.checkNameAvailability({ name, type: 'Microsoft.ContainerRegistry/registries' }); + return nameStatus.message; + } +} diff --git a/src/tree/registries/Azure/createWizard/AzureRegistrySkuStep.ts b/src/tree/registries/Azure/createWizard/AzureRegistrySkuStep.ts new file mode 100644 index 0000000000..bbfc3684c1 --- /dev/null +++ b/src/tree/registries/Azure/createWizard/AzureRegistrySkuStep.ts @@ -0,0 +1,23 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type { SkuName as AcrSkuName } from '@azure/arm-containerregistry'; // These are only dev-time imports so don't need to be lazy +import { AzureWizardPromptStep, IAzureQuickPickItem } from '@microsoft/vscode-azext-utils'; +import { l10n } from 'vscode'; +import { IAzureRegistryWizardContext } from './IAzureRegistryWizardContext'; + +export class AzureRegistrySkuStep extends AzureWizardPromptStep { + public async prompt(context: IAzureRegistryWizardContext): Promise { + const skus: AcrSkuName[] = ["Basic", "Standard", "Premium"]; + const picks: IAzureQuickPickItem[] = skus.map(s => { return { label: s, data: s }; }); + + const placeHolder: string = l10n.t('Select a SKU'); + context.newRegistrySku = (await context.ui.showQuickPick(picks, { placeHolder })).data; + } + + public shouldPrompt(context: IAzureRegistryWizardContext): boolean { + return !context.newRegistrySku; + } +} diff --git a/src/tree/registries/Azure/createWizard/IAzureRegistryWizardContext.ts b/src/tree/registries/Azure/createWizard/IAzureRegistryWizardContext.ts new file mode 100644 index 0000000000..65b42efa4c --- /dev/null +++ b/src/tree/registries/Azure/createWizard/IAzureRegistryWizardContext.ts @@ -0,0 +1,15 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type { Registry as AcrRegistry, SkuName as AcrSkuName } 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 { IResourceGroupWizardContext } from '@microsoft/vscode-azext-azureutils'; + +export interface IAzureRegistryWizardContext extends IResourceGroupWizardContext { + newRegistryName?: string; + newRegistrySku?: AcrSkuName; + registry?: AcrRegistry; + readonly azureSubscription: AzureSubscription; +} diff --git a/src/tree/registries/registryTreeUtils.ts b/src/tree/registries/registryTreeUtils.ts index b51236f0ca..eccab72abf 100644 --- a/src/tree/registries/registryTreeUtils.ts +++ b/src/tree/registries/registryTreeUtils.ts @@ -3,8 +3,6 @@ * 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"; @@ -36,7 +34,3 @@ export function getFullImageNameFromRegistryItem(node: UnifiedRegistryItem { - return new (await import('@azure/arm-containerregistry')).ContainerRegistryManagementClient(subscriptionItem.credential, subscriptionItem.subscriptionId); -} diff --git a/src/utils/azureUtils.ts b/src/utils/azureUtils.ts index 25c9bf7977..dbcc5ae66f 100644 --- a/src/utils/azureUtils.ts +++ b/src/utils/azureUtils.ts @@ -3,6 +3,8 @@ * Licensed under the MIT License. See LICENSE.md in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import type { ContainerRegistryManagementClient } from '@azure/arm-containerregistry'; +import { AzureSubscription } from '@microsoft/vscode-azext-azureauth'; import { ISubscriptionContext } from '@microsoft/vscode-azext-utils'; import { Request } from 'node-fetch'; import { URLSearchParams } from 'url'; @@ -69,4 +71,9 @@ export async function acquireAcrRefreshToken(registryHost: string, subContext: I return (await response.json()).refresh_token; } + +export async function createAzureContainerRegistryClient(subscriptionItem: AzureSubscription): Promise { + return new (await import('@azure/arm-containerregistry')).ContainerRegistryManagementClient(subscriptionItem.credential, subscriptionItem.subscriptionId); +} + /* eslint-enable @typescript-eslint/naming-convention */