From bc29acd29f78006d5d69e9560e8256c3a813a99e Mon Sep 17 00:00:00 2001 From: Esteban Rey Date: Mon, 27 Aug 2018 11:35:47 -0700 Subject: [PATCH] Azurecr/registry management (#383) * Added Azure Credentials Manager Singleton (#18) * Added Azure Credentials Manager Singleton * Added getResourceManagementClient * Sorted Existing Create Registry ready for code review * Added acquiring telemetry data for create registry * broke up createnewresourcegroup method and fixed client use Added try catch loop and awaited for resource group list again to check for duplicates with ResourceManagementClient * Jackson esteban/unified client nit Fix (#24) * Added Azure Credentials Manager Singleton * Small Style Fixes * Further Style fixes, added getResourceManagementClient * Lazy Initialization Patches * Enabled location selection * Location request fixes -Changed order of questions asked to user for better UX (location of new res group & location of new registry) -Placeholder of location is display name view * Refactor while loop for new res group * Added SKU selection * Quick fix- initializing array syntax * Added specific error messages and comments * Julia/delete image (#29) * first fully functional version of delete through input bar AND right click * refactored code to make it prettier! * comments * comments, added subscription function * fixed to style guide * style fixes, refactoring * delete image after reviews put my functions from azureCredentialsManager into two new files: utils/azure/acrTools.ts, and commands/utils/quick-pick-azure.ts Edited code based on Esteban's and Bin's reviews * One last little change to delete image * moved repository, azureimage, and getsubscriptions to the correct places within deleteImage * changes from PR reviews on delete image * fixed authentication issue, got rid of azureAccount property for repository and image **on constructor for repository, azurecredentialsmanager was being recreated and thus couldn't find the azureAccount. For this reason, I got rid of the azureAccount property of the classes Repository and AzureImage. This bug may lead to future problems (Esteban and I couldn't see why it was happening) * minor fixes deleteImage * delete a parentheses * Estebanreyl/dev merge fixes (#43) * Merge fixes to acquire latest telemetry items * Updated to master AzureUtilityManager * Rutusamai/list build tasks for each registry (#37) * tslint updates, transfered from old branch * updated icon * Addressed PR comments- mostly styling/code efficiency * Changed url to aka.ms link * changed Error window to Info message for no build tasks in your registry * Changed default sku and unified parsing resource group into a new method, getResourceGroup in acrTools.ts * Changed build task icon * Julia/delete repository final (#49) * deleteRepo moved over to branch off dev * Got rid of unnecessary code, fully functioning! * deleteRepo moved over to branch off dev * Got rid of unnecessary code, fully functioning! * spacing * final commit * Cleaned code * Added Telemetry * Julia/delete registry final (#47) Delete azure registry functionality added Delete azure registry moved to branch off dev Reorganized stye * began updating * Reorganized create registry and delete azure image * continued improvements * Began updating login * being credentials update * further updates * Finished updating, need to test functionality now * Updated requests, things all work now * Applied some nit fixes * Updates to naming * maintain UtilityManager standards * Updated Prompts * Updated imports and naming / standarized telemetry * Added explorer refresh capabilities on delete/add * Remove build task features from this branch This reverts commit 126c01ecf521e49c2fcbf6aaacb0628563da5929. * Merge bugfixes and name specification * updated weird naming issue * Deleted deprecated telemetry, added copyright comment and updated quick picks * Updated casing * Updated resource group and registry validity checking and other nit fixes * Updated Azure Utility Manager to by default sort registries alphabetically * Updated azureRegistryNodes and registryRootNode to use shared functions * Corrected resourcegroup name test * added delete button when deleting an image * Small changes in variables for better prompts and success notifications --- commands/azureCommands/create-registry.ts | 54 ++++++ commands/azureCommands/delete-image.ts | 50 ++++++ commands/azureCommands/delete-registry.ts | 33 ++++ commands/azureCommands/delete-repository.ts | 42 +++++ commands/utils/quick-pick-azure.ts | 174 ++++++++++++++++++++ constants.ts | 6 + dockerExtension.ts | 17 +- explorer/dockerExplorer.ts | 4 + explorer/models/azureRegistryNodes.ts | 150 +++-------------- explorer/models/registryRootNode.ts | 48 +----- package.json | 149 ++++++++++------- typings/vscode-extension-telemetry.d.ts | 2 +- utils/Azure/acrTools.ts | 150 +++++++++++++++++ utils/Azure/common.ts | 37 +++++ utils/Azure/models/image.ts | 30 ++++ utils/Azure/models/repository.ts | 26 +++ utils/azureUtilityManager.ts | 36 +++- 17 files changed, 770 insertions(+), 238 deletions(-) create mode 100644 commands/azureCommands/create-registry.ts create mode 100644 commands/azureCommands/delete-image.ts create mode 100644 commands/azureCommands/delete-registry.ts create mode 100644 commands/azureCommands/delete-repository.ts create mode 100644 commands/utils/quick-pick-azure.ts create mode 100644 utils/Azure/acrTools.ts create mode 100644 utils/Azure/common.ts create mode 100644 utils/Azure/models/image.ts create mode 100644 utils/Azure/models/repository.ts diff --git a/commands/azureCommands/create-registry.ts b/commands/azureCommands/create-registry.ts new file mode 100644 index 0000000000..fd3e645bfd --- /dev/null +++ b/commands/azureCommands/create-registry.ts @@ -0,0 +1,54 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { ContainerRegistryManagementClient } from 'azure-arm-containerregistry'; +import { Registry, RegistryNameStatus } from "azure-arm-containerregistry/lib/models"; +import { SubscriptionModels } from 'azure-arm-resource'; +import { ResourceGroup } from "azure-arm-resource/lib/resource/models"; +import * as vscode from "vscode"; +import { dockerExplorerProvider } from '../../dockerExtension'; +import { ext } from '../../extensionVariables'; +import { isValidAzureName } from '../../utils/Azure/common'; +import { AzureUtilityManager } from '../../utils/azureUtilityManager'; +import { quickPickLocation, quickPickResourceGroup, quickPickSKU, quickPickSubscription } from '../utils/quick-pick-azure'; + +/* Creates a new Azure container registry based on user input/selection of features */ +export async function createRegistry(): Promise { + const subscription: SubscriptionModels.Subscription = await quickPickSubscription(); + const resourceGroup: ResourceGroup = await quickPickResourceGroup(true, subscription); + const client = AzureUtilityManager.getInstance().getContainerRegistryManagementClient(subscription); + const registryName: string = await acquireRegistryName(client); + const sku: string = await quickPickSKU(); + const location = await quickPickLocation(subscription); + + const registry = await client.registries.beginCreate(resourceGroup.name, registryName, { + 'sku': { 'name': sku }, + 'location': location + }); + vscode.window.showInformationMessage(registry.name + ' has been created succesfully!'); + dockerExplorerProvider.refreshRegistries(); + return registry; +} + +/** Acquires a new registry name from a user, validating that the name is unique */ +async function acquireRegistryName(client: ContainerRegistryManagementClient): Promise { + let opt: vscode.InputBoxOptions = { + validateInput: async (value: string) => { return await checkForValidName(value, client) }, + ignoreFocusOut: false, + prompt: 'Enter the new registry name? ' + }; + let registryName: string = await ext.ui.showInputBox(opt); + + return registryName; +} + +async function checkForValidName(registryName: string, client: ContainerRegistryManagementClient): Promise { + let check = isValidAzureName(registryName); + if (!check.isValid) { return check.message; } + let registryStatus: RegistryNameStatus = await client.registries.checkNameAvailability({ 'name': registryName }); + if (registryStatus.message) { + return registryStatus.message; + } + return undefined; +} diff --git a/commands/azureCommands/delete-image.ts b/commands/azureCommands/delete-image.ts new file mode 100644 index 0000000000..3fb55de8a3 --- /dev/null +++ b/commands/azureCommands/delete-image.ts @@ -0,0 +1,50 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { Registry } from "azure-arm-containerregistry/lib/models"; +import * as vscode from "vscode"; +import { DialogResponses } from "vscode-azureextensionui"; +import { dockerExplorerProvider } from '../../dockerExtension'; +import { AzureImageTagNode } from '../../explorer/models/azureRegistryNodes'; +import { ext } from "../../extensionVariables"; +import * as acrTools from '../../utils/Azure/acrTools'; +import { AzureImage } from "../../utils/Azure/models/image"; +import { Repository } from "../../utils/Azure/models/repository"; +import * as quickPicks from '../utils/quick-pick-azure'; + +/** Function to delete an Azure hosted image + * @param context : if called through right click on AzureImageNode, the node object will be passed in. See azureRegistryNodes.ts for more info + */ +export async function deleteAzureImage(context?: AzureImageTagNode): Promise { + let registry: Registry; + let repoName: string; + let tag: string; + + if (!context) { + registry = await quickPicks.quickPickACRRegistry(); + const repository: Repository = await quickPicks.quickPickACRRepository(registry, 'Select the repository of the image you want to delete'); + repoName = repository.name; + const image: AzureImage = await quickPicks.quickPickACRImage(repository, 'Select the image you want to delete'); + tag = image.tag; + + } else { + registry = context.registry; + let wholeName: string[] = context.label.split(':'); + repoName = wholeName[0]; + tag = wholeName[1]; + } + + const shouldDelete = await ext.ui.showWarningMessage(`Are you sure you want to delete ${repoName}:${tag}? `, { modal: true }, DialogResponses.deleteResponse, DialogResponses.cancel); + if (shouldDelete === DialogResponses.deleteResponse) { + const { acrAccessToken } = await acrTools.acquireACRAccessTokenFromRegistry(registry, `repository:${repoName}:*`); + const path = `/v2/_acr/${repoName}/tags/${tag}`; + await acrTools.sendRequestToRegistry('delete', registry.loginServer, path, acrAccessToken); + vscode.window.showInformationMessage(`Successfully deleted image ${tag}`); + if (context) { + dockerExplorerProvider.refreshNode(context.parent); + } else { + dockerExplorerProvider.refreshRegistries(); + } + } +} diff --git a/commands/azureCommands/delete-registry.ts b/commands/azureCommands/delete-registry.ts new file mode 100644 index 0000000000..8d348ac6a3 --- /dev/null +++ b/commands/azureCommands/delete-registry.ts @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { Registry } from "azure-arm-containerregistry/lib/models"; +import { SubscriptionModels } from "azure-arm-resource"; +import * as vscode from "vscode"; +import { dockerExplorerProvider } from '../../dockerExtension'; +import { AzureRegistryNode } from '../../explorer/models/azureRegistryNodes'; +import * as acrTools from '../../utils/Azure/acrTools'; +import { AzureUtilityManager } from '../../utils/azureUtilityManager'; +import { confirmUserIntent, quickPickACRRegistry } from '../utils/quick-pick-azure'; + +/** Delete a registry and all it's associated nested items + * @param context : the AzureRegistryNode the user right clicked on to delete + */ +export async function deleteAzureRegistry(context?: AzureRegistryNode): Promise { + let registry: Registry; + if (context) { + registry = context.registry; + } else { + registry = await quickPickACRRegistry(false, 'Select the registry you want to delete'); + } + const shouldDelete = await confirmUserIntent(`Are you sure you want to delete ${registry.name} and its associated images?`); + if (shouldDelete) { + let subscription: SubscriptionModels.Subscription = acrTools.getSubscriptionFromRegistry(registry); + let resourceGroup: string = acrTools.getResourceGroupName(registry); + const client = AzureUtilityManager.getInstance().getContainerRegistryManagementClient(subscription); + await client.registries.beginDeleteMethod(resourceGroup, registry.name); + vscode.window.showInformationMessage(`Successfully deleted registry ${registry.name}`); + dockerExplorerProvider.refreshRegistries(); + } +} diff --git a/commands/azureCommands/delete-repository.ts b/commands/azureCommands/delete-repository.ts new file mode 100644 index 0000000000..0e9d313ee5 --- /dev/null +++ b/commands/azureCommands/delete-repository.ts @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { Registry } from "azure-arm-containerregistry/lib/models"; +import * as vscode from "vscode"; +import { dockerExplorerProvider } from '../../dockerExtension'; +import { AzureRepositoryNode } from '../../explorer/models/azureRegistryNodes'; +import * as acrTools from '../../utils/Azure/acrTools'; +import { Repository } from "../../utils/Azure/models/repository"; +import { AzureUtilityManager } from "../../utils/azureUtilityManager"; +import { confirmUserIntent, quickPickACRRegistry, quickPickACRRepository } from '../utils/quick-pick-azure'; + +/** + * function to delete an Azure repository and its associated images + * @param context : if called through right click on AzureRepositoryNode, the node object will be passed in. See azureRegistryNodes.ts for more info + */ +export async function deleteRepository(context?: AzureRepositoryNode): Promise { + let registry: Registry; + let repoName: string; + + if (context) { + repoName = context.label; + registry = context.registry; + } else { + registry = await quickPickACRRegistry(); + const repository: Repository = await quickPickACRRepository(registry, 'Select the repository you want to delete'); + repoName = repository.name; + } + const shouldDelete = await confirmUserIntent(`Are you sure you want to delete ${repoName} and its associated images? Enter yes to continue: `); + if (shouldDelete) { + const { acrAccessToken } = await acrTools.acquireACRAccessTokenFromRegistry(registry, `repository:${repoName}:*`); + const path = `/v2/_acr/${repoName}/repository`; + await acrTools.sendRequestToRegistry('delete', registry.loginServer, path, acrAccessToken); + vscode.window.showInformationMessage(`Successfully deleted repository ${repoName}`); + if (context) { + dockerExplorerProvider.refreshNode(context.parent); + } else { + dockerExplorerProvider.refreshRegistries(); + } + } +} diff --git a/commands/utils/quick-pick-azure.ts b/commands/utils/quick-pick-azure.ts new file mode 100644 index 0000000000..14bae5aa9d --- /dev/null +++ b/commands/utils/quick-pick-azure.ts @@ -0,0 +1,174 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { Registry } from 'azure-arm-containerregistry/lib/models'; +import { ResourceGroup } from 'azure-arm-resource/lib/resource/models'; +import { Location, Subscription } from 'azure-arm-resource/lib/subscription/models'; +import * as opn from 'opn'; +import * as vscode from "vscode"; +import { IAzureQuickPickItem, UserCancelledError } from 'vscode-azureextensionui'; +import { skus } from '../../constants' +import { ext } from '../../extensionVariables'; +import { ResourceManagementClient } from '../../node_modules/azure-arm-resource'; +import * as acrTools from '../../utils/Azure/acrTools'; +import { isValidAzureName } from '../../utils/Azure/common'; +import { AzureImage } from "../../utils/Azure/models/image"; +import { Repository } from "../../utils/Azure/models/repository"; +import { AzureUtilityManager } from '../../utils/azureUtilityManager'; + +export async function quickPickACRImage(repository: Repository, prompt?: string): Promise { + const placeHolder = prompt ? prompt : 'Select image to use'; + const repoImages: AzureImage[] = await acrTools.getImagesByRepository(repository); + const imageListNames = repoImages.map(img => >{ label: img.tag, data: img }); + let desiredImage = await ext.ui.showQuickPick(imageListNames, { 'canPickMany': false, 'placeHolder': placeHolder }); + return desiredImage.data; +} + +export async function quickPickACRRepository(registry: Registry, prompt?: string): Promise { + const placeHolder = prompt ? prompt : 'Select repository to use'; + const repositories: Repository[] = await acrTools.getRepositoriesByRegistry(registry); + const quickPickRepoList = repositories.map(repo => >{ label: repo.name, data: repo }); + let desiredRepo = await ext.ui.showQuickPick(quickPickRepoList, { 'canPickMany': false, 'placeHolder': placeHolder }); + return desiredRepo.data; +} + +export async function quickPickACRRegistry(canCreateNew: boolean = false, prompt?: string): Promise { + const placeHolder = prompt ? prompt : 'Select registry to use'; + let registries = await AzureUtilityManager.getInstance().getRegistries(); + let quickPickRegList = registries.map(reg => >{ label: reg.name, data: reg }); + + let createNewItem: IAzureQuickPickItem = { label: '+ Create new registry', data: undefined }; + if (canCreateNew) { quickPickRegList.unshift(createNewItem); } + + let desiredReg: IAzureQuickPickItem = await ext.ui.showQuickPick(quickPickRegList, { + 'canPickMany': false, + 'placeHolder': placeHolder + }); + let registry: Registry; + if (desiredReg === createNewItem) { + registry = await vscode.commands.executeCommand("vscode-docker.create-ACR-Registry"); + } else { + registry = desiredReg.data; + } + return registry; +} + +export async function quickPickSKU(): Promise { + const quickPickSkuList = skus.map(sk => >{ label: sk, data: sk }); + let desiredSku: IAzureQuickPickItem = await ext.ui.showQuickPick(quickPickSkuList, { + 'canPickMany': false, + 'placeHolder': 'Choose a SKU to use' + }); + return desiredSku.data; +} + +export async function quickPickSubscription(): Promise { + const subscriptions = AzureUtilityManager.getInstance().getFilteredSubscriptionList(); + if (subscriptions.length === 0) { + vscode.window.showErrorMessage("You do not have any subscriptions. You can create one in your Azure portal", "Open Portal").then(val => { + if (val === "Open Portal") { + opn('https://portal.azure.com/'); + } + }); + } + if (subscriptions.length === 1) { return subscriptions[0]; } + + let quickPickSubList = subscriptions.map(sub => >{ label: sub.displayName, data: sub }); + let desiredSub = await ext.ui.showQuickPick(quickPickSubList, { + 'canPickMany': false, + 'placeHolder': 'Select a subscription to use' + }); + return desiredSub.data; +} + +export async function quickPickLocation(subscription: Subscription): Promise { + let locations: Location[] = await AzureUtilityManager.getInstance().getLocationsBySubscription(subscription); + let quickPickLocList = locations.map(loc => >{ label: loc.displayName, data: loc }); + + quickPickLocList.sort((loc1, loc2): number => { + return loc1.data.displayName.localeCompare(loc2.data.displayName); + }); + + let desiredLocation: IAzureQuickPickItem = await ext.ui.showQuickPick(quickPickLocList, { + 'canPickMany': false, + 'placeHolder': 'Select a location to use' + }); + return desiredLocation.label; +} + +export async function quickPickResourceGroup(canCreateNew?: boolean, subscription?: Subscription): Promise { + let resourceGroups = await AzureUtilityManager.getInstance().getResourceGroups(subscription); + let quickPickResourceGroups = resourceGroups.map(res => >{ label: res.name, data: res }); + + let createNewItem: IAzureQuickPickItem = { label: '+ Create new resource group', data: undefined }; + if (canCreateNew) { quickPickResourceGroups.unshift(createNewItem); } + + let desiredResGroup: IAzureQuickPickItem = await ext.ui.showQuickPick(quickPickResourceGroups, { + 'canPickMany': false, + 'placeHolder': 'Choose a resource group to use' + }); + + let resourceGroup: ResourceGroup; + if (desiredResGroup === createNewItem) { + if (!subscription) { + subscription = await quickPickSubscription(); + } + const loc = await quickPickLocation(subscription); + resourceGroup = await createNewResourceGroup(loc, subscription); + } else { + resourceGroup = desiredResGroup.data; + } + return resourceGroup; +} + +/** Requests confirmation for an action and returns true only in the case that the user types in yes + * @param yesOrNoPrompt Should be a yes or no question + */ +export async function confirmUserIntent(yesOrNoPrompt: string): Promise { + let opt: vscode.InputBoxOptions = { + ignoreFocusOut: true, + placeHolder: 'Yes', + value: 'No', + prompt: yesOrNoPrompt + ' Enter yes to continue' + }; + let answer = await ext.ui.showInputBox(opt); + answer = answer.toLowerCase(); + if (answer === 'yes') { + return answer === 'yes'; + } else { + throw new UserCancelledError(); + } +} + +/*Creates a new resource group within the current subscription */ +async function createNewResourceGroup(loc: string, subscription?: Subscription): Promise { + const resourceGroupClient = AzureUtilityManager.getInstance().getResourceManagementClient(subscription); + + let opt: vscode.InputBoxOptions = { + validateInput: async (value: string) => { return await checkForValidResourcegroupName(value, resourceGroupClient) }, + ignoreFocusOut: false, + prompt: 'New resource group name?' + }; + + let resourceGroupName: string = await ext.ui.showInputBox(opt); + + let newResourceGroup: ResourceGroup = { + name: resourceGroupName, + location: loc, + }; + + return await resourceGroupClient.resourceGroups.createOrUpdate(resourceGroupName, newResourceGroup); +} + +async function checkForValidResourcegroupName(resourceGroupName: string, resourceGroupClient: ResourceManagementClient): Promise { + let check = isValidAzureName(resourceGroupName); + if (!check.isValid) { return check.message; } + let resourceGroupStatus: boolean = await resourceGroupClient.resourceGroups.checkExistence(resourceGroupName); + + if (resourceGroupStatus) { + return 'This resource group is already in use'; + } + return undefined; + +} diff --git a/constants.ts b/constants.ts index 4f4805520f..f570a26dca 100644 --- a/constants.ts +++ b/constants.ts @@ -17,3 +17,9 @@ export namespace keytarConstants { export const dockerHubUserNameKey: string = 'dockerhub.username'; export const dockerHubPasswordKey: string = 'dockerhub.password'; } + +//Credentials Constants +export const NULL_GUID = '00000000-0000-0000-0000-000000000000'; + +//Azure Container Registries +export const skus = ["Standard", "Basic", "Premium"]; diff --git a/dockerExtension.ts b/dockerExtension.ts index 2c49e13d37..d9599877a9 100644 --- a/dockerExtension.ts +++ b/dockerExtension.ts @@ -8,6 +8,10 @@ import * as path from 'path'; import * as vscode from 'vscode'; import { AzureUserInput, createTelemetryReporter, IActionContext, registerCommand, registerUIExtensionVariables, UserCancelledError } from 'vscode-azureextensionui'; import { ConfigurationParams, DidChangeConfigurationNotification, DocumentSelector, LanguageClient, LanguageClientOptions, Middleware, ServerOptions, TransportKind } from 'vscode-languageclient/lib/main'; +import { createRegistry } from './commands/azureCommands/create-registry'; +import { deleteAzureImage } from './commands/azureCommands/delete-image'; +import { deleteAzureRegistry } from './commands/azureCommands/delete-registry'; +import { deleteRepository } from './commands/azureCommands/delete-repository'; import { buildImage } from './commands/build-image'; import { composeDown, composeRestart, composeUp } from './commands/docker-compose'; import inspectImage from './commands/inspect-image'; @@ -43,6 +47,7 @@ import { browseDockerHub, dockerHubLogout } from './explorer/utils/dockerHubUtil import { ext } from "./extensionVariables"; import { initializeTelemetryReporter, reporter } from './telemetry/telemetry'; import { AzureAccount } from './typings/azure-account.api'; +import { registerAzureCommand } from './utils/Azure/common'; import { AzureUtilityManager } from './utils/azureUtilityManager'; import { Keytar } from './utils/keytar'; @@ -111,13 +116,14 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { ctx.subscriptions.push(vscode.workspace.registerTextDocumentContentProvider(DOCKER_INSPECT_SCHEME, new DockerInspectDocumentContentProvider())); + if (azureAccount) { + AzureUtilityManager.getInstance().setAccount(azureAccount); + } + registerDockerCommands(azureAccount); ctx.subscriptions.push(vscode.debug.registerDebugConfigurationProvider('docker', new DockerDebugConfigProvider())); - if (azureAccount) { - AzureUtilityManager.getInstance().setAccount(azureAccount); - } await consolidateDefaultRegistrySettings(); activateLanguageClient(ctx); } @@ -180,7 +186,10 @@ function registerDockerCommands(azureAccount: AzureAccount): void { }); registerCommand('vscode-docker.connectCustomRegistry', connectCustomRegistry); registerCommand('vscode-docker.disconnectCustomRegistry', disconnectCustomRegistry); - + registerAzureCommand('vscode-docker.delete-ACR-Registry', deleteAzureRegistry); + registerAzureCommand('vscode-docker.delete-ACR-Image', deleteAzureImage); + registerAzureCommand('vscode-docker.delete-ACR-Repository', deleteRepository); + registerAzureCommand('vscode-docker.create-ACR-Registry', createRegistry); } async function consolidateDefaultRegistrySettings(): Promise { diff --git a/explorer/dockerExplorer.ts b/explorer/dockerExplorer.ts index ca4638eff3..11d46f1320 100644 --- a/explorer/dockerExplorer.ts +++ b/explorer/dockerExplorer.ts @@ -40,6 +40,10 @@ export class DockerExplorerProvider implements vscode.TreeDataProvider this._onDidChangeTreeData.fire(this._registriesNode); } + public refreshNode(element: NodeBase): void { + this._onDidChangeTreeData.fire(element); + } + public getTreeItem(element: NodeBase): vscode.TreeItem { return element.getTreeItem(); } diff --git a/explorer/models/azureRegistryNodes.ts b/explorer/models/azureRegistryNodes.ts index 0e311af238..11ef615885 100644 --- a/explorer/models/azureRegistryNodes.ts +++ b/explorer/models/azureRegistryNodes.ts @@ -5,18 +5,14 @@ import * as ContainerModels from 'azure-arm-containerregistry/lib/models'; import { SubscriptionModels } from 'azure-arm-resource'; -import * as moment from 'moment'; import * as path from 'path'; -import * as request from 'request-promise'; import * as vscode from 'vscode'; -import { parseError } from 'vscode-azureextensionui'; -import { MAX_CONCURRENT_REQUESTS } from '../../constants' -import { AzureAccount, AzureSession } from '../../typings/azure-account.api'; -import { AsyncPool } from '../../utils/asyncpool'; -import { getRepositories } from '../utils/dockerHubUtils'; +import { AzureAccount } from '../../typings/azure-account.api'; +import { getImagesByRepository, getRepositoriesByRegistry } from '../../utils/Azure/acrTools'; +import { AzureImage } from '../../utils/Azure/models/image'; +import { Repository } from '../../utils/Azure/models/repository'; import { formatTag, getCatalog, getTags } from './commonRegistryUtils'; import { NodeBase } from './nodeBase'; -import { RegistryType } from './registryType'; export class AzureRegistryNode extends NodeBase { constructor( @@ -45,62 +41,20 @@ export class AzureRegistryNode extends NodeBase { public async getChildren(element: AzureRegistryNode): Promise { const repoNodes: AzureRepositoryNode[] = []; - let node: AzureRepositoryNode; - const tenantId: string = element.subscription.tenantId; if (!this.azureAccount) { return []; } - const session: AzureSession = this.azureAccount.sessions.find((s, i, array) => s.tenantId.toLowerCase() === tenantId.toLowerCase()); - const { accessToken, refreshToken } = await acquireToken(session); - - if (accessToken && refreshToken) { - let refreshTokenARC; - let accessTokenARC; - - await request.post('https://' + element.label + '/oauth2/exchange', { - form: { - grant_type: 'access_token_refresh_token', - service: element.label, - tenant: tenantId, - refresh_token: refreshToken, - access_token: accessToken - } - }, (err, httpResponse, body) => { - if (body.length > 0) { - refreshTokenARC = JSON.parse(body).refresh_token; - } else { - return []; - } - }); - - await request.post('https://' + element.label + '/oauth2/token', { - form: { - grant_type: 'refresh_token', - service: element.label, - scope: 'registry:catalog:*', - refresh_token: refreshTokenARC - } - }, (err, httpResponse, body) => { - if (body.length > 0) { - accessTokenARC = JSON.parse(body).access_token; - } else { - return []; - } - }); - - let repositories = await getCatalog('https://' + element.label, { bearer: accessTokenARC }); - for (let repository of repositories) { - node = new AzureRepositoryNode(repository, - this.azureAccount, - element.subscription, - accessTokenARC, - refreshTokenARC, - element.registry, - element.label); - repoNodes.push(node); - } + const repositories: Repository[] = await getRepositoriesByRegistry(element.registry); + for (let repository of repositories) { + let node = new AzureRepositoryNode(repository.name, + element, + this.azureAccount, + element.subscription, + element.registry, + element.label); + repoNodes.push(node); } //Note these are ordered by default in alphabetical order @@ -111,10 +65,9 @@ export class AzureRegistryNode extends NodeBase { export class AzureRepositoryNode extends NodeBase { constructor( public readonly label: string, + public parent: NodeBase, public readonly azureAccount: AzureAccount, public readonly subscription: SubscriptionModels.Subscription, - public readonly accessTokenARC: string, - public readonly refreshTokenARC: string, public readonly registry: ContainerModels.Registry, public readonly repositoryName: string ) { @@ -140,54 +93,18 @@ export class AzureRepositoryNode extends NodeBase { public async getChildren(element: AzureRepositoryNode): Promise { const imageNodes: AzureImageTagNode[] = []; let node: AzureImageTagNode; - let refreshTokenARC; - let accessTokenARC; - - const tenantId: string = element.subscription.tenantId; - const session: AzureSession = element.azureAccount.sessions.find((s, i, array) => s.tenantId.toLowerCase() === tenantId.toLowerCase()); - const { accessToken, refreshToken } = await acquireToken(session); - - await request.post('https://' + element.repositoryName + '/oauth2/exchange', { - form: { - grant_type: 'access_token_refresh_token', - service: element.repositoryName, - tenant: tenantId, - refresh_token: refreshToken, - access_token: accessToken - } - }, (err, httpResponse, body) => { - if (body.length > 0) { - refreshTokenARC = JSON.parse(body).refresh_token; - } else { - return []; - } - }); - - await request.post('https://' + element.repositoryName + '/oauth2/token', { - form: { - grant_type: 'refresh_token', - service: element.repositoryName, - scope: 'repository:' + element.label + ':pull', - refresh_token: refreshTokenARC - } - }, (err, httpResponse, body) => { - if (body.length > 0) { - accessTokenARC = JSON.parse(body).access_token; - } else { - return []; - } - }); - - let tagInfos = await getTags('https://' + element.repositoryName, element.label, { bearer: accessTokenARC }); - for (let tagInfo of tagInfos) { + let repo = new Repository(element.registry, element.label); + let images: AzureImage[] = await getImagesByRepository(repo); + for (let img of images) { node = new AzureImageTagNode( element.azureAccount, - element.subscription, - element.registry, - element.registry.loginServer, - element.label, - tagInfo.tag, - tagInfo.created); + element, + img.subscription, + img.registry, + img.registry.loginServer, + img.repository.name, + img.tag, + img.created); imageNodes.push(node); } @@ -198,6 +115,7 @@ export class AzureRepositoryNode extends NodeBase { export class AzureImageTagNode extends NodeBase { constructor( public readonly azureAccount: AzureAccount, + public readonly parent: NodeBase, public readonly subscription: SubscriptionModels.Subscription, public readonly registry: ContainerModels.Registry, public readonly serverUrl: string, @@ -249,21 +167,3 @@ export class AzureLoadingNode extends NodeBase { } } } - -async function acquireToken(session: AzureSession): Promise<{ accessToken: string; refreshToken: string; }> { - return new Promise<{ accessToken: string; refreshToken: string; }>((resolve, reject) => { - const credentials: any = session.credentials; - const environment: any = session.environment; - // tslint:disable-next-line:no-function-expression // Grandfathered in - credentials.context.acquireToken(environment.activeDirectoryResourceId, credentials.username, credentials.clientId, function (err: any, result: { accessToken: string; refreshToken: string; }): void { - if (err) { - reject(err); - } else { - resolve({ - accessToken: result.accessToken, - refreshToken: result.refreshToken - }); - } - }); - }); -} diff --git a/explorer/models/registryRootNode.ts b/explorer/models/registryRootNode.ts index 01f1a6a611..a7582be760 100644 --- a/explorer/models/registryRootNode.ts +++ b/explorer/models/registryRootNode.ts @@ -13,6 +13,7 @@ import { keytarConstants, MAX_CONCURRENT_REQUESTS, MAX_CONCURRENT_SUBSCRIPTON_RE import { ext } from '../../extensionVariables'; import { AzureAccount } from '../../typings/azure-account.api'; import { AsyncPool } from '../../utils/asyncpool'; +import { AzureUtilityManager } from '../../utils/azureUtilityManager'; import * as dockerHub from '../utils/dockerHubUtils' import { AzureLoadingNode, AzureNotSignedInNode, AzureRegistryNode } from './azureRegistryNodes'; import { getCustomRegistries } from './customRegistries'; @@ -20,9 +21,6 @@ import { CustomRegistryNode } from './customRegistryNodes'; import { DockerHubOrgNode } from './dockerHubNodes'; import { NodeBase } from './nodeBase'; -// tslint:disable-next-line:no-var-requires -const ContainerRegistryManagement = require('azure-arm-containerregistry'); - export class RegistryRootNode extends NodeBase { private _azureAccount: AzureAccount; @@ -136,19 +134,18 @@ export class RegistryRootNode extends NodeBase { } if (loggedIntoAzure) { - const subs: SubscriptionModels.Subscription[] = this.getFilteredSubscriptions(); + const subscriptions: SubscriptionModels.Subscription[] = AzureUtilityManager.getInstance().getFilteredSubscriptionList(); const subPool = new AsyncPool(MAX_CONCURRENT_SUBSCRIPTON_REQUESTS); let subsAndRegistries: { 'subscription': SubscriptionModels.Subscription, 'registries': ContainerModels.RegistryListResult }[] = []; //Acquire each subscription's data simultaneously - // tslint:disable-next-line:prefer-for-of // Grandfathered in - for (let i = 0; i < subs.length; i++) { + for (let sub of subscriptions) { subPool.addTask(async () => { - const client = new ContainerRegistryManagement(this.getCredentialByTenantId(subs[i].tenantId), subs[i].subscriptionId); + const client = AzureUtilityManager.getInstance().getContainerRegistryManagementClient(sub); try { let regs: ContainerModels.Registry[] = await client.registries.list(); subsAndRegistries.push({ - 'subscription': subs[i], + 'subscription': sub, 'registries': regs }); } catch (error) { @@ -181,42 +178,11 @@ export class RegistryRootNode extends NodeBase { } await regPool.runAll(); - function sortFunction(a: AzureRegistryNode, b: AzureRegistryNode): number { + function compareFn(a: AzureRegistryNode, b: AzureRegistryNode): number { return a.registry.loginServer.localeCompare(b.registry.loginServer); } - azureRegistryNodes.sort(sortFunction); + azureRegistryNodes.sort(compareFn); return azureRegistryNodes; } } - - private getCredentialByTenantId(tenantId: string): ServiceClientCredentials { - - const session = this._azureAccount.sessions.find((s, i, array) => s.tenantId.toLowerCase() === tenantId.toLowerCase()); - - if (session) { - return session.credentials; - } - - throw new Error(`Failed to get credentials, tenant ${tenantId} not found.`); - } - - private getFilteredSubscriptions(): SubscriptionModels.Subscription[] { - - if (this._azureAccount) { - return this._azureAccount.filters.map(filter => { - return { - id: filter.subscription.id, - session: filter.session, - subscriptionId: filter.subscription.subscriptionId, - tenantId: filter.session.tenantId, - displayName: filter.subscription.displayName, - state: filter.subscription.state, - subscriptionPolicies: filter.subscription.subscriptionPolicies, - authorizationSource: filter.subscription.authorizationSource - }; - }); - } else { - return []; - } - } } diff --git a/package.json b/package.json index eb5ba43cbc..5cd674b369 100644 --- a/package.json +++ b/package.json @@ -47,11 +47,15 @@ "onCommand:vscode-docker.compose.restart", "onCommand:vscode-docker.configure", "onCommand:vscode-docker.createWebApp", + "onCommand:vscode-docker.create-ACR-Registry", "onCommand:vscode-docker.system.prune", "onCommand:vscode-docker.dockerHubLogout", "onCommand:vscode-docker.browseDockerHub", "onCommand:vscode-docker.browseAzurePortal", "onCommand:vscode-docker.explorer.refresh", + "onCommand:vscode-docker.delete-ACR-Registry", + "onCommand:vscode-docker.delete-ACR-Repository", + "onCommand:vscode-docker.delete-ACR-Image", "onCommand:vscode-docker.connectCustomRegistry", "onCommand:vscode-docker.disconnectCustomRegistry", "onView:dockerExplorer", @@ -60,8 +64,7 @@ "main": "./out/dockerExtension", "contributes": { "menus": { - "commandPalette": [ - { + "commandPalette": [{ "command": "vscode-docker.browseDockerHub", "when": "false" }, @@ -74,8 +77,7 @@ "when": "never" } ], - "editor/context": [ - { + "editor/context": [{ "when": "editorLangId == dockerfile", "command": "vscode-docker.image.build", "group": "docker" @@ -111,8 +113,7 @@ "group": "docker" } ], - "explorer/context": [ - { + "explorer/context": [{ "when": "resourceFilename =~ /[dD]ocker[fF]ile/", "command": "vscode-docker.image.build", "group": "docker" @@ -133,8 +134,7 @@ "group": "docker" } ], - "view/title": [ - { + "view/title": [{ "command": "vscode-docker.explorer.refresh", "when": "view == dockerExplorer", "group": "navigation" @@ -145,8 +145,7 @@ "group": "navigation" } ], - "view/item/context": [ - { + "view/item/context": [{ "command": "vscode-docker.container.start", "when": "view == dockerExplorer && viewItem =~ /^(localImageNode|imagesRootNode)$/" }, @@ -194,10 +193,26 @@ "command": "vscode-docker.createWebApp", "when": "view == dockerExplorer && viewItem =~ /^(azureImageTagNode|dockerHubImageTagNode|customImageTagNode)$/" }, + { + "command": "vscode-docker.create-ACR-Registry", + "when": "view == dockerExplorer && viewItem == azureRegistryRootNode" + }, { "command": "vscode-docker.dockerHubLogout", "when": "view == dockerExplorer && viewItem == dockerHubRootNode" }, + { + "command": "vscode-docker.delete-ACR-Repository", + "when": "view == dockerExplorer && viewItem == azureRepositoryNode" + }, + { + "command": "vscode-docker.delete-ACR-Image", + "when": "view == dockerExplorer && viewItem == azureImageTagNode" + }, + { + "command": "vscode-docker.delete-ACR-Registry", + "when": "view == dockerExplorer && viewItem == azureRegistryNode" + }, { "command": "vscode-docker.browseDockerHub", "when": "view == dockerExplorer && viewItem =~ /^(dockerHubImageTagNode|dockerHubRepositoryNode|dockerHubOrgNode)$/" @@ -216,40 +231,34 @@ } ] }, - "debuggers": [ - { - "type": "docker", - "label": "Docker", - "configurationSnippets": [ - { - "label": "Docker: Attach to Node", - "description": "Docker: Attach to Node", - "body": { - "type": "node", - "request": "attach", - "name": "Docker: Attach to Node", - "port": 9229, - "address": "localhost", - "localRoot": "^\"\\${workspaceFolder}\"", - "remoteRoot": "/usr/src/app", - "protocol": "inspector" - } - } - ] - } - ], - "languages": [ - { - "id": "dockerfile", - "aliases": [ - "Dockerfile" - ], - "filenamePatterns": [ - "*.dockerfile", - "Dockerfile" - ] - } - ], + "debuggers": [{ + "type": "docker", + "label": "Docker", + "configurationSnippets": [{ + "label": "Docker: Attach to Node", + "description": "Docker: Attach to Node", + "body": { + "type": "node", + "request": "attach", + "name": "Docker: Attach to Node", + "port": 9229, + "address": "localhost", + "localRoot": "^\"\\${workspaceFolder}\"", + "remoteRoot": "/usr/src/app", + "protocol": "inspector" + } + }] + }], + "languages": [{ + "id": "dockerfile", + "aliases": [ + "Dockerfile" + ], + "filenamePatterns": [ + "*.dockerfile", + "Dockerfile" + ] + }], "configuration": { "type": "object", "title": "Docker configuration options", @@ -409,8 +418,7 @@ } } }, - "commands": [ - { + "commands": [{ "command": "vscode-docker.configure", "title": "Add Docker files to workspace", "description": "Add Dockerfile, docker-compose.yml files", @@ -510,6 +518,21 @@ "description": "Restarts a composition of containers", "category": "Docker" }, + { + "command": "vscode-docker.create-ACR-Registry", + "title": "Create Azure Registry", + "category": "Docker" + }, + { + "command": "vscode-docker.delete-ACR-Repository", + "title": "Delete Azure Repository", + "category": "Docker" + }, + { + "command": "vscode-docker.delete-ACR-Image", + "title": "Delete Azure Image", + "category": "Docker" + }, { "command": "vscode-docker.image.push", "title": "Push", @@ -554,6 +577,16 @@ "title": "Browse in the Azure Portal", "category": "Docker" }, + { + "command": "vscode-docker.delete-ACR-Registry", + "title": "Delete Azure Registry", + "category": "Docker" + }, + { + "command": "vscode-docker.delete-ACR-Image", + "title": "Delete Azure Image", + "category": "Docker" + }, { "command": "vscode-docker.connectCustomRegistry", "title": "Connect to a private registry...", @@ -566,22 +599,18 @@ } ], "views": { - "dockerView": [ - { - "id": "dockerExplorer", - "name": "Explorer", - "when": "config.docker.showExplorer == true" - } - ] + "dockerView": [{ + "id": "dockerExplorer", + "name": "Explorer", + "when": "config.docker.showExplorer == true" + }] }, "viewsContainers": { - "activitybar": [ - { - "icon": "images/docker.svg", - "id": "dockerView", - "title": "Docker" - } - ] + "activitybar": [{ + "icon": "images/docker.svg", + "id": "dockerView", + "title": "Docker" + }] } }, "engines": { diff --git a/typings/vscode-extension-telemetry.d.ts b/typings/vscode-extension-telemetry.d.ts index 216d4d0134..76722886f8 100644 --- a/typings/vscode-extension-telemetry.d.ts +++ b/typings/vscode-extension-telemetry.d.ts @@ -4,4 +4,4 @@ declare module 'vscode-extension-telemetry' { sendTelemetryEvent(eventName: string, properties?: { [key: string]: string }, measures?: { [key: string]: number }): void; dispose(); } -} \ No newline at end of file +} diff --git a/utils/Azure/acrTools.ts b/utils/Azure/acrTools.ts new file mode 100644 index 0000000000..ee74e1c096 --- /dev/null +++ b/utils/Azure/acrTools.ts @@ -0,0 +1,150 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { Registry } from "azure-arm-containerregistry/lib/models"; +import { SubscriptionModels } from 'azure-arm-resource'; +import { Subscription } from "azure-arm-resource/lib/subscription/models"; +import request = require('request-promise'); +import { NULL_GUID } from "../../constants"; +import { getCatalog, getTags, TagInfo } from "../../explorer/models/commonRegistryUtils"; +import { AzureSession } from "../../typings/azure-account.api"; +import { AzureUtilityManager } from '../azureUtilityManager'; +import { AzureImage } from "./models/image"; +import { Repository } from "./models/repository"; + +//General helpers +/** Gets the subscription for a given registry + * @returns a subscription object + */ +export function getSubscriptionFromRegistry(registry: Registry): SubscriptionModels.Subscription { + let subscriptionId = registry.id.slice('/subscriptions/'.length, registry.id.search('/resourceGroups/')); + const subs = AzureUtilityManager.getInstance().getFilteredSubscriptionList(); + let subscription = subs.find((sub): boolean => { + return sub.subscriptionId === subscriptionId; + }); + return subscription; +} + +export function getResourceGroupName(registry: Registry): string { + return registry.id.slice(registry.id.search('resourceGroups/') + 'resourceGroups/'.length, registry.id.search('/providers/')); +} + +//Registry item management +/** List images under a specific Repository */ +export async function getImagesByRepository(element: Repository): Promise { + let allImages: AzureImage[] = []; + let image: AzureImage; + const { acrAccessToken } = await acquireACRAccessTokenFromRegistry(element.registry, 'repository:' + element.name + ':pull'); + const tags: TagInfo[] = await getTags('https://' + element.registry.loginServer, element.name, { bearer: acrAccessToken }); + for (let tag of tags) { + image = new AzureImage(element, tag.tag, tag.created); + allImages.push(image); + } + return allImages; +} + +/** List repositories on a given Registry. */ +export async function getRepositoriesByRegistry(registry: Registry): Promise { + let repo: Repository; + const { acrAccessToken } = await acquireACRAccessTokenFromRegistry(registry, "registry:catalog:*"); + const repositories: string[] = await getCatalog('https://' + registry.loginServer, { bearer: acrAccessToken }); + let allRepos: Repository[] = []; + for (let tempRepo of repositories) { + repo = new Repository(registry, tempRepo); + allRepos.push(repo); + } + //Note these are ordered by default in alphabetical order + return allRepos; +} + +/** Sends a custon html request to a registry + * @param http_method : the http method, this function currently only uses delete + * @param login_server: the login server of the registry + * @param path : the URL path + * @param username : registry username, can be in generic form of 0's, used to generate authorization header + * @param password : registry password, can be in form of accessToken, used to generate authorization header + */ +export async function sendRequestToRegistry(http_method: string, login_server: string, path: string, bearerAccessToken: string): Promise { + let url: string = `https://${login_server}${path}`; + let header = 'Bearer ' + bearerAccessToken; + let opt = { + headers: { 'Authorization': header }, + http_method: http_method, + url: url + } + if (http_method === 'delete') { + await request.delete(opt); + } +} + +//Credential management +/** Obtains registry username and password compatible with docker login */ +export async function loginCredentials(registry: Registry): Promise<{ password: string, username: string }> { + const subscription: Subscription = getSubscriptionFromRegistry(registry); + const session: AzureSession = AzureUtilityManager.getInstance().getSession(subscription) + const { aadAccessToken, aadRefreshToken } = await acquireAADTokens(session); + const acrRefreshToken = await acquireACRRefreshToken(registry.loginServer, session.tenantId, aadRefreshToken, aadAccessToken); + return { 'password': acrRefreshToken, 'username': NULL_GUID }; +} + +/** Obtains tokens for using the Docker Registry v2 Api + * @param registry The targeted Azure Container Registry + * @param scope String determining the scope of the access token + * @returns acrRefreshToken: For use as a Password for docker registry access , acrAccessToken: For use with docker API + */ +export async function acquireACRAccessTokenFromRegistry(registry: Registry, scope: string): Promise<{ acrRefreshToken: string, acrAccessToken: string }> { + const subscription: Subscription = getSubscriptionFromRegistry(registry); + const session: AzureSession = AzureUtilityManager.getInstance().getSession(subscription); + const { aadAccessToken, aadRefreshToken } = await acquireAADTokens(session); + const acrRefreshToken = await acquireACRRefreshToken(registry.loginServer, session.tenantId, aadRefreshToken, aadAccessToken); + const acrAccessToken = await acquireACRAccessToken(registry.loginServer, scope, acrRefreshToken) + return { acrRefreshToken, acrAccessToken }; +} + +/** Obtains refresh and access tokens for Azure Active Directory. */ +export async function acquireAADTokens(session: AzureSession): Promise<{ aadAccessToken: string, aadRefreshToken: string }> { + return new Promise<{ aadAccessToken: string, aadRefreshToken: string }>((resolve, reject) => { + const credentials: any = session.credentials; + const environment: any = session.environment; + credentials.context.acquireToken(environment.activeDirectoryResourceId, credentials.username, credentials.clientId, (err: any, result: any) => { + if (err) { + reject(err); + } else { + resolve({ + aadAccessToken: result.accessToken, + aadRefreshToken: result.refreshToken, + }); + } + }); + }); +} + +/** Obtains refresh tokens for Azure Container Registry. */ +export async function acquireACRRefreshToken(registryUrl: string, tenantId: string, aadRefreshToken: string, aadAccessToken: string): Promise { + const acrRefreshTokenResponse = await request.post(`https://${registryUrl}/oauth2/exchange`, { + form: { + grant_type: "refresh_token", + service: registryUrl, + tenant: tenantId, + refresh_token: aadRefreshToken, + access_token: aadAccessToken, + }, + }); + + return JSON.parse(acrRefreshTokenResponse).refresh_token; + +} + +/** Gets an ACR accessToken by using an acrRefreshToken */ +export async function acquireACRAccessToken(registryUrl: string, scope: string, acrRefreshToken: string): Promise { + const acrAccessTokenResponse = await request.post(`https://${registryUrl}/oauth2/token`, { + form: { + grant_type: "refresh_token", + service: registryUrl, + scope, + refresh_token: acrRefreshToken, + }, + }); + return JSON.parse(acrAccessTokenResponse).access_token; +} diff --git a/utils/Azure/common.ts b/utils/Azure/common.ts new file mode 100644 index 0000000000..adddc66d1b --- /dev/null +++ b/utils/Azure/common.ts @@ -0,0 +1,37 @@ +import * as opn from 'opn'; +import * as vscode from "vscode"; +import { IActionContext, registerCommand } from "vscode-azureextensionui"; +import { AzureUtilityManager } from "../azureUtilityManager"; + +let alphaNum = new RegExp('^[a-zA-Z0-9]*$'); + +export function isValidAzureName(value: string): { isValid: boolean, message?: string } { + if (value.length < 5 || value.length > 50) { + return { isValid: false, message: 'Name must be between 5 and 50 characters' }; + } else if (!alphaNum.test(value)) { + return { isValid: false, message: 'Name may contain alpha numeric characters only' }; + } else { + return { isValid: true }; + } +} +/** Uses consistent error handling from register command to replace callbacks for commands that have a dependency on azure account. + * If the dependency is not found notifies users providing them with information to go download the extension. + */ +export function registerAzureCommand(commandId: string, callback: (...args: any[]) => any): void { + let commandItem; + + if (!AzureUtilityManager.hasLoadedUtilityManager()) { + commandItem = () => { + const open: vscode.MessageItem = { title: "View in Marketplace" }; + vscode.window.showErrorMessage('Please install the Azure Account extension to use Azure features.', open).then((response) => { + if (response === open) { + opn('https://marketplace.visualstudio.com/items?itemName=ms-vscode.azure-account'); + } + }); + } + + } else { + commandItem = callback; + } + registerCommand(commandId, commandItem); +} diff --git a/utils/Azure/models/image.ts b/utils/Azure/models/image.ts new file mode 100644 index 0000000000..67ef5dc0ef --- /dev/null +++ b/utils/Azure/models/image.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { Registry } from 'azure-arm-containerregistry/lib/models'; +import { SubscriptionModels } from 'azure-arm-resource'; +import { Repository } from './repository'; + +/** Class Azure Image: Used locally, Organizes data for managing images */ +export class AzureImage { + public created: Date; + public registry: Registry; + public repository: Repository; + public tag: string; + public subscription: SubscriptionModels.Subscription; + public resourceGroupName: string; + public password?: string; + public username?: string; + + constructor(repository: Repository, tag: string, created: Date) { + this.registry = repository.registry; + this.repository = repository; + this.tag = tag; + this.created = created; + this.subscription = repository.subscription; + this.resourceGroupName = repository.resourceGroupName; + if (repository.password) { this.password = repository.password; } + if (repository.username) { this.username = repository.username; } + } +} diff --git a/utils/Azure/models/repository.ts b/utils/Azure/models/repository.ts new file mode 100644 index 0000000000..be87acd391 --- /dev/null +++ b/utils/Azure/models/repository.ts @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { Registry } from 'azure-arm-containerregistry/lib/models'; +import { SubscriptionModels } from 'azure-arm-resource'; +import * as acrTools from '../acrTools'; + +/** Class Azure Repository: Used locally, Organizes data for managing Repositories */ +export class Repository { + public registry: Registry; + public name: string; + public subscription: SubscriptionModels.Subscription; + public resourceGroupName: string; + public password?: string; + public username?: string; + + constructor(registry: Registry, repository: string, password?: string, username?: string) { + this.registry = registry; + this.resourceGroupName = acrTools.getResourceGroupName(registry); + this.subscription = acrTools.getSubscriptionFromRegistry(registry); + this.name = repository; + if (password) { this.password = password; } + if (username) { this.username = username; } + } +} diff --git a/utils/azureUtilityManager.ts b/utils/azureUtilityManager.ts index d6b9f20343..7b9808f860 100644 --- a/utils/azureUtilityManager.ts +++ b/utils/azureUtilityManager.ts @@ -5,11 +5,11 @@ import { ContainerRegistryManagementClient } from 'azure-arm-containerregistry'; import * as ContainerModels from 'azure-arm-containerregistry/lib/models'; -import { ResourceManagementClient, SubscriptionModels } from 'azure-arm-resource'; +import { ResourceManagementClient, SubscriptionClient, SubscriptionModels } from 'azure-arm-resource'; import { ResourceGroup } from "azure-arm-resource/lib/resource/models"; import { ServiceClientCredentials } from 'ms-rest'; import { MAX_CONCURRENT_SUBSCRIPTON_REQUESTS } from '../constants'; -import { AzureAccount } from '../typings/azure-account.api'; +import { AzureAccount, AzureSession } from '../typings/azure-account.api'; import { AsyncPool } from './asyncpool'; /* Singleton for facilitating communication with Azure account services by providing extended shared @@ -25,6 +25,10 @@ export class AzureUtilityManager { private constructor() { } + public static hasLoadedUtilityManager(): boolean { + if (AzureUtilityManager._instance) { return true; } else { return false; } + } + public static getInstance(): AzureUtilityManager { if (!AzureUtilityManager._instance) { // lazy initialization AzureUtilityManager._instance = new AzureUtilityManager(); @@ -43,6 +47,12 @@ export class AzureUtilityManager { throw new Error('Azure account is not present, you may have forgotten to call setAccount'); } + public getSession(subscription: SubscriptionModels.Subscription): AzureSession { + const tenantId: string = subscription.tenantId; + const azureAccount: AzureAccount = this.getAccount(); + return azureAccount.sessions.find((s) => s.tenantId.toLowerCase() === tenantId.toLowerCase()); + } + public getFilteredSubscriptionList(): SubscriptionModels.Subscription[] { return this.getAccount().filters.map(filter => { return { @@ -65,7 +75,9 @@ export class AzureUtilityManager { return new ResourceManagementClient(this.getCredentialByTenantId(subscription.tenantId), subscription.subscriptionId); } - public async getRegistries(subscription?: SubscriptionModels.Subscription, resourceGroup?: string, sortFunction?: (a: ContainerModels.Registry, b: ContainerModels.Registry) => number): Promise { + public async getRegistries(subscription?: SubscriptionModels.Subscription, resourceGroup?: string, + compareFn: (a: ContainerModels.Registry, b: ContainerModels.Registry) => number = this.sortRegistriesAlphabetically): Promise { + let registries: ContainerModels.Registry[] = []; if (subscription && resourceGroup) { @@ -93,11 +105,14 @@ export class AzureUtilityManager { await subPool.runAll(); } - if (sortFunction && registries.length > 1) { - registries.sort(sortFunction); - } + registries.sort(compareFn); + + //Return only non classic registries + return registries.filter((registry) => { return !registry.sku.tier.includes('Classic') }); + } - return registries; + private sortRegistriesAlphabetically(a: ContainerModels.Registry, b: ContainerModels.Registry): number { + return a.loginServer.localeCompare(b.loginServer); } public async getResourceGroups(subscription?: SubscriptionModels.Subscription): Promise { @@ -131,6 +146,13 @@ export class AzureUtilityManager { throw new Error(`Failed to get credentials, tenant ${tenantId} not found.`); } + public async getLocationsBySubscription(subscription: SubscriptionModels.Subscription): Promise { + const credential = this.getCredentialByTenantId(subscription.tenantId); + const client = new SubscriptionClient(credential); + const locations = (await client.subscriptions.listLocations(subscription.subscriptionId)); + return locations; + } + //CHECKS //Provides a unified check for login that should be called once before using the rest of the singletons capabilities public async waitForLogin(): Promise {