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
10 changes: 10 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"@azure/arm-apimanagement": "^5.1.0",
"@azure/arm-appservice": "^5.7.0",
"@azure/arm-resources": "^1.0.1",
"@azure/arm-storage": "^9.0.1",
"@azure/ms-rest-nodeauth": "^1.0.1",
"@azure/storage-blob": "^10.3.0",
"axios": "^0.18.0",
Expand Down
2 changes: 1 addition & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const configConstants = {
logStreamApiPath: "/api/logstream/application/functions/function/",
masterKeyApiPath: "/api/functions/admin/masterkey",
providerName: "azure",
rollbackEnabled: false,
rollbackEnabled: true,
scmCommandApiPath: "/api/command",
scmDomain: ".scm.azurewebsites.net",
scmVfsPath: "/api/vfs/site/wwwroot/",
Expand Down
1 change: 0 additions & 1 deletion src/plugins/deploy/azureDeployPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ export class AzureDeployPlugin {
await resourceService.deployResourceGroup();

const functionAppService = new FunctionAppService(this.serverless, this.options);
await functionAppService.initialize();

const functionApp = await functionAppService.deploy();
await functionAppService.uploadFunctions(functionApp);
Expand Down
6 changes: 5 additions & 1 deletion src/services/armService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ describe("Arm Service", () => {

const expectedResourceGroup = sls.service.provider["resourceGroup"];
const expectedDeploymentName = sls.service.provider["deploymentName"] || `${this.resourceGroup}-deployment`;
const expectedDeploymentNameRegex = new RegExp(expectedDeploymentName + "-t([0-9]+)")
const expectedDeployment: Deployment = {
properties: {
mode: "Incremental",
Expand All @@ -190,7 +191,10 @@ describe("Arm Service", () => {
},
};

expect(Deployments.prototype.createOrUpdate).toBeCalledWith(expectedResourceGroup, expectedDeploymentName, expectedDeployment);
const call = (Deployments.prototype.createOrUpdate as any).mock.calls[0];
expect(call[0]).toEqual(expectedResourceGroup);
expect(call[1]).toMatch(expectedDeploymentNameRegex);
expect(call[2]).toEqual(expectedDeployment);
});
});
});
43 changes: 35 additions & 8 deletions src/services/azureBlobStorageService.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import mockFs from "mock-fs";
import { MockFactory } from "../test/mockFactory";
import { AzureBlobStorageService } from "./azureBlobStorageService";
import { AzureBlobStorageService, AzureStorageAuthType } from "./azureBlobStorageService";

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

jest.mock("@azure/arm-storage")
jest.genMockFromModule("@azure/arm-storage");
import { StorageAccounts, StorageManagementClientContext } from "@azure/arm-storage"

