diff --git a/package-lock.json b/package-lock.json index 751d80d84..df974e20d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@azure/arm-resources": "^4.2.2", "@azure/container-registry": "1.0.0-beta.5", "@azure/ms-rest-js": "^2.2.1", + "@azure/storage-blob": "^12.4.1", "@microsoft/vscode-azext-azureutils": "^0.3.7", "@microsoft/vscode-azext-utils": "^0.5.1", "@microsoft/vscode-azureresources-api": "^2.0.2", @@ -22,7 +23,8 @@ "dotenv": "^16.0.0", "open": "^8.0.4", "semver": "^7.3.5", - "vscode-nls": "^4.1.1" + "vscode-nls": "^4.1.1", + "vscode-uri": "^3.0.2" }, "devDependencies": { "@microsoft/eslint-config-azuretools": "^0.1.0", @@ -322,6 +324,35 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" }, + "node_modules/@azure/core-http": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@azure/core-http/-/core-http-3.0.0.tgz", + "integrity": "sha512-BxI2SlGFPPz6J1XyZNIVUf0QZLBKFX+ViFjKOkzqD18J1zOINIQ8JSBKKr+i+v8+MB6LacL6Nn/sP/TE13+s2Q==", + "dependencies": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-auth": "^1.3.0", + "@azure/core-tracing": "1.0.0-preview.13", + "@azure/core-util": "^1.1.1", + "@azure/logger": "^1.0.0", + "@types/node-fetch": "^2.5.0", + "@types/tunnel": "^0.0.3", + "form-data": "^4.0.0", + "node-fetch": "^2.6.7", + "process": "^0.11.10", + "tslib": "^2.2.0", + "tunnel": "^0.0.6", + "uuid": "^8.3.0", + "xml2js": "^0.4.19" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@azure/core-http/node_modules/tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + }, "node_modules/@azure/core-lro": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.2.3.tgz", @@ -412,14 +443,15 @@ "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" }, "node_modules/@azure/core-util": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.0.0.tgz", - "integrity": "sha512-yWshY9cdPthlebnb3Zuz/j0Lv4kjU6u7PR5sW7A9FF7EX+0irMRJAtyTq5TPiDHJfjH8gTSlnIYFj9m7Ed76IQ==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.2.0.tgz", + "integrity": "sha512-ffGIw+Qs8bNKNLxz5UPkz4/VBM/EZY07mPve1ZYFqYUdPwFqRj0RPk0U7LZMOfT7GCck9YjuT1Rfp1PApNl1ng==", "dependencies": { + "@azure/abort-controller": "^1.0.0", "tslib": "^2.2.0" }, "engines": { - "node": ">=12.0.0" + "node": ">=14.0.0" } }, "node_modules/@azure/core-util/node_modules/tslib": { @@ -499,6 +531,29 @@ "adal-node": "^0.2.2" } }, + "node_modules/@azure/storage-blob": { + "version": "12.13.0", + "resolved": "https://registry.npmjs.org/@azure/storage-blob/-/storage-blob-12.13.0.tgz", + "integrity": "sha512-t3Q2lvBMJucgTjQcP5+hvEJMAsJSk0qmAnjDLie2td017IiduZbbC9BOcFfmwzR6y6cJdZOuewLCNFmEx9IrXA==", + "dependencies": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-http": "^3.0.0", + "@azure/core-lro": "^2.2.0", + "@azure/core-paging": "^1.1.1", + "@azure/core-tracing": "1.0.0-preview.13", + "@azure/logger": "^1.0.0", + "events": "^3.0.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@azure/storage-blob/node_modules/tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + }, "node_modules/@babel/code-frame": { "version": "7.12.11", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", @@ -1383,8 +1438,29 @@ "node_modules/@types/node": { "version": "14.17.33", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.33.tgz", - "integrity": "sha512-noEeJ06zbn3lOh4gqe2v7NMGS33jrulfNqYFDjjEbhpDEHR5VTxgYNQSBqBlJIsBJW3uEYDgD6kvMnrrhGzq8g==", - "dev": true + "integrity": "sha512-noEeJ06zbn3lOh4gqe2v7NMGS33jrulfNqYFDjjEbhpDEHR5VTxgYNQSBqBlJIsBJW3uEYDgD6kvMnrrhGzq8g==" + }, + "node_modules/@types/node-fetch": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.2.tgz", + "integrity": "sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==", + "dependencies": { + "@types/node": "*", + "form-data": "^3.0.0" + } + }, + "node_modules/@types/node-fetch/node_modules/form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } }, "node_modules/@types/semver": { "version": "7.3.9", @@ -1398,6 +1474,14 @@ "integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==", "dev": true }, + "node_modules/@types/tunnel": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/tunnel/-/tunnel-0.0.3.tgz", + "integrity": "sha512-sOUTGn6h1SfQ+gbgqC364jLFBw2lnFqkgF3q0WovEHRLMrVD1sd5aufqi/aJObLekJO+Aq5z646U4Oxy6shXMA==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/uglify-js": { "version": "3.13.1", "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.13.1.tgz", @@ -4394,7 +4478,6 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true, "engines": { "node": ">=0.8.x" } @@ -8885,6 +8968,14 @@ "node": ">= 0.8" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -11371,6 +11462,11 @@ "vscode": "^1.19.1" } }, + "node_modules/vscode-uri": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.7.tgz", + "integrity": "sha512-eOpPHogvorZRobNqJGhapa0JdwaxpjVvyBp0QIUMRMSf8ZAlqOdEquKuRmw9Qwu0qXtJIWqFtMkmvJjUZmMjVA==" + }, "node_modules/watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", @@ -12107,6 +12203,34 @@ } } }, + "@azure/core-http": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@azure/core-http/-/core-http-3.0.0.tgz", + "integrity": "sha512-BxI2SlGFPPz6J1XyZNIVUf0QZLBKFX+ViFjKOkzqD18J1zOINIQ8JSBKKr+i+v8+MB6LacL6Nn/sP/TE13+s2Q==", + "requires": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-auth": "^1.3.0", + "@azure/core-tracing": "1.0.0-preview.13", + "@azure/core-util": "^1.1.1", + "@azure/logger": "^1.0.0", + "@types/node-fetch": "^2.5.0", + "@types/tunnel": "^0.0.3", + "form-data": "^4.0.0", + "node-fetch": "^2.6.7", + "process": "^0.11.10", + "tslib": "^2.2.0", + "tunnel": "^0.0.6", + "uuid": "^8.3.0", + "xml2js": "^0.4.19" + }, + "dependencies": { + "tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + } + } + }, "@azure/core-lro": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.2.3.tgz", @@ -12190,10 +12314,11 @@ } }, "@azure/core-util": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.0.0.tgz", - "integrity": "sha512-yWshY9cdPthlebnb3Zuz/j0Lv4kjU6u7PR5sW7A9FF7EX+0irMRJAtyTq5TPiDHJfjH8gTSlnIYFj9m7Ed76IQ==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.2.0.tgz", + "integrity": "sha512-ffGIw+Qs8bNKNLxz5UPkz4/VBM/EZY07mPve1ZYFqYUdPwFqRj0RPk0U7LZMOfT7GCck9YjuT1Rfp1PApNl1ng==", "requires": { + "@azure/abort-controller": "^1.0.0", "tslib": "^2.2.0" }, "dependencies": { @@ -12274,6 +12399,28 @@ "adal-node": "^0.2.2" } }, + "@azure/storage-blob": { + "version": "12.13.0", + "resolved": "https://registry.npmjs.org/@azure/storage-blob/-/storage-blob-12.13.0.tgz", + "integrity": "sha512-t3Q2lvBMJucgTjQcP5+hvEJMAsJSk0qmAnjDLie2td017IiduZbbC9BOcFfmwzR6y6cJdZOuewLCNFmEx9IrXA==", + "requires": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-http": "^3.0.0", + "@azure/core-lro": "^2.2.0", + "@azure/core-paging": "^1.1.1", + "@azure/core-tracing": "1.0.0-preview.13", + "@azure/logger": "^1.0.0", + "events": "^3.0.0", + "tslib": "^2.2.0" + }, + "dependencies": { + "tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + } + } + }, "@babel/code-frame": { "version": "7.12.11", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", @@ -13053,8 +13200,28 @@ "@types/node": { "version": "14.17.33", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.33.tgz", - "integrity": "sha512-noEeJ06zbn3lOh4gqe2v7NMGS33jrulfNqYFDjjEbhpDEHR5VTxgYNQSBqBlJIsBJW3uEYDgD6kvMnrrhGzq8g==", - "dev": true + "integrity": "sha512-noEeJ06zbn3lOh4gqe2v7NMGS33jrulfNqYFDjjEbhpDEHR5VTxgYNQSBqBlJIsBJW3uEYDgD6kvMnrrhGzq8g==" + }, + "@types/node-fetch": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.2.tgz", + "integrity": "sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==", + "requires": { + "@types/node": "*", + "form-data": "^3.0.0" + }, + "dependencies": { + "form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + } + } }, "@types/semver": { "version": "7.3.9", @@ -13068,6 +13235,14 @@ "integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==", "dev": true }, + "@types/tunnel": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/tunnel/-/tunnel-0.0.3.tgz", + "integrity": "sha512-sOUTGn6h1SfQ+gbgqC364jLFBw2lnFqkgF3q0WovEHRLMrVD1sd5aufqi/aJObLekJO+Aq5z646U4Oxy6shXMA==", + "requires": { + "@types/node": "*" + } + }, "@types/uglify-js": { "version": "3.13.1", "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.13.1.tgz", @@ -15405,8 +15580,7 @@ "events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" }, "execa": { "version": "5.1.1", @@ -18875,6 +19049,11 @@ "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=", "dev": true }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==" + }, "process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -20826,6 +21005,11 @@ "tas-client": "0.1.45" } }, + "vscode-uri": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.7.tgz", + "integrity": "sha512-eOpPHogvorZRobNqJGhapa0JdwaxpjVvyBp0QIUMRMSf8ZAlqOdEquKuRmw9Qwu0qXtJIWqFtMkmvJjUZmMjVA==" + }, "watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", diff --git a/package.json b/package.json index 5170fc59c..16c64e7d3 100644 --- a/package.json +++ b/package.json @@ -330,6 +330,7 @@ "dependencies": { "@azure/arm-appcontainers": "^1.1.0", "@azure/arm-containerregistry": "^10.0.0", + "@azure/storage-blob": "^12.4.1", "@azure/arm-operationalinsights": "^8.0.0", "@azure/arm-resources": "^4.2.2", "@azure/container-registry": "1.0.0-beta.5", @@ -341,7 +342,8 @@ "dotenv": "^16.0.0", "open": "^8.0.4", "semver": "^7.3.5", - "vscode-nls": "^4.1.1" + "vscode-nls": "^4.1.1", + "vscode-uri": "^3.0.2" }, "extensionDependencies": [ "ms-vscode.azure-account", diff --git a/src/commands/deploy/IDeployBaseContext.ts b/src/commands/deploy/IDeployBaseContext.ts index 184a5ef04..346a3757b 100644 --- a/src/commands/deploy/IDeployBaseContext.ts +++ b/src/commands/deploy/IDeployBaseContext.ts @@ -6,7 +6,7 @@ import type { EnvironmentVar, RegistryCredentials, Secret } from "@azure/arm-appcontainers"; import { ISubscriptionActionContext } from "@microsoft/vscode-azext-utils"; import { AzureSubscription } from "@microsoft/vscode-azureresources-api"; -import { ImageSourceValues } from "../../constants"; +import { ImageSource, ImageSourceValues } from "../../constants"; import { ContainerAppModel } from "../../tree/ContainerAppItem"; export interface IDeployBaseContext extends ISubscriptionActionContext { @@ -14,6 +14,7 @@ export interface IDeployBaseContext extends ISubscriptionActionContext { targetContainer?: ContainerAppModel; imageSource?: ImageSourceValues; + buildType?: ImageSource.LocalDockerBuild | ImageSource.RemoteAcrBuild; showQuickStartImage?: boolean; // Base image attributes used as a precursor for either creating or updating a container app diff --git a/src/commands/deploy/ImageSourceListStep.ts b/src/commands/deploy/ImageSourceListStep.ts index ff17e94d5..c48206598 100644 --- a/src/commands/deploy/ImageSourceListStep.ts +++ b/src/commands/deploy/ImageSourceListStep.ts @@ -9,6 +9,7 @@ import { localize } from "../../utils/localize"; import { setQuickStartImage } from "../createContainerApp/setQuickStartImage"; import { EnvironmentVariablesListStep } from "./EnvironmentVariablesListStep"; import { IDeployBaseContext } from "./IDeployBaseContext"; +import { BuildFromProjectListStep } from "./buildImageInAzure/BuildFromProjectListStep"; import { ContainerRegistryListStep } from "./deployFromRegistry/ContainerRegistryListStep"; import { DeployFromRegistryConfigureStep } from "./deployFromRegistry/DeployFromRegistryConfigureStep"; @@ -17,13 +18,13 @@ export class ImageSourceListStep extends AzureWizardPromptStep[] = [ { label: imageSourceLabels[0], data: ImageSource.ExternalRegistry, suppressPersistence: true }, - // { label: imageSourceLabels[2], data: undefined, suppressPersistence: true }, + { label: imageSourceLabels[2], data: ImageSource.RemoteAcrBuild, suppressPersistence: true }, ]; if (context.showQuickStartImage) { @@ -49,6 +50,10 @@ export class ImageSourceListStep extends AzureWizardPromptStep { + public async prompt(context: IDeployBaseContext): Promise { + const placeHolder: string = localize('buildType', 'Select how you want to build your project'); + const picks: IAzureQuickPickItem[] = [ + { label: buildFromProjectLabels[0], data: ImageSource.RemoteAcrBuild, suppressPersistence: true }, + //{ label: buildFromProjectLabels[1], data: ImageSource.LocalDockerBuild, suppressPersistence: true } + ]; + + context.buildType = (await context.ui.showQuickPick(picks, { placeHolder })).data; + } + + public async configureBeforePrompt(context: IDeployBaseContext): Promise { + if (buildFromProjectLabels.length === 1) { + context.buildType = ImageSource.RemoteAcrBuild; + } + } + + public shouldPrompt(context: IDeployBaseContext): boolean { + return !context.buildType; + } + + public async getSubWizard(context: IBuildImageInAzureContext): Promise | undefined> { + const promptSteps: AzureWizardPromptStep[] = []; + const executeSteps: AzureWizardExecuteStep[] = []; + + switch (context.buildType) { + case ImageSource.RemoteAcrBuild: + promptSteps.push(new AcrListStep(), new RootFolderStep(), new DockerFileItemStep(), new ImageNameStep(), new OSPickStep()); + executeSteps.push(new TarFileStep(), new UploadSourceCodeStep(), new RunStep(), new BuildImageStep()); + break; + //TODO: case for 'Build from project locally using Docker' + } + return { promptSteps, executeSteps }; + } +} diff --git a/src/commands/deploy/buildImageInAzure/BuildImageStep.ts b/src/commands/deploy/buildImageInAzure/BuildImageStep.ts new file mode 100644 index 000000000..d9b1df84d --- /dev/null +++ b/src/commands/deploy/buildImageInAzure/BuildImageStep.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 { AzureWizardExecuteStep } from "@microsoft/vscode-azext-utils"; +import { acrDomain } from "../../../constants"; +import { localize } from "../../../utils/localize"; +import { IBuildImageInAzureContext } from "./IBuildImageInAzureContext"; +import { buildImageInAzure } from "./buildImageInAzure"; + +export class BuildImageStep extends AzureWizardExecuteStep { + public priority: number = 225; + + public async execute(context: IBuildImageInAzureContext): Promise { + context.registryDomain = acrDomain; + + const run = await buildImageInAzure(context); + const outputImages = run?.outputImages; + context.telemetry.properties.outputImages = outputImages?.length?.toString(); + + if (outputImages) { + const image = outputImages[0]; + context.image = `${image.registry}/${image.repository}:${image.tag}`; + } else { + throw new Error(localize('noImagesBuilt', 'Failed to build image.')); + } + } + + public shouldExecute(context: IBuildImageInAzureContext): boolean { + return !context.image; + } +} diff --git a/src/commands/deploy/buildImageInAzure/DockerFileItemStep.ts b/src/commands/deploy/buildImageInAzure/DockerFileItemStep.ts new file mode 100644 index 000000000..8eb956e86 --- /dev/null +++ b/src/commands/deploy/buildImageInAzure/DockerFileItemStep.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 } from '@microsoft/vscode-azext-utils'; +import { DOCKERFILE_GLOB_PATTERN } from "../../../constants"; +import { localize } from '../../../utils/localize'; +import { selectWorkspaceFile } from "../../../utils/workspaceUtils"; +import { IBuildImageInAzureContext } from "./IBuildImageInAzureContext"; + +export class DockerFileItemStep extends AzureWizardPromptStep { + public async prompt(context: IBuildImageInAzureContext): Promise { + context.dockerFilePath = await selectWorkspaceFile(context, localize('dockerFilePick', 'Select a Dockerfile'), { filters: { 'Dockerfile': ['Dockerfile', 'Dockerfile.*'] } }, DOCKERFILE_GLOB_PATTERN); + } + + public shouldPrompt(context: IBuildImageInAzureContext): boolean { + return !context.dockerFilePath; + } +} diff --git a/src/commands/deploy/buildImageInAzure/IBuildImageInAzureContext.ts b/src/commands/deploy/buildImageInAzure/IBuildImageInAzureContext.ts new file mode 100644 index 000000000..e76a510b3 --- /dev/null +++ b/src/commands/deploy/buildImageInAzure/IBuildImageInAzureContext.ts @@ -0,0 +1,23 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Microsoft Corporation. All rights reserved. +* Licensed under the MIT License. See License.md in the project root for license information. +*--------------------------------------------------------------------------------------------*/ + +import type { Run as AcrRun, ContainerRegistryManagementClient } from '@azure/arm-containerregistry'; +import * as vscode from 'vscode'; +import { IDeployFromRegistryContext } from '../deployFromRegistry/IDeployFromRegistryContext'; + +export interface IBuildImageInAzureContext extends IDeployFromRegistryContext { + rootFolder: vscode.WorkspaceFolder; + dockerFilePath: string; + imageName: string; + os: 'Windows' | 'Linux'; + + uploadedSourceLocation: string; + tarFilePath: string; + + client: ContainerRegistryManagementClient; + resourceGroupName: string; + registryName: string; + run: AcrRun +} diff --git a/src/commands/deploy/buildImageInAzure/ImageNameStep.ts b/src/commands/deploy/buildImageInAzure/ImageNameStep.ts new file mode 100644 index 000000000..139ed95a8 --- /dev/null +++ b/src/commands/deploy/buildImageInAzure/ImageNameStep.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 { AzureWizardPromptStep } from "@microsoft/vscode-azext-utils"; +import { URI, Utils } from "vscode-uri"; +import { localize } from "../../../utils/localize"; +import { IBuildImageInAzureContext } from "./IBuildImageInAzureContext"; + +export class ImageNameStep extends AzureWizardPromptStep { + public async prompt(context: IBuildImageInAzureContext): Promise { + const suggestedImageName = await getSuggestedName(context, context.dockerFilePath); + + context.imageName = await context.ui.showInputBox({ + prompt: localize('imageNamePrompt', 'Enter a name for the image'), + value: suggestedImageName ? localize('dockerfilePlaceholder', suggestedImageName) : '' + }); + } + + public shouldPrompt(context: IBuildImageInAzureContext): boolean { + return !context.imageName; + } + +} + +async function getSuggestedName(context: IBuildImageInAzureContext, dockerFilePath: string): Promise { + let suggestedImageName: string | undefined; + suggestedImageName = Utils.dirname(URI.parse(dockerFilePath)).path.split('/').pop(); + if (suggestedImageName === '') { + if (context.rootFolder) { + suggestedImageName = Utils.basename(context.rootFolder.uri).toLowerCase().replace(/\s/g, ''); + } + } + suggestedImageName += ":{{.Run.ID}}"; + return suggestedImageName; +} diff --git a/src/commands/deploy/buildImageInAzure/OSPickStep.ts b/src/commands/deploy/buildImageInAzure/OSPickStep.ts new file mode 100644 index 000000000..08fb0cbfc --- /dev/null +++ b/src/commands/deploy/buildImageInAzure/OSPickStep.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 { AzureWizardPromptStep, IAzureQuickPickItem } from "@microsoft/vscode-azext-utils"; +import { localize } from "../../../utils/localize"; +import { IBuildImageInAzureContext } from "./IBuildImageInAzureContext"; + +export class OSPickStep extends AzureWizardPromptStep { + public async prompt(context: IBuildImageInAzureContext): Promise { + const placeHolder: string = localize('imageOSPrompt', 'Select image base OS'); + const picks: IAzureQuickPickItem<'Windows' | 'Linux'>[] = [ + { label: 'Linux', data: 'Linux', suppressPersistence: true }, + { label: 'Windows', data: 'Windows', suppressPersistence: true }, + ]; + + context.os = (await context.ui.showQuickPick(picks, { placeHolder })).data; + } + + public shouldPrompt(context: IBuildImageInAzureContext): boolean { + return !context.os; + } +} diff --git a/src/commands/deploy/buildImageInAzure/RootFolderStep.ts b/src/commands/deploy/buildImageInAzure/RootFolderStep.ts new file mode 100644 index 000000000..70da735e0 --- /dev/null +++ b/src/commands/deploy/buildImageInAzure/RootFolderStep.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 { AzureWizardPromptStep, UserCancelledError } from '@microsoft/vscode-azext-utils'; +import * as vscode from 'vscode'; +import { localize } from '../../../utils/localize'; +import { IBuildImageInAzureContext } from './IBuildImageInAzureContext'; + +export class RootFolderStep extends AzureWizardPromptStep { + public async prompt(context: IBuildImageInAzureContext): Promise { + context.rootFolder = await getRootWorkSpaceFolder(); + } + + public shouldPrompt(context: IBuildImageInAzureContext): boolean { + return !context.rootFolder; + } +} + +async function getRootWorkSpaceFolder(): Promise { + if (!vscode.workspace.workspaceFolders || vscode.workspace.workspaceFolders.length === 0) { + throw new Error(localize('noOpenFolder', 'No folder is open. Please open a folder and try again.')); + } else if (vscode.workspace.workspaceFolders.length === 1) { + return vscode.workspace.workspaceFolders[0]; + } else { + const placeHolder: string = localize('selectRootWorkspace', 'Select the folder containing your Dockerfile'); + const folder = await vscode.window.showWorkspaceFolderPick({ placeHolder }); + if (!folder) { + throw new UserCancelledError('selectRootWorkspace'); + } + return folder; + } +} diff --git a/src/commands/deploy/buildImageInAzure/RunStep.ts b/src/commands/deploy/buildImageInAzure/RunStep.ts new file mode 100644 index 000000000..fb2fd0c59 --- /dev/null +++ b/src/commands/deploy/buildImageInAzure/RunStep.ts @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Microsoft Corporation. All rights reserved. +* Licensed under the MIT License. See License.md in the project root for license information. +*--------------------------------------------------------------------------------------------*/ + +import type { DockerBuildRequest as AcrDockerBuildRequest } from "@azure/arm-containerregistry"; +import { AzExtFsExtra, AzureWizardExecuteStep } from "@microsoft/vscode-azext-utils"; +import * as path from 'path'; +import { Progress } from "vscode"; +import { localize } from "../../../utils/localize"; +import { IBuildImageInAzureContext } from "./IBuildImageInAzureContext"; + +export class RunStep extends AzureWizardExecuteStep { + public priority: number = 200; + + public async execute(context: IBuildImageInAzureContext, progress: Progress<{ message?: string | undefined; increment?: number | undefined }>): Promise { + try { + const rootUri = context.rootFolder.uri; + + const runRequest: AcrDockerBuildRequest = { + type: 'DockerBuildRequest', + imageNames: [context.imageName], + isPushEnabled: true, + sourceLocation: context.uploadedSourceLocation, + platform: { os: context.os }, + dockerFilePath: path.relative(rootUri.path, context.dockerFilePath) + }; + + const building: string = localize('buildingImage', 'Building image "{0}" in registry "{1}"...', context.imageName, context.registryName); + progress.report({ message: building }); + + context.run = await context.client.registries.beginScheduleRunAndWait(context.resourceGroupName, context.registryName, runRequest); + } finally { + if (await AzExtFsExtra.pathExists(context.tarFilePath)) { + await AzExtFsExtra.deleteResource(context.tarFilePath); + } + } + } + + public shouldExecute(context: IBuildImageInAzureContext): boolean { + return !context.run + } +} diff --git a/src/commands/deploy/buildImageInAzure/TarFileStep.ts b/src/commands/deploy/buildImageInAzure/TarFileStep.ts new file mode 100644 index 000000000..0f2f5e16e --- /dev/null +++ b/src/commands/deploy/buildImageInAzure/TarFileStep.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 { AzureWizardExecuteStep } from "@microsoft/vscode-azext-utils"; +import * as os from 'os'; +import { URI, Utils } from "vscode-uri"; +import { IBuildImageInAzureContext } from "./IBuildImageInAzureContext"; + +const idPrecision = 6; + +export class TarFileStep extends AzureWizardExecuteStep { + public priority: number = 150; + + public async execute(context: IBuildImageInAzureContext): Promise { + const id: number = Math.floor(Math.random() * Math.pow(10, idPrecision)); + const archive = `sourceArchive${id}.tar.gz`; + context.tarFilePath = Utils.joinPath(URI.parse(os.tmpdir()), archive).path; + } + + public shouldExecute(context: IBuildImageInAzureContext): boolean { + return !context.tarFilePath; + } +} diff --git a/src/commands/deploy/buildImageInAzure/UploadSourceCodeStep.ts b/src/commands/deploy/buildImageInAzure/UploadSourceCodeStep.ts new file mode 100644 index 000000000..7e1d1195a --- /dev/null +++ b/src/commands/deploy/buildImageInAzure/UploadSourceCodeStep.ts @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Microsoft Corporation. All rights reserved. +* Licensed under the MIT License. See License.md in the project root for license information. +*--------------------------------------------------------------------------------------------*/ +import { getResourceGroupFromId } from '@microsoft/vscode-azext-azureutils'; +import { AzureWizardExecuteStep, nonNullValue } from '@microsoft/vscode-azext-utils'; +import * as fse from 'fs-extra'; +import * as tar from 'tar'; +import { createContainerRegistryManagementClient } from '../../../utils/azureClients'; +import { IBuildImageInAzureContext } from './IBuildImageInAzureContext'; + +const vcsIgnoreList = ['.git', '.gitignore', '.bzr', 'bzrignore', '.hg', '.hgignore', '.svn']; + +export class UploadSourceCodeStep extends AzureWizardExecuteStep { + public priority: number = 175; + + public async execute(context: IBuildImageInAzureContext): Promise { + context.registryName = nonNullValue(context.registry?.name); + context.resourceGroupName = getResourceGroupFromId(nonNullValue(context.registry?.id)); + context.client = await createContainerRegistryManagementClient(context); + + const source: string = context.rootFolder.uri.fsPath; + let items = await fse.readdir(source); + items = items.filter(i => !(i in vcsIgnoreList)); + tar.c({ cwd: source }, items).pipe(fse.createWriteStream(context.tarFilePath)); + + const sourceUploadLocation = await context.client.registries.getBuildSourceUploadUrl(context.resourceGroupName, context.registryName); + const uploadUrl: string = nonNullValue(sourceUploadLocation.uploadUrl); + const relativePath: string = nonNullValue(sourceUploadLocation.relativePath); + + const storageBlob = await import('@azure/storage-blob'); + const blobClient = new storageBlob.BlockBlobClient(uploadUrl); + await blobClient.uploadFile(context.tarFilePath); + + context.uploadedSourceLocation = relativePath; + } + + public shouldExecute(context: IBuildImageInAzureContext): boolean { + return !context.uploadedSourceLocation + } +} diff --git a/src/commands/deploy/buildImageInAzure/buildImageInAzure.ts b/src/commands/deploy/buildImageInAzure/buildImageInAzure.ts new file mode 100644 index 000000000..4fce0358e --- /dev/null +++ b/src/commands/deploy/buildImageInAzure/buildImageInAzure.ts @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Microsoft Corporation. All rights reserved. +* Licensed under the MIT License. See License.md in the project root for license information. +*--------------------------------------------------------------------------------------------*/ + +import type { Run as AcrRun } from '@azure/arm-containerregistry'; +import { KnownRunStatus } from '@azure/arm-containerregistry'; +import { delay } from "@azure/ms-rest-js"; +import { nonNullValue } from '@microsoft/vscode-azext-utils'; +import { IBuildImageInAzureContext } from "./IBuildImageInAzureContext"; + +const WAIT_MS = 5000; + +export async function buildImageInAzure(context: IBuildImageInAzureContext): Promise { + const getRun = async () => context.client.runs.get(context.resourceGroupName, context.registryName, nonNullValue(context.run.runId)); + + let run = await getRun(); + while ( + run.status === KnownRunStatus.Started || + run.status === KnownRunStatus.Queued || + run.status === KnownRunStatus.Running + ) { + await delay(WAIT_MS); + run = await getRun(); + } + + return run; +} diff --git a/src/commands/deploy/buildImageInAzure/tar.d.ts b/src/commands/deploy/buildImageInAzure/tar.d.ts new file mode 100644 index 000000000..c8d11906b --- /dev/null +++ b/src/commands/deploy/buildImageInAzure/tar.d.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. + *--------------------------------------------------------------------------------------------*/ + +/** + * Originally from the Docker extension: https://github.com/microsoft/vscode-docker/blob/main/src/definitions/tar.d.ts + * + * The `@types/tar` package is riddled with inaccuracies, so it's easier + * to just declare typings for it here + */ +declare module "tar" { + //#region Parse + + export interface ParseOptions { + filter?: (path: string, entry: ReadEntryClass) => boolean; + onentry?: (entry: ReadEntryClass) => void; + } + + export interface ParseClass extends NodeJS.ReadWriteStream { + // eslint-disable-next-line @typescript-eslint/no-misused-new + new(options?: ParseOptions): ParseClass; + } + + export const Parse: ParseClass; + + //#endregion Parse + + //#region Pack + + export interface PackOptions { + portable?: boolean; + } + + export interface PackClass extends NodeJS.ReadWriteStream { + // eslint-disable-next-line @typescript-eslint/no-misused-new + new(options?: PackOptions): PackClass; + add(readEntry: ReadEntryClass): void; + } + + export const Pack: PackClass; + + //#endregion Pack + + //#region ReadEntry + + export interface ReadEntryOptions { + path: string; + type: 'File' | 'Directory'; + size: number; + atime: Date; + mtime: Date; + ctime: Date; + mode?: number; + gid?: number; + uid?: number; + } + + export interface ReadEntryClass extends NodeJS.EventEmitter, NodeJS.ReadWriteStream { + // eslint-disable-next-line @typescript-eslint/no-misused-new + new(options: ReadEntryOptions): ReadEntryClass; + path: string; + } + + export const ReadEntry: ReadEntryClass; + + //#endregion ReadEntry + + //#region Create + + export interface CreateOptions { + cwd?: string; + } + + export function create(options: CreateOptions, fileList: string[]): NodeJS.ReadableStream; + + export const c: typeof create; + + //#endregion +} diff --git a/src/constants.ts b/src/constants.ts index 1ecdd270e..239b09b10 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -68,3 +68,6 @@ export type QuickPicksCache = { cache: QuickPickItem[], next: string | null }; export const azResourceContextValue: string = 'azResource'; export const azResourceRegExp = new RegExp(azResourceContextValue, 'i'); + +// Originally from the Docker extension: https://github.com/microsoft/vscode-docker/blob/main/src/constants.ts +export const DOCKERFILE_GLOB_PATTERN = '**/{*.[dD][oO][cC][kK][eE][rR][fF][iI][lL][eE],[dD][oO][cC][kK][eE][rR][fF][iI][lL][eE],[dD][oO][cC][kK][eE][rR][fF][iI][lL][eE].*}'; diff --git a/src/utils/workspaceUtils.ts b/src/utils/workspaceUtils.ts index 52f80f213..afab20e32 100644 --- a/src/utils/workspaceUtils.ts +++ b/src/utils/workspaceUtils.ts @@ -16,9 +16,9 @@ export async function selectWorkspaceFile(context: IActionContext, placeHolder: const files = globPattern ? await workspace.findFiles(globPattern) : await workspace.findFiles('**/*'); quickPicks = files.map((uri: Uri) => { return { - label: basename(uri.fsPath), - description: uri.fsPath, - data: uri.fsPath + label: basename(uri.path), + description: uri.path, + data: uri.path }; });