Skip to content
This repository was archived by the owner on Dec 9, 2024. It is now read-only.

Commit 9b7bb74

Browse files
authored
feat: List deployments for rollback when no timestamp given (#208)
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]
1 parent 2b97f75 commit 9b7bb74

File tree

6 files changed

+92
-99
lines changed

6 files changed

+92
-99
lines changed

src/plugins/deploy/azureDeployPlugin.test.ts

Lines changed: 5 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -57,75 +57,11 @@ describe("Deploy plugin", () => {
5757
expect(uploadFunctions).toBeCalledWith(functionAppStub);
5858
});
5959

60-
it("does not call deploy if zip does not exist", async () => {
61-
const deployResourceGroup = jest.fn();
62-
const functionAppStub: Site = MockFactory.createTestSite();
63-
const deploy = jest.fn(() => Promise.resolve(functionAppStub));
64-
const uploadFunctions = jest.fn();
65-
66-
const zipFile = "fake.zip";
67-
68-
FunctionAppService.prototype.getFunctionZipFile = (() => zipFile);
69-
ResourceService.prototype.deployResourceGroup = deployResourceGroup;
70-
FunctionAppService.prototype.deploy = deploy;
71-
FunctionAppService.prototype.uploadFunctions = uploadFunctions;
72-
73-
await invokeHook(plugin, "deploy:deploy");
74-
75-
expect(deployResourceGroup).not.toBeCalled();
76-
expect(deploy).not.toBeCalled();
77-
expect(uploadFunctions).not.toBeCalled();
78-
expect(sls.cli.log).lastCalledWith(`Function app zip file '${zipFile}' does not exist`);
79-
});
80-
81-
it("lists deployments with timestamps", async () => {
82-
const deployments = MockFactory.createTestDeployments(5, true);
83-
ResourceService.prototype.getDeployments = jest.fn(() => Promise.resolve(deployments));
84-
85-
await invokeHook(plugin, "deploy:list:list");
86-
expect(ResourceService.prototype.getDeployments).toBeCalled();
87-
88-
let expectedLogStatement = "\n\nDeployments";
89-
const originalTimestamp = +MockFactory.createTestTimestamp();
90-
let i = 0
91-
for (const dep of deployments) {
92-
const timestamp = originalTimestamp + i
93-
expectedLogStatement += "\n-----------\n"
94-
expectedLogStatement += `Name: ${dep.name}\n`
95-
expectedLogStatement += `Timestamp: ${timestamp}\n`;
96-
expectedLogStatement += `Datetime: ${new Date(timestamp).toISOString()}\n`
97-
i++
98-
}
99-
expectedLogStatement += "-----------\n"
100-
expect(sls.cli.log).lastCalledWith(expectedLogStatement);
101-
});
102-
103-
it("lists deployments without timestamps", async () => {
104-
const deployments = MockFactory.createTestDeployments();
105-
ResourceService.prototype.getDeployments = jest.fn(() => Promise.resolve(deployments));
106-
60+
it("lists deployments", async () => {
61+
const deploymentString = "deployments";
62+
ResourceService.prototype.listDeployments = jest.fn(() => Promise.resolve(deploymentString));
10763
await invokeHook(plugin, "deploy:list:list");
108-
expect(ResourceService.prototype.getDeployments).toBeCalled();
109-
110-
let expectedLogStatement = "\n\nDeployments";
111-
for (const dep of deployments) {
112-
expectedLogStatement += "\n-----------\n"
113-
expectedLogStatement += `Name: ${dep.name}\n`
114-
expectedLogStatement += "Timestamp: None\n";
115-
expectedLogStatement += "Datetime: None\n"
116-
}
117-
expectedLogStatement += "-----------\n"
118-
expect(sls.cli.log).lastCalledWith(expectedLogStatement);
119-
});
120-
121-
it("logs empty deployment list", async () => {
122-
const resourceGroup = "rg1";
123-
ResourceService.prototype.getDeployments = jest.fn(() => Promise.resolve([])) as any;
124-
ResourceService.prototype.getResourceGroupName = jest.fn(() => resourceGroup);
125-
126-
await invokeHook(plugin, "deploy:list:list");
127-
expect(ResourceService.prototype.getDeployments).toBeCalled();
128-
129-
expect(sls.cli.log).lastCalledWith(`No deployments found for resource group '${resourceGroup}'`);
64+
expect(ResourceService.prototype.listDeployments).toBeCalled();
65+
expect(sls.cli.log).lastCalledWith(deploymentString);
13066
});
13167
});