jest.mock("./loginService");
import { AzureLoginService } from "./loginService"
Expand All @@ -18,16 +22,27 @@ describe("Azure Blob Storage Service", () => {

const containers = MockFactory.createTestAzureContainers();
const sls = MockFactory.createTestServerless();
const accountName = "slswesdevservicenamesa";
const options = MockFactory.createTestServerlessOptions();
const blockBlobUrl = MockFactory.createTestBlockBlobUrl(containerName, filePath);

let service: AzureBlobStorageService;
const token = "myToken";
const keyValue = "keyValue";

beforeAll(() => {
(TokenCredential as any).mockImplementation((token: string) => {
token
});
(SharedKeyCredential as any).mockImplementation();
(TokenCredential as any).mockImplementation();

StorageAccounts.prototype.listKeys = jest.fn(() => {
return {
keys: [
{
value: keyValue
}
]
}
}) as any;

BlockBlobURL.fromContainerURL = jest.fn(() => blockBlobUrl) as any;
AzureLoginService.login = jest.fn(() => Promise.resolve({
Expand All @@ -51,8 +66,20 @@ describe("Azure Blob Storage Service", () => {
mockFs.restore();
});

beforeEach(() => {
beforeEach( async () => {
service = new AzureBlobStorageService(sls, options);
await service.initialize();
});

it("should initialize authentication", async () => {
// Note: initialize called in beforeEach
expect(SharedKeyCredential).toBeCalledWith(accountName, keyValue);
expect(StorageManagementClientContext).toBeCalled();
expect(StorageAccounts).toBeCalled();

const tokenService = new AzureBlobStorageService(sls, options, AzureStorageAuthType.Token);
await tokenService.initialize();
expect(TokenCredential).toBeCalled();
});

it("should initialize authentication", async () => {
Expand All @@ -61,7 +88,7 @@ describe("Azure Blob Storage Service", () => {
});

it("should upload a file", async () => {
uploadFileToBlockBlob.prototype = jest.fn();
uploadFileToBlockBlob.prototype = jest.fn(() => Promise.resolve());
ContainerURL.fromServiceURL = jest.fn((serviceUrl, containerName) => (containerName as any));
await service.uploadFile(filePath, containerName);
expect(uploadFileToBlockBlob).toBeCalledWith(
Expand Down Expand Up @@ -99,7 +126,7 @@ describe("Azure Blob Storage Service", () => {
ContainerURL.fromServiceURL = jest.fn(() => new ContainerURL(null, null));
ContainerURL.prototype.create = jest.fn(() => Promise.resolve({ statusCode: 201 })) as any;
const newContainerName = "newContainer";
await service.createContainer(newContainerName);
await service.createContainerIfNotExists(newContainerName);
expect(ContainerURL.fromServiceURL).toBeCalledWith(expect.anything(), newContainerName);
expect(ContainerURL.prototype.create).toBeCalledWith(Aborter.none);
});
Expand Down
136 changes: 119 additions & 17 deletions src/services/azureBlobStorageService.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import { Aborter, BlockBlobURL, ContainerURL, Credential, ServiceURL, StorageURL, TokenCredential, uploadFileToBlockBlob } from "@azure/storage-blob";
import { StorageAccounts, StorageManagementClientContext } from "@azure/arm-storage";
import { Aborter, BlobSASPermissions, BlockBlobURL, ContainerURL, generateBlobSASQueryParameters,SASProtocol,
ServiceURL, SharedKeyCredential, StorageURL, TokenCredential, uploadFileToBlockBlob } from "@azure/storage-blob";
import Serverless from "serverless";
import { Guard } from "../shared/guard";
import { BaseService } from "./baseService";
import { AzureLoginService } from "./loginService";

export enum AzureStorageAuthType {
SharedKey,
Token
}

/**
* Wrapper for operations on Azure Blob Storage account
*/
Expand All @@ -13,15 +20,26 @@ export class AzureBlobStorageService extends BaseService {
* Account URL for Azure Blob Storage account. Depends on `storageAccountName` being set in baseService
*/
private accountUrl: string;
private storageCredential: Credential;
private authType: AzureStorageAuthType;
private storageCredential: SharedKeyCredential|TokenCredential;

public constructor(serverless: Serverless, options: Serverless.Options) {
public constructor(serverless: Serverless, options: Serverless.Options,
authType: AzureStorageAuthType = AzureStorageAuthType.SharedKey) {
super(serverless, options);
this.accountUrl = `https://${this.storageAccountName}.blob.core.windows.net`;
this.authType = authType;
}

/**
* Initialize Blob Storage service. This creates the credentials required
* to perform any operation with the service
*/
public async initialize() {
this.storageCredential = new TokenCredential(await this.getToken());
this.storageCredential = (this.authType === AzureStorageAuthType.SharedKey)
?
new SharedKeyCredential(this.storageAccountName, await this.getKey())
:
new TokenCredential(await this.getToken());
}

/**
Expand All @@ -31,11 +49,15 @@ export class AzureBlobStorageService extends BaseService {
* @param blobName Name of blob file created as a result of upload
*/
public async uploadFile(path: string, containerName: string, blobName?: string) {
Guard.empty(path);
Guard.empty(containerName);
Guard.empty(path, "path");
Guard.empty(containerName, "containerName");
this.checkInitialization();

// Use specified blob name or replace `/` in path with `-`
const name = blobName || path.replace(/^.*[\\\/]/, "-");
uploadFileToBlockBlob(Aborter.none, path, this.getBlockBlobURL(containerName, name));
this.log(`Uploading file at '${path}' to container '${containerName}' with name '${name}'`)
await uploadFileToBlockBlob(Aborter.none, path, this.getBlockBlobURL(containerName, name));
this.log("Finished uploading blob");
};

/**
Expand All @@ -44,8 +66,10 @@ export class AzureBlobStorageService extends BaseService {
* @param blobName Blob to delete
*/
public async deleteFile(containerName: string, blobName: string): Promise<void> {
Guard.empty(containerName);
Guard.empty(blobName);
Guard.empty(containerName, "containerName");
Guard.empty(blobName, "blobName");
this.checkInitialization();

const blockBlobUrl = await this.getBlockBlobURL(containerName, blobName)
await blockBlobUrl.delete(Aborter.none);
}
Expand All @@ -57,6 +81,8 @@ export class AzureBlobStorageService extends BaseService {
*/
public async listFiles(containerName: string, ext?: string): Promise<string[]> {
Guard.empty(containerName, "containerName");
this.checkInitialization();

const result: string[] = [];
let marker;
const containerURL = this.getContainerURL(containerName);
Expand All @@ -80,6 +106,8 @@ export class AzureBlobStorageService extends BaseService {
* Lists the containers within the Azure Blob Storage account
*/
public async listContainers() {
this.checkInitialization();

const result: string[] = [];
let marker;
do {
Expand All @@ -100,26 +128,73 @@ export class AzureBlobStorageService extends BaseService {
* Creates container specified in Azure Cloud Storage options
* @param containerName - Name of container to create
*/
public async createContainer(containerName: string): Promise<void> {
Guard.empty(containerName);
const containerURL = this.getContainerURL(containerName);
await containerURL.create(Aborter.none);
public async createContainerIfNotExists(containerName: string): Promise<void> {
Guard.empty(containerName, "containerName");
this.checkInitialization();

const containers = await this.listContainers();
if (!containers.find((name) => name === containerName)) {
const containerURL = this.getContainerURL(containerName);
await containerURL.create(Aborter.none);
}
}

/**
* Delete a container from Azure Blob Storage Account
* @param containerName Name of container to delete
*/
public async deleteContainer(containerName: string): Promise<void> {
Guard.empty(containerName);
Guard.empty(containerName, "containerName");
this.checkInitialization();

const containerUrl = await this.getContainerURL(containerName)
await containerUrl.delete(Aborter.none);
}

/**
* Generate URL with SAS token for a specific blob
* @param containerName Name of container containing blob
* @param blobName Name of blob to generate SAS token for
* @param days Number of days from current date until expiry of SAS token. Defaults to 1 year
*/
public async generateBlobSasTokenUrl(containerName: string, blobName: string, days: number = 365): Promise<string> {
this.checkInitialization();
if (this.authType !== AzureStorageAuthType.SharedKey) {
throw new Error("Need to authenticate with shared key in order to generate SAS tokens. " +
"Initialize Blob Service with SharedKey auth type");
}

const now = new Date();
const endDate = new Date(now);
endDate.setDate(endDate.getDate() + days);

const blobSas = generateBlobSASQueryParameters({
blobName,
cacheControl: "cache-control-override",
containerName,
contentDisposition: "content-disposition-override",
contentEncoding: "content-encoding-override",
contentLanguage: "content-language-override",
contentType: "content-type-override",
expiryTime: endDate,
ipRange: { start: "0.0.0.0", end: "255.255.255.255" },
permissions: BlobSASPermissions.parse("racwd").toString(),
protocol: SASProtocol.HTTPSandHTTP,
startTime: now,
version: "2016-05-31"
},
this.storageCredential as SharedKeyCredential);

const blobUrl = this.getBlockBlobURL(containerName, blobName);
return `${blobUrl.url}?${blobSas}`
}

/**
* Get ServiceURL object for Azure Blob Storage Account
*/
private getServiceURL(): ServiceURL {
this.checkInitialization();

const pipeline = StorageURL.newPipeline(this.storageCredential);
const accountUrl = this.accountUrl;
const serviceUrl = new ServiceURL(
Expand All @@ -135,7 +210,9 @@ export class AzureBlobStorageService extends BaseService {
* @param serviceURL Previously created ServiceURL object (will create if undefined)
*/
private getContainerURL(containerName: string): ContainerURL {
Guard.empty(containerName);
Guard.empty(containerName, "containerName");
this.checkInitialization();

return ContainerURL.fromServiceURL(
this.getServiceURL(),
containerName
Expand All @@ -148,19 +225,44 @@ export class AzureBlobStorageService extends BaseService {
* @param blobName Name of blob
*/
private getBlockBlobURL(containerName: string, blobName: string): BlockBlobURL {
Guard.empty(containerName);
Guard.empty(blobName);
Guard.empty(containerName, "containerName");
Guard.empty(blobName, "blobName");
this.checkInitialization();

return BlockBlobURL.fromContainerURL(
this.getContainerURL(containerName),
blobName,
);
}

/**
* Get access token by logging in (again) with a storage-specific context
*/
private async getToken(): Promise<string> {
const authResponse = await AzureLoginService.login({
tokenAudience: "https://storage.azure.com/"
});
const token = await authResponse.credentials.getToken();
return token.accessToken;
}

/**
* Get access key for storage account
*/
private async getKey(): Promise<string> {
const context = new StorageManagementClientContext(this.credentials, this.subscriptionId)
const storageAccounts = new StorageAccounts(context);
const keys = await storageAccounts.listKeys(this.resourceGroup, this.storageAccountName);
return keys.keys[0].value;
}

/**
* Ensure that the blob storage service has been initialized. If not initialized,
* the credentials will not be available for any operation
*/
private checkInitialization() {
Guard.null(this.storageCredential, "storageCredential",
"Azure Blob Storage Service has not been initialized. Make sure .initialize() has been called " +
"before performing any operation");
}
}
3 changes: 2 additions & 1 deletion src/services/baseService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ describe("Base Service", () => {
expect(props.subscriptionId).toEqual(sls.variables["subscriptionId"]);
expect(props.serviceName).toEqual(slsConfig.service);
expect(props.resourceGroup).toEqual(slsConfig.provider.resourceGroup);
expect(props.deploymentName).toEqual(slsConfig.provider.deploymentName);
const expectedDeploymentNameRegex = new RegExp(slsConfig.provider.deploymentName + "-t([0-9]+)")
expect(props.deploymentName).toMatch(expectedDeploymentNameRegex);
});

it("Sets default region and stage values if not defined", () => {
Expand Down
Loading