From 20000adcf85ed6f78a69b6b7d3d2371031c994f7 Mon Sep 17 00:00:00 2001 From: aksm-ms <58936966+aksm-ms@users.noreply.github.com> Date: Sat, 27 Jun 2020 09:51:24 +0530 Subject: [PATCH] Publish prof container master (#35) * Added support for container apps with publish profile * Renamed publish profile container provider * refactor * Addressed review comments * Integrated diagnostics runtime API to get appOS * updated error message * updated error messages * updated error message * updated unit tests for publish profile container * quotes fix * changes in PublishProfileContainerWebAppValidator * refactor * added package-lock.json * adding lib --- .../PublishProfileContainerWebAppValidator.js | 27 +++++++++++++++ lib/ActionInputValidator/Validations.js | 31 +++++++++++++---- lib/ActionInputValidator/ValidatorFactory.js | 23 +++++++++++-- .../DeploymentProviderFactory.js | 9 +++-- .../Providers/BaseWebAppDeploymentProvider.js | 6 ++-- ...rofileWebAppContainerDeploymentProvider.js | 23 +++++++++++++ lib/RuntimeConstants.js | 9 +++++ lib/Utilities/PublishProfile.js | 33 +++++++++++++++++++ lib/tests/main.test.js | 7 ++-- package-lock.json | 8 ++--- package.json | 2 +- .../PublishProfileContainerWebAppValidator.ts | 22 +++++++++++++ src/ActionInputValidator/Validations.ts | 27 +++++++++++---- src/ActionInputValidator/ValidatorFactory.ts | 27 +++++++++++---- .../DeploymentProviderFactory.ts | 12 ++++--- .../Providers/BaseWebAppDeploymentProvider.ts | 5 ++- ...rofileWebAppContainerDeploymentProvider.ts | 10 ++++++ src/RuntimeConstants.ts | 6 ++++ src/Utilities/PublishProfile.ts | 24 ++++++++++++++ src/tests/main.test.ts | 9 +++-- 20 files changed, 271 insertions(+), 49 deletions(-) create mode 100644 lib/ActionInputValidator/ActionValidators/PublishProfileContainerWebAppValidator.js create mode 100644 lib/DeploymentProvider/Providers/PublishProfileWebAppContainerDeploymentProvider.js create mode 100644 lib/RuntimeConstants.js create mode 100644 src/ActionInputValidator/ActionValidators/PublishProfileContainerWebAppValidator.ts create mode 100644 src/DeploymentProvider/Providers/PublishProfileWebAppContainerDeploymentProvider.ts create mode 100644 src/RuntimeConstants.ts diff --git a/lib/ActionInputValidator/ActionValidators/PublishProfileContainerWebAppValidator.js b/lib/ActionInputValidator/ActionValidators/PublishProfileContainerWebAppValidator.js new file mode 100644 index 000000000..0faab10f1 --- /dev/null +++ b/lib/ActionInputValidator/ActionValidators/PublishProfileContainerWebAppValidator.js @@ -0,0 +1,27 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const Validations_1 = require("../Validations"); +const actionparameters_1 = require("../../actionparameters"); +class PublishProfileContainerWebAppValidator { + validate() { + return __awaiter(this, void 0, void 0, function* () { + const actionParams = actionparameters_1.ActionParameters.getActionParams(); + Validations_1.packageNotAllowed(actionParams.packageInput); + yield Validations_1.windowsContainerAppNotAllowedForPublishProfile(); + Validations_1.multiContainerNotAllowed(actionParams.multiContainerConfigFile); + Validations_1.startupCommandNotAllowed(actionParams.startupCommand); + Validations_1.validateAppDetails(); + Validations_1.validateSingleContainerInputs(); + }); + } +} +exports.PublishProfileContainerWebAppValidator = PublishProfileContainerWebAppValidator; diff --git a/lib/ActionInputValidator/Validations.js b/lib/ActionInputValidator/Validations.js index ba568fa9d..9dec5b693 100644 --- a/lib/ActionInputValidator/Validations.js +++ b/lib/ActionInputValidator/Validations.js @@ -15,10 +15,14 @@ var __importStar = (this && this.__importStar) || function (mod) { result["default"] = mod; return result; }; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); const core = __importStar(require("@actions/core")); const packageUtility_1 = require("azure-actions-utility/packageUtility"); const PublishProfile_1 = require("../Utilities/PublishProfile"); +const RuntimeConstants_1 = __importDefault(require("../RuntimeConstants")); const actionparameters_1 = require("../actionparameters"); const fs = require("fs"); // Error is app-name is not provided @@ -31,12 +35,7 @@ exports.appNameIsRequired = appNameIsRequired; // Error if image info is provided function containerInputsNotAllowed(images, configFile, isPublishProfile = false) { if (!!images || !!configFile) { - if (!!isPublishProfile) { - throw new Error("Container Deployment is not supported with publish profile credentails. Instead add an Azure login action before this action. For more details refer https://github.com/azure/login"); - } - else { - throw new Error(`This is not a container web app. Please remove inputs like images and configuration-file which are only relevant for container deployment.`); - } + throw new Error(`This is not a container web app. Please remove inputs like images and configuration-file which are only relevant for container deployment.`); } } exports.containerInputsNotAllowed = containerInputsNotAllowed; @@ -75,6 +74,14 @@ function multiContainerNotAllowed(configFile) { } } exports.multiContainerNotAllowed = multiContainerNotAllowed; +// Error if image name is not provided +function validateSingleContainerInputs() { + const actionParams = actionparameters_1.ActionParameters.getActionParams(); + if (!actionParams.images) { + throw new Error("Image name not provided for container. Provide a valid image name"); + } +} +exports.validateSingleContainerInputs = validateSingleContainerInputs; // Validate container inputs function validateContainerInputs() { let actionParams = actionparameters_1.ActionParameters.getActionParams(); @@ -116,3 +123,15 @@ function validatePackageInput() { }); } exports.validatePackageInput = validatePackageInput; +// windows container app not allowed for publish profile auth scheme +function windowsContainerAppNotAllowedForPublishProfile() { + return __awaiter(this, void 0, void 0, function* () { + const actionParams = actionparameters_1.ActionParameters.getActionParams(); + const publishProfile = PublishProfile_1.PublishProfile.getPublishProfile(actionParams.publishProfileContent); + const appOS = yield publishProfile.getAppOS(); + if (appOS.includes(RuntimeConstants_1.default.Windows) || appOS.includes(RuntimeConstants_1.default.Windows.toLowerCase())) { + throw new Error("Publish profile auth scheme is not supported for Windows container Apps."); + } + }); +} +exports.windowsContainerAppNotAllowedForPublishProfile = windowsContainerAppNotAllowedForPublishProfile; diff --git a/lib/ActionInputValidator/ValidatorFactory.js b/lib/ActionInputValidator/ValidatorFactory.js index ac7e28e79..8ed135e8b 100644 --- a/lib/ActionInputValidator/ValidatorFactory.js +++ b/lib/ActionInputValidator/ValidatorFactory.js @@ -8,22 +8,34 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); const actionparameters_1 = require("../actionparameters"); const AzureResourceFilterUtility_1 = require("azure-actions-appservice-rest/Utilities/AzureResourceFilterUtility"); const BaseWebAppDeploymentProvider_1 = require("../DeploymentProvider/Providers/BaseWebAppDeploymentProvider"); const PublishProfileWebAppValidator_1 = require("./ActionValidators/PublishProfileWebAppValidator"); +const PublishProfileContainerWebAppValidator_1 = require("./ActionValidators/PublishProfileContainerWebAppValidator"); const SpnLinuxContainerWebAppValidator_1 = require("./ActionValidators/SpnLinuxContainerWebAppValidator"); const SpnLinuxWebAppValidator_1 = require("./ActionValidators/SpnLinuxWebAppValidator"); const SpnWindowsContainerWebAppValidator_1 = require("./ActionValidators/SpnWindowsContainerWebAppValidator"); const SpnWindowsWebAppValidator_1 = require("./ActionValidators/SpnWindowsWebAppValidator"); const Validations_1 = require("./Validations"); +const PublishProfile_1 = require("../Utilities/PublishProfile"); +const RuntimeConstants_1 = __importDefault(require("../RuntimeConstants")); class ValidatorFactory { static getValidator(type) { return __awaiter(this, void 0, void 0, function* () { let actionParams = actionparameters_1.ActionParameters.getActionParams(); - if (type == BaseWebAppDeploymentProvider_1.DEPLOYMENT_PROVIDER_TYPES.PUBLISHPROFILE) { - return new PublishProfileWebAppValidator_1.PublishProfileWebAppValidator(); + if (type === BaseWebAppDeploymentProvider_1.DEPLOYMENT_PROVIDER_TYPES.PUBLISHPROFILE) { + yield this.setResourceDetails(actionParams); + if (!!actionParams.images) { + return new PublishProfileContainerWebAppValidator_1.PublishProfileContainerWebAppValidator(); + } + else { + return new PublishProfileWebAppValidator_1.PublishProfileWebAppValidator(); + } } else if (type == BaseWebAppDeploymentProvider_1.DEPLOYMENT_PROVIDER_TYPES.SPN) { // app-name is required to get resource details @@ -56,5 +68,12 @@ class ValidatorFactory { params.isLinux = params.realKind.indexOf("linux") > -1; }); } + static setResourceDetails(actionParams) { + return __awaiter(this, void 0, void 0, function* () { + const publishProfile = PublishProfile_1.PublishProfile.getPublishProfile(actionParams.publishProfileContent); + const appOS = yield publishProfile.getAppOS(); + actionParams.isLinux = appOS.includes(RuntimeConstants_1.default.Unix) || appOS.includes(RuntimeConstants_1.default.Unix.toLowerCase()); + }); + } } exports.ValidatorFactory = ValidatorFactory; diff --git a/lib/DeploymentProvider/DeploymentProviderFactory.js b/lib/DeploymentProvider/DeploymentProviderFactory.js index 1571d05f0..b24205fb4 100644 --- a/lib/DeploymentProvider/DeploymentProviderFactory.js +++ b/lib/DeploymentProvider/DeploymentProviderFactory.js @@ -4,11 +4,16 @@ const actionparameters_1 = require("../actionparameters"); const BaseWebAppDeploymentProvider_1 = require("./Providers/BaseWebAppDeploymentProvider"); const WebAppContainerDeployment_1 = require("./Providers/WebAppContainerDeployment"); const WebAppDeploymentProvider_1 = require("./Providers/WebAppDeploymentProvider"); +const PublishProfileWebAppContainerDeploymentProvider_1 = require("./Providers/PublishProfileWebAppContainerDeploymentProvider"); class DeploymentProviderFactory { static getDeploymentProvider(type) { - // For publish profile type app kind is not available so we directly return WebAppDeploymentProvider if (type === BaseWebAppDeploymentProvider_1.DEPLOYMENT_PROVIDER_TYPES.PUBLISHPROFILE) { - return new WebAppDeploymentProvider_1.WebAppDeploymentProvider(type); + if (!!actionparameters_1.ActionParameters.getActionParams().images) { + return new PublishProfileWebAppContainerDeploymentProvider_1.PublishProfileWebAppContainerDeploymentProvider(type); + } + else { + return new WebAppDeploymentProvider_1.WebAppDeploymentProvider(type); + } } else if (type == BaseWebAppDeploymentProvider_1.DEPLOYMENT_PROVIDER_TYPES.SPN) { let kind = actionparameters_1.ActionParameters.getActionParams().kind; diff --git a/lib/DeploymentProvider/Providers/BaseWebAppDeploymentProvider.js b/lib/DeploymentProvider/Providers/BaseWebAppDeploymentProvider.js index 82c0581fc..09d632ba3 100644 --- a/lib/DeploymentProvider/Providers/BaseWebAppDeploymentProvider.js +++ b/lib/DeploymentProvider/Providers/BaseWebAppDeploymentProvider.js @@ -21,7 +21,6 @@ const PublishProfile_1 = require("../../Utilities/PublishProfile"); const actionparameters_1 = require("../../actionparameters"); const azure_app_service_1 = require("azure-actions-appservice-rest/Arm/azure-app-service"); const AzureAppServiceUtility_1 = require("azure-actions-appservice-rest/Utilities/AzureAppServiceUtility"); -const azure_app_kudu_service_1 = require("azure-actions-appservice-rest/Kudu/azure-app-kudu-service"); const KuduServiceUtility_1 = require("azure-actions-appservice-rest/Utilities/KuduServiceUtility"); const AnnotationUtility_1 = require("azure-actions-appservice-rest/Utilities/AnnotationUtility"); class BaseWebAppDeploymentProvider { @@ -68,9 +67,8 @@ class BaseWebAppDeploymentProvider { } initializeForPublishProfile() { return __awaiter(this, void 0, void 0, function* () { - let publishProfile = PublishProfile_1.PublishProfile.getPublishProfile(this.actionParams.publishProfileContent); - let scmCreds = publishProfile.creds; - this.kuduService = new azure_app_kudu_service_1.Kudu(scmCreds.uri, scmCreds.username, scmCreds.password); + const publishProfile = PublishProfile_1.PublishProfile.getPublishProfile(this.actionParams.publishProfileContent); + this.kuduService = publishProfile.kuduService; this.kuduServiceUtility = new KuduServiceUtility_1.KuduServiceUtility(this.kuduService); this.applicationURL = publishProfile.appUrl; }); diff --git a/lib/DeploymentProvider/Providers/PublishProfileWebAppContainerDeploymentProvider.js b/lib/DeploymentProvider/Providers/PublishProfileWebAppContainerDeploymentProvider.js new file mode 100644 index 000000000..1cdfa2aaa --- /dev/null +++ b/lib/DeploymentProvider/Providers/PublishProfileWebAppContainerDeploymentProvider.js @@ -0,0 +1,23 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const BaseWebAppDeploymentProvider_1 = require("./BaseWebAppDeploymentProvider"); +class PublishProfileWebAppContainerDeploymentProvider extends BaseWebAppDeploymentProvider_1.BaseWebAppDeploymentProvider { + DeployWebAppStep() { + return __awaiter(this, void 0, void 0, function* () { + const appName = this.actionParams.appName; + const images = this.actionParams.images; + const isLinux = this.actionParams.isLinux; + yield this.kuduServiceUtility.deployWebAppImage(appName, images, isLinux); + }); + } +} +exports.PublishProfileWebAppContainerDeploymentProvider = PublishProfileWebAppContainerDeploymentProvider; diff --git a/lib/RuntimeConstants.js b/lib/RuntimeConstants.js new file mode 100644 index 000000000..e235548d1 --- /dev/null +++ b/lib/RuntimeConstants.js @@ -0,0 +1,9 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +class RuntimeConstants { +} +exports.default = RuntimeConstants; +RuntimeConstants.system = "system"; +RuntimeConstants.osName = "os_name"; +RuntimeConstants.Windows = "Windows"; +RuntimeConstants.Unix = "Unix"; diff --git a/lib/Utilities/PublishProfile.js b/lib/Utilities/PublishProfile.js index d29517d4e..4354e2b9a 100644 --- a/lib/Utilities/PublishProfile.js +++ b/lib/Utilities/PublishProfile.js @@ -1,7 +1,21 @@ "use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); var core = require("@actions/core"); const actions_secret_parser_1 = require("actions-secret-parser"); +const azure_app_kudu_service_1 = require("azure-actions-appservice-rest/Kudu/azure-app-kudu-service"); +const RuntimeConstants_1 = __importDefault(require("../RuntimeConstants")); class PublishProfile { constructor(publishProfileContent) { try { @@ -16,6 +30,7 @@ class PublishProfile { throw new Error("Publish profile does not contain kudu URL"); } this._creds.uri = `https://${this._creds.uri}`; + this._kuduService = new azure_app_kudu_service_1.Kudu(this._creds.uri, this._creds.username, this._creds.password); } catch (error) { core.error("Failed to fetch credentials from Publish Profile. For more details on how to set publish profile credentials refer https://aka.ms/create-secrets-for-GitHub-workflows"); @@ -34,5 +49,23 @@ class PublishProfile { get appUrl() { return this._appUrl; } + get kuduService() { + return this._kuduService; + } + getAppOS() { + return __awaiter(this, void 0, void 0, function* () { + try { + if (!this._appOS) { + const appRuntimeDetails = yield this._kuduService.getAppRuntime(); + this._appOS = appRuntimeDetails[RuntimeConstants_1.default.system][RuntimeConstants_1.default.osName]; + core.debug(`App Runtime OS: ${this._appOS}`); + } + } + catch (error) { + throw Error("Internal Server Error. Please try again\n" + error); + } + return this._appOS; + }); + } } exports.PublishProfile = PublishProfile; diff --git a/lib/tests/main.test.js b/lib/tests/main.test.js index 5e537d1d1..d806f9073 100644 --- a/lib/tests/main.test.js +++ b/lib/tests/main.test.js @@ -23,7 +23,6 @@ const ValidatorFactory_1 = require("../ActionInputValidator/ValidatorFactory"); const DeploymentProviderFactory_1 = require("../DeploymentProvider/DeploymentProviderFactory"); const actionparameters_1 = require("../actionparameters"); const PublishProfileWebAppValidator_1 = require("../ActionInputValidator/ActionValidators/PublishProfileWebAppValidator"); -; const WebAppDeploymentProvider_1 = require("../DeploymentProvider/Providers/WebAppDeploymentProvider"); jest.mock('@actions/core'); jest.mock('../actionparameters'); @@ -49,9 +48,9 @@ describe('Test azure-webapps-deploy', () => { } return ''; }); - let getValidatorFactorySpy = jest.spyOn(ValidatorFactory_1.ValidatorFactory, 'getValidator'); + let getValidatorFactorySpy = jest.spyOn(ValidatorFactory_1.ValidatorFactory, 'getValidator').mockImplementation((_type) => __awaiter(void 0, void 0, void 0, function* () { return new PublishProfileWebAppValidator_1.PublishProfileWebAppValidator(); })); let ValidatorFactoryValidateSpy = jest.spyOn(PublishProfileWebAppValidator_1.PublishProfileWebAppValidator.prototype, 'validate'); - let getDeploymentProviderSpy = jest.spyOn(DeploymentProviderFactory_1.DeploymentProviderFactory, 'getDeploymentProvider'); + let getDeploymentProviderSpy = jest.spyOn(DeploymentProviderFactory_1.DeploymentProviderFactory, 'getDeploymentProvider').mockImplementation(type => new WebAppDeploymentProvider_1.WebAppDeploymentProvider(type)); let deployWebAppStepSpy = jest.spyOn(WebAppDeploymentProvider_1.WebAppDeploymentProvider.prototype, 'DeployWebAppStep'); let updateDeploymentStatusSpy = jest.spyOn(WebAppDeploymentProvider_1.WebAppDeploymentProvider.prototype, 'UpdateDeploymentStatus'); try { @@ -61,7 +60,7 @@ describe('Test azure-webapps-deploy', () => { console.log(e); } expect(getAuthorizerSpy).not.toHaveBeenCalled(); // When publish profile is given as input getAuthorizer is not called - expect(getActionParamsSpy).toHaveBeenCalledTimes(2); + expect(getActionParamsSpy).toHaveBeenCalledTimes(1); expect(getInputSpy).toHaveBeenCalledTimes(1); expect(getValidatorFactorySpy).toHaveBeenCalledTimes(1); expect(ValidatorFactoryValidateSpy).toHaveBeenCalledTimes(1); diff --git a/package-lock.json b/package-lock.json index 5cbc33487..d4819e583 100644 --- a/package-lock.json +++ b/package-lock.json @@ -81,13 +81,13 @@ } }, "azure-actions-appservice-rest": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/azure-actions-appservice-rest/-/azure-actions-appservice-rest-1.0.9.tgz", - "integrity": "sha512-/Ksj9wpn0GGSMJRaCCmjkO+51FHIjzIhU/zl9BjMSclo7BJT4I3Ge8Maa5aYIZGzYi4fUg+YzV/7uGsV/VPvTQ==", + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/azure-actions-appservice-rest/-/azure-actions-appservice-rest-1.2.9.tgz", + "integrity": "sha512-+E8T+dF2gh1GMrvA00KyhN1k7g1ZIfAceE6cF9nRWfNVvF6SHDP3fcdTaodY07rBaFf0buonyfMf3DJRc6m2iw==", "requires": { "@actions/core": "^1.1.1", "@actions/io": "^1.0.1", - "azure-actions-webclient": "^1.0.11", + "azure-actions-webclient": "^1.0.8", "fs": "0.0.1-security", "util": "^0.12.1", "uuid": "^3.3.3", diff --git a/package.json b/package.json index e061ccbc8..cd7eb66d4 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "dependencies": { "@actions/core": "^1.2.1", "actions-secret-parser": "^1.0.3", - "azure-actions-appservice-rest": "^1.0.9", + "azure-actions-appservice-rest": "^1.2.9", "azure-actions-utility": "^1.0.3", "azure-actions-webclient": "^1.0.11" } diff --git a/src/ActionInputValidator/ActionValidators/PublishProfileContainerWebAppValidator.ts b/src/ActionInputValidator/ActionValidators/PublishProfileContainerWebAppValidator.ts new file mode 100644 index 000000000..ce2a9be7b --- /dev/null +++ b/src/ActionInputValidator/ActionValidators/PublishProfileContainerWebAppValidator.ts @@ -0,0 +1,22 @@ +import { packageNotAllowed, windowsContainerAppNotAllowedForPublishProfile, multiContainerNotAllowed, startupCommandNotAllowed, validateSingleContainerInputs, validateAppDetails } from "../Validations"; +import { ActionParameters } from "../../actionparameters"; +import { IValidator } from "./IValidator"; + +export class PublishProfileContainerWebAppValidator implements IValidator { + async validate(): Promise { + const actionParams: ActionParameters = ActionParameters.getActionParams(); + + packageNotAllowed(actionParams.packageInput); + + await windowsContainerAppNotAllowedForPublishProfile(); + + multiContainerNotAllowed(actionParams.multiContainerConfigFile); + + startupCommandNotAllowed(actionParams.startupCommand); + + validateAppDetails(); + + validateSingleContainerInputs(); + } + +} \ No newline at end of file diff --git a/src/ActionInputValidator/Validations.ts b/src/ActionInputValidator/Validations.ts index fe733b3e5..58014bace 100644 --- a/src/ActionInputValidator/Validations.ts +++ b/src/ActionInputValidator/Validations.ts @@ -2,7 +2,7 @@ import * as core from '@actions/core'; import { Package, exist } from "azure-actions-utility/packageUtility"; import { PublishProfile, ScmCredentials } from "../Utilities/PublishProfile"; - +import RuntimeConstants from '../RuntimeConstants'; import { ActionParameters } from "../actionparameters"; import fs = require('fs'); @@ -17,12 +17,7 @@ export function appNameIsRequired(appname: string) { // Error if image info is provided export function containerInputsNotAllowed(images: string, configFile: string, isPublishProfile: boolean = false) { if(!!images || !!configFile) { - if(!!isPublishProfile) { - throw new Error("Container Deployment is not supported with publish profile credentails. Instead add an Azure login action before this action. For more details refer https://github.com/azure/login"); - } - else { - throw new Error(`This is not a container web app. Please remove inputs like images and configuration-file which are only relevant for container deployment.`); - } + throw new Error(`This is not a container web app. Please remove inputs like images and configuration-file which are only relevant for container deployment.`); } } @@ -63,6 +58,14 @@ export function multiContainerNotAllowed(configFile: string) { } } +// Error if image name is not provided +export function validateSingleContainerInputs() { + const actionParams: ActionParameters = ActionParameters.getActionParams(); + if(!actionParams.images) { + throw new Error("Image name not provided for container. Provide a valid image name"); + } +} + // Validate container inputs export function validateContainerInputs() { @@ -105,4 +108,14 @@ export async function validatePackageInput() { if(isMSBuildPackage) { throw new Error(`Deployment of msBuild generated package is not supported. Please change package format.`); } +} + +// windows container app not allowed for publish profile auth scheme +export async function windowsContainerAppNotAllowedForPublishProfile() { + const actionParams = ActionParameters.getActionParams(); + const publishProfile: PublishProfile = PublishProfile.getPublishProfile(actionParams.publishProfileContent); + const appOS: string = await publishProfile.getAppOS(); + if (appOS.includes(RuntimeConstants.Windows) || appOS.includes(RuntimeConstants.Windows.toLowerCase())) { + throw new Error("Publish profile auth scheme is not supported for Windows container Apps."); + } } \ No newline at end of file diff --git a/src/ActionInputValidator/ValidatorFactory.ts b/src/ActionInputValidator/ValidatorFactory.ts index 5ab76e9ae..eeb8403d7 100644 --- a/src/ActionInputValidator/ValidatorFactory.ts +++ b/src/ActionInputValidator/ValidatorFactory.ts @@ -4,24 +4,31 @@ import { AzureResourceFilterUtility } from "azure-actions-appservice-rest/Utilit import { DEPLOYMENT_PROVIDER_TYPES } from "../DeploymentProvider/Providers/BaseWebAppDeploymentProvider"; import { IValidator } from "./ActionValidators/IValidator"; import { PublishProfileWebAppValidator } from "./ActionValidators/PublishProfileWebAppValidator"; +import { PublishProfileContainerWebAppValidator } from "./ActionValidators/PublishProfileContainerWebAppValidator"; import { SpnLinuxContainerWebAppValidator } from "./ActionValidators/SpnLinuxContainerWebAppValidator"; import { SpnLinuxWebAppValidator } from "./ActionValidators/SpnLinuxWebAppValidator"; import { SpnWindowsContainerWebAppValidator } from "./ActionValidators/SpnWindowsContainerWebAppValidator"; import { SpnWindowsWebAppValidator } from "./ActionValidators/SpnWindowsWebAppValidator"; import { appNameIsRequired } from "./Validations"; +import { PublishProfile } from "../Utilities/PublishProfile"; +import RuntimeConstants from "../RuntimeConstants"; export class ValidatorFactory { public static async getValidator(type: DEPLOYMENT_PROVIDER_TYPES) : Promise { let actionParams: ActionParameters = ActionParameters.getActionParams(); - - if(type == DEPLOYMENT_PROVIDER_TYPES.PUBLISHPROFILE) { - return new PublishProfileWebAppValidator(); + if(type === DEPLOYMENT_PROVIDER_TYPES.PUBLISHPROFILE) { + await this.setResourceDetails(actionParams); + if (!!actionParams.images) { + return new PublishProfileContainerWebAppValidator(); + } + else { + return new PublishProfileWebAppValidator(); + } } else if(type == DEPLOYMENT_PROVIDER_TYPES.SPN) { // app-name is required to get resource details appNameIsRequired(actionParams.appName); await this.getResourceDetails(actionParams); - switch(actionParams.kind) { case WebAppKind.Linux: return new SpnLinuxWebAppValidator(); @@ -38,13 +45,19 @@ export class ValidatorFactory { else { throw new Error("Valid credentails are not available. Add Azure Login action before this action or provide publish-profile input."); } - } + } private static async getResourceDetails(params: ActionParameters) { let appDetails = await AzureResourceFilterUtility.getAppDetails(params.endpoint, params.appName); params.resourceGroupName = appDetails["resourceGroupName"]; params.realKind = appDetails["kind"]; params.kind = appKindMap.get(params.realKind); - params.isLinux = params.realKind.indexOf("linux") > -1; + params.isLinux = params.realKind.indexOf("linux") > -1; + } + + private static async setResourceDetails(actionParams: ActionParameters) { + const publishProfile: PublishProfile = PublishProfile.getPublishProfile(actionParams.publishProfileContent); + const appOS: string = await publishProfile.getAppOS(); + actionParams.isLinux = appOS.includes(RuntimeConstants.Unix) || appOS.includes(RuntimeConstants.Unix.toLowerCase()); } -} \ No newline at end of file +} \ No newline at end of file diff --git a/src/DeploymentProvider/DeploymentProviderFactory.ts b/src/DeploymentProvider/DeploymentProviderFactory.ts index 27d5e1ff9..cf89249ac 100644 --- a/src/DeploymentProvider/DeploymentProviderFactory.ts +++ b/src/DeploymentProvider/DeploymentProviderFactory.ts @@ -4,17 +4,21 @@ import { DEPLOYMENT_PROVIDER_TYPES } from "./Providers/BaseWebAppDeploymentProvi import { IWebAppDeploymentProvider } from "./Providers/IWebAppDeploymentProvider"; import { WebAppContainerDeploymentProvider } from "./Providers/WebAppContainerDeployment"; import { WebAppDeploymentProvider } from "./Providers/WebAppDeploymentProvider"; +import { PublishProfileWebAppContainerDeploymentProvider } from "./Providers/PublishProfileWebAppContainerDeploymentProvider"; export class DeploymentProviderFactory { public static getDeploymentProvider(type: DEPLOYMENT_PROVIDER_TYPES) : IWebAppDeploymentProvider { - - // For publish profile type app kind is not available so we directly return WebAppDeploymentProvider if(type === DEPLOYMENT_PROVIDER_TYPES.PUBLISHPROFILE) { - return new WebAppDeploymentProvider(type); + if (!!ActionParameters.getActionParams().images) { + return new PublishProfileWebAppContainerDeploymentProvider(type); + } + else { + return new WebAppDeploymentProvider(type); + } } else if(type == DEPLOYMENT_PROVIDER_TYPES.SPN) { - let kind = ActionParameters.getActionParams().kind; + let kind = ActionParameters.getActionParams().kind; switch(kind) { case WebAppKind.Linux: case WebAppKind.Windows: diff --git a/src/DeploymentProvider/Providers/BaseWebAppDeploymentProvider.ts b/src/DeploymentProvider/Providers/BaseWebAppDeploymentProvider.ts index 9a770fa10..dc6396de6 100644 --- a/src/DeploymentProvider/Providers/BaseWebAppDeploymentProvider.ts +++ b/src/DeploymentProvider/Providers/BaseWebAppDeploymentProvider.ts @@ -68,10 +68,9 @@ export abstract class BaseWebAppDeploymentProvider implements IWebAppDeploymentP } private async initializeForPublishProfile() { - let publishProfile: PublishProfile = PublishProfile.getPublishProfile(this.actionParams.publishProfileContent); - let scmCreds: ScmCredentials = publishProfile.creds; + const publishProfile: PublishProfile = PublishProfile.getPublishProfile(this.actionParams.publishProfileContent); - this.kuduService = new Kudu(scmCreds.uri, scmCreds.username, scmCreds.password); + this.kuduService = publishProfile.kuduService; this.kuduServiceUtility = new KuduServiceUtility(this.kuduService); this.applicationURL = publishProfile.appUrl; diff --git a/src/DeploymentProvider/Providers/PublishProfileWebAppContainerDeploymentProvider.ts b/src/DeploymentProvider/Providers/PublishProfileWebAppContainerDeploymentProvider.ts new file mode 100644 index 000000000..c53822402 --- /dev/null +++ b/src/DeploymentProvider/Providers/PublishProfileWebAppContainerDeploymentProvider.ts @@ -0,0 +1,10 @@ +import { BaseWebAppDeploymentProvider } from './BaseWebAppDeploymentProvider'; + +export class PublishProfileWebAppContainerDeploymentProvider extends BaseWebAppDeploymentProvider { + public async DeployWebAppStep() { + const appName: string = this.actionParams.appName; + const images: string = this.actionParams.images; + const isLinux: boolean = this.actionParams.isLinux; + await this.kuduServiceUtility.deployWebAppImage(appName, images, isLinux); + } +} \ No newline at end of file diff --git a/src/RuntimeConstants.ts b/src/RuntimeConstants.ts new file mode 100644 index 000000000..8822ca972 --- /dev/null +++ b/src/RuntimeConstants.ts @@ -0,0 +1,6 @@ +export default class RuntimeConstants { + static readonly system: string = "system"; + static readonly osName: string = "os_name"; + static readonly Windows: string = "Windows"; + static readonly Unix: string = "Unix"; +} \ No newline at end of file diff --git a/src/Utilities/PublishProfile.ts b/src/Utilities/PublishProfile.ts index 7861bf049..301b66b68 100644 --- a/src/Utilities/PublishProfile.ts +++ b/src/Utilities/PublishProfile.ts @@ -1,6 +1,9 @@ var core = require("@actions/core"); import { FormatType, SecretParser } from 'actions-secret-parser'; +import { Kudu } from 'azure-actions-appservice-rest/Kudu/azure-app-kudu-service'; + +import RuntimeConstants from '../RuntimeConstants'; export interface ScmCredentials { uri: string; @@ -11,6 +14,8 @@ export interface ScmCredentials { export class PublishProfile { private _creds: ScmCredentials; private _appUrl: string; + private _kuduService: any; + private _appOS: string; private static _publishProfile: PublishProfile; private constructor(publishProfileContent: string) { @@ -26,6 +31,7 @@ export class PublishProfile { throw new Error("Publish profile does not contain kudu URL"); } this._creds.uri = `https://${this._creds.uri}`; + this._kuduService = new Kudu(this._creds.uri, this._creds.username, this._creds.password); } catch(error) { core.error("Failed to fetch credentials from Publish Profile. For more details on how to set publish profile credentials refer https://aka.ms/create-secrets-for-GitHub-workflows"); throw error; @@ -46,4 +52,22 @@ export class PublishProfile { public get appUrl(): string { return this._appUrl; } + + public get kuduService() { + return this._kuduService; + } + + public async getAppOS() { + try { + if(!this._appOS) { + const appRuntimeDetails = await this._kuduService.getAppRuntime(); + this._appOS = appRuntimeDetails[RuntimeConstants.system][RuntimeConstants.osName]; + core.debug(`App Runtime OS: ${this._appOS}`); + } + } catch(error) { + throw Error("Internal Server Error. Please try again\n" + error); + } + return this._appOS; + } + } \ No newline at end of file diff --git a/src/tests/main.test.ts b/src/tests/main.test.ts index c89cbc782..3c062ca25 100644 --- a/src/tests/main.test.ts +++ b/src/tests/main.test.ts @@ -4,7 +4,7 @@ import { AuthorizerFactory } from "azure-actions-webclient/AuthorizerFactory"; import { ValidatorFactory } from '../ActionInputValidator/ValidatorFactory'; import { DeploymentProviderFactory } from '../DeploymentProvider/DeploymentProviderFactory'; import { ActionParameters} from "../actionparameters"; -import { PublishProfileWebAppValidator } from '../ActionInputValidator/ActionValidators/PublishProfileWebAppValidator';; +import { PublishProfileWebAppValidator } from '../ActionInputValidator/ActionValidators/PublishProfileWebAppValidator'; import { WebAppDeploymentProvider } from '../DeploymentProvider/Providers/WebAppDeploymentProvider'; jest.mock('@actions/core'); @@ -35,9 +35,9 @@ describe('Test azure-webapps-deploy', () => { } return ''; }); - let getValidatorFactorySpy = jest.spyOn(ValidatorFactory, 'getValidator'); + let getValidatorFactorySpy = jest.spyOn(ValidatorFactory, 'getValidator').mockImplementation(async _type => new PublishProfileWebAppValidator()); let ValidatorFactoryValidateSpy = jest.spyOn(PublishProfileWebAppValidator.prototype, 'validate'); - let getDeploymentProviderSpy = jest.spyOn(DeploymentProviderFactory, 'getDeploymentProvider'); + let getDeploymentProviderSpy = jest.spyOn(DeploymentProviderFactory, 'getDeploymentProvider').mockImplementation(type => new WebAppDeploymentProvider(type)); let deployWebAppStepSpy = jest.spyOn(WebAppDeploymentProvider.prototype, 'DeployWebAppStep'); let updateDeploymentStatusSpy = jest.spyOn(WebAppDeploymentProvider.prototype, 'UpdateDeploymentStatus'); @@ -49,13 +49,12 @@ describe('Test azure-webapps-deploy', () => { } expect(getAuthorizerSpy).not.toHaveBeenCalled(); // When publish profile is given as input getAuthorizer is not called - expect(getActionParamsSpy).toHaveBeenCalledTimes(2); + expect(getActionParamsSpy).toHaveBeenCalledTimes(1); expect(getInputSpy).toHaveBeenCalledTimes(1); expect(getValidatorFactorySpy).toHaveBeenCalledTimes(1); expect(ValidatorFactoryValidateSpy).toHaveBeenCalledTimes(1); expect(getDeploymentProviderSpy).toHaveBeenCalledTimes(1); expect(deployWebAppStepSpy).toHaveBeenCalled(); expect(updateDeploymentStatusSpy).toHaveBeenCalled(); - }); }); \ No newline at end of file