Skip to content

Commit

Permalink
Merge pull request #22 from AzureCR/Esteban-Julia-Rutu/CreateRegistry
Browse files Browse the repository at this point in the history
Create Registry
  • Loading branch information
rsamai authored Aug 2, 2018
2 parents bc905d8 + 048eb67 commit 37d8a27
Show file tree
Hide file tree
Showing 6 changed files with 263 additions and 16 deletions.
217 changes: 217 additions & 0 deletions commands/azureCommands/create-registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@

import { ContainerRegistryManagementClient } from 'azure-arm-containerregistry';
import { RegistryNameStatus } from "azure-arm-containerregistry/lib/models";
import { ResourceManagementClient, SubscriptionModels } from 'azure-arm-resource';
import { ResourceGroup } from "azure-arm-resource/lib/resource/models";
import * as vscode from "vscode";
import { reporter } from '../../telemetry/telemetry';
import { AzureCredentialsManager } from '../../utils/azureCredentialsManager';
const teleAzureId: string = 'vscode-docker.create.registry.azureContainerRegistry';
const teleCmdId: string = 'vscode-docker.createRegistry';
import * as opn from 'opn';

/* Creates a new registry based on user input/selection of features, such as location */
export async function createRegistry(): Promise<void> {
let subscription: SubscriptionModels.Subscription;
let resourceGroup: ResourceGroup;
let location: string;

try {
subscription = await acquireSubscription();
resourceGroup = await acquireResourceGroup(subscription);

} catch (error) {
return;
}
const client = AzureCredentialsManager.getInstance().getContainerRegistryManagementClient(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
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
});
}
}

}

