From 75c6ffd1184f0e40e60ff39213bb5a562747e7f8 Mon Sep 17 00:00:00 2001 From: My Ho Date: Thu, 23 May 2019 12:19:18 -0700 Subject: [PATCH 1/5] feature: change to zipdeploy & run from package 1. remove individual function deploy support --- package-lock.json | 40 ++----------- src/config.ts | 2 +- src/plugins/deploy/azureDeployPlugin.ts | 11 ++-- src/provider/armTemplates/azuredeploy.json | 4 ++ src/services/functionAppService.ts | 68 +++++++++------------- 5 files changed, 44 insertions(+), 81 deletions(-) diff --git a/package-lock.json b/package-lock.json index 90fb83e3..025338f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3491,13 +3491,11 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true, - "optional": true + "bundled": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3510,18 +3508,15 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true, - "optional": true + "bundled": true }, "concat-map": { "version": "0.0.1", - "bundled": true, - "optional": true + "bundled": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true, - "optional": true + "bundled": true }, "core-util-is": { "version": "1.0.2", @@ -3624,8 +3619,7 @@ }, "inherits": { "version": "2.0.3", - "bundled": true, - "optional": true + "bundled": true }, "ini": { "version": "1.3.5", @@ -3635,7 +3629,6 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -3648,7 +3641,6 @@ "minimatch": { "version": "3.0.4", "bundled": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -3748,8 +3740,7 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true, - "optional": true + "bundled": true }, "object-assign": { "version": "4.1.1", @@ -3864,7 +3855,6 @@ "string-width": { "version": "1.0.2", "bundled": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -7000,24 +6990,6 @@ } } }, - "npm-conf": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/npm-conf/-/npm-conf-1.1.3.tgz", - "integrity": "sha512-Yic4bZHJOt9RCFbRP3GgpqhScOY4HH3V2P8yBj6CeYq118Qr+BLXqT2JvpJ00mryLESpgOxf5XlFv4ZjXxLScw==", - "dev": true, - "requires": { - "config-chain": "^1.1.11", - "pify": "^3.0.0" - }, - "dependencies": { - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true - } - } - }, "npm-run-path": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", diff --git a/src/config.ts b/src/config.ts index ba5cf2c6..2d8a8bf5 100644 --- a/src/config.ts +++ b/src/config.ts @@ -13,7 +13,7 @@ export const constants = { scmCommandApiPath: '/api/command', scmDomain: '.scm.azurewebsites.net', scmVfsPath: '/api/vfs/site/wwwroot/', - scmZipApiPath: '/api/zip/site/wwwroot/' + scmZipDeployApiPath: '/api/zipdeploy' }; export default constants; \ No newline at end of file diff --git a/src/plugins/deploy/azureDeployPlugin.ts b/src/plugins/deploy/azureDeployPlugin.ts index c6acbb10..bb20d32b 100644 --- a/src/plugins/deploy/azureDeployPlugin.ts +++ b/src/plugins/deploy/azureDeployPlugin.ts @@ -16,9 +16,8 @@ export class AzureDeployPlugin { const functionAppService = new FunctionAppService(this.serverless, this.options); const functionApp = await functionAppService.get(); - if (functionApp) { - await functionAppService.cleanUp(functionApp); - } + this.serverless.cli.log("NOTHING to do before deploy") + // TODO: maybe this is the place to check if all relevant files exist before uploading } private async deploy() { @@ -26,9 +25,9 @@ export class AzureDeployPlugin { await resourceService.deployResourceGroup(); const functionAppService = new FunctionAppService(this.serverless, this.options); - const functionApp = await functionAppService.deploy(); - await functionAppService.uploadFunctions(functionApp); - await functionAppService.syncTriggers(functionApp); + // create all necessary resources: resource-group, storage account, app service plan, and app service + const functionApp = await functionAppService.deploy(); + await functionAppService.zipDeploy(functionApp); } } diff --git a/src/provider/armTemplates/azuredeploy.json b/src/provider/armTemplates/azuredeploy.json index 023ebf55..32df123f 100644 --- a/src/provider/armTemplates/azuredeploy.json +++ b/src/provider/armTemplates/azuredeploy.json @@ -103,6 +103,10 @@ { "name": "WEBSITE_NODE_DEFAULT_VERSION", "value": "8.11.1" + }, + { + "name": "WEBSITE_RUN_FROM_PACKAGE", + "value": "1" } ] } diff --git a/src/services/functionAppService.ts b/src/services/functionAppService.ts index 84f6eeb8..ca2c2496 100644 --- a/src/services/functionAppService.ts +++ b/src/services/functionAppService.ts @@ -7,6 +7,8 @@ import jsonpath from 'jsonpath'; import _ from 'lodash'; import Serverless from 'serverless'; import { BaseService } from './baseService'; +import * as request from 'request'; +import { constants } from '../config'; export class FunctionAppService extends BaseService { private resourceClient: ResourceManagementClient; @@ -55,49 +57,35 @@ export class FunctionAppService extends BaseService { await this.sendApiRequest('POST', syncTriggersUrl); } - public async cleanUp(functionApp) { - this.serverless.cli.log('Cleaning up existing functions'); - const deleteTasks = []; + async zipDeploy(functionApp) { + const functionAppName = functionApp.name; + this.serverless.cli.log(`Deploying zip file to function app: ${functionAppName}`); - const serviceFunctions = this.serverless.service.getAllFunctions(); - const deployedFunctions = await this.listFunctions(functionApp); - - deployedFunctions.forEach((func) => { - if (serviceFunctions.includes(func.name)) { - this.serverless.cli.log(`-> Deleting function '${func.name}'`); - deleteTasks.push(this.deleteFunction(func.name)); - } - }); - - return await Promise.all(deleteTasks); - } - - public async listFunctions(functionApp) { - const getTokenUrl = `${this.baseUrl}${functionApp.id}/functions?api-version=2016-08-01`; - const response = await this.sendApiRequest('GET', getTokenUrl); - - return response.data.value || []; - } - - public async uploadFunctions(functionApp): Promise { - this.serverless.cli.log('Creating azure functions'); - - const scmDomain = functionApp.enabledHostNames[0]; + // Upload function artifact if it exists, otherwise the full service is handled in 'uploadFunctions' method const functionZipFile = this.serverless.service['artifact']; + if (functionZipFile) { + this.serverless.cli.log(`-> Uploading ${functionZipFile}`); + + const uploadUrl = `https://${functionAppName}${constants.scmDomain}${constants.scmZipDeployApiPath}`; + this.serverless.cli.log(`-> Upload url: ${uploadUrl}`); + + // https://github.com/projectkudu/kudu/wiki/Deploying-from-a-zip-file-or-url + const requestOptions = { + method: 'POST', + uri: uploadUrl, + json: true, + headers: { + Authorization: `Bearer ${this.credentials.tokenCache._entries[0].accessToken}`, + Accept: '*/*', + "Content-type": "application/octet-stream" + } + }; - this.serverless.cli.log(`-> Deploying service package @ ${functionZipFile}`); - - const requestOptions = { - method: 'POST', - uri: `https://${scmDomain}/api/zipdeploy/`, - json: true, - headers: { - Authorization: `Bearer ${this.credentials.tokenCache._entries[0].accessToken}`, - Accept: '*/*' - } - }; - - await this.sendFile(requestOptions, functionZipFile); + await this.sendFile(requestOptions, functionZipFile); + this.serverless.cli.log('-> Function package uploaded successfully'); + } else { + throw new Error('-> No zip file found for function app'); + } } public async deploy() { From 35b24b562b457cf83128db368e6fd13e6df4c8f3 Mon Sep 17 00:00:00 2001 From: My Ho Date: Thu, 23 May 2019 14:59:17 -0700 Subject: [PATCH 2/5] add error handling for zip deploy --- src/services/functionAppService.ts | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/src/services/functionAppService.ts b/src/services/functionAppService.ts index ca2c2496..1b3f2132 100644 --- a/src/services/functionAppService.ts +++ b/src/services/functionAppService.ts @@ -7,7 +7,6 @@ import jsonpath from 'jsonpath'; import _ from 'lodash'; import Serverless from 'serverless'; import { BaseService } from './baseService'; -import * as request from 'request'; import { constants } from '../config'; export class FunctionAppService extends BaseService { @@ -45,18 +44,6 @@ export class FunctionAppService extends BaseService { return response.data.value; } - public async deleteFunction(functionName) { - this.serverless.cli.log(`-> Deleting function: ${functionName}`); - return await this.webClient.webApps.deleteFunction(this.resourceGroup, this.serviceName, functionName); - } - - public async syncTriggers(functionApp) { - this.serverless.cli.log('Syncing function triggers'); - - const syncTriggersUrl = `${this.baseUrl}${functionApp.id}/syncfunctiontriggers?api-version=2016-08-01`; - await this.sendApiRequest('POST', syncTriggersUrl); - } - async zipDeploy(functionApp) { const functionAppName = functionApp.name; this.serverless.cli.log(`Deploying zip file to function app: ${functionAppName}`); @@ -77,14 +64,18 @@ export class FunctionAppService extends BaseService { headers: { Authorization: `Bearer ${this.credentials.tokenCache._entries[0].accessToken}`, Accept: '*/*', - "Content-type": "application/octet-stream" + ContentType: 'application/octet-stream', } }; - await this.sendFile(requestOptions, functionZipFile); - this.serverless.cli.log('-> Function package uploaded successfully'); + try { + await this.sendFile(requestOptions, functionZipFile); + this.serverless.cli.log('-> Function package uploaded successfully'); + } catch (e) { + throw new Error(`Error uploading zip file:\n --> ${e}`); + } } else { - throw new Error('-> No zip file found for function app'); + throw new Error('No zip file found for function app'); } } From 65d59799aae5d58eef6565eea17fec376834d48b Mon Sep 17 00:00:00 2001 From: My Ho Date: Thu, 23 May 2019 15:07:41 -0700 Subject: [PATCH 3/5] clean up --- src/plugins/deploy/azureDeployPlugin.ts | 6 +----- src/services/functionAppService.ts | 4 ++++ 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/plugins/deploy/azureDeployPlugin.ts b/src/plugins/deploy/azureDeployPlugin.ts index bb20d32b..51f4ad31 100644 --- a/src/plugins/deploy/azureDeployPlugin.ts +++ b/src/plugins/deploy/azureDeployPlugin.ts @@ -13,11 +13,8 @@ export class AzureDeployPlugin { } private async beforeDeploy() { - const functionAppService = new FunctionAppService(this.serverless, this.options); - const functionApp = await functionAppService.get(); - - this.serverless.cli.log("NOTHING to do before deploy") // TODO: maybe this is the place to check if all relevant files exist before uploading + this.serverless.cli.log("NOTHING ELSE to do before deploy") } private async deploy() { @@ -26,7 +23,6 @@ export class AzureDeployPlugin { const functionAppService = new FunctionAppService(this.serverless, this.options); - // create all necessary resources: resource-group, storage account, app service plan, and app service const functionApp = await functionAppService.deploy(); await functionAppService.zipDeploy(functionApp); } diff --git a/src/services/functionAppService.ts b/src/services/functionAppService.ts index 1b3f2132..f04e8c54 100644 --- a/src/services/functionAppService.ts +++ b/src/services/functionAppService.ts @@ -79,6 +79,10 @@ export class FunctionAppService extends BaseService { } } + /** + * create all necessary resources as defined in src/provider/armTemplates + * resource-group, storage account, app service plan, and app service at the minimum + */ public async deploy() { this.serverless.cli.log(`Creating function app: ${this.serviceName}`); let parameters: any = { functionAppName: { value: this.serviceName } }; From 66bd3e4c3c0dc6842f31393273f12d1a9b547a9d Mon Sep 17 00:00:00 2001 From: My Ho Date: Fri, 24 May 2019 10:31:26 -0700 Subject: [PATCH 4/5] address W's comments: undo removed functions --- src/plugins/deploy/azureDeployPlugin.ts | 8 +-- src/services/functionAppService.ts | 90 ++++++++++++++++++------- 2 files changed, 66 insertions(+), 32 deletions(-) diff --git a/src/plugins/deploy/azureDeployPlugin.ts b/src/plugins/deploy/azureDeployPlugin.ts index 51f4ad31..56d5803a 100644 --- a/src/plugins/deploy/azureDeployPlugin.ts +++ b/src/plugins/deploy/azureDeployPlugin.ts @@ -7,16 +7,10 @@ export class AzureDeployPlugin { constructor(private serverless: Serverless, private options: Serverless.Options) { this.hooks = { - 'before:deploy:deploy': this.beforeDeploy.bind(this), 'deploy:deploy': this.deploy.bind(this) }; } - private async beforeDeploy() { - // TODO: maybe this is the place to check if all relevant files exist before uploading - this.serverless.cli.log("NOTHING ELSE to do before deploy") - } - private async deploy() { const resourceService = new ResourceService(this.serverless, this.options); await resourceService.deployResourceGroup(); @@ -24,6 +18,6 @@ export class AzureDeployPlugin { const functionAppService = new FunctionAppService(this.serverless, this.options); const functionApp = await functionAppService.deploy(); - await functionAppService.zipDeploy(functionApp); + await functionAppService.uploadFunctions(functionApp); } } diff --git a/src/services/functionAppService.ts b/src/services/functionAppService.ts index f04e8c54..222a5497 100644 --- a/src/services/functionAppService.ts +++ b/src/services/functionAppService.ts @@ -44,38 +44,78 @@ export class FunctionAppService extends BaseService { return response.data.value; } - async zipDeploy(functionApp) { + public async deleteFunction(functionName) { + this.serverless.cli.log(`-> Deleting function: ${functionName}`); + return await this.webClient.webApps.deleteFunction(this.resourceGroup, this.serviceName, functionName); + } + + public async syncTriggers(functionApp) { + this.serverless.cli.log('Syncing function triggers'); + + const syncTriggersUrl = `${this.baseUrl}${functionApp.id}/syncfunctiontriggers?api-version=2016-08-01`; + await this.sendApiRequest('POST', syncTriggersUrl); + } + + public async cleanUp(functionApp) { + this.serverless.cli.log('Cleaning up existing functions'); + const deleteTasks = []; + + const serviceFunctions = this.serverless.service.getAllFunctions(); + const deployedFunctions = await this.listFunctions(functionApp); + + deployedFunctions.forEach((func) => { + if (serviceFunctions.includes(func.name)) { + this.serverless.cli.log(`-> Deleting function '${func.name}'`); + deleteTasks.push(this.deleteFunction(func.name)); + } + }); + + return await Promise.all(deleteTasks); + } + + public async listFunctions(functionApp) { + const getTokenUrl = `${this.baseUrl}${functionApp.id}/functions?api-version=2016-08-01`; + const response = await this.sendApiRequest('GET', getTokenUrl); + + return response.data.value || []; + } + + public async uploadFunctions(functionApp) { + this.zipDeploy(functionApp); + } + + private async zipDeploy(functionApp) { const functionAppName = functionApp.name; this.serverless.cli.log(`Deploying zip file to function app: ${functionAppName}`); // Upload function artifact if it exists, otherwise the full service is handled in 'uploadFunctions' method const functionZipFile = this.serverless.service['artifact']; - if (functionZipFile) { - this.serverless.cli.log(`-> Uploading ${functionZipFile}`); - - const uploadUrl = `https://${functionAppName}${constants.scmDomain}${constants.scmZipDeployApiPath}`; - this.serverless.cli.log(`-> Upload url: ${uploadUrl}`); - - // https://github.com/projectkudu/kudu/wiki/Deploying-from-a-zip-file-or-url - const requestOptions = { - method: 'POST', - uri: uploadUrl, - json: true, - headers: { - Authorization: `Bearer ${this.credentials.tokenCache._entries[0].accessToken}`, - Accept: '*/*', - ContentType: 'application/octet-stream', - } - }; + if (!functionZipFile) { + throw new Error('No zip file found for function app'); + } + + this.serverless.cli.log(`-> Uploading ${functionZipFile}`); - try { - await this.sendFile(requestOptions, functionZipFile); - this.serverless.cli.log('-> Function package uploaded successfully'); - } catch (e) { - throw new Error(`Error uploading zip file:\n --> ${e}`); + const uploadUrl = `https://${functionAppName}${constants.scmDomain}${constants.scmZipDeployApiPath}`; + this.serverless.cli.log(`-> Upload url: ${uploadUrl}`); + + // https://github.com/projectkudu/kudu/wiki/Deploying-from-a-zip-file-or-url + const requestOptions = { + method: 'POST', + uri: uploadUrl, + json: true, + headers: { + Authorization: `Bearer ${this.credentials.tokenCache._entries[0].accessToken}`, + Accept: '*/*', + ContentType: 'application/octet-stream', } - } else { - throw new Error('No zip file found for function app'); + }; + + try { + await this.sendFile(requestOptions, functionZipFile); + this.serverless.cli.log('-> Function package uploaded successfully'); + } catch (e) { + throw new Error(`Error uploading zip file:\n --> ${e}`); } } From 72969c9ae18005d1b93911785b2242d9729358ad Mon Sep 17 00:00:00 2001 From: My Ho Date: Fri, 24 May 2019 10:48:47 -0700 Subject: [PATCH 5/5] wait for call to finish --- src/services/functionAppService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/functionAppService.ts b/src/services/functionAppService.ts index 222a5497..f40e6204 100644 --- a/src/services/functionAppService.ts +++ b/src/services/functionAppService.ts @@ -81,7 +81,7 @@ export class FunctionAppService extends BaseService { } public async uploadFunctions(functionApp) { - this.zipDeploy(functionApp); + await this.zipDeploy(functionApp); } private async zipDeploy(functionApp) {