From 1ece5ed826bcac6494079806707666b798c52ce8 Mon Sep 17 00:00:00 2001 From: My Ho Date: Fri, 31 May 2019 14:40:51 -0700 Subject: [PATCH 01/17] getting region value --- package-lock.json | 1 + src/services/baseService.ts | 1 + src/services/resourceService.ts | 1 + 3 files changed, 3 insertions(+) diff --git a/package-lock.json b/package-lock.json index 98cfecab..67675c8a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9382,6 +9382,7 @@ "lodash": "^4.17.4", "semver": "^5.4.1", "ts-node": "^3.2.0" + "optional": true, } }, "set-blocking": { diff --git a/src/services/baseService.ts b/src/services/baseService.ts index 4a81638e..f6a12821 100644 --- a/src/services/baseService.ts +++ b/src/services/baseService.ts @@ -18,6 +18,7 @@ export abstract class BaseService { authenticate: boolean = true, ) { Guard.null(serverless); + this.setDefaultRegion(); this.baseUrl = "https://management.azure.com"; this.serviceName = serverless.service["service"]; diff --git a/src/services/resourceService.ts b/src/services/resourceService.ts index e36dd909..764ace51 100644 --- a/src/services/resourceService.ts +++ b/src/services/resourceService.ts @@ -1,6 +1,7 @@ import Serverless from "serverless"; import { ResourceManagementClient } from "@azure/arm-resources"; import { BaseService } from "./baseService"; +import { Utils } from "../shared/utils"; export class ResourceService extends BaseService { private resourceClient: ResourceManagementClient; From da3e48fa247f1e4d84b4401ff51c803a2684e3a4 Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Fri, 14 Jun 2019 12:54:23 -0700 Subject: [PATCH 02/17] fix: Standardized on 'region' vs 'location' --- package-lock.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 67675c8a..98cfecab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9382,7 +9382,6 @@ "lodash": "^4.17.4", "semver": "^5.4.1", "ts-node": "^3.2.0" - "optional": true, } }, "set-blocking": { From 2a84590e0e473c6231999bbb7ca28ab2cc6c76e7 Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Fri, 14 Jun 2019 17:07:32 -0700 Subject: [PATCH 03/17] fix: Nomalized location to region --- src/services/resourceService.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/services/resourceService.ts b/src/services/resourceService.ts index 764ace51..e36dd909 100644 --- a/src/services/resourceService.ts +++ b/src/services/resourceService.ts @@ -1,7 +1,6 @@ import Serverless from "serverless"; import { ResourceManagementClient } from "@azure/arm-resources"; import { BaseService } from "./baseService"; -import { Utils } from "../shared/utils"; export class ResourceService extends BaseService { private resourceClient: ResourceManagementClient; From 599e1284f9785453439c716b5754f0f98748361d Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Fri, 14 Jun 2019 17:20:29 -0700 Subject: [PATCH 04/17] fix: Dynamically find scm domain within enabledHostNames --- src/services/functionAppService.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/services/functionAppService.ts b/src/services/functionAppService.ts index 4237f37a..9011542e 100644 --- a/src/services/functionAppService.ts +++ b/src/services/functionAppService.ts @@ -10,6 +10,8 @@ import { BaseService } from "./baseService"; import { FunctionAppHttpTriggerConfig } from "../models/functionApp"; import { Site, FunctionEnvelope } from "@azure/arm-appservice/esm/models"; import { Guard } from "../shared/guard"; +import { stringLiteral } from "@babel/types"; +import { hostname } from "os"; export class FunctionAppService extends BaseService { private resourceClient: ResourceManagementClient; From 2e006830e6292f4419c807080f7003d11b605ce8 Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Wed, 19 Jun 2019 14:17:10 -0700 Subject: [PATCH 05/17] fix: Removed unused references --- src/services/functionAppService.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/services/functionAppService.ts b/src/services/functionAppService.ts index 9011542e..4237f37a 100644 --- a/src/services/functionAppService.ts +++ b/src/services/functionAppService.ts @@ -10,8 +10,6 @@ import { BaseService } from "./baseService"; import { FunctionAppHttpTriggerConfig } from "../models/functionApp"; import { Site, FunctionEnvelope } from "@azure/arm-appservice/esm/models"; import { Guard } from "../shared/guard"; -import { stringLiteral } from "@babel/types"; -import { hostname } from "os"; export class FunctionAppService extends BaseService { private resourceClient: ResourceManagementClient; From de26bdb9786c4c86523e17ca0b8e9b8d3ad425f2 Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Wed, 19 Jun 2019 14:54:42 -0700 Subject: [PATCH 06/17] test: Verify region and state have valid Azure defaults --- src/plugins/deploy/azureDeployPlugin.ts | 5 +++++ src/services/baseService.test.ts | 19 +++++++++++++++++++ src/services/baseService.ts | 1 - 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/plugins/deploy/azureDeployPlugin.ts b/src/plugins/deploy/azureDeployPlugin.ts index 76c88ebc..3d06d44a 100644 --- a/src/plugins/deploy/azureDeployPlugin.ts +++ b/src/plugins/deploy/azureDeployPlugin.ts @@ -47,6 +47,11 @@ export class AzureDeployPlugin { } private async deploy() { + this.serverless.cli.log("OPTIONS"); + this.serverless.cli.log(JSON.stringify(this.options, null, 4)); + this.serverless.cli.log("PROVIDER REGION"); + this.serverless.cli.log(this.serverless.service.provider.region); + const resourceService = new ResourceService(this.serverless, this.options); await resourceService.deployResourceGroup(); diff --git a/src/services/baseService.test.ts b/src/services/baseService.test.ts index ac3f65a2..2ea8af60 100644 --- a/src/services/baseService.test.ts +++ b/src/services/baseService.test.ts @@ -84,6 +84,25 @@ describe("Base Service", () => { expect(props.deploymentName).toEqual(slsConfig.provider.deploymentName); }); + it("Sets default region and stage values if not defined", () => { + const testService = new TestService(sls); + + expect(testService).not.toBeNull(); + expect(sls.service.provider.region).toEqual("westus"); + expect(sls.service.provider.stage).toEqual("dev"); + }); + + it("returns region and stage based on CLI options", () => { + const cliOptions = { + stage: "prod", + region: "eastus2" + }; + const testService = new TestService(sls, cliOptions); + + expect(testService.getSlsRegion()).toEqual(cliOptions.region); + expect(testService.getSlsStage()).toEqual(cliOptions.stage); + }); + it("Sets default region and stage values if not defined", () => { const testService = new MockService(sls); diff --git a/src/services/baseService.ts b/src/services/baseService.ts index f6a12821..4a81638e 100644 --- a/src/services/baseService.ts +++ b/src/services/baseService.ts @@ -18,7 +18,6 @@ export abstract class BaseService { authenticate: boolean = true, ) { Guard.null(serverless); - this.setDefaultRegion(); this.baseUrl = "https://management.azure.com"; this.serviceName = serverless.service["service"]; From faccfea6b8d73228e2df8f4cd6f33e7c7d5d7759 Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Wed, 19 Jun 2019 14:12:25 -0700 Subject: [PATCH 07/17] Started to split out arm templates into smaller resource specific templates --- src/armTemplates/ase-with-apim.json | 340 ++++++++++++++++++ src/armTemplates/ase.js | 26 ++ src/armTemplates/consumption.js | 23 ++ src/armTemplates/premium.js | 23 ++ src/armTemplates/resources/apim.json | 42 +++ src/armTemplates/resources/appInsights.json | 28 ++ .../resources/appServicePlan.json | 42 +++ src/armTemplates/resources/functionApp.json | 83 +++++ .../resources/hostingEnvironment.json | 125 +++++++ .../resources/storageAccount.json | 39 ++ src/models/serverless.ts | 20 +- src/provider/armTemplates/azuredeploy.json | 116 ------ .../armTemplates/azuredeployWithGit.json | 131 ------- src/services/functionAppService.ts | 95 +++-- 14 files changed, 853 insertions(+), 280 deletions(-) create mode 100644 src/armTemplates/ase-with-apim.json create mode 100644 src/armTemplates/ase.js create mode 100644 src/armTemplates/consumption.js create mode 100644 src/armTemplates/premium.js create mode 100644 src/armTemplates/resources/apim.json create mode 100644 src/armTemplates/resources/appInsights.json create mode 100644 src/armTemplates/resources/appServicePlan.json create mode 100644 src/armTemplates/resources/functionApp.json create mode 100644 src/armTemplates/resources/hostingEnvironment.json create mode 100644 src/armTemplates/resources/storageAccount.json delete mode 100644 src/provider/armTemplates/azuredeploy.json delete mode 100644 src/provider/armTemplates/azuredeployWithGit.json diff --git a/src/armTemplates/ase-with-apim.json b/src/armTemplates/ase-with-apim.json new file mode 100644 index 00000000..b4057061 --- /dev/null +++ b/src/armTemplates/ase-with-apim.json @@ -0,0 +1,340 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "functionAppName": { + "defaultValue": "", + "type": "String" + }, + "appServicePlanName": { + "defaultValue": "", + "type": "String" + }, + "apiManagementName": { + "defaultValue": "", + "type": "String" + }, + "hostingEnvironmentName": { + "defaultValue": "", + "type": "String" + }, + "virtualNetworkName": { + "defaultValue": "", + "type": "String" + }, + "storageAccountName": { + "defaultValue": "", + "type": "String" + }, + "appInsightsName": { + "defaultValue": "", + "type": "String" + }, + "location": { + "defaultValue": "", + "type": "String" + } + }, + "variables": {}, + "resources": [ + { + "type": "Microsoft.ApiManagement/service", + "apiVersion": "2018-06-01-preview", + "name": "[parameters('apiManagementName')]", + "location": "[parameters('location')]", + "sku": { + "name": "Consumption", + "capacity": 0 + }, + "properties": { + "publisherEmail": "wabrez@microsoft.com", + "publisherName": "Microsoft", + "notificationSenderEmail": "apimgmt-noreply@mail.windowsazure.com", + "hostnameConfigurations": [], + "virtualNetworkType": "None" + } + }, + { + "type": "microsoft.insights/components", + "apiVersion": "2015-05-01", + "name": "[parameters('appInsightsName')]", + "location": "westus2", + "kind": "other", + "properties": { + "Application_Type": "other", + "Flow_Type": "Redfield", + "Request_Source": "IbizaAIExtension" + } + }, + { + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2018-07-01", + "name": "[parameters('storageAccountName')]", + "location": "[parameters('location')]", + "sku": { + "name": "Standard_LRS", + "tier": "Standard" + }, + "kind": "Storage", + "properties": { + "networkAcls": { + "bypass": "AzureServices", + "virtualNetworkRules": [], + "ipRules": [], + "defaultAction": "Allow" + }, + "supportsHttpsTrafficOnly": false, + "encryption": { + "services": { + "file": { + "enabled": true + }, + "blob": { + "enabled": true + } + }, + "keySource": "Microsoft.Storage" + } + } + }, + { + "type": "Microsoft.Network/virtualNetworks", + "apiVersion": "2018-12-01", + "name": "[parameters('virtualNetworkName')]", + "location": "[parameters('location')]", + "properties": { + "provisioningState": "Succeeded", + "resourceGuid": "b756ff30-43ac-4e83-9794-13011e7884ba", + "addressSpace": { + "addressPrefixes": [ + "172.17.0.0/16" + ] + }, + "subnets": [ + { + "name": "default", + "etag": "W/\"73e9f4aa-86a9-478e-ad11-2db211c9c2e3\"", + "properties": { + "provisioningState": "Succeeded", + "addressPrefix": "172.17.0.0/24", + "resourceNavigationLinks": [ + { + "name": "[concat('MicrosoftWeb_HostingEnvironments_', parameters('hostingEnvironmentName'))]", + "properties": { + "linkedResourceType": "Microsoft.Web/hostingEnvironments", + "link": "[resourceId('Microsoft.Web/hostingEnvironments', parameters('hostingEnvironmentName'))]" + } + } + ], + "serviceEndpoints": [ + { + "provisioningState": "Succeeded", + "service": "Microsoft.Web", + "locations": [ + "*" + ] + } + ], + "delegations": [] + } + } + ], + "virtualNetworkPeerings": [], + "enableDdosProtection": false, + "enableVmProtection": false + } + }, + { + "type": "Microsoft.Web/hostingEnvironments", + "apiVersion": "2016-09-01", + "name": "[parameters('hostingEnvironmentName')]", + "location": "West US", + "dependsOn": [ + "[resourceId('Microsoft.Network/virtualNetworks', parameters('virtualNetworkName'))]" + ], + "kind": "ASEV2", + "zones": [], + "properties": { + "name": "[parameters('hostingEnvironmentName')]", + "location": "West US", + "vnetName": "[parameters('virtualNetworkName')]", + "vnetResourceGroupName": "[resourceGroup().name]", + "vnetSubnetName": "default", + "virtualNetwork": { + "id": "[resourceId('Microsoft.Network/virtualNetworks', parameters('virtualNetworkName'))]", + "subnet": "default" + }, + "internalLoadBalancingMode": "None", + "multiSize": "Standard_D1_V2", + "multiRoleCount": 2, + "ipsslAddressCount": 2, + "dnsSuffix": "[concat(parameters('hostingEnvironmentName'), '.p.azurewebsites.net')]", + "networkAccessControlList": [], + "frontEndScaleFactor": 15, + "suspended": false + } + }, + { + "type": "Microsoft.Web/serverfarms", + "apiVersion": "2016-09-01", + "name": "[parameters('appServicePlanName')]", + "location": "[parameters('location')]", + "dependsOn": [ + "[resourceId('Microsoft.Web/hostingEnvironments', parameters('hostingEnvironmentName'))]" + ], + "sku": { + "name": "I1", + "tier": "Isolated", + "size": "I1", + "family": "I", + "capacity": 3 + }, + "kind": "app", + "properties": { + "name": "[parameters('appServicePlanName')]", + "hostingEnvironmentProfile": { + "id": "[resourceId('Microsoft.Web/hostingEnvironments', parameters('hostingEnvironmentName'))]" + }, + "perSiteScaling": false, + "reserved": false, + "targetWorkerCount": 0, + "targetWorkerSizeId": 0 + } + }, + { + "type": "Microsoft.Web/sites", + "apiVersion": "2016-08-01", + "name": "[parameters('functionAppName')]", + "location": "[parameters('location')]", + "dependsOn": [], + "kind": "functionapp", + "properties": { + "enabled": true, + "hostNameSslStates": [ + { + "name": "[concat(parameters('functionAppName'), '.scm.', parameters('hostingEnvironmentName'), '.p.azurewebsites.net')]", + "sslState": "Disabled", + "hostType": "Repository" + }, + { + "name": "[concat(parameters('functionAppName'), '.', parameters('hostingEnvironmentName'), '.p.azurewebsites.net')]", + "sslState": "Disabled", + "hostType": "Standard" + } + ], + "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', parameters('appServicePlanName'))]", + "reserved": false, + "scmSiteAlsoStopped": false, + "hostingEnvironmentProfile": { + "id": "[resourceId('Microsoft.Web/hostingEnvironments', parameters('hostingEnvironmentName'))]" + }, + "clientAffinityEnabled": false, + "clientCertEnabled": false, + "hostNamesDisabled": false, + "containerSize": 1536, + "dailyMemoryTimeQuota": 0, + "httpsOnly": false, + "siteConfig": { + "appSettings": [ + { + "name": "FUNCTIONS_EXTENSION_VERSION", + "value": "~2" + }, + { + "name": "WEBSITE_NODE_DEFAULT_VERSION", + "value": "8.11.1" + }, + { + "name": "FUNCTIONS_WORKER_RUNTIME", + "value": "node" + }, + { + "name": "AzureWebJobsStorage", + "value": "[concat('DefaultEndpointsProtocol=https;AccountName=',parameters('storageAccountName'),';AccountKey=',listKeys(resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2016-01-01').keys[0].value)]" + }, + { + "name": "AzureWebJobsDashboard", + "value": "[concat('DefaultEndpointsProtocol=https;AccountName=',parameters('storageAccountName'),';AccountKey=',listKeys(resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2016-01-01').keys[0].value)]" + }, + { + "name": "APPINSIGHTS_INSTRUMENTATIONKEY", + "value": "[reference(concat('microsoft.insights/components/', parameters('appInsightsName'))).InstrumentationKey]" + } + ] + } + } + }, + { + "type": "Microsoft.Web/sites/config", + "apiVersion": "2016-08-01", + "name": "[concat(parameters('functionAppName'), '/web')]", + "location": "[parameters('location')]", + "dependsOn": [ + "[resourceId('Microsoft.Web/sites', parameters('functionAppName'))]" + ], + "properties": { + "numberOfWorkers": 1, + "defaultDocuments": [ + "Default.htm", + "Default.html", + "Default.asp", + "index.htm", + "index.html", + "iisstart.htm", + "default.aspx", + "index.php" + ], + "netFrameworkVersion": "v4.0", + "phpVersion": "5.6", + "pythonVersion": "", + "nodeVersion": "", + "linuxFxVersion": "", + "requestTracingEnabled": false, + "remoteDebuggingEnabled": false, + "httpLoggingEnabled": false, + "logsDirectorySizeLimit": 35, + "detailedErrorLoggingEnabled": false, + "publishingUsername": "[concat('$', parameters('functionAppName'))]", + "scmType": "None", + "use32BitWorkerProcess": true, + "webSocketsEnabled": false, + "alwaysOn": true, + "appCommandLine": "", + "managedPipelineMode": "Integrated", + "virtualApplications": [ + { + "virtualPath": "/", + "physicalPath": "site\\wwwroot", + "preloadEnabled": true, + "virtualDirectories": null + } + ], + "winAuthAdminState": 0, + "winAuthTenantState": 0, + "customAppPoolIdentityAdminState": false, + "customAppPoolIdentityTenantState": false, + "loadBalancing": "LeastRequests", + "routingRules": [], + "experiments": { + "rampUpRules": [] + }, + "autoHealEnabled": false, + "virtualNetworkName": "", + "siteAuthEnabled": false, + "cors": { + "allowedOrigins": [ + "https://functions.azure.com", + "https://functions-staging.azure.com", + "https://functions-next.azure.com" + ], + "supportCredentials": false + }, + "localMySqlEnabled": false, + "http20Enabled": false, + "minTlsVersion": "1.2", + "ftpsState": "AllAllowed", + "reservedInstanceCount": 0 + } + } + ] +} \ No newline at end of file diff --git a/src/armTemplates/ase.js b/src/armTemplates/ase.js new file mode 100644 index 00000000..c768e023 --- /dev/null +++ b/src/armTemplates/ase.js @@ -0,0 +1,26 @@ +import * as functionApp from "./resources/functionApp.json"; +import * as appInsights from "./resources/appInsights.json"; +import * as storage from "./resources/storage.json"; +import * as appServicePlan from "./resources/appServicePlan.json"; +import * as hostingEnvironment from "./resources/hostingEnvironment.json"; + +export function generate() { + return { + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + ...functionApp.parameters, + ...appInsights.parameters, + ...storage.parameters, + ...appServicePlan.parameters, + ...hostingEnvironment.parameters, + }, + "resources": [ + ...functionApp.resources, + ...appInsights.resources, + ...storage.resources, + ...appServicePlan.resources, + ...hostingEnvironment.resources, + ], + }; +} \ No newline at end of file diff --git a/src/armTemplates/consumption.js b/src/armTemplates/consumption.js new file mode 100644 index 00000000..644c36b8 --- /dev/null +++ b/src/armTemplates/consumption.js @@ -0,0 +1,23 @@ +import * as functionApp from "./resources/functionApp.json"; +import * as appInsights from "./resources/appInsights.json"; +import * as storage from "./resources/storage.json"; +import * as appServicePlan from "./resources/appServicePlan.json"; + +export function generate() { + return { + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + ...functionApp.parameters, + ...appInsights.parameters, + ...storage.parameters, + ...appServicePlan.parameters, + }, + "resources": [ + ...functionApp.resources, + ...appInsights.resources, + ...storage.resources, + ...appServicePlan.resources + ], + }; +} \ No newline at end of file diff --git a/src/armTemplates/premium.js b/src/armTemplates/premium.js new file mode 100644 index 00000000..644c36b8 --- /dev/null +++ b/src/armTemplates/premium.js @@ -0,0 +1,23 @@ +import * as functionApp from "./resources/functionApp.json"; +import * as appInsights from "./resources/appInsights.json"; +import * as storage from "./resources/storage.json"; +import * as appServicePlan from "./resources/appServicePlan.json"; + +export function generate() { + return { + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + ...functionApp.parameters, + ...appInsights.parameters, + ...storage.parameters, + ...appServicePlan.parameters, + }, + "resources": [ + ...functionApp.resources, + ...appInsights.resources, + ...storage.resources, + ...appServicePlan.resources + ], + }; +} \ No newline at end of file diff --git a/src/armTemplates/resources/apim.json b/src/armTemplates/resources/apim.json new file mode 100644 index 00000000..665d5475 --- /dev/null +++ b/src/armTemplates/resources/apim.json @@ -0,0 +1,42 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "apiManagementName": { + "defaultValue": "", + "type": "String" + }, + "location": { + "defaultValue": "", + "type": "String" + }, + "apimSkuName": { + "defaultValue": "Consumption", + "type": "String" + }, + "apimCapacity": { + "defaultValue": 0, + "type": "int" + } + }, + "variables": {}, + "resources": [ + { + "type": "Microsoft.ApiManagement/service", + "apiVersion": "2018-06-01-preview", + "name": "[parameters('apiManagementName')]", + "location": "[parameters('location')]", + "sku": { + "name": "[parameters('apimSkuName')]", + "capacity": "[parameters('apimCapacity')]" + }, + "properties": { + "publisherEmail": "wabrez@microsoft.com", + "publisherName": "Microsoft", + "notificationSenderEmail": "apimgmt-noreply@mail.windowsazure.com", + "hostnameConfigurations": [], + "virtualNetworkType": "None" + } + } + ] +} \ No newline at end of file diff --git a/src/armTemplates/resources/appInsights.json b/src/armTemplates/resources/appInsights.json new file mode 100644 index 00000000..7288415f --- /dev/null +++ b/src/armTemplates/resources/appInsights.json @@ -0,0 +1,28 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "appInsightsName": { + "defaultValue": "", + "type": "String" + }, + "location": { + "defaultValue": "", + "type": "String" + } + }, + "variables": {}, + "resources": [ + { + "apiVersion": "2015-05-01", + "name": "[parameters('appInsightsName')]", + "type": "microsoft.insights/components", + "location": "[parameters('location')]", + "properties": { + "Application_Type": "web", + "ApplicationId": "[parameters('appInsightsName')]", + "Request_Source": "IbizaWebAppExtensionCreate" + } + } + ] +} \ No newline at end of file diff --git a/src/armTemplates/resources/appServicePlan.json b/src/armTemplates/resources/appServicePlan.json new file mode 100644 index 00000000..d9a438e7 --- /dev/null +++ b/src/armTemplates/resources/appServicePlan.json @@ -0,0 +1,42 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "appServicePlanName": { + "defaultValue": "", + "type": "String" + }, + "location": { + "defaultValue": "", + "type": "String" + }, + "appServicePlanSkuName": { + "defaultValue": "EP1", + "type": "String" + }, + "appServicePlanSkuTier": { + "defaultValue": "ElasticPremium", + "type": "String" + } + }, + "variables": {}, + "resources": [ + { + "apiVersion": "2016-09-01", + "name": "[parameters('appServicePlanName')]", + "type": "Microsoft.Web/serverfarms", + "location": "[parameters('location')]", + "properties": { + "name": "[parameters('appServicePlanName')]", + "workerSizeId": "3", + "numberOfWorkers": "1", + "maximumElasticWorkerCount": "10", + "hostingEnvironment": "" + }, + "sku": { + "name": "[parameters('appServicePlanSkuName')]", + "tier": "[parameters('appServicePlanSkuTier')]" + } + } + ] +} \ No newline at end of file diff --git a/src/armTemplates/resources/functionApp.json b/src/armTemplates/resources/functionApp.json new file mode 100644 index 00000000..3d58e32d --- /dev/null +++ b/src/armTemplates/resources/functionApp.json @@ -0,0 +1,83 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "functionAppName": { + "defaultValue": "", + "type": "String" + }, + "appServicePlanName": { + "defaultValue": "", + "type": "String" + }, + "storageAccountName": { + "defaultValue": "", + "type": "String" + }, + "appInsightsName": { + "defaultValue": "", + "type": "String" + }, + "location": { + "defaultValue": "", + "type": "String" + } + }, + "variables": {}, + "resources": [ + { + "type": "Microsoft.Web/sites", + "apiVersion": "2016-03-01", + "name": "[parameters('functionAppName')]", + "location": "[parameters('location')]", + "dependsOn": [ + "[concat('Microsoft.Web/serverfarms/', parameters('appServicePlanName'))]", + "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName'))]", + "[concat('microsoft.insights/components/', parameters('appInsightsName'))]" + ], + "kind": "functionapp", + "properties": { + "siteConfig": { + "appSettings": [ + { + "name": "FUNCTIONS_WORKER_RUNTIME", + "value": "node" + }, + { + "name": "FUNCTIONS_EXTENSION_VERSION", + "value": "~2" + }, + { + "name": "AzureWebJobsStorage", + "value": "[concat('DefaultEndpointsProtocol=https;AccountName=',parameters('storageAccountName'),';AccountKey=',listKeys(resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2016-01-01').keys[0].value)]" + }, + { + "name": "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING", + "value": "[concat('DefaultEndpointsProtocol=https;AccountName=',parameters('storageAccountName'),';AccountKey=',listKeys(resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2016-01-01').keys[0].value)]" + }, + { + "name": "WEBSITE_CONTENTSHARE", + "value": "[toLower(parameters('functionAppName'))]" + }, + { + "name": "WEBSITE_NODE_DEFAULT_VERSION", + "value": "10.14.1" + }, + { + "name": "WEBSITE_RUN_FROM_PACKAGE", + "value": "1" + }, + { + "name": "APPINSIGHTS_INSTRUMENTATIONKEY", + "value": "[reference(concat('microsoft.insights/components/', parameters('appInsightsName'))).InstrumentationKey]" + } + ] + }, + "name": "[parameters('functionAppName')]", + "clientAffinityEnabled": false, + "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', parameters('appServicePlanName'))]", + "hostingEnvironment": "" + } + } + ] +} \ No newline at end of file diff --git a/src/armTemplates/resources/hostingEnvironment.json b/src/armTemplates/resources/hostingEnvironment.json new file mode 100644 index 00000000..8b944a4d --- /dev/null +++ b/src/armTemplates/resources/hostingEnvironment.json @@ -0,0 +1,125 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "hostingEnvironmentName": { + "defaultValue": "", + "type": "String" + }, + "virtualNetworkName": { + "defaultValue": "", + "type": "String" + }, + "location": { + "defaultValue": "", + "type": "String" + } + }, + "variables": {}, + "resources": [ + { + "type": "Microsoft.Network/virtualNetworks", + "apiVersion": "2018-12-01", + "name": "[parameters('virtualNetworkName')]", + "location": "[parameters('location')]", + "properties": { + "provisioningState": "Succeeded", + "resourceGuid": "b756ff30-43ac-4e83-9794-13011e7884ba", + "addressSpace": { + "addressPrefixes": [ + "172.17.0.0/16" + ] + }, + "subnets": [ + { + "name": "default", + "etag": "W/\"73e9f4aa-86a9-478e-ad11-2db211c9c2e3\"", + "properties": { + "provisioningState": "Succeeded", + "addressPrefix": "172.17.0.0/24", + "resourceNavigationLinks": [ + { + "name": "[concat('MicrosoftWeb_HostingEnvironments_', parameters('hostingEnvironmentName'))]", + "properties": { + "linkedResourceType": "Microsoft.Web/hostingEnvironments", + "link": "[resourceId('Microsoft.Web/hostingEnvironments', parameters('hostingEnvironmentName'))]" + } + } + ], + "serviceEndpoints": [ + { + "provisioningState": "Succeeded", + "service": "Microsoft.Web", + "locations": [ + "*" + ] + } + ], + "delegations": [] + } + } + ], + "virtualNetworkPeerings": [], + "enableDdosProtection": false, + "enableVmProtection": false + } + }, + { + "type": "Microsoft.Web/hostingEnvironments", + "apiVersion": "2016-09-01", + "name": "[parameters('hostingEnvironmentName')]", + "location": "West US", + "dependsOn": [ + "[resourceId('Microsoft.Network/virtualNetworks', parameters('virtualNetworkName'))]" + ], + "kind": "ASEV2", + "zones": [], + "properties": { + "name": "[parameters('hostingEnvironmentName')]", + "location": "West US", + "vnetName": "[parameters('virtualNetworkName')]", + "vnetResourceGroupName": "[resourceGroup().name]", + "vnetSubnetName": "default", + "virtualNetwork": { + "id": "[resourceId('Microsoft.Network/virtualNetworks', parameters('virtualNetworkName'))]", + "subnet": "default" + }, + "internalLoadBalancingMode": "None", + "multiSize": "Standard_D1_V2", + "multiRoleCount": 2, + "ipsslAddressCount": 2, + "dnsSuffix": "[concat(parameters('hostingEnvironmentName'), '.p.azurewebsites.net')]", + "networkAccessControlList": [], + "frontEndScaleFactor": 15, + "suspended": false + } + }, + { + "type": "Microsoft.Web/serverfarms", + "apiVersion": "2016-09-01", + "name": "[parameters('appServicePlanName')]", + "location": "[parameters('location')]", + "dependsOn": [ + "[resourceId('Microsoft.Web/hostingEnvironments', parameters('hostingEnvironmentName'))]" + ], + "sku": { + "name": "I1", + "tier": "Isolated", + "size": "I1", + "family": "I", + "capacity": 3 + }, + "kind": "app", + "properties": { + "name": "[parameters('appServicePlanName')]", + "hostingEnvironmentProfile": { + "id": "[resourceId('Microsoft.Web/hostingEnvironments', parameters('hostingEnvironmentName'))]" + }, + "perSiteScaling": false, + "reserved": false, + "targetWorkerCount": 0, + "targetWorkerSizeId": 0 + } + } + ] +} \ No newline at end of file diff --git a/src/armTemplates/resources/storageAccount.json b/src/armTemplates/resources/storageAccount.json new file mode 100644 index 00000000..7699dcfe --- /dev/null +++ b/src/armTemplates/resources/storageAccount.json @@ -0,0 +1,39 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "storageAccountName": { + "defaultValue": "", + "type": "String" + }, + "location": { + "defaultValue": "", + "type": "String" + }, + "storageAccountSkuName": { + "defaultValue": "Standard_LRS", + "type": "String" + }, + "storageAccoutSkuTier": { + "defaultValue": "Standard", + "type": "String" + } + }, + "variables": {}, + "resources": [ + { + "apiVersion": "2018-07-01", + "name": "[parameters('storageAccountName')]", + "type": "Microsoft.Storage/storageAccounts", + "location": "[parameters('location')]", + "kind": "Storage", + "properties": { + "accountType": "[parameters('storageAccountSkuName')]" + }, + "sku": { + "name": "[parameters('storageAccountSkuName')]", + "tier": "[parameters('storageAccoutSkuTier')]" + } + } + ] +} \ No newline at end of file diff --git a/src/models/serverless.ts b/src/models/serverless.ts index debd6df5..6eeafcff 100644 --- a/src/models/serverless.ts +++ b/src/models/serverless.ts @@ -1,7 +1,25 @@ +import { ApiManagementConfig } from "./apiManagement"; + +export enum DeploymentType { + Consumption, + Premium, +} + +export interface ArmTemplateConfig { + type: string; + file: string; + parameters: + { + [key: string]: string; + }; +} + export interface ServerlessAzureConfig { provider: { name: string; - location: string; + region: string; + apim?: ApiManagementConfig; + armTemplate?: ArmTemplateConfig; }; plugins: string[]; functions: any; diff --git a/src/provider/armTemplates/azuredeploy.json b/src/provider/armTemplates/azuredeploy.json deleted file mode 100644 index 32df123f..00000000 --- a/src/provider/armTemplates/azuredeploy.json +++ /dev/null @@ -1,116 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "functionAppName": { - "type": "string", - "metadata": { - "description": "The name of the function app that you wish to create." - } - }, - "storageAccountType": { - "type": "string", - "defaultValue": "Standard_LRS", - "allowedValues": [ - "Standard_LRS", - "Standard_GRS", - "Standard_ZRS", - "Premium_LRS" - ], - "metadata": { - "description": "Storage Account type" - } - } - }, - "variables": { - "functionAppName": "[parameters('functionAppName')]", - "hostingPlanName": "[parameters('functionAppName')]", - "insightsName": "[concat(parameters('functionAppName'), '-insights')]", - "storageAccountName": "[concat(uniquestring(resourceGroup().id), 'azfunctions')]", - "storageAccountid": "[concat(resourceGroup().id,'/providers/','Microsoft.Storage/storageAccounts/', variables('storageAccountName'))]" - }, - "resources": [ - { - "type": "microsoft.insights/components", - "kind": "Node.JS", - "name": "[variables('insightsName')]", - "apiVersion": "2014-04-01", - "location": "eastus", - "properties": { - "ApplicationId": "[variables('insightsName')]" - } - }, - { - "type": "Microsoft.Storage/storageAccounts", - "name": "[variables('storageAccountName')]", - "apiVersion": "2015-06-15", - "location": "[resourceGroup().location]", - "properties": { - "accountType": "[parameters('storageAccountType')]" - } - }, - { - "type": "Microsoft.Web/serverfarms", - "apiVersion": "2015-04-01", - "name": "[variables('hostingPlanName')]", - "location": "[resourceGroup().location]", - "properties": { - "name": "[variables('hostingPlanName')]", - "computeMode": "Dynamic", - "sku": "Dynamic" - } - }, - { - "apiVersion": "2015-08-01", - "type": "Microsoft.Web/sites", - "name": "[variables('functionAppName')]", - "location": "[resourceGroup().location]", - "kind": "functionapp", - "dependsOn": [ - "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]", - "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]", - "[resourceId('Microsoft.insights/components', variables('insightsName'))]" - ], - "properties": { - "clientAffinityEnabled": false, - "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]", - "siteConfig": { - "appSettings": [ - { - "name": "APPINSIGHTS_INSTRUMENTATIONKEY", - "value": "[reference(variables('insightsName')).InstrumentationKey]" - }, - { - "name": "AzureWebJobsDashboard", - "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(variables('storageAccountid'),'2015-05-01-preview').key1)]" - }, - { - "name": "AzureWebJobsStorage", - "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(variables('storageAccountid'),'2015-05-01-preview').key1)]" - }, - { - "name": "FUNCTIONS_EXTENSION_VERSION", - "value": "~2" - }, - { - "name": "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING", - "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(variables('storageAccountid'),'2015-05-01-preview').key1)]" - }, - { - "name": "WEBSITE_CONTENTSHARE", - "value": "[toLower(variables('functionAppName'))]" - }, - { - "name": "WEBSITE_NODE_DEFAULT_VERSION", - "value": "8.11.1" - }, - { - "name": "WEBSITE_RUN_FROM_PACKAGE", - "value": "1" - } - ] - } - } - } - ] -} diff --git a/src/provider/armTemplates/azuredeployWithGit.json b/src/provider/armTemplates/azuredeployWithGit.json deleted file mode 100644 index 651f9187..00000000 --- a/src/provider/armTemplates/azuredeployWithGit.json +++ /dev/null @@ -1,131 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "functionAppName": { - "type": "string", - "metadata": { - "description": "The name of the function app that you wish to create." - } - }, - "storageAccountType": { - "type": "string", - "defaultValue": "Standard_LRS", - "allowedValues": [ - "Standard_LRS", - "Standard_GRS", - "Standard_ZRS", - "Premium_LRS" - ], - "metadata": { - "description": "Storage Account type" - } - }, - "gitUrl": { - "type": "string", - "metadata": { - "description": "Git URL" - } - } - }, - "variables": { - "functionAppName": "[parameters('functionAppName')]", - "hostingPlanName": "[parameters('functionAppName')]", - "insightsName": "[concat(parameters('functionAppName'), '-insights')]", - "storageAccountName": "[concat(uniquestring(resourceGroup().id), 'azfunctions')]", - "storageAccountid": "[concat(resourceGroup().id,'/providers/','Microsoft.Storage/storageAccounts/', variables('storageAccountName'))]", - "gitRepoUrl": "[parameters('gitUrl')]" - }, - "resources": [ - { - "type": "microsoft.insights/components", - "kind": "Node.JS", - "name": "[variables('insightsName')]", - "apiVersion": "2014-04-01", - "location": "eastus", - "properties": { - "ApplicationId": "[variables('insightsName')]" - } - }, - { - "type": "Microsoft.Storage/storageAccounts", - "name": "[variables('storageAccountName')]", - "apiVersion": "2015-06-15", - "location": "[resourceGroup().location]", - "properties": { - "accountType": "[parameters('storageAccountType')]" - } - }, - { - "type": "Microsoft.Web/serverfarms", - "apiVersion": "2015-04-01", - "name": "[variables('hostingPlanName')]", - "location": "[resourceGroup().location]", - "properties": { - "name": "[variables('hostingPlanName')]", - "computeMode": "Dynamic", - "sku": "Dynamic" - } - }, - { - "apiVersion": "2015-08-01", - "name": "web", - "type": "sourcecontrols", - "dependsOn": [ - "[resourceId('Microsoft.Web/sites', variables('functionAppName'))]" - ], - "properties": { - "repoUrl": "[variables('gitRepoUrl')]", - "IsManualIntegration": true - } - }, - { - "apiVersion": "2015-08-01", - "type": "Microsoft.Web/sites", - "name": "[variables('functionAppName')]", - "location": "[resourceGroup().location]", - "kind": "functionapp", - "dependsOn": [ - "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]", - "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]", - "[resourceId('Microsoft.insights/components', variables('insightsName'))]" - ], - "properties": { - "clientAffinityEnabled": false, - "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]", - "siteConfig": { - "appSettings": [ - { - "name": "APPINSIGHTS_INSTRUMENTATIONKEY", - "value": "[reference(variables('insightsName')).InstrumentationKey]" - }, - { - "name": "AzureWebJobsDashboard", - "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(variables('storageAccountid'),'2015-05-01-preview').key1)]" - }, - { - "name": "AzureWebJobsStorage", - "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(variables('storageAccountid'),'2015-05-01-preview').key1)]" - }, - { - "name": "FUNCTIONS_EXTENSION_VERSION", - "value": "~1" - }, - { - "name": "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING", - "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(variables('storageAccountid'),'2015-05-01-preview').key1)]" - }, - { - "name": "WEBSITE_CONTENTSHARE", - "value": "[toLower(variables('functionAppName'))]" - }, - { - "name": "WEBSITE_NODE_DEFAULT_VERSION", - "value": "6.5.0" - } - ] - } - } - } - ] -} diff --git a/src/services/functionAppService.ts b/src/services/functionAppService.ts index 4237f37a..c99d556a 100644 --- a/src/services/functionAppService.ts +++ b/src/services/functionAppService.ts @@ -123,16 +123,17 @@ export class FunctionAppService extends BaseService { */ public async deploy() { this.log(`Creating function app: ${this.serviceName}`); - let parameters: any = { functionAppName: { value: this.serviceName } }; - const gitUrl = this.serverless.service.provider["gitUrl"]; + const parameters = this.serverless.service.provider["armTemplate"]["parameters"]; - if (gitUrl) { - parameters = { - functionAppName: { value: this.serviceName }, - gitUrl: { value: gitUrl } - }; - } + // const gitUrl = this.serverless.service.provider["gitUrl"]; + + // if (gitUrl) { + // parameters = { + // functionAppName: { value: this.serviceName }, + // gitUrl: { value: gitUrl } + // }; + // } let templateFilePath = path.join(__dirname, "..", "provider", "armTemplates", "azuredeploy.json"); @@ -140,22 +141,66 @@ export class FunctionAppService extends BaseService { templateFilePath = path.join(__dirname, "armTemplates", "azuredeployWithGit.json"); } - if (this.serverless.service.provider["armTemplate"]) { - this.log(`-> Deploying custom ARM template: ${this.serverless.service.provider["armTemplate"].file}`); - templateFilePath = path.join(this.serverless.config.servicePath, this.serverless.service.provider["armTemplate"].file); - const userParameters = this.serverless.service.provider["armTemplate"].parameters; - const userParametersKeys = Object.keys(userParameters); + // Deploy ARM template + await this.resourceClient.deployments.createOrUpdate(this.resourceGroup, this.deploymentName, deploymentParameters); + + // Return function app + return await this.get(); + } + + private buildArmTemplateFromConfig(type: string): Deployment { + const apim = require("../armTemplates/resources/apim.json"); + const template = require(`../armTemplates/${type}`); + + if (this.serverless.service.provider["apim"]) { + template.parameters = { + ...template.parameters + apim.parameters + }; + template.resources = [ + ...template.resources, + apim.resources, + ] + } - for (let paramIndex = 0; paramIndex < userParametersKeys.length; paramIndex++) { - const item = {}; + this.applyAppSettings(template); - item[userParametersKeys[paramIndex]] = { "value": userParameters[userParametersKeys[paramIndex]] }; - parameters = _.merge(parameters, item); + return { + properties: { + mode: "Incremental", + parameters, + template } + }; + } + + private getDeploymentFromCustomArmTemplate(): Deployment { + const templateFilePath = path.join(this.serverless.config.servicePath, this.serverless.service.provider["armTemplate"].file); + const template = JSON.parse(fs.readFileSync(templateFilePath, "utf8")); + const userParameters = this.serverless.service.provider["armTemplate"].parameters; + const userParametersKeys = Object.keys(userParameters); + + let parameters = {}; + + for (let paramIndex = 0; paramIndex < userParametersKeys.length; paramIndex++) { + const item = {}; + + item[userParametersKeys[paramIndex]] = { "value": userParameters[userParametersKeys[paramIndex]] }; + parameters = _.merge(parameters, item); } - let template = JSON.parse(fs.readFileSync(templateFilePath, "utf8")); + this.applyAppSettings(template); + + return { + properties: { + mode: "Incremental", + parameters, + template + } + }; + } + private applyAppSettings(template: any) { // Check if there are custom environment variables defined that need to be // added to the ARM template used in the deployment. const environmentVariables = this.serverless.service.provider["environment"]; @@ -173,20 +218,6 @@ export class FunctionAppService extends BaseService { return appSettingsList; }); } - - const deploymentParameters: Deployment = { - properties: { - mode: "Incremental", - parameters, - template - } - }; - - // Deploy ARM template - await this.resourceClient.deployments.createOrUpdate(this.resourceGroup, this.deploymentName, deploymentParameters); - - // Return function app - return await this.get(); } private async zipDeploy(functionApp) { From d7619f0991286eb9c4528cc9a609a82defa3ccb2 Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Wed, 19 Jun 2019 18:01:09 -0700 Subject: [PATCH 08/17] test: Verify ARM templates are constructed with correct resources --- src/armTemplates/ase-with-apim.json | 340 ------------------ src/armTemplates/{ase.js => ase.ts} | 17 +- .../{consumption.js => consumption.ts} | 13 +- src/armTemplates/{premium.js => premium.ts} | 15 +- .../resources/hostingEnvironment.json | 31 +- src/services/apimService.test.ts | 14 +- src/services/armService.test.ts | 96 +++++ src/services/armService.ts | 62 ++++ src/services/functionAppService.ts | 95 ++--- src/test/mockFactory.ts | 18 +- 10 files changed, 236 insertions(+), 465 deletions(-) delete mode 100644 src/armTemplates/ase-with-apim.json rename src/armTemplates/{ase.js => ase.ts} (53%) rename src/armTemplates/{consumption.js => consumption.ts} (55%) rename src/armTemplates/{premium.js => premium.ts} (53%) create mode 100644 src/services/armService.test.ts create mode 100644 src/services/armService.ts diff --git a/src/armTemplates/ase-with-apim.json b/src/armTemplates/ase-with-apim.json deleted file mode 100644 index b4057061..00000000 --- a/src/armTemplates/ase-with-apim.json +++ /dev/null @@ -1,340 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "functionAppName": { - "defaultValue": "", - "type": "String" - }, - "appServicePlanName": { - "defaultValue": "", - "type": "String" - }, - "apiManagementName": { - "defaultValue": "", - "type": "String" - }, - "hostingEnvironmentName": { - "defaultValue": "", - "type": "String" - }, - "virtualNetworkName": { - "defaultValue": "", - "type": "String" - }, - "storageAccountName": { - "defaultValue": "", - "type": "String" - }, - "appInsightsName": { - "defaultValue": "", - "type": "String" - }, - "location": { - "defaultValue": "", - "type": "String" - } - }, - "variables": {}, - "resources": [ - { - "type": "Microsoft.ApiManagement/service", - "apiVersion": "2018-06-01-preview", - "name": "[parameters('apiManagementName')]", - "location": "[parameters('location')]", - "sku": { - "name": "Consumption", - "capacity": 0 - }, - "properties": { - "publisherEmail": "wabrez@microsoft.com", - "publisherName": "Microsoft", - "notificationSenderEmail": "apimgmt-noreply@mail.windowsazure.com", - "hostnameConfigurations": [], - "virtualNetworkType": "None" - } - }, - { - "type": "microsoft.insights/components", - "apiVersion": "2015-05-01", - "name": "[parameters('appInsightsName')]", - "location": "westus2", - "kind": "other", - "properties": { - "Application_Type": "other", - "Flow_Type": "Redfield", - "Request_Source": "IbizaAIExtension" - } - }, - { - "type": "Microsoft.Storage/storageAccounts", - "apiVersion": "2018-07-01", - "name": "[parameters('storageAccountName')]", - "location": "[parameters('location')]", - "sku": { - "name": "Standard_LRS", - "tier": "Standard" - }, - "kind": "Storage", - "properties": { - "networkAcls": { - "bypass": "AzureServices", - "virtualNetworkRules": [], - "ipRules": [], - "defaultAction": "Allow" - }, - "supportsHttpsTrafficOnly": false, - "encryption": { - "services": { - "file": { - "enabled": true - }, - "blob": { - "enabled": true - } - }, - "keySource": "Microsoft.Storage" - } - } - }, - { - "type": "Microsoft.Network/virtualNetworks", - "apiVersion": "2018-12-01", - "name": "[parameters('virtualNetworkName')]", - "location": "[parameters('location')]", - "properties": { - "provisioningState": "Succeeded", - "resourceGuid": "b756ff30-43ac-4e83-9794-13011e7884ba", - "addressSpace": { - "addressPrefixes": [ - "172.17.0.0/16" - ] - }, - "subnets": [ - { - "name": "default", - "etag": "W/\"73e9f4aa-86a9-478e-ad11-2db211c9c2e3\"", - "properties": { - "provisioningState": "Succeeded", - "addressPrefix": "172.17.0.0/24", - "resourceNavigationLinks": [ - { - "name": "[concat('MicrosoftWeb_HostingEnvironments_', parameters('hostingEnvironmentName'))]", - "properties": { - "linkedResourceType": "Microsoft.Web/hostingEnvironments", - "link": "[resourceId('Microsoft.Web/hostingEnvironments', parameters('hostingEnvironmentName'))]" - } - } - ], - "serviceEndpoints": [ - { - "provisioningState": "Succeeded", - "service": "Microsoft.Web", - "locations": [ - "*" - ] - } - ], - "delegations": [] - } - } - ], - "virtualNetworkPeerings": [], - "enableDdosProtection": false, - "enableVmProtection": false - } - }, - { - "type": "Microsoft.Web/hostingEnvironments", - "apiVersion": "2016-09-01", - "name": "[parameters('hostingEnvironmentName')]", - "location": "West US", - "dependsOn": [ - "[resourceId('Microsoft.Network/virtualNetworks', parameters('virtualNetworkName'))]" - ], - "kind": "ASEV2", - "zones": [], - "properties": { - "name": "[parameters('hostingEnvironmentName')]", - "location": "West US", - "vnetName": "[parameters('virtualNetworkName')]", - "vnetResourceGroupName": "[resourceGroup().name]", - "vnetSubnetName": "default", - "virtualNetwork": { - "id": "[resourceId('Microsoft.Network/virtualNetworks', parameters('virtualNetworkName'))]", - "subnet": "default" - }, - "internalLoadBalancingMode": "None", - "multiSize": "Standard_D1_V2", - "multiRoleCount": 2, - "ipsslAddressCount": 2, - "dnsSuffix": "[concat(parameters('hostingEnvironmentName'), '.p.azurewebsites.net')]", - "networkAccessControlList": [], - "frontEndScaleFactor": 15, - "suspended": false - } - }, - { - "type": "Microsoft.Web/serverfarms", - "apiVersion": "2016-09-01", - "name": "[parameters('appServicePlanName')]", - "location": "[parameters('location')]", - "dependsOn": [ - "[resourceId('Microsoft.Web/hostingEnvironments', parameters('hostingEnvironmentName'))]" - ], - "sku": { - "name": "I1", - "tier": "Isolated", - "size": "I1", - "family": "I", - "capacity": 3 - }, - "kind": "app", - "properties": { - "name": "[parameters('appServicePlanName')]", - "hostingEnvironmentProfile": { - "id": "[resourceId('Microsoft.Web/hostingEnvironments', parameters('hostingEnvironmentName'))]" - }, - "perSiteScaling": false, - "reserved": false, - "targetWorkerCount": 0, - "targetWorkerSizeId": 0 - } - }, - { - "type": "Microsoft.Web/sites", - "apiVersion": "2016-08-01", - "name": "[parameters('functionAppName')]", - "location": "[parameters('location')]", - "dependsOn": [], - "kind": "functionapp", - "properties": { - "enabled": true, - "hostNameSslStates": [ - { - "name": "[concat(parameters('functionAppName'), '.scm.', parameters('hostingEnvironmentName'), '.p.azurewebsites.net')]", - "sslState": "Disabled", - "hostType": "Repository" - }, - { - "name": "[concat(parameters('functionAppName'), '.', parameters('hostingEnvironmentName'), '.p.azurewebsites.net')]", - "sslState": "Disabled", - "hostType": "Standard" - } - ], - "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', parameters('appServicePlanName'))]", - "reserved": false, - "scmSiteAlsoStopped": false, - "hostingEnvironmentProfile": { - "id": "[resourceId('Microsoft.Web/hostingEnvironments', parameters('hostingEnvironmentName'))]" - }, - "clientAffinityEnabled": false, - "clientCertEnabled": false, - "hostNamesDisabled": false, - "containerSize": 1536, - "dailyMemoryTimeQuota": 0, - "httpsOnly": false, - "siteConfig": { - "appSettings": [ - { - "name": "FUNCTIONS_EXTENSION_VERSION", - "value": "~2" - }, - { - "name": "WEBSITE_NODE_DEFAULT_VERSION", - "value": "8.11.1" - }, - { - "name": "FUNCTIONS_WORKER_RUNTIME", - "value": "node" - }, - { - "name": "AzureWebJobsStorage", - "value": "[concat('DefaultEndpointsProtocol=https;AccountName=',parameters('storageAccountName'),';AccountKey=',listKeys(resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2016-01-01').keys[0].value)]" - }, - { - "name": "AzureWebJobsDashboard", - "value": "[concat('DefaultEndpointsProtocol=https;AccountName=',parameters('storageAccountName'),';AccountKey=',listKeys(resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2016-01-01').keys[0].value)]" - }, - { - "name": "APPINSIGHTS_INSTRUMENTATIONKEY", - "value": "[reference(concat('microsoft.insights/components/', parameters('appInsightsName'))).InstrumentationKey]" - } - ] - } - } - }, - { - "type": "Microsoft.Web/sites/config", - "apiVersion": "2016-08-01", - "name": "[concat(parameters('functionAppName'), '/web')]", - "location": "[parameters('location')]", - "dependsOn": [ - "[resourceId('Microsoft.Web/sites', parameters('functionAppName'))]" - ], - "properties": { - "numberOfWorkers": 1, - "defaultDocuments": [ - "Default.htm", - "Default.html", - "Default.asp", - "index.htm", - "index.html", - "iisstart.htm", - "default.aspx", - "index.php" - ], - "netFrameworkVersion": "v4.0", - "phpVersion": "5.6", - "pythonVersion": "", - "nodeVersion": "", - "linuxFxVersion": "", - "requestTracingEnabled": false, - "remoteDebuggingEnabled": false, - "httpLoggingEnabled": false, - "logsDirectorySizeLimit": 35, - "detailedErrorLoggingEnabled": false, - "publishingUsername": "[concat('$', parameters('functionAppName'))]", - "scmType": "None", - "use32BitWorkerProcess": true, - "webSocketsEnabled": false, - "alwaysOn": true, - "appCommandLine": "", - "managedPipelineMode": "Integrated", - "virtualApplications": [ - { - "virtualPath": "/", - "physicalPath": "site\\wwwroot", - "preloadEnabled": true, - "virtualDirectories": null - } - ], - "winAuthAdminState": 0, - "winAuthTenantState": 0, - "customAppPoolIdentityAdminState": false, - "customAppPoolIdentityTenantState": false, - "loadBalancing": "LeastRequests", - "routingRules": [], - "experiments": { - "rampUpRules": [] - }, - "autoHealEnabled": false, - "virtualNetworkName": "", - "siteAuthEnabled": false, - "cors": { - "allowedOrigins": [ - "https://functions.azure.com", - "https://functions-staging.azure.com", - "https://functions-next.azure.com" - ], - "supportCredentials": false - }, - "localMySqlEnabled": false, - "http20Enabled": false, - "minTlsVersion": "1.2", - "ftpsState": "AllAllowed", - "reservedInstanceCount": 0 - } - } - ] -} \ No newline at end of file diff --git a/src/armTemplates/ase.js b/src/armTemplates/ase.ts similarity index 53% rename from src/armTemplates/ase.js rename to src/armTemplates/ase.ts index c768e023..db3c66d4 100644 --- a/src/armTemplates/ase.js +++ b/src/armTemplates/ase.ts @@ -1,11 +1,11 @@ -import * as functionApp from "./resources/functionApp.json"; -import * as appInsights from "./resources/appInsights.json"; -import * as storage from "./resources/storage.json"; -import * as appServicePlan from "./resources/appServicePlan.json"; -import * as hostingEnvironment from "./resources/hostingEnvironment.json"; +import functionApp from "./resources/functionApp.json"; +import appInsights from "./resources/appInsights.json"; +import storage from "./resources/storageAccount.json"; +import appServicePlan from "./resources/appServicePlan.json"; +import hostingEnvironment from "./resources/hostingEnvironment.json"; export function generate() { - return { + const template = { "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": { @@ -23,4 +23,9 @@ export function generate() { ...hostingEnvironment.resources, ], }; + + template.parameters.appServicePlanSkuName.defaultValue = "I1"; + template.parameters.appServicePlanSkuTier.defaultValue = "Isolated"; + + return template; } \ No newline at end of file diff --git a/src/armTemplates/consumption.js b/src/armTemplates/consumption.ts similarity index 55% rename from src/armTemplates/consumption.js rename to src/armTemplates/consumption.ts index 644c36b8..33d81de2 100644 --- a/src/armTemplates/consumption.js +++ b/src/armTemplates/consumption.ts @@ -1,23 +1,22 @@ -import * as functionApp from "./resources/functionApp.json"; -import * as appInsights from "./resources/appInsights.json"; -import * as storage from "./resources/storage.json"; -import * as appServicePlan from "./resources/appServicePlan.json"; +import functionApp from "./resources/functionApp.json"; +import appInsights from "./resources/appInsights.json"; +import storage from "./resources/storageAccount.json"; export function generate() { - return { + const template = { "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": { ...functionApp.parameters, ...appInsights.parameters, ...storage.parameters, - ...appServicePlan.parameters, }, "resources": [ ...functionApp.resources, ...appInsights.resources, ...storage.resources, - ...appServicePlan.resources ], }; + + return template; } \ No newline at end of file diff --git a/src/armTemplates/premium.js b/src/armTemplates/premium.ts similarity index 53% rename from src/armTemplates/premium.js rename to src/armTemplates/premium.ts index 644c36b8..eeee9281 100644 --- a/src/armTemplates/premium.js +++ b/src/armTemplates/premium.ts @@ -1,10 +1,10 @@ -import * as functionApp from "./resources/functionApp.json"; -import * as appInsights from "./resources/appInsights.json"; -import * as storage from "./resources/storage.json"; -import * as appServicePlan from "./resources/appServicePlan.json"; +import functionApp from "./resources/functionApp.json"; +import appInsights from "./resources/appInsights.json"; +import storage from "./resources/storageAccount.json"; +import appServicePlan from "./resources/appServicePlan.json"; export function generate() { - return { + const template = { "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": { @@ -20,4 +20,9 @@ export function generate() { ...appServicePlan.resources ], }; + + template.parameters.appServicePlanSkuName.defaultValue = "EP1"; + template.parameters.appServicePlanSkuTier.defaultValue = "ElasticPremium"; + + return template; } \ No newline at end of file diff --git a/src/armTemplates/resources/hostingEnvironment.json b/src/armTemplates/resources/hostingEnvironment.json index 8b944a4d..5cbb4f20 100644 --- a/src/armTemplates/resources/hostingEnvironment.json +++ b/src/armTemplates/resources/hostingEnvironment.json @@ -68,7 +68,7 @@ "type": "Microsoft.Web/hostingEnvironments", "apiVersion": "2016-09-01", "name": "[parameters('hostingEnvironmentName')]", - "location": "West US", + "location": "[parameters('location')]", "dependsOn": [ "[resourceId('Microsoft.Network/virtualNetworks', parameters('virtualNetworkName'))]" ], @@ -76,7 +76,7 @@ "zones": [], "properties": { "name": "[parameters('hostingEnvironmentName')]", - "location": "West US", + "location": "[parameters('location')]", "vnetName": "[parameters('virtualNetworkName')]", "vnetResourceGroupName": "[resourceGroup().name]", "vnetSubnetName": "default", @@ -93,33 +93,6 @@ "frontEndScaleFactor": 15, "suspended": false } - }, - { - "type": "Microsoft.Web/serverfarms", - "apiVersion": "2016-09-01", - "name": "[parameters('appServicePlanName')]", - "location": "[parameters('location')]", - "dependsOn": [ - "[resourceId('Microsoft.Web/hostingEnvironments', parameters('hostingEnvironmentName'))]" - ], - "sku": { - "name": "I1", - "tier": "Isolated", - "size": "I1", - "family": "I", - "capacity": 3 - }, - "kind": "app", - "properties": { - "name": "[parameters('appServicePlanName')]", - "hostingEnvironmentProfile": { - "id": "[resourceId('Microsoft.Web/hostingEnvironments', parameters('hostingEnvironmentName'))]" - }, - "perSiteScaling": false, - "reserved": false, - "targetWorkerCount": 0, - "targetWorkerSizeId": 0 - } } ] } \ No newline at end of file diff --git a/src/services/apimService.test.ts b/src/services/apimService.test.ts index 1b14fe4b..028b8ec4 100644 --- a/src/services/apimService.test.ts +++ b/src/services/apimService.test.ts @@ -1,7 +1,6 @@ import Serverless from "serverless"; import _ from "lodash"; import { MockFactory } from "../test/mockFactory"; -import { ApiManagementConfig } from "../models/apiManagement"; import { ApimService } from "./apimService"; import { interpolateJson } from "../test/utils"; import axios from "axios"; @@ -22,18 +21,7 @@ import { } from "@azure/arm-apimanagement/esm/models"; describe("APIM Service", () => { - const apimConfig: ApiManagementConfig = { - name: "test-apim-resource", - api: { - name: "test-apim-api1", - subscriptionRequired: false, - displayName: "API 1", - description: "description of api 1", - protocols: ["https"], - path: "test-api1", - }, - }; - + const apimConfig = MockFactory.createTestApimConfig(); let serverless: Serverless; beforeEach(() => { diff --git a/src/services/armService.test.ts b/src/services/armService.test.ts new file mode 100644 index 00000000..4d36b086 --- /dev/null +++ b/src/services/armService.test.ts @@ -0,0 +1,96 @@ +import Serverless from "serverless"; +import { MockFactory } from "../test/mockFactory"; +import { ArmService } from "./armService"; + +describe("Arm Service", () => { + let sls: Serverless + let service: ArmService; + + function createService() { + return new ArmService(sls); + } + + beforeEach(() => { + sls = MockFactory.createTestServerless(); + sls.variables = { + ...sls.variables, + azureCredentials: MockFactory.createTestAzureCredentials(), + subscriptionId: "ABC123", + }; + + service = createService(); + }) + + describe("Creating Templates", () => { + it("Creates a custom ARM template from well-known type", async () => { + const template = await service.createTemplate("premium"); + + expect(template).not.toBeNull(); + expect(Object.keys(template.parameters).length).toBeGreaterThan(0); + expect(template.resources.length).toBeGreaterThan(0); + }); + + it("Creates a custom ARM template (with APIM support) from well-known type", async () => { + sls.service.provider["apim"] = MockFactory.createTestApimConfig(); + const template = await service.createTemplate("premium"); + + expect(template).not.toBeNull(); + expect(Object.keys(template.parameters).length).toBeGreaterThan(0); + expect(template.resources.length).toBeGreaterThan(0); + + expect(template.resources.find((resource) => resource.type === "Microsoft.ApiManagement/service")).not.toBeNull(); + }); + + it("throws error when specified type is not found", async () => { + await expect(service.createTemplate("not-found")).rejects.not.toBeNull(); + }); + + it("Premium template includes correct resources", async () => { + const template = await service.createTemplate("premium"); + + expect(template.parameters.appServicePlanSkuTier.defaultValue).toEqual("ElasticPremium"); + expect(template.parameters.appServicePlanSkuName.defaultValue).toEqual("EP1"); + + // Should not contain + expect(template.resources.find((resource) => resource.type === "Microsoft.Web/hostingEnvironments")).toBeUndefined(); + expect(template.resources.find((resource) => resource.type === "Microsoft.Network/virtualNetworks")).toBeUndefined(); + + // Should contain + expect(template.resources.find((resource) => resource.type === "Microsoft.Web/serverfarms")).not.toBeNull(); + expect(template.resources.find((resource) => resource.type === "Microsoft.Web/sites")).not.toBeNull(); + expect(template.resources.find((resource) => resource.type === "Microsoft.Storage/storageAccounts")).not.toBeNull(); + expect(template.resources.find((resource) => resource.type === "microsoft.insights/components")).not.toBeNull(); + }); + + it("ASE template includes correct resources", async () => { + const template = await service.createTemplate("ase"); + + expect(template.parameters.appServicePlanSkuTier.defaultValue).toEqual("Isolated"); + expect(template.parameters.appServicePlanSkuName.defaultValue).toEqual("I1"); + + expect(template.resources.find((resource) => resource.type === "Microsoft.Web/hostingEnvironments")).not.toBeNull(); + expect(template.resources.find((resource) => resource.type === "Microsoft.Network/virtualNetworks")).not.toBeNull(); + expect(template.resources.find((resource) => resource.type === "Microsoft.Web/serverfarms")).not.toBeNull(); + expect(template.resources.find((resource) => resource.type === "Microsoft.Web/sites")).not.toBeNull(); + expect(template.resources.find((resource) => resource.type === "Microsoft.Storage/storageAccounts")).not.toBeNull(); + expect(template.resources.find((resource) => resource.type === "microsoft.insights/components")).not.toBeNull(); + }); + + it("Consumption template includes correct resources", async () => { + const template = await service.createTemplate("consumption"); + + expect(template.resources.find((resource) => resource.type === "Microsoft.Web/hostingEnvironments")).toBeUndefined(); + expect(template.resources.find((resource) => resource.type === "Microsoft.Network/virtualNetworks")).toBeUndefined(); + expect(template.resources.find((resource) => resource.type === "Microsoft.Web/serverfarms")).toBeUndefined(); + + // Should contain + expect(template.resources.find((resource) => resource.type === "Microsoft.Web/sites")).not.toBeNull(); + expect(template.resources.find((resource) => resource.type === "Microsoft.Storage/storageAccounts")).not.toBeNull(); + expect(template.resources.find((resource) => resource.type === "microsoft.insights/components")).not.toBeNull(); + }); + }); + + describe("Deploying Templates", () => { + + }); +}); \ No newline at end of file diff --git a/src/services/armService.ts b/src/services/armService.ts new file mode 100644 index 00000000..184ba700 --- /dev/null +++ b/src/services/armService.ts @@ -0,0 +1,62 @@ +import Serverless from "serverless"; +import { Deployment } from "@azure/arm-resources/esm/models"; +import { BaseService } from "./baseService"; +import { ResourceManagementClient } from "@azure/arm-resources"; +import { Guard } from "../shared/guard"; + +export interface ArmTemplateGenerator { + generate(): any; +} + +export class ArmService extends BaseService { + private resourceClient: ResourceManagementClient; + + public constructor(serverless: Serverless) { + super(serverless); + this.resourceClient = new ResourceManagementClient(this.credentials, this.subscriptionId); + } + + public async createTemplate(type: string): Promise { + Guard.empty(type); + + const apim = await import("../armTemplates/resources/apim.json"); + let template: ArmTemplateGenerator; + + try { + template = await import(`../armTemplates/${type}`); + } catch (e) { + throw new Error(`Unable to find template with name ${type} `); + } + + const mergedTemplate = template.generate(); + + if (this.serverless.service.provider["apim"]) { + mergedTemplate.parameters = { + ...mergedTemplate.parameters, + ...apim.parameters, + }; + mergedTemplate.resources = [ + ...mergedTemplate.resources, + ...apim.resources, + ] + } + + return mergedTemplate; + } + + public async deployTemplate(template: any, parameters: string[]) { + Guard.null(template); + Guard.null(parameters); + + const deploymentParameters: Deployment = { + properties: { + mode: "Incremental", + parameters, + template + } + }; + + // Deploy ARM template + await this.resourceClient.deployments.createOrUpdate(this.resourceGroup, this.deploymentName, deploymentParameters); + } +} \ No newline at end of file diff --git a/src/services/functionAppService.ts b/src/services/functionAppService.ts index c99d556a..4237f37a 100644 --- a/src/services/functionAppService.ts +++ b/src/services/functionAppService.ts @@ -123,17 +123,16 @@ export class FunctionAppService extends BaseService { */ public async deploy() { this.log(`Creating function app: ${this.serviceName}`); + let parameters: any = { functionAppName: { value: this.serviceName } }; - const parameters = this.serverless.service.provider["armTemplate"]["parameters"]; + const gitUrl = this.serverless.service.provider["gitUrl"]; - // const gitUrl = this.serverless.service.provider["gitUrl"]; - - // if (gitUrl) { - // parameters = { - // functionAppName: { value: this.serviceName }, - // gitUrl: { value: gitUrl } - // }; - // } + if (gitUrl) { + parameters = { + functionAppName: { value: this.serviceName }, + gitUrl: { value: gitUrl } + }; + } let templateFilePath = path.join(__dirname, "..", "provider", "armTemplates", "azuredeploy.json"); @@ -141,66 +140,22 @@ export class FunctionAppService extends BaseService { templateFilePath = path.join(__dirname, "armTemplates", "azuredeployWithGit.json"); } - // Deploy ARM template - await this.resourceClient.deployments.createOrUpdate(this.resourceGroup, this.deploymentName, deploymentParameters); - - // Return function app - return await this.get(); - } - - private buildArmTemplateFromConfig(type: string): Deployment { - const apim = require("../armTemplates/resources/apim.json"); - const template = require(`../armTemplates/${type}`); - - if (this.serverless.service.provider["apim"]) { - template.parameters = { - ...template.parameters - apim.parameters - }; - template.resources = [ - ...template.resources, - apim.resources, - ] - } + if (this.serverless.service.provider["armTemplate"]) { + this.log(`-> Deploying custom ARM template: ${this.serverless.service.provider["armTemplate"].file}`); + templateFilePath = path.join(this.serverless.config.servicePath, this.serverless.service.provider["armTemplate"].file); + const userParameters = this.serverless.service.provider["armTemplate"].parameters; + const userParametersKeys = Object.keys(userParameters); - this.applyAppSettings(template); + for (let paramIndex = 0; paramIndex < userParametersKeys.length; paramIndex++) { + const item = {}; - return { - properties: { - mode: "Incremental", - parameters, - template + item[userParametersKeys[paramIndex]] = { "value": userParameters[userParametersKeys[paramIndex]] }; + parameters = _.merge(parameters, item); } - }; - } - - private getDeploymentFromCustomArmTemplate(): Deployment { - const templateFilePath = path.join(this.serverless.config.servicePath, this.serverless.service.provider["armTemplate"].file); - const template = JSON.parse(fs.readFileSync(templateFilePath, "utf8")); - const userParameters = this.serverless.service.provider["armTemplate"].parameters; - const userParametersKeys = Object.keys(userParameters); - - let parameters = {}; - - for (let paramIndex = 0; paramIndex < userParametersKeys.length; paramIndex++) { - const item = {}; - - item[userParametersKeys[paramIndex]] = { "value": userParameters[userParametersKeys[paramIndex]] }; - parameters = _.merge(parameters, item); } - this.applyAppSettings(template); - - return { - properties: { - mode: "Incremental", - parameters, - template - } - }; - } + let template = JSON.parse(fs.readFileSync(templateFilePath, "utf8")); - private applyAppSettings(template: any) { // Check if there are custom environment variables defined that need to be // added to the ARM template used in the deployment. const environmentVariables = this.serverless.service.provider["environment"]; @@ -218,6 +173,20 @@ export class FunctionAppService extends BaseService { return appSettingsList; }); } + + const deploymentParameters: Deployment = { + properties: { + mode: "Incremental", + parameters, + template + } + }; + + // Deploy ARM template + await this.resourceClient.deployments.createOrUpdate(this.resourceGroup, this.deploymentName, deploymentParameters); + + // Return function app + return await this.get(); } private async zipDeploy(functionApp) { diff --git a/src/test/mockFactory.ts b/src/test/mockFactory.ts index 2a7ede24..b859d0db 100644 --- a/src/test/mockFactory.ts +++ b/src/test/mockFactory.ts @@ -12,7 +12,7 @@ import PluginManager from "serverless/lib/classes/PluginManager"; import { ServerlessAzureConfig } from "../models/serverless"; import { AzureServiceProvider, ServicePrincipalEnvVariables } from "../models/azureProvider" import { Logger } from "../models/generic"; -import { ApiCorsPolicy } from "../models/apiManagement"; +import { ApiCorsPolicy, ApiManagementConfig } from "../models/apiManagement"; import { DeploymentsListByResourceGroupResponse } from "@azure/arm-resources/esm/models"; function getAttribute(object: any, prop: string, defaultValue: any): any { @@ -208,6 +208,20 @@ export class MockFactory { return (asYaml) ? yaml.dump(data) : data; } + public static createTestApimConfig(): ApiManagementConfig { + return { + name: "test-apim-resource", + api: { + name: "test-apim-api1", + subscriptionRequired: false, + displayName: "API 1", + description: "description of api 1", + protocols: ["https"], + path: "test-api1", + }, + }; + } + public static createTestFunctionApimConfig(name: string) { return { apim: { @@ -432,7 +446,7 @@ export class MockFactory { allowedOrigins: ["*"], allowedHeaders: ["*"], exposeHeaders: ["*"], - allowedMethods: ["GET","POST"], + allowedMethods: ["GET", "POST"], }; } From f94b1190ef01b8e52e4a8cd9064e9c9a667c5f57 Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Thu, 20 Jun 2019 17:58:05 -0700 Subject: [PATCH 09/17] fix: Applied composite arm template builder pattern --- src/armTemplates/ase.ts | 91 ++++++++++------ src/armTemplates/consumption.ts | 73 +++++++++---- src/armTemplates/premium.ts | 84 ++++++++++----- src/armTemplates/resources/apim.json | 42 -------- src/armTemplates/resources/apim.ts | 64 +++++++++++ src/armTemplates/resources/appInsights.json | 28 ----- src/armTemplates/resources/appInsights.ts | 46 ++++++++ .../resources/appServicePlan.json | 42 -------- src/armTemplates/resources/appServicePlan.ts | 63 +++++++++++ src/armTemplates/resources/functionApp.json | 83 -------------- src/armTemplates/resources/functionApp.ts | 101 ++++++++++++++++++ .../resources/hostingEnvironment.json | 98 ----------------- .../resources/hostingEnvironment.ts | 70 ++++++++++++ .../resources/storageAccount.json | 39 ------- src/armTemplates/resources/storageAccount.ts | 60 +++++++++++ src/armTemplates/resources/virtualNetwork.ts | 87 +++++++++++++++ src/models/apiManagement.ts | 4 + src/models/serverless.ts | 22 +++- src/services/armService.test.ts | 73 +++++++------ src/services/armService.ts | 66 ++++++++---- src/services/functionAppService.ts | 43 +++----- 21 files changed, 782 insertions(+), 497 deletions(-) delete mode 100644 src/armTemplates/resources/apim.json create mode 100644 src/armTemplates/resources/apim.ts delete mode 100644 src/armTemplates/resources/appInsights.json create mode 100644 src/armTemplates/resources/appInsights.ts delete mode 100644 src/armTemplates/resources/appServicePlan.json create mode 100644 src/armTemplates/resources/appServicePlan.ts delete mode 100644 src/armTemplates/resources/functionApp.json create mode 100644 src/armTemplates/resources/functionApp.ts delete mode 100644 src/armTemplates/resources/hostingEnvironment.json create mode 100644 src/armTemplates/resources/hostingEnvironment.ts delete mode 100644 src/armTemplates/resources/storageAccount.json create mode 100644 src/armTemplates/resources/storageAccount.ts create mode 100644 src/armTemplates/resources/virtualNetwork.ts diff --git a/src/armTemplates/ase.ts b/src/armTemplates/ase.ts index db3c66d4..a9b31ed1 100644 --- a/src/armTemplates/ase.ts +++ b/src/armTemplates/ase.ts @@ -1,31 +1,60 @@ -import functionApp from "./resources/functionApp.json"; -import appInsights from "./resources/appInsights.json"; -import storage from "./resources/storageAccount.json"; -import appServicePlan from "./resources/appServicePlan.json"; -import hostingEnvironment from "./resources/hostingEnvironment.json"; - -export function generate() { - const template = { - "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "parameters": { - ...functionApp.parameters, - ...appInsights.parameters, - ...storage.parameters, - ...appServicePlan.parameters, - ...hostingEnvironment.parameters, - }, - "resources": [ - ...functionApp.resources, - ...appInsights.resources, - ...storage.resources, - ...appServicePlan.resources, - ...hostingEnvironment.resources, - ], - }; - - template.parameters.appServicePlanSkuName.defaultValue = "I1"; - template.parameters.appServicePlanSkuTier.defaultValue = "Isolated"; - - return template; -} \ No newline at end of file +import { FunctionAppResource } from "./resources/functionApp"; +import { AppInsightsResource } from "./resources/appInsights"; +import { StorageAccountResource } from "./resources/storageAccount"; +import { AppServicePlanResource } from "./resources/appServicePlan"; +import { HostingEnvironmentResource } from "./resources/hostingEnvironment"; +import { VirtualNetworkResource } from "./resources/virtualNetwork"; +import { ArmResourceTemplateGenerator } from "../services/armService.js"; +import { ServerlessAzureConfig } from "../models/serverless.js"; + +const resources: ArmResourceTemplateGenerator[] = [ + FunctionAppResource, + AppInsightsResource, + StorageAccountResource, + AppServicePlanResource, + HostingEnvironmentResource, + VirtualNetworkResource, +]; + +const AseTemplate: ArmResourceTemplateGenerator = { + getTemplate: () => { + const template: any = { + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": {}, + "resources": [], + }; + + resources.forEach((resource) => { + const resourceTemplate = resource.getTemplate(); + template.parameters = { + ...template.parameters, + ...resourceTemplate.parameters, + }; + + template.resources = [ + ...template.resources, + ...resourceTemplate.resources, + ]; + }); + + template.parameters.appServicePlanSkuName.defaultValue = "I1"; + template.parameters.appServicePlanSkuTier.defaultValue = "Isolated"; + + return template; + }, + + getParameters: (config: ServerlessAzureConfig) => { + let parameters = {}; + resources.forEach((resource) => { + parameters = { + ...parameters, + ...resource.getParameters(config), + } + }); + + return parameters; + } +}; + +export default AseTemplate; \ No newline at end of file diff --git a/src/armTemplates/consumption.ts b/src/armTemplates/consumption.ts index 33d81de2..4b62decb 100644 --- a/src/armTemplates/consumption.ts +++ b/src/armTemplates/consumption.ts @@ -1,22 +1,51 @@ -import functionApp from "./resources/functionApp.json"; -import appInsights from "./resources/appInsights.json"; -import storage from "./resources/storageAccount.json"; - -export function generate() { - const template = { - "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "parameters": { - ...functionApp.parameters, - ...appInsights.parameters, - ...storage.parameters, - }, - "resources": [ - ...functionApp.resources, - ...appInsights.resources, - ...storage.resources, - ], - }; - - return template; -} \ No newline at end of file +import { FunctionAppResource } from "./resources/functionApp"; +import { AppInsightsResource } from "./resources/appInsights"; +import { StorageAccountResource } from "./resources/storageAccount"; +import { ArmResourceTemplateGenerator } from "../services/armService.js"; +import { ServerlessAzureConfig } from "../models/serverless"; + +const resources: ArmResourceTemplateGenerator[] = [ + FunctionAppResource, + AppInsightsResource, + StorageAccountResource, +]; + +const ConsumptionTemplate: ArmResourceTemplateGenerator = { + getTemplate: () => { + const template: any = { + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": {}, + "resources": [], + }; + + resources.forEach((resource) => { + const resourceTemplate = resource.getTemplate(); + template.parameters = { + ...template.parameters, + ...resourceTemplate.parameters, + }; + + template.resources = [ + ...template.resources, + ...resourceTemplate.resources, + ]; + }); + + return template; + }, + + getParameters: (config: ServerlessAzureConfig) => { + let parameters = {}; + resources.forEach((resource) => { + parameters = { + ...parameters, + ...resource.getParameters(config), + } + }); + + return parameters; + } +}; + +export default ConsumptionTemplate; \ No newline at end of file diff --git a/src/armTemplates/premium.ts b/src/armTemplates/premium.ts index eeee9281..c496abb0 100644 --- a/src/armTemplates/premium.ts +++ b/src/armTemplates/premium.ts @@ -1,28 +1,56 @@ -import functionApp from "./resources/functionApp.json"; -import appInsights from "./resources/appInsights.json"; -import storage from "./resources/storageAccount.json"; -import appServicePlan from "./resources/appServicePlan.json"; - -export function generate() { - const template = { - "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "parameters": { - ...functionApp.parameters, - ...appInsights.parameters, - ...storage.parameters, - ...appServicePlan.parameters, - }, - "resources": [ - ...functionApp.resources, - ...appInsights.resources, - ...storage.resources, - ...appServicePlan.resources - ], - }; - - template.parameters.appServicePlanSkuName.defaultValue = "EP1"; - template.parameters.appServicePlanSkuTier.defaultValue = "ElasticPremium"; - - return template; -} \ No newline at end of file +import { FunctionAppResource } from "./resources/functionApp"; +import { AppInsightsResource } from "./resources/appInsights"; +import { StorageAccountResource } from "./resources/storageAccount"; +import { AppServicePlanResource } from "./resources/appServicePlan"; +import { ArmResourceTemplateGenerator } from "../services/armService.js"; +import { ServerlessAzureConfig } from "../models/serverless"; + +const resources: ArmResourceTemplateGenerator[] = [ + FunctionAppResource, + AppInsightsResource, + StorageAccountResource, + AppServicePlanResource, +]; + +const PremiumTemplate: ArmResourceTemplateGenerator = { + getTemplate: () => { + const template: any = { + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": {}, + "resources": [], + }; + + resources.forEach((resource) => { + const resourceTemplate = resource.getTemplate(); + template.parameters = { + ...template.parameters, + ...resourceTemplate.parameters, + }; + + template.resources = [ + ...template.resources, + ...resourceTemplate.resources, + ]; + }); + + template.parameters.appServicePlanSkuName.defaultValue = "EP1"; + template.parameters.appServicePlanSkuTier.defaultValue = "ElasticPremium"; + + return template; + }, + + getParameters: (config: ServerlessAzureConfig) => { + let parameters = {}; + resources.forEach((resource) => { + parameters = { + ...parameters, + ...resource.getParameters(config), + } + }); + + return parameters; + } +}; + +export default PremiumTemplate; \ No newline at end of file diff --git a/src/armTemplates/resources/apim.json b/src/armTemplates/resources/apim.json deleted file mode 100644 index 665d5475..00000000 --- a/src/armTemplates/resources/apim.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "apiManagementName": { - "defaultValue": "", - "type": "String" - }, - "location": { - "defaultValue": "", - "type": "String" - }, - "apimSkuName": { - "defaultValue": "Consumption", - "type": "String" - }, - "apimCapacity": { - "defaultValue": 0, - "type": "int" - } - }, - "variables": {}, - "resources": [ - { - "type": "Microsoft.ApiManagement/service", - "apiVersion": "2018-06-01-preview", - "name": "[parameters('apiManagementName')]", - "location": "[parameters('location')]", - "sku": { - "name": "[parameters('apimSkuName')]", - "capacity": "[parameters('apimCapacity')]" - }, - "properties": { - "publisherEmail": "wabrez@microsoft.com", - "publisherName": "Microsoft", - "notificationSenderEmail": "apimgmt-noreply@mail.windowsazure.com", - "hostnameConfigurations": [], - "virtualNetworkType": "None" - } - } - ] -} \ No newline at end of file diff --git a/src/armTemplates/resources/apim.ts b/src/armTemplates/resources/apim.ts new file mode 100644 index 00000000..ca22c4cc --- /dev/null +++ b/src/armTemplates/resources/apim.ts @@ -0,0 +1,64 @@ +import { ServerlessAzureConfig } from "../../models/serverless"; +import { ArmResourceTemplateGenerator } from "../../services/armService"; +import { ApiManagementConfig } from "../../models/apiManagement"; + +export const ApimResource: ArmResourceTemplateGenerator = { + getTemplate: () => { + return { + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "apiManagementName": { + "defaultValue": "", + "type": "String" + }, + "location": { + "defaultValue": "", + "type": "String" + }, + "apimSkuName": { + "defaultValue": "Consumption", + "type": "String" + }, + "apimCapacity": { + "defaultValue": 0, + "type": "int" + } + }, + "variables": {}, + "resources": [ + { + "type": "Microsoft.ApiManagement/service", + "apiVersion": "2018-06-01-preview", + "name": "[parameters('apiManagementName')]", + "location": "[parameters('location')]", + "sku": { + "name": "[parameters('apimSkuName')]", + "capacity": "[parameters('apimCapacity')]" + }, + "properties": { + "publisherEmail": "wabrez@microsoft.com", + "publisherName": "Microsoft", + "notificationSenderEmail": "apimgmt-noreply@mail.windowsazure.com", + "hostnameConfigurations": [], + "virtualNetworkType": "None" + } + } + ] + }; + }, + + getParameters: (config: ServerlessAzureConfig) => { + const apimConfig: ApiManagementConfig = { + name: `${config.provider.prefix}-${config.provider.region}-${config.provider.stage}-apim`, + sku: {}, + ...config.provider.apim, + }; + + return { + apiManagementName: apimConfig.name, + apimSkuName: apimConfig.sku.name, + apimSkuCapacity: apimConfig.sku.capacity, + }; + } +}; \ No newline at end of file diff --git a/src/armTemplates/resources/appInsights.json b/src/armTemplates/resources/appInsights.json deleted file mode 100644 index 7288415f..00000000 --- a/src/armTemplates/resources/appInsights.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "appInsightsName": { - "defaultValue": "", - "type": "String" - }, - "location": { - "defaultValue": "", - "type": "String" - } - }, - "variables": {}, - "resources": [ - { - "apiVersion": "2015-05-01", - "name": "[parameters('appInsightsName')]", - "type": "microsoft.insights/components", - "location": "[parameters('location')]", - "properties": { - "Application_Type": "web", - "ApplicationId": "[parameters('appInsightsName')]", - "Request_Source": "IbizaWebAppExtensionCreate" - } - } - ] -} \ No newline at end of file diff --git a/src/armTemplates/resources/appInsights.ts b/src/armTemplates/resources/appInsights.ts new file mode 100644 index 00000000..c648005c --- /dev/null +++ b/src/armTemplates/resources/appInsights.ts @@ -0,0 +1,46 @@ +import { ArmResourceTemplateGenerator } from "../../services/armService"; +import { ServerlessAzureConfig, ResourceConfig } from "../../models/serverless"; + +export const AppInsightsResource: ArmResourceTemplateGenerator = { + getTemplate: () => { + return { + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "appInsightsName": { + "defaultValue": "", + "type": "String" + }, + "location": { + "defaultValue": "", + "type": "String" + } + }, + "variables": {}, + "resources": [ + { + "apiVersion": "2015-05-01", + "name": "[parameters('appInsightsName')]", + "type": "microsoft.insights/components", + "location": "[parameters('location')]", + "properties": { + "Application_Type": "web", + "ApplicationId": "[parameters('appInsightsName')]", + "Request_Source": "IbizaWebAppExtensionCreate" + } + } + ] + } + }, + + getParameters: (config: ServerlessAzureConfig) => { + const resourceConfig: ResourceConfig = { + name: `${config.provider.prefix}-${config.provider.region}-${config.provider.stage}-appinsights`, + ...config.provider.appInsightsConfig, + }; + + return { + appInsightsName: resourceConfig.name, + }; + } +}; \ No newline at end of file diff --git a/src/armTemplates/resources/appServicePlan.json b/src/armTemplates/resources/appServicePlan.json deleted file mode 100644 index d9a438e7..00000000 --- a/src/armTemplates/resources/appServicePlan.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "appServicePlanName": { - "defaultValue": "", - "type": "String" - }, - "location": { - "defaultValue": "", - "type": "String" - }, - "appServicePlanSkuName": { - "defaultValue": "EP1", - "type": "String" - }, - "appServicePlanSkuTier": { - "defaultValue": "ElasticPremium", - "type": "String" - } - }, - "variables": {}, - "resources": [ - { - "apiVersion": "2016-09-01", - "name": "[parameters('appServicePlanName')]", - "type": "Microsoft.Web/serverfarms", - "location": "[parameters('location')]", - "properties": { - "name": "[parameters('appServicePlanName')]", - "workerSizeId": "3", - "numberOfWorkers": "1", - "maximumElasticWorkerCount": "10", - "hostingEnvironment": "" - }, - "sku": { - "name": "[parameters('appServicePlanSkuName')]", - "tier": "[parameters('appServicePlanSkuTier')]" - } - } - ] -} \ No newline at end of file diff --git a/src/armTemplates/resources/appServicePlan.ts b/src/armTemplates/resources/appServicePlan.ts new file mode 100644 index 00000000..e149e56d --- /dev/null +++ b/src/armTemplates/resources/appServicePlan.ts @@ -0,0 +1,63 @@ +import { ArmResourceTemplateGenerator } from "../../services/armService"; +import { ServerlessAzureConfig, ResourceConfig } from "../../models/serverless"; + +export const AppServicePlanResource: ArmResourceTemplateGenerator = { + getTemplate: () => { + return { + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "appServicePlanName": { + "defaultValue": "", + "type": "String" + }, + "location": { + "defaultValue": "", + "type": "String" + }, + "appServicePlanSkuName": { + "defaultValue": "EP1", + "type": "String" + }, + "appServicePlanSkuTier": { + "defaultValue": "ElasticPremium", + "type": "String" + } + }, + "variables": {}, + "resources": [ + { + "apiVersion": "2016-09-01", + "name": "[parameters('appServicePlanName')]", + "type": "Microsoft.Web/serverfarms", + "location": "[parameters('location')]", + "properties": { + "name": "[parameters('appServicePlanName')]", + "workerSizeId": "3", + "numberOfWorkers": "1", + "maximumElasticWorkerCount": "10", + "hostingEnvironment": "" + }, + "sku": { + "name": "[parameters('appServicePlanSkuName')]", + "tier": "[parameters('appServicePlanSkuTier')]" + } + } + ] + }; + }, + + getParameters: (config: ServerlessAzureConfig) => { + const resourceConfig: ResourceConfig = { + name: `${config.provider.prefix}-${config.provider.region}-${config.provider.stage}-asp`, + sku: {}, + ...config.provider.storageAccount, + }; + + return { + appServicePlanName: resourceConfig.name, + appServicePlanSkuName: resourceConfig.sku.name, + appServicePlanSkuTier: resourceConfig.sku.tier, + } + } +}; \ No newline at end of file diff --git a/src/armTemplates/resources/functionApp.json b/src/armTemplates/resources/functionApp.json deleted file mode 100644 index 3d58e32d..00000000 --- a/src/armTemplates/resources/functionApp.json +++ /dev/null @@ -1,83 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "functionAppName": { - "defaultValue": "", - "type": "String" - }, - "appServicePlanName": { - "defaultValue": "", - "type": "String" - }, - "storageAccountName": { - "defaultValue": "", - "type": "String" - }, - "appInsightsName": { - "defaultValue": "", - "type": "String" - }, - "location": { - "defaultValue": "", - "type": "String" - } - }, - "variables": {}, - "resources": [ - { - "type": "Microsoft.Web/sites", - "apiVersion": "2016-03-01", - "name": "[parameters('functionAppName')]", - "location": "[parameters('location')]", - "dependsOn": [ - "[concat('Microsoft.Web/serverfarms/', parameters('appServicePlanName'))]", - "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName'))]", - "[concat('microsoft.insights/components/', parameters('appInsightsName'))]" - ], - "kind": "functionapp", - "properties": { - "siteConfig": { - "appSettings": [ - { - "name": "FUNCTIONS_WORKER_RUNTIME", - "value": "node" - }, - { - "name": "FUNCTIONS_EXTENSION_VERSION", - "value": "~2" - }, - { - "name": "AzureWebJobsStorage", - "value": "[concat('DefaultEndpointsProtocol=https;AccountName=',parameters('storageAccountName'),';AccountKey=',listKeys(resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2016-01-01').keys[0].value)]" - }, - { - "name": "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING", - "value": "[concat('DefaultEndpointsProtocol=https;AccountName=',parameters('storageAccountName'),';AccountKey=',listKeys(resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2016-01-01').keys[0].value)]" - }, - { - "name": "WEBSITE_CONTENTSHARE", - "value": "[toLower(parameters('functionAppName'))]" - }, - { - "name": "WEBSITE_NODE_DEFAULT_VERSION", - "value": "10.14.1" - }, - { - "name": "WEBSITE_RUN_FROM_PACKAGE", - "value": "1" - }, - { - "name": "APPINSIGHTS_INSTRUMENTATIONKEY", - "value": "[reference(concat('microsoft.insights/components/', parameters('appInsightsName'))).InstrumentationKey]" - } - ] - }, - "name": "[parameters('functionAppName')]", - "clientAffinityEnabled": false, - "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', parameters('appServicePlanName'))]", - "hostingEnvironment": "" - } - } - ] -} \ No newline at end of file diff --git a/src/armTemplates/resources/functionApp.ts b/src/armTemplates/resources/functionApp.ts new file mode 100644 index 00000000..7d701b7e --- /dev/null +++ b/src/armTemplates/resources/functionApp.ts @@ -0,0 +1,101 @@ +import { ArmResourceTemplateGenerator } from "../../services/armService"; +import { ServerlessAzureConfig, ResourceConfig } from "../../models/serverless"; + +export const FunctionAppResource: ArmResourceTemplateGenerator = { + getTemplate: () => { + return { + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "functionAppName": { + "defaultValue": "", + "type": "String" + }, + "appServicePlanName": { + "defaultValue": "", + "type": "String" + }, + "storageAccountName": { + "defaultValue": "", + "type": "String" + }, + "appInsightsName": { + "defaultValue": "", + "type": "String" + }, + "location": { + "defaultValue": "", + "type": "String" + } + }, + "variables": {}, + "resources": [ + { + "type": "Microsoft.Web/sites", + "apiVersion": "2016-03-01", + "name": "[parameters('functionAppName')]", + "location": "[parameters('location')]", + "dependsOn": [ + "[concat('Microsoft.Web/serverfarms/', parameters('appServicePlanName'))]", + "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName'))]", + "[concat('microsoft.insights/components/', parameters('appInsightsName'))]" + ], + "kind": "functionapp", + "properties": { + "siteConfig": { + "appSettings": [ + { + "name": "FUNCTIONS_WORKER_RUNTIME", + "value": "node" + }, + { + "name": "FUNCTIONS_EXTENSION_VERSION", + "value": "~2" + }, + { + "name": "AzureWebJobsStorage", + "value": "[concat('DefaultEndpointsProtocol=https;AccountName=',parameters('storageAccountName'),';AccountKey=',listKeys(resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2016-01-01').keys[0].value)]" + }, + { + "name": "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING", + "value": "[concat('DefaultEndpointsProtocol=https;AccountName=',parameters('storageAccountName'),';AccountKey=',listKeys(resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2016-01-01').keys[0].value)]" + }, + { + "name": "WEBSITE_CONTENTSHARE", + "value": "[toLower(parameters('functionAppName'))]" + }, + { + "name": "WEBSITE_NODE_DEFAULT_VERSION", + "value": "10.14.1" + }, + { + "name": "WEBSITE_RUN_FROM_PACKAGE", + "value": "1" + }, + { + "name": "APPINSIGHTS_INSTRUMENTATIONKEY", + "value": "[reference(concat('microsoft.insights/components/', parameters('appInsightsName'))).InstrumentationKey]" + } + ] + }, + "name": "[parameters('functionAppName')]", + "clientAffinityEnabled": false, + "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', parameters('appServicePlanName'))]", + "hostingEnvironment": "" + } + } + ] + }; + }, + + getParameters: (config: ServerlessAzureConfig) => { + const resourceConfig: ResourceConfig = { + name: `${config.provider.prefix}-${config.provider.region}-${config.provider.stage}-functionapp`, + ...config.provider.functionApp, + }; + + return { + functionAppName: resourceConfig.name, + }; + } +}; diff --git a/src/armTemplates/resources/hostingEnvironment.json b/src/armTemplates/resources/hostingEnvironment.json deleted file mode 100644 index 5cbb4f20..00000000 --- a/src/armTemplates/resources/hostingEnvironment.json +++ /dev/null @@ -1,98 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "hostingEnvironmentName": { - "defaultValue": "", - "type": "String" - }, - "virtualNetworkName": { - "defaultValue": "", - "type": "String" - }, - "location": { - "defaultValue": "", - "type": "String" - } - }, - "variables": {}, - "resources": [ - { - "type": "Microsoft.Network/virtualNetworks", - "apiVersion": "2018-12-01", - "name": "[parameters('virtualNetworkName')]", - "location": "[parameters('location')]", - "properties": { - "provisioningState": "Succeeded", - "resourceGuid": "b756ff30-43ac-4e83-9794-13011e7884ba", - "addressSpace": { - "addressPrefixes": [ - "172.17.0.0/16" - ] - }, - "subnets": [ - { - "name": "default", - "etag": "W/\"73e9f4aa-86a9-478e-ad11-2db211c9c2e3\"", - "properties": { - "provisioningState": "Succeeded", - "addressPrefix": "172.17.0.0/24", - "resourceNavigationLinks": [ - { - "name": "[concat('MicrosoftWeb_HostingEnvironments_', parameters('hostingEnvironmentName'))]", - "properties": { - "linkedResourceType": "Microsoft.Web/hostingEnvironments", - "link": "[resourceId('Microsoft.Web/hostingEnvironments', parameters('hostingEnvironmentName'))]" - } - } - ], - "serviceEndpoints": [ - { - "provisioningState": "Succeeded", - "service": "Microsoft.Web", - "locations": [ - "*" - ] - } - ], - "delegations": [] - } - } - ], - "virtualNetworkPeerings": [], - "enableDdosProtection": false, - "enableVmProtection": false - } - }, - { - "type": "Microsoft.Web/hostingEnvironments", - "apiVersion": "2016-09-01", - "name": "[parameters('hostingEnvironmentName')]", - "location": "[parameters('location')]", - "dependsOn": [ - "[resourceId('Microsoft.Network/virtualNetworks', parameters('virtualNetworkName'))]" - ], - "kind": "ASEV2", - "zones": [], - "properties": { - "name": "[parameters('hostingEnvironmentName')]", - "location": "[parameters('location')]", - "vnetName": "[parameters('virtualNetworkName')]", - "vnetResourceGroupName": "[resourceGroup().name]", - "vnetSubnetName": "default", - "virtualNetwork": { - "id": "[resourceId('Microsoft.Network/virtualNetworks', parameters('virtualNetworkName'))]", - "subnet": "default" - }, - "internalLoadBalancingMode": "None", - "multiSize": "Standard_D1_V2", - "multiRoleCount": 2, - "ipsslAddressCount": 2, - "dnsSuffix": "[concat(parameters('hostingEnvironmentName'), '.p.azurewebsites.net')]", - "networkAccessControlList": [], - "frontEndScaleFactor": 15, - "suspended": false - } - } - ] -} \ No newline at end of file diff --git a/src/armTemplates/resources/hostingEnvironment.ts b/src/armTemplates/resources/hostingEnvironment.ts new file mode 100644 index 00000000..7d60c61f --- /dev/null +++ b/src/armTemplates/resources/hostingEnvironment.ts @@ -0,0 +1,70 @@ +import { ArmResourceTemplateGenerator } from "../../services/armService"; +import { ServerlessAzureConfig, ResourceConfig } from "../../models/serverless"; + +export const HostingEnvironmentResource: ArmResourceTemplateGenerator = { + getTemplate: () => { + return { + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "hostingEnvironmentName": { + "defaultValue": "", + "type": "String" + }, + "virtualNetworkName": { + "defaultValue": "", + "type": "String" + }, + "location": { + "defaultValue": "", + "type": "String" + } + }, + "variables": {}, + "resources": [ + { + "type": "Microsoft.Web/hostingEnvironments", + "apiVersion": "2016-09-01", + "name": "[parameters('hostingEnvironmentName')]", + "location": "[parameters('location')]", + "dependsOn": [ + "[resourceId('Microsoft.Network/virtualNetworks', parameters('virtualNetworkName'))]" + ], + "kind": "ASEV2", + "zones": [], + "properties": { + "name": "[parameters('hostingEnvironmentName')]", + "location": "[parameters('location')]", + "vnetName": "[parameters('virtualNetworkName')]", + "vnetResourceGroupName": "[resourceGroup().name]", + "vnetSubnetName": "default", + "virtualNetwork": { + "id": "[resourceId('Microsoft.Network/virtualNetworks', parameters('virtualNetworkName'))]", + "subnet": "default" + }, + "internalLoadBalancingMode": "None", + "multiSize": "Standard_D1_V2", + "multiRoleCount": 2, + "ipsslAddressCount": 2, + "dnsSuffix": "[concat(parameters('hostingEnvironmentName'), '.p.azurewebsites.net')]", + "networkAccessControlList": [], + "frontEndScaleFactor": 15, + "suspended": false + } + } + ] + }; + }, + + getParameters: (config: ServerlessAzureConfig) => { + const resourceConfig: ResourceConfig = { + name: `${config.provider.prefix}-${config.provider.region}-${config.provider.stage}-ase`, + ...config.provider.hostingEnvironment, + }; + + return { + hostingEnvironmentName: resourceConfig.name, + } + } +}; + diff --git a/src/armTemplates/resources/storageAccount.json b/src/armTemplates/resources/storageAccount.json deleted file mode 100644 index 7699dcfe..00000000 --- a/src/armTemplates/resources/storageAccount.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "storageAccountName": { - "defaultValue": "", - "type": "String" - }, - "location": { - "defaultValue": "", - "type": "String" - }, - "storageAccountSkuName": { - "defaultValue": "Standard_LRS", - "type": "String" - }, - "storageAccoutSkuTier": { - "defaultValue": "Standard", - "type": "String" - } - }, - "variables": {}, - "resources": [ - { - "apiVersion": "2018-07-01", - "name": "[parameters('storageAccountName')]", - "type": "Microsoft.Storage/storageAccounts", - "location": "[parameters('location')]", - "kind": "Storage", - "properties": { - "accountType": "[parameters('storageAccountSkuName')]" - }, - "sku": { - "name": "[parameters('storageAccountSkuName')]", - "tier": "[parameters('storageAccoutSkuTier')]" - } - } - ] -} \ No newline at end of file diff --git a/src/armTemplates/resources/storageAccount.ts b/src/armTemplates/resources/storageAccount.ts new file mode 100644 index 00000000..2a9688d7 --- /dev/null +++ b/src/armTemplates/resources/storageAccount.ts @@ -0,0 +1,60 @@ +import { ArmResourceTemplateGenerator } from "../../services/armService"; +import { ServerlessAzureConfig, ResourceConfig } from "../../models/serverless"; + +export const StorageAccountResource: ArmResourceTemplateGenerator = { + getTemplate: () => { + return { + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "storageAccountName": { + "defaultValue": "", + "type": "String" + }, + "location": { + "defaultValue": "", + "type": "String" + }, + "storageAccountSkuName": { + "defaultValue": "Standard_LRS", + "type": "String" + }, + "storageAccoutSkuTier": { + "defaultValue": "Standard", + "type": "String" + } + }, + "variables": {}, + "resources": [ + { + "apiVersion": "2018-07-01", + "name": "[parameters('storageAccountName')]", + "type": "Microsoft.Storage/storageAccounts", + "location": "[parameters('location')]", + "kind": "Storage", + "properties": { + "accountType": "[parameters('storageAccountSkuName')]" + }, + "sku": { + "name": "[parameters('storageAccountSkuName')]", + "tier": "[parameters('storageAccoutSkuTier')]" + } + } + ] + } + }, + + getParameters: (config: ServerlessAzureConfig) => { + const resourceConfig: ResourceConfig = { + name: `${config.provider.prefix}${config.provider.region}${config.provider.stage}-sa`.toLocaleLowerCase(), + sku: {}, + ...config.provider.storageAccount, + }; + + return { + storageAccountName: resourceConfig.name, + storageAccountSkuName: resourceConfig.sku.name, + storageAccoutSkuTier: resourceConfig.sku.tier, + }; + } +}; \ No newline at end of file diff --git a/src/armTemplates/resources/virtualNetwork.ts b/src/armTemplates/resources/virtualNetwork.ts new file mode 100644 index 00000000..ab1dd3b9 --- /dev/null +++ b/src/armTemplates/resources/virtualNetwork.ts @@ -0,0 +1,87 @@ +import { ArmResourceTemplateGenerator } from "../../services/armService"; +import { ServerlessAzureConfig, ResourceConfig } from "../../models/serverless"; + +export const VirtualNetworkResource: ArmResourceTemplateGenerator = { + getTemplate: () => { + return { + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "hostingEnvironmentName": { + "defaultValue": "", + "type": "String" + }, + "virtualNetworkName": { + "defaultValue": "", + "type": "String" + }, + "location": { + "defaultValue": "", + "type": "String" + } + }, + "variables": {}, + "resources": [ + { + "type": "Microsoft.Network/virtualNetworks", + "apiVersion": "2018-12-01", + "name": "[parameters('virtualNetworkName')]", + "location": "[parameters('location')]", + "properties": { + "provisioningState": "Succeeded", + "resourceGuid": "b756ff30-43ac-4e83-9794-13011e7884ba", + "addressSpace": { + "addressPrefixes": [ + "172.17.0.0/16" + ] + }, + "subnets": [ + { + "name": "default", + "etag": "W/\"73e9f4aa-86a9-478e-ad11-2db211c9c2e3\"", + "properties": { + "provisioningState": "Succeeded", + "addressPrefix": "172.17.0.0/24", + "resourceNavigationLinks": [ + { + "name": "[concat('MicrosoftWeb_HostingEnvironments_', parameters('hostingEnvironmentName'))]", + "properties": { + "linkedResourceType": "Microsoft.Web/hostingEnvironments", + "link": "[resourceId('Microsoft.Web/hostingEnvironments', parameters('hostingEnvironmentName'))]" + } + } + ], + "serviceEndpoints": [ + { + "provisioningState": "Succeeded", + "service": "Microsoft.Web", + "locations": [ + "*" + ] + } + ], + "delegations": [] + } + } + ], + "virtualNetworkPeerings": [], + "enableDdosProtection": false, + "enableVmProtection": false + } + } + ] + }; + }, + + getParameters: (config: ServerlessAzureConfig) => { + const resourceConfig: ResourceConfig = { + name: `${config.provider.prefix}-${config.provider.region}-${config.provider.stage}-vnet`, + ...config.provider.hostingEnvironment, + }; + + return { + virtualNetworkName: resourceConfig.name, + } + } +}; + diff --git a/src/models/apiManagement.ts b/src/models/apiManagement.ts index 8c8abeac..0a05f1df 100644 --- a/src/models/apiManagement.ts +++ b/src/models/apiManagement.ts @@ -12,6 +12,10 @@ export interface ApiManagementConfig { backend?: BackendContract; /** The API's CORS policy */ cors?: ApiCorsPolicy; + sku?: { + name?: string; + capacity?: number; + }; } /** diff --git a/src/models/serverless.ts b/src/models/serverless.ts index 6eeafcff..f0f61649 100644 --- a/src/models/serverless.ts +++ b/src/models/serverless.ts @@ -14,11 +14,31 @@ export interface ArmTemplateConfig { }; } +export interface ResourceConfig { + name: string; + sku?: { + name?: string; + tier?: string; + }; + [key: string]: any; +} + export interface ServerlessAzureConfig { + service: string; provider: { - name: string; + type?: string; + prefix?: string; region: string; + stage: string; + name: string; + resourceGroup?: string; apim?: ApiManagementConfig; + functionApp?: ResourceConfig; + appInsightsConfig?: ResourceConfig; + appServicePlan?: ResourceConfig; + storageAccount?: ResourceConfig; + hostingEnvironment?: ResourceConfig; + virtualNetwork?: ResourceConfig; armTemplate?: ArmTemplateConfig; }; plugins: string[]; diff --git a/src/services/armService.test.ts b/src/services/armService.test.ts index 4d36b086..600c7fc5 100644 --- a/src/services/armService.test.ts +++ b/src/services/armService.test.ts @@ -12,6 +12,9 @@ describe("Arm Service", () => { beforeEach(() => { sls = MockFactory.createTestServerless(); + sls.service.provider["prefix"] = "myapp"; + sls.service.provider.region = "westus"; + sls.service.provider.stage = "dev"; sls.variables = { ...sls.variables, azureCredentials: MockFactory.createTestAzureCredentials(), @@ -23,70 +26,70 @@ describe("Arm Service", () => { describe("Creating Templates", () => { it("Creates a custom ARM template from well-known type", async () => { - const template = await service.createTemplate("premium"); + const deployment = await service.createDeployment("premium"); - expect(template).not.toBeNull(); - expect(Object.keys(template.parameters).length).toBeGreaterThan(0); - expect(template.resources.length).toBeGreaterThan(0); + expect(deployment).not.toBeNull(); + expect(Object.keys(deployment.parameters).length).toBeGreaterThan(0); + expect(deployment.template.resources.length).toBeGreaterThan(0); }); it("Creates a custom ARM template (with APIM support) from well-known type", async () => { sls.service.provider["apim"] = MockFactory.createTestApimConfig(); - const template = await service.createTemplate("premium"); + const deployment = await service.createDeployment("premium"); - expect(template).not.toBeNull(); - expect(Object.keys(template.parameters).length).toBeGreaterThan(0); - expect(template.resources.length).toBeGreaterThan(0); + expect(deployment).not.toBeNull(); + expect(Object.keys(deployment.parameters).length).toBeGreaterThan(0); + expect(deployment.template.resources.length).toBeGreaterThan(0); - expect(template.resources.find((resource) => resource.type === "Microsoft.ApiManagement/service")).not.toBeNull(); + expect(deployment.template.resources.find((resource) => resource.type === "Microsoft.ApiManagement/service")).not.toBeNull(); }); it("throws error when specified type is not found", async () => { - await expect(service.createTemplate("not-found")).rejects.not.toBeNull(); + await expect(service.createDeployment("not-found")).rejects.not.toBeNull(); }); it("Premium template includes correct resources", async () => { - const template = await service.createTemplate("premium"); + const deployment = await service.createDeployment("premium"); - expect(template.parameters.appServicePlanSkuTier.defaultValue).toEqual("ElasticPremium"); - expect(template.parameters.appServicePlanSkuName.defaultValue).toEqual("EP1"); + expect(deployment.template.parameters.appServicePlanSkuTier.defaultValue).toEqual("ElasticPremium"); + expect(deployment.template.parameters.appServicePlanSkuName.defaultValue).toEqual("EP1"); // Should not contain - expect(template.resources.find((resource) => resource.type === "Microsoft.Web/hostingEnvironments")).toBeUndefined(); - expect(template.resources.find((resource) => resource.type === "Microsoft.Network/virtualNetworks")).toBeUndefined(); + expect(deployment.template.resources.find((resource) => resource.type === "Microsoft.Web/hostingEnvironments")).toBeUndefined(); + expect(deployment.template.resources.find((resource) => resource.type === "Microsoft.Network/virtualNetworks")).toBeUndefined(); // Should contain - expect(template.resources.find((resource) => resource.type === "Microsoft.Web/serverfarms")).not.toBeNull(); - expect(template.resources.find((resource) => resource.type === "Microsoft.Web/sites")).not.toBeNull(); - expect(template.resources.find((resource) => resource.type === "Microsoft.Storage/storageAccounts")).not.toBeNull(); - expect(template.resources.find((resource) => resource.type === "microsoft.insights/components")).not.toBeNull(); + expect(deployment.template.resources.find((resource) => resource.type === "Microsoft.Web/serverfarms")).not.toBeNull(); + expect(deployment.template.resources.find((resource) => resource.type === "Microsoft.Web/sites")).not.toBeNull(); + expect(deployment.template.resources.find((resource) => resource.type === "Microsoft.Storage/storageAccounts")).not.toBeNull(); + expect(deployment.template.resources.find((resource) => resource.type === "microsoft.insights/components")).not.toBeNull(); }); it("ASE template includes correct resources", async () => { - const template = await service.createTemplate("ase"); + const deployment = await service.createDeployment("ase"); - expect(template.parameters.appServicePlanSkuTier.defaultValue).toEqual("Isolated"); - expect(template.parameters.appServicePlanSkuName.defaultValue).toEqual("I1"); + expect(deployment.template.parameters.appServicePlanSkuTier.defaultValue).toEqual("Isolated"); + expect(deployment.template.parameters.appServicePlanSkuName.defaultValue).toEqual("I1"); - expect(template.resources.find((resource) => resource.type === "Microsoft.Web/hostingEnvironments")).not.toBeNull(); - expect(template.resources.find((resource) => resource.type === "Microsoft.Network/virtualNetworks")).not.toBeNull(); - expect(template.resources.find((resource) => resource.type === "Microsoft.Web/serverfarms")).not.toBeNull(); - expect(template.resources.find((resource) => resource.type === "Microsoft.Web/sites")).not.toBeNull(); - expect(template.resources.find((resource) => resource.type === "Microsoft.Storage/storageAccounts")).not.toBeNull(); - expect(template.resources.find((resource) => resource.type === "microsoft.insights/components")).not.toBeNull(); + expect(deployment.template.resources.find((resource) => resource.type === "Microsoft.Web/hostingEnvironments")).not.toBeNull(); + expect(deployment.template.resources.find((resource) => resource.type === "Microsoft.Network/virtualNetworks")).not.toBeNull(); + expect(deployment.template.resources.find((resource) => resource.type === "Microsoft.Web/serverfarms")).not.toBeNull(); + expect(deployment.template.resources.find((resource) => resource.type === "Microsoft.Web/sites")).not.toBeNull(); + expect(deployment.template.resources.find((resource) => resource.type === "Microsoft.Storage/storageAccounts")).not.toBeNull(); + expect(deployment.template.resources.find((resource) => resource.type === "microsoft.insights/components")).not.toBeNull(); }); it("Consumption template includes correct resources", async () => { - const template = await service.createTemplate("consumption"); + const deployment = await service.createDeployment("consumption"); - expect(template.resources.find((resource) => resource.type === "Microsoft.Web/hostingEnvironments")).toBeUndefined(); - expect(template.resources.find((resource) => resource.type === "Microsoft.Network/virtualNetworks")).toBeUndefined(); - expect(template.resources.find((resource) => resource.type === "Microsoft.Web/serverfarms")).toBeUndefined(); + expect(deployment.template.resources.find((resource) => resource.type === "Microsoft.Web/hostingEnvironments")).toBeUndefined(); + expect(deployment.template.resources.find((resource) => resource.type === "Microsoft.Network/virtualNetworks")).toBeUndefined(); + expect(deployment.template.resources.find((resource) => resource.type === "Microsoft.Web/serverfarms")).toBeUndefined(); // Should contain - expect(template.resources.find((resource) => resource.type === "Microsoft.Web/sites")).not.toBeNull(); - expect(template.resources.find((resource) => resource.type === "Microsoft.Storage/storageAccounts")).not.toBeNull(); - expect(template.resources.find((resource) => resource.type === "microsoft.insights/components")).not.toBeNull(); + expect(deployment.template.resources.find((resource) => resource.type === "Microsoft.Web/sites")).not.toBeNull(); + expect(deployment.template.resources.find((resource) => resource.type === "Microsoft.Storage/storageAccounts")).not.toBeNull(); + expect(deployment.template.resources.find((resource) => resource.type === "microsoft.insights/components")).not.toBeNull(); }); }); diff --git a/src/services/armService.ts b/src/services/armService.ts index 184ba700..695213d3 100644 --- a/src/services/armService.ts +++ b/src/services/armService.ts @@ -1,11 +1,28 @@ import Serverless from "serverless"; -import { Deployment } from "@azure/arm-resources/esm/models"; +import { Deployment, DeploymentsCreateOrUpdateResponse, DeploymentExtended } from "@azure/arm-resources/esm/models"; import { BaseService } from "./baseService"; import { ResourceManagementClient } from "@azure/arm-resources"; import { Guard } from "../shared/guard"; +import { ServerlessAzureConfig } from "../models/serverless"; -export interface ArmTemplateGenerator { - generate(): any; +export interface ArmResourceTemplateGenerator { + getTemplate(): ArmResourceTemplate; + getParameters(config: ServerlessAzureConfig): any; +} + +export interface ArmResourceTemplate { + $schema: string; + contentVersion: string; + parameters: { + [key: string]: any; + }; + resources: any[]; + variables?: any; +} + +export interface ArmDeployment { + template: any; + parameters: { [key: string]: any }; } export class ArmService extends BaseService { @@ -16,47 +33,60 @@ export class ArmService extends BaseService { this.resourceClient = new ResourceManagementClient(this.credentials, this.subscriptionId); } - public async createTemplate(type: string): Promise { + public async createDeployment(type: string): Promise { Guard.empty(type); - const apim = await import("../armTemplates/resources/apim.json"); - let template: ArmTemplateGenerator; + const { ApimResource } = await import("../armTemplates/resources/apim"); + let template: ArmResourceTemplateGenerator; try { - template = await import(`../armTemplates/${type}`); + template = (await import(`../armTemplates/${type}`)).default; } catch (e) { throw new Error(`Unable to find template with name ${type} `); } - const mergedTemplate = template.generate(); + const azureConfig: ServerlessAzureConfig = this.serverless.service as any; + + const mergedTemplate = template.getTemplate(); + let parameters = template.getParameters(azureConfig); if (this.serverless.service.provider["apim"]) { + const apimTemplate = ApimResource.getTemplate(); + const apimParameters = ApimResource.getParameters(azureConfig); + mergedTemplate.parameters = { ...mergedTemplate.parameters, - ...apim.parameters, + ...apimTemplate.parameters, }; mergedTemplate.resources = [ ...mergedTemplate.resources, - ...apim.resources, - ] + ...apimTemplate.resources, + ]; + + parameters = { + ...parameters, + ...apimParameters, + }; } - return mergedTemplate; + return { + template: mergedTemplate, + parameters, + }; } - public async deployTemplate(template: any, parameters: string[]) { - Guard.null(template); - Guard.null(parameters); + public async deployTemplate(deployment: ArmDeployment): Promise { + Guard.null(deployment); const deploymentParameters: Deployment = { properties: { mode: "Incremental", - parameters, - template + template: deployment.template, + parameters: deployment.parameters, } }; // Deploy ARM template - await this.resourceClient.deployments.createOrUpdate(this.resourceGroup, this.deploymentName, deploymentParameters); + return await this.resourceClient.deployments.createOrUpdate(this.resourceGroup, this.deploymentName, deploymentParameters); } } \ No newline at end of file diff --git a/src/services/functionAppService.ts b/src/services/functionAppService.ts index 4237f37a..4b1eb687 100644 --- a/src/services/functionAppService.ts +++ b/src/services/functionAppService.ts @@ -10,6 +10,7 @@ import { BaseService } from "./baseService"; import { FunctionAppHttpTriggerConfig } from "../models/functionApp"; import { Site, FunctionEnvelope } from "@azure/arm-appservice/esm/models"; import { Guard } from "../shared/guard"; +import { ArmService, ArmDeployment } from "./armService"; export class FunctionAppService extends BaseService { private resourceClient: ResourceManagementClient; @@ -123,38 +124,29 @@ export class FunctionAppService extends BaseService { */ public async deploy() { this.log(`Creating function app: ${this.serviceName}`); - let parameters: any = { functionAppName: { value: this.serviceName } }; - const gitUrl = this.serverless.service.provider["gitUrl"]; - - if (gitUrl) { - parameters = { - functionAppName: { value: this.serviceName }, - gitUrl: { value: gitUrl } - }; - } - - let templateFilePath = path.join(__dirname, "..", "provider", "armTemplates", "azuredeploy.json"); - - if (gitUrl) { - templateFilePath = path.join(__dirname, "armTemplates", "azuredeployWithGit.json"); - } + const armService = new ArmService(this.serverless); + let deployment: ArmDeployment; if (this.serverless.service.provider["armTemplate"]) { this.log(`-> Deploying custom ARM template: ${this.serverless.service.provider["armTemplate"].file}`); - templateFilePath = path.join(this.serverless.config.servicePath, this.serverless.service.provider["armTemplate"].file); + const templateFilePath = path.join(this.serverless.config.servicePath, this.serverless.service.provider["armTemplate"].file); const userParameters = this.serverless.service.provider["armTemplate"].parameters; const userParametersKeys = Object.keys(userParameters); - + let template = JSON.parse(fs.readFileSync(templateFilePath, "utf8")); + let parameters: any; for (let paramIndex = 0; paramIndex < userParametersKeys.length; paramIndex++) { const item = {}; item[userParametersKeys[paramIndex]] = { "value": userParameters[userParametersKeys[paramIndex]] }; parameters = _.merge(parameters, item); } - } - let template = JSON.parse(fs.readFileSync(templateFilePath, "utf8")); + deployment = { template, parameters }; + + } else { + deployment = await armService.createDeployment(this.serverless.service.provider["type"] || "consumption"); + } // Check if there are custom environment variables defined that need to be // added to the ARM template used in the deployment. @@ -162,7 +154,7 @@ export class FunctionAppService extends BaseService { if (environmentVariables) { const appSettingsPath = "$.resources[?(@.kind==\"functionapp\")].properties.siteConfig.appSettings"; - jsonpath.apply(template, appSettingsPath, function (appSettingsList) { + jsonpath.apply(deployment.template, appSettingsPath, function (appSettingsList) { Object.keys(environmentVariables).forEach(function (key) { appSettingsList.push({ name: key, @@ -174,16 +166,7 @@ export class FunctionAppService extends BaseService { }); } - const deploymentParameters: Deployment = { - properties: { - mode: "Incremental", - parameters, - template - } - }; - - // Deploy ARM template - await this.resourceClient.deployments.createOrUpdate(this.resourceGroup, this.deploymentName, deploymentParameters); + await armService.deployTemplate(deployment); // Return function app return await this.get(); From db59819253883dc5686230e552c20ce6d1464fae Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Fri, 21 Jun 2019 12:47:42 -0700 Subject: [PATCH 10/17] set resource template defaults --- src/armTemplates/ase.ts | 12 +++- src/armTemplates/consumption.ts | 5 +- src/armTemplates/premium.ts | 12 +++- src/armTemplates/resources/functionApp.ts | 8 +-- src/armTemplates/resources/storageAccount.ts | 4 +- src/plugins/deploy/azureDeployPlugin.ts | 5 -- src/services/apimService.ts | 64 +++++++++++--------- src/services/armService.test.ts | 10 +++ src/services/armService.ts | 25 ++++++-- src/services/baseService.test.ts | 6 +- src/services/baseService.ts | 43 +++++++------ src/services/functionAppService.test.ts | 6 +- src/services/functionAppService.ts | 19 +++--- 13 files changed, 129 insertions(+), 90 deletions(-) diff --git a/src/armTemplates/ase.ts b/src/armTemplates/ase.ts index a9b31ed1..89a4aa9d 100644 --- a/src/armTemplates/ase.ts +++ b/src/armTemplates/ase.ts @@ -4,7 +4,7 @@ import { StorageAccountResource } from "./resources/storageAccount"; import { AppServicePlanResource } from "./resources/appServicePlan"; import { HostingEnvironmentResource } from "./resources/hostingEnvironment"; import { VirtualNetworkResource } from "./resources/virtualNetwork"; -import { ArmResourceTemplateGenerator } from "../services/armService.js"; +import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../services/armService.js"; import { ServerlessAzureConfig } from "../models/serverless.js"; const resources: ArmResourceTemplateGenerator[] = [ @@ -18,7 +18,7 @@ const resources: ArmResourceTemplateGenerator[] = [ const AseTemplate: ArmResourceTemplateGenerator = { getTemplate: () => { - const template: any = { + const template: ArmResourceTemplate = { "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": {}, @@ -41,6 +41,13 @@ const AseTemplate: ArmResourceTemplateGenerator = { template.parameters.appServicePlanSkuName.defaultValue = "I1"; template.parameters.appServicePlanSkuTier.defaultValue = "Isolated"; + // Update the functionApp resource to include the app service plan references + const app: any = template.resources.find((resource: any) => resource.type === "Microsoft.Web/sites"); + if (app) { + app.properties.serverFarmId = "[resourceId('Microsoft.Web/serverfarms', parameters('appServicePlanName'))]"; + app.dependsOn.push("[concat('Microsoft.Web/serverfarms/', parameters('appServicePlanName'))]") + } + return template; }, @@ -50,6 +57,7 @@ const AseTemplate: ArmResourceTemplateGenerator = { parameters = { ...parameters, ...resource.getParameters(config), + location: config.provider.region, } }); diff --git a/src/armTemplates/consumption.ts b/src/armTemplates/consumption.ts index 4b62decb..5f71883e 100644 --- a/src/armTemplates/consumption.ts +++ b/src/armTemplates/consumption.ts @@ -1,7 +1,7 @@ import { FunctionAppResource } from "./resources/functionApp"; import { AppInsightsResource } from "./resources/appInsights"; import { StorageAccountResource } from "./resources/storageAccount"; -import { ArmResourceTemplateGenerator } from "../services/armService.js"; +import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../services/armService.js"; import { ServerlessAzureConfig } from "../models/serverless"; const resources: ArmResourceTemplateGenerator[] = [ @@ -12,7 +12,7 @@ const resources: ArmResourceTemplateGenerator[] = [ const ConsumptionTemplate: ArmResourceTemplateGenerator = { getTemplate: () => { - const template: any = { + const template: ArmResourceTemplate = { "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": {}, @@ -41,6 +41,7 @@ const ConsumptionTemplate: ArmResourceTemplateGenerator = { parameters = { ...parameters, ...resource.getParameters(config), + location: config.provider.region, } }); diff --git a/src/armTemplates/premium.ts b/src/armTemplates/premium.ts index c496abb0..2bb84c56 100644 --- a/src/armTemplates/premium.ts +++ b/src/armTemplates/premium.ts @@ -2,7 +2,7 @@ import { FunctionAppResource } from "./resources/functionApp"; import { AppInsightsResource } from "./resources/appInsights"; import { StorageAccountResource } from "./resources/storageAccount"; import { AppServicePlanResource } from "./resources/appServicePlan"; -import { ArmResourceTemplateGenerator } from "../services/armService.js"; +import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../services/armService.js"; import { ServerlessAzureConfig } from "../models/serverless"; const resources: ArmResourceTemplateGenerator[] = [ @@ -14,7 +14,7 @@ const resources: ArmResourceTemplateGenerator[] = [ const PremiumTemplate: ArmResourceTemplateGenerator = { getTemplate: () => { - const template: any = { + const template: ArmResourceTemplate = { "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": {}, @@ -37,6 +37,13 @@ const PremiumTemplate: ArmResourceTemplateGenerator = { template.parameters.appServicePlanSkuName.defaultValue = "EP1"; template.parameters.appServicePlanSkuTier.defaultValue = "ElasticPremium"; + // Update the functionApp resource to include the app service plan references + const app: any = template.resources.find((resource: any) => resource.type === "Microsoft.Web/sites"); + if (app) { + app.properties.serverFarmId = "[resourceId('Microsoft.Web/serverfarms', parameters('appServicePlanName'))]"; + app.dependsOn.push("[concat('Microsoft.Web/serverfarms/', parameters('appServicePlanName'))]") + } + return template; }, @@ -46,6 +53,7 @@ const PremiumTemplate: ArmResourceTemplateGenerator = { parameters = { ...parameters, ...resource.getParameters(config), + location: config.provider.region, } }); diff --git a/src/armTemplates/resources/functionApp.ts b/src/armTemplates/resources/functionApp.ts index 7d701b7e..492b6bf3 100644 --- a/src/armTemplates/resources/functionApp.ts +++ b/src/armTemplates/resources/functionApp.ts @@ -11,10 +11,6 @@ export const FunctionAppResource: ArmResourceTemplateGenerator = { "defaultValue": "", "type": "String" }, - "appServicePlanName": { - "defaultValue": "", - "type": "String" - }, "storageAccountName": { "defaultValue": "", "type": "String" @@ -36,7 +32,6 @@ export const FunctionAppResource: ArmResourceTemplateGenerator = { "name": "[parameters('functionAppName')]", "location": "[parameters('location')]", "dependsOn": [ - "[concat('Microsoft.Web/serverfarms/', parameters('appServicePlanName'))]", "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName'))]", "[concat('microsoft.insights/components/', parameters('appInsightsName'))]" ], @@ -80,7 +75,6 @@ export const FunctionAppResource: ArmResourceTemplateGenerator = { }, "name": "[parameters('functionAppName')]", "clientAffinityEnabled": false, - "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', parameters('appServicePlanName'))]", "hostingEnvironment": "" } } @@ -90,7 +84,7 @@ export const FunctionAppResource: ArmResourceTemplateGenerator = { getParameters: (config: ServerlessAzureConfig) => { const resourceConfig: ResourceConfig = { - name: `${config.provider.prefix}-${config.provider.region}-${config.provider.stage}-functionapp`, + name: `${config.provider.prefix}-${config.provider.region}-${config.provider.stage}-${config.service}`, ...config.provider.functionApp, }; diff --git a/src/armTemplates/resources/storageAccount.ts b/src/armTemplates/resources/storageAccount.ts index 2a9688d7..6b08dc02 100644 --- a/src/armTemplates/resources/storageAccount.ts +++ b/src/armTemplates/resources/storageAccount.ts @@ -43,10 +43,10 @@ export const StorageAccountResource: ArmResourceTemplateGenerator = { ] } }, - + getParameters: (config: ServerlessAzureConfig) => { const resourceConfig: ResourceConfig = { - name: `${config.provider.prefix}${config.provider.region}${config.provider.stage}-sa`.toLocaleLowerCase(), + name: `${config.provider.prefix}${config.provider.region.substr(0,3)}${config.provider.stage.substr(0,3)}sa`.replace("-", "").toLocaleLowerCase(), sku: {}, ...config.provider.storageAccount, }; diff --git a/src/plugins/deploy/azureDeployPlugin.ts b/src/plugins/deploy/azureDeployPlugin.ts index 3d06d44a..76c88ebc 100644 --- a/src/plugins/deploy/azureDeployPlugin.ts +++ b/src/plugins/deploy/azureDeployPlugin.ts @@ -47,11 +47,6 @@ export class AzureDeployPlugin { } private async deploy() { - this.serverless.cli.log("OPTIONS"); - this.serverless.cli.log(JSON.stringify(this.options, null, 4)); - this.serverless.cli.log("PROVIDER REGION"); - this.serverless.cli.log(this.serverless.service.provider.region); - const resourceService = new ResourceService(this.serverless, this.options); await resourceService.deployResourceGroup(); diff --git a/src/services/apimService.ts b/src/services/apimService.ts index a6452f95..6126a70c 100644 --- a/src/services/apimService.ts +++ b/src/services/apimService.ts @@ -17,18 +17,22 @@ import { Guard } from "../shared/guard"; export class ApimService extends BaseService { private apimClient: ApiManagementClient; private functionAppService: FunctionAppService; - private config: ApiManagementConfig; + private apimConfig: ApiManagementConfig; public constructor(serverless: Serverless, options?: Serverless.Options) { super(serverless, options); - this.config = this.serverless.service.provider["apim"]; - if (!this.config) { + this.apimConfig = this.config.provider.apim; + if (!this.apimConfig) { return; } - if (!this.config.backend) { - this.config.backend = {} as any; + if (!this.apimConfig.name) { + this.apimConfig.name = `${this.config.provider.prefix}-${this.config.provider.region}-${this.config.provider.stage}-apim` + } + + if (!this.apimConfig.backend) { + this.apimConfig.backend = {} as any; } this.apimClient = new ApiManagementClient(this.credentials, this.subscriptionId); @@ -39,24 +43,24 @@ export class ApimService extends BaseService { * Gets the configured APIM resource */ public async get(): Promise { - if (!(this.config && this.config.name)) { + if (!(this.apimConfig && this.apimConfig.name)) { return null; } try { - return await this.apimClient.apiManagementService.get(this.resourceGroup, this.config.name); + return await this.apimClient.apiManagementService.get(this.resourceGroup, this.apimConfig.name); } catch (err) { return null; } } public async getApi(): Promise { - if (!(this.config && this.config.api && this.config.api.name)) { + if (!(this.apimConfig && this.apimConfig.api && this.apimConfig.api.name)) { return null; } try { - return await this.apimClient.api.get(this.resourceGroup, this.config.name, this.config.api.name); + return await this.apimClient.api.get(this.resourceGroup, this.apimConfig.name, this.apimConfig.api.name); } catch (err) { return null; } @@ -66,7 +70,7 @@ export class ApimService extends BaseService { * Deploys the APIM top level api */ public async deployApi() { - if (!(this.config && this.config.name)) { + if (!(this.apimConfig && this.apimConfig.name)) { return null; } @@ -86,7 +90,7 @@ export class ApimService extends BaseService { Guard.null(service); Guard.null(api); - if (!(this.config && this.config.name)) { + if (!(this.apimConfig && this.apimConfig.name)) { return null; } @@ -131,21 +135,21 @@ export class ApimService extends BaseService { this.log("-> Deploying API"); try { - const api = await this.apimClient.api.createOrUpdate(this.resourceGroup, this.config.name, this.config.api.name, { + const api = await this.apimClient.api.createOrUpdate(this.resourceGroup, this.apimConfig.name, this.apimConfig.api.name, { isCurrent: true, - subscriptionRequired: this.config.api.subscriptionRequired, - displayName: this.config.api.displayName, - description: this.config.api.description, - path: this.config.api.path, - protocols: this.config.api.protocols, + subscriptionRequired: this.apimConfig.api.subscriptionRequired, + displayName: this.apimConfig.api.displayName, + description: this.apimConfig.api.description, + path: this.apimConfig.api.path, + protocols: this.apimConfig.api.protocols, }); - if (this.config.cors) { + if (this.apimConfig.cors) { this.log("-> Deploying CORS policy"); - await this.apimClient.apiPolicy.createOrUpdate(this.resourceGroup, this.config.name, this.config.api.name, { + await this.apimClient.apiPolicy.createOrUpdate(this.resourceGroup, this.apimConfig.name, this.apimConfig.api.name, { format: "rawxml", - value: this.createCorsXmlPolicy(this.config.cors) + value: this.createCorsXmlPolicy(this.apimConfig.cors) }); } @@ -168,17 +172,17 @@ export class ApimService extends BaseService { try { const functionAppResourceId = `https://management.azure.com${functionApp.id}`; - return await this.apimClient.backend.createOrUpdate(this.resourceGroup, this.config.name, this.serviceName, { + return await this.apimClient.backend.createOrUpdate(this.resourceGroup, this.apimConfig.name, this.serviceName, { credentials: { header: { "x-functions-key": [`{{${this.serviceName}-key}}`], }, }, - title: this.config.backend.title || functionApp.name, - tls: this.config.backend.tls, - proxy: this.config.backend.proxy, - description: this.config.backend.description, - protocol: this.config.backend.protocol || "http", + title: this.apimConfig.backend.title || functionApp.name, + tls: this.apimConfig.backend.tls, + proxy: this.apimConfig.backend.proxy, + description: this.apimConfig.backend.description, + protocol: this.apimConfig.backend.protocol || "http", resourceId: functionAppResourceId, url: backendUrl, }); @@ -216,13 +220,13 @@ export class ApimService extends BaseService { const operation = await client.apiOperation.createOrUpdate( this.resourceGroup, - this.config.name, - this.config.api.name, + this.apimConfig.name, + this.apimConfig.api.name, options.function, operationConfig, ); - await client.apiOperationPolicy.createOrUpdate(this.resourceGroup, this.config.name, this.config.api.name, options.function, { + await client.apiOperationPolicy.createOrUpdate(this.resourceGroup, this.apimConfig.name, this.apimConfig.api.name, options.function, { format: "rawxml", value: this.createApiOperationXmlPolicy(), }); @@ -245,7 +249,7 @@ export class ApimService extends BaseService { const masterKey = await this.functionAppService.getMasterKey(functionApp); const keyName = `${this.serviceName}-key`; - return await this.apimClient.property.createOrUpdate(this.resourceGroup, this.config.name, keyName, { + return await this.apimClient.property.createOrUpdate(this.resourceGroup, this.apimConfig.name, keyName, { displayName: keyName, secret: true, value: masterKey, diff --git a/src/services/armService.test.ts b/src/services/armService.test.ts index 600c7fc5..71772ce7 100644 --- a/src/services/armService.test.ts +++ b/src/services/armService.test.ts @@ -63,6 +63,11 @@ describe("Arm Service", () => { expect(deployment.template.resources.find((resource) => resource.type === "Microsoft.Web/sites")).not.toBeNull(); expect(deployment.template.resources.find((resource) => resource.type === "Microsoft.Storage/storageAccounts")).not.toBeNull(); expect(deployment.template.resources.find((resource) => resource.type === "microsoft.insights/components")).not.toBeNull(); + + // Verify the ARM template includes the linkage to the correct server farm + const functionApp = deployment.template.resources.find((res) => res.type === "Microsoft.Web/sites"); + expect(functionApp.dependsOn).toContain("[concat('Microsoft.Web/serverfarms/', parameters('appServicePlanName'))]"); + expect(functionApp.properties.serverFarmId).toEqual("[resourceId('Microsoft.Web/serverfarms', parameters('appServicePlanName'))]"); }); it("ASE template includes correct resources", async () => { @@ -77,6 +82,11 @@ describe("Arm Service", () => { expect(deployment.template.resources.find((resource) => resource.type === "Microsoft.Web/sites")).not.toBeNull(); expect(deployment.template.resources.find((resource) => resource.type === "Microsoft.Storage/storageAccounts")).not.toBeNull(); expect(deployment.template.resources.find((resource) => resource.type === "microsoft.insights/components")).not.toBeNull(); + + // Verify the ARM template includes the linkage to the correct server farm + const functionApp = deployment.template.resources.find((res) => res.type === "Microsoft.Web/sites"); + expect(functionApp.dependsOn).toContain("[concat('Microsoft.Web/serverfarms/', parameters('appServicePlanName'))]"); + expect(functionApp.properties.serverFarmId).toEqual("[resourceId('Microsoft.Web/serverfarms', parameters('appServicePlanName'))]"); }); it("Consumption template includes correct resources", async () => { diff --git a/src/services/armService.ts b/src/services/armService.ts index 695213d3..1d8a5829 100644 --- a/src/services/armService.ts +++ b/src/services/armService.ts @@ -1,9 +1,10 @@ import Serverless from "serverless"; -import { Deployment, DeploymentsCreateOrUpdateResponse, DeploymentExtended } from "@azure/arm-resources/esm/models"; +import { Deployment, DeploymentExtended } from "@azure/arm-resources/esm/models"; import { BaseService } from "./baseService"; import { ResourceManagementClient } from "@azure/arm-resources"; import { Guard } from "../shared/guard"; import { ServerlessAzureConfig } from "../models/serverless"; +import fs from "fs"; export interface ArmResourceTemplateGenerator { getTemplate(): ArmResourceTemplate; @@ -50,7 +51,7 @@ export class ArmService extends BaseService { const mergedTemplate = template.getTemplate(); let parameters = template.getParameters(azureConfig); - if (this.serverless.service.provider["apim"]) { + if (this.config.provider.apim) { const apimTemplate = ApimResource.getTemplate(); const apimParameters = ApimResource.getParameters(azureConfig); @@ -78,15 +79,29 @@ export class ArmService extends BaseService { public async deployTemplate(deployment: ArmDeployment): Promise { Guard.null(deployment); - const deploymentParameters: Deployment = { + const deploymentParameters = {}; + Object.keys(deployment.parameters).forEach((key) => { + const parameterValue = deployment.parameters[key]; + if (parameterValue) { + deploymentParameters[key] = { value: deployment.parameters[key] }; + } + }); + + this.serverless.cli.log(JSON.stringify(deploymentParameters, null, 4)); + fs.writeFileSync(".serverless/arm-template.json", JSON.stringify(deployment.template, null, 4)); + + const armDeployment: Deployment = { properties: { mode: "Incremental", template: deployment.template, - parameters: deployment.parameters, + parameters: deploymentParameters, } }; // Deploy ARM template - return await this.resourceClient.deployments.createOrUpdate(this.resourceGroup, this.deploymentName, deploymentParameters); + const result = await this.resourceClient.deployments.createOrUpdate(this.resourceGroup, this.deploymentName, armDeployment); + this.serverless.cli.log("ARM deployment complete"); + + return result; } } \ No newline at end of file diff --git a/src/services/baseService.test.ts b/src/services/baseService.test.ts index 2ea8af60..1a522e91 100644 --- a/src/services/baseService.test.ts +++ b/src/services/baseService.test.ts @@ -85,7 +85,7 @@ describe("Base Service", () => { }); it("Sets default region and stage values if not defined", () => { - const testService = new TestService(sls); + const testService = new MockService(sls); expect(testService).not.toBeNull(); expect(sls.service.provider.region).toEqual("westus"); @@ -97,7 +97,7 @@ describe("Base Service", () => { stage: "prod", region: "eastus2" }; - const testService = new TestService(sls, cliOptions); + const testService = new MockService(sls, cliOptions); expect(testService.getSlsRegion()).toEqual(cliOptions.region); expect(testService.getSlsStage()).toEqual(cliOptions.stage); @@ -136,7 +136,7 @@ describe("Base Service", () => { const region = testService.getSlsRegion(); const stage = testService.getSlsStage(); - expect(resourceGroupName).toEqual(`${sls.service["service"]}-${region}-${stage}-rg`); + expect(resourceGroupName).toEqual(`sls-${region}-${stage}-${sls.service["service"]}-rg`); }); it("Fails if credentials have not been set in serverless config", () => { diff --git a/src/services/baseService.ts b/src/services/baseService.ts index 4a81638e..a7556eb7 100644 --- a/src/services/baseService.ts +++ b/src/services/baseService.ts @@ -3,6 +3,7 @@ import fs from "fs"; import request from "request"; import Serverless from "serverless"; import { Guard } from "../shared/guard"; +import { ServerlessAzureConfig } from "../models/serverless"; export abstract class BaseService { protected baseUrl: string; @@ -11,6 +12,7 @@ export abstract class BaseService { protected subscriptionId: string; protected resourceGroup: string; protected deploymentName: string; + protected config: ServerlessAzureConfig; protected constructor( protected serverless: Serverless, @@ -19,24 +21,41 @@ export abstract class BaseService { ) { Guard.null(serverless); + this.setDefaultValues(); + this.baseUrl = "https://management.azure.com"; + this.config = serverless.service as any; this.serviceName = serverless.service["service"]; this.credentials = serverless.variables["azureCredentials"]; this.subscriptionId = serverless.variables["subscriptionId"]; this.resourceGroup = this.getResourceGroupName(); this.deploymentName = serverless.service.provider["deploymentName"] || `${this.resourceGroup}-deployment`; - this.setDefaultValues(); - if (!this.credentials && authenticate) { throw new Error(`Azure Credentials has not been set in ${this.constructor.name}`); } } - public getResourceGroup(): string { - return this.resourceGroup; + public getRegion(): string { + return this.options.region || this.serverless.service.provider.region; } + public getStage(): string { + return this.options.stage || this.serverless.service.provider.stage; + } + + public getFunctionAppName(): string { + return this.config.provider.functionApp + ? this.config.provider.functionApp.name + : `${this.config.provider.prefix}-${this.config.provider.region}-${this.config.provider.stage}-${this.config.service}`; + } + + public getResourceGroupName(): string { + return this.options["resourceGroup"] + || this.serverless.service.provider["resourceGroup"] + || `${this.config.provider.prefix}-${this.getRegion()}-${this.getStage()}-${this.serviceName}-rg`; + } + /** * Sends an API request using axios HTTP library * @param method The HTTP method @@ -105,19 +124,9 @@ export abstract class BaseService { if (!this.serverless.service.provider.stage) { this.serverless.service.provider.stage = "dev"; } - } - - protected getRegion(): string { - return this.options.region || this.serverless.service.provider.region; - } - protected getStage(): string { - return this.options.stage || this.serverless.service.provider.stage; - } - - protected getResourceGroupName(): string { - return this.options["resourceGroup"] - || this.serverless.service.provider["resourceGroup"] - || `${this.serviceName}-${this.getRegion()}-${this.getStage()}-rg`; + if (!this.serverless.service.provider["prefix"]) { + this.serverless.service.provider["prefix"] = "sls"; + } } } diff --git a/src/services/functionAppService.test.ts b/src/services/functionAppService.test.ts index fce61cb8..a37426f5 100644 --- a/src/services/functionAppService.test.ts +++ b/src/services/functionAppService.test.ts @@ -33,7 +33,7 @@ describe("Function App Service", () => { beforeAll(() => { - // TODO: How to spy on defaul exported function? + // TODO: How to spy on default exported function? const axiosMock = new MockAdapter(axios); // Master Key @@ -86,7 +86,7 @@ describe("Function App Service", () => { const service = createService(); const result = await service.get(); expect(WebSiteManagementClient.prototype.webApps.get) - .toBeCalledWith(provider.resourceGroup, slsService["service"]); + .toBeCalledWith(provider.resourceGroup, service.getFunctionAppName()); expect(result).toEqual(app) }); @@ -98,7 +98,7 @@ describe("Function App Service", () => { } as any; const result = await service.get(); expect(WebSiteManagementClient.prototype.webApps.get) - .toBeCalledWith(provider.resourceGroup, slsService["service"]); + .toBeCalledWith(provider.resourceGroup, service.getFunctionAppName()); expect(result).toBeNull(); }); diff --git a/src/services/functionAppService.ts b/src/services/functionAppService.ts index 4b1eb687..1a9740b6 100644 --- a/src/services/functionAppService.ts +++ b/src/services/functionAppService.ts @@ -1,8 +1,6 @@ import fs from "fs"; import path from "path"; import { WebSiteManagementClient } from "@azure/arm-appservice"; -import { ResourceManagementClient } from "@azure/arm-resources"; -import { Deployment } from "@azure/arm-resources/esm/models"; import jsonpath from "jsonpath"; import _ from "lodash"; import Serverless from "serverless"; @@ -13,18 +11,15 @@ import { Guard } from "../shared/guard"; import { ArmService, ArmDeployment } from "./armService"; export class FunctionAppService extends BaseService { - private resourceClient: ResourceManagementClient; private webClient: WebSiteManagementClient; public constructor(serverless: Serverless, options: Serverless.Options) { super(serverless, options); - - this.resourceClient = new ResourceManagementClient(this.credentials, this.subscriptionId); this.webClient = new WebSiteManagementClient(this.credentials, this.subscriptionId); } public async get(): Promise { - const response: any = await this.webClient.webApps.get(this.resourceGroup, this.serviceName); + const response: any = await this.webClient.webApps.get(this.resourceGroup, this.getFunctionAppName()); if (response.error && (response.error.code === "ResourceNotFound" || response.error.code === "ResourceGroupNotFound")) { return null; } @@ -112,7 +107,7 @@ export class FunctionAppService extends BaseService { } public async uploadFunctions(functionApp: Site): Promise { - Guard.null(functionApp); + Guard.null(functionApp, "functionApp"); this.log("Deploying serverless functions..."); await this.zipDeploy(functionApp); @@ -128,10 +123,10 @@ export class FunctionAppService extends BaseService { const armService = new ArmService(this.serverless); let deployment: ArmDeployment; - if (this.serverless.service.provider["armTemplate"]) { - this.log(`-> Deploying custom ARM template: ${this.serverless.service.provider["armTemplate"].file}`); - const templateFilePath = path.join(this.serverless.config.servicePath, this.serverless.service.provider["armTemplate"].file); - const userParameters = this.serverless.service.provider["armTemplate"].parameters; + if (this.config.provider.armTemplate) { + this.log(`-> Deploying custom ARM template: ${this.config.provider.armTemplate.file}`); + const templateFilePath = path.join(this.serverless.config.servicePath, this.config.provider.armTemplate.file); + const userParameters = this.config.provider.armTemplate.parameters; const userParametersKeys = Object.keys(userParameters); let template = JSON.parse(fs.readFileSync(templateFilePath, "utf8")); let parameters: any; @@ -145,7 +140,7 @@ export class FunctionAppService extends BaseService { deployment = { template, parameters }; } else { - deployment = await armService.createDeployment(this.serverless.service.provider["type"] || "consumption"); + deployment = await armService.createDeployment(this.config.provider.type || "consumption"); } // Check if there are custom environment variables defined that need to be From c49b66c53c0d1b0a37decd040a34d4226127b32e Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Fri, 21 Jun 2019 15:36:57 -0700 Subject: [PATCH 11/17] test: Verify ARM deployments --- package-lock.json | 6 ++ package.json | 1 + src/armTemplates/ase.ts | 16 ++- src/armTemplates/premium.ts | 2 +- src/armTemplates/resources/functionApp.ts | 18 +++- src/models/serverless.ts | 4 +- src/services/armService.test.ts | 117 ++++++++++++++++++++-- src/services/armService.ts | 82 ++++++++++++++- src/services/functionAppService.test.ts | 54 ++++++++++ src/services/functionAppService.ts | 44 +------- 10 files changed, 284 insertions(+), 60 deletions(-) diff --git a/package-lock.json b/package-lock.json index 98cfecab..b3cfd585 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1491,6 +1491,12 @@ "integrity": "sha512-yALhelO3i0hqZwhjtcr6dYyaLoCHbAMshwtj6cGxTvHZAKXHsYGdff6E8EPw3xLKY0ELUTQ69Q1rQiJENnccMA==", "dev": true }, + "@types/jsonpath": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@types/jsonpath/-/jsonpath-0.2.0.tgz", + "integrity": "sha512-v7qlPA0VpKUlEdhghbDqRoKMxFB3h3Ch688TApBJ6v+XLDdvWCGLJIYiPKGZnS6MAOie+IorCfNYVHOPIHSWwQ==", + "dev": true + }, "@types/lodash": { "version": "4.14.133", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.133.tgz", diff --git a/package.json b/package.json index 744b556f..fa52a26c 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "devDependencies": { "@babel/runtime": "^7.4.5", "@types/jest": "^24.0.13", + "@types/jsonpath": "^0.2.0", "@types/lodash": "^4.14.130", "@types/mock-fs": "^3.6.30", "@types/open": "^6.1.0", diff --git a/src/armTemplates/ase.ts b/src/armTemplates/ase.ts index 89a4aa9d..d8ca59e0 100644 --- a/src/armTemplates/ase.ts +++ b/src/armTemplates/ase.ts @@ -41,11 +41,25 @@ const AseTemplate: ArmResourceTemplateGenerator = { template.parameters.appServicePlanSkuName.defaultValue = "I1"; template.parameters.appServicePlanSkuTier.defaultValue = "Isolated"; + // Update the app service plan to point to the hosting environment + const appServicePlan: any = template.resources.find((resource: any) => resource.type === "Microsoft.Web/serverfarms"); + if (appServicePlan) { + appServicePlan.dependsOn = [...(appServicePlan.dependsOn || []), "[resourceId('Microsoft.Web/hostingEnvironments', parameters('hostingEnvironmentName'))]"]; + appServicePlan.properties.hostingEnvironmentProfile = { + ...appServicePlan.properties.hostingEnvironmentProfile, + id: "[resourceId('Microsoft.Web/hostingEnvironments', parameters('hostingEnvironmentName'))]", + } + } + // Update the functionApp resource to include the app service plan references const app: any = template.resources.find((resource: any) => resource.type === "Microsoft.Web/sites"); if (app) { + app.dependsOn = [...(app.dependsOn || []), "[concat('Microsoft.Web/serverfarms/', parameters('appServicePlanName'))]"]; app.properties.serverFarmId = "[resourceId('Microsoft.Web/serverfarms', parameters('appServicePlanName'))]"; - app.dependsOn.push("[concat('Microsoft.Web/serverfarms/', parameters('appServicePlanName'))]") + app.properties.hostingEnvironmentProfile = { + ...app.properties.hostingEnvironmentProfile, + id: "[resourceId('Microsoft.Web/hostingEnvironments', parameters('hostingEnvironmentName'))]", + } } return template; diff --git a/src/armTemplates/premium.ts b/src/armTemplates/premium.ts index 2bb84c56..f38d6055 100644 --- a/src/armTemplates/premium.ts +++ b/src/armTemplates/premium.ts @@ -41,7 +41,7 @@ const PremiumTemplate: ArmResourceTemplateGenerator = { const app: any = template.resources.find((resource: any) => resource.type === "Microsoft.Web/sites"); if (app) { app.properties.serverFarmId = "[resourceId('Microsoft.Web/serverfarms', parameters('appServicePlanName'))]"; - app.dependsOn.push("[concat('Microsoft.Web/serverfarms/', parameters('appServicePlanName'))]") + app.dependsOn = [...(app.dependsOn || []), "[concat('Microsoft.Web/serverfarms/', parameters('appServicePlanName'))]"] } return template; diff --git a/src/armTemplates/resources/functionApp.ts b/src/armTemplates/resources/functionApp.ts index 492b6bf3..680900c9 100644 --- a/src/armTemplates/resources/functionApp.ts +++ b/src/armTemplates/resources/functionApp.ts @@ -11,6 +11,18 @@ export const FunctionAppResource: ArmResourceTemplateGenerator = { "defaultValue": "", "type": "String" }, + "functionAppNodeVersion": { + "defaultValue": "10.14.1", + "type": "String" + }, + "functionAppWorkerRuntime": { + "defaultValue": "node", + "type": "String" + }, + "functionAppExtensionVersion": { + "defaultValue": "~2", + "type": "String" + }, "storageAccountName": { "defaultValue": "", "type": "String" @@ -41,11 +53,11 @@ export const FunctionAppResource: ArmResourceTemplateGenerator = { "appSettings": [ { "name": "FUNCTIONS_WORKER_RUNTIME", - "value": "node" + "value": "[parameters('functionAppWorkerRuntime')]" }, { "name": "FUNCTIONS_EXTENSION_VERSION", - "value": "~2" + "value": "[parameters('functionAppExtensionVersion')]" }, { "name": "AzureWebJobsStorage", @@ -61,7 +73,7 @@ export const FunctionAppResource: ArmResourceTemplateGenerator = { }, { "name": "WEBSITE_NODE_DEFAULT_VERSION", - "value": "10.14.1" + "value": "[parameters('functionAppNodeVersion')]" }, { "name": "WEBSITE_RUN_FROM_PACKAGE", diff --git a/src/models/serverless.ts b/src/models/serverless.ts index f0f61649..636c3d39 100644 --- a/src/models/serverless.ts +++ b/src/models/serverless.ts @@ -6,7 +6,6 @@ export enum DeploymentType { } export interface ArmTemplateConfig { - type: string; file: string; parameters: { @@ -31,6 +30,9 @@ export interface ServerlessAzureConfig { region: string; stage: string; name: string; + environment?: { + [key: string]: any; + }; resourceGroup?: string; apim?: ApiManagementConfig; functionApp?: ResourceConfig; diff --git a/src/services/armService.test.ts b/src/services/armService.test.ts index 71772ce7..1325ab62 100644 --- a/src/services/armService.test.ts +++ b/src/services/armService.test.ts @@ -1,6 +1,11 @@ import Serverless from "serverless"; import { MockFactory } from "../test/mockFactory"; -import { ArmService } from "./armService"; +import { ArmService, ArmResourceTemplate, ArmTemplateType } from "./armService"; +import { ArmTemplateConfig } from "../models/serverless"; +import mockFs from "mock-fs"; +import jsonpath from "jsonpath"; +import { Deployments } from "@azure/arm-resources"; +import { Deployment } from "@azure/arm-resources/esm/models"; describe("Arm Service", () => { let sls: Serverless @@ -24,9 +29,54 @@ describe("Arm Service", () => { service = createService(); }) + afterEach(() => { + mockFs.restore(); + }) + describe("Creating Templates", () => { + it("Creates an ARM template from a specified file", async () => { + const armTemplateConfig: ArmTemplateConfig = { + file: "armTemplates/custom-template.json", + parameters: { + param1: "1", + param2: "2", + }, + }; + + const testTemplate: ArmResourceTemplate = { + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "param1": { + "defaultValue": "", + "type": "String" + }, + "param2": { + "defaultValue": "", + "type": "String" + }, + }, + "variables": {}, + "resources": [] + }; + + mockFs({ + "armTemplates": { + "custom-template.json": JSON.stringify(testTemplate), + }, + }); + + sls.service.provider["armTemplate"] = armTemplateConfig; + const deployment = await service.createDeploymentFromConfig(sls.service.provider["armTemplate"]); + + expect(deployment).not.toBeNull(); + expect(deployment.template.parameters).toEqual(testTemplate.parameters); + expect(deployment.template.resources).toEqual(testTemplate.resources); + expect(deployment.parameters).toEqual(armTemplateConfig.parameters); + }); + it("Creates a custom ARM template from well-known type", async () => { - const deployment = await service.createDeployment("premium"); + const deployment = await service.createDeploymentFromType("premium"); expect(deployment).not.toBeNull(); expect(Object.keys(deployment.parameters).length).toBeGreaterThan(0); @@ -35,7 +85,7 @@ describe("Arm Service", () => { it("Creates a custom ARM template (with APIM support) from well-known type", async () => { sls.service.provider["apim"] = MockFactory.createTestApimConfig(); - const deployment = await service.createDeployment("premium"); + const deployment = await service.createDeploymentFromType(ArmTemplateType.Premium); expect(deployment).not.toBeNull(); expect(Object.keys(deployment.parameters).length).toBeGreaterThan(0); @@ -45,11 +95,11 @@ describe("Arm Service", () => { }); it("throws error when specified type is not found", async () => { - await expect(service.createDeployment("not-found")).rejects.not.toBeNull(); + await expect(service.createDeploymentFromType("not-found")).rejects.not.toBeNull(); }); it("Premium template includes correct resources", async () => { - const deployment = await service.createDeployment("premium"); + const deployment = await service.createDeploymentFromType(ArmTemplateType.Premium); expect(deployment.template.parameters.appServicePlanSkuTier.defaultValue).toEqual("ElasticPremium"); expect(deployment.template.parameters.appServicePlanSkuName.defaultValue).toEqual("EP1"); @@ -63,7 +113,7 @@ describe("Arm Service", () => { expect(deployment.template.resources.find((resource) => resource.type === "Microsoft.Web/sites")).not.toBeNull(); expect(deployment.template.resources.find((resource) => resource.type === "Microsoft.Storage/storageAccounts")).not.toBeNull(); expect(deployment.template.resources.find((resource) => resource.type === "microsoft.insights/components")).not.toBeNull(); - + // Verify the ARM template includes the linkage to the correct server farm const functionApp = deployment.template.resources.find((res) => res.type === "Microsoft.Web/sites"); expect(functionApp.dependsOn).toContain("[concat('Microsoft.Web/serverfarms/', parameters('appServicePlanName'))]"); @@ -71,7 +121,7 @@ describe("Arm Service", () => { }); it("ASE template includes correct resources", async () => { - const deployment = await service.createDeployment("ase"); + const deployment = await service.createDeploymentFromType(ArmTemplateType.AppServiceEnvironment); expect(deployment.template.parameters.appServicePlanSkuTier.defaultValue).toEqual("Isolated"); expect(deployment.template.parameters.appServicePlanSkuName.defaultValue).toEqual("I1"); @@ -84,13 +134,19 @@ describe("Arm Service", () => { expect(deployment.template.resources.find((resource) => resource.type === "microsoft.insights/components")).not.toBeNull(); // Verify the ARM template includes the linkage to the correct server farm + const appServicePlan = deployment.template.resources.find((res) => res.type === "Microsoft.Web/serverfarms"); + expect(appServicePlan.dependsOn).toContain("[resourceId('Microsoft.Web/hostingEnvironments', parameters('hostingEnvironmentName'))]"); + expect(appServicePlan.properties.hostingEnvironmentProfile.id).toEqual("[resourceId('Microsoft.Web/hostingEnvironments', parameters('hostingEnvironmentName'))]"); + + // Verify the ARM template includes the linkage to the correct hosting environment const functionApp = deployment.template.resources.find((res) => res.type === "Microsoft.Web/sites"); expect(functionApp.dependsOn).toContain("[concat('Microsoft.Web/serverfarms/', parameters('appServicePlanName'))]"); expect(functionApp.properties.serverFarmId).toEqual("[resourceId('Microsoft.Web/serverfarms', parameters('appServicePlanName'))]"); + expect(functionApp.properties.hostingEnvironmentProfile.id).toEqual("[resourceId('Microsoft.Web/hostingEnvironments', parameters('hostingEnvironmentName'))]"); }); it("Consumption template includes correct resources", async () => { - const deployment = await service.createDeployment("consumption"); + const deployment = await service.createDeploymentFromType(ArmTemplateType.Consumption); expect(deployment.template.resources.find((resource) => resource.type === "Microsoft.Web/hostingEnvironments")).toBeUndefined(); expect(deployment.template.resources.find((resource) => resource.type === "Microsoft.Network/virtualNetworks")).toBeUndefined(); @@ -104,6 +160,51 @@ describe("Arm Service", () => { }); describe("Deploying Templates", () => { + beforeEach(() => { + Deployments.prototype.createOrUpdate = jest.fn(() => Promise.resolve(null)); + }); + + it("Appends environment variables into app settings of ARM template", async () => { + const environmentConfig: any = { + PARAM_1: "1", + PARAM_2: "2", + PARAM_3: "3", + }; + + sls.service.provider["environment"] = environmentConfig + + const deployment = await service.createDeploymentFromType(ArmTemplateType.Consumption); + await service.deployTemplate(deployment); + const appSettings: any[] = jsonpath.query(deployment.template, "$.resources[?(@.kind==\"functionapp\")].properties.siteConfig.appSettings[*]"); + expect(appSettings.find((setting) => setting.name === "PARAM_1")).toEqual({ name: "PARAM_1", value: environmentConfig.PARAM_1 }); + expect(appSettings.find((setting) => setting.name === "PARAM_2")).toEqual({ name: "PARAM_2", value: environmentConfig.PARAM_2 }); + expect(appSettings.find((setting) => setting.name === "PARAM_3")).toEqual({ name: "PARAM_3", value: environmentConfig.PARAM_3 }); + }); + + it("Deploys ARM template via resources REST API", async () => { + const deployment = await service.createDeploymentFromType(ArmTemplateType.Consumption); + const deploymentParameters = {}; + Object.keys(deployment.parameters).forEach((key) => { + const parameterValue = deployment.parameters[key]; + if (parameterValue) { + deploymentParameters[key] = { value: deployment.parameters[key] }; + } + }); + + await service.deployTemplate(deployment); + + const expectedResourceGroup = sls.service.provider["resourceGroup"]; + const expectedDeploymentName = sls.service.provider["deploymentName"] || `${this.resourceGroup}-deployment`; + const expectedDeployment: Deployment = { + properties: { + mode: "Incremental", + template: deployment.template, + parameters: deploymentParameters, + }, + }; + + expect(Deployments.prototype.createOrUpdate).toBeCalledWith(expectedResourceGroup, expectedDeploymentName, expectedDeployment); + }); }); }); \ No newline at end of file diff --git a/src/services/armService.ts b/src/services/armService.ts index 1d8a5829..0e8bd3da 100644 --- a/src/services/armService.ts +++ b/src/services/armService.ts @@ -3,14 +3,25 @@ import { Deployment, DeploymentExtended } from "@azure/arm-resources/esm/models" import { BaseService } from "./baseService"; import { ResourceManagementClient } from "@azure/arm-resources"; import { Guard } from "../shared/guard"; -import { ServerlessAzureConfig } from "../models/serverless"; +import { ServerlessAzureConfig, ArmTemplateConfig } from "../models/serverless"; import fs from "fs"; +import path from "path"; +import jsonpath from "jsonpath"; export interface ArmResourceTemplateGenerator { getTemplate(): ArmResourceTemplate; getParameters(config: ServerlessAzureConfig): any; } +/** + * The well-known serverless Azure template types + */ +export enum ArmTemplateType { + Consumption = "consumption", + Premium = "premium", + AppServiceEnvironment = "ase", +} + export interface ArmResourceTemplate { $schema: string; contentVersion: string; @@ -34,9 +45,15 @@ export class ArmService extends BaseService { this.resourceClient = new ResourceManagementClient(this.credentials, this.subscriptionId); } - public async createDeployment(type: string): Promise { + /** + * Creates an ARM deployment from a well-known configuration (consumption, premium, ase) + * @param type The well-known template type + */ + public async createDeploymentFromType(type: ArmTemplateType | string): Promise { Guard.empty(type); + this.log(`-> Creating ARM template from type: ${type}`); + const { ApimResource } = await import("../armTemplates/resources/apim"); let template: ArmResourceTemplateGenerator; @@ -76,9 +93,33 @@ export class ArmService extends BaseService { }; } + /** + * Creates an ARM deployment from the serverless custom yaml configuration + * @param armTemplateConfig The serverless yaml ARM template configuration + */ + public createDeploymentFromConfig(armTemplateConfig: ArmTemplateConfig): Promise { + Guard.null(armTemplateConfig); + + this.log(`-> Creating ARM template from file: ${armTemplateConfig.file}`); + const templateFilePath = path.join(this.serverless.config.servicePath, armTemplateConfig.file); + const template = JSON.parse(fs.readFileSync(templateFilePath, "utf8")); + + return Promise.resolve({ + template, + parameters: armTemplateConfig.parameters + }); + } + + /** + * Deploys the specified ARM template to Azure via REST service call + * @param deployment The ARM template to deploy + */ public async deployTemplate(deployment: ArmDeployment): Promise { Guard.null(deployment); + this.applyEnvironmentVariables(deployment); + + // Convert flat parameter list into ARM parameter format const deploymentParameters = {}; Object.keys(deployment.parameters).forEach((key) => { const parameterValue = deployment.parameters[key]; @@ -87,9 +128,10 @@ export class ArmService extends BaseService { } }); - this.serverless.cli.log(JSON.stringify(deploymentParameters, null, 4)); - fs.writeFileSync(".serverless/arm-template.json", JSON.stringify(deployment.template, null, 4)); + // this.serverless.cli.log(JSON.stringify(deploymentParameters, null, 4)); + // fs.writeFileSync(".serverless/arm-template.json", JSON.stringify(deployment.template, null, 4)); + // Construct deployment object const armDeployment: Deployment = { properties: { mode: "Incremental", @@ -99,9 +141,39 @@ export class ArmService extends BaseService { }; // Deploy ARM template + this.serverless.cli.log("-> Deploying ARM template..."); const result = await this.resourceClient.deployments.createOrUpdate(this.resourceGroup, this.deploymentName, armDeployment); - this.serverless.cli.log("ARM deployment complete"); + this.serverless.cli.log("-> ARM deployment complete"); return result; } + + /** + * Applies sls yaml environment variables into the appSettings section of the function app configuration + * @param deployment The ARM deployment + */ + private applyEnvironmentVariables(deployment: ArmDeployment) { + // Check if there are custom environment variables defined that need to be + // added to the ARM template used in the deployment. + const environmentVariables = this.config.provider.environment; + if (environmentVariables) { + this.serverless.cli.log("-> Merging environment configuration"); + + // This is a json path expression + // Learn more @ https://goessner.net/articles/JsonPath/index.html#e2 + const appSettingsPath = "$.resources[?(@.kind==\"functionapp\")].properties.siteConfig.appSettings"; + + // Merges serverless yaml environment configuration into the app settings of the template + jsonpath.apply(deployment.template, appSettingsPath, function (appSettingsList) { + Object.keys(environmentVariables).forEach(function (key) { + appSettingsList.push({ + name: key, + value: environmentVariables[key] + }); + }); + + return appSettingsList; + }); + } + } } \ No newline at end of file diff --git a/src/services/functionAppService.test.ts b/src/services/functionAppService.test.ts index a37426f5..b77f007d 100644 --- a/src/services/functionAppService.test.ts +++ b/src/services/functionAppService.test.ts @@ -7,6 +7,7 @@ import { FunctionAppService } from "./functionAppService"; jest.mock("@azure/arm-appservice") import { WebSiteManagementClient } from "@azure/arm-appservice"; +import { ArmService, ArmDeployment, ArmTemplateType } from "./armService"; jest.mock("@azure/arm-resources") describe("Function App Service", () => { @@ -140,6 +141,59 @@ describe("Function App Service", () => { expect(await service.listFunctions(app)).toEqual(functionsResponse.map((f) => f.properties)); }); + describe("Deployments", () => { + const expectedDeployment: ArmDeployment = { + parameters: {}, + template: {}, + }; + + const expectedSite = MockFactory.createTestSite(); + + beforeEach(() => { + FunctionAppService.prototype.get = jest.fn(() => Promise.resolve(expectedSite)); + ArmService.prototype.createDeploymentFromConfig = jest.fn(() => Promise.resolve(expectedDeployment)); + ArmService.prototype.createDeploymentFromType = jest.fn(() => Promise.resolve(expectedDeployment)); + ArmService.prototype.deployTemplate = jest.fn(() => Promise.resolve(null)); + }); + + it("deploys ARM templates with custom configuration", async () => { + slsService.provider["armTemplate"] = {}; + + const service = createService(); + const site = await service.deploy(); + + expect(site).toEqual(expectedSite); + expect(ArmService.prototype.createDeploymentFromConfig).toBeCalledWith(slsService.provider["armTemplate"]); + expect(ArmService.prototype.createDeploymentFromType).not.toBeCalled(); + expect(ArmService.prototype.deployTemplate).toBeCalledWith(expectedDeployment); + }); + + it("deploys ARM template from well-known (default) configuration", async () => { + slsService.provider["armTemplate"] = null; + + const service = createService(); + const site = await service.deploy(); + + expect(site).toEqual(expectedSite); + expect(ArmService.prototype.createDeploymentFromConfig).not.toBeCalled(); + expect(ArmService.prototype.createDeploymentFromType).toBeCalledWith(ArmTemplateType.Consumption); + expect(ArmService.prototype.deployTemplate).toBeCalledWith(expectedDeployment); + }); + + it("deploys ARM template from well-known configuration", async () => { + slsService.provider["armTemplate"] = null; + slsService.provider["type"] = "premium"; + + const service = createService(); + const site = await service.deploy(); + + expect(site).toEqual(expectedSite); + expect(ArmService.prototype.createDeploymentFromConfig).not.toBeCalled(); + expect(ArmService.prototype.createDeploymentFromType).toBeCalledWith(ArmTemplateType.Premium); + expect(ArmService.prototype.deployTemplate).toBeCalledWith(expectedDeployment); + }); + }); + it("uploads functions", async () => { const service = createService(); await service.uploadFunctions(app); diff --git a/src/services/functionAppService.ts b/src/services/functionAppService.ts index 1a9740b6..424101e5 100644 --- a/src/services/functionAppService.ts +++ b/src/services/functionAppService.ts @@ -1,8 +1,6 @@ import fs from "fs"; import path from "path"; import { WebSiteManagementClient } from "@azure/arm-appservice"; -import jsonpath from "jsonpath"; -import _ from "lodash"; import Serverless from "serverless"; import { BaseService } from "./baseService"; import { FunctionAppHttpTriggerConfig } from "../models/functionApp"; @@ -121,45 +119,9 @@ export class FunctionAppService extends BaseService { this.log(`Creating function app: ${this.serviceName}`); const armService = new ArmService(this.serverless); - let deployment: ArmDeployment; - - if (this.config.provider.armTemplate) { - this.log(`-> Deploying custom ARM template: ${this.config.provider.armTemplate.file}`); - const templateFilePath = path.join(this.serverless.config.servicePath, this.config.provider.armTemplate.file); - const userParameters = this.config.provider.armTemplate.parameters; - const userParametersKeys = Object.keys(userParameters); - let template = JSON.parse(fs.readFileSync(templateFilePath, "utf8")); - let parameters: any; - for (let paramIndex = 0; paramIndex < userParametersKeys.length; paramIndex++) { - const item = {}; - - item[userParametersKeys[paramIndex]] = { "value": userParameters[userParametersKeys[paramIndex]] }; - parameters = _.merge(parameters, item); - } - - deployment = { template, parameters }; - - } else { - deployment = await armService.createDeployment(this.config.provider.type || "consumption"); - } - - // Check if there are custom environment variables defined that need to be - // added to the ARM template used in the deployment. - const environmentVariables = this.serverless.service.provider["environment"]; - if (environmentVariables) { - const appSettingsPath = "$.resources[?(@.kind==\"functionapp\")].properties.siteConfig.appSettings"; - - jsonpath.apply(deployment.template, appSettingsPath, function (appSettingsList) { - Object.keys(environmentVariables).forEach(function (key) { - appSettingsList.push({ - name: key, - value: environmentVariables[key] - }); - }); - - return appSettingsList; - }); - } + let deployment: ArmDeployment = this.config.provider.armTemplate + ? await armService.createDeploymentFromConfig(this.config.provider.armTemplate) + : await armService.createDeploymentFromType(this.config.provider.type || "consumption"); await armService.deployTemplate(deployment); From 3791227f57a1b429093e7487d2683a756f99aa6b Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Fri, 21 Jun 2019 15:49:33 -0700 Subject: [PATCH 12/17] fix: Moved models out into seperate module --- src/models/armTemplates.ts | 39 +++++++++++++++++++++++++ src/services/armService.test.ts | 3 +- src/services/armService.ts | 30 +------------------ src/services/baseService.test.ts | 36 +++++++++++------------ src/services/functionAppService.test.ts | 3 +- 5 files changed, 62 insertions(+), 49 deletions(-) create mode 100644 src/models/armTemplates.ts diff --git a/src/models/armTemplates.ts b/src/models/armTemplates.ts new file mode 100644 index 00000000..a35d050a --- /dev/null +++ b/src/models/armTemplates.ts @@ -0,0 +1,39 @@ +import { ServerlessAzureConfig } from "./serverless"; + +/** + * ARM Resource Template Generator + */ +export interface ArmResourceTemplateGenerator { + getTemplate(): ArmResourceTemplate; + getParameters(config: ServerlessAzureConfig): any; +} + +/** + * The well-known serverless Azure template types + */ +export enum ArmTemplateType { + Consumption = "consumption", + Premium = "premium", + AppServiceEnvironment = "ase", +} + +/** + * Represents an Azure ARM template + */ +export interface ArmResourceTemplate { + $schema: string; + contentVersion: string; + parameters: { + [key: string]: any; + }; + resources: any[]; + variables?: any; +} + +/** + * Represents an Azure ARM deployment + */ +export interface ArmDeployment { + template: ArmResourceTemplate; + parameters: { [key: string]: any }; +} diff --git a/src/services/armService.test.ts b/src/services/armService.test.ts index 1325ab62..5cd5127b 100644 --- a/src/services/armService.test.ts +++ b/src/services/armService.test.ts @@ -1,6 +1,7 @@ import Serverless from "serverless"; import { MockFactory } from "../test/mockFactory"; -import { ArmService, ArmResourceTemplate, ArmTemplateType } from "./armService"; +import { ArmService } from "./armService"; +import { ArmResourceTemplate, ArmTemplateType } from "../models/armTemplates"; import { ArmTemplateConfig } from "../models/serverless"; import mockFs from "mock-fs"; import jsonpath from "jsonpath"; diff --git a/src/services/armService.ts b/src/services/armService.ts index 0e8bd3da..dbdc4a7b 100644 --- a/src/services/armService.ts +++ b/src/services/armService.ts @@ -4,39 +4,11 @@ import { BaseService } from "./baseService"; import { ResourceManagementClient } from "@azure/arm-resources"; import { Guard } from "../shared/guard"; import { ServerlessAzureConfig, ArmTemplateConfig } from "../models/serverless"; +import { ArmDeployment, ArmResourceTemplateGenerator, ArmTemplateType } from "../models/armTemplates"; import fs from "fs"; import path from "path"; import jsonpath from "jsonpath"; -export interface ArmResourceTemplateGenerator { - getTemplate(): ArmResourceTemplate; - getParameters(config: ServerlessAzureConfig): any; -} - -/** - * The well-known serverless Azure template types - */ -export enum ArmTemplateType { - Consumption = "consumption", - Premium = "premium", - AppServiceEnvironment = "ase", -} - -export interface ArmResourceTemplate { - $schema: string; - contentVersion: string; - parameters: { - [key: string]: any; - }; - resources: any[]; - variables?: any; -} - -export interface ArmDeployment { - template: any; - parameters: { [key: string]: any }; -} - export class ArmService extends BaseService { private resourceClient: ResourceManagementClient; diff --git a/src/services/baseService.test.ts b/src/services/baseService.test.ts index 1a522e91..29971246 100644 --- a/src/services/baseService.test.ts +++ b/src/services/baseService.test.ts @@ -61,7 +61,7 @@ describe("Base Service", () => { mockFs.restore(); }); - function createTestService(options?: Serverless.Options) { + function createMockService(options?: Serverless.Options) { sls = MockFactory.createTestServerless(); sls.variables["azureCredentials"] = MockFactory.createTestAzureCredentials(); sls.variables["subscriptionId"] = "ABC123"; @@ -71,7 +71,7 @@ describe("Base Service", () => { } beforeEach(() => { - service = createTestService(); + service = createMockService(); }); it("Initializes common service properties", () => { @@ -85,9 +85,9 @@ describe("Base Service", () => { }); it("Sets default region and stage values if not defined", () => { - const testService = new MockService(sls); + const mockService = new MockService(sls); - expect(testService).not.toBeNull(); + expect(mockService).not.toBeNull(); expect(sls.service.provider.region).toEqual("westus"); expect(sls.service.provider.stage).toEqual("dev"); }); @@ -97,16 +97,16 @@ describe("Base Service", () => { stage: "prod", region: "eastus2" }; - const testService = new MockService(sls, cliOptions); + const mockService = new MockService(sls, cliOptions); - expect(testService.getSlsRegion()).toEqual(cliOptions.region); - expect(testService.getSlsStage()).toEqual(cliOptions.stage); + expect(mockService.getSlsRegion()).toEqual(cliOptions.region); + expect(mockService.getSlsStage()).toEqual(cliOptions.stage); }); it("Sets default region and stage values if not defined", () => { - const testService = new MockService(sls); + const mockService = new MockService(sls); - expect(testService).not.toBeNull(); + expect(mockService).not.toBeNull(); expect(sls.service.provider.region).toEqual("westus"); expect(sls.service.provider.stage).toEqual("dev"); }); @@ -116,25 +116,25 @@ describe("Base Service", () => { stage: "prod", region: "eastus2" }; - const testService = new MockService(sls, cliOptions); + const mockService = new MockService(sls, cliOptions); - expect(testService.getSlsRegion()).toEqual(cliOptions.region); - expect(testService.getSlsStage()).toEqual(cliOptions.stage); + expect(mockService.getSlsRegion()).toEqual(cliOptions.region); + expect(mockService.getSlsStage()).toEqual(cliOptions.stage); }); it("Generates resource group name from sls yaml config", () => { - const testService = new MockService(sls); - const resourceGroupName = testService.getSlsResourceGroupName(); + const mockService = new MockService(sls); + const resourceGroupName = mockService.getSlsResourceGroupName(); expect(resourceGroupName).toEqual(sls.service.provider["resourceGroup"]); }); it("Generates resource group from convention when NOT defined in sls yaml", () => { sls.service.provider["resourceGroup"] = null; - const testService = new MockService(sls); - const resourceGroupName = testService.getSlsResourceGroupName(); - const region = testService.getSlsRegion(); - const stage = testService.getSlsStage(); + const mockService = new MockService(sls); + const resourceGroupName = mockService.getSlsResourceGroupName(); + const region = mockService.getSlsRegion(); + const stage = mockService.getSlsStage(); expect(resourceGroupName).toEqual(`sls-${region}-${stage}-${sls.service["service"]}-rg`); }); diff --git a/src/services/functionAppService.test.ts b/src/services/functionAppService.test.ts index b77f007d..17e421b1 100644 --- a/src/services/functionAppService.test.ts +++ b/src/services/functionAppService.test.ts @@ -4,10 +4,11 @@ import mockFs from "mock-fs"; import Serverless from "serverless"; import { MockFactory } from "../test/mockFactory"; import { FunctionAppService } from "./functionAppService"; +import { ArmService } from "./armService"; jest.mock("@azure/arm-appservice") import { WebSiteManagementClient } from "@azure/arm-appservice"; -import { ArmService, ArmDeployment, ArmTemplateType } from "./armService"; +import { ArmDeployment, ArmTemplateType } from "../models/armTemplates"; jest.mock("@azure/arm-resources") describe("Function App Service", () => { From 651a0bb976bb7e0f7d44807bc6ef6720d2d49599 Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Fri, 21 Jun 2019 16:09:15 -0700 Subject: [PATCH 13/17] Resolved import statements for ARM models --- src/armTemplates/ase.ts | 2 +- src/armTemplates/consumption.ts | 2 +- src/armTemplates/premium.ts | 2 +- src/armTemplates/resources/apim.ts | 2 +- src/armTemplates/resources/appInsights.ts | 2 +- src/armTemplates/resources/appServicePlan.ts | 2 +- src/armTemplates/resources/functionApp.ts | 2 +- .../resources/hostingEnvironment.ts | 2 +- src/armTemplates/resources/storageAccount.ts | 2 +- src/armTemplates/resources/virtualNetwork.ts | 2 +- src/plugins/deploy/azureDeployPlugin.test.ts | 2 +- src/plugins/deploy/azureDeployPlugin.ts | 2 +- src/services/armService.test.ts | 17 +------------ src/services/functionAppService.test.ts | 2 +- src/services/functionAppService.ts | 3 ++- src/test/mockFactory.ts | 24 +++++++++++++++++-- 16 files changed, 38 insertions(+), 32 deletions(-) diff --git a/src/armTemplates/ase.ts b/src/armTemplates/ase.ts index d8ca59e0..bd77f2cc 100644 --- a/src/armTemplates/ase.ts +++ b/src/armTemplates/ase.ts @@ -4,7 +4,7 @@ import { StorageAccountResource } from "./resources/storageAccount"; import { AppServicePlanResource } from "./resources/appServicePlan"; import { HostingEnvironmentResource } from "./resources/hostingEnvironment"; import { VirtualNetworkResource } from "./resources/virtualNetwork"; -import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../services/armService.js"; +import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../models/armTemplates"; import { ServerlessAzureConfig } from "../models/serverless.js"; const resources: ArmResourceTemplateGenerator[] = [ diff --git a/src/armTemplates/consumption.ts b/src/armTemplates/consumption.ts index 5f71883e..59c44130 100644 --- a/src/armTemplates/consumption.ts +++ b/src/armTemplates/consumption.ts @@ -1,7 +1,7 @@ import { FunctionAppResource } from "./resources/functionApp"; import { AppInsightsResource } from "./resources/appInsights"; import { StorageAccountResource } from "./resources/storageAccount"; -import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../services/armService.js"; +import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../models/armTemplates"; import { ServerlessAzureConfig } from "../models/serverless"; const resources: ArmResourceTemplateGenerator[] = [ diff --git a/src/armTemplates/premium.ts b/src/armTemplates/premium.ts index f38d6055..bc8c99af 100644 --- a/src/armTemplates/premium.ts +++ b/src/armTemplates/premium.ts @@ -2,7 +2,7 @@ import { FunctionAppResource } from "./resources/functionApp"; import { AppInsightsResource } from "./resources/appInsights"; import { StorageAccountResource } from "./resources/storageAccount"; import { AppServicePlanResource } from "./resources/appServicePlan"; -import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../services/armService.js"; +import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../models/armTemplates"; import { ServerlessAzureConfig } from "../models/serverless"; const resources: ArmResourceTemplateGenerator[] = [ diff --git a/src/armTemplates/resources/apim.ts b/src/armTemplates/resources/apim.ts index ca22c4cc..cb255e34 100644 --- a/src/armTemplates/resources/apim.ts +++ b/src/armTemplates/resources/apim.ts @@ -1,5 +1,5 @@ import { ServerlessAzureConfig } from "../../models/serverless"; -import { ArmResourceTemplateGenerator } from "../../services/armService"; +import { ArmResourceTemplateGenerator } from "../../models/armTemplates"; import { ApiManagementConfig } from "../../models/apiManagement"; export const ApimResource: ArmResourceTemplateGenerator = { diff --git a/src/armTemplates/resources/appInsights.ts b/src/armTemplates/resources/appInsights.ts index c648005c..85867a7e 100644 --- a/src/armTemplates/resources/appInsights.ts +++ b/src/armTemplates/resources/appInsights.ts @@ -1,4 +1,4 @@ -import { ArmResourceTemplateGenerator } from "../../services/armService"; +import { ArmResourceTemplateGenerator } from "../../models/armTemplates"; import { ServerlessAzureConfig, ResourceConfig } from "../../models/serverless"; export const AppInsightsResource: ArmResourceTemplateGenerator = { diff --git a/src/armTemplates/resources/appServicePlan.ts b/src/armTemplates/resources/appServicePlan.ts index e149e56d..b658a538 100644 --- a/src/armTemplates/resources/appServicePlan.ts +++ b/src/armTemplates/resources/appServicePlan.ts @@ -1,4 +1,4 @@ -import { ArmResourceTemplateGenerator } from "../../services/armService"; +import { ArmResourceTemplateGenerator } from "../../models/armTemplates"; import { ServerlessAzureConfig, ResourceConfig } from "../../models/serverless"; export const AppServicePlanResource: ArmResourceTemplateGenerator = { diff --git a/src/armTemplates/resources/functionApp.ts b/src/armTemplates/resources/functionApp.ts index 680900c9..5730e850 100644 --- a/src/armTemplates/resources/functionApp.ts +++ b/src/armTemplates/resources/functionApp.ts @@ -1,4 +1,4 @@ -import { ArmResourceTemplateGenerator } from "../../services/armService"; +import { ArmResourceTemplateGenerator } from "../../models/armTemplates"; import { ServerlessAzureConfig, ResourceConfig } from "../../models/serverless"; export const FunctionAppResource: ArmResourceTemplateGenerator = { diff --git a/src/armTemplates/resources/hostingEnvironment.ts b/src/armTemplates/resources/hostingEnvironment.ts index 7d60c61f..e143eb05 100644 --- a/src/armTemplates/resources/hostingEnvironment.ts +++ b/src/armTemplates/resources/hostingEnvironment.ts @@ -1,4 +1,4 @@ -import { ArmResourceTemplateGenerator } from "../../services/armService"; +import { ArmResourceTemplateGenerator } from "../../models/armTemplates"; import { ServerlessAzureConfig, ResourceConfig } from "../../models/serverless"; export const HostingEnvironmentResource: ArmResourceTemplateGenerator = { diff --git a/src/armTemplates/resources/storageAccount.ts b/src/armTemplates/resources/storageAccount.ts index 6b08dc02..ff891b73 100644 --- a/src/armTemplates/resources/storageAccount.ts +++ b/src/armTemplates/resources/storageAccount.ts @@ -1,4 +1,4 @@ -import { ArmResourceTemplateGenerator } from "../../services/armService"; +import { ArmResourceTemplateGenerator } from "../../models/armTemplates"; import { ServerlessAzureConfig, ResourceConfig } from "../../models/serverless"; export const StorageAccountResource: ArmResourceTemplateGenerator = { diff --git a/src/armTemplates/resources/virtualNetwork.ts b/src/armTemplates/resources/virtualNetwork.ts index ab1dd3b9..2b117dbf 100644 --- a/src/armTemplates/resources/virtualNetwork.ts +++ b/src/armTemplates/resources/virtualNetwork.ts @@ -1,4 +1,4 @@ -import { ArmResourceTemplateGenerator } from "../../services/armService"; +import { ArmResourceTemplateGenerator } from "../../models/armTemplates"; import { ServerlessAzureConfig, ResourceConfig } from "../../models/serverless"; export const VirtualNetworkResource: ArmResourceTemplateGenerator = { diff --git a/src/plugins/deploy/azureDeployPlugin.test.ts b/src/plugins/deploy/azureDeployPlugin.test.ts index 3c3db2f2..f4ae441b 100644 --- a/src/plugins/deploy/azureDeployPlugin.test.ts +++ b/src/plugins/deploy/azureDeployPlugin.test.ts @@ -58,7 +58,7 @@ describe("Deploy plugin", () => { const sls = MockFactory.createTestServerless(); const resourceGroup = "rg1"; ResourceService.prototype.getDeployments = jest.fn(() => Promise.resolve([])) as any; - ResourceService.prototype.getResourceGroup = jest.fn(() => resourceGroup); + ResourceService.prototype.getResourceGroupName = jest.fn(() => resourceGroup); const options = MockFactory.createTestServerlessOptions(); const plugin = new AzureDeployPlugin(sls, options); await invokeHook(plugin, "deploy:list:list"); diff --git a/src/plugins/deploy/azureDeployPlugin.ts b/src/plugins/deploy/azureDeployPlugin.ts index 76c88ebc..485e771a 100644 --- a/src/plugins/deploy/azureDeployPlugin.ts +++ b/src/plugins/deploy/azureDeployPlugin.ts @@ -31,7 +31,7 @@ export class AzureDeployPlugin { const resourceService = new ResourceService(this.serverless, this.options); const deployments = await resourceService.getDeployments(); if (!deployments || deployments.length === 0) { - this.serverless.cli.log(`No deployments found for resource group '${resourceService.getResourceGroup()}'`); + this.serverless.cli.log(`No deployments found for resource group '${resourceService.getResourceGroupName()}'`); return; } let stringDeployments = "\n\nDeployments"; diff --git a/src/services/armService.test.ts b/src/services/armService.test.ts index 5cd5127b..b61c48cf 100644 --- a/src/services/armService.test.ts +++ b/src/services/armService.test.ts @@ -44,22 +44,7 @@ describe("Arm Service", () => { }, }; - const testTemplate: ArmResourceTemplate = { - "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "param1": { - "defaultValue": "", - "type": "String" - }, - "param2": { - "defaultValue": "", - "type": "String" - }, - }, - "variables": {}, - "resources": [] - }; + const testTemplate: ArmResourceTemplate = MockFactory.createTestArmTemplate(); mockFs({ "armTemplates": { diff --git a/src/services/functionAppService.test.ts b/src/services/functionAppService.test.ts index 17e421b1..5a2989c3 100644 --- a/src/services/functionAppService.test.ts +++ b/src/services/functionAppService.test.ts @@ -145,7 +145,7 @@ describe("Function App Service", () => { describe("Deployments", () => { const expectedDeployment: ArmDeployment = { parameters: {}, - template: {}, + template: MockFactory.createTestArmTemplate(), }; const expectedSite = MockFactory.createTestSite(); diff --git a/src/services/functionAppService.ts b/src/services/functionAppService.ts index 424101e5..9e9cf4b0 100644 --- a/src/services/functionAppService.ts +++ b/src/services/functionAppService.ts @@ -6,7 +6,8 @@ import { BaseService } from "./baseService"; import { FunctionAppHttpTriggerConfig } from "../models/functionApp"; import { Site, FunctionEnvelope } from "@azure/arm-appservice/esm/models"; import { Guard } from "../shared/guard"; -import { ArmService, ArmDeployment } from "./armService"; +import { ArmService } from "./armService"; +import { ArmDeployment } from "../models/armTemplates"; export class FunctionAppService extends BaseService { private webClient: WebSiteManagementClient; diff --git a/src/test/mockFactory.ts b/src/test/mockFactory.ts index b859d0db..d6fa7a80 100644 --- a/src/test/mockFactory.ts +++ b/src/test/mockFactory.ts @@ -14,6 +14,7 @@ import { AzureServiceProvider, ServicePrincipalEnvVariables } from "../models/az import { Logger } from "../models/generic"; import { ApiCorsPolicy, ApiManagementConfig } from "../models/apiManagement"; import { DeploymentsListByResourceGroupResponse } from "@azure/arm-resources/esm/models"; +import { ArmResourceTemplate } from "../models/armTemplates"; function getAttribute(object: any, prop: string, defaultValue: any): any { if (object && object[prop]) { @@ -145,7 +146,7 @@ export class MockFactory { const result = []; for (let i = 0; i < count; i++) { result.push({ - name: `deployment${i+1}`, + name: `deployment${i + 1}`, properties: { timestamp: new Date(), } @@ -206,7 +207,7 @@ export class MockFactory { functions: functionMetadata || MockFactory.createTestSlsFunctionConfig(), } return (asYaml) ? yaml.dump(data) : data; - } + } public static createTestApimConfig(): ApiManagementConfig { return { @@ -450,6 +451,25 @@ export class MockFactory { }; } + public static createTestArmTemplate(): ArmResourceTemplate { + return { + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "param1": { + "defaultValue": "", + "type": "String" + }, + "param2": { + "defaultValue": "", + "type": "String" + }, + }, + "variables": {}, + "resources": [] + }; + } + private static createTestCli(): Logger { return { log: jest.fn(), From 5dfb50eedba5471c6bc21cd0f65e5ad28d24c88b Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Fri, 21 Jun 2019 16:11:43 -0700 Subject: [PATCH 14/17] Updated baseService tests to use public methods --- src/services/baseService.test.ts | 28 ++++++++-------------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/src/services/baseService.test.ts b/src/services/baseService.test.ts index 29971246..f4c83da8 100644 --- a/src/services/baseService.test.ts +++ b/src/services/baseService.test.ts @@ -21,18 +21,6 @@ class MockService extends BaseService { return this.sendFile(requestOptions, filePath); } - public getSlsResourceGroupName() { - return this.getResourceGroupName(); - } - - public getSlsRegion() { - return this.getRegion(); - } - - public getSlsStage() { - return this.getStage(); - } - public getProperties() { return { baseUrl: this.baseUrl, @@ -99,8 +87,8 @@ describe("Base Service", () => { }; const mockService = new MockService(sls, cliOptions); - expect(mockService.getSlsRegion()).toEqual(cliOptions.region); - expect(mockService.getSlsStage()).toEqual(cliOptions.stage); + expect(mockService.getRegion()).toEqual(cliOptions.region); + expect(mockService.getStage()).toEqual(cliOptions.stage); }); it("Sets default region and stage values if not defined", () => { @@ -118,13 +106,13 @@ describe("Base Service", () => { }; const mockService = new MockService(sls, cliOptions); - expect(mockService.getSlsRegion()).toEqual(cliOptions.region); - expect(mockService.getSlsStage()).toEqual(cliOptions.stage); + expect(mockService.getRegion()).toEqual(cliOptions.region); + expect(mockService.getStage()).toEqual(cliOptions.stage); }); it("Generates resource group name from sls yaml config", () => { const mockService = new MockService(sls); - const resourceGroupName = mockService.getSlsResourceGroupName(); + const resourceGroupName = mockService.getResourceGroupName(); expect(resourceGroupName).toEqual(sls.service.provider["resourceGroup"]); }); @@ -132,9 +120,9 @@ describe("Base Service", () => { it("Generates resource group from convention when NOT defined in sls yaml", () => { sls.service.provider["resourceGroup"] = null; const mockService = new MockService(sls); - const resourceGroupName = mockService.getSlsResourceGroupName(); - const region = mockService.getSlsRegion(); - const stage = mockService.getSlsStage(); + const resourceGroupName = mockService.getResourceGroupName(); + const region = mockService.getRegion(); + const stage = mockService.getStage(); expect(resourceGroupName).toEqual(`sls-${region}-${stage}-${sls.service["service"]}-rg`); }); From c17d2a8e4109018079f96ff79a1758c168cb77d6 Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Fri, 21 Jun 2019 16:31:29 -0700 Subject: [PATCH 15/17] Added additional configuration properties for function app --- src/armTemplates/resources/functionApp.ts | 7 +++++-- src/armTemplates/resources/hostingEnvironment.ts | 1 - src/models/serverless.ts | 13 +++++++------ 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/armTemplates/resources/functionApp.ts b/src/armTemplates/resources/functionApp.ts index 5730e850..7a1487aa 100644 --- a/src/armTemplates/resources/functionApp.ts +++ b/src/armTemplates/resources/functionApp.ts @@ -1,5 +1,5 @@ import { ArmResourceTemplateGenerator } from "../../models/armTemplates"; -import { ServerlessAzureConfig, ResourceConfig } from "../../models/serverless"; +import { ServerlessAzureConfig, ResourceConfig, FunctionAppConfig } from "../../models/serverless"; export const FunctionAppResource: ArmResourceTemplateGenerator = { getTemplate: () => { @@ -95,13 +95,16 @@ export const FunctionAppResource: ArmResourceTemplateGenerator = { }, getParameters: (config: ServerlessAzureConfig) => { - const resourceConfig: ResourceConfig = { + const resourceConfig: FunctionAppConfig = { name: `${config.provider.prefix}-${config.provider.region}-${config.provider.stage}-${config.service}`, ...config.provider.functionApp, }; return { functionAppName: resourceConfig.name, + functionAppNodeVersion: resourceConfig.nodeVersion, + functionAppWorkerRuntime: resourceConfig.workerRuntime, + functionAppExtensionVersion: resourceConfig.extensionVersion, }; } }; diff --git a/src/armTemplates/resources/hostingEnvironment.ts b/src/armTemplates/resources/hostingEnvironment.ts index e143eb05..a49a5985 100644 --- a/src/armTemplates/resources/hostingEnvironment.ts +++ b/src/armTemplates/resources/hostingEnvironment.ts @@ -46,7 +46,6 @@ export const HostingEnvironmentResource: ArmResourceTemplateGenerator = { "multiSize": "Standard_D1_V2", "multiRoleCount": 2, "ipsslAddressCount": 2, - "dnsSuffix": "[concat(parameters('hostingEnvironmentName'), '.p.azurewebsites.net')]", "networkAccessControlList": [], "frontEndScaleFactor": 15, "suspended": false diff --git a/src/models/serverless.ts b/src/models/serverless.ts index 636c3d39..ea96e0ac 100644 --- a/src/models/serverless.ts +++ b/src/models/serverless.ts @@ -1,10 +1,5 @@ import { ApiManagementConfig } from "./apiManagement"; -export enum DeploymentType { - Consumption, - Premium, -} - export interface ArmTemplateConfig { file: string; parameters: @@ -22,6 +17,12 @@ export interface ResourceConfig { [key: string]: any; } +export interface FunctionAppConfig extends ResourceConfig { + nodeVersion?: string; + workerRuntime?: string; + extensionVersion?; +} + export interface ServerlessAzureConfig { service: string; provider: { @@ -35,7 +36,7 @@ export interface ServerlessAzureConfig { }; resourceGroup?: string; apim?: ApiManagementConfig; - functionApp?: ResourceConfig; + functionApp?: FunctionAppConfig; appInsightsConfig?: ResourceConfig; appServicePlan?: ResourceConfig; storageAccount?: ResourceConfig; From 4c1d61ca04490ded415be63aff3c1405488da2a1 Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Fri, 21 Jun 2019 18:09:23 -0700 Subject: [PATCH 16/17] Added compiste arm template and addressed PR feedback --- src/armTemplates/ase.ts | 65 +++++-------------- src/armTemplates/compositeArmTemplate.ts | 47 ++++++++++++++ src/armTemplates/consumption.ts | 58 ++++------------- src/armTemplates/premium.ts | 61 +++++------------ src/armTemplates/resources/apim.ts | 21 +++--- src/armTemplates/resources/appInsights.ts | 25 +++---- src/armTemplates/resources/appServicePlan.ts | 23 ++++--- src/armTemplates/resources/functionApp.ts | 23 ++++--- .../resources/hostingEnvironment.ts | 25 +++---- src/armTemplates/resources/storageAccount.ts | 21 +++--- src/armTemplates/resources/virtualNetwork.ts | 25 +++---- src/models/serverless.ts | 2 +- src/services/armService.ts | 8 +-- src/services/baseService.ts | 6 -- src/services/functionAppService.test.ts | 5 +- src/services/functionAppService.ts | 3 +- 16 files changed, 194 insertions(+), 224 deletions(-) create mode 100644 src/armTemplates/compositeArmTemplate.ts diff --git a/src/armTemplates/ase.ts b/src/armTemplates/ase.ts index bd77f2cc..e79f4069 100644 --- a/src/armTemplates/ase.ts +++ b/src/armTemplates/ase.ts @@ -4,39 +4,23 @@ import { StorageAccountResource } from "./resources/storageAccount"; import { AppServicePlanResource } from "./resources/appServicePlan"; import { HostingEnvironmentResource } from "./resources/hostingEnvironment"; import { VirtualNetworkResource } from "./resources/virtualNetwork"; -import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../models/armTemplates"; -import { ServerlessAzureConfig } from "../models/serverless.js"; - -const resources: ArmResourceTemplateGenerator[] = [ - FunctionAppResource, - AppInsightsResource, - StorageAccountResource, - AppServicePlanResource, - HostingEnvironmentResource, - VirtualNetworkResource, -]; - -const AseTemplate: ArmResourceTemplateGenerator = { - getTemplate: () => { - const template: ArmResourceTemplate = { - "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "parameters": {}, - "resources": [], - }; - - resources.forEach((resource) => { - const resourceTemplate = resource.getTemplate(); - template.parameters = { - ...template.parameters, - ...resourceTemplate.parameters, - }; +import { CompositeArmTemplate } from "./compositeArmTemplate"; +import { ArmResourceTemplate } from "../models/armTemplates"; + +class AppServiceEnvironmentTemplate extends CompositeArmTemplate { + public constructor() { + super([ + new FunctionAppResource(), + new AppInsightsResource(), + new StorageAccountResource(), + new AppServicePlanResource(), + new HostingEnvironmentResource(), + new VirtualNetworkResource(), + ]) + } - template.resources = [ - ...template.resources, - ...resourceTemplate.resources, - ]; - }); + public getTemplate(): ArmResourceTemplate { + const template = super.getTemplate(); template.parameters.appServicePlanSkuName.defaultValue = "I1"; template.parameters.appServicePlanSkuTier.defaultValue = "Isolated"; @@ -63,20 +47,7 @@ const AseTemplate: ArmResourceTemplateGenerator = { } return template; - }, - - getParameters: (config: ServerlessAzureConfig) => { - let parameters = {}; - resources.forEach((resource) => { - parameters = { - ...parameters, - ...resource.getParameters(config), - location: config.provider.region, - } - }); - - return parameters; } -}; +} -export default AseTemplate; \ No newline at end of file +export default new AppServiceEnvironmentTemplate(); \ No newline at end of file diff --git a/src/armTemplates/compositeArmTemplate.ts b/src/armTemplates/compositeArmTemplate.ts new file mode 100644 index 00000000..a56b5474 --- /dev/null +++ b/src/armTemplates/compositeArmTemplate.ts @@ -0,0 +1,47 @@ +import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../models/armTemplates"; +import { Guard } from "../shared/guard"; +import { ServerlessAzureConfig } from "../models/serverless"; + +export class CompositeArmTemplate implements ArmResourceTemplateGenerator { + public constructor(private childTemplates: ArmResourceTemplateGenerator[]) { + Guard.null(childTemplates); + } + + public getTemplate(): ArmResourceTemplate { + const template: ArmResourceTemplate = { + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": {}, + "resources": [], + }; + + this.childTemplates.forEach((resource) => { + const resourceTemplate = resource.getTemplate(); + template.parameters = { + ...template.parameters, + ...resourceTemplate.parameters, + }; + + template.resources = [ + ...template.resources, + ...resourceTemplate.resources, + ]; + }); + + return template; + } + + public getParameters(config: ServerlessAzureConfig) { + let parameters = {}; + + this.childTemplates.forEach((resource) => { + parameters = { + ...parameters, + ...resource.getParameters(config), + location: config.provider.region, + } + }); + + return parameters; + } +} \ No newline at end of file diff --git a/src/armTemplates/consumption.ts b/src/armTemplates/consumption.ts index 59c44130..05b83165 100644 --- a/src/armTemplates/consumption.ts +++ b/src/armTemplates/consumption.ts @@ -1,52 +1,16 @@ import { FunctionAppResource } from "./resources/functionApp"; import { AppInsightsResource } from "./resources/appInsights"; import { StorageAccountResource } from "./resources/storageAccount"; -import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../models/armTemplates"; -import { ServerlessAzureConfig } from "../models/serverless"; - -const resources: ArmResourceTemplateGenerator[] = [ - FunctionAppResource, - AppInsightsResource, - StorageAccountResource, -]; - -const ConsumptionTemplate: ArmResourceTemplateGenerator = { - getTemplate: () => { - const template: ArmResourceTemplate = { - "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "parameters": {}, - "resources": [], - }; - - resources.forEach((resource) => { - const resourceTemplate = resource.getTemplate(); - template.parameters = { - ...template.parameters, - ...resourceTemplate.parameters, - }; - - template.resources = [ - ...template.resources, - ...resourceTemplate.resources, - ]; - }); - - return template; - }, - - getParameters: (config: ServerlessAzureConfig) => { - let parameters = {}; - resources.forEach((resource) => { - parameters = { - ...parameters, - ...resource.getParameters(config), - location: config.provider.region, - } - }); - - return parameters; +import { CompositeArmTemplate } from "./compositeArmTemplate"; + +class ConsumptionPlanTemplate extends CompositeArmTemplate { + public constructor() { + super([ + new FunctionAppResource(), + new AppInsightsResource(), + new StorageAccountResource(), + ]) } -}; +} -export default ConsumptionTemplate; \ No newline at end of file +export default new ConsumptionPlanTemplate(); \ No newline at end of file diff --git a/src/armTemplates/premium.ts b/src/armTemplates/premium.ts index bc8c99af..c15daa21 100644 --- a/src/armTemplates/premium.ts +++ b/src/armTemplates/premium.ts @@ -2,37 +2,21 @@ import { FunctionAppResource } from "./resources/functionApp"; import { AppInsightsResource } from "./resources/appInsights"; import { StorageAccountResource } from "./resources/storageAccount"; import { AppServicePlanResource } from "./resources/appServicePlan"; -import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../models/armTemplates"; -import { ServerlessAzureConfig } from "../models/serverless"; - -const resources: ArmResourceTemplateGenerator[] = [ - FunctionAppResource, - AppInsightsResource, - StorageAccountResource, - AppServicePlanResource, -]; - -const PremiumTemplate: ArmResourceTemplateGenerator = { - getTemplate: () => { - const template: ArmResourceTemplate = { - "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "parameters": {}, - "resources": [], - }; - - resources.forEach((resource) => { - const resourceTemplate = resource.getTemplate(); - template.parameters = { - ...template.parameters, - ...resourceTemplate.parameters, - }; +import { ArmResourceTemplate } from "../models/armTemplates"; +import { CompositeArmTemplate } from "./compositeArmTemplate"; + +class PremiumPlanTemplate extends CompositeArmTemplate { + public constructor() { + super([ + new FunctionAppResource(), + new AppInsightsResource(), + new StorageAccountResource(), + new AppServicePlanResource(), + ]) + } - template.resources = [ - ...template.resources, - ...resourceTemplate.resources, - ]; - }); + public getTemplate(): ArmResourceTemplate { + const template = super.getTemplate(); template.parameters.appServicePlanSkuName.defaultValue = "EP1"; template.parameters.appServicePlanSkuTier.defaultValue = "ElasticPremium"; @@ -45,20 +29,7 @@ const PremiumTemplate: ArmResourceTemplateGenerator = { } return template; - }, - - getParameters: (config: ServerlessAzureConfig) => { - let parameters = {}; - resources.forEach((resource) => { - parameters = { - ...parameters, - ...resource.getParameters(config), - location: config.provider.region, - } - }); - - return parameters; } -}; +} -export default PremiumTemplate; \ No newline at end of file +export default new PremiumPlanTemplate(); \ No newline at end of file diff --git a/src/armTemplates/resources/apim.ts b/src/armTemplates/resources/apim.ts index cb255e34..64985743 100644 --- a/src/armTemplates/resources/apim.ts +++ b/src/armTemplates/resources/apim.ts @@ -1,9 +1,15 @@ import { ServerlessAzureConfig } from "../../models/serverless"; -import { ArmResourceTemplateGenerator } from "../../models/armTemplates"; +import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../../models/armTemplates"; import { ApiManagementConfig } from "../../models/apiManagement"; -export const ApimResource: ArmResourceTemplateGenerator = { - getTemplate: () => { +export class ApimResource implements ArmResourceTemplateGenerator { + public static getResourceName(config: ServerlessAzureConfig) { + return config.provider.apim && config.provider.apim.name + ? config.provider.apim.name + : `${config.provider.prefix}-${config.provider.region}-${config.provider.stage}-apim`; + } + + public getTemplate(): ArmResourceTemplate { return { "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", @@ -46,19 +52,18 @@ export const ApimResource: ArmResourceTemplateGenerator = { } ] }; - }, + } - getParameters: (config: ServerlessAzureConfig) => { + public getParameters(config: ServerlessAzureConfig) { const apimConfig: ApiManagementConfig = { - name: `${config.provider.prefix}-${config.provider.region}-${config.provider.stage}-apim`, sku: {}, ...config.provider.apim, }; return { - apiManagementName: apimConfig.name, + apiManagementName: ApimResource.getResourceName(config), apimSkuName: apimConfig.sku.name, apimSkuCapacity: apimConfig.sku.capacity, }; } -}; \ No newline at end of file +} \ No newline at end of file diff --git a/src/armTemplates/resources/appInsights.ts b/src/armTemplates/resources/appInsights.ts index 85867a7e..9e7df067 100644 --- a/src/armTemplates/resources/appInsights.ts +++ b/src/armTemplates/resources/appInsights.ts @@ -1,8 +1,14 @@ -import { ArmResourceTemplateGenerator } from "../../models/armTemplates"; +import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../../models/armTemplates"; import { ServerlessAzureConfig, ResourceConfig } from "../../models/serverless"; -export const AppInsightsResource: ArmResourceTemplateGenerator = { - getTemplate: () => { +export class AppInsightsResource implements ArmResourceTemplateGenerator { + public static getResourceName(config: ServerlessAzureConfig) { + return config.provider.appInsights && config.provider.appInsights.name + ? config.provider.appInsights.name + : `${config.provider.prefix}-${config.provider.region}-${config.provider.stage}-appinsights`; + } + + public getTemplate(): ArmResourceTemplate { return { "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", @@ -31,16 +37,11 @@ export const AppInsightsResource: ArmResourceTemplateGenerator = { } ] } - }, - - getParameters: (config: ServerlessAzureConfig) => { - const resourceConfig: ResourceConfig = { - name: `${config.provider.prefix}-${config.provider.region}-${config.provider.stage}-appinsights`, - ...config.provider.appInsightsConfig, - }; + } + public getParameters(config: ServerlessAzureConfig): any { return { - appInsightsName: resourceConfig.name, + appInsightsName: AppInsightsResource.getResourceName(config), }; } -}; \ No newline at end of file +} \ No newline at end of file diff --git a/src/armTemplates/resources/appServicePlan.ts b/src/armTemplates/resources/appServicePlan.ts index b658a538..8a6a378f 100644 --- a/src/armTemplates/resources/appServicePlan.ts +++ b/src/armTemplates/resources/appServicePlan.ts @@ -1,8 +1,14 @@ -import { ArmResourceTemplateGenerator } from "../../models/armTemplates"; +import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../../models/armTemplates"; import { ServerlessAzureConfig, ResourceConfig } from "../../models/serverless"; -export const AppServicePlanResource: ArmResourceTemplateGenerator = { - getTemplate: () => { +export class AppServicePlanResource implements ArmResourceTemplateGenerator { + public static getResourceName(config: ServerlessAzureConfig) { + return config.provider.appServicePlan && config.provider.appServicePlan.name + ? config.provider.appServicePlan.name + : `${config.provider.prefix}-${config.provider.region}-${config.provider.stage}-asp`; + } + + public getTemplate(): ArmResourceTemplate { return { "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", @@ -45,19 +51,18 @@ export const AppServicePlanResource: ArmResourceTemplateGenerator = { } ] }; - }, - - getParameters: (config: ServerlessAzureConfig) => { + } + + public getParameters(config: ServerlessAzureConfig): any { const resourceConfig: ResourceConfig = { - name: `${config.provider.prefix}-${config.provider.region}-${config.provider.stage}-asp`, sku: {}, ...config.provider.storageAccount, }; return { - appServicePlanName: resourceConfig.name, + appServicePlanName: AppServicePlanResource.getResourceName(config), appServicePlanSkuName: resourceConfig.sku.name, appServicePlanSkuTier: resourceConfig.sku.tier, } } -}; \ No newline at end of file +} \ No newline at end of file diff --git a/src/armTemplates/resources/functionApp.ts b/src/armTemplates/resources/functionApp.ts index 7a1487aa..50caba93 100644 --- a/src/armTemplates/resources/functionApp.ts +++ b/src/armTemplates/resources/functionApp.ts @@ -1,8 +1,14 @@ -import { ArmResourceTemplateGenerator } from "../../models/armTemplates"; -import { ServerlessAzureConfig, ResourceConfig, FunctionAppConfig } from "../../models/serverless"; +import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../../models/armTemplates"; +import { ServerlessAzureConfig, FunctionAppConfig } from "../../models/serverless"; -export const FunctionAppResource: ArmResourceTemplateGenerator = { - getTemplate: () => { +export class FunctionAppResource implements ArmResourceTemplateGenerator { + public static getResourceName(config: ServerlessAzureConfig) { + return config.provider.functionApp && config.provider.functionApp.name + ? config.provider.functionApp.name + : `${config.provider.prefix}-${config.provider.region}-${config.provider.stage}-${config.service}`; + } + + public getTemplate(): ArmResourceTemplate { return { "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", @@ -92,19 +98,18 @@ export const FunctionAppResource: ArmResourceTemplateGenerator = { } ] }; - }, + } - getParameters: (config: ServerlessAzureConfig) => { + public getParameters(config: ServerlessAzureConfig): any { const resourceConfig: FunctionAppConfig = { - name: `${config.provider.prefix}-${config.provider.region}-${config.provider.stage}-${config.service}`, ...config.provider.functionApp, }; return { - functionAppName: resourceConfig.name, + functionAppName: FunctionAppResource.getResourceName(config), functionAppNodeVersion: resourceConfig.nodeVersion, functionAppWorkerRuntime: resourceConfig.workerRuntime, functionAppExtensionVersion: resourceConfig.extensionVersion, }; } -}; +} diff --git a/src/armTemplates/resources/hostingEnvironment.ts b/src/armTemplates/resources/hostingEnvironment.ts index a49a5985..f7854857 100644 --- a/src/armTemplates/resources/hostingEnvironment.ts +++ b/src/armTemplates/resources/hostingEnvironment.ts @@ -1,8 +1,14 @@ -import { ArmResourceTemplateGenerator } from "../../models/armTemplates"; +import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../../models/armTemplates"; import { ServerlessAzureConfig, ResourceConfig } from "../../models/serverless"; -export const HostingEnvironmentResource: ArmResourceTemplateGenerator = { - getTemplate: () => { +export class HostingEnvironmentResource implements ArmResourceTemplateGenerator { + public static getResourceName(config: ServerlessAzureConfig) { + return config.provider.hostingEnvironment && config.provider.hostingEnvironment.name + ? config.provider.hostingEnvironment.name + : `${config.provider.prefix}-${config.provider.region}-${config.provider.stage}-ase`; + } + + public getTemplate(): ArmResourceTemplate { return { "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", @@ -53,17 +59,12 @@ export const HostingEnvironmentResource: ArmResourceTemplateGenerator = { } ] }; - }, + } - getParameters: (config: ServerlessAzureConfig) => { - const resourceConfig: ResourceConfig = { - name: `${config.provider.prefix}-${config.provider.region}-${config.provider.stage}-ase`, - ...config.provider.hostingEnvironment, - }; - + public getParameters(config: ServerlessAzureConfig): any { return { - hostingEnvironmentName: resourceConfig.name, + hostingEnvironmentName: HostingEnvironmentResource.getResourceName(config) } } -}; +} diff --git a/src/armTemplates/resources/storageAccount.ts b/src/armTemplates/resources/storageAccount.ts index ff891b73..b786e983 100644 --- a/src/armTemplates/resources/storageAccount.ts +++ b/src/armTemplates/resources/storageAccount.ts @@ -1,8 +1,14 @@ -import { ArmResourceTemplateGenerator } from "../../models/armTemplates"; +import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../../models/armTemplates"; import { ServerlessAzureConfig, ResourceConfig } from "../../models/serverless"; -export const StorageAccountResource: ArmResourceTemplateGenerator = { - getTemplate: () => { +export class StorageAccountResource implements ArmResourceTemplateGenerator { + public static getResourceName(config: ServerlessAzureConfig) { + return config.provider.storageAccount && config.provider.storageAccount.name + ? config.provider.storageAccount.name + : `${config.provider.prefix}${config.provider.region.substr(0, 3)}${config.provider.stage.substr(0, 3)}sa`.replace("-", "").toLocaleLowerCase(); + } + + public getTemplate(): ArmResourceTemplate { return { "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", @@ -42,19 +48,18 @@ export const StorageAccountResource: ArmResourceTemplateGenerator = { } ] } - }, + } - getParameters: (config: ServerlessAzureConfig) => { + public getParameters(config: ServerlessAzureConfig): any { const resourceConfig: ResourceConfig = { - name: `${config.provider.prefix}${config.provider.region.substr(0,3)}${config.provider.stage.substr(0,3)}sa`.replace("-", "").toLocaleLowerCase(), sku: {}, ...config.provider.storageAccount, }; return { - storageAccountName: resourceConfig.name, + storageAccountName: StorageAccountResource.getResourceName(config), storageAccountSkuName: resourceConfig.sku.name, storageAccoutSkuTier: resourceConfig.sku.tier, }; } -}; \ No newline at end of file +} \ No newline at end of file diff --git a/src/armTemplates/resources/virtualNetwork.ts b/src/armTemplates/resources/virtualNetwork.ts index 2b117dbf..b93448b0 100644 --- a/src/armTemplates/resources/virtualNetwork.ts +++ b/src/armTemplates/resources/virtualNetwork.ts @@ -1,8 +1,14 @@ -import { ArmResourceTemplateGenerator } from "../../models/armTemplates"; +import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../../models/armTemplates"; import { ServerlessAzureConfig, ResourceConfig } from "../../models/serverless"; -export const VirtualNetworkResource: ArmResourceTemplateGenerator = { - getTemplate: () => { +export class VirtualNetworkResource implements ArmResourceTemplateGenerator { + public static getResourceName(config: ServerlessAzureConfig) { + return config.provider.virtualNetwork && config.provider.virtualNetwork.name + ? config.provider.virtualNetwork.name + : `${config.provider.prefix}-${config.provider.region}-${config.provider.stage}-vnet`; + } + + public getTemplate(): ArmResourceTemplate { return { "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", @@ -71,17 +77,12 @@ export const VirtualNetworkResource: ArmResourceTemplateGenerator = { } ] }; - }, + } - getParameters: (config: ServerlessAzureConfig) => { - const resourceConfig: ResourceConfig = { - name: `${config.provider.prefix}-${config.provider.region}-${config.provider.stage}-vnet`, - ...config.provider.hostingEnvironment, - }; - + public getParameters(config: ServerlessAzureConfig): any { return { - virtualNetworkName: resourceConfig.name, + virtualNetworkName: VirtualNetworkResource.getResourceName(config), } } -}; +} diff --git a/src/models/serverless.ts b/src/models/serverless.ts index ea96e0ac..b3f6d315 100644 --- a/src/models/serverless.ts +++ b/src/models/serverless.ts @@ -37,7 +37,7 @@ export interface ServerlessAzureConfig { resourceGroup?: string; apim?: ApiManagementConfig; functionApp?: FunctionAppConfig; - appInsightsConfig?: ResourceConfig; + appInsights?: ResourceConfig; appServicePlan?: ResourceConfig; storageAccount?: ResourceConfig; hostingEnvironment?: ResourceConfig; diff --git a/src/services/armService.ts b/src/services/armService.ts index dbdc4a7b..ce13d0c3 100644 --- a/src/services/armService.ts +++ b/src/services/armService.ts @@ -27,6 +27,7 @@ export class ArmService extends BaseService { this.log(`-> Creating ARM template from type: ${type}`); const { ApimResource } = await import("../armTemplates/resources/apim"); + const apimResource = new ApimResource(); let template: ArmResourceTemplateGenerator; try { @@ -41,8 +42,8 @@ export class ArmService extends BaseService { let parameters = template.getParameters(azureConfig); if (this.config.provider.apim) { - const apimTemplate = ApimResource.getTemplate(); - const apimParameters = ApimResource.getParameters(azureConfig); + const apimTemplate = apimResource.getTemplate(); + const apimParameters = apimResource.getParameters(azureConfig); mergedTemplate.parameters = { ...mergedTemplate.parameters, @@ -100,9 +101,6 @@ export class ArmService extends BaseService { } }); - // this.serverless.cli.log(JSON.stringify(deploymentParameters, null, 4)); - // fs.writeFileSync(".serverless/arm-template.json", JSON.stringify(deployment.template, null, 4)); - // Construct deployment object const armDeployment: Deployment = { properties: { diff --git a/src/services/baseService.ts b/src/services/baseService.ts index a7556eb7..44b42310 100644 --- a/src/services/baseService.ts +++ b/src/services/baseService.ts @@ -44,12 +44,6 @@ export abstract class BaseService { return this.options.stage || this.serverless.service.provider.stage; } - public getFunctionAppName(): string { - return this.config.provider.functionApp - ? this.config.provider.functionApp.name - : `${this.config.provider.prefix}-${this.config.provider.region}-${this.config.provider.stage}-${this.config.service}`; - } - public getResourceGroupName(): string { return this.options["resourceGroup"] || this.serverless.service.provider["resourceGroup"] diff --git a/src/services/functionAppService.test.ts b/src/services/functionAppService.test.ts index 5a2989c3..51aa5278 100644 --- a/src/services/functionAppService.test.ts +++ b/src/services/functionAppService.test.ts @@ -5,6 +5,7 @@ import Serverless from "serverless"; import { MockFactory } from "../test/mockFactory"; import { FunctionAppService } from "./functionAppService"; import { ArmService } from "./armService"; +import { FunctionAppResource } from "../armTemplates/resources/functionApp"; jest.mock("@azure/arm-appservice") import { WebSiteManagementClient } from "@azure/arm-appservice"; @@ -88,7 +89,7 @@ describe("Function App Service", () => { const service = createService(); const result = await service.get(); expect(WebSiteManagementClient.prototype.webApps.get) - .toBeCalledWith(provider.resourceGroup, service.getFunctionAppName()); + .toBeCalledWith(provider.resourceGroup, FunctionAppResource.getResourceName(slsService as any)); expect(result).toEqual(app) }); @@ -100,7 +101,7 @@ describe("Function App Service", () => { } as any; const result = await service.get(); expect(WebSiteManagementClient.prototype.webApps.get) - .toBeCalledWith(provider.resourceGroup, service.getFunctionAppName()); + .toBeCalledWith(provider.resourceGroup, FunctionAppResource.getResourceName(slsService as any)); expect(result).toBeNull(); }); diff --git a/src/services/functionAppService.ts b/src/services/functionAppService.ts index 9e9cf4b0..b4d26f63 100644 --- a/src/services/functionAppService.ts +++ b/src/services/functionAppService.ts @@ -8,6 +8,7 @@ import { Site, FunctionEnvelope } from "@azure/arm-appservice/esm/models"; import { Guard } from "../shared/guard"; import { ArmService } from "./armService"; import { ArmDeployment } from "../models/armTemplates"; +import { FunctionAppResource } from "../armTemplates/resources/functionApp"; export class FunctionAppService extends BaseService { private webClient: WebSiteManagementClient; @@ -18,7 +19,7 @@ export class FunctionAppService extends BaseService { } public async get(): Promise { - const response: any = await this.webClient.webApps.get(this.resourceGroup, this.getFunctionAppName()); + const response: any = await this.webClient.webApps.get(this.resourceGroup, FunctionAppResource.getResourceName(this.config)); if (response.error && (response.error.code === "ResourceNotFound" || response.error.code === "ResourceGroupNotFound")) { return null; } From 2e7e2358d7cc251a2a0bb1ffa85a2d3447fd9a00 Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Fri, 21 Jun 2019 18:13:03 -0700 Subject: [PATCH 17/17] Added publisher settings to APIM resource template --- src/armTemplates/resources/apim.ts | 14 ++++++++++++-- src/models/apiManagement.ts | 2 ++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/armTemplates/resources/apim.ts b/src/armTemplates/resources/apim.ts index 64985743..46587dbf 100644 --- a/src/armTemplates/resources/apim.ts +++ b/src/armTemplates/resources/apim.ts @@ -29,6 +29,14 @@ export class ApimResource implements ArmResourceTemplateGenerator { "apimCapacity": { "defaultValue": 0, "type": "int" + }, + "apimPublisherEmail": { + "defaultValue": "contact@contoso.com", + "type": "String" + }, + "apimPublisherName": { + "defaultValue": "Contoso", + "type": "String" } }, "variables": {}, @@ -43,8 +51,8 @@ export class ApimResource implements ArmResourceTemplateGenerator { "capacity": "[parameters('apimCapacity')]" }, "properties": { - "publisherEmail": "wabrez@microsoft.com", - "publisherName": "Microsoft", + "publisherEmail": "[parameters('apimPublisherEmail')]", + "publisherName": "[parameters('apimPublisherName')]", "notificationSenderEmail": "apimgmt-noreply@mail.windowsazure.com", "hostnameConfigurations": [], "virtualNetworkType": "None" @@ -64,6 +72,8 @@ export class ApimResource implements ArmResourceTemplateGenerator { apiManagementName: ApimResource.getResourceName(config), apimSkuName: apimConfig.sku.name, apimSkuCapacity: apimConfig.sku.capacity, + apimPublisherEmail: apimConfig.publisherEmail, + apimPublisherName: apimConfig.publisherName, }; } } \ No newline at end of file diff --git a/src/models/apiManagement.ts b/src/models/apiManagement.ts index 0a05f1df..3fc656d1 100644 --- a/src/models/apiManagement.ts +++ b/src/models/apiManagement.ts @@ -16,6 +16,8 @@ export interface ApiManagementConfig { name?: string; capacity?: number; }; + publisherEmail?: string; + publisherName?: string; } /**