From 341f09719c5e206e0ed67f6a9a0ce6542ef134fc Mon Sep 17 00:00:00 2001 From: Esteban Rey Date: Thu, 16 Aug 2018 14:32:33 -0700 Subject: [PATCH] Estebanreyl/ready for production (#55) * 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 --- commands/azureCommands/create-registry.ts | 207 ++--------- commands/azureCommands/delete-azure-image.ts | 71 ---- .../azureCommands/delete-azure-registry.ts | 49 --- commands/azureCommands/delete-image.ts | 57 +++ commands/azureCommands/delete-registry.ts | 41 +++ commands/azureCommands/delete-repository.ts | 51 ++- commands/azureCommands/pull-from-azure.ts | 13 +- commands/utils/quick-pick-azure.ts | 186 ++++++++-- constants.ts | 3 + dockerExtension.ts | 8 +- explorer/dockerExplorer.ts | 4 + explorer/models/azureRegistryNodes.ts | 6 +- explorer/models/registryRootNode.ts | 1 - explorer/models/taskNode.ts | 2 +- explorer/utils/azureUtils.ts | 12 - package.json | 6 +- utils/Azure/acrTools.ts | 336 ++++++------------ utils/Azure/models/image.ts | 10 +- utils/Azure/models/repository.ts | 23 +- utils/azureUtilityManager.ts | 6 + 20 files changed, 453 insertions(+), 639 deletions(-) delete mode 100644 commands/azureCommands/delete-azure-image.ts delete mode 100644 commands/azureCommands/delete-azure-registry.ts create mode 100644 commands/azureCommands/delete-image.ts create mode 100644 commands/azureCommands/delete-registry.ts diff --git a/commands/azureCommands/create-registry.ts b/commands/azureCommands/create-registry.ts index e615ea8ad4..9032ed4375 100644 --- a/commands/azureCommands/create-registry.ts +++ b/commands/azureCommands/create-registry.ts @@ -1,217 +1,60 @@ import { ContainerRegistryManagementClient } from 'azure-arm-containerregistry'; import { RegistryNameStatus } from "azure-arm-containerregistry/lib/models"; -import { ResourceManagementClient, SubscriptionModels } from 'azure-arm-resource'; +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 { UserCancelledError } from '../../explorer/deploy/wizard'; import { reporter } from '../../telemetry/telemetry'; import { AzureUtilityManager } from '../../utils/azureUtilityManager'; -const teleAzureId: string = 'vscode-docker.create.registry.azureContainerRegistry'; -const teleCmdId: string = 'vscode-docker.createRegistry'; -import * as opn from 'opn'; +import { quickPickLocation, quickPickResourceGroup, quickPickSKU, quickPickSubscription } from '../utils/quick-pick-azure'; +const teleCmdId: string = 'vscode-docker.create-ACR-Registry'; -/* Creates a new registry based on user input/selection of features, such as location */ -export async function createRegistry(): Promise { - let subscription: SubscriptionModels.Subscription; - let resourceGroup: ResourceGroup; - let location: string; - - try { - subscription = await acquireSubscription(); - resourceGroup = await acquireResourceGroup(subscription); - - } catch (error) { - return; - } +/* 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); - let registryName: string; - try { - registryName = await acquireRegistryName(client); - } catch (error) { - return; - } - - const sku: string = await acquireSKU(); - location = await acquireLocation(resourceGroup, subscription); - - client.registries.beginCreate(resourceGroup.name, registryName, { 'sku': { 'name': sku }, 'location': location }).then((response): void => { - vscode.window.showInformationMessage(response.name + ' has been created succesfully!'); - }, (error): void => { - vscode.window.showErrorMessage(error.message); - }) - - //Acquiring telemetry data here + 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(); if (reporter) { - /* __GDPR__ - "command" : { - "command" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } - */ reporter.sendTelemetryEvent('command', { command: teleCmdId }); - - if (registryName.toLowerCase().indexOf('azurecr.io')) { - /* __GDPR__ - "command" : { - "command" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } - */ - reporter.sendTelemetryEvent('command', { - command: teleAzureId - }); - } } + return registryName; } -// INPUT HELPERS -async function acquireSKU(): Promise { - let skus: string[] = ["Standard", "Basic", "Premium"]; - let sku: string; - sku = await vscode.window.showQuickPick(skus, { 'canPickMany': false, 'placeHolder': 'Choose a SKU' }); - if (sku === undefined) { throw new Error('User exit'); } - - return sku; -} - +/** Acquires a new registry name from a user, validating that the name is unique */ async function acquireRegistryName(client: ContainerRegistryManagementClient): Promise { let opt: vscode.InputBoxOptions = { ignoreFocusOut: false, - prompt: 'Registry name? ' + prompt: 'New Registry name? ' }; let registryName: string = await vscode.window.showInputBox(opt); + if (!registryName) { throw new UserCancelledError(); } let registryStatus: RegistryNameStatus = await client.registries.checkNameAvailability({ 'name': registryName }); + while (!registryStatus.nameAvailable) { opt = { ignoreFocusOut: false, - prompt: `The registry name '${registryName}' is unavailable. Try again: ` + prompt: `The Registry name '${registryName}' is unavailable. Try again: ` } registryName = await vscode.window.showInputBox(opt); + if (!registryName) { throw new UserCancelledError(); } - if (registryName === undefined) { throw new Error('user Exit'); } registryStatus = await client.registries.checkNameAvailability({ 'name': registryName }); } return registryName; } - -async function acquireSubscription(): Promise { - const subs = AzureUtilityManager.getInstance().getFilteredSubscriptionList(); - if (subs.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/'); - } - }); - } - - let subsNames: string[] = []; - for (let sub of subs) { - subsNames.push(sub.displayName); - } - let subscriptionName: string; - subscriptionName = await vscode.window.showQuickPick(subsNames, { 'canPickMany': false, 'placeHolder': 'Choose a subscription to be used' }); - if (subscriptionName === undefined) { throw new Error('User exit'); } - - return subs.find(sub => { return sub.displayName === subscriptionName }); -} - -async function acquireLocation(resourceGroup: ResourceGroup, subscription: SubscriptionModels.Subscription): Promise { - let locations: SubscriptionModels.Location[] = await AzureUtilityManager.getInstance().getLocationsBySubscription(subscription); - let locationNames: string[] = []; - let placeHolder: string; - - for (let loc of locations) { - locationNames.push(loc.displayName); - } - - locationNames.sort((loc1: string, loc2: string): number => { - return loc1.localeCompare(loc2); - }); - - if (resourceGroup === undefined) { - placeHolder = "Choose location for your new resource group"; - } else { - placeHolder = resourceGroup.location; - - //makes placeholder the Display Name version of the location's name - locations.forEach((locObj: SubscriptionModels.Location): string => { - if (locObj.name === resourceGroup.location) { - placeHolder = locObj.displayName; - return; - } - }); - } - let location: string; - do { - location = await vscode.window.showQuickPick(locationNames, { 'canPickMany': false, 'placeHolder': placeHolder }); - if (location === undefined) { throw new Error('User exit'); } - } while (!location); - return location; -} - -async function acquireResourceGroup(subscription: SubscriptionModels.Subscription): Promise { - //Acquire each subscription's data simultaneously - let resourceGroup; - let resourceGroupName; - const resourceGroupClient = new ResourceManagementClient(AzureUtilityManager.getInstance().getCredentialByTenantId(subscription.tenantId), subscription.subscriptionId); - let resourceGroups = await AzureUtilityManager.getInstance().getResourceGroups(subscription); - - let resourceGroupNames: string[] = []; - resourceGroupNames.push('+ Create new resource group'); - for (let resGroupName of resourceGroups) { - resourceGroupNames.push(resGroupName.name); - } - - do { - resourceGroupName = await vscode.window.showQuickPick(resourceGroupNames, { 'canPickMany': false, 'placeHolder': 'Choose a Resource Group to be used' }); - if (resourceGroupName === undefined) { throw new Error('user Exit'); } - if (resourceGroupName === '+ Create new resource group') { - let loc = await acquireLocation(resourceGroup, subscription); - resourceGroupName = await createNewResourceGroup(loc, resourceGroupClient); - } - resourceGroups = await AzureUtilityManager.getInstance().getResourceGroups(subscription); - resourceGroup = resourceGroups.find(resGroup => { return resGroup.name === resourceGroupName; }); - - if (!resourceGroupName) { vscode.window.showErrorMessage('You must select a valid resource group'); } - } while (!resourceGroupName); - return resourceGroup; -} - -/*Creates a new resource group within the current subscription */ -async function createNewResourceGroup(loc: string, resourceGroupClient: ResourceManagementClient): Promise { - let promptMessage = 'Resource group name?'; - - let opt: vscode.InputBoxOptions = { - ignoreFocusOut: false, - prompt: promptMessage - }; - - let resourceGroupName: string; - let resourceGroupStatus: boolean; - - while (opt.prompt) { - resourceGroupName = await vscode.window.showInputBox(opt); - resourceGroupStatus = await resourceGroupClient.resourceGroups.checkExistence(resourceGroupName); - if (!resourceGroupStatus) { - opt.prompt = null; - } else { - opt.prompt = `The resource group '${resourceGroupName}' already exists. Try again: `; - } - } - - let newResourceGroup: ResourceGroup = { - name: resourceGroupName, - location: loc, - }; - - //Potential error when two clients try to create same resource group name at once - try { - await resourceGroupClient.resourceGroups.createOrUpdate(resourceGroupName, newResourceGroup); - } catch (error) { - vscode.window.showErrorMessage(`The resource group '${resourceGroupName}' already exists. Try again: `); - } - return resourceGroupName; -} diff --git a/commands/azureCommands/delete-azure-image.ts b/commands/azureCommands/delete-azure-image.ts deleted file mode 100644 index 7c93ca49d5..0000000000 --- a/commands/azureCommands/delete-azure-image.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { Registry } from "azure-arm-containerregistry/lib/models"; -import { SubscriptionModels } from 'azure-arm-resource'; -import * as vscode from "vscode"; -import * as quickPicks from '../../commands/utils/quick-pick-azure'; -import { AzureImageNode } from '../../explorer/models/azureRegistryNodes'; -import { reporter } from '../../telemetry/telemetry'; -import * as acrTools from '../../utils/Azure/acrTools'; -import { Repository } from "../../utils/Azure/models/repository"; -import { AzureUtilityManager } from '../../utils/azureUtilityManager'; - -const teleCmdId: string = 'vscode-docker.deleteACRImage'; - -/** 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 deleteAzureImage(context?: AzureImageNode): Promise { - if (!AzureUtilityManager.getInstance().waitForLogin()) { - vscode.window.showErrorMessage('You are not logged into Azure'); - return; - } - let registry: Registry; - let subscription: SubscriptionModels.Subscription; - let repoName: string; - let username: string; - let password: string; - let tag: string; - if (!context) { - registry = await quickPicks.quickPickACRRegistry(); - subscription = acrTools.getRegistrySubscription(registry); - let repository: Repository = await quickPicks.quickPickACRRepository(registry); - repoName = repository.name; - const image = await quickPicks.quickPickACRImage(repository); - tag = image.tag; - } - - //ensure user truly wants to delete image - let opt: vscode.InputBoxOptions = { - ignoreFocusOut: true, - placeHolder: 'No', - value: 'No', - prompt: 'Are you sure you want to delete this image? Enter Yes to continue: ' - }; - let answer = await vscode.window.showInputBox(opt); - answer = answer.toLowerCase(); - if (answer !== 'yes') { return; } - - if (context) { - repoName = context.label; - subscription = context.subscription; - registry = context.registry; - let wholeName = repoName.split(':'); - repoName = wholeName[0]; - tag = wholeName[1]; - } - - let creds = await acrTools.acquireRegistryAccessToken(context.subscription, context.registry, context); - - username = creds.username; - password = creds.password; - let path = `/v2/_acr/${repoName}/tags/${tag}`; - await acrTools.sendRequestToRegistry('delete', registry.loginServer, path, username, password); //official call to delete the image - reportTelemetry(); -} - -function reportTelemetry(): void { - if (reporter) { - reporter.sendTelemetryEvent('command', { - command: teleCmdId - }); - } -} diff --git a/commands/azureCommands/delete-azure-registry.ts b/commands/azureCommands/delete-azure-registry.ts deleted file mode 100644 index 6d03c9cc31..0000000000 --- a/commands/azureCommands/delete-azure-registry.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Registry } from "azure-arm-containerregistry/lib/models"; -import * as vscode from "vscode"; -import { quickPickACRRegistry } from '../../commands/utils/quick-pick-azure'; -import { AzureRegistryNode } from '../../explorer/models/azureRegistryNodes'; -import { SubscriptionModels } from "../../node_modules/azure-arm-resource"; -import { reporter } from '../../telemetry/telemetry'; -import * as acrTools from '../../utils/Azure/acrTools'; -import { AzureUtilityManager } from '../../utils/AzureUtilityManager'; - -const teleCmdId: string = 'vscode-docker.deleteAzureRegistry'; - -/** 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(); - } - - let opt: vscode.InputBoxOptions = { - ignoreFocusOut: true, - placeHolder: 'No', - value: 'No', - prompt: 'Are you sure you want to delete this registry and its associated images? Enter yes to continue: ' - }; - let answer = await vscode.window.showInputBox(opt); - - answer = answer.toLowerCase(); - if (answer !== 'yes') { return; } - - let subscription: SubscriptionModels.Subscription = acrTools.getRegistrySubscription(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); - telemetryReport(); -} - -function telemetryReport(): void { - if (reporter) { - reporter.sendTelemetryEvent('command', { - command: teleCmdId - }); - } -} diff --git a/commands/azureCommands/delete-image.ts b/commands/azureCommands/delete-image.ts new file mode 100644 index 0000000000..7e49333189 --- /dev/null +++ b/commands/azureCommands/delete-image.ts @@ -0,0 +1,57 @@ +import { Registry } from "azure-arm-containerregistry/lib/models"; +import * as vscode from "vscode"; +import { dockerExplorerProvider } from '../../dockerExtension'; +import { UserCancelledError } from "../../explorer/deploy/wizard"; +import { AzureImageNode, AzureRepositoryNode } from '../../explorer/models/AzureRegistryNodes'; +import { reporter } from '../../telemetry/telemetry'; +import * as acrTools from '../../utils/Azure/acrTools'; +import { AzureImage } from "../../utils/Azure/models/image"; +import { Repository } from "../../utils/Azure/models/repository"; +import { AzureUtilityManager } from '../../utils/azureUtilityManager'; +import * as quickPicks from '../utils/quick-pick-azure'; + +const teleCmdId: string = 'vscode-docker.delete-ACR-Image'; + +/** 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?: AzureImageNode): Promise { + if (!AzureUtilityManager.getInstance().waitForLogin()) { + vscode.window.showErrorMessage('You are not logged into Azure'); + throw new Error('User is not logged into azure'); + } + let registry: Registry; + let repoName: string; + let tag: string; + + if (!context) { + registry = await quickPicks.quickPickACRRegistry(); + const repository: Repository = await quickPicks.quickPickACRRepository(registry, 'Choose the Repository of the image you want to delete'); + repoName = repository.name; + const image: AzureImage = await quickPicks.quickPickACRImage(repository, 'Choose 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 quickPicks.confirmUserIntent('Are you sure you want to delete this image? Enter Yes to continue: '); + if (shouldDelete) { + 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 { + throw new UserCancelledError(); + } + if (reporter) { + reporter.sendTelemetryEvent('command', { + command: teleCmdId + }); + } +} diff --git a/commands/azureCommands/delete-registry.ts b/commands/azureCommands/delete-registry.ts new file mode 100644 index 0000000000..1ea89aa4a6 --- /dev/null +++ b/commands/azureCommands/delete-registry.ts @@ -0,0 +1,41 @@ +import { Registry } from "azure-arm-containerregistry/lib/models"; +import { SubscriptionModels } from "azure-arm-resource"; +import * as vscode from "vscode"; +import { dockerExplorerProvider } from '../../dockerExtension'; +import { UserCancelledError } from "../../explorer/deploy/wizard"; +import { AzureRegistryNode } from '../../explorer/models/AzureRegistryNodes'; +import { reporter } from '../../telemetry/telemetry'; +import * as acrTools from '../../utils/Azure/acrTools'; +import { AzureUtilityManager } from '../../utils/azureUtilityManager'; +import { confirmUserIntent, quickPickACRRegistry } from '../utils/quick-pick-azure'; + +const teleCmdId: string = 'vscode-docker.delete-ACR-Registry'; + +/** 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, 'Choose the Registry you want to delete'); + } + const shouldDelete = await confirmUserIntent('Are you sure you want to delete this registry and its associated images? Enter yes to continue: '); + 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(); + } else { + throw new UserCancelledError(); + } + + if (reporter) { + reporter.sendTelemetryEvent('command', { + command: teleCmdId + }); + } +} diff --git a/commands/azureCommands/delete-repository.ts b/commands/azureCommands/delete-repository.ts index 78cad929f2..667bfc3025 100644 --- a/commands/azureCommands/delete-repository.ts +++ b/commands/azureCommands/delete-repository.ts @@ -1,13 +1,15 @@ import { Registry } from "azure-arm-containerregistry/lib/models"; -import { SubscriptionModels } from 'azure-arm-resource'; +import { Context } from "mocha"; import * as vscode from "vscode"; -import * as quickPicks from '../../commands/utils/quick-pick-azure'; -import { AzureRepositoryNode } from '../../explorer/models/azureRegistryNodes'; +import { dockerExplorerProvider } from '../../dockerExtension'; +import { UserCancelledError } from "../../explorer/deploy/wizard"; +import { AzureRegistryNode, AzureRepositoryNode } from '../../explorer/models/AzureRegistryNodes'; import { reporter } from '../../telemetry/telemetry'; import * as acrTools from '../../utils/Azure/acrTools'; import { Repository } from "../../utils/Azure/models/repository"; +import { confirmUserIntent, quickPickACRRegistry, quickPickACRRepository } from '../utils/quick-pick-azure'; -const teleCmdId: string = 'vscode-docker.deleteACRRepository'; +const teleCmdId: string = 'vscode-docker.delete-ACR-Repository'; /** * 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 @@ -15,41 +17,28 @@ const teleCmdId: string = 'vscode-docker.deleteACRRepository'; export async function deleteRepository(context?: AzureRepositoryNode): Promise { let registry: Registry; - let subscription: SubscriptionModels.Subscription; let repoName: string; if (context) { repoName = context.label; - subscription = context.subscription; registry = context.registry; } else { - registry = await quickPicks.quickPickACRRegistry(); - subscription = acrTools.getRegistrySubscription(registry); - const repository: Repository = await quickPicks.quickPickACRRepository(registry); + registry = await quickPickACRRegistry(); + const repository: Repository = await quickPickACRRepository(registry, 'Choose the Repository you want to delete'); repoName = repository.name; } - - // Ensure user truly wants to delete registry - let opt: vscode.InputBoxOptions = { - ignoreFocusOut: true, - placeHolder: 'No', - value: 'No', - prompt: 'Are you sure you want to delete this repository and its associated images? Enter Yes to continue: ' - }; - - let answer = await vscode.window.showInputBox(opt); - answer = answer.toLowerCase(); - if (answer !== 'yes') { return; } - - let creds = await acrTools.acquireRegistryAccessToken(subscription, registry); - const username: string = creds.username; - const password: string = creds.password; - let path = `/v2/_acr/${repoName}/repository`; - await acrTools.sendRequestToRegistry('delete', registry.loginServer, path, username, password); - reportTelemetry(); -} - -function reportTelemetry(): void { + const shouldDelete = await confirmUserIntent('Are you sure you want to delete this repository 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 ${Repository}`); + if (context) { + dockerExplorerProvider.refreshNode(context.parent); + } + } else { + throw new UserCancelledError(); + } if (reporter) { reporter.sendTelemetryEvent('command', { command: teleCmdId diff --git a/commands/azureCommands/pull-from-azure.ts b/commands/azureCommands/pull-from-azure.ts index fdf6d7ac56..903ad6f32f 100644 --- a/commands/azureCommands/pull-from-azure.ts +++ b/commands/azureCommands/pull-from-azure.ts @@ -8,15 +8,10 @@ import * as acrTools from '../../utils/Azure/acrTools'; export async function pullFromAzure(context?: AzureImageNode): Promise { // Step 1: Using loginCredentials function to get the username and password. This takes care of all users, even if they don't have the Azure CLI - let credentials; - try { - credentials = await acrTools.acquireRegistryLoginCredential(context.subscription, context.registry, context); - } catch (error) { - console.log(error); - } - let username = credentials.username; - let password = credentials.password; - let registry = context.registry.loginServer; + const credentials = await acrTools.loginCredentials(context.registry); + const username = credentials.username; + const password = credentials.password; + const registry = context.registry.loginServer; const terminal = vscode.window.createTerminal("Docker"); terminal.show(); diff --git a/commands/utils/quick-pick-azure.ts b/commands/utils/quick-pick-azure.ts index ae7b49f092..af0951eaa3 100644 --- a/commands/utils/quick-pick-azure.ts +++ b/commands/utils/quick-pick-azure.ts @@ -1,58 +1,190 @@ -import { ContainerRegistryManagementClient } from 'azure-arm-containerregistry'; 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 { skus } from '../../constants' +import { UserCancelledError } from '../../explorer/deploy/wizard'; import * as acrTools from '../../utils/Azure/acrTools'; import { AzureImage } from "../../utils/Azure/models/image"; -import { Repository } from "../../utils/Azure/models/Repository"; +import { Repository } from "../../utils/Azure/models/repository"; import { AzureUtilityManager } from '../../utils/azureUtilityManager'; -/** - * function to allow user to pick a desired image for use - * @param repository the repository to look in - * @returns an AzureImage object (see azureUtils.ts) - */ -export async function quickPickACRImage(repository: Repository): Promise { - const repoImages: AzureImage[] = await acrTools.getAzureImages(repository); +export async function quickPickACRImage(repository: Repository, prompt?: string): Promise { + const placeHolder = prompt ? prompt : 'Choose Image to Use'; + const repoImages: AzureImage[] = await acrTools.getImagesByRepository(repository); let imageListNames: string[] = []; for (let tempImage of repoImages) { imageListNames.push(tempImage.tag); } - let desiredImage = await vscode.window.showQuickPick(imageListNames, { 'canPickMany': false, 'placeHolder': 'Choose the image you want to delete' }); + let desiredImage = await vscode.window.showQuickPick(imageListNames, { 'canPickMany': false, 'placeHolder': placeHolder }); if (!desiredImage) { return; } const image = repoImages.find((myImage): boolean => { return desiredImage === myImage.tag }); return image; } -/** - * function to allow user to pick a desired repository for use - * @param registry the registry to choose a repository from - * @returns a Repository object (see azureUtils.ts) - */ -export async function quickPickACRRepository(registry: Registry): Promise { - const myRepos: Repository[] = await acrTools.getAzureRepositories(registry); +export async function quickPickACRRepository(registry: Registry, prompt?: string): Promise { + const placeHolder = prompt ? prompt : 'Choose Registry to Use'; + const myRepos: Repository[] = await acrTools.getRepositoriesByRegistry(registry); let rep: string[] = []; for (let repo of myRepos) { rep.push(repo.name); } - let desiredRepo = await vscode.window.showQuickPick(rep, { 'canPickMany': false, 'placeHolder': 'Choose the repository from which your desired image exists' }); + let desiredRepo = await vscode.window.showQuickPick(rep, { 'canPickMany': false, 'placeHolder': placeHolder }); if (!desiredRepo) { return; } const repository = myRepos.find((currentRepo): boolean => { return desiredRepo === currentRepo.name }); return repository; } -/** - * function to let user choose a registry for use - * @returns a Registry object - */ -export async function quickPickACRRegistry(): Promise { - //first get desired registry +export async function quickPickACRRegistry(canCreateNew: boolean = false, prompt?: string): Promise { + const placeHolder = prompt ? prompt : 'Choose Registry to Use'; let registries = await AzureUtilityManager.getInstance().getRegistries(); - let reg: string[] = []; + const reg: string[] = []; + if (canCreateNew) { reg.push('+ Create new registry'); } for (let registryName of registries) { reg.push(registryName.name); } - let desired = await vscode.window.showQuickPick(reg, { 'canPickMany': false, 'placeHolder': 'Choose the Registry from which your desired image exists' }); - if (!desired) { return; } + let desired: string = await vscode.window.showQuickPick(reg, { + 'canPickMany': false, + 'placeHolder': placeHolder + }); + + if (!desired) { + throw new UserCancelledError(); + } else if (canCreateNew && desired === reg[0]) { + desired = String(await vscode.commands.executeCommand("vscode-docker.create-ACR-Registry")); + registries = await AzureUtilityManager.getInstance().getRegistries(); // Reload + } + const registry = registries.find((currentReg): boolean => { return desired === currentReg.name }); return registry; } + +export async function quickPickSKU(): Promise { + let sku: string; + sku = await vscode.window.showQuickPick(skus, { 'canPickMany': false, 'placeHolder': 'Choose a SKU to use' }); + if (!sku) { throw new UserCancelledError(); } + return sku; +} + +export async function quickPickSubscription(): Promise { + const subs = AzureUtilityManager.getInstance().getFilteredSubscriptionList(); + if (subs.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/'); + } + }); + throw new Error('User has no azure subscriptions'); + } + + let subsNames: string[] = []; + for (let sub of subs) { + subsNames.push(sub.displayName); + } + let subscriptionName: string; + subscriptionName = await vscode.window.showQuickPick(subsNames, { + 'canPickMany': false, + 'placeHolder': 'Choose a subscription to use' + }); + if (!subscriptionName) { throw new UserCancelledError(); } + + return subs.find(sub => { return sub.displayName === subscriptionName }); +} + +export async function quickPickLocation(subscription: Subscription): Promise { + let locations: Location[] = await AzureUtilityManager.getInstance().getLocationsBySubscription(subscription); + let locationNames: string[] = []; + + for (let loc of locations) { + locationNames.push(loc.displayName); + } + + locationNames.sort((loc1: string, loc2: string): number => { + return loc1.localeCompare(loc2); + }); + + let location: string = await vscode.window.showQuickPick(locationNames, { + 'canPickMany': false, + 'placeHolder': 'Choose a Location to use' + }); + if (!location) { throw new UserCancelledError(); } + return location; +} + +export async function quickPickResourceGroup(canCreateNew?: boolean, subscription?: Subscription): Promise { + let resourceGroups = await AzureUtilityManager.getInstance().getResourceGroups(subscription); + let resourceGroupNames: string[] = []; + + if (canCreateNew) { resourceGroupNames.push('+ Create new resource group'); } + for (let resGroupName of resourceGroups) { + resourceGroupNames.push(resGroupName.name); + } + + let resourceGroupName = await vscode.window.showQuickPick(resourceGroupNames, { + 'canPickMany': false, + 'placeHolder': 'Choose a Resource Group to use' + }); + if (!resourceGroupName) { throw new UserCancelledError(); } + + let resourceGroup: ResourceGroup; + if (canCreateNew && resourceGroupName === '+ Create new Resource Group') { + if (!subscription) { + subscription = await quickPickSubscription(); + } + const loc = await quickPickLocation(subscription); + resourceGroup = await createNewResourceGroup(loc, subscription); + } else { + resourceGroup = resourceGroups.find(resGroup => { return resGroup.name === resourceGroupName; }); + } + 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 { + //ensure user truly wants to delete image + let opt: vscode.InputBoxOptions = { + ignoreFocusOut: true, + placeHolder: 'No', + value: 'No', + prompt: yesOrNoPrompt + }; + let answer = await vscode.window.showInputBox(opt); + if (!answer) { throw new UserCancelledError(); } + + answer = answer.toLowerCase(); + return answer === 'yes'; +} +/*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 = { + ignoreFocusOut: false, + prompt: 'New Resource Group name?' + }; + + let resourceGroupName: string; + let resourceGroupStatus: boolean; + + while (opt.prompt) { + resourceGroupName = await vscode.window.showInputBox(opt); + if (!resourceGroupName) { throw new UserCancelledError(); } + + resourceGroupStatus = await resourceGroupClient.resourceGroups.checkExistence(resourceGroupName); + if (!resourceGroupStatus) { + opt.prompt = undefined; + } else { + opt.prompt = `The Resource Group '${resourceGroupName}' already exists. Try again: `; + } + } + + let newResourceGroup: ResourceGroup = { + name: resourceGroupName, + location: loc, + }; + + return await resourceGroupClient.resourceGroups.createOrUpdate(resourceGroupName, newResourceGroup); +} diff --git a/constants.ts b/constants.ts index d099293e16..bc3b47a639 100644 --- a/constants.ts +++ b/constants.ts @@ -13,3 +13,6 @@ export namespace keytarConstants { //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 092ef9d5de..233d495ebe 100644 --- a/dockerExtension.ts +++ b/dockerExtension.ts @@ -6,10 +6,10 @@ import * as opn from 'opn'; import * as path from 'path'; import * as vscode from 'vscode'; import { AzureUserInput, createTelemetryReporter, registerCommand, registerUIExtensionVariables, UserCancelledError } from 'vscode-azureextensionui'; -import { ConfigurationParams, DidChangeConfigurationNotification, DocumentSelector, LanguageClient, LanguageClientOptions, Middleware, ServerOptions, TransportKind } from 'vscode-languageclient'; +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-azure-image'; -import { deleteAzureRegistry } from './commands/azureCommands/delete-azure-registry'; +import { deleteAzureImage } from './commands/azureCommands/delete-image'; +import { deleteAzureRegistry } from './commands/azureCommands/delete-registry'; import { deleteRepository } from './commands/azureCommands/delete-repository'; import { pullFromAzure } from './commands/azureCommands/pull-from-azure'; import { buildImage } from './commands/build-image'; @@ -171,7 +171,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { registerCommand('vscode-docker.delete-ACR-Registry', deleteAzureRegistry); registerCommand('vscode-docker.delete-ACR-Image', deleteAzureImage); registerCommand('vscode-docker.delete-ACR-Repository', deleteRepository); - registerCommand('vscode-docker.createRegistry', createRegistry); + registerCommand('vscode-docker.create-ACR-Registry', createRegistry); AzureUtilityManager.getInstance().setAccount(azureAccount); } diff --git a/explorer/dockerExplorer.ts b/explorer/dockerExplorer.ts index 3c9ecc7ea8..abca40be95 100644 --- a/explorer/dockerExplorer.ts +++ b/explorer/dockerExplorer.ts @@ -35,6 +35,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 35a6b1f79e..14cbbb627d 100644 --- a/explorer/models/azureRegistryNodes.ts +++ b/explorer/models/azureRegistryNodes.ts @@ -109,6 +109,7 @@ export class AzureRegistryNode extends NodeBase { node.registry = element.registry; node.repository = element.label; node.subscription = element.subscription; + node.parent = element; registryChildNodes.push(node); } } @@ -136,6 +137,7 @@ export class AzureRepositoryNode extends NodeBase { public registry: ContainerModels.Registry; public repository: string; public subscription: SubscriptionModels.Subscription; + public parent: NodeBase; public getTreeItem(): vscode.TreeItem { return { @@ -223,6 +225,7 @@ export class AzureRepositoryNode extends NodeBase { node.registry = element.registry; node.serverUrl = element.repository; node.subscription = element.subscription; + node.parent = element; node.created = moment(new Date(JSON.parse(manifest.history[0].v1Compatibility).created)).fromNow(); imageNodes.push(node); } @@ -252,8 +255,7 @@ export class AzureImageNode extends NodeBase { public registry: ContainerModels.Registry; public serverUrl: string; public subscription: SubscriptionModels.Subscription; - public userName: string; - public repository: string; + public parent: NodeBase; public getTreeItem(): vscode.TreeItem { let displayName: string = this.label; diff --git a/explorer/models/registryRootNode.ts b/explorer/models/registryRootNode.ts index 4a8ec679a8..049ed79bb9 100644 --- a/explorer/models/registryRootNode.ts +++ b/explorer/models/registryRootNode.ts @@ -11,7 +11,6 @@ import { parseError } from 'vscode-azureextensionui'; import { keytarConstants, MAX_CONCURRENT_REQUESTS, MAX_CONCURRENT_SUBSCRIPTON_REQUESTS } from '../../constants'; import { AzureAccount } from '../../typings/azure-account.api'; import { AsyncPool } from '../../utils/asyncpool'; -import * as acrTools from '../../utils/Azure/acrTools'; import * as dockerHub from '../utils/dockerHubUtils' import { getCoreNodeModule } from '../utils/utils'; import { AzureLoadingNode, AzureNotSignedInNode, AzureRegistryNode } from './azureRegistryNodes'; diff --git a/explorer/models/taskNode.ts b/explorer/models/taskNode.ts index eb6a74b21d..191e9beaa2 100644 --- a/explorer/models/taskNode.ts +++ b/explorer/models/taskNode.ts @@ -1,7 +1,7 @@ +import * as ContainerModels from 'azure-arm-containerregistry/lib/models'; import { ResourceManagementClient, SubscriptionClient, SubscriptionModels } from 'azure-arm-resource'; import * as opn from 'opn'; import * as vscode from 'vscode'; -import * as ContainerModels from '../../node_modules/azure-arm-containerregistry/lib/models'; import { AzureAccount, AzureSession } from '../../typings/azure-account.api'; import * as acrTools from '../../utils/Azure/acrTools'; import { AzureUtilityManager } from '../../utils/azureUtilityManager'; diff --git a/explorer/utils/azureUtils.ts b/explorer/utils/azureUtils.ts index 8a55faa55a..c31c1a9aa1 100644 --- a/explorer/utils/azureUtils.ts +++ b/explorer/utils/azureUtils.ts @@ -15,15 +15,3 @@ export function browseAzurePortal(context?: AzureRegistryNode | AzureRepositoryN } } - -export function openAzurePortal(): void { - - /* - let url: string = `${session.environment.portalUrl}/${tenantId}/#resource${context.registry.id}`; - if (context.contextValue === 'azureImageNode' || context.contextValue === 'azureRepositoryNode') { - url = `${url}/repository`; - } - opn(url); - }*/ - -} diff --git a/package.json b/package.json index fadb0b64a1..166f7d51bf 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "onCommand:vscode-docker.compose.restart", "onCommand:vscode-docker.configure", "onCommand:vscode-docker.createWebApp", - "onCommand:vscode-docker.createRegistry", + "onCommand:vscode-docker.create-ACR-Registry", "onCommand:vscode-docker.system.prune", "onCommand:vscode-docker.dockerHubLogout", "onCommand:vscode-docker.browseDockerHub", @@ -193,7 +193,7 @@ "when": "view == dockerExplorer && viewItem =~ /^(azureImageNode|dockerHubImageTag)$/" }, { - "command": "vscode-docker.createRegistry", + "command": "vscode-docker.create-ACR-Registry", "when": "view == dockerExplorer && viewItem == azureRegistryRootNode" }, { @@ -521,7 +521,7 @@ "category": "Docker" }, { - "command": "vscode-docker.createRegistry", + "command": "vscode-docker.create-ACR-Registry", "title": "Create Registry", "category": "Docker" }, diff --git a/utils/Azure/acrTools.ts b/utils/Azure/acrTools.ts index e17e8a31c2..bbc404f412 100644 --- a/utils/Azure/acrTools.ts +++ b/utils/Azure/acrTools.ts @@ -1,243 +1,86 @@ 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 * as vscode from "vscode"; -import { AzureImageNode, AzureRepositoryNode } from '../../explorer/models/AzureRegistryNodes'; -import { AzureAccount, AzureSession } from "../../typings/azure-account.api"; +import { NULL_GUID } from "../../constants"; +import { AzureSession } from "../../typings/azure-account.api"; import { AzureUtilityManager } from '../azureUtilityManager'; import { AzureImage } from "./models/image"; import { Repository } from "./models/repository"; +//General helpers /** - * Developers can use this to visualize and list repositories on a given Registry. This is not a command, just a developer tool. - * @param registry : the registry whose repositories you want to see - * @returns allRepos : an array of Repository objects that exist within the given registry + * @param registry gets the subscription for a given registry + * @returns a subscription object */ -export async function getAzureRepositories(registry: Registry): Promise { - const allRepos: Repository[] = []; - let repo: Repository; - let azureAccount: AzureAccount = AzureUtilityManager.getInstance().getAccount(); - if (!azureAccount) { - return []; - } - const { accessToken, refreshToken } = await getRegistryTokens(registry); - if (accessToken && refreshToken) { - - await request.get('https://' + registry.loginServer + '/v2/_catalog', { - auth: { - bearer: accessToken - } - }, (err, httpResponse, body) => { - if (body.length > 0) { - const repositories = JSON.parse(body).repositories; - for (let tempRepo of repositories) { - repo = new Repository(registry, tempRepo, accessToken, refreshToken); - allRepos.push(repo); - } - } - }); - } - //Note these are ordered by default in alphabetical order - return allRepos; +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; } - -/** - * @param registry gets the registry - * @returns a string, the resource group name - */ export function getResourceGroupName(registry: Registry): any { return registry.id.slice(registry.id.search('resourceGroups/') + 'resourceGroups/'.length, registry.id.search('/providers/')); } -/** - * @param registry : the registry to get credentials for - * @returns : the updated refresh and access tokens which can be used to generate a header for an API call - */ -export async function getRegistryTokens(registry: Registry): Promise<{ refreshToken: any, accessToken: any }> { - const subscription = getRegistrySubscription(registry); - const tenantId: string = subscription.tenantId; - let azureAccount: AzureAccount = AzureUtilityManager.getInstance().getAccount(); - - const session: AzureSession = azureAccount.sessions.find((s, i, array) => s.tenantId.toLowerCase() === tenantId.toLowerCase()); - const { accessToken } = await acquireARMToken(session); - - //regenerates in case they have expired - if (accessToken) { - let refreshTokenACR; - let accessTokenACR; - - await request.post('https://' + registry.loginServer + '/oauth2/exchange', { - form: { - grant_type: 'access_token', - service: registry.loginServer, - tenant: tenantId, - access_token: accessToken - } - }, (err, httpResponse, body) => { - if (body.length > 0) { - refreshTokenACR = JSON.parse(body).refresh_token; - } else { - return; - } - }); - - await request.post('https://' + registry.loginServer + '/oauth2/token', { - form: { - grant_type: 'refresh_token', - service: registry.loginServer, - scope: 'registry:catalog:*', - refresh_token: refreshTokenACR - } - }, (err, httpResponse, body) => { - if (body.length > 0) { - accessTokenACR = JSON.parse(body).access_token; - } else { - return; - } - }); - if (refreshTokenACR && accessTokenACR) { - return { 'refreshToken': refreshTokenACR, 'accessToken': accessTokenACR }; +//Registry item management +/** List images under a specific Repository */ +export async function getImagesByRepository(element: Repository): Promise { + let allImages: AzureImage[] = []; + let image: AzureImage; + let tags: string[]; + const { acrAccessToken } = await acquireACRAccessTokenFromRegistry(element.registry, 'repository:' + element.name + ':pull'); + await request.get('https://' + element.registry.loginServer + '/v2/' + element.name + '/tags/list', { + auth: { + bearer: acrAccessToken + } + }, (err, httpResponse, body) => { + if (err) { throw (err) } + if (body.length > 0) { + tags = JSON.parse(body).tags; } - } - vscode.window.showErrorMessage('Could not generate tokens'); -} - -export async function acquireARMToken(localSession: AzureSession): Promise<{ accessToken: string; }> { - return new Promise<{ accessToken: string; }>((resolve, reject) => { - const credentials: any = localSession.credentials; - const environment: any = localSession.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; }): void { - if (err) { - reject(err); - } else { - resolve({ - accessToken: result.accessToken - }); - } - }); }); -} -/** Function used to create header for http request to acr */ -export function getAuthorizationHeader(username: string, password: string): string { - let auth; - if (username === '00000000-0000-0000-0000-000000000000') { - auth = 'Bearer ' + password; - } else { - auth = ('Basic ' + (encode(username + ':' + password).trim())); + for (let tag of tags) { + image = new AzureImage(element, tag); + allImages.push(image); } - return auth; -} - -/** - * First encodes to base 64, and then to latin1. See online documentation to see typescript encoding capabilities - * see https://nodejs.org/api/buffer.html#buffer_buf_tostring_encoding_start_end for details {Buffers and Character Encodings} - * current character encodings include: ascii, utf8, utf16le, ucs2, base64, latin1, binary, hex. Version v6.4.0 - * @param str : the string to encode for api URL purposes - */ -export function encode(str: string): string { - let bufferB64 = new Buffer(str); - let bufferLat1 = new Buffer(bufferB64.toString('base64')); - return bufferLat1.toString('latin1'); + return allImages; } - -/** - * Lots of https requests but they must be separate from getTokens because the forms are different - * @param element the repository where the desired images are - * @returns a list of AzureImage objects from the given repository (see azureUtils.ts) - */ -export async function getAzureImages(element: Repository): Promise { - let allImages: AzureImage[] = []; - let image: AzureImage; - let tags; - let azureAccount: AzureAccount = AzureUtilityManager.getInstance().getAccount(); - let tenantId: string = element.subscription.tenantId; - let refreshTokenACR; - let accessTokenACR; - const session: AzureSession = azureAccount.sessions.find((s, i, array) => s.tenantId.toLowerCase() === tenantId.toLowerCase()); - const { accessToken } = await acquireARMToken(session); - if (accessToken) { - await request.post('https://' + element.registry.loginServer + '/oauth2/exchange', { - form: { - grant_type: 'access_token', - service: element.registry.loginServer, - tenant: tenantId, - access_token: accessToken - } - }, (err, httpResponse, body) => { - if (body.length > 0) { - refreshTokenACR = JSON.parse(body).refresh_token; - } else { - return []; - } - }); - - await request.post('https://' + element.registry.loginServer + '/oauth2/token', { - form: { - grant_type: 'refresh_token', - service: element.registry.loginServer, - scope: 'repository:' + element.name + ':pull', - refresh_token: refreshTokenACR - } - }, (err, httpResponse, body) => { - if (body.length > 0) { - accessTokenACR = JSON.parse(body).access_token; - } else { - return []; - } - }); - - await request.get('https://' + element.registry.loginServer + '/v2/' + element.name + '/tags/list', { - auth: { - bearer: accessTokenACR - } - }, (err, httpResponse, body) => { - if (err) { return []; } - - if (body.length > 0) { - tags = JSON.parse(body).tags; +/** List repositories on a given Registry. */ +export async function getRepositoriesByRegistry(registry: Registry): Promise { + const allRepos: Repository[] = []; + let repo: Repository; + const { acrRefreshToken, acrAccessToken } = await acquireACRAccessTokenFromRegistry(registry, "registry:catalog:*"); + await request.get('https://' + registry.loginServer + '/v2/_catalog', { + auth: { + bearer: acrAccessToken + } + }, (err, httpResponse, body) => { + if (body.length > 0) { + const repositories = JSON.parse(body).repositories; + for (let tempRepo of repositories) { + repo = new Repository(registry, tempRepo, acrAccessToken, acrRefreshToken); + allRepos.push(repo); } - }); - - for (let tag of tags) { - image = new AzureImage(element, tag); - allImages.push(image); } - } - return allImages; -} - -/** Acquires login credentials for a registry in the form of refresh tokens and NULL_GUID - * @param subscription : the subscription the registry is on - * @param registry : the registry to get login credentials for - * @param context : if command is invoked through a right click on an AzureRepositoryNode. This context has a password and username - */ - -export async function acquireRegistryLoginCredential(subscription: SubscriptionModels.Subscription, registry: Registry, context?: AzureImageNode | AzureRepositoryNode): Promise<{ password: string, username: string }> { - let creds = await getRegistryTokens(registry); - let password = creds.refreshToken; - let username = '00000000-0000-0000-0000-000000000000'; - return { password, username }; -} + }); -export async function acquireRegistryAccessToken(subscription: SubscriptionModels.Subscription, registry: Registry, context?: AzureImageNode | AzureRepositoryNode): Promise<{ password: string, username: string }> { - let creds = await getRegistryTokens(registry); - let password = creds.accessToken; - let username = '00000000-0000-0000-0000-000000000000'; - return { password, username }; + //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, username: string, password: string): Promise { +export async function sendRequestToRegistry(http_method: string, login_server: string, path: string, bearerAccessToken: string): Promise { let url: string = `https://${login_server}${path}`; - let header = getAuthorizationHeader(username, password); + let header = 'Bearer ' + bearerAccessToken; let opt = { headers: { 'Authorization': header }, http_method: http_method, @@ -245,19 +88,72 @@ export async function sendRequestToRegistry(http_method: string, login_server: s } if (http_method === 'delete') { await request.delete(opt); - vscode.window.showInformationMessage('Successfully deleted item'); } } -/** - * @param registry gets the subscription for a given registry - * @returns a subscription object +//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 function getRegistrySubscription(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; +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, + }); + } + }); }); - return subscription; +} +/** 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/models/image.ts b/utils/Azure/models/image.ts index fd1617bf08..40f6c3f8b3 100644 --- a/utils/Azure/models/image.ts +++ b/utils/Azure/models/image.ts @@ -1,20 +1,14 @@ import { Registry } from 'azure-arm-containerregistry/lib/models'; import { SubscriptionModels } from 'azure-arm-resource'; -import { AzureAccount, AzureSession } from '../../../typings/azure-account.api'; import { Repository } from './repository'; -/** - * class Repository: used locally as of August 2018, primarily for functions within azureUtils.ts and new commands such as delete Repository - * accessToken can be used like a password, and the username can be '00000000-0000-0000-0000-000000000000' - */ +/** Class Azure Image: Used locally, Organizes data for managing images */ export class AzureImage { public registry: Registry; public repository: Repository; public tag: string; public subscription: SubscriptionModels.Subscription; public resourceGroupName: string; - public accessToken?: string; - public refreshToken?: string; public password?: string; public username?: string; @@ -24,8 +18,6 @@ export class AzureImage { this.tag = tag; this.subscription = repository.subscription; this.resourceGroupName = repository.resourceGroupName; - if (repository.accessToken) { this.accessToken = repository.accessToken; } - if (repository.refreshToken) { this.refreshToken = repository.refreshToken; } 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 index 16b541d36a..bbdda80206 100644 --- a/utils/Azure/models/repository.ts +++ b/utils/Azure/models/repository.ts @@ -1,35 +1,22 @@ import { Registry } from 'azure-arm-containerregistry/lib/models'; import { SubscriptionModels } from 'azure-arm-resource'; -import { AzureAccount, AzureSession } from '../../../typings/azure-account.api'; -import * as acrTools from '../../../utils/Azure/acrTools'; -/** - * class Repository: used locally as of August 2018, primarily for functions within azureUtils.ts and new commands such as delete Repository - * accessToken can be used like a password, and the username can be '00000000-0000-0000-0000-000000000000' - */ +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 accessToken?: string; - public refreshToken?: string; public password?: string; public username?: string; - constructor(registry: Registry, repository: string, accessToken?: string, refreshToken?: string, password?: string, username?: string) { + constructor(registry: Registry, repository: string, password?: string, username?: string) { this.registry = registry; this.resourceGroupName = acrTools.getResourceGroupName(registry); - this.subscription = acrTools.getRegistrySubscription(registry); + this.subscription = acrTools.getSubscriptionFromRegistry(registry); this.name = repository; - if (accessToken) { this.accessToken = accessToken; } - if (refreshToken) { this.refreshToken = refreshToken; } if (password) { this.password = password; } if (username) { this.username = username; } } - - public async setTokens(registry: Registry): Promise { - let tokens = await acrTools.getRegistryTokens(registry); - this.accessToken = tokens.accessToken; - this.refreshToken = tokens.refreshToken; - } } diff --git a/utils/azureUtilityManager.ts b/utils/azureUtilityManager.ts index fd4abae68c..48e65b939a 100644 --- a/utils/azureUtilityManager.ts +++ b/utils/azureUtilityManager.ts @@ -39,6 +39,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 {