From 94a7ed2e0e66d5c16619ccddd37ec77e452c43ac Mon Sep 17 00:00:00 2001 From: Rodrigo Mendoza <43052640+rosanch@users.noreply.github.com> Date: Sat, 17 Nov 2018 15:09:45 -0800 Subject: [PATCH] merge cleanup (#617) * 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 * copied previous push to acr into new pull-from-azure.ts file * Estebanreyl/dev merge fixes (#43) * Merge fixes to acquire latest telemetry items * Updated to master AzureUtilityManager * added acrbuild stuff * added acrbuild stuff * Update to match utility manager class * 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 * Added quick pick for selecting resource group and registry * clean * Added subscription support * utility bug fix * 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 * Made loginCredentials method in acrTools more efficient, added Pull Image from Azure option, temporary fix for no registries * added folder select * Split the loginCredentials function, added docker login and docker pull and telemetry actions * Finished pull from azure right click feature * deleted push to azure * removed username and password from Azure Registry Node * Clean up * copied previous push to acr into new pull-from-azure.ts file * Made loginCredentials method in acrTools more efficient, added Pull Image from Azure option, temporary fix for no registries * Split the loginCredentials function, added docker login and docker pull and telemetry actions * Finished pull from azure right click feature * deleted push to azure * Clean up * Working again after rebasing and resolving merge conflicts * Updates and fixes * Fixes from PR comments -renamed loginCredentials functions -added await to docker commands in image pull -cleanup * uncapitalize AzureRegistryNode * Refactoring, prod. * Flexible OSType * 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 * Jackson/quick build dev (#46) * added acrbuild stuff * added acrbuild stuff * Update to match utility manager class * Added quick pick for selecting resource group and registry * clean * Added subscription support * utility bug fix * added folder select * Updates and fixes * Refactoring, prod. * Flexible OSType * added ID * Move build to azure commands * cleanup * Relative dockerfile support * Removed await, updating list in enumeration * fixed chdir * flexible ostype * Rutusamai/Show Build Task properties (#70) * Show build task works through input bar * added run build task * Right click not working on BuildTaskNode. Works through command palette * quick fixes * quick fix to enable context menu on buildTaskNode * comamnd not sending via right click * working from right click now. trying to do show in json * Acquire properties in JSON format internally, works * Now shows organized task jsons on right click * import * to do * issues with getImagesByRepository * try catch build step * acrTools * small refactor * Show build task works through input bar * added run build task * Right click not working on BuildTaskNode. Works through command palette * quick fixes * quick fix to enable context menu on buildTaskNode * comamnd not sending via right click * working from right click now. trying to do show in json * Acquire properties in JSON format internally, works * Now shows organized task jsons on right click * import * to do * issues with getImagesByRepository * try catch build step * acrTools * small refactor * Show is working- final * removed run build task stuff * cleanup * addressed esteban's comments * buildTask = context.task.name rather than label * fixes from Bin's comments * Merge branch 'dev' of https://github.com/AzureCR/vscode-docker into master4 * merge * missed small changes * Rutusamai/Run a Build Task (#71) * Show build task works through input bar * added run build task * Right click not working on BuildTaskNode. Works through command palette * quick fixes * quick fix to enable context menu on buildTaskNode * comamnd not sending via right click * working from right click now. trying to do show in json * Acquire properties in JSON format internally, works * Now shows organized task jsons on right click * import * to do * issues with getImagesByRepository * try catch build step * acrTools * small refactor * Show build task works through input bar * added run build task * Right click not working on BuildTaskNode. Works through command palette * quick fixes * quick fix to enable context menu on buildTaskNode * comamnd not sending via right click * working from right click now. trying to do show in json * Acquire properties in JSON format internally, works * Now shows organized task jsons on right click * import * to do * issues with getImagesByRepository * try catch build step * acrTools * small refactor * Show is working- final * run is working for right click * run build task works through command pallette. added success notification * removed show build task * cleanup- matched quickpick buils task and tasknode withshow build task branch * removed showTaskManager * spacing * outputChannel and null icon * merge to update with dev * Estebanreyl/build logs final (#72) * "Merge branch 'estebanreyl/buildlogsWithAccesibility' of https://github.com/AzureCR/vscode-docker into estebanreyl/buildlogsWithAccesibility" This reverts commit e645cbf5058c9bc838ee41a67cd6c5aee9945702, reversing changes made to fc4a477220c7819c9249557ba0d7570f5d0c1e0c. * Refactored and organized log view into readable components. * moved storage * Began incorporating icons * Added icons * Further standarization * added logs * Added download log capabilities * Updated Copy * Updated inner div size alignment * Added resizing script * header sort by is only clickable on text now * fixed header table * Fix minor display issues * Identified filtering strategy * Begin adding sorting * Merge with dev * Added proper filtering * Nit loading improvements * Added accesibility, key only behaviour * accesibility retouches and enable right click from tasknode * merges * fix module name * Adds streaming and command standarization (ext.ui) (#73) * Adds streaming and command standarization (ext.ui) * removed unecessary append lines * small fixes * Fix merge issues * changes for ACR 3.0.0 (#80) * This commit contains changes necessary for the azure-arm-containerregistry 3.0.0 * Fixed PR feedback * Run task fixed. Issue ID: 79 * missing changes added * Fixing ACR run logs for Images * Sajaya/top1 (#83) * Query only 1 record for runs * View Azure logs * Refactoring build to run and buildTask to task * Removed filter for top (#88) * adding run yaml file * Refactoring to run task file. * fixing logs filter for images * Last Update time Fixed * Cleanup + refactoring delete image to untag image * Adding delete ACR Image (delete digest) * Changing text promt on right click to run ACR task file * Update settings.json * minor PR review fixes 1 * PR fixes 1 * Missed: change any to string * merge clean up * Schedule run code reduction + minor improvements * ACR request Improvements * Minor grammar fixes --- .../acr-logs-utils/tableDataManager.ts | 33 ++--- commands/azureCommands/delete-image.ts | 71 ++++++++-- commands/azureCommands/delete-repository.ts | 16 +-- commands/azureCommands/quick-build.ts | 90 +----------- commands/azureCommands/run-task.ts | 7 + commands/build-image.ts | 22 +-- commands/utils/SourceArchiveUtility.ts | 114 +++++++++++++++ commands/utils/quick-pick-azure.ts | 4 +- commands/utils/quick-pick-image.ts | 16 ++- dockerExtension.ts | 11 +- explorer/models/commonRegistryUtils.ts | 2 +- explorer/models/rootNode.ts | 3 - package.json | 37 ++++- utils/Azure/acrTools.ts | 131 ++++++++++++++---- utils/Azure/models/image.ts | 14 +- 15 files changed, 375 insertions(+), 196 deletions(-) create mode 100644 commands/utils/SourceArchiveUtility.ts diff --git a/commands/azureCommands/acr-logs-utils/tableDataManager.ts b/commands/azureCommands/acr-logs-utils/tableDataManager.ts index 354523d7b4..4ddf46b6db 100644 --- a/commands/azureCommands/acr-logs-utils/tableDataManager.ts +++ b/commands/azureCommands/acr-logs-utils/tableDataManager.ts @@ -2,8 +2,10 @@ import ContainerRegistryManagementClient from "azure-arm-containerregistry"; import { Registry, Run, RunGetLogResult, RunListResult } from "azure-arm-containerregistry/lib/models"; import vscode = require('vscode'); import { parseError } from "vscode-azureextensionui"; -import { ext } from "../../../extensionVariables"; -import { acquireACRAccessTokenFromRegistry } from "../../../utils/Azure/acrTools"; +import { getImageDigest } from "../../../utils/Azure/acrTools"; +import { AzureImage } from "../../../utils/Azure/models/image"; +import { Repository } from "../../../utils/Azure/models/repository"; + /** Class to manage data and data acquisition for logs */ export class LogData { public registry: Registry; @@ -121,29 +123,14 @@ export class LogData { parsedFilter = `TaskName eq '${filter.task}'`; } else if (filter.image) { //Image let items: string[] = filter.image.split(':') - const { acrAccessToken } = await acquireACRAccessTokenFromRegistry(this.registry, 'repository:' + items[0] + ':pull'); - let digest = await new Promise((resolve, reject) => ext.request.get('https://' + this.registry.loginServer + `/v2/${items[0]}/manifests/${items[1]}`, { - auth: { - bearer: acrAccessToken - }, - headers: { - accept: 'application/vnd.docker.distribution.manifest.v2+json; 0.5, application/vnd.docker.distribution.manifest.list.v2+json; 0.6' - } - }, (err, httpResponse, body) => { - if (err) { - reject(err); - } else { - const imageDigest = httpResponse.headers['docker-content-digest']; - if (imageDigest instanceof Array) { - reject(new Error('docker-content-digest should be a string not an array.')) - } else { - resolve(imageDigest); - } - } - })); + if (items.length !== 2) { + throw new Error('Wrong format: It should be :'); + } + const image = new AzureImage(await Repository.Create(this.registry, items[0]), items[1]); + const imageDigest: string = await getImageDigest(image); if (parsedFilter.length > 0) { parsedFilter += ' and '; } - parsedFilter += `contains(OutputImageManifests, '${items[0]}@${digest}')`; + parsedFilter += `contains(OutputImageManifests, '${image.repository.name}@${imageDigest}')`; } return parsedFilter; } diff --git a/commands/azureCommands/delete-image.ts b/commands/azureCommands/delete-image.ts index 46296fdb6f..a92a3b8576 100644 --- a/commands/azureCommands/delete-image.ts +++ b/commands/azureCommands/delete-image.ts @@ -11,37 +11,80 @@ 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 { getLoginServer } from "../../utils/nonNull"; import * as quickPicks from '../utils/quick-pick-azure'; +/** Function to untag 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 untagAzureImage(context?: AzureImageTagNode): Promise { + let registry: Registry; + let repo: Repository; + let image: AzureImage; + + if (!context) { + registry = await quickPicks.quickPickACRRegistry(); + repo = await quickPicks.quickPickACRRepository(registry, `Select the repository of the image you want to untag`); + image = await quickPicks.quickPickACRImage(repo, `Select the image you want to untag`); + + } else { + registry = context.registry; + let wholeName: string[] = context.label.split(':'); + repo = await Repository.Create(registry, wholeName[0]); + image = new AzureImage(repo, wholeName[1]); + } + + const shouldDelete = await ext.ui.showWarningMessage( + `Are you sure you want to untag '${image.toString()}'? This does not delete the manifest referenced by the tag.`, + { modal: true }, + DialogResponses.deleteResponse, + DialogResponses.cancel); + + if (shouldDelete === DialogResponses.deleteResponse) { + await acrTools.untagImage(image); + vscode.window.showInformationMessage(`Successfully untagged '${image.toString()}'`); + + if (context) { + dockerExplorerProvider.refreshNode(context.parent); + } else { + dockerExplorerProvider.refreshRegistries(); + } + } +} + /** Function to delete an Azure hosted image * @param context : if called through right click on AzureImageNode, the node object will be passed in. See azureRegistryNodes.ts for more info */ export async function deleteAzureImage(context?: AzureImageTagNode): Promise { let registry: Registry; - let repoName: string; - let tag: string; + let repo: Repository; + let image: AzureImage; 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; + repo = await quickPicks.quickPickACRRepository(registry, `Select the repository of the image you want to delete`); + image = await quickPicks.quickPickACRImage(repo, `Select the image you want to delete`); } else { registry = context.registry; let wholeName: string[] = context.label.split(':'); - repoName = wholeName[0]; - tag = wholeName[1]; + repo = await Repository.Create(registry, wholeName[0]); + image = new AzureImage(repo, wholeName[1]); } - const shouldDelete = await ext.ui.showWarningMessage(`Are you sure you want to delete ${repoName}:${tag}? `, { modal: true }, DialogResponses.deleteResponse, DialogResponses.cancel); + const digest = await acrTools.getImageDigest(image); + const images = await acrTools.getImagesByDigest(repo, digest); + const imageList = images.join(', '); + + const shouldDelete = await ext.ui.showWarningMessage( + `Are you sure you want to delete the manifest '${digest}' and the associated image(s): ${imageList}?`, + { 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', getLoginServer(registry), path, acrAccessToken); - vscode.window.showInformationMessage(`Successfully deleted image ${tag}`); + await acrTools.deleteImage(repo, digest); + vscode.window.showInformationMessage(`Successfully deleted manifest '${digest}' and the associated image(s): ${imageList}.`); + if (context) { dockerExplorerProvider.refreshNode(context.parent); } else { diff --git a/commands/azureCommands/delete-repository.ts b/commands/azureCommands/delete-repository.ts index 3806ec3705..9e873e9ddd 100644 --- a/commands/azureCommands/delete-repository.ts +++ b/commands/azureCommands/delete-repository.ts @@ -8,7 +8,6 @@ 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 { getLoginServer } from "../../utils/nonNull"; import { confirmUserIntent, quickPickACRRegistry, quickPickACRRepository } from '../utils/quick-pick-azure'; /** @@ -17,22 +16,19 @@ import { confirmUserIntent, quickPickACRRegistry, quickPickACRRepository } from */ export async function deleteRepository(context?: AzureRepositoryNode): Promise { let registry: Registry; - let repoName: string; + let repo: Repository; if (context) { - repoName = context.label; registry = context.registry; + repo = await Repository.Create(registry, context.label); } else { registry = await quickPickACRRegistry(); - const repository: Repository = await quickPickACRRepository(registry, 'Select the repository you want to delete'); - repoName = repository.name; + repo = await quickPickACRRepository(registry, 'Select the repository you want to delete'); } - const shouldDelete = await confirmUserIntent(`Are you sure you want to delete ${repoName} and its associated images?`); + const shouldDelete = await confirmUserIntent(`Are you sure you want to delete ${repo.name} and its associated images?`); if (shouldDelete) { - const { acrAccessToken } = await acrTools.acquireACRAccessTokenFromRegistry(registry, `repository:${repoName}:*`); - const path = `/v2/_acr/${repoName}/repository`; - await acrTools.sendRequestToRegistry('delete', getLoginServer(registry), path, acrAccessToken); - vscode.window.showInformationMessage(`Successfully deleted repository ${repoName}`); + await acrTools.deleteRepository(repo); + vscode.window.showInformationMessage(`Successfully deleted repository ${repo.name}`); if (context) { dockerExplorerProvider.refreshNode(context.parent); } else { diff --git a/commands/azureCommands/quick-build.ts b/commands/azureCommands/quick-build.ts index 9e107db2c4..3427b4caab 100644 --- a/commands/azureCommands/quick-build.ts +++ b/commands/azureCommands/quick-build.ts @@ -1,94 +1,10 @@ -import { ContainerRegistryManagementClient } from 'azure-arm-containerregistry/lib/containerRegistryManagementClient'; -import { Registry, Run, SourceUploadDefinition } from 'azure-arm-containerregistry/lib/models'; -import { DockerBuildRequest } from "azure-arm-containerregistry/lib/models"; -import { Subscription } from 'azure-arm-resource/lib/subscription/models'; -import { BlobService, createBlobServiceWithSas } from "azure-storage"; -import * as fse from 'fs-extra'; -import * as os from 'os'; -import * as process from 'process'; -import * as tar from 'tar'; -import * as url from 'url'; import * as vscode from "vscode"; -import { IActionContext, IAzureQuickPickItem } from 'vscode-azureextensionui'; -import { ext } from '../../extensionVariables'; -import { getBlobInfo, getResourceGroupName, IBlobInfo, streamLogs } from "../../utils/Azure/acrTools"; -import { AzureUtilityManager } from "../../utils/azureUtilityManager"; -import { Item } from '../build-image'; -import { quickPickACRRegistry, quickPickSubscription } from '../utils/quick-pick-azure'; -import { quickPickDockerFileItem, quickPickImageName } from '../utils/quick-pick-image'; -import { quickPickWorkspaceFolder } from '../utils/quickPickWorkspaceFolder'; - -const idPrecision = 6; -const vcsIgnoreList: Set = new Set(['.git', '.gitignore', '.bzr', 'bzrignore', '.hg', '.hgignore', '.svn']); -const status = vscode.window.createOutputChannel('ACR Build Status'); +import { IActionContext } from 'vscode-azureextensionui'; +import { scheduleRunRequest } from '../utils/SourceArchiveUtility'; // Prompts user to select a subscription, resource group, then registry from drop down. If there are multiple folders in the workspace, the source folder must also be selected. // The user is then asked to name & tag the image. A build is queued for the image in the selected registry. // Selected source code must contain a path to the desired dockerfile. export async function quickBuild(actionContext: IActionContext, dockerFileUri?: vscode.Uri | undefined): Promise { - //Acquire information from user - let rootFolder: vscode.WorkspaceFolder = await quickPickWorkspaceFolder("To quick build Docker files you must first open a folder or workspace in VS Code."); - const dockerItem: Item = await quickPickDockerFileItem(actionContext, dockerFileUri, rootFolder); - const subscription: Subscription = await quickPickSubscription(); - const registry: Registry = await quickPickACRRegistry(true); - const osPick = ['Linux', 'Windows'].map(item => >{ label: item, data: item }); - const osType: string = (await ext.ui.showQuickPick(osPick, { 'canPickMany': false, 'placeHolder': 'Select image base OS' })).data; - const imageName: string = await quickPickImageName(actionContext, rootFolder, dockerItem); - - const resourceGroupName: string = getResourceGroupName(registry); - const tarFilePath: string = getTempSourceArchivePath(); - const client: ContainerRegistryManagementClient = await AzureUtilityManager.getInstance().getContainerRegistryManagementClient(subscription); - - //Begin readying build - status.show(); - - const uploadedSourceLocation: string = await uploadSourceCode(client, registry.name, resourceGroupName, rootFolder, tarFilePath); - status.appendLine("Uploaded Source Code to " + tarFilePath); - - const runRequest: DockerBuildRequest = { - type: 'DockerBuildRequest', - imageNames: [imageName], - isPushEnabled: true, - sourceLocation: uploadedSourceLocation, - platform: { os: osType }, - dockerFilePath: dockerItem.relativeFilePath - }; - status.appendLine("Set up Run Request"); - - const run: Run = await client.registries.scheduleRun(resourceGroupName, registry.name, runRequest); - status.appendLine("Scheduled Run " + run.runId); - - await streamLogs(registry, run, status, client); - await fse.unlink(tarFilePath); -} - -async function uploadSourceCode(client: ContainerRegistryManagementClient, registryName: string, resourceGroupName: string, rootFolder: vscode.WorkspaceFolder, tarFilePath: string): Promise { - status.appendLine(" Sending source code to temp file"); - let source: string = rootFolder.uri.fsPath; - let items = await fse.readdir(source); - items = items.filter(i => !(i in vcsIgnoreList)); - // tslint:disable-next-line:no-unsafe-any - tar.c({ cwd: source }, items).pipe(fse.createWriteStream(tarFilePath)); - - status.appendLine(" Getting Build Source Upload Url "); - let sourceUploadLocation: SourceUploadDefinition = await client.registries.getBuildSourceUploadUrl(resourceGroupName, registryName); - let upload_url: string = sourceUploadLocation.uploadUrl; - let relative_path: string = sourceUploadLocation.relativePath; - - status.appendLine(" Getting blob info from Upload Url "); - // Right now, accountName and endpointSuffix are unused, but will be used for streaming logs later. - let blobInfo: IBlobInfo = getBlobInfo(upload_url); - status.appendLine(" Creating Blob Service "); - let blob: BlobService = createBlobServiceWithSas(blobInfo.host, blobInfo.sasToken); - status.appendLine(" Creating Block Blob "); - blob.createBlockBlobFromLocalFile(blobInfo.containerName, blobInfo.blobName, tarFilePath, (): void => { }); - return relative_path; -} - -function getTempSourceArchivePath(): string { - /* tslint:disable-next-line:insecure-random */ - let id: number = Math.floor(Math.random() * Math.pow(10, idPrecision)); - status.appendLine("Setting up temp file with 'sourceArchive" + id + ".tar.gz' "); - let tarFilePath: string = url.resolve(os.tmpdir(), `sourceArchive${id}.tar.gz`); - return tarFilePath; + await scheduleRunRequest(dockerFileUri, "DockerBuildRequest", actionContext); } diff --git a/commands/azureCommands/run-task.ts b/commands/azureCommands/run-task.ts index f4f6f0dc37..2d5cee18e1 100644 --- a/commands/azureCommands/run-task.ts +++ b/commands/azureCommands/run-task.ts @@ -8,6 +8,13 @@ import { TaskNode } from "../../explorer/models/taskNode"; import * as acrTools from '../../utils/Azure/acrTools'; import { AzureUtilityManager } from "../../utils/azureUtilityManager"; import { quickPickACRRegistry, quickPickSubscription, quickPickTask } from '../utils/quick-pick-azure'; +import { scheduleRunRequest } from '../utils/SourceArchiveUtility'; + +// Runs the selected yaml file. Equivalent to az acr run -f +// Selected source code must contain a path to the desired dockerfile. +export async function runTaskFile(yamlFileUri?: vscode.Uri): Promise { + await scheduleRunRequest(yamlFileUri, "FileTaskRunRequest"); +} export async function runTask(context?: TaskNode): Promise { let taskName: string; diff --git a/commands/build-image.ts b/commands/build-image.ts index 2116f6424f..a999631345 100644 --- a/commands/build-image.ts +++ b/commands/build-image.ts @@ -6,14 +6,14 @@ import * as path from "path"; import * as vscode from "vscode"; import { DialogResponses, IActionContext, UserCancelledError } from "vscode-azureextensionui"; -import { DOCKERFILE_GLOB_PATTERN } from '../dockerExtension'; +import { DOCKERFILE_GLOB_PATTERN, YAML_GLOB_PATTERN } from '../dockerExtension'; import { delay } from "../explorer/utils/utils"; import { ext } from "../extensionVariables"; import { addImageTaggingTelemetry, getTagFromUserInput } from "./tag-image"; import { quickPickWorkspaceFolder } from "./utils/quickPickWorkspaceFolder"; -async function getDockerFileUris(folder: vscode.WorkspaceFolder): Promise { - return await vscode.workspace.findFiles(new vscode.RelativePattern(folder, DOCKERFILE_GLOB_PATTERN), undefined, 1000, undefined); +async function getFileUris(folder: vscode.WorkspaceFolder, globPattern: string): Promise { + return await vscode.workspace.findFiles(new vscode.RelativePattern(folder, globPattern), undefined, 1000, undefined); } export interface Item extends vscode.QuickPickItem { @@ -21,7 +21,7 @@ export interface Item extends vscode.QuickPickItem { relativeFolderPath: string; } -function createDockerfileItem(rootFolder: vscode.WorkspaceFolder, uri: vscode.Uri): Item { +function createFileItem(rootFolder: vscode.WorkspaceFolder, uri: vscode.Uri): Item { let relativeFilePath = path.join(".", uri.fsPath.substr(rootFolder.uri.fsPath.length)); return { @@ -32,21 +32,21 @@ function createDockerfileItem(rootFolder: vscode.WorkspaceFolder, uri: vscode.Ur }; } -export async function resolveDockerFileItem(rootFolder: vscode.WorkspaceFolder, dockerFileUri: vscode.Uri | undefined): Promise { - if (dockerFileUri) { - return createDockerfileItem(rootFolder, dockerFileUri); +export async function resolveFileItem(rootFolder: vscode.WorkspaceFolder, fileUri: vscode.Uri | undefined, globPattern: string, message: string): Promise { + if (fileUri) { + return createFileItem(rootFolder, fileUri); } - const uris: vscode.Uri[] = await getDockerFileUris(rootFolder); + let uris: vscode.Uri[] = await getFileUris(rootFolder, globPattern); if (!uris || uris.length === 0) { return undefined; } else { - let items: Item[] = uris.map(uri => createDockerfileItem(rootFolder, uri)); + let items: Item[] = uris.map(uri => createFileItem(rootFolder, uri)); if (items.length === 1) { return items[0]; } else { - const res: vscode.QuickPickItem = await ext.ui.showQuickPick(items, { placeHolder: 'Choose Dockerfile to build' }); + const res: vscode.QuickPickItem = await ext.ui.showQuickPick(items, { placeHolder: message }); return res; } } @@ -60,7 +60,7 @@ export async function buildImage(actionContext: IActionContext, dockerFileUri: v let rootFolder: vscode.WorkspaceFolder = await quickPickWorkspaceFolder('To build Docker files you must first open a folder or workspace in VS Code.'); while (!dockerFileItem) { - let resolvedItem: Item | undefined = await resolveDockerFileItem(rootFolder, dockerFileUri); + let resolvedItem: Item | undefined = await resolveFileItem(rootFolder, dockerFileUri, DOCKERFILE_GLOB_PATTERN, 'Choose a Dockerfile to build the image.'); if (resolvedItem) { dockerFileItem = resolvedItem; } else { diff --git a/commands/utils/SourceArchiveUtility.ts b/commands/utils/SourceArchiveUtility.ts new file mode 100644 index 0000000000..cdc848846e --- /dev/null +++ b/commands/utils/SourceArchiveUtility.ts @@ -0,0 +1,114 @@ +import { ContainerRegistryManagementClient } from 'azure-arm-containerregistry/lib/containerRegistryManagementClient'; +import { DockerBuildRequest, FileTaskRunRequest, Registry } from 'azure-arm-containerregistry/lib/models'; +import { Subscription } from 'azure-arm-resource/lib/subscription/models'; +import { BlobService, createBlobServiceWithSas } from "azure-storage"; +import * as fse from 'fs-extra'; +import * as os from 'os'; +import * as tar from 'tar'; +import * as url from 'url'; +import vscode = require('vscode'); +import { IActionContext, IAzureQuickPickItem } from 'vscode-azureextensionui'; +import { ext } from '../../extensionVariables'; +import { getBlobInfo, getResourceGroupName, streamLogs } from "../../utils/Azure/acrTools"; +import { AzureUtilityManager } from '../../utils/azureUtilityManager'; +import { Item } from '../build-image'; +import { quickPickACRRegistry, quickPickSubscription } from './quick-pick-azure'; +import { quickPickDockerFileItem, quickPickImageName, quickPickYamlFileItem } from './quick-pick-image'; +import { quickPickWorkspaceFolder } from './quickPickWorkspaceFolder'; + +const idPrecision = 6; +const vcsIgnoreList = ['.git', '.gitignore', '.bzr', 'bzrignore', '.hg', '.hgignore', '.svn'] + +export type runRequestType = + | 'DockerBuildRequest' + | 'FileTaskRunRequest'; + +export async function scheduleRunRequest(fileUri: vscode.Uri, requestType: runRequestType, actionContext?: IActionContext): Promise { + //Acquire information. + let rootFolder: vscode.WorkspaceFolder; + let fileItem: Item; + if (requestType === 'DockerBuildRequest') { + rootFolder = await quickPickWorkspaceFolder("To quick build Docker files you must first open a folder or workspace in VS Code."); + fileItem = await quickPickDockerFileItem(actionContext, fileUri, rootFolder); + } else if (requestType === 'FileTaskRunRequest') { + rootFolder = await quickPickWorkspaceFolder("To run a task from a .yaml file you must first open a folder or workspace in VS Code."); + fileItem = await quickPickYamlFileItem(fileUri, rootFolder); + } else { + throw new Error("Run Request Type Currently not supported."); + } + const subscription: Subscription = await quickPickSubscription(); + const registry: Registry = await quickPickACRRegistry(true); + const osPick = ['Linux', 'Windows'].map(item => >{ label: item, data: item }); + const osType: string = (await ext.ui.showQuickPick(osPick, { 'canPickMany': false, 'placeHolder': 'Select image base OS' })).data; + + const resourceGroupName: string = getResourceGroupName(registry); + const tarFilePath: string = getTempSourceArchivePath(); + const client: ContainerRegistryManagementClient = await AzureUtilityManager.getInstance().getContainerRegistryManagementClient(subscription); + + //Prepare to run. + ext.outputChannel.show(); + + const uploadedSourceLocation: string = await uploadSourceCode(client, registry.name, resourceGroupName, rootFolder, tarFilePath); + ext.outputChannel.appendLine("Uploaded source code to " + tarFilePath); + + let runRequest: DockerBuildRequest | FileTaskRunRequest; + if (requestType === 'DockerBuildRequest') { + const imageName: string = await quickPickImageName(actionContext, rootFolder, fileItem); + runRequest = { + type: requestType, + imageNames: [imageName], + isPushEnabled: true, + sourceLocation: uploadedSourceLocation, + platform: { os: osType }, + dockerFilePath: fileItem.relativeFilePath + }; + } else { + runRequest = { + type: 'FileTaskRunRequest', + taskFilePath: fileItem.relativeFilePath, + sourceLocation: uploadedSourceLocation, + platform: { os: osType } + } + } + + //Schedule the run and Clean up. + ext.outputChannel.appendLine("Set up run request"); + + const run = await client.registries.scheduleRun(resourceGroupName, registry.name, runRequest); + ext.outputChannel.appendLine("Scheduled run " + run.runId); + + await streamLogs(registry, run, client); + await fse.unlink(tarFilePath); +} + +async function uploadSourceCode(client: ContainerRegistryManagementClient, registryName: string, resourceGroupName: string, rootFolder: vscode.WorkspaceFolder, tarFilePath: string): Promise { + ext.outputChannel.appendLine(" Sending source code to temp file"); + let source: string = rootFolder.uri.fsPath; + let items = await fse.readdir(source); + items = items.filter(i => !(i in vcsIgnoreList)); + // tslint:disable-next-line:no-unsafe-any + tar.c({ cwd: source }, items).pipe(fse.createWriteStream(tarFilePath)); + + ext.outputChannel.appendLine(" Getting build source upload URL "); + let sourceUploadLocation = await client.registries.getBuildSourceUploadUrl(resourceGroupName, registryName); + let upload_url: string = sourceUploadLocation.uploadUrl; + let relative_path: string = sourceUploadLocation.relativePath; + + ext.outputChannel.appendLine(" Getting blob info from upload URL "); + // Right now, accountName and endpointSuffix are unused, but will be used for streaming logs later. + let blobInfo = getBlobInfo(upload_url); + ext.outputChannel.appendLine(" Creating blob service "); + let blob: BlobService = createBlobServiceWithSas(blobInfo.host, blobInfo.sasToken); + ext.outputChannel.appendLine(" Creating block blob "); + blob.createBlockBlobFromLocalFile(blobInfo.containerName, blobInfo.blobName, tarFilePath, (): void => { }); + return relative_path; +} + +function getTempSourceArchivePath(): string { + /* tslint:disable-next-line:insecure-random */ + const id: number = Math.floor(Math.random() * Math.pow(10, idPrecision)); + const archive = `sourceArchive${id}.tar.gz`; + ext.outputChannel.appendLine(`Setting up temp file with '${archive}'`); + const tarFilePath: string = url.resolve(os.tmpdir(), archive); + return tarFilePath; +} diff --git a/commands/utils/quick-pick-azure.ts b/commands/utils/quick-pick-azure.ts index 1544fd3aca..a8838bf1ed 100644 --- a/commands/utils/quick-pick-azure.ts +++ b/commands/utils/quick-pick-azure.ts @@ -9,7 +9,7 @@ import { Location, Subscription } from 'azure-arm-resource/lib/subscription/mode import * as opn from 'opn'; import * as vscode from "vscode"; import { IAzureQuickPickItem, UserCancelledError } from 'vscode-azureextensionui'; -import { imageTagRegExp, skus } from '../../constants' +import { skus } from '../../constants' import { ext } from '../../extensionVariables'; import { ResourceManagementClient } from '../../node_modules/azure-arm-resource'; import * as acrTools from '../../utils/Azure/acrTools'; @@ -46,7 +46,7 @@ export async function quickPickTask(registry: Registry, subscription: Subscripti } export async function quickPickACRRegistry(canCreateNew: boolean = false, prompt?: string): Promise { - const placeHolder = prompt ? prompt : 'Select registry to use'; + const placeHolder = prompt ? prompt : 'Select registry'; let registries = await AzureUtilityManager.getInstance().getRegistries(); let quickPickRegList = registries.map(reg => >{ label: reg.name, data: reg }); diff --git a/commands/utils/quick-pick-image.ts b/commands/utils/quick-pick-image.ts index 3bfe0f9642..b03672aa5d 100644 --- a/commands/utils/quick-pick-image.ts +++ b/commands/utils/quick-pick-image.ts @@ -2,14 +2,16 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See LICENSE.md in the project root for license information. *--------------------------------------------------------------------------------------------*/ + import * as Docker from 'dockerode'; import * as path from "path"; import vscode = require('vscode'); import { DialogResponses, IActionContext, parseError, TelemetryProperties } from 'vscode-azureextensionui'; +import { DOCKERFILE_GLOB_PATTERN, YAML_GLOB_PATTERN } from '../../dockerExtension'; import { showDockerConnectionError, throwDockerConnectionError } from '../../explorer/utils/dockerConnectionError'; import { delay } from '../../explorer/utils/utils'; import { ext } from '../../extensionVariables'; -import { Item, resolveDockerFileItem } from '../build-image'; +import { Item, resolveFileItem } from '../build-image'; import { addImageTaggingTelemetry, getTagFromUserInput } from '../tag-image'; import { docker } from './docker-endpoint'; @@ -115,7 +117,7 @@ export async function quickPickDockerFileItem(actionContext: IActionContext, doc let dockerFileItem: Item; while (!dockerFileItem) { - let resolvedItem: Item | undefined = await resolveDockerFileItem(rootFolder, dockerFileUri); + let resolvedItem: Item | undefined = await resolveFileItem(rootFolder, dockerFileUri, DOCKERFILE_GLOB_PATTERN, 'Choose a Dockerfile to build.'); if (resolvedItem) { dockerFileItem = resolvedItem; } else { @@ -129,3 +131,13 @@ export async function quickPickDockerFileItem(actionContext: IActionContext, doc } return dockerFileItem; } + +export async function quickPickYamlFileItem(fileUri: vscode.Uri | undefined, rootFolder: vscode.WorkspaceFolder): Promise { + let fileItem: Item; + + let resolvedItem: Item | undefined = await resolveFileItem(rootFolder, fileUri, YAML_GLOB_PATTERN, 'Choose a .yaml file to run.'); + if (resolvedItem) { + fileItem = resolvedItem; + } + return fileItem; +} diff --git a/dockerExtension.ts b/dockerExtension.ts index 36f8367e0b..a055e7bf4d 100644 --- a/dockerExtension.ts +++ b/dockerExtension.ts @@ -18,12 +18,12 @@ import { ConfigurationParams, DidChangeConfigurationNotification, DocumentSelect import { viewACRLogs } from "./commands/azureCommands/acr-logs"; import { LogContentProvider } from "./commands/azureCommands/acr-logs-utils/logFileManager"; import { createRegistry } from './commands/azureCommands/create-registry'; -import { deleteAzureImage } from './commands/azureCommands/delete-image'; +import { deleteAzureImage, untagAzureImage } 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 { quickBuild } from "./commands/azureCommands/quick-build"; -import { runTask } from "./commands/azureCommands/run-task"; +import { runTask, runTaskFile } from "./commands/azureCommands/run-task"; import { showTaskProperties } from "./commands/azureCommands/show-task"; import { TaskContentProvider } from "./commands/azureCommands/task-utils/showTaskManager"; import { buildImage } from './commands/build-image'; @@ -75,8 +75,9 @@ import { getTrustedCertificates } from './utils/getTrustedCertificates'; import { Keytar } from './utils/keytar'; export const FROM_DIRECTIVE_PATTERN = /^\s*FROM\s*([\w-\/:]*)(\s*AS\s*[a-z][a-z0-9-_\\.]*)?$/i; -export const COMPOSE_FILE_GLOB_PATTERN = '**/[dD]ocker-[cC]ompose*.{yaml,yml}'; -export const DOCKERFILE_GLOB_PATTERN = '**/{*.dockerfile,[dD]ocker[fF]ile}'; +export const COMPOSE_FILE_GLOB_PATTERN = '**/[dD][oO][cC][kK][eE][rR]-[cC][oO][mM][pP][oO][sS][eE]*.{[yY][aA][mM][lL],[yY][mM][lL]}'; +export const DOCKERFILE_GLOB_PATTERN = '**/{*.[dD][oO][cC][kK][eE][rR][fF][iI][lL][eE],[dD][oO][cC][kK][eE][rR][fF][iI][lL][eE]}'; +export const YAML_GLOB_PATTERN = '**/*.{[yY][aA][mM][lL],[yY][mM][lL]}'; export let dockerExplorerProvider: DockerExplorerProvider; @@ -282,7 +283,9 @@ function registerDockerCommands(): void { registerCommand('vscode-docker.acr.pullImage', pullFromAzure); registerCommand('vscode-docker.acr.quickBuild', async function (this: IActionContext, item: vscode.Uri | undefined): Promise { await quickBuild(this, item); }); registerCommand('vscode-docker.acr.runTask', runTask); + registerCommand("vscode-docker.acr.runTaskFile", runTaskFile); registerCommand('vscode-docker.acr.showTask', showTaskProperties); + registerCommand('vscode-docker.acr.untagImage', untagAzureImage); registerCommand('vscode-docker.acr.viewLogs', viewACRLogs); registerCommand('vscode-docker.api.configure', async function (this: IActionContext, options: ConfigureApiOptions): Promise { await configureApi(this, options); }); diff --git a/explorer/models/commonRegistryUtils.ts b/explorer/models/commonRegistryUtils.ts index 9e70b72c02..6f835e927e 100644 --- a/explorer/models/commonRegistryUtils.ts +++ b/explorer/models/commonRegistryUtils.ts @@ -9,7 +9,7 @@ import { parseError } from 'vscode-azureextensionui'; import { MAX_CONCURRENT_REQUESTS, PAGE_SIZE } from '../../constants' import { ext } from '../../extensionVariables'; import { AsyncPool } from '../../utils/asyncpool'; -import { Manifest, ManifestHistory, ManifestHistoryV1Compatibility, Repository } from '../utils/dockerHubUtils'; +import { Manifest, ManifestHistoryV1Compatibility } from '../utils/dockerHubUtils'; interface RegistryNonsensitiveInfo { url: string, diff --git a/explorer/models/rootNode.ts b/explorer/models/rootNode.ts index d77da41517..abad71f769 100644 --- a/explorer/models/rootNode.ts +++ b/explorer/models/rootNode.ts @@ -65,9 +65,6 @@ export class RootNode extends NodeBase { if (refreshInterval > 0) { this._imageDebounceTimer = setInterval(async () => { - let needToRefresh: boolean = false; - let found: boolean = false; - const images: Docker.ImageDesc[] = await docker.getImageDescriptors(imageFilters); images.sort((img1, img2) => { if (img1.Id > img2.Id) { diff --git a/package.json b/package.json index 0126930e5d..6c233d3aa2 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,9 @@ "onCommand:vscode-docker.acr.pullImage", "onCommand:vscode-docker.acr.quickBuild", "onCommand:vscode-docker.acr.runTask", + "onCommand:vscode-docker.acr.runTaskFile", "onCommand:vscode-docker.acr.showTask", + "onCommand:vscode-docker.acr.untagImage", "onCommand:vscode-docker.acr.viewLogs", "onCommand:vscode-docker.api.configure", "onCommand:vscode-docker.browseAzurePortal", @@ -93,6 +95,11 @@ "command": "vscode-docker.acr.quickBuild", "group": "docker" }, + { + "when": "editorLangId == yaml", + "command": "vscode-docker.acr.runTaskFile", + "group": "docker" + }, { "when": "resourceFilename == docker-compose.yml", "command": "vscode-docker.compose.down", @@ -131,27 +138,32 @@ ], "explorer/context": [ { - "when": "resourceFilename =~ /[dD]ocker[fF]ile/", + "when": "resourceFilename =~ /(^|\\.)dockerfile$/i", "command": "vscode-docker.acr.quickBuild", "group": "docker" }, { - "when": "resourceFilename =~ /[dD]ocker-[cC]ompose/", + "when": "resourceFilename =~ /^(?:(?!^docker-compose\\.ya?ml$).)*\\.ya?ml$/i", + "command": "vscode-docker.acr.runTaskFile", + "group": "docker" + }, + { + "when": "resourceFilename =~ /docker-compose/i", "command": "vscode-docker.compose.down", "group": "docker" }, { - "when": "resourceFilename =~ /[dD]ocker-[cC]ompose/", + "when": "resourceFilename =~ /docker-compose/i", "command": "vscode-docker.compose.restart", "group": "docker" }, { - "when": "resourceFilename =~ /[dD]ocker-[cC]ompose/", + "when": "resourceFilename =~ /docker-compose/i", "command": "vscode-docker.compose.up", "group": "docker" }, { - "when": "resourceFilename =~ /[dD]ocker[fF]ile/", + "when": "resourceFilename =~ /dockerfile/i", "command": "vscode-docker.image.build", "group": "docker" } @@ -197,6 +209,10 @@ "command": "vscode-docker.acr.showTask", "when": "view == dockerExplorer && viewItem == taskNode" }, + { + "command": "vscode-docker.acr.untagImage", + "when": "view == dockerExplorer && viewItem == azureImageTagNode" + }, { "command": "vscode-docker.acr.viewLogs", "when": "view == dockerExplorer && viewItem =~ /^(azureRegistryNode|azureImageTagNode|taskNode)$/" @@ -631,11 +647,22 @@ "title": "Run Task", "category": "Docker" }, + { + "command": "vscode-docker.acr.runTaskFile", + "title": "Run ACR Task File", + "description": "Run an ACR task from a yaml file.", + "category": "Docker" + }, { "command": "vscode-docker.acr.showTask", "title": "Show Task Properties", "category": "Docker" }, + { + "command": "vscode-docker.acr.untagImage", + "title": "Untag Azure Image", + "category": "Docker" + }, { "command": "vscode-docker.acr.viewLogs", "title": "View Azure Logs", diff --git a/utils/Azure/acrTools.ts b/utils/Azure/acrTools.ts index b0b2e79707..6deba9b918 100644 --- a/utils/Azure/acrTools.ts +++ b/utils/Azure/acrTools.ts @@ -12,10 +12,10 @@ import { Subscription } from "azure-arm-resource/lib/subscription/models"; import { BlobService, createBlobServiceWithSas } from "azure-storage"; import { ServiceClientCredentials } from 'ms-rest'; import { TokenResponse } from 'ms-rest-azure'; -import * as vscode from "vscode"; +import { isNull } from 'util'; import { parseError } from 'vscode-azureextensionui'; import { NULL_GUID } from "../../constants"; -import { getCatalog, getTags, TagInfo } from "../../explorer/models/commonRegistryUtils"; +import { Manifest } from '../../explorer/utils/dockerHubUtils'; import { ext } from '../../extensionVariables'; import { AzureSession } from "../../typings/azure-account.api"; import { AzureUtilityManager } from '../azureUtilityManager'; @@ -57,58 +57,127 @@ export async function getResourceGroup(registry: Registry, subscription: Subscri //Registry item management /** List images under a specific Repository */ -export async function getImagesByRepository(element: Repository): Promise { - let allImages: AzureImage[] = []; - let image: AzureImage; - const { acrAccessToken } = await acquireACRAccessTokenFromRegistry(element.registry, 'repository:' + element.name + ':pull'); +export async function getImagesByRepository(repo: Repository): Promise { + const { acrAccessToken } = await acquireACRAccessTokenFromRegistry(repo.registry, 'repository:' + repo.name + ':pull'); + let response = await sendRequest<{ tags: { name: string, lastUpdateTime: string }[] }>( + 'get', + getLoginServer(repo.registry), + `acr/v1/${repo.name}/_tags?orderby=timedesc`, + acrAccessToken); + + let tags = response.tags; + let images: AzureImage[] = []; + if (!isNull(tags)) { + //Acquires each image's manifest (in parallel) to acquire build time + for (let tag of tags) { + let img = new AzureImage(repo, tag.name, new Date(tag.lastUpdateTime)); + images.push(img); + } + } - const tags: TagInfo[] = await getTags('https://' + element.registry.loginServer, element.name, { bearer: acrAccessToken }); + return images; +} + +/** List images under a specific digest */ +export async function getImagesByDigest(repo: Repository, digest: string): Promise { + let allImages: AzureImage[] = []; + const { acrAccessToken } = await acquireACRAccessTokenFromRegistry(repo.registry, 'repository:' + repo.name + ':pull'); + let response = await sendRequest<{ manifest: ACRManifest }>('get', getLoginServer(repo.registry), `acr/v1/${repo.name}/_manifests/${digest}`, acrAccessToken); + const tags: string[] = response.manifest.tags; for (let tag of tags) { - image = new AzureImage(element, tag.tag, tag.created); - allImages.push(image); + allImages.push(new AzureImage(repo, tag)); } return allImages; } /** List repositories on a given Registry. */ export async function getRepositoriesByRegistry(registry: Registry): Promise { - let repo: Repository; const { acrAccessToken } = await acquireACRAccessTokenFromRegistry(registry, "registry:catalog:*"); - const repositories: string[] = await getCatalog('https://' + registry.loginServer, { bearer: acrAccessToken }); + let response = await sendRequest<{ repositories: string[] }>('get', getLoginServer(registry), 'acr/v1/_catalog', acrAccessToken); let allRepos: Repository[] = []; - for (let tempRepo of repositories) { - repo = await Repository.Create(registry, tempRepo); - allRepos.push(repo); + if (!isNull(response.repositories)) { + for (let tempRepo of response.repositories) { + allRepos.push(await Repository.Create(registry, tempRepo)); + } } + //Note these are ordered by default in alphabetical order return allRepos; } +export async function deleteRepository(repo: Repository): Promise { + const { acrAccessToken } = await acquireACRAccessTokenFromRegistry(repo.registry, `repository:${repo.name}:*`); + await sendRequest('delete', getLoginServer(repo.registry), `v2/_acr/${repo.name}/repository`, acrAccessToken); +} + +export async function deleteImage(repo: Repository, imageDigest: string): Promise { + const { acrAccessToken } = await acquireACRAccessTokenFromRegistry(repo.registry, `repository:${repo.name}:*`); + await sendRequest('delete', getLoginServer(repo.registry), `v2/${repo.name}/manifests/${imageDigest}`, acrAccessToken); +} + +export async function untagImage(img: AzureImage): Promise { + const { acrAccessToken } = await acquireACRAccessTokenFromRegistry(img.registry, `repository:${img.repository.name}:*`); + await sendRequest('delete', getLoginServer(img.registry), `v2/_acr/${img.repository.name}/tags/${img.tag}`, acrAccessToken); +} + /** Sends a custom html request to a registry - * @param http_method : the http method, this function currently only uses delete + * @param http_method : the http method * @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 + * @param accessToken : Bearer access token. */ -export async function sendRequestToRegistry(http_method: 'delete', login_server: string, path: string, bearerAccessToken: string): Promise { - let url: string = `https://${login_server}${path}`; - let header = 'Bearer ' + bearerAccessToken; - let opt = { - headers: { 'Authorization': header }, - http_method: http_method, - url: url - } +export async function sendRequest(http_method: string, login_server: string, path: string, accessToken: string): Promise { + let url: string = `https://${login_server}/${path}`; if (http_method === 'delete') { - await ext.request.delete(opt); - return; + return await ext.request.delete( + { + headers: { 'Authorization': `Bearer ${accessToken}` }, + url: url + } + ); + } else if (http_method === 'get') { + return await ext.request.get( + url, + { + headers: { 'Authorization': `Bearer ${accessToken}` }, + json: true, + resolveWithFullResponse: false, + } + ); } throw new Error('sendRequestToRegistry: Unexpected http method'); } +/** Sends a custom html request to a registry, and retrieves the image's digest. + * @param image : The targeted image. + */ +export async function getImageDigest(image: AzureImage): Promise { + const { acrAccessToken } = await acquireACRAccessTokenFromRegistry(image.registry, `repository:${image.repository.name}:pull`); + + return new Promise((resolve, reject) => ext.request.get('https://' + image.registry.loginServer + `/v2/${image.repository.name}/manifests/${image.tag}`, { + auth: { + bearer: acrAccessToken + }, + headers: { + accept: 'application/vnd.docker.distribution.manifest.v2+json; 0.5, application/vnd.docker.distribution.manifest.list.v2+json; 0.6' + } + }, (_err, _httpResponse, _body) => { + if (_err) { + reject(_err); + } else { + const imageDigest = _httpResponse.headers['docker-content-digest']; + if (imageDigest instanceof Array) { + reject(new Error('docker-content-digest should be a string not an array.')) + } else { + resolve(imageDigest); + } + } + })); +} + //Credential management /** Obtains registry username and password compatible with docker login */ export async function getLoginCredentials(registry: Registry): Promise<{ password: string, username: string }> { @@ -220,7 +289,7 @@ export function getBlobInfo(blobUrl: string): IBlobInfo { * streamed in which prevents updating of already appended lines. Usure if this can be fixed. Nonetheless * logs do load in chunks every 1 second. */ -export async function streamLogs(registry: Registry, run: Run, outputChannel: vscode.OutputChannel, providedClient?: ContainerRegistryManagementClient): Promise { +export async function streamLogs(registry: Registry, run: Run, providedClient?: ContainerRegistryManagementClient): Promise { //Prefer passed in client to avoid initialization but if not added obtains own const subscription = await getSubscriptionFromRegistry(registry); let client = providedClient ? providedClient : await AzureUtilityManager.getInstance().getContainerRegistryManagementClient(subscription); @@ -249,7 +318,7 @@ export async function streamLogs(registry: Registry, run: Run, outputChannel: vs text = await getBlobToText(blobInfo, blob, start); let utf8encoded = (new Buffer(text, 'ascii')).toString('utf8'); start += text.length; - outputChannel.append(utf8encoded); + ext.outputChannel.append(utf8encoded); } if (metadata.Complete) { clearInterval(obtainLogs); @@ -275,3 +344,7 @@ async function getBlobProperties(blobInfo: IBlobInfo, blob: BlobService): Promis }); }); } + +interface ACRManifest extends Manifest { + tags: string[]; +} diff --git a/utils/Azure/models/image.ts b/utils/Azure/models/image.ts index 67ef5dc0ef..351d7434ea 100644 --- a/utils/Azure/models/image.ts +++ b/utils/Azure/models/image.ts @@ -8,23 +8,27 @@ import { Repository } from './repository'; /** Class Azure Image: Used locally, Organizes data for managing images */ export class AzureImage { - public created: Date; public registry: Registry; public repository: Repository; public tag: string; public subscription: SubscriptionModels.Subscription; public resourceGroupName: string; + public created?: Date; public password?: string; public username?: string; - constructor(repository: Repository, tag: string, created: Date) { + constructor(repository: Repository, tag: string, created?: Date) { this.registry = repository.registry; this.repository = repository; this.tag = tag; - this.created = created; this.subscription = repository.subscription; this.resourceGroupName = repository.resourceGroupName; - if (repository.password) { this.password = repository.password; } - if (repository.username) { this.username = repository.username; } + this.created = created; + this.password = repository.password; + this.username = repository.username; + } + + public toString(): string { + return `${this.repository.name}:${this.tag}`; } }