Skip to content

Commit

Permalink
feat: Refactor runtime configuration to allow for non-node runtimes (#…
Browse files Browse the repository at this point in the history
…348)

- Added `FunctionRuntime` configuration to provider
- Extracting `FunctionRuntime` from `runtime` property of configuration within `ConfigService`
- Refactored node-specific code in ARM template generation
  • Loading branch information
tbarlow12 committed Oct 3, 2019
1 parent 90bfc16 commit e943b57
Show file tree
Hide file tree
Showing 17 changed files with 309 additions and 187 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ node_modules/
/coverage/
.env*
.idea/

.vscode/launch.json
52 changes: 10 additions & 42 deletions src/armTemplates/resources/functionApp.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import { ArmResourceTemplate, ArmResourceTemplateGenerator, ArmParamType, ArmParameters, DefaultArmParams, ArmParameter } from "../../models/armTemplates";
import { FunctionAppConfig, ServerlessAzureConfig } from "../../models/serverless";
import { ArmParameter, ArmParameters, ArmParamType, ArmResourceTemplate, ArmResourceTemplateGenerator, DefaultArmParams } from "../../models/armTemplates";
import { FunctionAppConfig, ServerlessAzureConfig, SupportedRuntimeLanguage } from "../../models/serverless";
import { AzureNamingService, AzureNamingServiceOptions } from "../../services/namingService";

//Runtime versions found at " https://<sitename>.scm.azurewebsites.net/api/diagnostics/runtime".
import runtimeVersionsJson from "../../services/runtimeVersions.json";
import semver from "semver";

