Skip to content
This repository was archived by the owner on Dec 9, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 22 additions & 74 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion src/armTemplates/resources/functionApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ export class FunctionAppResource implements ArmResourceTemplateGenerator {
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"functionAppRunFromPackage": {
"defaultValue": "1",
"type": "String"
},
"functionAppName": {
"defaultValue": "",
"type": "String"
Expand Down Expand Up @@ -86,7 +90,7 @@ export class FunctionAppResource implements ArmResourceTemplateGenerator {
},
{
"name": "WEBSITE_RUN_FROM_PACKAGE",
"value": "1"
"value": "[parameters('functionAppRunFromPackage')]"
},
{
"name": "APPINSIGHTS_INSTRUMENTATIONKEY",
Expand Down
7 changes: 5 additions & 2 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
export const configConstants = {
bearer: "Bearer ",
deploymentArtifactContainer: "deployment-artifacts",
deploymentConfig: {
container: "deployment-artifacts",
rollback: true,
runFromBlobUrl: false,
},
functionAppApiPath: "/api/",
functionAppDomain: ".azurewebsites.net",
functionsAdminApiPath: "/admin/functions/",
Expand All @@ -11,7 +15,6 @@ export const configConstants = {
logStreamApiPath: "/api/logstream/application/functions/function/",
masterKeyApiPath: "/api/functions/admin/masterkey",
providerName: "azure",
rollbackEnabled: true,
scmCommandApiPath: "/api/command",
scmDomain: ".scm.azurewebsites.net",
scmVfsPath: "/api/vfs/site/wwwroot/",
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { AzureApimServicePlugin } from "./plugins/apim/apimServicePlugin";
import { AzureApimFunctionPlugin } from "./plugins/apim/apimFunctionPlugin";
import { AzureFuncPlugin } from "./plugins/func/azureFunc";
import { AzureOfflinePlugin } from "./plugins/offline/azureOfflinePlugin"
import { AzureRollbackPlugin } from "./plugins/rollback/azureRollbackPlugin"


export default class AzureIndex {
Expand All @@ -34,6 +35,7 @@ export default class AzureIndex {
this.serverless.pluginManager.addPlugin(AzureApimFunctionPlugin);
this.serverless.pluginManager.addPlugin(AzureFuncPlugin);
this.serverless.pluginManager.addPlugin(AzureOfflinePlugin);
this.serverless.pluginManager.addPlugin(AzureRollbackPlugin);
}
}

Expand Down
1 change: 1 addition & 0 deletions src/models/serverless.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export interface FunctionAppConfig extends ResourceConfig {
export interface DeploymentConfig {
rollback?: boolean;
container?: string;
runFromBlobUrl?: boolean;
}

export interface ServerlessAzureProvider {
Expand Down
28 changes: 25 additions & 3 deletions src/plugins/deploy/azureDeployPlugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,29 @@ describe("Deploy plugin", () => {
expect(uploadFunctions).toBeCalledWith(functionAppStub);
});

it("lists deployments", async () => {
it("lists deployments with timestamps", async () => {
const deployments = MockFactory.createTestDeployments(5, true);
ResourceService.prototype.getDeployments = jest.fn(() => Promise.resolve(deployments));
const sls = MockFactory.createTestServerless();
const options = MockFactory.createTestServerlessOptions();
const plugin = new AzureDeployPlugin(sls, options);
await invokeHook(plugin, "deploy:list:list");
let expectedLogStatement = "\n\nDeployments";
const originalTimestamp = +MockFactory.createTestTimestamp();
let i = 0
for (const dep of deployments) {
const timestamp = originalTimestamp + i
expectedLogStatement += "\n-----------\n"
expectedLogStatement += `Name: ${dep.name}\n`
expectedLogStatement += `Timestamp: ${timestamp}\n`;
expectedLogStatement += `Datetime: ${new Date(timestamp).toISOString()}\n`
i++
}
expectedLogStatement += "-----------\n"
expect(sls.cli.log).lastCalledWith(expectedLogStatement);
});

it("lists deployments without timestamps", async () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would a deployment NOT have a timestamp?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think i see now - You are interpolating it from the file name - not the timestamp from the actual deployment?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I "stamp" it when the package is being created. I wanted to use the timestamp from the deployment, but since someone "could" have the same deployment name, I'd have to scan through all deployments to check which one was most recent. It seemed more safe to just include the timestamp in the name of the deployment. I checked the response from the deployment of the resource group, and the only property included is the provisioning state

const deployments = MockFactory.createTestDeployments();
ResourceService.prototype.getDeployments = jest.fn(() => Promise.resolve(deployments));
const sls = MockFactory.createTestServerless();
Expand All @@ -47,8 +69,8 @@ describe("Deploy plugin", () => {
for (const dep of deployments) {
expectedLogStatement += "\n-----------\n"
expectedLogStatement += `Name: ${dep.name}\n`
expectedLogStatement += `Timestamp: ${dep.properties.timestamp.getTime()}\n`;
expectedLogStatement += `Datetime: ${dep.properties.timestamp.toISOString()}\n`
expectedLogStatement += "Timestamp: None\n";
expectedLogStatement += "Datetime: None\n"
}
expectedLogStatement += "-----------\n"
expect(sls.cli.log).lastCalledWith(expectedLogStatement);
Expand Down
11 changes: 9 additions & 2 deletions src/plugins/deploy/azureDeployPlugin.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Serverless from "serverless";
import { FunctionAppService } from "../../services/functionAppService";
import { ResourceService } from "../../services/resourceService";
import { Utils } from "../../shared/utils";

export class AzureDeployPlugin {
public hooks: { [eventName: string]: Promise<any> };
Expand Down Expand Up @@ -45,8 +46,14 @@ export class AzureDeployPlugin {
for (const dep of deployments) {
stringDeployments += "\n-----------\n"
stringDeployments += `Name: ${dep.name}\n`
stringDeployments += `Timestamp: ${dep.properties.timestamp.getTime()}\n`;
stringDeployments += `Datetime: ${dep.properties.timestamp.toISOString()}\n`
const timestampFromName = Utils.getTimestampFromName(dep.name);
stringDeployments += `Timestamp: ${(timestampFromName) ? timestampFromName : "None"}\n`;
stringDeployments += `Datetime: ${(timestampFromName)
?
new Date(+timestampFromName).toISOString()
:
"None"
}\n`
}
stringDeployments += "-----------\n"
this.serverless.cli.log(stringDeployments);
Expand Down
1 change: 1 addition & 0 deletions src/plugins/login/loginPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export class AzureLoginPlugin {
this.hooks = {
"before:package:initialize": this.login.bind(this),
"before:deploy:list:list": this.login.bind(this),
"before:rollback:rollback": this.login.bind(this),
};
}

Expand Down
17 changes: 17 additions & 0 deletions src/plugins/rollback/azureRollbackPlugin.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { MockFactory } from "../../test/mockFactory";
import { invokeHook } from "../../test/utils";
import { AzureRollbackPlugin } from "./azureRollbackPlugin";

jest.mock("../../services/rollbackService");
import { RollbackService } from "../../services/rollbackService";

describe("Rollback Plugin", () => {
it("should call rollback service", async () => {
const plugin = new AzureRollbackPlugin(
MockFactory.createTestServerless(),
MockFactory.createTestServerlessOptions(),
);
await invokeHook(plugin, "rollback:rollback");
expect(RollbackService.prototype.rollback).toBeCalled();
})
});
20 changes: 20 additions & 0 deletions src/plugins/rollback/azureRollbackPlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import Serverless from "serverless";
import { RollbackService } from "../../services/rollbackService";

/**
* Plugin for rolling back Function App Service to previous deployment
*/
export class AzureRollbackPlugin {
public hooks: { [eventName: string]: Promise<any> };

public constructor(private serverless: Serverless, private options: Serverless.Options) {
this.hooks = {
"rollback:rollback": this.rollback.bind(this)
};
}

private async rollback() {
const service = new RollbackService(this.serverless, this.options);
await service.rollback();
}
}
16 changes: 8 additions & 8 deletions src/services/armService.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import Serverless from "serverless";
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, ArmTemplateConfig, ServerlessAzureOptions } from "../models/serverless";
import { ArmDeployment, ArmResourceTemplateGenerator, ArmTemplateType } from "../models/armTemplates";
import { Deployment, DeploymentExtended } from "@azure/arm-resources/esm/models";
import fs from "fs";
import path from "path";
import jsonpath from "jsonpath";
import path from "path";
import Serverless from "serverless";
import { ArmDeployment, ArmResourceTemplateGenerator, ArmTemplateType } from "../models/armTemplates";
import { ArmTemplateConfig, ServerlessAzureConfig, ServerlessAzureOptions } from "../models/serverless";
import { Guard } from "../shared/guard";
import { BaseService } from "./baseService";

export class ArmService extends BaseService {
private resourceClient: ResourceManagementClient;
Expand Down Expand Up @@ -97,7 +97,7 @@ export class ArmService extends BaseService {
Object.keys(deployment.parameters).forEach((key) => {
const parameterValue = deployment.parameters[key];
if (parameterValue) {
deploymentParameters[key] = { value: deployment.parameters[key] };
deploymentParameters[key] = { value: parameterValue }
}
});

Expand Down
57 changes: 55 additions & 2 deletions src/services/azureBlobStorageService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { AzureBlobStorageService, AzureStorageAuthType } from "./azureBlobStorag

jest.mock("@azure/storage-blob");
jest.genMockFromModule("@azure/storage-blob")
import { Aborter, BlockBlobURL, ContainerURL, ServiceURL, uploadFileToBlockBlob, TokenCredential, SharedKeyCredential } from "@azure/storage-blob";
import { Aborter, BlockBlobURL, ContainerURL, ServiceURL, uploadFileToBlockBlob, TokenCredential, SharedKeyCredential, downloadBlobToBuffer, generateBlobSASQueryParameters, BlobSASPermissions } from "@azure/storage-blob";

jest.mock("@azure/arm-storage")
jest.genMockFromModule("@azure/arm-storage");
Expand All @@ -14,6 +14,9 @@ jest.mock("./loginService");
import { AzureLoginService } from "./loginService"
import { StorageAccountResource } from "../armTemplates/resources/storageAccount";

jest.mock("fs")
import fs from "fs";

describe("Azure Blob Storage Service", () => {
const filePath = "deployments/deployment.zip";
const fileName = "deployment.zip";
Expand All @@ -24,7 +27,8 @@ describe("Azure Blob Storage Service", () => {
const sls = MockFactory.createTestServerless();
const accountName = StorageAccountResource.getResourceName(sls.service as any);
const options = MockFactory.createTestServerlessOptions();
const blockBlobUrl = MockFactory.createTestBlockBlobUrl(containerName, filePath);
const blobContent = "testContent";
const blockBlobUrl = MockFactory.createTestBlockBlobUrl(containerName, filePath, blobContent);

let service: AzureBlobStorageService;
const token = "myToken";
Expand Down Expand Up @@ -130,6 +134,14 @@ describe("Azure Blob Storage Service", () => {
expect(ContainerURL.fromServiceURL).toBeCalledWith(expect.anything(), newContainerName);
expect(ContainerURL.prototype.create).toBeCalledWith(Aborter.none);
});

it("should not create a container if it exists already", async () => {
ContainerURL.fromServiceURL = jest.fn(() => new ContainerURL(null, null));
ContainerURL.prototype.create = jest.fn(() => Promise.resolve({ statusCode: 201 })) as any;
await service.createContainerIfNotExists("container1");
expect(ContainerURL.fromServiceURL).not.toBeCalled();
expect(ContainerURL.prototype.create).not.toBeCalled
})

it("should delete a container", async () => {
const containerToDelete = "delete container";
Expand All @@ -139,4 +151,45 @@ describe("Azure Blob Storage Service", () => {
expect(ContainerURL.fromServiceURL).toBeCalledWith(expect.anything(), containerToDelete);
expect(ContainerURL.prototype.delete).toBeCalledWith(Aborter.none);
});

it("should download a binary file", async () => {
const targetPath = "";
await service.downloadBinary(containerName, fileName, targetPath);
const buffer = Buffer.alloc(blobContent.length)
expect(downloadBlobToBuffer).toBeCalledWith(
Aborter.timeout(30 * 60 * 1000),
buffer,
blockBlobUrl,
0,
undefined,
{
blockSize: 4 * 1024 * 1024, // 4MB block size
parallelism: 20, // 20 concurrency
}
);
expect(fs.writeFileSync).toBeCalledWith(
targetPath,
buffer,
"binary"
)
});

it("should generate a SAS url", async () => {
const sasToken = "myToken"
BlobSASPermissions.parse = jest.fn(() => {
return {
toString: jest.fn()
}
}) as any;
(generateBlobSASQueryParameters as any).mockReturnValue(token);
const url = await service.generateBlobSasTokenUrl(containerName, fileName);
expect(generateBlobSASQueryParameters).toBeCalled();
expect(url).toEqual(`${blockBlobUrl.url}?${sasToken}`)
});

it("should throw an error when trying to get a SAS Token with Token Auth", async () => {
const newService = new AzureBlobStorageService(sls, options, AzureStorageAuthType.Token);
await newService.initialize();
await expect(newService.generateBlobSasTokenUrl(containerName, fileName)).rejects.not.toBeNull();
})
});
39 changes: 37 additions & 2 deletions src/services/azureBlobStorageService.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import { StorageAccounts, StorageManagementClientContext } from "@azure/arm-storage";
import { Aborter, BlobSASPermissions, BlockBlobURL, ContainerURL, generateBlobSASQueryParameters,SASProtocol,
ServiceURL, SharedKeyCredential, StorageURL, TokenCredential, uploadFileToBlockBlob } from "@azure/storage-blob";
import { Aborter, BlobSASPermissions, BlockBlobURL, ContainerURL,
generateBlobSASQueryParameters, SASProtocol, ServiceURL, SharedKeyCredential,
StorageURL, TokenCredential, uploadFileToBlockBlob, downloadBlobToBuffer } from "@azure/storage-blob";
import fs from "fs";
import Serverless from "serverless";
import { Guard } from "../shared/guard";
import { BaseService } from "./baseService";
import { AzureLoginService } from "./loginService";

/**
* Type of authentication with Azure Storage
* @member SharedKey - Retrieve and use a Shared Key for Azure Blob BStorage
* @member Token - Retrieve and use an Access Token to authenticate with Azure Blob Storage
*/
export enum AzureStorageAuthType {
SharedKey,
Token
Expand Down Expand Up @@ -35,6 +42,9 @@ export class AzureBlobStorageService extends BaseService {
* to perform any operation with the service
*/
public async initialize() {
if (this.storageCredential) {
return;
}
this.storageCredential = (this.authType === AzureStorageAuthType.SharedKey)
?
new SharedKeyCredential(this.storageAccountName, await this.getKey())
Expand All @@ -59,6 +69,31 @@ export class AzureBlobStorageService extends BaseService {
await uploadFileToBlockBlob(Aborter.none, path, this.getBlockBlobURL(containerName, name));
this.log("Finished uploading blob");
};

/**
* Download blob to file
* https://github.com/Azure/azure-storage-js/blob/master/blob/samples/highlevel.sample.js#L82-L97
* @param containerName Container containing blob to download
* @param blobName Blob to download
* @param targetPath Path to which blob will be downloaded
*/
public async downloadBinary(containerName: string, blobName: string, targetPath: string) {
const blockBlobUrl = this.getBlockBlobURL(containerName, blobName);
const props = await blockBlobUrl.getProperties(Aborter.none);
const buffer = Buffer.alloc(props.contentLength);
await downloadBlobToBuffer(
Aborter.timeout(30 * 60 * 1000),
buffer,
blockBlobUrl,
0,
undefined,
{
blockSize: 4 * 1024 * 1024, // 4MB block size
parallelism: 20, // 20 concurrency
}
);
fs.writeFileSync(targetPath, buffer, "binary");
}

/**
* Delete a blob from Azure Blob Storage
Expand Down
Loading