diff --git a/README.md b/README.md index 96e5eadc6..2588b32ea 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ The definition of this Github Action is in [action.yml](https://github.com/Azure ## Dependencies on other Github Actions * [Checkout](https://github.com/actions/checkout) Checkout your Git repository content into Github Actions agent. -* Authenticate using [Azure Web App Publish Profile](https://github.com/projectkudu/kudu/wiki/Deployment-credentials#site-credentials-aka-publish-profile-credentials) or using [Azure Login](https://github.com/Azure/login) +* Authenticate using [Azure Web App Publish Profile](https://github.com/projectkudu/kudu/wiki/Deployment-credentials#site-credentials-aka-publish-profile-credentials) or using [Azure Login](https://github.com/Azure/login). Action supports publish profile for [Azure Web Apps](https://azure.microsoft.com/en-us/services/app-service/web/) (both Windows and Linux) and [Azure Web Apps for Containers](https://azure.microsoft.com/en-us/services/app-service/containers/) (Linux only). Action does not support multi-container scenario with publish profile. * Environment setup actions * [Setup DotNet](https://github.com/actions/setup-dotnet) Sets up a dotnet environment by optionally downloading and caching a version of dotnet by SDK version and adding to PATH . * [Setup Node](https://github.com/actions/setup-node) sets up a node environment by optionally downloading and caching a version of node - npm by version spec and add to PATH @@ -70,7 +70,39 @@ jobs: app-name: node-rn publish-profile: ${{ secrets.azureWebAppPublishProfile }} +``` +### Sample workflow to build and deploy a Node.js app to Containerized WebApp using publish profile + +```yaml + +on: [push] +name: Linux_Container_Node_Workflow + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + steps: + # checkout the repo + - name: 'Checkout Github Action' + uses: actions/checkout@master + + - uses: azure/docker-login@v1 + with: + login-server: contoso.azurecr.io + username: ${{ secrets.REGISTRY_USERNAME }} + password: ${{ secrets.REGISTRY_PASSWORD }} + + - run: | + docker build . -t contoso.azurecr.io/nodejssampleapp:${{ github.sha }} + docker push contoso.azurecr.io/nodejssampleapp:${{ github.sha }} + + - uses: azure/webapps-deploy@v2 + with: + app-name: 'node-rnc' + publish-profile: ${{ secrets.azureWebAppPublishProfile }} + images: 'contoso.azurecr.io/nodejssampleapp:${{ github.sha }}' + ``` #### Configure deployment credentials: diff --git a/action.yml b/action.yml index d62a6d82c..826cf7801 100644 --- a/action.yml +++ b/action.yml @@ -6,7 +6,7 @@ inputs: description: 'Name of the Azure Web App' required: true publish-profile: - description: 'Applies to Web App only: Publish profile (*.publishsettings) file contents with Web Deploy secrets' + description: 'Applies to Web Apps(Windows and Linux) and Web App Containers(linux). Multi container scenario not supported. Publish profile (*.publishsettings) file contents with Web Deploy secrets' required: false slot-name: description: 'Enter an existing Slot other than the Production slot' 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 new file mode 100644 index 000000000..d806f9073 --- /dev/null +++ b/lib/tests/main.test.js @@ -0,0 +1,71 @@ +"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 __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; + result["default"] = mod; + return result; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const core = __importStar(require("@actions/core")); +const main_1 = require("../main"); +const AuthorizerFactory_1 = require("azure-actions-webclient/AuthorizerFactory"); +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'); +jest.mock('azure-actions-webclient/AuthorizerFactory'); +jest.mock('../ActionInputValidator/ActionValidators/PublishProfileWebAppValidator'); +jest.mock('../DeploymentProvider/Providers/WebAppDeploymentProvider'); +describe('Test azure-webapps-deploy', () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + it("gets inputs and executes all the functions", () => __awaiter(void 0, void 0, void 0, function* () { + let getAuthorizerSpy = jest.spyOn(AuthorizerFactory_1.AuthorizerFactory, 'getAuthorizer'); + let getActionParamsSpy = jest.spyOn(actionparameters_1.ActionParameters, 'getActionParams'); + let getInputSpy = jest.spyOn(core, 'getInput').mockImplementation((name, options) => { + switch (name) { + case 'publish-profile': return 'MOCK_PUBLISH_PROFILE'; + case 'app-name': return 'MOCK_APP_NAME'; + case 'slot-name': return 'MOCK_SLOT_NAME'; + case 'package': return 'MOCK_PACKAGE'; + case 'images': return 'MOCK_IMAGES'; + case 'configuration-file': return 'MOCK_CONFIGFILE'; + case 'startup-command': return 'MOCK_STARTUP_COMMAND'; + } + return ''; + }); + 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').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 { + yield main_1.main(); + } + catch (e) { + console.log(e); + } + expect(getAuthorizerSpy).not.toHaveBeenCalled(); // When publish profile is given as input getAuthorizer is not called + 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(); + })); +}); diff --git a/node_modules/azure-actions-appservice-rest/Kudu/azure-app-kudu-service.d.ts b/node_modules/azure-actions-appservice-rest/Kudu/azure-app-kudu-service.d.ts index 3c976beb3..155a0cc1c 100644 --- a/node_modules/azure-actions-appservice-rest/Kudu/azure-app-kudu-service.d.ts +++ b/node_modules/azure-actions-appservice-rest/Kudu/azure-app-kudu-service.d.ts @@ -7,9 +7,11 @@ export declare class Kudu { constructor(scmUri: string, username: string, password: string); updateDeployment(requestBody: any): Promise; getAppSettings(): Promise>; + getAppRuntime(): Promise; runCommand(physicalPath: string, command: string): Promise; extractZIP(webPackage: string, physicalPath: string): Promise; zipDeploy(webPackage: string, queryParameters?: Array): Promise; + imageDeploy(headers: any): Promise; warDeploy(webPackage: string, queryParameters?: Array): Promise; getDeploymentDetails(deploymentID: string): Promise; getDeploymentLogs(log_url: string): Promise; diff --git a/node_modules/azure-actions-appservice-rest/Kudu/azure-app-kudu-service.js b/node_modules/azure-actions-appservice-rest/Kudu/azure-app-kudu-service.js index aa0fd19c2..08f79ea76 100644 --- a/node_modules/azure-actions-appservice-rest/Kudu/azure-app-kudu-service.js +++ b/node_modules/azure-actions-appservice-rest/Kudu/azure-app-kudu-service.js @@ -63,6 +63,26 @@ class Kudu { } }); } + getAppRuntime() { + return __awaiter(this, void 0, void 0, function* () { + var httpRequest = { + method: 'GET', + uri: this._client.getRequestUri(`/diagnostics/runtime`) + }; + try { + var response = yield this._client.beginRequest(httpRequest); + core.debug(`getAppRuntime. Data: ${JSON.stringify(response)}`); + if (response.statusCode == 200) { + return response.body; + } + throw response; + } + catch (error) { + core.debug("Failed to fetch Kudu App Runtime diagnostics.\n" + this._getFormattedError(error)); + throw Error(error); + } + }); + } runCommand(physicalPath, command) { return __awaiter(this, void 0, void 0, function* () { var httpRequest = { @@ -156,6 +176,27 @@ class Kudu { } }); } + imageDeploy(headers) { + return __awaiter(this, void 0, void 0, function* () { + const _Error = "Error"; + let httpRequest = { + method: 'POST', + uri: this._client.getRequestUri(`/api/app/update`), + headers: headers + }; + let response = yield this._client.beginRequest(httpRequest, null); + core.debug(`Image Deploy response: ${JSON.stringify(response)}`); + if (response.statusCode == 200) { + if (!!response.body && typeof response.body === 'object' && _Error in response.body) { + throw response.body[_Error]; + } + } + else { + throw JSON.stringify(response); + } + core.debug('Deployment passed'); + }); + } warDeploy(webPackage, queryParameters) { return __awaiter(this, void 0, void 0, function* () { let httpRequest = { diff --git a/node_modules/azure-actions-appservice-rest/Utilities/KuduServiceUtility.d.ts b/node_modules/azure-actions-appservice-rest/Utilities/KuduServiceUtility.d.ts index cff5370c5..f22b94a9a 100644 --- a/node_modules/azure-actions-appservice-rest/Utilities/KuduServiceUtility.d.ts +++ b/node_modules/azure-actions-appservice-rest/Utilities/KuduServiceUtility.d.ts @@ -13,4 +13,5 @@ export declare class KuduServiceUtility { private _processDeploymentResponse; private _printZipDeployLogs; private _getUpdateHistoryRequest; + deployWebAppImage(appName: string, images: string, isLinux: boolean): Promise; } diff --git a/node_modules/azure-actions-appservice-rest/Utilities/KuduServiceUtility.js b/node_modules/azure-actions-appservice-rest/Utilities/KuduServiceUtility.js index d01d399b8..d5d6c1504 100644 --- a/node_modules/azure-actions-appservice-rest/Utilities/KuduServiceUtility.js +++ b/node_modules/azure-actions-appservice-rest/Utilities/KuduServiceUtility.js @@ -189,5 +189,23 @@ class KuduServiceUtility { deployer: 'GitHub' }; } + deployWebAppImage(appName, images, isLinux) { + return __awaiter(this, void 0, void 0, function* () { + try { + core.debug(`DeployWebAppImage - appName: ${appName}; images: ${images}; isLinux:${isLinux}`); + if (!isLinux) { + throw new Error("Windows Containerized web app is not available for Publish profile auth scheme."); + } + console.log(`Deploying image ${images} to App Service ${appName}`); + let headers = { 'LinuxFxVersion': `DOCKER|${images}` }; + yield this._webAppKuduService.imageDeploy(headers); + console.log('Successfully deployed image to App Service.'); + } + catch (error) { + core.error('Failed to deploy image to Web app Container.'); + throw error; + } + }); + } } exports.KuduServiceUtility = KuduServiceUtility; diff --git a/node_modules/azure-actions-appservice-rest/package.json b/node_modules/azure-actions-appservice-rest/package.json index 6d44bcc72..941edf34d 100644 --- a/node_modules/azure-actions-appservice-rest/package.json +++ b/node_modules/azure-actions-appservice-rest/package.json @@ -1,37 +1,28 @@ { - "_from": "azure-actions-appservice-rest@^1.0.9", - "_id": "azure-actions-appservice-rest@1.0.9", + "_from": "azure-actions-appservice-rest@^1.2.9", + "_id": "azure-actions-appservice-rest@1.2.9", "_inBundle": false, - "_integrity": "sha512-/Ksj9wpn0GGSMJRaCCmjkO+51FHIjzIhU/zl9BjMSclo7BJT4I3Ge8Maa5aYIZGzYi4fUg+YzV/7uGsV/VPvTQ==", + "_integrity": "sha512-+E8T+dF2gh1GMrvA00KyhN1k7g1ZIfAceE6cF9nRWfNVvF6SHDP3fcdTaodY07rBaFf0buonyfMf3DJRc6m2iw==", "_location": "/azure-actions-appservice-rest", - "_phantomChildren": { - "@actions/core": "1.2.1", - "@actions/exec": "1.0.3", - "@actions/io": "1.0.1", - "fs": "0.0.1-security", - "q": "1.5.1", - "querystring": "0.2.0", - "typed-rest-client": "1.7.1", - "util": "0.12.1" - }, + "_phantomChildren": {}, "_requested": { "type": "range", "registry": true, - "raw": "azure-actions-appservice-rest@^1.0.9", + "raw": "azure-actions-appservice-rest@^1.2.9", "name": "azure-actions-appservice-rest", "escapedName": "azure-actions-appservice-rest", - "rawSpec": "^1.0.9", + "rawSpec": "^1.2.9", "saveSpec": null, - "fetchSpec": "^1.0.9" + "fetchSpec": "^1.2.9" }, "_requiredBy": [ "#USER", "/" ], - "_resolved": "https://registry.npmjs.org/azure-actions-appservice-rest/-/azure-actions-appservice-rest-1.0.9.tgz", - "_shasum": "bb7032919c1d86290e613c34f6f6ac45a49fefe5", - "_spec": "azure-actions-appservice-rest@^1.0.9", - "_where": "D:\\GithubActions\\webapps-deploy", + "_resolved": "https://registry.npmjs.org/azure-actions-appservice-rest/-/azure-actions-appservice-rest-1.2.9.tgz", + "_shasum": "5b708800eac2d1868348497cbdedbc276953b000", + "_spec": "azure-actions-appservice-rest@^1.2.9", + "_where": "C:\\coderepos\\github-actions\\webapps-deploy", "author": { "name": "Sumiran Aggarwal" }, @@ -42,7 +33,7 @@ "dependencies": { "@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", @@ -70,5 +61,5 @@ "dist": "npm run build && npm run copypackage && cd lib && npm publish", "test": "echo \"Error: no test specified\" && exit 1" }, - "version": "1.0.9" + "version": "1.2.9" } diff --git a/package-lock.json b/package-lock.json index ea3bc4288..d4819e583 100644 --- a/package-lock.json +++ b/package-lock.json @@ -81,34 +81,17 @@ } }, "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", "xml2js": "^0.4.22" - }, - "dependencies": { - "azure-actions-webclient": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/azure-actions-webclient/-/azure-actions-webclient-1.0.11.tgz", - "integrity": "sha512-4l0V47W3DDreTFCXEdhWr9LhmgSZecWWyKhm06XOAUodQ82x9ndFFet4OywCDcvQ1u4Gh11xPaSv62FACcgyyw==", - "requires": { - "@actions/core": "^1.1.3", - "@actions/exec": "^1.0.1", - "@actions/io": "^1.0.1", - "fs": "0.0.1-security", - "q": "^1.5.1", - "querystring": "^0.2.0", - "typed-rest-client": "^1.5.0", - "util": "^0.12.1" - } - } } }, "azure-actions-utility": { diff --git a/package.json b/package.json index 074ac8968..d5c60481a 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,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