diff --git a/commands/azureCommands/create-registry.ts b/commands/azureCommands/create-registry.ts index 9c0ac8507d..e615ea8ad4 100644 --- a/commands/azureCommands/create-registry.ts +++ b/commands/azureCommands/create-registry.ts @@ -68,7 +68,7 @@ export async function createRegistry(): Promise { // INPUT HELPERS async function acquireSKU(): Promise { - let skus: string[] = ["Basic", "Standard", "Premium"]; + 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'); } diff --git a/explorer/models/azureRegistryNodes.ts b/explorer/models/azureRegistryNodes.ts index 410f81adc0..31d4cd5714 100644 --- a/explorer/models/azureRegistryNodes.ts +++ b/explorer/models/azureRegistryNodes.ts @@ -9,6 +9,7 @@ import { AsyncPool } from '../../utils/asyncpool'; import { MAX_CONCURRENT_REQUESTS } from '../../utils/constants' import { NodeBase } from './nodeBase'; import { RegistryType } from './registryType'; +import { TaskRootNode } from './taskNode'; export class AzureRegistryNode extends NodeBase { private _azureAccount: AzureAccount; @@ -38,9 +39,17 @@ export class AzureRegistryNode extends NodeBase { } } - public async getChildren(element: AzureRegistryNode): Promise { - const repoNodes: AzureRepositoryNode[] = []; - let node: AzureRepositoryNode; + public async getChildren(element: AzureRegistryNode): Promise { + const registryChildNodes: NodeBase[] = []; + + let iconPath = { + light: path.join(__filename, '..', '..', '..', '..', 'images', 'light', 'buildTasks_light.svg'), + dark: path.join(__filename, '..', '..', '..', '..', 'images', 'dark', 'buildTasks_dark.svg') + }; + + //Pushing single TaskRootNode under the current registry. All the following nodes added to registryNodes are type AzureRepositoryNode + let taskNode = new TaskRootNode("Build Tasks", "taskRootNode", element.subscription, element.azureAccount, element.registry, iconPath); + registryChildNodes.push(taskNode); const tenantId: string = element.subscription.tenantId; if (!this._azureAccount) { @@ -50,6 +59,8 @@ export class AzureRegistryNode extends NodeBase { const session: AzureSession = this._azureAccount.sessions.find((s, i, array) => s.tenantId.toLowerCase() === tenantId.toLowerCase()); const { accessToken, refreshToken } = await acquireToken(session); + let node: AzureRepositoryNode; + if (accessToken && refreshToken) { let refreshTokenARC; let accessTokenARC; @@ -91,9 +102,8 @@ export class AzureRegistryNode extends NodeBase { }, (err, httpResponse, body) => { if (body.length > 0) { const repositories = JSON.parse(body).repositories; - // tslint:disable-next-line:prefer-for-of // Grandfathered in - for (let i = 0; i < repositories.length; i++) { - node = new AzureRepositoryNode(repositories[i], "azureRepositoryNode"); + for (let repo of repositories) { + node = new AzureRepositoryNode(repo, "azureRepositoryNode"); node.accessTokenARC = accessTokenARC; node.azureAccount = element.azureAccount; node.password = element.password; @@ -102,13 +112,13 @@ export class AzureRegistryNode extends NodeBase { node.repository = element.label; node.subscription = element.subscription; node.userName = element.userName; - repoNodes.push(node); + registryChildNodes.push(node); } } }); } //Note these are ordered by default in alphabetical order - return repoNodes; + return registryChildNodes; } } @@ -117,7 +127,7 @@ export class AzureRepositoryNode extends NodeBase { constructor( public readonly label: string, public readonly contextValue: string, - public readonly iconPath: { light: string | vscode.Uri; dark: string | vscode.Uri } = { + public readonly iconPath: AzureRegistryNode["iconPath"] = { light: path.join(__filename, '..', '..', '..', '..', 'images', 'light', 'Repository_16x.svg'), dark: path.join(__filename, '..', '..', '..', '..', 'images', 'dark', 'Repository_16x.svg') } @@ -199,10 +209,9 @@ export class AzureRepositoryNode extends NodeBase { }); const pool = new AsyncPool(MAX_CONCURRENT_REQUESTS); - // tslint:disable-next-line:prefer-for-of // Grandfathered in - for (let i = 0; i < tags.length; i++) { + for (let tag of tags) { pool.addTask(async () => { - let data = await request.get('https://' + element.repository + '/v2/' + element.label + `/manifests/${tags[i]}`, { + let data = await request.get('https://' + element.repository + '/v2/' + element.label + `/manifests/${tag}`, { auth: { bearer: accessTokenARC } @@ -210,7 +219,7 @@ export class AzureRepositoryNode extends NodeBase { //Acquires each image's manifest to acquire build time. let manifest = JSON.parse(data); - node = new AzureImageNode(`${element.label}:${tags[i]}`, 'azureImageNode'); + node = new AzureImageNode(`${element.label}:${tag}`, 'azureImageNode'); node.azureAccount = element.azureAccount; node.password = element.password; node.registry = element.registry; @@ -296,7 +305,7 @@ async function acquireToken(session: AzureSession): Promise<{ accessToken: strin const credentials: any = session.credentials; const environment: any = session.environment; // tslint:disable-next-line:no-function-expression // Grandfathered in - credentials.context.acquireToken(environment.activeDirectoryResourceId, credentials.username, credentials.clientId, function (err: any, result: { accessToken: string; refreshToken: string; }): void { + credentials.context.acquireToken(environment.activeDirectoryResourceId, credentials.username, credentials.clientId, function (err: any, result: any): any { if (err) { reject(err); } else { diff --git a/explorer/models/registryRootNode.ts b/explorer/models/registryRootNode.ts index 4456dcfebd..649b993dcc 100644 --- a/explorer/models/registryRootNode.ts +++ b/explorer/models/registryRootNode.ts @@ -9,6 +9,7 @@ import * as ContainerModels from '../../node_modules/azure-arm-containerregistry import * as ContainerOps from '../../node_modules/azure-arm-containerregistry/lib/operations'; import { AzureAccount, AzureSession } from '../../typings/azure-account.api'; import { AsyncPool } from '../../utils/asyncpool'; +import * as acrTools from '../../utils/Azure/acrTools'; import { MAX_CONCURRENT_REQUESTS, MAX_CONCURRENT_SUBSCRIPTON_REQUESTS } from '../../utils/constants' import * as dockerHub from '../utils/dockerHubUtils' import { getCoreNodeModule } from '../utils/utils'; @@ -158,8 +159,10 @@ export class RegistryRootNode extends NodeBase { //Go through the registries and add them to the async pool // tslint:disable-next-line:prefer-for-of // Grandfathered in for (let j = 0; j < registries.length; j++) { + if (registries[j].adminUserEnabled && !registries[j].sku.tier.includes('Classic')) { - const resourceGroup: string = registries[j].id.slice(registries[j].id.search('resourceGroups/') + 'resourceGroups/'.length, registries[j].id.search('/providers/')); + const resourceGroup: string = acrTools.getResourceGroup(registries[j]); + regPool.addTask(async () => { let creds = await client.registries.listCredentials(resourceGroup, registries[j].name); let iconPath = { diff --git a/explorer/models/rootNode.ts b/explorer/models/rootNode.ts index a14001f48f..e9bb05ac31 100644 --- a/explorer/models/rootNode.ts +++ b/explorer/models/rootNode.ts @@ -53,7 +53,6 @@ export class RootNode extends NodeBase { // clearInterval(this._imageDebounceTimer); // return; // } - clearInterval(this._imageDebounceTimer); if (refreshInterval > 0) { @@ -70,13 +69,9 @@ export class RootNode extends NodeBase { if (this._imageCache.length !== images.length) { needToRefresh = true; } else { - // tslint:disable-next-line:prefer-for-of // Grandfathered in - for (let i: number = 0; i < this._imageCache.length; i++) { - let before: string = JSON.stringify(this._imageCache[i]); - // tslint:disable-next-line:prefer-for-of // Grandfathered in - for (let j: number = 0; j < images.length; j++) { - let after: string = JSON.stringify(images[j]); - if (before === after) { + for (let cachedImage of this._imageCache) { + for (let image of images) { + if (image === cachedImage) { found = true; break; } @@ -108,18 +103,21 @@ export class RootNode extends NodeBase { } - public async getChildren(element: NodeBase): Promise { - - if (element.contextValue === 'imagesRootNode') { - return this.getImages(); - } - if (element.contextValue === 'containersRootNode') { - return this.getContainers(); - } - if (element.contextValue === 'registriesRootNode') { - return this.getRegistries() + public async getChildren(element: RootNode): Promise { + switch (element.contextValue) { + case 'imagesRootNode': { + return this.getImages(); + } + case 'containersRootNode': { + return this.getContainers(); + } + case 'registriesRootNode': { + return this.getRegistries(); + } + default: { + break; + } } - } private async getImages(): Promise { @@ -132,19 +130,15 @@ export class RootNode extends NodeBase { return []; } - // tslint:disable-next-line:prefer-for-of // Grandfathered in - for (let i = 0; i < images.length; i++) { - // tslint:disable-next-line:prefer-for-of // Grandfathered in - if (!images[i].RepoTags) { + for (let image of images) { + if (!image.RepoTags) { let node = new ImageNode(`:`, "localImageNode", this.eventEmitter); - node.imageDesc = images[i]; + node.imageDesc = image; imageNodes.push(node); } else { - // tslint:disable-next-line:prefer-for-of // Grandfathered in - for (let j = 0; j < images[i].RepoTags.length; j++) { - // tslint:disable-next-line:prefer-for-of // Grandfathered in - let node = new ImageNode(`${images[i].RepoTags[j]}`, "localImageNode", this.eventEmitter); - node.imageDesc = images[i]; + for (let repoTag of image.RepoTags) { + let node = new ImageNode(`${repoTag}`, "localImageNode", this.eventEmitter); + node.imageDesc = image; imageNodes.push(node); } } @@ -168,7 +162,6 @@ export class RootNode extends NodeBase { // clearInterval(this._containerDebounceTimer); // return; // } - clearInterval(this._containerDebounceTimer); if (refreshInterval > 0) { @@ -186,15 +179,13 @@ export class RootNode extends NodeBase { if (this._containerCache.length !== containers.length) { needToRefresh = true; } else { - // tslint:disable-next-line:prefer-for-of // Grandfathered in - for (let i = 0; i < this._containerCache.length; i++) { - let ctr: Docker.ContainerDesc = this._containerCache[i]; - // tslint:disable-next-line:prefer-for-of // Grandfathered in - for (let j = 0; j < containers.length; j++) { + for (let cachedContainer of this._containerCache) { + let ctr: Docker.ContainerDesc = cachedContainer; + for (let cont of containers) { // can't do a full object compare because "Status" keeps changing for running containers - if (ctr.Id === containers[j].Id && - ctr.Image === containers[j].Image && - ctr.State === containers[j].State) { + if (ctr.Id === cont.Id && + ctr.Image === cont.Image && + ctr.State === cont.State) { found = true; break; } @@ -230,9 +221,8 @@ export class RootNode extends NodeBase { return []; } - // tslint:disable-next-line:prefer-for-of // Grandfathered in - for (let i = 0; i < containers.length; i++) { - if (['exited', 'dead'].includes(containers[i].State)) { + for (let container of containers) { + if (['exited', 'dead'].includes(container.State)) { contextValue = "stoppedLocalContainerNode"; iconPath = { light: path.join(__filename, '..', '..', '..', '..', 'images', 'light', 'stoppedContainer.svg'), @@ -246,8 +236,8 @@ export class RootNode extends NodeBase { }; } - let containerNode: ContainerNode = new ContainerNode(`${containers[i].Image} (${containers[i].Names[0].substring(1)}) (${containers[i].Status})`, contextValue, iconPath); - containerNode.containerDesc = containers[i]; + let containerNode: ContainerNode = new ContainerNode(`${container.Image} (${container.Names[0].substring(1)}) (${container.Status})`, contextValue, iconPath); + containerNode.containerDesc = container; containerNodes.push(containerNode); } diff --git a/explorer/models/taskNode.ts b/explorer/models/taskNode.ts new file mode 100644 index 0000000000..a3ddc9059d --- /dev/null +++ b/explorer/models/taskNode.ts @@ -0,0 +1,67 @@ +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 { AzureCredentialsManager } from '../../utils/azureCredentialsManager'; +import { NodeBase } from './nodeBase'; + +/* Single TaskRootNode under each Repository. Labeled "Build Tasks" */ +export class TaskRootNode extends NodeBase { + constructor( + public readonly label: string, + public readonly contextValue: string, + public subscription: SubscriptionModels.Subscription, + public readonly azureAccount: AzureAccount, + public registry: ContainerModels.Registry, + public readonly iconPath: any = {} + ) { + super(label); + } + + public name: string; + + public getTreeItem(): vscode.TreeItem { + return { + label: this.label, + collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, + contextValue: this.contextValue, + iconPath: this.iconPath + } + } + + /* Making a list view of BuildTaskNodes, or the Build Tasks of the current registry */ + public async getChildren(element: TaskRootNode): Promise { + const buildTaskNodes: BuildTaskNode[] = []; + let buildTasks: ContainerModels.BuildTask[] = []; + + const client = AzureCredentialsManager.getInstance().getContainerRegistryManagementClient(element.subscription); + const resourceGroup: string = acrTools.getResourceGroup(element.registry); + + buildTasks = await client.buildTasks.list(resourceGroup, element.registry.name); + if (buildTasks.length === 0) { + vscode.window.showInformationMessage(`You do not have any Build Tasks in the registry, '${element.registry.name}'. You can create one with ACR Build. `, "Learn More").then(val => { + if (val === "Learn More") { + opn('https://aka.ms/acr/buildtask'); + } + }) + } + + for (let buildTask of buildTasks) { + let node = new BuildTaskNode(buildTask.name, "buildTaskNode"); + buildTaskNodes.push(node); + } + return buildTaskNodes; + } +} + +export class BuildTaskNode extends NodeBase { + + constructor( + public readonly label: string, + public readonly contextValue: string, + ) { + super(label); + } +} diff --git a/images/dark/buildTasks_dark.svg b/images/dark/buildTasks_dark.svg new file mode 100644 index 0000000000..618310ec7c --- /dev/null +++ b/images/dark/buildTasks_dark.svg @@ -0,0 +1 @@ +Manufacture_16x \ No newline at end of file diff --git a/images/light/buildTasks_light.svg b/images/light/buildTasks_light.svg new file mode 100644 index 0000000000..27f50db2ce --- /dev/null +++ b/images/light/buildTasks_light.svg @@ -0,0 +1 @@ +Manufacture_16x \ No newline at end of file diff --git a/utils/Azure/acrTools.ts b/utils/Azure/acrTools.ts index 796113d4bc..f31653e39a 100644 --- a/utils/Azure/acrTools.ts +++ b/utils/Azure/acrTools.ts @@ -7,7 +7,6 @@ import { AzureAccount, AzureSession } from "../../typings/azure-account.api"; import { AzureImage } from "../Azure/models/image"; import { Repository } from "../Azure/models/Repository"; import { AzureUtilityManager } from '../azureUtilityManager'; -const teleCmdId: string = 'vscode-docker.deleteAzureImage'; /** * Developers can use this to visualize and list repositories on a given Registry. This is not a command, just a developer tool. @@ -42,6 +41,15 @@ export async function getAzureRepositories(registry: Registry): Promise