diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..5418960e --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,35 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Jest All", + "program": "${workspaceFolder}/node_modules/.bin/jest", + "args": ["--runInBand"], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "disableOptimisticBPs": true, + "windows": { + "program": "${workspaceFolder}/node_modules/jest/bin/jest", + } + }, + { + "type": "node", + "request": "launch", + "name": "Jest Current File", + "program": "${workspaceFolder}/node_modules/.bin/jest", + "args": [ + "${fileBasenameNoExtension}", + "--config", + "jest.config.js" + ], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "disableOptimisticBPs": true, + "windows": { + "program": "${workspaceFolder}/node_modules/jest/bin/jest", + } + } + ] + } \ No newline at end of file diff --git a/package.json b/package.json index 562a64de..25625335 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "serverless-azure-functions", - "version": "1.0.0", + "version": "1.0.1", "description": "Provider plugin for the Serverless Framework v1.x which adds support for Azure Functions.", "license": "MIT", "main": "./lib/index.js", diff --git a/src/config.ts b/src/config.ts index b1988e8b..88a1e45a 100644 --- a/src/config.ts +++ b/src/config.ts @@ -34,7 +34,13 @@ export const configConstants = { scmVfsPath: "/api/vfs/site/wwwroot/", scmZipDeployApiPath: "/api/zipdeploy", resourceGroupHashLength: 6, - defaultLocalPort: 7071, + defaults: { + awsRegion: "us-east-1", + region: "westus", + stage: "dev", + prefix: "sls", + localPort: 7071, + }, }; export default configConstants; diff --git a/src/models/serverless.ts b/src/models/serverless.ts index 91b77d42..55a59cf0 100644 --- a/src/models/serverless.ts +++ b/src/models/serverless.ts @@ -35,6 +35,8 @@ export interface ServerlessAzureProvider { stage: string; name: string; subscriptionId?: string; + tenantId?: string; + appId?: string; environment?: { [key: string]: any; }; diff --git a/src/services/apimService.test.ts b/src/services/apimService.test.ts index 23a4d8f4..0dbb742d 100644 --- a/src/services/apimService.test.ts +++ b/src/services/apimService.test.ts @@ -70,8 +70,8 @@ describe("APIM Service", () => { const apimConfigName = MockFactory.createTestApimConfig(true); (serverless.service.provider as any).apim = apimConfigName; - const service = new ApimService(serverless); - const expectedRegionName = AzureNamingService.createShortAzureRegionName(service.getRegion()); + new ApimService(serverless); + const expectedRegionName = AzureNamingService.createShortAzureRegionName(serverless.service.provider.region); expect(apimConfigName.name.includes(expectedRegionName)).toBeTruthy(); }); diff --git a/src/services/baseService.test.ts b/src/services/baseService.test.ts index f37557a6..a7430e0e 100644 --- a/src/services/baseService.test.ts +++ b/src/services/baseService.test.ts @@ -1,10 +1,9 @@ import fs from "fs"; import mockFs from "mock-fs"; import Serverless from "serverless"; -import { ServerlessAzureOptions, ServerlessAzureConfig } from "../models/serverless"; +import { ServerlessAzureOptions } from "../models/serverless"; import { MockFactory } from "../test/mockFactory"; import { BaseService } from "./baseService"; -import { AzureNamingService } from "./namingService"; jest.mock("axios", () => jest.fn()); import axios from "axios"; @@ -40,11 +39,9 @@ class MockService extends BaseService { describe("Base Service", () => { let service: MockService; let serverless: Serverless; - const serviceName = "my-custom-service" - const loginResultSubscriptionId = "ABC123"; - const envVarSubscriptionId = "env var sub id"; - + const serviceName = "my-custom-service"; + const slsConfig = { service: serviceName, provider: { @@ -59,7 +56,6 @@ describe("Base Service", () => { function createMockService(sls: Serverless, options?: Serverless.Options) { sls.variables["azureCredentials"] = MockFactory.createTestAzureCredentials(); - sls.variables["subscriptionId"] = loginResultSubscriptionId; Object.assign(sls.service, slsConfig); return new MockService(sls, options); @@ -87,65 +83,7 @@ describe("Base Service", () => { expect(mockService).not.toBeNull(); expect(serverless.service.provider.region).toEqual("westus"); expect(serverless.service.provider.stage).toEqual("dev"); - }); - - it("returns region and stage based on CLI options", () => { - const cliOptions = { - stage: "prod", - region: "eastus2", - }; - const mockService = new MockService(serverless, cliOptions); - - expect(mockService.getRegion()).toEqual(cliOptions.region); - expect(mockService.getStage()).toEqual(cliOptions.stage); - }); - - it("use the resource group name specified in CLI", () => { - const resourceGroupName = "cliResourceGroupName" - const cliOptions = { - stage: "prod", - region: "eastus2", - resourceGroup: resourceGroupName - }; - - const mockService = new MockService(serverless, cliOptions); - const actualResourceGroupName = mockService.getResourceGroupName(); - - expect(actualResourceGroupName).toEqual(resourceGroupName); - }); - - it("use the resource group name from sls yaml config", () => { - const mockService = new MockService(serverless); - const actualResourceGroupName = mockService.getResourceGroupName(); - - expect(actualResourceGroupName).toEqual(serverless.service.provider["resourceGroup"]); - }); - - it("Generates resource group from convention when NOT defined in sls yaml", () => { - serverless.service.provider["resourceGroup"] = null; - const mockService = new MockService(serverless); - const actualResourceGroupName = mockService.getResourceGroupName(); - const expectedRegion = AzureNamingService.createShortAzureRegionName(mockService.getRegion()); - const expectedStage = AzureNamingService.createShortStageName(mockService.getStage()); - const expectedResourceGroupName = `sls-${expectedRegion}-${expectedStage}-${serverless.service["service"]}-rg`; - - expect(actualResourceGroupName).toEqual(expectedResourceGroupName); - }); - - it("set default prefix when one is not defined in yaml config", () => { - const mockService = new MockService(serverless); - const actualPrefix = mockService.getPrefix(); - expect(actualPrefix).toEqual("sls"); - }); - - it("use the prefix defined in sls yaml config", () => { - const expectedPrefix = "testPrefix" - serverless.service.provider["prefix"] = expectedPrefix; - const mockService = new MockService(serverless); - const actualPrefix = mockService.getPrefix(); - - expect(actualPrefix).toEqual(expectedPrefix); - }); + }); it("Fails if credentials have not been set in serverless config", () => { serverless.variables["azureCredentials"] = null; @@ -202,91 +140,4 @@ describe("Base Service", () => { readStreamSpy.mockRestore(); }); - - it("sets stage name from CLI", async () => { - const stage = "test"; - delete (serverless.service as any as ServerlessAzureConfig).provider.resourceGroup; - expect(serverless.service.provider.stage).not.toEqual(stage); - service = new MockService(serverless, { stage } as any); - expect(service.getStage()).toEqual(stage); - expect(service.getResourceGroupName()).toEqual(`sls-wus-${stage}-${serviceName}-rg`); - }); - - it("sets region name from CLI", async () => { - const region = "East US"; - delete (serverless.service as any as ServerlessAzureConfig).provider.resourceGroup; - expect(serverless.service.provider.region).not.toEqual(region); - service = new MockService(serverless, { region } as any); - expect(service.getRegion()).toEqual(region); - expect(service.getResourceGroupName()).toEqual(`sls-eus-dev-${serviceName}-rg`); - }); - - it("sets prefix from CLI", async () => { - const prefix = "prefix"; - delete (serverless.service as any as ServerlessAzureConfig).provider.resourceGroup; - expect(serverless.service.provider["prefix"]).not.toEqual(prefix); - service = new MockService(serverless, { prefix } as any); - expect(service.getPrefix()).toEqual(prefix); - expect(service.getResourceGroupName()).toEqual(`${prefix}-wus-dev-${serviceName}-rg`); - }); - - it("sets resource group from CLI", async () => { - const resourceGroup = "resourceGroup"; - delete (serverless.service as any as ServerlessAzureConfig).provider.resourceGroup; - expect(serverless.service.provider["resourceGroup"]).not.toEqual(resourceGroup); - service = new MockService(serverless, { resourceGroup } as any); - expect(service.getResourceGroupName()).toEqual(resourceGroup); - }); - - const cliSubscriptionId = "cli sub id"; - const configSubscriptionId = "config sub id"; - - it("sets subscription ID from CLI", async () => { - process.env.AZURE_SUBSCRIPTION_ID = envVarSubscriptionId; - serverless.service.provider["subscriptionId"] = configSubscriptionId; - serverless.variables["subscriptionId"] = loginResultSubscriptionId - service = new MockService(serverless, { subscriptionId: cliSubscriptionId } as any); - expect(service.getSubscriptionId()).toEqual(cliSubscriptionId); - expect(serverless.service.provider["subscriptionId"]).toEqual(cliSubscriptionId); - }); - - it("sets subscription ID from environment variable", async () => { - process.env.AZURE_SUBSCRIPTION_ID = envVarSubscriptionId; - serverless.service.provider["subscriptionId"] = configSubscriptionId; - serverless.variables["subscriptionId"] = loginResultSubscriptionId - service = new MockService(serverless, { } as any); - expect(service.getSubscriptionId()).toEqual(envVarSubscriptionId); - expect(serverless.service.provider["subscriptionId"]).toEqual(envVarSubscriptionId); - }); - - it("sets subscription ID from config", async () => { - delete process.env.AZURE_SUBSCRIPTION_ID; - serverless.service.provider["subscriptionId"] = configSubscriptionId; - serverless.variables["subscriptionId"] = loginResultSubscriptionId - service = new MockService(serverless, { } as any); - expect(service.getSubscriptionId()).toEqual(configSubscriptionId); - expect(serverless.service.provider["subscriptionId"]).toEqual(configSubscriptionId); - }); - - it("sets subscription ID from login result", async () => { - delete process.env.AZURE_SUBSCRIPTION_ID; - serverless.variables["subscriptionId"] = loginResultSubscriptionId - service = new MockService(serverless, { } as any); - expect(service.getSubscriptionId()).toEqual(loginResultSubscriptionId); - expect(serverless.service.provider["subscriptionId"]).toEqual(loginResultSubscriptionId); - }); - - it("sets region to be value from location property if region not set", () => { - const slsService = MockFactory.createTestService(); - delete slsService.provider.region; - const location = "East US"; - slsService.provider["location"] = location; - - const sls = MockFactory.createTestServerless({ - service: slsService - }); - - service = new MockService(sls, {} as any); - expect(service.getRegion()).toEqual(location); - }); }); diff --git a/src/services/baseService.ts b/src/services/baseService.ts index 1e433b08..730a22d0 100644 --- a/src/services/baseService.ts +++ b/src/services/baseService.ts @@ -4,17 +4,15 @@ import fs from "fs"; import request from "request"; import Serverless from "serverless"; import { StorageAccountResource } from "../armTemplates/resources/storageAccount"; -import { configConstants } from "../config"; -import { - DeploymentConfig, +import { ServerlessAzureConfig, - ServerlessAzureFunctionConfig, ServerlessAzureOptions, - ServerlessLogOptions + ServerlessLogOptions } from "../models/serverless"; +import { constants } from "../shared/constants"; import { Guard } from "../shared/guard"; import { Utils } from "../shared/utils"; -import { AzureNamingService, AzureNamingServiceOptions } from "./namingService"; +import { ConfigService } from "./configService"; export abstract class BaseService { protected baseUrl: string; @@ -24,9 +22,9 @@ export abstract class BaseService { protected resourceGroup: string; protected deploymentName: string; protected artifactName: string; - protected deploymentConfig: DeploymentConfig; protected storageAccountName: string; protected config: ServerlessAzureConfig; + protected configService: ConfigService; protected constructor( protected serverless: Serverless, @@ -34,18 +32,16 @@ export abstract class BaseService { authenticate: boolean = true ) { Guard.null(serverless); - this.setDefaultValues(); - this.config = serverless.service as any; - this.setupConfig(); + this.configService = new ConfigService(serverless, options); + this.config = this.configService.getConfig(); this.baseUrl = "https://management.azure.com"; - this.serviceName = this.getServiceName(); - this.credentials = serverless.variables["azureCredentials"]; + this.serviceName = this.configService.getServiceName(); + this.credentials = serverless.variables[constants.variableKeys.azureCredentials]; this.subscriptionId = this.config.provider.subscriptionId; - this.resourceGroup = this.getResourceGroupName(); - this.deploymentConfig = this.getDeploymentConfig(); - this.deploymentName = this.getDeploymentName(); - this.artifactName = this.getArtifactName(this.deploymentName); + this.resourceGroup = this.configService.getResourceGroupName(); + this.deploymentName = this.configService.getDeploymentName(); + this.artifactName = this.configService.getArtifactName(this.deploymentName); this.storageAccountName = StorageAccountResource.getResourceName(this.config); if (!this.credentials && authenticate) { @@ -53,87 +49,6 @@ export abstract class BaseService { } } - /** - * Name of Azure Region for deployment - */ - public getRegion(): string { - return this.config.provider.region; - } - - /** - * Name of current deployment stage - */ - public getStage(): string { - return this.config.provider.stage; - } - - /** - * Prefix for service - */ - public getPrefix(): string { - return this.config.provider.prefix; - } - - /** - * Name of current resource group - */ - public getResourceGroupName(): string { - return this.config.provider.resourceGroup; - } - - /** - * Azure Subscription ID - */ - public getSubscriptionId(): string { - return this.config.provider.subscriptionId; - } - - /** - * Deployment config from `serverless.yml` or default. - * Defaults can be found in the `config.ts` file - */ - public getDeploymentConfig(): DeploymentConfig { - return { - ...configConstants.deploymentConfig, - ...this.config.provider.deployment, - } - } - - /** - * Name of current ARM deployment. - * - * Naming convention: - * - * {safeName (see naming service)}--{serviceName}(if rollback enabled: -t{timestamp}) - * - * The string is guaranteed to be less than 64 characters, since that is the limit - * imposed by Azure deployment names. If a trim is needed, the service name will be trimmed - */ - public getDeploymentName(): string { - return AzureNamingService.getDeploymentName( - this.config, - (this.deploymentConfig.rollback) ? `t${this.getTimestamp()}` : null - ) - } - - /** - * Name of Function App Service - */ - public getServiceName(): string { - return this.serverless.service["service"]; - } - - /** - * Get rollback-configured artifact name. Contains `-t{timestamp}` - * Takes name of deployment and replaces `rg-deployment` or `deployment` with `artifact` - */ - protected getArtifactName(deploymentName: string): string { - const { deployment, artifact } = configConstants.naming.suffix; - return `${deploymentName - .replace(`rg-${deployment}`, artifact) - .replace(deployment, artifact)}.zip` - } - /** * Get the access token from credentials token cache */ @@ -196,85 +111,13 @@ export abstract class BaseService { */ protected log(message: string, options?: ServerlessLogOptions, entity?: string) { (this.serverless.cli.log as any)(message, entity, options); - } - - /** - * Get function objects - */ - protected slsFunctions(): { [functionName: string]: ServerlessAzureFunctionConfig } { - return this.serverless.service["functions"]; - } - - protected slsConfigFile(): string { - return "config" in this.options ? this.options["config"] : "serverless.yml"; - } - - protected getOption(key: string, defaultValue?: any) { - return Utils.get(this.options, key, defaultValue); - } + } protected prettyPrint(object: any) { this.log(JSON.stringify(object, null, 2)); } - private setDefaultValues(): void { - // TODO: Right now the serverless core will always default to AWS default region if the - // region has not been set in the serverless.yml or CLI options - const awsDefault = "us-east-1"; - const providerRegion = this.serverless.service.provider.region; - - if (!providerRegion || providerRegion === awsDefault) { - // no region specified in serverless.yml - this.serverless.service.provider.region = this.serverless.service.provider["location"] || "westus"; - } - - if (!this.serverless.service.provider.stage) { - this.serverless.service.provider.stage = "dev"; - } - - if (!this.serverless.service.provider["prefix"]) { - this.serverless.service.provider["prefix"] = "sls"; - } - } - - /** - * Get timestamp from `packageTimestamp` serverless variable - * If not set, create timestamp, set variable and return timestamp - */ - private getTimestamp(): number { - let timestamp = +this.serverless.variables["packageTimestamp"]; - if (!timestamp) { - timestamp = Date.now(); - this.serverless.variables["packageTimestamp"] = timestamp; - } - return timestamp; - } - - /** - * Overwrite values for resourceGroup, prefix, region and stage - * in config if passed through CLI - */ - private setupConfig() { - const { prefix, region, stage, subscriptionId } = this.config.provider; - - const options: AzureNamingServiceOptions = { - config: this.config, - suffix: `${this.getServiceName()}-rg`, - includeHash: false, - } - - this.config.provider = { - ...this.config.provider, - prefix: this.getOption("prefix") || prefix, - stage: this.getOption("stage") || stage, - region: this.getOption("region") || region, - subscriptionId: this.getOption("subscriptionId") - || process.env.AZURE_SUBSCRIPTION_ID - || subscriptionId - || this.serverless.variables["subscriptionId"] - } - this.config.provider.resourceGroup = ( - this.getOption("resourceGroup", this.config.provider.resourceGroup) - ) || AzureNamingService.getResourceName(options); + protected getOption(key: string, defaultValue?: any) { + return Utils.get(this.options, key, defaultValue); } } diff --git a/src/services/configService.test.ts b/src/services/configService.test.ts new file mode 100644 index 00000000..8c248746 --- /dev/null +++ b/src/services/configService.test.ts @@ -0,0 +1,164 @@ +import { ConfigService } from "./configService"; +import Serverless from "serverless"; +import { MockFactory } from "../test/mockFactory"; +import { ServerlessAzureConfig } from "../models/serverless"; +import configConstants from "../config"; +import { AzureNamingService } from "./namingService"; + +describe("Config Service", () => { + const serviceName = "my-custom-service" + + let serverless: Serverless; + + beforeEach(() => { + serverless = MockFactory.createTestServerless(); + const config = (serverless.service as any as ServerlessAzureConfig); + config.service = serviceName; + + delete config.provider.resourceGroup; + delete config.provider.region; + delete config.provider.stage; + delete config.provider.prefix; + }); + + describe("Configurable Variables", () => { + it("returns default values if not specified", () => { + const service = new ConfigService(serverless, {} as any); + const { prefix, region, stage } = configConstants.defaults; + expect(service.getPrefix()).toEqual(prefix); + expect(service.getStage()).toEqual(stage); + expect(service.getRegion()).toEqual(region); + }); + + it("use prefix from the CLI over the SLS yml config", () => { + const prefix = "prefix"; + const config = (serverless.service as any as ServerlessAzureConfig); + delete config.provider.resourceGroup; + config.provider.prefix = "not the prefix"; + expect(serverless.service.provider["prefix"]).not.toEqual(prefix); + const service = new ConfigService(serverless, { prefix } as any); + expect(service.getPrefix()).toEqual(prefix); + expect(service.getResourceGroupName()).toEqual(`${prefix}-wus-dev-${serviceName}-rg`); + }); + + it("use region name from the CLI over the SLS yml config", () => { + const region = "East US"; + const config = (serverless.service as any as ServerlessAzureConfig); + delete config.provider.resourceGroup; + + expect(serverless.service.provider.region).not.toEqual(region); + const service = new ConfigService(serverless, { region } as any); + expect(service.getRegion()).toEqual(region); + expect(service.getResourceGroupName()).toEqual(`sls-eus-dev-${serviceName}-rg`); + }); + + it("use stage name from the CLI over the SLS yml config", () => { + const stage = "test"; + expect(serverless.service.provider.stage).not.toEqual(stage); + const service = new ConfigService(serverless, { stage } as any); + expect(service.getStage()).toEqual(stage); + expect(service.getResourceGroupName()).toEqual(`sls-wus-${stage}-${serviceName}-rg`); + }); + + it("use the resource group name from the CLI over the SLS yml config", () => { + const resourceGroup = "resourceGroup"; + const config = (serverless.service as any as ServerlessAzureConfig); + config.provider.resourceGroup = "not the resource group"; + expect(serverless.service.provider["resourceGroup"]).not.toEqual(resourceGroup); + const service = new ConfigService(serverless, { resourceGroup } as any); + expect(service.getResourceGroupName()).toEqual(resourceGroup); + }); + + it("use the prefix defined in SLS yml config", () => { + const expectedPrefix = "testPrefix" + serverless.service.provider["prefix"] = expectedPrefix; + const service = new ConfigService(serverless, { } as any); + expect(service.getPrefix()).toEqual(expectedPrefix); + }); + + it("use region from SLS yml config", () => { + const expectedRegion = "eastus2" + serverless.service.provider["region"] = expectedRegion; + const service = new ConfigService(serverless, { } as any); + expect(service.getRegion()).toEqual(expectedRegion); + }); + + it("use stage name from SLS yml config", () => { + const expectedStage = "testStage" + serverless.service.provider["stage"] = expectedStage; + const service = new ConfigService(serverless, { } as any); + expect(service.getStage()).toEqual(expectedStage); + }); + + it("use the resource group name from SLS yml config", () => { + const service = new ConfigService(serverless, { } as any); + expect(service.getResourceGroupName()).toEqual(serverless.service.provider["resourceGroup"]); + }); + + it("use location property as region if region not set", () => { + const slsService = MockFactory.createTestService(); + delete slsService.provider.region; + const location = "East US"; + slsService.provider["location"] = location; + + const sls = MockFactory.createTestServerless({ + service: slsService + }); + + const service = new ConfigService(sls, {} as any); + expect(service.getRegion()).toEqual(location); + }); + + it("Generates resource group from convention when NOT defined in sls yaml", () => { + serverless.service.provider["resourceGroup"] = null; + const service = new ConfigService(serverless, { } as any); + const actualResourceGroupName = service.getResourceGroupName(); + const expectedRegion = AzureNamingService.createShortAzureRegionName(service.getRegion()); + const expectedStage = AzureNamingService.createShortStageName(service.getStage()); + const expectedResourceGroupName = `sls-${expectedRegion}-${expectedStage}-${serverless.service["service"]}-rg`; + expect(actualResourceGroupName).toEqual(expectedResourceGroupName); + }); + }); + + describe("Service Principal Configuration", () => { + const cliSubscriptionId = "cli sub id"; + const envVarSubscriptionId = "env var sub id"; + const configSubscriptionId = "config sub id"; + const loginResultSubscriptionId = "ABC123"; + + it("use subscription ID from the CLI", () => { + process.env.AZURE_SUBSCRIPTION_ID = envVarSubscriptionId; + serverless.service.provider["subscriptionId"] = configSubscriptionId; + serverless.variables["subscriptionId"] = loginResultSubscriptionId + const service = new ConfigService(serverless, { subscriptionId: cliSubscriptionId } as any); + expect(service.getSubscriptionId()).toEqual(cliSubscriptionId); + expect(serverless.service.provider["subscriptionId"]).toEqual(cliSubscriptionId); + }); + + it("use subscription ID from environment variable", () => { + process.env.AZURE_SUBSCRIPTION_ID = envVarSubscriptionId; + serverless.service.provider["subscriptionId"] = configSubscriptionId; + serverless.variables["subscriptionId"] = loginResultSubscriptionId + const service = new ConfigService(serverless, { } as any); + expect(service.getSubscriptionId()).toEqual(envVarSubscriptionId); + expect(serverless.service.provider["subscriptionId"]).toEqual(envVarSubscriptionId); + }); + + it("use subscription ID from config", () => { + delete process.env.AZURE_SUBSCRIPTION_ID; + serverless.service.provider["subscriptionId"] = configSubscriptionId; + serverless.variables["subscriptionId"] = loginResultSubscriptionId + const service = new ConfigService(serverless, { } as any); + expect(service.getSubscriptionId()).toEqual(configSubscriptionId); + expect(serverless.service.provider["subscriptionId"]).toEqual(configSubscriptionId); + }); + + it("use subscription ID from login result", () => { + delete process.env.AZURE_SUBSCRIPTION_ID; + serverless.variables["subscriptionId"] = loginResultSubscriptionId + const service = new ConfigService(serverless, { } as any); + expect(service.getSubscriptionId()).toEqual(loginResultSubscriptionId); + expect(serverless.service.provider["subscriptionId"]).toEqual(loginResultSubscriptionId); + }); + }); +}); diff --git a/src/services/configService.ts b/src/services/configService.ts new file mode 100644 index 00000000..4a2fe25a --- /dev/null +++ b/src/services/configService.ts @@ -0,0 +1,210 @@ +import Serverless from "serverless"; +import Service from "serverless/classes/Service"; +import configConstants from "../config"; +import { ServerlessAzureConfig, ServerlessAzureFunctionConfig } from "../models/serverless"; +import { constants } from "../shared/constants"; +import { Utils } from "../shared/utils"; +import { AzureNamingService, AzureNamingServiceOptions } from "./namingService"; + +/** + * Handles all Service Configuration + */ +export class ConfigService { + + /** Configuration for service */ + private config: ServerlessAzureConfig; + + public constructor(private serverless: Serverless, private options: Serverless.Options) { + this.config = this.initializeConfig(serverless.service); + } + + /** + * Get Azure Provider Configuration + */ + public getConfig(): ServerlessAzureConfig { + return this.config; + } + + /** + * Name of Azure Region for deployment + */ + public getRegion(): string { + return this.config.provider.region; + } + + /** + * Name of current deployment stage + */ + public getStage(): string { + return this.config.provider.stage; + } + + /** + * Prefix for service + */ + public getPrefix(): string { + return this.config.provider.prefix; + } + + /** + * Name of current resource group + */ + public getResourceGroupName(): string { + return this.config.provider.resourceGroup; + } + + /** + * Azure Subscription ID + */ + public getSubscriptionId(): string { + return this.config.provider.subscriptionId; + } + + /** + * Name of current deployment + */ + public getDeploymentName(): string { + return AzureNamingService.getDeploymentName( + this.config, + (this.config.provider.deployment.rollback) ? `t${this.getTimestamp()}` : null + ) + } + + /** + * Get rollback-configured artifact name. Contains `-t{timestamp}` + * Takes name of deployment and replaces `rg-deployment` or `deployment` with `artifact` + */ + public getArtifactName(deploymentName?: string): string { + deploymentName = deploymentName || this.getDeploymentName(); + const { deployment, artifact } = configConstants.naming.suffix; + return `${deploymentName + .replace(`rg-${deployment}`, artifact) + .replace(deployment, artifact)}.zip` + } + + /** + * Function configuration from serverless.yml + */ + public getFunctionConfig(): { [functionName: string]: ServerlessAzureFunctionConfig } { + return this.config.functions; + } + + /** + * Name of file containing serverless config + */ + public getConfigFile(): string { + return this.getOption("config", "serverless.yml"); + } + + /** + * Name of Function App Service + */ + public getServiceName(): string { + return this.config.service; + } + + /** + * Set any default values required for service + * @param config Current Serverless configuration + */ + private setDefaultValues(config: ServerlessAzureConfig) { + const { awsRegion, region, stage, prefix } = configConstants.defaults; + const providerRegion = config.provider.region; + + if (!providerRegion || providerRegion === awsRegion) { + config.provider.region = this.serverless.service.provider["location"] || region; + } + + if (!config.provider.stage) { + config.provider.stage = stage; + } + + if (!config.provider.prefix) { + config.provider.prefix = prefix; + } + } + + /** + * Overwrite values for resourceGroup, prefix, region and stage + * in config if passed through CLI + */ + private initializeConfig(service: Service): ServerlessAzureConfig { + const config: ServerlessAzureConfig = service as any; + this.setDefaultValues(config); + + const { + prefix, + region, + stage, + subscriptionId, + tenantId, + appId, + deployment + } = config.provider; + + const options: AzureNamingServiceOptions = { + config: config, + suffix: `${config.service}-rg`, + includeHash: false, + } + + config.provider = { + ...config.provider, + prefix: this.getOption("prefix") || prefix, + stage: this.getOption("stage") || stage, + region: this.getOption("region") || region, + subscriptionId: this.getOption(constants.variableKeys.subscriptionId) + || process.env.AZURE_SUBSCRIPTION_ID + || subscriptionId + || this.serverless.variables[constants.variableKeys.subscriptionId], + tenantId: this.getOption(constants.variableKeys.tenantId) + || process.env.AZURE_TENANT_ID + || tenantId, + appId: this.getOption(constants.variableKeys.appId) + || process.env.AZURE_CLIENT_ID + || appId + } + config.provider.resourceGroup = ( + this.getOption("resourceGroup", config.provider.resourceGroup) + ) || AzureNamingService.getResourceName(options); + + config.provider.deployment = { + ...configConstants.deploymentConfig, + ...deployment + } + + return config; + } + + /** + * Get timestamp from `packageTimestamp` serverless variable + * If not set, create timestamp, set variable and return timestamp + */ + private getTimestamp(): number { + const key = constants.variableKeys.packageTimestamp + let timestamp = +this.getVariable(key); + if (!timestamp) { + timestamp = Date.now(); + this.serverless.variables[key] = timestamp; + } + return timestamp; + } + + /** + * Get value of option from Serverless CLI + * @param key Key of option + * @param defaultValue Default value if key not found in options object + */ + protected getOption(key: string, defaultValue?: any) { + return Utils.get(this.options, key, defaultValue); + } + + /** + * Get variable value from Serverless variables + * @param key Key for variable + * @param defaultValue Default value if key not found in variable object + */ + protected getVariable(key: string, defaultValue?: any) { + return Utils.get(this.serverless.variables, key, defaultValue); + } +} diff --git a/src/services/funcService.ts b/src/services/funcService.ts index b84cda7a..580f1555 100644 --- a/src/services/funcService.ts +++ b/src/services/funcService.ts @@ -44,7 +44,7 @@ export class FuncService extends BaseService { } private exists(functionName: string) { - return (functionName in this.slsFunctions()); + return (functionName in this.configService.getFunctionConfig()); } private createHandler(functionName: string) { @@ -52,26 +52,26 @@ export class FuncService extends BaseService { } private addToServerlessYml(functionName: string) { - const functions = this.slsFunctions(); + const functions = this.configService.getFunctionConfig(); functions[functionName] = this.getFunctionSlsObject(functionName) this.updateFunctionsYml(functions) } private removeFromServerlessYml(functionName: string) { - const functions = this.slsFunctions(); + const functions = this.configService.getFunctionConfig(); delete functions[functionName]; this.updateFunctionsYml(functions) } private getServerlessYml() { - return this.serverless.utils.readFileSync(this.slsConfigFile()); + return this.serverless.utils.readFileSync(this.configService.getConfigFile()); } private updateFunctionsYml(functionYml: any) { const serverlessYml = this.getServerlessYml(); serverlessYml["functions"] = functionYml; this.serverless.utils.writeFileSync( - this.slsConfigFile(), + this.configService.getConfigFile(), yaml.dump(serverlessYml) ); } diff --git a/src/services/functionAppService.ts b/src/services/functionAppService.ts index 6942f8be..3eb5bd4b 100644 --- a/src/services/functionAppService.ts +++ b/src/services/functionAppService.ts @@ -155,12 +155,12 @@ export class FunctionAppService extends BaseService { const functionZipFile = this.getFunctionZipFile(); - if (this.deploymentConfig.runFromBlobUrl) { + if (this.config.provider.deployment.runFromBlobUrl) { this.log("Updating function app setting to run from external package..."); await this.uploadZippedArtifactToBlobStorage(functionZipFile); const sasUrl = await this.blobService.generateBlobSasTokenUrl( - this.deploymentConfig.container, + this.config.provider.deployment.container, this.artifactName ); @@ -250,6 +250,10 @@ export class FunctionAppService extends BaseService { return functionZipFile; } + public getDeploymentName(): string { + return this.configService.getDeploymentName(); + } + public async updateFunctionAppSetting(functionApp: Site, setting: string, value: string) { const { properties } = await this.webClient.webApps.listApplicationSettings(this.resourceGroup, functionApp.name); properties[setting] = value; @@ -261,10 +265,10 @@ export class FunctionAppService extends BaseService { */ private async uploadZippedArtifactToBlobStorage(functionZipFile: string): Promise { await this.blobService.initialize(); - await this.blobService.createContainerIfNotExists(this.deploymentConfig.container); + await this.blobService.createContainerIfNotExists(this.config.provider.deployment.container); await this.blobService.uploadFile( functionZipFile, - this.deploymentConfig.container, + this.config.provider.deployment.container, this.artifactName, ); } diff --git a/src/services/invokeService.test.ts b/src/services/invokeService.test.ts index ab975875..f4b6b8ac 100644 --- a/src/services/invokeService.test.ts +++ b/src/services/invokeService.test.ts @@ -20,7 +20,7 @@ describe("Invoke Service ", () => { const functionName = "hello"; const urlPOST = `http://${app.defaultHostName}/api/${functionName}`; const urlGET = `http://${app.defaultHostName}/api/${functionName}?name%3D${testData}`; - const localUrl = `http://localhost:${configConstants.defaultLocalPort}/api/${functionName}` + const localUrl = `http://localhost:${configConstants.defaults.localPort}/api/${functionName}` let masterKey: string; let sls = MockFactory.createTestServerless(); let options = { diff --git a/src/services/invokeService.ts b/src/services/invokeService.ts index 00366e49..dbb4b52e 100644 --- a/src/services/invokeService.ts +++ b/src/services/invokeService.ts @@ -24,7 +24,7 @@ export class InvokeService extends BaseService { */ public async invoke(method: string, functionName: string, data?: any){ - const functionObject = this.slsFunctions()[functionName]; + const functionObject = this.configService.getFunctionConfig()[functionName]; /* accesses the admin key */ if (!functionObject) { this.serverless.cli.log(`Function ${functionName} does not exist`); @@ -63,7 +63,7 @@ export class InvokeService extends BaseService { } private getLocalHost() { - return `http://localhost:${this.getOption("port", configConstants.defaultLocalPort)}` + return `http://localhost:${this.getOption("port", configConstants.defaults.localPort)}` } private getConfiguredFunctionRoute(functionName: string) { diff --git a/src/services/loginService.ts b/src/services/loginService.ts index 49bf602f..3a2953ea 100644 --- a/src/services/loginService.ts +++ b/src/services/loginService.ts @@ -27,7 +27,7 @@ export class AzureLoginService extends BaseService { * @param options Options for different authentication methods */ public async login(options?: AzureTokenCredentialsOptions | InteractiveLoginOptions): Promise { - const subscriptionId = this.getSubscriptionId(); + const subscriptionId = this.configService.getSubscriptionId(); const clientId = process.env.AZURE_CLIENT_ID; const secret = process.env.AZURE_CLIENT_SECRET; const tenantId = process.env.AZURE_TENANT_ID; @@ -57,4 +57,8 @@ export class AzureLoginService extends BaseService { public async servicePrincipalLogin(clientId: string, secret: string, tenantId: string, options: AzureTokenCredentialsOptions): Promise { return await loginWithServicePrincipalSecretWithAuthResponse(clientId, secret, tenantId, options); } + + public getSubscriptionId() { + return this.configService.getSubscriptionId(); + } } diff --git a/src/services/packageService.ts b/src/services/packageService.ts index 8ab94776..cec641cf 100644 --- a/src/services/packageService.ts +++ b/src/services/packageService.ts @@ -93,7 +93,7 @@ export class PackageService extends BaseService { const functionJSON = functionMetadata.params.functionsJson; functionJSON.entryPoint = functionMetadata.entryPoint; functionJSON.scriptFile = functionMetadata.handlerPath; - const functionObject = this.slsFunctions()[functionName]; + const functionObject = this.configService.getFunctionConfig()[functionName]; const bindingAzureSettings = Utils.getIncomingBindingConfig(functionObject)["x-azure-settings"]; if (bindingAzureSettings.route) { diff --git a/src/services/resourceService.ts b/src/services/resourceService.ts index 6a032659..b76a1531 100644 --- a/src/services/resourceService.ts +++ b/src/services/resourceService.ts @@ -58,7 +58,7 @@ export class ResourceService extends BaseService { public async listDeployments(): Promise { const deployments = await this.getDeployments() if (!deployments || deployments.length === 0) { - this.log(`No deployments found for resource group '${this.getResourceGroupName()}'`); + this.log(`No deployments found for resource group '${this.configService.getResourceGroupName()}'`); return; } let stringDeployments = "\n\nDeployments"; @@ -89,7 +89,7 @@ export class ResourceService extends BaseService { this.log(`Creating resource group: ${this.resourceGroup}`); return await this.resourceClient.resourceGroups.createOrUpdate(this.resourceGroup, { - location: AzureNamingService.getNormalizedRegionName(this.getRegion()), + location: AzureNamingService.getNormalizedRegionName(this.configService.getRegion()), }); } diff --git a/src/services/rollbackService.ts b/src/services/rollbackService.ts index 9747196f..b1a1ec90 100644 --- a/src/services/rollbackService.ts +++ b/src/services/rollbackService.ts @@ -41,7 +41,7 @@ export class RollbackService extends BaseService { return; } // Name of artifact in blob storage - const artifactName = this.getArtifactName(deployment.name); + const artifactName = this.configService.getArtifactName(deployment.name); // Redeploy resource group (includes SAS token URL if running from blob URL) await this.redeployDeployment(deployment, artifactName); } @@ -56,12 +56,12 @@ export class RollbackService extends BaseService { const armDeployment = await this.convertToArmDeployment(deployment); // Initialize blob service for either creating SAS token or downloading artifact to uplod to function app await this.blobService.initialize(); - if (this.deploymentConfig.runFromBlobUrl) { + if (this.config.provider.deployment.runFromBlobUrl) { // Set functionRunFromPackage param to SAS URL of blob armDeployment.parameters.functionAppRunFromPackage = { type: ArmParamType.String, value: await this.blobService.generateBlobSasTokenUrl( - this.deploymentConfig.container, + this.config.provider.deployment.container, artifactName ) } @@ -71,7 +71,7 @@ export class RollbackService extends BaseService { * Cannot use an `else` statement just because deploying the artifact * depends on `deployTemplate` already being called */ - if (!this.deploymentConfig.runFromBlobUrl) { + if (!this.config.provider.deployment.runFromBlobUrl) { const artifactPath = await this.downloadArtifact(artifactName); await this.redeployArtifact(artifactPath); } @@ -132,7 +132,7 @@ export class RollbackService extends BaseService { private async downloadArtifact(artifactName: string): Promise { const artifactPath = path.join(this.serverless.config.servicePath, ".serverless", artifactName) await this.blobService.downloadBinary( - this.deploymentConfig.container, + this.config.provider.deployment.container, artifactName, artifactPath ); diff --git a/src/shared/constants.ts b/src/shared/constants.ts index 4ed98f73..08b7d4eb 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -21,4 +21,12 @@ export const constants = { queueName: "queueName", xAzureSettings: "x-azure-settings", entryPoint: "entryPoint", -} \ No newline at end of file + variableKeys: { + config: "serverlessAzureConfig", + subscriptionId: "subscriptionId", + tenantId: "tenantId", + appId: "appId", + packageTimestamp: "packageTimestamp", + azureCredentials: "azureCredentials", + } +}