Skip to content

Commit

Permalink
feat: List deployments for rollback when no timestamp given (#208)
Browse files Browse the repository at this point in the history
Rollback requires a timestamp. Rather than forcing users to run `sls deploy list` to generate the deployments they can choose from, `sls rollback` with no args also generates the same list.

Resolves [AB#493]
  • Loading branch information
tbarlow12 authored Jul 22, 2019
1 parent 2b97f75 commit 9b7bb74
Show file tree
Hide file tree
Showing 6 changed files with 92 additions and 99 deletions.
74 changes: 5 additions & 69 deletions src/plugins/deploy/azureDeployPlugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,75 +57,11 @@ describe("Deploy plugin", () => {
expect(uploadFunctions).toBeCalledWith(functionAppStub);
});

it("does not call deploy if zip does not exist", async () => {
const deployResourceGroup = jest.fn();
const functionAppStub: Site = MockFactory.createTestSite();
const deploy = jest.fn(() => Promise.resolve(functionAppStub));
const uploadFunctions = jest.fn();

const zipFile = "fake.zip";

FunctionAppService.prototype.getFunctionZipFile = (() => zipFile);
ResourceService.prototype.deployResourceGroup = deployResourceGroup;
FunctionAppService.prototype.deploy = deploy;
FunctionAppService.prototype.uploadFunctions = uploadFunctions;

await invokeHook(plugin, "deploy:deploy");

expect(deployResourceGroup).not.toBeCalled();
expect(deploy).not.toBeCalled();
expect(uploadFunctions).not.toBeCalled();
expect(sls.cli.log).lastCalledWith(`Function app zip file '${zipFile}' does not exist`);
});

it("lists deployments with timestamps", async () => {
const deployments = MockFactory.createTestDeployments(5, true);
ResourceService.prototype.getDeployments = jest.fn(() => Promise.resolve(deployments));

await invokeHook(plugin, "deploy:list:list");
expect(ResourceService.prototype.getDeployments).toBeCalled();

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 () => {
const deployments = MockFactory.createTestDeployments();
ResourceService.prototype.getDeployments = jest.fn(() => Promise.resolve(deployments));

it("lists deployments", async () => {
const deploymentString = "deployments";
ResourceService.prototype.listDeployments = jest.fn(() => Promise.resolve(deploymentString));
await invokeHook(plugin, "deploy:list:list");
expect(ResourceService.prototype.getDeployments).toBeCalled();

let expectedLogStatement = "\n\nDeployments";
for (const dep of deployments) {
expectedLogStatement += "\n-----------\n"
expectedLogStatement += `Name: ${dep.name}\n`
expectedLogStatement += "Timestamp: None\n";
expectedLogStatement += "Datetime: None\n"
}
expectedLogStatement += "-----------\n"
expect(sls.cli.log).lastCalledWith(expectedLogStatement);
});

it("logs empty deployment list", async () => {
const resourceGroup = "rg1";
ResourceService.prototype.getDeployments = jest.fn(() => Promise.resolve([])) as any;
ResourceService.prototype.getResourceGroupName = jest.fn(() => resourceGroup);

await invokeHook(plugin, "deploy:list:list");
expect(ResourceService.prototype.getDeployments).toBeCalled();

expect(sls.cli.log).lastCalledWith(`No deployments found for resource group '${resourceGroup}'`);
expect(ResourceService.prototype.listDeployments).toBeCalled();
expect(sls.cli.log).lastCalledWith(deploymentString);
});
});
21 changes: 1 addition & 20 deletions src/plugins/deploy/azureDeployPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import Serverless from "serverless";
import { FunctionAppService } from "../../services/functionAppService";
import { AzureLoginOptions } from "../../services/loginService";
import { ResourceService } from "../../services/resourceService";
import { Utils } from "../../shared/utils";
import { AzureBasePlugin } from "../azureBasePlugin";

export class AzureDeployPlugin extends AzureBasePlugin<AzureLoginOptions> {
Expand Down Expand Up @@ -49,25 +48,7 @@ export class AzureDeployPlugin extends AzureBasePlugin<AzureLoginOptions> {
private async list() {
this.log("Listing deployments");
const resourceService = new ResourceService(this.serverless, this.options);
const deployments = await resourceService.getDeployments();
if (!deployments || deployments.length === 0) {
this.log(`No deployments found for resource group '${resourceService.getResourceGroupName()}'`);
return;
}
let stringDeployments = "\n\nDeployments";

for (const dep of deployments) {
stringDeployments += "\n-----------\n"
stringDeployments += `Name: ${dep.name}\n`
const timestampFromName = Utils.getTimestampFromName(dep.name);
stringDeployments += `Timestamp: ${(timestampFromName) ? timestampFromName : "None"}\n`;

const dateTime = timestampFromName ? new Date(+timestampFromName).toISOString() : "None";
stringDeployments += `Datetime: ${dateTime}\n`
}

stringDeployments += "-----------\n"
this.log(stringDeployments);
this.log(await resourceService.listDeployments());
}

private async deploy() {
Expand Down
60 changes: 54 additions & 6 deletions src/services/resourceService.test.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import { DeploymentsListByResourceGroupResponse } from "@azure/arm-resources/esm/models";
import { Utils } from "../shared/utils";
import { MockFactory } from "../test/mockFactory";
import { ResourceService } from "./resourceService";


jest.mock("@azure/arm-resources")
import { ResourceManagementClient } from "@azure/arm-resources";
import { Utils } from "../shared/utils";

describe("Resource Service", () => {
const deployments = MockFactory.createTestDeployments();
let deployments: DeploymentsListByResourceGroupResponse;
const template = "myTemplate";

beforeAll(() => {
beforeEach(() => {
deployments = MockFactory.createTestDeployments(5, true);
ResourceManagementClient.prototype.resourceGroups = {
createOrUpdate: jest.fn(),
deleteMethod: jest.fn(),
Expand Down Expand Up @@ -80,7 +81,7 @@ describe("Resource Service", () => {
.toBeCalledWith(resourceGroup);
});

it("lists deployments", async () => {
it("gets deployments", async () => {
const sls = MockFactory.createTestServerless();
const resourceGroup = "myResourceGroup";
sls.service.provider["resourceGroup"] = resourceGroup
Expand All @@ -91,6 +92,53 @@ describe("Resource Service", () => {
expect(deps).toEqual(deployments);
});

it("lists deployments as string with timestamps", async () => {
const sls = MockFactory.createTestServerless();
const resourceGroup = "myResourceGroup";
sls.service.provider["resourceGroup"] = resourceGroup
sls.variables["azureCredentials"] = "fake credentials"
const options = MockFactory.createTestServerlessOptions();
const service = new ResourceService(sls, options);
const deploymentString = await service.listDeployments();
let expectedDeploymentString = "\n\nDeployments";
const originalTimestamp = +MockFactory.createTestTimestamp();
let i = 0
for (const dep of deployments) {
const timestamp = originalTimestamp + i
expectedDeploymentString += "\n-----------\n"
expectedDeploymentString += `Name: ${dep.name}\n`
expectedDeploymentString += `Timestamp: ${timestamp}\n`;
expectedDeploymentString += `Datetime: ${new Date(timestamp).toISOString()}\n`
i++
}
expectedDeploymentString += "-----------\n"
expect(deploymentString).toEqual(expectedDeploymentString);
});

it("lists deployments as string without timestamps", async () => {
deployments = MockFactory.createTestDeployments();
ResourceManagementClient.prototype.deployments = {
listByResourceGroup: jest.fn(() => Promise.resolve(deployments)),
} as any;

const sls = MockFactory.createTestServerless();
const resourceGroup = "myResourceGroup";
sls.service.provider["resourceGroup"] = resourceGroup
sls.variables["azureCredentials"] = "fake credentials"
const options = MockFactory.createTestServerlessOptions();
const service = new ResourceService(sls, options);
const deploymentString = await service.listDeployments();
let expectedDeploymentString = "\n\nDeployments";
for (const dep of deployments) {
expectedDeploymentString += "\n-----------\n"
expectedDeploymentString += `Name: ${dep.name}\n`
expectedDeploymentString += "Timestamp: None\n";
expectedDeploymentString += "Datetime: None\n"
}
expectedDeploymentString += "-----------\n"
expect(deploymentString).toEqual(expectedDeploymentString);
});

it("gets deployment template",async () => {
const sls = MockFactory.createTestServerless();
const resourceGroup = "myResourceGroup";
Expand All @@ -107,4 +155,4 @@ describe("Resource Service", () => {
);
expect(result).toEqual(template);
});
});
});
27 changes: 26 additions & 1 deletion src/services/resourceService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,31 @@ export class ResourceService extends BaseService {
return await this.resourceClient.deployments.listByResourceGroup(this.resourceGroup);
}

/**
* Returns stringified list of deployments with timestamps
*/
public async listDeployments(): Promise<string> {
const deployments = await this.getDeployments()
if (!deployments || deployments.length === 0) {
this.log(`No deployments found for resource group '${this.getResourceGroupName()}'`);
return;
}
let stringDeployments = "\n\nDeployments";

for (const dep of deployments) {
stringDeployments += "\n-----------\n"
stringDeployments += `Name: ${dep.name}\n`
const timestampFromName = Utils.getTimestampFromName(dep.name);
stringDeployments += `Timestamp: ${(timestampFromName) ? timestampFromName : "None"}\n`;

const dateTime = timestampFromName ? new Date(+timestampFromName).toISOString() : "None";
stringDeployments += `Datetime: ${dateTime}\n`
}

stringDeployments += "-----------\n"
return stringDeployments
}

/**
* Get ARM template for previous deployment
* @param deploymentName Name of deployment
Expand All @@ -45,4 +70,4 @@ export class ResourceService extends BaseService {
this.log(`Deleting resource group: ${this.resourceGroup}`);
return await this.resourceClient.resourceGroups.deleteMethod(this.resourceGroup);
}
}
}
5 changes: 3 additions & 2 deletions src/services/rollbackService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ describe("Rollback Service", () => {
const artifactName = MockFactory.createTestDeployment().name.replace("deployment", "artifact") + ".zip";
const artifactPath = `.serverless${path.sep}${artifactName}`
const armDeployment: ArmDeployment = { template, parameters };
const deploymentString = "deployments";

function createOptions(timestamp?: string): Serverless.Options {
return {
Expand Down Expand Up @@ -58,6 +59,7 @@ describe("Rollback Service", () => {
ResourceService.prototype.getDeploymentTemplate = jest.fn(
() => Promise.resolve({ template })
) as any;
ResourceService.prototype.listDeployments = jest.fn(() => Promise.resolve(deploymentString))
AzureBlobStorageService.prototype.generateBlobSasTokenUrl = jest.fn(() => sasURL) as any;
FunctionAppService.prototype.get = jest.fn(() => appStub) as any;
});
Expand All @@ -72,8 +74,7 @@ describe("Rollback Service", () => {
const options = {} as any;
const service = createService(sls, options);
await service.rollback();
const calls = (sls.cli.log as any).mock.calls;
expect(calls[0][0]).toEqual("Need to specify a timestamp for rollback. Run `sls deploy list` to see timestamps of deployments");
expect(sls.cli.log).lastCalledWith(deploymentString);
});

it("should return early with invalid timestamp", async () => {
Expand Down
4 changes: 3 additions & 1 deletion src/services/rollbackService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,12 +102,14 @@ export class RollbackService extends BaseService {

/**
* Get deployment specified by timestamp in Serverless options
* Lists deployments if no timestamp is provided
*/
private async getDeployment(): Promise<DeploymentExtended> {
let timestamp = Utils.get(this.options, "timestamp");
if (!timestamp) {
this.log("Need to specify a timestamp for rollback. Run `sls deploy list` to see timestamps of deployments");
this.log("Need to specify a timestamp for rollback.");
this.log("Example usage:\n\nsls rollback -t 1562014362");
this.log(await this.resourceService.listDeployments());
return null;
}
const deployments = await this.getArmDeploymentsByTimestamp();
Expand Down

0 comments on commit 9b7bb74

Please sign in to comment.