From 2a286c318fc64430e7ac07013ab4500d944b6da8 Mon Sep 17 00:00:00 2001 From: "Stephen Weatherford (MSFT)" Date: Thu, 20 Sep 2018 17:33:54 -0700 Subject: [PATCH 01/36] Be more tolerant of \r\n vs \n in removing mult blank lines (#501) --- configureWorkspace/configure_dotnetcore.ts | 6 ++++-- test/configure.test.ts | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/configureWorkspace/configure_dotnetcore.ts b/configureWorkspace/configure_dotnetcore.ts index 9f0fa557eb..dde9c12153 100644 --- a/configureWorkspace/configure_dotnetcore.ts +++ b/configureWorkspace/configure_dotnetcore.ts @@ -226,8 +226,10 @@ function genDockerFile(serviceNameAndRelativePath: string, platform: Platform, o .replace(/\$assembly_name\$/g, assemblyNameNoExtension) .replace(/\$copy_project_commands\$/g, copyProjectCommands); - // Remove multiple empty lines, as might be produced if there's no EXPOSE statement - contents = contents.replace(new RegExp(`${nodeOs.EOL}\{3\}`, 'g'), `${nodeOs.EOL}${nodeOs.EOL}`); + // Remove multiple empty lines with single empty lines, as might be produced + // if $expose_statements$ or another template variable is an empty string + contents = contents.replace(/(\r\n){3}/g, "\r\n\r\n") + .replace(/(\n){3}/g, "\n\n"); let unreplacedToken = extractRegExGroups(contents, /(\$[a-z_]+\$)/, ['']); if (unreplacedToken[0]) { diff --git a/test/configure.test.ts b/test/configure.test.ts index 3f9ab977a3..7ba9d28033 100644 --- a/test/configure.test.ts +++ b/test/configure.test.ts @@ -1070,7 +1070,7 @@ suite("Configure (Add Docker files to Workspace)", function (this: Suite): void // Ruby suite("Ruby", () => { - testInEmptyFolder("Ruby", async () => { + testInEmptyFolder("Ruby, empty folder", async () => { await testConfigureDocker( 'Ruby', { From 82abf3c84867a9049cacefe43438fb1e3e895f66 Mon Sep 17 00:00:00 2001 From: "Stephen Weatherford (MSFT)" Date: Fri, 21 Sep 2018 16:37:45 -0700 Subject: [PATCH 02/36] Move to typescript 3.0, move to next alpha version (#502) * Move to typescript 3.0, move to next alpha version * Update version --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 16e36630af..a74c54ddb1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vscode-docker", - "version": "0.3.0", + "version": "0.3.1-alpha", "publisher": "PeterJausovec", "displayName": "Docker", "description": "Adds syntax highlighting, commands, hover tips, and linting for Dockerfile and docker-compose files.", @@ -674,7 +674,7 @@ "mocha": "5.2.0", "tslint": "^5.11.0", "tslint-microsoft-contrib": "5.0.1", - "typescript": "^2.1.5", + "typescript": "^3.0.0", "vsce": "^1.37.5", "vscode": "^1.1.18" }, From 7227ce4c951994f4b5adbb936fcbc8a67e698607 Mon Sep 17 00:00:00 2001 From: "Stephen Weatherford (MSFT)" Date: Fri, 21 Sep 2018 17:48:28 -0700 Subject: [PATCH 03/36] Don't output EXPOSE if empty port specified (#490) * Don't output EXPOSE if empty port specified * Tests * PR fix * Lint and fix --- configureWorkspace/config-utils.ts | 14 +++-- configureWorkspace/configure.ts | 29 +++++++---- configureWorkspace/configure_dotnetcore.ts | 22 ++++---- configureWorkspace/configure_go.ts | 11 ++-- configureWorkspace/configure_java.ts | 12 +++-- configureWorkspace/configure_node.ts | 11 ++-- configureWorkspace/configure_python.ts | 11 ++-- configureWorkspace/configure_ruby.ts | 11 ++-- test/configure.test.ts | 59 +++++++++++++++++++++- 9 files changed, 134 insertions(+), 46 deletions(-) diff --git a/configureWorkspace/config-utils.ts b/configureWorkspace/config-utils.ts index a4b907c3db..b299649d0a 100644 --- a/configureWorkspace/config-utils.ts +++ b/configureWorkspace/config-utils.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See LICENSE.md in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { isNumber } from 'util'; import vscode = require('vscode'); import { IAzureQuickPickItem, IAzureUserInput } from 'vscode-azureextensionui'; import { ext } from "../extensionVariables"; @@ -22,11 +23,18 @@ export type Platform = * Prompts for a port number * @throws `UserCancelledError` if the user cancels. */ -export async function promptForPort(port: number): Promise { +export async function promptForPort(port: string): Promise { let opt: vscode.InputBoxOptions = { placeHolder: `${port}`, - prompt: 'What port does your app listen on?', - value: `${port}` + prompt: 'What port does your app listen on? ENTER for none.', + value: `${port}`, + validateInput: (value: string): string | undefined => { + if (value && (!Number.isInteger(Number(value)) || Number(value) <= 0)) { + return 'Port must be a positive integer or else empty for no exposed port'; + } + + return undefined; + } } return ext.ui.showInputBox(opt); diff --git a/configureWorkspace/configure.ts b/configureWorkspace/configure.ts index 4958a94f21..d1c0b31285 100644 --- a/configureWorkspace/configure.ts +++ b/configureWorkspace/configure.ts @@ -53,11 +53,18 @@ export type ConfigureTelemetryProperties = { packageFileSubfolderDepth?: string; // 0 = project/etc file in root folder, 1 = in subfolder, 2 = in subfolder of subfolder, etc. }; -const generatorsByPlatform = new Map(); + genDockerComposeDebug: GeneratorFunction, + defaultPort: string +} + +export function getExposeStatements(port: string): string { + return port ? `EXPOSE ${port}` : ''; +} + +const generatorsByPlatform = new Map(); generatorsByPlatform.set('ASP.NET Core', configureAspDotNetCore); generatorsByPlatform.set('Go', configureGo); generatorsByPlatform.set('Java', configureJava); @@ -70,7 +77,14 @@ function genDockerFile(serviceNameAndRelativePath: string, platform: Platform, o let generators = generatorsByPlatform.get(platform); assert(generators, `Could not find dockerfile generator functions for "${platform}"`); if (generators.genDockerFile) { - return generators.genDockerFile(serviceNameAndRelativePath, platform, os, port, { cmd, author, version, artifactName }); + let contents = generators.genDockerFile(serviceNameAndRelativePath, platform, os, port, { cmd, author, version, artifactName }); + + // Remove multiple empty lines with single empty lines, as might be produced + // if $expose_statements$ or another template variable is an empty string + contents = contents.replace(/(\r\n){3}/g, "\r\n\r\n") + .replace(/(\n){3}/g, "\n\n"); + + return contents; } } @@ -325,6 +339,7 @@ async function configureCore(actionContext: IActionContext, options: ConfigureAp const platformType: Platform = options.platform || await quickPickPlatform(); properties.configurePlatform = platformType; + let generatorInfo = generatorsByPlatform.get(platformType); let os: OS | undefined = options.os; if (!os && platformType.toLowerCase().includes('.net')) { @@ -334,11 +349,7 @@ async function configureCore(actionContext: IActionContext, options: ConfigureAp let port: string | undefined = options.port; if (!port) { - if (platformType.toLowerCase().includes('.net')) { - port = await promptForPort(80); - } else { - port = await promptForPort(3000); - } + port = await promptForPort(generatorInfo.defaultPort); } let targetFramework: string; diff --git a/configureWorkspace/configure_dotnetcore.ts b/configureWorkspace/configure_dotnetcore.ts index dde9c12153..0e8fd36abf 100644 --- a/configureWorkspace/configure_dotnetcore.ts +++ b/configureWorkspace/configure_dotnetcore.ts @@ -10,18 +10,23 @@ import * as semver from 'semver'; import { extractRegExGroups } from '../helpers/extractRegExGroups'; import { isWindows, isWindows10RS3OrNewer, isWindows10RS4OrNewer } from '../helpers/windowsVersion'; import { OS, Platform } from './config-utils'; -import { PackageInfo } from './configure'; +import { getExposeStatements, IPlatformGeneratorInfo, PackageInfo } from './configure'; // This file handles both ASP.NET core and .NET Core Console -let configureDotNetCore = { +export const configureAspDotNetCore: IPlatformGeneratorInfo = { genDockerFile, genDockerCompose: undefined, // We don't generate compose files for .net core - genDockerComposeDebug: undefined // We don't generate compose files for .net core + genDockerComposeDebug: undefined, // We don't generate compose files for .net core + defaultPort: '80' }; -export let configureAspDotNetCore = configureDotNetCore; -export let configureDotNetCoreConsole = configureDotNetCore; +export const configureDotNetCoreConsole: IPlatformGeneratorInfo = { + genDockerFile, + genDockerCompose: undefined, // We don't generate compose files for .net core + genDockerComposeDebug: undefined, // We don't generate compose files for .net core + defaultPort: '' +}; const AspNetCoreRuntimeImageFormat = "microsoft/aspnetcore:{0}.{1}{2}"; const AspNetCoreSdkImageFormat = "microsoft/aspnetcore-build:{0}.{1}{2}"; @@ -164,7 +169,7 @@ function genDockerFile(serviceNameAndRelativePath: string, platform: Platform, o let assemblyNameNoExtension = serviceName; // example: COPY Core2.0ConsoleAppWindows/Core2.0ConsoleAppWindows.csproj Core2.0ConsoleAppWindows/ let copyProjectCommands = `COPY ["${serviceNameAndRelativePath}.csproj", "${projectDirectory}/"]` - let exposeStatements = port ? `EXPOSE ${port}` : ''; + let exposeStatements = getExposeStatements(port); // Parse version from TargetFramework // Example: netcoreapp1.0 @@ -226,11 +231,6 @@ function genDockerFile(serviceNameAndRelativePath: string, platform: Platform, o .replace(/\$assembly_name\$/g, assemblyNameNoExtension) .replace(/\$copy_project_commands\$/g, copyProjectCommands); - // Remove multiple empty lines with single empty lines, as might be produced - // if $expose_statements$ or another template variable is an empty string - contents = contents.replace(/(\r\n){3}/g, "\r\n\r\n") - .replace(/(\n){3}/g, "\n\n"); - let unreplacedToken = extractRegExGroups(contents, /(\$[a-z_]+\$)/, ['']); if (unreplacedToken[0]) { assert.fail(`Unreplaced template token "${unreplacedToken}"`); diff --git a/configureWorkspace/configure_go.ts b/configureWorkspace/configure_go.ts index 380ff20c52..9a890efd4e 100644 --- a/configureWorkspace/configure_go.ts +++ b/configureWorkspace/configure_go.ts @@ -3,15 +3,18 @@ * Licensed under the MIT License. See LICENSE.md in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { PackageInfo } from './configure'; +import { getExposeStatements, IPlatformGeneratorInfo, PackageInfo } from './configure'; -export let configureGo = { +export let configureGo: IPlatformGeneratorInfo = { genDockerFile, genDockerCompose, - genDockerComposeDebug + genDockerComposeDebug, + defaultPort: '3000' }; function genDockerFile(serviceNameAndRelativePath: string, platform: string, os: string | undefined, port: string, { cmd, author, version, artifactName }: Partial): string { + let exposeStatements = getExposeStatements(port); + return ` #build stage FROM golang:alpine AS builder @@ -27,7 +30,7 @@ RUN apk --no-cache add ca-certificates COPY --from=builder /go/bin/app /app ENTRYPOINT ./app LABEL Name=${serviceNameAndRelativePath} Version=${version} -EXPOSE ${port} +${exposeStatements} `; } diff --git a/configureWorkspace/configure_java.ts b/configureWorkspace/configure_java.ts index d7e3364816..6c1ba6615c 100644 --- a/configureWorkspace/configure_java.ts +++ b/configureWorkspace/configure_java.ts @@ -3,24 +3,26 @@ * Licensed under the MIT License. See LICENSE.md in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { PackageInfo } from './configure'; +import { getExposeStatements, IPlatformGeneratorInfo, PackageInfo } from './configure'; -export let configureJava = { +export let configureJava: IPlatformGeneratorInfo = { genDockerFile, genDockerCompose, - genDockerComposeDebug + genDockerComposeDebug, + defaultPort: '3000' }; function genDockerFile(serviceNameAndRelativePath: string, platform: string, os: string | undefined, port: string, { cmd, author, version, artifactName }: Partial): string { - + let exposeStatements = getExposeStatements(port); const artifact = artifactName ? artifactName : `${serviceNameAndRelativePath}.jar`; + return ` FROM openjdk:8-jdk-alpine VOLUME /tmp ARG JAVA_OPTS ENV JAVA_OPTS=$JAVA_OPTS ADD ${artifact} ${serviceNameAndRelativePath}.jar -EXPOSE ${port} +${exposeStatements} ENTRYPOINT exec java $JAVA_OPTS -jar ${serviceNameAndRelativePath}.jar # For Spring-Boot project, use the entrypoint below to reduce Tomcat startup time. #ENTRYPOINT exec java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar ${serviceNameAndRelativePath}.jar diff --git a/configureWorkspace/configure_node.ts b/configureWorkspace/configure_node.ts index 43e328c22c..1584350947 100644 --- a/configureWorkspace/configure_node.ts +++ b/configureWorkspace/configure_node.ts @@ -3,22 +3,25 @@ * Licensed under the MIT License. See LICENSE.md in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { PackageInfo } from './configure'; +import { getExposeStatements, IPlatformGeneratorInfo, PackageInfo } from './configure'; -export let configureNode = { +export let configureNode: IPlatformGeneratorInfo = { genDockerFile, genDockerCompose, - genDockerComposeDebug + genDockerComposeDebug, + defaultPort: '3000' }; function genDockerFile(serviceNameAndRelativePath: string, platform: string, os: string | undefined, port: string, { cmd, author, version, artifactName }: Partial): string { + let exposeStatements = getExposeStatements(port); + return `FROM node:8.9-alpine ENV NODE_ENV production WORKDIR /usr/src/app COPY ["package.json", "package-lock.json*", "npm-shrinkwrap.json*", "./"] RUN npm install --production --silent && mv node_modules ../ COPY . . -EXPOSE ${port} +${exposeStatements} CMD ${cmd}`; } diff --git a/configureWorkspace/configure_python.ts b/configureWorkspace/configure_python.ts index 6e4df2d5ed..80ac583195 100644 --- a/configureWorkspace/configure_python.ts +++ b/configureWorkspace/configure_python.ts @@ -3,15 +3,18 @@ * Licensed under the MIT License. See LICENSE.md in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { PackageInfo } from './configure'; +import { getExposeStatements, IPlatformGeneratorInfo, PackageInfo } from './configure'; -export let configurePython = { +export let configurePython: IPlatformGeneratorInfo = { genDockerFile, genDockerCompose, - genDockerComposeDebug + genDockerComposeDebug, + defaultPort: '3000' }; function genDockerFile(serviceNameAndRelativePath: string, platform: string, os: string | undefined, port: string, { cmd, author, version, artifactName }: Partial): string { + let exposeStatements = getExposeStatements(port); + return `# Python support can be specified down to the minor or micro version # (e.g. 3.6 or 3.6.3). # OS Support also exists for jessie & stretch (slim and full). @@ -23,7 +26,7 @@ FROM python:alpine #FROM continuumio/miniconda3 LABEL Name=${serviceNameAndRelativePath} Version=${version} -EXPOSE ${port} +${exposeStatements} WORKDIR /app ADD . /app diff --git a/configureWorkspace/configure_ruby.ts b/configureWorkspace/configure_ruby.ts index bd4f097282..f1d55d4a8e 100644 --- a/configureWorkspace/configure_ruby.ts +++ b/configureWorkspace/configure_ruby.ts @@ -3,19 +3,22 @@ * Licensed under the MIT License. See LICENSE.md in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { PackageInfo } from './configure'; +import { getExposeStatements, IPlatformGeneratorInfo, PackageInfo } from './configure'; -export let configureRuby = { +export let configureRuby: IPlatformGeneratorInfo = { genDockerFile, genDockerCompose, - genDockerComposeDebug + genDockerComposeDebug, + defaultPort: '3000' }; function genDockerFile(serviceNameAndRelativePath: string, platform: string, os: string | undefined, port: string, { cmd, author, version, artifactName }: Partial): string { + let exposeStatements = getExposeStatements(port); + return `FROM ruby:2.5-slim LABEL Name=${serviceNameAndRelativePath} Version=${version} -EXPOSE ${port} +${exposeStatements} # throw errors if Gemfile has been modified since Gemfile.lock RUN bundle config --global frozen 1 diff --git a/test/configure.test.ts b/test/configure.test.ts index 7ba9d28033..8f79a6121f 100644 --- a/test/configure.test.ts +++ b/test/configure.test.ts @@ -11,7 +11,7 @@ import * as path from 'path'; import { Platform, OS } from "../configureWorkspace/config-utils"; import { ext } from '../extensionVariables'; import { Suite } from 'mocha'; -import { configure, ConfigureTelemetryProperties, configureApi, ConfigureApiOptions } from '../configureWorkspace/configure'; +import { configure, ConfigureTelemetryProperties, ConfigureApiOptions } from '../configureWorkspace/configure'; import { TestUserInput, IActionContext, TelemetryProperties } from 'vscode-azureextensionui'; import { globAsync } from '../helpers/async'; import { getTestRootFolder, constants, testInEmptyFolder } from './global.test'; @@ -393,7 +393,7 @@ suite("Configure (Add Docker files to Workspace)", function (this: Suite): void packageFileType: '.csproj', packageFileSubfolderDepth: '1' }, - [os, '' /* no port */], + [os, undefined /* no port */], ['Dockerfile', '.dockerignore', `${projectFolder}/Program.cs`, `${projectFolder}/${projectFileName}`] ); @@ -599,6 +599,17 @@ suite("Configure (Add Docker files to Workspace)", function (this: Suite): void }); suite(".NET Core Console 2.1", async () => { + testInEmptyFolder("Default port (none)", async () => { + await writeFile('projectFolder1', 'aspnetapp.csproj', dotNetCoreConsole_21_ProjectFileContents); + await testConfigureDocker( + '.NET Core Console', + undefined, + ['Windows', undefined] + ); + + assertNotFileContains('Dockerfile', 'EXPOSE'); + }); + testInEmptyFolder("Windows", async () => { await testDotNetCoreConsole( 'Windows', @@ -783,6 +794,28 @@ suite("Configure (Add Docker files to Workspace)", function (this: Suite): void // ASP.NET Core suite("ASP.NET Core 2.2", async () => { + testInEmptyFolder("Default port (80)", async () => { + await writeFile('projectFolder1', 'aspnetapp.csproj', dotNetCoreConsole_21_ProjectFileContents); + await testConfigureDocker( + 'ASP.NET Core', + undefined, + ['Windows', undefined] + ); + + assertFileContains('Dockerfile', 'EXPOSE 80'); + }); + + testInEmptyFolder("No port", async () => { + await writeFile('projectFolder1', 'aspnetapp.csproj', dotNetCoreConsole_21_ProjectFileContents); + await testConfigureDocker( + 'ASP.NET Core', + undefined, + ['Windows', ''] + ); + + assertNotFileContains('Dockerfile', 'EXPOSE'); + }); + testInEmptyFolder("Windows 10 RS4", async () => { await testAspNetCore( 'Windows', @@ -922,6 +955,28 @@ suite("Configure (Add Docker files to Workspace)", function (this: Suite): void // Java suite("Java", () => { + testInEmptyFolder("No port", async () => { + await testConfigureDocker( + 'Java', + undefined, + [''], + ['Dockerfile', 'docker-compose.debug.yml', 'docker-compose.yml', '.dockerignore'] + ); + + assertNotFileContains('Dockerfile', 'EXPOSE'); + }); + + testInEmptyFolder("Default port", async () => { + await testConfigureDocker( + 'Java', + undefined, + [undefined], + ['Dockerfile', 'docker-compose.debug.yml', 'docker-compose.yml', '.dockerignore'] + ); + + assertFileContains('Dockerfile', 'EXPOSE 3000'); + }); + testInEmptyFolder("No pom file", async () => { await testConfigureDocker( 'Java', From ae7b5625147130e00ff14134a0282e164a4d533a Mon Sep 17 00:00:00 2001 From: "Stephen Weatherford (MSFT)" Date: Mon, 24 Sep 2018 14:38:00 -0700 Subject: [PATCH 04/36] Change default shell from /bin/sh to /bin/bash (#505) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a74c54ddb1..261c895572 100644 --- a/package.json +++ b/package.json @@ -415,7 +415,7 @@ }, "docker.attachShellCommand.linuxContainer": { "type": "string", - "default": "/bin/sh", + "default": "/bin/bash", "description": "Attach command to use for Linux containers" }, "docker.attachShellCommand.windowsContainer": { From 648fe64fb9b6b856b2ee248d2167a0296cb9c977 Mon Sep 17 00:00:00 2001 From: "Stephen Weatherford (MSFT)" Date: Mon, 24 Sep 2018 16:56:51 -0700 Subject: [PATCH 05/36] dockerHubApi.ts -> dockerHubSearch.ts (#507) --- dockerCompose/dockerComposeCompletionItemProvider.ts | 2 +- dockerCompose/dockerComposeHoverProvider.ts | 2 +- dockerHubApi.ts => dockerHubSearch.ts | 0 helpers/suggestSupportHelper.ts | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename dockerHubApi.ts => dockerHubSearch.ts (100%) diff --git a/dockerCompose/dockerComposeCompletionItemProvider.ts b/dockerCompose/dockerComposeCompletionItemProvider.ts index 87282447e7..5ef6bcd246 100644 --- a/dockerCompose/dockerComposeCompletionItemProvider.ts +++ b/dockerCompose/dockerComposeCompletionItemProvider.ts @@ -7,7 +7,7 @@ import { CancellationToken, CompletionItem, CompletionItemKind, CompletionItemProvider, Position, TextDocument, Uri } from 'vscode'; import { KeyInfo } from '../dockerExtension'; -import hub = require('../dockerHubApi'); +import hub = require('../dockerHubSearch'); import helper = require('../helpers/suggestSupportHelper'); import composeVersions from './dockerComposeKeyInfo'; diff --git a/dockerCompose/dockerComposeHoverProvider.ts b/dockerCompose/dockerComposeHoverProvider.ts index c8596f705c..65825ab54f 100644 --- a/dockerCompose/dockerComposeHoverProvider.ts +++ b/dockerCompose/dockerComposeHoverProvider.ts @@ -7,7 +7,7 @@ import { CancellationToken, Hover, HoverProvider, MarkedString, Position, Range, TextDocument } from 'vscode'; import { KeyInfo } from "../dockerExtension"; -import hub = require('../dockerHubApi'); +import hub = require('../dockerHubSearch'); import suggestHelper = require('../helpers/suggestSupportHelper'); import parser = require('../parser'); diff --git a/dockerHubApi.ts b/dockerHubSearch.ts similarity index 100% rename from dockerHubApi.ts rename to dockerHubSearch.ts diff --git a/helpers/suggestSupportHelper.ts b/helpers/suggestSupportHelper.ts index 74396c602c..37ed945179 100644 --- a/helpers/suggestSupportHelper.ts +++ b/helpers/suggestSupportHelper.ts @@ -7,7 +7,7 @@ import vscode = require('vscode'); import { FROM_DIRECTIVE_PATTERN } from "../dockerExtension"; -import hub = require('../dockerHubApi'); +import hub = require('../dockerHubSearch'); import parser = require('../parser'); export class SuggestSupportHelper { From d468a0f67946aa9ecd9ab7bf5706b8e965655e26 Mon Sep 17 00:00:00 2001 From: "Stephen Weatherford (MSFT)" Date: Tue, 25 Sep 2018 11:24:18 -0700 Subject: [PATCH 06/36] Update to next alpha after hotfix (#510) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 261c895572..c61ac1e15d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vscode-docker", - "version": "0.3.1-alpha", + "version": "0.3.2-alpha", "publisher": "PeterJausovec", "displayName": "Docker", "description": "Adds syntax highlighting, commands, hover tips, and linting for Dockerfile and docker-compose files.", From aa62c93bd072c2c8cd4d779eca2582cf689f8bf5 Mon Sep 17 00:00:00 2001 From: "Stephen Weatherford (MSFT)" Date: Tue, 25 Sep 2018 11:42:14 -0700 Subject: [PATCH 07/36] Fix 'Other' platform generation (hotfix 0.3.1) (#509) (#514) --- CHANGELOG.md | 6 +++ configureWorkspace/configure.ts | 2 + configureWorkspace/configure_other.ts | 47 +++++++++++++++++++ configureWorkspace/configure_python.ts | 5 +- test/configure.test.ts | 65 +++++++++++++++++++++++++- 5 files changed, 122 insertions(+), 3 deletions(-) create mode 100644 configureWorkspace/configure_other.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index a97d8faa6f..ac2408cf37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 0.3.1 - 25 September 2018 + +### Fixed + +* Error while generating Dockerfile for 'other' [#504](https://github.com/Microsoft/vscode-docker/issues/504) + ## 0.3.0 - 21 September 2018 ### Added diff --git a/configureWorkspace/configure.ts b/configureWorkspace/configure.ts index d1c0b31285..28b112d81a 100644 --- a/configureWorkspace/configure.ts +++ b/configureWorkspace/configure.ts @@ -20,6 +20,7 @@ import { configureAspDotNetCore, configureDotNetCoreConsole } from './configure_ import { configureGo } from './configure_go'; import { configureJava } from './configure_java'; import { configureNode } from './configure_node'; +import { configureOther } from './configure_other'; import { configurePython } from './configure_python'; import { configureRuby } from './configure_ruby'; @@ -72,6 +73,7 @@ generatorsByPlatform.set('.NET Core Console', configureDotNetCoreConsole); generatorsByPlatform.set('Node.js', configureNode); generatorsByPlatform.set('Python', configurePython); generatorsByPlatform.set('Ruby', configureRuby); +generatorsByPlatform.set('Other', configureOther); function genDockerFile(serviceNameAndRelativePath: string, platform: Platform, os: OS | undefined, port: string | undefined, { cmd, author, version, artifactName }: Partial): string { let generators = generatorsByPlatform.get(platform); diff --git a/configureWorkspace/configure_other.ts b/configureWorkspace/configure_other.ts new file mode 100644 index 0000000000..460c9db7a2 --- /dev/null +++ b/configureWorkspace/configure_other.ts @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { PackageInfo } from './configure'; + +export let configureOther = { + genDockerFile, + genDockerCompose, + genDockerComposeDebug, + defaultPort: '3000' +}; + +function genDockerFile(serviceNameAndRelativePath: string, platform: string, os: string | undefined, port: string, { cmd, author, version, artifactName }: Partial): string { + return `FROM docker/whalesay:latest +LABEL Name=${serviceNameAndRelativePath} Version=${version} +RUN apt-get -y update && apt-get install -y fortunes +CMD /usr/games/fortune -a | cowsay +`; +} + +function genDockerCompose(serviceNameAndRelativePath: string, platform: string, os: string | undefined, port: string): string { + return `version: '2.1' + +services: + ${serviceNameAndRelativePath}: + image: ${serviceNameAndRelativePath} + build: . + ports: + - ${port}:${port} +`; +} + +function genDockerComposeDebug(serviceNameAndRelativePath: string, platform: string, os: string | undefined, port: string, { fullCommand: cmd }: Partial): string { + return `version: '2.1' + +services: + ${serviceNameAndRelativePath}: + image: ${serviceNameAndRelativePath} + build: + context: . + dockerfile: Dockerfile + ports: + - ${port}:${port} +`; +} diff --git a/configureWorkspace/configure_python.ts b/configureWorkspace/configure_python.ts index 80ac583195..aa8d11b59e 100644 --- a/configureWorkspace/configure_python.ts +++ b/configureWorkspace/configure_python.ts @@ -54,7 +54,8 @@ services: image: ${serviceNameAndRelativePath} build: . ports: - - ${port}:${port}`; + - ${port}:${port} +`; } function genDockerComposeDebug(serviceNameAndRelativePath: string, platform: string, os: string | undefined, port: string, { fullCommand: cmd }: Partial): string { @@ -67,6 +68,6 @@ services: context: . dockerfile: Dockerfile ports: - - ${port}:${port} + - ${port}:${port} `; } diff --git a/test/configure.test.ts b/test/configure.test.ts index 8f79a6121f..63a7059c62 100644 --- a/test/configure.test.ts +++ b/test/configure.test.ts @@ -41,7 +41,6 @@ as the easier to read: sub-indented text `; */ - function removeIndentation(text: string): string { while (text[0] === '\r' || text[0] === '\n') { text = text.substr(1); @@ -1145,6 +1144,70 @@ suite("Configure (Add Docker files to Workspace)", function (this: Suite): void }); }); + suite("'Other'", () => { + testInEmptyFolder("with package.json", async () => { + await writeFile('', 'package.json', JSON.stringify({ + "name": "myexpressapp", + "version": "1.2.3", + "private": true, + "scripts": { + "start": "node ./bin/www" + }, + "dependencies": { + "cookie-parser": "~1.4.3", + "debug": "~2.6.9", + "express": "~4.16.0", + "http-errors": "~1.6.2", + "jade": "~1.11.0", + "morgan": "~1.9.0" + } + })) + await testConfigureDocker( + 'Other', + { + configurePlatform: 'Other', + configureOs: undefined, + packageFileType: undefined, + packageFileSubfolderDepth: undefined + }, + [undefined /*port*/], + ['Dockerfile', 'docker-compose.debug.yml', 'docker-compose.yml', '.dockerignore', 'package.json']); + + let dockerfileContents = await readFile('Dockerfile'); + let composeContents = await readFile('docker-compose.yml'); + let debugComposeContents = await readFile('docker-compose.debug.yml'); + + assert.strictEqual(dockerfileContents, removeIndentation(` + FROM docker/whalesay:latest + LABEL Name=testoutput Version=1.2.3 + RUN apt-get -y update && apt-get install -y fortunes + CMD /usr/games/fortune -a | cowsay + `)); + assert.strictEqual(composeContents, removeIndentation(` + version: '2.1' + + services: + testoutput: + image: testoutput + build: . + ports: + - 3000:3000 + `)); + assert.strictEqual(debugComposeContents, removeIndentation(` + version: '2.1' + + services: + testoutput: + image: testoutput + build: + context: . + dockerfile: Dockerfile + ports: + - 3000:3000 + `)); + }); + }); + // API (vscode-docker.api.configure) suite("API", () => { From 8a5349d0f4c0f27d4d427a34cccab131dd0c21be Mon Sep 17 00:00:00 2001 From: "Stephen Weatherford (MSFT)" Date: Wed, 3 Oct 2018 16:22:32 -0700 Subject: [PATCH 08/36] Update to latest telemetry module (#525) * Update to latest telemetry module * auto update --- package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index c61ac1e15d..36b42e769a 100644 --- a/package.json +++ b/package.json @@ -664,7 +664,7 @@ "@types/glob": "5.0.35", "@types/keytar": "^4.0.1", "@types/mocha": "^5.2.5", - "@types/node": "^8.0.34", + "@types/node": "^8.10.34", "@types/request-promise-native": "^1.0.15", "@types/semver": "^5.5.0", "adm-zip": "^0.4.11", @@ -674,8 +674,8 @@ "mocha": "5.2.0", "tslint": "^5.11.0", "tslint-microsoft-contrib": "5.0.1", - "typescript": "^3.0.0", - "vsce": "^1.37.5", + "typescript": "^3.1.1", + "vsce": "^1.51.1", "vscode": "^1.1.18" }, "dependencies": { @@ -693,7 +693,7 @@ "request-promise-native": "^1.0.5", "semver": "^5.5.1", "vscode-azureextensionui": "^0.17.0", - "vscode-extension-telemetry": "0.0.18", + "vscode-extension-telemetry": "^0.0.22", "vscode-languageclient": "^4.4.0" } } From 2b166c7d0e87744f20189d5ca138581579abd872 Mon Sep 17 00:00:00 2001 From: "Stephen Weatherford (MSFT)" Date: Tue, 9 Oct 2018 13:49:38 -0700 Subject: [PATCH 09/36] Update index.ts (#534) * Remove unused imports * Update to version from storage which has more linting --- test/index.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test/index.ts b/test/index.ts index 5099681e89..baf60a0e01 100644 --- a/test/index.ts +++ b/test/index.ts @@ -1,6 +1,3 @@ -import { endianness } from "os"; -import { isNumber } from "util"; - /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See LICENSE.md in the project root for license information. @@ -18,7 +15,8 @@ import { isNumber } from "util"; // to report the results back to the caller. When the tests are finished, return // a possible error to the callback or null if none. -var testRunner = require('vscode/lib/testrunner'); +// tslint:disable-next-line:no-require-imports no-var-requires +let testRunner = require('vscode/lib/testrunner'); let options: { [key: string]: string | boolean | number } = { ui: 'tdd', // the TDD UI is being used in extension.test.ts (suite, test, etc.) @@ -39,19 +37,21 @@ let options: { [key: string]: string | boolean | number } = { // // See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for all available options -for (let envVar of Object.keys(process.env)) { +let environmentVariables = <{ [key: string]: string }>process.env; +for (let envVar of Object.keys(environmentVariables)) { let match = envVar.match(/^mocha_(.+)/i); if (match) { let [, option] = match; - let value: string | number = process.env[envVar] || ''; - if (!isNaN(parseInt(value))) { - value = parseInt(value); + let value: string | number = environmentVariables[envVar]; + if (typeof value === 'string' && !isNaN(parseInt(value, undefined))) { + value = parseInt(value, undefined); } options[option] = value; } } console.warn(`Mocha options: ${JSON.stringify(options, null, 2)}`); +// tslint:disable-next-line: no-unsafe-any testRunner.configure(options); module.exports = testRunner; From a1ba0a202a60a961dd04f5f468df9f8b2857e43c Mon Sep 17 00:00:00 2001 From: "Stephen Weatherford (MSFT)" Date: Tue, 9 Oct 2018 14:01:16 -0700 Subject: [PATCH 10/36] Improve message that opening a folder is required for some commands (#533) --- commands/build-image.ts | 21 ++------------------- commands/docker-compose.ts | 18 ++---------------- commands/utils/quickPickWorkspaceFolder.ts | 21 +++++++++++++++++++++ configureWorkspace/configure.ts | 17 ++--------------- 4 files changed, 27 insertions(+), 50 deletions(-) create mode 100644 commands/utils/quickPickWorkspaceFolder.ts diff --git a/commands/build-image.ts b/commands/build-image.ts index 1f3e350deb..0d758beb8b 100644 --- a/commands/build-image.ts +++ b/commands/build-image.ts @@ -10,6 +10,7 @@ import { DOCKERFILE_GLOB_PATTERN } from '../dockerExtension'; import { delay } from "../explorer/utils/utils"; import { ext } from "../extensionVariables"; import { addImageTaggingTelemetry, getTagFromUserInput } from "./tag-image"; +import { quickPickWorkspaceFolder } from "./utils/quickPickWorkspaceFolder"; async function getDockerFileUris(folder: vscode.WorkspaceFolder): Promise { return await vscode.workspace.findFiles(new vscode.RelativePattern(folder, DOCKERFILE_GLOB_PATTERN), undefined, 1000, undefined); @@ -56,25 +57,7 @@ export async function buildImage(actionContext: IActionContext, dockerFileUri: v const defaultContextPath = configOptions.get('imageBuildContextPath', ''); let dockerFileItem: Item | undefined; - let rootFolder: vscode.WorkspaceFolder; - if (vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length === 1) { - rootFolder = vscode.workspace.workspaceFolders[0]; - } else { - let selected = await vscode.window.showWorkspaceFolderPick(); - if (!selected) { - throw new UserCancelledError(); - } - rootFolder = selected; - } - - if (!rootFolder) { - if (!vscode.workspace.workspaceFolders) { - vscode.window.showErrorMessage('Docker files can only be built if VS Code is opened on a folder.'); - } else { - vscode.window.showErrorMessage('Docker files can only be built if a workspace folder is picked in VS Code.'); - } - return; - } + let rootFolder: vscode.WorkspaceFolder = await quickPickWorkspaceFolder('To build Docker files you must first open a folder or workspace in VS Code.'); while (!dockerFileItem) { let resolvedItem: Item | undefined = await resolveDockerFileItem(rootFolder, dockerFileUri); diff --git a/commands/docker-compose.ts b/commands/docker-compose.ts index ff13ab25ae..4c15f398a9 100644 --- a/commands/docker-compose.ts +++ b/commands/docker-compose.ts @@ -9,6 +9,7 @@ import { UserCancelledError } from 'vscode-azureextensionui'; import { COMPOSE_FILE_GLOB_PATTERN } from '../dockerExtension'; import { ext } from '../extensionVariables'; import { reporter } from '../telemetry/telemetry'; +import { quickPickWorkspaceFolder } from './utils/quickPickWorkspaceFolder'; const teleCmdId: string = 'vscode-docker.compose.'; // we append up or down when reporting telemetry async function getDockerComposeFileUris(folder: vscode.WorkspaceFolder): Promise { @@ -41,22 +42,7 @@ function computeItems(folder: vscode.WorkspaceFolder, uris: vscode.Uri[]): vscod } async function compose(commands: ('up' | 'down')[], message: string, dockerComposeFileUri?: vscode.Uri, selectedComposeFileUris?: vscode.Uri[]): Promise { - let folder: vscode.WorkspaceFolder | undefined; - - if (!vscode.workspace.workspaceFolders) { - vscode.window.showErrorMessage('Docker compose can only run if VS Code is opened on a folder.'); - return; - } - - if (vscode.workspace.workspaceFolders.length === 1) { - folder = vscode.workspace.workspaceFolders[0]; - } else { - folder = await vscode.window.showWorkspaceFolderPick(); - } - - if (!folder) { - throw new UserCancelledError(); - } + let folder: vscode.WorkspaceFolder = await quickPickWorkspaceFolder('To run Docker compose you must first open a folder or workspace in VS Code.'); let commandParameterFileUris: vscode.Uri[]; if (selectedComposeFileUris && selectedComposeFileUris.length) { diff --git a/commands/utils/quickPickWorkspaceFolder.ts b/commands/utils/quickPickWorkspaceFolder.ts new file mode 100644 index 0000000000..541ad5d993 --- /dev/null +++ b/commands/utils/quickPickWorkspaceFolder.ts @@ -0,0 +1,21 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { UserCancelledError } from 'vscode-azureextensionui'; + +export async function quickPickWorkspaceFolder(noWorkspacesMessage: string): Promise { + if (vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length === 1) { + return vscode.workspace.workspaceFolders[0]; + } else if (vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 1) { + let selected = await vscode.window.showWorkspaceFolderPick(); + if (!selected) { + throw new UserCancelledError(); + } + return selected; + } else { + throw new Error(noWorkspacesMessage); + } +} diff --git a/configureWorkspace/configure.ts b/configureWorkspace/configure.ts index 28b112d81a..2e5bfa26fe 100644 --- a/configureWorkspace/configure.ts +++ b/configureWorkspace/configure.ts @@ -12,6 +12,7 @@ import * as path from "path"; import * as pomParser from "pom-parser"; import * as vscode from "vscode"; import { IActionContext, TelemetryProperties } from 'vscode-azureextensionui'; +import { quickPickWorkspaceFolder } from '../commands/utils/quickPickWorkspaceFolder'; import { ext } from '../extensionVariables'; import { globAsync } from '../helpers/async'; import { extractRegExGroups } from '../helpers/extractRegExGroups'; @@ -303,21 +304,7 @@ export interface ConfigureApiOptions { export async function configure(actionContext: IActionContext, rootFolderPath: string | undefined): Promise { if (!rootFolderPath) { - let folder: vscode.WorkspaceFolder | undefined; - if (vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length === 1) { - folder = vscode.workspace.workspaceFolders[0]; - } else { - folder = await vscode.window.showWorkspaceFolderPick(); - } - - if (!folder) { - if (!vscode.workspace.workspaceFolders) { - throw new Error('Docker files can only be generated if VS Code is opened on a folder.'); - } else { - throw new Error('Docker files can only be generated if a workspace folder is picked in VS Code.'); - } - } - + let folder: vscode.WorkspaceFolder = await quickPickWorkspaceFolder('To generate Docker files you must first open a folder or workspace in VS Code.'); rootFolderPath = folder.uri.fsPath; } From 6b6c77befa4d845c19c4ad5b93240ae041a301b2 Mon Sep 17 00:00:00 2001 From: "Stephen Weatherford (MSFT)" Date: Tue, 9 Oct 2018 15:52:35 -0700 Subject: [PATCH 11/36] Update to typescript 3 (#528) --- package.json | 2 +- tslint.json | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 36b42e769a..f29e3b149a 100644 --- a/package.json +++ b/package.json @@ -673,7 +673,7 @@ "gulp": "^3.9.1", "mocha": "5.2.0", "tslint": "^5.11.0", - "tslint-microsoft-contrib": "5.0.1", + "tslint-microsoft-contrib": "^5.2.1", "typescript": "^3.1.1", "vsce": "^1.51.1", "vscode": "^1.1.18" diff --git a/tslint.json b/tslint.json index 8195b90218..f3d6b11d67 100644 --- a/tslint.json +++ b/tslint.json @@ -53,7 +53,7 @@ "no-constant-condition": true, "no-control-regex": true, "no-debugger": true, - "no-duplicate-case": true, + "no-duplicate-switch-case": true, "no-duplicate-super": true, "no-duplicate-variable": true, "no-empty": false, // changed @@ -70,7 +70,7 @@ "no-reference-import": true, "no-regex-spaces": true, "no-sparse-arrays": true, - "no-stateless-class": true, + "no-unnecessary-class": true, "no-string-literal": true, "no-string-throw": true, "no-unnecessary-bind": true, @@ -101,7 +101,7 @@ ], "use-isnan": true, "use-named-parameter": true, - "valid-typeof": true, + "typeof-compare": true, "adjacent-overload-signatures": true, "array-type": [ true, @@ -169,7 +169,7 @@ "no-useless-files": true, "no-var-keyword": true, "no-var-requires": true, - "no-var-self": true, + "no-this-assignment": true, "no-void-expression": [ false, // changed "ignore-arrow-function-shorthand" // changed From 8cdc0bd8a6ec13c7b28811c58e60d04226fdfb02 Mon Sep 17 00:00:00 2001 From: rsamai Date: Thu, 11 Oct 2018 15:05:32 -0700 Subject: [PATCH 12/36] AzureCR/Pull Image from Azure (#417) * added pull on Repositories, cleaned up pull for upstream * header * added non context pull image * added pull on Repositories, cleaned up pull for upstream * header * added command palette option for pull * deleted blob function-unused here * cleanup acrTools * Enabled pull from repository * changed loginCredentials function to the one in master * Addressed most pr comments, without docker logged in not working * added getimagenamewithtag function * cleanup * change try catch tp callwithtelemetry * Tested workaround for error saving credentials, added stdin child process for password, broke up functions * Notify user before logging in to docker CLI * Fix lint --- commands/azureCommands/pull-from-azure.ts | 106 ++++++++++++++++++++++ constants.ts | 2 +- dockerExtension.ts | 4 +- explorer/models/azureRegistryNodes.ts | 6 +- package.json | 10 ++ utils/Azure/acrTools.ts | 5 +- 6 files changed, 128 insertions(+), 5 deletions(-) create mode 100644 commands/azureCommands/pull-from-azure.ts diff --git a/commands/azureCommands/pull-from-azure.ts b/commands/azureCommands/pull-from-azure.ts new file mode 100644 index 0000000000..f708a1dda9 --- /dev/null +++ b/commands/azureCommands/pull-from-azure.ts @@ -0,0 +1,106 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +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 path from "path"; +import vscode = require('vscode'); +import { callWithTelemetryAndErrorHandling, IActionContext, parseError } from 'vscode-azureextensionui'; +import { UserCancelledError } from '../../explorer/deploy/wizard'; +import { AzureImageTagNode, AzureRepositoryNode } from '../../explorer/models/azureRegistryNodes'; +import { ext } from '../../extensionVariables'; +import * as acrTools from '../../utils/Azure/acrTools'; +import { AzureImage } from "../../utils/Azure/models/image"; +import { Repository } from "../../utils/Azure/models/repository"; +import * as quickPicks from '../utils/quick-pick-azure'; + +/* Pulls an image from Azure. The context is the image node the user has right clicked on */ +export async function pullFromAzure(context?: AzureImageTagNode | AzureRepositoryNode): Promise { + let registryName: string; + let registry: Registry; + let imageName: string; + + if (context) { // Right Click + registryName = context.registry.loginServer; + registry = context.registry; + + if (context instanceof AzureImageTagNode) { // Right Click on AzureImageNode + imageName = context.label; + } else if (context instanceof AzureRepositoryNode) { // Right Click on AzureRepositoryNode + imageName = `${context.label} -a`; // Pull all images in repository + } else { + assert.fail(`Unexpected node type`); + } + + } else { // Command Palette + registry = await quickPicks.quickPickACRRegistry(); + registryName = registry.loginServer; + const repository: Repository = await quickPicks.quickPickACRRepository(registry, 'Select the repository of the image you want to pull'); + const image: AzureImage = await quickPicks.quickPickACRImage(repository, 'Select the image you want to pull'); + imageName = `${repository.name}:${image.tag}`; + } + + // Using loginCredentials function to get the username and password. This takes care of all users, even if they don't have the Azure CLI + const credentials = await acrTools.getLoginCredentials(registry); + const username = credentials.username; + const password = credentials.password; + await pullImage(registryName, imageName, username, password); +} + +async function pullImage(registryName: string, imageName: string, username: string, password: string): Promise { + // Check if user is logged into Docker and send appropriate commands to terminal + let result = await isLoggedIntoDocker(registryName); + if (!result.loggedIn) { // If not logged in to Docker + let login: vscode.MessageItem = { title: 'Log in to Docker CLI' }; + let msg = `You are not currently logged in to "${registryName}" in the Docker CLI.`; + let response = await vscode.window.showErrorMessage(msg, login) + if (response !== login) { + throw new UserCancelledError(msg); + } + + await new Promise((resolve, reject) => { + let childProcess = exec(`docker login ${registryName} --username ${username} --password-stdin`, (err, stdout, stderr) => { + ext.outputChannel.append(stdout); + ext.outputChannel.append(stderr); + if (err && err.message.match(/error storing credentials.*The stub received bad data/)) { + // Temporary work-around for this error- same as Azure CLI + // See https://github.com/Azure/azure-cli/issues/4843 + reject(new Error(`In order to log in to the Docker CLI using tokens, you currently need to go to \n${result.configPath} and remove "credsStore": "wincred" from the config.json file, then try again. \nDoing this will disable wincred and cause Docker to store credentials directly in the .docker/config.json file. All registries that are currently logged in will be effectly logged out.`)); + } else if (err) { + reject(err); + } else if (stderr) { + reject(stderr); + } + + resolve(); + }); + + childProcess.stdin.write(password); // Prevents insecure password error + childProcess.stdin.end(); + }); + } + + const terminal: vscode.Terminal = ext.terminalProvider.createTerminal("docker pull"); + terminal.show(); + + terminal.sendText(`docker pull ${registryName}/${imageName}`); +} + +async function isLoggedIntoDocker(registryName: string): Promise<{ configPath: string, loggedIn: boolean }> { + let home = process.env.HOMEPATH; + let configPath: string = path.join(home, '.docker', 'config.json'); + let buffer: Buffer; + + await callWithTelemetryAndErrorHandling('findDockerConfig', async function (this: IActionContext): Promise { + this.suppressTelemetry = true; + buffer = fs.readFileSync(configPath); + }); + + let index = buffer.indexOf(registryName); + let loggedIn = index >= 0; // Returns -1 if user is not logged into Docker + return { configPath, loggedIn }; // Returns object with configuration path and boolean indicating if user was logged in or not +} diff --git a/constants.ts b/constants.ts index d4c4114828..aafc08acf0 100644 --- a/constants.ts +++ b/constants.ts @@ -23,7 +23,7 @@ export namespace configurationKeys { } //Credentials Constants -export const NULL_GUID = '00000000-0000-0000-0000-000000000000'; +export const NULL_GUID = '00000000-0000-0000-0000-000000000000'; //Empty GUID is a special username to indicate the login credential is based on JWT token. //Azure Container Registries export const skus = ["Standard", "Basic", "Premium"]; diff --git a/dockerExtension.ts b/dockerExtension.ts index 37b4bfa90c..30359da48b 100644 --- a/dockerExtension.ts +++ b/dockerExtension.ts @@ -13,6 +13,7 @@ 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 { buildImage } from './commands/build-image'; import { composeDown, composeRestart, composeUp } from './commands/docker-compose'; import inspectImage from './commands/inspect-image'; @@ -127,7 +128,6 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { 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())); if (azureAccount) { @@ -137,7 +137,6 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { registerDockerCommands(azureAccount); ctx.subscriptions.push(vscode.debug.registerDebugConfigurationProvider('docker', new DockerDebugConfigProvider())); - await consolidateDefaultRegistrySettings(); activateLanguageClient(ctx); } @@ -230,6 +229,7 @@ function registerDockerCommands(azureAccount: AzureAccount | undefined): void { registerAzureCommand('vscode-docker.delete-ACR-Image', deleteAzureImage); registerAzureCommand('vscode-docker.delete-ACR-Repository', deleteRepository); registerAzureCommand('vscode-docker.create-ACR-Registry', createRegistry); + registerAzureCommand('vscode-docker.pull-ACR-Image', pullFromAzure); } export async function deactivate(): Promise { diff --git a/explorer/models/azureRegistryNodes.ts b/explorer/models/azureRegistryNodes.ts index 3f8aef822b..7e683be731 100644 --- a/explorer/models/azureRegistryNodes.ts +++ b/explorer/models/azureRegistryNodes.ts @@ -124,12 +124,16 @@ export class AzureImageTagNode extends NodeBase { public readonly tag: string, public readonly created: Date, ) { - super(`${repositoryName}:${tag}`); + super(AzureImageTagNode.getImageNameWithTag(repositoryName, tag)); } public static readonly contextValue: string = 'azureImageTagNode'; public readonly contextValue: string = AzureImageTagNode.contextValue; + public static getImageNameWithTag(repositoryName: string, tag: string): string { + return `${repositoryName}:${tag}`; + } + public getTreeItem(): vscode.TreeItem { return { label: formatTag(this.label, this.created), diff --git a/package.json b/package.json index f29e3b149a..8be1649760 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "onCommand:vscode-docker.connectCustomRegistry", "onCommand:vscode-docker.setRegistryAsDefault", "onCommand:vscode-docker.disconnectCustomRegistry", + "onCommand:vscode-docker.pull-ACR-Image", "onView:dockerExplorer", "onDebugInitialConfigurations" ], @@ -227,6 +228,10 @@ "command": "vscode-docker.browseAzurePortal", "when": "view == dockerExplorer && viewItem =~ /^(azureRegistryNode|azureRepositoryNode|azureImageTagNode)$/" }, + { + "command": "vscode-docker.pull-ACR-Image", + "when": "view == dockerExplorer && viewItem =~ /^(azureImageTagNode|azureRepositoryNode)$/" + }, { "command": "vscode-docker.connectCustomRegistry", "when": "view == dockerExplorer && viewItem == customRootNode" @@ -619,6 +624,11 @@ "command": "vscode-docker.disconnectCustomRegistry", "title": "Disconnect from Private Registry", "category": "Docker" + }, + { + "command": "vscode-docker.pull-ACR-Image", + "title": "Pull image from Azure", + "category": "Docker" } ], "views": { diff --git a/utils/Azure/acrTools.ts b/utils/Azure/acrTools.ts index 8ec082c743..089dc8edc7 100644 --- a/utils/Azure/acrTools.ts +++ b/utils/Azure/acrTools.ts @@ -21,6 +21,7 @@ import { Repository } from "./models/repository"; //General helpers /** Gets the subscription for a given registry + * @param registry gets the subscription for a given regsitry * @returns a subscription object */ export function getSubscriptionFromRegistry(registry: Registry): SubscriptionModels.Subscription { @@ -49,6 +50,7 @@ export async function getImagesByRepository(element: Repository): Promise { +export async function getLoginCredentials(registry: Registry): Promise<{ password: string, username: string }> { const subscription: Subscription = getSubscriptionFromRegistry(registry); const session: AzureSession = AzureUtilityManager.getInstance().getSession(subscription) const { aadAccessToken, aadRefreshToken } = await acquireAADTokens(session); From 4567018571e4b072bbb7214aac7ea4c5cfd39f2b Mon Sep 17 00:00:00 2001 From: "Stephen Weatherford (MSFT)" Date: Thu, 11 Oct 2018 15:58:25 -0700 Subject: [PATCH 13/36] Remove files written notification (#526) * Remove files written notification * Fix sentence * work * Open dockerfile --- configureWorkspace/configure.ts | 35 ++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/configureWorkspace/configure.ts b/configureWorkspace/configure.ts index 2e5bfa26fe..72612c696d 100644 --- a/configureWorkspace/configure.ts +++ b/configureWorkspace/configure.ts @@ -300,6 +300,11 @@ export interface ConfigureApiOptions { * The OS for the images. Currently only needed for .NET platforms. */ os?: OS; + + /** + * Open the Dockerfile that was generated + */ + openDockerFile?: boolean; } export async function configure(actionContext: IActionContext, rootFolderPath: string | undefined): Promise { @@ -308,20 +313,31 @@ export async function configure(actionContext: IActionContext, rootFolderPath: s rootFolderPath = folder.uri.fsPath; } - return configureCore( + let filesWritten = await configureCore( actionContext, { rootPath: rootFolderPath, - outputFolder: rootFolderPath + outputFolder: rootFolderPath, + openDockerFile: true }); + + // Open the dockerfile (if written) + try { + let dockerfile = filesWritten.find(fp => path.basename(fp).toLowerCase() === 'dockerfile'); + if (dockerfile) { + await vscode.window.showTextDocument(vscode.Uri.file(dockerfile)); + } + } catch (err) { + // Ignore + } } export async function configureApi(actionContext: IActionContext, options: ConfigureApiOptions): Promise { - return configureCore(actionContext, options); + await configureCore(actionContext, options); } // tslint:disable-next-line:max-func-body-length // Because of nested functions -async function configureCore(actionContext: IActionContext, options: ConfigureApiOptions): Promise { +async function configureCore(actionContext: IActionContext, options: ConfigureApiOptions): Promise { let properties: TelemetryProperties & ConfigureTelemetryProperties = actionContext.properties; let rootFolderPath: string = options.rootPath; let outputFolder = options.outputFolder; @@ -391,18 +407,13 @@ async function configureCore(actionContext: IActionContext, options: ConfigureAp return createWorkspaceFileIfNotExists(fileName, DOCKER_FILE_TYPES[fileName]); })); - // Don't wait - vscode.window.showInformationMessage( - filesWritten.length ? - `The following files were written into the workspace:${EOL}${EOL}${filesWritten.join(', ')}` : - "No files were written" - ); + return filesWritten; async function createWorkspaceFileIfNotExists(fileName: string, generatorFunction: GeneratorFunction): Promise { const filePath = path.join(outputFolder, fileName); let writeFile = false; if (await fse.pathExists(filePath)) { - const response: vscode.MessageItem | undefined = await vscode.window.showErrorMessage(`"${fileName}" already exists.Would you like to overwrite it?`, ...YES_OR_NO_PROMPTS); + const response: vscode.MessageItem | undefined = await vscode.window.showErrorMessage(`"${fileName}" already exists. Would you like to overwrite it?`, ...YES_OR_NO_PROMPTS); if (response === YES_PROMPT) { writeFile = true; } @@ -415,7 +426,7 @@ async function configureCore(actionContext: IActionContext, options: ConfigureAp let fileContents = generatorFunction(serviceNameAndPathRelativeToOutput, platformType, os, port, packageInfo); if (fileContents) { fs.writeFileSync(filePath, fileContents, { encoding: 'utf8' }); - filesWritten.push(fileName); + filesWritten.push(filePath); } } } From 0c2cc975b1aee418e811088556510eb50decafb7 Mon Sep 17 00:00:00 2001 From: "Stephen Weatherford (MSFT)" Date: Thu, 11 Oct 2018 17:59:38 -0700 Subject: [PATCH 14/36] Add simple activation perf telemetry and fix git type (#542) --- dockerExtension.ts | 54 +++++++++++++++++++++++++--------------------- package.json | 3 ++- 2 files changed, 31 insertions(+), 26 deletions(-) diff --git a/dockerExtension.ts b/dockerExtension.ts index 30359da48b..cbf00feb0c 100644 --- a/dockerExtension.ts +++ b/dockerExtension.ts @@ -7,7 +7,7 @@ import * as opn from 'opn'; import * as path from 'path'; import * as request from 'request-promise-native'; import * as vscode from 'vscode'; -import { AzureUserInput, createTelemetryReporter, IActionContext, parseError, registerCommand as uiRegisterCommand, registerUIExtensionVariables, TelemetryProperties, UserCancelledError } from 'vscode-azureextensionui'; +import { AzureUserInput, callWithTelemetryAndErrorHandling, createTelemetryReporter, IActionContext, parseError, registerCommand as uiRegisterCommand, registerUIExtensionVariables, TelemetryProperties, UserCancelledError } from 'vscode-azureextensionui'; import { ConfigurationParams, DidChangeConfigurationNotification, DocumentSelector, LanguageClient, LanguageClientOptions, Middleware, ServerOptions, TransportKind } from 'vscode-languageclient/lib/main'; import { createRegistry } from './commands/azureCommands/create-registry'; import { deleteAzureImage } from './commands/azureCommands/delete-image'; @@ -109,36 +109,40 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { initializeExtensionVariables(ctx); - // tslint:disable-next-line:prefer-for-of // Grandfathered in - for (let i = 0; i < installedExtensions.length; i++) { - const extension = installedExtensions[i]; - if (extension.id === 'ms-vscode.azure-account') { - try { - // tslint:disable-next-line:no-unsafe-any - azureAccount = await extension.activate(); - } catch (error) { - console.log('Failed to activate the Azure Account Extension: ' + parseError(error).message); + await callWithTelemetryAndErrorHandling('docker.activate', async function (this: IActionContext): Promise { + this.properties.isActivationEvent = 'true'; + + // tslint:disable-next-line:prefer-for-of // Grandfathered in + for (let i = 0; i < installedExtensions.length; i++) { + const extension = installedExtensions[i]; + if (extension.id === 'ms-vscode.azure-account') { + try { + // tslint:disable-next-line:no-unsafe-any + azureAccount = await extension.activate(); + } catch (error) { + console.log('Failed to activate the Azure Account Extension: ' + parseError(error).message); + } + break; } - break; } - } - ctx.subscriptions.push(vscode.languages.registerCompletionItemProvider(DOCUMENT_SELECTOR, new DockerfileCompletionItemProvider(), '.')); + 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())); + 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())); - if (azureAccount) { - AzureUtilityManager.getInstance().setAccount(azureAccount); - } + if (azureAccount) { + AzureUtilityManager.getInstance().setAccount(azureAccount); + } - registerDockerCommands(azureAccount); + registerDockerCommands(azureAccount); - ctx.subscriptions.push(vscode.debug.registerDebugConfigurationProvider('docker', new DockerDebugConfigProvider())); - await consolidateDefaultRegistrySettings(); - activateLanguageClient(ctx); + ctx.subscriptions.push(vscode.debug.registerDebugConfigurationProvider('docker', new DockerDebugConfigProvider())); + await consolidateDefaultRegistrySettings(); + activateLanguageClient(ctx); + }); } async function createWebApp(context?: AzureImageTagNode | DockerHubImageTagNode, azureAccount?: AzureAccount): Promise { diff --git a/package.json b/package.json index 8be1649760..d7efd6a138 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,8 @@ "multi-root ready" ], "repository": { - "url": "https://github.com/microsoft/vscode-docker.git" + "type": "git", + "url": "https://github.com/microsoft/vscode-docker" }, "homepage": "https://github.com/Microsoft/vscode-docker/blob/master/README.md", "activationEvents": [ From 5d15a1645943daa94d509d1ba20237b09199ef22 Mon Sep 17 00:00:00 2001 From: "Stephen Weatherford (MSFT)" Date: Fri, 12 Oct 2018 11:31:26 -0700 Subject: [PATCH 15/36] Fix truncation of multi-level image IDs (#530) * Fix truncation of multi-level image IDs * PR fix --- explorer/models/containerNode.ts | 11 +--- .../models/getImageOrContainerDisplayName.ts | 25 +++++++++ explorer/models/imageNode.ts | 11 +--- test/getImageOrContainerDisplayName.test.ts | 47 ++++++++++++++++ test/trimWithElipsis.test.ts | 55 +++++++++++++++++++ 5 files changed, 133 insertions(+), 16 deletions(-) create mode 100644 explorer/models/getImageOrContainerDisplayName.ts create mode 100644 test/getImageOrContainerDisplayName.test.ts create mode 100644 test/trimWithElipsis.test.ts diff --git a/explorer/models/containerNode.ts b/explorer/models/containerNode.ts index 2761126e33..8896b2e13c 100644 --- a/explorer/models/containerNode.ts +++ b/explorer/models/containerNode.ts @@ -5,6 +5,7 @@ import * as vscode from 'vscode'; import { trimWithElipsis } from '../utils/utils'; +import { getImageOrContainerDisplayName } from './getImageOrContainerDisplayName'; import { IconPath, NodeBase } from './nodeBase'; export type ContainerNodeContextValue = 'stoppedLocalContainerNode' | 'runningLocalContainerNode'; @@ -21,14 +22,8 @@ export class ContainerNode extends NodeBase { } public getTreeItem(): vscode.TreeItem { - let displayName: string = this.label; - - if (vscode.workspace.getConfiguration('docker').get('truncateLongRegistryPaths', false)) { - if (/\//.test(displayName)) { - let parts: string[] = this.label.split(/\//); - displayName = trimWithElipsis(parts[0], vscode.workspace.getConfiguration('docker').get('truncateMaxLength', 10)) + '/' + parts[1]; - } - } + let config = vscode.workspace.getConfiguration('docker'); + let displayName: string = getImageOrContainerDisplayName(this.label, config.get('truncateLongRegistryPaths'), config.get('truncateMaxLength')); return { label: `${displayName}`, diff --git a/explorer/models/getImageOrContainerDisplayName.ts b/explorer/models/getImageOrContainerDisplayName.ts new file mode 100644 index 0000000000..9ab49e07a4 --- /dev/null +++ b/explorer/models/getImageOrContainerDisplayName.ts @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { extractRegExGroups } from '../../helpers/extractRegExGroups'; +import { trimWithElipsis } from '../utils/utils'; + +export function getImageOrContainerDisplayName(fullName: string, truncateLongRegistryPaths: boolean, truncateMaxLength: number): string { + if (!truncateLongRegistryPaths) { + return fullName; + } + + // Extra registry from the rest of the name + let [registry, restOfName] = extractRegExGroups(fullName, /^([^\/]+)\/(.*)$/, ['', fullName]); + let trimmedRegistry: string | undefined; + + if (registry) { + registry = trimWithElipsis(registry, truncateMaxLength); + return `${registry}/${restOfName}`; + } + + return fullName; +} diff --git a/explorer/models/imageNode.ts b/explorer/models/imageNode.ts index c87211af96..54dc913e73 100644 --- a/explorer/models/imageNode.ts +++ b/explorer/models/imageNode.ts @@ -7,6 +7,7 @@ import * as moment from 'moment'; import * as path from 'path'; import * as vscode from 'vscode'; import { trimWithElipsis } from '../utils/utils'; +import { getImageOrContainerDisplayName } from './getImageOrContainerDisplayName'; import { NodeBase } from './nodeBase'; export class ImageNode extends NodeBase { @@ -23,14 +24,8 @@ export class ImageNode extends NodeBase { public readonly contextValue: string = ImageNode.contextValue; public getTreeItem(): vscode.TreeItem { - let displayName: string = this.label; - - if (vscode.workspace.getConfiguration('docker').get('truncateLongRegistryPaths', false)) { - if (/\//.test(displayName)) { - let parts: string[] = this.label.split(/\//); - displayName = trimWithElipsis(parts[0], vscode.workspace.getConfiguration('docker').get('truncateMaxLength', 10)) + '/' + parts[1]; - } - } + let config = vscode.workspace.getConfiguration('docker'); + let displayName: string = getImageOrContainerDisplayName(this.label, config.get('truncateLongRegistryPaths'), config.get('truncateMaxLength')); displayName = `${displayName} (${moment(new Date(this.imageDesc.Created * 1000)).fromNow()})`; diff --git a/test/getImageOrContainerDisplayName.test.ts b/test/getImageOrContainerDisplayName.test.ts new file mode 100644 index 0000000000..8a10d636a5 --- /dev/null +++ b/test/getImageOrContainerDisplayName.test.ts @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { getImageOrContainerDisplayName } from '../explorer/models/getImageOrContainerDisplayName'; + +suite('getImageOrContainerDisplayName', () => { + function genTest(fullName: string, trim: boolean, max: number, expected: string): void { + test(`${String(fullName)}: ${trim}/${max}`, () => { + let s2 = getImageOrContainerDisplayName(fullName, trim, max); + assert.equal(s2, expected); + }); + } + + genTest('', false, 0, ''); + genTest('', false, 1, ''); + genTest('', true, 0, ''); + genTest('', true, 1, ''); + + genTest('a', false, 1, 'a'); + genTest('abcdefghijklmnopqrstuvwxyz', false, 0, 'abcdefghijklmnopqrstuvwxyz'); + genTest('abcdefghijklmnopqrstuvwxyz', false, 1, 'abcdefghijklmnopqrstuvwxyz'); + genTest('abcdefghijklmnopqrstuvwxyz', false, 25, 'abcdefghijklmnopqrstuvwxyz'); + genTest('abcdefghijklmnopqrstuvwxyz', false, 90, 'abcdefghijklmnopqrstuvwxyz'); + + // No registry - use full image name + genTest('abcdefghijklmnopqrstuvwxyz', true, 0, 'abcdefghijklmnopqrstuvwxyz'); + genTest('abcdefghijklmnopqrstuvwxyz', true, 1, 'abcdefghijklmnopqrstuvwxyz'); + genTest('abcdefghijklmnopqrstuvwxyz', true, 2, 'abcdefghijklmnopqrstuvwxyz'); + genTest('abcdefghijklmnopqrstuvwxyz', true, 10, 'abcdefghijklmnopqrstuvwxyz'); + genTest('abcdefghijklmnopqrstuvwxyz', true, 99, 'abcdefghijklmnopqrstuvwxyz'); + + genTest('abcdefghijklmnopqrstuvwxyz:latest', true, 10, 'abcdefghijklmnopqrstuvwxyz:latest'); + + // Registry + one level + genTest('a/abcdefghijklmnopqrstuvwxyz:latest', true, 10, 'a/abcdefghijklmnopqrstuvwxyz:latest'); + genTest('abcdefghijklmnopqrstuvwxyz/abcdefghijklmnopqrstuvwxyz:latest', true, 10, 'abc...wxyz/abcdefghijklmnopqrstuvwxyz:latest'); + + // Registry + two or more levels + genTest('abcdefghijklmnopqrstuvwxyz/abcdefghijklmnopqrstuvwxyz/abcdefghijklmnopqrstuvwxyz:latest', true, 10, 'abc...wxyz/abcdefghijklmnopqrstuvwxyz/abcdefghijklmnopqrstuvwxyz:latest'); + genTest('abcdefghijklmnopqrstuvwxyz/abcdefghijklmnopqrstuvwxyz/abcdefghijklmnopqrstuvwxyz/abcdefghijklmnopqrstuvwxyz:latest', true, 10, 'abc...wxyz/abcdefghijklmnopqrstuvwxyz/abcdefghijklmnopqrstuvwxyz/abcdefghijklmnopqrstuvwxyz:latest'); + + // Real examples + genTest('registry.gitlab.com/sweatherford/hello-world/sub:latest', true, 7, 're...om/sweatherford/hello-world/sub:latest'); +}); diff --git a/test/trimWithElipsis.test.ts b/test/trimWithElipsis.test.ts new file mode 100644 index 0000000000..cee544f261 --- /dev/null +++ b/test/trimWithElipsis.test.ts @@ -0,0 +1,55 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { trimWithElipsis } from "../explorer/utils/utils"; +import * as assert from 'assert'; + +suite('trimWithElipsis', () => { + function genTest(s: string, max: number, expected: string): void { + test(`${String(s)}: ${max}`, () => { + let s2 = trimWithElipsis(s, max); + assert.equal(s2, expected); + }); + } + + genTest('', 0, ''); + genTest('', 100, ''); + + genTest('a', 0, 'a'); + genTest('a', 1, 'a'); + genTest('a', 2, 'a'); + + genTest('ab', 0, 'ab'); + genTest('ab', 1, 'a'); + genTest('ab', 2, 'ab'); + genTest('ab', 3, 'ab'); + + genTest('abc', 0, 'abc'); + genTest('abc', 1, 'a'); + genTest('abc', 2, 'ab'); + genTest('abc', 3, 'abc'); + genTest('abc', 4, 'abc'); + + genTest('abcd', 0, 'abcd'); + genTest('abcd', 1, 'a'); + genTest('abcd', 2, 'ab'); + genTest('abcd', 3, '...'); + genTest('abcd', 4, 'abcd'); + genTest('abcd', 5, 'abcd'); + + genTest('abcdefghijklmnopqrstuvwxyz', 1, 'a'); + genTest('abcdefghijklmnopqrstuvwxyz', 2, 'ab'); + genTest('abcdefghijklmnopqrstuvwxyz', 3, '...'); + genTest('abcdefghijklmnopqrstuvwxyz', 4, '...z'); + genTest('abcdefghijklmnopqrstuvwxyz', 5, 'a...z'); + genTest('abcdefghijklmnopqrstuvwxyz', 6, 'a...yz'); + genTest('abcdefghijklmnopqrstuvwxyz', 7, 'ab...yz'); + genTest('abcdefghijklmnopqrstuvwxyz', 8, 'ab...xyz'); + genTest('abcdefghijklmnopqrstuvwxyz', 9, 'abc...xyz'); + genTest('abcdefghijklmnopqrstuvwxyz', 10, 'abc...wxyz'); + genTest('abcdefghijklmnopqrstuvwxyz', 25, 'abcdefghijk...pqrstuvwxyz'); + genTest('abcdefghijklmnopqrstuvwxyz', 25, 'abcdefghijk...pqrstuvwxyz'); + genTest('abcdefghijklmnopqrstuvwxyz', 26, 'abcdefghijklmnopqrstuvwxyz'); +}); From 46bba4d233691cf07637895da21e41b3eb71cebc Mon Sep 17 00:00:00 2001 From: "Stephen Weatherford (MSFT)" Date: Fri, 12 Oct 2018 12:28:18 -0700 Subject: [PATCH 16/36] Better error message for .csproj not found. (#541) * Better error message for .csproj not found. * fix tests --- configureWorkspace/configure.ts | 2 +- test/configure.test.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/configureWorkspace/configure.ts b/configureWorkspace/configure.ts index 72612c696d..0f2169eb28 100644 --- a/configureWorkspace/configure.ts +++ b/configureWorkspace/configure.ts @@ -242,7 +242,7 @@ async function findCSProjFile(folderPath: string): Promise { const projectFiles: string[] = await globAsync('**/*.csproj', { cwd: folderPath }); if (!projectFiles || !projectFiles.length) { - throw new Error("No .csproj file could be found."); + throw new Error("No .csproj file could be found. You need a C# project file in the workspace to generate Docker files for the selected platform."); } if (projectFiles.length > 1) { diff --git a/test/configure.test.ts b/test/configure.test.ts index 63a7059c62..4314528e72 100644 --- a/test/configure.test.ts +++ b/test/configure.test.ts @@ -565,13 +565,13 @@ suite("Configure (Add Docker files to Workspace)", function (this: Suite): void }, ['Windows', '1234'] ), - { message: "No .csproj file could be found." } + { message: "No .csproj file could be found. You need a C# project file in the workspace to generate Docker files for the selected platform." } ); }); testInEmptyFolder("ASP.NET Core no project file", async () => { await assertEx.throwsOrRejectsAsync(async () => testConfigureDocker('ASP.NET Core', {}, ['Windows', '1234']), - { message: "No .csproj file could be found." } + { message: "No .csproj file could be found. You need a C# project file in the workspace to generate Docker files for the selected platform." } ); }); From b79a10cc8cf9374479490714c8bf747d22deb5b4 Mon Sep 17 00:00:00 2001 From: "Stephen Weatherford (MSFT)" Date: Mon, 15 Oct 2018 11:18:44 -0700 Subject: [PATCH 17/36] Add more timing telemetry (#546) --- dockerExtension.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/dockerExtension.ts b/dockerExtension.ts index cbf00feb0c..1b5c0c6d4e 100644 --- a/dockerExtension.ts +++ b/dockerExtension.ts @@ -3,6 +3,8 @@ * Licensed under the MIT License. See LICENSE.md in the project root for license information. *--------------------------------------------------------------------------------------------*/ +let loadStartTime = Date.now(); + import * as opn from 'opn'; import * as path from 'path'; import * as request from 'request-promise-native'; @@ -104,6 +106,8 @@ function initializeExtensionVariables(ctx: vscode.ExtensionContext): void { } export async function activate(ctx: vscode.ExtensionContext): Promise { + let activateStartTime = Date.now(); + const installedExtensions = vscode.extensions.all; let azureAccount: AzureAccount | undefined; @@ -111,14 +115,20 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { 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; // tslint:disable-next-line:prefer-for-of // Grandfathered in for (let i = 0; i < installedExtensions.length; i++) { const extension = installedExtensions[i]; if (extension.id === 'ms-vscode.azure-account') { try { + let activateAccountExtStart = Date.now(); // tslint:disable-next-line:no-unsafe-any azureAccount = await extension.activate(); + let activateAccountExtEnd = Date.now(); + + this.measurements.activateAccountExt = (activateAccountExtEnd - activateAccountExtStart) / 1000; } catch (error) { console.log('Failed to activate the Azure Account Extension: ' + parseError(error).message); } @@ -316,3 +326,5 @@ function activateLanguageClient(ctx: vscode.ExtensionContext): void { }); client.start(); } + +let loadEndTime = Date.now(); From d00f39aa34f544951ea044cd730223ca4257008b Mon Sep 17 00:00:00 2001 From: "Stephen Weatherford (MSFT)" Date: Tue, 16 Oct 2018 18:28:17 -0700 Subject: [PATCH 18/36] Delay loading of Azure Account extension (#552) * Delay loading of Azure Account extension * Remove registerAzureCommand --- commands/azureCommands/create-registry.ts | 2 +- commands/azureCommands/delete-registry.ts | 4 +- commands/utils/quick-pick-azure.ts | 4 +- dockerExtension.ts | 90 ++++++++------------- explorer/dockerExplorer.ts | 9 +-- explorer/models/azureRegistryNodes.ts | 4 +- explorer/models/registryRootNode.ts | 8 +- explorer/models/rootNode.ts | 15 ++-- explorer/utils/browseAzurePortal.ts | 2 - utils/Azure/acrTools.ts | 14 ++-- utils/Azure/common.ts | 26 +----- utils/Azure/models/repository.ts | 21 +++-- utils/azureUtilityManager.ts | 99 +++++++++++++++-------- 13 files changed, 138 insertions(+), 160 deletions(-) diff --git a/commands/azureCommands/create-registry.ts b/commands/azureCommands/create-registry.ts index 12bbac334b..816c428aaf 100644 --- a/commands/azureCommands/create-registry.ts +++ b/commands/azureCommands/create-registry.ts @@ -18,7 +18,7 @@ import { quickPickLocation, quickPickResourceGroup, quickPickSKU, quickPickSubsc export async function createRegistry(): Promise { const subscription: SubscriptionModels.Subscription = await quickPickSubscription(); const resourceGroup: ResourceGroup = await quickPickResourceGroup(true, subscription); - const client = AzureUtilityManager.getInstance().getContainerRegistryManagementClient(subscription); + const client = await AzureUtilityManager.getInstance().getContainerRegistryManagementClient(subscription); const registryName: string = await acquireRegistryName(client); const sku: string = await quickPickSKU(); const location = await quickPickLocation(subscription); diff --git a/commands/azureCommands/delete-registry.ts b/commands/azureCommands/delete-registry.ts index 8faf3d63db..3b2918c7a8 100644 --- a/commands/azureCommands/delete-registry.ts +++ b/commands/azureCommands/delete-registry.ts @@ -24,9 +24,9 @@ export async function deleteAzureRegistry(context?: AzureRegistryNode): Promise< } const shouldDelete = await confirmUserIntent(`Are you sure you want to delete ${registry.name} and its associated images?`); if (shouldDelete) { - let subscription: SubscriptionModels.Subscription = acrTools.getSubscriptionFromRegistry(registry); + let subscription: SubscriptionModels.Subscription = await acrTools.getSubscriptionFromRegistry(registry); let resourceGroup: string = acrTools.getResourceGroupName(registry); - const client = AzureUtilityManager.getInstance().getContainerRegistryManagementClient(subscription); + const client = await AzureUtilityManager.getInstance().getContainerRegistryManagementClient(subscription); await client.registries.beginDeleteMethod(resourceGroup, nonNullProp(registry, 'name')); vscode.window.showInformationMessage(`Successfully deleted registry ${registry.name}`); dockerExplorerProvider.refreshRegistries(); diff --git a/commands/utils/quick-pick-azure.ts b/commands/utils/quick-pick-azure.ts index cf4cecc1d9..259407bd16 100644 --- a/commands/utils/quick-pick-azure.ts +++ b/commands/utils/quick-pick-azure.ts @@ -64,7 +64,7 @@ export async function quickPickSKU(): Promise { } export async function quickPickSubscription(): Promise { - const subscriptions = AzureUtilityManager.getInstance().getFilteredSubscriptionList(); + const subscriptions = await AzureUtilityManager.getInstance().getFilteredSubscriptionList(); if (subscriptions.length === 0) { vscode.window.showErrorMessage("You do not have any subscriptions. You can create one in your Azure portal", "Open Portal").then(val => { if (val === "Open Portal") { @@ -144,7 +144,7 @@ export async function confirmUserIntent(yesOrNoPrompt: string): Promise /*Creates a new resource group within the current subscription */ async function createNewResourceGroup(loc: string, subscription?: Subscription): Promise { - const resourceGroupClient = AzureUtilityManager.getInstance().getResourceManagementClient(subscription); + const resourceGroupClient = await AzureUtilityManager.getInstance().getResourceManagementClient(subscription); let opt: vscode.InputBoxOptions = { validateInput: async (value: string) => { return await checkForValidResourcegroupName(value, resourceGroupClient) }, diff --git a/dockerExtension.ts b/dockerExtension.ts index 1b5c0c6d4e..856e35c360 100644 --- a/dockerExtension.ts +++ b/dockerExtension.ts @@ -5,6 +5,7 @@ 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'; @@ -29,7 +30,7 @@ import { showLogsContainer } from './commands/showlogs-container'; import { startAzureCLI, startContainer, startContainerInteractive } from './commands/start-container'; import { stopContainer } from './commands/stop-container'; import { systemPrune } from './commands/system-prune'; -import { IHasImageDescriptorAndLabel, tagImage } from './commands/tag-image'; +import { tagImage } from './commands/tag-image'; import { docker } from './commands/utils/docker-endpoint'; import { DefaultTerminalProvider } from './commands/utils/TerminalProvider'; import { DockerDebugConfigProvider } from './configureWorkspace/configDebugProvider'; @@ -57,7 +58,6 @@ import { ext } from "./extensionVariables"; import { initializeTelemetryReporter, reporter } from './telemetry/telemetry'; import { AzureAccount } from './typings/azure-account.api'; import { addUserAgent } from './utils/addUserAgent'; -import { registerAzureCommand } from './utils/Azure/common'; import { AzureUtilityManager } from './utils/azureUtilityManager'; import { Keytar } from './utils/keytar'; @@ -108,9 +108,6 @@ function initializeExtensionVariables(ctx: vscode.ExtensionContext): void { export async function activate(ctx: vscode.ExtensionContext): Promise { let activateStartTime = Date.now(); - const installedExtensions = vscode.extensions.all; - let azureAccount: AzureAccount | undefined; - initializeExtensionVariables(ctx); await callWithTelemetryAndErrorHandling('docker.activate', async function (this: IActionContext): Promise { @@ -118,23 +115,6 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { this.measurements.mainFileLoad = (loadEndTime - loadStartTime) / 1000; this.measurements.mainFileLoadedToActivate = (activateStartTime - loadEndTime) / 1000; - // tslint:disable-next-line:prefer-for-of // Grandfathered in - for (let i = 0; i < installedExtensions.length; i++) { - const extension = installedExtensions[i]; - if (extension.id === 'ms-vscode.azure-account') { - try { - let activateAccountExtStart = Date.now(); - // tslint:disable-next-line:no-unsafe-any - azureAccount = await extension.activate(); - let activateAccountExtEnd = Date.now(); - - this.measurements.activateAccountExt = (activateAccountExtEnd - activateAccountExtStart) / 1000; - } catch (error) { - console.log('Failed to activate the Azure Account Extension: ' + parseError(error).message); - } - break; - } - } 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 }; @@ -143,37 +123,32 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { ctx.subscriptions.push(vscode.languages.registerCompletionItemProvider(YAML_MODE_ID, new DockerComposeCompletionItemProvider(), '.')); ctx.subscriptions.push(vscode.workspace.registerTextDocumentContentProvider(DOCKER_INSPECT_SCHEME, new DockerInspectDocumentContentProvider())); - if (azureAccount) { - AzureUtilityManager.getInstance().setAccount(azureAccount); - } - - registerDockerCommands(azureAccount); + registerDockerCommands(); ctx.subscriptions.push(vscode.debug.registerDebugConfigurationProvider('docker', new DockerDebugConfigProvider())); 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, azureAccount?: AzureAccount): Promise { - if (context) { - if (azureAccount) { - 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(); - } - } else { - const open: vscode.MessageItem = { title: "View in Marketplace" }; - const response = await vscode.window.showErrorMessage('Please install the Azure Account extension to deploy to Azure.', open); - if (response === open) { - // tslint:disable-next-line:no-unsafe-any - opn('https://marketplace.visualstudio.com/items?itemName=ms-vscode.azure-account'); - } - } +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(); } } @@ -201,8 +176,8 @@ function registerCommand(commandId: string, callback: (this: IActionContext, ... }); } -function registerDockerCommands(azureAccount: AzureAccount | undefined): void { - dockerExplorerProvider = new DockerExplorerProvider(azureAccount); +function registerDockerCommands(): void { + dockerExplorerProvider = new DockerExplorerProvider(); vscode.window.registerTreeDataProvider('dockerExplorer', dockerExplorerProvider); registerCommand('vscode-docker.explorer.refresh', () => dockerExplorerProvider.refresh()); @@ -228,22 +203,23 @@ function registerDockerCommands(azureAccount: AzureAccount | undefined): void { registerCommand('vscode-docker.compose.down', composeDown); registerCommand('vscode-docker.compose.restart', composeRestart); registerCommand('vscode-docker.system.prune', systemPrune); - registerCommand('vscode-docker.createWebApp', async (context?: AzureImageTagNode | DockerHubImageTagNode) => await createWebApp(context, azureAccount)); registerCommand('vscode-docker.dockerHubLogout', dockerHubLogout); registerCommand('vscode-docker.browseDockerHub', (context?: DockerHubImageTagNode | DockerHubRepositoryNode | DockerHubOrgNode) => { browseDockerHub(context); }); - registerCommand('vscode-docker.browseAzurePortal', (context?: AzureRegistryNode | AzureRepositoryNode | AzureImageTagNode) => { - browseAzurePortal(context); - }); registerCommand('vscode-docker.connectCustomRegistry', connectCustomRegistry); registerCommand('vscode-docker.disconnectCustomRegistry', disconnectCustomRegistry); registerCommand('vscode-docker.setRegistryAsDefault', setRegistryAsDefault); - registerAzureCommand('vscode-docker.delete-ACR-Registry', deleteAzureRegistry); - registerAzureCommand('vscode-docker.delete-ACR-Image', deleteAzureImage); - registerAzureCommand('vscode-docker.delete-ACR-Repository', deleteRepository); - registerAzureCommand('vscode-docker.create-ACR-Registry', createRegistry); - registerAzureCommand('vscode-docker.pull-ACR-Image', pullFromAzure); + + 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); } export async function deactivate(): Promise { diff --git a/explorer/dockerExplorer.ts b/explorer/dockerExplorer.ts index 73ae253ff5..f08a81ee30 100644 --- a/explorer/dockerExplorer.ts +++ b/explorer/dockerExplorer.ts @@ -4,8 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { docker } from '../commands/utils/docker-endpoint'; -import { AzureAccount } from '../typings/azure-account.api'; import { NodeBase } from './models/nodeBase'; import { RootNode } from './models/rootNode'; @@ -16,11 +14,6 @@ export class DockerExplorerProvider implements vscode.TreeDataProvider private _imagesNode: RootNode | undefined; private _containersNode: RootNode | undefined; private _registriesNode: RootNode | undefined; - private _azureAccount: AzureAccount | undefined; - - constructor(azureAccount: AzureAccount | undefined) { - this._azureAccount = azureAccount; - } public refresh(): void { this.refreshImages(); @@ -67,7 +60,7 @@ export class DockerExplorerProvider implements vscode.TreeDataProvider this._containersNode = node; rootNodes.push(node); - node = new RootNode('Registries', 'registriesRootNode', this._onDidChangeTreeData, this._azureAccount); + node = new RootNode('Registries', 'registriesRootNode', this._onDidChangeTreeData); this._registriesNode = node; rootNodes.push(node); diff --git a/explorer/models/azureRegistryNodes.ts b/explorer/models/azureRegistryNodes.ts index 7e683be731..bf37520984 100644 --- a/explorer/models/azureRegistryNodes.ts +++ b/explorer/models/azureRegistryNodes.ts @@ -18,7 +18,7 @@ import { IconPath, NodeBase } from './nodeBase'; export class AzureRegistryNode extends NodeBase { constructor( public readonly label: string, - public readonly azureAccount: AzureAccount | undefined, + public readonly azureAccount: AzureAccount, public readonly registry: ContainerModels.Registry, public readonly subscription: SubscriptionModels.Subscription ) { @@ -94,7 +94,7 @@ export class AzureRepositoryNode extends NodeBase { public async getChildren(element: AzureRepositoryNode): Promise { const imageNodes: AzureImageTagNode[] = []; let node: AzureImageTagNode; - let repo = new Repository(element.registry, element.label); + let repo = await Repository.Create(element.registry, element.label); let images: AzureImage[] = await getImagesByRepository(repo); for (let img of images) { node = new AzureImageTagNode( diff --git a/explorer/models/registryRootNode.ts b/explorer/models/registryRootNode.ts index 39ee0c7720..808600b130 100644 --- a/explorer/models/registryRootNode.ts +++ b/explorer/models/registryRootNode.ts @@ -27,8 +27,8 @@ export class RegistryRootNode extends NodeBase { constructor( public readonly label: string, public readonly contextValue: 'dockerHubRootNode' | 'azureRegistryRootNode' | 'customRootNode', - public readonly eventEmitter?: vscode.EventEmitter, - public readonly azureAccount?: AzureAccount + public readonly eventEmitter: vscode.EventEmitter | undefined, // Needed only for Azure + public readonly azureAccount: AzureAccount | undefined // Needed only for Azure ) { super(label); @@ -133,14 +133,14 @@ export class RegistryRootNode extends NodeBase { } if (loggedIntoAzure) { - const subscriptions: SubscriptionModels.Subscription[] = AzureUtilityManager.getInstance().getFilteredSubscriptionList(); + const subscriptions: SubscriptionModels.Subscription[] = await AzureUtilityManager.getInstance().getFilteredSubscriptionList(); const subPool = new AsyncPool(MAX_CONCURRENT_SUBSCRIPTON_REQUESTS); let subsAndRegistries: { 'subscription': SubscriptionModels.Subscription, 'registries': ContainerModels.RegistryListResult }[] = []; //Acquire each subscription's data simultaneously for (let sub of subscriptions) { subPool.addTask(async () => { - const client = AzureUtilityManager.getInstance().getContainerRegistryManagementClient(sub); + const client = await AzureUtilityManager.getInstance().getContainerRegistryManagementClient(sub); try { let regs: ContainerModels.Registry[] = await client.registries.list(); subsAndRegistries.push({ diff --git a/explorer/models/rootNode.ts b/explorer/models/rootNode.ts index 6312f58104..3dfece2407 100644 --- a/explorer/models/rootNode.ts +++ b/explorer/models/rootNode.ts @@ -7,6 +7,7 @@ import * as path from 'path'; import * as vscode from 'vscode'; import { docker } from '../../commands/utils/docker-endpoint'; import { AzureAccount } from '../../typings/azure-account.api'; +import { AzureUtilityManager } from '../../utils/azureUtilityManager'; import { ContainerNode, ContainerNodeContextValue } from './containerNode'; import { ImageNode } from './imageNode'; import { IconPath, NodeBase } from './nodeBase'; @@ -31,13 +32,11 @@ export class RootNode extends NodeBase { private _containerCache: Docker.ContainerDesc[] | undefined; private _containerDebounceTimer: NodeJS.Timer | undefined; private _containersNode: RootNode | undefined; - private _azureAccount: AzureAccount | undefined; constructor( public readonly label: string, public readonly contextValue: 'imagesRootNode' | 'containersRootNode' | 'registriesRootNode', - public eventEmitter: vscode.EventEmitter, - public azureAccount?: AzureAccount + public eventEmitter: vscode.EventEmitter ) { super(label); if (this.contextValue === 'imagesRootNode') { @@ -45,7 +44,6 @@ export class RootNode extends NodeBase { } else if (this.contextValue === 'containersRootNode') { this._containersNode = this; } - this._azureAccount = azureAccount; } public autoRefreshImages(): void { @@ -260,13 +258,14 @@ export class RootNode extends NodeBase { private async getRegistries(): Promise { const registryRootNodes: RegistryRootNode[] = []; - registryRootNodes.push(new RegistryRootNode('Docker Hub', "dockerHubRootNode")); + registryRootNodes.push(new RegistryRootNode('Docker Hub', "dockerHubRootNode", undefined, undefined)); - if (this._azureAccount) { - registryRootNodes.push(new RegistryRootNode('Azure', "azureRegistryRootNode", this.eventEmitter, this._azureAccount)); + let azureAccount: AzureAccount = await AzureUtilityManager.getInstance().tryGetAzureAccount(); + if (azureAccount) { + registryRootNodes.push(new RegistryRootNode('Azure', "azureRegistryRootNode", this.eventEmitter, azureAccount)); } - registryRootNodes.push(new RegistryRootNode('Private Registries', 'customRootNode')); + registryRootNodes.push(new RegistryRootNode('Private Registries', 'customRootNode', undefined, undefined)); return registryRootNodes; } diff --git a/explorer/utils/browseAzurePortal.ts b/explorer/utils/browseAzurePortal.ts index dfb9480b39..063e815ce8 100644 --- a/explorer/utils/browseAzurePortal.ts +++ b/explorer/utils/browseAzurePortal.ts @@ -9,7 +9,6 @@ import { getTenantId, nonNullValue } from '../../utils/nonNull'; import { AzureImageTagNode, AzureRegistryNode, AzureRepositoryNode } from '../models/azureRegistryNodes'; export function browseAzurePortal(node?: AzureRegistryNode | AzureRepositoryNode | AzureImageTagNode): void { - if (node && node.azureAccount) { const tenantId: string = getTenantId(node.subscription); const session: AzureSession = nonNullValue( @@ -22,5 +21,4 @@ export function browseAzurePortal(node?: AzureRegistryNode | AzureRepositoryNode // tslint:disable-next-line:no-unsafe-any opn(url); } - } diff --git a/utils/Azure/acrTools.ts b/utils/Azure/acrTools.ts index 089dc8edc7..cec81371c2 100644 --- a/utils/Azure/acrTools.ts +++ b/utils/Azure/acrTools.ts @@ -24,10 +24,10 @@ import { Repository } from "./models/repository"; * @param registry gets the subscription for a given regsitry * @returns a subscription object */ -export function getSubscriptionFromRegistry(registry: Registry): SubscriptionModels.Subscription { +export async function getSubscriptionFromRegistry(registry: Registry): Promise { let id = getId(registry); let subscriptionId = id.slice('/subscriptions/'.length, id.search('/resourceGroups/')); - const subs = AzureUtilityManager.getInstance().getFilteredSubscriptionList(); + const subs = await AzureUtilityManager.getInstance().getFilteredSubscriptionList(); let subscription = subs.find((sub): boolean => { return sub.subscriptionId === subscriptionId; }); @@ -67,7 +67,7 @@ export async function getRepositoriesByRegistry(registry: Registry): Promise { - const subscription: Subscription = getSubscriptionFromRegistry(registry); - const session: AzureSession = AzureUtilityManager.getInstance().getSession(subscription) + const subscription: Subscription = await getSubscriptionFromRegistry(registry); + const session: AzureSession = await AzureUtilityManager.getInstance().getSession(subscription) const { aadAccessToken, aadRefreshToken } = await acquireAADTokens(session); const acrRefreshToken = await acquireACRRefreshToken(getLoginServer(registry), session.tenantId, aadRefreshToken, aadAccessToken); return { 'password': acrRefreshToken, 'username': NULL_GUID }; @@ -114,8 +114,8 @@ export async function getLoginCredentials(registry: Registry): Promise<{ passwor * @returns acrRefreshToken: For use as a Password for docker registry access , acrAccessToken: For use with docker API */ export async function acquireACRAccessTokenFromRegistry(registry: Registry, scope: string): Promise<{ acrRefreshToken: string, acrAccessToken: string }> { - const subscription: Subscription = getSubscriptionFromRegistry(registry); - const session: AzureSession = AzureUtilityManager.getInstance().getSession(subscription); + const subscription: Subscription = await getSubscriptionFromRegistry(registry); + const session: AzureSession = await AzureUtilityManager.getInstance().getSession(subscription); const { aadAccessToken, aadRefreshToken } = await acquireAADTokens(session); let loginServer = getLoginServer(registry); const acrRefreshToken = await acquireACRRefreshToken(loginServer, session.tenantId, aadRefreshToken, aadAccessToken); diff --git a/utils/Azure/common.ts b/utils/Azure/common.ts index 1514d5f206..74c1c0d747 100644 --- a/utils/Azure/common.ts +++ b/utils/Azure/common.ts @@ -1,6 +1,7 @@ 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]*$'); @@ -14,28 +15,3 @@ export function isValidAzureName(value: string): { isValid: boolean, message?: s return { isValid: true }; } } - -/** Uses consistent error handling from register command to replace callbacks for commands that have a dependency on azure account. - * If the dependency is not found notifies users providing them with information to go download the extension. - */ -// tslint:disable-next-line:no-any -export function registerAzureCommand(commandId: string, callback: (...args: any[]) => any): void { - let commandItem: (actionContext: IActionContext) => void; - - if (!AzureUtilityManager.hasLoadedUtilityManager()) { - commandItem = () => { - const open: vscode.MessageItem = { title: "View in Marketplace" }; - vscode.window.showErrorMessage('Please install the Azure Account extension to use Azure features.', open).then((response) => { - if (response === open) { - // tslint:disable-next-line:no-unsafe-any - opn('https://marketplace.visualstudio.com/items?itemName=ms-vscode.azure-account'); - } - }); - } - - } else { - commandItem = callback; - } - - registerCommand(commandId, commandItem); -} diff --git a/utils/Azure/models/repository.ts b/utils/Azure/models/repository.ts index be87acd391..de3b3ae483 100644 --- a/utils/Azure/models/repository.ts +++ b/utils/Azure/models/repository.ts @@ -15,12 +15,19 @@ export class Repository { public password?: string; public username?: string; - constructor(registry: Registry, repository: string, password?: string, username?: string) { - this.registry = registry; - this.resourceGroupName = acrTools.getResourceGroupName(registry); - this.subscription = acrTools.getSubscriptionFromRegistry(registry); - this.name = repository; - if (password) { this.password = password; } - if (username) { this.username = username; } + private constructor() { + } + + public static async Create(registry: Registry, repositoryName: string, password?: string, username?: string): Promise { + let repository = new Repository(); + + repository.registry = registry; + repository.resourceGroupName = acrTools.getResourceGroupName(registry); + repository.subscription = await acrTools.getSubscriptionFromRegistry(registry); + repository.name = repositoryName; + repository.password = password; + repository.username = username; + + return repository; } } diff --git a/utils/azureUtilityManager.ts b/utils/azureUtilityManager.ts index 4d5efe2e68..624d0e81ed 100644 --- a/utils/azureUtilityManager.ts +++ b/utils/azureUtilityManager.ts @@ -10,7 +10,9 @@ import { ResourceManagementClient, SubscriptionClient, SubscriptionModels } from import { ResourceGroup } from "azure-arm-resource/lib/resource/models"; import { Subscription } from 'azure-arm-resource/lib/subscription/models'; import { ServiceClientCredentials } from 'ms-rest'; -import { addExtensionUserAgent } from 'vscode-azureextensionui'; +import * as opn from 'opn'; +import * as vscode from 'vscode'; +import { addExtensionUserAgent, callWithTelemetryAndErrorHandling, IActionContext, parseError, UserCancelledError } from 'vscode-azureextensionui'; import { MAX_CONCURRENT_SUBSCRIPTON_REQUESTS } from '../constants'; import { AzureAccount, AzureSession } from '../typings/azure-account.api'; import { AsyncPool } from './asyncpool'; @@ -22,15 +24,28 @@ import { getSubscriptionId, getTenantId } from './nonNull'; */ export class AzureUtilityManager { - - //SETUP private static _instance: AzureUtilityManager; - private azureAccount: AzureAccount | undefined; + private _azureAccountPromise: Promise; private constructor() { } - public static hasLoadedUtilityManager(): boolean { - if (AzureUtilityManager._instance) { return true; } else { return false; } + private async loadAzureAccountExtension(): Promise { + let azureAccount: AzureAccount | undefined; + + // tslint:disable-next-line:no-function-expression + await callWithTelemetryAndErrorHandling('docker.loadAzureAccountExt', async function (this: IActionContext): Promise { + try { + let azureAccountExtension = vscode.extensions.getExtension('ms-vscode.azure-account'); + this.properties.found = azureAccountExtension ? 'true' : 'false'; + if (azureAccountExtension) { + azureAccount = await azureAccountExtension.activate(); + } + } catch (error) { + throw new Error('Failed to activate the Azure Account Extension: ' + parseError(error).message); + } + }); + + return azureAccount; } public static getInstance(): AzureUtilityManager { @@ -40,22 +55,34 @@ export class AzureUtilityManager { return AzureUtilityManager._instance; } - //This function has to be called explicitly before using the singleton. - public setAccount(azureAccount: AzureAccount): void { - this.azureAccount = azureAccount; + public async tryGetAzureAccount(): Promise { + if (!this._azureAccountPromise) { + this._azureAccountPromise = this.loadAzureAccountExtension(); + } + + return await this._azureAccountPromise; } - //GETTERS - public getAccount(): AzureAccount { - if (this.azureAccount) { - return this.azureAccount; + public async requireAzureAccount(): Promise { + let azureAccount = await this.tryGetAzureAccount(); + if (azureAccount) { + return azureAccount; + } else { + const open: vscode.MessageItem = { title: "View in Marketplace" }; + const msg = 'This functionality requires installing the Azure Account extension.'; + let response = await vscode.window.showErrorMessage(msg, open); + if (response === open) { + // tslint:disable-next-line:no-unsafe-any + opn('https://marketplace.visualstudio.com/items?itemName=ms-vscode.azure-account'); + } + + throw new UserCancelledError(msg); } - throw new Error('Azure account is not present, you may have forgotten to call setAccount'); } - public getSession(subscription: SubscriptionModels.Subscription): AzureSession { + public async getSession(subscription: SubscriptionModels.Subscription): Promise { const tenantId: string = getTenantId(subscription); - const azureAccount: AzureAccount = this.getAccount(); + const azureAccount: AzureAccount = await this.requireAzureAccount(); let foundSession = azureAccount.sessions.find((s) => s.tenantId.toLowerCase() === tenantId.toLowerCase()); if (!foundSession) { throw new Error(`Could not find a session with tenantId "${tenantId}"`); @@ -64,8 +91,8 @@ export class AzureUtilityManager { return foundSession; } - public getFilteredSubscriptionList(): SubscriptionModels.Subscription[] { - return this.getAccount().filters.map(filter => { + public async getFilteredSubscriptionList(): Promise { + return (await this.requireAzureAccount()).filters.map(filter => { return { id: filter.subscription.id, subscriptionId: filter.subscription.subscriptionId, @@ -78,39 +105,40 @@ export class AzureUtilityManager { }); } - public getContainerRegistryManagementClient(subscription: SubscriptionModels.Subscription): ContainerRegistryManagementClient { - let client = new ContainerRegistryManagementClient(this.getCredentialByTenantId(subscription), getSubscriptionId(subscription)); + public async getContainerRegistryManagementClient(subscription: SubscriptionModels.Subscription): Promise { + let client = new ContainerRegistryManagementClient(await this.getCredentialByTenantId(subscription), getSubscriptionId(subscription)); addExtensionUserAgent(client); return client; } - public getResourceManagementClient(subscription: SubscriptionModels.Subscription): ResourceManagementClient { - return new ResourceManagementClient(this.getCredentialByTenantId(getTenantId(subscription)), getSubscriptionId(subscription)); + public async getResourceManagementClient(subscription: SubscriptionModels.Subscription): Promise { + return new ResourceManagementClient(await this.getCredentialByTenantId(getTenantId(subscription)), getSubscriptionId(subscription)); } public async getRegistries(subscription?: Subscription, resourceGroup?: string, - compareFn: (a: ContainerModels.Registry, b: ContainerModels.Registry) => number = this.sortRegistriesAlphabetically): Promise { + compareFn: (a: ContainerModels.Registry, b: ContainerModels.Registry) => number = this.sortRegistriesAlphabetically + ): Promise { let registries: ContainerModels.Registry[] = []; if (subscription && resourceGroup) { //Get all registries under one resourcegroup - const client = this.getContainerRegistryManagementClient(subscription); + const client = await this.getContainerRegistryManagementClient(subscription); registries = await client.registries.listByResourceGroup(resourceGroup); } else if (subscription) { //Get all registries under one subscription - const client = this.getContainerRegistryManagementClient(subscription); + const client = await this.getContainerRegistryManagementClient(subscription); registries = await client.registries.list(); } else { //Get all registries for all subscriptions - const subs: SubscriptionModels.Subscription[] = this.getFilteredSubscriptionList(); + const subs: SubscriptionModels.Subscription[] = await this.getFilteredSubscriptionList(); const subPool = new AsyncPool(MAX_CONCURRENT_SUBSCRIPTON_REQUESTS); for (let sub of subs) { subPool.addTask(async () => { - const client = this.getContainerRegistryManagementClient(sub); + const client = await this.getContainerRegistryManagementClient(sub); let subscriptionRegistries: ContainerModels.Registry[] = await client.registries.list(); registries = registries.concat(subscriptionRegistries); }); @@ -130,16 +158,16 @@ export class AzureUtilityManager { public async getResourceGroups(subscription?: SubscriptionModels.Subscription): Promise { if (subscription) { - const resourceClient = this.getResourceManagementClient(subscription); + const resourceClient = await this.getResourceManagementClient(subscription); return await resourceClient.resourceGroups.list(); } - const subs = this.getFilteredSubscriptionList(); + const subs = await this.getFilteredSubscriptionList(); 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 = this.getResourceManagementClient(sub); + const resourceClient = await this.getResourceManagementClient(sub); const internalGroups = await resourceClient.resourceGroups.list(); resourceGroups = resourceGroups.concat(internalGroups); }); @@ -148,9 +176,9 @@ export class AzureUtilityManager { return resourceGroups; } - public getCredentialByTenantId(tenantIdOrSubscription: string | Subscription): ServiceClientCredentials { + public async getCredentialByTenantId(tenantIdOrSubscription: string | Subscription): Promise { let tenantId = typeof tenantIdOrSubscription === 'string' ? tenantIdOrSubscription : getTenantId(tenantIdOrSubscription); - const session = this.getAccount().sessions.find((azureSession) => azureSession.tenantId.toLowerCase() === tenantId.toLowerCase()); + const session = (await this.requireAzureAccount()).sessions.find((azureSession) => azureSession.tenantId.toLowerCase() === tenantId.toLowerCase()); if (session) { return session.credentials; @@ -160,7 +188,7 @@ export class AzureUtilityManager { } public async getLocationsBySubscription(subscription: SubscriptionModels.Subscription): Promise { - const credential = this.getCredentialByTenantId(getTenantId(subscription)); + const credential = await this.getCredentialByTenantId(getTenantId(subscription)); const client = new SubscriptionClient(credential); const locations = (await client.subscriptions.listLocations(getSubscriptionId(subscription))); return locations; @@ -169,9 +197,10 @@ export class AzureUtilityManager { //CHECKS //Provides a unified check for login that should be called once before using the rest of the singletons capabilities public async waitForLogin(): Promise { - if (!this.azureAccount) { + let account = await this.tryGetAzureAccount(); + if (!account) { return false; } - return await this.azureAccount.waitForLogin(); + return await account.waitForLogin(); } } From 88b25efe69e78cd631699c140f731d2cc0f40ef7 Mon Sep 17 00:00:00 2001 From: "Stephen Weatherford (MSFT)" Date: Tue, 16 Oct 2018 18:35:46 -0700 Subject: [PATCH 19/36] Allow configure template to specify not to ask for a port (#559) --- configureWorkspace/configure.ts | 4 ++-- configureWorkspace/configure_dotnetcore.ts | 2 +- test/configure.test.ts | 19 ++++--------------- 3 files changed, 7 insertions(+), 18 deletions(-) diff --git a/configureWorkspace/configure.ts b/configureWorkspace/configure.ts index 0f2169eb28..011dddb220 100644 --- a/configureWorkspace/configure.ts +++ b/configureWorkspace/configure.ts @@ -59,7 +59,7 @@ export interface IPlatformGeneratorInfo { genDockerFile: GeneratorFunction, genDockerCompose: GeneratorFunction, genDockerComposeDebug: GeneratorFunction, - defaultPort: string + defaultPort: string | undefined // '' = defaults to empty but still asks user if they want a port, undefined = don't ask at all } export function getExposeStatements(port: string): string { @@ -353,7 +353,7 @@ async function configureCore(actionContext: IActionContext, options: ConfigureAp properties.configureOs = os; let port: string | undefined = options.port; - if (!port) { + if (!port && generatorInfo.defaultPort !== undefined) { port = await promptForPort(generatorInfo.defaultPort); } diff --git a/configureWorkspace/configure_dotnetcore.ts b/configureWorkspace/configure_dotnetcore.ts index 0e8fd36abf..2e62f22808 100644 --- a/configureWorkspace/configure_dotnetcore.ts +++ b/configureWorkspace/configure_dotnetcore.ts @@ -25,7 +25,7 @@ export const configureDotNetCoreConsole: IPlatformGeneratorInfo = { genDockerFile, genDockerCompose: undefined, // We don't generate compose files for .net core genDockerComposeDebug: undefined, // We don't generate compose files for .net core - defaultPort: '' + defaultPort: undefined }; const AspNetCoreRuntimeImageFormat = "microsoft/aspnetcore:{0}.{1}{2}"; diff --git a/test/configure.test.ts b/test/configure.test.ts index 4314528e72..0e6da5cd19 100644 --- a/test/configure.test.ts +++ b/test/configure.test.ts @@ -392,7 +392,7 @@ suite("Configure (Add Docker files to Workspace)", function (this: Suite): void packageFileType: '.csproj', packageFileSubfolderDepth: '1' }, - [os, undefined /* no port */], + [os /* it doesn't ask for a port, so we don't specify one here */], ['Dockerfile', '.dockerignore', `${projectFolder}/Program.cs`, `${projectFolder}/${projectFileName}`] ); @@ -563,7 +563,7 @@ suite("Configure (Add Docker files to Workspace)", function (this: Suite): void packageFileType: undefined, packageFileSubfolderDepth: undefined }, - ['Windows', '1234'] + ['Windows'] ), { message: "No .csproj file could be found. You need a C# project file in the workspace to generate Docker files for the selected platform." } ); @@ -586,7 +586,7 @@ suite("Configure (Add Docker files to Workspace)", function (this: Suite): void packageFileType: '.csproj', packageFileSubfolderDepth: '1' }, - ['Windows', '1234', 'projectFolder2/aspnetapp.csproj'], + ['Windows', 'projectFolder2/aspnetapp.csproj'], ['Dockerfile', '.dockerignore', 'projectFolder1/aspnetapp.csproj', 'projectFolder2/aspnetapp.csproj'] ); @@ -598,17 +598,6 @@ suite("Configure (Add Docker files to Workspace)", function (this: Suite): void }); suite(".NET Core Console 2.1", async () => { - testInEmptyFolder("Default port (none)", async () => { - await writeFile('projectFolder1', 'aspnetapp.csproj', dotNetCoreConsole_21_ProjectFileContents); - await testConfigureDocker( - '.NET Core Console', - undefined, - ['Windows', undefined] - ); - - assertNotFileContains('Dockerfile', 'EXPOSE'); - }); - testInEmptyFolder("Windows", async () => { await testDotNetCoreConsole( 'Windows', @@ -1248,13 +1237,13 @@ suite("Configure (Add Docker files to Workspace)", function (this: Suite): void os: "Linux" }, [ - "555", // port 'projectFolder2/aspnetapp.csproj' ], ['Dockerfile', '.dockerignore', 'projectFolder1/aspnetapp.csproj', 'projectFolder2/aspnetapp.csproj'] ); assertFileContains('Dockerfile', 'ENTRYPOINT ["dotnet", "aspnetapp.dll"]'); assertNotFileContains('Dockerfile', 'projectFolder1'); + assertNotFileContains('Dockerfile', 'EXPOSE'); }); testInEmptyFolder("Only port specified, others come from user", async () => { From ed5c358f64f5611333187ec11182de5547f181a2 Mon Sep 17 00:00:00 2001 From: "Stephen Weatherford (MSFT)" Date: Wed, 17 Oct 2018 11:33:44 -0700 Subject: [PATCH 20/36] Fix casing of Pull Image from Azure (#558) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d7efd6a138..3704426be7 100644 --- a/package.json +++ b/package.json @@ -628,7 +628,7 @@ }, { "command": "vscode-docker.pull-ACR-Image", - "title": "Pull image from Azure", + "title": "Pull Image from Azure", "category": "Docker" } ], From ddb600aa2f1432ee12ea3b5d28f6b4ee90494167 Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Wed, 17 Oct 2018 14:17:02 -0700 Subject: [PATCH 21/36] Add debugging support for .NET Core in Docker containers (#500) * Pull in debugging prototype. * Support both launch configuration entrypoints. * Only show Docker output channel once per session. * Sketch removal of containers when debugging session ends. * Refactor launch cleanup. * Provide access to result/error in operation output. * Add nested output. * Some type renaming. * Hide debugging behind "feature flag" and add minimal docs. * Fix issue with running container using app folder containing variables. * Add C# extension dependency check. * Begin support for inferring app output. * Refactoring to please linter. * Properly deal with temp files. * Fix spelling error. * Switch to dotnet msbuild and normalize. * Cleanup of linting issues. * Updates per PR feedback. * Move dependency check into prereq infrastructure. * Add prerequisite for Mac on shared folder. * Cleanup only after Docker debugging. * Remove feature flag for debugging. * Switch to 'docker-coreclr' for debug adaptor name. * Move Docker-related options to the root of configuration. * Move debugger registration to separate file. * Rename directory. * Add tests for Mac prerequisite. * Constant extraction. * Update README per PR feedback. * More doc updates per PR feedback. * Resolved strictNullChecks errors. * Move Platform types to common utils folder. * Rename OS to PlatformOS and use in debugging implementation. * Add Docker shared folders image. * Doc updates per PR feedback. * Switch from any to unknown. * Update tests for Linux. * Add Linux prerequisite for user in docker group. * Add tests for Linux prerequisite. * Allow for older Docker versions on Mac in prerequisite check. * Add prerequisite check for a Linux Docker daemon. * Minor test fixes. * Add line-splitting utility. * Refactored to support Windows. * Refactored and added new test. * Use line splitter for output manager. * Use splitter in Docker client. * Fix lint warnings. * Fix generated default tag. * Some updates per PR feedback. * Add telemetry to configuration resolution. * Add prerequisite for the .NET Core SDK. * Rename MSBuild client. * Minor tweaks for strictNullChecks. --- CHANGELOG.md | 5 + README.md | 81 +++++ configureWorkspace/config-utils.ts | 18 +- configureWorkspace/configure.ts | 17 +- configureWorkspace/configure_dotnetcore.ts | 4 +- debugging/coreclr/appStorage.ts | 63 ++++ debugging/coreclr/browserClient.ts | 23 ++ debugging/coreclr/debugSessionManager.ts | 44 +++ debugging/coreclr/debuggerClient.ts | 25 ++ debugging/coreclr/dockerClient.ts | 232 ++++++++++++ .../dockerDebugConfigurationProvider.ts | 252 +++++++++++++ debugging/coreclr/dockerManager.ts | 330 ++++++++++++++++++ debugging/coreclr/dotNetClient.ts | 53 +++ debugging/coreclr/fsProvider.ts | 82 +++++ debugging/coreclr/lazy.ts | 26 ++ debugging/coreclr/lineSplitter.ts | 66 ++++ debugging/coreclr/netCoreProjectProvider.ts | 62 ++++ debugging/coreclr/osProvider.ts | 46 +++ debugging/coreclr/outputManager.ts | 82 +++++ debugging/coreclr/prereqManager.ts | 174 +++++++++ debugging/coreclr/processProvider.ts | 51 +++ debugging/coreclr/registerDebugger.ts | 95 +++++ debugging/coreclr/tempFileProvider.ts | 24 ++ debugging/coreclr/vsdbgClient.ts | 149 ++++++++ dockerExtension.ts | 3 + images/dockerSharedFolders.png | Bin 0 -> 64797 bytes package.json | 73 +++- test/buildAndRun.test.ts | 2 +- test/configure.test.ts | 6 +- test/debugging/coreclr/lineSplitter.test.ts | 45 +++ test/debugging/coreclr/prereqManager.test.ts | 215 ++++++++++++ utils/platform.ts | 15 + 32 files changed, 2333 insertions(+), 30 deletions(-) create mode 100644 debugging/coreclr/appStorage.ts create mode 100644 debugging/coreclr/browserClient.ts create mode 100644 debugging/coreclr/debugSessionManager.ts create mode 100644 debugging/coreclr/debuggerClient.ts create mode 100644 debugging/coreclr/dockerClient.ts create mode 100644 debugging/coreclr/dockerDebugConfigurationProvider.ts create mode 100644 debugging/coreclr/dockerManager.ts create mode 100644 debugging/coreclr/dotNetClient.ts create mode 100644 debugging/coreclr/fsProvider.ts create mode 100644 debugging/coreclr/lazy.ts create mode 100644 debugging/coreclr/lineSplitter.ts create mode 100644 debugging/coreclr/netCoreProjectProvider.ts create mode 100644 debugging/coreclr/osProvider.ts create mode 100644 debugging/coreclr/outputManager.ts create mode 100644 debugging/coreclr/prereqManager.ts create mode 100644 debugging/coreclr/processProvider.ts create mode 100644 debugging/coreclr/registerDebugger.ts create mode 100644 debugging/coreclr/tempFileProvider.ts create mode 100644 debugging/coreclr/vsdbgClient.ts create mode 100644 images/dockerSharedFolders.png create mode 100644 test/debugging/coreclr/lineSplitter.test.ts create mode 100644 test/debugging/coreclr/prereqManager.test.ts create mode 100644 utils/platform.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index ac2408cf37..e1830cb0c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.x.x - Unreleased + +### Added +* Adds preview support for debugging .NET Core web applications running in Linux Docker containers. + ## 0.3.1 - 25 September 2018 ### Fixed diff --git a/README.md b/README.md index 555eddead6..b4c0b8066a 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ The Docker extension makes it easy to build, manage and deploy containerized app * Command Palette (`F1`) integration for the most common Docker commands (for example `docker build`, `docker push`, etc.) * Explorer integration for managing Images, running Containers, and Docker Hub registries * Deploy images from Docker Hub and Azure Container Registries directly to Azure App Service +* Debug .NET Core applications running in Linux Docker containers * [Working with docker](https://code.visualstudio.com/docs/azure/docker) will walk you through many of the features of this extension @@ -86,6 +87,86 @@ After the container is started, you will be prompted to login to your Azure acco This build includes preview support for connecting to private registries (such as those described in Docker Hub [documentation](https://docs.docker.com/registry/deploying/)). At the moment, OAuth is not supported, only basic authentication. We hope to extend this support in the future. +## Debugging .NET Core ASP.NET (Preview) + +> Note that Windows containers are **not** currently supported, only Linux containers. + +### Prerequisites + + +1. (All users) Install the [.NET Core SDK](https://www.microsoft.com/net/download) which includes support for attaching to the .NET Core debugger. + +1. (All users) Install the [C# VS Code extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode.csharp) which includes support for attaching to the .NET Core debugger in VS Code. + +1. (Mac users) add `/usr/local/share/dotnet/sdk/NuGetFallbackFolder` as a shared folder in your Docker preferences. + +![Docker Shared Folders](images/dockerSharedFolders.png) + +### Starting the Debugger + +To debug a .NET Core ASP.NET application running in a Linux Docker container, add a Docker .NET Core launch configuration: + +1. Switch to the debugging tab. +1. Select `Add configuration...` +1. Select `Docker: Launch .NET Core ASP.NET (Preview)` +1. Set a breakpoint. +1. Start debugging. + +Upon debugging, a Docker image will be built and a container will be run based on that image. The container will have volumes mapped to the locally-built application and the .NET Core debugger. After the debugger is attached, the browser will be launched and navigate to the application's initial page. + +Most properties of the configuration are optional and will be inferred from the project. If not, or if there are additional customizations to be made to the Docker image build or container run process, those can be added under the `dockerBuild` and `dockerRun` properties of the configuration, respectively. + +```json +{ + "configurations": [ + { + "name": "Docker: Launch .NET Core ASP.NET (Preview)", + "type": "docker-coreclr", + "request": "launch", + "preLaunchTask": "build", + "dockerBuild": { + // Image customizations + }, + "dockerRun": { + // Container customizations + } + } + ] +} +``` + +### Application Customizations + +When possible, the location and output of the application will be inferred from the workspace folder opened in VS Code. When they cannot be inferred, these properties can be used to make them explicit: + +| Property | Description | Default | +| --- | --- | --- | +| `appFolder` | The root folder of the application | The workspace folder | +| `appProject` | The path to the project file | The first `.csproj` found in the application folder | +| `appOutput` | The application folder relative path to the output assembly | The `TargetPath` MS Build property | + +> You can specify either `appFolder` or `appProject` but should not specify *both*. + +### Docker Build Customizations + +Customize the Docker image build process by adding properties under the `dockerBuild` configuration property. + +| Property | Description | Default | +| --- | --- | --- | +| `context` | The Docker context used during the build process. | The workspace folder, if the same as the application folder; otherwise, the application's parent (i.e. solution) folder | +| `dockerfile` | The path to the Dockerfile used to build the image. | The file `Dockerfile` in the application folder | +| `tag` | The tag added to the image. | `:dev` | +| `target` | The target (stage) of the Dockerfile from which to build the image. | `base` + + +### Docker Run Customization + +Customize the Docker container run process by adding properties under the `dockerRun` configuration property. + +| Property | Description | Default | +| --- | --- | --- | +| `containerName` | The name of the container. | `-dev` | + ## Configuration Settings The Docker extension comes with a number of useful configuration settings allowing you to customize your workflow. diff --git a/configureWorkspace/config-utils.ts b/configureWorkspace/config-utils.ts index b299649d0a..8252492cb8 100644 --- a/configureWorkspace/config-utils.ts +++ b/configureWorkspace/config-utils.ts @@ -7,17 +7,7 @@ import { isNumber } from 'util'; import vscode = require('vscode'); import { IAzureQuickPickItem, IAzureUserInput } from 'vscode-azureextensionui'; import { ext } from "../extensionVariables"; - -export type OS = 'Windows' | 'Linux'; -export type Platform = - 'Go' | - 'Java' | - '.NET Core Console' | - 'ASP.NET Core' | - 'Node.js' | - 'Python' | - 'Ruby' | - 'Other'; +import { Platform, PlatformOS } from '../utils/platform'; /** * Prompts for a port number @@ -71,15 +61,15 @@ export async function quickPickPlatform(): Promise { * Prompts for an OS * @throws `UserCancelledError` if the user cancels. */ -export async function quickPickOS(): Promise { +export async function quickPickOS(): Promise { let opt: vscode.QuickPickOptions = { matchOnDescription: true, matchOnDetail: true, placeHolder: 'Select Operating System' } - const OSes: OS[] = ['Windows', 'Linux']; - const items = OSes.map(p => >{ label: p, data: p }); + const OSes: PlatformOS[] = ['Windows', 'Linux']; + const items = OSes.map(p => >{ label: p, data: p }); let response = await ext.ui.showQuickPick(items, opt); return response.data; diff --git a/configureWorkspace/configure.ts b/configureWorkspace/configure.ts index 011dddb220..1b99dea27f 100644 --- a/configureWorkspace/configure.ts +++ b/configureWorkspace/configure.ts @@ -16,7 +16,8 @@ import { quickPickWorkspaceFolder } from '../commands/utils/quickPickWorkspaceFo import { ext } from '../extensionVariables'; import { globAsync } from '../helpers/async'; import { extractRegExGroups } from '../helpers/extractRegExGroups'; -import { OS, Platform, promptForPort, quickPickOS, quickPickPlatform } from './config-utils'; +import { Platform, PlatformOS } from '../utils/platform'; +import { promptForPort, quickPickOS, quickPickPlatform } from './config-utils'; import { configureAspDotNetCore, configureDotNetCoreConsole } from './configure_dotnetcore'; import { configureGo } from './configure_go'; import { configureJava } from './configure_java'; @@ -50,7 +51,7 @@ interface PomXmlContents { export type ConfigureTelemetryProperties = { configurePlatform?: Platform; - configureOs?: OS; + configureOs?: PlatformOS; packageFileType?: string; // 'build.gradle', 'pom.xml', 'package.json', '.csproj' packageFileSubfolderDepth?: string; // 0 = project/etc file in root folder, 1 = in subfolder, 2 = in subfolder of subfolder, etc. }; @@ -76,7 +77,7 @@ generatorsByPlatform.set('Python', configurePython); generatorsByPlatform.set('Ruby', configureRuby); generatorsByPlatform.set('Other', configureOther); -function genDockerFile(serviceNameAndRelativePath: string, platform: Platform, os: OS | undefined, port: string | undefined, { cmd, author, version, artifactName }: Partial): string { +function genDockerFile(serviceNameAndRelativePath: string, platform: Platform, os: PlatformOS | undefined, port: string | undefined, { cmd, author, version, artifactName }: Partial): string { let generators = generatorsByPlatform.get(platform); assert(generators, `Could not find dockerfile generator functions for "${platform}"`); if (generators.genDockerFile) { @@ -91,7 +92,7 @@ function genDockerFile(serviceNameAndRelativePath: string, platform: Platform, o } } -function genDockerCompose(serviceNameAndRelativePath: string, platform: Platform, os: OS | undefined, port: string): string { +function genDockerCompose(serviceNameAndRelativePath: string, platform: Platform, os: PlatformOS | undefined, port: string): string { let generators = generatorsByPlatform.get(platform); assert(generators, `Could not find docker compose file generator function for "${platform}"`); if (generators.genDockerCompose) { @@ -99,7 +100,7 @@ function genDockerCompose(serviceNameAndRelativePath: string, platform: Platform } } -function genDockerComposeDebug(serviceNameAndRelativePath: string, platform: Platform, os: OS | undefined, port: string, packageInfo: Partial): string { +function genDockerComposeDebug(serviceNameAndRelativePath: string, platform: Platform, os: PlatformOS | undefined, port: string, packageInfo: Partial): string { let generators = generatorsByPlatform.get(platform); assert(generators, `Could not find docker debug compose file generator function for "${platform}"`); if (generators.genDockerComposeDebug) { @@ -254,7 +255,7 @@ async function findCSProjFile(folderPath: string): Promise { } } -type GeneratorFunction = (serviceName: string, platform: Platform, os: OS | undefined, port: string, packageJson?: Partial) => string; +type GeneratorFunction = (serviceName: string, platform: Platform, os: PlatformOS | undefined, port: string, packageJson?: Partial) => string; const DOCKER_FILE_TYPES: { [key: string]: GeneratorFunction } = { 'docker-compose.yml': genDockerCompose, @@ -299,7 +300,7 @@ export interface ConfigureApiOptions { /** * The OS for the images. Currently only needed for .NET platforms. */ - os?: OS; + os?: PlatformOS; /** * Open the Dockerfile that was generated @@ -346,7 +347,7 @@ async function configureCore(actionContext: IActionContext, options: ConfigureAp properties.configurePlatform = platformType; let generatorInfo = generatorsByPlatform.get(platformType); - let os: OS | undefined = options.os; + let os: PlatformOS | undefined = options.os; if (!os && platformType.toLowerCase().includes('.net')) { os = await quickPickOS(); } diff --git a/configureWorkspace/configure_dotnetcore.ts b/configureWorkspace/configure_dotnetcore.ts index 2e62f22808..5f05bdfe9a 100644 --- a/configureWorkspace/configure_dotnetcore.ts +++ b/configureWorkspace/configure_dotnetcore.ts @@ -9,7 +9,7 @@ import * as path from 'path'; import * as semver from 'semver'; import { extractRegExGroups } from '../helpers/extractRegExGroups'; import { isWindows, isWindows10RS3OrNewer, isWindows10RS4OrNewer } from '../helpers/windowsVersion'; -import { OS, Platform } from './config-utils'; +import { Platform, PlatformOS } from '../utils/platform'; import { getExposeStatements, IPlatformGeneratorInfo, PackageInfo } from './configure'; // This file handles both ASP.NET core and .NET Core Console @@ -155,7 +155,7 @@ ENTRYPOINT ["dotnet", "$assembly_name$.dll"] //#endregion -function genDockerFile(serviceNameAndRelativePath: string, platform: Platform, os: OS | undefined, port: string, { version }: Partial): string { +function genDockerFile(serviceNameAndRelativePath: string, platform: Platform, os: PlatformOS | undefined, port: string, { version }: Partial): string { // VS version of this function is in ResolveImageNames (src/Docker/Microsoft.VisualStudio.Docker.DotNetCore/DockerDotNetCoreScaffoldingProvider.cs) if (os !== 'Windows' && os !== 'Linux') { diff --git a/debugging/coreclr/appStorage.ts b/debugging/coreclr/appStorage.ts new file mode 100644 index 0000000000..00297d3c1d --- /dev/null +++ b/debugging/coreclr/appStorage.ts @@ -0,0 +1,63 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +import * as path from 'path'; +import { FileSystemProvider } from "./fsProvider"; + +export interface MementoAsync { + get(name: string, defaultValue?: T): Promise; + update(name: string, item: T | undefined): Promise; +} + +export interface AppStorageProvider { + getStorage(appFolder: string): Promise; +} + +export class DefaultAppStorage implements MementoAsync { + constructor( + private readonly appFolder: string, + private readonly fileSystemProvider: FileSystemProvider) { + } + + public async get(name: string, defaultValue?: T): Promise { + const itemPath = this.createItemPath(name); + + if (await this.fileSystemProvider.fileExists(itemPath)) { + const itemData = await this.fileSystemProvider.readFile(itemPath); + + return JSON.parse(itemData); + } + + return defaultValue; + } + + public async update(name: string, item: T | undefined): Promise { + const itemPath = this.createItemPath(name); + + if (item) { + const itemDir = path.dirname(itemPath); + + if (!await this.fileSystemProvider.dirExists(itemDir)) { + await this.fileSystemProvider.makeDir(itemDir); + } + + await this.fileSystemProvider.writeFile(itemPath, JSON.stringify(item)); + } else { + await this.fileSystemProvider.unlinkFile(itemPath); + } + } + + private createItemPath(name: string): string { + return path.join(this.appFolder, 'obj', 'docker', `${name}.json`); + } +} + +export class DefaultAppStorageProvider implements AppStorageProvider { + constructor(private readonly fileSystemProvider: FileSystemProvider) { + } + + public async getStorage(appFolder: string): Promise { + return await Promise.resolve(new DefaultAppStorage(appFolder, this.fileSystemProvider)); + } +} diff --git a/debugging/coreclr/browserClient.ts b/debugging/coreclr/browserClient.ts new file mode 100644 index 0000000000..46a0333b13 --- /dev/null +++ b/debugging/coreclr/browserClient.ts @@ -0,0 +1,23 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +import * as opn from 'opn'; +import { Uri } from "vscode"; + +export interface BrowserClient { + openBrowser(url: string): void; +} + +export class OpnBrowserClient implements BrowserClient { + public openBrowser(url: string): void { + const uri = Uri.parse(url); + + if (uri.scheme === 'http' || uri.scheme === 'https') { + // tslint:disable-next-line:no-unsafe-any + opn(url); + } + } +} + +export default OpnBrowserClient; diff --git a/debugging/coreclr/debugSessionManager.ts b/debugging/coreclr/debugSessionManager.ts new file mode 100644 index 0000000000..1719512574 --- /dev/null +++ b/debugging/coreclr/debugSessionManager.ts @@ -0,0 +1,44 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { DockerManager } from './dockerManager'; + +export interface DebugSessionManager { + startListening(): void; + stopListening(): void; +} + +export class DockerDebugSessionManager implements DebugSessionManager, vscode.Disposable { + private eventSubscription: vscode.Disposable | undefined; + + constructor( + private readonly debugSessionTerminated: vscode.Event, + private readonly dockerManager: DockerManager) { + } + + public dispose(): void { + this.stopListening(); + } + + public startListening(): void { + if (this.eventSubscription === undefined) { + this.eventSubscription = this.debugSessionTerminated( + () => { + this.dockerManager + .cleanupAfterLaunch() + .catch(reason => console.log(`Unable to clean up Docker images after launch: ${reason}`)); + + this.stopListening(); + }); + } + } + + public stopListening(): void { + if (this.eventSubscription) { + this.eventSubscription.dispose(); + this.eventSubscription = undefined; + } + } +} diff --git a/debugging/coreclr/debuggerClient.ts b/debugging/coreclr/debuggerClient.ts new file mode 100644 index 0000000000..9f5a2e0964 --- /dev/null +++ b/debugging/coreclr/debuggerClient.ts @@ -0,0 +1,25 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +import { PlatformOS } from '../../utils/platform'; +import { VsDbgClient } from './vsdbgClient'; + +export interface DebuggerClient { + getDebugger(os: PlatformOS): Promise; +} + +export class DefaultDebuggerClient { + private static debuggerVersion: string = 'vs2017u5'; + private static debuggerLinuxRuntime: string = 'debian.8-x64'; + private static debuggerWindowsRuntime: string = 'win7-x64'; + + constructor(private readonly vsdbgClient: VsDbgClient) { + } + + public async getDebugger(os: PlatformOS): Promise { + return await this.vsdbgClient.getVsDbgVersion( + DefaultDebuggerClient.debuggerVersion, + os === 'Windows' ? DefaultDebuggerClient.debuggerWindowsRuntime : DefaultDebuggerClient.debuggerLinuxRuntime); + } +} diff --git a/debugging/coreclr/dockerClient.ts b/debugging/coreclr/dockerClient.ts new file mode 100644 index 0000000000..1cfa71bc71 --- /dev/null +++ b/debugging/coreclr/dockerClient.ts @@ -0,0 +1,232 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +import { LineSplitter } from "./lineSplitter"; +import { ProcessProvider } from "./processProvider"; + +export type DockerBuildImageOptions = { + context?: string; + dockerfile?: string; + tag?: string; + target?: string; +}; + +export type DockerInspectObjectOptions = { + format?: string; +}; + +export type DockerContainersListOptions = { + format?: string; +}; + +export type DockerContainerRemoveOptions = { + force?: boolean; +}; + +export type DockerContainerVolume = { + localPath: string; + containerPath: string; + permissions?: 'ro' | 'rw'; +}; + +export type DockerRunContainerOptions = { + command?: string; + containerName?: string; + entrypoint?: string; + volumes?: DockerContainerVolume[]; +}; + +export type DockerVersionOptions = { + format?: string; +} + +export interface DockerClient { + buildImage(options: DockerBuildImageOptions, progress?: (content: string) => void): Promise; + getVersion(options?: DockerVersionOptions): Promise; + inspectObject(nameOrId: string, options?: DockerInspectObjectOptions): Promise; + listContainers(options?: DockerContainersListOptions): Promise; + matchId(id1: string, id2: string): boolean; + removeContainer(containerNameOrId: string, options?: DockerContainerRemoveOptions): Promise; + runContainer(imageTagOrId: string, options?: DockerRunContainerOptions): Promise; + trimId(id: string): string; +} + +export class CliDockerClient implements DockerClient { + constructor(private readonly processProvider: ProcessProvider) { + // CONSIDER: Use dockerode client as basis for debugging. + } + + public async buildImage(options?: DockerBuildImageOptions, progress?: (content: string) => void): Promise { + let command = `docker build --rm`; + + if (options && options.dockerfile) { + command += ` -f ${options.dockerfile}`; + } + + if (options && options.tag) { + command += ` -t ${options.tag}`; + } + + if (options && options.target) { + command += ` --target ${options.target}`; + } + + if (options && options.context) { + command += ` ${options.context}`; + } + + let imageId: string | undefined; + + const lineSplitter = new LineSplitter(); + + lineSplitter.onLine( + line => { + // Expected output is: 'Successfully built 7cc5654ca3b6' + const buildSuccessPrefix = 'Successfully built '; + + if (line.startsWith(buildSuccessPrefix)) { + imageId = line.substr(buildSuccessPrefix.length, 12); + } + }); + + const buildProgress = + (content: string) => { + if (progress) { + progress(content); + } + + lineSplitter.write(content); + }; + + await this.processProvider.exec(command, { progress: buildProgress }); + + lineSplitter.close(); + + if (!imageId) { + throw new Error('The Docker image was built successfully but the image ID could not be retrieved.'); + } + + return imageId; + } + + public async getVersion(options?: DockerVersionOptions): Promise { + let command = 'docker version'; + + if (options && options.format) { + command += ` --format "${options.format}"`; + } + + const result = await this.processProvider.exec(command, {}); + + return result.stdout; + } + + public async inspectObject(nameOrId: string, options?: DockerInspectObjectOptions): Promise { + let command = 'docker inspect'; + + if (options && options.format) { + command += ` \"--format=${options.format}\"`; + } + + command += ` ${nameOrId}`; + + try { + const output = await this.processProvider.exec(command, {}); + + return output.stdout; + } catch { + // Failure (typically) means the object wasn't found... + return undefined; + } + } + + public async listContainers(options?: DockerContainersListOptions): Promise { + let command = 'docker ps -a'; + + if (options && options.format) { + command += ` \"--format=${options.format}\"`; + } + + const output = await this.processProvider.exec(command, {}); + + return output.stdout; + } + + public matchId(id1: string, id2: string): boolean { + const validateArgument = + id => { + if (id === undefined || id1.length < 12) { + throw new Error(`'${id}' must be defined and at least 12 characters.`) + } + }; + + validateArgument(id1); + validateArgument(id2); + + return id1.length < id2.length + ? id2.startsWith(id1) + : id1.startsWith(id2); + } + + public async removeContainer(containerNameOrId: string, options?: DockerContainerRemoveOptions): Promise { + let command = 'docker rm'; + + if (options && options.force) { + command += ' --force'; + } + + command += ` ${containerNameOrId}`; + + await this.processProvider.exec(command, {}); + } + + public async runContainer(imageTagOrId: string, options?: DockerRunContainerOptions): Promise { + let command = 'docker run -dt -P'; + + if (options && options.containerName) { + command += ` --name ${options.containerName}`; + } + + if (options && options.volumes) { + command += ' ' + options.volumes.map(volume => `-v \"${volume.localPath}:${volume.containerPath}${volume.permissions ? ':' + volume.permissions : ''}\"`).join(' '); + } + + if (options && options.entrypoint) { + command += ` --entrypoint ${options.entrypoint}`; + } + + command += ' ' + imageTagOrId; + + if (options && options.command) { + command += ' ' + options.command; + } + + const result = await this.processProvider.exec(command, {}); + + // The '-d' option returns the container ID (with whitespace) upon completion. + const containerId = result.stdout.trim(); + + if (!containerId) { + throw new Error('The Docker container was run successfully but the container ID could not be retrieved.') + } + + return containerId; + } + + public trimId(id: string): string { + if (!id) { + throw new Error('The ID to be trimmed must be non-empty.'); + } + + const trimmedId = id.trim(); + + if (trimmedId.length < 12) { + throw new Error('The ID to be trimmed must be at least 12 characters.'); + } + + return id.substring(0, 12); + } +} + +export default CliDockerClient; diff --git a/debugging/coreclr/dockerDebugConfigurationProvider.ts b/debugging/coreclr/dockerDebugConfigurationProvider.ts new file mode 100644 index 0000000000..544bd73fd4 --- /dev/null +++ b/debugging/coreclr/dockerDebugConfigurationProvider.ts @@ -0,0 +1,252 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +import * as path from 'path'; +import { CancellationToken, DebugConfiguration, DebugConfigurationProvider, ProviderResult, WorkspaceFolder } from 'vscode'; +import { callWithTelemetryAndErrorHandling } from 'vscode-azureextensionui'; +import { PlatformOS } from '../../utils/platform'; +import { DebugSessionManager } from './debugSessionManager'; +import { DockerManager, LaunchResult } from './dockerManager'; +import { FileSystemProvider } from './fsProvider'; +import { NetCoreProjectProvider } from './netCoreProjectProvider'; +import { OSProvider } from './osProvider'; +import { Prerequisite } from './prereqManager'; + +interface DockerDebugBuildOptions { + context?: string; + dockerfile?: string; + tag?: string; + target?: string; +} + +interface DockerDebugRunOptions { + containerName?: string; + os?: PlatformOS; +} + +interface DebugConfigurationBrowserBaseOptions { + enabled?: boolean; + command?: string; + args?: string; +} + +interface DebugConfigurationBrowserOptions extends DebugConfigurationBrowserBaseOptions { + windows?: DebugConfigurationBrowserBaseOptions; + osx?: DebugConfigurationBrowserBaseOptions; + linux?: DebugConfigurationBrowserBaseOptions; +} + +interface DockerDebugConfiguration extends DebugConfiguration { + appFolder?: string; + appOutput?: string; + appProject?: string; + dockerBuild?: DockerDebugBuildOptions; + dockerRun?: DockerDebugRunOptions; +} + +export class DockerDebugConfigurationProvider implements DebugConfigurationProvider { + constructor( + private readonly debugSessionManager: DebugSessionManager, + private readonly dockerManager: DockerManager, + private readonly fsProvider: FileSystemProvider, + private readonly osProvider: OSProvider, + private readonly netCoreProjectProvider: NetCoreProjectProvider, + private readonly prerequisite: Prerequisite) { + } + + public provideDebugConfigurations(folder: WorkspaceFolder | undefined, token?: CancellationToken): ProviderResult { + return [ + { + name: 'Docker: Launch .NET Core ASP.NET (Preview)', + type: 'docker-coreclr', + request: 'launch', + preLaunchTask: 'build', + dockerBuild: { + }, + dockerRun: { + } + } + ]; + } + + public resolveDebugConfiguration(folder: WorkspaceFolder | undefined, debugConfiguration: DockerDebugConfiguration, token?: CancellationToken): ProviderResult { + return callWithTelemetryAndErrorHandling( + 'debugCoreClr', + async () => await this.resolveDockerDebugConfiguration(folder, debugConfiguration)); + } + + private static resolveFolderPath(folderPath: string, folder: WorkspaceFolder): string { + return folderPath.replace(/\$\{workspaceFolder\}/gi, folder.uri.fsPath); + } + + private async resolveDockerDebugConfiguration(folder: WorkspaceFolder | undefined, debugConfiguration: DockerDebugConfiguration): Promise { + if (!folder) { + throw new Error('No workspace folder is associated with debugging.'); + } + + const prerequisiteSatisfied = await this.prerequisite.checkPrerequisite(); + + if (!prerequisiteSatisfied) { + return undefined; + } + + const appFolder = this.inferAppFolder(folder, debugConfiguration); + + const resolvedAppFolder = DockerDebugConfigurationProvider.resolveFolderPath(appFolder, folder); + + const appProject = await this.inferAppProject(debugConfiguration, resolvedAppFolder); + + const resolvedAppProject = DockerDebugConfigurationProvider.resolveFolderPath(appProject, folder); + + const context = this.inferContext(folder, resolvedAppFolder, debugConfiguration); + + const resolvedContext = DockerDebugConfigurationProvider.resolveFolderPath(context, folder); + + let dockerfile = debugConfiguration && debugConfiguration.dockerBuild && debugConfiguration.dockerBuild.dockerfile + ? DockerDebugConfigurationProvider.resolveFolderPath(debugConfiguration.dockerBuild.dockerfile, folder) + : path.join(appFolder, 'Dockerfile'); // CONSIDER: Omit dockerfile argument if not specified or possibly infer from context. + + dockerfile = DockerDebugConfigurationProvider.resolveFolderPath(dockerfile, folder); + + const target = debugConfiguration && debugConfiguration.dockerBuild && debugConfiguration.dockerBuild.target + ? debugConfiguration.dockerBuild.target + : 'base'; // CONSIDER: Omit target if not specified, or possibly infer from Dockerfile. + + const appName = path.basename(resolvedAppProject, '.csproj'); + + const tag = debugConfiguration && debugConfiguration.dockerBuild && debugConfiguration.dockerBuild.tag + ? debugConfiguration.dockerBuild.tag + : `${appName.toLowerCase()}:dev`; + + const containerName = debugConfiguration && debugConfiguration.dockerRun && debugConfiguration.dockerRun.containerName + ? debugConfiguration.dockerRun.containerName + : `${appName}-dev`; // CONSIDER: Use unique ID instead? + + const os = debugConfiguration && debugConfiguration.dockerRun && debugConfiguration.dockerRun.os + ? debugConfiguration.dockerRun.os + : 'Linux'; + + const appOutput = await this.inferAppOutput(debugConfiguration, os, resolvedAppProject); + + const result = await this.dockerManager.prepareForLaunch({ + appFolder: resolvedAppFolder, + appOutput, + build: { + context: resolvedContext, + dockerfile, + tag, + target + }, + run: { + containerName, + os, + } + }); + + const configuration = this.createConfiguration(debugConfiguration, appFolder, result); + + this.debugSessionManager.startListening(); + + return configuration; + } + + private inferAppFolder(folder: WorkspaceFolder, configuration: DockerDebugConfiguration): string { + if (configuration) { + if (configuration.appFolder) { + return configuration.appFolder; + } + + if (configuration.appProject) { + return path.dirname(configuration.appProject); + } + } + + return folder.uri.fsPath; + } + + private async inferAppOutput(configuration: DockerDebugConfiguration, targetOS: PlatformOS, resolvedAppProject: string): Promise { + if (configuration && configuration.appOutput) { + return configuration.appOutput; + } + + const targetPath = await this.netCoreProjectProvider.getTargetPath(resolvedAppProject); + const relativeTargetPath = this.osProvider.pathNormalize(targetOS, path.relative(path.dirname(resolvedAppProject), targetPath)); + + return relativeTargetPath; + } + + private async inferAppProject(configuration: DockerDebugConfiguration, resolvedAppFolder: string): Promise { + if (configuration) { + if (configuration.appProject) { + return configuration.appProject; + } + } + + const files = await this.fsProvider.readDir(resolvedAppFolder); + + const projectFile = files.find(file => path.extname(file) === '.csproj'); + + if (projectFile) { + return path.join(resolvedAppFolder, projectFile); + } + + throw new Error('Unable to infer the application project file. Set either the `appFolder` or `appProject` property in the Docker debug configuration.'); + } + + private inferContext(folder: WorkspaceFolder, resolvedAppFolder: string, configuration: DockerDebugConfiguration): string { + return configuration && configuration.dockerBuild && configuration.dockerBuild.context + ? configuration.dockerBuild.context + : path.normalize(resolvedAppFolder) === path.normalize(folder.uri.fsPath) + ? resolvedAppFolder // The context defaults to the application folder if it's the same as the workspace folder (i.e. there's no solution folder). + : path.dirname(resolvedAppFolder); // The context defaults to the application's parent (i.e. solution) folder. + } + + private createLaunchBrowserConfiguration(result: LaunchResult): DebugConfigurationBrowserOptions { + return result.browserUrl + ? { + enabled: true, + args: result.browserUrl, + windows: { + command: 'cmd.exe', + args: `/C start ${result.browserUrl}` + }, + osx: { + command: 'open' + }, + linux: { + command: 'xdg-open' + } + } + : { + enabled: false + }; + } + + private createConfiguration(debugConfiguration: DockerDebugConfiguration, appFolder: string, result: LaunchResult): DebugConfiguration { + const launchBrowser = this.createLaunchBrowserConfiguration(result); + + return { + name: debugConfiguration.name, + type: 'coreclr', + request: 'launch', + program: result.program, + args: result.programArgs.join(' '), + cwd: result.programCwd, + launchBrowser, + pipeTransport: { + pipeCwd: result.pipeCwd, + pipeProgram: result.pipeProgram, + pipeArgs: result.pipeArgs, + debuggerPath: result.debuggerPath, + quoteArgs: false + }, + preLaunchTask: debugConfiguration.preLaunchTask, + sourceFileMap: { + '/app/Views': path.join(appFolder, 'Views') + } + }; + } +} + +export default DockerDebugConfigurationProvider; diff --git a/debugging/coreclr/dockerManager.ts b/debugging/coreclr/dockerManager.ts new file mode 100644 index 0000000000..169834a5cb --- /dev/null +++ b/debugging/coreclr/dockerManager.ts @@ -0,0 +1,330 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +import * as path from 'path'; +import { Memento } from 'vscode'; +import { PlatformOS } from '../../utils/platform'; +import { AppStorageProvider } from './appStorage'; +import { DebuggerClient } from './debuggerClient'; +import { DockerBuildImageOptions, DockerClient, DockerContainerVolume, DockerRunContainerOptions } from "./dockerClient"; +import { FileSystemProvider } from './fsProvider'; +import Lazy from './lazy'; +import { OSProvider } from './osProvider'; +import { OutputManager } from './outputManager'; +import { ProcessProvider } from './processProvider'; + +export type DockerManagerBuildImageOptions + = DockerBuildImageOptions + & { + appFolder: string; + context: string; + dockerfile: string; + }; + +export type DockerManagerRunContainerOptions + = DockerRunContainerOptions + & { + appFolder: string; + os: PlatformOS; + }; + +type Omit = Pick>; + +export type LaunchOptions = { + appFolder: string; + appOutput: string; + build: Omit; + run: Omit; +}; + +export type LaunchResult = { + browserUrl: string | undefined; + debuggerPath: string; + pipeArgs: string[]; + pipeCwd: string; + pipeProgram: string; + program: string; + programArgs: string[]; + programCwd: string; +}; + +type LastImageBuildMetadata = { + dockerfileHash: string; + dockerIgnoreHash: string | undefined; + imageId: string; + options: DockerBuildImageOptions; +}; + +type LastContainerRunMetadata = { + containerId: string; + options: DockerRunContainerOptions; +} + +export interface DockerManager { + buildImage(options: DockerManagerBuildImageOptions): Promise; + runContainer(imageTagOrId: string, options: DockerManagerRunContainerOptions): Promise; + prepareForLaunch(options: LaunchOptions): Promise; + cleanupAfterLaunch(): Promise; +} + +export const MacNuGetPackageFallbackFolderPath = '/usr/local/share/dotnet/sdk/NuGetFallbackFolder'; + +export class DefaultDockerManager implements DockerManager { + private static readonly DebugContainersKey: string = 'DefaultDockerManager.debugContainers'; + + constructor( + private readonly appCacheFactory: AppStorageProvider, + private readonly debuggerClient: DebuggerClient, + private readonly dockerClient: DockerClient, + private readonly dockerOutputManager: OutputManager, + private readonly fileSystemProvider: FileSystemProvider, + private readonly osProvider: OSProvider, + private readonly processProvider: ProcessProvider, + private readonly workspaceState: Memento) { + } + + public async buildImage(options: DockerManagerBuildImageOptions): Promise { + const cache = await this.appCacheFactory.getStorage(options.appFolder); + const buildMetadata = await cache.get('build'); + const dockerIgnorePath = path.join(options.context, '.dockerignore'); + + const dockerfileHasher = new Lazy(async () => await this.fileSystemProvider.hashFile(options.dockerfile)); + const dockerIgnoreHasher = new Lazy( + async () => { + if (await this.fileSystemProvider.fileExists(dockerIgnorePath)) { + return await this.fileSystemProvider.hashFile(dockerIgnorePath); + } else { + return undefined; + } + }); + + if (buildMetadata && buildMetadata.imageId) { + const imageObject = await this.dockerClient.inspectObject(buildMetadata.imageId); + + if (imageObject + && buildMetadata.options + && buildMetadata.options.context === options.context + && buildMetadata.options.tag === options.tag + && buildMetadata.options.target === options.target) { + const currentDockerfileHash = await dockerfileHasher.value; + const currentDockerIgnoreHash = await dockerIgnoreHasher.value; + + if (buildMetadata.dockerfileHash === currentDockerfileHash + && buildMetadata.dockerIgnoreHash === currentDockerIgnoreHash) { + + // The image is up to date, no build is necessary... + return buildMetadata.imageId; + } + } + } + + const imageId = await this.dockerOutputManager.performOperation( + 'Building Docker image...', + async outputManager => await this.dockerClient.buildImage(options, content => outputManager.append(content)), + id => `Docker image ${this.dockerClient.trimId(id)} built.`, + err => `Failed to build Docker image: ${err}`); + + const dockerfileHash = await dockerfileHasher.value; + const dockerIgnoreHash = await dockerIgnoreHasher.value; + + await cache.update( + 'build', + { + dockerfileHash, + dockerIgnoreHash, + imageId, + options + }); + + return imageId; + } + + public async runContainer(imageTagOrId: string, options: DockerManagerRunContainerOptions): Promise { + if (options.containerName === undefined) { + throw new Error('No container name was provided.'); + } + + const containerName = options.containerName; + + const debuggerFolder = await this.debuggerClient.getDebugger(options.os); + + const command = options.os === 'Windows' + ? '-t localhost' + : '-f /dev/null'; + + const entrypoint = options.os === 'Windows' + ? 'ping' + : 'tail'; + + const volumes = this.getVolumes(debuggerFolder, options); + + const containerId = await this.dockerOutputManager.performOperation( + 'Starting container...', + async () => { + const containers = (await this.dockerClient.listContainers({ format: '{{.Names}}' })).split('\n'); + + if (containers.find(container => container === containerName)) { + await this.dockerClient.removeContainer(containerName, { force: true }); + } + + return await this.dockerClient.runContainer( + imageTagOrId, + { + command, + containerName: options.containerName, + entrypoint, + volumes + }); + }, + id => `Container ${this.dockerClient.trimId(id)} started.`, + err => `Unable to start container: ${err}`); + + const cache = await this.appCacheFactory.getStorage(options.appFolder); + + await cache.update( + 'run', + { + containerId, + options + }); + + return containerId; + } + + public async prepareForLaunch(options: LaunchOptions): Promise { + const imageId = await this.buildImage({ appFolder: options.appFolder, ...options.build }); + + const containerId = await this.runContainer(imageId, { appFolder: options.appFolder, ...options.run }); + + await this.addToDebugContainers(containerId); + + const browserUrl = await this.getContainerWebEndpoint(containerId); + + const additionalProbingPaths = options.run.os === 'Windows' + ? [ + 'C:\\.nuget\\packages', + 'C:\\.nuget\\fallbackpackages' + ] + : [ + '/root/.nuget/packages', + '/root/.nuget/fallbackpackages' + ]; + const additionalProbingPathsArgs = additionalProbingPaths.map(probingPath => `--additionalProbingPath ${probingPath}`).join(' '); + + const containerAppOutput = options.run.os === 'Windows' + ? this.osProvider.pathJoin(options.run.os, 'C:\\app', options.appOutput) + : this.osProvider.pathJoin(options.run.os, '/app', options.appOutput); + + return { + browserUrl, + debuggerPath: options.run.os === 'Windows' ? 'C:\\remote_debugger\\vsdbg' : '/remote_debugger/vsdbg', + // tslint:disable-next-line:no-invalid-template-strings + pipeArgs: ['exec', '-i', containerId, '${debuggerCommand}'], + // tslint:disable-next-line:no-invalid-template-strings + pipeCwd: '${workspaceFolder}', + pipeProgram: 'docker', + program: 'dotnet', + programArgs: [additionalProbingPathsArgs, containerAppOutput], + programCwd: options.run.os === 'Windows' ? 'C:\\app' : '/app' + }; + } + + public async cleanupAfterLaunch(): Promise { + const debugContainers = this.workspaceState.get(DefaultDockerManager.DebugContainersKey, []); + + const runningContainers = (await this.dockerClient.listContainers({ format: '{{.ID}}' })).split('\n'); + + let remainingContainers; + + if (runningContainers && runningContainers.length >= 0) { + const removeContainerTasks = + debugContainers + .filter(containerId => runningContainers.find(runningContainerId => this.dockerClient.matchId(containerId, runningContainerId))) + .map( + async containerId => { + try { + await this.dockerClient.removeContainer(containerId, { force: true }); + + return undefined; + } catch { + return containerId; + } + }); + + remainingContainers = (await Promise.all(removeContainerTasks)).filter(containerId => containerId !== undefined); + } else { + remainingContainers = []; + } + + await this.workspaceState.update(DefaultDockerManager.DebugContainersKey, remainingContainers); + } + + private async addToDebugContainers(containerId: string): Promise { + const runningContainers = this.workspaceState.get(DefaultDockerManager.DebugContainersKey, []); + + runningContainers.push(containerId); + + await this.workspaceState.update(DefaultDockerManager.DebugContainersKey, runningContainers); + } + + private async getContainerWebEndpoint(containerNameOrId: string): Promise { + const webPorts = await this.dockerClient.inspectObject(containerNameOrId, { format: '{{(index (index .NetworkSettings.Ports \\\"80/tcp\\\") 0).HostPort}}' }); + + if (webPorts) { + const webPort = webPorts.split('\n')[0]; + + // tslint:disable-next-line:no-http-string + return `http://localhost:${webPort}`; + } + + return undefined; + } + + private static readonly ProgramFilesEnvironmentVariable: string = 'ProgramFiles'; + + private getVolumes(debuggerFolder: string, options: DockerManagerRunContainerOptions): DockerContainerVolume[] { + const appVolume: DockerContainerVolume = { + localPath: options.appFolder, + containerPath: options.os === 'Windows' ? 'C:\\app' : '/app', + permissions: 'rw' + }; + + const debuggerVolume: DockerContainerVolume = { + localPath: debuggerFolder, + containerPath: options.os === 'Windows' ? 'C:\\remote_debugger' : '/remote_debugger', + permissions: 'ro' + }; + + const nugetVolume: DockerContainerVolume = { + localPath: path.join(this.osProvider.homedir, '.nuget', 'packages'), + containerPath: options.os === 'Windows' ? 'C:\\.nuget\\packages' : '/root/.nuget/packages', + permissions: 'ro' + }; + + let programFilesEnvironmentVariable: string | undefined; + + if (this.osProvider.os === 'Windows') { + programFilesEnvironmentVariable = this.processProvider.env[DefaultDockerManager.ProgramFilesEnvironmentVariable]; + + if (programFilesEnvironmentVariable === undefined) { + throw new Error(`The environment variable '${DefaultDockerManager.ProgramFilesEnvironmentVariable}' is not defined. This variable is used to locate the NuGet fallback folder.`); + } + } + + const nugetFallbackVolume: DockerContainerVolume = { + localPath: this.osProvider.os === 'Windows' ? path.join(programFilesEnvironmentVariable, 'dotnet', 'sdk', 'NuGetFallbackFolder') : MacNuGetPackageFallbackFolderPath, + containerPath: options.os === 'Windows' ? 'C:\\.nuget\\fallbackpackages' : '/root/.nuget/fallbackpackages', + permissions: 'ro' + }; + + const volumes: DockerContainerVolume[] = [ + appVolume, + debuggerVolume, + nugetVolume, + nugetFallbackVolume + ]; + + return volumes; + } +} diff --git a/debugging/coreclr/dotNetClient.ts b/debugging/coreclr/dotNetClient.ts new file mode 100644 index 0000000000..913bc54a77 --- /dev/null +++ b/debugging/coreclr/dotNetClient.ts @@ -0,0 +1,53 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +import { ProcessProvider } from "./processProvider"; + +export type MSBuildExecOptions = { + target?: string; + properties?: { [key: string]: string }; +}; + +export interface DotNetClient { + execTarget(projectFile: string, options?: MSBuildExecOptions): Promise; + getVersion(): Promise; +} + +export class CommandLineDotNetClient implements DotNetClient { + constructor(private readonly processProvider: ProcessProvider) { + } + + public async execTarget(projectFile: string, options?: MSBuildExecOptions): Promise { + let command = `dotnet msbuild "${projectFile}"`; + + if (options) { + if (options.target) { + command += ` "/t:${options.target}"`; + } + + if (options.properties) { + const properties = options.properties; + + command += Object.keys(properties).map(key => ` "/p:${key}=${properties[key]}"`).join(''); + } + } + + await this.processProvider.exec(command, {}); + } + + public async getVersion(): Promise { + try { + + const command = `dotnet --version`; + + const result = await this.processProvider.exec(command, {}); + + return result.stdout.trim(); + } catch { + return undefined; + } + } +} + +export default CommandLineDotNetClient; diff --git a/debugging/coreclr/fsProvider.ts b/debugging/coreclr/fsProvider.ts new file mode 100644 index 0000000000..9d3c2162af --- /dev/null +++ b/debugging/coreclr/fsProvider.ts @@ -0,0 +1,82 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +import * as crypto from 'crypto'; +import * as fse from 'fs-extra'; + +export interface FileSystemProvider { + dirExists(path: string): Promise; + fileExists(path: string): Promise; + hashFile(path: string): Promise; + makeDir(path: string): Promise; + readDir(path: string): Promise; + readFile(filename: string, encoding?: string): Promise; + unlinkFile(filename: string): Promise; + // tslint:disable-next-line:no-any + writeFile(filename: string, data: any): Promise; +} + +export class LocalFileSystemProvider implements FileSystemProvider { + public async dirExists(path: string): Promise { + try { + const stats = await fse.stat(path); + + return stats.isDirectory(); + } catch (err) { + // tslint:disable-next-line:no-unsafe-any + if (err.code === "ENOENT") { + return false; + } + + throw err; + } + } + + public async fileExists(path: string): Promise { + try { + const stats = await fse.stat(path); + + return stats.isFile(); + } catch (err) { + // tslint:disable-next-line:no-unsafe-any + if (err.code === "ENOENT") { + return false; + } + + throw err; + } + } + + public async hashFile(path: string): Promise { + const hash = crypto.createHash('sha256'); + + const contents = await this.readFile(path); + + hash.update(contents); + + return hash.digest('hex'); + } + + public async makeDir(path: string): Promise { + return await fse.mkdir(path); + } + + public async readDir(path: string): Promise { + return await fse.readdir(path); + } + + public async readFile(filename: string, encoding?: string): Promise { + // NOTE: If encoding is specified, output is a string; if omitted, output is a Buffer. + return (await (encoding ? fse.readFile(filename, encoding) : fse.readFile(filename))).toString(); + } + + public async unlinkFile(filename: string): Promise { + return await fse.unlink(filename); + } + + // tslint:disable-next-line:no-any + public async writeFile(filename: string, data: any): Promise { + return await fse.writeFile(filename, data); + } +} diff --git a/debugging/coreclr/lazy.ts b/debugging/coreclr/lazy.ts new file mode 100644 index 0000000000..a04a779277 --- /dev/null +++ b/debugging/coreclr/lazy.ts @@ -0,0 +1,26 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +export class Lazy { + private _isValueCreated: boolean = false; + private _value: T | undefined; + + constructor(private readonly valueFactory: () => T) { + } + + get isValueCreated(): boolean { + return this._isValueCreated; + } + + get value(): T { + if (!this._isValueCreated) { + this._value = this.valueFactory(); + this._isValueCreated = true; + } + + return this._value; + } +} + +export default Lazy; diff --git a/debugging/coreclr/lineSplitter.ts b/debugging/coreclr/lineSplitter.ts new file mode 100644 index 0000000000..eb5f268f30 --- /dev/null +++ b/debugging/coreclr/lineSplitter.ts @@ -0,0 +1,66 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +import * as vscode from 'vscode'; + +export class LineSplitter implements vscode.Disposable { + private readonly emitter: vscode.EventEmitter = new vscode.EventEmitter(); + private buffer: string | undefined; + + public get onLine(): vscode.Event { + return this.emitter.event; + } + + public close(): void { + if (this.buffer !== undefined) { + this.emitter.fire(this.buffer); + this.buffer = undefined; + } + } + + public dispose(): void { + this.close(); + } + + public write(data: string): void { + if (data === undefined) { + return; + } + + this.buffer = this.buffer !== undefined ? this.buffer + data : data; + + let index = 0; + let lineStart = 0; + + while (index < this.buffer.length) { + if (this.buffer[index] === '\n') { + const line = index === 0 || this.buffer[index - 1] !== '\r' + ? this.buffer.substring(lineStart, index) + : this.buffer.substring(lineStart, index - 1); + + this.emitter.fire(line); + + lineStart = index + 1; + } + + index++; + } + + this.buffer = lineStart < index ? this.buffer.substring(lineStart) : undefined; + } + + public static splitLines(data: string): string[] { + const splitter = new LineSplitter(); + + const lines: string[] = []; + + splitter.onLine(line => lines.push(line)); + splitter.write(data); + splitter.close(); + + return lines; + } +} + +export default LineSplitter; diff --git a/debugging/coreclr/netCoreProjectProvider.ts b/debugging/coreclr/netCoreProjectProvider.ts new file mode 100644 index 0000000000..54d0547ae2 --- /dev/null +++ b/debugging/coreclr/netCoreProjectProvider.ts @@ -0,0 +1,62 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +import * as path from 'path'; +import { DotNetClient } from "./dotNetClient"; +import { FileSystemProvider } from "./fsProvider"; +import { TempFileProvider } from './tempFileProvider'; + +const getTargetPathProjectFileContent = +` + + + + + + +`; + +export interface NetCoreProjectProvider { + getTargetPath(projectFile: string): Promise; +} + +export class MsBuildNetCoreProjectProvider implements NetCoreProjectProvider { + constructor( + private readonly fsProvider: FileSystemProvider, + private readonly msBuildClient: DotNetClient, + private readonly tempFileProvider: TempFileProvider) { + } + + public async getTargetPath(projectFile: string): Promise { + const getTargetPathProjectFile = this.tempFileProvider.getTempFilename(); + const targetOutputFilename = this.tempFileProvider.getTempFilename(); + await this.fsProvider.writeFile(getTargetPathProjectFile, getTargetPathProjectFileContent); + try { + await this.msBuildClient.execTarget( + getTargetPathProjectFile, + { + target: 'GetTargetPath', + properties: { + 'ProjectFilename': projectFile, + 'TargetOutputFilename': targetOutputFilename + } + }); + + const targetOutputContent = await this.fsProvider.readFile(targetOutputFilename); + + return targetOutputContent.split(/\r?\n/)[0]; + } + finally { + await this.fsProvider.unlinkFile(getTargetPathProjectFile); + await this.fsProvider.unlinkFile(targetOutputFilename); + } + } +} diff --git a/debugging/coreclr/osProvider.ts b/debugging/coreclr/osProvider.ts new file mode 100644 index 0000000000..9b97f15539 --- /dev/null +++ b/debugging/coreclr/osProvider.ts @@ -0,0 +1,46 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +import * as os from 'os'; +import * as path from 'path'; +import { PlatformOS } from '../../utils/platform'; + +export interface OSProvider { + homedir: string; + isMac: boolean; + os: PlatformOS; + tmpdir: string; + pathJoin(os: PlatformOS, ...paths: string[]): string; + pathNormalize(os: PlatformOS, rawPath: string): string; +} + +export class LocalOSProvider implements OSProvider { + get homedir(): string { + return os.homedir(); + } + + get isMac(): boolean { + return os.platform() === 'darwin'; + } + + get os(): PlatformOS { + return os.platform() === 'win32' ? 'Windows' : 'Linux'; + } + + get tmpdir(): string { + return os.tmpdir(); + } + + public pathJoin(pathOS: PlatformOS, ...paths: string[]): string { + return pathOS === 'Windows' ? path.win32.join(...paths) : path.posix.join(...paths); + } + + public pathNormalize(pathOS: PlatformOS, rawPath: string): string { + return rawPath.replace( + pathOS === 'Windows' ? /\//g : /\\/g, + pathOS === 'Windows' ? '\\' : '/'); + } +} + +export default LocalOSProvider; diff --git a/debugging/coreclr/outputManager.ts b/debugging/coreclr/outputManager.ts new file mode 100644 index 0000000000..4ddcae7d89 --- /dev/null +++ b/debugging/coreclr/outputManager.ts @@ -0,0 +1,82 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { LineSplitter } from './lineSplitter'; + +type outputCallback = (result: T) => string; + +export interface OutputManager { + append(content: string): void; + appendLine(content: string): void; + performOperation(startContent: string, operation: (outputManager: OutputManager) => Promise, endContent?: string | outputCallback, errorContent?: string | outputCallback): Promise; +} + +export class DefaultOutputManager implements OutputManager, vscode.Disposable { + private readonly lineSplitter: LineSplitter = new LineSplitter(); + private isShown: boolean = false; + + constructor(private readonly outputChannel: vscode.OutputChannel, private readonly level: number = 0) { + this.lineSplitter.onLine(line => this.outputChannel.appendLine(this.generatePrefix(line))); + } + + public append(content: string): void { + if (this.level) { + this.lineSplitter.write(content); + } else { + this.outputChannel.append(content); + } + } + + public appendLine(content: string): void { + if (this.level) { + this.lineSplitter.write(content + '\n'); + } else { + this.outputChannel.appendLine(content); + } + } + + public dispose(): void { + this.lineSplitter.close(); + } + + public async performOperation(startContent: string, operation: (outputManager: OutputManager) => Promise, endContent?: string | outputCallback, errorContent?: string | outputCallback): Promise { + if (!this.isShown) { + this.outputChannel.show(true); + this.isShown = true; + } + + this.appendLine(startContent); + + try { + const nextLevelOutputManager = new DefaultOutputManager(this.outputChannel, this.level + 1); + + let result: T; + + try { + + result = await operation(nextLevelOutputManager); + } + finally { + nextLevelOutputManager.dispose(); + } + + if (endContent) { + this.appendLine(typeof endContent === 'string' ? endContent : endContent(result)); + } + + return result; + } catch (error) { + if (errorContent) { + this.appendLine(typeof errorContent === 'string' ? errorContent : errorContent(error)); + } + + throw error; + } + } + + private generatePrefix(content?: string): string { + return '>'.repeat(this.level) + ' ' + (content || ''); + } +} diff --git a/debugging/coreclr/prereqManager.ts b/debugging/coreclr/prereqManager.ts new file mode 100644 index 0000000000..9d3476bd5a --- /dev/null +++ b/debugging/coreclr/prereqManager.ts @@ -0,0 +1,174 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +import * as path from 'path'; +import * as vscode from 'vscode'; +import { BrowserClient } from './browserClient'; +import { DockerClient } from './dockerClient'; +import { MacNuGetPackageFallbackFolderPath } from './dockerManager'; +import { DotNetClient } from './dotNetClient'; +import { FileSystemProvider } from './fsProvider'; +import { OSProvider } from './osProvider'; +import { ProcessProvider } from './processProvider'; + +export interface Prerequisite { + checkPrerequisite(): Promise; +} + +export type ShowErrorMessageFunction = (message: string, ...items: vscode.MessageItem[]) => Thenable; + +export class DockerDaemonIsLinuxPrerequisite implements Prerequisite { + constructor( + private readonly dockerClient: DockerClient, + private readonly showErrorMessage: ShowErrorMessageFunction) { + } + + public async checkPrerequisite(): Promise { + const daemonOsJson = await this.dockerClient.getVersion({ format: '{{json .Server.Os}}' }); + const daemonOs = JSON.parse(daemonOsJson.trim()); + + if (daemonOs === 'linux') { + return true; + } + + this.showErrorMessage('The Docker daemon is not configured to run Linux containers. Only Linux containers can be used for .NET Core ASP.NET debugging.') + + return false; + } +} + +export class DotNetExtensionInstalledPrerequisite implements Prerequisite { + constructor( + private readonly browserClient: BrowserClient, + private readonly getExtension: (extensionId: string) => vscode.Extension | undefined, + private readonly showErrorMessage: ShowErrorMessageFunction) { + } + + public async checkPrerequisite(): Promise { + // NOTE: Debugging .NET Core in Docker containers requires the C# (i.e. .NET Core debugging) extension. + // As this extension targets Docker in general and not .NET Core in particular, we don't want the + // extension as a whole to depend on it. Hence, we only check for its existence if/when asked to + // debug .NET Core in Docker containers. + const dependenciesSatisfied = this.getExtension('ms-vscode.csharp') !== undefined; + + if (!dependenciesSatisfied) { + const openExtensionInGallery: vscode.MessageItem = { + title: 'View extension in gallery' + }; + + this + .showErrorMessage( + 'To debug .NET Core in Docker containers, install the C# extension for VS Code.', + openExtensionInGallery) + .then(result => { + if (result === openExtensionInGallery) { + this.browserClient.openBrowser('https://marketplace.visualstudio.com/items?itemName=ms-vscode.csharp'); + } + }); + } + + return await Promise.resolve(dependenciesSatisfied); + } +} + +export class DotNetSdkInstalledPrerequisite implements Prerequisite { + constructor( + private readonly msbuildClient: DotNetClient, + private readonly showErrorMessage: ShowErrorMessageFunction) { + } + + public async checkPrerequisite(): Promise { + const result = await this.msbuildClient.getVersion(); + + if (result) { + return true; + } + + this.showErrorMessage('The .NET Core SDK must be installed to debug .NET Core applications running within Docker containers.'); + + return false; + } +} + +type DockerSettings = { + filesharingDirectories?: string[]; +}; + +export class LinuxUserInDockerGroupPrerequisite implements Prerequisite { + constructor( + private readonly osProvider: OSProvider, + private readonly processProvider: ProcessProvider, + private readonly showErrorMessage: ShowErrorMessageFunction) { + } + + public async checkPrerequisite(): Promise { + if (this.osProvider.os !== 'Linux' || this.osProvider.isMac) { + return true; + } + + const result = await this.processProvider.exec('id -Gn', {}); + const groups = result.stdout.trim().split(' '); + const inDockerGroup = groups.find(group => group === 'docker') !== undefined; + + if (inDockerGroup) { + return true; + } + + this.showErrorMessage('The current user is not a member of the "docker" group. Add it using the command "sudo usermod -a -G docker $USER".') + + return false; + } +} + +export class MacNuGetFallbackFolderSharedPrerequisite implements Prerequisite { + constructor( + private readonly fileSystemProvider: FileSystemProvider, + private readonly osProvider: OSProvider, + private readonly showErrorMessage: ShowErrorMessageFunction) { + } + + public async checkPrerequisite(): Promise { + if (!this.osProvider.isMac) { + // Only Mac requires this folder be specifically shared. + return true; + } + + const settingsPath = path.posix.join(this.osProvider.homedir, 'Library/Group Containers/group.com.docker/settings.json'); + + if (!await this.fileSystemProvider.fileExists(settingsPath)) { + // Docker versions earlier than 17.12.0-ce-mac46 may not have the settings file. + return true; + } + + const settingsContent = await this.fileSystemProvider.readFile(settingsPath); + const settings = JSON.parse(settingsContent); + + if (settings === undefined || settings.filesharingDirectories === undefined) { + // Docker versions earlier than 17.12.0-ce-mac46 may not have the property. + return true; + } + + if (settings.filesharingDirectories.find(directory => directory === MacNuGetPackageFallbackFolderPath) !== undefined) { + return true; + } + + this.showErrorMessage(`To debug .NET Core in Docker containers, add "${MacNuGetPackageFallbackFolderPath}" as a shared folder in your Docker preferences.`); + + return false; + } +} + +export class AggregatePrerequisite implements Prerequisite { + private readonly prerequisites: Prerequisite[]; + + constructor(...prerequisites: Prerequisite[]) { + this.prerequisites = prerequisites; + } + + public async checkPrerequisite(): Promise { + const results = await Promise.all(this.prerequisites.map(async prerequisite => await prerequisite.checkPrerequisite())); + + return results.every(result => result); + } +} diff --git a/debugging/coreclr/processProvider.ts b/debugging/coreclr/processProvider.ts new file mode 100644 index 0000000000..b131c0d942 --- /dev/null +++ b/debugging/coreclr/processProvider.ts @@ -0,0 +1,51 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +import * as cp from 'child_process'; +import * as process from 'process'; + +export type ProcessProviderExecOptions = cp.ExecOptions & { progress?(content: string): void }; + +export interface ProcessProvider { + env: { [key: string]: string | undefined }; + pid: number; + + exec(command: string, options: ProcessProviderExecOptions): Promise<{ stdout: string, stderr: string }>; +} + +export class ChildProcessProvider implements ProcessProvider { + + get env(): { [key: string]: string | undefined } { + return process.env; + } + + get pid(): number { + return process.pid; + } + + public async exec(command: string, options: ProcessProviderExecOptions): Promise<{ stdout: string, stderr: string }> { + return await new Promise<{ stdout: string, stderr: string }>( + (resolve, reject) => { + const p = cp.exec( + command, + options, + (error, stdout, stderr) => { + if (error) { + return reject(error); + } + + resolve({ stdout, stderr }); + }); + + if (options.progress) { + const progress = options.progress; + + p.stderr.on('data', chunk => progress(chunk.toString())); + p.stdout.on('data', chunk => progress(chunk.toString())); + } + }); + } +} + +export default ChildProcessProvider; diff --git a/debugging/coreclr/registerDebugger.ts b/debugging/coreclr/registerDebugger.ts new file mode 100644 index 0000000000..4ce76f9e84 --- /dev/null +++ b/debugging/coreclr/registerDebugger.ts @@ -0,0 +1,95 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { DefaultAppStorageProvider } from './appStorage'; +import OpnBrowserClient from './browserClient'; +import { DefaultDebuggerClient } from './debuggerClient'; +import { DockerDebugSessionManager } from './debugSessionManager'; +import CliDockerClient from './dockerClient'; +import DockerDebugConfigurationProvider from './dockerDebugConfigurationProvider'; +import { DefaultDockerManager } from './dockerManager'; +import CommandLineDotNetClient from './dotNetClient'; +import { LocalFileSystemProvider } from './fsProvider'; +import { MsBuildNetCoreProjectProvider } from './netCoreProjectProvider'; +import LocalOSProvider from './osProvider'; +import { DefaultOutputManager } from './outputManager'; +import { AggregatePrerequisite, DockerDaemonIsLinuxPrerequisite, DotNetExtensionInstalledPrerequisite, DotNetSdkInstalledPrerequisite, LinuxUserInDockerGroupPrerequisite, MacNuGetFallbackFolderSharedPrerequisite } from './prereqManager'; +import ChildProcessProvider from './processProvider'; +import { OSTempFileProvider } from './tempFileProvider'; +import { RemoteVsDbgClient } from './vsdbgClient'; + +export function registerDebugConfigurationProvider(ctx: vscode.ExtensionContext): void { + const fileSystemProvider = new LocalFileSystemProvider(); + + const processProvider = new ChildProcessProvider(); + const dockerClient = new CliDockerClient(processProvider); + const msBuildClient = new CommandLineDotNetClient(processProvider); + const osProvider = new LocalOSProvider(); + + const dockerOutputChannel = vscode.window.createOutputChannel('Docker'); + + ctx.subscriptions.push(dockerOutputChannel); + + const dockerOutputManager = new DefaultOutputManager(dockerOutputChannel); + + const dockerManager = + new DefaultDockerManager( + new DefaultAppStorageProvider(fileSystemProvider), + new DefaultDebuggerClient( + new RemoteVsDbgClient( + dockerOutputManager, + fileSystemProvider, + ctx.globalState, + osProvider, + processProvider)), + dockerClient, + dockerOutputManager, + fileSystemProvider, + osProvider, + processProvider, + ctx.workspaceState); + + const debugSessionManager = new DockerDebugSessionManager( + vscode.debug.onDidTerminateDebugSession, + dockerManager + ); + + ctx.subscriptions.push(debugSessionManager); + + ctx.subscriptions.push( + vscode.debug.registerDebugConfigurationProvider( + 'docker-coreclr', + new DockerDebugConfigurationProvider( + debugSessionManager, + dockerManager, + fileSystemProvider, + osProvider, + new MsBuildNetCoreProjectProvider( + fileSystemProvider, + msBuildClient, + new OSTempFileProvider( + osProvider, + processProvider)), + new AggregatePrerequisite( + new DockerDaemonIsLinuxPrerequisite( + dockerClient, + vscode.window.showErrorMessage), + new DotNetExtensionInstalledPrerequisite( + new OpnBrowserClient(), + vscode.extensions.getExtension, + vscode.window.showErrorMessage), + new DotNetSdkInstalledPrerequisite( + msBuildClient, + vscode.window.showErrorMessage), + new MacNuGetFallbackFolderSharedPrerequisite( + fileSystemProvider, + osProvider, + vscode.window.showErrorMessage), + new LinuxUserInDockerGroupPrerequisite( + osProvider, + processProvider, + vscode.window.showErrorMessage) + )))); +} diff --git a/debugging/coreclr/tempFileProvider.ts b/debugging/coreclr/tempFileProvider.ts new file mode 100644 index 0000000000..eaa5de3a24 --- /dev/null +++ b/debugging/coreclr/tempFileProvider.ts @@ -0,0 +1,24 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +import * as path from 'path'; +import { OSProvider } from "./osProvider"; +import { ProcessProvider } from './processProvider'; + +export interface TempFileProvider { + getTempFilename(prefix?: string): string; +} + +export class OSTempFileProvider implements TempFileProvider { + private count: number = 1; + + constructor( + private readonly osProvider: OSProvider, + private readonly processProvider: ProcessProvider) { + } + + public getTempFilename(prefix: string = 'temp'): string { + return path.join(this.osProvider.tmpdir, `${prefix}_${new Date().valueOf()}_${this.processProvider.pid}_${this.count++}.tmp` ); + } +} diff --git a/debugging/coreclr/vsdbgClient.ts b/debugging/coreclr/vsdbgClient.ts new file mode 100644 index 0000000000..58f8b33548 --- /dev/null +++ b/debugging/coreclr/vsdbgClient.ts @@ -0,0 +1,149 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +import * as path from 'path'; +import * as process from 'process'; +import * as request from 'request-promise-native'; +import { Memento } from 'vscode'; +import { FileSystemProvider } from './fsProvider'; +import { OSProvider } from './osProvider'; +import { OutputManager } from './outputManager'; +import { ProcessProvider } from './processProvider'; + +export interface VsDbgClient { + getVsDbgVersion(version: string, runtime: string): Promise; +} + +type VsDbgScriptPlatformOptions = { + name: string; + url: string; + getAcquisitionCommand(vsdbgAcquisitionScriptPath: string, version: string, runtime: string, vsdbgVersionPath: string): Promise; + onScriptAcquired?(path: string): Promise; +}; + +export class RemoteVsDbgClient implements VsDbgClient { + private static readonly stateKey: string = 'RemoteVsDbgClient'; + private static readonly winDir: string = 'WINDIR'; + + private readonly vsdbgPath: string; + private readonly options: VsDbgScriptPlatformOptions; + + constructor( + private readonly dockerOutputManager: OutputManager, + private readonly fileSystemProvider: FileSystemProvider, + private readonly globalState: Memento, + osProvider: OSProvider, + private readonly processProvider: ProcessProvider) { + this.vsdbgPath = path.join(osProvider.homedir, '.vsdbg'); + this.options = osProvider.os === 'Windows' + ? { + name: 'GetVsDbg.ps1', + url: 'https://aka.ms/getvsdbgps1', + getAcquisitionCommand: async (vsdbgAcquisitionScriptPath: string, version: string, runtime: string, vsdbgVersionPath: string) => { + const powershellCommand = `${process.env[RemoteVsDbgClient.winDir]}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`; + return await Promise.resolve(`${powershellCommand} -NonInteractive -NoProfile -WindowStyle Hidden -ExecutionPolicy RemoteSigned -File \"${vsdbgAcquisitionScriptPath}\" -Version ${version} -RuntimeID ${runtime} -InstallPath \"${vsdbgVersionPath}\"`); + } + } + : { + name: 'getvsdbg.sh', + url: 'https://aka.ms/getvsdbgsh', + getAcquisitionCommand: async (vsdbgAcquisitionScriptPath: string, version: string, runtime: string, vsdbgVersionPath: string) => { + return await Promise.resolve(`${vsdbgAcquisitionScriptPath} -v ${version} -r ${runtime} -l \"${vsdbgVersionPath}\"`); + }, + onScriptAcquired: async (scriptPath: string) => { + await this.processProvider.exec(`chmod +x \"${scriptPath}\"`, { cwd: this.vsdbgPath }); + } + }; + } + + public async getVsDbgVersion(version: string, runtime: string): Promise { + const vsdbgVersionPath = path.join(this.vsdbgPath, runtime, version); + const vsdbgVersionExists = await this.fileSystemProvider.dirExists(vsdbgVersionPath); + + if (vsdbgVersionExists && await this.isUpToDate(this.lastDebuggerAcquisitionKey(version, runtime))) { + // The debugger is up to date... + return vsdbgVersionPath; + } + + return await this.dockerOutputManager.performOperation( + 'Acquiring the latest .NET Core debugger...', + async () => { + + await this.getVsDbgAcquisitionScript(); + + const vsdbgAcquisitionScriptPath = path.join(this.vsdbgPath, this.options.name); + + const acquisitionCommand = await this.options.getAcquisitionCommand(vsdbgAcquisitionScriptPath, version, runtime, vsdbgVersionPath); + + await this.processProvider.exec(acquisitionCommand, { cwd: this.vsdbgPath }); + + await this.updateDate(this.lastDebuggerAcquisitionKey(version, runtime), new Date()); + + return vsdbgVersionPath; + }, + 'Debugger acquired.', + 'Unable to acquire the .NET Core debugger.'); + } + + private async getVsDbgAcquisitionScript(): Promise { + const vsdbgAcquisitionScriptPath = path.join(this.vsdbgPath, this.options.name); + const acquisitionScriptExists = await this.fileSystemProvider.fileExists(vsdbgAcquisitionScriptPath); + + if (acquisitionScriptExists && await this.isUpToDate(this.lastScriptAcquisitionKey)) { + // The acquisition script is up to date... + return; + } + + const directoryExists = await this.fileSystemProvider.dirExists(this.vsdbgPath); + + if (!directoryExists) { + await this.fileSystemProvider.makeDir(this.vsdbgPath); + } + + const script = await request(this.options.url); + + await this.fileSystemProvider.writeFile(vsdbgAcquisitionScriptPath, script); + + if (this.options.onScriptAcquired) { + await this.options.onScriptAcquired(vsdbgAcquisitionScriptPath); + } + + await this.updateDate(this.lastScriptAcquisitionKey, new Date()); + } + + private async isUpToDate(key: string): Promise { + const lastAcquisitionDate = await this.getDate(key); + + if (lastAcquisitionDate) { + let aquisitionExpirationDate = new Date(lastAcquisitionDate); + + aquisitionExpirationDate.setDate(lastAcquisitionDate.getDate() + 1); + + if (aquisitionExpirationDate.valueOf() > new Date().valueOf()) { + // The acquisition is up to date... + return true; + } + } + + return false; + } + + private get lastScriptAcquisitionKey(): string { + return `${RemoteVsDbgClient.stateKey}.lastScriptAcquisition`; + } + + private lastDebuggerAcquisitionKey(version: string, runtime: string): string { + return `${RemoteVsDbgClient.stateKey}.lastDebuggerAcquisition(${version}, ${runtime})`; + } + + private async getDate(key: string): Promise { + const dateString = this.globalState.get(key); + + return await Promise.resolve(dateString ? new Date(dateString) : undefined); + } + + private async updateDate(key: string, timestamp: Date): Promise { + await this.globalState.update(key, timestamp); + } +} diff --git a/dockerExtension.ts b/dockerExtension.ts index 856e35c360..3d38fd094e 100644 --- a/dockerExtension.ts +++ b/dockerExtension.ts @@ -35,6 +35,7 @@ import { docker } from './commands/utils/docker-endpoint'; import { DefaultTerminalProvider } from './commands/utils/TerminalProvider'; import { DockerDebugConfigProvider } from './configureWorkspace/configDebugProvider'; import { configure, configureApi, ConfigureApiOptions } from './configureWorkspace/configure'; +import { registerDebugConfigurationProvider } from './debugging/coreclr/registerDebugger'; import { DockerComposeCompletionItemProvider } from './dockerCompose/dockerComposeCompletionItemProvider'; import { DockerComposeHoverProvider } from './dockerCompose/dockerComposeHoverProvider'; import composeVersionKeys from './dockerCompose/dockerComposeKeyInfo'; @@ -126,6 +127,8 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { registerDockerCommands(); ctx.subscriptions.push(vscode.debug.registerDebugConfigurationProvider('docker', new DockerDebugConfigProvider())); + registerDebugConfigurationProvider(ctx); + await consolidateDefaultRegistrySettings(); activateLanguageClient(ctx); diff --git a/images/dockerSharedFolders.png b/images/dockerSharedFolders.png new file mode 100644 index 0000000000000000000000000000000000000000..284fa440097b67089b0c24b82a69749e9aafcdcd GIT binary patch literal 64797 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 mdK4&}fBj~AICICimsmCJm6OWCl { + async function testDotNetCoreConsole(os: PlatformOS, projectFolder: string, projectFileName: string, projectFileContents: string, expectedDockerFileContents?: string): Promise { await writeFile(projectFolder, projectFileName, projectFileContents); await writeFile(projectFolder, 'Program.cs', dotnetCoreConsole_ProgramCsContents); @@ -402,7 +402,7 @@ suite("Configure (Add Docker files to Workspace)", function (this: Suite): void } } - async function testAspNetCore(os: OS, hostOs: OS, hostOsRelease: string, projectFolder: string, projectFileName: string, projectFileContents: string, expectedDockerFileContents?: string): Promise { + async function testAspNetCore(os: PlatformOS, hostOs: PlatformOS, hostOsRelease: string, projectFolder: string, projectFileName: string, projectFileContents: string, expectedDockerFileContents?: string): Promise { let previousOs = ext.os; ext.os = { platform: hostOs === 'Windows' ? 'win32' : 'linux', diff --git a/test/debugging/coreclr/lineSplitter.test.ts b/test/debugging/coreclr/lineSplitter.test.ts new file mode 100644 index 0000000000..cf88e43dca --- /dev/null +++ b/test/debugging/coreclr/lineSplitter.test.ts @@ -0,0 +1,45 @@ +import * as assert from 'assert'; +import LineSplitter from '../../../debugging/coreclr/lineSplitter'; + +suite('debugging', () => { + suite('coreclr', () => { + suite('LineSplitter', () => { + const testCase = (name: string, input: string | string[], output: string[]) => { + test(name, () => { + const splitter = new LineSplitter(); + + const lines: string[] = []; + + splitter.onLine(line => lines.push(line)); + + if (typeof input === 'string') { + splitter.write(input); + } else { + for (let i = 0; i < input.length; i++) { + splitter.write(input[i]); + } + } + + splitter.close(); + + assert.deepEqual(lines, output, 'The number or contents of the lines are not the same.'); + }); + }; + + testCase('Empty string', '', []); + testCase('Only LF', '\n', ['']); + testCase('CR & LF', '\r\n', ['']); + testCase('Multiple LFs', '\n\n', ['', '']); + testCase('Multiple CR & LFs', '\r\n\r\n', ['', '']); + testCase('Single line', 'line one', ['line one']); + testCase('Leading LF', '\nline two', ['', 'line two']); + testCase('Leading CR & LF', '\r\nline two', ['', 'line two']); + testCase('Trailing LF', 'line one\n', ['line one']); + testCase('Trailing CR & LF', 'line one\r\n', ['line one']); + testCase('Multiple lines with LF', 'line one\nline two', ['line one', 'line two']); + testCase('Multiple lines with CR & LF', 'line one\r\nline two', ['line one', 'line two']); + testCase('CR & LF spanning writes', ['line one\r', '\nline two'], ['line one', 'line two']); + }); + }); +}); + diff --git a/test/debugging/coreclr/prereqManager.test.ts b/test/debugging/coreclr/prereqManager.test.ts new file mode 100644 index 0000000000..0e0e697715 --- /dev/null +++ b/test/debugging/coreclr/prereqManager.test.ts @@ -0,0 +1,215 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +import * as assert from 'assert'; +import * as vscode from 'vscode'; +import { FileSystemProvider } from '../../../debugging/coreclr/fsProvider'; +import { OSProvider } from '../../../debugging/coreclr/osProvider'; +import { ProcessProvider } from '../../../debugging/coreclr/processProvider'; +import { MacNuGetFallbackFolderSharedPrerequisite, LinuxUserInDockerGroupPrerequisite, ShowErrorMessageFunction, DockerDaemonIsLinuxPrerequisite, DotNetSdkInstalledPrerequisite } from '../../../debugging/coreclr/prereqManager'; +import { PlatformOS } from '../../../utils/platform'; +import { DockerClient } from '../../../debugging/coreclr/dockerClient'; +import { DotNetClient } from '../../../debugging/coreclr/dotNetClient'; + +suite('debugging', () => { + suite('coreclr', () => { + suite('prereqManager', () => { + suite('DockerDaemonIsLinuxPrerequisite', () => { + const generateTest = (name: string, result: boolean, os: PlatformOS) => { + test(name, async () => { + let gotVersion = false; + + const dockerClient = { + getVersion: (options) => { + gotVersion = true; + + assert.deepEqual(options, { format: '{{json .Server.Os}}' }, 'The server OS should be requested, in JSON format.'); + + return Promise.resolve(`"${os.toLowerCase()}"`); + } + }; + + let shown = false; + + const showErrorMessage = (message: string, ...items: vscode.MessageItem[]): Thenable => { + shown = true; + return Promise.resolve(undefined); + }; + + const prerequisite = new DockerDaemonIsLinuxPrerequisite(dockerClient, showErrorMessage); + + const prereqResult = await prerequisite.checkPrerequisite(); + + assert.equal(gotVersion, true, 'The Docker version should have been requested.'); + + assert.equal(prereqResult, result, 'The prerequisite should return `false`.'); + assert.equal(shown, !result, `An error message should ${result ? 'not ' : ''} have been shown.`); + }); + } + + generateTest('Linux daemon', true, 'Linux'); + generateTest('Windows daemon', false, 'Windows'); + }); + + suite('DotNetSdkInstalledPrerequisite', () => { + test('Installed', async () => { + const msBuildClient = { + getVersion: () => Promise.resolve('2.1.402') + }; + + let shown = false; + + const showErrorMessage = (message: string, ...items: vscode.MessageItem[]): Thenable => { + shown = true; + return Promise.resolve(undefined); + }; + + const prerequisite = new DotNetSdkInstalledPrerequisite(msBuildClient, showErrorMessage); + + const prereqResult = await prerequisite.checkPrerequisite(); + + assert.equal(prereqResult, true, 'The prerequisite should pass if the SDK is installed.'); + assert.equal(shown, false, 'No error should be shown.'); + }); + + test('Not installed', async () => { + const msBuildClient = { + getVersion: () => Promise.resolve(undefined) + }; + + let shown = false; + + const showErrorMessage = (message: string, ...items: vscode.MessageItem[]): Thenable => { + shown = true; + return Promise.resolve(undefined); + }; + + const prerequisite = new DotNetSdkInstalledPrerequisite(msBuildClient, showErrorMessage); + + const prereqResult = await prerequisite.checkPrerequisite(); + + assert.equal(prereqResult, false, 'The prerequisite should fail if no SDK is installed.'); + assert.equal(shown, true, 'An error should be shown.'); + }); + }); + + suite('LinuxUserInDockerGroupPrerequisite', () => { + const generateTest = (name: string, result: boolean, os: PlatformOS, isMac?: boolean, inGroup?: boolean) => { + test(name, async () => { + const osProvider = { + os, + isMac + } + + let processProvider = {}; + let listed = false; + + if (os === 'Linux' && !isMac) { + processProvider = { + exec: (command: string, _) => { + listed = true; + + assert.equal(command, 'id -Gn', 'The prerequisite should list the user\'s groups.') + + const groups = inGroup ? 'groupA docker groupB' : 'groupA groupB'; + + return Promise.resolve({ stdout: groups, stderr: ''}); + } + }; + } + + let shown = false; + + const showErrorMessage = (message: string, ...items: vscode.MessageItem[]): Thenable => { + shown = true; + return Promise.resolve(undefined); + }; + + const prerequisite = new LinuxUserInDockerGroupPrerequisite(osProvider, processProvider, showErrorMessage); + + const prereqResult = await prerequisite.checkPrerequisite(); + + if (os === 'Linux' && !isMac) { + assert.equal(listed, true, 'The user\'s groups should have been listed.'); + } + + assert.equal(prereqResult, result, 'The prerequisite should return `false`.'); + assert.equal(shown, !result, `An error message should ${result ? 'not ' : ''} have been shown.`); + }); + }; + + generateTest('Windows: No-op', true, 'Windows'); + generateTest('Mac: No-op', true, 'Linux', true); + generateTest('Linux: In group', true, 'Linux', false, true); + generateTest('Linux: Not in group', false, 'Linux', false, false); + }); + + suite('MacNuGetFallbackFolderSharedPrerequisite', () => { + const generateTest = (name: string, fileContents: string | undefined, result: boolean) => { + const settingsPath = '/Users/User/Library/Group Containers/group.com.docker/settings.json'; + + test(name, async () => { + const fsProvider = { + fileExists: (path: string) => { + assert.equal(settingsPath, path, 'The prerequisite should check for the settings file in the user\'s home directory.'); + + return Promise.resolve(fileContents !== undefined); + }, + readFile: (path: string) => { + if (fileContents === undefined) { + assert.fail('The prerequisite should not attempt to read a file that does not exist.'); + } + + assert.equal(settingsPath, path, 'The prerequisite should read the settings file in the user\'s home directory.'); + + return Promise.resolve(fileContents); + } + }; + + const osProvider = { + homedir: '/Users/User', + isMac: true + }; + + let shown = false; + + const showErrorMessage = (message: string, ...items: vscode.MessageItem[]): Thenable => { + shown = true; + return Promise.resolve(undefined); + }; + + const prereq = new MacNuGetFallbackFolderSharedPrerequisite(fsProvider, osProvider, showErrorMessage); + + const prereqResult = await prereq.checkPrerequisite(); + + assert.equal(prereqResult, result, 'The prerequisite should return `false`.'); + assert.equal(shown, !result, `An error message should ${result ? 'not ' : ''} have been shown.`); + }); + } + + generateTest('Mac: no Docker settings file', undefined, true); + generateTest('Mac: no shared folders in Docker settings file', '{}', true); + generateTest('Mac: no NuGetFallbackFolder in Docker settings file', '{ "filesharingDirectories": [] }', false); + generateTest('Mac: NuGetFallbackFolder in Docker settings file', '{ "filesharingDirectories": [ "/usr/local/share/dotnet/sdk/NuGetFallbackFolder" ] }', true); + + test('Non-Mac: No-op', async () => { + const osProvider = { + isMac: false + }; + + const showErrorMessage = (message: string, ...items: vscode.MessageItem[]): Thenable => { + assert.fail('Should not be called on non-Mac.'); + return Promise.resolve(undefined); + }; + + const prereq = new MacNuGetFallbackFolderSharedPrerequisite({}, osProvider, showErrorMessage); + + const result = await prereq.checkPrerequisite(); + + assert.equal(true, result, 'The prerequisite should return `true` on non-Mac.'); + }); + }); + }); + }); +}); diff --git a/utils/platform.ts b/utils/platform.ts new file mode 100644 index 0000000000..9f874db3cb --- /dev/null +++ b/utils/platform.ts @@ -0,0 +1,15 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export type PlatformOS = 'Windows' | 'Linux'; +export type Platform = + 'Go' | + 'Java' | + '.NET Core Console' | + 'ASP.NET Core' | + 'Node.js' | + 'Python' | + 'Ruby' | + 'Other'; From c5df06b9e821cefc95e615904d12804a2387fee7 Mon Sep 17 00:00:00 2001 From: "Stephen Weatherford (MSFT)" Date: Wed, 17 Oct 2018 17:41:04 -0700 Subject: [PATCH 22/36] Set loadAzureAccountExt event as an activation event (#568) --- utils/azureUtilityManager.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/utils/azureUtilityManager.ts b/utils/azureUtilityManager.ts index 624d0e81ed..e4dce6aea0 100644 --- a/utils/azureUtilityManager.ts +++ b/utils/azureUtilityManager.ts @@ -34,6 +34,8 @@ export class AzureUtilityManager { // tslint:disable-next-line:no-function-expression await callWithTelemetryAndErrorHandling('docker.loadAzureAccountExt', async function (this: IActionContext): Promise { + this.properties.isActivationEvent = 'true'; + try { let azureAccountExtension = vscode.extensions.getExtension('ms-vscode.azure-account'); this.properties.found = azureAccountExtension ? 'true' : 'false'; From d85ce66ac0f1bf5a651069d530c1cf66bbf1175f Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Fri, 19 Oct 2018 14:36:53 -0700 Subject: [PATCH 23/36] Remove ASP.NET qualifier to .NET Core debugging scenario. (#575) --- README.md | 12 +++++++----- .../coreclr/dockerDebugConfigurationProvider.ts | 2 +- debugging/coreclr/prereqManager.ts | 2 +- package.json | 14 ++++++-------- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index b4c0b8066a..5f25485fcd 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ After the container is started, you will be prompted to login to your Azure acco This build includes preview support for connecting to private registries (such as those described in Docker Hub [documentation](https://docs.docker.com/registry/deploying/)). At the moment, OAuth is not supported, only basic authentication. We hope to extend this support in the future. -## Debugging .NET Core ASP.NET (Preview) +## Debugging .NET Core (Preview) > Note that Windows containers are **not** currently supported, only Linux containers. @@ -104,15 +104,17 @@ This build includes preview support for connecting to private registries (such a ### Starting the Debugger -To debug a .NET Core ASP.NET application running in a Linux Docker container, add a Docker .NET Core launch configuration: +To debug a .NET Core application running in a Linux Docker container, add a Docker .NET Core launch configuration: 1. Switch to the debugging tab. 1. Select `Add configuration...` -1. Select `Docker: Launch .NET Core ASP.NET (Preview)` +1. Select `Docker: Launch .NET Core (Preview)` 1. Set a breakpoint. 1. Start debugging. -Upon debugging, a Docker image will be built and a container will be run based on that image. The container will have volumes mapped to the locally-built application and the .NET Core debugger. After the debugger is attached, the browser will be launched and navigate to the application's initial page. +Upon debugging, a Docker image will be built and a container will be run based on that image. The container will have volumes mapped to the locally-built application and the .NET Core debugger. If the Docker container exposes port 80, after the debugger is attached the browser will be launched and navigate to the application's initial page. + +> NOTE: you may see errors in the debug console when debugging ends (e.g. "`Error from pipe program 'docker': ...`"). This appears due to debugger issue [#2439](https://github.com/OmniSharp/omnisharp-vscode/issues/2439) and should not impact debugging. Most properties of the configuration are optional and will be inferred from the project. If not, or if there are additional customizations to be made to the Docker image build or container run process, those can be added under the `dockerBuild` and `dockerRun` properties of the configuration, respectively. @@ -120,7 +122,7 @@ Most properties of the configuration are optional and will be inferred from the { "configurations": [ { - "name": "Docker: Launch .NET Core ASP.NET (Preview)", + "name": "Docker: Launch .NET Core (Preview)", "type": "docker-coreclr", "request": "launch", "preLaunchTask": "build", diff --git a/debugging/coreclr/dockerDebugConfigurationProvider.ts b/debugging/coreclr/dockerDebugConfigurationProvider.ts index 544bd73fd4..115723d99b 100644 --- a/debugging/coreclr/dockerDebugConfigurationProvider.ts +++ b/debugging/coreclr/dockerDebugConfigurationProvider.ts @@ -58,7 +58,7 @@ export class DockerDebugConfigurationProvider implements DebugConfigurationProvi public provideDebugConfigurations(folder: WorkspaceFolder | undefined, token?: CancellationToken): ProviderResult { return [ { - name: 'Docker: Launch .NET Core ASP.NET (Preview)', + name: 'Docker: Launch .NET Core (Preview)', type: 'docker-coreclr', request: 'launch', preLaunchTask: 'build', diff --git a/debugging/coreclr/prereqManager.ts b/debugging/coreclr/prereqManager.ts index 9d3476bd5a..377d009147 100644 --- a/debugging/coreclr/prereqManager.ts +++ b/debugging/coreclr/prereqManager.ts @@ -32,7 +32,7 @@ export class DockerDaemonIsLinuxPrerequisite implements Prerequisite { return true; } - this.showErrorMessage('The Docker daemon is not configured to run Linux containers. Only Linux containers can be used for .NET Core ASP.NET debugging.') + this.showErrorMessage('The Docker daemon is not configured to run Linux containers. Only Linux containers can be used for .NET Core debugging.') return false; } diff --git a/package.json b/package.json index d397010e98..14fa3c7325 100644 --- a/package.json +++ b/package.json @@ -271,20 +271,18 @@ }, { "type": "docker-coreclr", - "label": "Docker: Launch .NET Core ASP.NET (Preview)", + "label": "Docker: Launch .NET Core (Preview)", "configurationSnippets": [ { - "label": "Docker: Launch .NET Core ASP.NET (Preview)", - "description": "Docker: Launch .NET Core ASP.NET (Preview)", + "label": "Docker: Launch .NET Core (Preview)", + "description": "Docker: Launch .NET Core (Preview)", "body": { - "name": "Docker: Launch .NET Core ASP.NET (Preview)", + "name": "Docker: Launch .NET Core (Preview)", "type": "docker-coreclr", "request": "launch", "preLaunchTask": "build", - "dockerBuild": { - }, - "dockerRun": { - } + "dockerBuild": {}, + "dockerRun": {} } } ], From 1a1c7ba90bc886b1f948ad1c7c8facfd9ebc269c Mon Sep 17 00:00:00 2001 From: Nguyen Long Nhat Date: Tue, 23 Oct 2018 22:53:16 +0700 Subject: [PATCH 24/36] workspaceRoot -> workspaceFolder (#574) --- .vscode/launch.json | 12 ++++++------ .vscode/tasks.json | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 11e26728ed..bf12d467fc 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -8,7 +8,7 @@ "request": "launch", "runtimeExecutable": "${execPath}", "args": [ - "--extensionDevelopmentPath=${workspaceRoot}" + "--extensionDevelopmentPath=${workspaceFolder}" ], "env": { "DEBUGTELEMETRY": "1" @@ -16,7 +16,7 @@ "stopOnEntry": false, "sourceMaps": true, "outFiles": [ - "${workspaceRoot}/out/**/*.js" + "${workspaceFolder}/out/**/*.js" ], "preLaunchTask": "npm" }, @@ -31,14 +31,14 @@ // "c:/Repos/vscode-docker/test/test.code-workspace", // "--extensionDevelopmentPath=c:/Repos/vscode-docker", // "--extensionTestsPath=c:/Repos/vscode-docker/out/test" - "${workspaceRoot}/test/test.code-workspace", - "--extensionDevelopmentPath=${workspaceRoot}", - "--extensionTestsPath=${workspaceRoot}/out/test" + "${workspaceFolder}/test/test.code-workspace", + "--extensionDevelopmentPath=${workspaceFolder}", + "--extensionTestsPath=${workspaceFolder}/out/test" ], "stopOnEntry": false, "sourceMaps": true, "outFiles": [ - "${workspaceRoot}/out/test" + "${workspaceFolder}/out/test" ], "preLaunchTask": "npm", "env": { diff --git a/.vscode/tasks.json b/.vscode/tasks.json index f2f0e5e0f6..eda1f8ad6a 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,5 +1,5 @@ // Available variables which can be used inside of strings. -// ${workspaceRoot}: the root folder of the team +// ${workspaceFolder}: the root folder of the team // ${file}: the current opened file // ${fileBasename}: the current opened file's basename // ${fileDirname}: the current opened file's dirname From 2dda7e69e69e5dd59a49e23e1927d40bb3268e90 Mon Sep 17 00:00:00 2001 From: Remco Haszing Date: Tue, 23 Oct 2018 22:05:37 +0200 Subject: [PATCH 25/36] Specify .dockerignore language (#564) By specifying .dockerignore as an ignore file, support is added for syntax highlighting and toggling of comments. --- package.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/package.json b/package.json index 14fa3c7325..238b07e5af 100644 --- a/package.json +++ b/package.json @@ -346,6 +346,12 @@ "*.dockerfile", "Dockerfile" ] + }, + { + "id": "ignore", + "filenames": [ + ".dockerignore" + ] } ], "configuration": { From 4d70489f7249a97236e14533896683eeb1f11d27 Mon Sep 17 00:00:00 2001 From: Eric Jizba Date: Tue, 23 Oct 2018 14:01:07 -0700 Subject: [PATCH 26/36] Add CODEOWNERS (#581) --- .github/CODEOWNERS | 1 + .vscodeignore | 1 + 2 files changed, 2 insertions(+) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000000..4df3d45b4a --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @Microsoft/vscodeazuretools diff --git a/.vscodeignore b/.vscodeignore index 46d3fec9e3..cc72406ce9 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -6,3 +6,4 @@ typings/** tsconfig.json test/** testOutput/** +.github/** From ddbaef140b9d6f4ce8b19c50729c0b8dac0c31aa Mon Sep 17 00:00:00 2001 From: Joonas Tiala Date: Wed, 24 Oct 2018 18:59:03 +0300 Subject: [PATCH 27/36] Upgrade gulp to v4 (#577) Always call the callback in upload-vsix gulp task. Otherwise the task will fail with gulp 4. --- gulpfile.js | 6 ++++-- package.json | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index 4a1c7668e3..ec63c28a1f 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -17,6 +17,7 @@ gulp.task('package', async () => { }); gulp.task('upload-vsix', (callback) => { + let callbackArg; if (process.env.TRAVIS_PULL_REQUEST_BRANCH) { console.log('Skipping upload-vsix for PR build.'); } else { @@ -32,11 +33,11 @@ gulp.task('upload-vsix', (callback) => { const blobService = azureStorage.createBlobService(process.env.STORAGE_NAME, process.env.STORAGE_KEY); blobService.createContainerIfNotExists(containerName, { publicAccessLevel: "blob" }, (err) => { if (err) { - callback(err); + callbackArg = err; } else { blobService.createBlockBlobFromLocalFile(containerName, blobPath, vsixName, (err) => { if (err) { - callback(err); + callbackArg = err; } else { console.log(); console.log(brightYellowFormatting, '================================================ vsix url ================================================'); @@ -51,4 +52,5 @@ gulp.task('upload-vsix', (callback) => { }); } } + callback(callbackArg); }); diff --git a/package.json b/package.json index 238b07e5af..38782c214d 100644 --- a/package.json +++ b/package.json @@ -754,7 +754,7 @@ "adm-zip": "^0.4.11", "azure-storage": "^2.8.1", "cross-env": "^5.2.0", - "gulp": "^3.9.1", + "gulp": "^4.0.0", "mocha": "5.2.0", "tslint": "^5.11.0", "tslint-microsoft-contrib": "^5.2.1", From e30b403c069b2c62d48cfb29d1e260d0557787ab Mon Sep 17 00:00:00 2001 From: "Stephen Weatherford (MSFT)" Date: Wed, 24 Oct 2018 15:02:24 -0700 Subject: [PATCH 28/36] Fix lint task warning (#591) * Fix lint task warning * Fix --- .vscode/tasks.json | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index eda1f8ad6a..a85061ecce 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -34,10 +34,7 @@ { "type": "npm", "script": "lint", - "problemMatcher": { - "base": "$tslint5", - "fileLocation": "absolute" - } + "problemMatcher": "$tslint5" } ] } From 22e45ec7c7ef6ee568f0e592ca614fdcdd3b2c8b Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Wed, 24 Oct 2018 15:29:54 -0700 Subject: [PATCH 29/36] Support additional Docker build and run customization (#573) * Add support for build labels. * Support providing args to build command. * Add support for runtime environment variables. * Added support for specifying environment files during Docker run. * Add support for labels on Docker containers. * Refactor to resolve linting errors. * Sketch switch to command line builder. * Refactor into CommandLineBuilder. * Add initial CommandLineBuilder tests. * Simplify tests. * Resolve linter errors. * Extract options comparer for testing. * Switch to deep-equal. --- README.md | 62 ++++ debugging/coreclr/commandLineBuilder.ts | 75 +++++ debugging/coreclr/dockerClient.ts | 108 ++++--- .../dockerDebugConfigurationProvider.ts | 97 ++++-- debugging/coreclr/dockerManager.ts | 66 +++- package.json | 37 +++ .../coreclr/commandLineBuilder.test.ts | 65 ++++ test/debugging/coreclr/dockerManager.test.ts | 146 +++++++++ test/debugging/coreclr/lineSplitter.test.ts | 78 ++--- test/debugging/coreclr/prereqManager.test.ts | 286 +++++++++--------- thirdpartynotices.txt | 21 ++ 11 files changed, 754 insertions(+), 287 deletions(-) create mode 100644 debugging/coreclr/commandLineBuilder.ts create mode 100644 test/debugging/coreclr/commandLineBuilder.test.ts create mode 100644 test/debugging/coreclr/dockerManager.test.ts diff --git a/README.md b/README.md index 5f25485fcd..620285d8b5 100644 --- a/README.md +++ b/README.md @@ -155,11 +155,41 @@ Customize the Docker image build process by adding properties under the `dockerB | Property | Description | Default | | --- | --- | --- | +| `args` | Build arguments applied to the image. | None | | `context` | The Docker context used during the build process. | The workspace folder, if the same as the application folder; otherwise, the application's parent (i.e. solution) folder | | `dockerfile` | The path to the Dockerfile used to build the image. | The file `Dockerfile` in the application folder | +| `labels` | The set of labels added to the image. | `com.microsoft.created-by` = `visual-studio-code` | | `tag` | The tag added to the image. | `:dev` | | `target` | The target (stage) of the Dockerfile from which to build the image. | `base` +Example build customizations: + +```json +{ + "configurations": [ + { + "name": "Launch .NET Core in Docker", + "type": "docker-coreclr", + "request": "launch", + "preLaunchTask": "build", + "dockerBuild": { + "args": { + "arg1": "value1", + "arg2": "value2" + }, + "context": "${workspaceFolder}/src", + "dockerfile": "${workspaceFolder}/src/Dockerfile", + "labels": { + "label1": "value1", + "label2": "value2" + }, + "tag": "mytag", + "target": "publish" + } + } + ] +} +``` ### Docker Run Customization @@ -168,6 +198,38 @@ Customize the Docker container run process by adding properties under the `docke | Property | Description | Default | | --- | --- | --- | | `containerName` | The name of the container. | `-dev` | +| `env` | Environment variables applied to the container. | None | +| `envFiles` | Files of environment variables read in and applied to the container. Environment variables are specified one per line, in `=` format. | None | +| `labels` | The set of labels added to the container. | `com.microsoft.created-by` = `visual-studio-code` | + +Example run customization: + +```json +{ + "configurations": [ + { + "name": "Launch .NET Core in Docker", + "type": "docker-coreclr", + "request": "launch", + "preLaunchTask": "build", + "dockerRun": { + "containerName": "my-container", + "env": { + "var1": "value1", + "var2": "value2" + }, + "envFiles": [ + "${workspaceFolder}/staging.env" + ], + "labels": { + "label1": "value1", + "label2": "value2" + } + } + } + ] +} +``` ## Configuration Settings diff --git a/debugging/coreclr/commandLineBuilder.ts b/debugging/coreclr/commandLineBuilder.ts new file mode 100644 index 0000000000..7e49aac09a --- /dev/null +++ b/debugging/coreclr/commandLineBuilder.ts @@ -0,0 +1,75 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + + export type CommandLineArgFactory = () => (string | undefined); + +export class CommandLineBuilder { + private readonly args: CommandLineArgFactory[] = []; + + public static create(...args: (undefined | string | CommandLineArgFactory)[]): CommandLineBuilder { + let builder = new CommandLineBuilder(); + + for (let arg of args) { + if (arg) { + if (typeof arg === 'string') { + builder = builder.withArg(arg); + } else { + builder = builder.withArgFactory(arg); + } + } + } + + return builder; + } + + public build(): string { + return this.args.map(arg => arg()).filter(arg => arg !== undefined).join(' '); + } + + public withArg(arg: string | undefined): CommandLineBuilder { + return this.withArgFactory(() => arg); + } + + public withArrayArgs(name: string, values: T[] | undefined, formatter?: (value: T) => string): CommandLineBuilder { + formatter = formatter || ((value: T) => value.toString()); + + return this.withArgFactory(() => values ? values.map(value => `${name} "${formatter(value)}"`).join(' ') : undefined); + } + + public withArgFactory(factory: CommandLineArgFactory | undefined): CommandLineBuilder { + if (factory) { + this.args.push(factory); + } + + return this; + } + + public withFlagArg(name: string, value: boolean | undefined): CommandLineBuilder { + return this.withArgFactory(() => value ? name : undefined); + } + + public withKeyValueArgs(name: string, values: { [key: string]: string }): CommandLineBuilder { + return this.withArgFactory(() => { + if (values) { + const keys = Object.keys(values); + + if (keys.length > 0) { + return keys.map(key => `${name} "${key}=${values[key]}"`).join(' '); + } + } + + return undefined; + }); + } + + public withNamedArg(name: string, value: string | undefined): CommandLineBuilder { + return this.withArgFactory(() => value ? `${name} "${value}"` : undefined); + } + + public withQuotedArg(value: string | undefined): CommandLineBuilder { + return this.withArgFactory(() => value ? `"${value}"` : undefined); + } +} + +export default CommandLineBuilder; diff --git a/debugging/coreclr/dockerClient.ts b/debugging/coreclr/dockerClient.ts index 1cfa71bc71..baa750a09b 100644 --- a/debugging/coreclr/dockerClient.ts +++ b/debugging/coreclr/dockerClient.ts @@ -2,12 +2,15 @@ * Copyright (C) Microsoft Corporation. All rights reserved. *--------------------------------------------------------*/ +import CommandLineBuilder from "./commandLineBuilder"; import { LineSplitter } from "./lineSplitter"; import { ProcessProvider } from "./processProvider"; export type DockerBuildImageOptions = { + args?: { [key: string]: string }; context?: string; dockerfile?: string; + labels?: { [key: string]: string }; tag?: string; target?: string; }; @@ -34,6 +37,9 @@ export type DockerRunContainerOptions = { command?: string; containerName?: string; entrypoint?: string; + env?: { [key: string]: string }; + envFiles?: string[]; + labels?: { [key: string]: string }; volumes?: DockerContainerVolume[]; }; @@ -58,23 +64,17 @@ export class CliDockerClient implements DockerClient { } public async buildImage(options?: DockerBuildImageOptions, progress?: (content: string) => void): Promise { - let command = `docker build --rm`; - - if (options && options.dockerfile) { - command += ` -f ${options.dockerfile}`; - } - - if (options && options.tag) { - command += ` -t ${options.tag}`; - } - - if (options && options.target) { - command += ` --target ${options.target}`; - } - - if (options && options.context) { - command += ` ${options.context}`; - } + options = options || {}; + + let command = CommandLineBuilder + .create('docker', 'build', '--rm') + .withNamedArg('-f', options.dockerfile) + .withKeyValueArgs('--build-arg', options.args) + .withKeyValueArgs('--label', options.labels) + .withNamedArg('-t', options.tag) + .withNamedArg('--target', options.target) + .withQuotedArg(options.context) + .build(); let imageId: string | undefined; @@ -111,11 +111,12 @@ export class CliDockerClient implements DockerClient { } public async getVersion(options?: DockerVersionOptions): Promise { - let command = 'docker version'; + options = options || {}; - if (options && options.format) { - command += ` --format "${options.format}"`; - } + const command = CommandLineBuilder + .create('docker', 'version') + .withNamedArg('--format', options.format) + .build(); const result = await this.processProvider.exec(command, {}); @@ -123,13 +124,13 @@ export class CliDockerClient implements DockerClient { } public async inspectObject(nameOrId: string, options?: DockerInspectObjectOptions): Promise { - let command = 'docker inspect'; + options = options || {}; - if (options && options.format) { - command += ` \"--format=${options.format}\"`; - } - - command += ` ${nameOrId}`; + const command = CommandLineBuilder + .create('docker', 'inspect') + .withNamedArg('--format', options.format) + .withQuotedArg(nameOrId) + .build(); try { const output = await this.processProvider.exec(command, {}); @@ -142,11 +143,12 @@ export class CliDockerClient implements DockerClient { } public async listContainers(options?: DockerContainersListOptions): Promise { - let command = 'docker ps -a'; + options = options || {}; - if (options && options.format) { - command += ` \"--format=${options.format}\"`; - } + const command = CommandLineBuilder + .create('docker', 'ps', '-a') + .withNamedArg('--format', options.format) + .build(); const output = await this.processProvider.exec(command, {}); @@ -170,37 +172,31 @@ export class CliDockerClient implements DockerClient { } public async removeContainer(containerNameOrId: string, options?: DockerContainerRemoveOptions): Promise { - let command = 'docker rm'; + options = options || {}; - if (options && options.force) { - command += ' --force'; - } - - command += ` ${containerNameOrId}`; + const command = CommandLineBuilder + .create('docker', 'rm') + .withFlagArg('--force', options.force) + .withQuotedArg(containerNameOrId) + .build(); await this.processProvider.exec(command, {}); } public async runContainer(imageTagOrId: string, options?: DockerRunContainerOptions): Promise { - let command = 'docker run -dt -P'; - - if (options && options.containerName) { - command += ` --name ${options.containerName}`; - } - - if (options && options.volumes) { - command += ' ' + options.volumes.map(volume => `-v \"${volume.localPath}:${volume.containerPath}${volume.permissions ? ':' + volume.permissions : ''}\"`).join(' '); - } - - if (options && options.entrypoint) { - command += ` --entrypoint ${options.entrypoint}`; - } - - command += ' ' + imageTagOrId; - - if (options && options.command) { - command += ' ' + options.command; - } + options = options || {}; + + const command = CommandLineBuilder + .create('docker', 'run', '-dt', '-P') + .withNamedArg('--name', options.containerName) + .withKeyValueArgs('-e', options.env) + .withArrayArgs('--env-file', options.envFiles) + .withKeyValueArgs('--label', options.labels) + .withArrayArgs('-v', options.volumes, volume => `${volume.localPath}:${volume.containerPath}${volume.permissions ? ':' + volume.permissions : ''}`) + .withNamedArg('--entrypoint', options.entrypoint) + .withQuotedArg(imageTagOrId) + .withArg(options.command) + .build(); const result = await this.processProvider.exec(command, {}); diff --git a/debugging/coreclr/dockerDebugConfigurationProvider.ts b/debugging/coreclr/dockerDebugConfigurationProvider.ts index 115723d99b..a592df2930 100644 --- a/debugging/coreclr/dockerDebugConfigurationProvider.ts +++ b/debugging/coreclr/dockerDebugConfigurationProvider.ts @@ -7,21 +7,26 @@ import { CancellationToken, DebugConfiguration, DebugConfigurationProvider, Prov import { callWithTelemetryAndErrorHandling } from 'vscode-azureextensionui'; import { PlatformOS } from '../../utils/platform'; import { DebugSessionManager } from './debugSessionManager'; -import { DockerManager, LaunchResult } from './dockerManager'; +import { DockerManager, LaunchBuildOptions, LaunchResult, LaunchRunOptions } from './dockerManager'; import { FileSystemProvider } from './fsProvider'; import { NetCoreProjectProvider } from './netCoreProjectProvider'; import { OSProvider } from './osProvider'; import { Prerequisite } from './prereqManager'; interface DockerDebugBuildOptions { + args?: { [key: string]: string }; context?: string; dockerfile?: string; + labels?: { [key: string]: string }; tag?: string; target?: string; } interface DockerDebugRunOptions { containerName?: string; + env?: { [key: string]: string }; + envFiles?: string[]; + labels?: { [key: string]: string }; os?: PlatformOS; } @@ -46,6 +51,8 @@ interface DockerDebugConfiguration extends DebugConfiguration { } export class DockerDebugConfigurationProvider implements DebugConfigurationProvider { + private static readonly defaultLabels: { [key: string]: string } = { 'com.microsoft.created-by': 'visual-studio-code' }; + constructor( private readonly debugSessionManager: DebugSessionManager, private readonly dockerManager: DockerManager, @@ -99,8 +106,33 @@ export class DockerDebugConfigurationProvider implements DebugConfigurationProvi const resolvedAppProject = DockerDebugConfigurationProvider.resolveFolderPath(appProject, folder); - const context = this.inferContext(folder, resolvedAppFolder, debugConfiguration); + const appName = path.basename(resolvedAppProject, '.csproj'); + + const os = debugConfiguration && debugConfiguration.dockerRun && debugConfiguration.dockerRun.os + ? debugConfiguration.dockerRun.os + : 'Linux'; + + const appOutput = await this.inferAppOutput(debugConfiguration, os, resolvedAppProject); + + const buildOptions = DockerDebugConfigurationProvider.inferBuildOptions(folder, debugConfiguration, appFolder, resolvedAppFolder, appName); + const runOptions = DockerDebugConfigurationProvider.inferRunOptions(folder, debugConfiguration, appName, os); + + const result = await this.dockerManager.prepareForLaunch({ + appFolder: resolvedAppFolder, + appOutput, + build: buildOptions, + run: runOptions + }); + + const configuration = this.createConfiguration(debugConfiguration, appFolder, result); + this.debugSessionManager.startListening(); + + return configuration; + } + + private static inferBuildOptions(folder: WorkspaceFolder, debugConfiguration: DockerDebugConfiguration, appFolder: string, resolvedAppFolder: string, appName: string): LaunchBuildOptions { + const context = DockerDebugConfigurationProvider.inferContext(folder, resolvedAppFolder, debugConfiguration); const resolvedContext = DockerDebugConfigurationProvider.resolveFolderPath(context, folder); let dockerfile = debugConfiguration && debugConfiguration.dockerBuild && debugConfiguration.dockerBuild.dockerfile @@ -109,46 +141,49 @@ export class DockerDebugConfigurationProvider implements DebugConfigurationProvi dockerfile = DockerDebugConfigurationProvider.resolveFolderPath(dockerfile, folder); - const target = debugConfiguration && debugConfiguration.dockerBuild && debugConfiguration.dockerBuild.target - ? debugConfiguration.dockerBuild.target - : 'base'; // CONSIDER: Omit target if not specified, or possibly infer from Dockerfile. + const args = debugConfiguration && debugConfiguration.dockerBuild && debugConfiguration.dockerBuild.args; - const appName = path.basename(resolvedAppProject, '.csproj'); + const labels = (debugConfiguration && debugConfiguration.dockerBuild && debugConfiguration.dockerBuild.labels) + || DockerDebugConfigurationProvider.defaultLabels; const tag = debugConfiguration && debugConfiguration.dockerBuild && debugConfiguration.dockerBuild.tag ? debugConfiguration.dockerBuild.tag : `${appName.toLowerCase()}:dev`; + const target = debugConfiguration && debugConfiguration.dockerBuild && debugConfiguration.dockerBuild.target + ? debugConfiguration.dockerBuild.target + : 'base'; // CONSIDER: Omit target if not specified, or possibly infer from Dockerfile. + + return { + args, + context: resolvedContext, + dockerfile, + labels, + tag, + target + }; + } + + private static inferRunOptions(folder: WorkspaceFolder, debugConfiguration: DockerDebugConfiguration, appName: string, os: PlatformOS): LaunchRunOptions { const containerName = debugConfiguration && debugConfiguration.dockerRun && debugConfiguration.dockerRun.containerName ? debugConfiguration.dockerRun.containerName : `${appName}-dev`; // CONSIDER: Use unique ID instead? - const os = debugConfiguration && debugConfiguration.dockerRun && debugConfiguration.dockerRun.os - ? debugConfiguration.dockerRun.os - : 'Linux'; + const env = debugConfiguration && debugConfiguration.dockerRun && debugConfiguration.dockerRun.env; + const envFiles = debugConfiguration && debugConfiguration.dockerRun && debugConfiguration.dockerRun.envFiles + ? debugConfiguration.dockerRun.envFiles.map(file => DockerDebugConfigurationProvider.resolveFolderPath(file, folder)) + : undefined; - const appOutput = await this.inferAppOutput(debugConfiguration, os, resolvedAppProject); + const labels = (debugConfiguration && debugConfiguration.dockerRun && debugConfiguration.dockerRun.labels) + || DockerDebugConfigurationProvider.defaultLabels; - const result = await this.dockerManager.prepareForLaunch({ - appFolder: resolvedAppFolder, - appOutput, - build: { - context: resolvedContext, - dockerfile, - tag, - target - }, - run: { - containerName, - os, - } - }); - - const configuration = this.createConfiguration(debugConfiguration, appFolder, result); - - this.debugSessionManager.startListening(); - - return configuration; + return { + containerName, + env, + envFiles, + labels, + os, + }; } private inferAppFolder(folder: WorkspaceFolder, configuration: DockerDebugConfiguration): string { @@ -194,7 +229,7 @@ export class DockerDebugConfigurationProvider implements DebugConfigurationProvi throw new Error('Unable to infer the application project file. Set either the `appFolder` or `appProject` property in the Docker debug configuration.'); } - private inferContext(folder: WorkspaceFolder, resolvedAppFolder: string, configuration: DockerDebugConfiguration): string { + private static inferContext(folder: WorkspaceFolder, resolvedAppFolder: string, configuration: DockerDebugConfiguration): string { return configuration && configuration.dockerBuild && configuration.dockerBuild.context ? configuration.dockerBuild.context : path.normalize(resolvedAppFolder) === path.normalize(folder.uri.fsPath) diff --git a/debugging/coreclr/dockerManager.ts b/debugging/coreclr/dockerManager.ts index 169834a5cb..00d7f98904 100644 --- a/debugging/coreclr/dockerManager.ts +++ b/debugging/coreclr/dockerManager.ts @@ -2,6 +2,7 @@ * Copyright (C) Microsoft Corporation. All rights reserved. *--------------------------------------------------------*/ +import deepEqual = require('deep-equal'); import * as path from 'path'; import { Memento } from 'vscode'; import { PlatformOS } from '../../utils/platform'; @@ -31,11 +32,14 @@ export type DockerManagerRunContainerOptions type Omit = Pick>; +export type LaunchBuildOptions = Omit; +export type LaunchRunOptions = Omit; + export type LaunchOptions = { appFolder: string; appOutput: string; - build: Omit; - run: Omit; + build: LaunchBuildOptions; + run: LaunchRunOptions; }; export type LaunchResult = { @@ -70,6 +74,46 @@ export interface DockerManager { export const MacNuGetPackageFallbackFolderPath = '/usr/local/share/dotnet/sdk/NuGetFallbackFolder'; +function compareProperty(obj1: T | undefined, obj2: T | undefined, getter: (obj: T) => (U | undefined)): boolean { + const prop1 = obj1 ? getter(obj1) : undefined; + const prop2 = obj2 ? getter(obj2) : undefined; + + return prop1 === prop2; +} + +function compareDictionary(obj1: T | undefined, obj2: T | undefined, getter: (obj: T) => ({ [key: string]: string } | undefined)): boolean { + const dict1 = (obj1 ? getter(obj1) : {}) || {}; + const dict2 = (obj2 ? getter(obj2) : {}) || {}; + + return deepEqual(dict1, dict2); +} + +export function compareBuildImageOptions(options1: DockerBuildImageOptions | undefined, options2: DockerBuildImageOptions | undefined): boolean { + // NOTE: We do not compare options.dockerfile as it (i.e. the name itself) has no impact on the built image. + + if (!compareProperty(options1, options2, options => options.context)) { + return false; + } + + if (!compareDictionary(options1, options2, options => options.args)) { + return false; + } + + if (!compareProperty(options1, options2, options => options.tag)) { + return false; + } + + if (!compareProperty(options1, options2, options => options.target)) { + return false; + } + + if (!compareDictionary(options1, options2, options => options.labels)) { + return false; + } + + return true; +} + export class DefaultDockerManager implements DockerManager { private static readonly DebugContainersKey: string = 'DefaultDockerManager.debugContainers'; @@ -102,11 +146,7 @@ export class DefaultDockerManager implements DockerManager { if (buildMetadata && buildMetadata.imageId) { const imageObject = await this.dockerClient.inspectObject(buildMetadata.imageId); - if (imageObject - && buildMetadata.options - && buildMetadata.options.context === options.context - && buildMetadata.options.tag === options.tag - && buildMetadata.options.target === options.target) { + if (imageObject && compareBuildImageOptions(buildMetadata.options, options)) { const currentDockerfileHash = await dockerfileHasher.value; const currentDockerIgnoreHash = await dockerIgnoreHasher.value; @@ -174,21 +214,15 @@ export class DefaultDockerManager implements DockerManager { command, containerName: options.containerName, entrypoint, + env: options.env, + envFiles: options.envFiles, + labels: options.labels, volumes }); }, id => `Container ${this.dockerClient.trimId(id)} started.`, err => `Unable to start container: ${err}`); - const cache = await this.appCacheFactory.getStorage(options.appFolder); - - await cache.update( - 'run', - { - containerId, - options - }); - return containerId; } diff --git a/package.json b/package.json index 38782c214d..2f42f0962e 100644 --- a/package.json +++ b/package.json @@ -304,6 +304,13 @@ "dockerBuild": { "description": "Options for building the Docker image used for debugging.", "properties": { + "args": { + "type": "object", + "description": "Build arguments applied to the Docker image used for debugging.", + "additionalProperties": { + "type": "string" + } + }, "context": { "type": "string", "description": "Path to the Docker build context." @@ -312,6 +319,13 @@ "type": "string", "description": "Path to the Dockerfile used for the build." }, + "labels": { + "type": "object", + "description": "Labels applied to the Docker image used for debugging.", + "additionalProperties": { + "type": "string" + } + }, "tag": { "type": "string", "description": "Tag applied to the Docker image used for debugging." @@ -328,6 +342,27 @@ "containerName": { "type": "string", "description": "Name of the container used for debugging." + }, + "env": { + "type": "object", + "description": "Environment variables applied to the Docker container used for debugging.", + "additionalProperties": { + "type": "string" + } + }, + "envFiles": { + "type": "array", + "description": "Files of environment variables read in and applied to the Docker container used for debugging.", + "items": { + "type": "string" + } + }, + "labels": { + "type": "object", + "description": "Labels applied to the Docker container used for debugging.", + "additionalProperties": { + "type": "string" + } } } } @@ -743,6 +778,7 @@ ], "devDependencies": { "@types/adm-zip": "^0.4.31", + "@types/deep-equal": "^1.0.1", "@types/dockerode": "^2.5.5", "@types/fs-extra": "^5.0.4", "@types/glob": "5.0.35", @@ -766,6 +802,7 @@ "azure-arm-containerregistry": "^2.3.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", "dockerode": "^2.5.1", "fs-extra": "^6.0.1", diff --git a/test/debugging/coreclr/commandLineBuilder.test.ts b/test/debugging/coreclr/commandLineBuilder.test.ts new file mode 100644 index 0000000000..f7f197cf5d --- /dev/null +++ b/test/debugging/coreclr/commandLineBuilder.test.ts @@ -0,0 +1,65 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +import * as assert from 'assert'; +import CommandLineBuilder from '../../../debugging/coreclr/commandLineBuilder'; + +suite('debugging/coreclr/CommandLineBuilder', () => { + function testBuilder(name: string, builderInitializer: (CommandLineBuilder) => CommandLineBuilder, expected: string, message: string) { + test(name, () => { + let builder = CommandLineBuilder.create(); + + builder = builderInitializer(builder); + + assert.equal(builder.build(), expected, message); + }); + } + + suite('create', () => { + test('No args', () => assert.equal(CommandLineBuilder.create().build(), '', 'No arguments should return an empty command line.')); + test('With args', () => assert.equal(CommandLineBuilder.create('--arg1', '--arg2').build(), '--arg1 --arg2', 'The command line should contain the arguments.')); + test('With factory', () => assert.equal(CommandLineBuilder.create(() => '--arg1').build(), '--arg1', 'The command line should contain the argument.')); + test('With undefined', () => assert.equal(CommandLineBuilder.create(undefined).build(), '', 'No arguments should return an empty command line.')); + }); + + suite('withArg', () => { + testBuilder('With value', builder => builder.withArg('value'), 'value', 'The command line should contain the value.'); + testBuilder('With undefined', builder => builder.withArg(undefined), '', 'The command line should not contain the value.'); + }); + + suite('withArgFactory', () => { + testBuilder('With factory', builder => builder.withArgFactory(() => 'value'), 'value', 'The command line should contain the value.'); + testBuilder('With undefined', builder => builder.withArgFactory(undefined), '', 'The command line should not contain the value.'); + }); + + suite('withArrayArgs', () => { + testBuilder('With values', builder => builder.withArrayArgs('--arg', ['value1', 'value2']), '--arg "value1" --arg "value2"', 'The command line should contain the values.'); + testBuilder('With value', builder => builder.withArrayArgs('--arg', ['value1']), '--arg "value1"', 'The command line should contain the value.'); + testBuilder('With empty', builder => builder.withArrayArgs('--arg', []), '', 'The command line should not contain the value.'); + testBuilder('With undefined', builder => builder.withArrayArgs('--arg', undefined), '', 'The command line should not contain the value.'); + }); + + suite('withFlagArg', () => { + testBuilder('With true', builder => builder.withFlagArg('--arg', true), '--arg', 'The command line should contain the value.'); + testBuilder('With false', builder => builder.withFlagArg('--arg', false), '', 'The command line should not contain the value.'); + testBuilder('With undefined', builder => builder.withFlagArg('--arg', undefined), '', 'The command line should not contain the value.'); + }); + + suite('withKeyValueArgs', () => { + testBuilder('With values', builder => builder.withKeyValueArgs('--arg', { key1: 'value1', key2: 'value2' }), '--arg "key1=value1" --arg "key2=value2"', 'The command line should contain the values.'); + testBuilder('With value', builder => builder.withKeyValueArgs('--arg', { key1: 'value1' }), '--arg "key1=value1"', 'The command line should contain the value.'); + testBuilder('With empty', builder => builder.withKeyValueArgs('--arg', {}), '', 'The command line should not contain the value.'); + testBuilder('With undefined', builder => builder.withKeyValueArgs('--arg', undefined), '', 'The command line should not contain the value.'); + }); + + suite('withNamedArg', () => { + testBuilder('With value', builder => builder.withNamedArg('--arg', 'value'), '--arg "value"', 'The command line should contain the value.'); + testBuilder('With undefined', builder => builder.withNamedArg('--arg', undefined), '', 'The command line should not contain the value.'); + }); + + suite('withQuotedArg', () => { + testBuilder('With value', builder => builder.withQuotedArg('value'), '"value"', 'The command line should contain the value.'); + testBuilder('With undefined', builder => builder.withQuotedArg(undefined), '', 'The command line should not contain the value.'); + }); +}); diff --git a/test/debugging/coreclr/dockerManager.test.ts b/test/debugging/coreclr/dockerManager.test.ts new file mode 100644 index 0000000000..7ab1660e62 --- /dev/null +++ b/test/debugging/coreclr/dockerManager.test.ts @@ -0,0 +1,146 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +import assert = require("assert"); +import { DockerBuildImageOptions } from "../../../debugging/coreclr/dockerClient"; +import { compareBuildImageOptions } from "../../../debugging/coreclr/dockerManager"; + +suite('debugging/coreclr/dockerManager', () => { + suite('compareBuildImageOptions', () => { + function testComparison(name: string, options1: DockerBuildImageOptions | undefined, options2: DockerBuildImageOptions | undefined, expected: boolean, message: string) { + test(name, () => assert.equal(compareBuildImageOptions(options1, options2), expected, message)); + } + + function testComparisonOfProperty(property: U, included: boolean = true) { + suite(property, () => { + test('Options undefined', () => { + const options1: DockerBuildImageOptions = {}; + + options1[property] = undefined; + + const options2: DockerBuildImageOptions = undefined; + + assert.equal(compareBuildImageOptions(options1, options2), true, 'Property undefined equates to options undefined.'); + }); + + test('Property unspecified', () => { + const options1: DockerBuildImageOptions = {}; + + options1[property] = undefined; + + const options2: DockerBuildImageOptions = {}; + + assert.equal(compareBuildImageOptions(options1, options2), true, 'Property undefined equates to property unspecified.'); + }); + + test('Properties equal', () => { + const options1: DockerBuildImageOptions = {}; + + options1[property] = 'value'; + + const options2: DockerBuildImageOptions = {}; + + options2[property] = 'value'; + + assert.equal(compareBuildImageOptions(options1, options2), true, 'Equal properties should be equal.'); + }); + + test('Properties different', () => { + const options1: DockerBuildImageOptions = {}; + + options1[property] = 'value1'; + + const options2: DockerBuildImageOptions = {}; + + options2[property] = 'value2'; + + assert.equal(compareBuildImageOptions(options1, options2), !included, 'Different properties should be unequal.'); + }); + }); + } + + function testComparisonOfDictionary(property: U, included: boolean = true) { + suite(property, () => { + test('Options undefined', () => { + const options1: DockerBuildImageOptions = {}; + + options1[property] = undefined; + + const options2: DockerBuildImageOptions = undefined; + + assert.equal(compareBuildImageOptions(options1, options2), true, 'Dictionary undefined equates to options undefined.'); + }); + + test('Dictionary unspecified', () => { + const options1: DockerBuildImageOptions = {}; + + options1[property] = undefined; + + const options2: DockerBuildImageOptions = {}; + + assert.equal(compareBuildImageOptions(options1, options2), true, 'Dictionary undefined equates to dictionary unspecified.'); + }); + + test('Dictionary empty', () => { + const options1: DockerBuildImageOptions = {}; + + options1[property] = {}; + + const options2: DockerBuildImageOptions = {}; + + options2[property] = {}; + + assert.equal(compareBuildImageOptions(options1, options2), true, 'Empty dictionaries should be equal.'); + }); + + test('Dictionary equal', () => { + const options1: DockerBuildImageOptions = {}; + + options1[property] = { arg1: 'value1', arg2: 'value2' }; + + const options2: DockerBuildImageOptions = {}; + + options2[property] = { arg1: 'value1', arg2: 'value2' }; + + assert.equal(compareBuildImageOptions(options1, options2), true, 'Equal dictionaries should be equal.'); + }); + + test('Dictionary different keys', () => { + const options1: DockerBuildImageOptions = {}; + + options1[property] = { arg1: 'value1', arg2: 'value2' }; + + const options2: DockerBuildImageOptions = {}; + + options2[property] = { arg2: 'value2', arg3: 'value3' }; + + assert.equal(compareBuildImageOptions(options1, options2), !included, 'Different properties should be unequal.'); + }); + + test('Dictionary different values', () => { + const options1: DockerBuildImageOptions = {}; + + options1[property] = { arg1: 'value1', arg2: 'value2' }; + + const options2: DockerBuildImageOptions = {}; + + options2[property] = { arg1: 'value1', arg2: 'value3' }; + + assert.equal(compareBuildImageOptions(options1, options2), !included, 'Different properties should be unequal.'); + }); + }); + } + + testComparison('Both undefined', undefined, undefined, true, 'Both being undefined are considered equal.'); + testComparison('One undefined, one empty', undefined, {}, true, 'Undefined and empty are considered equal.'); + testComparison('Both empty', {}, {}, true, 'Both empty are considered equal.'); + + testComparisonOfProperty('context'); + testComparisonOfProperty('dockerfile', false); + testComparisonOfProperty('tag'); + + testComparisonOfDictionary('args'); + testComparisonOfDictionary('labels'); + }); +}); diff --git a/test/debugging/coreclr/lineSplitter.test.ts b/test/debugging/coreclr/lineSplitter.test.ts index cf88e43dca..15443396f6 100644 --- a/test/debugging/coreclr/lineSplitter.test.ts +++ b/test/debugging/coreclr/lineSplitter.test.ts @@ -1,45 +1,45 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + import * as assert from 'assert'; import LineSplitter from '../../../debugging/coreclr/lineSplitter'; -suite('debugging', () => { - suite('coreclr', () => { - suite('LineSplitter', () => { - const testCase = (name: string, input: string | string[], output: string[]) => { - test(name, () => { - const splitter = new LineSplitter(); - - const lines: string[] = []; - - splitter.onLine(line => lines.push(line)); - - if (typeof input === 'string') { - splitter.write(input); - } else { - for (let i = 0; i < input.length; i++) { - splitter.write(input[i]); - } - } - - splitter.close(); - - assert.deepEqual(lines, output, 'The number or contents of the lines are not the same.'); - }); - }; - - testCase('Empty string', '', []); - testCase('Only LF', '\n', ['']); - testCase('CR & LF', '\r\n', ['']); - testCase('Multiple LFs', '\n\n', ['', '']); - testCase('Multiple CR & LFs', '\r\n\r\n', ['', '']); - testCase('Single line', 'line one', ['line one']); - testCase('Leading LF', '\nline two', ['', 'line two']); - testCase('Leading CR & LF', '\r\nline two', ['', 'line two']); - testCase('Trailing LF', 'line one\n', ['line one']); - testCase('Trailing CR & LF', 'line one\r\n', ['line one']); - testCase('Multiple lines with LF', 'line one\nline two', ['line one', 'line two']); - testCase('Multiple lines with CR & LF', 'line one\r\nline two', ['line one', 'line two']); - testCase('CR & LF spanning writes', ['line one\r', '\nline two'], ['line one', 'line two']); +suite('debugging/coreclr/LineSplitter', () => { + const testCase = (name: string, input: string | string[], output: string[]) => { + test(name, () => { + const splitter = new LineSplitter(); + + const lines: string[] = []; + + splitter.onLine(line => lines.push(line)); + + if (typeof input === 'string') { + splitter.write(input); + } else { + for (let i = 0; i < input.length; i++) { + splitter.write(input[i]); + } + } + + splitter.close(); + + assert.deepEqual(lines, output, 'The number or contents of the lines are not the same.'); }); - }); + }; + + testCase('Empty string', '', []); + testCase('Only LF', '\n', ['']); + testCase('CR & LF', '\r\n', ['']); + testCase('Multiple LFs', '\n\n', ['', '']); + testCase('Multiple CR & LFs', '\r\n\r\n', ['', '']); + testCase('Single line', 'line one', ['line one']); + testCase('Leading LF', '\nline two', ['', 'line two']); + testCase('Leading CR & LF', '\r\nline two', ['', 'line two']); + testCase('Trailing LF', 'line one\n', ['line one']); + testCase('Trailing CR & LF', 'line one\r\n', ['line one']); + testCase('Multiple lines with LF', 'line one\nline two', ['line one', 'line two']); + testCase('Multiple lines with CR & LF', 'line one\r\nline two', ['line one', 'line two']); + testCase('CR & LF spanning writes', ['line one\r', '\nline two'], ['line one', 'line two']); }); diff --git a/test/debugging/coreclr/prereqManager.test.ts b/test/debugging/coreclr/prereqManager.test.ts index 0e0e697715..8fba7789d7 100644 --- a/test/debugging/coreclr/prereqManager.test.ts +++ b/test/debugging/coreclr/prereqManager.test.ts @@ -12,204 +12,200 @@ import { PlatformOS } from '../../../utils/platform'; import { DockerClient } from '../../../debugging/coreclr/dockerClient'; import { DotNetClient } from '../../../debugging/coreclr/dotNetClient'; -suite('debugging', () => { - suite('coreclr', () => { - suite('prereqManager', () => { - suite('DockerDaemonIsLinuxPrerequisite', () => { - const generateTest = (name: string, result: boolean, os: PlatformOS) => { - test(name, async () => { - let gotVersion = false; +suite('debugging/coreclr/prereqManager', () => { + suite('DockerDaemonIsLinuxPrerequisite', () => { + const generateTest = (name: string, result: boolean, os: PlatformOS) => { + test(name, async () => { + let gotVersion = false; - const dockerClient = { - getVersion: (options) => { - gotVersion = true; + const dockerClient = { + getVersion: (options) => { + gotVersion = true; - assert.deepEqual(options, { format: '{{json .Server.Os}}' }, 'The server OS should be requested, in JSON format.'); + assert.deepEqual(options, { format: '{{json .Server.Os}}' }, 'The server OS should be requested, in JSON format.'); - return Promise.resolve(`"${os.toLowerCase()}"`); - } - }; + return Promise.resolve(`"${os.toLowerCase()}"`); + } + }; - let shown = false; + let shown = false; - const showErrorMessage = (message: string, ...items: vscode.MessageItem[]): Thenable => { - shown = true; - return Promise.resolve(undefined); - }; + const showErrorMessage = (message: string, ...items: vscode.MessageItem[]): Thenable => { + shown = true; + return Promise.resolve(undefined); + }; - const prerequisite = new DockerDaemonIsLinuxPrerequisite(dockerClient, showErrorMessage); + const prerequisite = new DockerDaemonIsLinuxPrerequisite(dockerClient, showErrorMessage); - const prereqResult = await prerequisite.checkPrerequisite(); + const prereqResult = await prerequisite.checkPrerequisite(); - assert.equal(gotVersion, true, 'The Docker version should have been requested.'); + assert.equal(gotVersion, true, 'The Docker version should have been requested.'); - assert.equal(prereqResult, result, 'The prerequisite should return `false`.'); - assert.equal(shown, !result, `An error message should ${result ? 'not ' : ''} have been shown.`); - }); - } - - generateTest('Linux daemon', true, 'Linux'); - generateTest('Windows daemon', false, 'Windows'); + assert.equal(prereqResult, result, 'The prerequisite should return `false`.'); + assert.equal(shown, !result, `An error message should ${result ? 'not ' : ''} have been shown.`); }); + } - suite('DotNetSdkInstalledPrerequisite', () => { - test('Installed', async () => { - const msBuildClient = { - getVersion: () => Promise.resolve('2.1.402') - }; + generateTest('Linux daemon', true, 'Linux'); + generateTest('Windows daemon', false, 'Windows'); + }); - let shown = false; + suite('DotNetSdkInstalledPrerequisite', () => { + test('Installed', async () => { + const msBuildClient = { + getVersion: () => Promise.resolve('2.1.402') + }; - const showErrorMessage = (message: string, ...items: vscode.MessageItem[]): Thenable => { - shown = true; - return Promise.resolve(undefined); - }; + let shown = false; - const prerequisite = new DotNetSdkInstalledPrerequisite(msBuildClient, showErrorMessage); + const showErrorMessage = (message: string, ...items: vscode.MessageItem[]): Thenable => { + shown = true; + return Promise.resolve(undefined); + }; - const prereqResult = await prerequisite.checkPrerequisite(); + const prerequisite = new DotNetSdkInstalledPrerequisite(msBuildClient, showErrorMessage); - assert.equal(prereqResult, true, 'The prerequisite should pass if the SDK is installed.'); - assert.equal(shown, false, 'No error should be shown.'); - }); + const prereqResult = await prerequisite.checkPrerequisite(); - test('Not installed', async () => { - const msBuildClient = { - getVersion: () => Promise.resolve(undefined) - }; + assert.equal(prereqResult, true, 'The prerequisite should pass if the SDK is installed.'); + assert.equal(shown, false, 'No error should be shown.'); + }); - let shown = false; + test('Not installed', async () => { + const msBuildClient = { + getVersion: () => Promise.resolve(undefined) + }; - const showErrorMessage = (message: string, ...items: vscode.MessageItem[]): Thenable => { - shown = true; - return Promise.resolve(undefined); - }; + let shown = false; - const prerequisite = new DotNetSdkInstalledPrerequisite(msBuildClient, showErrorMessage); + const showErrorMessage = (message: string, ...items: vscode.MessageItem[]): Thenable => { + shown = true; + return Promise.resolve(undefined); + }; - const prereqResult = await prerequisite.checkPrerequisite(); + const prerequisite = new DotNetSdkInstalledPrerequisite(msBuildClient, showErrorMessage); - assert.equal(prereqResult, false, 'The prerequisite should fail if no SDK is installed.'); - assert.equal(shown, true, 'An error should be shown.'); - }); - }); + const prereqResult = await prerequisite.checkPrerequisite(); - suite('LinuxUserInDockerGroupPrerequisite', () => { - const generateTest = (name: string, result: boolean, os: PlatformOS, isMac?: boolean, inGroup?: boolean) => { - test(name, async () => { - const osProvider = { - os, - isMac - } + assert.equal(prereqResult, false, 'The prerequisite should fail if no SDK is installed.'); + assert.equal(shown, true, 'An error should be shown.'); + }); + }); + + suite('LinuxUserInDockerGroupPrerequisite', () => { + const generateTest = (name: string, result: boolean, os: PlatformOS, isMac?: boolean, inGroup?: boolean) => { + test(name, async () => { + const osProvider = { + os, + isMac + } - let processProvider = {}; - let listed = false; + let processProvider = {}; + let listed = false; - if (os === 'Linux' && !isMac) { - processProvider = { - exec: (command: string, _) => { - listed = true; + if (os === 'Linux' && !isMac) { + processProvider = { + exec: (command: string, _) => { + listed = true; - assert.equal(command, 'id -Gn', 'The prerequisite should list the user\'s groups.') + assert.equal(command, 'id -Gn', 'The prerequisite should list the user\'s groups.') - const groups = inGroup ? 'groupA docker groupB' : 'groupA groupB'; + const groups = inGroup ? 'groupA docker groupB' : 'groupA groupB'; - return Promise.resolve({ stdout: groups, stderr: ''}); - } - }; + return Promise.resolve({ stdout: groups, stderr: ''}); } + }; + } - let shown = false; - - const showErrorMessage = (message: string, ...items: vscode.MessageItem[]): Thenable => { - shown = true; - return Promise.resolve(undefined); - }; + let shown = false; - const prerequisite = new LinuxUserInDockerGroupPrerequisite(osProvider, processProvider, showErrorMessage); + const showErrorMessage = (message: string, ...items: vscode.MessageItem[]): Thenable => { + shown = true; + return Promise.resolve(undefined); + }; - const prereqResult = await prerequisite.checkPrerequisite(); + const prerequisite = new LinuxUserInDockerGroupPrerequisite(osProvider, processProvider, showErrorMessage); - if (os === 'Linux' && !isMac) { - assert.equal(listed, true, 'The user\'s groups should have been listed.'); - } + const prereqResult = await prerequisite.checkPrerequisite(); - assert.equal(prereqResult, result, 'The prerequisite should return `false`.'); - assert.equal(shown, !result, `An error message should ${result ? 'not ' : ''} have been shown.`); - }); - }; + if (os === 'Linux' && !isMac) { + assert.equal(listed, true, 'The user\'s groups should have been listed.'); + } - generateTest('Windows: No-op', true, 'Windows'); - generateTest('Mac: No-op', true, 'Linux', true); - generateTest('Linux: In group', true, 'Linux', false, true); - generateTest('Linux: Not in group', false, 'Linux', false, false); + assert.equal(prereqResult, result, 'The prerequisite should return `false`.'); + assert.equal(shown, !result, `An error message should ${result ? 'not ' : ''} have been shown.`); }); + }; - suite('MacNuGetFallbackFolderSharedPrerequisite', () => { - const generateTest = (name: string, fileContents: string | undefined, result: boolean) => { - const settingsPath = '/Users/User/Library/Group Containers/group.com.docker/settings.json'; + generateTest('Windows: No-op', true, 'Windows'); + generateTest('Mac: No-op', true, 'Linux', true); + generateTest('Linux: In group', true, 'Linux', false, true); + generateTest('Linux: Not in group', false, 'Linux', false, false); + }); - test(name, async () => { - const fsProvider = { - fileExists: (path: string) => { - assert.equal(settingsPath, path, 'The prerequisite should check for the settings file in the user\'s home directory.'); + suite('MacNuGetFallbackFolderSharedPrerequisite', () => { + const generateTest = (name: string, fileContents: string | undefined, result: boolean) => { + const settingsPath = '/Users/User/Library/Group Containers/group.com.docker/settings.json'; - return Promise.resolve(fileContents !== undefined); - }, - readFile: (path: string) => { - if (fileContents === undefined) { - assert.fail('The prerequisite should not attempt to read a file that does not exist.'); - } + test(name, async () => { + const fsProvider = { + fileExists: (path: string) => { + assert.equal(settingsPath, path, 'The prerequisite should check for the settings file in the user\'s home directory.'); - assert.equal(settingsPath, path, 'The prerequisite should read the settings file in the user\'s home directory.'); + return Promise.resolve(fileContents !== undefined); + }, + readFile: (path: string) => { + if (fileContents === undefined) { + assert.fail('The prerequisite should not attempt to read a file that does not exist.'); + } - return Promise.resolve(fileContents); - } - }; + assert.equal(settingsPath, path, 'The prerequisite should read the settings file in the user\'s home directory.'); - const osProvider = { - homedir: '/Users/User', - isMac: true - }; + return Promise.resolve(fileContents); + } + }; - let shown = false; + const osProvider = { + homedir: '/Users/User', + isMac: true + }; + + let shown = false; - const showErrorMessage = (message: string, ...items: vscode.MessageItem[]): Thenable => { - shown = true; - return Promise.resolve(undefined); - }; + const showErrorMessage = (message: string, ...items: vscode.MessageItem[]): Thenable => { + shown = true; + return Promise.resolve(undefined); + }; - const prereq = new MacNuGetFallbackFolderSharedPrerequisite(fsProvider, osProvider, showErrorMessage); + const prereq = new MacNuGetFallbackFolderSharedPrerequisite(fsProvider, osProvider, showErrorMessage); - const prereqResult = await prereq.checkPrerequisite(); + const prereqResult = await prereq.checkPrerequisite(); - assert.equal(prereqResult, result, 'The prerequisite should return `false`.'); - assert.equal(shown, !result, `An error message should ${result ? 'not ' : ''} have been shown.`); - }); - } + assert.equal(prereqResult, result, 'The prerequisite should return `false`.'); + assert.equal(shown, !result, `An error message should ${result ? 'not ' : ''} have been shown.`); + }); + } - generateTest('Mac: no Docker settings file', undefined, true); - generateTest('Mac: no shared folders in Docker settings file', '{}', true); - generateTest('Mac: no NuGetFallbackFolder in Docker settings file', '{ "filesharingDirectories": [] }', false); - generateTest('Mac: NuGetFallbackFolder in Docker settings file', '{ "filesharingDirectories": [ "/usr/local/share/dotnet/sdk/NuGetFallbackFolder" ] }', true); + generateTest('Mac: no Docker settings file', undefined, true); + generateTest('Mac: no shared folders in Docker settings file', '{}', true); + generateTest('Mac: no NuGetFallbackFolder in Docker settings file', '{ "filesharingDirectories": [] }', false); + generateTest('Mac: NuGetFallbackFolder in Docker settings file', '{ "filesharingDirectories": [ "/usr/local/share/dotnet/sdk/NuGetFallbackFolder" ] }', true); - test('Non-Mac: No-op', async () => { - const osProvider = { - isMac: false - }; + test('Non-Mac: No-op', async () => { + const osProvider = { + isMac: false + }; - const showErrorMessage = (message: string, ...items: vscode.MessageItem[]): Thenable => { - assert.fail('Should not be called on non-Mac.'); - return Promise.resolve(undefined); - }; + const showErrorMessage = (message: string, ...items: vscode.MessageItem[]): Thenable => { + assert.fail('Should not be called on non-Mac.'); + return Promise.resolve(undefined); + }; - const prereq = new MacNuGetFallbackFolderSharedPrerequisite({}, osProvider, showErrorMessage); + const prereq = new MacNuGetFallbackFolderSharedPrerequisite({}, osProvider, showErrorMessage); - const result = await prereq.checkPrerequisite(); + const result = await prereq.checkPrerequisite(); - assert.equal(true, result, 'The prerequisite should return `true` on non-Mac.'); - }); - }); + assert.equal(true, result, 'The prerequisite should return `true` on non-Mac.'); }); }); }); diff --git a/thirdpartynotices.txt b/thirdpartynotices.txt index 8a3820cda9..8d30120152 100644 --- a/thirdpartynotices.txt +++ b/thirdpartynotices.txt @@ -397,3 +397,24 @@ 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. +12. deep-equal (https://github.com/substack/node-deep-equal) + +This software is released under the MIT license: + +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. + From 0095ce10e53de124a4867ef4626045bc47913cae Mon Sep 17 00:00:00 2001 From: "Stephen Weatherford (MSFT)" Date: Fri, 26 Oct 2018 11:28:05 -0700 Subject: [PATCH 30/36] Fix title casing of configure command (#593) --- README.md | 2 +- package.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 620285d8b5..737ba3eb2f 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ The Docker extension makes it easy to build, manage and deploy containerized app ## Generating Docker Files -Press `F1` and search for `Docker: Add Docker files to Workspace` to generate `Dockerfile`, `docker-compose.yml`, `docker-compose.debug.yml`, and `.dockerignore` files for your workspace type: +Press `F1` and search for `Docker: Add Docker Files to Workspace` to generate `Dockerfile`, `docker-compose.yml`, `docker-compose.debug.yml`, and `.dockerignore` files for your workspace type: ![dockerfile](images/generateFiles.gif) diff --git a/package.json b/package.json index 2f42f0962e..2932018ad5 100644 --- a/package.json +++ b/package.json @@ -557,13 +557,13 @@ "commands": [ { "command": "vscode-docker.configure", - "title": "Add Docker files to workspace", + "title": "Add Docker Files to Workspace", "description": "Add Dockerfile, docker-compose.yml files", "category": "Docker" }, { "command": "vscode-docker.api.configure", - "title": "Add Docker files to Workspace (API)" + "title": "Add Docker Files to Workspace (API)" }, { "command": "vscode-docker.image.build", From 8a79d416da8133f2b3762bf22b34d314adb57e0e Mon Sep 17 00:00:00 2001 From: wachino Date: Sat, 27 Oct 2018 21:37:33 +0200 Subject: [PATCH 31/36] Add jobs to pipeline (#596) --- .travis.yml | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4d512b80f8..56cd6452c1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,24 +6,33 @@ services: - docker node_js: - - 'stable' + - "stable" + - "7" before_install: - if [ $TRAVIS_OS_NAME == "linux" ]; then - export CXX="g++-4.9" CC="gcc-4.9" DISPLAY=:99.0; - sh -e /etc/init.d/xvfb start; - sleep 3; + export CXX="g++-4.9" CC="gcc-4.9" DISPLAY=:99.0; + sh -e /etc/init.d/xvfb start; + sleep 3; fi -install: - - npm install +jobs: + include: + - stage: "build" + name: Build + script: npm run build -script: - - npm run build - - gulp package - - gulp upload-vsix - - npm run lint - - npm test + - stage: "gulp package" + name: Gulp package + script: gulp package + + - stage: "gulp upload-vsix" + name: Gulp upload vsix + script: gulp upload-vsix + + - stage: "lint" + name: Check lint syntax + script: npm run lint notifications: email: From 5acf973679c0ffd8258fb3ff1aa9f98629eb34e7 Mon Sep 17 00:00:00 2001 From: "Stephen Weatherford (MSFT)" Date: Thu, 1 Nov 2018 17:40:21 -0700 Subject: [PATCH 32/36] Remove vscode telemetry from deps (#605) --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 2932018ad5..a9a8f864a2 100644 --- a/package.json +++ b/package.json @@ -814,7 +814,6 @@ "request-promise-native": "^1.0.5", "semver": "^5.5.1", "vscode-azureextensionui": "^0.17.0", - "vscode-extension-telemetry": "^0.0.22", "vscode-languageclient": "^4.4.0" } } From 405f6e55135684196ca143f782306dc9be295590 Mon Sep 17 00:00:00 2001 From: Eric Jizba Date: Fri, 2 Nov 2018 09:30:06 -0700 Subject: [PATCH 33/36] Switch from travis ci to azure pipelines (#607) --- .azure-pipelines/common/build.yml | 9 +++ .azure-pipelines/common/lint.yml | 9 +++ .../common/publish-test-results.yml | 7 ++ .azure-pipelines/common/publish-vsix.yml | 18 +++++ .azure-pipelines/common/test.yml | 11 +++ .azure-pipelines/linux/test-linux.yml | 18 +++++ .azure-pipelines/linux/xvfb.init | 56 +++++++++++++ .azure-pipelines/main.yml | 27 +++++++ .gitignore | 1 + .travis.yml | 40 ---------- .vscodeignore | 2 + README.md | 2 +- gulpfile.js | 80 +++++++++---------- package.json | 13 +-- 14 files changed, 202 insertions(+), 91 deletions(-) create mode 100644 .azure-pipelines/common/build.yml create mode 100644 .azure-pipelines/common/lint.yml create mode 100644 .azure-pipelines/common/publish-test-results.yml create mode 100644 .azure-pipelines/common/publish-vsix.yml create mode 100644 .azure-pipelines/common/test.yml create mode 100644 .azure-pipelines/linux/test-linux.yml create mode 100644 .azure-pipelines/linux/xvfb.init create mode 100644 .azure-pipelines/main.yml delete mode 100644 .travis.yml diff --git a/.azure-pipelines/common/build.yml b/.azure-pipelines/common/build.yml new file mode 100644 index 0000000000..8f29e12086 --- /dev/null +++ b/.azure-pipelines/common/build.yml @@ -0,0 +1,9 @@ +steps: +- task: Npm@1 + displayName: 'npm install' + +- task: Npm@1 + displayName: 'Build' + inputs: + command: custom + customCommand: run build diff --git a/.azure-pipelines/common/lint.yml b/.azure-pipelines/common/lint.yml new file mode 100644 index 0000000000..8d19be60e8 --- /dev/null +++ b/.azure-pipelines/common/lint.yml @@ -0,0 +1,9 @@ +steps: +- task: Npm@1 + displayName: 'Lint' + inputs: + command: custom + customCommand: run lint + +- task: ComponentGovernanceComponentDetection@0 + displayName: 'Component Detection' diff --git a/.azure-pipelines/common/publish-test-results.yml b/.azure-pipelines/common/publish-test-results.yml new file mode 100644 index 0000000000..338a8ba2a3 --- /dev/null +++ b/.azure-pipelines/common/publish-test-results.yml @@ -0,0 +1,7 @@ +steps: +- task: PublishTestResults@2 + displayName: 'Publish Test Results' + inputs: + testResultsFiles: '*-results.xml' + testRunTitle: '$(Agent.OS)' + condition: succeededOrFailed() diff --git a/.azure-pipelines/common/publish-vsix.yml b/.azure-pipelines/common/publish-vsix.yml new file mode 100644 index 0000000000..1a26b5e881 --- /dev/null +++ b/.azure-pipelines/common/publish-vsix.yml @@ -0,0 +1,18 @@ +steps: +- task: Npm@1 + displayName: 'Package' + inputs: + command: custom + customCommand: run package + +- task: CopyFiles@2 + displayName: 'Copy vsix to staging directory' + inputs: + Contents: '**/*.vsix' + TargetFolder: '$(build.artifactstagingdirectory)' + +- task: PublishBuildArtifacts@1 + displayName: 'Publish artifacts: vsix' + inputs: + PathtoPublish: '$(build.artifactstagingdirectory)' + ArtifactName: vsix diff --git a/.azure-pipelines/common/test.yml b/.azure-pipelines/common/test.yml new file mode 100644 index 0000000000..da4a9900dc --- /dev/null +++ b/.azure-pipelines/common/test.yml @@ -0,0 +1,11 @@ +steps: +- task: Gulp@0 + displayName: 'Test' + inputs: + targets: 'test' + env: + SERVICE_PRINCIPAL_CLIENT_ID: $(SERVICE_PRINCIPAL_CLIENT_ID) + SERVICE_PRINCIPAL_SECRET: $(SERVICE_PRINCIPAL_SECRET) + SERVICE_PRINCIPAL_DOMAIN: $(SERVICE_PRINCIPAL_DOMAIN) + +- template: publish-test-results.yml diff --git a/.azure-pipelines/linux/test-linux.yml b/.azure-pipelines/linux/test-linux.yml new file mode 100644 index 0000000000..a7bd8afe1c --- /dev/null +++ b/.azure-pipelines/linux/test-linux.yml @@ -0,0 +1,18 @@ +steps: +- script: | + sudo cp .azure-pipelines/linux/xvfb.init /etc/init.d/xvfb + sudo chmod +x /etc/init.d/xvfb + sudo update-rc.d xvfb defaults + sudo service xvfb start + displayName: 'Start X Virtual Frame Buffer' + +- script: | + export DISPLAY=:10 + gulp test + displayName: 'Test' + env: + SERVICE_PRINCIPAL_CLIENT_ID: $(SERVICE_PRINCIPAL_CLIENT_ID) + SERVICE_PRINCIPAL_SECRET: $(SERVICE_PRINCIPAL_SECRET) + SERVICE_PRINCIPAL_DOMAIN: $(SERVICE_PRINCIPAL_DOMAIN) + +- template: ../common/publish-test-results.yml diff --git a/.azure-pipelines/linux/xvfb.init b/.azure-pipelines/linux/xvfb.init new file mode 100644 index 0000000000..7d3bb8f609 --- /dev/null +++ b/.azure-pipelines/linux/xvfb.init @@ -0,0 +1,56 @@ +#!/bin/bash +# +# COPIED FROM https://github.com/Microsoft/vscode/blob/e29c517386fe6f3a40e2f0ff00effae4919406aa/build/tfs/linux/x64/xvfb.init +# +# +# /etc/rc.d/init.d/xvfbd +# +# chkconfig: 345 95 28 +# description: Starts/Stops X Virtual Framebuffer server +# processname: Xvfb +# +### BEGIN INIT INFO +# Provides: xvfb +# Required-Start: $remote_fs $syslog +# Required-Stop: $remote_fs $syslog +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: Start xvfb at boot time +# Description: Enable xvfb provided by daemon. +### END INIT INFO + +[ "${NETWORKING}" = "no" ] && exit 0 + +PROG="/usr/bin/Xvfb" +PROG_OPTIONS=":10 -ac" +PROG_OUTPUT="/tmp/Xvfb.out" + +case "$1" in + start) + echo "Starting : X Virtual Frame Buffer " + $PROG $PROG_OPTIONS>>$PROG_OUTPUT 2>&1 & + disown -ar + ;; + stop) + echo "Shutting down : X Virtual Frame Buffer" + killproc $PROG + RETVAL=$? + [ $RETVAL -eq 0 ] && /bin/rm -f /var/lock/subsys/Xvfb + /var/run/Xvfb.pid + echo + ;; + restart|reload) + $0 stop + $0 start + RETVAL=$? + ;; + status) + status Xvfb + RETVAL=$? + ;; + *) + echo $"Usage: $0 (start|stop|restart|reload|status)" + exit 1 +esac + +exit $RETVAL diff --git a/.azure-pipelines/main.yml b/.azure-pipelines/main.yml new file mode 100644 index 0000000000..beebf4f7b4 --- /dev/null +++ b/.azure-pipelines/main.yml @@ -0,0 +1,27 @@ +jobs: +- job: Windows + pool: + vmImage: VS2017-Win2016 + steps: + - template: common/build.yml + - template: common/lint.yml + # https://github.com/Microsoft/vscode-docker/issues/606 + # - template: common/test.yml + +- job: Linux + pool: + vmImage: ubuntu-16.04 + steps: + - template: common/build.yml + - template: common/publish-vsix.yml # Only publish vsix from linux build since we use this to release and want to stay consistent + - template: common/lint.yml + - template: linux/test-linux.yml + +- job: macOS + pool: + vmImage: macOS 10.13 + steps: + - template: common/build.yml + - template: common/lint.yml + # https://github.com/Microsoft/vscode-docker/issues/606 + # - template: common/test.yml diff --git a/.gitignore b/.gitignore index d0797fedc3..13475e3938 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ coverage/** .vscode-test testOutput .vs +test-results.xml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 56cd6452c1..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,40 +0,0 @@ -language: node_js - -sudo: required - -services: - - docker - -node_js: - - "stable" - - "7" - -before_install: - - if [ $TRAVIS_OS_NAME == "linux" ]; then - export CXX="g++-4.9" CC="gcc-4.9" DISPLAY=:99.0; - sh -e /etc/init.d/xvfb start; - sleep 3; - fi - -jobs: - include: - - stage: "build" - name: Build - script: npm run build - - - stage: "gulp package" - name: Gulp package - script: gulp package - - - stage: "gulp upload-vsix" - name: Gulp upload vsix - script: gulp upload-vsix - - - stage: "lint" - name: Check lint syntax - script: npm run lint - -notifications: - email: - on_success: never - on_failure: always diff --git a/.vscodeignore b/.vscodeignore index cc72406ce9..3586132196 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -7,3 +7,5 @@ tsconfig.json test/** testOutput/** .github/** +test-results.xml +.azure-pipelines/** diff --git a/README.md b/README.md index 737ba3eb2f..0721381ea9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Docker Support for Visual Studio Code -[![Version](https://vsmarketplacebadge.apphb.com/version/PeterJausovec.vscode-docker.svg)](https://marketplace.visualstudio.com/items?itemName=PeterJausovec.vscode-docker) [![Installs](https://vsmarketplacebadge.apphb.com/installs-short/PeterJausovec.vscode-docker.svg)](https://marketplace.visualstudio.com/items?itemName=PeterJausovec.vscode-docker) [![Build Status](https://travis-ci.org/Microsoft/vscode-docker.svg?branch=master)](https://travis-ci.org/Microsoft/vscode-docker) +[![Version](https://vsmarketplacebadge.apphb.com/version/PeterJausovec.vscode-docker.svg)](https://marketplace.visualstudio.com/items?itemName=PeterJausovec.vscode-docker) [![Installs](https://vsmarketplacebadge.apphb.com/installs-short/PeterJausovec.vscode-docker.svg)](https://marketplace.visualstudio.com/items?itemName=PeterJausovec.vscode-docker) [![Build Status](https://dev.azure.com/ms-azuretools/AzCode/_apis/build/status/vscode-docker)](https://dev.azure.com/ms-azuretools/AzCode/_build/latest?definitionId=8) The Docker extension makes it easy to build, manage and deploy containerized applications from Visual Studio Code, for example: diff --git a/gulpfile.js b/gulpfile.js index ec63c28a1f..d050bdb748 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -1,56 +1,48 @@ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. + * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ const gulp = require('gulp'); +const decompress = require('gulp-decompress'); +const download = require('gulp-download'); const path = require('path'); -const azureStorage = require('azure-storage'); -const vsce = require('vsce'); -const packageJson = require('./package.json'); +const os = require('os'); +const cp = require('child_process'); +const glob = require('glob'); -const brightYellowFormatting = '\x1b[33m\x1b[1m%s\x1b[0m'; -const brightWhiteFormatting = '\x1b[1m%s\x1b[0m'; - -gulp.task('package', async () => { - await vsce.createVSIX(); +gulp.task('test', ['install-azure-account'], (cb) => { + const env = process.env; + env.DEBUGTELEMETRY = 1; + env.CODE_TESTS_WORKSPACE = './test/test.code-workspace'; + env.MOCHA_reporter = 'mocha-junit-reporter'; + env.MOCHA_FILE = path.join(__dirname, 'test-results.xml'); + const cmd = cp.spawn('node', ['./node_modules/vscode/bin/test'], { stdio: 'inherit', env }); + cmd.on('close', (code) => { + cb(code); + }); }); -gulp.task('upload-vsix', (callback) => { - let callbackArg; - if (process.env.TRAVIS_PULL_REQUEST_BRANCH) { - console.log('Skipping upload-vsix for PR build.'); - } else { - const containerName = packageJson.name; - const vsixName = `${packageJson.name}-${packageJson.version}.vsix`; - const blobPath = path.join(process.env.TRAVIS_BRANCH, process.env.TRAVIS_BUILD_NUMBER, vsixName); - const storageName = process.env.STORAGE_NAME; - const storageKey = process.env.STORAGE_KEY; - if (!storageName || !storageKey) { - console.log(); - console.log(brightYellowFormatting, '======== Skipping upload of VSIX to storage account because STORAGE_NAME and STORAGE_KEY have not been set'); - } else { - const blobService = azureStorage.createBlobService(process.env.STORAGE_NAME, process.env.STORAGE_KEY); - blobService.createContainerIfNotExists(containerName, { publicAccessLevel: "blob" }, (err) => { - if (err) { - callbackArg = err; - } else { - blobService.createBlockBlobFromLocalFile(containerName, blobPath, vsixName, (err) => { - if (err) { - callbackArg = err; - } else { - console.log(); - console.log(brightYellowFormatting, '================================================ vsix url ================================================'); - console.log(); - console.log(brightWhiteFormatting, blobService.getUrl(containerName, blobPath)); - console.log(); - console.log(brightYellowFormatting, '=========================================================================================================='); - console.log(); - } - }); +/** + * Installs the azure account extension before running tests (otherwise our extension would fail to activate) + * NOTE: The version isn't super important since we don't actually use the account extension in tests + */ +gulp.task('install-azure-account', () => { + const version = '0.4.3'; + const extensionPath = path.join(os.homedir(), `.vscode/extensions/ms-vscode.azure-account-${version}`); + const existingExtensions = glob.sync(extensionPath.replace(version, '*')); + if (existingExtensions.length === 0) { + return download(`http://ms-vscode.gallery.vsassets.io/_apis/public/gallery/publisher/ms-vscode/extension/azure-account/${version}/assetbyname/Microsoft.VisualStudio.Services.VSIXPackage`) + .pipe(decompress({ + filter: file => file.path.startsWith('extension/'), + map: file => { + file.path = file.path.slice(10); + return file; } - }); - } + })) + .pipe(gulp.dest(extensionPath)); + } else { + console.log('Azure Account extension already installed.'); } - callback(callbackArg); }); + diff --git a/package.json b/package.json index a9a8f864a2..f8a2c34ef9 100644 --- a/package.json +++ b/package.json @@ -763,13 +763,13 @@ "vscode": "^1.25.0" }, "scripts": { - "vscode:prepublish": "tsc -p ./", "build": "tsc -p ./", "compile": "tsc -watch -p ./", + "package": "vsce package", "lint": "tslint --project tsconfig.json -t verbose", "lint-fix": "tslint --project tsconfig.json -t verbose --fix", "postinstall": "node ./node_modules/vscode/bin/install", - "test": "npm run build && cross-env CODE_TESTS_WORKSPACE=./test/test.code-workspace DEBUGTELEMETRY=1 node ./node_modules/vscode/bin/test", + "test": "gulp test", "all": "npm i && npm run lint && npm test" }, "extensionDependencies": [ @@ -788,10 +788,11 @@ "@types/request-promise-native": "^1.0.15", "@types/semver": "^5.5.0", "adm-zip": "^0.4.11", - "azure-storage": "^2.8.1", - "cross-env": "^5.2.0", - "gulp": "^4.0.0", - "mocha": "5.2.0", + "gulp": "^3.9.1", + "gulp-decompress": "^2.0.2", + "gulp-download": "^0.0.1", + "mocha": "^2.3.3", + "mocha-junit-reporter": "^1.18.0", "tslint": "^5.11.0", "tslint-microsoft-contrib": "^5.2.1", "typescript": "^3.1.1", From c779dd17ce87b2ca90eca7398af20ac83de5ac24 Mon Sep 17 00:00:00 2001 From: Eric Jizba Date: Tue, 6 Nov 2018 11:16:06 -0800 Subject: [PATCH 34/36] Fix PR builds from forks (#610) --- .azure-pipelines/common/lint.yml | 1 + .azure-pipelines/common/publish-vsix.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.azure-pipelines/common/lint.yml b/.azure-pipelines/common/lint.yml index 8d19be60e8..66d82cb8bb 100644 --- a/.azure-pipelines/common/lint.yml +++ b/.azure-pipelines/common/lint.yml @@ -7,3 +7,4 @@ steps: - task: ComponentGovernanceComponentDetection@0 displayName: 'Component Detection' + condition: ne(variables['System.PullRequest.IsFork'], 'True') diff --git a/.azure-pipelines/common/publish-vsix.yml b/.azure-pipelines/common/publish-vsix.yml index 1a26b5e881..f83b384102 100644 --- a/.azure-pipelines/common/publish-vsix.yml +++ b/.azure-pipelines/common/publish-vsix.yml @@ -16,3 +16,4 @@ steps: inputs: PathtoPublish: '$(build.artifactstagingdirectory)' ArtifactName: vsix + condition: ne(variables['System.PullRequest.IsFork'], 'True') From ac05b65a38039da1334d72cda45c2dd55d2c42ba Mon Sep 17 00:00:00 2001 From: "Stephen Weatherford (MSFT)" Date: Wed, 7 Nov 2018 17:00:44 -0800 Subject: [PATCH 35/36] Update to ui 0.19.0 (#611) * Update to ui 0.19.0 * Update usage --- dockerExtension.ts | 12 ++++++------ package.json | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/dockerExtension.ts b/dockerExtension.ts index 3d38fd094e..95bded7761 100644 --- a/dockerExtension.ts +++ b/dockerExtension.ts @@ -83,8 +83,6 @@ const DOCUMENT_SELECTOR: DocumentSelector = [ ]; function initializeExtensionVariables(ctx: vscode.ExtensionContext): void { - 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); @@ -100,10 +98,7 @@ function initializeExtensionVariables(ctx: vscode.ExtensionContext): void { ext.keytar = Keytar.tryCreate(); } - // 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); + registerUIExtensionVariables(ext); } export async function activate(ctx: vscode.ExtensionContext): Promise { @@ -111,6 +106,11 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { 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; diff --git a/package.json b/package.json index f8a2c34ef9..07ea87c723 100644 --- a/package.json +++ b/package.json @@ -814,7 +814,7 @@ "pom-parser": "^1.1.1", "request-promise-native": "^1.0.5", "semver": "^5.5.1", - "vscode-azureextensionui": "^0.17.0", + "vscode-azureextensionui": "^0.19.0", "vscode-languageclient": "^4.4.0" } } From 6204e4c685efe3f044536535aa2f1237d44b6c07 Mon Sep 17 00:00:00 2001 From: Rodrigo Mendoza <43052640+rosanch@users.noreply.github.com> Date: Fri, 9 Nov 2018 08:55:21 -0800 Subject: [PATCH 36/36] ACR-3.0.0 API upgrade (#506) * Added Azure Credentials Manager Singleton (#18) * Added Azure Credentials Manager Singleton * Added getResourceManagementClient * Sorted Existing Create Registry ready for code review * Added acquiring telemetry data for create registry * broke up createnewresourcegroup method and fixed client use Added try catch loop and awaited for resource group list again to check for duplicates with ResourceManagementClient * Jackson esteban/unified client nit Fix (#24) * Added Azure Credentials Manager Singleton * Small Style Fixes * Further Style fixes, added getResourceManagementClient * Lazy Initialization Patches * Enabled location selection * Location request fixes -Changed order of questions asked to user for better UX (location of new res group & location of new registry) -Placeholder of location is display name view * Refactor while loop for new res group * Added SKU selection * Quick fix- initializing array syntax * Added specific error messages and comments * Julia/delete image (#29) * first fully functional version of delete through input bar AND right click * refactored code to make it prettier! * comments * comments, added subscription function * fixed to style guide * style fixes, refactoring * delete image after reviews put my functions from azureCredentialsManager into two new files: utils/azure/acrTools.ts, and commands/utils/quick-pick-azure.ts Edited code based on Esteban's and Bin's reviews * One last little change to delete image * moved repository, azureimage, and getsubscriptions to the correct places within deleteImage * changes from PR reviews on delete image * fixed authentication issue, got rid of azureAccount property for repository and image **on constructor for repository, azurecredentialsmanager was being recreated and thus couldn't find the azureAccount. For this reason, I got rid of the azureAccount property of the classes Repository and AzureImage. This bug may lead to future problems (Esteban and I couldn't see why it was happening) * minor fixes deleteImage * delete a parentheses * copied previous push to acr into new pull-from-azure.ts file * Estebanreyl/dev merge fixes (#43) * Merge fixes to acquire latest telemetry items * Updated to master AzureUtilityManager * added acrbuild stuff * added acrbuild stuff * Update to match utility manager class * Rutusamai/list build tasks for each registry (#37) * tslint updates, transfered from old branch * updated icon * Addressed PR comments- mostly styling/code efficiency * Changed url to aka.ms link * changed Error window to Info message for no build tasks in your registry * Changed default sku and unified parsing resource group into a new method, getResourceGroup in acrTools.ts * Changed build task icon * Added quick pick for selecting resource group and registry * clean * Added subscription support * utility bug fix * Julia/delete repository final (#49) * deleteRepo moved over to branch off dev * Got rid of unnecessary code, fully functioning! * deleteRepo moved over to branch off dev * Got rid of unnecessary code, fully functioning! * spacing * final commit * Cleaned code * Added Telemetry * Julia/delete registry final (#47) Delete azure registry functionality added Delete azure registry moved to branch off dev Reorganized stye * Made loginCredentials method in acrTools more efficient, added Pull Image from Azure option, temporary fix for no registries * added folder select * Split the loginCredentials function, added docker login and docker pull and telemetry actions * Finished pull from azure right click feature * deleted push to azure * removed username and password from Azure Registry Node * Clean up * copied previous push to acr into new pull-from-azure.ts file * Made loginCredentials method in acrTools more efficient, added Pull Image from Azure option, temporary fix for no registries * Split the loginCredentials function, added docker login and docker pull and telemetry actions * Finished pull from azure right click feature * deleted push to azure * Clean up * Working again after rebasing and resolving merge conflicts * Updates and fixes * Fixes from PR comments -renamed loginCredentials functions -added await to docker commands in image pull -cleanup * uncapitalize AzureRegistryNode * Refactoring, prod. * Flexible OSType * Estebanreyl/ready for production (#55) * began updating * Reorganized create registry and delete azure image * continued improvements * Began updating login * being credentials update * further updates * Finished updating, need to test functionality now * Updated requests, things all work now * Applied some nit fixes * Updates to naming * maintain UtilityManager standards * Updated Prompts * Updated imports and naming / standarized telemetry * Added explorer refresh capabilities on delete/add * Jackson/quick build dev (#46) * added acrbuild stuff * added acrbuild stuff * Update to match utility manager class * Added quick pick for selecting resource group and registry * clean * Added subscription support * utility bug fix * added folder select * Updates and fixes * Refactoring, prod. * Flexible OSType * added ID * Move build to azure commands * cleanup * Relative dockerfile support * Removed await, updating list in enumeration * fixed chdir * flexible ostype * Rutusamai/Show Build Task properties (#70) * Show build task works through input bar * added run build task * Right click not working on BuildTaskNode. Works through command palette * quick fixes * quick fix to enable context menu on buildTaskNode * comamnd not sending via right click * working from right click now. trying to do show in json * Acquire properties in JSON format internally, works * Now shows organized task jsons on right click * import * to do * issues with getImagesByRepository * try catch build step * acrTools * small refactor * Show build task works through input bar * added run build task * Right click not working on BuildTaskNode. Works through command palette * quick fixes * quick fix to enable context menu on buildTaskNode * comamnd not sending via right click * working from right click now. trying to do show in json * Acquire properties in JSON format internally, works * Now shows organized task jsons on right click * import * to do * issues with getImagesByRepository * try catch build step * acrTools * small refactor * Show is working- final * removed run build task stuff * cleanup * addressed esteban's comments * buildTask = context.task.name rather than label * fixes from Bin's comments * Merge branch 'dev' of https://github.com/AzureCR/vscode-docker into master4 * merge * missed small changes * Rutusamai/Run a Build Task (#71) * Show build task works through input bar * added run build task * Right click not working on BuildTaskNode. Works through command palette * quick fixes * quick fix to enable context menu on buildTaskNode * comamnd not sending via right click * working from right click now. trying to do show in json * Acquire properties in JSON format internally, works * Now shows organized task jsons on right click * import * to do * issues with getImagesByRepository * try catch build step * acrTools * small refactor * Show build task works through input bar * added run build task * Right click not working on BuildTaskNode. Works through command palette * quick fixes * quick fix to enable context menu on buildTaskNode * comamnd not sending via right click * working from right click now. trying to do show in json * Acquire properties in JSON format internally, works * Now shows organized task jsons on right click * import * to do * issues with getImagesByRepository * try catch build step * acrTools * small refactor * Show is working- final * run is working for right click * run build task works through command pallette. added success notification * removed show build task * cleanup- matched quickpick buils task and tasknode withshow build task branch * removed showTaskManager * spacing * outputChannel and null icon * merge to update with dev * Estebanreyl/build logs final (#72) * "Merge branch 'estebanreyl/buildlogsWithAccesibility' of https://github.com/AzureCR/vscode-docker into estebanreyl/buildlogsWithAccesibility" This reverts commit e645cbf5058c9bc838ee41a67cd6c5aee9945702, reversing changes made to fc4a477220c7819c9249557ba0d7570f5d0c1e0c. * Refactored and organized log view into readable components. * moved storage * Began incorporating icons * Added icons * Further standarization * added logs * Added download log capabilities * Updated Copy * Updated inner div size alignment * Added resizing script * header sort by is only clickable on text now * fixed header table * Fix minor display issues * Identified filtering strategy * Begin adding sorting * Merge with dev * Added proper filtering * Nit loading improvements * Added accesibility, key only behaviour * accesibility retouches and enable right click from tasknode * merges * fix module name * Adds streaming and command standarization (ext.ui) (#73) * Adds streaming and command standarization (ext.ui) * removed unecessary append lines * small fixes * Fix merge issues * changes for ACR 3.0.0 (#80) * This commit contains changes necessary for the azure-arm-containerregistry 3.0.0 * Fixed PR feedback * Run task fixed. Issue ID: 79 * missing changes added * Fixing ACR run logs for Images * Sajaya/top1 (#83) * Query only 1 record for runs * View Azure logs * Refactoring build to run and buildTask to task * Update Credentail Management Sorted Existing Create Registry ready for code review Jackson esteban/unified client nit Fix (#24) * Added Azure Credentials Manager Singleton * Small Style Fixes * Further Style fixes, added getResourceManagementClient * Lazy Initialization Patches Enabled location selection Location request fixes -Changed order of questions asked to user for better UX (location of new res group & location of new registry) -Placeholder of location is display name view Added SKU selection Quick fix- initializing array syntax Julia/delete image (#29) * first fully functional version of delete through input bar AND right click * refactored code to make it prettier! * comments * comments, added subscription function * fixed to style guide * style fixes, refactoring * delete image after reviews put my functions from azureCredentialsManager into two new files: utils/azure/acrTools.ts, and commands/utils/quick-pick-azure.ts Edited code based on Esteban's and Bin's reviews * One last little change to delete image * moved repository, azureimage, and getsubscriptions to the correct places within deleteImage * changes from PR reviews on delete image * fixed authentication issue, got rid of azureAccount property for repository and image **on constructor for repository, azurecredentialsmanager was being recreated and thus couldn't find the azureAccount. For this reason, I got rid of the azureAccount property of the classes Repository and AzureImage. This bug may lead to future problems (Esteban and I couldn't see why it was happening) * minor fixes deleteImage * delete a parentheses copied previous push to acr into new pull-from-azure.ts file Estebanreyl/dev merge fixes (#43) * Merge fixes to acquire latest telemetry items * Updated to master AzureUtilityManager added acrbuild stuff Rutusamai/list build tasks for each registry (#37) * tslint updates, transfered from old branch * updated icon * Addressed PR comments- mostly styling/code efficiency * Changed url to aka.ms link * changed Error window to Info message for no build tasks in your registry * Changed default sku and unified parsing resource group into a new method, getResourceGroup in acrTools.ts * Changed build task icon utility bug fix Julia/delete repository final (#49) * deleteRepo moved over to branch off dev * Got rid of unnecessary code, fully functioning! * deleteRepo moved over to branch off dev * Got rid of unnecessary code, fully functioning! * spacing * final commit * Cleaned code * Added Telemetry Julia/delete registry final (#47) Delete azure registry functionality added Delete azure registry moved to branch off dev Reorganized stye Made loginCredentials method in acrTools more efficient, added Pull Image from Azure option, temporary fix for no registries Split the loginCredentials function, added docker login and docker pull and telemetry actions copied previous push to acr into new pull-from-azure.ts file Made loginCredentials method in acrTools more efficient, added Pull Image from Azure option, temporary fix for no registries Split the loginCredentials function, added docker login and docker pull and telemetry actions Finished pull from azure right click feature Working again after rebasing and resolving merge conflicts Refactoring, prod. Estebanreyl/ready for production (#55) * began updating * Reorganized create registry and delete azure image * continued improvements * Began updating login * being credentials update * further updates * Finished updating, need to test functionality now * Updated requests, things all work now * Applied some nit fixes * Updates to naming * maintain UtilityManager standards * Updated Prompts * Updated imports and naming / standarized telemetry * Added explorer refresh capabilities on delete/add Jackson/quick build dev (#46) * added acrbuild stuff * added acrbuild stuff * Update to match utility manager class * Added quick pick for selecting resource group and registry * clean * Added subscription support * utility bug fix * added folder select * Updates and fixes * Refactoring, prod. * Flexible OSType added ID Move build to azure commands cleanup Relative dockerfile support Removed await, updating list in enumeration fixed chdir flexible ostype Rutusamai/Show Build Task properties (#70) * Show build task works through input bar * added run build task * Right click not working on BuildTaskNode. Works through command palette * quick fixes * quick fix to enable context menu on buildTaskNode * comamnd not sending via right click * working from right click now. trying to do show in json * Acquire properties in JSON format internally, works * Now shows organized task jsons on right click * import * to do * issues with getImagesByRepository * try catch build step * acrTools * small refactor * Show build task works through input bar * added run build task * Right click not working on BuildTaskNode. Works through command palette * quick fixes * quick fix to enable context menu on buildTaskNode * comamnd not sending via right click * working from right click now. trying to do show in json * Acquire properties in JSON format internally, works * Now shows organized task jsons on right click * import * to do * issues with getImagesByRepository * try catch build step * acrTools * small refactor * Show is working- final * removed run build task stuff * cleanup * addressed esteban's comments * buildTask = context.task.name rather than label * fixes from Bin's comments Merge branch 'dev' of https://github.com/AzureCR/vscode-docker into master4 merge missed small changes Rutusamai/Run a Build Task (#71) * Show build task works through input bar * added run build task * Right click not working on BuildTaskNode. Works through command palette * quick fixes * quick fix to enable context menu on buildTaskNode * comamnd not sending via right click * working from right click now. trying to do show in json * Acquire properties in JSON format internally, works * Now shows organized task jsons on right click * import * to do * issues with getImagesByRepository * try catch build step * acrTools * small refactor * Show build task works through input bar * added run build task * Right click not working on BuildTaskNode. Works through command palette * quick fixes * quick fix to enable context menu on buildTaskNode * comamnd not sending via right click * working from right click now. trying to do show in json * Acquire properties in JSON format internally, works * Now shows organized task jsons on right click * import * to do * issues with getImagesByRepository * try catch build step * acrTools * small refactor * Show is working- final * run is working for right click * run build task works through command pallette. added success notification * removed show build task * cleanup- matched quickpick buils task and tasknode withshow build task branch * removed showTaskManager * spacing * outputChannel and null icon * merge to update with dev Estebanreyl/build logs final (#72) * "Merge branch 'estebanreyl/buildlogsWithAccesibility' of https://github.com/AzureCR/vscode-docker into estebanreyl/buildlogsWithAccesibility" This reverts commit e645cbf5058c9bc838ee41a67cd6c5aee9945702, reversing changes made to fc4a477220c7819c9249557ba0d7570f5d0c1e0c. * Refactored and organized log view into readable components. * moved storage * Began incorporating icons * Added icons * Further standarization * added logs * Added download log capabilities * Updated Copy * Updated inner div size alignment * Added resizing script * header sort by is only clickable on text now * fixed header table * Fix minor display issues * Identified filtering strategy * Begin adding sorting * Merge with dev * Added proper filtering * Nit loading improvements * Added accesibility, key only behaviour * accesibility retouches and enable right click from tasknode * merges * fix module name Adds streaming and command standarization (ext.ui) (#73) * Adds streaming and command standarization (ext.ui) * removed unecessary append lines * small fixes * Fix merge issues changes for ACR 3.0.0 (#80) * This commit contains changes necessary for the azure-arm-containerregistry 3.0.0 * Fixed PR feedback Run task fixed. Issue ID: 79 missing changes added Fixing ACR run logs for Images Sajaya/top1 (#83) * Query only 1 record for runs * View Azure logs bug fix * Removed filter for top (#88) * fixing Image Log Filter * fixing tslint error messages * tslint fixes 2 * First PR #506 review Update include: Deletion of resizable.js Package.json commands alphabetically ordered. General typos, grammar and naming fixes. Change from fs - fs-extra. Other general improvments. * Hide Azure Quick Build if Azure Account not available * Second PR #506 review update. Quick Build Image name and dockerFile selection Improvements. Upgrade from fs to fs-extra. Log table bug fixes. Error Handeling Improvements. Other general improvements. * Improving Logs generation Error handeling * Third PR #506 review update. loadLogs parameters update. uploadSourceCode Improvements. * UploadSourceCode no longer has to change process working directory. * lint fix --- .../acr-logs-utils/logFileManager.ts | 65 +++ .../acr-logs-utils/logScripts.js | 306 +++++++++++ .../fabric-components/css/vscmdl2-icons.css | 71 +++ .../fonts/vscmdl2-icons-d3699964.woff | Bin 0 -> 2792 bytes .../fabric-components/fonts/vscmdl2-icons.ttf | Bin 0 -> 5308 bytes .../microsoft-ui-fabric-assets-license.pdf | Bin 0 -> 467888 bytes .../acr-logs-utils/style/stylesheet.css | 387 ++++++++++++++ .../acr-logs-utils/tableDataManager.ts | 156 ++++++ .../acr-logs-utils/tableViewManager.ts | 278 ++++++++++ commands/azureCommands/acr-logs.ts | 81 +++ commands/azureCommands/create-registry.ts | 1 + commands/azureCommands/pull-from-azure.ts | 4 +- commands/azureCommands/quick-build.ts | 94 ++++ commands/azureCommands/run-task.ts | 42 ++ commands/azureCommands/show-task.ts | 32 ++ .../task-utils/showTaskManager.ts | 38 ++ commands/build-image.ts | 4 +- commands/registrySettings.ts | 1 - commands/start-container.ts | 9 +- commands/utils/docker-endpoint.ts | 1 - commands/utils/quick-pick-azure.ts | 17 +- commands/utils/quick-pick-container.ts | 1 - commands/utils/quick-pick-image.ts | 55 +- configureWorkspace/configure.ts | 6 +- constants.ts | 3 + debugging/coreclr/vsdbgClient.ts | 4 +- dockerExtension.ts | 488 ++++++++++-------- explorer/models/azureRegistryNodes.ts | 10 +- explorer/models/imageNode.ts | 2 +- explorer/models/registryRootNode.ts | 2 + explorer/models/rootNode.ts | 63 ++- explorer/models/taskNode.ts | 88 ++++ images/dark/tasks_dark.svg | 1 + images/light/tasks_light.svg | 1 + package.json | 397 +++++++------- thirdpartynotices.txt | 29 ++ tslint.json | 3 +- utils/Azure/acrTools.ts | 104 +++- utils/Azure/common.ts | 5 - utils/addUserAgent.ts | 1 - utils/azureUtilityManager.ts | 7 +- utils/nonNull.ts | 2 - 42 files changed, 2409 insertions(+), 450 deletions(-) create mode 100644 commands/azureCommands/acr-logs-utils/logFileManager.ts create mode 100644 commands/azureCommands/acr-logs-utils/logScripts.js create mode 100644 commands/azureCommands/acr-logs-utils/style/fabric-components/css/vscmdl2-icons.css create mode 100644 commands/azureCommands/acr-logs-utils/style/fabric-components/fonts/vscmdl2-icons-d3699964.woff create mode 100644 commands/azureCommands/acr-logs-utils/style/fabric-components/fonts/vscmdl2-icons.ttf create mode 100644 commands/azureCommands/acr-logs-utils/style/fabric-components/microsoft-ui-fabric-assets-license.pdf create mode 100644 commands/azureCommands/acr-logs-utils/style/stylesheet.css create mode 100644 commands/azureCommands/acr-logs-utils/tableDataManager.ts create mode 100644 commands/azureCommands/acr-logs-utils/tableViewManager.ts create mode 100644 commands/azureCommands/acr-logs.ts create mode 100644 commands/azureCommands/quick-build.ts create mode 100644 commands/azureCommands/run-task.ts create mode 100644 commands/azureCommands/show-task.ts create mode 100644 commands/azureCommands/task-utils/showTaskManager.ts create mode 100644 explorer/models/taskNode.ts create mode 100644 images/dark/tasks_dark.svg create mode 100644 images/light/tasks_light.svg 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 0000000000000000000000000000000000000000..6c579dbdbb31cd67233f298992444bea615de776 GIT binary patch literal 2792 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..03770761ccffad6d5f2b7f840c157945d6b57e31 GIT binary patch literal 5308 zcmds5eQX=$8Gqip^H-WCb=@STN#iqVNXds&l9qO5pp>T73N0%MRBu+ z(-08aUpwpGd!FZgK7P-~dpU&|5!I1NA|3kbaBpAdt@r*Kk`HkoiY67~G56s~BDR-E zSQnkl%Dy*z&k%`m?01eE6UpLt-#ZTe1au-3O2!~BH4{nauwOfom>z#4=f4kp5%=b} zs>I57|K)2$zD?M_Iu5}q<`uzne;eY->^=28JeKc4@GS{FsiVW6(F) zL*QQqFQ=5G`ox_6F!*b!W?j&xReW)-eZ#U$1BLqYuX-(sy%Mh-Nk|!&%okMAzFX`) zwgwWe)dIDDtH?Z4JO+N3w#)^zQf$-t>qGxc9zxvA!QI~Sn;LKky?A+!HCqfWY& z44kv488K5FV6!x^gv82vpvLF*xLuMcFzUWB@7q7}EMvzbFR%;u=Lgq6>l5#|y{B82 zcWHxpcBrR2&pJckcjv|S-9sa75qUac~GBd+H4Ue>i^5)D;^9*oT_}Pp37Z@!T3@j8N z?_Do2b3a_+(H3gvg0@gw2-qTn2s=Eo3(!K59$sBW0uF2qPEtLnQ#ijzT$4DZ9-^)O zkiR|T51kVKT6|u3zH|e*Cg)~ONH^Jb0rsBfth-2+NI zJ6cQ<6GRf-2R$XO*A6uwB;pT>w~vbDqHvq|r1<1$Q7rNo1+WAt;5q9#ODpjHVjdss zund}{gW|(*R!okH4;NDiAU-lG(M4=A1r_)Z;KuZrfbXT%phNz)Ev$pJ`FS=W4{kRU z%<{H_?3I(|e@qvdMRGyEY@W1)CDSaCWST`2Zzhw-W8!`01tLE+!5{bH53&A0e@A-= z-mM#s%}=OI;z77>KY6lf3hfuD1Wq!`X33ODdh?TZX1c*4&YO(pOu=O4oXPQW_lVI< z9q`{LwF;{n)(CZi)OPuXupn&Ny!tv}Lx7h81>uF3=ooXq^Wp=0_B`<7JLS1!$I5dR zj=9V1z}@Ai%TM3U4p<9VFD<2b%yCjA%iJ=-BK4{mPQ-`dgGBeRfD+qiMPpPQC`({N>dc~-{UVx+v7BAgovgnSI`EaKwpHAFS|K;ZDWH4d4wDz=pYS6LceW(g2Oo zQTi!!v)$}*_G9+0uns%iQ!QO>)!ycaY9NcT2v&ZrWmi~QoW_@rLo@)r z2P`q+9}dk(7H@KBf$GGRLyM5#>(CPD#~s>5YsH^Aw3}MQ*BshIY4Myxd#O_zbLbUx zi}avFuN03;1&6MqTfFgKJ|*{Ux@xPuPm8AYj6R-~cj{?FPb*nXPle?jiG)0)O~kVq zc}UHu=}9#f-kpPR-{9Upc}FIrW-~+TL@uGE7mFA32h?;1KIOjfwtl|HJ=hM0)d^jd zwT!ID*|ZWPz5v-2f@-N~A{Rq@s_6ApVp`s)T`8-{F<_Zb1Rj<{ zYpKVy)P$T?GugBj<)bhk0FmZ>U1L>XqlPG1HOc2Dts!bmpGqZkWnoZMPq9m-rg=I# zx{EcJHF8-wrt&m-h2v_%xVWMi!ql{*&KrVupgwVJOhbjkpTFDm6v@;_o9HUq3Thu| z6s0ujl)*htS;%&RO5=N#uL>j@s1${0MSyGOIMoLH4E$Q)QjJg z^$Ww_;!cIpIUc7M+S69QI1&X^9DFZu^S<$V_bwSdV_`PXDULA)^c4%<>xh+;Y*k_! zd=7qiwRk1$idD3^OvetNBNg$pSi8Z;EIsaD0Uj>l_26*~XlfQVZe^oX9bU1zVZcY) z%9+=~MmGj8^PS+Rqu5DRaoHBUmwa-$=0XYYqMZXr^4VSG7HRPxe0+ F=)V_d(H8&! literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..47153a3b8090f343b8f3064c1523769d8d4d2992 GIT binary patch literal 467888 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�v-kPquQLS$%k5&0&zO}1B*gv$3e5zNo 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
      ^q(iU*&IVxzrS}faMW{!h}KiXfacP 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

      yW?rvBcoc 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

      ?ODc&9>aUpC-O>|cd_3O_W@X2#O6Ei#zF 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
      +
      +
      + 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(

      Q`Nx|=SdABz#~1kfMQ^d=41P=Y^W zT+!AH&06Z8tP0v^Bs;>S3OpQ_iI?*<6{*+eL3wBWD_N~P(EQw>Ch5W0;Nw`>N#9TIkJVix8B72Gz5GEA^8qH^NK(>864UW^(CjCvE`lI(jTmV*3s()&p0nMe_v~90tsJJ$h zC%;6s`D4B=QX4WG1RFXV!u_|i>2hJ@z@=SJgf&~Jckl*Yl{lOQC&@yn`qCr#+uf17 zgdg=E_)7z_!m31_!#!WEbohs)ayccD0vDWwf{d})na%hC@#GyLppFpsFW{5WGf-T> zO%jmBxR6F{qQU9Oh-A;9&ZVQl!AaLB-|NfVsE_HaxO*}JiBfwVg@S7PbPq&@_F|m& ze4O^)UH3X&3uc_YOLY|7+E)JNvjCUv?Z7jHim3^dctAO|61o#=%ncJp+_8fQ5n|kJ zgL*#0_G7&L&whlvFJ#B)$$~IG0k1w0Z_^{w#BpK*d*2VhPjU~WNIxmm7kFAYN{k+pZof7pQ#QC65@n?DQ{~>N@{LuS|of0U(|^JF>k{6fVeKfV6)SBLHLjT zT=70L`*+AE*?30ruJ6x|2Z$pYzx7GAcn%g}5>f1c9U&>RUfkU<+p(7fpcz|!zVe@F z1ae<;-a6<+9f-1jhDoz-@TK2<27cB-0srMzuPJI16{$;=V$S^uF&QKJizQVAg{1*i z=IxxoU-VN&J19H%i}n-tbHx?B8|exLoRN>Dkr;$}bKgQA&@xSLG1I>9dvt9gjW!r8 zbQ?6ka;d*9`U;TW4l5W`x6f=dC%LAEX{i}v#m!`gY)5gXMLwoRHm63qrABh5#)8R9 zv{UEGUcfvS$U|9hl%lc0+@q92vmvV?cw%F0(w5ic(`fp#s;h-y{_1VZG+dpmIW^OC zVO3YhTo)|2__Plw32F=Ig2P4XT2W+Dv<>Va5`?`AapCN*%Onnv{Jl6?b8J@GRc;Ij zz~rnyW2~$G0GfP$CcUCO4J-eLFn7VzvK8vPp9h4YZ~gIp*tf`{ABH2J+dIgT#QRJp z_@UAL@ebH5NTNfABV6Bc!u}`J?N7h)N(XvfhZV%zU<){4+#>cf#)|wOoxh>P^GqhV zp>bJ(y8YP|IH=t4tD(f}OeT1t*8SHp1@U&+9}XBSNd1zrB2$J>qeM@m)FV9K4Y;8^ z!i-Dyy{9eX+U02Y==5 zdg;X&w)|;_+L2iI*W19_~wzd!zkZu_yH8gvOU zDYX4JSBq*pt^=G6PYo^u!t+~+@G$o?O4a}S0%6+`ytqCMaOj2!G3Hf4Jz{;u$j6;S zeg5}(RIYr)KE&fEP8FFx%R91m#d`aM{FK%G#92Oymn2!8adVw0sWTQ>M1PqM{ix6T zv_AIZzXwcKN4Y;Y3sJAESH;Nz%T^ZWg_QlqL-i`d^i290>I{FZ)kgMMtLIjE8)#M#f1x(C^YvsBCG3QnIADuyKE5g(u+;LEM-AO4Mo`r{-> zh2mm@zNW&}6L4K4#eGf%%M0HZ8(5P^WK9K$2j3eSc<;LRWzpA&!S#sYS-pZARh+ST zp+}==#~GhooZK%f=DyBW|6eOcuF1h$Q~cmS51xcHWEEbKL*YO-F@n(w>mp!Y{+}fo zu>5+}*9>VdYEoQRq-oC!2p&*_ZNl$r37Wo+4gDN}H8H>G^l9S;t$;N#MAoFR>#%(s zYle8jD-x{M1vst&ROy?r?JWs@crKiY7~n#bSiF@JsTqINbxp8z?3kZw8zAT{x0B6NA}Yie7Ba^ZT=i62ArNr4POJ zE94iZ@ZHHewK_P@?;x%BA+7fy&vzl6eZbwWV%i@=85D)wD07?9XIA0P?}}!o;f@a> zFK6&r1bfGb&s3uE`$mX8J&(7kz0m78iT@+DuaW7i!ABDTU-&8Ku7K%m*ynWE<8&xY zSu`U}ZH5`R47-^=69f1EPihI4f)(VB2mVKzVLQ`jWU22};O*dgb%K56j!Q6Js6(2A zcj$@TWns5ikkL?iSa<2~h;Z_F(dSTKSn~wYUuZ)<S_$LBrr-+ogBf`44w= zFLWUS@>1%@3&9HV|A$CvA1?+sgxJbU=^ih4>_q&b7z*|gS*BnkzPIU6f~}zeiD|8qRI?*ag16wM7~Lnq<)Q5a!(Yn!tJ|CoO1vJg z8~t-acX&lZ2oIX2TmGETMUy+{oY_Uf&*qpAn;B`e^la{Mt>0Ao&A6{aT@5P zMO?khKGARGLC@+D52J+&h)bFnKeLa0elm8axxO2I-lyyNv44*{n6`6{JDIk7fZLB} zZ@Wp2m=MZzBX1PLO?(7CvMwe7d7JaS^}%GEczKI#8BeN;Q>6S#D6(7p<)<`TCK*pS zS%xSY!Gr=aFi|(GMo_rQgKRmEYO$?8{YXyeeGd!>q0o4?!9X6=|* zPWZ2f9sZgzvYg;w@Q4@*&>e+fgnxPzabe@3B@&<|lAwp;p)bi0qk%zBM6N$h4DnBP zZ3PnJ22@MKyjCFf2ck5Y9~}rYrCz9s-{nZ&lW*If^v+@4Y<)R1Cn&GstlTAK^HO2+(qWIcjtsyK84dbh0}ivr?m>FU!?(w;s*`^NkohK`w$Hg4 zo-N_Rc!5RSbn!(wbma&R|jSb-Szxsb~!v2FrjAk&Xv0F|nZBaH~Js9p*BzxaR zgVXjiGC?*zYXu~`TaOpNw<1CIV|_qB_>-eHZMHt;J=z8R3RJwUA#FTsFoQ#OXnoIa zYdCRi?k(w^L+uOmuU>CjllNxRSUPH6_h?aL_!K5;`?uqJ*qI=6fR1 zFKJN@WOk^8@C#XWLHsoCudI_1nfyFEr&GJ?W~Ua(WFE{U@hJB1Hwf9{tI&sz!xt-+ zRhQ)r`$@!eomwv^ZmDhSNh8%&UwtC(LMW2F1|dr!ZNxQd4M~rVZ&<1*^wPWqaPL&g zaZt0`*FVJ8CD3ITuA;Icv*s#btzZd>--vff`UuelgjgI6xlrvTAF>HPn|goVbr7F&m>@ z5cHdm2LcPG4Az!@*Rd}qcp9}!swL#b8)5FpzSc6To-ovImTcuA@py(M+&?RAUmV(X z3$1OZ)L&f|T4$*U>Jx@RMq6mIb0ufG6*MYHGSFP$9bJj2c$!ep>k9hrl-Ix9s zQn`7*(&bv)7w~sxv*W0H!g@%bh%St!E?ql+B#l$rRnB|Bv0}Z+JB|+lont{)0%!>^ z4>14p67>>Ar-yvPSZjAyeOz6G2U@;g{++$2fn=BOh9L2n`+Y6Iaq!CWqyPk$zAENJ zqI$xK%5J}ZFa1JhUve^kyo~AxeGvj;(!N@9GhXrnxuvTLmJ$vy;1K+ZX8$f7%}X{k zp8HuwQ+wkX-VI^+fsM>dcImf_xrOSLA&_IQYpr&JC%FG-q67QLHTuL9ibEgk7QYaT z!i&9~rOsGKWJ)I0O+3`>C)v$YKk8Dz% z4d0I>^5$gfBAwPZ)~rASDF~a6e!0V8d!oYCmUbqI{FpnMHG%`XYfhUD7cSauG0THY zn>WwHgG?&=U0NIV8tJTsU)~8Zyh!^C*Jw&9F>$QPiV|Z-gB-rlLyTf$;E45$Ez0w8 zD#!zTskpA85!-s`$_rY*-E=mE56o8$x5kHj(0O==Tx*SWYi~FMx?!lrx>Xhj?Rrj@ zqwg_}{r{OUSgNbS*Ku02lZSslRHcEisab?OqY<4j1j3!&l1?@{gt!2eGVu4;|H-a= zifN`@ca)lTd*g6WuGtWI^LP^Cd{AyPEYI?X4!PsVr2^9G541RJNt{LyrABYLXbpIx zmgMwF?P?t2@?8EptLRfPHr+fjl~|7zHz?+iYphyk{06N99r5hwhZGVx#*A$?JfklX z_2$q#N@EPb2+LB@fpmCod`?nVvPE-gj&(79yiPK9U$rELP0T8KUyA6;VToiKuq)`bmLEOP>< zD+jZk$kshkC3#WB7%;jROjj(6za~j!1iZGBHUFzZslUDBkgjup+4(ui2VRiQmn{by>xQChQ@uHC(1Gt?y-$x;c*pXJWpm$Y zW-ZLq^LIQy%*&L4rJnKnXKWpL`VM)%h{amQn~h}S3fe&n)zvWFy%UbEr^Zyh6`)&f z65p13Qpju*3F5a4Q-I4-1iQ=ISn-aSk8)qxE91-buH9$()j-62adR)k~<7PfPIK zqcZ|{JjoRmc1CzgSeFoyghn~PjiVo1V&DyM=V{V$zJOw-_Zmk&zGC}&UzmDrG4-x& zvd?}Q-DPiaZ=e0?YX03-U3)ciE*FX9ddxd>srRnez*|U|Z?K0^5V4&esr`7tJY|?= zX!}qbvnxOC7cq2hvExKm1 zA35*Q7?TvywXf9~lNQWJSVl91$vlQs`FUI9%FTmW0;Z@b1>~xeGmP2$vC!eX%^Oac z6ZUF6K%;KF8)*R6dgkWDa@H642Q6;?wG zQO-vgOD&~*b4+}%nDc(~jc1K^T@2C|@KkH0Jey>Bw$SD#bAR*r$IvEO^JU>P06wC6QZ_php&$YBJDul?2h_Z-?BAQeP`gyK=jnZK9jYB&0*NJINacd&;d0w+W6@)_q3< zrVEUHGpQ+MqBhCyiJ~WjOOfNhB8?0+>R!SkmwYbMlkWk`QExq2Mbj>b3e<{Ft8Ns(2RbbG@REggxGKJG4{fcJ^42%^4ON? zL{RPwzLAMv)HSGh{#rBE@@j-+1ey=b2i_-ZS`F(ynf>{`XFs(qsaK1UtgbzTKb9uF zihsNF58JqIq4AZg$eB7@L%NeoK_@m{Y%DGED{NLE6@Wh7j(?VhK`mO@Y|IgEaYtoz zq+&2GH|LI*6Tv=62nzEEO^<*NpdaDC0P5py8!Ii zoGSn?DY=fD-@}vdSe=u66ZuWLE>}_&T(PLvxk9o9htGG}x}Q*4o>3Pru?m)>eb=GEd=fj^|mcS9v?yc^KGNEs^l zRCQh)1a}767ZLo4 zpZRBoWj=pJ?nv8A8zb{it7);aK66FGM8h$S8P5>MJTKg^Z_!j+M}J|v=b(W7tj_`c zIVsHbTj4Acnj&v_X2&|w)#b6` z6fI3D-Mc%fNTBbwxbe;x^Fj)$0lA)$2v| zo&U|}&FB67ef5LylgTe{n{Yo4^y8KCwdB?Ox#Ere;b^KEb{vN&%iUnLN04yM@XGX}k=X*4K zG(s}u7&tH>QphEjL&F>;IC za~R>e0*G#b7z|+(&=~*`AgVkn#+{ZN{R>x}+q(!de=c^4n8U0=!@20kRHUcgtzRO8 zTldD)0D7$ZV?uCDUh2rz`nlDu&12(BUl+%={#9ldM3?A@s*j}iuuq9kN$0$exwrZI z1l}#}*87LMU%ucfU{CSUJn}$Ze0;xa|HJnq09!Iz_?xo zdzb1y-T~Z#w`*)((mDE9V$}|9Im@}mP#(pg72&9cE`q!-CYY#2LZ%()OF%ZHgEk*Z z)QfH${_H%_?0;fi$uqNCyQe^Z)G$ln()iJHpwUK;14 z)*r;XlE0_$QjJ9NnD6{cS`roP8a8v;(hQ&s8Tu0i;ysuYqm|Ly&-Ceph(b#DF)je0 zlFqke7kD~#HhUp-=wD%9xhKnXs;;K^W=v`crfxP)p-P7DMPXskN^IV?_SpNM}h ze-3>vZbfjpIQ$3{OHk6+Ob$uCDhR!h?6i;% zc1V54dWUeQo6yhqs}R%0oB;4we-SCMJ9sd2e*&%kyP8pIfi74jbdgJ}H)xK|81F#D>p(U~Owq{jD-ZT7Pm(aZC=* zLfJ4S<->!2dol@(w^MX_cR^Se2f@a`$!ehzfhX}!BmbX07uo}fWakyHy!Cbl@*1y>C4%|-6Q ziVNaE&P(}~`g(MhOabKk-R^dd^D2J~Ds)t9E!{YzZW2TGi*itCTbkncpCp7Tp4*->0gOlPIk56TosAR zMzg8e)-J_WhBpPN2;rqyAa=)L=I~oXe%)O6LNUGuhpdgl)dK57`J<>kufYc^-Lw$$T1^9?`Tsk40md=XDqycFV`W&I2N2JTrHEC43 zB~3_orD^GbG%qd6Oiq^5pnhJ?5L@N-a+;hY=TVz%xlrCB7t5u9E2LYpOf^2JG31zB zE7!}rp{7aREjI(-BDVuR1o*Ig6x#I2C*?EpIr+RiBu&c~r9JW$`MP{lzAfL8@5%RJ zy?iJ?l3TojH${5jUF%)v%@P|djC%8K^yYgviT&Op?>4CfB7UW$dKGU#>hPN0xHS40 zuJgv>IK*$ZQY(RufKTcKv&#s{HIC#+d> zMx_KWjcE0L!7Jd}E{#|%Y3I-ZYkU{*M_@nD;mudt#D3+3a$1~H&MK#s0cB7b0se$? z8SpjGkNOxMMU6d%Zz&UUvE^gdJ{I?|(jMHy%4cA2njwwg9@D$l@>`x~?KM#<-TikG~tyh+aE&Ade!K%eC8XnlT&ALd8JPVYGESm${UKgv(?Gq5i{XL%ir z3-v%2(vygL|u!PRj#Qi>N>P6=Gy5KD$yG)^7FxucXvz997Y3XW;IHqN4+0uZPs};cY`Btq&E7wG2M0ud8I14l# zJxYrbzeBkU^*eEoD5GxhSr} z^k(YwwMv&NdU{!QXBf02Ki%zS6WCE&}zH<-T?=dxeHHvWL$ z1U`=Sa=Vl+_Q~!3I_a9a0R8v*8?2e`Z&cd+d;R%u@9%^*WBv>NVd)6fUqadBcH*y+oHtOn{xQpk{o^o0Q~i_PO|aT_Ngn@{ ze@2#l=h2`2W&Sx?_Ah|0GJWUKlYKq@rGUqGK9I^E0-qvf`WLW1kRHgCG6NKC0=a>L zz-DQ4U~8a6t__q2M2E-KJfPyb8PEe!@3z1W^n9^Duv6L^*k#$5_&vx6_JQ5E1`b#= zMco)^lcMTcWtZHp90_zN9f2-oSKvtCc;E!|a2g{O&YHkkoHgJx$PXYNNLTkNZGk~` z6Hg9|U}OgQFUuzb*VMAWD9)_FE%{7fA~4F&1@00*4ecLLd)q$)ix}m|XU?O~1R3(d zWa86^&mexi<&(i2`C>3H(LepBB>LmQLiu7KJGdoSEFJOI1xw|dxL5H{;jSQ95tM^I z>2%P*xrV!!U<~&O!P;PbaJMuNYzj8Z4}uodzERKTTl7tOk=Uzm)64WqUC{%&$qhXo9MbFb z2E9?=tM3oC=&kxexmoYjyY*vwuYO9rr1$Cl`UQPhzocK)Z|Gz4dVO4!yN-OZpEGNh0&~B?%tk&j{#z@cr6n`h zEHF2lTg?))+*T%?E(zp?=YKC&D7hk+eug zWPKzjk{2liyajME;L=D%M2`3(MkE%gHRt6#%Z`!y$nHo}q&d4wQy942E_iT8I8-$(Yk0uv@yCjxQmKy)&12M zs)wsDRbQ>XQ9V{YUOiboW$o>&XR7C_7f{31OQ?;wC!QKlhc(5zwzaNmt?SvuYgOwi zm99_Yneps+ZsJv{72o0o@y+Ig_||wyygV*K(9z@3_>TC__^!yE_#Wj}d>>pnQa|wn z@wRw}Wi>o+ybI48KZ56tA9t>!WjTI=cEbBmlK5$q1ZtKgf%*hL!PPK+7O#%u12nEd z>zW9!tm7xpI&duQj&L5uM{pj+FWcAew5y3+Nvp}=cdT55b=6<9z9y$8ucokOOHHwrbFj2$WfiP_W=&~L zMU8CPrN$@wY79GrVdX2}sG3+!ZB0GDShKsPsis-3uW5mn?KOvL4&&9RhWBB;OQ;ki z#yoYN&ohtb3!X2rY)_k~oxRudRnOO0p6BbHZ?YFXCp>3aani4n8QY#*pWMWxHQ_aJ zW~}+?nxCu*%lIz26mpk3{QTH?M%`VCPQFoc^Rre>{G1sTvueh6CU-9&N?z;Ze zGv#^Rb<^`>&yQVWo(G;K*Y}gwB&~5xC8Z^auGf=zk}BjT`I7=dUXqzKAmk^XP5zef z+M2R8+l8;J`Qe(o!XK`=x8^6pAFcUCN|Nv=DQi;lg;SV^6fUR!P3m>wZ&QDi`Xk|c zsjsL0R2WO0Pn{Qjkot?%UkDRVcRt-I{7<@H@*>>{v6-bXrVIgGWG>|j(Caq6LKGoO z>4$Pb8CEVqxeDb5lrd#onN+5f8D&mcfU?9rJe8-HpD#bpGkG@8g;Kyb^R2vumqS_k zFCzY*kN%5j|BtVWAK}NbJ!Vq{kv!&A&rT+I>OJ+~H=p)=nk9J}J&i2cv&XZC-m{$H z`hn{QjJbZ~`Vn)vUU$9D1lNpfhPhoobImf3>*ubYvt-wzYmu!H;E$yU9$^i8hmaV1>*nfXTA3of*jlO3ZJn%WB;miRZ=wzQPb&V$ll17661(Vj*|%Jd z+hv4b=GWl3QGi=~g5TxS`~jcmiz-u-)igCjUC;NaIclC-sBXa=cJenso0*Tzs!y>- zm{ogVj+C&^!wjpSS;b*C{T0(-J`FJ+%%)q+&wc_WKspM7pGKLU6ibS+aMF&X9n4Is zP5LB@B)yvSDyvHRRMMwdEa@{zpJCNWpH2EKizn?*`U0y->Pq_8?3JXiC4CKMr;GK_ z{k>@{7vHa<+_E75GSCTtyR1~XiT{&a`Hv+27m}~#>v$I5$n*IoUIeg>m+?xj@BlY? zoY(ON-pKdz{k)YQl*d`b1FscO2K zsb;IWYJs{Lbma1MdyX=E|99$R+WI_{t!xy^N7y|m#Smkr*hkrqp=?WfE$KD(Qc_1! z2P;YXdeYa~$CxW^;azKK?iGS3GrkjG7jwPXK$LjC2R?VYKm2QWhh^{4x3aZkX|-4_ zRV!3k^{Iv$Q)|_Fb+_82HmfaayLw1HtR7W+)RXEN^_+TM9a1lchr08 zeFEfX)raaMO+a2t(bm#&&`w*YWoa9=d~K6fq;1p6v`S6U0-CACwK}aqYt;5?`?XfS z1A)c^W6=)sMXgg!*1AD1l;O<)WkPupKs%=OYNxb5tzWyK4QrPiS)KrtBN47@H?%Qr z9QC73YE#;bHm5CUOO9M9i_har^`-kVQD)Q+*88$SR+|z$cBdF0p)oiBV{rg=g?6#- zlTwROKfWF6A=Cxh)4I0|&VlDbdx0)}`}k?pC)yV6hR2}3P!GNXHf{5Dpl_gl9O~f zec+p?aiU&r>WpofF8Y~s7um9$2Ob~G6S6wg*2A(sPFU2R!w39%>K(l65M{RRZ}ZE3 zpWpDu{I&ji|89SiznSz0KI?Dsx8rv>N63ExaQx^)G*|GmP026NryZJS>mgzMYu4x0)`u>G7ZRpIN!H+tC;LokQ_%=EpwY_TM3T@9}m| z9Cz<=N_JSSZ<{)IBs-s-JCpHqAS zx1x?-R_i*j&C=7`QJTY`Uq{c&^Ub!Ub6>pUhmPI@Cf|kg5#w(nUG`s?Z{(K;{_k|P zewWX)%I^blemqd8-gNvO{hWOBtVJEacRoAx$vf5gEox```!WTe$NTy*7T~@8^!Y+y zm|_9MhQKBM0OPttaV*##i4Iy*Jb-&wc?sXo!GKGw-P?%a|X+E&5nKeIWYb> zrTS^hevTcH;u8u)X99D}b5VZtxr#Kf0DiQj#&E5|brGpEFP%9Y^r%g^hB*r$>b%HSh*FN+!@TFj?|1gf-d18Wio8laCo*zJ;S$>89pC?Fok^9HK2KmhClMUM{+xKW@{~jHE}eHe_#ikRT-2GKtf%Q2v<|G&qrP6x(ev~|eT!b~ z>=AHmdZ}Ka%eqfD^q5|&*Xz6WCcRm2(cASy`eFU3-lLz?&*gq3+PJP;cneYF|!V z7ejra{?+Rr`cLRWXxN#ncy6?L=u+q^>OXWNG`1{*)2HoExDTQI7T72>9-0hIg=Ru? zp@q_gupe*yo&`4Kk4v2bm;KD;~J z6mAZ;pe@7g;X~oW;iKW6@X7F*@VW5$@KE?-_)7SC_-6Qa_zvm^#}>ZlD-Yif&xRj{ zAL00&V@$zJG1r>w%q(-GnQv|~i_C54H)fexNozXzv#FQ?><{;6s2kHX8T%}$J$X195a_PQ`9%wF>p%4GJL{kUgFe>X3f!z8DzBlD7Z zm0|+4N86e=%rSG^oHVD*8FS8DFqa}8j8Bo&NO~kQl8wjGng{(waw7$i&5^BDQME{~u>``q>+lxv3I<=x%mZQ$FVJJyV_^3;CFd5eo5oR`?iZVuVu4#C(o2F`uVQ z%oivVbDJ_T{9b$4>& zJ;aIkbMPj-lY_Tg_iykXeiG`jwDWf85z8VL{KRVskO#mlfoDI%_rE@V-Mb$w^+on2 zxZ?dbl$YolxP;wg--D~%??d^xtJ(E=RwleA9AxE~IioikM(G`f_IPtHvRoYBvzZ?h zHq0oO*Kcvg`RpF$?)wH3N6ORQYgnT!nubrwMbkyoaLirhE>9*IzXCEoOYa_JET7@~ z#R}-HiG@(yY!eg@+ss~MN$f+c2(G<9!nUz@(0dl&MeitD%Va1|F$GFGgP_5lW%bSIER*eplEwBxd5*O}$)>m3JpqEtSAObIBa5(iwTG$@VAUS+@13ZD)totRmA{#a(A(hJ|5Qu>}-v@%SYEgSME z%M`#o(}oOXT)CRrg4rhEkJ))R$~Qr6nNr5npFhSkmDcBvJ-4`0cy?xsRW@W;1$%J{ z^g-p>N$90_+QT%v+^`JY^s|0FD6U3Gfn-yA{uZ1$t{#tlU^199zysQ9S2J-B9p%rv8tW;vefpNDejo6C~`(eBYtx(?ypAn#Usfh2v zF@qFy2zWL+Bjri3U*{~?qXQ%Z0Pg@Ahh7l^&?5ow+7#eNK>8a>!}H^rKAz2w!@2wL z%9GAdGVm@MUBbB$K=!j>uO%9725}BR4IW8Smhl0S?Fo3ZdOnbShz};pp9uKfhT;tw zP6lXM!UVvq&=Yl(B~peHrC%P!yfTd0Ba#^avq;KZW?@;KngKpA26pOqa!M5^)GcT? zwOHx1a!Wvc?<;cTgMF%#Y03aO^GNrk^BmRDy_16?{Wn1W4OTWvnNyDfz$*8`RlL@M z*)eW`dfxVkK6R+zQSNQ^;_C5jC|0i^Sl$BG0>E_?t%my>vL5nq&5BlZ@>#UcvvXLg zPd(%41m@v_TKnNH%bwaMrGa}0P)Fc#PPVJy5zN3Pp8DJ(oVOoyUrPUT6Tq2@qQ#ZU zY+qs?Ofg@8D=V#YLotpUj+ny;pmi&)E4i)b^R)|n0Qa^irU_L6GT z)yiS{JW?M&VCAmJe^95k-({!a(~XYz;~6P`1pwDETa_DD5BMEsv>;|&b-ZwS<~Xxt zc^+7E(b3Wi8;i-RK2hnk{9Q7Z;po6`|*Woa31-zCmqMem`Eb6skp6UtHMGgU?$& z_rQAMa$`R}Z~5Ys){rCm=J|jx7huuPxCgUP>u=FhlR5spXD>Z>JZma@J?4ScdQu>~s0(@gbhA$IyN|lZNCf^Q!vmawC#+F9#p*#L|Z3d$AF!(n_V`UQN zV;78lCbI=|J};X7qyCn~c7ncVv4S7r+mnWf2L799Bq5j?G)FXMY`*voYLyO-XeqQQP4nT0@~?*z@) zuxFSnZ6@s@NtA=X)hV9^i3WfU0v&OGSRRlEN^zz1Le#N@$kjX-NH-disZ7VogY zyW6HsR=u~`J`UTp0JH-fvcEeFa1@{i;3U8qkZ-_-%hp(yWzDl?cr!38c@n{qXIU;s zmeo+d3eEwXx6e6bwb@p=A{W(P1h`_$Tx!*m-fc?!-N%8BOHLSz4JaGh#e3bhLA$N* zGnPHQH*Fi7wC&}yY>WD&e(hsWU#JI%()RU7So3x?F&4cUeY-j8 zP+SiWC(zaN6W7U}1nT(D>R9K{di(rG?RK&~|6%OPMu2>q-(=Gw8@5?Kqm%(u+TSSv z0RYqT4<&BNsnh{905n?mRrcC81gh+}Z1YmKWdo%Z;2=P!-DmfT4wYj7y#O#*l)ePo z4{!ls7~s;%*p#al;F_u202l)p2bcty0+<1q16Tl9k_Wj5AQd3pnpZsYP2z=Z2ikMM z+RU@xB<4G@dv)vpS$M8J_5>N6<9GqU<`rLEmcgdKX-p%XCi?0sU*cN=73FSq=L ziz_;G&b{2X?N206u>~kQ`U3*(t?WJ7OVEa^7NB2n9puzk+VH65!`#^u*}!*NzQT6_ z>;c$k`6NFeTXWn7-e!Tbf8rfheB@pBcvk^GV#9G8P5_(+IBWSQAFzE2fDZyd4CR;Q zmAxU3`-8lx=dJn1VNa}rebr=p zu7FRgX%?W3Q8y~sQ-wL9I%|SrftqK5vuCBaKX1pFc1v!xP+r*s*UBqU9GTeqT2g!)o1DGvHoK6llyMFZ3F{mYO0jEd)jepGuOkNQL# zkzatWRp?u7ve!2pZ*#(Yu=h@Ci`}Q=_qNZf?Up`N7@LYRq}gp`YrHDhSnaX;z&oYZ zl&twU-6{1?J-3g4_^Tg?@%qr^`j3d+sZ{eBs(GFG|3EmKYK{`Wh4?=v{4T1QBm6$9 z`6Tu8ajH2%*!rcke<1uXsSPLozfsGNQJY@E>#3%M_-_(^mhc8@{|NCLKc^acx1{SY z3BN=&8u9&v3#lLaJp|$35`GUI`|l7h(Hl8`^)+hwfN+rd|F3k^N2&dB!s`f66DFBm ze@pyVh_~K3PvaG6EJ6|0TWD*0vz+cM) z{x#yK2-i{#y#rtPA>nGm-=IF12scyBw+N?G%^~6~$qT6FB=K8`|2pBP2~QCIJ(AzD zJL%9xR%5@Qv;Pg@9}y-kx!*~cB=K01|32~bD*~Q()3=`>OlS8zPi_7S@$?%V?mr_; zzXIU?6yl_E;>o@qdIzkhifZUr_1qnVE${mGgh@6JjbiCnB<<82_`3i+-{-hT{5Og3B3>fC zlk0#D9l)Iu{!tzKr;t#6M4bAMvjeekaw85dS&i=ZODH;{T5L9>UhIm0TnK zHR6vDuaRs!5DRBW&p#n-`Oi7xP2wpAy2x8xSE`0o(TC;T$u4-+O| z6==S@Ddq_CgkPXzzhryeEXgoIXPKZL5P!{(65px2VmF#M7^dxGACv ze@Offi2o|_qlCXqm}0VC;VXsN4EwFIp>I6!C;FP)!eE%f5d>UeQlHoyAR>cYCR(m3Yz#yFoP+Aw8BhtaH(> z!2Q4ITUvA7?1(kmS!G&zOaic7pb9*fsc;a?$2%5xePkLfq3Bw+TO_Ut3Bg%sed=;YHeCLCyc6 z?)>AUx~>ENe$FoxfsulKSe}t&jNh9XjUVoETjlu}BF>Lr*`N?4Yqgc8Ds5K1v77-K>(A(Uc5WQr+~DIR*x_l_|2c0b+z zvHg4&ea^XO&O7(qbI(2ZynFAw$K3cQ%dEA`Zx8URfj52xJ|kEEp&-fcm3aJ)m;Rch zy~#4a@1|FiruBlJF}m);^4Gyz3bs7~);ARTdglN5H@|WCJK9QtWB!MKes|2DHbl>=U zjR81^;Os=Ejao7uRIl;h7Sw5wMg+AW3SU`Yq9beOC8%c8#@+M3`GLS_){j)p&o zc6Y*`kIa1P9#A^E2F(YM>EvoBbPIF~bj&YRn{_mu7?s z8;zbSI87>JpH{v-R5g3UjC@{yiSZizu897Rbl3E%bxAqa7`-YUo1sO1=1}M%BVu*NU5R*D_4n{Sd^HBIjwC8X(#aTHhz(Ci)*nH?bS9otKMTju z#f-0~ql2C{>ON*L8rsk>NIBkk(-xe%$U5Onh&W2Cq{c1!Q@1b{n;8YoY1h298jYU%zqk;$%}O)Ag9`1c z&ZUghSZwQ5zS!2qsEs32DgVL9a*=<=zoIlAZo!g|@cHK=O>*^F@0!x+pAcCroUQ(7 zrI{VoXdZ*dULh*(h{ohJaSTggs68 zVZK`KtwttGd^SYNnG3365u>@8tEZ{eTHwrsqcfrhA8u4E8AGF}DDsTptC3JOs1Kd< z{uYtBpBa6=Fi#r<_b~1~WP`oLLyPhy4t4CRxH?E%l=<7tD5zB}W?b?!m>mP8?i7Em z-aC-*jA~nz+6(6$waTC9>+cJ_L%WYe56l{O2G3=U1{@7$u^|RC+iREcxzJW-X0blEutDWA9 zSX+*5S^ov0xB2zV4wc_YR18w=9#tEnYw%l-j*fo`zs1Ok^T<3)BKkX42e9@-V&DL? z??dKwrOHPtiR~XUIy)oBM9(#J4$?g4>%v`DzxmyQ`;+1OL$`oi!3~i;_(4|@apg#l zc5@HiO+4?$^4(Z&@B(5_uyaN&^k?}UN;5}XZ@aES^;fVQ*JMTJ*P6UT?;K)dJ~`0j z5S>#EXznz&-598}VwqVgvmTlC#vVMg2$@#!PW)d5-h$*7;rukN!5rGO~K#AEr75Yp*-GPj%9xHnPMdvcyvOi_oy9;B&pR90cz%_inxF z4UATi&o7Y&4wI=&9#eZdRK`9A&Z?w6)p$mA%8CM?5cyNFp^CAqLM8(}pD`*W+9#R& zJH0-o{AhP!9%)WHK&=l=MA4T+^yOG&AZs(Nd)h<-_8i29acY(1gmTsoIv4&MBe*1@ zzx?wn?vj(uorS2LMdn=Tk5U?M|0bN*^nUI9CRuT-FZ%-LQJo`RtKVN~>h_28I>c3a3iqfEP+C!L8p6TMDm;zYC7 zBG2zavJD$N&FylJYvP`JK}Sa;S=RY#&%4+&j}}K#YaTKAg5GB%o75Zb3q--4D?X+^DBh?c3G@MXtMI8N2K{rJ6SMlTv zjNn%bD~ffDdk2|8ua0KD(R$;Hg8SjOx9Dj<`m#agonrN>{k! z&~p#9D(G)JK8dM?b~~0-FbY0>Pta2z{=4+6KmL3d&Wm{X6r2~a`B~QFKfoK$GMl== z0oHD2bOJ{0YkH4!c5tWr8rtgdm2{t!U{pw75+@UQ+v~iq9Pr4_b*wutf#Ht*IKa_trcz7T9G|k zD>_|eA~RJ+)=R>P+^rn{PCT5~)+Lr$t?A5l_K|`2(e5(Mw2@`xjb*H~$7q&_j3JAT zq1FlHPvDch&U(p^Gq`gpjbd!WA`W> z-NbtD`;6Vo#6|}h_+>KiXtVY%tVT3DZKtj6-2I7RdpkW{%DuH(XSBT(4VBdW0L>3E zmp{P5c4BNQk=$Nb$LiYnelpTXGTxhHT_2r0$Py;To*@R->AY~B!H4UJrN@~IQ~V7| z>NU^azi6iMI;Yn-%*7J9_MbG#KMc%pG5KktGl<6ynt7aBC}rmYagW-k9cQv zovAX;BWQbszE`sUEz-5Iz2EU0Pvx>-f;H*N! zugHPja2}$?9(I2}W?k3Ae3`47Bg<8@-0M~5HnZbUzBdVL+wjaJ^^E(w-Zs6bcvF<7 zU+?Kve-?gw56-KM(E;vjui}ljuzwErPsU?!VgDS>&-P?w4tg5H3(ZLJo2)-GJ%wYk z$3ZwB;O%w#YpV7K%)WJK-o!e56xni>MuoK%%@g5lhO?hl&{lJ&VAgPVv!^0+7MYXO z%4$!YsqQ}J#ZgwL>YuORnP>6LSLpAT;p~C)WjGJuvAc=(2he{7o9{*DtkQm9XwxIc z?mb584J60Yqc^y^hkbgH#=pIk)zNHn`-9k@WxOszcR{aK&)aS06v)g(EP0a5FpIqL zB$5C7>O-dyYx^^TjnrDiI^m>7tgMPn^lCJEnkVfw8l_f;$&*;S5o_~iJpiXvf0tI~ zrrbgN4H|XPaT<0026Ea;LzAChDDb6s?h`r}Jg&M=(3cI|iJoANBk%bt<4^Z~UuiPQ z45E4l@%#`pZMhF&c{iLT#C*3oVJoa{^bCZvFZ-HutV{T|i!-ITSt}CBUnf?p3g>1z zr&hpeE#!gi?91eiZ2z6^&SbTtc6PD`_@1^N*`YSO-^1tM<4k4;7JiS2TCBg4?Y_op zbIpx!VY6_&d66tO|DmNjFUkDgSnn+zQ^P@KH8O*=mc;)OqD8RNCo)%b?oT=aJn*BdjtkGzn zj8y3OqVo(^one0McY3rh8forszgB0ZuB^Pj@T0<64*yRSdvEHms(Zg8CQq~8sMGTt zFAHay;>dqh?EgCWi=Lk8xvJCs75LB5Y@_Wh>b|9P^f%xP1COS~8Lo`1*VmODnfqVd z_k@o5?+d+B`OZ4sdwMbVhH%cf_v-z^y{_w6SNrmkr}L{>&$ZVy2n)7{k~>kf30G73V1jCu}Vi8ux+?2{)t>sT2=z0$6erR=9%sl z$<;NA-Q%uC=|8}KSig;Oj?vb9w4H*#T7R3|{xbY~X!m|ECK?>&c72(*AH{hAZuERodMOy&QTu@)wc62z?p)veNb?rR_^f zI~k>&3^MDGS%;o3^mHj5nW?nwn6%wy+HHp30KI{_A$3Fe2jL%tzZ?E;be=-zDfmV3 zi=f|zejEB6^f{%Yla!85g1-v>Droqg;ZKI1jQ*F=|1$h>@W;XLfZqYXAN+pse+vJn z_+c%6SPQ=geh9pac4l!K)!!$h>it#gJSP(#Wwx2>6iT^^pn^S#fB&}esJ)E zSBm|m^a~$G@L^;;^mt@eAhQA+wqwI~#)w#r606Y~=o%A~Sn{#KwAi4u(*QjTdYFku zX!JP!O`On|$>4I@EjQy1=OTD9cqLa?ni+u~zKajPYhoD9C1@_8-5)^z0Q&3DUpG;T zCyXUPT}V#3`TMSk`th-;a5WshCkTMEXMs?^x|7)ju@-(#$hls%V%b} zZ_?tMCgb6^<@9CbMCG zNL{?){Lsu?IDY`=57565dZC%W)H(-Vg=7abbKGN&d&8iKaQ|U?`Y`-r_{H#-!e5Hc zcksYF$h0HV4xe#x&A42FzCzuvQTJ=~f=G@K$E}zd&uMY2%e96 z%vlee8_~HD8=l36XQ7EMkLYsol8cv|k7?7Wo0l4^TS3Q#~Ii-^M;re(VEiAKItC+?yii-gH0q z5Hs%6wDmN8Kz|hd(IL=7kpCs}zeFCNxcI~|F@sL$Hf+1iebl%MqKqw`*L z-V479eib?yWtUNQ_d)MNhMD3pQyf-74yz!i1-b>A+2Jrd9PD&VUfM!iTd*yLZ87-x z#KR{(s|cS}gwHC%XBFY?KxT)@z1VY)!Ejh3IjoVKBy!y3b3 zjp5R3*Q}kmD|u#(!Q6Ds+;reO)FqF(;|+#G{&dKnQQ|O497dT`bV$Wx$`^ zdpF?UfIbO*5*ad{OU85WgT4=$hmd&)8#ZCXCi*^tzK?(=t{f9r1CbvHjl74vf1N&F zNB>9Y|A;yJbLQ;N(NBhnkYT)UQumw4|2gu14*hxP&m%)l@W=@st2>X?okxUtCPEfK zFQD!N)O`SZPGiq$>?hJ3BF$q};F(py_o2U!JW=5j6+Wv+pVec;#%3EmjHAOidaQ*! z)1z6a-F&J#9qlF$zSxG(!M_a|rz(X-aq z6NZTYZNVq>ti$^b=Lv1JTdliquhiWp^p~JL|FY0ey1GkUqvuZEXFUBL`E7TDwD={@ z7Cc6=yVcX*eE+J-M_ODxC;WTxDR%)nzpmOiL-@X)HaMr<4+YOqj(e2d{dd{Rzs~8w z6X300k@|$w-Q_oQcYKOdtg+B}#jXcuG8~?Mbp9IL1kRY}67%|7r`PnURQqF<6k2sY z0)3SyW_d!%=Baquw_X)%c}CaaO$WD~Zz9Zd-8}c}ndigJv%b8A;hf-EXx@78%-bBp zIn|$WW3+JkA=5!y2a)6rAe(3DUEZT{cxKpH4u|(IoFjZ=+fG|%Ey^1xKJS%S8}ilc zo&?+5Y0Er+-i3x!XgG}INuD$5QEi@i+P_6Xa|`W@mZG+#3E$Xv4Ylwg$= zr>!(A)fs9(dPeh&?OE__p7CH0<^00ZQ8v2?o-LQFf5aOQcnryM`gIKXZzDOye7BC} zIqM-kRmgW^`AL!aW&Wb2-(UX|ccqKiFOlY4&Htm$0_Zom`bW@x^Lo}{<-aO;VSXho z3UE7n^!;|>DT{XQZpxno=X2oU`DGga`0!qE8ktt3fm&xd8J^8aZ>{pJ zON!mO!~^diId`fy>j+o(Q@0BJWjsT5PO;B3*dOsd&MGuy zpuYpY0)7B|3CWkx)1PPg#$oxE{6^L)inpSH=O;a$#upD9cZ zo9E4*Bi?x0+?~bMv&05b#_32EFB&A zXZaRpPW}zn0Dml3@2B1OZ|E}yZTTi*MxPv5iahVU#EAJAQ55Bk0FO5UB0N#=Un2u^ zBH{DCm~Y+?IYA6J5t#?oOA+(d(k{LMd4(6fdiuhN4EgYWYcX(gGTAY7M zzk}e;Xx{Sp4f?_x7B+ABxkZdM^VIL6l^gkc1%D12Z`~?X?ZiKCli8gr>5kZ?xIw{g>y6hd)z)T92rVvok+H zYR$%4-usB~9)!O&KUA)kss?W}Ufs;|evZjtc-7n)7w|pHOZ0Irahozv=bGoi?5p}z z9<}hB(`j-P->0l(26gi+;t|FEN%#xk&xc;8*vo;3!P&->PGgaILEkTMuXDAAu{dWf z)*S)8*ad$F`uAi1I;*dam%dqWW13=jl)jJPylqtqy%Qhu_J`d}kDBtY2_8v{yme*0 zod1UICeUx*D_KXacFkb!ck>VHIW2vh#G4Km@ZQ}j z&!^pZo~J|Eomm#U@)h7KoH?v!&vhR9Iq)WSOf}%2AjzqbyH|0Pvk3n>d%74`p9TL2 z%*mVAh|CMnoc2VYf!+@81TO`1UgvQ#=5B%BM!PRUe;xic+ByqoGgtS+{|YqnE~k9H z;co=L2VO(1325MqBC;NPIOT|9iG#Hs7Wzi#Q{bc+7@UZpr@G|+2*4>C4eKMi6XAr) zr590p;hPgPqQ#v7htctnap|Ks75pF^#>goL6DJNQnXZG(kHL(fPbB+{fzRw-oysk?2Y=es;mW>2(_%Gmr3|!%3shXh!Fv zZ2&U=1N@h8{*s}EtCfeaQK#M#{^_u7oHI~U*L5Y^js-?KL06wvQE|) z&(jfsq5||cpt8O6t-M~i^>bS8r|G28nI2g9+zZk`x}g9=3-Dk89=4jp5#i`?Y}gh~ z3@3-v!WrT0a9;RQ_)54$K>wBtZ&kQf{+5TYha1B!;r4Ks@b`uX!o%UQ@ML%k}e9Dr;F14)8*;%a7H>Qe*@)D|20^Sb(_=Tv{)9umLxIe{hsA| z&v?&RpY{IO`x9$`x7J%{Rd`)qmsRDh_cmB}@cUZT{Jz$}=zog-XR9&V9&NXVL_dgr zVBH%V9s7bcH1=rhQEOP@CyCS6{fRS)GjhlHk88=c``gnr#;hGy#lr!=OOt1cnGS323~J zxCKTR^o$jdome>0$^_ejorS+W1-w5v6daZEgurQd=YosD<)A0Xho12Ego)sC*e@I) z5S$Z^ajQ1^%V@VXDApQlwSvUY5 zSx!(I$ht+^m@L?F-juzc{h2Gp6ZQW>w3k?u^i@d#SrY~_j{}*XL3x2r7GPjOM&@MD zAlGCr1`kU4@J*f-$O<-S6_B-mAS<6h^~&lv=oGn`1$qvp9Xv1P0x1_sDeLrLg@CMX z16h3s>xI7w8%&+80y_kD3+yY<`wI3LumjmS2eQKsjtj_IKR8>scA?M@t#heRD#+eG zxPD7{-LgXmu)-K%hs9FLJ|Gm|g|c%92MJ^a2FvGR0>cGlPKL6x31tlzj+e5X>qe%d zP-m*p(*dJTc17XbLcRF{3*Cw!5%dcN2vi9qg4!Sma>0OWDuK1Z>%m4Tw+L((dRMR)I8cDY!Li_Ea3(k(bO%>N|FzHx zBVke4KP(S|Fc}UE(_uqcF6Geh!JsL8I2;+ahU0>&a6&L3oDfb5r>Ko#XE-xR$mcoX z^WlPUQMfc*5v~qbV55TA7_JM~hnobphC9OD0{g;)z>)Aca4I|tTnJtdF9}=?ucw`K zEM1%~O;@I?(}U94^x*Wc^zd{`dQ`Y5Jtmxz9-nR(=nx+@rKbja6~u-W(x+qLob>b{ zNYA=y!-(|U^!#u~dSQC8_B*{Sy*Ry6O3|VH)BdK{gyrcj(XDN52v?*xi{5SF`t(kz zw@2FDA1)G|htfyWCsa=Rnm(OAm%f<3obHh_pT3;&(y>e;*bDT_49HZaOEb0MgiNiD ztNKmHH4_++&obVbTsS2&B-4~>PPb=9ghMli`5m^l}cGDWwrWLe7MW_ux^mfi@L1N>AJze zmb$^|!F9vxhNl+;Ep?;n#)RwZ#+z|csA~_cx{k1@Zff21x>hls$ITLT`S>aMChiI9d5L~HSEON_4Ze`t?aE03XNnbx5KXqLK8;B8^ z+jX0R*>&5*=AED9p}IZtdH*M4|LM_>!`7J>1lU$+ko2xZ8YaZ>* zQRIuA#+U!6xbLl-3v#(|U~Y)cQw7b9bj@MoSxGLo3&oo2ap=`O<*oc+HnW zYo6-Op}qMiH#q=$bK$?0mwI#V|CXOLFKMo!&ys(j({mMa)1>cBnI!_tbIl5TUY?sF zWdZ(Ox#fIrgu>07BA|Jrx757Rdmq&N(VI_lvkUp7kWX%Y{x|YTZeF2$$>f#XJd;dWhs^|JP>c<7M z6>ff>AfE+@rQUcFsGn3nrM^>OX8jz2DfQ16KtC^#a!~=62FrmJ1z26b4p>^h9$0Y` z)&ZMDX45V5TMMwG0J{sYFL>o&Lj6JDh|miAGRJ`9^``{R)?bkFf|Qq}yd>pSDX&U- zy#9Lq@dl??t|8V?+)ye|DNrpC0|p6Xg+GXEgKxQ}^e}<`c03Tda|P{YxN6ToSq&ox|ZxGc1S(mf6NMz3L6qZb^5 zPBivw91wH^N>?@3HU^D3fgz1ejm?cC3wmJw=FT~78XMDOfBR(@e)B3&m zy!bq8R(yWE$9gJ$Em3TBCk7@mwwtI+)Y*L!!xInLMf(3h+Qo@+iO21}iQh_e*ky^C ziT`Qe-lwQfx&7th>f+({^y0^gzihu+JiU0Py}Ec-@!R&dir*=|;JC#X`#$QF_3h~U z1LxTit0Z)OTI!WX-DqjNbbzbhS?_lSf3ia^mtAG~S<5cJAf@ai%dZyb>jo+uBU2%} z$BJUhu8^Hxh3p(Fs&DeF3fT=-$PThXc8C?j1cnR94zgmD$c-t`<0hZ~8%< zLxob|sKAL^$`hjFG+Gl%oqChPN-HDI;C zI)P0BTLpF$V0Yo#K7oS*$8Vv}3draTkcb-~qdh<(txO`VELJE>3#CL^nM78ZL{^zZ zR+&UrS&M0h>k?gM5?N&uRb?Fox}(r;0m>xG$|TClB+AO>7kCQ`vWp9)f<#-{%3Df> zt^!Del}Ut^NpzL%7m$c5lV~ZENGa zdd;kwxi#|zrq?X2SuC>Inq@UBYu40s)oiHQEWB;shgBE(fxBIc|O^lydu17DgEBs?xUx|QN6EPR-Y&O zJRzK?`aESheV*>~wB`1h+h?xj^?9z(bC%!d)joe<>04j>owJrFt>|y}S^d^q&Mlyy z?N5PIDBW9NOtLdMGdU;ud~!i@QF3W=MRIj=U2=VLQ*vu^M{;*^U-Dq`Nb-2{RPt={ zLh@4bYVvx@NySpdsnS$ssya0&l}!y!4NDDAwWLO+#-zsIyxyMbNKH*mPt8irP0dd& zOf60=ORY?;Np+<*q&BCv-7I6NovA&k{i#E#qat-8bvkt}buo20)sxDnwpDx8iPXjF zf2QqA;G-(C^-uMduG^t5q?@giUV;%3140Buh%rD!L_~&7)(A;}03pOAA~H-wKtx20 z$eS^s5bYApjrNT8iT01W!Uv;6qWRIH=!9rlbV_u3bY^sJbU}0x z`%9wBqEAPkjjoBVk8X%=PV~>W@0@?GeYPLn9^Dn)9o--OJbE;GJbLQ<_#})+!{X@K z@WFIS?4NE=x~E<{&b8}j{9il%>B;FS>CMu^=^5#*)7$Y3*yDaK{TIhQVVwWh`2Naq zJvYzi+I#MO=j%B)Za>qHbM^45clieRhV!Z)=^IJX_mb}=GE#=73@0DwdF&lg?7N$H z#9Uta5#PhUCDg|ExbH=3pK?n|0rk=UlSME8xZ@fu<#nB(x-3)|E(%WwmxZTy+77^ZpSa-J^%UnJwM{t_t3S^9iSxc;RI?*BAMkEcH$?T$M#*e z?_}qwmtX%m_fI_ko6wHX`=Pz;4u%egz7CxToeupFIvsX|z5JuXfpCyrG~AMZGQ(Nn z&g^o+dEwsSew;p--H31j{}hKy!l)6q2$>pMLq12L!H9C zqbbp5(J;G=XzOUZXs2j)v^%?AdL$D1Fg4mYI*{G4=%{GxP~Yg-XbHRWND-Hv6P+5Z zVmDj&;X<*5I^$x|dFRF@S{+>+ALCk`D#^(13xYFymEk3IBzwZF0_&)P}MuC(ODI-~S z{ZU@}8SeCb>id-XfI)B6|G6n%g%v``OG#7zBiFCl+GZQmj-{PUJCjxya)pf49ifJy zCe3#?3pcL}=7-V($3rbbtwL>eJk%l7rD@yX@lbB4XRt%454--MA))+W|HjLMQ$j_d z38Avkl+g6h%+TD>g5aXiqR^7maA;ZR=}^zmv!OMi^`Q-^!$OQ7GxVv+k$lVD}*v?rmEK<+FcOYD6<)^twrGA*YFZEF4BbFcNWcjIfJoQ+;{F59% zW2f7G)3Nn@onLn@p5{tDnP#N+PIIL-Of#BoJ?H<5(SGmzX!|hQ$rqH!w6%x@Kicwi zWx)yCHA(H1n*D2BaDQrdgnOm-Wj8Q&7`suaV^d2~%h^p$t>XOIsq<2+Qx|joSbJs$ zaGP%S4dH$a^9>^>xTAhcbsr71*N2`aqo)mg+qkC+>&iq=8*@L(s0RJWVMjs!(sfSY z)u4Y%cBGy=X1gOKbbjz8!pXrc!NtKH!Sdky!M*GT+U{V=oZ#W$*TEAUJ{&w9{2^sd zszax1uKL+|*mi!^yRCWnH%n`)9qX{_2ebT3&Gm0h*Ar=bH$lDHvn@ZsE?CK~rS+As z^Usxm&*{Nw*7NMAG+)zvbFfqM>A~z^aTF3HEOI|L~-sAhke$Vc}NaG|;*`nm>b`TIj>?0GTb$ zY2g2lbnn}m_Uq})xAf%4>Be9A)4K7`>BIkN9r%{^d(q5uJGB0KwGI=FL|U^AL0>)N zok+1&v84nKV$NxDn}HfVsfU$X$c*3EewS3*9@d zSyZg_^hSCe%IpIk2F^hGN+4?1dYl+9(LMEOT_qp8XXyT-wgH}PWZOX?+&zi2Xz(A| zGO#R?#91+@XTN~z*(RX+RtaEzz_L3a210u<;YJTM9>o5o?}c&LN}mRd0@nd&qa_Uy zUXJi!gg?|+dj{bqz#QBFLYcZ`{a$Dio}tm%1Ue+gVf{FvQ@2XbnsCOTNd{|Rvz6kR z0xSTAIQ?4>m!UPWiHzSvPplK+uRLLtDdX@lPXyryfWHOmnj@YuI+xs}wE~vm**B1@ zwGTz81LywJGl=nT8bu@2lctfrLk-SY70Tef?k2z}uoZI0>pPEj%k^n_25sJh8a~r{ z!#I5u%4=;oXEh2Zy@9<@u2b~wKamUVSkNggGI1YpE5gNA8Q=zeFG=KUq^$^NqKsvg zU^>$GBkV$W8J?Q2=hra<>AitYt-pxt%m;?i2ECikdQ&xO4O(=zc85lnzUh;x?~jOT zcQ2IGw{ogIZS{>Co$F|+a~%rW!&cPl8=5Uq=9KFnI^C^T#U@u*eXj)R`7W)MG1K|J zzUdQdf!uYvy#3TX(mE$ca_)V)HnB;+SDeDgfA1cHmdw?N@|$#+N3{kcgZi;5G7;|W z`~>&c&ggX1bEoSo(mFGB?px5)I0$cvxwOWu_DIh~f44aG?Z2Wrt#K#@y<}(V@D}%n zdJno@)FY3S7EZl3?uvEB_$);W_5HhQ)VvDXK@ZjGTVq|FT60lFjHN5_l$+OpL+TN5 zbVb|tU^FV+S_@XtrFZLUqy%+IQSH<#Clhs6yLkOO9?-RU{ISar=JE7=qQlOk?hJi{ z3C}+0I;qk99k*?<<5G=vT0`0sE`1ZKE6TKY-NE5EFn3+`vrZ?{-$p$T;MU$1&bv5# z3*I%-g>3(ha}h`AbLl=L0dqSz&9r@j%^4dL=y=-JcA<1gC)#=VQ7;`;SCD7kB7B!a$ zt=)ZW?P(pxyp%Yf#0cn-+2mQqILiTzzA>$7?Vrv&u^06JFfZ1-fY+FQ>QC;s8E;3g zx47=Z3h9j&)?iL`AM??>Ju%InXX~`?yKXv{cfog9!_#!wGZ^6mq;e($=DvCyU8_=)QVes*Hr3G7y$3&%Vic69ap6`Z5-sU*#xnMt*n zyT=j!9&@eNc0Ou;3S+XzJrU#it$w37SLY@@ituw-MGMhKy*5Vb@u7Ecmv*GP9O0J` zHqiH8m|tr~eh+*b_!@E#VBEe(Sg*rVdL4>YdKZf=urIL1RgE$m5Z(`#xCHa~6?Se5 z@aiQ%y({OTFW zgRrwO8mK`a+*_mL7Oayz+y{KD?m<1a9=%`kyA9?$Be5JoAAQMwKza~WE@TlW#X zR0Zxj9=jMT?``sb+PG@`^I1%ry!d&b1Hp``N ztmAEm#}d9J&`{(4%eoOc4)jb`{&e zKV!f@V>JJ8$}FH>727bjTU?ufH(?CNVcc9?^Nu>4dI&Gkb4p7vs%K)Qdc?$Ax(t}P zUEKcy=HmAEoZ5L>esE`LnZc4Vbl7>&vfzW-4#7sop*pR} zvqqn8qFAq9$KfCJ>AYL-M^TIuZ5~cEXbX4Lxb;2m5}a$$87?}(mQbmdctyEepKiIj zPSG2f>pBgofMo!O7ra!izpYqq>8$6%shNL2#kt_-R;)u6XA?5Nqf9MAXopb>oe5vw9Bl+?yf<2JI=QbY+Ns2)_ZeEL47mdSE@! zvNlVwEl8H02Ur6XJAb_IPq0U6TS3LW@7R+-%T6(DQxYu0#=Zl>Zm(qt*U}auV9k(V zWz~WE<^p9bOO#|;V}w0Kg0(@*rf>|cwXOQ1cLW=bF<34Zu!&62nLyBU5`(>6TT2OS zQwrE1CbX))mEj-uAQx5?2{u4uu>5FQ&$KL!3f#N5Y|GkO9qvHNqX@r;@Mt^>ohkxz zBt{_}+Ei!;5L#L4+a0d`-u@ofXxSu1zXWDYSXNCf+qGoe0G%Xctq0hICF|{$J!tF&VQJcF zgcPh97xuCf_Gg7P8nEwa*`F23K-kG8OGAaVt}I*9&{*Qas<3RES{g_!OS^99#IUse z2JV(`;K6D6((J6OTMlz zQ1b6c!OXIBs8e85)v_v1*1op%|Da~e`nkXwvuuvrnt#BeF05WlkC1={6|!su--=x9 z2+Oj$z$gh=^(E;1aas0u!F~ezi!7VMF0ip>0i2*?#IiDMS(SBIYaTn-vO8?stHqub zg+N$<6|m54StWMiDJS-$Wue#&u5(#??RJ!eo+Xw-2!A=yDVF72^u=K@SeQptkMMqs z@CmYQ96K!=@i=9YEbHwUXE*j^%xYm@`(653>%_r~2uqiXz*EqRB47(xU<3sAJ3-@< zWl1@PyGOXgJo%#V7b_`u$HVUdCEgN ztUtT0Ho%&>c*vd?osJXJvU}^q>EP&yuvI@8%z^dh!2WW;lDn9UGM4r&oDPhDY}>xp8jInn z-yjSPV=l{r_6#6e<>-d;yYP0j!3oB;HDzIa;@8%q4!w@yjj_EzXex_A+fp5#s#!c`uvTkv%GBYc zs6}6D!6bD!`)aYeYSD%|?DSfT2e8XWkg*Y#+GG4or|4VixpdoGvx=Y(mHT>t#yLvmg^^o496NHnHrBSA6-B* z3QEUiiLAmq%1cB}(U>V&V(hpH?-r#NH})ACrT#bG)IUlS2K3E~(#%2q8F}_%F*4`| za*?0?R@9cRW~t+H3M0o$ZXQt|T}nB0BekOlQYCq)F(p%Lx{Bq{zSN$gc6t&up$2pr z^FuD(#PgDFr+b+b6lJnZ+l@!_3Tk1eOKQfaeH(6Pclr%=po{e1UXdrL;(XYo=9EDl zsRvz0TIw4>!|4_(q;WKnCR1g-XKt(aOpq?7PRt3tSmG2kkVf!%T|~uv&X%#9^y4$P z*9%WQA9k`l)R6ku%e$jqcv`*iZ}nf&2ll@q%HyL`8d4gyq(*clT}#(9yE|wQjb!O_ z49hZ;=vJC;mkP0bo3>$dHgF!W8n_s^bbQ6+64!Em)o9mB;2Pkoz|Fv&z}+RK zw@q>#0v-jP0G=tCTv*~Jpa+-?Oqn#MXuP`_FbvE9wg$EXcACsJxU+$Iz&^l%Wg15S zi-0A-%JRaJO84|~ZjgH>a2{|Wa0zfZ@EPEm3Z6Undf;Z@F5v!(Nrh$Zqrl_9QyM)~ zQPHlwCmEOmYz7QhRJ8Bx$pB^oI{>p8ukz#pdjtCc2Uk>NweySsjs=zhr&ZimP~n*k zoCmB1F0Qz(tirPtxE#0=xT=zS?O6+a8Mq0!6}Sty8@M0%Iq)d(xW=R)um!Lcur06? zFdNt%*bCSfX#KU<`UcTwoRh;rN{b5#O8G@}GP5S(d*{cHgF_ecn=+sDX0GnTT-=xW zwI6e4e`dph%rJwQ8HX_23}bE_!F-d?3`zW)pZU8nGiyh@$%@grZa#r66E_8=)yCKxwBDR$Z>%b^L$Ec%DjMY{{(ZMH8 z%M1FK(4`mftremFXQ#ox;S18gg>ms&-HuP-tX~-xgl|-Q?YlUf%I8qHUKn3B`Xu`a zbnr>t_5!}3_CJYEet#BS@rxlrO@0QO{tR};+glO7VX0GG%=*f_5@>y4L+aF;&UhE- zQk^)rGnebaa!EFq$zf?GmnD?$ENSHN>D`mAi>uN44a~Jj(`9tb`fZu@do@V()^851 z-_LPf)9AGDh$f=R#$vkt=%bhLK#b>urr9aWA6jvB`* z#|Fnv$3e$&N1anS!_KzO9A{r=zO&R>yXD zN?ld11+E&`D%S?rPS-)#aaWyNxx?rzMZ~{SWR)CFZkwEw+&4Ktxs>@#?w&*Be9zkcntCOf4H{<0#~Te!h>u0QUQLbcni{Q-e`lkk@$n`nzZ6(7&`CdAVc;_VXRc?t0$3GtHn_GizC&%e55d^{&RA)e6RYqrP7 zbIarF&n-)cCyZY=NBr~M<|o9LCB)Yz#J48I_b0?p#K*h465`Dg;+YBY+=TeRgm`g6 zJYoHGuTF@sNQkdbh$pO%?uX*{X!lbI@gER(u~gB7&&Zb6s_Q|vZ`ghl+i!u=AwEHR zv1B%cWvF8Q(wxd)X>+NX9-(Ell2+4t+DKbz7wx4(bd*le8GfNxG!)H5)UKz}_GjAu zLfc<%`)h4~qwVjs{r$Fo%=Uk<{5)m*VcTzK``NbN+xCaq{sh~fYWs6-e~Ine44U_{ z?QggJeYXF#?c49a)_(uBEo{F-{4DfT@uS`|IU(LGA)dhbJ?k?s*Wb$%pWiE?e6K$7 z@xf=}<2NTT_t2Dtc<1<&VQ4Pm%~+bqVQHW*zT?O8H~bX-Zl6O7Xff5$)3l1#(FWQ= zJ83r^q$70PYVFX5wr_L((AKu!#rFHyzCAlb?b#VR-S(?(f2r+1WBc~(4BcV-_UsHj zYWrs_f0#XL!-BS-Y5TdhKhX9^*?y_*Pm3S*;R&NUd;;PDmJ5H~xLCE=ZyR1@`{lN8 z?}g!ZZ-+0l{Z+QV!S?Om4!8HhaCH9CrBRcS?LA;zt@I5wryL*$a7}~cU=k{z0$hia zOY+9Ktzc{fQ&ChJ~zU|#khMZ3WXM#*YfO{3*PxsVFvBDshP6SkYCX6 zAh~`K|nx9+Q8Uf2XDLJNX?wF29$jsYafW zXK0x`EB{4L%38xqe=rO~(O--VBZJl&Esd7+g3-#jl-3zpMps&|^&`GYmaWmM7o#th>w`oNfF%%+cw zImW%T$GFd!PoEeM7}a#Zc*s~ppBfJv57TGH5@QK{Zai*0PG1T=ayB&jRZ6~a&*RY&1dSE?&TvbsuLB^sz~l`WLI zM&$}q^-y`Dk-ARx5`J}q>LVJfn^Zs1R1HvrM00hsx>=;Dp=ziIseF|$!m3o2iij#x zWg@C7)omhOO;uCHMQWOwCNk7?HCUq&a zy`WwYdFn;=qPSMQq+Sv|)hp^1ah-Ziy(W68*VXIddi92SL-bZ}syD?A>Miw_=%e0N zZ;Kn%Hua9^tKL<+!~pey`ald)AE}STV6{(uA#PSj)DbaS9aG0dfjX{^i$Zlmoe)Lp zq&g|as8i~c7^_aJ(_)-Dqt1w8byl4f}+-u3(P$8TJeb4%e-DJG5eS|ilt_OIZ@P@kC-dOO7m&+dGUhzg88a=6YSOs|4pRt z`L*LSvkyxpWqew$WhrDAofIy8CzFy@f!1@Q#xqt~Da?{OWh_#dU)4mdWyQ`LgS>I| zN{>ZK@%hpvD4j$N`CB55&usmdwhnmia+Ga{`rF{WnVPGav2|{R|0|8CW5SR1BQFbi zSV6MoakQ*xTqa#*Z5F~a-T6#}2Bedod={@fU%m?k5HyWt+%n2Qt3$9Yx zd@_B>yL_M;B!}`oAL{*1<$M3({hoLDOxeSyc;CGyZ;&_g{(it`s&{2M@03sESXr!A zsxo<(oF(s;_h=4~_wnwZ&bxjF@AEnGQMpt;A(zXiCvzJRZ>uZenuSzsye{$*;pe2e$|)9R1vPwE-(&!>f)M$%f*ceB8LyxW*V{+-wZx zy?m1Q@M+$=XVnkJUB+zQv%++kPGi1NZ7kxw>oJo|uPIG~cd+7}n_@iYYwBxe27E!4 zu5wkL>ZSUqer8iM)r^=InHQUvnwObv%=TtS^D48ed9`_s+0DGxyxzRgPky)G=QsU+ ze^Y;|KjOd0f3g1(|D}PXfWhKKN+1wu5||Vy3seNA1nvm@E>IP?D=<5756|xXEF=7# zXIIMt0@t!-O9y|X>meQVhIB9t(!p>@2O}UIjAZFx zGvz}%7zOE|0MbDrq=Oer)sPe(gru+#lEOof6c(|hFoS;2 zlEN%n3`yZ(mK5%z$5>K$fR?hP@DM!?NudUk!V{1bmO)Z@5|YAlND6;|q_6^#!c&kG zo`$6GN0t;;(@IDRfATf=HK%7;Mu^g%`JBIoR`EICgZ{$j{B^XN&-okZIX>raqBVRX zHKFJEL<-VcK9R!o0-s1NXq}m1X3&d#F14cdW^1!Gy<}#Zne;NBQSImzKBGF&t9(Xv zrq|3aW*6FEW}Dgcx|w6<&_*-Y%%wN@?8>7}e0KGsH~H-9L!12$zk}ZLyZkQN;`jQ! z^tM0QpG;f*4gC#in?K+W&~|?_e>3`Tf0{pycKDnx zTmcuo7mxu-?+2Jl=mS1nP5O{4@Y6?u#(~DPJ1`(Hg7z>ocA$sVUUjj$L|v*bQ<b-!ApHmJku4p|^eRk^xd z{Z{=>-Kp+UcdL8UTy?LSr|whp)dKaXdQ3g8o={J!74mI)wft|nQ@$rZRDV{h)mpVq ztyeFrSJg(fNo`hJ)K=pbW0W!4C{R1q`|3lrTYaqls*b9!)i>%}<1+QKFhi9Pt{xXRsGH8 zX2?u8FEKASuQ0DPdzjamH<&m1g+Iw}_?17!-^3sEhy5)Ao| z8*olraL(oOCFY!V;GFj0oGft872uo>;GB-&oGZaOSAlc7fOEQnbF#rX*MM_!!8zT) zIo-iIJ-|76;GAp0IX%HS*MW0-fpe}0=kx~W+yKt$1J1b-oO2U6rytnnH-?Ydra#zb z0N7?A*k%yeW-!?1W-!bUFw9Ue%rG#_a4^gWFw96WOghMoVJ3rN%8Yf)Ft>tR%E2ua;Fd~o z%WdG6Dd3jd!7Wq4Eq8!hrh!#{3nrNkCixwhqza62Cm3S}7~?K5#!N8AEHK7wFvi_r zj5%P8d%zfT!5H^~G3J3W?gKx}2Q%CcW>~;%a5Fs+82Uf#eRr4?#rF28-r1S%>8VaE zOInd6SwKVt1Ot*qM9DdbfJj_&&Im}BEIBMWh$ImK0TB@p5D^g(6}<*T6a-P!->G++ zVcXC9yVvJ_&wai>zU^nbYwE4;t~q_qdrnpLRHZbd>C6dbX}0x&enWppKdc|okLt(t z6Z%R0ef^aFsa3(Mq<^M=p?{@+tAD3o(!bX)Tg|Li`cHhwg&TE~s+#tq|U)Uf_xeI0e8cSQ?DiyG&m zO`|=dy`z1j{i6e;gQG*EBch|DDYv!gFX7ep6Fmqk}ZS4G!E*F`r(H$}Hu z`K(8*s#ZEbcVNbAU;cbk+b0xv8e$~HO0Igcj=)q+lGD;gg^(&lCG9J>e07;2HO=11gEsQ^f z)F=H4`a~}ol8*&&B{c#Vil1w!5sT2RviKA)ds}APQU7Puth&snr!c4ar+n@XV+UVl zr?Hdkv&-1Ub>Cy`p@^~9*h@O|+g)TZ#}%R|^VxVZquZj}xJ_T$Dov6jwK8OJv{sgE zj@ZhP!_n3payjC9i9C+F=2I4qycSXlM_-F6E5}gZQL6R5^*v>?Lw1O=+ga_bl!KWs zFXd#;t4?Xmdyi3W)#lcJ(tqMwUe~YllU2R28n<{K;~KwUyujZz91Zi6)uTE;Su1K$ zgxivhxW-$eTgXuDX^NV*nT_jO!>Ymeu5H!kd)Kw<^1U@%<9p|~^Ygv$w(sV9-(%lH zxwy4>A3y0oKlAo@=AG>q@XWLRje5nRF|L=YosQZWs9lt6myJwrx#p#8s9ko{E(dCt z6Sd2Q+NGg(cc6B;QM)^D)h_<)ApY+F_CKV*!*x2UALZwMRlmx$i5ez9*K9_1exgMT z)V=4QORmNKOAM1_v>@7AK+u2(v-6{gzDhk~y2Hh&o z{`MXA1V@ac>;sM&$C>@l8|Rt#zcjvN-v7$@ig{mIZdv21ah2Ksx^bOb#5bcMD$gy! zC{@M`!;fNy;VPJ6xGFTY8Z@R=Y)C)i*2 zWM=Oj?aj>IH`?}nR{hobmD{R+u#RrzHjd<$ zDgSZHG`pRHTc&CD9o#0($GW?NW0{iN4lQlp%Pr8d_5X{A^QII5w(oROi^J zrcsk)BV}KOI4W(IUZt4VmnfiQ!`j=b69H&SZhm+_O!-0 zPbZG1n{hOwymnXEX*%q*C+xHr?6f!Rv=8jGFUQ+MsUOGOBd9;y>}VPQUp0{9?@2U> zWAJG-gyZm;Gz_*p0=7JgWAcSGhU4-jG!}L}p5yZsGy&E<5!OA4WA$}3nd9{hGzIa) zRQSIaG1@a7mOj(E%esrLN!j}xtB6&EZK{}6f?k4;TnHbz2tIN#eB=^t#XP_kSIK&i zmU9cBGOYkVtO7r*0Y9t*KWwlXS`F!CtFhIXHd#%rX7q~H!fHWVtX9k$Tdi(ZH+q%Z zIq9^+>SOh#omPLVKkc#xS_5e}m}C!_Pxe`(t2q+=1?#wVg1!JF{X_M$&{x(4>kGQb zEvs+n5*X=6Fwzw;(p50hPhg~LV5I9{q@Te^H>}^RKj;@k6~Ec+`001B(;teR1lg|b z3Snolvj|Pa93n~al?W-m5@8j82%TGBg+x@bmWV0V5*D-8{lZbKB|H_Kh!ho_h*Z0d z{e;NQ5kY5>)9z|_6}jy0c6X6xr`ze`4!f7#OXRlu*nPyEc0aqH$YT$%2Z+4(AbXI= zXAiN5i2U|2dzdI-kFZCGyX;Z+C{fTJV~-Jq?6LM(QP>`Dj~7Ka%A6pI+LP=_qL@9! zo+66d)9h*DZhN{tU6inA+A~E-X0*=#mhV^GWWe7?!ACLpdkH?W;O`yydl&xRgTGG! zH>HA`vVo7XgM)H_gL1;(=Yqe#6aGFAn5Yn#s4$qQ2$-m(y>(n%KesQ8yKC_Q3Wee_ zxJz*;F2&v5-6_QecPZ{#in|T&R@_~R7Juj0=Q-~^=X~xx_m3MsVXxR)E8nbS&(2PE z=$1;}Egjg`D#=`q3T`INY$o~ikePLp-3RfH^lofm2E4KU*~Y$6V@>TV`F~2-yGTh2GT*=-V zZR^%(>(yxM)0j1?=-3>6>W;b(JG+fILl2xs4<)34V-b3%?@QPI$IHZMQjXZz~J89VK{X<1r-ta?3TjM;;s17|&UY-oG zL|b-+zn%C4bOwtI2Ar%s5o1StYqpvN+Hr@XFHdy6wLJ*~bS8>CcA7cc5r@pSxE*!L z*P~26c~Ye6ldn(avNzvk@-U-QKm`3^B2A6J+)l&x&y=%xmZ1u`eLWUV1XKE6(`v6}#7|z2?Dp(j2zbChA zV?ed>2j_tR=V2igENNiUc}?RooH};jY04~a;(xh!UBHsd(5YMf==?C83I-ZbE&4@1 zpUUlqv&0(!;tl*FS0UPDvczvdqOnt@Ltn!s?q-enWGu9)Hm2fkqNXG`skU(dX#&A}WmEidz)Y6mA$3zBr&JSdUXY)WOX z%Z{yNCI4PdS13NI*_TRt=PI&ra1TdDb%Qg3i8)pJu349gciTB~H+qyNfq^--izz+| zIdXMN?tK$WJilM$?r)le#Z+k_>GxeM@i3hd=T|f;Q>oIF22^2`@l>4>-yU)3_vH$k zS>oILB9j6oWFcV)GUNtSMLWc}EsLr0L`7`zPrk!Rw=^zYEa}cp!=?Vj>JEF*{hZI` zMGS3F*&RWTcCPd(n)v17F}FYQP#RH-Z02uDUew$=$W#P*FNlQI^&BS4?&ubKl++jb zsftDfPfE(K{hU>xKZ@>Mnk$K0k5{fog->a9sBSAx(wxunI+ZX-^1CUlmVMb)T*LJ^ zb~;gfgi>@vYusrM@-J@{LMpq<;$BjEaRACH&6Qf@f9k*i9%q*-=Ssvp(pt~4C>Oj& z5~It^Sz6|Q>casN=agc9$)zhZC&h6<1wO4HJBf9$_1#7vkKUsBg>|YJ6xA#&{&ULu zqxUW2a%Rc*Nha>5;e*45+w@09zv|ZnA*I|JmMOwxj{cX+^WV;mUQsiN?PWw=E6Osw z`kjCe;7I3$EUJ=W!aY}B@rk5{U0YtMiDZ^xFXv8O0O5$FbDb_#>WGlyJr$wKJjDGyOCDkJWb5KWElnv)^X< zpBpkhoL5TznDISe*CFb>=Cb2I*A2NEnYiLz*275Y+e6}2iA^Zjd-u@ptKpuYXR5iZ za0fc7FFB?085OFpIsNc73hkUn`aS~u-8qevVW^BJR^#`l{N~RMUd#jxbni7H^*}aE zBSV@eW16Z5Yn@6TI%`_}?}gVy^@L~VUmkB%J&gS5`aG7I?o_l+2`%W#*J{?3ykv0s z&{NxtHwKvpJEQcz`BAiyK2hG8oXVbBpO!2&F5NCo){)d17$1l&0uqauO-MJ_>HF$qewb>|o0X4IHH?z7myFR~fLhEl#y(t+wzE^^ zCQX7%+vSL_^tF{2k|j-*wWSGC2KLsj$hCEZ7#T*T%<#1p782ryhs?+xk}6p;!Npfr z%ZeHa@O$=Nxr~%@BPz~ujI^WR`YV5J#gtV1UDzwp2avCXM)JHFy0+>$3EjX#E1?ulqv35va zsA4}RH96n#I_w>xu78};Y!~d#xg;qEp9aL&^i-L9gY2hRNK@B$&#)h}n?|T)-cJ`$ zX}?#+hfH&ne?-GBvZkUnRuk9i&Clq?#+b*+xjbZxr&1~XDUDxt(+p;=S0#&A-7Wke zks)2sO4S5bQ2Hi7g(*mL73r&{gog!R+DS);q5`X|g;yM&UrHw%BjKf-LmX9}{}G5i zFX?9e(fV7)oV)-ndBOg+({G+eEXcxu(jre40huZZLFG7|D1?0IM(h{~YHNm!ue3Km zENL^8`KQHx_o#g&ilhNTo$QdZ#kNkDw^TWmhH8&3PQOMb(le72h ziRGvSY&NY-whOMB&6P*m5(Eo3l~fEi-dyo9K*0!bPezX1uCYRw_-apve@Sy9P_PC( znvr9$YoX93w>praFO6rcz(Vz{wwMriKx>xaD@x_jyaaIJhBzO=#+ykw3fS?pbUZ^} zYR*WZhT*urSePcqSfPgHxS^OE4_$kfBx%^HtgGIbDqJvmHx%FF`)SWUMZH!Y z!48Nl-1Hc;+B|+{O9J{eznjPqQi(QIpr9VCEsn!C(ViWO`dxX1J0P)eGi=Om^Qgv_ z41AINO=a^K@+*Xs^K~?K3ICbB(ET~F@-RS!I_>yHuaUS~yzI(NalY`wu8nSW@hbHA zXBwM){F*^N7ZcmL1b*fEpT5b$6(^y)io!(`zD)iUzakqt`la=S0^ss+sbaI+l}~4v zxOq<8lfF~MN4lo_=TS5|>v;JkXo&iVrmp_$0Rm&fXUz%QOt23$Fsl%+=2f$;FGN+ubNqM#bRIWwDCSdMWM{0!Fc& z*_yn?bdwrc+ozYR+S!ZGc!PDf+%2bmh5j_e&U307U%mHZuNhKRT|TazJP(%nfHW*f zyGb#Mo_%&*?b&rilV|T;^x<2-Y(!-FV3C9^iI;1$$R>EBz6h<0jo~LtzYJQwQ+Muk z)c0`bZOdlK4&X81HQ?Xi+2AA0zQ_`sAelh7sA-s7zR-2<^kC)}<`L%8%&M5koKTto z9t2*exJ-Gi@b2n*tUF=wOJx;Iv`w%bG`sYAP4XVKUt~E8IV$o~@L6QVWi?C;P4FJz z9<*PVT{m2(U$?l3#(-Uz5?buB+kq3VlexCe4=WYg>}auD3@)d@j6p|T;UMlP^J!aV zwp8v^jalN?*Gx~j@8dsWa<5O?_cl{DGM~q)r+~7!dlI}O)WuOpNt$DL9Q3(qO#1oH z@woA`YIM%DSIEgzB*zM+(L$oRB6~NBw~J-IKlFZ(sk26945S$W?IrPED-ParcYDac zUsQ^gT)Ge9l;b`D-1+W|PUTMvmX?=>>RjrSj1TtWuD)?BdJSuz+dBT_v&nXw;Ie3I z81Y(8-!bK3$O2AyTNH83F7GnBrSM#5!B6;F#5YW7pE9~|@qfvRnP8Z(wMc1rtf_vz z*yrWA{IN}-lhs7`RKosji!s~wY-@kH4=8rRj`#d3T@Rp=5tT?#XT%+)Q?DdEq0jWx zYgb$z@N&v=wiIvto}@}rjb5U=ny@5hCF7o;;PJjihIvm>C-q1AIhr6|xqO%yw-|S+ zmh*4Z&H%{oIUH9H9tR$l)U*0}QrGat9tD9D zkA+S~vT0bQDTA#f;~INqso9rUGi^3Go$9@Yitu2@L>K}8j( ztSoZwhs5!Kgta#nkUBmiU3y$h7FNoM1S^{H)u58-hkF0Rirzl62gfS@xvWQW+b_MH zYPZF$vlP|44^2GlYv-NE=Z*$09<4QPZL4eNoK+ljrF7g!h{F#ZM$7 zc&@UvBhTlrTXj}G8L7Kr?WQcJlAnu{R9`iSlqB2KCzb7R$|lht;$NZo zNVezev(+?vG->uSM6$LO{L@kqC&fx65O?S znl)BN-w<&hD623XN`Q^81J>j5sg8gUFUr1HP z#T@RI*687K(LZMS%3Qaj_tV66EftH~iPN@pXGfe-pWN-O#zJ!n_oh+4U8i5U3_b+{ z^%{O8$=_ENm#Ks1j}ucbbfFbV%l8D&Pd#Xd&}*7 zY3D^C?K_h)LzlhtVW-TNM2=S~#mAZxz$Zd}pPsY5V1?pieOn>h=spxGZ;@OSwW^|F z<6Fur)M~-U5YvSFP7g<`An$t|eN_!&735Jswn6Zu6TZS}2}X#PumUyZD#M2f+Xi(b zVIw~vU`=`4c6hPV%q%=$p&_!td8cyQR|6rLT{9z-M{#XTv(!3=ds0wr+T z=NZ61=z%fK#wTfU#PRrzd0PE3tb&AqZL98Xp8#G$dEvZ1Kfko{=F=RZLb7??dJ)68 zNfqg2E3fJp$BEP0N&2mq!G>-36M`=0xUD7Ec83m)8V5<{F+Vrk{JouZ`Nc1JL5c=m zm(oG`UpSLh_zNC(eNSAp4JbF_CgHnW{5Fxs;5FKnx|X5c=3d{v-Gy?8HULpc^1T=3YrXZZQ6&|V zbHRzHoqgo>T-J@97oUBZ^;}cMg|T}Ddp|=n`SIt#+^#{!7s`wIkLej+;psXY!t9e* z`c+Al=@bo$Hv1*y0S1+sWnN5LT|i8}fnQ!v9U-T+xR#`8+ZWn$#Rfu|qz4is6&;-v zd`oem^AVqr#ouI5>3Xwsy(1rA%Vv!64khw?(v=xC!2CNlEp%;`DbRj&50J@L{lp>;o0MZcA zfwsaI`2}SKTc`kKg;^*Fx`Qr+1!0Kr!GU5$_+XCSiR8dvF@h{$r9S|&16@%7lh9rm zU>E2@4$y=MA0miZq!sE2N2C?z2&K>l>Ik8b1Y{3g$PYRfX+;E)i+DpFVT;TJ6~fA3 z0tAC@;AS`iZ(wFnrE8#ONTeMA9k9|A0KdQ+WPo4L4g3su;0^2yrgRt7425(6-~|>8 z1%M4iLju4Cp~11U2BN{RBZ2dv*x!K-0U@wpEP!|*8ZtmU2o0W{GY}1y9SvLy#ZC%# z1oXjzkpR?z(U5;Z(QxeSfzdGRDBw~kc4Dw4pa2%E1uzMUMhEN!M#Hi*2Sy{ZlY{?2 zUBQ3|1KY7gXra=fOvRwmkxZ#TkP#Lr- zF~~}!7#>6z)Q+?}0L4xQb_LABg3$mRfn!L3hQKixc80()1a=beDHJ;*m=xdw1J(t& z1dX8su7k!9*@Xicq3WT)!BF*BU@-tHESL%q708GJhzepvsOJe}gsz7JCqUKXfmHx< zuwZt;XdojhU^Ivkv0fn16lxeM0b4`^N*mf#5=tA{lm?UxZHf<47tw$R&5LNjYLkg9 zKxxC8DnMzYnlgbJp-ss^ZXyftpj(jzSZxXsE~sTFQ*o$eBvWco4D_xUfCbizH_#TQ z9vNH=b%g@b3~a{{;euU8Gj)MlhA~A2F+rO$fT~1(B7p2fenKx3i0H##eFSO3g6#oq zL8ll1pTJY-`VWEa(2x(aJHUZDMDAdgNks0Tmk~`3pq4RBg+VXSy9xjY7%z@MQYdFA za1hiL0jMdk9bW_kh8Jnq5Xy@LydtuW0f-1Bg>j|}Y=_+?16;s?`b02bc}YcLp?G0- zrJ;CHcIiL`(7Sqo3m7l%KvGy|G;kf1Gbz{!Fa!%m27Cz2LIPYsdtriopm*Pc5=9gd zK=(oIn7c1f&M07dKnV;OQA7ypN)(D0eU}q73%x52pn!Q`4a|aeMgXTnU15Xb0)6pB zCSZ6`cWt2_-hqcigfIZPFb{a(l5}j$>DCC#HB^Zc@_aW4P55;|@Gs4RRps-S)(7a^ zL*C$^7rH@-l3q~8-orSxFA_+90oVmunm!7$meVjyB{VkE@MqZG3?@l9$kW_x2?YPN zbNONSCC`rk>&j0%E7m5%R8XOSTGGf=h+0Ta{{9?!Ltae23@3>n^I?bx$_tkCcgW%9 zH&TEwA>_g-mLR-GT9DWqoXTMVBoR;gdkrK}8sHYIdNAkX_Kg1J@?LGDhjY?NAB`cY zNX4pL+N~5=^wg*)W@E)bR*37G(pflq6ZJJxEnW%_ALuynP|nitFvKF#1va0j-acSd z<2}}`~3N!7sL zWK3mFVV~MR=D(T)+841$T-wKqZPx1)M{b8t*f$8F=IUgZvM}ctw!m=Sr;@rQ$2j0K zCAab%2zV_2z`DM(xO#!A_QI8+q3DwqN|39WSCOkJ!Z7hP;h+1bN>@&<=zSHXgH`(V z0Pr_|*jyV+dl(E@+bL+WqIe!-mmEv#0kR)amVpFp!17b&YmK|YX!RZjV0J)2cE6(R z3^E5i3g{5ky#%Gy6rcPq4q#jVlO&u9UD?Oo*ga;`EwRF`m; zFbXB6e354vmoRDGh(hT-jeQ=Ko=SnTue0wGj_uUi(+OXqAD;DX(K{ND?ZwW0PYrtfW%A=*F776Bi{ATo zaYkPd-fbiwBS4nPia!e}CfnbX2#wU0F7o7Bgr!$k(_%!`u+|rRir?NrSc~t{Cf#{f zwY9nB*khjhH8kYjPgCVs{0v#DJz)@;{MF&WeUZF>DVFx`s{#8Sw?+|b{k2Q+>WEIc za6N|t=|qx2(wTb9DUuVNs{Oam3{9zFw~Wo%O5Ez%^PaJ3sr2`9PEZBBBi^kRBf=jS zhHj-?$CKK~GzALQ?Fo`O_E>PNhH+1q$RCNYc40??@A}@1(66&Wrr#qp~ zWNvSGLFKpM>(j3>Pl`fLqMjYe4!ZK4asfysgZl-`u9g*%~%K|yeix@OapkhBJ*3IbhT)v@nf z-Y)v*2g}v5`|G5}IafHhpcmiNA~2-M^vhAx60=xyfMM>C5yQOf-BlP)2Z8 z(}Bc-WZ%)8ry&j3##8Tdg`_a?+^jI-aI)S|P~M?m?DpV>P)occYCRh`lX%-xS(a0d zX~DGm{hal#oPRibgsNk_BU?-A0{sGg;X|{(qaWnE{Choo5uDeV2Y?h7amlL-0S!iR zno6~B^|#iwv529Y7m|VeB1LTa6NMzw5~&e=7WHsnX9+C&Ctm8fKR9QBMJZSBXc{v& zvh>n^7b%jV_C9>!ahXgH)S@X2teL*-E#%Fl)D7C{$f0 zC`R|zXi)KDu!VI&V&R^(VbFusB_rm5X{cdHd!(H#mZD7KN{LI@{aWVB=x?4vFwX?eNVrf&>C5KD?@;dCIJumdkb!qzP&ST1L?r0VcC(W^^J77~+pdrStM z(7~@qD|*&)Yh&w5$KpLKyow_9{dYgDwGNZ zM`{ucmKfuC>71GSGXfctDWnWpYAeo|?{!IXJC*|U6(w&Ggd)j{ z=s$86riOmRk67ZK6^yHiRainZ6^8uyDwJoeBQIx3&s4DYBjHY!zN*OWN3g~$!7tgB zrP=%C2t8ZN4`;<4g$oRVV5M)eEg|mHOILf*v;UYN4`F2!dz3k2cY2k+P5{U@Sshx?47V9if}Cbm5p0QUm3B%r>2C;dG&^ z;WUFPdVq%X%>=rL)rj1IBt7hoxXnbXF!u0GK`T9En~IKv%|kQBn1Q*0xxt7`Ob;BF zte$Yst24cE!9(3Uo2iDpf99$0VM@AZ)dj`Y{#RF0zR4T({%j&xT1P&or3CZU`$R_N zIkj^Ep7DD}Q~LN?FWHGdFRM6c==sFsOZ~eGvVEtYi5D7MXzYIYs8U4@+5F03S>vSm z?5m^FVNLc-Dl%ln;cC#?K*38=1#Js2CZb77sb=6C7g5*C;lcdNMVk%W^<)6|yxjv(K?3L1s357}ee^guC zNO)eP+I{Om_`6f=8M z7guL9BfGz(gRwO_3Og?wfEDnU`~qP80^s80(gOfEz5v*{IUxk^7XS|{D}-QU`vTx# zW&g+j69PgquyF%8Sy}&HIe7oc!wxAA@s*a{VJ82+0TJ zU;_ZzIUyyv0bJ~C0A7xNs=>trNnwR(2I0xc#SXbbXm$<&2OGz~Xf7@QJ16^J&&dnX z4MOv9K{!F;fjs|s9tam6NUb>_5;-CKIsU09B$bmJqBDdOCy*DS@jtTIxc>_K3mOju z5e{zdzn-0)7Xa~k06oBehM)a^!4H8P68Mil2<{O2-A&Uw z#reOX4N3Vo(*NX!)Cj`$e~<8A9{(GB|Hl7^*8jTxKVl8x_KX+Yviflt1X?x zxazmxxqe+A4#d<$Px{y;g;)vjBDaSWa4{;j%*^B>Z#Y{rMTxm^D3OQ%x}j!3A@2y^ z-i6E%dsv@thD%N=2F>n+H80e0n|ZaLkYV3V!ZXNK=V5+|tMao(+{FG_WrKhGYw~33 zy1+A&=@O2&nAKU0by*|WW*l3KviGCcZ!C@B?$QcxMLj(hVV#I$&eAYxiAYzaA5yaL zD()g9?1c*QKPY8?Cp8#6pZOyvt|bh_&OK-=-2UN{*NC^hU*UYQ$WX1$%6FjC&zapP z5MF`fzEAS%n!~}?42U%-ZqVH}ySducP2*eVjP)VbZ0#@kKf}+>%gg@nwqOIWaY9Bc_W#-*r+&zWYV)`EEf*cjP24`TiDu!$ zLdy}Ap{CO8G5|(OI2o-ESb#w3B!!RlYM;?c=?23d>5Gz7P7FR60xjS0JgQXglgpdc zs-$hIei9h%^Iwd=w=GNbr%bL{rD)&q@$kCH@j34}$i7}5gF}Mq2RPMMd(RyHk|Dp< z1(2~o_X}%(Ijcy&o3x4uT=istYWeZgsJBoGlc|jYtRZkj9B@`RoGi?Cu?!bIjfv&6 zyi_dXrcg9F33FKqJC!!{Ky1?k1DAHXniltz5cdeVE})* z4h`UCw{Wr<+8cWK5cIwye6MZ4Qft5Yebpdzf5;WINe5V&P8^XsBma{b zjM@;?J;ATH4-cu;8BJ%29y39B`2yxvJ}cV{sxSLcBIpYN4&G0z3qOVLRu`lFmV<9m zSJ?Wxhf3LA-WMnSB4la1N;P|CTGHSb(I3li(bP3hI?9D+Cp<~Kl1m-4$}Y=oz?2qD z8KPyf@2C&yQn%`Sf%l!bs%(@mI2iH9={R-w3=N zT8#k&d2GWL0#XfRFwF{0&Qx1J^XV4Komtw@yIr(`1Xj{M`geCGpzwCb-Ezk^s2y5f z$54@LG6pXoPrT(WKM?}Qx!1LVk&4VuR6%&f1n4$Nuhr@5m=?GI=cWzOxnz-Pnfx$h3QbmM284y?pqSU>ko+BD(*8e(XXA@qJY%G1VTnIsnAK0L*s+ZSU=v-M2E`mI+uQ`hN7q^d|+Wku?8`GCul)A22~ij z9!_qCuz`V}<8)C?;!g~958p+HBZE!ypj*gksp2GGVFpF1uRJZqa-b69Nj7nIBUx!& z@rb_@dExv3aQDnj9k3D=QyRd`#+P)<@0!PdG1NFzV#U{qFNqW&+Yc2AA({#oz}=^s z4EFl2)Ps2)|HTOU$~OL>TWpt03-1wbzt?!z%aQ2HS1WiGgY3I-FHAG37vxdB9Vvy% z0A*C1qR&_>$TuA+~?eO-fFVOaZEAbAY{P7OKXW%dKn0@1yxVv9A znRo3Rxv#1ml@7@cgUP^6JxcKo5!qmm?nyC+zU*35FaBrJhYz(R56pig51IZ5*CIaH z|Dm}OykyuV13UC8Z3TcldSAf8A&4LkQIA9PKP})kBbF-`Ly9Z%-NmicORTGoOV+EJ zJj4f~-NnrSL*c8JOQb8)N>7*vBTtkT z#0Rq_OeJbzm`N&Olu61r1b;$r!xq++=q}+Fn5f(i3A{;+3pD>{*~a<{73P*Bct6hXNG9aus#Sd?k0Of9T~& z6btBrp#XHDP*8OtP=xs-*7Oa59l9yvH$s&_8^KEAlRYb!L|6QmyjMm%-<}a6jP-cJ zpUIzz9{l3tx>LVfY{LFwxQe)Jnwq_g2UGMdf&)SwK*B*?)Bz|PQ~}}7c+beO)BzY9 zloa?6s60g8II)q>7D$wDVOYB?@wtIrI0O$2@o%4ac55Qv-|+Z~n}P#AIPV5v!Cy5= z5P74$%BX>dB8eaP^F@V3UB8DoCR~C=!A(QE}a)6W&VZ|3rI8;l7-61020eU(WvX5BKcD<)KrV>!Y9 zxNYB%K?yu@Od_xvY;`nSd?h!n3fL^IkZn5P1E-jLzms3V%Ep&Gj%f&wE#ISR=u>u#u{2i_JDR=VzlO@sSy53- z26$O1hp*@ZUV)@|uy6aNq`N3ax^R;!MQVm$-zTG&yFD^r!ob(4!?@_k;|bcII}p%| z6ocd-wNy=`eEC3xC2C_|m)*-NN{MTJ$g4iAh8-%Xlh=3+}nGCL-`x-D8;x)S6NEh1oWfwPAKF2o+`wDZY2NQN<>Yl zAom!Yq*iE6jX<$wLfkS-`I`;Y0{s}a>9VH4=?$8zmtA_%YBlM;lXOk`(jXt1y zw8b+}MK@a&Goktyr|Xy~9$FmI5>f9JJlGJFxkqTtK`2?8)CfVjDxE59bM6*to{o_6Dg?B|NksBjUj*I@EG5kZ+y7>cbW~KPu zySY4~KNRvBL9$h!Lj~osH9{y8s2(vB{3t9T)utL8gQZTqk71^`MN<{_BK~YWSAWTY z(eSANI?PyF)9q%NuCtg}mI$GV{NK0Vi2+GcTx7*jK!@G8AW>Al>p+GI(JJ+ieK|!z zi$*HEmlRbem^at)gmkBD-iBu1I&(8QJ_+{CDCJP}Pl7EOIVh$d@$406tQ{()`|HlT1ah3lXSC)hBE}OFJ zTlls>AYzDJcd}dFY{mT6=RsRe!>@(pe=xo75s7j2@*MlqgVgDssMcTCVX`Ba{I72E z=C9+^!1hxoqD4eAAIOeNv0LBaq8FJFt$OGE%kdBr{yz8yH$?weZ$l`P)j)>pZj#0f zh~B_V)-s~M+#j>+Q1K=_DQWp`38si|(R_rx5T=Md&HfNcWlbAGHJVH3x=J(=ZQGQ8 z|G~&foBx+L&qniC6RIAkd`J(4`@}kc*<4S4^Qnr9bfo_)TfFA0-My=TMiaBf%Ihj{3ZkN3#>mp?yDu&n8t;1?ZV2wNR*A@UdOx65nKz4hR30lt z@-NmB$$|fF$c=u1*VdC0EU=V)r+f;sFuYvG0Rjq=-E%AbsH6k^!_#T1`gAjo_> z|L12_znJc?K!z_p*4#vWIeWFCJg8bR(*dB z>9Aa<=ABb_*0ev@A9h+;KfMe){x)$Dx6=OaQpgdu{|XTm+Ye+|=_YXCiVz%q zFJ+9jH1;ncUGe@~!BA0|SWV83h)729uyOB58;mx?ou*K(4WV7T*%OA^&%S!-o@+gk z`3{AP0ir9YRV*+Tw9z!bx@{}YBm3b#9##ejW3o$~|6x3mjyo?r=datNyM#p z{^>t<*!nA@d_;Nu+f}?7wXEE?C=}FipQ~$Um-+_wHKHS{GWubeY9mD{V=><>!36@7 z#x#XbAvudyK0;sB6KHA=> zy-#l1qdxeAig>BNMs>Vcr6({}y?rH_)Sj0#kRsl$(() z=i?DF*{ZWQ_NG;b)ZnGE*4Mxv9|5SSTUuC`KM@QY3iz)L4^qg<&7anexir6=-0VZG ze8X=IQn+j6hTlFWDK;kH3d`(TqeSy)l9&yXzkyor*XqF}R^bCjt;8FK$|D zf$eDyMn$aedHwomCkChOwl_VwoA_W(%vsPTI~JHat~Zv45$6ItwhuS|Y^HBVVypjr zAVi^JvBhsa`f_-NZjL7Pqef=zifaWH_(cb%y(>Fu8-RLURa_@zn8l;;E{9mk$7V@O ztvPGJy`;0KNwKf%V{9cy)^DDRFt>%_0LbR*X{u#1V6fMbXoVr>qqaEIouJgmlpC4~q@At|xTPzA%$kq&>B9S!Y;rea~|ke{&65M*UjUM>{HHvA~O3 zt7L+ylK4kHOG^%Q@EIH~1;7m282bv^uFP^q+lGzcPJuk4W|ksduf&7F8P?L~qRT-m z!o%qPbnDSOMDB14HD4x5>v)&gh-o={i`Ya5MyH1y@rS|J(5qh+8Dy4zj*W_Vn505d zZLCYs5nVc*uuONe4L)L0&-1~{;_+c0@4!DkVN~3WwQyY$bz)fjd1PWb<7tCkM>`EF zfXc+zlv8z)kQ=xIf5iDk%;dT^b|u7gx4^5}cB^1fsMuDmrLo#{nVA6#JYPQvoo%## zIvQ?m_$GBv09!7~B3~DV;S}Tf5v;bWVg>1@9}%QB~x z^|e2FcU@VT7mIk>#{e@q51bU>g-0IC`+at?Y7qTPVTrK0p_d&{A&sBzLh_(*ipdq#>Mz*9lJV%#$R_=44&=A zG&9>6g>tC?rq(fI1J~tW492NU-03^}ED{+_P*(PB!$M7dJ9~9Mi&7}mm$NDyEB-K* zNg%tL;+=NKAd`7r)*PMVIAH!hC(*`V|4@Yd^vy4wcdG-axDpRlgk!xM7i4;;w3JjJ$r+k#e9{jdQVbL`0KFOysc>5xs)_*-G{fHyo=b+bLJzyKBIGVE+q# ziokEYox4tx#$?h#xSaNjzEy$WR6j9yaAn&aw5n*NFj>!GSBUPjo9!+V46CRy-3~j&J(&ME!|x z+(3nP)76yF(1a6yUXKHWA|DQ}7JnuugN|^Qnq`3d<5iob_)Q-1E+0R%ewsh%y8?`N=IuGvMvF|5VR?vjkT_9tx}ZCP!V zTu8x-=B;=A4ku{h7n-776<8Q(g1ZU}srt?2nK_yJuD0#tnuST3w49G>ht!%CeWyrx z4G?s6tr-PB>iu=DDKCP~4^<~#Z(dM>T=D5QSe=ZwBO(cg0V?Qv?~q3D)y-&ITOQ9??5rFP zB;38a1k$1FZn9rW%1s}%CrlqY1D{p>@_ELdq|tnF%Y;B7L8BrYGE`u0)`%- z^R^&UKPd4tf!WQ8~%98Tx4O}YO)(d0cttjGFD%#aIL`0JRw)Pr_y@Hu6;I z713g?ZLy&z@6NE2Avi%{a?N8E1TwEH{&0AUe54_#6C7l_V>(39RE*4PwIGem+TTrX zPL5N<3_Bho(L175dPzD$SUv6?8q?xX^!&KFWmc|u$NFIBi1)yRw1B+eeD7rcxrdcy z(o(xdO@m$)%EvG`Nt)7Pur47%-2Q9K)(}~r+>xHQMUZM-2WP`in_$#oyG4*)G-Y#| zZCi>hDsv|R`s+K=jw(7C_I@uQPGU&Ryrr#79b=tnY)P*P-7uS$+?8`Svx$*Wyxi!> z=VsfV-89J~T7_Flyw6uhC|kr@l}0G{H4Hh`o3k-zi^gA zMTkGrXv2hprNv8S>}$UK{II!s6JJH?+ox@*2r3w927vOT=?cg^oO`<1 zJ{`Ix32SmZ>Crnl9Xl@tz?hi5 z5HXzi$}!U`aa3#TdYOdT%ssZIXO>uCR%CSCRa>3^UiT&Y0y-VGv%Jts@R_ZWJlCsM za~R5IQV?$Qa*L9(t1qX@(2aBVdY2S9UZK2KKwh+uH&=+82a_CGFOd}4@L+34EVygy zXP;!1RPRH>&O1so50q!BJW2z_^ORS)T$l~`Yd%eEuFxks7u#m3CRcN}R=D^8`Fh#h zbV4KzRP>GM?1It2j4Z#LVGq>p4u1J8lTb0I+V}JN#G*T9UjCC=8EK>Et-f*W zgjEwWelcU*$SeFc{7uiUDP{SI8)pqCDz!>c=&#Et$lK}2tyz9&)6`#VUD=eKG1^~V zQByEueSLiKMB!PmGI$@`DCNM8`d4e!3SE~$2a>KBf)?fJiesMq927Mh)h?CEJTe!- ze?^|6a#qkkTcnJ0xcJ5vYw}mluY$*S6L}ypkZ!D=xw1C7)+U)LQOG5W7Vg^oT@m-( z<^#>E$0w=fs5yV=E5bjJP<7!7_C>QNsE2L{`QX9wD zj!4J!-Ev;5WOqok5~++!YMME($UA+cgH4{?x_IQebxcrGP&UGJ@<1ip=J)JPM%P0Nu8XOkybjp7G1m9WyP(v>0Q{@61>A$u&-Hyre$WU zIbP@DyVAwAUQtnTZZDbsI>uSBOOlnlu}e~JbY10-FPoq}s{^jdbC!&(o;@m&1>Nwn z5oSSfJ|n}W9NRqp%B@vrN?Fu8IwO5ddt?$J%hWD4##KI|&zzSxFlRGNsTzl{Q5lh|kb|06i71BKtIa;LJy$b|; zw8xm~%O5@sEu>3PVaP8mj`1y@QJWjXY#(#a_Ra+(ExMqM+YvTNr4=I#t5)Xvx6Uh^ zUYX*qn>BBKR`5-^Mk&pnuxQnKVFi28c-uwkhdrn=>ve)b_o@J_oY!cnfiH20*{*ELXjtq8ck)XxG0#s^hN=kU>QM}i)I*Mn`Hzt=Pcg%6m z>{^siH@eE^G>!1pC%4VfrIhE)Yt5e4IlXRu7CSaR>5|&`Tz8sv)b*E?E}1-B&RcbN zY2z%~hT7Q7%&fTb>n4|9K4o~EMbO`D^rv2y#E|sdY^3*99e%ARa1joEeEi_3#t*9$BATrmco!}F#_IdAi_z_ybgA@!X{1glIQ;e;;@svqn# z#{bF^_hWtgF}mm%-pxJ1_$AkY9!y@9=CKZTE6eCuf)}4LLtNp~(-%17FU~N!VtGE+ zWi+|$yd*WFRA)~%n_V`Y&gMcXuVD_cUP%CFyVxb=6gcX8IznhEZSeyM!gdOyRmOr9ryYE6!uImEnpSCTh3u5G5d)D@ga9#>eb zCRs+o@NvyasMpO{ft4Obz3Ra5FiGOP%5_4+v44m?6mh*%dv8wIG*Pbo`8qZ^Cnq^2 zH&?iJkK37;nVIOwjQH@a|BeqociAHkg$C%u!%_AlwqCLqqung@;ghvl7MuW{CnKJe z$s#Vc@TlTEmh_Xy@WaZoV)e#2YhG^2$gI)RbIDAXkYlp<ui6X6~@q;);25%vT1d^!h?MTi?0k%6SPbmoM1Xn#9!K#=qF4 zzQ!z763Dl$0p!*K?dIE#cklvT3N=ChyfrUlb_B}{E3mt9ita?WsEdUdRm zyq)gqxudpRGE<}Nb=*WbcaR#>EgJ2@Mb{qKwbK-qRQf1NS_tW0OHNk?adz3kptW>) zlC3)4)T_cXW+sT7{Kd{8-Rq(t-B~=iK8E32*cpekNPSz=e znJk+c4msI7?<)w7-2Di1%dVtKDa)m@pOM($C!FWxQnJ0)rk=tC7opx7L&w+?oDXae z+YM|F@$IuJHA>uf^A^$95Cfi6abERBUanzKFx(($g?%Tx^@`3nRz7goh;@F~n%%z- zhIWzU2YGfdRfFiMuqoHW&!r_c2KglDG#RJG6H9t%M=*%!=UK!EVlT?8Bel zed5^H!rc?E&t8kyS3%BxP@uN7Iw{GKR!Boqyju_nwzQW~h#e&122p=AE*CV2ZQ}5X z8*|{~^&d0S5`v?vNLNB|Q5|`e^K1?t+#J0Bv^Erww;v?fhDFCzFcTOy2X{-DwM>YS zFlgQdZORogvv4!$ZH0tea2cI!dvJ6u>5307n!O9#V)q2QJA-+6*>DTwMLc9dxsOo@ zf}$bzpdmiG>*HORFV>L!U-rHOE{devw|f#WAZZX$0m&c|nhXX+KqQNRz|0VwK$w96 zgdvIw=wepZ1cCt&fkjl3${GMOVpwz)F)Jd5Rb(+M!mF+sklkg&z4v|Zd-qVJ8hp`wSWyHckouZ(upr=4gR>)BxL@J;P5+o@pSqX_TV`Zh#F~eS>Xs2MI zU=b3sQ1l^%4RCcLa1|hn1#O%K{>cRC_|A0!G4{lJAjhD@4Cpok?Uy#;d6z`h&bE{E33p%HS)ayfFun`BguETu@5LZya8 zunPhI@Yzv4eQhN;Gm3vI0*-Q$!mfcZ16i_c#kQ_Wv`AluNEnyDjflEgDfsn2CRy}< z@=&*pCS1htM1aqd_5q(Aga0uEc+v$+Dos^^KBPQ+r%oUZdnp2+U5EVFnzV1vz@r0q z2VR$=+6Qi<)T)|-qzlQl1?jB|3BvsYZ9kx-D+kcYdj|$?7dEVTl2dSK#bc4@65D{L zR74R`{SlEzUFt|Aid+N8C8Q`riwx%`0k;J!Y_2@y!Nj&fXONk0VZG zpt}O*4y=#I*U}Wk0vfQZF5=`MBdM%LmWxEt08nvsuonS_2c9mkP1UE_&uws%~6|vqw z*p2tkafpbUXtyvf_mhLdf8z2o{|=2kaj-CcI30bbfR)*j9){8v^^#ibSi ztKEeWQn3c^?>dSU5iW;0+-|@?Hk!odfgjG6o*k%ABl;05`h$oSYmmV~+~NcU@*aYo zG=_gTi`=de{ap0~adJb#|B%-IUr4wASwby9%0%xGW)T*_bX1gxX2dMwDUudQ5u}sg zdH26-G9Z|Ph5mcFHL>0A1izNP=qQ~$U0zb{dy zh*J`!>0A1i{%0f!NsvM$&i@P2M~RP;W|Az)O_HZ!x-LpmkG)QGoQ~!-= z*|+p9eM|o|A^4jo1ph-J#2jfx$Vd$0g18L60Mr#y-@zWl4W!B7@mfckcmweRd@7)B5Z6PB^XDVZgMEOy4E6&$2U1tiYd)N7 zKAdYl;sfbCNPS_O07MDUK-i`pSp?`oFjD{+?FFDNVB`QW8jf8d^#z#J+&mgI)qa=RoQTX~19^7#APyAmFI$hZqyeB!JJcHgH=WRq|MCNg2+FdE zq8O9lPle-{4Eg3_jPo@hrU1SSkU)e|DDi!sg*g;)~uw~8^&Uk)+;yEj~< zM2t~HUff;^Z;z^oF%eNmDPl|-fysypsw2h}L=~Mb#u7*Z8YIS&h#^uf#?pvC+A79l zh${)|Vk|q%r;e}#b%Y)8@1-k=^^yjBJ+YiJfGx5Su)Odz*F zERT;*ej~VNazI_8be_TZ%E>UJGJn$oCgxobL$nczhg+ zP>jJi6gI>YVf&3@jJMB(SZ&0-Y9r>I1jnfrV|<(8-e zmKE43w3ivtUS@>-WrpnUhlC@MNF1OVa7St&PDm1xfFvS3BpG3i zQXdWL(~*~dSug7U$6AjOwQ+z7LHQ4@h(*Ms#42JP@ke3}@&n+SBN$?aSOMe(`f$Jq zabS!%Bm(ruC5{7}ct}}*!viQ5F#$ERK$MFCxi8>FB2l0&5thP19u8V31Ih(0|0t&$ zXv+e9@!0}k+eGoW8bA&cAd%gOUU(nl-}D(dA`i~W0+$d<$OT*h$P<9AG@urEO%!Z9 zntqY6oCGu+YRv(;0F+rk9v;-j1ZpP)ys3yONGULxfL6aB&qUnY6yP*ywMZycJQyJj za3i3WxTQ=-l_n8N5)bvnt*0^E1GhLGN~!_%O9Oclw6sV+ksXFwfpVGNuc$Ni2iw##Uvi)8rE+V<<{TsTrB;3Yus6vCC`4DZd+G#Hxw zk8({L;RCoPB3*=VzM)-@k1x`d3pgoI^EkNI{>t}QzwxSYxGr&GD$+`Xli4yLwh6E{5Kz=i8k3@sr^pfs6UYe0qEM$$P!gJLLqJJI ze%L(BXaq;KL_Jd#nGVxj-~xDHi2>r{j$n@`OmoCwB&Cb8xmEP@4z(qp)w|mZ)qSfp zOg|N6t8lSw(kU#PxVMN%KnVnz1rV|Dfzal{46k)2P~bvr%&@d58OW6a#U#!rQD}tu zer7aG0WV0>sDZ4+C|+!&FfP_i9+SnH5;O_laIPROmTRVtsp1?eZLAlMlMt5}7a`Pe zic5%(OJE6kpobenOTYUc<*}7N$OKjF~lNVS$<1nVXs0F+kA< zvmP#DS#|#s$;M#Pc)c`@>g3fCy)i&ho>#VoSCSLYhS|VSY;k3v|CGc2Pd0EclwBm#J%p*Ro@7K7rH! zlpS#kwnwMFKC|zK<4n&f^DBKbXF1J^*tVw^i^@47rHwF5Tnl?!VQ_fVyGaFn_$kB$5~Afw>jUDVm+*6eSeKPc=2=B*d5qhStf16DKf@ zkLKZA)A)oqZW2eBXga)%_$tCh1YT(ZnqWZ`Nw6`=WC;`{d1CIE>rfdZkGXm^PWokoLjqPYf)&GjUC~7=xcw2w1v0F zopS2G#Gmp}&d$L(!zildYSzY2dzpysI^d+LN($7yV&X z(cyO9aYwzYZ}+V5MPE!_@|j%U-I13;%Uoy`wn^_vSD0|i!VKcNgOifpmKBHG6~U-z+ZTpS1zVC{GjJPu13 zt}!b~7!{Ym6Q&dQs!o`Xo2Cye_#0gyFjz6IHu~bY|N;ZJ&LY@2Ys~$n{()zv8)1h-oPNB5~b+%UHL~6CF41&kx4*ZXwZ67af1G zN*pV0PeUH57BPf~562imF~NhKf4 zVm3wIFu3F5SDn-L*ucd^zba?$d|#O^qS0r5ZY~zP;x!%H@o7nWUfom8yd@v5(q2m* z_7nKl&(7WDinz~-P|%+oQIXg6GbPKt5q=)o%;EtF)L?%KDePXSRIWKILNfqCNXO6NBDJ&`iQHA5lnj!{%VK zi=2vP<;?u&A#u1m0nCWUG8hj1M}$LH%ms5E5e{wtBjFI&!$~yiKMaMy*7A9)PX!a5 zt?xdo-eeHDOCbvEBAa<1_PQpR6c@Rr5zVt^Gnd|!CH@|h6gjz zaa1MQtGAT$-%kHABB;*T^vTEXaxTsbc)BBgb4-<;=e2ifO_eI??8&c63MX4qQYNs^ z&rlak%I=juXLR%@r@F@>Z-jWTXs1P zW~OOo?i`CnJatqIozSvVOS8@C*_3BT-?&~hy4GQy>8WSvsuRX|9`LH{-nJdBQ{#?j z=?f_eJ;|C)h1nQBBl%fzQ@bM>Coy>&%flP28UxPq3XWolj_8C%3$tJ_rc zYI&<>`!DKq&lmK#J$R3X$E}iH+`?<=j(uL9ciC+6pzIlDaEI5#;*L+IJ7=2ASB^eU z+x-O`1@ho1_%Py*mOE$p=A7HJ#@oU(fjiP`q`{f6=o*S;=$BT>w zqvAnX!yGd+gEzKTW|+CHIXEUPtwlGsX2V4+YuW#c8(VR6;-57C+w)_S$*qoV`}~^| z<+x#96+vD~)83~q@1L$(HmvcHzPt1@U#jR%B+w<1suD#<&#w2V%sj?w-*vYIEFSD)&cxQR}CcXnE4g33z2&wHCN`=XTNzAKy3 zJDyf=+E+Yr?X@Lk@;>s74_j2ZpH`TBK0PNwxq0~_>jPm&zH^Qt$QeJVmYrB)uRC;p zECm)>B)SvT#qUIiqZM)Z@y$~b9dVun1uP`>9zSaFc3ITLCz&qq0}ou)!sg-C3M6nZ zSqcK^@7D@E^x#D@&CtTk+R}WArCqo!$0EYQ*xbU(&e+l>!rGW+!45aJ=Gby9ZLG{V zmTc?a+#I{cav%GUf6lHPXJeyvP*BmDMA-7@H^-v}>$vztco+b00d5VP8u&Br&S5w) z#%zo+TX-6*+s-n>=pI_|2kR2gS{BYN4j zv*l_1KkH6(X-y`qG~e&z|B3i98$Gk8j-%qvd0aD~G)?Bz&wZH<({-OMsO_Eq;~gt^ zvhgpmk(^^zjmLC8Hs5`6`V~>jM~Qs!;;#Dh2IxEHp(aU7%kq8Gv_7YdJ8ZbzBYK*D z!k(wBSjXHAYn#%JsXnRPs=8~ewb^v_pfDY&>7`QaNfy%WU)r`@n0w@w#gDn~*A~e> zjM!4K?IUUPoTTeV@|<^4yCcYGx7FJ_KW)ehk9+a@j9*6h?j+3(!Nx~6Ild13VYfoD zzQ??V!_ItcP^c$7y{a?)bf9Eu_JjB}cWC+^O`VZ^roD$xiUu^776vw3MWR zSVc5AGk+;f!~N*vf2qFUuo+hk`VBIZiZ;e#Y>rY@paq zAh{h`dP+w9u1)k=#SZCS+mqWU)h%|{Q6)3SpI41Zg9yQp<`sa>hS9Z2Iu%p2V2z!D)NkCO;X|SVT){+&tse=SNc*@{_y* z3`)`y9w<&d>=xSb@}<-F%Wf{JU6eENrhU!Ykf*EWE}zg>Y)bFSoo2k>BIwLv`vLQx z>xeUIY7cC*U2=6trqNsPfbTV}bWhpEa+mrY*)F$ly!P@7Z;lXi*1ZqwZS!qfyLrX2 zW=)}Pn2N!ni~0t(y7_kQ)<0#`Y}~J^UH(JF3)Uq5Lj$)RVJkazLw?ruu=hQEFmR?0 zvG+`Z``ui`KyAJfr25YgAGO-9Bk&Ec=(<3s?$=QXRhsWG8JxlANwIy zBglvrvS3Sr{alOXx2ji;ySLN(&DQE;E=4imb-LHAKIZawtd-6iU zm**~CFLuO;f)d?P|ucte%@olUA68*?t!enw`_l${qt{qfM*=*POjCdh%Yr>blQc=Gk;OXLd*|##Qsezu=VT3f?uDDa$a_~DytM;qFLpUjf1S5S z%EMKDTjr!TWb&Ds!aXx*r>I+^{YecPID0e)))MD&JJrH-R1NUHrPj#s27EBk|;`rAM1nd#&2g-s%F!-JHAiai=r6 ziwh3M$q62i;{N=^ z_bu*$iATxl@+n-aT9ppDa-*5W*#rY{d!qAojVNZ>gt36709|wazwN`>(O?7g2{(#$YyfzC7CU_O|&qx!^|0f@W>0cH*IMN96Ro=X$qiO>kSEvbSqefyco`d`0^= zs_iCE-dwjm(66VfyXH;nFGdp1-Zm-Me*sYN2UH_L(5v8*V#W zF0E5C>CLw~dSw3gAJ10IpRu|bi70qiY92Y?{myp=ukS8>cOudv$+yq)+L=rA`fZZd zO9^(zK2Ver%PUW=QOUm1ZP43LL>x|SO`jDlMWz6QZ>0BLCIh6 z@0L-BlyC&4L{Jg{PNWn7Tu3Pdcspeu!22mT5d!5FE`JxGk0l=iPPb$?z)x_EpdM6YE!u|=IcW(oh*D%icr@Z2!xF|K!H7IU@bYlh zK-B$w9W@Xm1pj>#5<-FNqaZFxfHB4(isF(a{AL?X^z+|+9wI;jU`%kbe+g10uc$ z7~g|Be4!jIgy3o^x;$5dD;MyA?|W_q*Y*)tiMUP0*Y=}bL-n5f!Br2gBj9Ql^UlIt zB!{v{ZVy#)S!=kC;CWHp{^|&w9*AV{>;P>Izy%I04@>biReZ^hz$1C`BkG2*1Grqo z^W6hi*YJgSc!K;nxOxEwQQ0F1UIfo~AP2j|gDbxH;wv6p$)LU;9>kaP~U5E7vV8h_6 zLGpBrjqo18HbS~Jo5JhHX%L^T$d&<|>1;*7ab&CTUeI0H>VV_T*5UO*`T=kZ0rh8N zya76cZOtdq7qF-EsdO2(BX1|*8}jAp5o}k!GChXX$ycQ(uo1o%J(cavH=r+J`}2J0 z%W;a^jc-C<&1Sep(R0}gc$xIA>-VZ3y z&OeqxVAu1mi704a&-Y|V0=$zU%Ra*E2DBN{vw$i=I)QzWCuC@_uk!l7Qob)k4`N8c zI2}M2NI`7}7~cp|bM`%c5WR$*z_$!)z@=r_vb*@9K=tD_VzMA=WV!(EYGY zFh`Of5A4~+PiEM&d-+-nHoF9@R6T5YguR4674%vTa5H-~z^mCyfKAS_b9u4!i|ky! z1N|y{E5K*j+X3wW_R0ATt%nx72PwpiD2^V&j{cUcn=sk93x&ZV=c!V;Efzxz60)){;_cN zGZ^_Cdr+GR>~@W@6Jp$+*>rV|Gw&9o6xsoqj30l=j^A(Mn#l}Y7P^2?a=oN9g*Q<-y!U&~a*?ZDK+ z?EppqRs!AyEXFk890L}cz&Q#0$ArBajHU;C=^9vz)xZL{|L$;N0SyFg-aBz`pJP)`(*Ia=LlPm_eKu{6=Odr;mS{ z$z^Zlw=((oE?~yv9>q+?G4QeNfS-Zy6g*P#+wf?>Z)awQDhGHoS8_fC@$uN=7lFqI z{vGCe&H%p?w?D8Ius{DP9>MrMc&y;xf%px?pO~AW|LeisQ2?<9j`7GAjo>jPngZ5$ z*fW_$TvD_Qvy4mSQ<+uJll8#b?PS(M-+l*i1H}C}#^YhMA|837RhW%jdEO!BX|6I~ znIXhInb`_F`4h8^s~W8ynvB(=w?=@AV|DtMbU}ES`RqDdj+9zua$y`w4Zk^v;eGWDOh)~rlCcV zBwlQ2StM0tX8{+tvw)9BdqF%NT?NUZRlvJ)n5~iWz)C$uQ(G@w){%|h?KlwlGeScJ2*`43LPZBVUUy9qKr&<5C}%QXlH$QXhx- zu*B;U*Co^;9*1}c@e)E@rr+ibbH+u2N7E9(s~>) z&Lhy-L?pFUVJ^;e&3Z3eI8;x zH0_x2*R^!Htp3K9Wml13fcjpn<7(da2-nTF4Oh8G3=>zS=SF3~RpXuv$GPe~*)THJ zwOD_>5$rx0PGu|Xf}_JPwe&is{jDwQE{A`AYYFJ#K+C#g-G2~95HV9>)PyrIYT6{M z*H>XZob8hRhr>CpkheRW?`n771gk-24qIKF{$nkJu0#Igt!md1|FPD?t{$v|tq->e ztiKiX5B}*ofmx3EOcC>$y{ul&>Xle;(6V|B`ee8seKPD|{&|}DrvSFT3bw}j!=@wt zlP#l;1i!g81@|gp5qAY)*>%c)x@F8YfU$aZ**PopKVQaU1E8l%HImt zuK3TlOuNSY7lYZ32B{=yZC;ehf0z*kKO{X0p5Tv28^Ke2kB^yo);wl<#3PdihnYE!gbAmHGIl-!+$+I;F|T{3=g?(`)`HMx$gS! zgfF=6`4_{NTo3&BThF^z{13z9t~LLo@D&4>`X9Q!kQkwA)LXzjc_(VVD-VZ z71jk@qcO{cXN8O2{?@}H2RZ7tm=K5y-xZVnjp2LI9d&f?3VRY*2f?!D*2|7z?_gy> zO!GK`AeCpNHM9PVBQo2~`+wuBwYf%29mkq=bD{;i1ZLXWb&?h5QhSB3Lfhcz8FFz)Sw zaXf*VmQAsN#r`3zvvmRU{|b2KXYLkUnG3a&X5eD$GZXR_}|D9xGX>j`B#GojQ$ zc>+qE?^Gz)nd=(}nVcryP{@vU4B4G_-?`ueXR+@>sMuNRyA&#QR`|w470xQ(m6i@x zt~+ae*Fsg!2H%aAB`5Ej4b?h5zT2S&XTWzi#5=>jdm)dr!}p-|ytB)<5(=;~jCr`S z8?$SuBU(DUeQT8g=TYC|mQ`o3%!Rs~eR4vm+u1KC!(9epG17X zTb58LPXrxJsqz%8!rMnHk*C`XP|mdFvAS6(ljqtBg?;jTLxxZ-FSL~ib<+K|GNDmk zYTG9W@^XtukmS|2YN1tLZ>x42S`r#tv1|#8gMs!)vNIg$jHHRffkUAV{#@WlBvaTg zZ-TU}6h{I*5uG?1I1$Me#{#D!CUGJ#5V4C>fuTrobZ;+C2hK%GQNxi6)JCLAoC#cr z)GE)m(Wlzz^X!giRj@dFJ{9Kzmm&>MJ*BezCe8=OTQ|goz?BFuF8RhI9&tHvEfNq{ z12-aJaXl~#&$euw&mRlGo?qMy+zrj(eIp%sU)a4o^&IWdM!Ils5a~uqB1cgYXxWkk z+L@oY)q#7VF*)i=7_p z+39hP`g`iXAQtr*^?6dnPKH0vPKLi2|9U)6>f?VM|1RO#*|m$ET6~4ty5tYs8=Ql@$3?gfs^b2HJ4&D7 z{+t`6ALG8yjnYqWKjvoW^W4w52Xr_0Yi^BxhWi7zNncP=RSJDc^>LM%eo^(3s+4+F zpHn$#P~}o}(zn#3>P7mO>Lv9r>8$!4^*eM<{ZPG0f2xVs#M32>R-br|en@#vt!f&|0OIS}>=jIdsknjiYXV1hv6UW_ocH`L%?!OaC`pFgcKgrE_ z{%YOFK!XdwtAWo0uLC|uXwdp<|1btt%KJr}%hy%2n8y%c<89S?3;uK-;GIugQj-8lBM2cq_> zCBms?wG4g}R0oMh9ae`)oO-`{Kf!kyq|%?!pAkZTL4QFg{U!Y+;pj45CMx5p7nus1+H5IJcfxR*ph8&woIGOmTNQF?6zWCsddO!VZCOnven}M zFO2(h&}RBmQHB4fw22JuFA)dxb4g1TcuaRE?)iND@1^l#; z)M*?V2YFp1Xav%r5jAe|hQ_P$k|s@yriC~(ZJIXHtchqMgx7RwzCoOtMwwsW=%woA5g+ZEe2+YQ^S?Y8Z%?Vjy{ZN;``du-?I z3HD@rnmyC5v*+4PcDudUUTUwfSJ`Xr4R#)X(xL8CpHhDIuw7ek0u_>*KrfPeKt+&a zmdH!w9iU>(w>94;FKdozj*$}0_cY%luMnzTPfBL7mk*vyn$7@V)`b&{v${I~pZirW zG#z)>Hl1`gG@W+yO=sMmrnBw7u)%>9V`4Y0}-@bk%*d>AJhO>887{>6W{{ z>5hAlU5k9tJ=}EPJ<{~hJ=*lhJ=V10o^TNNlq1eP?a;bs9I5U(M}~Xek?me^#(2bF*syTo+IQjJK8;k zj!sXB+9%EyC5KtzN=PBIv9CqCE z9CJMI9CxgEPHxGv9c!M`Opfh%>^bAO;AwPR@(9hG=Pc@oFY#VrGL3sAl%+Y@bFn$i zbD7DE`ho9??t!cdXZ0(dR>w8Ze#Z^Z0mrQ8Ae%!BI2Q%YnVw11W3$e4wK>;wz1ifs z*=z@WgD%jXOh>oj9&oSb1kZWYWplAR++6Cp)m-sn9oh|biuP}=^4w{z1>WFUZ00@p zQFhc%j5mASp5}n(Va!iZPenbRN6lf+#>+ik(%j*VWB2LuYMZ;gsm({d8L0o}UT-$z zecl{4|9)?N^PqdUdDv@Ze8gMSJnG%sJmxJ&nHVPAqs>#^N;Xf_E6#P=TN9hx7N7Cf zH_v$;>MyOHQ1_f>%b4w%w8y^xC{sHpBD_pHUC2zrxYy+2>_rO z^tSVAZzrGVJ;bMYkCfzkd-!zk35*T&Sw72qiZ^%%Fh-dF0+?@uA7iYb?`(017xbMi z4zYuN&F8sCc(Zp1V~F{;GH;Bh*!-XRjPZrOuh^C^^qvz2qis9+R-ZEOC4QfGoPQ`a zuy|JV#e5U^%W=L9VOS>kSs0Pygi%>5jLE6Ogq$Hv$=R`f!nB+dyAJyyfBA|WSG?i9 zEzHRIv3@Z<$2j_kFeh7udAUefkoO8p-Wnmzx4&dnE*F;N$`=pHHNvV~AL}oydrk_Q zvLmMFn9f)X0C%Zn(UmC6_!-Ax9OOmrN|8fb{nDfJ{xBZX%T8UpvZ8wWD9&OSWBK;e zJZ4M2+It0KBIbuNy}JzV28>6{zuWoC_dIp3Jh>g?4Ra-n<9~!drDu%$*nPJ7y~`|j zGF}L|Lh*N3iF^qC+f}A;_*_&eAHlqjpSL*ZpP!U+UHg3ImxkpYS2fJN?sIuDf8XMY z&%c!GODq|dPi)12!d;E-KFsSej%x|a^8-6#WCuq(^2fZ7nOz0>RLrhh9Q_T~2!&%# z!I+B0RxHPCk2RL_l-yUO{am3-@)f#TrDEi3@yqxS~d zo)^5ctWEOX=3BjY`TgE|=rjBQ?*smzcLjZiKkQvY-SWpJjz8&n#GjTD_%l*6e^yH4 z&zC0h7o|-8vZO=%q7U#_B@=&LvZHPIn^G~xB!5dP)YRV)(V_^?>;}yuieix>A{$9)fn%nnl(pN zch|fo?r&9p9K(4v`!P@=Rh!=b?;An_cMDw%c>u<>ERl!Zk^*y;!49J zn5*RcFuJ0Vyv4oE)TMuyZ>3>}(P{4Zy`C2L-uVw=^0O0A(_(?i0ouWi*3lT0;_@+a zKRp^cKf0li>9lHoYO}2F})VEsm`mM`6bQ9j4Zw=<9M&G{Nu2?DO`%pg z@5iWWIuZ`tTO8+T*ehQ+ki&qVSAM!(&+K$&gQ4A|DO|G`X*XK)40f@L>D?6kwG*G> zY^{UfM#sKHizeYxrbF8It=duv{)Knc1B1q96{j;&yOI~7K05K&u@Z9^(&2dnwHM>}zwY3;4LF-=PCu0FOnvJ;b29K~@rKO1Tbr!QK^5sR1w8xV?DN|(ftR<@ z(QfpYOdapf9tGGk_TJL-50TLBmBoFa+g;0-pX%*5^eNDJ-Q2daD20_6HBd3b=!W06 zV@wayHKhmHmd7R8mc}JbZ_}8hr?tNTL@^iqBfqrvD{=(`rHpTJ#ABZiXvE(K(}Nf= z)7!rky5VP+|B?#-fum60X0+Vh9gt4_-a|ekQ(^q`2H4-}LaR~A_9xMn%<-cO2JfOK zfB&DJFnDjZ`K&#I(ISETy|L4c%j3Wc{p)u=56xr+vNDDhwoGA;7w*>qK2ptvx??3< z5Nr#Pk3!fxk_CcQ;RxaTBUp&z@NUwXy9hFT^-Hxim@kgy4(a#XFGVsFC&6~;$+W(x z3bg{r7cX{lY-3;VPS3o09Yk+xM@t(a>c*zqCS|~y%AU%Z3QFZjg=n#Bamqd}eq2my z^Vo)DmzWUy6dM;i3X9YejnXE{MPV~NoNv<8;bfUKQY0Di=M4Om9$OPKn-9KGFFc8( z|FdpHVcZ^i1_OlG1mLm<*kyP;yq?XHq1|9gMP#XOr*W}HuhC&-c?i-^#{?;;IhHx1 zVT?JEjZZ6Maw;IP#<7Tk^<7UhUkqczc+WvEIeWb$w?7RGt-VVh{M!FjL}AsUPN^TG?mjW%_tG-NAAV&$6V|*I+Z-oCjq|KlWb8Pkd>1m`@eEbvqwGhyOj23pg`n1G zq^9AH-`$r2?9qQ1FXh`$eYKfk&x>gLB-)nryd&P`2OCLd)wr7FkHGqcFXUfc!erm> zTgR_H(wMbK<_s6>Rzk==P5u$C_(_Zmb2Eaj)dP2qMW3!Sqd20&6L70=7Q<&QtCFJ5h6}b7n+3SdbG*-wUoDfr}{4jt7JJp_Yg4v|H=@087?p>*3CqcG;Pq z^{N**e}uQhNwy{3#BSsqqr`n~<$7gTv!K%&81O!TV?@7Fv(<3vOEQ=~DbD|MGT87i z*?Co~qT33R#!mDhzb{wzRCbLu@}sBY+u9#mmNt49dk7gQjUU{>k_I@Gz50&9wLRfO zWn^MPKBLsV5`B%U{Ep4)HI6=+an48{q3$nROWl!*OvpK+KwJ9W2%CChNAd1cyoTxE zfe%EzGONZ%`R-3vhaQcf_wsi%n7@+PF_%Ijsd&&^ykI}*`6Jdq3UZo^|MUWDVaPR{ z;M3)&;FtUeR(?eC#H1LKLS$w#n1nJ+U93Tv<(LjFK)0A%_-V)mMr?oOUzPbq&sNA? zDA#%XwHmN5iSB}*CoSPSWCx)ynS35U1Ns9*-1xQZax%4`N7|kab3Iq6&iQ9)`tS;V zhI3J_MxR^#5Q^OAOrj$F=1JI(vpb2H!u;>h9=Y)yc&uv8ddQp>`*jbs#LD5 zEHB8S-|UVxUT1Y8rhkDVlip!PR`F0n`h{_VcY=jXS}q<q>&AueGg0h~}V=riXq#ARJum|E{U*j?{%Vgt2FZL2s9cx)MIk2Nqar|yPe zn;)6hN>nKTeG7S^#uMQ`A=RM&!iCV5tV+i(nI&no8imyMXad2F(-w1@ThAm(wJxHK zu&StM9DDppU~l-O+iVU>h{jWh>~!RmhqZUL&vd5s#8(^AGLs2$Yr|^vrEu#XoYo)8 z`h>O`tXrk6WcK^Yt;ecurYj3}LQWpG1@O(L8$%|J3O~;Fs)T8DU|^mId!k$L=c^jl zRFTo33-*(OX{UO&FSp06n0PU^=1gzqXVhD-5RC+Q^gC+^<9gMFU6o@hdr2~(ndjP< zY(#zuehL06{^S0fuF89__wuLTqs1e{Hx<~cjx*~g>L)&}a7(OY#O=aT`dZ+u+Rf}$ z5BhWN%yw`guRG+Ca# zEeeP%N{#zh2Yl`W8Az!psqm@Dsi;~=S}3vz#R$cSHV8KGyKsc?r;lzP?4y;I(X)Jj zTZGdPWS~Th#_oC*U1G)|Ou5>uJS6Ma%{2BQlg> zO;5rUT2}Ez+((%wRX=j-U39BB!JqU3Kj~W#9o6B|tz=q?Qz#bl<HR_4f5b?K~*v zJ0KVxA#BcD-v}~LU9Na_oxNLqW}rmB4mKNLB9Akqw$S}<%jmd0^Zk9T^+CO#ryuI{ z@(d{PwEOWAh;Ms2F`K`(gZ>epFLCqZW9@Lf)n*O_*g@9Od|q(Uj>^N^Ip|)RR>4`5 z!Sr^OwKY%euqlKnZD3blZ~<}q(l^C7Y3^37NpXP3`BdVt$lVudK5r>0oj=reQV;bf z(GF%$YL%k;DJ zOSj%Nf$+8|fA?!|FVk=@rQYr3vIfE>9><>Gm6WK(X~!Yd{iyIE;?Cp!LCz*HzkS;F zRoqSuh(#srQ8U_VY3B71}?3ynpPme~ov>`}n=S9gB@4gM4RtweL50<2RZ4Z*H zc#OmC8N=gBBRY*#zdj|2M)4SePsmC~_p5tKq=zt3Gpk85@)GOWtl`|@TjUOT89(FP zCJZ%HE8*}0_z=H`g4~ptdhbp~R^WU6gatzu!{D^l*&*xs@XmhXesIxnMSj@Loz@WJq%aGe=`JI8Q!aTt{JoBg6ouTjrsUHLaN>cU=Sgzlg_C!X>)Lf)JW z%87f+pl(&})&+b}@M|gx)XE9;%0~1Z7a||xQ(6wz%A9_=S`7OsqP6uj;rKzb$2)bt z_5$)N)eM3NbiQSqMrHW32z`{7@Lv!{5gxZN=M7n6^Lm{;OTMy>$d93GWDR6bg9n(x$KDr8g5k53dmSeqyd8!Wee~2X!uBZoR6z;@9?T6S6`2 zEogd9(`SaSG8IVa;1olBmxxD(EPoG40u{UHss5PM%D$kwmnlP z+J!pUUlezT&>q0W2B@_7AfrAcLn1>Vqc)z>vCgR0<@xetJkm^}%5VyOC*DPaCp!(1 zvd@yJQYPGF!QEZCzIaylwWVn+e%$NW;kfcxEGc&>lT6`Ed=)}EVAPrK{IQwCz2o&- z`?X;#91U$1-Hrk`oD=H?t7J<|R9THywDn%iyQaxSf^BTV4dBQy*emWbuq|>+zg8)a zyjF3*@l<>p(U|=)x-o50?8GIM>O?2h05}pSNuS@G3;arZCYM{ZZHl$jj>fM?MROzn zP=hD)_oXxu#ISNXMei+uR<>7#4F_~-r1U zYQnvO8g*%IZ>PS-)8Jz-OI=Odk4ZPdqT!ZhdSg7g!m^EUjXIiXGlCk<6k}1{57w4x z4+Z=~yIK5ng2>!6;-gUoOQMZ48<$-LGiaOXNps?PQ5dK{2xbIg5k50FvpMso_x|QO{>fv@hL{toLi25j6KjKnq>somBhk~u#c)_ z$SOt7A}?~YOZAKYW639dS(X34Os6!@C$n(fzyC5&S+M>k9NDZ(|wHG}=o&y6k79NR(2yAPa0bcC`V=aryI2F@uQs)Oq)(#2EX3FhHOXG zeQ6=sFQP$+4tghOZ-n+N9Se~*8t-tg3%pEqj;95cx*?E4a0f5gXbN=DaDQuL+yC+0 z<;RO(B+pI>cIoy{N3+lP684x3@`gK_;f%E#P=EaRiZ5Ji|3X>7V1>c5nGAI@%=4it z>hWbEu$AYg*b8N{`b~j+eJ`8n_XL~Zm;Ie2nGM|3oHac$!L4YL~uxOo1uQ zZw`%SK4)G(jFe)w-+I~n5sK|t=@$S^N&EMCYRWow3B4O-$4 z8pAA1k1(F8Q5%@Aa1iyzZ`x@ADM#7M9ygor*#1&ZX;X{3MYNPMbX7N6dCaJ&T$7Jznv}%)U{ro}63Z2r2L7VS&7LTfll`7ukZt^CuB2;!hF{b7fkLb{D;pU5= z{$ey{{H$|SW4*TbhsBs}CN0Hut!Mt_Gcc)oa4vW7v97Y|H#EiVUHZuU8Ia$53^qr%np2H1a+zEd078W=rokW^zWTqOZxW&BRE5A`K%aP5n7b%q0 zzowNV_qzOS_2wb=l$q9%ZPqo{MbP&UZ~RKDH^v==&H5hXBG#oB#(fN%>c@t#HLj~> zIH(23ho5Nul5($PHcf?MwI{$fYGd6_!y!BQ)K;*AC%(i*zxURlj=IU(!0;5)Z^eO70T?x<`?3)EI%M-LzOv)Var)!%#xtR=B zsCS!%o`3N{zCJDZFKKSFk3(FF;Xb`bn(sOcm^C_9>cUI%wxZc3?cJHAKaxHM^@e9l=mve zy{?mH6u~!)+qV~5?Jn!hVaKiM8Oy@!&Syb%9%1kI9B`ib1~^9_?M;TywON+Q-v$`? zNahOogM9s|2dN&pp}9tiW*R>}mW9l>)hacelEU1re&Oh)HS{&5wb*XJS>l$a&lav> zS?=i{3mDG(1dUCrL&>b8#vwBLl~M?E^4Ht>6r~QKv$Vatu|GU~R((%hIPLgVrpf0o z-7jx%E8f!4NWG>(mmr9J^M+k2jz;1zA?IK?{IvZI%?hnlYz~O#DKwT{DmL4J2DLK= z*wse9JBG1urm^X=*0FzQ&c?Za&GCamv#Gl0PSsBx&!X$?HTRQk|0ar?#|C*WW15PU8_dL<)~ISvosr?|x6Ku0_Xh%J*29?7E&Hwkr&26UV~z>Vsmmtm%2)*S-@`93t4v75p5twVMxevf`xq240UlUm`rz^kHfat@BQs3H?c z{+RTthKZkDDKCE^Q4`#7Fym?4a?LHCBjG6h%2=I`maM(t!-{(&{F0sG-MIS9V{O@H z>1lp>{ON}7g|=T0Hauz0%b&=K?x}au}B(;u_*~h|HX+~Mucd#hFzK3Zd9kz)bqRE2WTdlCd>0_kz z&AA&Z%(^uSDSIxU)nQ+qOVv*sxp61p#%}p2dV@OYD@yWvy{EZ5EzDgnkehQVp8^}u zcVlK<21Yg!%8J~$E{s&F5#G>Flmb8Jc%I~zI^6RsRs81FKEdGe?rUj^mY}S`v=;i7 z7qFL%;hEEIw^%%k-$-QYQDa&W;n#K17Ypp3Ua4l$1D~~;Kj(rk%6^)HZVEr~xZU#? zmBp{Pb!#r`7XSTOH`=U>K%H>weQ#XMsBfQLb>L%rqR$_s4gPup+mbqa&Lz|Gy$hC^ zK)U5*W_3~#1CgxuyCqsDiTA;}CcjEw(BtyXa?EB$XfpCtMj zhUL==^ckTrrmPs)L3(1;a^M6sqG{WIgyaInjwZEl)Zn4lPu<`rLn7@STdi?jxLKoO zboP(5kI>j=%a~9_HQ(K|h&_a^Lj{QZ zi#tpPU4;nBnkx47TOVd8d`qWUB5n*^t%<(EyP+rxP6{E;D`mIVhO5(Hp3@?WMTRGN z2)Y=u;tCHNKts9A+(-ob8ePqAMO2<_sRdg_ zbHx4(tdj5^e`*@Q*otMOfq_yub7g2_Oo_5XKSDo^!xaOASV8=$xK2hJ5n{!}y zRXOn~>J<4I;XGB}Kr~_MG&#G?K7FYwxz&o!o=E8KTQtMCGi61#qcJMeT5OQT?Uz>b zqqf$A1~&BZv${yAtYFc#*$0NHQ5z?dcR6DtV(G8TYT05PXS~%_K3IsA6$(;q;viGN zeE@UYtaNjaTS_z&CoWDp`2OsCO90FL3}faMY^GcI^<0v9$;Z3WExJ&9XeRP_?fb{X z!Ts=+f$Sp9P=)TJw_(v#?r|^GlvKxqD}5fOKDX4H-pVHQd^A~=;i@@=r(Q;Mt+%md zaFJAa8+H>}@}?-D`*<*KExGE5`noOMsV3y| zj@pD8w1c>G#e7GEq<1uGpoA)vDjn5*%MTS2TQ{jL)wjm9vrF=tC|; zP<)M3jlsq)k17orj>L^=KI@M?Z>6bF510vebHRCBs~%}j!a{ri!#gpcgVGQmYLRSb zckr5IA(*^>C3++8#UIv1!D6-(Ka{s-;>P|{d|G4OBtWRsEwa3#*r#2r+|wg>m&{w( zgW($6{YJSxDdpFyi`{dDK@p^|(;*K$_n)GyTtXXU(plEbCCend8D8D=LCB^7h=%=~ ztJ77z=SBN8VEO^Bf)37RNGfQYs=hpT+{bB7*teuzl}OOE8yr?6Hb{YrgCyh6h!M$u1nYt4a} zREMfm-X!dCL5C7v*Gra`4fwgW%2$?|yhj)6dU@?^Wh58gth%gfQEBS&#cu>2)LDeP z8-xtjJ^Ri}lj~HL>5Bg%-qd36*;hsk!K^hu%(vO5LnZ3GmU|K=n7MC3OlRux1DE0( z9oKeEA)hq00+wqUwwRxEpN%`+bbce7)A$_b(}U-!K_&2NP#~!4(LxTrXd4%oW$T?J1)K7C7UcNQ;&~#=3#2<&)^ibL+Zw(w}v}J zH^XMA_BKbO6e1hsG+`}`tqi;)N#Q*onTkRG*_vJHK%$0%owlG54mH?$z{L`v8d%gS65#Q4>2(lj3d0xeV1~#;rZlRo-&Fpq zsJM#sF?T82HNnA{z7UTNo2}^ji?``aAL+RSq9I2LdShG`e4-nPgOjZe$*N_h1UoJt zope&wdwXHb&pjHn%S>9MVg^2SBo`BMPYc!1BwD_zK_QHcgSd?czrH{|VSkTTc7mJu z<$=%Wdty(4cw2aFk`Z&^U?M1gQdB2K$d}x3Rc^i~VlyuErn;0-gkLA zAB`owPGw0iX4DD|JdAN%;iRF(c(m_D=*6$br&sh1e%0 zG^i4T+1Pe$nT&zQrJNtIjA7RzU8WCktKQY%!A>6lzYmZP+2eY>i3dK7;YdE?WK0S5 z{lpenq&ND;hw-ug0iDs02S-u0?g|YLcN0##FWA*<KO8(|rDx zC(ApEz9V_A_^tr@i$*!58~wu6|Lu`OT?84 z%0jG~tYj#W{$O|yl&_kDtE)Ct7LfT%(N8gYl{kgizSLL8dURy8Nxcq-hu%+##nPK_ z8{^P;k@$oLJu=x-f`uRi*YP+A;MOLu%Ux`jtZ}CpTygX#&Vrxt-B=$#PoYiEo)Vsx zBq)5AWY=iYqCtZas7Ux!>nOGC2j$H~Y;+ zqK_gvB$gBKIr!{Wq}$U1T5H1LgRjYU6JQiHZe?Tyleq z@n)43Dul^uujb46Qx&)zf?ESlgi>N6g_ZZdLGV|Jnzo|BTF z?A;VRi7>)0RW*@cZFDwTgl>@TPF$pV)J=ep9vB!hTkE%Z$%y@NZ5e+<|R>zj6sG$9PWi5JWvfP|)^C-l9 z+ES$=;AHOU=?t%XU5JY`jRX%#>o}}YN9hCC?K_g!>v(MAJ}O?S`CZ}k^-+&wkv&7A zoxIpWCuHd5r(%S>@@kq^l4H=wP;WSjX`R^Ol?O@K5JkxyG1VVbK8REW8$}8$ATFEs zr15=aF{7Vk*kgy;&-p~H@$IxP20AGC+w@M?YDufMs+0GinS|xcEt}1?)5WA>_%d8Q z9t?Ib$Pn7Vu`_QVSfT{J5bVkO4tua+HYF_RTC&)Y``2lJ;w!FA)~jO4IrwIhiIK)f zPUDB196dYa_Lt(B^{-E*_T~1UUo;_mz@H%`?BPw7NQceubGkjxg*F^G@`#KJnP%U@ z!F-G&k6e1HnjWUO?s=fcwh4CJ#69w&)upv93@LpN;MHbOslqXg2#lAMvu5obO3hoxYUkXunKnMOM&o+ zMj>nkLNY(YyVZv&i|2$+Pm2^qG~A|!t>TuN@~2Ec)g>?NyF7n7tjNd`n_Jem@~v|L z%={fG3{so4mUhk-gC#EHN`O5+z!u;(M$w7p@{xJch}webpf=%|Mh_&c%4Z+YR$+U~mC!+i|)j7x)ib0Zj^-|gX0&#K~U=pwC2 z@ivpCkqUksBE=se($A`Nm{2G8m4bFXBWu6441sTj>lOOMZfz~*i+#4#DyL_CVq-k` zsOU3YFaL_~3Gi(?Fs|+6!MmeMIeKS&EZT$hd2HZo_{Y&hN73 z`J+C7Gl34Afw{&nZJX;Fk-4>?PkYK1W@&`YQeXQ`I^Y$O4hp@W%au;RTxY<|Lumyx zlrDM=stOb`nGNUoV!tK|AD+@My+i7Cn%rq2c2o+#w+rw0OZ@ube;TxT4rU1ZN^T{# zxKn6_zTLm>SPkAoseq|5p%^S5nr*5OA1mi@j+^N{InSQ)ojle?sbX~BSdI7?ZfI2_?a zAAJy7jIbdl8U8lA1#92L0&gd)!v7uiB;O;PV3$kW*`}|gtw?w`C@DQH$t&fE+?agz z?~Gj9-RG<!PneCqVD@1k?i3-emQ_OrJl&ja_;L3GP3VOoyA~iPwwLDn&NhGhT3E z^p;>c5k0r9w+i?kyCBEKEZ;=PzFjO!ZTT9#Fm1;Je$Bo?REI`G@l>-iBCsvJplhXM zy7iblH;TK@?Gx4GE`&X}#}j$$1Y`WK9+xYEEZ`$hzcux@b;-VMB2H0f7GkE^Y#k84 z@rriCOcsgdXLpqMGYWvVE=3ZsVIq5G)kYz;d$ALDo7nNAYP)>B{7FK2WC5c&c~ApC zxmJ5coCm486q5Pi zimczl>`b5b1edpeE@*3kB}kl~7rDHLu_3ljz;Bxo#|KpaZqhNU&hf=TK7O`GS zfa7y=8Mu|lf%8~~_axNKd0~(!qv@=q65QdKPGKp>@Zhyf7+1Yk>7;&o~pQOIKSq;mI%sw*L&zC=nANf){DV~{~ zxCU%{9vtYL!4qd?d>syV*jF_p!mW8FU2-iJj@*c*jpPy><>kbb`g)Z1Ca@Z|=)N4* z`rt+;b6N%rH{(y^xpCb%I6;^ibVF>U)!@88ludHbBE~hel0B&_`JDPo_mcr*3Qn`{ zd<VFtr~-N#FG@ z;Oc3Ya$MD9l@0n5G~o8v>v`^jSkYsJ`0J}-_!>_c5n-2%Et)NK@r#-iqzx97fG!^CL2+MM zU9I;1munEU@+)O!j)@p;O^(-c$}(drRjN;7o)`4LG<|+fs;NARylLQEYrV$ioLeWZ z{n_Rvq+lBO#3w&hI$wIzOIdDAiR6c(vy$PMY}M;z1{vpA#Yan=JzowPa{HBu@?}OJ z{ZMrtQ_iooD6&zi`7*7XGW?|6=8J_T&qjHEz>2a8QJzM=|JZ8jh&~n;y<_b;{Q|&Kv3iHJ?>6d*MC=Shu_=l^ICCM)7ED^-E*J9&#y$8e$r`f1kAqBA%P*I28gu8ED+lX+t^%tv>Mr= zaG`3!F(a$4;_Swd7Q+7w+zZrfd9tuO(~9UA=)LfC|Ct*ZFD-Gt&^V$#QUVI2pK{CN z%m^;hh(}hON*6@WjUB1a5}ys2h)&sly+|ds``#?-&Y9738&r_cd`y^adeqQsTbH1` z(fn(3dh*#*--CVKOavaxRKvCRKWBU! zzwHk%@m>V>vE+s{5q|%fc5wW)KBfGi%Eil?Kv%WiGA~KjLfDIMq1br*tK~)E8FymS zCJgzY`DaJEg6g5C;I69;Ur$fvDKyt}o&3jA3Dd=GhAG}#Z)?KDv9SiMX633g<$FL zb@mCOx}J}4NzbvwwR_!7N8%SVD zvNgOV3%fqj-cDR9)4d`0To&Dq*)xnv1*}q(Nyz_-p)iO_jpREt+>ZG;LEX_$?ZS3* zOs-zSnP+WLb1d1toix}_jXvN`^NS~n24O8mbe%W$T|0Hin51bSa>A6D7!|&vp4llDs|;5?%5h}>|MouS-6V*@`6t;bq9D~ zzl!DRJg0rteNJm%eAHum(iMXrl}hB9BfTASWEiCs!RH{f9b@6P{B44osDnI@+%rdf zJ0{F9YP6j?v7J2ck>~R81a*~Rlop|94i0%9sb`MPcFa70+yj2o>%N_g6_V8csLb?n zd%8KX=ckyX@YJ$rE{>3*oZ;A`ILufU8X`JfS!U^`c&${eSSa@4nOT24vB1sx4}RAT$t#ahgPN^Y5Sf#YkDT;S!FD2 zPTHH9XM}UpdVZl?f3)mY6w7Cr?_5r#RJ=0#NUrEg?2TnVDUPn27X?#~KDhrTAe%iE8Zm?H`9| z30K~$!Sq7JTvDC1wSUgd&W||wbgDEe_Y+nyPP1}wzfbgF<)GtG>3?_>dpO;#lS03S zr@U{~@4n`?HgSwy7W+8S;~5A4v7LqRX3S{&Xm(!b>$n@u!kp(U9JCzC?{!1zcL?*P zEi5(@&>w4A*leCF`B>=WQ*sDYJv)BA z*)fWl_cpI1kG2qoIIeo=RBX-uLnAlk`d|#>IE6{QWKLPi+E*!sxnMv^T=xr$lIeN@ zOgX=@N!eA&7^YoU!>xEacA8(|6xpx5a_q#XomOPJHJSf0J$vnoV?#mXr~KH{@|xZg zv4Su)yvq~c%lHfvQ+0O}h0CF8Z|^DdFvz5XwR=i4{`(;17=|>JH%2B?xT#&H8JWYO z2GSPN*EzYEd1=heLVXN{kGDX%{iaG7J5glK2_MG=8W<lp6k zFUM#6NMcy^QXJFL%TLDa6Z=$`r_X#t!L}mnuGpGt&5+_KOY2yiXVp(Y!t*WeMrM^r zV)};~@Y8+#0=;LF}MljiepWUQ!t zPmgk;R!wZcFtPgO{KNToFU!c}y|5|&&Vm>M3 zc5K~4q7!6&&SCqAspb_?5>cOUMSCHqWv`}5k^6KZcrWSL#NWO31sh9phe@9MF+jlPvf0oJN8mUAi0kGC0> zohJOoK z=e2CudLMoAH#d8n%qVB*_}<*0m{YEg<5RM^jSc%wuiUE1NcW6>-3P)|ZpX%McJ1CO zlgQduqvYyU!jZWd3+|L@S=5oqtGOAQUG`a7)sbOY&ht-I8~Pf_{w80OjXXZo%dt2n z)^@@;uKVDQ{79@1djm}h-6v#O3Hq{4LfvP2Srw9NSKYziVC?fB$Q(<$Ij?)AMmlFq z9r`H26IdXTK32zO5_9onkV&e0S2!dpsk4{;=uNUsIy<;;WK|tJ@G$AzB*FcAFXuq= zEvv(cArDXeCoA5bkc<@PzS`6?^#}q1mc{mgl+-hKbb237KxBj-RDkGxbUH#Z6IX@|u%?9{X5?vM-!0N??% zA%NC#ajmZ|zc{}fsESbjYP%&QqXsxF0h|U*^Vl!0?WCSfCKCww4uoWA10Wm#1lV&0 zKoCIe56R%;I;fzh_c;N4s|Q2~z0X`_Pb2X0c&pne1bsDLxx zfHQ3&830M>J3s?C@<9HI45+$|>%ftV-Y0UQec%#$;7CL7!w-0sy{)}w!I9!9aAf5r z!La8WvZoulTg{nbTM9}ik#P4U9jciy+ZE&mcB=NOk67oiYM3{tuKUpE$GPk$>f4wh z`Za8?$^Ol4b0_3fg=m&(44boVM{U}7Hk-Wis6O@98%pnM`bBkH0kLZ8hP`sYmB%W4 zuR=g|isQ{MctgQb0As zlKPt0J3Rt+bbQVUY+XP!A(=SV#Z*^UY(sr*;9V90lY!9!C&m(|By9^xrzLH3H76x) zv!^E{Z40KSBnL*5Ifth=T!24YC!E6-PJj`dy1)onq>J*1w(D@3RgUX;nsturkTxKV zX#>*8tW{2rXKKAO%WRLhhi&~$t;941>Bi}l@?c|K#;z%~w;#D4^)Q*b~aPVdp@Mr56=kVUo(`iXxK>T99v%m`SSM_~l)zyn~1?hz0H3-S0E z0h98GjetG(2#tVs9xrhYXACL;PFws`&7QsJ95uX-Be}J7>ju8-St%#MEcH_*GXM!5`m~%&c5k8YVNWjK0wlQ*aaxm7n{!6qq zu)xM(gF+}-DgP2RDOoisIoUb2DJj94lxz?VKmf4=Dz4uWJC`OU7l;dxI01=+oehv6 zKmm5}?*b4uK;nW>vO#}iA)G*Y4k#dT0SGP*E=mqgHcAdK$G^&PacEM4IXEc6P~cz# z131ucAQ-~_`_QJ;ru@5uT>tGLFE5LdyPYwMoUxO>k-n2Yi=wlE({F{EoGgp1v5l#d z86}t%0^;Y##`tfZyaH}^`i7RqPLu}5rsg($RD1Q!RFvjMd{n9&a;$Q8BF1Lsk{%Am ziXQSxh8~uNP$MbyT?;1pn9St4K?VQYQZ76@&sBd8F?8HY!MftCK?fzD;jU)44jxihB zTC)HrSU}8RmfsZtGWSadTO(&fV+VdYb3+GPM_Uu8Z^e|Vwhl&=Y^)$o-rxBDh2drS zZR>Bg|HbETntQ!5GW;KscFqn~e^E3tWHGifwl=nLas(U#{pFC6A-5sW^_*-S`2UKN zqOtvd30CI(icZFMX8Jbf#*~t_Hl~!%M8sr;p8uCaz}f#2{=&2Ty_Ww&hk=&;ACLbL zJ~w?kz+2Y8yahnqBLC&ClC7=P|1N@m!g&8jVJxceWXuo58N$R0WdcE!KpfnxU~Uc$ zCRPYHD=RO{AIQH-%Gny3o4Eh~Dfu_#|7pVk4#>&h=aglf6@GB+22F~jg*a} zlfI3i@&8F}e=BjaQ#~m__x6S>0SQ`*S{5ke+&Gd z-u3T=>%VUMK&lNShyRnS^Z&10ADHOyQMm${{QqXo1O3~SLe$*R&Pv}MNN<$Pove&0 zogAEvf6pL*I{um#fmvDqhW={;qW0R@ghkQZ!x)%uu(GIXQi3SiDS-(Q%WGR(C*U7e z%GWGXHYT=|oWF|#vZyAnkQgTi7l$wym^q0-MA*50=SqK1mH7X^5r6@4b1Nrf2NrQF zAiESbHncS|{_lwogpKX*^B%WUzA<&BaDB4QgFLCcHy`C9%vD`NUne{$S4xQ2mOd$Z z@pNbMXEd#4xzW4^UaaLZSuiFMvljGgIfeY(+U;>t*m42hhzJirF}#*1Pj3n2Ri$(Aki~wH*dU#0kqj4NEEp5{8R%vvli{GiQDJWkeg?tkq^`7i8K)3K;$qqZwd9Iwnt7B^1t=ELNtvDRw>=#Rv(@h z^{IotSFol_r*6Qrw~DwSo&M52EF0><8B^sPgty9ilGKW`-HJ|@$;TA{{wgS8$1&bx zIlAQ>B)@>d&v2+NQRfeFf@h(QrT9p_JNSsV711jzilKMp?c5;div`r#R*ahmNbnEP zS|6Z19Yhq)qKCbOw;+%>rn?XkAV#BBUVa;i<^K#Vxb*>iASJ$mK9Xn~>pTA}Rxtcp0<=Yq18bR zz92$4CXKl;;Qc<*xFDfXo5n=u=1^S|`M3~{)f>4|5(oVc`6WZg_|JlvUc$LY~@lb39L{`D~Rtuz;^vK=|IU{y+N$!D#AFguds7G*aKh|=CI#V>BHzz=qqHf>M7 zVsgAJ`Q$aIxS2`#I1_J|q#IuVN+%tJk?RmN?fu@dV^_0mzw!0i^R*=JDa)sPVqIVS zxZR!`$>aIB3pRdhZRNtgMQ}2i+`RWvc5^T`!Ny<(u0s&;_OBNwh?0}z_iDrMQ~!E_ zfMt%~FG`#L(SbODm4!caY#eNNbe!NjI1Y9W@SiwfiG%%)j_dab@J|~YP|)47P)^Q2 zahxD_U?llxS-_P$z5rB#;p`6_7wg@!Q1Bi9IH8<(bPy05$Dj2#8ct9Z> zclr>N>rQ)sSlKxK3`{$6JLM;n2Fg|9!k0lGVJ0fE`UckAO|y;~M?hc5`s1^u%> zgTPR>J34k&=%4)*h}50F0Ro!*PrG1NPSBt22GHgDGp1lR2;@&5U^eI-|F~E=+5Yea z1Oh?9f5Z<2;)49?3jkvUmJt6DXAm3EAb;2eLU6Y%CnxmJ{tSY0K-vDV%f`wM{_|P@ zbdW#$E*mQ+E98$p#0KI5I`2RIV`Bp@-#=ptFaXv|?~Gxb;CpoJ_vkq8(Q)3RgWRLz zx0R7CJRtY*fZW4_>mD9l_weAlhX>a^ zJh=YwnT?GL0=+X{v9faA!vk19zrzg}U+(CDYxN&Gzy=rlAARljMF5Pu|G@pFyN3th z-(B2Y9#Gal`tD!#-NOSI>+aTfACJ530RryOf0hL<;D71>zEI#Zh2Q4?aX)9h%L7QV z|5+9&0s_X)yEg9f0I}ZX0b;$&1H^g{570e4KzG{%1p0&9Z~uV&;EsQwdw2lj-(B22 zJb>KiF76&4pnG@#W9L80vavx~{~ULJ`wZMu{(<{TcMlIBN4ksq!{^^NfN=w;;-Bpc zjKz1$-ou0K9vJ_@51)aUf`GW(i7ER%JlOBy0gNwqZT#W$ZyUh4cGt!|JUH&*0o)t^VFRd-4d8L7 zJ|Or02Mz#$IPT#Ah5WeKkwbY zZQRW#f%@*@0px^tZQR2Hn9uwJ2iRZ-#+=_Y{=tLu9v=7H&p|-G_Wu-X#<&f`FcjSb z_zqn%H=<>yK0v#+Ku?e%LxFZNf}X#hP6LGGTZ@hnD3hO(^F+$wd9F9J+kHSC)qv*> zc;0~LEwfubrdpFI7SI7aZ@}{gJa54B_5|yZ8Si-kp6j$1^XEF`*Ow!#htA3+&zbS= z19;waey(%j{m>aR*eRB|);RJ3`wDp8mZWWquj()<4Sr>(OHmlBI&`d#b{=!7Q~Y^J z+vcO{FyOfX&kcC4Dk8?>K7i+z*{zt5`j3@)SaSoO8}QszPnY}w&sAE09ju3HtMa_e zc-ICzH`U#BZd#*lZi?A+p#9=0xZn2%JlD|!&(4ny@_pdazyMK%+=VKnWwR1-jhD2|BXEN zsC|&emX>?heL7n1acivfx%zyV^j^m2&1!Vr`M9j7kI(Zu?DVnT?+sb|^J}r|#n)-Q zT(1NA+_VV8ZohwieHnJQJA%Wo`tpu+1x&{6N*F+K( literal 0 HcmV?d00001 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 + + + +