// INPUT HELPERS
async function acquireSKU(): Promise<string> {
let skus: string[] = ["Basic", "Standard", "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;
}

async function acquireRegistryName(client: ContainerRegistryManagementClient): Promise<string> {
let opt: vscode.InputBoxOptions = {
ignoreFocusOut: false,
prompt: 'Registry name? '
};
let registryName: string = await vscode.window.showInputBox(opt);

let registryStatus: RegistryNameStatus = await client.registries.checkNameAvailability({ 'name': registryName });
while (!registryStatus.nameAvailable) {
opt = {
ignoreFocusOut: false,
prompt: `The registry name '${registryName}' is unavailable. Try again: `
}
registryName = await vscode.window.showInputBox(opt);

if (registryName === undefined) { throw new Error('user Exit'); }
registryStatus = await client.registries.checkNameAvailability({ 'name': registryName });
}
return registryName;
}

async function acquireSubscription(): Promise<SubscriptionModels.Subscription> {
const subs = AzureCredentialsManager.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<string> {
let locations: SubscriptionModels.Location[] = await AzureCredentialsManager.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<ResourceGroup> {
//Acquire each subscription's data simultaneously
let resourceGroup;
let resourceGroupName;
const resourceGroupClient = new ResourceManagementClient(AzureCredentialsManager.getInstance().getCredentialByTenantId(subscription.tenantId), subscription.subscriptionId);
let resourceGroups = await AzureCredentialsManager.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 AzureCredentialsManager.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<string> {
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;
}
3 changes: 2 additions & 1 deletion dockerExtension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { DockerDebugConfigProvider } from './configureWorkspace/configDebugProvi
import { configure } from './configureWorkspace/configure';
import { DockerComposeCompletionItemProvider } from './dockerCompose/dockerComposeCompletionItemProvider';
import { DockerComposeHoverProvider } from './dockerCompose/dockerComposeHoverProvider';
import { createRegistry } from './commands/azureCommands/create-registry';
import composeVersionKeys from './dockerCompose/dockerComposeKeyInfo';
import { DockerComposeParser } from './dockerCompose/dockerComposeParser';
import { DockerfileCompletionItemProvider } from './dockerfile/dockerfileCompletionItemProvider';
Expand Down Expand Up @@ -116,7 +117,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
ctx.subscriptions.push(vscode.commands.registerCommand('vscode-docker.compose.down', composeDown));
ctx.subscriptions.push(vscode.commands.registerCommand('vscode-docker.compose.restart', composeRestart));
ctx.subscriptions.push(vscode.commands.registerCommand('vscode-docker.system.prune', systemPrune));

ctx.subscriptions.push(vscode.commands.registerCommand('vscode-docker.createRegistry', createRegistry));
ctx.subscriptions.push(vscode.commands.registerCommand('vscode-docker.createWebApp', async (context?: AzureImageNode | DockerHubImageNode) => {
if (context) {
if (azureAccount) {
Expand Down
12 changes: 12 additions & 0 deletions explorer/utils/azureUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,15 @@ 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);
}*/

}
10 changes: 10 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"onCommand:vscode-docker.compose.restart",
"onCommand:vscode-docker.configure",
"onCommand:vscode-docker.createWebApp",
"onCommand:vscode-docker.createRegistry",
"onCommand:vscode-docker.system.prune",
"onCommand:vscode-docker.dockerHubLogout",
"onCommand:vscode-docker.browseDockerHub",
Expand Down Expand Up @@ -247,6 +248,10 @@
"command": "vscode-docker.createWebApp",
"when": "view == dockerExplorer && viewItem == dockerHubImageTag"
},
{
"command": "vscode-docker.createRegistry",
"when": "view == dockerExplorer && viewItem == azureRegistryRootNode"
},
{
"command": "vscode-docker.dockerHubLogout",
"when": "view == dockerExplorer && viewItem == dockerHubRootNode"
Expand Down Expand Up @@ -567,6 +572,11 @@
"description": "Restarts a composition of containers",
"category": "Docker"
},
{
"command": "vscode-docker.createRegistry",
"title": "Create Registry",
"category": "Docker"
},
{
"command": "vscode-docker.image.push",
"title": "Push",
Expand Down
2 changes: 1 addition & 1 deletion typings/vscode-extension-telemetry.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ declare module 'vscode-extension-telemetry' {
sendTelemetryEvent(eventName: string, properties?: { [key: string]: string }, measures?: { [key: string]: number }): void;
dispose();
}
}
}
35 changes: 21 additions & 14 deletions utils/azureCredentialsManager.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { SubscriptionClient, ResourceManagementClient, SubscriptionModels } from 'azure-arm-resource';
import { AzureAccount } from '../typings/azure-account.api';
import { ServiceClientCredentials } from 'ms-rest';
import { AsyncPool } from '../utils/asyncpool';
import { ContainerRegistryManagementClient } from 'azure-arm-containerregistry';
import * as ContainerModels from '../node_modules/azure-arm-containerregistry/lib/models';
import { ResourceManagementClient, SubscriptionClient, SubscriptionModels } from 'azure-arm-resource';
import { ResourceGroup, ResourceGroupListResult } from "azure-arm-resource/lib/resource/models";
import { ServiceClientCredentials } from 'ms-rest';
import * as ContainerModels from '../node_modules/azure-arm-containerregistry/lib/models';
import { AzureAccount } from '../typings/azure-account.api';
import { AsyncPool } from '../utils/asyncpool';
import { MAX_CONCURRENT_SUBSCRIPTON_REQUESTS } from './constants';

/* Singleton for facilitating communication with Azure account services by providing extended shared
Expand All @@ -28,14 +28,14 @@ export class AzureCredentialsManager {
}

//This function has to be called explicitly before using the singleton.
public setAccount(azureAccount) {
public setAccount(azureAccount: AzureAccount): void {
this.azureAccount = azureAccount;
}

//GETTERS
public getAccount() {
if (this.azureAccount) return this.azureAccount;
throw ('Azure account is not present, you may have forgotten to call setAccount');
public getAccount(): AzureAccount {
if (this.azureAccount) { return this.azureAccount; }
throw new Error(('Azure account is not present, you may have forgotten to call setAccount'));
}

public getFilteredSubscriptionList(): SubscriptionModels.Subscription[] {
Expand All @@ -61,7 +61,7 @@ export class AzureCredentialsManager {
return new ResourceManagementClient(this.getCredentialByTenantId(subscription.tenantId), subscription.subscriptionId);
}

public async getRegistries(subscription?: SubscriptionModels.Subscription, resourceGroup?: string, sortFunction?): Promise<ContainerModels.Registry[]> {
public async getRegistries(subscription?: SubscriptionModels.Subscription, resourceGroup?: string, sortFunction?: any): Promise<ContainerModels.Registry[]> {
let registries: ContainerModels.Registry[] = [];

if (subscription && resourceGroup) {
Expand All @@ -79,9 +79,9 @@ export class AzureCredentialsManager {
const subs: SubscriptionModels.Subscription[] = this.getFilteredSubscriptionList();
const subPool = new AsyncPool(MAX_CONCURRENT_SUBSCRIPTON_REQUESTS);

for (let i = 0; i < subs.length; i++) {
for (let sub of subs) {
subPool.addTask(async () => {
const client = this.getContainerRegistryManagementClient(subs[i]);
const client = this.getContainerRegistryManagementClient(sub);
let subscriptionRegistries: ContainerModels.Registry[] = await client.registries.list();
registries = registries.concat(subscriptionRegistries);
});
Expand All @@ -105,9 +105,9 @@ export class AzureCredentialsManager {
const subPool = new AsyncPool(MAX_CONCURRENT_SUBSCRIPTON_REQUESTS);
let resourceGroups: ResourceGroup[] = [];
//Acquire each subscription's data simultaneously
for (let i = 0; i < subs.length; i++) {
for (let sub of subs) {
subPool.addTask(async () => {
const resourceClient = this.getResourceManagementClient(subs[i]);
const resourceClient = this.getResourceManagementClient(sub);
const internalGroups = await resourceClient.resourceGroups.list();
resourceGroups = resourceGroups.concat(internalGroups);
});
Expand All @@ -127,6 +127,13 @@ export class AzureCredentialsManager {
throw new Error(`Failed to get credentials, tenant ${tenantId} not found.`);
}

public async getLocationsBySubscription(subscription: SubscriptionModels.Subscription): Promise<SubscriptionModels.Location[]> {
const credential = this.getCredentialByTenantId(subscription.tenantId);
const client = new SubscriptionClient(credential);
const locations = <SubscriptionModels.Location[]>(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 isLoggedIn(): Promise<boolean> {
Expand Down

0 comments on commit 37d8a27

Please sign in to comment.