src/plugins/deploy/azureDeployPlugin.ts

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import Serverless from "serverless";
33
import { FunctionAppService } from "../../services/functionAppService";
44
import { AzureLoginOptions } from "../../services/loginService";
55
import { ResourceService } from "../../services/resourceService";
6-
import { Utils } from "../../shared/utils";
76
import { AzureBasePlugin } from "../azureBasePlugin";
87

98
export class AzureDeployPlugin extends AzureBasePlugin<AzureLoginOptions> {
@@ -49,25 +48,7 @@ export class AzureDeployPlugin extends AzureBasePlugin<AzureLoginOptions> {
4948
private async list() {
5049
this.log("Listing deployments");
5150
const resourceService = new ResourceService(this.serverless, this.options);
52-
const deployments = await resourceService.getDeployments();
53-
if (!deployments || deployments.length === 0) {
54-
this.log(`No deployments found for resource group '${resourceService.getResourceGroupName()}'`);
55-
return;
56-
}
57-
let stringDeployments = "\n\nDeployments";
58-
59-
for (const dep of deployments) {
60-
stringDeployments += "\n-----------\n"
61-
stringDeployments += `Name: ${dep.name}\n`
62-
const timestampFromName = Utils.getTimestampFromName(dep.name);
63-
stringDeployments += `Timestamp: ${(timestampFromName) ? timestampFromName : "None"}\n`;
64-
65-
const dateTime = timestampFromName ? new Date(+timestampFromName).toISOString() : "None";
66-
stringDeployments += `Datetime: ${dateTime}\n`
67-
}
68-
69-
stringDeployments += "-----------\n"
70-
this.log(stringDeployments);
51+
this.log(await resourceService.listDeployments());
7152
}
7253

7354
private async deploy() {

src/services/resourceService.test.ts

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
1+
import { DeploymentsListByResourceGroupResponse } from "@azure/arm-resources/esm/models";
2+
import { Utils } from "../shared/utils";
13
import { MockFactory } from "../test/mockFactory";
24
import { ResourceService } from "./resourceService";
35

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

99
describe("Resource Service", () => {
10-
const deployments = MockFactory.createTestDeployments();
10+
let deployments: DeploymentsListByResourceGroupResponse;
1111
const template = "myTemplate";
1212

13-
beforeAll(() => {
13+
beforeEach(() => {
14+
deployments = MockFactory.createTestDeployments(5, true);
1415
ResourceManagementClient.prototype.resourceGroups = {
1516
createOrUpdate: jest.fn(),
1617
deleteMethod: jest.fn(),
@@ -80,7 +81,7 @@ describe("Resource Service", () => {
8081
.toBeCalledWith(resourceGroup);
8182
});
8283

83-
it("lists deployments", async () => {
84+
it("gets deployments", async () => {
8485
const sls = MockFactory.createTestServerless();
8586
const resourceGroup = "myResourceGroup";
8687
sls.service.provider["resourceGroup"] = resourceGroup
@@ -91,6 +92,53 @@ describe("Resource Service", () => {
9192
expect(deps).toEqual(deployments);
9293
});
9394

95+
it("lists deployments as string with timestamps", async () => {
96+
const sls = MockFactory.createTestServerless();
97+
const resourceGroup = "myResourceGroup";
98+
sls.service.provider["resourceGroup"] = resourceGroup
99+
sls.variables["azureCredentials"] = "fake credentials"
100+
const options = MockFactory.createTestServerlessOptions();
101+
const service = new ResourceService(sls, options);
102+
const deploymentString = await service.listDeployments();
103+
let expectedDeploymentString = "\n\nDeployments";
104+
const originalTimestamp = +MockFactory.createTestTimestamp();
105+
let i = 0
106+
for (const dep of deployments) {
107+
const timestamp = originalTimestamp + i
108+
expectedDeploymentString += "\n-----------\n"
109+
expectedDeploymentString += `Name: ${dep.name}\n`
110+
expectedDeploymentString += `Timestamp: ${timestamp}\n`;
111+
expectedDeploymentString += `Datetime: ${new Date(timestamp).toISOString()}\n`
112+
i++
113+
}
114+
expectedDeploymentString += "-----------\n"
115+
expect(deploymentString).toEqual(expectedDeploymentString);
116+
});
117+
118+
it("lists deployments as string without timestamps", async () => {
119+
deployments = MockFactory.createTestDeployments();
120+
ResourceManagementClient.prototype.deployments = {
121+
listByResourceGroup: jest.fn(() => Promise.resolve(deployments)),
122+
} as any;
123+
124+
const sls = MockFactory.createTestServerless();
125+
const resourceGroup = "myResourceGroup";
126+
sls.service.provider["resourceGroup"] = resourceGroup
127+
sls.variables["azureCredentials"] = "fake credentials"
128+
const options = MockFactory.createTestServerlessOptions();
129+
const service = new ResourceService(sls, options);
130+
const deploymentString = await service.listDeployments();
131+
let expectedDeploymentString = "\n\nDeployments";
132+
for (const dep of deployments) {
133+
expectedDeploymentString += "\n-----------\n"
134+
expectedDeploymentString += `Name: ${dep.name}\n`
135+
expectedDeploymentString += "Timestamp: None\n";
136+
expectedDeploymentString += "Datetime: None\n"
137+
}
138+
expectedDeploymentString += "-----------\n"
139+
expect(deploymentString).toEqual(expectedDeploymentString);
140+
});
141+
94142
it("gets deployment template",async () => {
95143
const sls = MockFactory.createTestServerless();
96144
const resourceGroup = "myResourceGroup";
@@ -107,4 +155,4 @@ describe("Resource Service", () => {
107155
);
108156
expect(result).toEqual(template);
109157
});
110-
});
158+
});

src/services/resourceService.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,31 @@ export class ResourceService extends BaseService {
2020
return await this.resourceClient.deployments.listByResourceGroup(this.resourceGroup);
2121
}
2222

23+
/**
24+
* Returns stringified list of deployments with timestamps
25+
*/
26+
public async listDeployments(): Promise<string> {
27+
const deployments = await this.getDeployments()
28+
if (!deployments || deployments.length === 0) {
29+
this.log(`No deployments found for resource group '${this.getResourceGroupName()}'`);
30+
return;
31+
}
32+
let stringDeployments = "\n\nDeployments";
33+
34+
for (const dep of deployments) {
35+
stringDeployments += "\n-----------\n"
36+
stringDeployments += `Name: ${dep.name}\n`
37+
const timestampFromName = Utils.getTimestampFromName(dep.name);
38+
stringDeployments += `Timestamp: ${(timestampFromName) ? timestampFromName : "None"}\n`;
39+
40+
const dateTime = timestampFromName ? new Date(+timestampFromName).toISOString() : "None";
41+
stringDeployments += `Datetime: ${dateTime}\n`
42+
}
43+
44+
stringDeployments += "-----------\n"
45+
return stringDeployments
46+
}
47+
2348
/**
2449
* Get ARM template for previous deployment
2550
* @param deploymentName Name of deployment
@@ -45,4 +70,4 @@ export class ResourceService extends BaseService {
4570
this.log(`Deleting resource group: ${this.resourceGroup}`);
4671
return await this.resourceClient.resourceGroups.deleteMethod(this.resourceGroup);
4772
}
48-
}
73+
}

src/services/rollbackService.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ describe("Rollback Service", () => {
3131
const artifactName = MockFactory.createTestDeployment().name.replace("deployment", "artifact") + ".zip";
3232
const artifactPath = `.serverless${path.sep}${artifactName}`
3333
const armDeployment: ArmDeployment = { template, parameters };
34+
const deploymentString = "deployments";
3435

3536
function createOptions(timestamp?: string): Serverless.Options {
3637
return {
@@ -58,6 +59,7 @@ describe("Rollback Service", () => {
5859
ResourceService.prototype.getDeploymentTemplate = jest.fn(
5960
() => Promise.resolve({ template })
6061
) as any;
62+
ResourceService.prototype.listDeployments = jest.fn(() => Promise.resolve(deploymentString))
6163
AzureBlobStorageService.prototype.generateBlobSasTokenUrl = jest.fn(() => sasURL) as any;
6264
FunctionAppService.prototype.get = jest.fn(() => appStub) as any;
6365
});
@@ -72,8 +74,7 @@ describe("Rollback Service", () => {
7274
const options = {} as any;
7375
const service = createService(sls, options);
7476
await service.rollback();
75-
const calls = (sls.cli.log as any).mock.calls;
76-
expect(calls[0][0]).toEqual("Need to specify a timestamp for rollback. Run `sls deploy list` to see timestamps of deployments");
77+
expect(sls.cli.log).lastCalledWith(deploymentString);
7778
});
7879

7980
it("should return early with invalid timestamp", async () => {

src/services/rollbackService.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,12 +102,14 @@ export class RollbackService extends BaseService {
102102

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

0 commit comments

Comments
 (0)