diff --git a/commands/azureCommands/acr-logs-utils/logFileManager.ts b/commands/azureCommands/acr-logs-utils/logFileManager.ts new file mode 100644 index 0000000000..a44b0d6518 --- /dev/null +++ b/commands/azureCommands/acr-logs-utils/logFileManager.ts @@ -0,0 +1,65 @@ +import { BlobService, createBlobServiceWithSas } from 'azure-storage'; +import * as fse from 'fs-extra'; +import * as vscode from 'vscode'; +import { getBlobInfo, getBlobToText, IBlobInfo } from '../../../utils/Azure/acrTools'; + +export class LogContentProvider implements vscode.TextDocumentContentProvider { + public static scheme: string = 'purejs'; + private onDidChangeEvent: vscode.EventEmitter = new vscode.EventEmitter(); + + constructor() { } + + public provideTextDocumentContent(uri: vscode.Uri): string { + let parse: { log: string } = <{ log: string }>JSON.parse(uri.query); + return decodeBase64(parse.log); + } + + get onDidChange(): vscode.Event { + return this.onDidChangeEvent.event; + } + + public update(uri: vscode.Uri, message: string): void { + this.onDidChangeEvent.fire(uri); + } + +} + +export function decodeBase64(str: string): string { + return Buffer.from(str, 'base64').toString('ascii'); +} + +export function encodeBase64(str: string): string { + return Buffer.from(str, 'ascii').toString('base64'); +} + +/** Loads log text from remote url using azure blobservices */ +export async function accessLog(url: string, title: string, download: boolean): Promise { + let blobInfo: IBlobInfo = getBlobInfo(url); + let blob: BlobService = createBlobServiceWithSas(blobInfo.host, blobInfo.sasToken); + let text1 = await getBlobToText(blobInfo, blob, 0); + if (download) { + await downloadLog(text1, title); + } else { + openLogInNewWindow(text1, title); + } +} + +function openLogInNewWindow(content: string, title: string): void { + const scheme = 'purejs'; + let query = JSON.stringify({ 'log': encodeBase64(content) }); + let uri: vscode.Uri = vscode.Uri.parse(`${scheme}://authority/${title}.log?${query}#idk`); + vscode.workspace.openTextDocument(uri).then((doc) => { + return vscode.window.showTextDocument(doc, vscode.ViewColumn.Active + 1, true); + }); +} + +export async function downloadLog(content: string, title: string): Promise { + let uri = await vscode.window.showSaveDialog({ + filters: { 'Log': ['.log', '.txt'] }, + defaultUri: vscode.Uri.file(`${title}.log`) + }); + fse.writeFile(uri.fsPath, content, + (err) => { + if (err) { throw err; } + }); +} diff --git a/commands/azureCommands/acr-logs-utils/logScripts.js b/commands/azureCommands/acr-logs-utils/logScripts.js new file mode 100644 index 0000000000..a69911fdf6 --- /dev/null +++ b/commands/azureCommands/acr-logs-utils/logScripts.js @@ -0,0 +1,306 @@ +// Global Variables +const status = { + 'Succeeded': 4, + 'Queued': 3, + 'Error': 2, + 'Failed': 1 +} + +var currentItemsCount = 4; +var currentDir = "asc" +var triangles = { + 'down': ' ', + 'up': ' ' +} + +document.addEventListener("scroll", function () { + var translate = "translate(0," + this.lastChild.scrollTop + "px)"; + let fixedItems = this.querySelectorAll(".fixed"); + for (item of fixedItems) { + item.style.transform = translate; + } +}); + +// Main +let content = document.querySelector('#core'); +const vscode = acquireVsCodeApi(); +setLoadMoreListener(); +setInputListeners(); +loading(); + +document.onkeydown = function (event) { + if (event.key === "Enter") { // The Enter/Return key + document.activeElement.onclick(event); + } +}; + +/* Sorting + * PR note, while this does not use a particularly quick algorithm + * it allows a low stuttering experience that allowed rapid testing. + * I will improve it soon.*/ +function sortTable(n, dir = "asc", holdDir = false) { + currentItemsCount = n; + let table, rows, switching, i, x, y, shouldSwitch, switchcount = 0; + let cmpFunc = acquireCompareFunction(n); + table = document.getElementById("core"); + switching = true; + //Set the sorting direction to ascending: + + while (switching) { + switching = false; + rows = table.querySelectorAll(".holder"); + for (i = 0; i < rows.length - 1; i++) { + shouldSwitch = false; + x = rows[i].getElementsByTagName("TD")[n + 1]; + y = rows[i + 1].getElementsByTagName("TD")[n + 1]; + if (dir == "asc") { + if (cmpFunc(x, y)) { + shouldSwitch = true; + break; + } + } else if (dir == "desc") { + if (cmpFunc(y, x)) { + shouldSwitch = true; + break; + } + } + } + if (shouldSwitch) { + rows[i].parentNode.insertBefore(rows[i + 1], rows[i]); + switching = true; + switchcount++; + } else { + /*If no switching has been done AND the direction is "asc", set the direction to "desc" and run the while loop again.*/ + if (switchcount == 0 && dir == "asc" && !holdDir) { + dir = "desc"; + switching = true; + } + } + } + if (!holdDir) { + let sortColumns = document.querySelectorAll(".sort"); + if (sortColumns[n].innerHTML === triangles['down']) { + sortColumns[n].innerHTML = triangles['up']; + } else if (sortColumns[n].innerHTML === triangles['up']) { + sortColumns[n].innerHTML = triangles['down']; + } else { + for (cell of sortColumns) { + cell.innerHTML = ' '; + } + sortColumns[n].innerHTML = triangles['down']; + } + } + currentDir = dir; +} + +function acquireCompareFunction(n) { + switch (n) { + case 0: //Name + case 1: //Task + return (x, y) => { + return x.innerHTML.toLowerCase() > y.innerHTML.toLowerCase() + } + case 2: //Status + return (x, y) => { + return status[x.dataset.status] > status[y.dataset.status];; + } + case 3: //Created time + return (x, y) => { + if (x.dataset.createdtime === '') return true; + if (y.dataset.createdtime === '') return false; + let dateX = new Date(x.dataset.createdtime); + let dateY = new Date(y.dataset.createdtime); + return dateX > dateY; + } + case 4: //Elapsed time + return (x, y) => { + if (x.innerHTML === '') return true; + if (y.innerHTML === '') return false; + return Number(x.innerHTML.substring(0, x.innerHTML.length - 1)) > Number(y.innerHTML.substring(0, y.innerHTML.length - 1)); + } + case 5: //OS Type + return (x, y) => { + return x.innerHTML.toLowerCase() > y.innerHTML.toLowerCase() + } + default: + throw 'Could not acquire Compare function, invalid n'; + } +} + +// Event Listener Setup +window.addEventListener('message', event => { + const message = event.data; // The JSON data our extension sent + if (message.type === 'populate') { + content.insertAdjacentHTML('beforeend', message.logComponent); + + let item = content.querySelector(`#btn${message.id}`); + setSingleAccordion(item); + + let panel = item.nextElementSibling; + + const logButton = panel.querySelector('.openLog'); + setLogBtnListener(logButton, false); + const downloadlogButton = panel.querySelector('.downloadlog'); + setLogBtnListener(downloadlogButton, true); + + const digestClickables = panel.querySelectorAll('.copy'); + setDigestListener(digestClickables); + + } else if (message.type === 'endContinued') { + sortTable(currentItemsCount, currentDir, true); + loading(); + } else if (message.type === 'end') { + window.addEventListener("resize", setAccordionTableWidth); + setAccordionTableWidth(); + setTableSorter(); + loading(); + } + + if (message.canLoadMore) { + const loadBtn = document.querySelector('.loadMoreBtn'); + loadBtn.style.display = 'flex'; + } + +}); + +function setSingleAccordion(item) { + item.onclick = function (event) { + this.classList.toggle('active'); + this.querySelector('.arrow').classList.toggle('activeArrow'); + let panel = this.nextElementSibling; + if (panel.style.maxHeight) { + panel.style.display = 'none'; + panel.style.maxHeight = null; + let index = openAccordions.indexOf(panel); + if (index > -1) { + openAccordions.splice(index, 1); + } + } else { + openAccordions.push(panel); + setAccordionTableWidth(); + panel.style.display = 'table-row'; + let paddingTop = +panel.style.paddingTop.split('px')[0]; + let paddingBottom = +panel.style.paddingBottom.split('px')[0]; + panel.style.maxHeight = (panel.scrollHeight + paddingTop + paddingBottom) + 'px'; + } + }; +} + +function setTableSorter() { + let tableHeader = document.querySelector("#tableHead"); + let items = tableHeader.querySelectorAll(".colTitle"); + for (let i = 0; i < items.length; i++) { + items[i].onclick = () => { + sortTable(i); + }; + } +} + +function setLogBtnListener(item, download) { + item.onclick = (event) => { + vscode.postMessage({ + logRequest: { + 'id': event.target.dataset.id, + 'download': download + } + }); + }; +} + +function setLoadMoreListener() { + let item = document.querySelector("#loadBtn"); + item.onclick = function () { + const loadBtn = document.querySelector('.loadMoreBtn'); + loadBtn.style.display = 'none'; + loading(); + vscode.postMessage({ + loadMore: true + }); + }; +} + +function setDigestListener(digestClickables) { + for (digest of digestClickables) { + digest.onclick = function (event) { + vscode.postMessage({ + copyRequest: { + 'text': event.target.parentNode.dataset.digest, + } + }); + }; + } +} + +let openAccordions = []; + +function setAccordionTableWidth() { + let headerCells = document.querySelectorAll("#core thead tr th"); + let topWidths = []; + for (let cell of headerCells) { + topWidths.push(parseInt(getComputedStyle(cell).width)); + } + for (acc of openAccordions) { + let cells = acc.querySelectorAll(".innerTable th, .innerTable td"); // 4 items + const cols = acc.querySelectorAll(".innerTable th").length + 1; //Account for arrowHolder + const rows = cells.length / cols; + //cells[0].style.width = topWidths[0]; + for (let row = 0; row < rows; row++) { + for (let col = 1; col < cols - 1; col++) { + let cell = cells[row * cols + col]; + cell.style.width = topWidths[col - 1] + "px" + } + } + } +} + +function setInputListeners() { + const inputFields = document.querySelectorAll("input"); + const loadBtn = document.querySelector('.loadMoreBtn'); + for (let inputField of inputFields) { + inputField.addEventListener("keyup", function (event) { + if (event.key === "Enter") { + clearLogs(); + loading(); + loadBtn.style.display = 'none'; + vscode.postMessage({ + loadFiltered: { + filterString: getFilterString(inputFields) + } + }); + } + }); + } +} + +/*interface Filter + image?: string; + runId?: string; + runTask?: string; +*/ +function getFilterString(inputFields) { + let filter = {}; + if (inputFields[0].value.length > 0) { //Run Id + filter.runId = inputFields[0].value; + } else if (inputFields[1].value.length > 0) { //Task id + filter.task = inputFields[1].value; + } + return filter; +} + +function clearLogs() { + let items = document.querySelectorAll("#core tbody"); + for (let item of items) { + item.remove(); + } +} +var shouldLoad = false; + +function loading() { + const loader = document.querySelector('#loadingDiv'); + if (shouldLoad) { + loader.style.display = 'flex'; + } else { + loader.style.display = 'none'; + } + shouldLoad = !shouldLoad; +} diff --git a/commands/azureCommands/acr-logs-utils/style/fabric-components/css/vscmdl2-icons.css b/commands/azureCommands/acr-logs-utils/style/fabric-components/css/vscmdl2-icons.css new file mode 100644 index 0000000000..cd0e84c017 --- /dev/null +++ b/commands/azureCommands/acr-logs-utils/style/fabric-components/css/vscmdl2-icons.css @@ -0,0 +1,71 @@ +/* + Your use of the content in the files referenced here is subject to the terms of the license at https://aka.ms/fabric-assets-license +*/ + +@font-face { + font-family: 'VSC MDL2 Assets'; + src: url('../fonts/vscmdl2-icons-d3699964.woff') format('woff'); +} + +.ms-Icon { + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + display: inline-block; + font-family: 'VSC MDL2 Assets'; + font-style: normal; + font-weight: normal; + speak: none; +} + +.ms-Icon--ChevronDown:before { + cursor: pointer; + content: "\E70D"; +} + +.ms-Icon--ChevronRight:before { + cursor: pointer; + content: "\E76C"; +} + +.ms-Icon--Clear:before { + content: "\E894"; +} + +.ms-Icon--OpenInNewWindow:before { + cursor: pointer; + content: "\E8A7"; +} + +.ms-Icon--Copy:before { + cursor: pointer; + content: "\E8C8"; +} + +.ms-Icon--StatusErrorFull:before { + color: var(--vscode-list-errorForeground); + content: "\EB90"; +} + +.ms-Icon--CompletedSolid:before { + color: var(--vscode-list-warningForeground); + content: "\EC61"; +} + +.ms-Icon--SkypeCircleClock:before { + color: #CCCCCC; + content: "\EF7E"; +} + +.ms-Icon--CaretSolidDown:before { + content: "\F08E"; +} + +.ms-Icon--MSNVideosSolid:before { + color: var(--vscode-activityBarBadge-foreground); + content: "\F2DA"; +} + +.ms-Icon--CriticalErrorSolid:before { + color: var(--vscode-list-invalidItemForeground); + content: "\F5C9"; +} diff --git a/commands/azureCommands/acr-logs-utils/style/fabric-components/fonts/vscmdl2-icons-d3699964.woff b/commands/azureCommands/acr-logs-utils/style/fabric-components/fonts/vscmdl2-icons-d3699964.woff new file mode 100644 index 0000000000..6c579dbdbb Binary files /dev/null and b/commands/azureCommands/acr-logs-utils/style/fabric-components/fonts/vscmdl2-icons-d3699964.woff differ diff --git a/commands/azureCommands/acr-logs-utils/style/fabric-components/fonts/vscmdl2-icons.ttf b/commands/azureCommands/acr-logs-utils/style/fabric-components/fonts/vscmdl2-icons.ttf new file mode 100644 index 0000000000..03770761cc Binary files /dev/null and b/commands/azureCommands/acr-logs-utils/style/fabric-components/fonts/vscmdl2-icons.ttf differ diff --git a/commands/azureCommands/acr-logs-utils/style/fabric-components/microsoft-ui-fabric-assets-license.pdf b/commands/azureCommands/acr-logs-utils/style/fabric-components/microsoft-ui-fabric-assets-license.pdf new file mode 100644 index 0000000000..47153a3b80 Binary files /dev/null and b/commands/azureCommands/acr-logs-utils/style/fabric-components/microsoft-ui-fabric-assets-license.pdf differ diff --git a/commands/azureCommands/acr-logs-utils/style/stylesheet.css b/commands/azureCommands/acr-logs-utils/style/stylesheet.css new file mode 100644 index 0000000000..112a760315 --- /dev/null +++ b/commands/azureCommands/acr-logs-utils/style/stylesheet.css @@ -0,0 +1,387 @@ +.accordion { + background-color: var(--vscode-editor-background); + color: var(--color); + cursor: pointer; + margin: 0px; + height: 30px; + width: 100%; + border: none; + text-align: left; + outline: none; + font-size: var(--vscode-editor-font-size); + font-family: var(--vscode-editor-font-family); + transition: 0.4s; + text-align: left; +} + +.accordion:hover { + cursor: pointer; + background-color: var(--vscode-list-hoverBackground); +} + +.accordion:focus { + background-color: var(--vscode-list-hoverBackground); +} + +.active { + background-color: var(--vscode-list-activeSelectionBackground); +} + +.active.accordion:focus, +.active.accordion:hover { + background-color: var(--vscode-list-activeSelectionBackground); +} + +.panel { + width: 100%; + display: none; + max-height: 0; + overflow: hidden; + transition: max-height 0.2s ease-out; +} + +table { + text-align: left; + border-collapse: collapse; + width: 100%; +} + +.widthControl { + box-sizing: border-box; + text-align: left; +} + +.solidBackground { + background-color: var(--vscode-editor-background); +} + +h2 { + padding-left: 10px; + font-size: var(--vscode-editor-font-size); + font-family: var(--vscode-editor-font-family); +} + +#core { + padding-top: 0.2cm; + table-layout: auto; + box-sizing: border-box; +} + +#core td, +#core th { + box-sizing: border-box; + padding-right: 0.35cm; + padding-left: 0.35cm; +} + +.colTitle { + cursor: pointer; + align-items: center; + display: flex; +} + +body { + padding: 0px; + width: 100%; + color: var(--color); +} + +.logConsole { + height: 100px; + overflow-y: auto; +} + +.innerTable td { + box-sizing: border-box; + border-bottom: 1px solid rgba(196, 196, 196, 0.2); + font-family: var(--vscode-editor-font-family); + font-size: var(--vscode-editor-font-size); +} + +.innerTable td.lastTd { + border-bottom: 0px; +} + +.innerTable td.arrowHolder { + border-bottom: 0px; + padding-right: 0.7cm; +} + +.innerTable td, +.innerTable th { + text-align: left; +} + +.button-holder { + box-sizing: border-box; + display: flex; + justify-content: center; + align-content: center; + align-items: center; + width: 100%; + padding-left: 0.7cm; +} + +.viewLog { + background-color: var(--vscode-button-background); + border: none; + color: var(--vscode-button-foreground); + padding: 5px 13px; + text-align: center; + text-decoration: none; + display: inline-block; + font-size: var(--vscode-editor-font-size); + cursor: pointer; + align-items: center; +} + +.loadMoreBtn { + display: none; + justify-content: center; + align-content: center; + align-items: center; + width: 100%; + margin-top: 1cm; + margin-bottom: 1cm; +} + +.viewLog:hover { + background-color: var(--vscode-button-hoverBackground); +} + +.arrow { + -webkit-transition: -webkit-transform .2s ease-in-out; + transition: transform .2s ease-in-out; +} + +.rotate180 { + transform: rotate(180deg); +} + +.activeArrow { + -webkit-transform: rotate(90deg); + transform: rotate(90deg); +} + +.paddingDiv { + display: flex; + width: 100%; + padding-top: 10px; + padding-bottom: 10px; +} + +.copy:hover { + cursor: pointer; +} + +.borderLimit { + padding-left: 40px; +} + +.sort { + display: flex; + align-items: center; +} + +#digestVisualizer { + position: absolute; + width: 1px; + visibility: hidden; +} + +.arrowHolder { + box-sizing: border-box; + width: 4.6%; + padding-right: 0.7cm; + text-align: center; +} + +.overflowX { + overflow-x: auto; +} + +.IconContainer { + font-size: 14px; + float: left; + margin: 0 5px 5px 0; + width: 50px; + height: 50px; + line-height: 51px; +} + +.IconContainer-icon { + text-align: center; +} + +.IconContainer-name, +.IconContainer-unicode { + display: none; +} + +.holder { + border-bottom: 1px solid rgba(196, 196, 196, 0.2); +} + +.textAlignRight { + text-align: right; +} + +p { + margin: 0px; +} + +.innerTable th { + border-bottom: 1px solid rgba(196, 196, 196, 0.5); +} + +.doubleLine { + border-bottom: 2px solid rgba(196, 196, 196, 0.5); +} + +main { + display: grid; + box-sizing: border-box; + margin-left: 10px; + margin-right: 10px; +} + +@media screen and (min-width: 1399px) { + main { + margin-left: 5%; + margin-right: 5% + } +} + +@media screen and (min-width: 1920px) { + main { + width: 1920px; + } +} + +#tableHead { + border-bottom: 1.1px solid var(--vscode-editor-foreground); + box-shadow: 0px 1px var(--vscode-editor-foreground); +} + +#tableHead tr { + height: 0.85cm; +} + +.dragLine { + position: absolute; + height: 80%; + width: 1px; + background-color: white; + background-clip: content-box; + padding-left: 0.35cm; + padding-right: 0.35cm; + left: 100%; + top: 10%; + cursor: w-resize; + z-index: 10; +} + +.dragWrapper { + position: relative; + height: 100%; + width: 100%; + display: flex; + justify-content: first baseline; +} + +.searchBoxes { + padding-top: 8px; + display: flex; + flex-direction: row; +} + +.searchBoxes .middle { + padding-right: 0.5%; + padding-left: 0.5%; + width: 34%; + box-sizing: border-box; +} + +.searchBoxes div { + padding-right: 0px; + width: 33%; + box-sizing: border-box; +} + +.searchBoxes div input { + padding: 5px; + border: 0px; + width: 100%; + box-sizing: border-box; + background-color: var(--vscode-list-hoverBackground); + color: var(--vscode-dropdown-foreground); +} + +.tooltip { + position: relative; +} + +.tooltip .tooltiptext { + box-sizing: border-box; + display: none; + background-color: var(--vscode-editor-foreground); + color: var(--vscode-activityBar-background); + text-align: center; + padding: 5px 16px; + position: absolute; + z-index: 1; + left: 50%; + bottom: 100%; + transform: translateX(-50%); + opacity: 0; + transition: opacity 0.5s; +} + +.tooltip .tooltiptext::after { + content: ""; + position: absolute; + top: 100%; + left: 50%; + margin-left: -5px; + border-width: 5px; + border-style: solid; + border-color: var(--vscode-editor-foreground) transparent transparent transparent; +} + +.tooltip:hover .tooltiptext { + display: inline; + opacity: 1; +} + +#loading { + display: inline-block; + border: 3px solid var(--vscode-editor-foreground); + border-radius: 50%; + border-top-color: var(--vscode-editor-background); + animation: spin 1s ease-in-out infinite; + -webkit-animation: spin 1s ease-in-out infinite; + height: calc(var(--vscode-editor-font-size)*1.5); + width: calc(var(--vscode-editor-font-size)*1.5); +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +@-webkit-keyframes spin { + to { + transform: rotate(360deg); + } +} + +#loadingDiv { + display: flex; + justify-content: center; + align-items: center; + text-align: center; + width: 100%; + margin-top: 1cm; + margin-bottom: 1cm; +} diff --git a/commands/azureCommands/acr-logs-utils/tableDataManager.ts b/commands/azureCommands/acr-logs-utils/tableDataManager.ts new file mode 100644 index 0000000000..354523d7b4 --- /dev/null +++ b/commands/azureCommands/acr-logs-utils/tableDataManager.ts @@ -0,0 +1,156 @@ +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"; +/** Class to manage data and data acquisition for logs */ +export class LogData { + public registry: Registry; + public resourceGroup: string; + public links: { requesting: boolean, url?: string }[]; + public logs: Run[]; + public client: ContainerRegistryManagementClient; + private nextLink: string; + + constructor(client: ContainerRegistryManagementClient, registry: Registry, resourceGroup: string) { + this.registry = registry; + this.resourceGroup = resourceGroup; + this.client = client; + this.logs = []; + this.links = []; + } + /** Acquires Links from an item number corresponding to the index of the corresponding log, caches + * logs in order to avoid unecessary requests if opened multiple times. + */ + public async getLink(itemNumber: number): Promise { + if (itemNumber >= this.links.length) { + throw new Error('Log for which the link was requested has not been added'); + } + + if (this.links[itemNumber].url) { + return this.links[itemNumber].url; + } + + //If user is simply clicking many times impatiently it makes sense to only have one request at once + if (this.links[itemNumber].requesting) { return 'requesting' } + + this.links[itemNumber].requesting = true; + const temp: RunGetLogResult = await this.client.runs.getLogSasUrl(this.resourceGroup, this.registry.name, this.logs[itemNumber].runId); + this.links[itemNumber].url = temp.logLink; + this.links[itemNumber].requesting = false; + return this.links[itemNumber].url + } + + //contains(TaskName, 'testTask') + //`TaskName eq 'testTask' + // + /** Loads logs from azure + * @param loadNext Determines if the next page of logs should be loaded, will throw an error if there are no more logs to load + * @param removeOld Cleans preexisting information on links and logs imediately before new requests, if loadNext is specified + * the next page of logs will be saved and all preexisting data will be deleted. + * @param filter Specifies a filter for log items, if run Id is specified this will take precedence + */ + public async loadLogs(options: { webViewEvent: boolean, loadNext: boolean, removeOld?: boolean, filter?: Filter }): Promise { + let runListResult: RunListResult; + + if (options.filter && Object.keys(options.filter).length) { + if (!options.filter.runId) { + let runOptions: { + filter?: string, + top?: number, + customHeaders?: { + [headerName: string]: string; + }; + } = {}; + runOptions.filter = await this.parseFilter(options.filter); + if (options.filter.image) { runOptions.top = 1; } + runListResult = await this.client.runs.list(this.resourceGroup, this.registry.name, runOptions); + } else { + runListResult = []; + try { + runListResult.push(await this.client.runs.get(this.resourceGroup, this.registry.name, options.filter.runId)); + } catch (err) { + const error = parseError(err); + if (!options.webViewEvent) { + throw err; + } else if (error.errorType !== "EntityNotFound") { + vscode.window.showErrorMessage(`Error '${error.errorType}': ${error.message}`); + } + } + } + } else { + if (options.loadNext) { + if (this.nextLink) { + runListResult = await this.client.runs.listNext(this.nextLink); + } else if (options.webViewEvent) { + vscode.window.showErrorMessage("No more logs to show."); + } else { + throw new Error('No more logs to show'); + } + } else { + runListResult = await this.client.runs.list(this.resourceGroup, this.registry.name); + } + } + if (options.removeOld) { + //Clear Log Items + this.logs = []; + this.links = []; + this.nextLink = ''; + } + this.nextLink = runListResult.nextLink; + this.logs = this.logs.concat(runListResult); + + const itemCount = runListResult.length; + for (let i = 0; i < itemCount; i++) { + this.links.push({ 'requesting': false }); + } + } + + public hasNextPage(): boolean { + return this.nextLink !== undefined; + } + + public isEmpty(): boolean { + return this.logs.length === 0; + } + + private async parseFilter(filter: Filter): Promise { + let parsedFilter = ""; + if (filter.task) { //Task id + 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 (parsedFilter.length > 0) { parsedFilter += ' and '; } + parsedFilter += `contains(OutputImageManifests, '${items[0]}@${digest}')`; + } + return parsedFilter; + } +} + +export interface Filter { + image?: string; + runId?: string; + task?: string; +} diff --git a/commands/azureCommands/acr-logs-utils/tableViewManager.ts b/commands/azureCommands/acr-logs-utils/tableViewManager.ts new file mode 100644 index 0000000000..6d8e6cec6b --- /dev/null +++ b/commands/azureCommands/acr-logs-utils/tableViewManager.ts @@ -0,0 +1,278 @@ + +import { ImageDescriptor, Run } from "azure-arm-containerregistry/lib/models"; +import * as clipboardy from 'clipboardy' +import * as path from 'path'; +import * as vscode from "vscode"; +import { parseError } from "vscode-azureextensionui"; +import { ext } from "../../../extensionVariables"; +import { accessLog } from './logFileManager'; +import { Filter, LogData } from './tableDataManager' +export class LogTableWebview { + private logData: LogData; + private panel: vscode.WebviewPanel; + + constructor(webviewName: string, logData: LogData) { + this.logData = logData; + this.panel = vscode.window.createWebviewPanel('log Viewer', webviewName, vscode.ViewColumn.One, { enableScripts: true, retainContextWhenHidden: true }); + + //Get path to resource on disk + const extensionPath = ext.context.extensionPath; + const scriptFile = vscode.Uri.file(path.join(extensionPath, 'commands', 'azureCommands', 'acr-logs-utils', 'logScripts.js')).with({ scheme: 'vscode-resource' }); + const styleFile = vscode.Uri.file(path.join(extensionPath, 'commands', 'azureCommands', 'acr-logs-utils', 'style', 'stylesheet.css')).with({ scheme: 'vscode-resource' }); + const iconStyle = vscode.Uri.file(path.join(extensionPath, 'commands', 'azureCommands', 'acr-logs-utils', 'style', 'fabric-components', 'css', 'vscmdl2-icons.css')).with({ scheme: 'vscode-resource' }); + //Populate Webview + this.panel.webview.html = this.getBaseHtml(scriptFile, styleFile, iconStyle); + this.setupIncomingListeners(); + this.addLogsToWebView(); + } + //Post Opening communication from webview + /** Setup communication with the webview sorting out received mesages from its javascript file */ + private setupIncomingListeners(): void { + this.panel.webview.onDidReceiveMessage(async (message: IMessage) => { + if (message.logRequest) { + const itemNumber: number = +message.logRequest.id; + try { + await this.logData.getLink(itemNumber).then(async (url) => { + if (url !== 'requesting') { + await accessLog(url, this.logData.logs[itemNumber].runId, message.logRequest.download); + } + }); + } catch (err) { + const error = parseError(err); + vscode.window.showErrorMessage(`Error '${error.errorType}': ${error.message}`); + } + } else if (message.copyRequest) { + // tslint:disable-next-line:no-unsafe-any + clipboardy.writeSync(message.copyRequest.text); + + } else if (message.loadMore) { + const alreadyLoaded = this.logData.logs.length; + await this.logData.loadLogs({ + webViewEvent: true, + loadNext: true + }); + this.addLogsToWebView(alreadyLoaded); + + } else if (message.loadFiltered) { + await this.logData.loadLogs({ + webViewEvent: true, + loadNext: false, + removeOld: true, + filter: message.loadFiltered.filterString + }); + this.addLogsToWebView(); + } + }); + } + + //Content Management + /** Communicates with the webview javascript file through post requests to populate the log table */ + private addLogsToWebView(startItem?: number): void { + const begin = startItem ? startItem : 0; + for (let i = begin; i < this.logData.logs.length; i++) { + const log = this.logData.logs[i]; + this.panel.webview.postMessage({ + 'type': 'populate', + 'id': i, + 'logComponent': this.getLogTableItem(log, i) + }); + } + if (startItem) { + this.panel.webview.postMessage({ 'type': 'endContinued', 'canLoadMore': this.logData.hasNextPage() }); + } else { + this.panel.webview.postMessage({ 'type': 'end', 'canLoadMore': this.logData.hasNextPage() }); + } + } + + private getImageOutputTable(log: Run): string { + let imageOutput: string = ''; + if (log.outputImages) { + //Adresses strange error where the image list can exist and contain only one null item. + if (!log.outputImages[0]) { + imageOutput += this.getImageItem(true); + } else { + for (let j = 0; j < log.outputImages.length; j++) { + let img = log.outputImages[j] + imageOutput += this.getImageItem(j === log.outputImages.length - 1, img); + } + } + } else { + imageOutput += this.getImageItem(true); + } + return imageOutput; + } + + //HTML Content Loaders + /** Create the table in which to push the logs */ + private getBaseHtml(scriptFile: vscode.Uri, stylesheet: vscode.Uri, iconStyles: vscode.Uri): string { + return ` + + + + + + + + Logs + + + +
+
+
+ Filter by ID:
+ +
+
+ Filter by Task:
+ +
+
+ + + + + + + + + + + + + + + + + + + +
ID Task Status Created Elapsed Time Platform
+
+
+ Loading     +
+
+ +
+ + `; + } + + private getLogTableItem(log: Run, logId: number): string { + const task: string = log.task ? log.task : ''; + const prettyDate: string = log.createTime ? this.getPrettyDate(log.createTime) : ''; + const timeElapsed: string = log.startTime && log.finishTime ? Math.ceil((log.finishTime.valueOf() - log.startTime.valueOf()) / 1000).toString() + 's' : ''; + const osType: string = log.platform.os ? log.platform.os : ''; + const name: string = log.name ? log.name : ''; + const imageOutput: string = this.getImageOutputTable(log); + const statusIcon: string = this.getLogStatusIcon(log.status); + + return ` + + +
+ ${name} + ${task} + ${statusIcon} ${log.status} + ${prettyDate} + ${timeElapsed} + ${osType} + + + +
+ + + + + + + + + ${imageOutput} +
 TagRepositoryDigest +

