Skip to content

Commit

Permalink
Azurecr/registry management (#383)
Browse files Browse the repository at this point in the history
* 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 126c01e.

* 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
  • Loading branch information
Esteban Rey authored and StephenWeatherford committed Aug 27, 2018
1 parent f935cb7 commit bc29acd
Show file tree
Hide file tree
Showing 17 changed files with 770 additions and 238 deletions.
54 changes: 54 additions & 0 deletions commands/azureCommands/create-registry.ts
Original file line number Diff line number Diff line change
@@ -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<Registry> {
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<string> {
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<string> {
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;
}
50 changes: 50 additions & 0 deletions commands/azureCommands/delete-image.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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();
}
}
}
33 changes: 33 additions & 0 deletions commands/azureCommands/delete-registry.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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();
}
}
42 changes: 42 additions & 0 deletions commands/azureCommands/delete-repository.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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();
}
}
}
174 changes: 174 additions & 0 deletions commands/utils/quick-pick-azure.ts
Original file line number Diff line number Diff line change
@@ -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<AzureImage> {
const placeHolder = prompt ? prompt : 'Select image to use';
const repoImages: AzureImage[] = await acrTools.getImagesByRepository(repository);
const imageListNames = repoImages.map(img => <IAzureQuickPickItem<AzureImage>>{ 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<Repository> {
const placeHolder = prompt ? prompt : 'Select repository to use';
const repositories: Repository[] = await acrTools.getRepositoriesByRegistry(registry);
const quickPickRepoList = repositories.map(repo => <IAzureQuickPickItem<Repository>>{ 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<Registry> {
const placeHolder = prompt ? prompt : 'Select registry to use';
let registries = await AzureUtilityManager.getInstance().getRegistries();
let quickPickRegList = registries.map(reg => <IAzureQuickPickItem<Registry | undefined>>{ label: reg.name, data: reg });

let createNewItem: IAzureQuickPickItem<Registry | undefined> = { label: '+ Create new registry', data: undefined };
if (canCreateNew) { quickPickRegList.unshift(createNewItem); }

let desiredReg: IAzureQuickPickItem<Registry | undefined> = await ext.ui.showQuickPick(quickPickRegList, {
'canPickMany': false,
'placeHolder': placeHolder
});
let registry: Registry;
if (desiredReg === createNewItem) {
registry = <Registry>await vscode.commands.executeCommand("vscode-docker.create-ACR-Registry");
} else {
registry = desiredReg.data;
}
return registry;
}

export async function quickPickSKU(): Promise<string> {
const quickPickSkuList = skus.map(sk => <IAzureQuickPickItem<string>>{ label: sk, data: sk });
let desiredSku: IAzureQuickPickItem<string> = await ext.ui.showQuickPick(quickPickSkuList, {
'canPickMany': false,
'placeHolder': 'Choose a SKU to use'
});
return desiredSku.data;
}

export async function quickPickSubscription(): Promise<Subscription> {
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 => <IAzureQuickPickItem<Subscription>>{ 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<string> {
let locations: Location[] = await AzureUtilityManager.getInstance().getLocationsBySubscription(subscription);
let quickPickLocList = locations.map(loc => <IAzureQuickPickItem<Location>>{ label: loc.displayName, data: loc });

quickPickLocList.sort((loc1, loc2): number => {
return loc1.data.displayName.localeCompare(loc2.data.displayName);
});

let desiredLocation: IAzureQuickPickItem<Location> = 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<ResourceGroup> {
let resourceGroups = await AzureUtilityManager.getInstance().getResourceGroups(subscription);
let quickPickResourceGroups = resourceGroups.map(res => <IAzureQuickPickItem<ResourceGroup | undefined>>{ label: res.name, data: res });

let createNewItem: IAzureQuickPickItem<ResourceGroup | undefined> = { label: '+ Create new resource group', data: undefined };
if (canCreateNew) { quickPickResourceGroups.unshift(createNewItem); }

let desiredResGroup: IAzureQuickPickItem<ResourceGroup | undefined> = 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<boolean> {
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<ResourceGroup> {
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<string> {
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;

}
6 changes: 6 additions & 0 deletions constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"];
Loading

0 comments on commit bc29acd

Please sign in to comment.