From 1ef900aac7ade0e2e3ffda2bb3171677e2816eef Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Wed, 8 Jan 2020 17:04:40 -0800 Subject: [PATCH] Scaffold Dockerfile next to project file for .NET (#1502) * Sketch switch to scaffolder. * Sketch moving .NET Core configuration to scaffolder. * Fixup relative paths. * Scaffold Dockerfile to the project directory. * Add back telemetry capture of prompts. * Capture project prompt cancellation step. * Fixup .NET telemetry properties. * Consolidate dockerignore generation. * Add copyright text. * Fixup tests. * Prevent tests from generating debug tasks. * Fixup test telemetry. * Fixup tests. * More merge from master. * Updates per PR feedback. * Some comments per PR feedback. --- extension.bundle.ts | 2 +- src/commands/containers/browseContainer.ts | 2 +- src/configureWorkspace/configUtils.ts | 57 +++- src/configureWorkspace/configure.ts | 261 ++++++------------ src/configureWorkspace/configureDotNetCore.ts | 116 ++++++-- src/configureWorkspace/scaffolding.ts | 151 ++++++++++ src/utils/captureCancelStep.ts | 25 +- test/configure.test.ts | 90 +++--- 8 files changed, 458 insertions(+), 246 deletions(-) create mode 100644 src/configureWorkspace/scaffolding.ts diff --git a/extension.bundle.ts b/extension.bundle.ts index 05ff850977..ce0cf1f36c 100644 --- a/extension.bundle.ts +++ b/extension.bundle.ts @@ -17,7 +17,7 @@ export { activateInternal } from './src/extension'; // // The tests should import '../extension.bundle.ts'. At design-time they live in tests/ and so will pick up this file (extension.bundle.ts). // At runtime the tests live in dist/tests and will therefore pick up the main webpack bundle at dist/extension.bundle.js. -export { configure, ConfigureApiOptions, ConfigureTelemetryProperties } from './src/configureWorkspace/configure'; +export { configure, ConfigureApiOptions } from './src/configureWorkspace/configure'; export { configPrefix } from './src/constants'; export { ProcessProvider } from './src/debugging/coreclr/ChildProcessProvider'; export { DockerBuildImageOptions, DockerClient } from './src/debugging/coreclr/CliDockerClient'; diff --git a/src/commands/containers/browseContainer.ts b/src/commands/containers/browseContainer.ts index c5f1069d49..24423af059 100644 --- a/src/commands/containers/browseContainer.ts +++ b/src/commands/containers/browseContainer.ts @@ -14,7 +14,7 @@ type BrowseTelemetryProperties = TelemetryProperties & { possiblePorts?: number[ type ConfigureBrowseCancelStep = 'node' | 'port'; async function captureBrowseCancelStep(cancelStep: ConfigureBrowseCancelStep, properties: BrowseTelemetryProperties, prompt: () => Promise): Promise { - return await captureCancelStep(cancelStep, properties, prompt) + return await captureCancelStep(cancelStep, properties, prompt)(); } // NOTE: These ports are ordered in order of preference. diff --git a/src/configureWorkspace/configUtils.ts b/src/configureWorkspace/configUtils.ts index 12c37b23d6..f016afbaae 100644 --- a/src/configureWorkspace/configUtils.ts +++ b/src/configureWorkspace/configUtils.ts @@ -3,11 +3,28 @@ * Licensed under the MIT License. See LICENSE.md in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as path from 'path'; import vscode = require('vscode'); -import { IAzureQuickPickItem } from 'vscode-azureextensionui'; +import { IAzureQuickPickItem, TelemetryProperties } from 'vscode-azureextensionui'; +import { DockerOrchestration } from '../constants'; import { ext } from "../extensionVariables"; +import { captureCancelStep } from '../utils/captureCancelStep'; import { Platform, PlatformOS } from '../utils/platform'; +export type ConfigureTelemetryProperties = { + configurePlatform?: Platform; + configureOs?: PlatformOS; + orchestration?: DockerOrchestration; + packageFileType?: string; // 'build.gradle', 'pom.xml', 'package.json', '.csproj', '.fsproj' + packageFileSubfolderDepth?: string; // 0 = project/etc file in root folder, 1 = in subfolder, 2 = in subfolder of subfolder, etc. +}; + +export type ConfigureTelemetryCancelStep = 'folder' | 'platform' | 'os' | 'compose' | 'port' | 'project'; + +export async function captureConfigureCancelStep Promise>(cancelStep: ConfigureTelemetryCancelStep, properties: TelemetryProperties, prompt: TPrompt): Promise { + return await captureCancelStep(cancelStep, properties, prompt)(); +} + /** * Prompts for port numbers * @throws `UserCancelledError` if the user cancels. @@ -110,3 +127,41 @@ export async function quickPickGenerateComposeFiles(): Promise { return response.data; } + +export function getSubfolderDepth(outputFolder: string, filePath: string): string { + let relativeToRoot = path.relative(outputFolder, path.resolve(outputFolder, filePath)); + let matches = relativeToRoot.match(/[\/\\]/g); + let depth: number = matches ? matches.length : 0; + return String(depth); +} + +export function genCommonDockerIgnoreFile(platformType: Platform): string { + const ignoredItems = [ + '**/.classpath', + '**/.dockerignore', + '**/.env', + '**/.git', + '**/.gitignore', + '**/.project', + '**/.settings', + '**/.toolstarget', + '**/.vs', + '**/.vscode', + '**/*.*proj.user', + '**/*.dbmdl', + '**/*.jfm', + '**/azds.yaml', + platformType !== 'Node.js' ? '**/bin' : undefined, + '**/charts', + '**/docker-compose*', + '**/Dockerfile*', + '**/node_modules', + '**/npm-debug.log', + '**/obj', + '**/secrets.dev.yaml', + '**/values.dev.yaml', + 'README.md' + ]; + + return ignoredItems.filter(item => item !== undefined).join('\n'); +} diff --git a/src/configureWorkspace/configure.ts b/src/configureWorkspace/configure.ts index 10689bfdd0..0d9d6ffa31 100644 --- a/src/configureWorkspace/configure.ts +++ b/src/configureWorkspace/configure.ts @@ -10,22 +10,17 @@ import * as path from "path"; import * as vscode from "vscode"; import { IActionContext, TelemetryProperties } from 'vscode-azureextensionui'; import * as xml2js from 'xml2js'; -import { DockerOrchestration } from '../constants'; -import { ext } from '../extensionVariables'; -import { captureCancelStep } from '../utils/captureCancelStep'; -import { extractRegExGroups } from '../utils/extractRegExGroups'; -import { globAsync } from '../utils/globAsync'; import { Platform, PlatformOS } from '../utils/platform'; -import { quickPickWorkspaceFolder } from '../utils/quickPickWorkspaceFolder'; import { configureCpp } from './configureCpp'; -import { configureAspDotNetCore, configureDotNetCoreConsole } from './configureDotNetCore'; +import { scaffoldNetCore } from './configureDotNetCore'; import { configureGo } from './configureGo'; import { configureJava } from './configureJava'; import { configureNode } from './configureNode'; import { configureOther } from './configureOther'; import { configurePython } from './configurePython'; import { configureRuby } from './configureRuby'; -import { promptForPorts, quickPickGenerateComposeFiles, quickPickOS, quickPickPlatform } from './configUtils'; +import { ConfigureTelemetryProperties, genCommonDockerIgnoreFile, getSubfolderDepth, quickPickGenerateComposeFiles } from './configUtils'; +import { registerScaffolder, scaffold, Scaffolder, ScaffolderContext, ScaffoldFile } from './scaffolding'; export interface PackageInfo { npmStart: boolean; // has npm start @@ -50,19 +45,6 @@ interface PomXmlContents { }; } -type ConfigureTelemetryCancelStep = 'folder' | 'platform' | 'os' | 'compose' | 'port'; - -async function captureConfigureCancelStep(cancelStep: ConfigureTelemetryCancelStep, properties: TelemetryProperties, prompt: () => Promise): Promise { - return await captureCancelStep(cancelStep, properties, prompt) -} - -export type ConfigureTelemetryProperties = { - configurePlatform?: Platform; - configureOs?: PlatformOS; - packageFileType?: string; // 'build.gradle', 'pom.xml', 'package.json', '.csproj', '.fsproj' - packageFileSubfolderDepth?: string; // 0 = project/etc file in root folder, 1 = in subfolder, 2 = in subfolder of subfolder, etc. -}; - export interface IPlatformGeneratorInfo { genDockerFile: GeneratorFunction, genDockerCompose: GeneratorFunction, @@ -79,12 +61,46 @@ export function getComposePorts(ports: number[]): string { return ports && ports.length > 0 ? ' ports:\n' + ports.map(port => ` - ${port}:${port}`).join('\n') : ''; } +function configureScaffolder(generator: IPlatformGeneratorInfo): Scaffolder { + return async context => { + let files = await configureCore( + context, + { + folder: context.folder, + os: context.os, + outputFolder: context.outputFolder, + platform: context.platform, + ports: context.ports, + rootPath: context.rootFolder, + }); + + const updatedFiles = files.map( + file => { + return { + fileName: file.fileName, + contents: file.contents, + open: path.basename(file.fileName).toLowerCase() === 'dockerfile' + }; + }); + + return updatedFiles; + }; +} + +registerScaffolder('.NET Core Console', scaffoldNetCore); +registerScaffolder('ASP.NET Core', scaffoldNetCore); +registerScaffolder('C++', configureScaffolder(configureCpp)); +registerScaffolder('Go', configureScaffolder(configureGo)); +registerScaffolder('Java', configureScaffolder(configureJava)); +registerScaffolder('Node.js', configureScaffolder(configureNode)); +registerScaffolder('Python', configureScaffolder(configurePython)); +registerScaffolder('Ruby', configureScaffolder(configureRuby)); +registerScaffolder('Other', configureScaffolder(configureOther)); + const generatorsByPlatform = new Map(); -generatorsByPlatform.set('ASP.NET Core', configureAspDotNetCore); generatorsByPlatform.set('C++', configureCpp); generatorsByPlatform.set('Go', configureGo); generatorsByPlatform.set('Java', configureJava); -generatorsByPlatform.set('.NET Core Console', configureDotNetCoreConsole); generatorsByPlatform.set('Node.js', configureNode); generatorsByPlatform.set('Python', configurePython); generatorsByPlatform.set('Ruby', configureRuby); @@ -122,34 +138,7 @@ function genDockerComposeDebug(serviceNameAndRelativePath: string, platform: Pla } function genDockerIgnoreFile(service: string, platformType: Platform, os: string, ports: number[]): string { - const ignoredItems = [ - '**/.classpath', - '**/.dockerignore', - '**/.env', - '**/.git', - '**/.gitignore', - '**/.project', - '**/.settings', - '**/.toolstarget', - '**/.vs', - '**/.vscode', - '**/*.*proj.user', - '**/*.dbmdl', - '**/*.jfm', - '**/azds.yaml', - platformType !== 'Node.js' ? '**/bin' : undefined, - '**/charts', - '**/docker-compose*', - '**/Dockerfile*', - '**/node_modules', - '**/npm-debug.log', - '**/obj', - '**/secrets.dev.yaml', - '**/values.dev.yaml', - 'README.md' - ]; - - return ignoredItems.filter(item => item !== undefined).join('\n'); + return genCommonDockerIgnoreFile(platformType); } async function getPackageJson(folderPath: string): Promise { @@ -265,29 +254,6 @@ async function readPomOrGradle(folderPath: string): Promise<{ foundPath?: string return { foundPath, packageInfo: pkg }; } -// Returns the relative path of the project file without the extension -async function findCSProjOrFSProjFile(folderPath: string): Promise { - const opt: vscode.QuickPickOptions = { - matchOnDescription: true, - matchOnDetail: true, - placeHolder: 'Select Project' - } - - const projectFiles: string[] = await globAsync('**/*.@(c|f)sproj', { cwd: folderPath }); - - if (!projectFiles || !projectFiles.length) { - throw new Error("No .csproj or .fsproj file could be found. You need a C# or F# project file in the workspace to generate Docker files for the selected platform."); - } - - if (projectFiles.length > 1) { - let items = projectFiles.map(p => { label: p }); - let result = await ext.ui.showQuickPick(items, opt); - return result.label; - } else { - return projectFiles[0]; - } -} - type GeneratorFunction = (serviceName: string, platform: Platform, os: PlatformOS | undefined, ports: number[], packageJson?: Partial) => string; type DebugScaffoldFunction = (context: IActionContext, folder: vscode.WorkspaceFolder, os: PlatformOS, dockerfile: string, packageInfo: PackageInfo) => Promise; @@ -298,19 +264,12 @@ const DOCKER_FILE_TYPES: { [key: string]: { generator: GeneratorFunction, isComp '.dockerignore': { generator: genDockerIgnoreFile } }; -const YES_PROMPT: vscode.MessageItem = { - title: "Yes", - isCloseAffordance: false -}; -const YES_OR_NO_PROMPTS: vscode.MessageItem[] = [ - YES_PROMPT, - { - title: "No", - isCloseAffordance: true - } -]; - export interface ConfigureApiOptions { + /** + * Determines whether to add debugging tasks/configuration during scaffolding. + */ + initializeForDebugging?: boolean; + /** * Root folder from which to search for .csproj, package.json, .pom or .gradle files */ @@ -319,7 +278,7 @@ export interface ConfigureApiOptions { /** * Output folder for the docker files. Relative paths in the Dockerfile we will calculated based on this folder */ - outputFolder: string; + outputFolder?: string; /** * Platform @@ -336,11 +295,6 @@ export interface ConfigureApiOptions { */ os?: PlatformOS; - /** - * Open the Dockerfile that was generated - */ - openDockerFile?: boolean; - /** * The workspace folder for configuring */ @@ -348,65 +302,62 @@ export interface ConfigureApiOptions { } export async function configure(context: IActionContext, rootFolderPath: string | undefined): Promise { - const properties: TelemetryProperties & ConfigureTelemetryProperties = context.telemetry.properties; - let folder: vscode.WorkspaceFolder; - if (!rootFolderPath) { - /* eslint-disable-next-line @typescript-eslint/promise-function-async */ - folder = await captureConfigureCancelStep('folder', properties, () => quickPickWorkspaceFolder('To generate Docker files you must first open a folder or workspace in VS Code.')); - rootFolderPath = folder.uri.fsPath; - } + const scaffoldContext = { + ...context, + // NOTE: Currently only tests use rootFolderPath and they do not function when debug tasks/configuration are added. + // TODO: Refactor tests to allow for (and verify) debug tasks/configuration. + initializeForDebugging: rootFolderPath === undefined, + rootFolder: rootFolderPath + }; - let filesWritten = await configureCore( - context, - { - rootPath: rootFolderPath, - outputFolder: rootFolderPath, - openDockerFile: true, - folder: folder, - }); + const files = await scaffold(scaffoldContext); - // 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 - } + files.filter(file => file.open).forEach( + file => { + /* eslint-disable-next-line @typescript-eslint/no-floating-promises */ + vscode.window.showTextDocument(vscode.Uri.file(file.filePath)); + }); } export async function configureApi(context: IActionContext, options: ConfigureApiOptions): Promise { - await configureCore(context, options); + const scaffoldContext = { + ...context, + folder: options?.folder, + initializeForDebugging: options?.initializeForDebugging, + os: options?.os, + outputFolder: options?.outputFolder, + platform: options?.platform, + ports: options?.ports, + rootFolder: options?.rootPath, + }; + + await scaffold(scaffoldContext); } // tslint:disable-next-line:max-func-body-length // Because of nested functions -async function configureCore(context: IActionContext, options: ConfigureApiOptions): Promise { +async function configureCore(context: ScaffolderContext, options: ConfigureApiOptions): Promise { const properties: TelemetryProperties & ConfigureTelemetryProperties = context.telemetry.properties; const rootFolderPath: string = options.rootPath; - const outputFolder = options.outputFolder; + const outputFolder = options.outputFolder ?? rootFolderPath; - const platformType: Platform = options.platform || await captureConfigureCancelStep('platform', properties, quickPickPlatform); - properties.configurePlatform = platformType; + const platformType: Platform = options.platform; let generatorInfo = generatorsByPlatform.get(platformType); let os: PlatformOS | undefined = options.os; - if (!os && platformType.toLowerCase().includes('.net')) { - os = await captureConfigureCancelStep('os', properties, quickPickOS); - } properties.configureOs = os; - properties.orchestration = 'single' as DockerOrchestration; let generateComposeFiles = true; if (platformType === 'Node.js') { - generateComposeFiles = await captureConfigureCancelStep('compose', properties, quickPickGenerateComposeFiles); + generateComposeFiles = await context.captureStep('compose', quickPickGenerateComposeFiles)(); + if (generateComposeFiles) { + properties.orchestration = 'docker-compose'; + } } let ports: number[] | undefined = options.ports; if (!ports && generatorInfo.defaultPorts !== undefined) { - /* eslint-disable-next-line @typescript-eslint/promise-function-async */ - ports = await captureConfigureCancelStep('port', properties, () => promptForPorts(generatorInfo.defaultPorts)); + ports = await context.promptForPorts(generatorInfo.defaultPorts); } let targetFramework: string; @@ -415,20 +366,7 @@ async function configureCore(context: IActionContext, options: ConfigureApiOptio { // Scope serviceNameAndPathRelativeToRoot only to this block of code let serviceNameAndPathRelativeToRoot: string; - if (platformType.toLowerCase().includes('.net')) { - let projFilePath = await findCSProjOrFSProjFile(rootFolderPath); - serviceNameAndPathRelativeToRoot = projFilePath.slice(0, -(path.extname(projFilePath).length)); - let projFileContents = (await fse.readFile(path.join(rootFolderPath, projFilePath))).toString(); - - // Extract TargetFramework for version - [targetFramework] = extractRegExGroups(projFileContents, /(.+)<\/TargetFramework/, ['']); - projFile = projFilePath; - - properties.packageFileType = projFilePath.endsWith('.csproj') ? '.csproj' : '.fsproj'; - properties.packageFileSubfolderDepth = getSubfolderDepth(serviceNameAndPathRelativeToRoot); - } else { - serviceNameAndPathRelativeToRoot = path.basename(rootFolderPath).toLowerCase(); - } + serviceNameAndPathRelativeToRoot = path.basename(rootFolderPath).toLowerCase(); // We need paths in the Dockerfile to be relative to the output folder, not the root serviceNameAndPathRelativeToOutput = path.relative(outputFolder, path.join(rootFolderPath, serviceNameAndPathRelativeToRoot)); @@ -441,14 +379,14 @@ async function configureCore(context: IActionContext, options: ConfigureApiOptio ({ packageInfo, foundPath: foundPomOrGradlePath } = await readPomOrGradle(rootFolderPath)); if (foundPomOrGradlePath) { properties.packageFileType = path.basename(foundPomOrGradlePath); - properties.packageFileSubfolderDepth = getSubfolderDepth(foundPomOrGradlePath); + properties.packageFileSubfolderDepth = getSubfolderDepth(outputFolder, foundPomOrGradlePath); } } else { let packagePath: string | undefined; ({ packagePath, packageInfo } = await readPackageJson(rootFolderPath)); if (packagePath) { properties.packageFileType = 'package.json'; - properties.packageFileSubfolderDepth = getSubfolderDepth(packagePath); + properties.packageFileSubfolderDepth = getSubfolderDepth(outputFolder, packagePath); } } @@ -457,12 +395,12 @@ async function configureCore(context: IActionContext, options: ConfigureApiOptio packageInfo.artifactName = projFile; } - let filesWritten: string[] = []; + let filesWritten: ScaffoldFile[] = []; await Promise.all(Object.keys(DOCKER_FILE_TYPES).map(async (fileName) => { const dockerFileType = DOCKER_FILE_TYPES[fileName]; if (dockerFileType.isComposeGenerator && generateComposeFiles) { - properties.orchestration = 'docker-compose' as DockerOrchestration; + properties.orchestration = 'docker-compose'; } return dockerFileType.isComposeGenerator !== true || generateComposeFiles @@ -471,38 +409,17 @@ async function configureCore(context: IActionContext, options: ConfigureApiOptio })); // Can only configure for debugging if there's a workspace folder, and there's a scaffold function - if (options.folder && generatorInfo.initializeForDebugging) { + if (options.folder && context.initializeForDebugging && generatorInfo.initializeForDebugging) { await generatorInfo.initializeForDebugging(context, options.folder, os, path.join(outputFolder, 'Dockerfile'), packageInfo); } 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); - if (response === YES_PROMPT) { - writeFile = true; - } - } else { - writeFile = true; - } - - if (writeFile) { - // Paths in the docker files should be relative to the Dockerfile (which is in the output folder) - let fileContents = generatorFunction(serviceNameAndPathRelativeToOutput, platformType, os, ports, packageInfo); - if (fileContents) { - fse.writeFileSync(filePath, fileContents, { encoding: 'utf8' }); - filesWritten.push(filePath); - } + // Paths in the docker files should be relative to the Dockerfile (which is in the output folder) + let fileContents = generatorFunction(serviceNameAndPathRelativeToOutput, platformType, os, ports, packageInfo); + if (fileContents) { + filesWritten.push({ contents: fileContents, fileName }); } } - - function getSubfolderDepth(filePath: string): string { - let relativeToRoot = path.relative(outputFolder, path.resolve(outputFolder, filePath)); - let matches = relativeToRoot.match(/[\/\\]/g); - let depth: number = matches ? matches.length : 0; - return String(depth); - } } diff --git a/src/configureWorkspace/configureDotNetCore.ts b/src/configureWorkspace/configureDotNetCore.ts index 24dad60a37..578b553fdd 100644 --- a/src/configureWorkspace/configureDotNetCore.ts +++ b/src/configureWorkspace/configureDotNetCore.ts @@ -4,35 +4,25 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; +import * as fse from 'fs-extra'; import * as path from 'path'; import * as semver from 'semver'; +import * as vscode from 'vscode'; import { WorkspaceFolder } from 'vscode'; import { IActionContext } from 'vscode-azureextensionui'; import { DockerDebugScaffoldContext } from '../debugging/DebugHelper'; import { dockerDebugScaffoldingProvider, NetCoreScaffoldingOptions } from '../debugging/DockerDebugScaffoldingProvider'; +import { ext } from '../extensionVariables'; import { extractRegExGroups } from '../utils/extractRegExGroups'; +import { globAsync } from '../utils/globAsync'; import { isWindows, isWindows1019H1OrNewer, isWindows10RS3OrNewer, isWindows10RS4OrNewer, isWindows10RS5OrNewer } from '../utils/osUtils'; import { Platform, PlatformOS } from '../utils/platform'; -import { getExposeStatements, IPlatformGeneratorInfo, PackageInfo } from './configure'; +import { getExposeStatements } from './configure'; +import { ConfigureTelemetryProperties, genCommonDockerIgnoreFile, getSubfolderDepth } from './configUtils'; +import { ScaffolderContext, ScaffoldFile } from './scaffolding'; // This file handles both ASP.NET core and .NET Core Console -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 - defaultPorts: [80, 443], - initializeForDebugging, -}; - -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 - defaultPorts: undefined, - initializeForDebugging, -}; - // .NET Core 1.0 - 2.0 images are published to Docker Hub Registry. const LegacyAspNetCoreRuntimeImageFormat = "microsoft/aspnetcore:{0}.{1}{2}"; const LegacyAspNetCoreSdkImageFormat = "microsoft/aspnetcore-build:{0}.{1}{2}"; @@ -172,7 +162,7 @@ ENTRYPOINT ["dotnet", "$assembly_name$.dll"] // #endregion -function genDockerFile(serviceNameAndRelativePath: string, platform: Platform, os: PlatformOS | undefined, ports: number[], { version, artifactName }: Partial): string { +function genDockerFile(serviceNameAndRelativePath: string, platform: Platform, os: PlatformOS | undefined, ports: number[], version: string, artifactName: string): string { // VS version of this function is in ResolveImageNames (src/Docker/Microsoft.VisualStudio.Docker.DotNetCore/DockerDotNetCoreScaffoldingProvider.cs) if (os !== 'Windows' && os !== 'Linux') { @@ -267,20 +257,104 @@ function genDockerFile(serviceNameAndRelativePath: string, platform: Platform, o return contents; } -async function initializeForDebugging(context: IActionContext, folder: WorkspaceFolder, platformOS: PlatformOS, dockerfile: string, { artifactName }: Partial): Promise { +// Returns the relative path of the project file without the extension +async function findCSProjOrFSProjFile(folderPath?: string): Promise { + const opt: vscode.QuickPickOptions = { + matchOnDescription: true, + matchOnDetail: true, + placeHolder: 'Select Project' + } + + const projectFiles: string[] = await globAsync('**/*.@(c|f)sproj', { cwd: folderPath }); + + if (!projectFiles || !projectFiles.length) { + throw new Error("No .csproj or .fsproj file could be found. You need a C# or F# project file in the workspace to generate Docker files for the selected platform."); + } + + if (projectFiles.length > 1) { + let items = projectFiles.map(p => { label: p }); + let result = await ext.ui.showQuickPick(items, opt); + return result.label; + } else { + return projectFiles[0]; + } +} + +async function initializeForDebugging(context: IActionContext, folder: WorkspaceFolder, platformOS: PlatformOS, workspaceRelativeDockerfileName: string, workspaceRelativeProjectFileName: string): Promise { const scaffoldContext: DockerDebugScaffoldContext = { folder: folder, platform: 'netCore', actionContext: context, - dockerfile: dockerfile, + // always use posix for debug config because it's committed to source control and works on all OS's + /* eslint-disable-next-line no-template-curly-in-string */ + dockerfile: path.posix.join('${workspaceFolder}', workspaceRelativeDockerfileName), } const options: NetCoreScaffoldingOptions = { // always use posix for debug config because it's committed to source control and works on all OS's /* eslint-disable-next-line no-template-curly-in-string */ - appProject: path.posix.join('${workspaceFolder}', artifactName), + appProject: path.posix.join('${workspaceFolder}', workspaceRelativeProjectFileName), platformOS: platformOS, } await dockerDebugScaffoldingProvider.initializeNetCoreForDebugging(scaffoldContext, options); } + +// tslint:disable-next-line: export-name +export async function scaffoldNetCore(context: ScaffolderContext): Promise { + const os = context.os ?? await context.promptForOS(); + + const telemetryProperties = context.telemetry.properties; + + telemetryProperties.configureOs = os; + + const ports = context.ports ?? (context.platform === 'ASP.NET Core' ? await context.promptForPorts([80, 443]) : undefined); + + const rootRelativeProjectFileName = await context.captureStep('project', findCSProjOrFSProjFile)(context.rootFolder); + const rootRelativeProjectDirectory = path.dirname(rootRelativeProjectFileName); + + telemetryProperties.packageFileType = path.extname(rootRelativeProjectFileName); + telemetryProperties.packageFileSubfolderDepth = getSubfolderDepth(context.rootFolder, rootRelativeProjectFileName); + + const projectFilePath = path.posix.join(context.rootFolder, rootRelativeProjectFileName); + const workspaceRelativeProjectFileName = path.posix.relative(context.folder.uri.fsPath, projectFilePath); + + let serviceNameAndPathRelative = rootRelativeProjectFileName.slice(0, -(path.extname(rootRelativeProjectFileName).length)); + const projFileContents = (await fse.readFile(path.join(context.rootFolder, rootRelativeProjectFileName))).toString(); + + // Extract TargetFramework for version + const [version] = extractRegExGroups(projFileContents, /(.+)<\/TargetFramework/, ['']); + + if (context.outputFolder) { + // We need paths in the Dockerfile to be relative to the output folder, not the root + serviceNameAndPathRelative = path.relative(context.outputFolder, path.join(context.rootFolder, serviceNameAndPathRelative)); + } + + // Ensure the path scaffolded in the Dockerfile uses POSIX separators (which work on both Linux and Windows). + serviceNameAndPathRelative = serviceNameAndPathRelative.replace(/\\/g, '/'); + + let dockerFileContents = genDockerFile(serviceNameAndPathRelative, context.platform, os, ports, version, workspaceRelativeProjectFileName); + + // Remove multiple empty lines with single empty lines, as might be produced + // if $expose_statements$ or another template variable is an empty string + dockerFileContents = dockerFileContents + .replace(/(\r\n){3,4}/g, "\r\n\r\n") + .replace(/(\n){3,4}/g, "\n\n"); + + const dockerFileName = path.join(context.outputFolder ?? rootRelativeProjectDirectory, 'Dockerfile'); + const dockerIgnoreFileName = path.join(context.outputFolder ?? '', '.dockerignore'); + + const files: ScaffoldFile[] = [ + { fileName: dockerFileName, contents: dockerFileContents, open: true }, + { fileName: dockerIgnoreFileName, contents: genCommonDockerIgnoreFile(context.platform) } + ]; + + if (context.initializeForDebugging) { + const dockerFilePath = path.resolve(context.rootFolder, dockerFileName); + const workspaceRelativeDockerfileName = path.relative(context.folder.uri.fsPath, dockerFilePath); + + await initializeForDebugging(context, context.folder, context.os, workspaceRelativeDockerfileName, workspaceRelativeProjectFileName); + } + + return files; +} diff --git a/src/configureWorkspace/scaffolding.ts b/src/configureWorkspace/scaffolding.ts new file mode 100644 index 0000000000..6f3b2bddd8 --- /dev/null +++ b/src/configureWorkspace/scaffolding.ts @@ -0,0 +1,151 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as fse from 'fs-extra'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { IActionContext, IAzureQuickPickItem, } from "vscode-azureextensionui"; +import { ext } from "../extensionVariables"; +import { captureCancelStep } from '../utils/captureCancelStep'; +import { Platform, PlatformOS } from "../utils/platform"; +import { quickPickWorkspaceFolder } from '../utils/quickPickWorkspaceFolder'; +import { ConfigureTelemetryCancelStep, ConfigureTelemetryProperties, promptForPorts as promptForPortsUtil, quickPickOS } from './configUtils'; + +/** + * Represents the options that can be passed by callers (e.g. the programmatic scaffolding API used by IoT extension). + */ +export interface ScaffoldContext extends IActionContext { + folder?: vscode.WorkspaceFolder; + initializeForDebugging?: boolean; + os?: PlatformOS; + outputFolder?: string; + platform?: Platform; + ports?: number[]; + rootFolder?: string; +} + +/** + * Represents the context passed to individual scaffolders, with suitable defaults for critical properties. + */ +export interface ScaffolderContext extends ScaffoldContext { + captureStep Promise>(step: ConfigureTelemetryCancelStep, prompt: TPrompt): TPrompt; + folder: vscode.WorkspaceFolder; + initializeForDebugging: boolean; + platform: Platform; + promptForOS(): Promise; + promptForPorts(defaultPorts?: number[]): Promise; + rootFolder: string; +} + +export type ScaffoldedFile = { + filePath: string; + open?: boolean; +}; + +export type ScaffoldFile = { + contents: string; + fileName: string; + open?: boolean; +}; + +export type Scaffolder = (context: ScaffolderContext) => Promise; + +async function promptForFolder(): Promise { + return await quickPickWorkspaceFolder('To generate Docker files you must first open a folder or workspace in VS Code.'); +} + +async function promptForOS(): Promise { + return await quickPickOS(); +} + +async function promptForOverwrite(fileName: string): Promise { + const YES_PROMPT: vscode.MessageItem = { + title: 'Yes', + isCloseAffordance: false + }; + const YES_OR_NO_PROMPTS: vscode.MessageItem[] = [ + YES_PROMPT, + { + title: 'No', + isCloseAffordance: true + } + ]; + + const response = await vscode.window.showErrorMessage(`"${fileName}" already exists. Would you like to overwrite it?`, ...YES_OR_NO_PROMPTS); + + return response === YES_PROMPT; +} + +async function promptForPorts(defaultPorts?: number[]): Promise { + return await promptForPortsUtil(defaultPorts); +} + +const scaffolders: Map = new Map(); + +async function promptForPlatform(): Promise { + let opt: vscode.QuickPickOptions = { + matchOnDescription: true, + matchOnDetail: true, + placeHolder: 'Select Application Platform' + } + + const items = Array.from(scaffolders.keys()).map(p => >{ label: p, data: p }); + let response = await ext.ui.showQuickPick(items, opt); + return response.data; +} + +export function registerScaffolder(platform: Platform, scaffolder: Scaffolder): void { + scaffolders.set(platform, scaffolder); +} + +export async function scaffold(context: ScaffoldContext): Promise { + function captureStep Promise>(step: ConfigureTelemetryCancelStep, prompt: TPrompt): TPrompt { + return captureCancelStep(step, context.telemetry.properties, prompt); + } + + const folder = context.folder ?? await captureStep('folder', promptForFolder)(); + const rootFolder = context.rootFolder ?? folder.uri.fsPath; + const telemetryProperties = context.telemetry.properties; + + const platform = context.platform ?? await captureStep('platform', promptForPlatform)(); + + telemetryProperties.configurePlatform = platform; + + const scaffolder = scaffolders.get(platform); + + if (!scaffolder) { + throw new Error(`No scaffolder is registered for platform '${context.platform}'.`); + } + + telemetryProperties.orchestration = 'single'; + + // Invoke the individual scaffolder, passing a copy of the original context, with omitted properies given suitable defaults... + const files = await scaffolder({ + ...context, + captureStep, + folder, + initializeForDebugging: context.initializeForDebugging === undefined || context.initializeForDebugging, + platform, + promptForOS: captureStep('os', promptForOS), + promptForPorts: captureStep('port', promptForPorts), + rootFolder + }); + + const writtenFiles: ScaffoldedFile[] = []; + + await Promise.all( + files.map( + async file => { + const filePath = path.resolve(rootFolder, file.fileName); + + if (await fse.pathExists(filePath) === false || await promptForOverwrite(file.fileName)) { + await fse.writeFile(filePath, file.contents, 'utf8'); + + writtenFiles.push({ filePath, open: file.open }); + } + })); + + return writtenFiles; +} diff --git a/src/utils/captureCancelStep.ts b/src/utils/captureCancelStep.ts index 742918f0a3..694508e62a 100644 --- a/src/utils/captureCancelStep.ts +++ b/src/utils/captureCancelStep.ts @@ -5,14 +5,21 @@ import { TelemetryProperties, UserCancelledError } from "vscode-azureextensionui"; -export async function captureCancelStep(cancelStep: TCancelStep, properties: TelemetryProperties, prompt: () => Promise): Promise { - try { - return await prompt(); - } catch (error) { - if (error instanceof UserCancelledError) { - properties.cancelStep = cancelStep.toString(); - } +/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ +export function wrapWithCatch Promise>(prompt: TPrompt, onRejected: (reason: any) => Promise): TPrompt { + return (async (...args: []) => { + return prompt(...args).catch(onRejected); + }); +} + +export function captureCancelStep Promise>(cancelStep: TCancelStep, properties: TelemetryProperties, prompt: TPrompt): TPrompt { + return wrapWithCatch( + prompt, + error => { + if (error instanceof UserCancelledError) { + properties.cancelStep = cancelStep.toString(); + } - throw error; - } + throw error; + }); } diff --git a/test/configure.test.ts b/test/configure.test.ts index 90518492b4..a77e9b3cd0 100644 --- a/test/configure.test.ts +++ b/test/configure.test.ts @@ -9,10 +9,11 @@ import * as vscode from 'vscode'; import * as fse from 'fs-extra'; import * as path from 'path'; import { Suite } from 'mocha'; -import { PlatformOS, Platform, ext, configure, ConfigureTelemetryProperties, ConfigureApiOptions, globAsync } from '../extension.bundle'; +import { PlatformOS, Platform, ext, configure, ConfigureApiOptions, globAsync } from '../extension.bundle'; import { IActionContext, TelemetryProperties, IAzExtOutputChannel, createAzExtOutputChannel } from 'vscode-azureextensionui'; import { getTestRootFolder, testInEmptyFolder, testUserInput } from './global.test'; import { TestInput } from 'vscode-azureextensiondev'; +import { ConfigureTelemetryProperties } from '../src/configureWorkspace/configUtils'; // Can be useful for testing const outputAllGeneratedFileContents = false; @@ -66,7 +67,14 @@ async function readFile(pathRelativeToTestRootFolder: string): Promise { async function testConfigureDockerViaApi(options: ConfigureApiOptions, inputs: (string | TestInput)[] = [], expectedOutputFiles?: string[]): Promise { await testUserInput.runWithInputs(inputs, async () => { - await vscode.commands.executeCommand('vscode-docker.api.configure', options); + await vscode.commands.executeCommand( + 'vscode-docker.api.configure', + { + // NOTE: Currently the tests do not comprehend adding debug tasks/configuration. + // TODO: Refactor tests to do so (and verify results). + initializeForDebugging: false, + ...options + }); }); if (expectedOutputFiles) { @@ -396,10 +404,10 @@ suite("Configure (Add Docker files to Workspace)", function (this: Suite): void packageFileSubfolderDepth: projectFolder.includes('/') ? '2' : '1' }, [os /* it doesn't ask for a port, so we don't specify one here */], - ['Dockerfile', '.dockerignore', `${projectFolder}/Program.cs`, `${projectFolder}/${projectFileName}`] + [`${projectFolder}/Dockerfile`, '.dockerignore', `${projectFolder}/Program.cs`, `${projectFolder}/${projectFileName}`] ); - let dockerFileContents = await readFile('Dockerfile'); + let dockerFileContents = await readFile(`${projectFolder}/Dockerfile`); if (expectedDockerFileContents) { assert.equal(dockerFileContents, expectedDockerFileContents); } @@ -427,10 +435,10 @@ suite("Configure (Add Docker files to Workspace)", function (this: Suite): void packageFileSubfolderDepth: '1' }, [os, '1234'], - ['Dockerfile', '.dockerignore', `${projectFolder}/Program.cs`, `${projectFolder}/${projectFileName}`] + [`${projectFolder}/Dockerfile`, '.dockerignore', `${projectFolder}/Program.cs`, `${projectFolder}/${projectFileName}`] ); - let dockerFileContents = await readFile('Dockerfile'); + let dockerFileContents = await readFile(`${projectFolder}/Dockerfile`); if (expectedDockerFileContents) { assert.equal(dockerFileContents, expectedDockerFileContents); } @@ -648,13 +656,13 @@ suite("Configure (Add Docker files to Workspace)", function (this: Suite): void packageFileSubfolderDepth: '1' }, ['Windows', 'projectFolder2/aspnetapp.csproj'], - ['Dockerfile', '.dockerignore', 'projectFolder1/aspnetapp.csproj', 'projectFolder2/aspnetapp.csproj'] + ['projectFolder2/Dockerfile', '.dockerignore', 'projectFolder1/aspnetapp.csproj', 'projectFolder2/aspnetapp.csproj'] ); - assertNotFileContains('Dockerfile', 'projectFolder1'); - assertFileContains('Dockerfile', `COPY ["projectFolder2/aspnetapp.csproj", "projectFolder2/"]`); - assertFileContains('Dockerfile', `RUN dotnet restore "projectFolder2/aspnetapp.csproj"`); - assertFileContains('Dockerfile', `ENTRYPOINT ["dotnet", "aspnetapp.dll"]`); + assertFileContains('projectFolder2/Dockerfile', 'projectFolder2'); + assertFileContains('projectFolder2/Dockerfile', `COPY ["projectFolder2/aspnetapp.csproj", "projectFolder2/"]`); + assertFileContains('projectFolder2/Dockerfile', `RUN dotnet restore "projectFolder2/aspnetapp.csproj"`); + assertFileContains('projectFolder2/Dockerfile', `ENTRYPOINT ["dotnet", "aspnetapp.dll"]`); }); }); @@ -691,7 +699,7 @@ suite("Configure (Add Docker files to Workspace)", function (this: Suite): void ENTRYPOINT ["dotnet", "ConsoleApp1.dll"] `)); - assertNotFileContains('Dockerfile', 'EXPOSE'); + assertNotFileContains('ConsoleApp1Folder/Dockerfile', 'EXPOSE'); }); testInEmptyFolder("Linux", async () => { @@ -723,7 +731,7 @@ suite("Configure (Add Docker files to Workspace)", function (this: Suite): void ENTRYPOINT ["dotnet", "ConsoleApp1.dll"] `)); - assertNotFileContains('Dockerfile', 'EXPOSE'); + assertNotFileContains('ConsoleApp1Folder/Dockerfile', 'EXPOSE'); }); }); @@ -760,7 +768,7 @@ suite("Configure (Add Docker files to Workspace)", function (this: Suite): void ENTRYPOINT ["dotnet", "ConsoleApp1.dll"] `)); - assertNotFileContains('Dockerfile', 'EXPOSE'); + assertNotFileContains('subfolder/projectFolder/Dockerfile', 'EXPOSE'); }); testInEmptyFolder("Linux", async () => { @@ -792,7 +800,7 @@ suite("Configure (Add Docker files to Workspace)", function (this: Suite): void ENTRYPOINT ["dotnet", "ConsoleApp1.dll"] `)); - assertNotFileContains('Dockerfile', 'EXPOSE'); + assertNotFileContains('subfolder/projectFolder/Dockerfile', 'EXPOSE'); }); }); @@ -806,9 +814,9 @@ suite("Configure (Add Docker files to Workspace)", function (this: Suite): void 'ConsoleApp1.csproj', dotNetCoreConsole_11_ProjectFileContents); - assertNotFileContains('Dockerfile', 'EXPOSE'); - assertFileContains('Dockerfile', 'FROM microsoft/dotnet:1.1-runtime AS base'); - assertFileContains('Dockerfile', 'FROM microsoft/dotnet:1.1-sdk AS build'); + assertNotFileContains('subfolder/projectFolder/Dockerfile', 'EXPOSE'); + assertFileContains('subfolder/projectFolder/Dockerfile', 'FROM microsoft/dotnet:1.1-runtime AS base'); + assertFileContains('subfolder/projectFolder/Dockerfile', 'FROM microsoft/dotnet:1.1-sdk AS build'); }); testInEmptyFolder("Linux", async () => { @@ -820,9 +828,9 @@ suite("Configure (Add Docker files to Workspace)", function (this: Suite): void 'ConsoleApp1.csproj', dotNetCoreConsole_11_ProjectFileContents); - assertNotFileContains('Dockerfile', 'EXPOSE'); - assertFileContains('Dockerfile', 'FROM microsoft/dotnet:1.1-runtime AS base'); - assertFileContains('Dockerfile', 'FROM microsoft/dotnet:1.1-sdk AS build'); + assertNotFileContains('subfolder/projectFolder/Dockerfile', 'EXPOSE'); + assertFileContains('subfolder/projectFolder/Dockerfile', 'FROM microsoft/dotnet:1.1-runtime AS base'); + assertFileContains('subfolder/projectFolder/Dockerfile', 'FROM microsoft/dotnet:1.1-sdk AS build'); }); }); @@ -836,9 +844,9 @@ suite("Configure (Add Docker files to Workspace)", function (this: Suite): void 'ConsoleApp1.csproj', dotNetCoreConsole_22_ProjectFileContents); - assertNotFileContains('Dockerfile', 'EXPOSE'); - assertFileContains('Dockerfile', 'FROM mcr.microsoft.com/dotnet/core/runtime:2.2-nanoserver-1809 AS base'); - assertFileContains('Dockerfile', 'FROM mcr.microsoft.com/dotnet/core/sdk:2.2-nanoserver-1809 AS build'); + assertNotFileContains('subfolder/projectFolder/Dockerfile', 'EXPOSE'); + assertFileContains('subfolder/projectFolder/Dockerfile', 'FROM mcr.microsoft.com/dotnet/core/runtime:2.2-nanoserver-1809 AS base'); + assertFileContains('subfolder/projectFolder/Dockerfile', 'FROM mcr.microsoft.com/dotnet/core/sdk:2.2-nanoserver-1809 AS build'); }); testInEmptyFolder("Linux", async () => { @@ -850,9 +858,9 @@ suite("Configure (Add Docker files to Workspace)", function (this: Suite): void 'ConsoleApp1.csproj', dotNetCoreConsole_22_ProjectFileContents); - assertNotFileContains('Dockerfile', 'EXPOSE'); - assertFileContains('Dockerfile', 'FROM mcr.microsoft.com/dotnet/core/runtime:2.2 AS base'); - assertFileContains('Dockerfile', 'FROM mcr.microsoft.com/dotnet/core/sdk:2.2 AS build'); + assertNotFileContains('subfolder/projectFolder/Dockerfile', 'EXPOSE'); + assertFileContains('subfolder/projectFolder/Dockerfile', 'FROM mcr.microsoft.com/dotnet/core/runtime:2.2 AS base'); + assertFileContains('subfolder/projectFolder/Dockerfile', 'FROM mcr.microsoft.com/dotnet/core/sdk:2.2 AS build'); }); }); @@ -867,7 +875,7 @@ suite("Configure (Add Docker files to Workspace)", function (this: Suite): void ['Windows', TestInput.UseDefaultValue] ); - assertFileContains('Dockerfile', 'EXPOSE 80'); + assertFileContains('projectFolder1/Dockerfile', 'EXPOSE 80'); }); testInEmptyFolder("No port", async () => { @@ -878,7 +886,7 @@ suite("Configure (Add Docker files to Workspace)", function (this: Suite): void ['Windows', ''] ); - assertNotFileContains('Dockerfile', 'EXPOSE'); + assertNotFileContains('projectFolder1/Dockerfile', 'EXPOSE'); }); testInEmptyFolder("Windows 10 RS5", async () => { @@ -955,8 +963,8 @@ suite("Configure (Add Docker files to Workspace)", function (this: Suite): void 'project1.csproj', aspNet_22_ProjectFileContents); - assertFileContains('Dockerfile', 'FROM mcr.microsoft.com/dotnet/core/aspnet:2.2-nanoserver-1803 AS base'); - assertFileContains('Dockerfile', 'FROM mcr.microsoft.com/dotnet/core/sdk:2.2-nanoserver-1803 AS build'); + assertFileContains('AspNetApp1/Dockerfile', 'FROM mcr.microsoft.com/dotnet/core/aspnet:2.2-nanoserver-1803 AS base'); + assertFileContains('AspNetApp1/Dockerfile', 'FROM mcr.microsoft.com/dotnet/core/sdk:2.2-nanoserver-1803 AS build'); }); testInEmptyFolder("Windows 10 RS3", async () => { @@ -968,8 +976,8 @@ suite("Configure (Add Docker files to Workspace)", function (this: Suite): void 'project1.csproj', aspNet_22_ProjectFileContents); - assertFileContains('Dockerfile', 'FROM mcr.microsoft.com/dotnet/core/aspnet:2.2-nanoserver-1709 AS base'); - assertFileContains('Dockerfile', 'FROM mcr.microsoft.com/dotnet/core/sdk:2.2-nanoserver-1709 AS build'); + assertFileContains('AspNetApp1/Dockerfile', 'FROM mcr.microsoft.com/dotnet/core/aspnet:2.2-nanoserver-1709 AS base'); + assertFileContains('AspNetApp1/Dockerfile', 'FROM mcr.microsoft.com/dotnet/core/sdk:2.2-nanoserver-1709 AS build'); }); testInEmptyFolder("Windows Server 2016", async () => { @@ -981,8 +989,8 @@ suite("Configure (Add Docker files to Workspace)", function (this: Suite): void 'project1.csproj', aspNet_22_ProjectFileContents); - assertFileContains('Dockerfile', 'FROM mcr.microsoft.com/dotnet/core/aspnet:2.2-nanoserver-sac2016 AS base'); - assertFileContains('Dockerfile', 'FROM mcr.microsoft.com/dotnet/core/sdk:2.2-nanoserver-sac2016 AS build'); + assertFileContains('AspNetApp1/Dockerfile', 'FROM mcr.microsoft.com/dotnet/core/aspnet:2.2-nanoserver-sac2016 AS base'); + assertFileContains('AspNetApp1/Dockerfile', 'FROM mcr.microsoft.com/dotnet/core/sdk:2.2-nanoserver-sac2016 AS build'); }); testInEmptyFolder("Host=Linux", async () => { @@ -994,8 +1002,8 @@ suite("Configure (Add Docker files to Workspace)", function (this: Suite): void 'project1.csproj', aspNet_22_ProjectFileContents); - assertFileContains('Dockerfile', 'FROM mcr.microsoft.com/dotnet/core/aspnet:2.2-nanoserver-1903 AS base'); - assertFileContains('Dockerfile', 'FROM mcr.microsoft.com/dotnet/core/sdk:2.2-nanoserver-1903 AS build'); + assertFileContains('AspNetApp1/Dockerfile', 'FROM mcr.microsoft.com/dotnet/core/aspnet:2.2-nanoserver-1903 AS base'); + assertFileContains('AspNetApp1/Dockerfile', 'FROM mcr.microsoft.com/dotnet/core/sdk:2.2-nanoserver-1903 AS build'); }); }); @@ -1009,8 +1017,8 @@ suite("Configure (Add Docker files to Workspace)", function (this: Suite): void 'project1.csproj', aspNet_10_ProjectFileContents); - assertFileContains('Dockerfile', 'FROM microsoft/aspnetcore:1.1 AS base'); - assertFileContains('Dockerfile', 'FROM microsoft/aspnetcore-build:1.1 AS build'); + assertFileContains('AspNetApp1/Dockerfile', 'FROM microsoft/aspnetcore:1.1 AS base'); + assertFileContains('AspNetApp1/Dockerfile', 'FROM microsoft/aspnetcore-build:1.1 AS build'); }); }); @@ -1024,8 +1032,8 @@ suite("Configure (Add Docker files to Workspace)", function (this: Suite): void 'project2.csproj', aspNet_20_ProjectFileContents); - assertFileContains('Dockerfile', 'FROM microsoft/aspnetcore:2.0 AS base'); - assertFileContains('Dockerfile', 'FROM microsoft/aspnetcore-build:2.0 AS build'); + assertFileContains('project2/Dockerfile', 'FROM microsoft/aspnetcore:2.0 AS base'); + assertFileContains('project2/Dockerfile', 'FROM microsoft/aspnetcore-build:2.0 AS build'); }); });