Log

+
+
+ + + ` + } + + private getImageItem(islastTd: boolean, img?: ImageDescriptor): string { + if (img) { + const tag: string = img.tag ? img.tag : ''; + const repository: string = img.repository ? img.repository : ''; + const digest: string = img.digest ? img.digest : ''; + const truncatedDigest: string = digest ? digest.substr(0, 5) + '...' + digest.substr(digest.length - 5) : ''; + const lastTd: string = islastTd ? 'lastTd' : ''; + return ` +   + ${tag} + ${repository} + + + ${truncatedDigest} + ${digest} + + + + + ` + } else { + return ` +   + NA + NA + NA + + `; + } + + } + + private getLogStatusIcon(status?: string): string { + if (!status) { return ''; } + switch (status) { + case 'Error': + return ''; + case 'Failed': + return ''; + case 'Succeeded': + return ''; + case 'Queued': + return ''; + case 'Running': + return ''; + default: + return ''; + } + } + + private getPrettyDate(date: Date): string { + let currentDate = new Date(); + let secs = Math.floor((currentDate.getTime() - date.getTime()) / 1000); + if (secs === 1) { return "1 second ago"; } + if (secs < 60) { return secs + " seconds ago"; } + if (secs < 120) { return " 1 minute ago"; } + if (secs < 3600) { return Math.floor(secs / 60) + " minutes ago"; } + if (secs < 7200) { return Math.floor(secs / 60) + "1 hour ago"; } + if (secs < 86400) { return Math.floor(secs / 3600) + " hours ago"; } + if (secs < 172800) { return "1 day ago"; } + if (secs < 604800) { return Math.floor(secs / 86400) + " days ago"; } + if (secs < 1209600) { return "1 week ago"; } + if (secs < 2592000) { return Math.floor(secs / 604800) + " weeks ago"; } + if (secs < 5184000) { return "1 month ago"; } + if (secs < 31536000) { return Math.floor(secs / 2592000) + " months ago"; } + if (secs < 63072000) { return "1 year ago"; } + return Math.floor(secs / 31536000) + " years ago"; + } +} + +interface IMessage { + logRequest?: { id: number; download: boolean }; + copyRequest?: { text: string }; + loadMore?: string; + loadFiltered?: { filterString: Filter }; +} diff --git a/commands/azureCommands/acr-logs.ts b/commands/azureCommands/acr-logs.ts new file mode 100644 index 0000000000..ee09302b88 --- /dev/null +++ b/commands/azureCommands/acr-logs.ts @@ -0,0 +1,81 @@ +"use strict"; + +import { Registry } from "azure-arm-containerregistry/lib/models"; +import { Subscription } from "azure-arm-resource/lib/subscription/models"; +import * as vscode from "vscode"; +import { AzureImageTagNode, AzureRegistryNode } from '../../explorer/models/azureRegistryNodes'; +import { TaskNode } from "../../explorer/models/taskNode"; +import { getResourceGroupName, getSubscriptionFromRegistry } from '../../utils/Azure/acrTools'; +import { AzureUtilityManager } from '../../utils/azureUtilityManager'; +import { quickPickACRRegistry } from '../utils/quick-pick-azure' +import { accessLog } from "./acr-logs-utils/logFileManager"; +import { LogData } from "./acr-logs-utils/tableDataManager"; +import { LogTableWebview } from "./acr-logs-utils/tableViewManager"; + +/** This command is used through a right click on an azure registry, repository or image in the Docker Explorer. It is used to view ACR logs for a given item. */ +export async function viewACRLogs(context: AzureRegistryNode | AzureImageTagNode | TaskNode): Promise { + let registry: Registry; + let subscription: Subscription; + if (!context) { + registry = await quickPickACRRegistry(); + subscription = await getSubscriptionFromRegistry(registry); + } else { + registry = context.registry; + subscription = context.subscription; + } + let resourceGroup: string = getResourceGroupName(registry); + const client = await AzureUtilityManager.getInstance().getContainerRegistryManagementClient(subscription); + let logData: LogData = new LogData(client, registry, resourceGroup); + + // Filtering provided + if (context && context instanceof AzureImageTagNode) { + //ACR Image Logs + await logData.loadLogs({ + webViewEvent: false, + loadNext: false, + removeOld: false, + filter: { image: context.label } + }); + if (!hasValidLogContent(context, logData)) { return; } + const url = await logData.getLink(0); + await accessLog(url, logData.logs[0].runId, false); + } else { + if (context && context instanceof TaskNode) { + //ACR Task Logs + await logData.loadLogs({ + webViewEvent: false, + loadNext: false, + removeOld: false, + filter: { task: context.label } + }); + } else { + //ACR Registry Logs + await logData.loadLogs({ + webViewEvent: false, + loadNext: false + }); + } + if (!hasValidLogContent(context, logData)) { return; } + let webViewTitle = registry.name; + if (context instanceof TaskNode) { + webViewTitle += '/' + context.label; + } + const webview = new LogTableWebview(webViewTitle, logData); + } +} + +function hasValidLogContent(context: AzureRegistryNode | AzureImageTagNode | TaskNode, logData: LogData): boolean { + if (logData.logs.length === 0) { + let itemType: string; + if (context && context instanceof TaskNode) { + itemType = 'task'; + } else if (context && context instanceof AzureImageTagNode) { + itemType = 'image'; + } else { + itemType = 'registry'; + } + vscode.window.showInformationMessage(`This ${itemType} has no associated logs`); + return false; + } + return true; +} diff --git a/commands/azureCommands/create-registry.ts b/commands/azureCommands/create-registry.ts index 816c428aaf..d81196d48b 100644 --- a/commands/azureCommands/create-registry.ts +++ b/commands/azureCommands/create-registry.ts @@ -7,6 +7,7 @@ import { Registry, RegistryNameStatus } from "azure-arm-containerregistry/lib/mo import { SubscriptionModels } from 'azure-arm-resource'; import { ResourceGroup } from "azure-arm-resource/lib/resource/models"; import * as vscode from "vscode"; +import { skus } from '../../constants'; import { dockerExplorerProvider } from '../../dockerExtension'; import { ext } from '../../extensionVariables'; import { isValidAzureName } from '../../utils/Azure/common'; diff --git a/commands/azureCommands/pull-from-azure.ts b/commands/azureCommands/pull-from-azure.ts index f708a1dda9..b2c2bd91dd 100644 --- a/commands/azureCommands/pull-from-azure.ts +++ b/commands/azureCommands/pull-from-azure.ts @@ -6,7 +6,7 @@ import * as assert from 'assert'; import { Registry } from "azure-arm-containerregistry/lib/models"; import { exec } from 'child_process'; -import * as fs from 'fs'; +import * as fse from 'fs-extra'; import * as path from "path"; import vscode = require('vscode'); import { callWithTelemetryAndErrorHandling, IActionContext, parseError } from 'vscode-azureextensionui'; @@ -97,7 +97,7 @@ async function isLoggedIntoDocker(registryName: string): Promise<{ configPath: s await callWithTelemetryAndErrorHandling('findDockerConfig', async function (this: IActionContext): Promise { this.suppressTelemetry = true; - buffer = fs.readFileSync(configPath); + buffer = fse.readFileSync(configPath); }); let index = buffer.indexOf(registryName); diff --git a/commands/azureCommands/quick-build.ts b/commands/azureCommands/quick-build.ts new file mode 100644 index 0000000000..9e107db2c4 --- /dev/null +++ b/commands/azureCommands/quick-build.ts @@ -0,0 +1,94 @@ +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'); + +// 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; +} diff --git a/commands/azureCommands/run-task.ts b/commands/azureCommands/run-task.ts new file mode 100644 index 0000000000..f4f6f0dc37 --- /dev/null +++ b/commands/azureCommands/run-task.ts @@ -0,0 +1,42 @@ +import { TaskRunRequest } from "azure-arm-containerregistry/lib/models"; +import { Registry } from "azure-arm-containerregistry/lib/models"; +import { ResourceGroup } from "azure-arm-resource/lib/resource/models"; +import { Subscription } from "azure-arm-resource/lib/subscription/models"; +import vscode = require('vscode'); +import { parseError } from "vscode-azureextensionui"; +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'; + +export async function runTask(context?: TaskNode): Promise { + let taskName: string; + let subscription: Subscription; + let resourceGroup: ResourceGroup; + let registry: Registry; + + if (context) { // Right Click + subscription = context.subscription; + registry = context.registry; + resourceGroup = await acrTools.getResourceGroup(registry, subscription); + taskName = context.task.name; + } else { // Command Palette + subscription = await quickPickSubscription(); + registry = await quickPickACRRegistry(); + resourceGroup = await acrTools.getResourceGroup(registry, subscription); + taskName = (await quickPickTask(registry, subscription, resourceGroup)).name; + } + + const client = await AzureUtilityManager.getInstance().getContainerRegistryManagementClient(subscription); + let runRequest: TaskRunRequest = { + type: 'TaskRunRequest', + taskName: taskName + }; + + try { + let taskRun = await client.registries.scheduleRun(resourceGroup.name, registry.name, runRequest); + vscode.window.showInformationMessage(`Successfully scheduled the Task '${taskName}' with ID '${taskRun.runId}'.`); + } catch (err) { + throw new Error(`Failed to schedule the Task '${taskName}'\nError: '${parseError(err).message}'`); + } +} diff --git a/commands/azureCommands/show-task.ts b/commands/azureCommands/show-task.ts new file mode 100644 index 0000000000..e515eda150 --- /dev/null +++ b/commands/azureCommands/show-task.ts @@ -0,0 +1,32 @@ +import { Registry, Task } from "azure-arm-containerregistry/lib/models"; +import { ResourceGroup } from "azure-arm-resource/lib/resource/models"; +import { Subscription } from "azure-arm-resource/lib/subscription/models"; +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 { openTask } from "./task-utils/showTaskManager"; + +export async function showTaskProperties(context?: TaskNode): Promise { + let subscription: Subscription; + let registry: Registry; + let resourceGroup: ResourceGroup; + let task: string; + + if (context) { // Right click + subscription = context.subscription; + registry = context.registry; + resourceGroup = await acrTools.getResourceGroup(registry, subscription); + task = context.task.name; + } else { // Command palette + subscription = await quickPickSubscription(); + registry = await quickPickACRRegistry(); + resourceGroup = await acrTools.getResourceGroup(registry, subscription); + task = (await quickPickTask(registry, subscription, resourceGroup)).name; + } + + const client = await AzureUtilityManager.getInstance().getContainerRegistryManagementClient(subscription); + let item: Task = await client.tasks.get(resourceGroup.name, registry.name, task); + let indentation = 2; + openTask(JSON.stringify(item, undefined, indentation), task); +} diff --git a/commands/azureCommands/task-utils/showTaskManager.ts b/commands/azureCommands/task-utils/showTaskManager.ts new file mode 100644 index 0000000000..19d5b2ff78 --- /dev/null +++ b/commands/azureCommands/task-utils/showTaskManager.ts @@ -0,0 +1,38 @@ +import * as vscode from 'vscode'; + +export class TaskContentProvider implements vscode.TextDocumentContentProvider { + public static scheme: string = 'task'; + private onDidChangeEvent: vscode.EventEmitter = new vscode.EventEmitter(); + + constructor() { } + + public provideTextDocumentContent(uri: vscode.Uri): string { + const parse: { content: string } = <{ content: string }>JSON.parse(uri.query); + return decodeBase64(parse.content); + } + + get onDidChange(): vscode.Event { + return this.onDidChangeEvent.event; + } + + public update(uri: vscode.Uri, message: string): void { + this.onDidChangeEvent.fire(uri); + } +} + +export function decodeBase64(str: string): string { + return Buffer.from(str, 'base64').toString('utf8'); +} + +export function encodeBase64(str: string): string { + return Buffer.from(str, 'ascii').toString('base64'); +} + +export function openTask(content: string, title: string): void { + const scheme = 'task'; + let query = JSON.stringify({ 'content': encodeBase64(content) }); + let uri: vscode.Uri = vscode.Uri.parse(`${scheme}://authority/${title}.json?${query}#idk`); + vscode.workspace.openTextDocument(uri).then((doc) => { + return vscode.window.showTextDocument(doc, vscode.ViewColumn.Active + 1, true); + }); +} diff --git a/commands/build-image.ts b/commands/build-image.ts index 0d758beb8b..2116f6424f 100644 --- a/commands/build-image.ts +++ b/commands/build-image.ts @@ -16,7 +16,7 @@ async function getDockerFileUris(folder: vscode.WorkspaceFolder): Promise { +export async function resolveDockerFileItem(rootFolder: vscode.WorkspaceFolder, dockerFileUri: vscode.Uri | undefined): Promise { if (dockerFileUri) { return createDockerfileItem(rootFolder, dockerFileUri); } diff --git a/commands/registrySettings.ts b/commands/registrySettings.ts index dceb316126..e264a46eeb 100644 --- a/commands/registrySettings.ts +++ b/commands/registrySettings.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See LICENSE.md in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; import * as vscode from 'vscode'; import { DialogResponses } from 'vscode-azureextensionui'; import { configurationKeys } from '../constants'; diff --git a/commands/start-container.ts b/commands/start-container.ts index 8f78ece3eb..e6d30709eb 100644 --- a/commands/start-container.ts +++ b/commands/start-container.ts @@ -4,14 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import * as cp from 'child_process'; -import * as fs from 'fs'; +import * as fse from 'fs-extra'; import os = require('os'); import vscode = require('vscode'); import { IActionContext, parseError } from 'vscode-azureextensionui'; import { ImageNode } from '../explorer/models/imageNode'; import { RootNode } from '../explorer/models/rootNode'; import { ext } from '../extensionVariables'; -import { reporter } from '../telemetry/telemetry'; import { docker, DockerEngineType } from './utils/docker-endpoint'; import { ImageItem, quickPickImage } from './utils/quick-pick-image'; @@ -93,13 +92,13 @@ export async function startAzureCLI(actionContext: IActionContext): Promise { const placeHolder = prompt ? prompt : 'Select image to use'; @@ -33,6 +35,16 @@ export async function quickPickACRRepository(registry: Registry, prompt?: string return desiredRepo.data; } +export async function quickPickTask(registry: Registry, subscription: Subscription, resourceGroup: ResourceGroup, prompt?: string): Promise { + const placeHolder = prompt ? prompt : 'Choose a Task'; + + const client = await AzureUtilityManager.getInstance().getContainerRegistryManagementClient(subscription); + let tasks: ContainerModels.Task[] = await client.tasks.list(resourceGroup.name, registry.name); + const quickpPickBTList = tasks.map(task => >{ label: task.name, data: task }); + let desiredTask = await ext.ui.showQuickPick(quickpPickBTList, { 'canPickMany': false, 'placeHolder': placeHolder }); + return desiredTask.data; +} + export async function quickPickACRRegistry(canCreateNew: boolean = false, prompt?: string): Promise { const placeHolder = prompt ? prompt : 'Select registry to use'; let registries = await AzureUtilityManager.getInstance().getRegistries(); @@ -47,7 +59,7 @@ export async function quickPickACRRegistry(canCreateNew: boolean = false, prompt }); let registry: Registry; if (desiredReg === createNewItem) { - registry = await vscode.commands.executeCommand("vscode-docker.create-ACR-Registry"); + registry = await createRegistry(); } else { registry = desiredReg.data; } @@ -153,7 +165,6 @@ async function createNewResourceGroup(loc: string, subscription?: Subscription): }; let resourceGroupName: string = await ext.ui.showInputBox(opt); - let newResourceGroup: ResourceGroup = { name: resourceGroupName, location: loc, diff --git a/commands/utils/quick-pick-container.ts b/commands/utils/quick-pick-container.ts index 09e2b56e28..738ba9093b 100644 --- a/commands/utils/quick-pick-container.ts +++ b/commands/utils/quick-pick-container.ts @@ -9,7 +9,6 @@ import * as os from 'os'; import vscode = require('vscode'); import { IActionContext, parseError, TelemetryProperties } from 'vscode-azureextensionui'; import { ext } from '../../extensionVariables'; -import { openShellContainer } from '../open-shell-container'; import { docker } from './docker-endpoint'; export interface ContainerItem extends vscode.QuickPickItem { diff --git a/commands/utils/quick-pick-image.ts b/commands/utils/quick-pick-image.ts index a9c9454c42..4ffd483bfa 100644 --- a/commands/utils/quick-pick-image.ts +++ b/commands/utils/quick-pick-image.ts @@ -2,11 +2,14 @@ * 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 { IActionContext, parseError, TelemetryProperties } from 'vscode-azureextensionui'; +import { DialogResponses, IActionContext, parseError, TelemetryProperties } from 'vscode-azureextensionui'; +import { delay } from '../../explorer/utils/utils'; import { ext } from '../../extensionVariables'; +import { Item, resolveDockerFileItem } from '../build-image'; +import { addImageTaggingTelemetry, getTagFromUserInput } from '../tag-image'; import { docker } from './docker-endpoint'; export interface ImageItem extends vscode.QuickPickItem { @@ -78,3 +81,51 @@ export async function quickPickImage(actionContext: IActionContext, includeAll?: return response; } } + +export async function quickPickImageName(actionContext: IActionContext, rootFolder: vscode.WorkspaceFolder, dockerFileItem: Item | undefined): Promise { + let absFilePath: string = path.join(rootFolder.uri.fsPath, dockerFileItem.relativeFilePath); + let dockerFileKey = `ACR_buildTag_${absFilePath}`; + let prevImageName: string | undefined = ext.context.globalState.get(dockerFileKey); + let suggestedImageName: string; + + if (!prevImageName) { + // Get imageName based on name of subfolder containing the Dockerfile, or else workspacefolder + suggestedImageName = path.basename(dockerFileItem.relativeFolderPath).toLowerCase(); + if (suggestedImageName === '.') { + suggestedImageName = path.basename(rootFolder.uri.fsPath).toLowerCase().replace(/\s/g, ''); + } + + suggestedImageName += ":{{.Run.ID}}" + } else { + suggestedImageName = prevImageName; + } + + // Temporary work-around for vscode bug where valueSelection can be messed up if a quick pick is followed by a showInputBox + await delay(500); + + addImageTaggingTelemetry(actionContext, suggestedImageName, '.before'); + const imageName: string = await getTagFromUserInput(suggestedImageName, false); + addImageTaggingTelemetry(actionContext, imageName, '.after'); + + await ext.context.globalState.update(dockerFileKey, imageName); + return imageName; +} + +export async function quickPickDockerFileItem(actionContext: IActionContext, dockerFileUri: vscode.Uri | undefined, rootFolder: vscode.WorkspaceFolder): Promise { + let dockerFileItem: Item; + + while (!dockerFileItem) { + let resolvedItem: Item | undefined = await resolveDockerFileItem(rootFolder, dockerFileUri); + if (resolvedItem) { + dockerFileItem = resolvedItem; + } else { + let msg = "Couldn't find a Dockerfile in your workspace. Would you like to add Docker files to the workspace?"; + actionContext.properties.cancelStep = msg; + await ext.ui.showWarningMessage(msg, DialogResponses.yes, DialogResponses.cancel); + actionContext.properties.cancelStep = undefined; + await vscode.commands.executeCommand('vscode-docker.configure'); + // Try again + } + } + return dockerFileItem; +} diff --git a/configureWorkspace/configure.ts b/configureWorkspace/configure.ts index 1b99dea27f..4b1c6cf9a7 100644 --- a/configureWorkspace/configure.ts +++ b/configureWorkspace/configure.ts @@ -4,10 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import * as fs from 'fs'; import * as fse from 'fs-extra'; import * as gradleParser from "gradle-to-js/lib/parser"; -import { EOL } from 'os'; import * as path from "path"; import * as pomParser from "pom-parser"; import * as vscode from "vscode"; @@ -147,7 +145,7 @@ async function readPackageJson(folderPath: string): Promise<{ packagePath?: stri if (uris && uris.length > 0) { packagePath = uris[0].fsPath; - const json = JSON.parse(fs.readFileSync(packagePath, 'utf8')); + const json = JSON.parse(fse.readFileSync(packagePath, 'utf8')); if (json.scripts && typeof json.scripts.start === "string") { packageInfo.npmStart = true; @@ -426,7 +424,7 @@ async function configureCore(actionContext: IActionContext, options: ConfigureAp // Paths in the docker files should be relative to the Dockerfile (which is in the output folder) let fileContents = generatorFunction(serviceNameAndPathRelativeToOutput, platformType, os, port, packageInfo); if (fileContents) { - fs.writeFileSync(filePath, fileContents, { encoding: 'utf8' }); + fse.writeFileSync(filePath, fileContents, { encoding: 'utf8' }); filesWritten.push(filePath); } } diff --git a/constants.ts b/constants.ts index aafc08acf0..01bb59df85 100644 --- a/constants.ts +++ b/constants.ts @@ -27,3 +27,6 @@ export const NULL_GUID = '00000000-0000-0000-0000-000000000000'; //Empty GUID is //Azure Container Registries export const skus = ["Standard", "Basic", "Premium"]; + +//Repository + Tag format +export const imageTagRegExp = new RegExp('^[a-zA-Z0-9.-_/]{1,256}:(?![.-])[a-zA-Z0-9.-_]{1,128}$'); diff --git a/debugging/coreclr/vsdbgClient.ts b/debugging/coreclr/vsdbgClient.ts index 58f8b33548..0e2131f0af 100644 --- a/debugging/coreclr/vsdbgClient.ts +++ b/debugging/coreclr/vsdbgClient.ts @@ -4,8 +4,8 @@ import * as path from 'path'; import * as process from 'process'; -import * as request from 'request-promise-native'; import { Memento } from 'vscode'; +import { ext } from '../../extensionVariables'; import { FileSystemProvider } from './fsProvider'; import { OSProvider } from './osProvider'; import { OutputManager } from './outputManager'; @@ -101,7 +101,7 @@ export class RemoteVsDbgClient implements VsDbgClient { await this.fileSystemProvider.makeDir(this.vsdbgPath); } - const script = await request(this.options.url); + const script = await ext.request(this.options.url); await this.fileSystemProvider.writeFile(vsdbgAcquisitionScriptPath, script); diff --git a/dockerExtension.ts b/dockerExtension.ts index 95bded7761..b4b17ad753 100644 --- a/dockerExtension.ts +++ b/dockerExtension.ts @@ -6,17 +6,22 @@ let loadStartTime = Date.now(); import * as assert from 'assert'; -import * as opn from 'opn'; import * as path from 'path'; import * as request from 'request-promise-native'; import * as vscode from 'vscode'; -import { AzureUserInput, callWithTelemetryAndErrorHandling, createTelemetryReporter, IActionContext, parseError, registerCommand as uiRegisterCommand, registerUIExtensionVariables, TelemetryProperties, UserCancelledError } from 'vscode-azureextensionui'; +import { AzureUserInput, callWithTelemetryAndErrorHandling, createTelemetryReporter, IActionContext, registerCommand as uiRegisterCommand, registerUIExtensionVariables, TelemetryProperties, UserCancelledError } from 'vscode-azureextensionui'; import { ConfigurationParams, DidChangeConfigurationNotification, DocumentSelector, LanguageClient, LanguageClientOptions, Middleware, ServerOptions, TransportKind } from 'vscode-languageclient/lib/main'; +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 { 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 { showTaskProperties } from "./commands/azureCommands/show-task"; +import { TaskContentProvider } from "./commands/azureCommands/task-utils/showTaskManager"; import { buildImage } from './commands/build-image'; import { composeDown, composeRestart, composeUp } from './commands/docker-compose'; import inspectImage from './commands/inspect-image'; @@ -43,7 +48,7 @@ import { DockerComposeParser } from './dockerCompose/dockerComposeParser'; import { DockerfileCompletionItemProvider } from './dockerfile/dockerfileCompletionItemProvider'; import DockerInspectDocumentContentProvider, { SCHEME as DOCKER_INSPECT_SCHEME } from './documentContentProviders/dockerInspect'; import { AzureAccountWrapper } from './explorer/deploy/azureAccountWrapper'; -import * as util from "./explorer/deploy/util"; +import * as util from './explorer/deploy/util'; import { WebAppCreator } from './explorer/deploy/webAppCreator'; import { DockerExplorerProvider } from './explorer/dockerExplorer'; import { AzureImageTagNode, AzureRegistryNode, AzureRepositoryNode } from './explorer/models/azureRegistryNodes'; @@ -55,9 +60,8 @@ import { NodeBase } from './explorer/models/nodeBase'; import { RootNode } from './explorer/models/rootNode'; import { browseAzurePortal } from './explorer/utils/browseAzurePortal'; import { browseDockerHub, dockerHubLogout } from './explorer/utils/dockerHubUtils'; -import { ext } from "./extensionVariables"; +import { ext } from './extensionVariables'; import { initializeTelemetryReporter, reporter } from './telemetry/telemetry'; -import { AzureAccount } from './typings/azure-account.api'; import { addUserAgent } from './utils/addUserAgent'; import { AzureUtilityManager } from './utils/azureUtilityManager'; import { Keytar } from './utils/keytar'; @@ -68,242 +72,322 @@ export const DOCKERFILE_GLOB_PATTERN = '**/{*.dockerfile,[dD]ocker[fF]ile}'; export let dockerExplorerProvider: DockerExplorerProvider; -export type KeyInfo = { [keyName: string]: string; }; +export type KeyInfo = { [keyName: string]: string }; export interface ComposeVersionKeys { - All: KeyInfo, - v1: KeyInfo, - v2: KeyInfo + All: KeyInfo; + v1: KeyInfo; + v2: KeyInfo; } let client: LanguageClient; const DOCUMENT_SELECTOR: DocumentSelector = [ - { language: 'dockerfile', scheme: 'file' } + { language: 'dockerfile', scheme: 'file' } ]; function initializeExtensionVariables(ctx: vscode.ExtensionContext): void { - if (!ext.ui) { - // This allows for standard interactions with the end user (as opposed to test input) - ext.ui = new AzureUserInput(ctx.globalState); - } - ext.context = ctx; - ext.outputChannel = util.getOutputChannel(); - if (!ext.terminalProvider) { - ext.terminalProvider = new DefaultTerminalProvider(); - } - initializeTelemetryReporter(createTelemetryReporter(ctx)); - ext.reporter = reporter; - if (!ext.keytar) { - ext.keytar = Keytar.tryCreate(); - } - - registerUIExtensionVariables(ext); + if (!ext.ui) { + // This allows for standard interactions with the end user (as opposed to test input) + ext.ui = new AzureUserInput(ctx.globalState); + } + ext.context = ctx; + ext.outputChannel = util.getOutputChannel(); + if (!ext.terminalProvider) { + ext.terminalProvider = new DefaultTerminalProvider(); + } + initializeTelemetryReporter(createTelemetryReporter(ctx)); + ext.reporter = reporter; + if (!ext.keytar) { + ext.keytar = Keytar.tryCreate(); + } + + registerUIExtensionVariables(ext); } export async function activate(ctx: vscode.ExtensionContext): Promise { - let activateStartTime = Date.now(); - - initializeExtensionVariables(ctx); - - // Set up the user agent for all direct 'request' calls in the extension (must use ext.request) - let defaultRequestOptions = {}; - addUserAgent(defaultRequestOptions); - ext.request = request.defaults(defaultRequestOptions); - - await callWithTelemetryAndErrorHandling('docker.activate', async function (this: IActionContext): Promise { - this.properties.isActivationEvent = 'true'; - this.measurements.mainFileLoad = (loadEndTime - loadStartTime) / 1000; - this.measurements.mainFileLoadedToActivate = (activateStartTime - loadEndTime) / 1000; - - ctx.subscriptions.push(vscode.languages.registerCompletionItemProvider(DOCUMENT_SELECTOR, new DockerfileCompletionItemProvider(), '.')); - - const YAML_MODE_ID: vscode.DocumentFilter = { language: 'yaml', scheme: 'file', pattern: COMPOSE_FILE_GLOB_PATTERN }; - let yamlHoverProvider = new DockerComposeHoverProvider(new DockerComposeParser(), composeVersionKeys.All); - ctx.subscriptions.push(vscode.languages.registerHoverProvider(YAML_MODE_ID, yamlHoverProvider)); - ctx.subscriptions.push(vscode.languages.registerCompletionItemProvider(YAML_MODE_ID, new DockerComposeCompletionItemProvider(), '.')); - ctx.subscriptions.push(vscode.workspace.registerTextDocumentContentProvider(DOCKER_INSPECT_SCHEME, new DockerInspectDocumentContentProvider())); - - registerDockerCommands(); - - ctx.subscriptions.push(vscode.debug.registerDebugConfigurationProvider('docker', new DockerDebugConfigProvider())); - registerDebugConfigurationProvider(ctx); - - await consolidateDefaultRegistrySettings(); - activateLanguageClient(ctx); - - // Start loading the Azure account after we're completely done activating. - setTimeout( - // Do not wait - // tslint:disable-next-line:promise-function-async - () => AzureUtilityManager.getInstance().tryGetAzureAccount(), - 1); - }); + let activateStartTime = Date.now(); + + initializeExtensionVariables(ctx); + + // Set up the user agent for all direct 'request' calls in the extension (must use ext.request) + let defaultRequestOptions = {}; + addUserAgent(defaultRequestOptions); + ext.request = request.defaults(defaultRequestOptions); + + await callWithTelemetryAndErrorHandling('docker.activate', async function (this: IActionContext): Promise { + this.properties.isActivationEvent = 'true'; + this.measurements.mainFileLoad = (loadEndTime - loadStartTime) / 1000; + this.measurements.mainFileLoadedToActivate = (activateStartTime - loadEndTime) / 1000; + + ctx.subscriptions.push( + vscode.languages.registerCompletionItemProvider( + DOCUMENT_SELECTOR, + new DockerfileCompletionItemProvider(), + '.' + ) + ); + + const YAML_MODE_ID: vscode.DocumentFilter = { + language: 'yaml', + scheme: 'file', + pattern: COMPOSE_FILE_GLOB_PATTERN + }; + let yamlHoverProvider = new DockerComposeHoverProvider( + new DockerComposeParser(), + composeVersionKeys.All + ); + ctx.subscriptions.push( + vscode.languages.registerHoverProvider(YAML_MODE_ID, yamlHoverProvider) + ); + ctx.subscriptions.push( + vscode.languages.registerCompletionItemProvider( + YAML_MODE_ID, + new DockerComposeCompletionItemProvider(), + "." + ) + ); + + ctx.subscriptions.push( + vscode.workspace.registerTextDocumentContentProvider( + DOCKER_INSPECT_SCHEME, + new DockerInspectDocumentContentProvider() + ) + ); + ctx.subscriptions.push( + vscode.workspace.registerTextDocumentContentProvider( + LogContentProvider.scheme, + new LogContentProvider() + ) + ); + ctx.subscriptions.push( + vscode.workspace.registerTextDocumentContentProvider( + TaskContentProvider.scheme, + new TaskContentProvider() + ) + ); + + registerDockerCommands(); + + ctx.subscriptions.push( + vscode.debug.registerDebugConfigurationProvider( + 'docker', + new DockerDebugConfigProvider() + ) + ); + registerDebugConfigurationProvider(ctx); + + await consolidateDefaultRegistrySettings(); + activateLanguageClient(ctx); + + // Start loading the Azure account after we're completely done activating. + setTimeout( + // Do not wait + // tslint:disable-next-line:promise-function-async + () => AzureUtilityManager.getInstance().tryGetAzureAccount(), + 1); + }); } async function createWebApp(context?: AzureImageTagNode | DockerHubImageTagNode): Promise { - assert(!!context, "Should not be available through command palette"); - - let azureAccount = await AzureUtilityManager.getInstance().requireAzureAccount(); - const azureAccountWrapper = new AzureAccountWrapper(ext.context, azureAccount); - const wizard = new WebAppCreator(ext.outputChannel, azureAccountWrapper, context); - const result = await wizard.run(); - if (result.status === 'Faulted') { - throw result.error; - } else if (result.status === 'Cancelled') { - throw new UserCancelledError(); - } + assert(!!context, "Should not be available through command palette"); + + let azureAccount = await AzureUtilityManager.getInstance().requireAzureAccount(); + const azureAccountWrapper = new AzureAccountWrapper(ext.context, azureAccount); + const wizard = new WebAppCreator(ext.outputChannel, azureAccountWrapper, context); + const result = await wizard.run(); + if (result.status === 'Faulted') { + throw result.error; + } else if (result.status === 'Cancelled') { + throw new UserCancelledError(); + } } // Remove this when https://github.com/Microsoft/vscode-docker/issues/445 fixed -// tslint:disable-next-line:no-any -function registerCommand(commandId: string, callback: (this: IActionContext, ...args: any[]) => any): void { - return uiRegisterCommand( - commandId, - // tslint:disable-next-line:no-function-expression no-any - async function (this: IActionContext, ...args: any[]): Promise { - if (args.length) { - let properties: { - contextValue?: string; - } & TelemetryProperties = this.properties; - const contextArg = args[0]; - - if (contextArg instanceof NodeBase) { - properties.contextValue = contextArg.contextValue; - } else if (contextArg instanceof vscode.Uri) { - properties.contextValue = 'Uri'; - } - } - - return callback.call(this, ...args); - }); +function registerCommand( + commandId: string, + // tslint:disable-next-line: no-any + callback: (this: IActionContext, ...args: any[]) => any +): void { + return uiRegisterCommand( + commandId, + // tslint:disable-next-line:no-function-expression no-any + async function (this: IActionContext, ...args: any[]): Promise { + if (args.length) { + let properties: { + contextValue?: string; + } & TelemetryProperties = this.properties; + const contextArg = args[0]; + + if (contextArg instanceof NodeBase) { + properties.contextValue = contextArg.contextValue; + } else if (contextArg instanceof vscode.Uri) { + properties.contextValue = "Uri"; + } + } + + return callback.call(this, ...args); + } + ); } +// tslint:disable-next-line:max-func-body-length function registerDockerCommands(): void { - dockerExplorerProvider = new DockerExplorerProvider(); - vscode.window.registerTreeDataProvider('dockerExplorer', dockerExplorerProvider); - registerCommand('vscode-docker.explorer.refresh', () => dockerExplorerProvider.refresh()); - - registerCommand('vscode-docker.configure', async function (this: IActionContext): Promise { await configure(this, undefined); }); - registerCommand('vscode-docker.api.configure', async function (this: IActionContext, options: ConfigureApiOptions): Promise { - await configureApi(this, options); - }); - - registerCommand('vscode-docker.container.start', async function (this: IActionContext, node: ImageNode | undefined): Promise { await startContainer(this, node); }); - registerCommand('vscode-docker.container.start.interactive', async function (this: IActionContext, node: ImageNode | undefined): Promise { await startContainerInteractive(this, node); }); - registerCommand('vscode-docker.container.start.azurecli', async function (this: IActionContext): Promise { await startAzureCLI(this); }); - registerCommand('vscode-docker.container.stop', async function (this: IActionContext, node: ContainerNode | RootNode | undefined): Promise { await stopContainer(this, node); }); - registerCommand('vscode-docker.container.restart', async function (this: IActionContext, node: ContainerNode | RootNode | undefined): Promise { await restartContainer(this, node); }); - registerCommand('vscode-docker.container.show-logs', async function (this: IActionContext, node: ContainerNode | RootNode | undefined): Promise { await showLogsContainer(this, node); }); - registerCommand('vscode-docker.container.open-shell', async function (this: IActionContext, node: ContainerNode | RootNode | undefined): Promise { await openShellContainer(this, node); }); - registerCommand('vscode-docker.container.remove', async function (this: IActionContext, node: ContainerNode | RootNode | undefined): Promise { await removeContainer(this, node); }); - registerCommand('vscode-docker.image.build', async function (this: IActionContext, item: vscode.Uri | undefined): Promise { await buildImage(this, item); }); - registerCommand('vscode-docker.image.inspect', async function (this: IActionContext, node: ImageNode | undefined): Promise { await inspectImage(this, node); }); - registerCommand('vscode-docker.image.remove', async function (this: IActionContext, node: ImageNode | RootNode | undefined): Promise { await removeImage(this, node); }); - registerCommand('vscode-docker.image.push', async function (this: IActionContext, node: ImageNode | undefined): Promise { await pushImage(this, node); }); - registerCommand('vscode-docker.image.tag', async function (this: IActionContext, node: ImageNode | undefined): Promise { await tagImage(this, node); }); - registerCommand('vscode-docker.compose.up', composeUp); - registerCommand('vscode-docker.compose.down', composeDown); - registerCommand('vscode-docker.compose.restart', composeRestart); - registerCommand('vscode-docker.system.prune', systemPrune); - registerCommand('vscode-docker.dockerHubLogout', dockerHubLogout); - registerCommand('vscode-docker.browseDockerHub', (context?: DockerHubImageTagNode | DockerHubRepositoryNode | DockerHubOrgNode) => { - browseDockerHub(context); - }); - registerCommand('vscode-docker.connectCustomRegistry', connectCustomRegistry); - registerCommand('vscode-docker.disconnectCustomRegistry', disconnectCustomRegistry); - registerCommand('vscode-docker.setRegistryAsDefault', setRegistryAsDefault); - - registerCommand('vscode-docker.browseAzurePortal', (context?: AzureRegistryNode | AzureRepositoryNode | AzureImageTagNode) => { - browseAzurePortal(context); - }); - registerCommand('vscode-docker.createWebApp', async (context?: AzureImageTagNode | DockerHubImageTagNode) => await createWebApp(context)); - registerCommand('vscode-docker.delete-ACR-Registry', deleteAzureRegistry); - registerCommand('vscode-docker.delete-ACR-Image', deleteAzureImage); - registerCommand('vscode-docker.delete-ACR-Repository', deleteRepository); - registerCommand('vscode-docker.create-ACR-Registry', createRegistry); - registerCommand('vscode-docker.pull-ACR-Image', pullFromAzure); + dockerExplorerProvider = new DockerExplorerProvider(); + vscode.window.registerTreeDataProvider( + 'dockerExplorer', + dockerExplorerProvider + ); + + registerCommand('vscode-docker.acr.createRegistry', createRegistry); + registerCommand('vscode-docker.acr.deleteImage', deleteAzureImage); + registerCommand('vscode-docker.acr.deleteRegistry', deleteAzureRegistry); + registerCommand('vscode-docker.acr.deleteRepository', deleteRepository); + 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.showTask', showTaskProperties); + registerCommand('vscode-docker.acr.viewLogs', viewACRLogs); + + registerCommand('vscode-docker.api.configure', async function (this: IActionContext, options: ConfigureApiOptions): Promise { await configureApi(this, options); }); + registerCommand('vscode-docker.browseDockerHub', (context?: DockerHubImageTagNode | DockerHubRepositoryNode | DockerHubOrgNode) => { browseDockerHub(context); }); + registerCommand('vscode-docker.browseAzurePortal', (context?: AzureRegistryNode | AzureRepositoryNode | AzureImageTagNode) => { browseAzurePortal(context); }); + + registerCommand('vscode-docker.compose.down', composeDown); + registerCommand('vscode-docker.compose.restart', composeRestart); + registerCommand('vscode-docker.compose.up', composeUp); + registerCommand('vscode-docker.configure', async function (this: IActionContext): Promise { await configure(this, undefined); }); + registerCommand('vscode-docker.connectCustomRegistry', connectCustomRegistry); + + registerCommand('vscode-docker.container.open-shell', async function (this: IActionContext, node: ContainerNode | RootNode | undefined): Promise { await openShellContainer(this, node); }); + registerCommand('vscode-docker.container.remove', async function (this: IActionContext, node: ContainerNode | RootNode | undefined): Promise { await removeContainer(this, node); }); + registerCommand('vscode-docker.container.restart', async function (this: IActionContext, node: ContainerNode | RootNode | undefined): Promise { await restartContainer(this, node); }); + registerCommand('vscode-docker.container.show-logs', async function (this: IActionContext, node: ContainerNode | RootNode | undefined): Promise { await showLogsContainer(this, node); }); + registerCommand('vscode-docker.container.start', async function (this: IActionContext, node: ImageNode | undefined): Promise { await startContainer(this, node); }); + registerCommand('vscode-docker.container.start.azurecli', async function (this: IActionContext): Promise { await startAzureCLI(this); }); + registerCommand('vscode-docker.container.start.interactive', async function (this: IActionContext, node: ImageNode | undefined): Promise { await startContainerInteractive(this, node); }); + registerCommand('vscode-docker.container.stop', async function (this: IActionContext, node: ContainerNode | RootNode | undefined): Promise { await stopContainer(this, node); }); + + registerCommand('vscode-docker.createWebApp', async (context?: AzureImageTagNode | DockerHubImageTagNode) => await createWebApp(context)); + registerCommand('vscode-docker.disconnectCustomRegistry', disconnectCustomRegistry); + registerCommand('vscode-docker.dockerHubLogout', dockerHubLogout); + registerCommand('vscode-docker.explorer.refresh', () => dockerExplorerProvider.refresh()); + + registerCommand('vscode-docker.image.build', async function (this: IActionContext, item: vscode.Uri | undefined): Promise { await buildImage(this, item); }); + registerCommand('vscode-docker.image.inspect', async function (this: IActionContext, node: ImageNode | undefined): Promise { await inspectImage(this, node); }); + registerCommand('vscode-docker.image.push', async function (this: IActionContext, node: ImageNode | undefined): Promise { await pushImage(this, node); }); + registerCommand('vscode-docker.image.remove', async function (this: IActionContext, node: ImageNode | RootNode | undefined): Promise { await removeImage(this, node); }); + registerCommand('vscode-docker.image.tag', async function (this: IActionContext, node: ImageNode | undefined): Promise { await tagImage(this, node); }); + + registerCommand('vscode-docker.setRegistryAsDefault', setRegistryAsDefault); + registerCommand('vscode-docker.system.prune', systemPrune); } export async function deactivate(): Promise { - if (!client) { - return undefined; - } - // perform cleanup - Configuration.dispose(); - return await client.stop(); + if (!client) { + return undefined; + } + // perform cleanup + Configuration.dispose(); + return await client.stop(); } namespace Configuration { - - let configurationListener: vscode.Disposable; - - export function computeConfiguration(params: ConfigurationParams): vscode.WorkspaceConfiguration[] { - let result: vscode.WorkspaceConfiguration[] = []; - for (let item of params.items) { - let config: vscode.WorkspaceConfiguration; - - if (item.scopeUri) { - config = vscode.workspace.getConfiguration(item.section, client.protocol2CodeConverter.asUri(item.scopeUri)); - } else { - config = vscode.workspace.getConfiguration(item.section); - } - result.push(config); - } - return result; + let configurationListener: vscode.Disposable; + + export function computeConfiguration(params: ConfigurationParams): vscode.WorkspaceConfiguration[] { + let result: vscode.WorkspaceConfiguration[] = []; + for (let item of params.items) { + let config: vscode.WorkspaceConfiguration; + + if (item.scopeUri) { + config = vscode.workspace.getConfiguration( + item.section, + client.protocol2CodeConverter.asUri(item.scopeUri) + ); + } else { + config = vscode.workspace.getConfiguration(item.section); + } + result.push(config); } - - export function initialize(): void { - configurationListener = vscode.workspace.onDidChangeConfiguration((e: vscode.ConfigurationChangeEvent) => { - // notify the language server that settings have change - client.sendNotification(DidChangeConfigurationNotification.type, { settings: null }); - - // Update endpoint and refresh explorer if needed - if (e.affectsConfiguration('docker')) { - docker.refreshEndpoint(); - vscode.commands.executeCommand("vscode-docker.explorer.refresh"); - } + return result; + } + + export function initialize(): void { + configurationListener = vscode.workspace.onDidChangeConfiguration( + (e: vscode.ConfigurationChangeEvent) => { + // notify the language server that settings have change + client.sendNotification(DidChangeConfigurationNotification.type, { + settings: null }); - } - export function dispose(): void { - if (configurationListener) { - // remove this listener when disposed - configurationListener.dispose(); + // Update endpoint and refresh explorer if needed + if (e.affectsConfiguration('docker')) { + docker.refreshEndpoint(); + vscode.commands.executeCommand('vscode-docker.explorer.refresh'); } + } + ); + } + + export function dispose(): void { + if (configurationListener) { + // remove this listener when disposed + configurationListener.dispose(); } + } } function activateLanguageClient(ctx: vscode.ExtensionContext): void { - let serverModule = ctx.asAbsolutePath(path.join("node_modules", "dockerfile-language-server-nodejs", "lib", "server.js")); - let debugOptions = { execArgv: ["--nolazy", "--inspect=6009"] }; - - let serverOptions: ServerOptions = { - run: { module: serverModule, transport: TransportKind.ipc, args: ["--node-ipc"] }, - debug: { module: serverModule, transport: TransportKind.ipc, options: debugOptions } + let serverModule = ctx.asAbsolutePath( + path.join( + "node_modules", + "dockerfile-language-server-nodejs", + "lib", + "server.js" + ) + ); + let debugOptions = { execArgv: ["--nolazy", "--inspect=6009"] }; + + let serverOptions: ServerOptions = { + run: { + module: serverModule, + transport: TransportKind.ipc, + args: ["--node-ipc"] + }, + debug: { + module: serverModule, + transport: TransportKind.ipc, + options: debugOptions } + }; - let middleware: Middleware = { - workspace: { - configuration: Configuration.computeConfiguration - } - }; - - let clientOptions: LanguageClientOptions = { - documentSelector: DOCUMENT_SELECTOR, - synchronize: { - fileEvents: vscode.workspace.createFileSystemWatcher('**/.clientrc') - }, - middleware: middleware + let middleware: Middleware = { + workspace: { + configuration: Configuration.computeConfiguration } - - client = new LanguageClient("dockerfile-langserver", "Dockerfile Language Server", serverOptions, clientOptions); - // tslint:disable-next-line:no-floating-promises - client.onReady().then(() => { - // attach the VS Code settings listener - Configuration.initialize(); - }); - client.start(); + }; + + let clientOptions: LanguageClientOptions = { + documentSelector: DOCUMENT_SELECTOR, + synchronize: { + fileEvents: vscode.workspace.createFileSystemWatcher("**/.clientrc") + }, + middleware: middleware + }; + + client = new LanguageClient( + "dockerfile-langserver", + "Dockerfile Language Server", + serverOptions, + clientOptions + ); + // tslint:disable-next-line:no-floating-promises + client.onReady().then(() => { + // attach the VS Code settings listener + Configuration.initialize(); + }); + client.start(); } let loadEndTime = Date.now(); diff --git a/explorer/models/azureRegistryNodes.ts b/explorer/models/azureRegistryNodes.ts index bf37520984..8e41ff2044 100644 --- a/explorer/models/azureRegistryNodes.ts +++ b/explorer/models/azureRegistryNodes.ts @@ -14,6 +14,7 @@ import { Repository } from '../../utils/Azure/models/repository'; import { getLoginServer, } from '../../utils/nonNull'; import { formatTag } from './commonRegistryUtils'; import { IconPath, NodeBase } from './nodeBase'; +import { TaskRootNode } from './taskNode'; export class AzureRegistryNode extends NodeBase { constructor( @@ -40,8 +41,12 @@ export class AzureRegistryNode extends NodeBase { } } - public async getChildren(element: AzureRegistryNode): Promise { - const repoNodes: AzureRepositoryNode[] = []; + public async getChildren(element: AzureRegistryNode): Promise { + const repoNodes: NodeBase[] = []; + + //Pushing single TaskRootNode under the current registry. All the following nodes added to registryNodes are type AzureRepositoryNode + let taskNode = new TaskRootNode("Tasks", element.azureAccount, element.subscription, element.registry); + repoNodes.push(taskNode); if (!this.azureAccount) { return []; @@ -62,7 +67,6 @@ export class AzureRegistryNode extends NodeBase { return repoNodes; } } - export class AzureRepositoryNode extends NodeBase { constructor( public readonly label: string, diff --git a/explorer/models/imageNode.ts b/explorer/models/imageNode.ts index 54dc913e73..c363408b3e 100644 --- a/explorer/models/imageNode.ts +++ b/explorer/models/imageNode.ts @@ -40,5 +40,5 @@ export class ImageNode extends NodeBase { } } - // no children + // No children } diff --git a/explorer/models/registryRootNode.ts b/explorer/models/registryRootNode.ts index 808600b130..5b0558db62 100644 --- a/explorer/models/registryRootNode.ts +++ b/explorer/models/registryRootNode.ts @@ -5,6 +5,7 @@ import * as assert from 'assert'; import * as ContainerModels from 'azure-arm-containerregistry/lib/models'; +import * as ContainerOps from 'azure-arm-containerregistry/lib/operations'; import { SubscriptionModels } from 'azure-arm-resource'; import * as vscode from 'vscode'; import { parseError } from 'vscode-azureextensionui'; @@ -150,6 +151,7 @@ export class RegistryRootNode extends NodeBase { } catch (error) { vscode.window.showErrorMessage(parseError(error).message); } + }); } await subPool.runAll(); diff --git a/explorer/models/rootNode.ts b/explorer/models/rootNode.ts index 3dfece2407..915d121334 100644 --- a/explorer/models/rootNode.ts +++ b/explorer/models/rootNode.ts @@ -102,19 +102,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: { + throw new Error(`Unexpected contextValue ${element.contextValue}`); + } } - - throw new Error(`Unexpected contextValue ${element.contextValue}`); } private async getImages(): Promise { @@ -127,17 +129,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) { - let node = new ImageNode(`:`, images[i], this.eventEmitter); + for (let image of images) { + if (!image.RepoTags) { + let node = new ImageNode(`:`, image, this.eventEmitter); + 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]}`, images[i], this.eventEmitter); + for (let repoTag of image.RepoTags) { + let node = new ImageNode(`${repoTag}`, image, this.eventEmitter); + node.imageDesc = image; imageNodes.push(node); } } @@ -181,15 +181,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; } @@ -225,9 +223,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'), @@ -241,7 +238,7 @@ export class RootNode extends NodeBase { }; } - let containerNode: ContainerNode = new ContainerNode(`${containers[i].Image} (${containers[i].Names[0].substring(1)}) (${containers[i].Status})`, containers[i], contextValue, iconPath); + let containerNode: ContainerNode = new ContainerNode(`${container.Image} (${container.Names[0].substring(1)}) (${container.Status})`, container, contextValue, iconPath); containerNodes.push(containerNode); } diff --git a/explorer/models/taskNode.ts b/explorer/models/taskNode.ts new file mode 100644 index 0000000000..f0cb25ef2a --- /dev/null +++ b/explorer/models/taskNode.ts @@ -0,0 +1,88 @@ +import ContainerRegistryManagementClient from 'azure-arm-containerregistry'; +import * as ContainerModels from 'azure-arm-containerregistry/lib/models'; +import { SubscriptionModels } from 'azure-arm-resource'; +import * as opn from 'opn'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { AzureAccount } from '../../typings/azure-account.api'; +import * as acrTools from '../../utils/Azure/acrTools'; +import { AzureUtilityManager } from '../../utils/azureUtilityManager'; +import { NodeBase } from './nodeBase'; +/* Single TaskRootNode under each Repository. Labeled "Tasks" */ +export class TaskRootNode extends NodeBase { + public static readonly contextValue: string = 'taskRootNode'; + private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); + public readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; + constructor( + public readonly label: string, + public readonly azureAccount: AzureAccount, + public readonly subscription: SubscriptionModels.Subscription, + public readonly registry: ContainerModels.Registry, + //public readonly iconPath: any = null, + ) { + super(label); + } + + public readonly contextValue: string = 'taskRootNode'; + public name: string; + public readonly iconPath: { light: string | vscode.Uri; dark: string | vscode.Uri } = { + light: path.join(__filename, '..', '..', '..', '..', 'images', 'light', 'tasks_light.svg'), + dark: path.join(__filename, '..', '..', '..', '..', 'images', 'dark', 'tasks_dark.svg') + }; + + public getTreeItem(): vscode.TreeItem { + return { + label: this.label, + collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, + contextValue: TaskRootNode.contextValue, + iconPath: this.iconPath + } + } + + /* Making a list view of TaskNodes, or the Tasks of the current registry */ + public async getChildren(element: TaskRootNode): Promise { + const taskNodes: TaskNode[] = []; + let tasks: ContainerModels.Task[] = []; + const client: ContainerRegistryManagementClient = await AzureUtilityManager.getInstance().getContainerRegistryManagementClient(element.subscription); + const resourceGroup: string = acrTools.getResourceGroupName(element.registry); + tasks = await client.tasks.list(resourceGroup, element.registry.name); + if (tasks.length === 0) { + vscode.window.showInformationMessage(`You do not have any Tasks in the registry '${element.registry.name}'.`, "Learn How to Create Build Tasks").then(val => { + if (val === "Learn More") { + // tslint:disable-next-line:no-unsafe-any + opn('https://aka.ms/acr/task'); + } + }) + } + + for (let task of tasks) { + let node = new TaskNode(task, element.registry, element.subscription, element); + taskNodes.push(node); + } + return taskNodes; + } +} +export class TaskNode extends NodeBase { + constructor( + public task: ContainerModels.Task, + public registry: ContainerModels.Registry, + + public subscription: SubscriptionModels.Subscription, + public parent: NodeBase + + ) { + super(task.name); + } + + public label: string; + public readonly contextValue: string = 'taskNode'; + + public getTreeItem(): vscode.TreeItem { + return { + label: this.label, + collapsibleState: vscode.TreeItemCollapsibleState.None, + contextValue: this.contextValue, + iconPath: null + } + } +} diff --git a/images/dark/tasks_dark.svg b/images/dark/tasks_dark.svg new file mode 100644 index 0000000000..618310ec7c --- /dev/null +++ b/images/dark/tasks_dark.svg @@ -0,0 +1 @@ +Manufacture_16x \ No newline at end of file diff --git a/images/light/tasks_light.svg b/images/light/tasks_light.svg new file mode 100644 index 0000000000..27f50db2ce --- /dev/null +++ b/images/light/tasks_light.svg @@ -0,0 +1 @@ +Manufacture_16x \ No newline at end of file diff --git a/package.json b/package.json index 07ea87c723..6db69af78a 100644 --- a/package.json +++ b/package.json @@ -28,47 +28,56 @@ }, "homepage": "https://github.com/Microsoft/vscode-docker/blob/master/README.md", "activationEvents": [ - "onLanguage:dockerfile", - "onLanguage:yaml", + "onCommand:vscode-docker.acr.createRegistry", + "onCommand:vscode-docker.acr.deleteImage", + "onCommand:vscode-docker.acr.deleteRegistry", + "onCommand:vscode-docker.acr.deleteRepository", + "onCommand:vscode-docker.acr.pullImage", + "onCommand:vscode-docker.acr.quickBuild", + "onCommand:vscode-docker.acr.runTask", + "onCommand:vscode-docker.acr.showTask", + "onCommand:vscode-docker.acr.viewLogs", "onCommand:vscode-docker.api.configure", - "onCommand:vscode-docker.image.build", - "onCommand:vscode-docker.image.inspect", - "onCommand:vscode-docker.image.remove", - "onCommand:vscode-docker.image.push", - "onCommand:vscode-docker.image.tag", - "onCommand:vscode-docker.container.start", - "onCommand:vscode-docker.container.start.interactive", - "onCommand:vscode-docker.container.start.azurecli", - "onCommand:vscode-docker.container.stop", - "onCommand:vscode-docker.container.restart", - "onCommand:vscode-docker.container.show-logs", - "onCommand:vscode-docker.container.open-shell", - "onCommand:vscode-docker.compose.up", + "onCommand:vscode-docker.browseAzurePortal", + "onCommand:vscode-docker.browseDockerHub", "onCommand:vscode-docker.compose.down", "onCommand:vscode-docker.compose.restart", + "onCommand:vscode-docker.compose.up", "onCommand:vscode-docker.configure", + "onCommand:vscode-docker.connectCustomRegistry", + "onCommand:vscode-docker.container.open-shell", + "onCommand:vscode-docker.container.remove", + "onCommand:vscode-docker.container.restart", + "onCommand:vscode-docker.container.show-logs", + "onCommand:vscode-docker.container.start", + "onCommand:vscode-docker.container.start.azurecli", + "onCommand:vscode-docker.container.start.interactive", + "onCommand:vscode-docker.container.stop", "onCommand:vscode-docker.createWebApp", - "onCommand:vscode-docker.create-ACR-Registry", - "onCommand:vscode-docker.system.prune", + "onCommand:vscode-docker.disconnectCustomRegistry", "onCommand:vscode-docker.dockerHubLogout", - "onCommand:vscode-docker.browseDockerHub", - "onCommand:vscode-docker.browseAzurePortal", "onCommand:vscode-docker.explorer.refresh", - "onCommand:vscode-docker.delete-ACR-Registry", - "onCommand:vscode-docker.delete-ACR-Repository", - "onCommand:vscode-docker.delete-ACR-Image", - "onCommand:vscode-docker.connectCustomRegistry", + "onCommand:vscode-docker.image.build", + "onCommand:vscode-docker.image.inspect", + "onCommand:vscode-docker.image.push", + "onCommand:vscode-docker.image.remove", + "onCommand:vscode-docker.image.tag", "onCommand:vscode-docker.setRegistryAsDefault", - "onCommand:vscode-docker.disconnectCustomRegistry", - "onCommand:vscode-docker.pull-ACR-Image", - "onView:dockerExplorer", + "onCommand:vscode-docker.system.prune", "onDebugInitialConfigurations", - "onDebugResolve:docker-coreclr" + "onDebugResolve:docker-coreclr", + "onLanguage:dockerfile", + "onLanguage:yaml", + "onView:dockerExplorer" ], "main": "./out/dockerExtension", "contributes": { "menus": { "commandPalette": [ + { + "command": "vscode-docker.api.configure", + "when": "never" + }, { "command": "vscode-docker.browseDockerHub", "when": "false" @@ -76,21 +85,12 @@ { "command": "vscode-docker.createWebApp", "when": "false" - }, - { - "command": "vscode-docker.api.configure", - "when": "never" } ], "editor/context": [ { - "when": "editorLangId == dockerfile", - "command": "vscode-docker.image.build", - "group": "docker" - }, - { - "when": "resourceFilename == docker-compose.yml", - "command": "vscode-docker.compose.up", + "when": "editorLangId == dockerfile && isAzureAccountInstalled", + "command": "vscode-docker.acr.quickBuild", "group": "docker" }, { @@ -104,7 +104,7 @@ "group": "docker" }, { - "when": "resourceFilename == docker-compose.debug.yml", + "when": "resourceFilename == docker-compose.yml", "command": "vscode-docker.compose.up", "group": "docker" }, @@ -117,27 +117,42 @@ "when": "resourceFilename == docker-compose.debug.yml", "command": "vscode-docker.compose.restart", "group": "docker" + }, + { + "when": "resourceFilename == docker-compose.debug.yml", + "command": "vscode-docker.compose.up", + "group": "docker" + }, + { + "when": "editorLangId == dockerfile", + "command": "vscode-docker.image.build", + "group": "docker" } ], "explorer/context": [ { "when": "resourceFilename =~ /[dD]ocker[fF]ile/", - "command": "vscode-docker.image.build", + "command": "vscode-docker.acr.quickBuild", "group": "docker" }, { "when": "resourceFilename =~ /[dD]ocker-[cC]ompose/", - "command": "vscode-docker.compose.up", + "command": "vscode-docker.compose.down", "group": "docker" }, { "when": "resourceFilename =~ /[dD]ocker-[cC]ompose/", - "command": "vscode-docker.compose.down", + "command": "vscode-docker.compose.restart", "group": "docker" }, { "when": "resourceFilename =~ /[dD]ocker-[cC]ompose/", - "command": "vscode-docker.compose.restart", + "command": "vscode-docker.compose.up", + "group": "docker" + }, + { + "when": "resourceFilename =~ /[dD]ocker[fF]ile/", + "command": "vscode-docker.image.build", "group": "docker" } ], @@ -155,40 +170,48 @@ ], "view/item/context": [ { - "command": "vscode-docker.container.start", - "when": "view == dockerExplorer && viewItem =~ /^(localImageNode|imagesRootNode)$/" + "command": "vscode-docker.acr.createRegistry", + "when": "view == dockerExplorer && viewItem == azureRegistryRootNode" }, { - "command": "vscode-docker.container.start.interactive", - "when": "view == dockerExplorer && viewItem =~ /^(localImageNode|imagesRootNode)$/" + "command": "vscode-docker.acr.deleteImage", + "when": "view == dockerExplorer && viewItem == azureImageTagNode" }, { - "command": "vscode-docker.image.push", - "when": "view == dockerExplorer && viewItem =~ /^(localImageNode|imagesRootNode)$/" + "command": "vscode-docker.acr.deleteRegistry", + "when": "view == dockerExplorer && viewItem == azureRegistryNode" }, { - "command": "vscode-docker.image.remove", - "when": "view == dockerExplorer && viewItem =~ /^(localImageNode|imagesRootNode)$/" + "command": "vscode-docker.acr.deleteRepository", + "when": "view == dockerExplorer && viewItem == azureRepositoryNode" }, { - "command": "vscode-docker.image.inspect", - "when": "view == dockerExplorer && viewItem =~ /^(localImageNode|imagesRootNode)$/" + "command": "vscode-docker.acr.pullImage", + "when": "view == dockerExplorer && viewItem == azureImageNode" }, { - "command": "vscode-docker.image.tag", - "when": "view == dockerExplorer && viewItem =~ /^(localImageNode|imagesRootNode)$/" + "command": "vscode-docker.acr.runTask", + "when": "view == dockerExplorer && viewItem == taskNode" }, { - "command": "vscode-docker.container.stop", - "when": "view == dockerExplorer && viewItem =~ /^(runningLocalContainerNode|containersRootNode)$/" + "command": "vscode-docker.acr.showTask", + "when": "view == dockerExplorer && viewItem == taskNode" }, { - "command": "vscode-docker.container.restart", - "when": "view == dockerExplorer && viewItem =~ /^(runningLocalContainerNode|stoppedLocalContainerNode|containersRootNode)$/" + "command": "vscode-docker.acr.viewLogs", + "when": "view == dockerExplorer && viewItem =~ /^(azureRegistryNode|azureImageTagNode|taskNode)$/" }, { - "command": "vscode-docker.container.show-logs", - "when": "view == dockerExplorer && viewItem =~ /^(runningLocalContainerNode|stoppedLocalContainerNode|containersRootNode)$/" + "command": "vscode-docker.browseDockerHub", + "when": "view == dockerExplorer && viewItem =~ /^(dockerHubImageTagNode|dockerHubRepositoryNode|dockerHubOrgNode)$/" + }, + { + "command": "vscode-docker.browseAzurePortal", + "when": "view == dockerExplorer && viewItem =~ /^(azureRegistryNode|azureRepositoryNode|azureImageNode)$/" + }, + { + "command": "vscode-docker.connectCustomRegistry", + "when": "view == dockerExplorer && viewItem == customRootNode" }, { "command": "vscode-docker.container.open-shell", @@ -199,52 +222,56 @@ "when": "view == dockerExplorer && viewItem =~ /^(stoppedLocalContainerNode|runningLocalContainerNode|containersRootNode)$/" }, { - "command": "vscode-docker.createWebApp", - "when": "view == dockerExplorer && viewItem =~ /^(azureImageTagNode|dockerHubImageTagNode|customImageTagNode)$/" + "command": "vscode-docker.container.restart", + "when": "view == dockerExplorer && viewItem =~ /^(runningLocalContainerNode|stoppedLocalContainerNode|containersRootNode)$/" }, { - "command": "vscode-docker.create-ACR-Registry", - "when": "view == dockerExplorer && viewItem == azureRegistryRootNode" + "command": "vscode-docker.container.show-logs", + "when": "view == dockerExplorer && viewItem =~ /^(runningLocalContainerNode|stoppedLocalContainerNode|containersRootNode)$/" }, { - "command": "vscode-docker.dockerHubLogout", - "when": "view == dockerExplorer && viewItem == dockerHubRootNode" + "command": "vscode-docker.container.start", + "when": "view == dockerExplorer && viewItem =~ /^(localImageNode|imagesRootNode)$/" }, { - "command": "vscode-docker.delete-ACR-Repository", - "when": "view == dockerExplorer && viewItem == azureRepositoryNode" + "command": "vscode-docker.container.start.interactive", + "when": "view == dockerExplorer && viewItem =~ /^(localImageNode|imagesRootNode)$/" }, { - "command": "vscode-docker.delete-ACR-Image", - "when": "view == dockerExplorer && viewItem == azureImageTagNode" + "command": "vscode-docker.container.stop", + "when": "view == dockerExplorer && viewItem =~ /^(runningLocalContainerNode|containersRootNode)$/" }, { - "command": "vscode-docker.delete-ACR-Registry", - "when": "view == dockerExplorer && viewItem == azureRegistryNode" + "command": "vscode-docker.createWebApp", + "when": "view == dockerExplorer && viewItem =~ /^(azureImageTagNode|dockerHubImageTagNode|customImageTagNode)$/" }, { - "command": "vscode-docker.browseDockerHub", - "when": "view == dockerExplorer && viewItem =~ /^(dockerHubImageTagNode|dockerHubRepositoryNode|dockerHubOrgNode)$/" + "command": "vscode-docker.disconnectCustomRegistry", + "when": "view == dockerExplorer && viewItem =~ /^(customRegistryNode)$/" }, { - "command": "vscode-docker.browseAzurePortal", - "when": "view == dockerExplorer && viewItem =~ /^(azureRegistryNode|azureRepositoryNode|azureImageTagNode)$/" + "command": "vscode-docker.dockerHubLogout", + "when": "view == dockerExplorer && viewItem == dockerHubRootNode" }, { - "command": "vscode-docker.pull-ACR-Image", - "when": "view == dockerExplorer && viewItem =~ /^(azureImageTagNode|azureRepositoryNode)$/" + "command": "vscode-docker.image.inspect", + "when": "view == dockerExplorer && viewItem =~ /^(localImageNode|imagesRootNode)$/" }, { - "command": "vscode-docker.connectCustomRegistry", - "when": "view == dockerExplorer && viewItem == customRootNode" + "command": "vscode-docker.image.push", + "when": "view == dockerExplorer && viewItem =~ /^(localImageNode|imagesRootNode)$/" }, { - "command": "vscode-docker.setRegistryAsDefault", - "when": "view == dockerExplorer && viewItem =~ /^(customRegistryNode|azureRegistryNode|dockerHubOrgNode)$/" + "command": "vscode-docker.image.remove", + "when": "view == dockerExplorer && viewItem =~ /^(localImageNode|imagesRootNode)$/" }, { - "command": "vscode-docker.disconnectCustomRegistry", - "when": "view == dockerExplorer && viewItem =~ /^(customRegistryNode)$/" + "command": "vscode-docker.image.tag", + "when": "view == dockerExplorer && viewItem =~ /^(localImageNode|imagesRootNode)$/" + }, + { + "command": "vscode-docker.setRegistryAsDefault", + "when": "view == dockerExplorer && viewItem =~ /^(customRegistryNode|azureRegistryNode|dockerHubOrgNode)$/" } ] }, @@ -556,85 +583,75 @@ }, "commands": [ { - "command": "vscode-docker.configure", - "title": "Add Docker Files to Workspace", - "description": "Add Dockerfile, docker-compose.yml files", + "command": "vscode-docker.acr.createRegistry", + "title": "Create Azure Registry", "category": "Docker" }, { - "command": "vscode-docker.api.configure", - "title": "Add Docker Files to Workspace (API)" + "command": "vscode-docker.acr.deleteImage", + "title": "Delete Azure Image", + "category": "Docker" }, { - "command": "vscode-docker.image.build", - "title": "Build Image", - "description": "Build a Docker image from a Dockerfile", + "command": "vscode-docker.acr.deleteRegistry", + "title": "Delete Azure Registry", "category": "Docker" }, { - "command": "vscode-docker.image.inspect", - "title": "Inspect Image", - "description": "Inspect the metadata of a Docker image", + "command": "vscode-docker.acr.deleteRepository", + "title": "Delete Azure Repository", "category": "Docker" }, { - "command": "vscode-docker.image.remove", - "title": "Remove Image", - "description": "Remove a Docker image", + "command": "vscode-docker.acr.pullImage", + "title": "Pull Image from Azure", "category": "Docker" }, { - "command": "vscode-docker.image.tag", - "title": "Tag Image", - "description": "Tag a Docker image", + "command": "vscode-docker.acr.quickBuild", + "title": "ACR Tasks: Build Image", + "description": "Queue an Azure build from a Dockerfile", "category": "Docker" }, { - "command": "vscode-docker.container.start", - "title": "Run", - "description": "Starts a container from an image", + "command": "vscode-docker.acr.runTask", + "title": "Run Task", "category": "Docker" }, { - "command": "vscode-docker.container.start.interactive", - "title": "Run Interactive", - "description": "Starts a container from an image and runs it interactively", + "command": "vscode-docker.acr.showTask", + "title": "Show Task Properties", "category": "Docker" }, { - "command": "vscode-docker.container.start.azurecli", - "title": "Azure CLI", - "description": "Starts a container from the Azure CLI image and runs it interactively", + "command": "vscode-docker.acr.viewLogs", + "title": "View Azure Logs", "category": "Docker" }, { - "command": "vscode-docker.container.stop", - "title": "Stop Container", - "description": "Stop a running container", - "category": "Docker" + "command": "vscode-docker.api.configure", + "title": "Add Docker Files to Workspace (API)" }, { - "command": "vscode-docker.container.restart", - "title": "Restart Container", - "description": "Restart one or more containers", + "command": "vscode-docker.browseDockerHub", + "title": "Browse in Docker Hub", "category": "Docker" }, { - "command": "vscode-docker.container.remove", - "title": "Remove Container", - "description": "Remove a stopped container", + "command": "vscode-docker.browseAzurePortal", + "title": "Browse in the Azure Portal", "category": "Docker" }, { - "command": "vscode-docker.container.show-logs", - "title": "Show Logs", - "description": "Show the logs of a running container", + "command": "vscode-docker.compose.down", + "title": "Compose Down", + "description": "Stops a composition of containers", "category": "Docker" }, { - "command": "vscode-docker.container.open-shell", - "title": "Attach Shell", - "description": "Open a terminal with an interactive shell for a running container", + "command": "vscode-docker.compose.restart", + "title": "Compose Restart", + "description": "Restarts a composition of containers", "category": "Docker" }, { @@ -644,100 +661,131 @@ "category": "Docker" }, { - "command": "vscode-docker.compose.down", - "title": "Compose Down", - "description": "Stops a composition of containers", + "command": "vscode-docker.configure", + "title": "Add Docker Files to Workspace", + "description": "Add Dockerfile, docker-compose.yml files", "category": "Docker" }, { - "command": "vscode-docker.compose.restart", - "title": "Compose Restart", - "description": "Restarts a composition of containers", + "command": "vscode-docker.connectCustomRegistry", + "title": "Connect to a Private Registry... (Preview)", "category": "Docker" }, { - "command": "vscode-docker.create-ACR-Registry", - "title": "Create Azure Registry", + "command": "vscode-docker.container.open-shell", + "title": "Attach Shell", + "description": "Open a terminal with an interactive shell for a running container", "category": "Docker" }, { - "command": "vscode-docker.delete-ACR-Repository", - "title": "Delete Azure Repository", + "command": "vscode-docker.container.remove", + "title": "Remove Container", + "description": "Remove a stopped container", "category": "Docker" }, { - "command": "vscode-docker.image.push", - "title": "Push", - "description": "Push an image to a registry", + "command": "vscode-docker.container.restart", + "title": "Restart Container", + "description": "Restart one or more containers", "category": "Docker" }, { - "command": "vscode-docker.system.prune", - "title": "System Prune", - "category": "Docker", - "icon": { - "light": "images/light/prune.svg", - "dark": "images/dark/prune.svg" - } + "command": "vscode-docker.container.show-logs", + "title": "Show Logs", + "description": "Show the logs of a running container", + "category": "Docker" }, { - "command": "vscode-docker.explorer.refresh", - "title": "Refresh Explorer", - "category": "Docker", - "icon": { - "light": "images/light/refresh.svg", - "dark": "images/dark/refresh.svg" - } + "command": "vscode-docker.container.start", + "title": "Run", + "description": "Starts a container from an image", + "category": "Docker" + }, + { + "command": "vscode-docker.container.start.azurecli", + "title": "Azure CLI", + "description": "Starts a container from the Azure CLI image and runs it interactively", + "category": "Docker" + }, + { + "command": "vscode-docker.container.start.interactive", + "title": "Run Interactive", + "description": "Starts a container from an image and runs it interactively", + "category": "Docker" + }, + { + "command": "vscode-docker.container.stop", + "title": "Stop Container", + "description": "Stop a running container", + "category": "Docker" }, { "command": "vscode-docker.createWebApp", "title": "Deploy Image to Azure App Service", "category": "Docker" }, + { + "command": "vscode-docker.disconnectCustomRegistry", + "title": "Disconnect from Private Registry", + "category": "Docker" + }, { "command": "vscode-docker.dockerHubLogout", "title": "Docker Hub Logout", "category": "Docker" }, { - "command": "vscode-docker.browseDockerHub", - "title": "Browse in Docker Hub", - "category": "Docker" + "command": "vscode-docker.explorer.refresh", + "title": "Refresh Explorer", + "category": "Docker", + "icon": { + "light": "images/light/refresh.svg", + "dark": "images/dark/refresh.svg" + } }, { - "command": "vscode-docker.browseAzurePortal", - "title": "Browse in the Azure Portal", + "command": "vscode-docker.image.build", + "title": "Build Image", + "description": "Build a Docker image from a Dockerfile", "category": "Docker" }, { - "command": "vscode-docker.delete-ACR-Registry", - "title": "Delete Azure Registry", + "command": "vscode-docker.image.inspect", + "title": "Inspect Image", + "description": "Inspect the metadata of a Docker image", "category": "Docker" }, { - "command": "vscode-docker.delete-ACR-Image", - "title": "Delete Azure Image", + "command": "vscode-docker.image.push", + "title": "Push", + "description": "Push an image to a registry", "category": "Docker" }, { - "command": "vscode-docker.connectCustomRegistry", - "title": "Connect to a Private Registry... (Preview)", + "command": "vscode-docker.image.remove", + "title": "Remove Image", + "description": "Remove a Docker image", "category": "Docker" }, { - "command": "vscode-docker.setRegistryAsDefault", - "title": "Set as Default Registry Path", + "command": "vscode-docker.image.tag", + "title": "Tag Image", + "description": "Tag a Docker image", "category": "Docker" }, { - "command": "vscode-docker.disconnectCustomRegistry", - "title": "Disconnect from Private Registry", + "command": "vscode-docker.setRegistryAsDefault", + "title": "Set as Default Registry Path", "category": "Docker" }, { - "command": "vscode-docker.pull-ACR-Image", - "title": "Pull Image from Azure", - "category": "Docker" + "command": "vscode-docker.system.prune", + "title": "System Prune", + "category": "Docker", + "icon": { + "light": "images/light/prune.svg", + "dark": "images/dark/prune.svg" + } } ], "views": { @@ -800,11 +848,13 @@ "vscode": "^1.1.18" }, "dependencies": { - "azure-arm-containerregistry": "^2.3.0", + "azure-arm-containerregistry": "^3.0.0", "azure-arm-resource": "^2.0.0-preview", "azure-arm-website": "^1.0.0-preview", "deep-equal": "^1.0.1", "dockerfile-language-server-nodejs": "^0.0.19", + "azure-storage": "^2.8.1", + "clipboardy": "^1.2.3", "dockerode": "^2.5.1", "fs-extra": "^6.0.1", "glob": "7.1.2", @@ -814,6 +864,7 @@ "pom-parser": "^1.1.1", "request-promise-native": "^1.0.5", "semver": "^5.5.1", + "tar": "^4.4.6", "vscode-azureextensionui": "^0.19.0", "vscode-languageclient": "^4.4.0" } diff --git a/thirdpartynotices.txt b/thirdpartynotices.txt index 8d30120152..23e175f98b 100644 --- a/thirdpartynotices.txt +++ b/thirdpartynotices.txt @@ -418,3 +418,32 @@ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +13. clipboardy (https://github.com/sindresorhus/clipboardy) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +14. tar (https://github.com/npm/node-tar) + +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/tslint.json b/tslint.json index f3d6b11d67..98a48f8279 100644 --- a/tslint.json +++ b/tslint.json @@ -303,8 +303,7 @@ "no-empty-interfaces": false, "no-missing-visibility-modifiers": false, "no-multiple-var-decl": false, - "no-switch-case-fall-through": false, - "typeof-compare": false + "no-switch-case-fall-through": false }, "rulesDirectory": "node_modules/tslint-microsoft-contrib/", "linterOptions": { diff --git a/utils/Azure/acrTools.ts b/utils/Azure/acrTools.ts index cec81371c2..b0b2e79707 100644 --- a/utils/Azure/acrTools.ts +++ b/utils/Azure/acrTools.ts @@ -4,12 +4,16 @@ *--------------------------------------------------------------------------------------------*/ import { AuthenticationContext } from 'adal-node'; -import * as assert from 'assert'; -import { Registry } from "azure-arm-containerregistry/lib/models"; +import ContainerRegistryManagementClient from 'azure-arm-containerregistry'; +import { Registry, Run, RunGetLogResult } from "azure-arm-containerregistry/lib/models"; import { SubscriptionModels } from 'azure-arm-resource'; +import { ResourceGroup } from "azure-arm-resource/lib/resource/models"; 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 { parseError } from 'vscode-azureextensionui'; import { NULL_GUID } from "../../constants"; import { getCatalog, getTags, TagInfo } from "../../explorer/models/commonRegistryUtils"; import { ext } from '../../extensionVariables'; @@ -44,6 +48,13 @@ export function getResourceGroupName(registry: Registry): string { return id.slice(id.search('resourceGroups/') + 'resourceGroups/'.length, id.search('/providers/')); } +//Gets resource group object from registry and subscription +export async function getResourceGroup(registry: Registry, subscription: Subscription): Promise { + let resourceGroups: ResourceGroup[] = await AzureUtilityManager.getInstance().getResourceGroups(subscription); + const resourceGroupName = getResourceGroupName(registry); + return resourceGroups.find((res) => { return res.name === resourceGroupName }); +} + //Registry item management /** List images under a specific Repository */ export async function getImagesByRepository(element: Repository): Promise { @@ -175,3 +186,92 @@ export async function acquireACRAccessToken(registryUrl: string, scope: string, }); return acrAccessTokenResponse.access_token; } + +export interface IBlobInfo { + accountName: string; + endpointSuffix: string; + containerName: string; + blobName: string; + sasToken: string; + host: string; +} + +/** Parses information into a readable format from a blob url */ +export function getBlobInfo(blobUrl: string): IBlobInfo { + let items: string[] = blobUrl.slice(blobUrl.search('https://') + 'https://'.length).split('/'); + const accountName = blobUrl.slice(blobUrl.search('https://') + 'https://'.length, blobUrl.search('.blob')); + const endpointSuffix = items[0].slice(items[0].search('.blob.') + '.blob.'.length); + const containerName = items[1]; + const blobName = items[2] + '/' + items[3] + '/' + items[4].slice(0, items[4].search('[?]')); + const sasToken = items[4].slice(items[4].search('[?]') + 1); + const host = accountName + '.blob.' + endpointSuffix; + return { + accountName: accountName, + endpointSuffix: endpointSuffix, + containerName: containerName, + blobName: blobName, + sasToken: sasToken, + host: host + }; +} + +/** Stream logs from a blob into output channel. + * Note, since output streams don't actually deal with streams directly, text is not actually + * 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 { + //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); + let temp: RunGetLogResult = await client.runs.getLogSasUrl(getResourceGroupName(registry), registry.name, run.runId); + const link = temp.logLink; + let blobInfo: IBlobInfo = getBlobInfo(link); + let blob: BlobService = createBlobServiceWithSas(blobInfo.host, blobInfo.sasToken); + let available = 0; + let start = 0; + + let obtainLogs = setInterval(async () => { + let props: BlobService.BlobResult; + let metadata: { [key: string]: string; }; + try { + props = await getBlobProperties(blobInfo, blob); + metadata = props.metadata; + } catch (err) { + const error = parseError(err); + //Not found happens when the properties havent yet been set, blob is not ready. Wait 1 second and try again + if (error.errorType === "NotFound") { return; } else { throw error; } + } + available = +props.contentLength; + let text: string; + //Makes sure that if item fails it does so due to network/azure errors not lack of new content + if (available > start) { + text = await getBlobToText(blobInfo, blob, start); + let utf8encoded = (new Buffer(text, 'ascii')).toString('utf8'); + start += text.length; + outputChannel.append(utf8encoded); + } + if (metadata.Complete) { + clearInterval(obtainLogs); + } + }, 1000); +} + +// Promisify getBlobToText for readability and error handling purposes +export async function getBlobToText(blobInfo: IBlobInfo, blob: BlobService, rangeStart: number): Promise { + return new Promise((resolve, reject) => { + blob.getBlobToText(blobInfo.containerName, blobInfo.blobName, { rangeStart: rangeStart }, + (error, result) => { + if (error) { reject(error) } else { resolve(result); } + }); + }); +} + +// Promisify getBlobProperties for readability and error handling purposes +async function getBlobProperties(blobInfo: IBlobInfo, blob: BlobService): Promise { + return new Promise((resolve, reject) => { + blob.getBlobProperties(blobInfo.containerName, blobInfo.blobName, (error, result) => { + if (error) { reject(error) } else { resolve(result); } + }); + }); +} diff --git a/utils/Azure/common.ts b/utils/Azure/common.ts index 74c1c0d747..7e69a06f3e 100644 --- a/utils/Azure/common.ts +++ b/utils/Azure/common.ts @@ -1,8 +1,3 @@ -import * as opn from 'opn'; -import * as vscode from "vscode"; -import { IActionContext, registerCommand } from "vscode-azureextensionui"; -import { UserCancelledError } from '../../explorer/deploy/wizard'; -import { AzureUtilityManager } from "../azureUtilityManager"; let alphaNum = new RegExp('^[a-zA-Z0-9]*$'); diff --git a/utils/addUserAgent.ts b/utils/addUserAgent.ts index 39a5eb6b0b..fa82a77b53 100644 --- a/utils/addUserAgent.ts +++ b/utils/addUserAgent.ts @@ -9,7 +9,6 @@ import { appendExtensionUserAgent } from 'vscode-azureextensionui'; const userAgentKey = 'User-Agent'; export function addUserAgent(options: { headers?: OutgoingHttpHeaders }): void { - // tslint:disable-next-line:no-any if (!options.headers) { options.headers = {}; } diff --git a/utils/azureUtilityManager.ts b/utils/azureUtilityManager.ts index e4dce6aea0..9c2cc02157 100644 --- a/utils/azureUtilityManager.ts +++ b/utils/azureUtilityManager.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See LICENSE.md in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; import { ContainerRegistryManagementClient } from 'azure-arm-containerregistry'; import * as ContainerModels from 'azure-arm-containerregistry/lib/models'; import { ResourceManagementClient, SubscriptionClient, SubscriptionModels } from 'azure-arm-resource'; @@ -20,7 +19,7 @@ import { getSubscriptionId, getTenantId } from './nonNull'; /* Singleton for facilitating communication with Azure account services by providing extended shared functionality and extension wide access to azureAccount. Tool for internal use. - Authors: Esteban Rey L, Jackson Stokes + Authors: Esteban Rey L, Jackson Stokes, Julia Lieberman */ export class AzureUtilityManager { @@ -42,6 +41,8 @@ export class AzureUtilityManager { if (azureAccountExtension) { azureAccount = await azureAccountExtension.activate(); } + + vscode.commands.executeCommand('setContext', 'isAzureAccountInstalled', !!azureAccount); } catch (error) { throw new Error('Failed to activate the Azure Account Extension: ' + parseError(error).message); } @@ -167,6 +168,7 @@ export class AzureUtilityManager { const subPool = new AsyncPool(MAX_CONCURRENT_SUBSCRIPTON_REQUESTS); let resourceGroups: ResourceGroup[] = []; //Acquire each subscription's data simultaneously + for (let sub of subs) { subPool.addTask(async () => { const resourceClient = await this.getResourceManagementClient(sub); @@ -185,7 +187,6 @@ export class AzureUtilityManager { if (session) { return session.credentials; } - throw new Error(`Failed to get credentials, tenant ${tenantId} not found.`); } diff --git a/utils/nonNull.ts b/utils/nonNull.ts index 82aeb1aeaa..c824517db7 100644 --- a/utils/nonNull.ts +++ b/utils/nonNull.ts @@ -12,7 +12,6 @@ import { isNullOrUndefined } from 'util'; * for the property and will give a compile error if the given name is not a property of the source. */ export function nonNullProp(source: TSource, name: TKey): NonNullable { - // tslint:disable-next-line:no-any let value = >source[name]; return nonNullValue(value, name); } @@ -20,7 +19,6 @@ export function nonNullProp(source: TSource /** * Validates that a given value is not null and not undefined. */ -// tslint:disable-next-line:no-any export function nonNullValue(value: T | undefined, propertyNameOrMessage?: string): T { if (isNullOrUndefined(value)) { throw new Error(