Skip to content

Commit

Permalink
adding support for flex consumption plan
Browse files Browse the repository at this point in the history
  • Loading branch information
patelchandni committed May 21, 2024
1 parent 9b4104f commit d6e5d5f
Show file tree
Hide file tree
Showing 16 changed files with 269 additions and 14 deletions.
78 changes: 78 additions & 0 deletions .github/workflows/run-e2e-tests-python310-flexcon.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
name: RUN_E2E_TESTS_PYTHON310_FLEXCON
on:
push:
branches:
- master
- dev
- releases/*
paths-ignore:
- '**.md'
pull_request:
paths-ignore:
- '**.md'

env:
AZURE_FUNCTIONAPP_NAME: gae-fa-python310-flexcon
AZURE_FUNCTIONAPP_PACKAGE_PATH: './tests/e2e/python310'
PYTHON_VERSION: '3.10'

jobs:
run:
name: Run E2E Tests
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2

- name: Set Node.js 20.x for GitHub Action
uses: actions/setup-node@v1
with:
node-version: 20.x

- name: Setup Python ${{ env.PYTHON_VERSION }} Environment
uses: actions/setup-python@v1
with:
python-version: ${{ env.PYTHON_VERSION }}

- name: Run Npm Install for GitHub Action
run: npm install

- name: Build GitHub Action
run: npm run build

- name: E2E Resolve Project Dependencies Using Pip
shell: bash
run: |
pushd '${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }}'
python -m pip install --upgrade pip
pip install -r requirements.txt --target=".python_packages/lib/site-packages"
echo "$GITHUB_SHA" > sha.txt
popd
- name: 'Login via Azure CLI'
uses: azure/login@v1
with:
creds: ${{ secrets.RBAC_GAE_FA_PYTHON310_FLEXCON }}

- name: E2E Run Azure Functions Action
uses: ./
id: fa
with:
app-name: ${{ env.AZURE_FUNCTIONAPP_NAME }}
package: ${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }}
remote-build: true

- name: E2E Check HttpTrigger Result
shell: pwsh
run: |
$i = 0
while ($i -lt 10) {
sleep 10
$RESPONSE = $(curl "${{ steps.fa.outputs.app-url }}/api/HttpTrigger")
$RESULT = ($RESPONSE -eq "$env:GITHUB_SHA")
if ($RESULT) {
exit 0
}
$i = $i + 1
}
exit 1
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,13 @@ If `WEBSITE_RUN_FROM_PACKAGE_BLOB_MI_RESOURCE_ID` is defined then user-assigned
- **respect-funcignore**: Allow GitHub Action to parse your .funcignore file and exclude files and folders defined in it. By default, this value is set to `false`. If your GitHub repo contains .funcignore file and want to exclude certain paths (e.g. text editor configs .vscode/, Python virtual environment .venv/), we recommend setting this to `true`.
- **scm-do-build-during-deployment**: Allow Kudu site (e.g. https://your-site-name.scm.azurewebsites.net/) to perform pre-deployment operation. By default, this is set to `false`. If you don't want to resolve the dependencies in the GitHub Workflow, and instead, you want to control the deployments in Kudu / KuduLite, you may want to change this setting to `true`. For more information on SCM_DO_BUILD_DURING_DEPLOYMENT setting, please visit our [Kudu doc](https://github.com/projectkudu/kudu/wiki/Configurable-settings#enabledisable-build-actions).
- **enable-oryx-build**: Allow Kudu site to resolve your project dependencies with [Oryx](https://github.com/Microsoft/Oryx). By default, this is set to `false`. If you want to use Oryx to resolve your dependencies (e.g. [remote build](https://docs.microsoft.com/en-us/azure/azure-functions/functions-deployment-technologies#remote-build)) instead of GitHub Workflow, please consider setting **scm-do-build-during-deployment** and **enable-oryx-build** to `true`.
- **sku**: For function app on Flex Consumption plan, set this to `flexconsumption`. You can skip this parameter for function app on other plans.
If using RBAC credentials, then by default, GitHub Action will resolve the value for this paramter. But if using **publish-profile**,
then you must set this for function app on Flex Consumption plan.
- **remote-build**: For function app on Flex Consumption plan, enable build action from Kudu when the package is deployed onto the function app by setting this to `true`.
Parameter **remote-build** is only supported for function app on Flex Consumption plan. For other plans, you can use **scm-do-build-during-deployment** and **enable-oryx-build**.
For function app on Flex Consumption plan, do not set **scm-do-build-during-deployment** and **enable-oryx-build**.
By default, this is set to `false`.

## Dependencies on other GitHub Actions
* [Checkout](https://github.com/actions/checkout) Checkout your Git repository content into GitHub Actions agent.
Expand Down
13 changes: 13 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,19 @@ inputs:
redundant build action from Kudu endpoint. (default: 'false')."
required: false
default: 'false'
sku:
description: "For function app on Flex Consumption plan, please set this to 'flexconsumption'. You can skip this parameter for function app on other plans.
If using RBAC credentials, then by default, GitHub Action will resolve the value for this paramter. But if using publish-profile,
then you must set this for function app on Flex Consumption plan."
required: false
remote-build:
description: "For function app on Flex Consumption plan, enable build action from Kudu when the package is deployed onto the function app by setting this to 'true'.
Parameter 'remote-build' is only supported for function app on Flex Consumption plan. For other plans, please set 'scm-do-build-during-deployment' and 'enable-oryx-build'.
For function app on Flex Consumption plan, please do not set 'scm-do-build-during-deployment' and 'enable-oryx-build'.
By default, GitHub Action respects the packages resolved in GitHub workflow, disabling the
redundant build action from Kudu endpoint. (default: 'false')."
required: false
default: 'false'
outputs:
app-url:
description: 'URL to work with your function app'
Expand Down
71 changes: 70 additions & 1 deletion src/appservice-rest/Kudu/azure-app-kudu-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@ import { KuduServiceClient } from './KuduServiceClient';
import { exists } from '@actions/io/lib/io-util';

import core = require('@actions/core');
import { CANCELLED } from 'dns';

export const KUDU_DEPLOYMENT_CONSTANTS = {
CANCELLED: -1,
FAILED: 3,
SUCCESS: 4,
FAILED: 3
CONFLICT: 5,
PARTIALSUCCESS: 6
}

export class Kudu {
Expand Down Expand Up @@ -236,6 +240,42 @@ export class Kudu {
}
}

public async oneDeployFlex(webPackage: string, queryParameters?: Array<string>): Promise<any> {
let httpRequest: WebRequest = {
method: 'POST',
uri: this._client.getRequestUri(`/api/publish`, queryParameters),
body: fs.createReadStream(webPackage)
};

try {
let response = await this._client.beginRequest(httpRequest, null, 'application/zip');
core.debug(`One Deploy response: ${JSON.stringify(response)}`);
if(response.statusCode == 200) {
core.debug('Deployment passed');
return null;
}
else if(response.statusCode == 202) {
let deploymentId: string = response.body;
if(!!deploymentId) {
core.debug(`Polling for deployment ID: ${deploymentId}`);
return await this._getDeploymentDetailsFromDeploymentID(deploymentId);
}
else {
core.debug('one deploy returned 202 without deployment ID.');
return null;
}
}
else {
throw response;
}
}
catch(error) {
const deploymentError = new Error("Failed to deploy web package to Function App.\n" + this._getFormattedError(error));
(deploymentError as any).statusCode = error.statusCode;
throw deploymentError;
}
}

public async validateZipDeploy(webPackage: string): Promise<any> {
try {
core.info("Validating deployment package for functions app before Zip Deploy (RBAC)");
Expand Down Expand Up @@ -486,6 +526,35 @@ export class Kudu {
}
}

private async _getDeploymentDetailsFromDeploymentID(deploymentId: string):Promise<any> {
let httpRequest: WebRequest = {
method: 'GET',
uri: this._client.getRequestUri(`/api/deployments/${deploymentId}`),
headers: {}
};

while(true) {
let response = await this._client.beginRequest(httpRequest);
if(response.statusCode == 200 || response.statusCode == 202) {
var result = response.body;
core.debug(`POLL URL RESULT: ${JSON.stringify(response)}`);
if(result.status == KUDU_DEPLOYMENT_CONSTANTS.SUCCESS || result.status == KUDU_DEPLOYMENT_CONSTANTS.PARTIALSUCCESS
|| result.status == KUDU_DEPLOYMENT_CONSTANTS.CANCELLED || result.status == KUDU_DEPLOYMENT_CONSTANTS.CONFLICT
|| result.status == KUDU_DEPLOYMENT_CONSTANTS.FAILED) {
return result;
}
else {
core.debug(`Deployment status: ${result.status} '${result.status_text}'. retry after 5 seconds`);
await this._sleep(5);
continue;
}
}
else {
throw response;
}
}
}

private _getFormattedError(error: any) {
if(error && error.statusCode) {
return `${error.statusMessage} (CODE: ${error.statusCode})`;
Expand Down
22 changes: 22 additions & 0 deletions src/appservice-rest/Utilities/KuduServiceUtility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,28 @@ export class KuduServiceUtility {
}
}

public async deployUsingOneDeployFlex(packagePath: string, remoteBuild: string, customMessage?: any): Promise<string> {
try {
console.log('Package deployment using One Deploy initiated.');

let queryParameters: Array<string> = [
'remoteBuild=' + remoteBuild,
'deployer=' + GITHUB_ZIP_DEPLOY
];
var deploymentMessage = this._getUpdateHistoryRequest(null, null, customMessage).message;
queryParameters.push('message=' + encodeURIComponent(deploymentMessage));
let deploymentDetails = await this._webAppKuduService.oneDeployFlex(packagePath, queryParameters);
await this._processDeploymentResponse(deploymentDetails);

console.log('Successfully deployed web package to Function App.');
return deploymentDetails.id;
}
catch(error) {
core.error('Failed to deploy web package to Function App.');
throw error;
}
}

public async deployUsingWarDeploy(packagePath: string, customMessage?: any, targetFolderName?: any): Promise<string> {
try {
console.log('Package deployment using WAR Deploy initiated.');
Expand Down
2 changes: 2 additions & 0 deletions src/constants/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export class ConfigurationConstant {
public static readonly ParamInRespectFuncignore: string = 'respect-funcignore';
public static readonly ParamInEnableOryxBuild: string = 'enable-oryx-build';
public static readonly ParamInScmDoBuildDuringDeployment: string = 'scm-do-build-during-deployment';
public static readonly ParamInRemoteBuild: string = 'remote-build';
public static readonly ParamInSku: string = 'sku';

public static readonly ParamOutResultName: string = 'app-url';
public static readonly ParamOutPackageUrl: string = 'package-url';
Expand Down
6 changes: 5 additions & 1 deletion src/constants/function_sku.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { UnexpectedConversion } from "../exceptions";
export enum FunctionSkuConstant {
Consumption = 1,
Dedicated,
ElasticPremium
ElasticPremium,
FlexConsumption
}

export class FunctionSkuUtil {
Expand All @@ -15,6 +16,9 @@ export class FunctionSkuUtil {
if (skuLowercasedString.startsWith('elasticpremium')) {
return FunctionSkuConstant.ElasticPremium;
}
if (skuLowercasedString.startsWith('flexconsumption')) {
return FunctionSkuConstant.FlexConsumption;
}
return FunctionSkuConstant.Dedicated;
}
}
5 changes: 4 additions & 1 deletion src/constants/publish_method.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,8 @@ export enum PublishMethodConstant {
ZipDeploy = 1,

// Setting WEBSITE_RUN_FROM_PACKAGE app setting
WebsiteRunFromPackageDeploy
WebsiteRunFromPackageDeploy,

// OneDeploy for function apps on Flex consumption plan
OneDeployFlex
}
26 changes: 20 additions & 6 deletions src/handlers/contentPreparer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { IActionContext } from "../interfaces/IActionContext";
import { IActionParameters } from "../interfaces/IActionParameters";
import { ValidationError, FileIOError } from "../exceptions";
import { PublishMethodConstant } from "../constants/publish_method";
import { FunctionSkuConstant } from "../constants/function_sku";
import { FunctionSkuConstant, FunctionSkuUtil } from '../constants/function_sku';
import { RuntimeStackConstant } from "../constants/runtime_stack";
import { Logger, FuncIgnore } from '../utils';
import { AuthenticationType } from '../constants/authentication_type';
Expand All @@ -25,7 +25,7 @@ export class ContentPreparer implements IOrchestratable {
this.validatePackageType(state, context.package);
this._packageType = context.package.getPackageType();
this._publishContentPath = await this.generatePublishContent(state, params, this._packageType);
this._publishMethod = this.derivePublishMethod(state, this._packageType, context.os, context.sku, context.authenticationType);
this._publishMethod = this.derivePublishMethod(state, this._packageType, context.os, context.sku, context.authenticationType, params);

// Warm up instances
await this.warmUpInstance(params, context);
Expand Down Expand Up @@ -173,7 +173,8 @@ export class ContentPreparer implements IOrchestratable {
packageType: PackageType,
osType: RuntimeStackConstant,
sku: FunctionSkuConstant,
authenticationType: AuthenticationType
authenticationType: AuthenticationType,
params: IActionParameters
): PublishMethodConstant {
// Package Type Check early
if (packageType !== PackageType.zip && packageType !== PackageType.folder) {
Expand All @@ -184,12 +185,25 @@ export class ContentPreparer implements IOrchestratable {
);
}

// Uses api/zipdeploy endpoint if scm credential is provided
if (authenticationType == AuthenticationType.Scm) {
Logger.Info('Will use Kudu https://<scmsite>/api/zipdeploy to deploy since publish-profile is detected.');
return PublishMethodConstant.ZipDeploy;
// Flex Consumption plan uses api/publish endpoint
if (!!params.sku && FunctionSkuUtil.FromString(params.sku) === FunctionSkuConstant.FlexConsumption) {
Logger.Info('Will use Kudu https://<scmsite>/api/publish to deploy since Flex consumption plan is detected.');
return PublishMethodConstant.OneDeployFlex;
}
// Uses api/zipdeploy endpoint if scm credential is provided
else{
Logger.Info('Will use Kudu https://<scmsite>/api/zipdeploy to deploy since publish-profile is detected.');
return PublishMethodConstant.ZipDeploy;
}
}

// Flex Consumption plan uses api/publish endpoint
if (osType === RuntimeStackConstant.Linux && sku === FunctionSkuConstant.FlexConsumption) {
Logger.Info('Will use Kudu https://<scmsite>/api/publish to deploy since Flex consumption plan is detected.');
return PublishMethodConstant.OneDeployFlex;
}

// Linux Consumption sets WEBSITE_RUN_FROM_PACKAGE app settings when scm credential is not available
if (osType === RuntimeStackConstant.Linux && sku === FunctionSkuConstant.Consumption) {
Logger.Info('Will use WEBSITE_RUN_FROM_PACKAGE to deploy since RBAC is detected and your function app is ' +
Expand Down
7 changes: 5 additions & 2 deletions src/handlers/contentPublisher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { IActionParameters } from "../interfaces/IActionParameters";
import { IActionContext } from "../interfaces/IActionContext";
import { PublishMethodConstant } from "../constants/publish_method";
import { ValidationError } from "../exceptions";
import { ZipDeploy, WebsiteRunFromPackageDeploy } from "../publishers";
import { ZipDeploy, WebsiteRunFromPackageDeploy, OneDeployFlex } from "../publishers";

export class ContentPublisher implements IOrchestratable {

Expand All @@ -16,8 +16,11 @@ export class ContentPublisher implements IOrchestratable {
case PublishMethodConstant.WebsiteRunFromPackageDeploy:
await WebsiteRunFromPackageDeploy.execute(state, context);
break;
case PublishMethodConstant.OneDeployFlex:
await OneDeployFlex.execute(state, context, params.remoteBuild);
break;
default:
throw new ValidationError(state, "publisher", "can only performs ZipDeploy or WebsiteRunFromPackageDeploy");
throw new ValidationError(state, "publisher", "can only performs ZipDeploy or WebsiteRunFromPackageDeploy or OneDeploy (for Flex Consumption plan only)");
}
return StateConstant.ValidatePublishedContent;
}
Expand Down
6 changes: 6 additions & 0 deletions src/handlers/parameterValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@ export class ParameterValidator implements IOrchestratable {
private _respectPomXml: string;
private _respectFuncignore: string;
private _scmDoBuildDuringDeployment: string;
private _remoteBuild: string;
private _enableOryxBuild: string;
private _scmCredentials: IScmCredentials
private _sku: string;

constructor() {
this.parseScmCredentials = this.parseScmCredentials.bind(this);
Expand All @@ -42,7 +44,9 @@ export class ParameterValidator implements IOrchestratable {
this._respectPomXml = core.getInput(ConfigurationConstant.ParamInRespectPomXml);
this._respectFuncignore = core.getInput(ConfigurationConstant.ParamInRespectFuncignore);
this._scmDoBuildDuringDeployment = core.getInput(ConfigurationConstant.ParamInScmDoBuildDuringDeployment);
this._remoteBuild = core.getInput(ConfigurationConstant.ParamInRemoteBuild);
this._enableOryxBuild = core.getInput(ConfigurationConstant.ParamInEnableOryxBuild);
this._sku = core.getInput(ConfigurationConstant.ParamInSku);

// Validate field
if (this._slot !== undefined && this._slot.trim() === "") {
Expand All @@ -62,7 +66,9 @@ export class ParameterValidator implements IOrchestratable {
params.respectPomXml = Parser.IsTrueLike(this._respectPomXml);
params.respectFuncignore = Parser.IsTrueLike(this._respectFuncignore);
params.scmDoBuildDuringDeployment = ScmBuildUtil.FromString(this._scmDoBuildDuringDeployment);
params.remoteBuild = Parser.IsTrueLike(this._remoteBuild);
params.enableOryxBuild = EnableOryxBuildUtil.FromString(this._enableOryxBuild);
params.sku = this._sku.toLowerCase();
return params;
}

Expand Down
2 changes: 2 additions & 0 deletions src/interfaces/IActionParameters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,7 @@ export interface IActionParameters {
respectPomXml: boolean;
respectFuncignore: boolean;
scmDoBuildDuringDeployment: ScmBuildConstant;
remoteBuild: boolean;
enableOryxBuild: EnableOryxBuildConstant;
sku: string;
}
Loading

0 comments on commit d6e5d5f

Please sign in to comment.