diff --git a/.gitattributes b/.gitattributes index ecf6dba8ef..5956e76d21 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,2 @@ # Stop git from breaking this script by putting CRLF in place of LF -resources/vsdbg text eol=lf +resources/netCore/vsdbg text eol=lf diff --git a/.vscode/settings.json b/.vscode/settings.json index 0826b04e3a..980392bb73 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -20,5 +20,8 @@ "typescript" ], "typescript.tsdk": "node_modules/typescript/lib", - "typescript.preferences.importModuleSpecifier": "relative" + "typescript.preferences.importModuleSpecifier": "relative", + "files.associations": { + "*.template": "plaintext" + } } diff --git a/extension.bundle.ts b/extension.bundle.ts index 9de6466297..9cacd58b3f 100644 --- a/extension.bundle.ts +++ b/extension.bundle.ts @@ -18,8 +18,6 @@ export { deactivateInternal } 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 } from './src/configureWorkspace/configure'; -export { splitPorts } from './src/configureWorkspace/configUtils'; export { configPrefix } from './src/constants'; export { ProcessProvider } from './src/debugging/coreclr/ChildProcessProvider'; export { DockerBuildImageOptions, DockerClient } from './src/debugging/coreclr/CliDockerClient'; @@ -34,12 +32,11 @@ export { OSProvider } from './src/utils/LocalOSProvider'; export { bufferToString } from './src/utils/spawnAsync'; export { DockerDaemonIsLinuxPrerequisite, DockerfileExistsPrerequisite, DotNetSdkInstalledPrerequisite, LinuxUserInDockerGroupPrerequisite, MacNuGetFallbackFolderSharedPrerequisite } from './src/debugging/coreclr/prereqManager'; export { ext } from './src/extensionVariables'; -export { globAsync } from './src/utils/globAsync'; export { httpsRequestBinary } from './src/utils/httpRequest'; export { IKeytar } from './src/utils/keytar'; export { inferCommand, inferPackageName, InspectMode, NodePackage } from './src/utils/nodeUtils'; export { nonNullProp } from './src/utils/nonNull'; -export { getDockerOSType, isWindows10RS3OrNewer, isWindows10RS4OrNewer, isWindows10RS5OrNewer, isWindows1019H1OrNewer } from "./src/utils/osUtils"; +export { getDockerOSType } from "./src/utils/osUtils"; export { Platform, PlatformOS } from './src/utils/platform'; export { trimWithElipsis } from './src/utils/trimWithElipsis'; export { recursiveFindTaskByType } from './src/tasks/TaskHelper'; diff --git a/package-lock.json b/package-lock.json index a55eb3960f..f17d90fcda 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3925,14 +3925,6 @@ } } }, - "dockerfile-ast": { - "version": "0.0.30", - "resolved": "https://registry.npmjs.org/dockerfile-ast/-/dockerfile-ast-0.0.30.tgz", - "integrity": "sha512-QOeP5NjbjoSLtnMz6jzBLsrKtywLEVPoCOAwA54cQpulyKb1gBnZ63tr6Amq8oVDvu5PXa3aifBVw+wcoCGHKg==", - "requires": { - "vscode-languageserver-types": "^3.15.1" - } - }, "dockerfile-language-server-nodejs": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/dockerfile-language-server-nodejs/-/dockerfile-language-server-nodejs-0.1.1.tgz", @@ -7058,6 +7050,18 @@ "glogg": "^1.0.0" } }, + "handlebars": { + "version": "4.7.6", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.6.tgz", + "integrity": "sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA==", + "requires": { + "minimist": "^1.2.5", + "neo-async": "^2.6.0", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4", + "wordwrap": "^1.0.0" + } + }, "har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", @@ -9197,8 +9201,7 @@ "neo-async": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", - "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", - "dev": true + "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==" }, "next-tick": { "version": "1.0.0", @@ -11085,8 +11088,7 @@ "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" }, "source-map-resolve": { "version": "0.5.3", @@ -12184,6 +12186,12 @@ "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", "dev": true }, + "uglify-js": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.10.0.tgz", + "integrity": "sha512-Esj5HG5WAyrLIdYU74Z3JdG2PxdIusvj6IWHMtlyESxc7kcDz7zYlYjpnSokn1UbpV0d/QX9fan7gkCNd/9BQA==", + "optional": true + }, "umd-compat-loader": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/umd-compat-loader/-/umd-compat-loader-2.1.2.tgz", @@ -13873,6 +13881,11 @@ "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", "dev": true }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" + }, "worker-farm": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.7.0.tgz", diff --git a/package.json b/package.json index 0c139f485e..d3ad5cc675 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,6 @@ }, "homepage": "https://github.com/Microsoft/vscode-docker/blob/master/README.md", "activationEvents": [ - "onCommand:vscode-docker.api.configure", "onCommand:vscode-docker.compose.down", "onCommand:vscode-docker.compose.restart", "onCommand:vscode-docker.compose.up", @@ -127,10 +126,6 @@ "contributes": { "menus": { "commandPalette": [ - { - "command": "vscode-docker.api.configure", - "when": "never" - }, { "command": "vscode-docker.containers.select", "when": "never" @@ -2164,6 +2159,10 @@ "type": "boolean", "default": true, "description": "%vscode-docker.config.docker.showRemoteWorkspaceWarning%" + }, + "docker.scaffolding.templatePath": { + "type": "string", + "description": "%vscode-docker.config.docker.scaffolding.templatePath%" } } }, @@ -2177,11 +2176,6 @@ } }, "commands": [ - { - "command": "vscode-docker.api.configure", - "title": "%vscode-docker.commands.api.configure%", - "category": "%vscode-docker.commands.category.docker%" - }, { "command": "vscode-docker.compose.down", "title": "%vscode-docker.commands.compose.down%", @@ -2766,12 +2760,12 @@ "@azure/arm-containerregistry": "^8.0.0", "@azure/storage-blob": "^12.1.2", "@docker/sdk": "^0.1.13", - "dockerfile-ast": "^0.0.30", "dockerfile-language-server-nodejs": "^0.1.1", "dockerode": "^3.2.1", "fs-extra": "^9.0.1", "glob": "^7.1.6", "gradle-to-js": "^2.0.0", + "handlebars": "^4.7.6", "moment": "^2.27.0", "request": "^2.88.2", "request-promise-native": "^1.0.9", diff --git a/package.nls.json b/package.nls.json index a7d341b774..d7ba299c04 100644 --- a/package.nls.json +++ b/package.nls.json @@ -167,8 +167,8 @@ "vscode-docker.config.docker.dockerComposeBuild": "Set to true to include --build option when docker-compose command is invoked", "vscode-docker.config.docker.dockerComposeDetached": "Set to true to include --d (detached) option when docker-compose command is invoked", "vscode-docker.config.docker.showRemoteWorkspaceWarning": "Set to true to prompt to switch from \"UI\" extension mode to \"Workspace\" extension mode if an operation is not supported in UI mode.", + "vscode-docker.config.docker.scaffolding.templatePath": "The path to use for scaffolding templates.", "vscode-docker.config.deprecated": "This setting has been deprecated and will be removed in a future release.", - "vscode-docker.commands.api.configure": "Add Docker Files to Workspace (API)...", "vscode-docker.commands.compose.down": "Compose Down", "vscode-docker.commands.compose.restart": "Compose Restart", "vscode-docker.commands.compose.up": "Compose Up", diff --git a/resources/GetBlazorManifestLocations.targets b/resources/netCore/GetBlazorManifestLocations.targets similarity index 93% rename from resources/GetBlazorManifestLocations.targets rename to resources/netCore/GetBlazorManifestLocations.targets index d700a1d083..325a175621 100644 --- a/resources/GetBlazorManifestLocations.targets +++ b/resources/netCore/GetBlazorManifestLocations.targets @@ -7,7 +7,7 @@ diff --git a/resources/netCore/GetProjectProperties.targets b/resources/netCore/GetProjectProperties.targets new file mode 100644 index 0000000000..9c274f7f7e --- /dev/null +++ b/resources/netCore/GetProjectProperties.targets @@ -0,0 +1,10 @@ + + + + + + diff --git a/resources/GetTargetPath.proj b/resources/netCore/GetTargetPath.proj similarity index 100% rename from resources/GetTargetPath.proj rename to resources/netCore/GetTargetPath.proj diff --git a/resources/vsdbg b/resources/netCore/vsdbg similarity index 100% rename from resources/vsdbg rename to resources/netCore/vsdbg diff --git a/resources/template.docker-compose.yml b/resources/template.docker-compose.yml deleted file mode 100644 index 441c719fc8..0000000000 --- a/resources/template.docker-compose.yml +++ /dev/null @@ -1,19 +0,0 @@ -# Please refer https://docs.docker.com/compose to learn more about Docker Compose. - -# This is a sample docker-compose file with two services -# 1. yourwebapp is an sample web service where the docker container image will be built from the Dockerfile as -# part of starting the compose. -# 2. redis is an existing image hosted in docker hub. -version: '3.4' - -services: - # yourwebapp: - # image: yourwebapp - # build: - # context: . - # dockerfile: Dockerfile - # ports: - # - 80 - - redis: - image: redis diff --git a/resources/templates/.dockerignore.template b/resources/templates/.dockerignore.template new file mode 100644 index 0000000000..d5aa51560f --- /dev/null +++ b/resources/templates/.dockerignore.template @@ -0,0 +1,29 @@ +{{#if pythonProjectType}} +**/__pycache__ +{{/if}} +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +{{#unless (eq platform 'Node.js')}} +**/bin +{{/unless}} +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +README.md diff --git a/resources/templates/cpp/Dockerfile.template b/resources/templates/cpp/Dockerfile.template new file mode 100644 index 0000000000..d19d7ae92d --- /dev/null +++ b/resources/templates/cpp/Dockerfile.template @@ -0,0 +1,19 @@ +# GCC support can be specified at major, minor, or micro version +# (e.g. 8, 8.2 or 8.2.0). +# See https://hub.docker.com/r/library/gcc/ for all supported GCC +# tags from Docker Hub. +# See https://docs.docker.com/samples/library/gcc/ for more on how to use this image +FROM gcc:latest + +# These commands copy your files into the specified directory in the image +# and set that as the working location +COPY . /usr/src/myapp +WORKDIR /usr/src/myapp + +# This command compiles your app using GCC, adjust for your source code +RUN g++ -o myapp main.cpp + +# This command runs your application, comment out this line to compile only +CMD ["./myapp"] + +LABEL Name={{ serviceName }} Version={{ version }} diff --git a/resources/templates/docker-compose.debug.yml.template b/resources/templates/docker-compose.debug.yml.template new file mode 100644 index 0000000000..5205a44091 --- /dev/null +++ b/resources/templates/docker-compose.debug.yml.template @@ -0,0 +1,14 @@ +version: '3.4' + +services: + {{ serviceName }}: + image: {{ serviceName }} + build: + context: . + dockerfile: Dockerfile +{{#if ports}} + ports: +{{#each ports}} + - {{ . }}:{{ . }} +{{/each}} +{{/if}} diff --git a/resources/templates/docker-compose.yml.template b/resources/templates/docker-compose.yml.template new file mode 100644 index 0000000000..a471472f40 --- /dev/null +++ b/resources/templates/docker-compose.yml.template @@ -0,0 +1,12 @@ +version: '3.4' + +services: + {{ serviceName }}: + image: {{ serviceName }} + build: . +{{#if ports}} + ports: +{{#each ports}} + - {{ . }}:{{ . }} +{{/each}} +{{/if}} diff --git a/resources/templates/go/Dockerfile.template b/resources/templates/go/Dockerfile.template new file mode 100644 index 0000000000..b35370305a --- /dev/null +++ b/resources/templates/go/Dockerfile.template @@ -0,0 +1,17 @@ +#build stage +FROM golang:alpine AS builder +WORKDIR /go/src/app +COPY . . +RUN apk add --no-cache git +RUN go get -d -v ./... +RUN go install -v ./... + +#final stage +FROM alpine:latest +RUN apk --no-cache add ca-certificates +COPY --from=builder /go/bin/app /app +ENTRYPOINT ./app +LABEL Name={{ serviceName }} Version={{ version }} +{{#each ports}} +EXPOSE {{ . }} +{{/each}} diff --git a/resources/templates/java/Dockerfile.template b/resources/templates/java/Dockerfile.template new file mode 100644 index 0000000000..dd5bcc082d --- /dev/null +++ b/resources/templates/java/Dockerfile.template @@ -0,0 +1,11 @@ +FROM openjdk:8-jdk-alpine +VOLUME /tmp +ARG JAVA_OPTS +ENV JAVA_OPTS=$JAVA_OPTS +ADD {{ relativeJavaOutputPath }} {{ serviceName }}.jar +{{#each ports}} +EXPOSE {{ . }} +{{/each}} +ENTRYPOINT exec java $JAVA_OPTS -jar {{ serviceName }}.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 {{ serviceName }}.jar diff --git a/resources/templates/java/docker-compose.debug.yml.template b/resources/templates/java/docker-compose.debug.yml.template new file mode 100644 index 0000000000..c37e15deba --- /dev/null +++ b/resources/templates/java/docker-compose.debug.yml.template @@ -0,0 +1,16 @@ +version: '3.4' + +services: + {{ serviceName }}: + image: {{ serviceName }} + build: + context: . + dockerfile: Dockerfile + environment: + JAVA_OPTS: -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005,quiet=y +{{#if (join ports debugPorts)}} + ports: +{{#each (join ports debugPorts)}} + - {{ . }}:{{ . }} +{{/each}} +{{/if}} diff --git a/resources/templates/netCore/Dockerfile.template b/resources/templates/netCore/Dockerfile.template new file mode 100644 index 0000000000..279a162c1a --- /dev/null +++ b/resources/templates/netCore/Dockerfile.template @@ -0,0 +1,21 @@ +FROM {{ netCoreRuntimeBaseImage }} AS base +WORKDIR /app +{{#each ports}} +EXPOSE {{ . }} +{{/each}} + +FROM {{ netCoreSdkBaseImage }} AS build +WORKDIR /src +COPY ["{{ workspaceRelative . artifact }}", "{{ dirname (workspaceRelative . artifact) }}/"] +RUN dotnet restore "{{ workspaceRelative . artifact netCorePlatformOS }}" +COPY . . +WORKDIR "/src/{{ dirname (workspaceRelative . artifact 'Linux') 'Linux' }}" +RUN dotnet build "{{ basename artifact }}" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "{{ basename artifact }}" -c Release -o /app/publish + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "{{ netCoreAssemblyName }}"] diff --git a/resources/templates/netCore/docker-compose.debug.yml.template b/resources/templates/netCore/docker-compose.debug.yml.template new file mode 100644 index 0000000000..26f706c555 --- /dev/null +++ b/resources/templates/netCore/docker-compose.debug.yml.template @@ -0,0 +1,31 @@ +{{#if (eq platform '.NET: ASP.NET Core')}} +# Please refer https://aka.ms/HTTPSinContainer on how to setup an https developer certificate for your ASP .NET Core service. +{{/if}} + +version: '3.4' + +services: + {{ serviceName }}: + image: {{ serviceName }} + build: + context: . + dockerfile: {{ dirname (workspaceRelative . artifact) 'Linux' }}/Dockerfile +{{#if ports}} + ports: +{{#each ports}} +{{#unless (eq . 443)}} + - {{ . }}:{{ . }} +{{/unless}} +{{/each}} +{{/if}} +{{#if ports}} + environment: + - ASPNETCORE_ENVIRONMENT=Development + - ASPNETCORE_URLS=http://+:{{ ports.[0] }} +{{/if}} + volumes: +{{#if (eq netCorePlatformOS 'Windows')}} + - ~/.vsdbg:c:\remote_debugger:rw +{{else}} + - ~/.vsdbg:/remote_debugger:rw +{{/if}} diff --git a/resources/templates/netCore/docker-compose.yml.template b/resources/templates/netCore/docker-compose.yml.template new file mode 100644 index 0000000000..21dad34fff --- /dev/null +++ b/resources/templates/netCore/docker-compose.yml.template @@ -0,0 +1,20 @@ +{{#if (eq platform '.NET: ASP.NET Core')}} +# Please refer https://aka.ms/HTTPSinContainer on how to setup an https developer certificate for your ASP .NET Core service. +{{/if}} + +version: '3.4' + +services: + {{ serviceName }}: + image: {{ serviceName }} + build: + context: . + dockerfile: {{ dirname (workspaceRelative . artifact) 'Linux' }}/Dockerfile +{{#if ports}} + ports: +{{#each ports}} +{{#unless (eq . 443)}} + - {{ . }}:{{ . }} +{{/unless}} +{{/each}} +{{/if}} diff --git a/resources/templates/node/Dockerfile.template b/resources/templates/node/Dockerfile.template new file mode 100644 index 0000000000..e03a7418bc --- /dev/null +++ b/resources/templates/node/Dockerfile.template @@ -0,0 +1,10 @@ +FROM node:12.18-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 . . +{{#each ports}} +EXPOSE {{ . }} +{{/each}} +CMD {{{ toQuotedArray nodeCmdParts }}} diff --git a/resources/templates/node/docker-compose.debug.yml.template b/resources/templates/node/docker-compose.debug.yml.template new file mode 100644 index 0000000000..2052a01759 --- /dev/null +++ b/resources/templates/node/docker-compose.debug.yml.template @@ -0,0 +1,15 @@ +version: '3.4' + +services: + {{ serviceName }}: + image: {{ serviceName }} + build: . + environment: + NODE_ENV: development +{{#if (join ports debugPorts)}} + ports: +{{#each (join ports debugPorts)}} + - {{ . }}:{{ . }} +{{/each}} +{{/if}} + command: {{{ toQuotedArray nodeDebugCmdParts }}} diff --git a/resources/templates/node/docker-compose.yml.template b/resources/templates/node/docker-compose.yml.template new file mode 100644 index 0000000000..b4e52b18c3 --- /dev/null +++ b/resources/templates/node/docker-compose.yml.template @@ -0,0 +1,14 @@ +version: '3.4' + +services: + {{ serviceName }}: + image: {{ serviceName }} + build: . + environment: + NODE_ENV: production +{{#if ports}} + ports: +{{#each ports}} + - {{ . }}:{{ . }} +{{/each}} +{{/if}} diff --git a/resources/templates/other/Dockerfile.template b/resources/templates/other/Dockerfile.template new file mode 100644 index 0000000000..ab69209629 --- /dev/null +++ b/resources/templates/other/Dockerfile.template @@ -0,0 +1,4 @@ +FROM docker/whalesay:latest +LABEL Name={{ serviceName }} Version={{ version }} +RUN apt-get -y update && apt-get install -y fortunes +CMD ["sh", "-c", "/usr/games/fortune -a | cowsay"] diff --git a/resources/templates/python/Dockerfile.template b/resources/templates/python/Dockerfile.template new file mode 100644 index 0000000000..ece195bd18 --- /dev/null +++ b/resources/templates/python/Dockerfile.template @@ -0,0 +1,37 @@ +# For more information, please refer to https://aka.ms/vscode-docker-python +FROM python:3.8-slim-buster + +{{#if (isRootPort ports)}} +# Warning: A port below 1024 has been exposed. This requires the image to run as a root user which is not a best practice. +# For more information, please refer to https://aka.ms/vscode-docker-python-user-rights` +{{/if}} +{{#if ports}} +{{#each ports}} +EXPOSE {{ . }} +{{/each}} + +{{/if}} +# Keeps Python from generating .pyc files in the container +ENV PYTHONDONTWRITEBYTECODE 1 + +# Turns off buffering for easier container logging +ENV PYTHONUNBUFFERED 1 + +# Install pip requirements +ADD requirements.txt . +RUN python -m pip install -r requirements.txt + +WORKDIR /app +ADD . /app + +{{#unless (isRootPort ports)}} +# Switching to a non-root user, please refer to https://aka.ms/vscode-docker-python-user-rights +RUN useradd appuser && chown -R appuser /app +USER appuser + +{{/unless}} +# During debugging, this entry point will be overridden. For more information, please refer to https://aka.ms/vscode-docker-python-debug +{{#if wsgiComment}} +{{{ wsgiComment }}} +{{/if}} +CMD {{{ toQuotedArray pythonCmdParts }}} diff --git a/resources/templates/python/docker-compose.debug.yml.template b/resources/templates/python/docker-compose.debug.yml.template new file mode 100644 index 0000000000..b6b479ce65 --- /dev/null +++ b/resources/templates/python/docker-compose.debug.yml.template @@ -0,0 +1,19 @@ +version: '3.4' + +services: + {{ serviceName }}: + image: {{ serviceName }} + build: + context: . + dockerfile: Dockerfile + command: {{{ toQuotedArray pythonDebugCmdParts }}} +{{#if (join ports debugPorts)}} + ports: +{{#each (join ports debugPorts)}} + - {{ . }}:{{ . }} +{{/each}} +{{/if}} +{{#if (eq platform 'Python: Flask')}} + environment: + - FLASK_APP={{#if pythonArtifact.file}}{{ pythonArtifact.file }}{{else}}-m {{ pythonArtifact.module }}{{/if}} +{{/if}} diff --git a/resources/templates/python/docker-compose.yml.template b/resources/templates/python/docker-compose.yml.template new file mode 100644 index 0000000000..5205a44091 --- /dev/null +++ b/resources/templates/python/docker-compose.yml.template @@ -0,0 +1,14 @@ +version: '3.4' + +services: + {{ serviceName }}: + image: {{ serviceName }} + build: + context: . + dockerfile: Dockerfile +{{#if ports}} + ports: +{{#each ports}} + - {{ . }}:{{ . }} +{{/each}} +{{/if}} diff --git a/resources/templates/python/requirements.txt.template b/resources/templates/python/requirements.txt.template new file mode 100644 index 0000000000..9ae2503d2d --- /dev/null +++ b/resources/templates/python/requirements.txt.template @@ -0,0 +1,10 @@ +# To ensure app dependencies are ported from your virtual environment/host machine into your container, run 'pip freeze > requirements.txt' in the terminal to overwrite this file +{{#if (eq platform 'Python: Django')}} +django==3.1.1 +{{/if}} +{{#if (eq platform 'Python: Flask')}} +flask==1.1.2 +{{/if}} +{{#unless (eq platform 'Python: General')}} +gunicorn==20.0.4 +{{/unless}} diff --git a/resources/templates/ruby/Dockerfile.template b/resources/templates/ruby/Dockerfile.template new file mode 100644 index 0000000000..4c2b282606 --- /dev/null +++ b/resources/templates/ruby/Dockerfile.template @@ -0,0 +1,18 @@ +FROM ruby:2.5-slim + +LABEL Name={{ serviceName }} Version={{ version }} + +{{#each ports}} +EXPOSE {{ . }} +{{/each}} + +# throw errors if Gemfile has been modified since Gemfile.lock +RUN bundle config --global frozen 1 + +WORKDIR /app +COPY . /app + +COPY Gemfile Gemfile.lock ./ +RUN bundle install + +CMD ["ruby", "{{ serviceName }}.rb"] diff --git a/src/commands/debugging/initializeForDebugging.ts b/src/commands/debugging/initializeForDebugging.ts deleted file mode 100644 index 623eb161d9..0000000000 --- a/src/commands/debugging/initializeForDebugging.ts +++ /dev/null @@ -1,76 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as os from 'os'; -import { IActionContext } from 'vscode-azureextensionui'; -import { ensureDotNetCoreDependencies } from '../../configureWorkspace/configureDotNetCore'; -import { promptForLaunchFile } from '../../configureWorkspace/configurePython'; -import { quickPickOS, quickPickPlatform } from '../../configureWorkspace/configUtils'; -import { DockerDebugScaffoldContext } from '../../debugging/DebugHelper'; -import { dockerDebugScaffoldingProvider, NetCoreScaffoldingOptions, PythonScaffoldingOptions } from '../../debugging/DockerDebugScaffoldingProvider'; -import { DockerPlatform } from '../../debugging/DockerPlatformHelper'; -import { localize } from '../../localize'; -import { getPythonProjectType } from '../../utils/pythonUtils'; -import { quickPickDockerFileItem, quickPickProjectFileItem } from '../../utils/quickPickFile'; -import { quickPickWorkspaceFolder } from '../../utils/quickPickWorkspaceFolder'; - -export async function initializeForDebugging(actionContext: IActionContext): Promise { - const folder = await quickPickWorkspaceFolder(localize('vscode-docker.commands.debugging.initialize.workspaceFolder', 'To configure Docker debugging you must first open a folder or workspace in VS Code.')); - const platform = await quickPickPlatform(['Node.js', '.NET: ASP.NET Core', '.NET: Core Console', 'Python: Django', 'Python: Flask', 'Python: General']); - - let debugPlatform: DockerPlatform; - switch (platform) { - case '.NET: Core Console': - case '.NET: ASP.NET Core': - debugPlatform = 'netCore'; - break; - case 'Node.js': - debugPlatform = 'node'; - break; - case 'Python: Django': - case 'Python: Flask': - case 'Python: General': - debugPlatform = 'python'; - break; - default: - throw new Error(localize('vscode-docker.commands.debugging.initialize.platformNotSupported', 'The selected platform is not yet supported for debugging.')); - } - - actionContext.telemetry.properties.dockerPlatform = debugPlatform; - if (debugPlatform === 'netCore') { - ensureDotNetCoreDependencies(folder, actionContext); - } - - const context: DockerDebugScaffoldContext = { - folder: folder, - platform: debugPlatform, - actionContext: actionContext, - dockerfile: (await quickPickDockerFileItem(actionContext, undefined, folder)).absoluteFilePath - } - - switch (context.platform) { - case 'netCore': - const options: NetCoreScaffoldingOptions = { - appProject: (await quickPickProjectFileItem(undefined, context.folder, localize('vscode-docker.commands.debugging.initialize.noCsproj', 'No .NET Core project file (.csproj or .fsproj) could be found.'))).absoluteFilePath, - platformOS: os.platform() === 'win32' ? await quickPickOS() : 'Linux', - } - await dockerDebugScaffoldingProvider.initializeNetCoreForDebugging(context, options); - break; - case 'node': - await dockerDebugScaffoldingProvider.initializeNodeForDebugging(context); - break; - case 'python': - const pythonProjectType = getPythonProjectType(platform); - - const pyOptions: PythonScaffoldingOptions = { - projectType: pythonProjectType, - target: await promptForLaunchFile(pythonProjectType) - } - - await dockerDebugScaffoldingProvider.initializePythonForDebugging(context, pyOptions); - break; - default: - } -} diff --git a/src/commands/images/buildImage.ts b/src/commands/images/buildImage.ts index 112ab62621..fabd68c617 100644 --- a/src/commands/images/buildImage.ts +++ b/src/commands/images/buildImage.ts @@ -10,7 +10,7 @@ import { ext } from "../../extensionVariables"; import { localize } from '../../localize'; import { getOfficialBuildTaskForDockerfile } from "../../tasks/TaskHelper"; import { executeAsTask } from "../../utils/executeAsTask"; -import { getValidImageName } from "../../utils/getValidImageName"; +import { getValidImageNameFromPath } from "../../utils/getValidImageName"; import { delay } from "../../utils/promiseUtils"; import { quickPickDockerFileItem } from "../../utils/quickPickFile"; import { quickPickWorkspaceFolder } from "../../utils/quickPickWorkspaceFolder"; @@ -52,7 +52,7 @@ export async function buildImage(context: IActionContext, dockerFileUri: vscode. const prevImageName: string | undefined = ext.context.globalState.get(dockerFileKey); // Get imageName based previous entries, else on name of subfolder containing the Dockerfile - const suggestedImageName = prevImageName || getValidImageName(dockerFileItem.absoluteFolderPath, 'latest'); + const suggestedImageName = prevImageName || getValidImageNameFromPath(dockerFileItem.absoluteFolderPath, 'latest'); // Temporary work-around for vscode bug where valueSelection can be messed up if a quick pick is followed by a showInputBox await delay(500); diff --git a/src/commands/images/tagImage.ts b/src/commands/images/tagImage.ts index 36993d1318..9b4ce6108f 100644 --- a/src/commands/images/tagImage.ts +++ b/src/commands/images/tagImage.ts @@ -9,7 +9,6 @@ import { ext } from '../../extensionVariables'; import { localize } from '../../localize'; import { ImageTreeItem } from '../../tree/images/ImageTreeItem'; import { RegistryTreeItemBase } from '../../tree/registries/RegistryTreeItemBase'; -import { extractRegExGroups } from '../../utils/extractRegExGroups'; export async function tagImage(context: IActionContext, node?: ImageTreeItem, registry?: RegistryTreeItemBase): Promise { if (!node) { @@ -69,7 +68,7 @@ export function addImageTaggingTelemetry(context: IActionContext, fullImageName: try { let properties: TelemetryProperties = {}; - let [repository, tag] = extractRegExGroups(fullImageName, /^(.*):(.*)$/, [fullImageName, '']); + let [, repository, tag] = /^(.*):(.*)$/.exec(fullImageName) ?? [undefined, fullImageName, '']; if (!!tag.match(/^[0-9.-]*(|alpha|beta|latest|edge|v|version)?[0-9.-]*$/)) { properties.safeTag = tag diff --git a/src/commands/registerCommands.ts b/src/commands/registerCommands.ts index ad9549b275..59b186255e 100644 --- a/src/commands/registerCommands.ts +++ b/src/commands/registerCommands.ts @@ -5,9 +5,10 @@ import { commands } from "vscode"; import { IActionContext, registerCommand as registerCommandAzUI } from "vscode-azureextensionui"; -import { configure, configureApi } from "../configureWorkspace/configure"; -import { configureCompose } from "../configureWorkspace/configureCompose"; import { ext } from "../extensionVariables"; +import { scaffold } from "../scaffolding/scaffold"; +import { scaffoldCompose } from "../scaffolding/scaffoldCompose"; +import { scaffoldDebugConfig } from "../scaffolding/scaffoldDebugConfig"; import { deployImageToAzure } from "../utils/lazyLoad"; import { viewAzureTaskLogs } from "../utils/lazyLoad"; import { composeDown, composeRestart, composeUp } from "./compose"; @@ -90,12 +91,13 @@ export function registerCommand(commandId: string, callback: (context: IActionCo } export function registerCommands(): void { - registerWorkspaceCommand('vscode-docker.api.configure', configureApi); + registerWorkspaceCommand('vscode-docker.configure', scaffold); + registerWorkspaceCommand('vscode-docker.configureCompose', scaffoldCompose); + registerWorkspaceCommand('vscode-docker.debugging.initializeForDebugging', scaffoldDebugConfig); + registerWorkspaceCommand('vscode-docker.compose.down', composeDown); registerWorkspaceCommand('vscode-docker.compose.restart', composeRestart); registerWorkspaceCommand('vscode-docker.compose.up', composeUp); - registerWorkspaceCommand('vscode-docker.configure', configure); - registerWorkspaceCommand('vscode-docker.configureCompose', configureCompose); registerCommand('vscode-docker.pruneSystem', pruneSystem); registerWorkspaceCommand('vscode-docker.containers.attachShell', attachShellContainer); diff --git a/src/configureWorkspace/LegacyDockerDebugConfigProvider.ts b/src/configureWorkspace/LegacyDockerDebugConfigProvider.ts deleted file mode 100644 index d9a16e4bf7..0000000000 --- a/src/configureWorkspace/LegacyDockerDebugConfigProvider.ts +++ /dev/null @@ -1,23 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -import * as vscode from 'vscode'; -import { ext } from '../extensionVariables'; -import { localize } from '../localize'; - -export class LegacyDockerDebugConfigProvider implements vscode.DebugConfigurationProvider { - - public async provideDebugConfigurations(folder: vscode.WorkspaceFolder | undefined, token?: vscode.CancellationToken): Promise { - const remoteRoot = await ext.ui.showInputBox({ value: '/usr/src/app', prompt: localize('vscode-docker.legacyDebug.remoteRoot', 'Please enter your Docker remote root') }); - return [{ - name: 'Docker: Attach to Node', - type: 'node', - request: 'attach', - remoteRoot - }]; - } -} diff --git a/src/configureWorkspace/configUtils.ts b/src/configureWorkspace/configUtils.ts deleted file mode 100644 index 36c7ecbde2..0000000000 --- a/src/configureWorkspace/configUtils.ts +++ /dev/null @@ -1,175 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * 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, TelemetryProperties } from 'vscode-azureextensionui'; -import { DockerOrchestration } from '../constants'; -import { ext } from "../extensionVariables"; -import { localize } from '../localize'; -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' | 'pythonFile'; - -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. - */ -export async function promptForPorts(ports: number[]): Promise { - let opt: vscode.InputBoxOptions = { - placeHolder: ports.join(', '), - prompt: localize('vscode-docker.configUtils.whatPort', 'What port(s) does your app listen on? Enter a comma-separated list, or empty for no exposed port.'), - value: ports.join(', '), - validateInput: (value: string): string | undefined => { - let result = splitPorts(value); - if (!result) { - return localize('vscode-docker.configUtils.portsFormat', 'Ports must be a comma-separated list of positive integers (1 to 65535), or empty for no exposed port.'); - } - - return undefined; - } - } - - return splitPorts(await ext.ui.showInputBox(opt)); -} - -/** - * Splits a comma separated string of port numbers - */ -export function splitPorts(value: string): number[] | undefined { - if (!value || value === '') { - return []; - } - - let elements = value.split(',').map(p => p.trim()); - let matches = elements.filter(p => p.match(/^-*\d+$/)); - - if (matches.length < elements.length) { - return undefined; - } - - let ports = matches.map(Number); - - // If anything is non-integral or less than 1 or greater than 65535, it's not valid - if (ports.some(p => !Number.isInteger(p) || p < 1 || p > 65535)) { - return undefined; - } - - return ports; -} - -/** - * Prompts for a platform - * @throws `UserCancelledError` if the user cancels. - */ -export async function quickPickPlatform(platforms?: Platform[]): Promise { - let opt: vscode.QuickPickOptions = { - matchOnDescription: true, - matchOnDetail: true, - placeHolder: localize('vscode-docker.configUtils.selectPlatform', 'Select Application Platform') - } - - platforms = platforms || [ - 'Node.js', - '.NET: ASP.NET Core', - '.NET: Core Console', - 'Python: Django', - 'Python: Flask', - 'Python: General', - 'Java', - 'C++', - 'Go', - 'Ruby', - 'Other' - ]; - - const items = platforms.map(p => >{ label: p, data: p }); - let response = await ext.ui.showQuickPick(items, opt); - return response.data; -} - -/** - * Prompts for an OS - * @throws `UserCancelledError` if the user cancels. - */ -export async function quickPickOS(): Promise { - let opt: vscode.QuickPickOptions = { - matchOnDescription: true, - matchOnDetail: true, - placeHolder: localize('vscode-docker.configUtils.selectOS', 'Select Operating System') - } - - 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; -} - -export async function quickPickGenerateComposeFiles(): Promise { - let opt: vscode.QuickPickOptions = { - placeHolder: localize('vscode-docker.configUtils.includeCompose', 'Include optional Docker Compose files?') - } - - let response = await ext.ui.showQuickPick( - [ - { label: 'No', data: false }, - { label: 'Yes', data: true } - ], - opt); - - 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 deleted file mode 100644 index d52d486fa1..0000000000 --- a/src/configureWorkspace/configure.ts +++ /dev/null @@ -1,426 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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 * as fse from 'fs-extra'; -import * as gradleParser from "gradle-to-js/lib/parser"; -import * as path from "path"; -import * as vscode from "vscode"; -import { IActionContext, parseError, TelemetryProperties } from 'vscode-azureextensionui'; -import * as xml2js from 'xml2js'; -import { localize } from '../localize'; -import { Platform, PlatformOS } from '../utils/platform'; -import { configureCpp } from './configureCpp'; -import { scaffoldNetCore } from './configureDotNetCore'; -import { configureGo } from './configureGo'; -import { configureJava } from './configureJava'; -import { configureNode } from './configureNode'; -import { configureOther } from './configureOther'; -import { scaffoldPython } from './configurePython'; -import { configureRuby } from './configureRuby'; -import { ConfigureTelemetryProperties, genCommonDockerIgnoreFile, getSubfolderDepth } from './configUtils'; -import { openFilesIfRequired, registerScaffolder, scaffold, Scaffolder, ScaffolderContext, ScaffoldFile } from './scaffolding'; - -export interface PackageInfo { - cmd: string | string[]; - author: string; - version: string; - artifactName: string; - main?: string; -} - -interface JsonPackageContents { - main?: string; - scripts?: { [key: string]: string }; - author?: string; - version?: string; -} - -interface PomXmlContents { - project?: { - version?: string; - artifactid?: string; - }; -} - -export interface IPlatformGeneratorInfo { - genDockerFile: GeneratorFunction, - genDockerCompose: GeneratorFunction, - genDockerComposeDebug: GeneratorFunction, - defaultPorts: number[] | undefined, // [] = defaults to empty but still asks user if they want a port, undefined = don't ask at all - initializeForDebugging: DebugScaffoldFunction | undefined, -} - -export function getExposeStatements(ports: number[]): string { - return ports ? ports.map(port => `EXPOSE ${port}`).join('\n') : ''; -} - -export function getComposePorts(ports: number[], debugPort?: number): string { - let portMappings: string[] = ports?.map(port => ` - ${port}`) ?? []; - - if (debugPort) { - portMappings.push(` - ${debugPort}:${debugPort}`); - } - - return portMappings && portMappings.length > 0 ? ' ports:\n' + portMappings.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('Node.js', configureScaffolder(configureNode)); -registerScaffolder('.NET: ASP.NET Core', scaffoldNetCore); -registerScaffolder('.NET: Core Console', scaffoldNetCore); -registerScaffolder('Python: Django', scaffoldPython); -registerScaffolder('Python: Flask', scaffoldPython); -registerScaffolder('Python: General', scaffoldPython); -registerScaffolder('Java', configureScaffolder(configureJava)); -registerScaffolder('C++', configureScaffolder(configureCpp)); -registerScaffolder('Go', configureScaffolder(configureGo)); -registerScaffolder('Ruby', configureScaffolder(configureRuby)); -registerScaffolder('Other', configureScaffolder(configureOther)); - -const generatorsByPlatform = new Map(); -generatorsByPlatform.set('C++', configureCpp); -generatorsByPlatform.set('Go', configureGo); -generatorsByPlatform.set('Java', configureJava); -generatorsByPlatform.set('Node.js', configureNode); -generatorsByPlatform.set('Ruby', configureRuby); -generatorsByPlatform.set('Other', configureOther); - -function genDockerFile(serviceNameAndRelativePath: string, platform: Platform, os: PlatformOS | undefined, ports: number[] | 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) { - let contents = generators.genDockerFile(serviceNameAndRelativePath, platform, os, ports, { 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,4}/g, "\r\n\r\n") - .replace(/(\n){3,4}/g, "\n\n"); - - return contents; - } -} - -function genDockerCompose(serviceNameAndRelativePath: string, platform: Platform, os: PlatformOS | undefined, ports: number[]): string { - let generators = generatorsByPlatform.get(platform); - assert(generators, `Could not find docker compose file generator function for "${platform}"`); - if (generators.genDockerCompose) { - return generators.genDockerCompose(serviceNameAndRelativePath, platform, os, ports); - } -} - -function genDockerComposeDebug(serviceNameAndRelativePath: string, platform: Platform, os: PlatformOS | undefined, ports: number[], packageInfo: Partial): string { - let generators = generatorsByPlatform.get(platform); - assert(generators, `Could not find docker debug compose file generator function for "${platform}"`); - if (generators.genDockerComposeDebug) { - return generators.genDockerComposeDebug(serviceNameAndRelativePath, platform, os, ports, packageInfo); - } -} - -function genDockerIgnoreFile(service: string, platformType: Platform, os: string, ports: number[]): string { - return genCommonDockerIgnoreFile(platformType); -} - -async function getPackageJson(folderPath: string): Promise { - return vscode.workspace.findFiles(new vscode.RelativePattern(folderPath, 'package.json'), null, 1, undefined); -} - -function getDefaultPackageInfo(): PackageInfo { - return { - cmd: ['npm', 'start'], - author: 'author', - version: '0.0.1', - artifactName: '', - main: 'index.js', - }; -} - -async function readPackageJson(folderPath: string): Promise<{ packagePath?: string, packageInfo: PackageInfo }> { - // open package.json and look for main, scripts start - const uris: vscode.Uri[] = await getPackageJson(folderPath); - let packageInfo: PackageInfo = getDefaultPackageInfo(); // default - let packagePath: string | undefined; - - if (uris && uris.length > 0) { - packagePath = uris[0].fsPath; - const json = JSON.parse(fse.readFileSync(packagePath, 'utf8')); - - if (json.scripts && typeof json.scripts.start === "string") { - packageInfo.cmd = ['npm', 'start']; - - const matches = /node (.+)/i.exec(json.scripts.start); - if (matches?.[1]) { - packageInfo.main = matches[1]; - } - } else if (typeof json.main === "string") { - packageInfo.cmd = ['node', json.main]; - packageInfo.main = json.main; - } - - if (typeof json.author === "string") { - packageInfo.author = json.author; - } - - if (typeof json.version === "string") { - packageInfo.version = json.version; - } - } - - return { packagePath, packageInfo }; -} - -/** - * Looks for a pom.xml or build.gradle file, and returns its parsed contents, or else a default package contents if none path - */ -async function readPomOrGradle(folderPath: string): Promise<{ foundPath?: string, packageInfo: PackageInfo }> { - let pkg: PackageInfo = getDefaultPackageInfo(); // default - let foundPath: string | undefined; - - let pomPath = path.join(folderPath, 'pom.xml'); - let gradlePath = path.join(folderPath, 'build.gradle'); - - if (await fse.pathExists(pomPath)) { - foundPath = pomPath; - const pomString = await fse.readFile(pomPath); - let json = await new Promise((resolve, reject) => { - const options = { - trim: true, - normalizeTags: true, - normalize: true, - mergeAttrs: true - }; - // tslint:disable-next-line:no-unsafe-any - xml2js.parseString(pomString, options, (error, result: PomXmlContents): void => { - if (error) { - reject(localize('vscode-docker.configure.pomError', 'Failed to parse pom.xml: {0}', parseError(error).message)); - return; - } - resolve(result); - }); - }); - json = json || {}; - - if (json.project && json.project.version) { - pkg.version = json.project.version; - } - - if (json.project && json.project.artifactid) { - pkg.artifactName = `target/${json.project.artifactid}-${pkg.version}.jar`; - } - } else if (await fse.pathExists(gradlePath)) { - foundPath = gradlePath; - const json: { - archivesBaseName?: string; - jar?: { version?: string; archiveName?: string; baseName?: string; }; - version?: string; - // tslint:disable-next-line:no-unsafe-any - } = await gradleParser.parseFile(gradlePath); - - if (json.jar && json.jar.version) { - pkg.version = json.jar.version; - } else if (json.version) { - pkg.version = json.version; - } - - if (json.jar && json.jar.archiveName) { - pkg.artifactName = `build/libs/${json.jar.archiveName}`; - } else { - const baseName = json.jar && json.jar.baseName ? json.jar.baseName : json.archivesBaseName || path.basename(folderPath); - pkg.artifactName = `build/libs/${baseName}-${pkg.version}.jar`; - } - } - - return { foundPath, packageInfo: pkg }; -} - -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; - -const DOCKER_FILE_TYPES: { [key: string]: { generator: GeneratorFunction, isComposeGenerator?: boolean } } = { - 'docker-compose.yml': { generator: genDockerCompose, isComposeGenerator: true }, - 'docker-compose.debug.yml': { generator: genDockerComposeDebug, isComposeGenerator: true }, - 'Dockerfile': { generator: genDockerFile }, - '.dockerignore': { generator: genDockerIgnoreFile } -}; - -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 - */ - rootPath: string; - - /** - * Output folder for the docker files. Relative paths in the Dockerfile we will calculated based on this folder - */ - outputFolder?: string; - - /** - * Platform - */ - platform?: Platform; - - /** - * Ports to expose - */ - ports?: number[]; - - /** - * The OS for the images. Currently only needed for .NET platforms. - */ - os?: PlatformOS; - - /** - * The workspace folder for configuring - */ - folder?: vscode.WorkspaceFolder; -} - -export async function configure(context: IActionContext, rootFolderPath: string | undefined): Promise { - 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 - }; - - const files = await scaffold(scaffoldContext); - openFilesIfRequired(files); -} - -export async function configureApi(context: IActionContext, options: ConfigureApiOptions): Promise { - 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: ScaffolderContext, options: ConfigureApiOptions): Promise { - const properties: TelemetryProperties & ConfigureTelemetryProperties = context.telemetry.properties; - const rootFolderPath: string = options.rootPath; - const outputFolder = options.outputFolder ?? rootFolderPath; - - const platformType: Platform = options.platform; - let generatorInfo = generatorsByPlatform.get(platformType); - - let os: PlatformOS | undefined = options.os; - properties.configureOs = os; - - let generateComposeFiles = true; - - if (platformType === 'Node.js') { - generateComposeFiles = await context.promptForCompose(); - if (generateComposeFiles) { - properties.orchestration = 'docker-compose'; - } - } - - let ports: number[] | undefined = options.ports; - if (!ports && generatorInfo.defaultPorts !== undefined) { - ports = await context.promptForPorts(generatorInfo.defaultPorts); - } - - let targetFramework: string; - let projFile: string; - let serviceNameAndPathRelativeToOutput: string; - { - // Scope serviceNameAndPathRelativeToRoot only to this block of code - let serviceNameAndPathRelativeToRoot: string; - 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)); - serviceNameAndPathRelativeToOutput = serviceNameAndPathRelativeToOutput.replace(/\\/g, '/'); - } - - let packageInfo: PackageInfo = getDefaultPackageInfo(); - if (platformType === 'Java') { - let foundPomOrGradlePath: string | undefined; - ({ packageInfo, foundPath: foundPomOrGradlePath } = await readPomOrGradle(rootFolderPath)); - if (foundPomOrGradlePath) { - properties.packageFileType = path.basename(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(outputFolder, packagePath); - } - } - - if (targetFramework) { - packageInfo.version = targetFramework; - packageInfo.artifactName = projFile; - } - - 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'; - } - - return dockerFileType.isComposeGenerator !== true || generateComposeFiles - ? createWorkspaceFileIfNotExists(fileName, dockerFileType.generator) - : Promise.resolve(); - })); - - // Can only configure for debugging if there's a workspace folder, and there's a scaffold function - 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 { - // 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 }); - } - } -} diff --git a/src/configureWorkspace/configureCompose.ts b/src/configureWorkspace/configureCompose.ts deleted file mode 100644 index 257bc9ddaa..0000000000 --- a/src/configureWorkspace/configureCompose.ts +++ /dev/null @@ -1,120 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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 } from 'vscode-azureextensionui'; -import { dockerComposeHeader } from '../constants'; -import { ext } from '../extensionVariables'; -import { localize } from '../localize'; -import { captureCancelStep } from '../utils/captureCancelStep'; -import { DockerfileInfo, parseDockerfile } from '../utils/dockerfileUtils'; -import { getValidImageName } from '../utils/getValidImageName'; -import { Item, quickPickDockerFileItems } from '../utils/quickPickFile'; -import { quickPickWorkspaceFolder } from '../utils/quickPickWorkspaceFolder'; -import { generateUniqueName } from '../utils/uniqueNameUtils'; -import { getComposePorts } from './configure'; -import { ConfigureTelemetryCancelStep } from './configUtils'; -import { generateNonConflictFileNameWithPrompt, openFilesIfRequired, ScaffoldFile, writeFiles } from './scaffolding'; - -export interface ComposeScaffoldContext extends DockerfileInfo { - serviceName: string -} - -export async function configureCompose(context: IActionContext): Promise { - - function captureStep Promise>(step: ConfigureTelemetryCancelStep, prompt: TPrompt): TPrompt { - return captureCancelStep(step, context.telemetry.properties, prompt); - } - - const telemetry = context.telemetry.properties; - - const uniqueServiceNames: string[] = []; - let hasDuplicateService: boolean = false; - function addToServiceNames(serviceName: string): void { - if (uniqueServiceNames.includes(serviceName)) { - hasDuplicateService = true; - } else { - uniqueServiceNames.push(serviceName); - } - } - - const rootFolder: vscode.WorkspaceFolder = await captureStep('folder', promptForFolder)(); - let composeFile: ScaffoldFile; - - // Get list of dockerfiles (services) to add to compose - const dockerFiles: Item[] = await getDockerFilesInWorkspace(context, rootFolder); - telemetry.serviceCount = dockerFiles ? dockerFiles.length.toString() : '0'; - telemetry.uniqueServiceCount = '0'; - - // Add the services to docker-compose.yaml - if (dockerFiles) { - const composeScaffoldContexts: ComposeScaffoldContext[] = []; - await Promise.all( - dockerFiles.map(async dockerfile => { - let serviceName = path.basename(path.dirname(dockerfile.absoluteFilePath)); - serviceName = getValidImageName(serviceName); - const dockerfileInfo: DockerfileInfo = await parseDockerfile(rootFolder.uri.fsPath, dockerfile.absoluteFilePath); - addToServiceNames(serviceName); - composeScaffoldContexts.push({ - ...dockerfileInfo, - serviceName: serviceName, - }); - }) - ); - telemetry.uniqueServiceCount = uniqueServiceNames.length.toString(); - if (hasDuplicateService) { - updateWithUniqueServiceName(composeScaffoldContexts, uniqueServiceNames); - } - composeFile = generateComposeFile(composeScaffoldContexts); - } else { - // No dockerfile is present in the workspace. Create a template docker-compose file. - const templateComposeFile = path.join(ext.context.asAbsolutePath('resources'), 'template.docker-compose.yml'); - const composeContent = (await fse.readFile(templateComposeFile)).toString(); - composeFile = { fileName: 'docker-compose.yml', contents: composeContent } - } - - composeFile.onConflict = async (filePath) => { return await generateNonConflictFileNameWithPrompt(filePath) }; - composeFile.open = true; - let files = await writeFiles([composeFile], rootFolder.uri.fsPath); - openFilesIfRequired(files); -} - -function updateWithUniqueServiceName(contexts: ComposeScaffoldContext[], existingNames: string[]): void { - const processedServices: string[] = []; - contexts.map(context => { - if (processedServices.includes(context.serviceName)) { - context.serviceName = generateUniqueName(context.serviceName, existingNames, i => `${i}`); - existingNames.push(context.serviceName); - } - processedServices.push(context.serviceName); - }) -} - -async function promptForFolder(): Promise { - return await quickPickWorkspaceFolder(localize('vscode-docker.scaffolding.dockerCompose.noWorkspaceFolder', 'To generate docker-compose files you must first open a folder or workspace in VS Code.')); -} - -async function getDockerFilesInWorkspace(context: IActionContext, rootFolder: vscode.WorkspaceFolder): Promise { - const message = localize('vscode-docker.scaffolding.dockerCompose.selectDockerFiles', 'Choose Dockerfiles to include in docker-compose.') - const items: Item[] = await quickPickDockerFileItems(context, undefined, rootFolder, message); - return items; -} - -function generateComposeFile(contexts: ComposeScaffoldContext[]): ScaffoldFile { - let services: string = ''; - contexts.forEach(context => { - let ports: string = context.ports?.length > 0 ? `\n${getComposePorts(context.ports)}` : ''; - services += '\n' + ` ${context.serviceName}: - image: ${context.serviceName} - build: - context: . - dockerfile: ${context.dockerfileNameRelativeToRoot}${ports}`; - }); - - const composeContent = dockerComposeHeader + services; - return { fileName: 'docker-compose.yml', contents: composeContent }; -} diff --git a/src/configureWorkspace/configureCpp.ts b/src/configureWorkspace/configureCpp.ts deleted file mode 100644 index 5a6d0f98b9..0000000000 --- a/src/configureWorkspace/configureCpp.ts +++ /dev/null @@ -1,37 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { IPlatformGeneratorInfo, PackageInfo } from './configure'; - -export let configureCpp: IPlatformGeneratorInfo = { - genDockerFile, - genDockerCompose: undefined, // We don't generate compose files for Cpp - genDockerComposeDebug: undefined, // We don't generate compose files for Cpp - defaultPorts: undefined, // We don't open a port for Cpp - initializeForDebugging: undefined, -}; - -function genDockerFile(serviceNameAndRelativePath: string, platform: string, os: string | undefined, ports: number[], { cmd, author, version, artifactName }: Partial): string { - return `# GCC support can be specified at major, minor, or micro version -# (e.g. 8, 8.2 or 8.2.0). -# See https://hub.docker.com/r/library/gcc/ for all supported GCC -# tags from Docker Hub. -# See https://docs.docker.com/samples/library/gcc/ for more on how to use this image -FROM gcc:latest - -# These commands copy your files into the specified directory in the image -# and set that as the working location -COPY . /usr/src/myapp -WORKDIR /usr/src/myapp - -# This command compiles your app using GCC, adjust for your source code -RUN g++ -o myapp main.cpp - -# This command runs your application, comment out this line to compile only -CMD ["./myapp"] - -LABEL Name=${serviceNameAndRelativePath} Version=${version} -`; -} diff --git a/src/configureWorkspace/configureDotNetCore.ts b/src/configureWorkspace/configureDotNetCore.ts deleted file mode 100644 index 0aae594a59..0000000000 --- a/src/configureWorkspace/configureDotNetCore.ts +++ /dev/null @@ -1,491 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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 * 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, TelemetryProperties } from 'vscode-azureextensionui'; -import ChildProcessProvider from '../debugging/coreclr/ChildProcessProvider'; -import CommandLineDotNetClient from '../debugging/coreclr/CommandLineDotNetClient'; -import { LocalFileSystemProvider } from '../debugging/coreclr/fsProvider'; -import { MsBuildNetCoreProjectProvider, NetCoreProjectProvider } from '../debugging/coreclr/netCoreProjectProvider'; -import { OSTempFileProvider } from '../debugging/coreclr/tempFileProvider'; -import { DockerDebugScaffoldContext } from '../debugging/DebugHelper'; -import { dockerDebugScaffoldingProvider, NetCoreScaffoldingOptions } from '../debugging/DockerDebugScaffoldingProvider'; -import { ext } from '../extensionVariables'; -import { localize } from '../localize'; -import { hasTask } from '../tasks/TaskHelper'; -import { extractRegExGroups } from '../utils/extractRegExGroups'; -import { getValidImageName } from '../utils/getValidImageName'; -import { globAsync } from '../utils/globAsync'; -import LocalOSProvider from '../utils/LocalOSProvider'; -import { isWindows, isWindows1019H1OrNewer, isWindows1019H2OrNewer, isWindows10RS3OrNewer, isWindows10RS4OrNewer, isWindows10RS5OrNewer } from '../utils/osUtils'; -import { Platform, PlatformOS } from '../utils/platform'; -import { generateNonConflictFileName } from '../utils/uniqueNameUtils'; -import { getComposePorts, 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 - -// .NET Core 1.0 - 2.0 images are published to Docker Hub Registry. -const LegacyAspNetCoreRuntimeImageFormat = "microsoft/aspnetcore:{1}.{2}{3}"; -const LegacyAspNetCoreSdkImageFormat = "microsoft/aspnetcore-build:{1}.{2}{3}"; -const LegacyDotNetCoreRuntimeImageFormat = "microsoft/dotnet:{1}.{2}-runtime{3}"; -const LegacyDotNetCoreSdkImageFormat = "microsoft/dotnet:{1}.{2}-sdk{3}"; - -// .NET Core 2.1+ images are now published to Microsoft Container Registry (MCR). -// .NET Core 5.0+ images do not have "core/" in the name. -// https://hub.docker.com/_/microsoft-dotnet-core-runtime/ -const DotNetCoreRuntimeImageFormat = "mcr.microsoft.com/dotnet/{0}runtime:{1}.{2}{3}"; -// https://hub.docker.com/_/microsoft-dotnet-core-aspnet/ -const AspNetCoreRuntimeImageFormat = "mcr.microsoft.com/dotnet/{0}aspnet:{1}.{2}{3}"; -// https://hub.docker.com/_/microsoft-dotnet-core-sdk/ -const DotNetCoreSdkImageFormat = "mcr.microsoft.com/dotnet/{0}sdk:{1}.{2}{3}"; - -function GetWindowsImageTag(): string { - // The host OS version needs to match the version of .NET core images being created - if (!isWindows()) { - // If we're not on Windows (and therefore can't detect the version), assume a Windows 19H2 host - return "-nanoserver-1909"; - } else if (isWindows1019H2OrNewer()) { - return "-nanoserver-1909"; - } else if (isWindows1019H1OrNewer()) { - return "-nanoserver-1903"; - } else if (isWindows10RS5OrNewer()) { - return "-nanoserver-1809"; - } else if (isWindows10RS4OrNewer()) { - return "-nanoserver-1803"; - } else if (isWindows10RS3OrNewer()) { - return "-nanoserver-1709"; - } else { - return "-nanoserver-sac2016"; - } -} - -function formatVersion(format: string, version: string, tagForWindowsVersion: string): string { - let asSemVer = new semver.SemVer(version); - return format.replace('{0}', asSemVer.major >= 5 ? '' : 'core/') - .replace('{1}', asSemVer.major.toString()) - .replace('{2}', asSemVer.minor.toString()) - .replace('{3}', tagForWindowsVersion); -} - -// #region ASP.NET Core templates - -// AT-Kube: /src/Containers.Tools/Containers.Tools.Package/Templates/windows/dotnetcore/aspnetcore/Dockerfile -const aspNetCoreWindowsTemplate = `#Depending on the operating system of the host machines(s) that will build or run the containers, the image specified in the FROM statement may need to be changed. -#For more information, please see https://aka.ms/containercompat - -FROM $base_image_name$ AS base -WORKDIR /app -$expose_statements$ - -FROM $sdk_image_name$ AS build -WORKDIR /src -$copy_project_commands$ -RUN dotnet restore "$container_project_directory$/$project_file_name$" -COPY . . -WORKDIR "/src/$container_project_directory$" -RUN dotnet build "$project_file_name$" -c Release -o /app/build - -FROM build AS publish -RUN dotnet publish "$project_file_name$" -c Release -o /app/publish - -FROM base AS final -WORKDIR /app -COPY --from=publish /app/publish . -ENTRYPOINT ["dotnet", "$assembly_name$"] -`; - -// AT-Kube: /src/Containers.Tools/Containers.Tools.Package/Templates/linux/dotnetcore/aspnetcore/Dockerfile -const aspNetCoreLinuxTemplate = `FROM $base_image_name$ AS base -WORKDIR /app -$expose_statements$ - -FROM $sdk_image_name$ AS build -WORKDIR /src -$copy_project_commands$ -RUN dotnet restore "$container_project_directory$/$project_file_name$" -COPY . . -WORKDIR "/src/$container_project_directory$" -RUN dotnet build "$project_file_name$" -c Release -o /app/build - -FROM build AS publish -RUN dotnet publish "$project_file_name$" -c Release -o /app/publish - -FROM base AS final -WORKDIR /app -COPY --from=publish /app/publish . -ENTRYPOINT ["dotnet", "$assembly_name$"] -`; - -// #endregion - -// #region .NET Core Console templates - -// AT-Kube: /src/Containers.Tools/Containers.Tools.Package/Templates/windows/dotnetcore/console/Dockerfile -const dotNetCoreConsoleWindowsTemplate = `#Depending on the operating system of the host machines(s) that will build or run the containers, the image specified in the FROM statement may need to be changed. -#For more information, please see https://aka.ms/containercompat - -FROM $base_image_name$ AS base -WORKDIR /app -$expose_statements$ - -FROM $sdk_image_name$ AS build -WORKDIR /src -$copy_project_commands$ -RUN dotnet restore "$container_project_directory$/$project_file_name$" -COPY . . -WORKDIR "/src/$container_project_directory$" -RUN dotnet build "$project_file_name$" -c Release -o /app/build - -FROM build AS publish -RUN dotnet publish "$project_file_name$" -c Release -o /app/publish - -FROM base AS final -WORKDIR /app -COPY --from=publish /app/publish . -ENTRYPOINT ["dotnet", "$assembly_name$"] -`; - -// AT-Kube: /src/Containers.Tools/Containers.Tools.Package/Templates/linux/dotnetcore/console/Dockerfile -const dotNetCoreConsoleLinuxTemplate = `FROM $base_image_name$ AS base -WORKDIR /app -$expose_statements$ - -FROM $sdk_image_name$ AS build -WORKDIR /src -$copy_project_commands$ -RUN dotnet restore "$container_project_directory$/$project_file_name$" -COPY . . -WORKDIR "/src/$container_project_directory$" -RUN dotnet build "$project_file_name$" -c Release -o /app/build - -FROM build AS publish -RUN dotnet publish "$project_file_name$" -c Release -o /app/publish - -FROM base AS final -WORKDIR /app -COPY --from=publish /app/publish . -ENTRYPOINT ["dotnet", "$assembly_name$"] -`; - -const dotNetComposeTemplate = `$comment$version: '3.4' - -services: - $service_name$: - image: $image_name$ - build: - context: . - dockerfile: $dockerfile$$ports$`; - -const dotNetComposeDebugTemplate = `${dotNetComposeTemplate}$environment$ - volumes: -$volumes_list$ -`; -// #endregion - -function extractNetCoreVersion(projFileContent: string): string { - // Parse version from TargetFramework or TargetFrameworks - // Example: netcoreapp1.0 or net5.0 - let [tfm] = extractRegExGroups(projFileContent, /(.+)<\/TargetFramework>/, [undefined]); - if (!tfm) { - [tfm] = extractRegExGroups(projFileContent, /(.+)<\/TargetFrameworks>/, ['']); - } - - const defaultNetCoreVersion = '2.1'; - let [netCoreVersion] = extractRegExGroups(tfm, /^netcoreapp([0-9.]+)|net([0-9.]+)$/, [defaultNetCoreVersion]); - - // semver requires a patch in the version, so add it if only major.minor - if (netCoreVersion.match(/^[^.]+\.[^.]+$/)) { - netCoreVersion += '.0'; - } - - return netCoreVersion; -} - -function genDockerFile(serviceNameAndRelativePath: string, platform: Platform, os: PlatformOS | undefined, ports: number[], netCoreAppVersion: string, artifactName: string, assemblyName: string): string { - // VS version of this function is in ResolveImageNames (src/Docker/Microsoft.VisualStudio.Docker.DotNetCore/DockerDotNetCoreScaffoldingProvider.cs) - - if (os !== 'Windows' && os !== 'Linux') { - throw new Error(localize('vscode-docker.configureDotNetCore.unexpectedOs', 'Unexpected OS "{0}"', os)); - } - - let projectDirectory = path.dirname(serviceNameAndRelativePath); - let projectFileName = path.basename(artifactName); - - // example: COPY Core2.0ConsoleAppWindows/Core2.0ConsoleAppWindows.csproj Core2.0ConsoleAppWindows/ - let copyProjectCommands = `COPY ["${artifactName}", "${projectDirectory}/"]` - let exposeStatements = getExposeStatements(ports); - let baseImageFormat: string; - let sdkImageNameFormat: string; - - // For .NET Core 2.1+ use mcr.microsoft.com/dotnet/core/[sdk|aspnet|runtime|runtime-deps] repository. - // See details here: https://devblogs.microsoft.com/dotnet/net-core-container-images-now-published-to-microsoft-container-registry/ - if (semver.gte(netCoreAppVersion, '2.1.0')) { - if (platform === '.NET: ASP.NET Core') { - baseImageFormat = AspNetCoreRuntimeImageFormat; - } else if (platform === '.NET: Core Console') { - baseImageFormat = DotNetCoreRuntimeImageFormat; - } else { - assert.fail(`Unknown platform`); - } - - sdkImageNameFormat = DotNetCoreSdkImageFormat; - } else { - if (platform === '.NET: ASP.NET Core') { - baseImageFormat = LegacyAspNetCoreRuntimeImageFormat; - sdkImageNameFormat = LegacyAspNetCoreSdkImageFormat; - } else if (platform === '.NET: Core Console') { - - baseImageFormat = LegacyDotNetCoreRuntimeImageFormat; - sdkImageNameFormat = LegacyDotNetCoreSdkImageFormat; - } else { - assert.fail(`Unknown platform`); - } - } - - // When targeting Linux container or the dotnet core version is less than 2.0, use MA tag. - // Otherwise, use specific nanoserver tags depending on Windows build. - let tagForWindowsVersion: string; - if (os === 'Linux' || semver.lt(netCoreAppVersion, '2.0.0')) { - tagForWindowsVersion = ''; - } else { - tagForWindowsVersion = GetWindowsImageTag(); - } - - let baseImageName = formatVersion(baseImageFormat, netCoreAppVersion, tagForWindowsVersion); - let sdkImageName = formatVersion(sdkImageNameFormat, netCoreAppVersion, tagForWindowsVersion); - - let template: string; - switch (platform) { - case ".NET: Core Console": - template = os === "Linux" ? dotNetCoreConsoleLinuxTemplate : dotNetCoreConsoleWindowsTemplate; - break; - case ".NET: ASP.NET Core": - template = os === "Linux" ? aspNetCoreLinuxTemplate : aspNetCoreWindowsTemplate; - break; - default: - throw new Error(localize('vscode-docker.configureDotNetCore.unexpectedPlatform', 'Unexpected platform "{0}"', platform)); - } - - let contents = template.replace('$base_image_name$', baseImageName) - .replace(/\$expose_statements\$/g, exposeStatements) - .replace(/\$sdk_image_name\$/g, sdkImageName) - .replace(/\$container_project_directory\$/g, projectDirectory) - .replace(/\$project_file_name\$/g, projectFileName) - .replace(/\$assembly_name\$/g, assemblyName) - .replace(/\$copy_project_commands\$/g, copyProjectCommands); - - validateForUnresolvedToken(contents); - - return contents; -} - -function validateForUnresolvedToken(contents: string): void { - let unreplacedToken = extractRegExGroups(contents, /(\$[a-z_]+\$)/, ['']); - if (unreplacedToken[0]) { - assert.fail(`Unreplaced template token "${unreplacedToken}"`); - } -} - -function generateComposeFiles(dockerfileName: string, platform: Platform, os: PlatformOS | undefined, ports: number[], artifactName: string): ScaffoldFile[] { - const serviceName = path.basename(artifactName, path.extname(artifactName)); - let comment = ''; - // Compose doesn't configure the https, so expose only the http port. - // Otherwise the 'Open in Browser' command will try to open https endpoint and will not work. - let jsonPorts: string = ports?.length > 0 ? `\n${getComposePorts([ports[0]])}` : ''; - - let environmentVariables: string = ''; - if (platform === '.NET: ASP.NET Core') { - comment = '# Please refer https://aka.ms/HTTPSinContainer on how to setup an https developer certificate for your ASP .NET Core service.\n'; - environmentVariables = `\n environment: - - ASPNETCORE_ENVIRONMENT=Development`; - // For now assume the first port is http. (default scaffolding behavior) - // TODO: This is not the perfect logic, this should be improved later. - if (ports && ports.length > 0) { - // eslint-disable-next-line @typescript-eslint/tslint/config - let aspNetCoreUrl: string = ` - ASPNETCORE_URLS=http://+:${ports[0]}`; - environmentVariables += `\n${aspNetCoreUrl}` - } - } - - let volumesList = os === 'Windows' ? - ' - ~/.vsdbg:c:\\remote_debugger:rw' - : ' - ~/.vsdbg:/remote_debugger:rw'; - - // Ensure the path scaffolded in the Dockerfile uses POSIX separators (which work on both Linux and Windows). - dockerfileName = dockerfileName.replace(/\\/g, '/'); - - const normalizedServiceName = getValidImageName(serviceName); - - let composeFileContent = dotNetComposeTemplate.replace('$service_name$', normalizedServiceName) - .replace(/\$image_name\$/g, normalizedServiceName) - .replace(/\$dockerfile\$/g, dockerfileName) - .replace(/\$ports\$/g, jsonPorts) - .replace('$comment$', comment); - validateForUnresolvedToken(composeFileContent); - - let composeDebugFileContent = dotNetComposeDebugTemplate.replace('$service_name$', normalizedServiceName) - .replace(/\$image_name\$/g, normalizedServiceName) - .replace(/\$dockerfile\$/g, dockerfileName) - .replace(/\$ports\$/g, jsonPorts) - .replace(/\$environment\$/g, environmentVariables) - .replace(/\$volumes_list\$/g, volumesList) - .replace('$comment$', comment); - validateForUnresolvedToken(composeDebugFileContent); - - return [ - { fileName: 'docker-compose.yml', contents: composeFileContent, onConflict: async (filePath) => { return await generateNonConflictFileName(filePath) } }, - { fileName: 'docker-compose.debug.yml', contents: composeDebugFileContent, onConflict: async (filePath) => { return await generateNonConflictFileName(filePath) } } - ]; -} - -// Returns the relative path of the project file without the extension -async function findCSProjOrFSProjFile(context?: ScaffolderContext): Promise { - const opt: vscode.QuickPickOptions = { - matchOnDescription: true, - matchOnDetail: true, - placeHolder: 'Select Project' - } - - const projectFiles: string[] = await globAsync('**/*.@(c|f)sproj', { cwd: context?.rootFolder }); - - if (!projectFiles || !projectFiles.length) { - context.errorHandling.suppressReportIssue = true; - throw new Error(localize('vscode-docker.configureDotNetCore.noCsproj', '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: ScaffolderContext, folder: WorkspaceFolder, platformOS: PlatformOS, workspaceRelativeDockerfileName: string, workspaceRelativeProjectFileName: string): Promise { - const scaffoldContext: DockerDebugScaffoldContext = { - folder: folder, - platform: 'netCore', - actionContext: context, - // 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), - ports: context.ports, - }; - - 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}', workspaceRelativeProjectFileName), - platformOS: platformOS, - }; - - await dockerDebugScaffoldingProvider.initializeNetCoreForDebugging(scaffoldContext, options); -} - -async function inferOutputAssemblyName(appProjectFilePath: string): Promise { - const processProvider = new ChildProcessProvider(); - const fsProvider = new LocalFileSystemProvider(); - const osProvider = new LocalOSProvider(); - const dotNetClient = new CommandLineDotNetClient( - processProvider, - fsProvider, - osProvider - ); - const netCoreProjectProvider: NetCoreProjectProvider = new MsBuildNetCoreProjectProvider( - fsProvider, - dotNetClient, - new OSTempFileProvider(osProvider, processProvider) - ); - - const fullOutputPath = await netCoreProjectProvider.getTargetPath(appProjectFilePath); - return path.basename(fullOutputPath); -} - -// tslint:disable-next-line: export-name -export async function scaffoldNetCore(context: ScaffolderContext): Promise { - ensureDotNetCoreDependencies(context.folder, context); - const os = context.os ?? (context.os = await context.promptForOS()); - const isCompose = await context.promptForCompose(); - - const telemetryProperties = context.telemetry.properties; - - telemetryProperties.configureOs = os; - if (isCompose) { - telemetryProperties.orchestration = 'docker-compose'; - } - - context.ports = context.ports ?? (context.platform === '.NET: ASP.NET Core' ? await context.promptForPorts([80, 443]) : undefined); - - const rootRelativeProjectFileName = await context.captureStep('project', findCSProjOrFSProjFile)(context); - const projectFullPath = path.join(context.rootFolder, rootRelativeProjectFileName); - 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 projFileContent = (await fse.readFile(path.join(context.rootFolder, rootRelativeProjectFileName))).toString(); - const netCoreVersion = extractNetCoreVersion(projFileContent); - telemetryProperties.netCoreVersion = netCoreVersion; - - 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, '/'); - - const assemblyName = await inferOutputAssemblyName(projectFullPath); - let dockerFileContents = genDockerFile(serviceNameAndPathRelative, context.platform, os, context.ports, netCoreVersion, workspaceRelativeProjectFileName, assemblyName); - - // 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 composeFiles = isCompose ? generateComposeFiles(dockerFileName, context.platform, os, context.ports, workspaceRelativeProjectFileName) : []; - - let files: ScaffoldFile[] = [ - { fileName: dockerFileName, contents: dockerFileContents, open: true }, - { fileName: dockerIgnoreFileName, contents: genCommonDockerIgnoreFile(context.platform) } - ]; - - files = files.concat(composeFiles); - - 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; -} - -export function ensureDotNetCoreDependencies(workspaceFolder: WorkspaceFolder, context: IActionContext): void { - // Even if the build task is created by the test and validated, sometimes the hasTask check fails. - // So disabling this check for unit test. - if (!ext.runningTests && !hasTask('build', workspaceFolder)) { - context.errorHandling.suppressReportIssue = true; - const message = localize('vscode-docker.configureDotNetCore.missingDependencies', 'A build task is missing. Please generate build task by running \'.NET: Generate Assets for Build and Debug\' before running this command'); - throw new Error(message); - } -} diff --git a/src/configureWorkspace/configureGo.ts b/src/configureWorkspace/configureGo.ts deleted file mode 100644 index 83c425c078..0000000000 --- a/src/configureWorkspace/configureGo.ts +++ /dev/null @@ -1,58 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { getComposePorts, getExposeStatements, IPlatformGeneratorInfo, PackageInfo } from './configure'; - -export let configureGo: IPlatformGeneratorInfo = { - genDockerFile, - genDockerCompose, - genDockerComposeDebug, - defaultPorts: [3000], - initializeForDebugging: undefined, -}; - -function genDockerFile(serviceNameAndRelativePath: string, platform: string, os: string | undefined, ports: number[], { cmd, author, version, artifactName }: Partial): string { - let exposeStatements = getExposeStatements(ports); - - return ` -#build stage -FROM golang:alpine AS builder -WORKDIR /go/src/app -COPY . . -RUN apk add --no-cache git -RUN go get -d -v ./... -RUN go install -v ./... - -#final stage -FROM alpine:latest -RUN apk --no-cache add ca-certificates -COPY --from=builder /go/bin/app /app -ENTRYPOINT ./app -LABEL Name=${serviceNameAndRelativePath} Version=${version} -${exposeStatements} -`; -} - -function genDockerCompose(serviceNameAndRelativePath: string, platform: string, os: string | undefined, ports: number[]): string { - return `version: '3.4' - -services: - ${serviceNameAndRelativePath}: - image: ${serviceNameAndRelativePath} - build: . -${getComposePorts(ports)}`; -} - -function genDockerComposeDebug(serviceNameAndRelativePath: string, platform: string, os: string | undefined, ports: number[], _: Partial): string { - return `version: '3.4' - -services: - ${serviceNameAndRelativePath}: - image: ${serviceNameAndRelativePath} - build: - context: . - dockerfile: Dockerfile -${getComposePorts(ports)}`; -} diff --git a/src/configureWorkspace/configureJava.ts b/src/configureWorkspace/configureJava.ts deleted file mode 100644 index fcedf65662..0000000000 --- a/src/configureWorkspace/configureJava.ts +++ /dev/null @@ -1,55 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { getComposePorts, getExposeStatements, IPlatformGeneratorInfo, PackageInfo } from './configure'; - -export let configureJava: IPlatformGeneratorInfo = { - genDockerFile, - genDockerCompose, - genDockerComposeDebug, - defaultPorts: [3000], - initializeForDebugging: undefined, -}; - -function genDockerFile(serviceNameAndRelativePath: string, platform: string, os: string | undefined, ports: number[], { cmd, author, version, artifactName }: Partial): string { - let exposeStatements = getExposeStatements(ports); - 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 -${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 -`; -} - -function genDockerCompose(serviceNameAndRelativePath: string, platform: string, os: string | undefined, ports: number[]): string { - return `version: '3.4' - -services: - ${serviceNameAndRelativePath}: - image: ${serviceNameAndRelativePath} - build: . -${getComposePorts(ports)}`; -} - -function genDockerComposeDebug(serviceNameAndRelativePath: string, platform: string, os: string | undefined, ports: number[], _: Partial): string { - return `version: '3.4' - -services: - ${serviceNameAndRelativePath}: - image: ${serviceNameAndRelativePath} - build: - context: . - dockerfile: Dockerfile - environment: - JAVA_OPTS: -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005,quiet=y -${getComposePorts(ports, 5005)}`; -} diff --git a/src/configureWorkspace/configureNode.ts b/src/configureWorkspace/configureNode.ts deleted file mode 100644 index b2cda4a0e8..0000000000 --- a/src/configureWorkspace/configureNode.ts +++ /dev/null @@ -1,94 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { WorkspaceFolder } from 'vscode'; -import { IActionContext } from 'vscode-azureextensionui'; -import { DockerDebugScaffoldContext } from '../debugging/DebugHelper'; -import { dockerDebugScaffoldingProvider } from '../debugging/DockerDebugScaffoldingProvider'; -import { PlatformOS } from '../utils/platform'; -import { getComposePorts, getExposeStatements, IPlatformGeneratorInfo, PackageInfo } from './configure'; - -export let configureNode: IPlatformGeneratorInfo = { - genDockerFile, - genDockerCompose, - genDockerComposeDebug, - defaultPorts: [3000], - initializeForDebugging, -}; - -function genDockerFile(serviceNameAndRelativePath: string, platform: string, os: string | undefined, ports: number[], { cmd }: Partial): string { - let exposeStatements = getExposeStatements(ports); - - let cmdDirective: string; - if (Array.isArray(cmd)) { - cmdDirective = `CMD ${toCMDArray(cmd)}`; - } else { - cmdDirective = `CMD ${cmd}`; - } - - return `FROM node:12.18-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 . . -${exposeStatements} -${cmdDirective}`; -} - -function genDockerCompose(serviceNameAndRelativePath: string, platform: string, os: string | undefined, ports: number[]): string { - return `version: '3.4' - -services: - ${serviceNameAndRelativePath}: - image: ${serviceNameAndRelativePath} - build: . - environment: - NODE_ENV: production -${getComposePorts(ports)}`; -} - -function genDockerComposeDebug(serviceNameAndRelativePath: string, platform: string, os: string | undefined, ports: number[], { cmd, main }: Partial): string { - const inspectConfig = '--inspect=0.0.0.0:9229'; - - let cmdDirective: string; - if (main) { - cmdDirective = `command: ${toCMDArray(['node', inspectConfig, main])}`; - } else { - cmdDirective = `## set your startup file here\n command: ["node", "${inspectConfig}", "index.js"]`; - } - - return `version: '3.4' - -services: - ${serviceNameAndRelativePath}: - image: ${serviceNameAndRelativePath} - build: . - environment: - NODE_ENV: development -${getComposePorts(ports, 9229)} - ${cmdDirective}`; -} - -async function initializeForDebugging(context: IActionContext, folder: WorkspaceFolder, platformOS: PlatformOS, dockerfile: string, packageInfo: PackageInfo): Promise { - const scaffoldContext: DockerDebugScaffoldContext = { - folder: folder, - platform: 'node', - actionContext: context, - dockerfile: dockerfile, - } - - await dockerDebugScaffoldingProvider.initializeNodeForDebugging(scaffoldContext); -} - -function toCMDArray(cmdArray: string[]): string { - return `[${cmdArray.map(part => { - if (part.startsWith('"') && part.endsWith('"')) { - return part; - } - - return `"${part}"`; - }).join(', ')}]`; -} diff --git a/src/configureWorkspace/configureOther.ts b/src/configureWorkspace/configureOther.ts deleted file mode 100644 index 52c7222da2..0000000000 --- a/src/configureWorkspace/configureOther.ts +++ /dev/null @@ -1,44 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { getComposePorts, PackageInfo } from './configure'; - -export let configureOther = { - genDockerFile, - genDockerCompose, - genDockerComposeDebug, - defaultPorts: [3000], - initializeForDebugging: undefined, -}; - -function genDockerFile(serviceNameAndRelativePath: string, platform: string, os: string | undefined, ports: number[], { 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 ["sh", "-c", "/usr/games/fortune -a | cowsay"] -`; -} - -function genDockerCompose(serviceNameAndRelativePath: string, platform: string, os: string | undefined, ports: number[]): string { - return `version: '3.4' - -services: - ${serviceNameAndRelativePath}: - image: ${serviceNameAndRelativePath} - build: . -${getComposePorts(ports)}`; -} - -function genDockerComposeDebug(serviceNameAndRelativePath: string, platform: string, os: string | undefined, ports: number[], _: Partial): string { - return `version: '3.4' - -services: - ${serviceNameAndRelativePath}: - image: ${serviceNameAndRelativePath} - build: - context: . - dockerfile: Dockerfile -${getComposePorts(ports)}`; -} diff --git a/src/configureWorkspace/configurePython.ts b/src/configureWorkspace/configurePython.ts deleted file mode 100644 index b4bbe17177..0000000000 --- a/src/configureWorkspace/configurePython.ts +++ /dev/null @@ -1,281 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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 vscode = require('vscode'); -import { TelemetryProperties } from 'vscode-azureextensionui'; -import { DockerDebugScaffoldContext } from '../debugging/DebugHelper'; -import { dockerDebugScaffoldingProvider, PythonScaffoldingOptions } from '../debugging/DockerDebugScaffoldingProvider'; -import { ext } from "../extensionVariables"; -import { localize } from '../localize'; -import { getValidImageName } from '../utils/getValidImageName'; -import { getPythonProjectType, inferPythonArgs, PythonDefaultPorts, PythonFileExtension, PythonFileTarget, PythonModuleTarget, PythonProjectType, PythonTarget } from "../utils/pythonUtils"; -import { getComposePorts, getExposeStatements } from './configure'; -import { ConfigureTelemetryProperties, genCommonDockerIgnoreFile, quickPickGenerateComposeFiles } from './configUtils'; -import { ScaffolderContext, ScaffoldFile } from './scaffolding'; - -interface LaunchFilePrompt { - prompt: string, - defaultFile: string -} - -const defaultLaunchFile: Map = new Map([ - ['django', { prompt: localize('vscode-docker.configurePython.djangoPrompt', 'Enter the relative path to the app’s entry point (e.g. manage.py or subfolder_name/manage.py)'), defaultFile: 'manage.py' }], - ['flask', { prompt: localize('vscode-docker.configurePython.flaskPrompt', 'Enter the relative path to the app’s entry point (e.g. app.py or subfolder_name/app.py)'), defaultFile: 'app.py' }], - ['general', { prompt: localize('vscode-docker.configurePython.generalPrompt', 'Enter the relative path to the app’s entry point (e.g. app.py or subfolder_name/app.py)'), defaultFile: 'app.py' }], -]); - -const pythonDockerfile = `# For more information, please refer to https://aka.ms/vscode-docker-python -FROM python:3.8-slim-buster -$rootUserWarning$ -$expose_statements$ - -# Keeps Python from generating .pyc files in the container -ENV PYTHONDONTWRITEBYTECODE 1 - -# Turns off buffering for easier container logging -ENV PYTHONUNBUFFERED 1 - -# Install pip requirements -ADD requirements.txt . -RUN python -m pip install -r requirements.txt - -WORKDIR /app -ADD . /app -$user$ -# During debugging, this entry point will be overridden. For more information, please refer to https://aka.ms/vscode-docker-python-debug -$cmd$ -`; - -const dockerComposefile = `version: '3.4' - -services: - $service_name$: - image: $service_name$ - build: - context: . - dockerfile: Dockerfile -$ports$`; - -const dockerComposeDebugfile = `version: '3.4' - -services: - $service_name$: - image: $service_name$ - build: - context: . - dockerfile: Dockerfile - entrypoint: /bin/bash - command: -c "pip install debugpy -t /tmp && python /tmp/debugpy --wait-for-client --listen 0.0.0.0:5678 $cmd$" -$ports$ -$env$ -` - -const djangoRequirements = `django==3.0.3 -gunicorn==20.0.4`; - -const flaskRequirements = `flask==1.1.1 -gunicorn==20.0.4`; - -const lowRightsUser = ` -# Switching to a non-root user, please refer to https://aka.ms/vscode-docker-python-user-rights -RUN useradd appuser && chown -R appuser /app -USER appuser -`; - -const rootUserWarning = ` -# Warning: A port below 1024 has been exposed. This requires the image to run as a root user which is not a best practice. -# For more information, please refer to https://aka.ms/vscode-docker-python-user-rights` - -async function genDockerFile(serviceName: string, target: PythonTarget, projectType: PythonProjectType, ports: number[], outputFolder: string): Promise { - const exposeStatements = getExposeStatements(ports); - let command = ''; - - if (projectType === 'general') { - if ((target as PythonFileTarget).file) { - command = `CMD ["python", "${(target as PythonFileTarget).file}"]`; - } else { - command = `CMD ["python", "-m", "${(target as PythonModuleTarget).module}"]`; - } - } else if (projectType === 'django') { - command = await inferDjangoCommand(ports, serviceName, outputFolder); - } else if (projectType === 'flask') { - // For Flask apps, our guess is to assume there is a callable "app" object in the file/module that the user provided. - command = `CMD ["gunicorn", "--bind", "0.0.0.0:${ports ? ports[0] : PythonDefaultPorts[projectType]}", "${inferPythonWsgiModule(target)}:app"]`; - } else { - // Unlikely - throw new Error(localize('vscode-docker.configurePython.unknownProjectType', 'Unknown project type: {0}', projectType)); - } - - const rootPort: boolean = ports?.some(p => p < 1024) ?? false; - - return pythonDockerfile - .replace(/\$rootUserWarning\$/g, rootPort ? rootUserWarning : '') - .replace(/\$expose_statements\$/g, exposeStatements) - .replace(/\$user\$/g, !rootPort ? lowRightsUser : '') - .replace(/\$cmd\$/g, command); -} - -function genDockerCompose(serviceName: string, ports: number[]): string { - return dockerComposefile - .replace(/\$service_name\$/g, getValidImageName(serviceName)) - .replace(/\$ports\$/g, getComposePorts(ports)); -} - -function genDockerComposeDebug(serviceName: string, ports: number[], target: PythonTarget, projectType: PythonProjectType): string { - const args: string = (inferPythonArgs(projectType, ports) || []).join(' '); - const app: string = 'module' in target ? `-m ${target.module}` : target.file; - - const cmd: string = projectType === "flask" ? `-m flask ${args}` : `${app} ${args}`; - const env: string = projectType === "flask" ? ` environment:\n - FLASK_APP=${app}` : ''; - - return dockerComposeDebugfile - .replace(/\$service_name\$/g, getValidImageName(serviceName)) - .replace(/\$ports\$/g, getComposePorts(ports, 5678)) - .replace(/\$cmd\$/g, cmd.trimRight()) - .replace(/\$env\$/g, env); -} - -function genRequirementsFile(projectType: PythonProjectType): string { - let contents = '# To ensure app dependencies are ported from your virtual environment/host machine into your container, run \'pip freeze > requirements.txt\' in the terminal to overwrite this file'; - - switch (projectType) { - case 'django': - contents = contents.concat('\n', djangoRequirements); - break; - case 'flask': - contents = contents.concat('\n', flaskRequirements); - break; - default: - } - - return contents; -} - -async function initializeForDebugging(context: ScaffolderContext, dockerfile: string, ports: number[], target: PythonTarget, projectType: PythonProjectType): Promise { - const scaffoldContext: DockerDebugScaffoldContext = { - folder: context.folder, - platform: 'python', - actionContext: context, - dockerfile: dockerfile, - ports: ports - } - - const pyOptions: PythonScaffoldingOptions = { - projectType: projectType, - target: target - } - - await dockerDebugScaffoldingProvider.initializePythonForDebugging(scaffoldContext, pyOptions); -} - -function inferPythonWsgiModule(target: PythonTarget): string { - let wsgiModule: string; - - if ('module' in target) { - wsgiModule = target.module; - } else if ('file' in target) { - // Get rid of the file extension. - wsgiModule = target.file.replace(/\.[^/.]+$/, ''); - } - - // Replace forward-slashes with dots. - return wsgiModule.replace(/\//g, '.'); -} - -async function inferDjangoCommand(ports: number[], serviceName: string, outputFolder: string): Promise { - // For Django apps, there **usually** exists a "wsgi" module in a sub-folder named the same as the project folder. - // So we check if that path exists, then use it. Else, we output the comment below instructing the user to enter - // the correct python path to the wsgi module. - - const wsgiPath = path.join(outputFolder, path.join(serviceName, "wsgi.py")); - const command = `CMD ["gunicorn", "--bind", "0.0.0.0:${ports ? ports[0] : PythonDefaultPorts.get('django')}", "$wsgi_path$"]`; - - if (await fse.pathExists(wsgiPath)) { - return command.replace(/\$wsgi_path\$/g, `${serviceName}.wsgi`); - } else { - return `# File wsgi.py was not found in subfolder:${serviceName}. Please enter the Python path to wsgi file.` - .concat('\n', command.replace(/\$wsgi_path\$/g, 'pythonPath.to.wsgi')); - } -} - -export async function promptForLaunchFile(projectType?: PythonProjectType): Promise { - const launchFilePrompt = defaultLaunchFile.get(projectType); - - const opt: vscode.InputBoxOptions = { - placeHolder: launchFilePrompt.defaultFile, - prompt: launchFilePrompt.prompt, - value: launchFilePrompt.defaultFile, - validateInput: (value: string): string | undefined => { return value && value.trim().length > 0 ? undefined : localize('vscode-docker.configurePython.enterValidPythonFile', 'Enter a valid Python file path/module.') } - }; - - // Ensure to change any \ to /. - const file = (await ext.ui.showInputBox(opt)).replace(/\\/g, '/'); - - // If the input has the .py extension or a forward-slash, then treat it as a file/directory (i.e. execute without the -m flag). - if (path.extname(file).toLocaleUpperCase() === PythonFileExtension.toLocaleUpperCase() || - file.indexOf('/') > 0) { - return { file: file }; - } else { - return { module: file }; - } -} - -export async function scaffoldPython(context: ScaffolderContext): Promise { - const properties: TelemetryProperties & ConfigureTelemetryProperties = context.telemetry.properties; - const serviceName = context.folder.name; - const rootFolderPath: string = context.rootFolder; - const outputFolder = context.outputFolder ?? rootFolderPath; - - const generateComposeFiles = await context.captureStep('compose', quickPickGenerateComposeFiles)(); - const projectType = getPythonProjectType(context.platform); - const launchFile = await context.captureStep('pythonFile', promptForLaunchFile)(projectType); - - const defaultPort = PythonDefaultPorts.get(projectType); - let ports = []; - - if (defaultPort) { - ports = await context.promptForPorts([defaultPort]); - } - - const dockerFileContents = await genDockerFile(serviceName, launchFile, projectType, ports, outputFolder); - const dockerIgnoreContents = '**/__pycache__\n'.concat(genCommonDockerIgnoreFile(context.platform)); - - const files: ScaffoldFile[] = [ - { fileName: 'Dockerfile', contents: dockerFileContents, open: true }, - { fileName: '.dockerignore', contents: dockerIgnoreContents } - ]; - - const requirementsFileExists = await fse.pathExists(path.join(outputFolder, 'requirements.txt')); - - if (!requirementsFileExists) { - files.push({ fileName: 'requirements.txt', contents: genRequirementsFile(projectType) }); - } - - if (generateComposeFiles) { - properties.orchestration = 'docker-compose'; - - const dockerComposeFile = genDockerCompose(serviceName, ports); - const dockerComposeDebugFile = genDockerComposeDebug(serviceName, ports, launchFile, projectType); - - files.push({ fileName: 'docker-compose.yml', contents: dockerComposeFile }); - files.push({ fileName: 'docker-compose.debug.yml', contents: dockerComposeDebugFile }); - } - - files.forEach(file => { - // Remove multiple empty lines with single empty lines, as might be produced - // if $expose_statements$ or another template variable is an empty string. - file.contents = file.contents - .replace(/(\r\n){3,4}/g, '\r\n\r\n') - .replace(/(\n){3,4}/g, '\n\n'); - }); - - if (context.initializeForDebugging) { - await initializeForDebugging(context, path.join(outputFolder, 'Dockerfile'), ports, launchFile, projectType); - } - - return files; -} diff --git a/src/configureWorkspace/configureRuby.ts b/src/configureWorkspace/configureRuby.ts deleted file mode 100644 index f964bba9ed..0000000000 --- a/src/configureWorkspace/configureRuby.ts +++ /dev/null @@ -1,57 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { getComposePorts, getExposeStatements, IPlatformGeneratorInfo, PackageInfo } from './configure'; - -export let configureRuby: IPlatformGeneratorInfo = { - genDockerFile, - genDockerCompose, - genDockerComposeDebug, - defaultPorts: [3000], - initializeForDebugging: undefined, -}; - -function genDockerFile(serviceNameAndRelativePath: string, platform: string, os: string | undefined, ports: number[], { cmd, author, version, artifactName }: Partial): string { - let exposeStatements = getExposeStatements(ports); - - return `FROM ruby:2.5-slim - -LABEL Name=${serviceNameAndRelativePath} Version=${version} -${exposeStatements} - -# throw errors if Gemfile has been modified since Gemfile.lock -RUN bundle config --global frozen 1 - -WORKDIR /app -COPY . /app - -COPY Gemfile Gemfile.lock ./ -RUN bundle install - -CMD ["ruby", "${serviceNameAndRelativePath}.rb"] - `; -} - -function genDockerCompose(serviceNameAndRelativePath: string, platform: string, os: string | undefined, ports: number[]): string { - return `version: '3.4' - -services: - ${serviceNameAndRelativePath}: - image: ${serviceNameAndRelativePath} - build: . -${getComposePorts(ports)}`; -} - -function genDockerComposeDebug(serviceNameAndRelativePath: string, platform: string, os: string | undefined, ports: number[], _: Partial): string { - return `version: '3.4' - -services: - ${serviceNameAndRelativePath}: - image: ${serviceNameAndRelativePath} - build: - context: . - dockerfile: Dockerfile -${getComposePorts(ports)}`; -} diff --git a/src/configureWorkspace/scaffolding.ts b/src/configureWorkspace/scaffolding.ts deleted file mode 100644 index 3411484cc6..0000000000 --- a/src/configureWorkspace/scaffolding.ts +++ /dev/null @@ -1,210 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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 { localize } from '../localize'; -import { captureCancelStep } from '../utils/captureCancelStep'; -import { Platform, PlatformOS } from "../utils/platform"; -import { quickPickWorkspaceFolder } from '../utils/quickPickWorkspaceFolder'; -import { generateNonConflictFileName } from '../utils/uniqueNameUtils'; -import { ConfigureTelemetryCancelStep, ConfigureTelemetryProperties, promptForPorts as promptForPortsUtil, quickPickGenerateComposeFiles, 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; - promptForCompose(): Promise; - rootFolder: string; -} - -export type ScaffoldedFile = { - filePath: string; - open?: boolean; -}; - -export type ScaffoldFile = { - contents: string; - fileName: string; - open?: boolean; - onConflict?(filPath: string): Promise; -}; - -export type Scaffolder = (context: ScaffolderContext) => Promise; - -async function promptForFolder(): Promise { - return await quickPickWorkspaceFolder(localize('vscode-docker.scaffolding.workspaceFolder', '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(localize('vscode-docker.scaffolding.fileExists', '"{0}" already exists. Would you like to overwrite it?', fileName), ...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: localize('vscode-docker.scaffolding.selectPlatform', '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 async function generateNonConflictFileNameWithPrompt(filePath: string): Promise { - const OVERWRITE_PROMPT: vscode.MessageItem = { - title: localize('vscode-docker.scaffolding.Prompt.Overwrite', 'Overwrite'), - isCloseAffordance: false - }; - const NEWFILE_PROMPT: vscode.MessageItem = { - title: localize('vscode-docker.scaffolding.Prompt.CreateNew', 'Create file'), - isCloseAffordance: false - } - const prompts: vscode.MessageItem[] = [OVERWRITE_PROMPT, NEWFILE_PROMPT]; - - const generateNewFile = await vscode.window.showErrorMessage(localize('vscode-docker.scaffolding.fileExists', 'This file already exists: {0}. \r\n Do you want to overwrite it or create a new file?', filePath), ...prompts); - switch (generateNewFile) { - case OVERWRITE_PROMPT: - return filePath; - case NEWFILE_PROMPT: - return await generateNonConflictFileName(filePath); - default: - return undefined; - } -} - -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); - } - - // eslint-disable-next-line @typescript-eslint/no-floating-promises - ext.activityMeasurementService.recordActivity('overallnoedit'); - - let folder: vscode.WorkspaceFolder; - - try { - folder = context.folder ?? await captureStep('folder', promptForFolder)(); - } catch (err) { - // Suppress reporting issues due to high volume - context.errorHandling.suppressReportIssue = true; - throw err; - } - - 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(localize('vscode-docker.scaffolding.noScaffolder', 'No scaffolder is registered for platform \'{0}\'.', 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), - promptForCompose: captureStep('compose', quickPickGenerateComposeFiles), - rootFolder - }); - - return await writeFiles(files, rootFolder); -} - -export async function writeFiles(files: ScaffoldFile[], rootFolder: string): Promise { - const writtenFiles: ScaffoldedFile[] = []; - - await Promise.all( - files.map( - async file => { - let filePath = path.resolve(rootFolder, file.fileName); - - if (await fse.pathExists(filePath)) { - if (file.onConflict) { - filePath = await file.onConflict(filePath); - if (!filePath) { - return; - } - } else if (await promptForOverwrite(file.fileName) === false) { - return; - } - } - - await fse.writeFile(filePath, file.contents, 'utf8'); - writtenFiles.push({ filePath, open: file.open }); - })); - - return writtenFiles; -} - -export function openFilesIfRequired(files: ScaffoldedFile[]): void { - files.filter(file => file.open).forEach( - file => { - /* eslint-disable-next-line @typescript-eslint/no-floating-promises */ - vscode.window.showTextDocument(vscode.Uri.file(file.filePath), { preview: false }); - }); -} diff --git a/src/debugging/DebugHelper.ts b/src/debugging/DebugHelper.ts index 4df884c75d..991b35e57a 100644 --- a/src/debugging/DebugHelper.ts +++ b/src/debugging/DebugHelper.ts @@ -4,8 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken, ConfigurationTarget, debug, DebugConfiguration, ExtensionContext, workspace, WorkspaceFolder } from 'vscode'; -import { IActionContext, registerCommand } from 'vscode-azureextensionui'; -import { initializeForDebugging } from '../commands/debugging/initializeForDebugging'; +import { IActionContext } from 'vscode-azureextensionui'; import { localize } from '../localize'; import { DockerRunTaskDefinition } from '../tasks/DockerRunTaskProvider'; import { DockerTaskScaffoldContext, getDefaultContainerName } from '../tasks/TaskHelper'; @@ -62,8 +61,6 @@ export function registerDebugProvider(ctx: ExtensionContext): void { ); registerServerReadyAction(ctx); - - registerCommand('vscode-docker.debugging.initializeForDebugging', initializeForDebugging); } // TODO: This is stripping out a level of indentation, but the tasks one isn't diff --git a/src/debugging/DockerDebugScaffoldingProvider.ts b/src/debugging/DockerDebugScaffoldingProvider.ts index 552c78b3cc..1fb030a058 100644 --- a/src/debugging/DockerDebugScaffoldingProvider.ts +++ b/src/debugging/DockerDebugScaffoldingProvider.ts @@ -99,7 +99,7 @@ export class DockerDebugScaffoldingProvider implements IDockerDebugScaffoldingPr title: 'Overwrite' }; - overwrite = (overwriteMessageItem === await window.showErrorMessage(localize('vscode-docker.debug.scaffoldProvider.confirm', 'Docker launch configurations and/or tasks already exist. Do you want to overwrite them?'), ...[overwriteMessageItem, DialogResponses.no])); + overwrite = (overwriteMessageItem === await window.showWarningMessage(localize('vscode-docker.debug.scaffoldProvider.confirm', 'Docker launch configurations and/or tasks already exist. Do you want to overwrite them?'), ...[overwriteMessageItem, DialogResponses.no])); if (overwrite) { // Try again if needed diff --git a/src/debugging/coreclr/netCoreProjectProvider.ts b/src/debugging/coreclr/netCoreProjectProvider.ts index 1808afad17..be2a73647e 100644 --- a/src/debugging/coreclr/netCoreProjectProvider.ts +++ b/src/debugging/coreclr/netCoreProjectProvider.ts @@ -20,7 +20,7 @@ export class MsBuildNetCoreProjectProvider implements NetCoreProjectProvider { } public async getTargetPath(projectFile: string): Promise { - const getTargetPathProjectFile = path.join(ext.context.asAbsolutePath('resources'), 'GetTargetPath.proj'); + const getTargetPathProjectFile = path.join(ext.context.asAbsolutePath('resources'), 'netCore', 'GetTargetPath.proj'); const targetOutputFilename = this.tempFileProvider.getTempFilename(); try { await this.dotNetClient.execTarget( diff --git a/src/debugging/netcore/NetCoreDebugHelper.ts b/src/debugging/netcore/NetCoreDebugHelper.ts index 47b180490c..2bffa0281b 100644 --- a/src/debugging/netcore/NetCoreDebugHelper.ts +++ b/src/debugging/netcore/NetCoreDebugHelper.ts @@ -254,7 +254,7 @@ export class NetCoreDebugHelper implements DebugHelper { await this.vsDbgClientFactory().getVsDbgVersion('latest', 'linux-musl-x64'); } - const debuggerScriptPath = path.join(ext.context.asAbsolutePath('resources'), 'vsdbg'); + const debuggerScriptPath = path.join(ext.context.asAbsolutePath('resources'), 'netCore', 'vsdbg'); const destPath = path.join(NetCoreDebugHelper.getHostDebuggerPathBase(), 'vsdbg'); await fse.copyFile(debuggerScriptPath, destPath); await fse.chmod(destPath, 0o755); // Give all read and execute permissions diff --git a/src/docker/DockerodeApiClient/DockerodeUtils.ts b/src/docker/DockerodeApiClient/DockerodeUtils.ts index e792f1d5e1..bc2291f232 100644 --- a/src/docker/DockerodeApiClient/DockerodeUtils.ts +++ b/src/docker/DockerodeApiClient/DockerodeUtils.ts @@ -5,12 +5,12 @@ import Dockerode = require('dockerode'); import { Socket } from 'net'; +import * as os from 'os'; import { CancellationTokenSource, workspace } from 'vscode'; import { ext } from '../../extensionVariables'; import { localize } from '../../localize'; import { addDockerSettingsToEnv } from '../../utils/addDockerSettingsToEnv'; import { cloneObject } from '../../utils/cloneObject'; -import { isWindows } from '../../utils/osUtils'; import { TimeoutPromiseSource } from '../../utils/promiseUtils'; import { DockerContext } from '../Contexts'; @@ -66,7 +66,7 @@ export function refreshDockerode(currentContext: DockerContext): Dockerode { // If host is an SSH URL, we need to configure / validate SSH_AUTH_SOCK for Dockerode if (newEnv.DOCKER_HOST && SSH_URL_REGEX.test(newEnv.DOCKER_HOST)) { - if (!newEnv.SSH_AUTH_SOCK && isWindows()) { + if (!newEnv.SSH_AUTH_SOCK && os.platform() === 'win32') { // On Windows, we can use this one by default newEnv.SSH_AUTH_SOCK = '\\\\.\\pipe\\openssh-ssh-agent'; } diff --git a/src/extension.ts b/src/extension.ts index 9215d25263..0ec95c8275 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -11,7 +11,6 @@ import * as vscode from 'vscode'; import { AzureUserInput, callWithTelemetryAndErrorHandling, createAzExtOutputChannel, IActionContext, registerTelemetryHandler, registerUIExtensionVariables, UserCancelledError } from 'vscode-azureextensionui'; import { ConfigurationParams, DidChangeConfigurationNotification, DocumentSelector, LanguageClient, LanguageClientOptions, Middleware, ServerOptions, TransportKind } from 'vscode-languageclient/lib/main'; import { registerCommands } from './commands/registerCommands'; -import { LegacyDockerDebugConfigProvider } from './configureWorkspace/LegacyDockerDebugConfigProvider'; import { COMPOSE_FILE_GLOB_PATTERN } from './constants'; import { registerDebugConfigurationProvider } from './debugging/coreclr/registerDebugConfigurationProvider'; import { registerDebugProvider } from './debugging/DebugHelper'; @@ -124,12 +123,6 @@ export async function activateInternal(ctx: vscode.ExtensionContext, perfStats: registerTrees(); registerCommands(); - ctx.subscriptions.push( - vscode.debug.registerDebugConfigurationProvider( - 'docker-node', - new LegacyDockerDebugConfigProvider() - ) - ); registerDebugConfigurationProvider(ctx); registerDebugProvider(ctx); diff --git a/src/scaffolding/copyWizardContext.ts b/src/scaffolding/copyWizardContext.ts new file mode 100644 index 0000000000..7d3e8b6fc5 --- /dev/null +++ b/src/scaffolding/copyWizardContext.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 { ScaffoldingWizardContext } from './wizard/ScaffoldingWizardContext'; + +export function copyWizardContext(wizardContext: Partial, priorWizardContext: ScaffoldingWizardContext | undefined): void { + if (!priorWizardContext) { + return; + } + + for (const prop of Object.keys(priorWizardContext)) { + // Skip telemetry + error handling + if (prop === 'errorHandling' || prop === 'telemetry') { + continue; + } + + wizardContext[prop] = priorWizardContext[prop]; + } +} diff --git a/src/scaffolding/scaffold.ts b/src/scaffolding/scaffold.ts new file mode 100644 index 0000000000..cb92c0127a --- /dev/null +++ b/src/scaffolding/scaffold.ts @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AzureWizard, AzureWizardExecuteStep, AzureWizardPromptStep } from 'vscode-azureextensionui'; +import { localize } from '../localize'; +import { copyWizardContext } from './copyWizardContext'; +import { ChooseComposeStep } from './wizard/ChooseComposeStep'; +import { ChoosePlatformStep } from './wizard/ChoosePlatformStep'; +import { ChooseWorkspaceFolderStep } from './wizard/ChooseWorkspaceFolderStep'; +import { ScaffoldFileStep } from './wizard/ScaffoldFileStep'; +import { ScaffoldingWizardContext } from './wizard/ScaffoldingWizardContext'; + +export async function scaffold(wizardContext: Partial, apiInput?: ScaffoldingWizardContext): Promise { + copyWizardContext(wizardContext, apiInput); + wizardContext.scaffoldType = 'all'; + + const promptSteps: AzureWizardPromptStep[] = [ + new ChooseWorkspaceFolderStep(), + new ChoosePlatformStep(), + new ChooseComposeStep(), + ]; + + const executeSteps: AzureWizardExecuteStep[] = [ + new ScaffoldFileStep('.dockerignore', 100), + new ScaffoldFileStep('Dockerfile', 200), + ]; + + const wizard = new AzureWizard(wizardContext as ScaffoldingWizardContext, { + promptSteps: promptSteps, + executeSteps: executeSteps, + title: localize('vscode-docker.scaffold.addDockerFiles', 'Add Docker Files'), + }); + + await wizard.prompt(); + + if (wizardContext.scaffoldCompose) { + executeSteps.push(new ScaffoldFileStep('docker-compose.yml', 300)); + executeSteps.push(new ScaffoldFileStep('docker-compose.debug.yml', 400)); + } + + await wizard.execute(); +} diff --git a/src/scaffolding/scaffoldCompose.ts b/src/scaffolding/scaffoldCompose.ts new file mode 100644 index 0000000000..ca2b6e143d --- /dev/null +++ b/src/scaffolding/scaffoldCompose.ts @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AzureWizard, AzureWizardExecuteStep, AzureWizardPromptStep } from 'vscode-azureextensionui'; +import { localize } from '../localize'; +import { copyWizardContext } from './copyWizardContext'; +import { ChoosePlatformStep } from './wizard/ChoosePlatformStep'; +import { ChooseWorkspaceFolderStep } from './wizard/ChooseWorkspaceFolderStep'; +import { ScaffoldFileStep } from './wizard/ScaffoldFileStep'; +import { ScaffoldingWizardContext } from './wizard/ScaffoldingWizardContext'; + +export async function scaffoldCompose(wizardContext: Partial, apiInput?: ScaffoldingWizardContext): Promise { + copyWizardContext(wizardContext, apiInput); + wizardContext.scaffoldType = 'compose'; + wizardContext.scaffoldCompose = true; + + const promptSteps: AzureWizardPromptStep[] = [ + new ChooseWorkspaceFolderStep(), + new ChoosePlatformStep(), + ]; + + const executeSteps: AzureWizardExecuteStep[] = [ + new ScaffoldFileStep('docker-compose.yml', 300), + new ScaffoldFileStep('docker-compose.debug.yml', 400), + ]; + + const wizard = new AzureWizard(wizardContext as ScaffoldingWizardContext, { + promptSteps: promptSteps, + executeSteps: executeSteps, + title: localize('vscode-docker.scaffold.addDockerFiles', 'Add Docker Compose Files'), + }); + + await wizard.prompt(); + await wizard.execute(); +} diff --git a/src/scaffolding/scaffoldDebugConfig.ts b/src/scaffolding/scaffoldDebugConfig.ts new file mode 100644 index 0000000000..dee76ed78d --- /dev/null +++ b/src/scaffolding/scaffoldDebugConfig.ts @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AzureWizard, AzureWizardPromptStep } from 'vscode-azureextensionui'; +import { localize } from '../localize'; +import { copyWizardContext } from './copyWizardContext'; +import { ChoosePlatformStep } from './wizard/ChoosePlatformStep'; +import { ChooseWorkspaceFolderStep } from './wizard/ChooseWorkspaceFolderStep'; +import { ScaffoldingWizardContext } from './wizard/ScaffoldingWizardContext'; + +export async function scaffoldDebugConfig(wizardContext: Partial, apiInput?: ScaffoldingWizardContext): Promise { + copyWizardContext(wizardContext, apiInput); + wizardContext.scaffoldType = 'debugging'; + + const promptSteps: AzureWizardPromptStep[] = [ + new ChooseWorkspaceFolderStep(), + new ChoosePlatformStep(['Node.js', '.NET: ASP.NET Core', '.NET: Core Console', 'Python: Django', 'Python: Flask', 'Python: General']), + ]; + + const wizard = new AzureWizard(wizardContext as ScaffoldingWizardContext, { + promptSteps: promptSteps, + title: localize('vscode-docker.scaffold.addDockerFiles', 'Initialize for Debugging'), + }); + + await wizard.prompt(); + await wizard.execute(); +} diff --git a/src/scaffolding/wizard/ChooseArtifactStep.ts b/src/scaffolding/wizard/ChooseArtifactStep.ts new file mode 100644 index 0000000000..ade186bcb6 --- /dev/null +++ b/src/scaffolding/wizard/ChooseArtifactStep.ts @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ext } from '../../extensionVariables'; +import { resolveFilesOfPattern } from '../../utils/quickPickFile'; +import { ScaffoldingWizardContext } from './ScaffoldingWizardContext'; +import { TelemetryPromptStep } from './TelemetryPromptStep'; + +export class ChooseArtifactStep extends TelemetryPromptStep { + public constructor(protected readonly promptText: string, protected readonly globPatterns: string[], protected readonly noItemsMessage: string) { + super(); + } + + public async prompt(wizardContext: TWizardContext): Promise { + const items = await resolveFilesOfPattern(wizardContext.workspaceFolder, this.globPatterns); + + if (!items) { + wizardContext.errorHandling.suppressReportIssue = true; + throw new Error(this.noItemsMessage); + } else if (items.length === 1) { + wizardContext.artifact = items[0].absoluteFilePath; + } else { + const item = await ext.ui.showQuickPick(items, { placeHolder: this.promptText }); + wizardContext.artifact = item.absoluteFilePath; + } + } + + public shouldPrompt(wizardContext: TWizardContext): boolean { + return !wizardContext.artifact; + } +} diff --git a/src/scaffolding/wizard/ChooseComposeStep.ts b/src/scaffolding/wizard/ChooseComposeStep.ts new file mode 100644 index 0000000000..8c53e27be6 --- /dev/null +++ b/src/scaffolding/wizard/ChooseComposeStep.ts @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { ext } from '../../extensionVariables'; +import { localize } from '../../localize'; +import { ScaffoldingWizardContext } from './ScaffoldingWizardContext'; +import { TelemetryPromptStep } from './TelemetryPromptStep'; + +export class ChooseComposeStep extends TelemetryPromptStep { + public async prompt(wizardContext: ScaffoldingWizardContext): Promise { + const opt: vscode.QuickPickOptions = { + placeHolder: localize('vscode-docker.scaffold.chooseComposeStep.includeCompose', 'Include optional Docker Compose files?') + }; + + const response = await ext.ui.showQuickPick( + [ + { label: 'No', data: false }, + { label: 'Yes', data: true } + ], + opt + ); + + wizardContext.scaffoldCompose = response.data; + } + + public shouldPrompt(wizardContext: ScaffoldingWizardContext): boolean { + return wizardContext.scaffoldCompose === undefined; + } + + protected setTelemetry(wizardContext: ScaffoldingWizardContext): void { + wizardContext.telemetry.properties.orchestration = wizardContext.scaffoldCompose ? 'docker-compose' : 'single'; + } +} diff --git a/src/scaffolding/wizard/ChoosePlatformStep.ts b/src/scaffolding/wizard/ChoosePlatformStep.ts new file mode 100644 index 0000000000..2553de6aee --- /dev/null +++ b/src/scaffolding/wizard/ChoosePlatformStep.ts @@ -0,0 +1,89 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { IAzureQuickPickItem, IWizardOptions } from 'vscode-azureextensionui'; +import { ext } from '../../extensionVariables'; +import { localize } from '../../localize'; +import { AllPlatforms, Platform } from '../../utils/platform'; +import { ChoosePortsStep } from './ChoosePortsStep'; +import { GatherInformationStep } from './GatherInformationStep'; +import { getJavaSubWizardOptions } from './java/JavaScaffoldingWizardContext'; +import { getNetCoreSubWizardOptions } from './netCore/NetCoreScaffoldingWizardContext'; +import { getNodeSubWizardOptions } from './node/NodeScaffoldingWizardContext'; +import { getPythonSubWizardOptions } from './python/PythonScaffoldingWizardContext'; +import { ScaffoldingWizardContext } from './ScaffoldingWizardContext'; +import { TelemetryPromptStep } from './TelemetryPromptStep'; + +export class ChoosePlatformStep extends TelemetryPromptStep { + public constructor(private readonly platformsList?: Platform[]) { + super(); + } + + public async prompt(wizardContext: ScaffoldingWizardContext): Promise { + const opt: vscode.QuickPickOptions = { + matchOnDescription: true, + matchOnDetail: true, + placeHolder: localize('vscode-docker.scaffold.choosePlatformStep.selectPlatform', 'Select Application Platform') + }; + + const platforms = this.platformsList || AllPlatforms as readonly Platform[]; + + const items: IAzureQuickPickItem[] = platforms.map(p => { + return { label: p, data: p }; + }); + + const response = await ext.ui.showQuickPick(items, opt); + wizardContext.platform = response.data; + } + + public shouldPrompt(wizardContext: ScaffoldingWizardContext): boolean { + return !wizardContext.platform; + } + + public async getSubWizard(wizardContext: ScaffoldingWizardContext): Promise | undefined> { + // No output is expected from this but it will call the setTelemetry method + await super.getSubWizard(wizardContext); + + switch (wizardContext.platform) { + case 'Node.js': + return getNodeSubWizardOptions(wizardContext); + case '.NET: ASP.NET Core': + case '.NET: Core Console': + return getNetCoreSubWizardOptions(wizardContext); + case 'Python: Django': + case 'Python: Flask': + case 'Python: General': + return getPythonSubWizardOptions(wizardContext); + case 'Java': + return getJavaSubWizardOptions(wizardContext); + case 'Go': + case 'Ruby': + // Too simple to justify having their own methods + return { + promptSteps: [ + new ChoosePortsStep([3000]), + new GatherInformationStep(), + ] + }; + + case 'C++': + case 'Other': + // Too simple to justify having their own methods + return { + promptSteps: [ + new GatherInformationStep(), + ] + }; + + default: + throw new Error(localize('vscode-docker.scaffold.choosePlatformStep.unexpectedPlatform', 'Unexpected platform')); + } + } + + protected setTelemetry(wizardContext: ScaffoldingWizardContext): void { + wizardContext.telemetry.properties.configurePlatform = wizardContext.platform; + } +} diff --git a/src/scaffolding/wizard/ChoosePortsStep.ts b/src/scaffolding/wizard/ChoosePortsStep.ts new file mode 100644 index 0000000000..4d5033ba48 --- /dev/null +++ b/src/scaffolding/wizard/ChoosePortsStep.ts @@ -0,0 +1,67 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { ext } from '../../extensionVariables'; +import { localize } from '../../localize'; +import { ScaffoldingWizardContext } from './ScaffoldingWizardContext'; +import { TelemetryPromptStep } from './TelemetryPromptStep'; + +export class ChoosePortsStep extends TelemetryPromptStep { + public constructor(private readonly defaultPorts: number[]) { + super(); + } + + public async prompt(wizardContext: ScaffoldingWizardContext): Promise { + const opt: vscode.InputBoxOptions = { + placeHolder: this.defaultPorts.join(', '), + prompt: localize('vscode-docker.scaffold.choosePortsStep.whatPorts', 'What port(s) does your app listen on? Enter a comma-separated list, or empty for no exposed port.'), + value: this.defaultPorts.join(', '), + validateInput: (value: string): string | undefined => { + const result = splitPorts(value); + if (!result) { + return localize('vscode-docker.scaffold.choosePortsStep.portsFormat', 'Ports must be a comma-separated list of positive integers (1 to 65535), or empty for no exposed port.'); + } + + return undefined; + } + }; + + wizardContext.ports = splitPorts(await ext.ui.showInputBox(opt)) + } + + public shouldPrompt(wizardContext: ScaffoldingWizardContext): boolean { + return wizardContext.ports === undefined; + } + + protected setTelemetry(wizardContext: ScaffoldingWizardContext): void { + wizardContext.telemetry.measurements.numPorts = wizardContext.ports?.length ?? 0; + } +} + +/** + * Splits a comma separated string of port numbers + */ +function splitPorts(value: string): number[] | undefined { + if (!value || value === '') { + return []; + } + + let elements = value.split(',').map(p => p.trim()); + let matches = elements.filter(p => p.match(/^-*\d+$/)); + + if (matches.length < elements.length) { + return undefined; + } + + let ports = matches.map(Number); + + // If anything is non-integral or less than 1 or greater than 65535, it's not valid + if (ports.some(p => !Number.isInteger(p) || p < 1 || p > 65535)) { + return undefined; + } + + return ports; +} diff --git a/src/scaffolding/wizard/ChooseWorkspaceFolderStep.ts b/src/scaffolding/wizard/ChooseWorkspaceFolderStep.ts new file mode 100644 index 0000000000..0a4c78b2d9 --- /dev/null +++ b/src/scaffolding/wizard/ChooseWorkspaceFolderStep.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 { localize } from '../../localize'; +import { quickPickWorkspaceFolder } from '../../utils/quickPickWorkspaceFolder'; +import { ScaffoldingWizardContext } from './ScaffoldingWizardContext'; +import { TelemetryPromptStep } from './TelemetryPromptStep'; + +export class ChooseWorkspaceFolderStep extends TelemetryPromptStep { + public async prompt(wizardContext: ScaffoldingWizardContext): Promise { + try { + wizardContext.workspaceFolder = await quickPickWorkspaceFolder(localize('vscode-docker.scaffold.chooseWorkspaceFolderStep.noWorkspaceFolders', 'No workspace folders are open. Please open a workspace or workspace folder.')); + } catch (err) { + // This will only fail if the user cancels or has no folder open. To prevent a common class of non-bugs from being filed, suppress report issue here. + wizardContext.errorHandling.suppressReportIssue = true; + throw err; + } + } + + public shouldPrompt(wizardContext: ScaffoldingWizardContext): boolean { + return !wizardContext.workspaceFolder; + } +} diff --git a/src/scaffolding/wizard/GatherInformationStep.ts b/src/scaffolding/wizard/GatherInformationStep.ts new file mode 100644 index 0000000000..48b527a75d --- /dev/null +++ b/src/scaffolding/wizard/GatherInformationStep.ts @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { getValidImageNameFromPath } from '../../utils/getValidImageName'; +import { ScaffoldingWizardContext } from './ScaffoldingWizardContext'; +import { TelemetryPromptStep } from './TelemetryPromptStep'; + +export class GatherInformationStep extends TelemetryPromptStep { + public async prompt(wizardContext: TWizardContext): Promise { + if (!wizardContext.serviceName) { + wizardContext.serviceName = getValidImageNameFromPath(wizardContext.workspaceFolder.uri.fsPath); + } + + if (!wizardContext.version) { + wizardContext.version = '0.0.1'; + } + } + + public shouldPrompt(wizardContext: TWizardContext): boolean { + return !wizardContext.serviceName || !wizardContext.version; + } +} diff --git a/src/scaffolding/wizard/ScaffoldDebuggingStep.ts b/src/scaffolding/wizard/ScaffoldDebuggingStep.ts new file mode 100644 index 0000000000..0fbae58306 --- /dev/null +++ b/src/scaffolding/wizard/ScaffoldDebuggingStep.ts @@ -0,0 +1,85 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Progress } from 'vscode'; +import { AzureWizardExecuteStep } from 'vscode-azureextensionui'; +import { DockerDebugScaffoldContext } from '../../debugging/DebugHelper'; +import { dockerDebugScaffoldingProvider, NetCoreScaffoldingOptions, PythonScaffoldingOptions } from '../../debugging/DockerDebugScaffoldingProvider'; +import { localize } from '../../localize'; +import { unresolveWorkspaceFolder } from '../../utils/resolveVariables'; +import { NetCoreScaffoldingWizardContext } from './netCore/NetCoreScaffoldingWizardContext'; +import { PythonScaffoldingWizardContext } from './python/PythonScaffoldingWizardContext'; +import { ScaffoldingWizardContext } from './ScaffoldingWizardContext'; + +export class ScaffoldDebuggingStep extends AzureWizardExecuteStep { + public readonly priority: number = 1000; + + public async execute(wizardContext: ScaffoldingWizardContext, progress: Progress<{ message?: string; increment?: number; }>): Promise { + progress.report({ message: localize('vscode-docker.scaffold.scaffoldDebuggingStep.progress', 'Adding debug configuration and tasks...') }); + + let dockerfilePath: string; + if (await fse.pathExists(wizardContext.artifact)) { + dockerfilePath = path.join(path.dirname(wizardContext.artifact) || wizardContext.workspaceFolder.uri.fsPath, 'Dockerfile'); + } else { + // In the case of Python the artifact might be a module instead of a file; if so use the workspace folder as the root + dockerfilePath = path.join(wizardContext.workspaceFolder.uri.fsPath, 'Dockerfile'); + } + + const scaffoldContext: DockerDebugScaffoldContext = { + folder: wizardContext.workspaceFolder, + actionContext: wizardContext, + dockerfile: dockerfilePath, + ports: wizardContext.ports, + }; + + switch (wizardContext.platform) { + case 'Node.js': + scaffoldContext.platform = 'node'; + await dockerDebugScaffoldingProvider.initializeNodeForDebugging(scaffoldContext); + break; + + case '.NET: ASP.NET Core': + case '.NET: Core Console': + scaffoldContext.platform = 'netCore'; + const netCoreOptions: NetCoreScaffoldingOptions = { + appProject: unresolveWorkspaceFolder(wizardContext.artifact, wizardContext.workspaceFolder), + platformOS: (wizardContext as NetCoreScaffoldingWizardContext).netCorePlatformOS, + }; + await dockerDebugScaffoldingProvider.initializeNetCoreForDebugging(scaffoldContext, netCoreOptions); + break; + + case 'Python: Django': + case 'Python: Flask': + case 'Python: General': + scaffoldContext.platform = 'python'; + const pythonOptions: PythonScaffoldingOptions = { + projectType: (wizardContext as PythonScaffoldingWizardContext).pythonProjectType, + target: (wizardContext as PythonScaffoldingWizardContext).pythonArtifact, + }; + await dockerDebugScaffoldingProvider.initializePythonForDebugging(scaffoldContext, pythonOptions); + break; + + default: + throw new Error(localize('vscode-docker.scaffold.scaffoldDebuggingStep.invalidPlatform', 'Invalid platform for debug config scaffolding.')); + } + } + + public shouldExecute(wizardContext: ScaffoldingWizardContext): boolean { + switch (wizardContext.platform) { + case 'Node.js': + case '.NET: ASP.NET Core': + case '.NET: Core Console': + case 'Python: Django': + case 'Python: Flask': + case 'Python: General': + return wizardContext.scaffoldType === 'all' || wizardContext.scaffoldType === 'debugging'; + + default: + return false; + } + } +} diff --git a/src/scaffolding/wizard/ScaffoldFileStep.ts b/src/scaffolding/wizard/ScaffoldFileStep.ts new file mode 100644 index 0000000000..60eaecb60e --- /dev/null +++ b/src/scaffolding/wizard/ScaffoldFileStep.ts @@ -0,0 +1,195 @@ +/*--------------------------------------------------------------------------------------------- + * 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 Handlebars = require('handlebars'); +import * as path from 'path'; +import * as vscode from 'vscode'; +import { MessageItem, Progress } from 'vscode'; +import { AzureWizardExecuteStep, DialogResponses, UserCancelledError } from 'vscode-azureextensionui'; +import { ext } from '../../extensionVariables'; +import { localize } from '../../localize'; +import { pathNormalize } from '../../utils/pathNormalize'; +import { PlatformOS } from '../../utils/platform'; +import { ScaffoldedFileType, ScaffoldingWizardContext } from './ScaffoldingWizardContext'; + +Handlebars.registerHelper('workspaceRelative', (wizardContext: ScaffoldingWizardContext, absolutePath: string, platform: PlatformOS = 'Linux') => { + const workspaceFolder: vscode.WorkspaceFolder = wizardContext.workspaceFolder; + + return pathNormalize( + path.relative(workspaceFolder.uri.fsPath, absolutePath), + platform + ); +}); + +Handlebars.registerHelper('eq', (a: string, b: string) => { + return a === b; +}); + +Handlebars.registerHelper('basename', (a: string) => { + return path.basename(a); +}); + +Handlebars.registerHelper('dirname', (a: string, platform: PlatformOS = 'Linux') => { + return pathNormalize( + path.dirname(a), + platform + ); +}); + +Handlebars.registerHelper('toQuotedArray', (arr: string[]) => { + return `[${arr.map(a => `"${a}"`).join(', ')}]`; +}); + +Handlebars.registerHelper('isRootPort', (ports: number[]) => { + return ports?.some(p => p < 1024); +}); + +Handlebars.registerHelper('join', (a: never[] | undefined, b: never[] | undefined) => { + if (!a) { + return b; + } else if (!b) { + return a; + } else { + return a.concat(b); + } +}); + +export class ScaffoldFileStep extends AzureWizardExecuteStep { + public constructor(private readonly fileType: ScaffoldedFileType, public readonly priority: number) { + super(); + } + + public async execute(wizardContext: TWizardContext, progress: Progress<{ message?: string; increment?: number; }>): Promise { + progress.report({ message: localize('vscode-docker.scaffold.scaffoldFileStep.progress', 'Creating \'{0}\'...', this.fileType) }); + + const inputPath = await this.getInputPath(wizardContext); + + if (!inputPath) { + // If there's no template, skip + return; + } + + const outputPath = await this.getOutputPath(wizardContext); + + const input = await fse.readFile(inputPath, 'utf-8'); + const template = Handlebars.compile(input); + + const output = template(wizardContext); + + await this.promptForOverwriteIfNeeded(wizardContext, output, outputPath); + + await fse.writeFile(outputPath, output, { encoding: 'utf-8' }); + } + + public shouldExecute(wizardContext: TWizardContext): boolean { + // If this step is created it always need to be executed + return true; + } + + private async getInputPath(wizardContext: TWizardContext): Promise { + const config = vscode.workspace.getConfiguration('docker'); + const settingsTemplatesPath = config.get('scaffolding.templatePath', undefined); + const defaultTemplatesPath = path.join(ext.context.asAbsolutePath('resources'), 'templates'); + + let subPath: string; + switch (wizardContext.platform) { + case 'Node.js': + subPath = path.join('node', `${this.fileType}.template`); + break; + case '.NET: ASP.NET Core': + case '.NET: Core Console': + subPath = path.join('netCore', `${this.fileType}.template`); + break; + case 'Python: Django': + case 'Python: Flask': + case 'Python: General': + subPath = path.join('python', `${this.fileType}.template`); + break; + case 'Java': + subPath = path.join('java', `${this.fileType}.template`); + break; + case 'C++': + subPath = path.join('cpp', `${this.fileType}.template`); + break; + case 'Go': + subPath = path.join('go', `${this.fileType}.template`); + break; + case 'Ruby': + subPath = path.join('ruby', `${this.fileType}.template`); + break; + case 'Other': + subPath = path.join('other', `${this.fileType}.template`); + break; + default: + throw new Error(localize('vscode-docker.scaffold.scaffoldFileStep.unknownPlatform', 'Unknown platform \'{0}\'', wizardContext.platform)); + } + + return (settingsTemplatesPath && await this.scanUpwardForFile(path.join(settingsTemplatesPath, subPath))) || + await this.scanUpwardForFile(path.join(defaultTemplatesPath, subPath)); + } + + private async scanUpwardForFile(file: string, maxDepth: number = 1): Promise { + const fileName = path.basename(file); + let currentFile = file; + + for (let i = 0; i <= maxDepth; i++) { + if (await fse.pathExists(currentFile)) { + return currentFile; + } + + const parentDir = path.resolve(path.join(path.dirname(currentFile), '..')); + + currentFile = path.join(parentDir, fileName); + } + + return undefined; + } + + private async getOutputPath(wizardContext: TWizardContext): Promise { + if (this.fileType === 'Dockerfile' && wizardContext.artifact && + (wizardContext.platform === 'Node.js' || wizardContext.platform === '.NET: ASP.NET Core' || wizardContext.platform === '.NET: Core Console')) { + // Dockerfiles may be placed in subpaths for Node and .NET; the others are always at the workspace folder level + return path.join(path.dirname(wizardContext.artifact), this.fileType); + } else { + return path.join(wizardContext.workspaceFolder.uri.fsPath, this.fileType); + } + } + + private async promptForOverwriteIfNeeded(wizardContext: TWizardContext, output: string, outputPath: string): Promise { + if (wizardContext.overwriteAll) { + // If overwriteAll is set, no need to prompt + return; + } else if (!(await fse.pathExists(outputPath))) { + // If the output file does not exist, no need to prompt + return; + } else { + const existingContents = await fse.readFile(outputPath, 'utf-8'); + + if (output === existingContents) { + // If the output contents are identical, no need to prompt + return; + } + } + + // Otherwise, prompt + const prompt = localize('vscode-docker.scaffold.scaffoldFileStep.prompt', 'Do you want to overwrite \'{0}\'?', this.fileType); + const overwrite: MessageItem = { + title: localize('vscode-docker.scaffold.scaffoldFileStep.overwrite', 'Overwrite') + }; + const overwriteAll: MessageItem = { + title: localize('vscode-docker.scaffold.scaffoldFileStep.overwriteAll', 'Overwrite All') + }; + + const response = await ext.ui.showWarningMessage(prompt, overwriteAll, overwrite, DialogResponses.cancel); + + // Throw if the response is Cancel (Escape / X will throw above) + if (response === DialogResponses.cancel) { + throw new UserCancelledError(); + } else if (response === overwriteAll) { + wizardContext.overwriteAll = true; + } + } +} diff --git a/src/scaffolding/wizard/ScaffoldingWizardContext.ts b/src/scaffolding/wizard/ScaffoldingWizardContext.ts new file mode 100644 index 0000000000..508e2539ba --- /dev/null +++ b/src/scaffolding/wizard/ScaffoldingWizardContext.ts @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { IActionContext } from 'vscode-azureextensionui'; +import { Platform } from '../../utils/platform'; + +export type ScaffoldedFileType = '.dockerignore' | 'Dockerfile' | 'docker-compose.yml' | 'docker-compose.debug.yml' | 'requirements.txt'; + +export interface ScaffoldingWizardContext extends IActionContext { + // These are set at the beginning + scaffoldType: 'all' | 'compose' | 'debugging'; + + // These come from user choice + platform?: Platform; + ports?: number[]; + debugPorts?: number[]; + scaffoldCompose?: boolean; + workspaceFolder?: vscode.WorkspaceFolder; + + // A project file (.NET Core), entrypoint file (Python), or package.json (Node). For applicable platforms, guaranteed to be defined after the prompt phase. + artifact?: string; + + // These are calculated depending on platform, with defaults + version?: string; + serviceName?: string; + + // Other properties that get calculated or set later + overwriteAll?: boolean; +} diff --git a/src/scaffolding/wizard/TelemetryPromptStep.ts b/src/scaffolding/wizard/TelemetryPromptStep.ts new file mode 100644 index 0000000000..d8b848c7d0 --- /dev/null +++ b/src/scaffolding/wizard/TelemetryPromptStep.ts @@ -0,0 +1,20 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AzureWizardPromptStep, IActionContext, IWizardOptions } from 'vscode-azureextensionui'; + +export abstract class TelemetryPromptStep extends AzureWizardPromptStep { + public async getSubWizard(wizardContext: T): Promise | undefined> { + // getSubWizard gets called after prompt regardless of whether or not prompt gets called. Returning undefined makes it so no actual subwizard results. + + if (this.setTelemetry) { + this.setTelemetry(wizardContext); + } + + return undefined; + } + + protected setTelemetry?(wizardContext: T): void; +} diff --git a/src/scaffolding/wizard/java/ChooseJavaArtifactStep.ts b/src/scaffolding/wizard/java/ChooseJavaArtifactStep.ts new file mode 100644 index 0000000000..3862bfb344 --- /dev/null +++ b/src/scaffolding/wizard/java/ChooseJavaArtifactStep.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 { localize } from "../../../localize"; +import { ChooseArtifactStep } from "../ChooseArtifactStep"; +import { ScaffoldingWizardContext } from "../ScaffoldingWizardContext"; + +export class ChooseJavaArtifactStep extends ChooseArtifactStep { + public constructor() { + super( + localize('vscode-docker.scaffold.chooseJavaArtifactStep.promptText', 'Choose a build metadata file (pom.xml or build.gradle)'), + ['**/[Pp][Oo][Mm].[Xx][Mm][Ll]', '**/[Bb][Uu][Ii][Ll][Dd].[Gg][Rr][Aa][Dd][Ll][Ee]'], + localize('vscode-docker.scaffold.chooseJavaArtifactStep.noItemsFound', 'No build metadata files were found.') + ); + } + + public async prompt(wizardContext: ScaffoldingWizardContext): Promise { + // Java's behavior is to look for a POM or Gradle file, but if none is present no error is thrown + try { + await super.prompt(wizardContext); + } catch { } // Not a problem + } +} diff --git a/src/scaffolding/wizard/java/JavaGatherInformationStep.ts b/src/scaffolding/wizard/java/JavaGatherInformationStep.ts new file mode 100644 index 0000000000..aaa3b8edd3 --- /dev/null +++ b/src/scaffolding/wizard/java/JavaGatherInformationStep.ts @@ -0,0 +1,80 @@ +/*--------------------------------------------------------------------------------------------- + * 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 gradleParser from 'gradle-to-js/lib/parser'; +import * as xml2js from 'xml2js'; +import { GatherInformationStep } from '../GatherInformationStep'; +import { JavaScaffoldingWizardContext } from './JavaScaffoldingWizardContext'; + +interface PomContents { + project?: { + version?: string; + artifactid?: string; + }; +} + +interface GradleContents { + archivesBaseName?: string; + jar?: { version?: string; archiveName?: string; baseName?: string; }; + version?: string; +} + +export class JavaGatherInformationStep extends GatherInformationStep { + private javaProjectType: 'pom' | 'gradle' | 'unknown' = 'unknown'; + + public async prompt(wizardContext: JavaScaffoldingWizardContext): Promise { + if (wizardContext.artifact) { + // If an artifact exists, it's a POM or Gradle file, we can find some info in there + const contents = await fse.readFile(wizardContext.artifact, 'utf-8'); + + if (/pom.xml$/i.test(wizardContext.artifact)) { + // If it's a POM file, parse as XML + this.javaProjectType = 'pom'; + const pomObject = await xml2js.parseStringPromise(contents, { trim: true, normalizeTags: true, normalize: true, mergeAttrs: true }); + + wizardContext.version = pomObject?.project?.version || '0.0.1'; + + if (pomObject?.project?.artifactid) { + wizardContext.relativeJavaOutputPath = `target/${pomObject.project.artifactid}-${wizardContext.version}.jar`; + } + } else { + // Otherwise it's a gradle file, parse with that + this.javaProjectType = 'gradle'; + // eslint-disable-next-line @typescript-eslint/tslint/config + const gradleObject = await gradleParser.parseText(contents); + + wizardContext.version = gradleObject?.jar?.version || gradleObject?.version || '0.0.1'; + + if (gradleObject?.jar?.archiveName) { + wizardContext.relativeJavaOutputPath = `build/libs/${gradleObject.jar.archiveName}`; + } else if (gradleObject?.jar?.baseName) { + wizardContext.relativeJavaOutputPath = `build/libs/${gradleObject.jar.baseName}-${wizardContext.version}.jar` + } else if (gradleObject?.archivesBaseName) { + wizardContext.relativeJavaOutputPath = `build/libs/${gradleObject.archivesBaseName}-${wizardContext.version}.jar`; + } else { + wizardContext.relativeJavaOutputPath = `build/libs/${wizardContext.workspaceFolder.name}-${wizardContext.version}.jar`; + } + } + } + + await super.prompt(wizardContext); + + if (!wizardContext.relativeJavaOutputPath) { + // If the artifact is not set (fell through the above if/else), it will just be the service name + .jar + wizardContext.relativeJavaOutputPath = `${wizardContext.serviceName}.jar`; + } + + wizardContext.debugPorts = [5005]; + } + + public shouldPrompt(wizardContext: JavaScaffoldingWizardContext): boolean { + return !wizardContext.relativeJavaOutputPath; + } + + protected setTelemetry(wizardContext: JavaScaffoldingWizardContext): void { + wizardContext.telemetry.properties.javaProjectType = this.javaProjectType; + } +} diff --git a/src/scaffolding/wizard/java/JavaScaffoldingWizardContext.ts b/src/scaffolding/wizard/java/JavaScaffoldingWizardContext.ts new file mode 100644 index 0000000000..27d6ebbb72 --- /dev/null +++ b/src/scaffolding/wizard/java/JavaScaffoldingWizardContext.ts @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AzureWizardPromptStep, IWizardOptions } from 'vscode-azureextensionui'; +import { ChoosePortsStep } from '../ChoosePortsStep'; +import { ScaffoldingWizardContext } from '../ScaffoldingWizardContext'; +import { ChooseJavaArtifactStep } from './ChooseJavaArtifactStep'; +import { JavaGatherInformationStep } from './JavaGatherInformationStep'; + +export interface JavaScaffoldingWizardContext extends ScaffoldingWizardContext { + relativeJavaOutputPath?: string; +} + +export function getJavaSubWizardOptions(wizardContext: ScaffoldingWizardContext): IWizardOptions { + const promptSteps: AzureWizardPromptStep[] = [ + new ChooseJavaArtifactStep(), + new ChoosePortsStep([3000]), + new JavaGatherInformationStep(), + ]; + + return { + promptSteps: promptSteps, + }; +} diff --git a/src/scaffolding/wizard/netCore/NetCoreChooseOsStep.ts b/src/scaffolding/wizard/netCore/NetCoreChooseOsStep.ts new file mode 100644 index 0000000000..0eba63c8f0 --- /dev/null +++ b/src/scaffolding/wizard/netCore/NetCoreChooseOsStep.ts @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { IAzureQuickPickItem } from 'vscode-azureextensionui'; +import { ext } from '../../../extensionVariables'; +import { localize } from '../../../localize'; +import { PlatformOS } from '../../../utils/platform'; +import { TelemetryPromptStep } from '../TelemetryPromptStep'; +import { NetCoreScaffoldingWizardContext } from './NetCoreScaffoldingWizardContext'; + +export class NetCoreChooseOsStep extends TelemetryPromptStep { + public async prompt(wizardContext: NetCoreScaffoldingWizardContext): Promise { + const opt: vscode.QuickPickOptions = { + matchOnDescription: true, + matchOnDetail: true, + placeHolder: localize('vscode-docker.scaffold.chooseOsStep.selectOS', 'Select Operating System'), + }; + + const OSes: PlatformOS[] = ['Linux', 'Windows']; + const items = OSes.map(p => >{ label: p, data: p }); + + const response = await ext.ui.showQuickPick(items, opt); + wizardContext.netCorePlatformOS = response.data; + } + + public shouldPrompt(wizardContext: NetCoreScaffoldingWizardContext): boolean { + return !wizardContext.netCorePlatformOS; + } + + protected setTelemetry(wizardContext: NetCoreScaffoldingWizardContext): void { + wizardContext.telemetry.properties.configureOS = wizardContext.netCorePlatformOS; + } +} diff --git a/src/scaffolding/wizard/netCore/NetCoreGatherInformationStep.ts b/src/scaffolding/wizard/netCore/NetCoreGatherInformationStep.ts new file mode 100644 index 0000000000..bc276efa60 --- /dev/null +++ b/src/scaffolding/wizard/netCore/NetCoreGatherInformationStep.ts @@ -0,0 +1,83 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { SemVer } from 'semver'; +import { localize } from '../../../localize'; +import { hasTask } from '../../../tasks/TaskHelper'; +import { getValidImageNameFromPath } from '../../../utils/getValidImageName'; +import { getNetCoreProjectInfo } from '../../../utils/netCoreUtils'; +import { GatherInformationStep } from '../GatherInformationStep'; +import { NetCoreScaffoldingWizardContext } from './NetCoreScaffoldingWizardContext'; + +// .NET 5 and above +const aspNetBaseImage = 'mcr.microsoft.com/dotnet/aspnet'; +const consoleNetBaseImage = 'mcr.microsoft.com/dotnet/runtime'; +const netSdkImage = 'mcr.microsoft.com/dotnet/sdk'; + +// .NET Core 3.1 and below +// Note: this will work for .NET Core 2.2/2.1 (which are EOL), but not for <2.0 (which are way EOL) +const aspNetCoreBaseImage = 'mcr.microsoft.com/dotnet/core/aspnet'; +const consoleNetCoreBaseImage = 'mcr.microsoft.com/dotnet/core/runtime'; +const netCoreSdkImage = 'mcr.microsoft.com/dotnet/core/sdk'; + +export class NetCoreGatherInformationStep extends GatherInformationStep { + private targetFramework: string; + + public async prompt(wizardContext: NetCoreScaffoldingWizardContext): Promise { + // First, we need to validate that build tasks are created + if (!hasTask('build', wizardContext.workspaceFolder)) { + wizardContext.errorHandling.suppressReportIssue = true; + throw new Error(localize('vscode-docker.scaffold.netCoreGatherInformationStep.prereqs', 'A build task is missing. Please generate a build task by running \'.NET: Generate Assets for Build and Debug\' before running this command.')); + } + + const projectInfo = await getNetCoreProjectInfo('GetProjectProperties', wizardContext.artifact); + + if (projectInfo.length < 2) { + throw new Error(localize('vscode-docker.scaffold.netCoreGatherInformationStep.noProjectInfo', 'Unable to determine project info for \'{0}\'', wizardContext.artifact)); + } + + if (!wizardContext.netCoreAssemblyName) { + wizardContext.netCoreAssemblyName = projectInfo[0]; // Line 1 is the assembly name including ".dll" + } + + if (!wizardContext.netCoreRuntimeBaseImage || !wizardContext.netCoreSdkBaseImage) { + this.targetFramework = projectInfo[1]; // Line 2 is the value, or first item from + + const [, , netCoreVersionString] = /net(coreapp)?([\d\.]+)/i.exec(this.targetFramework); + + let netCoreVersion: SemVer; + if (/^\d+\.\d+$/.test(netCoreVersionString)) { + // SemVer requires a patch number in the version, so add it if the version matches e.g. 5.0 + netCoreVersion = new SemVer(netCoreVersionString + '.0'); + } else { + netCoreVersion = new SemVer(netCoreVersionString) + } + + if (netCoreVersion.major >= 5) { + // .NET 5 or above + wizardContext.netCoreRuntimeBaseImage = wizardContext.platform === '.NET: ASP.NET Core' ? `${aspNetBaseImage}:${netCoreVersionString}` : `${consoleNetBaseImage}:${netCoreVersionString}`; + wizardContext.netCoreSdkBaseImage = `${netSdkImage}:${netCoreVersionString}`; + } else { + // .NET 3.1 or below + wizardContext.netCoreRuntimeBaseImage = wizardContext.platform === '.NET: ASP.NET Core' ? `${aspNetCoreBaseImage}:${netCoreVersionString}` : `${consoleNetCoreBaseImage}:${netCoreVersionString}`; + wizardContext.netCoreSdkBaseImage = `${netCoreSdkImage}:${netCoreVersionString}`; + } + } + + if (!wizardContext.serviceName) { + wizardContext.serviceName = getValidImageNameFromPath(wizardContext.artifact); + } + + await super.prompt(wizardContext); + } + + public shouldPrompt(wizardContext: NetCoreScaffoldingWizardContext): boolean { + return !wizardContext.netCoreAssemblyName || !wizardContext.netCoreRuntimeBaseImage || !wizardContext.netCoreSdkBaseImage || !wizardContext.serviceName; + } + + protected setTelemetry(wizardContext: NetCoreScaffoldingWizardContext): void { + wizardContext.telemetry.properties.netCoreVersion = this.targetFramework; + } +} diff --git a/src/scaffolding/wizard/netCore/NetCoreScaffoldingWizardContext.ts b/src/scaffolding/wizard/netCore/NetCoreScaffoldingWizardContext.ts new file mode 100644 index 0000000000..c2a3c4e572 --- /dev/null +++ b/src/scaffolding/wizard/netCore/NetCoreScaffoldingWizardContext.ts @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AzureWizardPromptStep, IWizardOptions } from 'vscode-azureextensionui'; +import { CSPROJ_GLOB_PATTERN, FSPROJ_GLOB_PATTERN } from '../../../constants'; +import { localize } from '../../../localize'; +import { PlatformOS } from '../../../utils/platform'; +import { ChooseArtifactStep } from '../ChooseArtifactStep'; +import { ChoosePortsStep } from '../ChoosePortsStep'; +import { ScaffoldDebuggingStep } from '../ScaffoldDebuggingStep'; +import { ScaffoldingWizardContext } from '../ScaffoldingWizardContext'; +import { NetCoreChooseOsStep } from './NetCoreChooseOsStep'; +import { NetCoreGatherInformationStep } from './NetCoreGatherInformationStep'; + +const chooseProjectFile = localize('vscode-docker.scaffold.platforms.netCore.chooseProject', 'Choose a project file'); +const netCoreGlobPatterns = [CSPROJ_GLOB_PATTERN, FSPROJ_GLOB_PATTERN]; +const noProjectFile = localize('vscode-docker.scaffold.platforms.netCore.noProject', 'No C# or F# project files were found in the workspace.'); + +export interface NetCoreScaffoldingWizardContext extends ScaffoldingWizardContext { + netCoreAssemblyName?: string; + netCoreRuntimeBaseImage?: string; + netCoreSdkBaseImage?: string; + netCorePlatformOS?: PlatformOS; +} + +export function getNetCoreSubWizardOptions(wizardContext: ScaffoldingWizardContext): IWizardOptions { + const promptSteps: AzureWizardPromptStep[] = [ + new ChooseArtifactStep(chooseProjectFile, netCoreGlobPatterns, noProjectFile), + new NetCoreChooseOsStep(), + ]; + + if (wizardContext.platform === '.NET: ASP.NET Core' && (wizardContext.scaffoldType === 'all' || wizardContext.scaffoldType === 'compose')) { + promptSteps.push(new ChoosePortsStep([80, 443])); + } + + promptSteps.push(new NetCoreGatherInformationStep()); + + return { + promptSteps: promptSteps, + executeSteps: [ + new ScaffoldDebuggingStep(), + ], + }; +} diff --git a/src/scaffolding/wizard/node/NodeGatherInformationStep.ts b/src/scaffolding/wizard/node/NodeGatherInformationStep.ts new file mode 100644 index 0000000000..4b506243e6 --- /dev/null +++ b/src/scaffolding/wizard/node/NodeGatherInformationStep.ts @@ -0,0 +1,51 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { getValidImageName } from '../../../utils/getValidImageName'; +import { readPackage } from '../../../utils/nodeUtils'; +import { GatherInformationStep } from '../GatherInformationStep'; +import { NodeScaffoldingWizardContext } from './NodeScaffoldingWizardContext'; + +export class NodeGatherInformationStep extends GatherInformationStep { + private packageHasStartScript: boolean = false; + + public async prompt(wizardContext: NodeScaffoldingWizardContext): Promise { + const nodePackage = await readPackage(wizardContext.artifact); + + if (nodePackage.scripts?.start) { + this.packageHasStartScript = true; + wizardContext.nodeCmdParts = ['npm', 'start']; + + const [, main] = /node (.+)/i.exec(nodePackage.scripts.start) ?? [undefined, undefined]; + wizardContext.nodeDebugCmdParts = ['node', '--inspect=0.0.0.0:9229', nodePackage.main || main || 'index.js']; + } else if (nodePackage.main) { + wizardContext.nodeCmdParts = ['node', nodePackage.main] + wizardContext.nodeDebugCmdParts = ['node', '--inspect=0.0.0.0:9229', nodePackage.main]; + } else { + wizardContext.nodeCmdParts = ['npm', 'start'] + wizardContext.nodeDebugCmdParts = ['node', '--inspect=0.0.0.0:9229', 'index.js']; + } + + if (nodePackage.version) { + wizardContext.version = nodePackage.version; + } + + if (nodePackage.name) { + wizardContext.serviceName = getValidImageName(nodePackage.name); + } + + wizardContext.debugPorts = [9229]; + + await super.prompt(wizardContext); + } + + public shouldPrompt(wizardContext: NodeScaffoldingWizardContext): boolean { + return !wizardContext.nodeCmdParts || !wizardContext.nodeDebugCmdParts; + } + + protected setTelemetry(wizardContext: NodeScaffoldingWizardContext): void { + wizardContext.telemetry.properties.packageHasStartScript = this.packageHasStartScript.toString(); + } +} diff --git a/src/scaffolding/wizard/node/NodeScaffoldingWizardContext.ts b/src/scaffolding/wizard/node/NodeScaffoldingWizardContext.ts new file mode 100644 index 0000000000..de92320971 --- /dev/null +++ b/src/scaffolding/wizard/node/NodeScaffoldingWizardContext.ts @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AzureWizardPromptStep, IWizardOptions } from 'vscode-azureextensionui'; +import { localize } from '../../../localize'; +import { ChooseArtifactStep } from '../ChooseArtifactStep'; +import { ChoosePortsStep } from '../ChoosePortsStep'; +import { ScaffoldDebuggingStep } from '../ScaffoldDebuggingStep'; +import { ScaffoldingWizardContext } from '../ScaffoldingWizardContext'; +import { NodeGatherInformationStep } from './NodeGatherInformationStep'; + +const choosePackageFile = localize('vscode-docker.scaffold.platforms.node.choosePackage', 'Choose a package.json file'); +const nodeGlobPatterns = ['**/{[Pp][Aa][Cc][Kk][Aa][Gg][Ee].[Jj][Ss][Oo][Nn]}']; +const noPackageFile = localize('vscode-docker.scaffold.platforms.node.noPackage', 'No package.json files were found in the workspace.'); + +export interface NodeScaffoldingWizardContext extends ScaffoldingWizardContext { + nodeCmdParts?: string[]; + nodeDebugCmdParts?: string[]; +} + +export function getNodeSubWizardOptions(wizardContext: ScaffoldingWizardContext): IWizardOptions { + const promptSteps: AzureWizardPromptStep[] = [ + new ChooseArtifactStep(choosePackageFile, nodeGlobPatterns, noPackageFile), + ]; + + if (wizardContext.scaffoldType === 'all') { + promptSteps.push(new ChoosePortsStep([3000])); + } + + promptSteps.push(new NodeGatherInformationStep()); + + return { + promptSteps: promptSteps, + executeSteps: [ + new ScaffoldDebuggingStep(), + ], + }; +} diff --git a/src/scaffolding/wizard/python/ChoosePythonArtifactStep.ts b/src/scaffolding/wizard/python/ChoosePythonArtifactStep.ts new file mode 100644 index 0000000000..985521a321 --- /dev/null +++ b/src/scaffolding/wizard/python/ChoosePythonArtifactStep.ts @@ -0,0 +1,79 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IAzureQuickPickItem } from 'vscode-azureextensionui'; +import { ext } from '../../../extensionVariables'; +import { localize } from '../../../localize'; +import { Item, resolveFilesOfPattern } from '../../../utils/quickPickFile'; +import { ChooseArtifactStep } from '../ChooseArtifactStep'; +import { PythonScaffoldingWizardContext } from './PythonScaffoldingWizardContext'; + +const moduleRegex = /([a-z_]+[.])*([a-z_])/i; + +export class ChoosePythonArtifactStep extends ChooseArtifactStep { + public constructor() { + super( + localize('vscode-docker.scaffold.choosePythonArtifactStep.promptText', 'Choose the app\'s entry point (e.g. manage.py, app.py)'), + ['**/*.{[Pp][Yy]}'], + localize('vscode-docker.scaffold.choosePythonArtifactStep.noItemsFound', 'No Python files were found.') + ); + } + + public async prompt(wizardContext: PythonScaffoldingWizardContext): Promise { + const items = await resolveFilesOfPattern(wizardContext.workspaceFolder, this.globPatterns) ?? []; + + const pickChoices: IAzureQuickPickItem[] = items.map(i => { + return { + label: i.relativeFilePath, + data: i, + }; + }); + + const enterModuleChoice: IAzureQuickPickItem = { + label: localize('vscode-docker.scaffold.choosePythonArtifactStep.chooseModule', 'Enter a Python module instead...'), + data: undefined, + }; + + pickChoices.push(enterModuleChoice); + + const result = await ext.ui.showQuickPick(pickChoices, { + placeHolder: this.promptText, + suppressPersistence: true, + }); + + if (result === enterModuleChoice) { + // User wants a module target + const module = await ext.ui.showInputBox({ + prompt: localize('vscode-docker.scaffold.choosePythonArtifactStep.enterModule', 'Enter a Python module name (e.g. myapp.manage)'), + validateInput: (value: string): string | undefined => { + if (moduleRegex.test(value)) { + return undefined; + } + + return localize('vscode-docker.scaffold.choosePythonArtifactStep.moduleInvalid', 'Enter a valid Python module name (e.g. myapp.manage)'); + }, + }); + + wizardContext.artifact = module; + wizardContext.pythonArtifact = { + module: module, + }; + } else { + // User chose a file target + wizardContext.artifact = result.data.absoluteFilePath; + wizardContext.pythonArtifact = { + file: result.data.relativeFilePath, + }; + } + } + + public shouldPrompt(wizardContext: PythonScaffoldingWizardContext): boolean { + return super.shouldPrompt(wizardContext) || !wizardContext.pythonArtifact; + } + + protected setTelemetry(wizardContext: PythonScaffoldingWizardContext): void { + wizardContext.telemetry.properties.pythonArtifact = ('module' in wizardContext.pythonArtifact) ? 'module' : 'file'; + } +} diff --git a/src/scaffolding/wizard/python/PythonGatherInformationStep.ts b/src/scaffolding/wizard/python/PythonGatherInformationStep.ts new file mode 100644 index 0000000000..2e7cb73440 --- /dev/null +++ b/src/scaffolding/wizard/python/PythonGatherInformationStep.ts @@ -0,0 +1,132 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { inferPythonArgs, PythonDefaultDebugPort, PythonDefaultPorts } from '../../../utils/pythonUtils'; +import { GatherInformationStep } from '../GatherInformationStep'; +import { PythonScaffoldingWizardContext } from './PythonScaffoldingWizardContext'; + +const debugCmdPart = 'pip install debugpy -t /tmp && python /tmp/debugpy --wait-for-client --listen 0.0.0.0:5678'; + +export class PythonGatherInformationStep extends GatherInformationStep { + public async prompt(wizardContext: PythonScaffoldingWizardContext): Promise { + switch (wizardContext.platform) { + case 'Python: Django': + wizardContext.pythonProjectType = 'django'; + await this.getDjangoCmdParts(wizardContext); + break; + case 'Python: Flask': + wizardContext.pythonProjectType = 'flask'; + await this.getFlaskCmdParts(wizardContext); + break; + case 'Python: General': + default: + wizardContext.pythonProjectType = 'general'; + await this.getGeneralCmdParts(wizardContext); + } + + await super.prompt(wizardContext); + } + + public shouldPrompt(wizardContext: PythonScaffoldingWizardContext): boolean { + return !wizardContext.pythonCmdParts || !wizardContext.pythonDebugCmdParts || !wizardContext.pythonProjectType; + } + + private async getDjangoCmdParts(wizardContext: PythonScaffoldingWizardContext): Promise { + const { app, args, bindPort } = this.getCommonProps(wizardContext); + + // For Django apps, there **usually** exists a "wsgi" module in a sub-folder named the same as the project folder. + // So we check if that path exists, then use it. Else, we output the comment below instructing the user to enter + // the correct python path to the wsgi module. + + let wsgiModule: string; + const serviceName = path.basename(wizardContext.workspaceFolder.uri.fsPath); + const wsgiPath = path.join(wizardContext.workspaceFolder.uri.fsPath, serviceName, 'wsgi.py'); + + if (!(await fse.pathExists(wsgiPath))) { + wizardContext.wsgiComment = `# File wsgi.py was not found in subfolder: '${serviceName}'. Please enter the Python path to wsgi file.`; + wsgiModule = 'pythonPath.to.wsgi'; + } else { + wsgiModule = `${serviceName}.wsgi`; + } + + wizardContext.pythonCmdParts = [ + 'gunicorn', + '--bind', + `0.0.0.0:${bindPort}`, + wsgiModule, + ]; + + wizardContext.pythonDebugCmdParts = [ + 'sh', + '-c', + `${debugCmdPart} ${app.join(' ')} ${args.join(' ')}`, + ]; + + wizardContext.debugPorts = [PythonDefaultDebugPort]; + } + + private async getFlaskCmdParts(wizardContext: PythonScaffoldingWizardContext): Promise { + const { args, bindPort } = this.getCommonProps(wizardContext); + + let wsgiModule: string; + + if ('module' in wizardContext.pythonArtifact) { + wsgiModule = wizardContext.pythonArtifact.module; + } else if ('file' in wizardContext.pythonArtifact) { + // Get rid of the file extension. + wsgiModule = wizardContext.pythonArtifact.file.replace(/\.[^/.]+$/, ''); + } + + // Replace forward-slashes with dots. + wsgiModule = wsgiModule.replace(/\//g, '.'); + + wizardContext.pythonCmdParts = [ + 'gunicorn', + '--bind', + `0.0.0.0:${bindPort}`, + `${wsgiModule}:app`, + ]; + + wizardContext.pythonDebugCmdParts = [ + 'sh', + '-c', + `${debugCmdPart} -m flask ${args.join(' ')}`, + ]; + + wizardContext.debugPorts = [PythonDefaultDebugPort]; + } + + private async getGeneralCmdParts(wizardContext: PythonScaffoldingWizardContext): Promise { + const { app, args } = this.getCommonProps(wizardContext); + + wizardContext.pythonCmdParts = [ + 'python', + ...app, + ...args, + ]; + + wizardContext.pythonDebugCmdParts = [ + 'sh', + '-c', + `${debugCmdPart} ${app.join(' ')} ${args.join(' ')}`, + ]; + + wizardContext.debugPorts = [PythonDefaultDebugPort]; + } + + private getCommonProps(wizardContext: PythonScaffoldingWizardContext): { app: string[], args: string[], bindPort: number } { + return { + app: 'module' in wizardContext.pythonArtifact ? + ['-m', wizardContext.pythonArtifact.module] : + [wizardContext.pythonArtifact.file], + + args: inferPythonArgs(wizardContext.pythonProjectType, wizardContext.ports) ?? [], + + bindPort: wizardContext.ports ? wizardContext.ports[0] : PythonDefaultPorts.get(wizardContext.pythonProjectType), + } + } +} diff --git a/src/scaffolding/wizard/python/PythonScaffoldingWizardContext.ts b/src/scaffolding/wizard/python/PythonScaffoldingWizardContext.ts new file mode 100644 index 0000000000..e18a098855 --- /dev/null +++ b/src/scaffolding/wizard/python/PythonScaffoldingWizardContext.ts @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AzureWizardPromptStep, IWizardOptions } from 'vscode-azureextensionui'; +import { PythonDefaultPorts, PythonProjectType, PythonTarget } from '../../../utils/pythonUtils'; +import { ChoosePortsStep } from '../ChoosePortsStep'; +import { ScaffoldDebuggingStep } from '../ScaffoldDebuggingStep'; +import { ScaffoldFileStep } from '../ScaffoldFileStep'; +import { ScaffoldingWizardContext } from '../ScaffoldingWizardContext'; +import { ChoosePythonArtifactStep } from './ChoosePythonArtifactStep'; +import { PythonGatherInformationStep } from './PythonGatherInformationStep'; + +export interface PythonScaffoldingWizardContext extends ScaffoldingWizardContext { + pythonProjectType?: PythonProjectType; + pythonArtifact?: PythonTarget; + pythonCmdParts?: string[]; + pythonDebugCmdParts?: string[]; + + // Optional + wsgiComment?: string; +} + +export function getPythonSubWizardOptions(wizardContext: ScaffoldingWizardContext): IWizardOptions { + const promptSteps: AzureWizardPromptStep[] = [ + new ChoosePythonArtifactStep(), + ]; + + if (wizardContext.platform === 'Python: Django' && (wizardContext.scaffoldType === 'all' || wizardContext.scaffoldType === 'compose')) { + promptSteps.push(new ChoosePortsStep([PythonDefaultPorts.get('django')])); + } else if (wizardContext.platform === 'Python: Flask' && (wizardContext.scaffoldType === 'all' || wizardContext.scaffoldType === 'compose')) { + promptSteps.push(new ChoosePortsStep([PythonDefaultPorts.get('flask')])); + } + + promptSteps.push(new PythonGatherInformationStep()); + + return { + promptSteps: promptSteps, + executeSteps: [ + new ScaffoldFileStep('requirements.txt', 0), + new ScaffoldDebuggingStep(), + ], + }; +} diff --git a/src/tasks/TaskHelper.ts b/src/tasks/TaskHelper.ts index f52739ea79..1322fc3737 100644 --- a/src/tasks/TaskHelper.ts +++ b/src/tasks/TaskHelper.ts @@ -10,6 +10,7 @@ import { DockerDebugConfiguration } from '../debugging/DockerDebugConfigurationP import { DockerPlatform } from '../debugging/DockerPlatformHelper'; import { ext } from '../extensionVariables'; import { localize } from '../localize'; +import { getValidImageName, getValidImageNameWithTag } from '../utils/getValidImageName'; import { pathNormalize } from '../utils/pathNormalize'; import { resolveVariables } from '../utils/resolveVariables'; import { DockerBuildOptions } from './DockerBuildTaskDefinitionBase'; @@ -236,17 +237,3 @@ async function findTaskByLabel(allTasks: TaskDefinitionBase[], label: string): P async function findTaskByType(allTasks: TaskDefinitionBase[], type: string): Promise { return allTasks.find(t => t.type === type); } - -function getValidImageName(nameHint: string): string { - let result = nameHint.replace(/[^a-z0-9]/gi, '').toLowerCase(); - - if (result.length === 0) { - result = 'image' - } - - return result; -} - -function getValidImageNameWithTag(nameHint: string, tag: 'dev' | 'latest'): string { - return `${getValidImageName(nameHint)}:${tag}` -} diff --git a/src/tasks/netcore/updateBlazorManifest.ts b/src/tasks/netcore/updateBlazorManifest.ts index 76c3f83054..374f5c6e24 100644 --- a/src/tasks/netcore/updateBlazorManifest.ts +++ b/src/tasks/netcore/updateBlazorManifest.ts @@ -4,17 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import * as fse from 'fs-extra'; -import * as path from 'path'; import * as xml2js from 'xml2js'; -import ChildProcessProvider from "../../debugging/coreclr/ChildProcessProvider"; import { DockerContainerVolume } from '../../debugging/coreclr/CliDockerClient'; -import { OSTempFileProvider } from "../../debugging/coreclr/tempFileProvider"; -import { ext } from "../../extensionVariables"; import { localize } from '../../localize'; -import LocalOSProvider from "../../utils/LocalOSProvider"; +import { getNetCoreProjectInfo } from '../../utils/netCoreUtils'; import { pathNormalize } from '../../utils/pathNormalize'; import { PlatformOS } from '../../utils/platform'; -import { execAsync } from '../../utils/spawnAsync'; import { DockerRunTaskDefinition } from "../DockerRunTaskProvider"; import { DockerRunTaskContext } from "../TaskHelper"; @@ -36,33 +31,13 @@ interface Manifest { } export async function updateBlazorManifest(context: DockerRunTaskContext, runDefinition: DockerRunTaskDefinition): Promise { - const tempFileProvider = new OSTempFileProvider(new LocalOSProvider(), new ChildProcessProvider()); + const contents = await getNetCoreProjectInfo('GetBlazorManifestLocations', runDefinition.netCore.appProject); - const locationsFile = tempFileProvider.getTempFilename(); - - const targetsFile = path.join(ext.context.asAbsolutePath('resources'), 'GetBlazorManifestLocations.targets'); - - const command = `dotnet build /r:false /t:GetBlazorManifestLocations /p:CustomAfterMicrosoftCommonTargets="${targetsFile}" /p:BlazorManifestLocationsOutput="${locationsFile}" "${runDefinition.netCore.appProject}"`; - - try { - await execAsync(command, { timeout: 5000 }); - - if (await fse.pathExists(locationsFile)) { - const contents = (await fse.readFile(locationsFile)).toString().split(/\r?\n/ig); - - if (contents.length < 2) { - throw new Error(localize('vscode-docker.tasks.netCore.noBlazorManifest1', 'Unable to determine Blazor manifest locations from output file.')); - } - - await transformBlazorManifest(context, contents[0].trim(), contents[1].trim(), runDefinition.dockerRun.volumes, runDefinition.dockerRun.os); - } else { - throw new Error(localize('vscode-docker.tasks.netCore.noBlazorManifest2', 'Unable to determine Blazor manifest locations from output file.')) - } - } finally { - if (await fse.pathExists(locationsFile)) { - await fse.unlink(locationsFile); - } + if (contents.length < 2) { + throw new Error(localize('vscode-docker.tasks.netCore.noBlazorManifest1', 'Unable to determine Blazor manifest locations from output file.')); } + + await transformBlazorManifest(context, contents[0].trim(), contents[1].trim(), runDefinition.dockerRun.volumes, runDefinition.dockerRun.os); } async function transformBlazorManifest(context: DockerRunTaskContext, inputManifest: string, outputManifest: string, volumes: DockerContainerVolume[], os: PlatformOS): Promise { diff --git a/src/utils/dockerfileUtils.ts b/src/utils/dockerfileUtils.ts deleted file mode 100644 index 919812ce2b..0000000000 --- a/src/utils/dockerfileUtils.ts +++ /dev/null @@ -1,48 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Dockerfile, DockerfileParser, Keyword } from 'dockerfile-ast'; -import * as fse from 'fs-extra'; -import * as path from 'path'; -import { localize } from '../localize'; -import { pathNormalize } from './pathNormalize'; - -export interface DockerfileInfo { - rootFolder?: string; - dockerfileNameRelativeToRoot: string; - ports?: number[]; -} - -export async function parseDockerfile(rootFolderPath: string, dockerfilePath: string): Promise { - const content = await readFileContent(dockerfilePath); - const dockerFile = DockerfileParser.parse(content); - const ports: number[] = await getExposedPorts(dockerFile); - let dockerFilenameRelativeToRoot = path.relative(rootFolderPath, dockerfilePath); - dockerFilenameRelativeToRoot = pathNormalize(dockerFilenameRelativeToRoot, 'Linux') - - return { - rootFolder: rootFolderPath, - dockerfileNameRelativeToRoot: dockerFilenameRelativeToRoot, - ports: ports - }; -} - -async function getExposedPorts(dockerFile: Dockerfile): Promise { - const insts = dockerFile.getInstructions(); - const exposes = insts.filter(i => i.getKeyword() === Keyword.EXPOSE); - const ports = exposes.map(e => { - const port = e.getArgumentsContent(); - const index = port.indexOf('/'); - return index > 0 ? +port.substr(0, index) : +port; - }); - return ports; -} - -async function readFileContent(dockerfilePath: string): Promise { - if (dockerfilePath && await fse.pathExists(dockerfilePath)) { - return (await fse.readFile(dockerfilePath)).toString(); - } - throw new Error(localize('vscode-docker-utils.dockerfileUtils.invalidDockerfile', 'The dockerfile "{0}" was not provided or does not exist', dockerfilePath)); -} diff --git a/src/utils/extractRegExGroups.ts b/src/utils/extractRegExGroups.ts deleted file mode 100644 index 6a6241ca65..0000000000 --- a/src/utils/extractRegExGroups.ts +++ /dev/null @@ -1,26 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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'; - -/** - * Match an input string against a regex expression. If it matches, return an array of all - * group results returned by the match, otherwise return the given defaults array. - */ -export function extractRegExGroups(input: string, regex: RegExp, defaults: string[]): string[] { - let matches = input.match(regex); - if (matches) { - // Ignore first item, which is the text of the entire match - let [, ...groups] = matches; - - // Ignore the undefined matches - groups = groups.filter(g => g !== undefined); - - assert(groups.length === defaults.length, "extractRegExGroups: length of defaults array does not match length of actual match groups"); - return groups; - } - - return defaults; -} diff --git a/src/utils/getValidImageName.ts b/src/utils/getValidImageName.ts index c7f91dfdfc..3e7e29dad6 100644 --- a/src/utils/getValidImageName.ts +++ b/src/utils/getValidImageName.ts @@ -5,16 +5,16 @@ import * as path from 'path'; -/** - * Given a path to an application, creates a valid Docker image name based on that path - * @param appPath The application path to make an image name from (e.g. the app folder, .NET Core project file, etc.) - */ -export function getValidImageName(appPath: string, tag?: string): string { - let result = path.parse(appPath).name.replace(/[^a-z0-9]/gi, '').toLowerCase(); +export function getValidImageName(nameHint: string): string { + return nameHint.replace(/[^a-z0-9]/gi, '').toLowerCase() || 'image'; +} + +export function getValidImageNameFromPath(appPath: string, tag?: string): string { + const hint = path.parse(appPath).name; - if (result.length === 0) { - result = 'image' - } + return tag ? getValidImageNameWithTag(hint, tag) : getValidImageName(hint); +} - return tag ? `${result}:${tag}` : result; +export function getValidImageNameWithTag(nameHint: string, tag: string): string { + return `${getValidImageName(nameHint)}:${tag}` } diff --git a/src/utils/globAsync.ts b/src/utils/globAsync.ts deleted file mode 100644 index ff134491e4..0000000000 --- a/src/utils/globAsync.ts +++ /dev/null @@ -1,18 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as glob from 'glob'; - -export async function globAsync(pattern: string, options: glob.IOptions): Promise { - return await new Promise((resolve, reject) => { - glob(pattern, options, (err, matches: string[]) => { - if (err) { - reject(); - } else { - resolve(matches); - } - }); - }); -} diff --git a/src/utils/netCoreUtils.ts b/src/utils/netCoreUtils.ts new file mode 100644 index 0000000000..2c9582a510 --- /dev/null +++ b/src/utils/netCoreUtils.ts @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { ChildProcessProvider } from '../debugging/coreclr/ChildProcessProvider'; +import { OSTempFileProvider } from '../debugging/coreclr/tempFileProvider'; +import { ext } from '../extensionVariables'; +import { localize } from '../localize'; +import { LocalOSProvider } from './LocalOSProvider'; +import { execAsync } from './spawnAsync'; + +// eslint-disable-next-line @typescript-eslint/tslint/config +export async function getNetCoreProjectInfo(target: 'GetBlazorManifestLocations' | 'GetProjectProperties', project: string): Promise { + const targetsFile = path.join(ext.context.asAbsolutePath('resources'), 'netCore', `${target}.targets`); + const tempFileProvider = new OSTempFileProvider(new LocalOSProvider(), new ChildProcessProvider()); + const outputFile = tempFileProvider.getTempFilename(); + + const command = `dotnet build /r:false /t:${target} /p:CustomAfterMicrosoftCommonTargets="${targetsFile}" /p:CustomAfterMicrosoftCommonCrossTargetingTargets="${targetsFile}" /p:InfoOutputPath="${outputFile}" "${project}"`; + + try { + await execAsync(command, { timeout: 10000 }); + + if (await fse.pathExists(outputFile)) { + const contents = await fse.readFile(outputFile, 'utf-8'); + + if (contents) { + return contents.split(/\r?\n/ig); + } + } + + throw new Error(localize('vscode-docker.netCoreUtils.noProjectInfo', 'Unable to determine project information for target \'{0}\' on project \'{1}\'', target, project)); + } finally { + if (await fse.pathExists(outputFile)) { + await fse.unlink(outputFile); + } + } +} diff --git a/src/utils/nodeUtils.ts b/src/utils/nodeUtils.ts index fbee65fc62..34d7d7267a 100644 --- a/src/utils/nodeUtils.ts +++ b/src/utils/nodeUtils.ts @@ -11,6 +11,7 @@ export interface NodePackage { main?: string; name?: string; scripts?: { [key: string]: string }; + version?: string; } export type InspectMode = 'default' | 'break'; diff --git a/src/utils/osUtils.ts b/src/utils/osUtils.ts index f28a0427a0..de24927a33 100644 --- a/src/utils/osUtils.ts +++ b/src/utils/osUtils.ts @@ -3,80 +3,14 @@ * Licensed under the MIT License. See LICENSE.md in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as semver from 'semver'; +import * as os from 'os'; import { IActionContext } from 'vscode-azureextensionui'; import { DockerOSType } from '../docker/Common'; import { ext } from '../extensionVariables'; -// Minimum Windows RS3 version number -const windows10RS3MinVersion = '10.0.16299'; - -// Minimum Windows RS4 version number -const windows10RS4MinVersion = '10.0.17134'; - -// Minimum Windows RS5 version number -const windows10RS5MinVersion = "10.0.17763"; - -// Minimum Windows 19H1 version number -const windows1019H1MinVersion = "10.0.18362"; - -// Minimum Windows 19H2 version number -const windows1019H2MinVersion = "10.0.18363"; - -export function isWindows(): boolean { - return ext.os.platform === 'win32'; -} - -export function isWindows1019H2OrNewer(): boolean { - if (!isWindows()) { - return false; - } - - return semver.gte(ext.os.release, windows1019H2MinVersion); -} - -export function isWindows1019H1OrNewer(): boolean { - if (!isWindows()) { - return false; - } - - return semver.gte(ext.os.release, windows1019H1MinVersion); -} - -export function isWindows10RS5OrNewer(): boolean { - if (!isWindows()) { - return false; - } - - return semver.gte(ext.os.release, windows10RS5MinVersion); -} - -export function isWindows10RS4OrNewer(): boolean { - if (!isWindows()) { - return false; - } - - return semver.gte(ext.os.release, windows10RS4MinVersion); -} - -export function isWindows10RS3OrNewer(): boolean { - if (!isWindows()) { - return false; - } - - return semver.gte(ext.os.release, windows10RS3MinVersion); -} - -export function isLinux(): boolean { - return !isMac() && !isWindows(); -} - -export function isMac(): boolean { - return ext.os.platform === 'darwin'; -} - +// eslint-disable-next-line @typescript-eslint/tslint/config export async function getDockerOSType(context: IActionContext): Promise { - if (!isWindows()) { + if (os.platform() !== 'win32') { // On Linux or macOS, this can only ever be linux, // so short-circuit the Docker call entirely. return 'linux'; diff --git a/src/utils/platform.ts b/src/utils/platform.ts index e4e9f2264e..ad9eb026ba 100644 --- a/src/utils/platform.ts +++ b/src/utils/platform.ts @@ -3,16 +3,23 @@ * Licensed under the MIT License. See LICENSE.md in the project root for license information. *--------------------------------------------------------------------------------------------*/ -export type PlatformOS = 'Windows' | 'Linux' | 'Mac'; -export type Platform = - 'Node.js' | - '.NET: ASP.NET Core' | - '.NET: Core Console' | - 'Python: Django' | - 'Python: Flask' | - 'Python: General' | - 'Java' | - 'C++' | - 'Go' | - 'Ruby' | - 'Other'; +export const AllPlatformOSs = ['Windows', 'Linux', 'Mac'] as const; +export const AllPlatforms = [ + 'Node.js', + '.NET: ASP.NET Core', + '.NET: Core Console', + 'Python: Django', + 'Python: Flask', + 'Python: General', + 'Java', + 'C++', + 'Go', + 'Ruby', + 'Other' +] as const; + +type PlatformOSTuple = typeof AllPlatformOSs; +export type PlatformOS = PlatformOSTuple[number]; + +type PlatformTuple = typeof AllPlatforms; +export type Platform = PlatformTuple[number]; diff --git a/src/utils/pythonUtils.ts b/src/utils/pythonUtils.ts index 75ea75fc64..92bf258edd 100644 --- a/src/utils/pythonUtils.ts +++ b/src/utils/pythonUtils.ts @@ -11,9 +11,10 @@ export type PythonProjectType = 'django' | 'flask' | 'general'; export const PythonFileExtension = ".py"; export const PythonDefaultDebugPort: number = 5678; -export const PythonDefaultPorts: Map = new Map([ +export const PythonDefaultPorts = new Map([ ['django', 8000], ['flask', 5000], + ['general', undefined], ]); export type PythonTarget = PythonFileTarget | PythonModuleTarget; diff --git a/src/utils/quickPickFile.ts b/src/utils/quickPickFile.ts index 3c20c1ceb7..fceab71a93 100644 --- a/src/utils/quickPickFile.ts +++ b/src/utils/quickPickFile.ts @@ -60,7 +60,7 @@ function getGlobPatterns(globPatterns: string[], fileTypeRegEx: RegExp): string[ return result; } -async function resolveFilesOfPattern(rootFolder: vscode.WorkspaceFolder, filePatterns: string[]) +export async function resolveFilesOfPattern(rootFolder: vscode.WorkspaceFolder, filePatterns: string[]) : Promise { let uris: vscode.Uri[] = []; await Promise.all(filePatterns.map(async (pattern: string) => { @@ -86,25 +86,6 @@ async function quickPickFileItem(items: Item[], message: string): Promise { - if (items) { - if (items.length === 1) { - return items; - } else { - return await ext.ui.showQuickPick(items, { placeHolder: message, canPickMany: true }); - } - } -} - -export async function quickPickDockerFileItems(context: IActionContext, dockerFileUri: vscode.Uri | undefined, rootFolder: vscode.WorkspaceFolder, message: string): Promise { - if (dockerFileUri) { - return [createFileItem(rootFolder, dockerFileUri)]; - } - const globPatterns: string[] = getDockerFileGlobPatterns(); - const dockerFiles: Item[] | undefined = await resolveFilesOfPattern(rootFolder, globPatterns); - return await quickPickFileItems(dockerFiles, message); -} - export async function quickPickDockerFileItem(context: IActionContext, dockerFileUri: vscode.Uri | undefined, rootFolder: vscode.WorkspaceFolder): Promise { if (dockerFileUri) { return createFileItem(rootFolder, dockerFileUri); diff --git a/src/utils/timeUtils.ts b/src/utils/timeUtils.ts deleted file mode 100644 index 4891f5a8f6..0000000000 --- a/src/utils/timeUtils.ts +++ /dev/null @@ -1,30 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as process from 'process'; - -const NANOS_TO_MILLIS: bigint = BigInt(1e6); - -// Maximum exponent that results in a safe integer -// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER -const MAX_SAFE_INTEGER_EXPONENT: number = 53; - -export namespace timeUtils { - export interface ITimedResult { - Result: T, - DurationMs: number - } - - export async function timeIt(doWork: () => Promise): Promise> { - const start = process.hrtime.bigint(); - const result = await doWork(); - const stop = process.hrtime.bigint(); - const duration: number = Number(BigInt.asUintN(MAX_SAFE_INTEGER_EXPONENT, ((stop - start) / NANOS_TO_MILLIS))); - return { - Result: result, - DurationMs: duration - }; - } -} diff --git a/src/utils/uniqueNameUtils.ts b/src/utils/uniqueNameUtils.ts deleted file mode 100644 index d779034443..0000000000 --- a/src/utils/uniqueNameUtils.ts +++ /dev/null @@ -1,28 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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'; - -export function generateUniqueName(name: string, existingNames: string[], generateSuffix: (currentIndex: number) => string): string { - let newName = name; - let i = 1; - while (existingNames.includes(newName)) { - newName = `${name}${generateSuffix(i++)}`; - } - return newName; -} - -export async function generateNonConflictFileName(filePath: string): Promise { - let newFilepath = filePath; - let i = 1; - const extName = path.extname(filePath); - const extNameRegEx = new RegExp(`${extName}$`); - - while (await fse.pathExists(newFilepath)) { - newFilepath = filePath.replace(extNameRegEx, i + extName); - i++; - } - return newFilepath; -} diff --git a/test/buildAndRun.test.ts b/test/buildAndRun.test.ts deleted file mode 100644 index a07c2ac9ff..0000000000 --- a/test/buildAndRun.test.ts +++ /dev/null @@ -1,146 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -// The module 'assert' provides assertion methods from node -import * as AdmZip from 'adm-zip'; -import * as fse from 'fs-extra'; -import { Context, Suite } from 'mocha'; -import * as path from 'path'; -import { commands, tasks, Uri } from 'vscode'; -import { IActionContext } from 'vscode-azureextensionui'; -import { configure, httpsRequestBinary, Platform, bufferToString } from '../extension.bundle'; -import * as assertEx from './assertEx'; -import { shouldSkipDockerTest } from './dockerInfo'; -import { getTestRootFolder, testInEmptyFolder, testUserInput } from './global.test'; -import { runWithSetting } from './runWithSetting'; - -let testRootFolder: string = getTestRootFolder(); -let buildOutputIndex: number = 0; - -/** - * Downloads and then extracts only a specific folder and its subfolders. - */ -async function unzipFileFromUrl(uri: Uri, sourceFolderInZip: string, outputFolder: string): Promise { - let zipContents = await httpsRequestBinary(uri.toString()); - let zip = new AdmZip(zipContents); - await extractFolderTo(zip, sourceFolderInZip, outputFolder); -} - -/** - * Extracts only a specific folder and its subfolders. - * Not using AdmZip.extractAllTo because depending on the .zip file we may end up with an extraneous top-level folder - */ -async function extractFolderTo(zip: AdmZip, sourceFolderInZip: string, outputFolder: string): Promise { - if (!(sourceFolderInZip.endsWith('/') || sourceFolderInZip.endsWith('\\'))) { - sourceFolderInZip += '/'; - } - - var zipEntries = zip.getEntries(); - for (let entry of zipEntries) { - if (entry.entryName.startsWith(sourceFolderInZip)) { - let relativePath = entry.entryName.slice(sourceFolderInZip.length); - if (!relativePath) { - // root folder - continue; - } - - let outPath = path.join(outputFolder, relativePath); - if (entry.isDirectory) { - //console.log(`Folder: ${entry.entryName}`); - await fse.mkdirs(outPath) - } else { - //console.log(`File: ${entry.entryName}`); - let data: Buffer = entry.getData(); - await fse.writeFile(outPath, data); - } - } - } -} - -suite("Build Image", function (this: Suite): void { - this.timeout(2 * 60 * 1000); - - async function testConfigureAndBuildImage( - platform: Platform, - configureInputs: (string | undefined)[], - buildInputs: (string | undefined)[] - ): Promise { - const testOutputFile = path.join(testRootFolder, `buildoutput${buildOutputIndex++}.txt`); - - // Set up simulated user input - configureInputs.unshift(platform); - - const context: IActionContext = { - telemetry: { properties: {}, measurements: {} }, - errorHandling: { issueProperties: {} } - }; - - await testUserInput.runWithInputs(configureInputs, async () => { - await configure(context, testRootFolder); - }); - - // Build image - const dockerFile = Uri.file(path.join(testRootFolder, 'Dockerfile')); - - try { - await runWithSetting('commands.build', `docker build --pull --rm -f "\${dockerfile}" -t \${tag} "\${context}" > ${testOutputFile} 2>&1`, async () => { - await testUserInput.runWithInputs(buildInputs, async () => { - const taskFinishedPromise = new Promise((resolve) => { - const disposable = tasks.onDidEndTask(() => { - disposable.dispose(); - resolve(); - }); - }); - - await commands.executeCommand('vscode-docker.images.build', dockerFile); - - // Wait for the task to finish - await taskFinishedPromise; - }); - }); - - const outputText = bufferToString(await fse.readFile(testOutputFile)); - - assertEx.assertContains(outputText, 'Successfully built'); - assertEx.assertContains(outputText, 'Successfully tagged'); - } finally { - if (await fse.pathExists(testOutputFile)) { - await fse.unlink(testOutputFile); - } - } - } - - // Go - - testInEmptyFolder("Go", async function (this: Context) { - let context: IActionContext = { - telemetry: { properties: {}, measurements: {} }, - errorHandling: { issueProperties: {} } - }; - if (await shouldSkipDockerTest(context, { linuxContainers: true })) { - this.skip(); - return; - } - - let uri = 'https://codeload.github.com/cloudfoundry-community/simple-go-web-app/zip/master'; // https://github.com/cloudfoundry-community/simple-go-web-app/archive/master.zip - await unzipFileFromUrl(Uri.parse(uri), 'simple-go-web-app-master', testRootFolder); - await testConfigureAndBuildImage( - 'Go', - ['3001'], - ['testoutput:latest'] - ); - - // CONSIDER: Run the built image - }); - - // CONSIDER TESTS: - // 'Java' - // '.NET Core Console' - // 'ASP.NET Core' - // 'Node.js' - // 'Python' - // 'Ruby' - -}); diff --git a/test/configure.test.ts b/test/configure.test.ts deleted file mode 100644 index 4e21d800bd..0000000000 --- a/test/configure.test.ts +++ /dev/null @@ -1,1503 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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 * as assertEx from './assertEx'; -import * as vscode from 'vscode'; -import * as fse from 'fs-extra'; -import * as os from 'os'; -import * as path from 'path'; -import { Suite } from 'mocha'; -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; - -const windowsServer2016 = '10.0.14393'; -const windows10RS3 = '10.0.16299'; -const windows10RS4 = '10.0.17134'; -const windows10RS5 = '10.0.17763'; - -let testRootFolder: string = getTestRootFolder(); - -/* Removes any leading blank lines, and also unindents all lines by however much the first non-empty line is indented. - This lets you write this: - - const text = ` -indented text: - sub-indented text -`; - -as the easier to read: - - const text = ` - indented text: - sub-indented text - `; -*/ -function removeIndentation(text: string): string { - while (text[0] === '\r' || text[0] === '\n') { - text = text.substr(1); - } - - // Figure out indentation of first line - let spaces = text.match(/^[ ]+/); - if (spaces) { - let indentationPattern = new RegExp(`^${spaces[0]}`, 'gm'); - text = text.replace(indentationPattern, ''); - } - - // Truncate last line if only contains blanks - text = text.replace(/[ ]+$/, ''); - - return text; -} - -async function readFile(pathRelativeToTestRootFolder: string): Promise { - let dockerFilePath = path.join(testRootFolder, pathRelativeToTestRootFolder); - let dockerFileBuffer = await fse.readFile(dockerFilePath); - let dockerFileContents = dockerFileBuffer.toString(); - return dockerFileContents; -} - -async function testConfigureDockerViaApi(options: ConfigureApiOptions, inputs: (string | TestInput)[] = [], expectedOutputFiles?: string[]): Promise { - await testUserInput.runWithInputs(inputs, async () => { - 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) { - let projectFiles = await getFilesInProject(); - assertEx.unorderedArraysEqual(projectFiles, expectedOutputFiles, "The set of files in the project folder after configure was run is not correct."); - - if (outputAllGeneratedFileContents) { - for (let file of projectFiles) { - console.log(file); - let contents = readFile(file); - console.log(contents); - } - } - } -} - -function verifyTelemetryProperties(context: IActionContext, expectedTelemetryProperties?: ConfigureTelemetryProperties) { - if (expectedTelemetryProperties) { - let properties: TelemetryProperties & ConfigureTelemetryProperties = context.telemetry.properties; - assert.equal(properties.configureOs, expectedTelemetryProperties.configureOs, "telemetry wrong: os"); - assert.equal(properties.packageFileSubfolderDepth, expectedTelemetryProperties.packageFileSubfolderDepth, "telemetry wrong: packageFileSubfolderDepth"); - assert.equal(properties.packageFileType, expectedTelemetryProperties.packageFileType, "telemetry wrong: packageFileType"); - assert.equal(properties.configurePlatform, expectedTelemetryProperties.configurePlatform, "telemetry wrong: platform"); - } -} -async function writeFile(subfolderName: string, fileName: string, text: string): Promise { - await fse.mkdirs(path.join(testRootFolder, subfolderName)); - await fse.writeFile(path.join(testRootFolder, subfolderName, fileName), text); -} - -function assertFileContains(fileName: string, text: string): void { - let filePath = path.join(testRootFolder, fileName); - assertEx.assertFileContains(filePath, text); -} - -function assertNotFileContains(fileName: string, text: string): void { - let filePath = path.join(testRootFolder, fileName); - assertEx.assertNotFileContains(filePath, text); -} - -async function getFilesInProject(): Promise { - let files = await globAsync('**/*', { - cwd: testRootFolder, - dot: true, // include files beginning with dot - nodir: true - }); - return files; -} - -async function testConfigureDocker(platform: Platform, expectedTelemetryProperties?: ConfigureTelemetryProperties, inputs: (string | TestInput)[] = [], expectedOutputFiles?: string[]): Promise { - // Set up simulated user input - inputs.unshift(platform); - let context: IActionContext = { - telemetry: { properties: {}, measurements: {} }, - errorHandling: { issueProperties: {} } - }; - - await testUserInput.runWithInputs(inputs, async () => { - await configure(context, testRootFolder); - }); - - if (expectedOutputFiles) { - let projectFiles = await getFilesInProject(); - assertEx.unorderedArraysEqual(projectFiles, expectedOutputFiles, "The set of files in the project folder after configure was run is not correct."); - - if (outputAllGeneratedFileContents) { - for (let file of projectFiles) { - console.log(file); - let contents = readFile(file); - console.log(contents); - } - } - } - - verifyTelemetryProperties(context, expectedTelemetryProperties); -} - -//#region .NET Core Console projects - -const dotnetCoreConsole_ProgramCsContents = ` -using System; - -namespace ConsoleApp1 -{ - class Program - { - static void Main(string[] args) - { - Console.WriteLine("Hello World!"); - } - } -} -`; - -// Created in Visual Studio 2017 -const dotNetCoreConsole_10_ProjectFileContents = ` - - - - Exe - netcoreapp1.0 - Core1._0ConsoleApp - - - - `; - -const dotNetCoreConsole_11_ProjectFileContents = removeIndentation(` - - - - Exe - netcoreapp1.1 - Core1._1ConsoleApp - - - - `); - -const dotNetCoreConsole_20_ProjectFileContents = removeIndentation(` - - - - Exe - netcoreapp2.0 - Core2._0ConsoleApp - - - - `); - -// https://github.com/dotnet/dotnet-docker/tree/master/samples/dotnetapp -const dotNetCoreConsole_21_ProjectFileContents = removeIndentation(` - - - - Exe - netcoreapp2.1 - - - - - - - - `); - -const dotNetCoreConsole_22_ProjectFileContents = removeIndentation(` - - - - Exe - netcoreapp2.2 - - - -`); - -//#endregion - -//#region ASP.NET Core projects - -// https://github.com/dotnet/dotnet-docker/tree/master/samples/aspnetapp -const aspNet_21_ProjectFileContents = removeIndentation(` - - - - netcoreapp2.1 - - - - - - - - `); - -// Generated by VS -const aspNet_22_ProjectFileContents = removeIndentation(` - - - - netcoreapp2.2 - inprocess - Linux - - - - - - - - - - -`); - -const aspNet_10_ProjectFileContents = removeIndentation(` - - - - netcoreapp1.1 - Windows - 22a9bd21-dbf0-4ef0-9963-d56730908d16 - - - - - - - - -`); - -const aspNet_20_ProjectFileContents = removeIndentation(` - - - - netcoreapp2.0 - Linux - - - - - - - - -`); - -//#endregion - -const gradleWithJarContents = removeIndentation(` - apply plugin: 'groovy' - - dependencies { - compile gradleApi() - compile localGroovy() - } - - apply plugin: 'maven' - apply plugin: 'signing' - - repositories { - mavenCentral() - } - - group = 'com.github.test' - version = '1.2.3' - sourceCompatibility = 1.7 - targetCompatibility = 1.7 - - task javadocJar(type: Jar) { - classifier = 'javadoc' - from javadoc - } - - task sourcesJar(type: Jar) { - classifier = 'sources' - from sourceSets.main.allSource - } - - artifacts { - archives javadocJar, sourcesJar - } - - jar { - configurations.shade.each { dep -> - from(project.zipTree(dep)){ - duplicatesStrategy 'warn' - } - } - - manifest { - attributes 'version':project.version - attributes 'javaCompliance': project.targetCompatibility - attributes 'group':project.group - attributes 'Implementation-Version': project.version + getGitHash() - } - archiveName 'abc.jar' - } - - uploadArchives { - repositories { - mavenDeployer { - - beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } - - repository(url: uri('../repo')) - - pom.project { - name 'test' - packaging 'jar' - description 'test' - url 'https://github.com/test' - } - } - } - } -`); - -suite("Configure (Add Docker files to Workspace)", function (this: Suite): void { - this.timeout(30 * 1000); - - const outputChannel: IAzExtOutputChannel = createAzExtOutputChannel('Docker extension tests', 'docker'); - ext.outputChannel = outputChannel; - - async function testDotNetCoreConsole(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', - release: hostOsRelease - }; - try { - - await writeFile(projectFolder, projectFileName, projectFileContents); - await writeFile(projectFolder, 'Program.cs', dotnetCoreConsole_ProgramCsContents); - - await testConfigureDocker( - '.NET: Core Console', - { - configurePlatform: '.NET: Core Console', - configureOs: os, - packageFileType: '.csproj', - packageFileSubfolderDepth: projectFolder.includes('/') ? '2' : '1' - }, - [os, 'No' /* it doesn't ask for a port, so we don't specify one here */], - [`${projectFolder}/Dockerfile`, '.dockerignore', `${projectFolder}/Program.cs`, `${projectFolder}/${projectFileName}`] - ); - - let dockerFileContents = await readFile(`${projectFolder}/Dockerfile`); - if (expectedDockerFileContents) { - assert.equal(dockerFileContents, expectedDockerFileContents); - } - } finally { - ext.os = previousOs; - } - } - - 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', - release: hostOsRelease - }; - try { - await writeFile(projectFolder, projectFileName, projectFileContents); - await writeFile(projectFolder, 'Program.cs', dotNetCoreConsole_10_ProjectFileContents); - - await testConfigureDocker( - '.NET: ASP.NET Core', - { - configurePlatform: '.NET: ASP.NET Core', - configureOs: os, - packageFileType: '.csproj', - packageFileSubfolderDepth: '1' - }, - [os, 'No', '1234'], - [`${projectFolder}/Dockerfile`, '.dockerignore', `${projectFolder}/Program.cs`, `${projectFolder}/${projectFileName}`] - ); - - let dockerFileContents = await readFile(`${projectFolder}/Dockerfile`); - if (expectedDockerFileContents) { - assert.equal(dockerFileContents, expectedDockerFileContents); - } - } finally { - ext.os = previousOs; - } - } - - // Node.js - - suite("Node.js", () => { - testInEmptyFolder("No package.json", async () => { - await testConfigureDocker( - 'Node.js', - { - configurePlatform: 'Node.js', - configureOs: undefined, - packageFileType: undefined, - packageFileSubfolderDepth: undefined - }, - ['Yes', '1234'], - ['Dockerfile', 'docker-compose.debug.yml', 'docker-compose.yml', '.dockerignore'] - ); - - assertFileContains('Dockerfile', 'EXPOSE 1234'); - assertFileContains('Dockerfile', 'CMD ["npm", "start"]'); - - assertFileContains('docker-compose.debug.yml', '1234'); - assertFileContains('docker-compose.debug.yml', '9229:9229'); - assertFileContains('docker-compose.debug.yml', 'image: testoutput'); - assertFileContains('docker-compose.debug.yml', 'NODE_ENV: development'); - assertFileContains('docker-compose.debug.yml', 'command: ["node", "--inspect=0.0.0.0:9229", "index.js"]'); - - assertFileContains('docker-compose.yml', '1234'); - assertNotFileContains('docker-compose.yml', '9229:9229'); - assertFileContains('docker-compose.yml', 'image: testoutput'); - assertFileContains('docker-compose.yml', 'NODE_ENV: production'); - assertNotFileContains('docker-compose.yml', 'command: ["node", "--inspect=0.0.0.0:9229", "index.js"]'); - - assertFileContains('.dockerignore', '.vscode'); - }); - - testInEmptyFolder("No Docker Compose", async () => { - await testConfigureDocker( - 'Node.js', - { - configurePlatform: 'Node.js', - configureOs: undefined, - packageFileType: undefined, - packageFileSubfolderDepth: undefined - }, - ['No', '1234'], - ['Dockerfile', '.dockerignore'] - ); - - assertFileContains('Dockerfile', 'EXPOSE 1234'); - assertFileContains('Dockerfile', 'CMD ["npm", "start"]'); - - assertFileContains('.dockerignore', '.vscode'); - }); - - testInEmptyFolder("With start script that explicitly calls node", async () => { - await writeFile('', 'package.json', - JSON.stringify({ - "name": "myexpressapp", - "version": "1.23.345", - "private": true, - "scripts": { - "vscode:prepublish": "tsc -p ./", - "start": "node ./bin/www", - "test": "npm run build && node ./node_modules/vscode/bin/test" - }, - "dependencies": { - "cookie-parser": "~1.4.4", - "debug": "~2.6.9", - "express": "~4.16.1", - "http-errors": "~1.6.3", - "jade": "~1.11.0", - "morgan": "~1.9.1" - } - } - )); - - await testConfigureDocker( - 'Node.js', - { - configurePlatform: 'Node.js', - configureOs: undefined, - packageFileType: 'package.json', - packageFileSubfolderDepth: '0' - }, - ['Yes', '4321'], - ['package.json', 'Dockerfile', 'docker-compose.debug.yml', 'docker-compose.yml', '.dockerignore'] - ); - - assertFileContains('Dockerfile', 'EXPOSE 4321'); - assertFileContains('Dockerfile', 'CMD ["npm", "start"]'); - - assertFileContains('docker-compose.debug.yml', '4321'); - assertFileContains('docker-compose.debug.yml', '9229:9229'); - assertFileContains('docker-compose.debug.yml', 'image: testoutput'); - assertFileContains('docker-compose.debug.yml', 'NODE_ENV: development'); - assertFileContains('docker-compose.debug.yml', 'command: ["node", "--inspect=0.0.0.0:9229", "./bin/www"]'); - - assertFileContains('docker-compose.yml', '4321'); - assertNotFileContains('docker-compose.yml', '9229:9229'); - assertFileContains('docker-compose.yml', 'image: testoutput'); - assertFileContains('docker-compose.yml', 'NODE_ENV: production'); - assertNotFileContains('docker-compose.yml', 'command: ["node", "--inspect=0.0.0.0:9229", "./bin/www"]'); - - assertFileContains('.dockerignore', '.vscode'); - }); - - testInEmptyFolder("With start script that implicitly calls node", async () => { - await writeFile('', 'package.json', - `{ - "name": "vscode-docker", - "version": "0.0.28", - "main": "./out/dockerExtension", - "author": "Azure", - "scripts": { - "vscode:prepublish": "tsc -p ./", - "start": "./bin/www", - "test": "npm run build && node ./node_modules/vscode/bin/test" - }, - "dependencies": { - "azure-arm-containerregistry": "^1.0.0-preview" - } - } - `); - - await testConfigureDocker( - 'Node.js', - { - configurePlatform: 'Node.js', - configureOs: undefined, - packageFileType: 'package.json', - packageFileSubfolderDepth: '0' - }, - ['Yes', '4321'], - ['package.json', 'Dockerfile', 'docker-compose.debug.yml', 'docker-compose.yml', '.dockerignore'] - ); - - assertNotFileContains('docker-compose.yml', 'command: ["node", "--inspect=0.0.0.0:9229", "index.js"]'); - }); - - testInEmptyFolder("Without start script", async () => { - await writeFile('', 'package.json', - `{ - "name": "vscode-docker", - "version": "0.0.28", - "main": "./out/dockerExtension", - "author": "Azure", - "scripts": { - "vscode:prepublish": "tsc -p ./", - "test": "npm run build && node ./node_modules/vscode/bin/test" - }, - "dependencies": { - "azure-arm-containerregistry": "^1.0.0-preview" - } - } - `); - - await testConfigureDocker( - 'Node.js', - { - configurePlatform: 'Node.js', - configureOs: undefined, - packageFileType: 'package.json', - packageFileSubfolderDepth: '0', - }, - ['Yes', '4321'], - ['package.json', 'Dockerfile', 'docker-compose.debug.yml', 'docker-compose.yml', '.dockerignore'] - ); - - assertFileContains('Dockerfile', 'EXPOSE 4321'); - assertFileContains('Dockerfile', 'CMD ["node", "./out/dockerExtension"]'); - }); - }); - - // .NET Core Console - - suite(".NET Core General", () => { - testInEmptyFolder("No project file", async () => { - await assertEx.throwsOrRejectsAsync(async () => - testConfigureDocker( - '.NET: Core Console', - { - configurePlatform: '.NET: Core Console', - configureOs: 'Windows', - packageFileType: undefined, - packageFileSubfolderDepth: undefined - }, - ['Windows', 'No'] - ), - { message: "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." } - ); - }); - - testInEmptyFolder("ASP.NET Core no project file", async () => { - await assertEx.throwsOrRejectsAsync(async () => testConfigureDocker('.NET: ASP.NET Core', {}, ['Windows', 'No', '1234']), - { message: "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." } - ); - }); - - testInEmptyFolder("Multiple project files", async () => { - await writeFile('projectFolder1', 'aspnetapp.csproj', dotNetCoreConsole_21_ProjectFileContents); - await writeFile('projectFolder2', 'aspnetapp.csproj', dotNetCoreConsole_21_ProjectFileContents); - await testConfigureDocker( - '.NET: Core Console', - { - configurePlatform: '.NET: Core Console', - configureOs: 'Windows', - packageFileType: '.csproj', - packageFileSubfolderDepth: '1' - }, - ['Windows', 'No', 'projectFolder2/aspnetapp.csproj'], - ['projectFolder2/Dockerfile', '.dockerignore', 'projectFolder1/aspnetapp.csproj', 'projectFolder2/aspnetapp.csproj'] - ); - - 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"]`); - }); - }); - - suite(".NET Core Console 2.1", async () => { - testInEmptyFolder("Windows", async () => { - await testDotNetCoreConsole( - 'Windows', - 'Windows', - windows10RS5, - 'ConsoleApp1Folder', - 'ConsoleApp1.csproj', - dotNetCoreConsole_21_ProjectFileContents, - removeIndentation(` - #Depending on the operating system of the host machines(s) that will build or run the containers, the image specified in the FROM statement may need to be changed. - #For more information, please see https://aka.ms/containercompat - - FROM mcr.microsoft.com/dotnet/core/runtime:2.1-nanoserver-1809 AS base - WORKDIR /app - - FROM mcr.microsoft.com/dotnet/core/sdk:2.1-nanoserver-1809 AS build - WORKDIR /src - COPY ["ConsoleApp1Folder/ConsoleApp1.csproj", "ConsoleApp1Folder/"] - RUN dotnet restore "ConsoleApp1Folder/ConsoleApp1.csproj" - COPY . . - WORKDIR "/src/ConsoleApp1Folder" - RUN dotnet build "ConsoleApp1.csproj" -c Release -o /app/build - - FROM build AS publish - RUN dotnet publish "ConsoleApp1.csproj" -c Release -o /app/publish - - FROM base AS final - WORKDIR /app - COPY --from=publish /app/publish . - ENTRYPOINT ["dotnet", "ConsoleApp1.dll"] - `)); - - assertNotFileContains('ConsoleApp1Folder/Dockerfile', 'EXPOSE'); - }); - - testInEmptyFolder("Linux", async () => { - await testDotNetCoreConsole( - 'Linux', - 'Linux', - '', - 'ConsoleApp1Folder', - 'ConsoleApp1.csproj', - dotNetCoreConsole_21_ProjectFileContents, - removeIndentation(` - FROM mcr.microsoft.com/dotnet/core/runtime:2.1 AS base - WORKDIR /app - - FROM mcr.microsoft.com/dotnet/core/sdk:2.1 AS build - WORKDIR /src - COPY ["ConsoleApp1Folder/ConsoleApp1.csproj", "ConsoleApp1Folder/"] - RUN dotnet restore "ConsoleApp1Folder/ConsoleApp1.csproj" - COPY . . - WORKDIR "/src/ConsoleApp1Folder" - RUN dotnet build "ConsoleApp1.csproj" -c Release -o /app/build - - FROM build AS publish - RUN dotnet publish "ConsoleApp1.csproj" -c Release -o /app/publish - - FROM base AS final - WORKDIR /app - COPY --from=publish /app/publish . - ENTRYPOINT ["dotnet", "ConsoleApp1.dll"] - `)); - - assertNotFileContains('ConsoleApp1Folder/Dockerfile', 'EXPOSE'); - }); - }); - - suite(".NET Core Console 2.0", async () => { - testInEmptyFolder("Windows", async () => { - await testDotNetCoreConsole( - 'Windows', - 'Windows', - windows10RS5, - 'subfolder/projectFolder', - 'ConsoleApp1.csproj', - dotNetCoreConsole_20_ProjectFileContents, - removeIndentation(` - #Depending on the operating system of the host machines(s) that will build or run the containers, the image specified in the FROM statement may need to be changed. - #For more information, please see https://aka.ms/containercompat - - FROM microsoft/dotnet:2.0-runtime-nanoserver-1809 AS base - WORKDIR /app - - FROM microsoft/dotnet:2.0-sdk-nanoserver-1809 AS build - WORKDIR /src - COPY ["subfolder/projectFolder/ConsoleApp1.csproj", "subfolder/projectFolder/"] - RUN dotnet restore "subfolder/projectFolder/ConsoleApp1.csproj" - COPY . . - WORKDIR "/src/subfolder/projectFolder" - RUN dotnet build "ConsoleApp1.csproj" -c Release -o /app/build - - FROM build AS publish - RUN dotnet publish "ConsoleApp1.csproj" -c Release -o /app/publish - - FROM base AS final - WORKDIR /app - COPY --from=publish /app/publish . - ENTRYPOINT ["dotnet", "ConsoleApp1.dll"] - `)); - - assertNotFileContains('subfolder/projectFolder/Dockerfile', 'EXPOSE'); - }); - - testInEmptyFolder("Linux", async () => { - await testDotNetCoreConsole( - 'Linux', - 'Linux', - '', - 'subfolder/projectFolder', - 'ConsoleApp1.csproj', - dotNetCoreConsole_20_ProjectFileContents, - removeIndentation(` - FROM microsoft/dotnet:2.0-runtime AS base - WORKDIR /app - - FROM microsoft/dotnet:2.0-sdk AS build - WORKDIR /src - COPY ["subfolder/projectFolder/ConsoleApp1.csproj", "subfolder/projectFolder/"] - RUN dotnet restore "subfolder/projectFolder/ConsoleApp1.csproj" - COPY . . - WORKDIR "/src/subfolder/projectFolder" - RUN dotnet build "ConsoleApp1.csproj" -c Release -o /app/build - - FROM build AS publish - RUN dotnet publish "ConsoleApp1.csproj" -c Release -o /app/publish - - FROM base AS final - WORKDIR /app - COPY --from=publish /app/publish . - ENTRYPOINT ["dotnet", "ConsoleApp1.dll"] - `)); - - assertNotFileContains('subfolder/projectFolder/Dockerfile', 'EXPOSE'); - }); - }); - - suite(".NET Core Console 1.1", async () => { - testInEmptyFolder("Windows", async () => { - await testDotNetCoreConsole( - 'Windows', - 'Windows', - windows10RS5, - 'subfolder/projectFolder', - 'ConsoleApp1.csproj', - dotNetCoreConsole_11_ProjectFileContents); - - 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 () => { - await testDotNetCoreConsole( - 'Linux', - 'Linux', - '', - 'subfolder/projectFolder', - 'ConsoleApp1.csproj', - dotNetCoreConsole_11_ProjectFileContents); - - 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'); - }); - }); - - suite(".NET Core Console 2.2", async () => { - testInEmptyFolder("Windows", async () => { - await testDotNetCoreConsole( - 'Windows', - 'Windows', - windows10RS5, - 'subfolder/projectFolder', - 'ConsoleApp1.csproj', - dotNetCoreConsole_22_ProjectFileContents); - - 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 () => { - await testDotNetCoreConsole( - 'Linux', - 'Linux', - '', - 'subfolder/projectFolder', - 'ConsoleApp1.csproj', - dotNetCoreConsole_22_ProjectFileContents); - - 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'); - }); - }); - - // 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( - '.NET: ASP.NET Core', - undefined, - ['Windows', 'No', TestInput.UseDefaultValue] - ); - - assertFileContains('projectFolder1/Dockerfile', 'EXPOSE 80'); - }); - - testInEmptyFolder("No port", async () => { - await writeFile('projectFolder1', 'aspnetapp.csproj', dotNetCoreConsole_21_ProjectFileContents); - await testConfigureDocker( - '.NET: ASP.NET Core', - undefined, - ['Windows', 'No', ''] - ); - - assertNotFileContains('projectFolder1/Dockerfile', 'EXPOSE'); - }); - - testInEmptyFolder("Windows 10 RS5", async () => { - await testAspNetCore( - 'Windows', - 'Windows', - windows10RS5, - 'AspNetApp1', - 'project1.csproj', - aspNet_22_ProjectFileContents, - removeIndentation(` - #Depending on the operating system of the host machines(s) that will build or run the containers, the image specified in the FROM statement may need to be changed. - #For more information, please see https://aka.ms/containercompat - - FROM mcr.microsoft.com/dotnet/core/aspnet:2.2-nanoserver-1809 AS base - WORKDIR /app - EXPOSE 1234 - - FROM mcr.microsoft.com/dotnet/core/sdk:2.2-nanoserver-1809 AS build - WORKDIR /src - COPY ["AspNetApp1/project1.csproj", "AspNetApp1/"] - RUN dotnet restore "AspNetApp1/project1.csproj" - COPY . . - WORKDIR "/src/AspNetApp1" - RUN dotnet build "project1.csproj" -c Release -o /app/build - - FROM build AS publish - RUN dotnet publish "project1.csproj" -c Release -o /app/publish - - FROM base AS final - WORKDIR /app - COPY --from=publish /app/publish . - ENTRYPOINT ["dotnet", "project1.dll"] - `)); - }); - - testInEmptyFolder("Linux", async () => { - await testAspNetCore( - 'Linux', - 'Linux', - '', - 'project2', - 'project2.csproj', - aspNet_22_ProjectFileContents, - removeIndentation(` - FROM mcr.microsoft.com/dotnet/core/aspnet:2.2 AS base - WORKDIR /app - EXPOSE 1234 - - FROM mcr.microsoft.com/dotnet/core/sdk:2.2 AS build - WORKDIR /src - COPY ["project2/project2.csproj", "project2/"] - RUN dotnet restore "project2/project2.csproj" - COPY . . - WORKDIR "/src/project2" - RUN dotnet build "project2.csproj" -c Release -o /app/build - - FROM build AS publish - RUN dotnet publish "project2.csproj" -c Release -o /app/publish - - FROM base AS final - WORKDIR /app - COPY --from=publish /app/publish . - ENTRYPOINT ["dotnet", "project2.dll"] - `)); - }); - - testInEmptyFolder("Windows 10 RS4", async () => { - await testAspNetCore( - 'Windows', - 'Windows', - windows10RS4, - 'AspNetApp1', - 'project1.csproj', - aspNet_22_ProjectFileContents); - - 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 () => { - await testAspNetCore( - 'Windows', - 'Windows', - windows10RS3, - 'AspNetApp1', - 'project1.csproj', - aspNet_22_ProjectFileContents); - - 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 () => { - await testAspNetCore( - 'Windows', - 'Windows', - windowsServer2016, - 'AspNetApp1', - 'project1.csproj', - aspNet_22_ProjectFileContents); - - 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 () => { - await testAspNetCore( - 'Windows', - 'Linux', - '', - 'AspNetApp1', - 'project1.csproj', - aspNet_22_ProjectFileContents); - - assertFileContains('AspNetApp1/Dockerfile', 'FROM mcr.microsoft.com/dotnet/core/aspnet:2.2-nanoserver-1909 AS base'); - assertFileContains('AspNetApp1/Dockerfile', 'FROM mcr.microsoft.com/dotnet/core/sdk:2.2-nanoserver-1909 AS build'); - }); - }); - - suite("ASP.NET Core 1.1", async () => { - testInEmptyFolder("Windows", async () => { - await testAspNetCore( - 'Windows', - 'Windows', - windows10RS5, - 'AspNetApp1', - 'project1.csproj', - aspNet_10_ProjectFileContents); - - assertFileContains('AspNetApp1/Dockerfile', 'FROM microsoft/aspnetcore:1.1 AS base'); - assertFileContains('AspNetApp1/Dockerfile', 'FROM microsoft/aspnetcore-build:1.1 AS build'); - }); - }); - - suite("ASP.NET Core 2.0", async () => { - testInEmptyFolder("Linux", async () => { - await testAspNetCore( - 'Linux', - 'Linux', - '', - 'project2', - 'project2.csproj', - aspNet_20_ProjectFileContents); - - assertFileContains('project2/Dockerfile', 'FROM microsoft/aspnetcore:2.0 AS base'); - assertFileContains('project2/Dockerfile', 'FROM microsoft/aspnetcore-build:2.0 AS build'); - }); - }); - - - // Java - - suite("Java", () => { - if (os.platform() === 'win32') { - // Skip tests that are faulty on Windows - // TODO: re-enable? - return; - } - - 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, - [TestInput.UseDefaultValue], - ['Dockerfile', 'docker-compose.debug.yml', 'docker-compose.yml', '.dockerignore'] - ); - - assertFileContains('Dockerfile', 'EXPOSE 3000'); - }); - - testInEmptyFolder("No pom file", async () => { - await testConfigureDocker( - 'Java', - { - configurePlatform: 'Java', - configureOs: undefined, - packageFileType: undefined, - packageFileSubfolderDepth: undefined, - }, - ['1234'], - ['Dockerfile', 'docker-compose.debug.yml', 'docker-compose.yml', '.dockerignore'] - ); - - assertFileContains('Dockerfile', 'EXPOSE 1234'); - assertFileContains('Dockerfile', 'ARG JAVA_OPTS'); - assertFileContains('Dockerfile', 'ADD testoutput.jar testoutput.jar'); - assertFileContains('Dockerfile', 'ENTRYPOINT exec java $JAVA_OPTS -jar testoutput.jar'); - }); - - testInEmptyFolder("Empty pom file", async () => { - await writeFile('', 'pom.xml', ` - - `); - - await testConfigureDocker( - 'Java', - { - configurePlatform: 'Java', - configureOs: undefined, - packageFileType: 'pom.xml', - packageFileSubfolderDepth: '0', - }, - [TestInput.UseDefaultValue /*port*/], - ['pom.xml', 'Dockerfile', 'docker-compose.debug.yml', 'docker-compose.yml', '.dockerignore'] - ); - - assertFileContains('Dockerfile', 'EXPOSE 3000'); - assertFileContains('Dockerfile', 'ARG JAVA_OPTS'); - assertFileContains('Dockerfile', 'ADD testoutput.jar testoutput.jar'); - assertFileContains('Dockerfile', 'ENTRYPOINT exec java $JAVA_OPTS -jar testoutput.jar'); - }); - - testInEmptyFolder("Pom file", async () => { - await writeFile('', 'pom.xml', ` - - - 4.0.0 - - com.microsoft.azure - app-artifact-id - 1.0-SNAPSHOT - jar - - app-on-azure - Test - - `); - - await testConfigureDocker( - 'Java', - { - configurePlatform: 'Java', - configureOs: undefined, - packageFileType: 'pom.xml', - packageFileSubfolderDepth: '0', - }, - [TestInput.UseDefaultValue /*port*/], - ['pom.xml', 'Dockerfile', 'docker-compose.debug.yml', 'docker-compose.yml', '.dockerignore']); - - assertFileContains('Dockerfile', 'EXPOSE 3000'); - assertFileContains('Dockerfile', 'ARG JAVA_OPTS'); - assertFileContains('Dockerfile', 'ADD target/app-artifact-id-1.0-SNAPSHOT.jar testoutput.jar'); - assertFileContains('Dockerfile', 'ENTRYPOINT exec java $JAVA_OPTS -jar testoutput.jar'); - }); - - testInEmptyFolder("Empty gradle file - defaults", async () => { - // https://github.com/dotnet/dotnet-docker/tree/master/samples/aspnetapp - await writeFile('', 'build.gradle', ``); - - await testConfigureDocker('Java', - { - configurePlatform: 'Java', - configureOs: undefined, - packageFileType: 'build.gradle', - packageFileSubfolderDepth: '0', - }, - [TestInput.UseDefaultValue /*port*/], - ['build.gradle', 'Dockerfile', 'docker-compose.debug.yml', 'docker-compose.yml', '.dockerignore'] - ); - - assertFileContains('Dockerfile', 'EXPOSE 3000'); - assertFileContains('Dockerfile', 'ARG JAVA_OPTS'); - assertFileContains('Dockerfile', 'ADD build/libs/testOutput-0.0.1.jar testoutput.jar'); - assertFileContains('Dockerfile', 'ENTRYPOINT exec java $JAVA_OPTS -jar testoutput.jar'); - }); - - testInEmptyFolder("Gradle with jar", async () => { - // https://github.com/dotnet/dotnet-docker/tree/master/samples/aspnetapp - await writeFile('', 'build.gradle', gradleWithJarContents); - - await testConfigureDocker( - 'Java', - { - configurePlatform: 'Java', - configureOs: undefined, - packageFileType: 'build.gradle', - packageFileSubfolderDepth: '0', - }, - [TestInput.UseDefaultValue /*port*/], - ['build.gradle', 'Dockerfile', 'docker-compose.debug.yml', 'docker-compose.yml', '.dockerignore'] - ); - - assertFileContains('Dockerfile', 'EXPOSE 3000'); - assertFileContains('Dockerfile', 'ARG JAVA_OPTS'); - assertFileContains('Dockerfile', 'ADD build/libs/testOutput-1.2.3.jar testoutput.jar'); - assertFileContains('Dockerfile', 'ENTRYPOINT exec java $JAVA_OPTS -jar testoutput.jar'); - }); - - }); - // Python - - suite("Python", () => { - if (os.platform() === 'win32') { - // Skip tests that are faulty on Windows - // TODO: re-enable? - return; - } - - testInEmptyFolder("Python: General", async () => { - await testConfigureDocker( - 'Python: General', - { - configurePlatform: 'Python: General', - configureOs: undefined, - packageFileType: undefined, - packageFileSubfolderDepth: undefined - }, - ['No', 'app.py'], - ['Dockerfile', '.dockerignore', 'requirements.txt'] - ); - - assertFileContains('Dockerfile', 'FROM python'); - assertFileContains('Dockerfile', 'ADD requirements.txt .'); - assertFileContains('Dockerfile', 'RUN python -m pip install -r requirements.txt'); - assertFileContains('Dockerfile', 'CMD ["python", "app.py"]'); - }); - }); - - suite("Python", () => { - testInEmptyFolder("Python: Django", async () => { - await testConfigureDocker( - 'Python: Django', - { - configurePlatform: 'Python: Django', - configureOs: undefined, - packageFileType: undefined, - packageFileSubfolderDepth: undefined - }, - ['No', 'manage.py', '8000'], - ['Dockerfile', '.dockerignore', 'requirements.txt'] - ); - - assertFileContains('Dockerfile', 'FROM python'); - assertFileContains('Dockerfile', 'EXPOSE 8000'); - assertFileContains('Dockerfile', 'ADD requirements.txt .'); - assertFileContains('Dockerfile', 'RUN python -m pip install -r requirements.txt'); - assertFileContains('Dockerfile', 'CMD ["gunicorn", "--bind", "0.0.0.0:8000", "pythonPath.to.wsgi"]'); - assertFileContains('requirements.txt', 'django'); - assertFileContains('requirements.txt', 'gunicorn'); - }); - }); - - suite("Python", () => { - testInEmptyFolder("Python: Flask", async () => { - await testConfigureDocker( - 'Python: Flask', - { - configurePlatform: 'Python: Flask', - configureOs: undefined, - packageFileType: undefined, - packageFileSubfolderDepth: undefined - }, - ['No', 'flask_app.py', '5000'], - ['Dockerfile', '.dockerignore', 'requirements.txt'] - ); - - assertFileContains('Dockerfile', 'FROM python'); - assertFileContains('Dockerfile', 'EXPOSE 5000'); - assertFileContains('Dockerfile', 'ADD requirements.txt .'); - assertFileContains('Dockerfile', 'RUN python -m pip install -r requirements.txt'); - assertFileContains('Dockerfile', 'CMD ["gunicorn", "--bind", "0.0.0.0:5000", "flask_app:app"]'); - assertFileContains('requirements.txt', 'flask'); - assertFileContains('requirements.txt', 'gunicorn'); - }); - }); - - // Ruby - - suite("Ruby", () => { - testInEmptyFolder("Ruby, empty folder", async () => { - await testConfigureDocker( - 'Ruby', - { - configurePlatform: 'Ruby', - configureOs: undefined, - packageFileType: undefined, - packageFileSubfolderDepth: undefined - }, - [TestInput.UseDefaultValue /*port*/], - ['Dockerfile', 'docker-compose.debug.yml', 'docker-compose.yml', '.dockerignore']); - - assertFileContains('Dockerfile', 'FROM ruby:2.5-slim'); - assertFileContains('Dockerfile', 'LABEL Name=testoutput Version=0.0.1'); - assertFileContains('Dockerfile', 'COPY Gemfile Gemfile.lock ./'); - assertFileContains('Dockerfile', 'RUN bundle install'); - assertFileContains('Dockerfile', 'CMD ["ruby", "testoutput.rb"]'); - }); - }); - - // C++ - - suite("C++", () => { - testInEmptyFolder("C++", async () => { - await testConfigureDocker( - 'C++', - { - configurePlatform: 'C++', - configureOs: undefined, - packageFileType: undefined, - packageFileSubfolderDepth: undefined - }); - - assertFileContains('Dockerfile', 'FROM gcc:latest'); - assertFileContains('Dockerfile', 'COPY . /usr/src/myapp'); - assertFileContains('Dockerfile', 'WORKDIR /usr/src/myapp'); - assertFileContains('Dockerfile', 'RUN g++ -o myapp main.cpp'); - assertFileContains('Dockerfile', 'CMD ["./myapp"]'); - assertNotFileContains('Dockerfile', 'EXPOSE'); - }); - }); - - 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: 'package.json', - packageFileSubfolderDepth: '0' - }, - [TestInput.UseDefaultValue /*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 ["sh", "-c", "/usr/games/fortune -a | cowsay"] - `)); - assert.strictEqual(composeContents, removeIndentation(` - version: '3.4' - - services: - testoutput: - image: testoutput - build: . - ports: - - 3000`)); - assert.strictEqual(debugComposeContents, removeIndentation(` - version: '3.4' - - services: - testoutput: - image: testoutput - build: - context: . - dockerfile: Dockerfile - ports: - - 3000`)); - }); - }); - - // API (vscode-docker.api.configure) - - suite("API", () => { - suite("Partially-specified options", async () => { - testInEmptyFolder("Telemetry properties are set correctly", async () => { - await testConfigureDockerViaApi( - { - rootPath: testRootFolder, - outputFolder: testRootFolder, - platform: 'Ruby', - ports: [234] - } - ); - }); - - testInEmptyFolder("Only platform specified, others come from user", async () => { - await testConfigureDockerViaApi( - { - rootPath: testRootFolder, - outputFolder: testRootFolder, - platform: 'Ruby' - }, - ["555"], // port - ['Dockerfile', 'docker-compose.debug.yml', 'docker-compose.yml', '.dockerignore'] - ); - assertFileContains('Dockerfile', 'EXPOSE 555'); - }); - - testInEmptyFolder("Only platform/OS specified, others come from user", async () => { - await writeFile('projectFolder1', 'aspnetapp.csproj', dotNetCoreConsole_21_ProjectFileContents); - await writeFile('projectFolder2', 'aspnetapp.csproj', dotNetCoreConsole_21_ProjectFileContents); - - await testConfigureDockerViaApi( - { - rootPath: testRootFolder, - outputFolder: testRootFolder, - platform: '.NET: Core Console', - os: "Linux" - }, - [ - 'No', - '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 () => { - await testConfigureDockerViaApi( - { - rootPath: testRootFolder, - outputFolder: testRootFolder, - ports: [444] - }, - ["Ruby"], - ['Dockerfile', 'docker-compose.debug.yml', 'docker-compose.yml', '.dockerignore'] - ); - assertFileContains('Dockerfile', 'EXPOSE 444'); - }); - - suite("Requirements from IoT team", async () => { - // We will be passed a directory path which will be the service folder. The dockerFile needs to be generated at this location. This holds true for all language types. - // The csproj might be present in this folder or a sub directory or none at all (if the app is not C# type). - // We will not be passed the csproj location. The language-type prompts will be presented by the plugin like they appear today. Platform will not be passed in. All the language specific processing should happen within this plugin. - // Will pass in: Port number, Operating System, folder path where the dockerFile should be created, service name if desired. - // The service folder will only have 0 or 1 csproj within them. So even though there are multiple service directories within the root, we will only be passed 1 service directory at a time, so that only 1 dockerFile generation happens at a time. - // So the command which is "Add DockerFile to this Workspace" now extends to "Add DockerFile to the directory", and we would do all the searching and processing only within the passed directory path. - - testInEmptyFolder("All files in service folder, output to service folder", async () => { - let rootFolder = 'serviceFolder'; - await writeFile(rootFolder, 'somefile1.cs', "// Some file"); - await writeFile(rootFolder, 'aspnetapp.csproj', dotNetCoreConsole_21_ProjectFileContents); - - await testConfigureDockerViaApi( - { - rootPath: path.join(testRootFolder, 'serviceFolder'), - outputFolder: path.join(testRootFolder, 'serviceFolder'), - os: "Linux", - ports: [1234] - }, - ['.NET: Core Console', 'No'], - ['serviceFolder/Dockerfile', 'serviceFolder/.dockerignore', 'serviceFolder/somefile1.cs', 'serviceFolder/aspnetapp.csproj'] - ); - assertFileContains('serviceFolder/Dockerfile', 'ENTRYPOINT ["dotnet", "aspnetapp.dll"]'); - }); - - - testInEmptyFolder(".csproj file in subfolder, output to service folder", async () => { - let rootFolder = 'serviceFolder'; - await writeFile(path.join(rootFolder, 'subfolder1'), 'somefile1.cs', "// Some file"); - await writeFile(path.join(rootFolder, 'subfolder1'), 'aspnetapp.csproj', dotNetCoreConsole_21_ProjectFileContents); - - await testConfigureDockerViaApi( - { - rootPath: path.join(testRootFolder, 'serviceFolder'), - outputFolder: path.join(testRootFolder, 'serviceFolder'), - os: "Windows", - ports: [1234] - }, - ['.NET: Core Console', 'No'], - ['serviceFolder/Dockerfile', 'serviceFolder/.dockerignore', 'serviceFolder/subfolder1/somefile1.cs', 'serviceFolder/subfolder1/aspnetapp.csproj'] - ); - assertFileContains('serviceFolder/Dockerfile', 'ENTRYPOINT ["dotnet", "aspnetapp.dll"]'); - }); - - testInEmptyFolder(".csproj file in subfolder, output to subfolder", async () => { - let rootFolder = 'serviceFolder'; - await writeFile(path.join(rootFolder, 'subfolder1'), 'somefile1.cs', "// Some file"); - await writeFile(path.join(rootFolder, 'subfolder1'), 'aspnetapp.csproj', aspNet_21_ProjectFileContents); - - await testConfigureDockerViaApi( - { - rootPath: path.join(testRootFolder, 'serviceFolder'), - outputFolder: path.join(testRootFolder, 'serviceFolder', 'subfolder1'), - os: "Windows", - ports: [1234, 5678] - }, - ['.NET: ASP.NET Core', 'No'], ['serviceFolder/subfolder1/Dockerfile', 'serviceFolder/subfolder1/.dockerignore', 'serviceFolder/subfolder1/somefile1.cs', 'serviceFolder/subfolder1/aspnetapp.csproj'] - ); - assertFileContains('serviceFolder/subfolder1/Dockerfile', 'ENTRYPOINT ["dotnet", "aspnetapp.dll"]'); - }); - }); - }); - }); -}); diff --git a/test/configureWorkspace/configUtils.test.ts b/test/configureWorkspace/configUtils.test.ts deleted file mode 100644 index 2f8a3b7b54..0000000000 --- a/test/configureWorkspace/configUtils.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { splitPorts } from "../../extension.bundle"; -import * as assert from 'assert'; - -suite('(unit) configureWorkspace/configUtils', () => { - suite('splitPorts', () => { - function genTest(s: string, expected: number[]): void { - test(`${String(s)}`, () => { - let s2 = splitPorts(s); - assert.deepEqual(s2, expected); - }); - } - - genTest('', []); - genTest('-1', undefined); - genTest('1', [1]); - genTest('80', [80]); - genTest('65535', [65535]); - genTest('65536', undefined); - - genTest('80,81', [80, 81]); - genTest('80, 81', [80, 81]); - genTest('80;81', undefined); - genTest('80,81;82', undefined); - - genTest('3;;\'[\'\']?><', undefined); - genTest('abc', undefined); - genTest('80,abc', undefined); - }); -}); diff --git a/test/windowsVersion.test.ts b/test/windowsVersion.test.ts deleted file mode 100644 index 94f124742f..0000000000 --- a/test/windowsVersion.test.ts +++ /dev/null @@ -1,122 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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 { ext, isWindows10RS3OrNewer, isWindows10RS4OrNewer, isWindows10RS5OrNewer, isWindows1019H1OrNewer } from '../extension.bundle'; - -suite("(unit) windowsVersion", () => { - function testIsWindows1019H1OrNewer(release: string, expectedResult: boolean): void { - test(`isWindows1019H1OrNewer: ${release}`, () => { - let previousOs = ext.os; - try { - ext.os = { - platform: 'win32', - release - }; - - let result = isWindows1019H1OrNewer(); - assert.equal(result, expectedResult); - } finally { - ext.os = previousOs; - } - }); - } - - function testIsWindows10RS5OrNewer(release: string, expectedResult: boolean): void { - test(`isWindows10RS5OrNewer: ${release}`, () => { - let previousOs = ext.os; - try { - ext.os = { - platform: 'win32', - release - }; - - let result = isWindows10RS5OrNewer(); - assert.equal(result, expectedResult); - } finally { - ext.os = previousOs; - } - }); - } - - function testIsWindows10RS4OrNewer(release: string, expectedResult: boolean): void { - test(`isWindows10RS4OrNewer: ${release}`, () => { - let previousOs = ext.os; - try { - ext.os = { - platform: 'win32', - release - }; - - let result = isWindows10RS4OrNewer(); - assert.equal(result, expectedResult); - } finally { - ext.os = previousOs; - } - }); - } - - function testIsWindows10RS3OrNewer(release: string, expectedResult: boolean): void { - test(`isWindows10RS4OrNewer: ${release}`, () => { - let previousOs = ext.os; - try { - ext.os = { - platform: 'win32', - release - }; - - let result = isWindows10RS3OrNewer(); - assert.equal(result, expectedResult); - } finally { - ext.os = previousOs; - } - }); - } - - suite('isWindows1019H1OrNewer', () => { - testIsWindows1019H1OrNewer('10.0.18362', true); - testIsWindows1019H1OrNewer('10.0.18363', true); - testIsWindows1019H1OrNewer('10.0.18361', false); - testIsWindows1019H1OrNewer('9.9.18363', false); - testIsWindows1019H1OrNewer('10.1.0', true); - testIsWindows1019H1OrNewer('11.1.0', true); - - testIsWindows1019H1OrNewer('10.0.14393', false); // Windows Server 2016 - testIsWindows1019H1OrNewer('10.0.18362', true); // Windows 10 Version 1809 (build 17763) - }); - - suite('isWindows10RS5OrNewer', () => { - testIsWindows10RS5OrNewer('10.0.17763', true); - testIsWindows10RS5OrNewer('10.0.17764', true); - testIsWindows10RS5OrNewer('10.0.17762', false); - testIsWindows10RS5OrNewer('9.9.17764', false); - testIsWindows10RS5OrNewer('10.1.0', true); - testIsWindows10RS5OrNewer('11.1.0', true); - - testIsWindows10RS5OrNewer('10.0.14393', false); // Windows Server 2016 - testIsWindows10RS5OrNewer('10.0.17763', true); // Windows 10 Version 1809 (build 17763) - }); - - suite('isWindows10RS4OrNewer', () => { - testIsWindows10RS4OrNewer('10.0.17134', true); - testIsWindows10RS4OrNewer('10.0.17135', true); - testIsWindows10RS4OrNewer('10.0.17133', false); - testIsWindows10RS4OrNewer('9.9.17135', false); - testIsWindows10RS4OrNewer('10.1.0', true); - testIsWindows10RS4OrNewer('11.1.0', true); - - testIsWindows10RS4OrNewer('10.0.14393', false); // Windows Server 2016 - testIsWindows10RS4OrNewer('10.0.17134', true); // Windows 10 Version 1803 (build 17134.285) - }); - - suite('isWindows10RS3OrNewer', () => { - testIsWindows10RS3OrNewer('10.0.16299', true); - testIsWindows10RS3OrNewer('10.0.16300', true); - testIsWindows10RS3OrNewer('10.0.16298', false); - - testIsWindows10RS3OrNewer('10.0.14393', false); // Windows Server 2016 - testIsWindows10RS3OrNewer('10.0.17134', true); // Windows 10 Version 1803 (build 17134.285) - }); -});