From 7227ce4c951994f4b5adbb936fcbc8a67e698607 Mon Sep 17 00:00:00 2001 From: "Stephen Weatherford (MSFT)" Date: Fri, 21 Sep 2018 17:48:28 -0700 Subject: [PATCH] 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',