interface FunctionAppParams extends DefaultArmParams {
functionAppName: ArmParameter;
functionAppNodeVersion: ArmParameter;
Expand Down Expand Up @@ -132,18 +128,23 @@ export class FunctionAppResource implements ArmResourceTemplateGenerator {
public getParameters(config: ServerlessAzureConfig): ArmParameters {
const resourceConfig: FunctionAppConfig = {
...config.provider.functionApp,
nodeVersion: this.getRuntimeVersion(config.provider.runtime)
};
const { functionRuntime } = config.provider;


const params: FunctionAppParams = {
functionAppName: {
value: FunctionAppResource.getResourceName(config),
},
functionAppNodeVersion: {
value: resourceConfig.nodeVersion,
value: (functionRuntime.language === SupportedRuntimeLanguage.NODE)
?
functionRuntime.version
:
undefined
},
functionAppWorkerRuntime: {
value: resourceConfig.workerRuntime,
value: functionRuntime.language,
},
functionAppExtensionVersion: {
value: resourceConfig.extensionVersion,
Expand All @@ -152,37 +153,4 @@ export class FunctionAppResource implements ArmResourceTemplateGenerator {

return params as unknown as ArmParameters;
}

private getRuntimeVersion(runtime: string): string {
if (!runtime) {
throw new Error("Runtime version not specified in serverless.yml");
}
const extractedVersion = runtime.split("nodejs")[1];
const runtimeVersionsList = runtimeVersionsJson["nodejs"];

//Searches for a specific version. For example nodejs10.6.0.
if (!extractedVersion.endsWith(".x")) {
let retrivedVersion: string;
for (const version of runtimeVersionsList) {
retrivedVersion = version["version"];
if (extractedVersion === retrivedVersion && semver.valid(retrivedVersion)) {
return retrivedVersion;
}
}
}
else {
// User specified something like nodejs10.14.x
const extractedVersionNumber = extractedVersion.replace(/[^0-9\.]/g, "");

const selectedVersions = runtimeVersionsList.filter(({ version }) => {
return version.startsWith(extractedVersionNumber) && semver.valid(version)
}).map((item) => item.version);

if (!selectedVersions.length) {
throw new Error(`Could not find runtime version matching ${runtime}`)
}
return selectedVersions.sort(semver.rcompare)[0]
}
throw new Error(`Could not find runtime version matching ${runtime}`)
}
}
16 changes: 16 additions & 0 deletions src/models/serverless.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,22 @@ export interface ServerlessAzureProvider {
armTemplate?: ArmTemplateConfig;
keyVaultConfig?: AzureKeyVaultConfig;
runtime: string;
functionRuntime?: FunctionRuntime;
}

export enum FunctionAppOS {
WINDOWS = "windows",
LINUX = "linux"
}

export interface FunctionRuntime {
language: SupportedRuntimeLanguage;
version: string;
}

export enum SupportedRuntimeLanguage {
PYTHON = "python",
NODE = "node"
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/services/apimService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ describe("APIM Service", () => {
resourceGroup: "test-sls-rg",
region: "West US",
apim: apimConfig,
runtime: "nodejs10.x"
},
};

Expand Down
35 changes: 0 additions & 35 deletions src/services/armService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,41 +92,6 @@ describe("Arm Service", () => {
await expect(service.createDeploymentFromType("not-found")).rejects.not.toBeNull();
});

it("throws error when invalid nodejs version in defined", async () => {
sls.service.provider.runtime = "nodejs10.6.1";
await expect(service.createDeploymentFromType("premium")).rejects.toThrowError("Could not find runtime version matching nodejs10.6.1");
});

it("throws error when incomplete nodejs version in defined", async () => {
sls.service.provider.runtime = "nodejs8";
await expect(service.createDeploymentFromType("premium")).rejects.toThrowError("Could not find runtime version matching nodejs8");
});

it("throws error when unsupported nodejs version in defined", async () => {
sls.service.provider.runtime = "nodejs5.x";
await expect(service.createDeploymentFromType("premium")).rejects.toThrowError("Could not find runtime version matching nodejs5.x");
});

it("Does not throw an error when valid nodejs version is defined", async () => {
sls.service.provider.runtime = "nodejs10.x";
await expect(service.createDeploymentFromType("premium")).resolves.not.toThrow();
});

it("Does not throw an error when nodejs version with major and minor is defined", async () => {
sls.service.provider.runtime = "nodejs6.9.x";
await expect(service.createDeploymentFromType("premium")).resolves.not.toThrow();
});

it("Does not throw an error when specific nodejs version is defined", async () => {
sls.service.provider.runtime = "nodejs10.6.0";
await expect(service.createDeploymentFromType("premium")).resolves.not.toThrow();
});

it("throws an error when no nodejs version is defined", async () => {
sls.service.provider.runtime = undefined;
await expect(service.createDeploymentFromType("premium")).rejects.toThrowError("Runtime version not specified in serverless.yml");
});

it("Premium template includes correct resources", async () => {
sls.service.provider.runtime = "nodejs10.14.1";
const deployment = await service.createDeploymentFromType(ArmTemplateType.Premium);
Expand Down
15 changes: 7 additions & 8 deletions src/services/armService.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { ResourceManagementClient } from "@azure/arm-resources";
import { Deployment, DeploymentExtended } from "@azure/arm-resources/esm/models";
import deepEqual from "deep-equal";
import fs from "fs";
import jsonpath from "jsonpath";
import path from "path";
import Serverless from "serverless";
import { ArmDeployment, ArmResourceTemplateGenerator, ArmTemplateType, ArmResourceTemplate, ArmParameters } from "../models/armTemplates";
import { ArmTemplateConfig, ServerlessAzureConfig, ServerlessAzureOptions } from "../models/serverless";
import { ArmDeployment, ArmParameters, ArmResourceTemplate, ArmResourceTemplateGenerator, ArmTemplateType } from "../models/armTemplates";
import { DeploymentExtendedError } from "../models/azureProvider";
import { ArmTemplateConfig, ServerlessAzureOptions } from "../models/serverless";
import { Guard } from "../shared/guard";
import { BaseService } from "./baseService";
import { ResourceService } from "./resourceService"
import { DeploymentExtendedError } from "../models/azureProvider";
import deepEqual from "deep-equal";
import { ResourceService } from "./resourceService";

export class ArmService extends BaseService {
private resourceClient: ResourceManagementClient;
Expand Down Expand Up @@ -39,13 +39,12 @@ export class ArmService extends BaseService {
throw new Error(`Unable to find template with name ${type} `);
}

const azureConfig: ServerlessAzureConfig = this.serverless.service as any;
const mergedTemplate = template.getTemplate();
let parameters = template.getParameters(azureConfig);
let parameters = template.getParameters(this.config);

if (this.config.provider.apim) {
const apimTemplate = apimResource.getTemplate();
const apimParameters = apimResource.getParameters(azureConfig);
const apimParameters = apimResource.getParameters(this.config);

mergedTemplate.parameters = {
...mergedTemplate.parameters,
Expand Down
3 changes: 2 additions & 1 deletion src/services/azureKeyVaultService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ describe("Azure Key Vault Service", () => {
name: "azure",
resourceGroup: "test-sls-rg",
region: "West US",
keyVault
keyVault,
runtime: "nodejs10.x",
}
};

Expand Down
1 change: 1 addition & 0 deletions src/services/baseService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ describe("Base Service", () => {
provider: {
resourceGroup: "My-Resource-Group",
deploymentName: "My-Deployment",
runtime: "nodejs10.x"
},
};

Expand Down
6 changes: 1 addition & 5 deletions src/services/baseService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,7 @@ import fs from "fs";
import request from "request";
import Serverless from "serverless";
import { StorageAccountResource } from "../armTemplates/resources/storageAccount";
import {
ServerlessAzureConfig,
ServerlessAzureOptions,
ServerlessLogOptions
} from "../models/serverless";
import { ServerlessAzureConfig, ServerlessAzureOptions, ServerlessLogOptions } from "../models/serverless";
import { constants } from "../shared/constants";
import { Guard } from "../shared/guard";
import { Utils } from "../shared/utils";
Expand Down
80 changes: 77 additions & 3 deletions src/services/configService.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { ConfigService } from "./configService";
import Serverless from "serverless";
import { MockFactory } from "../test/mockFactory";
import { ServerlessAzureConfig, DeploymentConfig } from "../models/serverless";
import configConstants from "../config";
import { DeploymentConfig, FunctionRuntime, ServerlessAzureConfig, SupportedRuntimeLanguage } from "../models/serverless";
import { MockFactory } from "../test/mockFactory";
import { ConfigService } from "./configService";
import { AzureNamingService } from "./namingService";

describe("Config Service", () => {
Expand Down Expand Up @@ -193,4 +193,78 @@ describe("Config Service", () => {
expect(serverless.service.provider["subscriptionId"]).toEqual(loginResultSubscriptionId);
});
});

describe("Runtime version", () => {
it("throws error when invalid nodejs version in defined", () => {
const sls = MockFactory.createTestServerless();
sls.service.provider.runtime = "nodejs10.6.1";
expect(() => new ConfigService(sls, {} as any)).toThrowError("Runtime nodejs10.6.1 is not supported");
expect((sls.service as any as ServerlessAzureConfig).provider.functionRuntime).toBeUndefined();
});

it("throws error when incomplete nodejs version in defined", () => {
const sls = MockFactory.createTestServerless();
sls.service.provider.runtime = "nodejs8";
expect(() => new ConfigService(sls, {} as any)).toThrowError("Invalid runtime: nodejs8");
expect((sls.service as any as ServerlessAzureConfig).provider.functionRuntime).toBeUndefined();
});

it("throws error when unsupported nodejs version in defined", () => {
const sls = MockFactory.createTestServerless();
sls.service.provider.runtime = "nodejs5.x";
expect(() => new ConfigService(sls, {} as any)).toThrowError("Runtime nodejs5.x is not supported");
expect((sls.service as any as ServerlessAzureConfig).provider.functionRuntime).toBeUndefined();
});

it("Does not throw an error when valid nodejs version is defined", () => {
const sls = MockFactory.createTestServerless();
sls.service.provider.runtime = "nodejs10.x";
expect(() => new ConfigService(sls, {} as any)).not.toThrow();
const expectedRuntime: FunctionRuntime = {
language: SupportedRuntimeLanguage.NODE,
version: "10.15.2"
}
expect((sls.service as any as ServerlessAzureConfig).provider.functionRuntime).toEqual(expectedRuntime);
});

it("Does not throw an error when nodejs version with major and minor is defined", () => {
const sls = MockFactory.createTestServerless();
sls.service.provider.runtime = "nodejs6.9.x";
expect(() => new ConfigService(sls, {} as any)).not.toThrow();
const expectedRuntime: FunctionRuntime = {
language: SupportedRuntimeLanguage.NODE,
version: "6.9.5"
}
expect((sls.service as any as ServerlessAzureConfig).provider.functionRuntime).toEqual(expectedRuntime);
});

it("Does not throw an error when specific nodejs version is defined", () => {
const sls = MockFactory.createTestServerless();
sls.service.provider.runtime = "nodejs10.6.0";
expect(() => new ConfigService(sls, {} as any)).not.toThrow();
const expectedRuntime: FunctionRuntime = {
language: SupportedRuntimeLanguage.NODE,
version: "10.6.0"
}
expect((sls.service as any as ServerlessAzureConfig).provider.functionRuntime).toEqual(expectedRuntime);
});

it("throws an error when no nodejs version is defined", () => {
const sls = MockFactory.createTestServerless();
sls.service.provider.runtime = undefined;
expect(() => new ConfigService(sls, {} as any)).toThrowError("Runtime version not specified in serverless.yml");
expect((sls.service as any as ServerlessAzureConfig).provider.functionRuntime).toBeUndefined();
});

it("does not throw an error with python3.6", () => {
const sls = MockFactory.createTestServerless();
sls.service.provider.runtime = "python3.6";
expect(() => new ConfigService(sls, {} as any)).not.toThrow();
const expectedRuntime: FunctionRuntime = {
language: SupportedRuntimeLanguage.PYTHON,
version: "3.6"
}
expect((sls.service as any as ServerlessAzureConfig).provider.functionRuntime).toEqual(expectedRuntime);
});
})
});
Loading

0 comments on commit e943b57

Please sign in to comment.