Repository: serverless/serverless-azure-functions
Pull request #176 (2019-06-21 15:58:50)

feat: List deployments sub-command for deploy plugin
Resolves [AB#352]


Patch:
diff --git a/src/plugins/deploy/azureDeployPlugin.ts b/src/plugins/deploy/azureDeployPlugin.ts
index 48f0967..76c88eb 100644
--- a/src/plugins/deploy/azureDeployPlugin.ts
+++ b/src/plugins/deploy/azureDeployPlugin.ts
@@ -4,11 +4,46 @@ import { FunctionAppService } from "../../services/functionAppService";
 
 export class AzureDeployPlugin {
   public hooks: { [eventName: string]: Promise<any> };
+  public commands: any;
 
   public constructor(private serverless: Serverless, private options: Serverless.Options) {
     this.hooks = {
-      "deploy:deploy": this.deploy.bind(this)
+      "deploy:deploy": this.deploy.bind(this),
+      "deploy:list:list": this.list.bind(this),
     };
+
+    this.commands = {
+      deploy: {
+        commands: {
+          list: {
+            usage: "List deployments",
+            lifecycleEvents: [
+              "list"
+            ]
+          }
+        }
+      }
+    }
+  }
+
+  private async list() {
+    this.serverless.cli.log("Listing deployments");
+    const resourceService = new ResourceService(this.serverless, this.options);
+    const deployments = await resourceService.getDeployments();
+    if (!deployments || deployments.length === 0) {
+      this.serverless.cli.log(`No deployments found for resource group '${resourceService.getResourceGroup()}'`);
+      return;
+    }
+    let stringDeployments = "\n\nDeployments";
+
+    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`
+    }
+    stringDeployments += "-----------\n"
+    this.serverless.cli.log(stringDeployments);
   }
 
   private async deploy() {
diff --git a/src/plugins/login/loginPlugin.ts b/src/plugins/login/loginPlugin.ts
index d64635e..083831f 100644
--- a/src/plugins/login/loginPlugin.ts
+++ b/src/plugins/login/loginPlugin.ts
@@ -10,7 +10,8 @@ export class AzureLoginPlugin {
     this.provider = (this.serverless.getProvider("azure") as any) as AzureProvider;
 
     this.hooks = {
-      "before:package:initialize": this.login.bind(this)
+      "before:package:initialize": this.login.bind(this),
+      "before:deploy:list:list": this.login.bind(this),
     };
   }
 
diff --git a/src/services/baseService.ts b/src/services/baseService.ts
index 3f3c73a..4a81638 100644
--- a/src/services/baseService.ts
+++ b/src/services/baseService.ts
@@ -33,6 +33,10 @@ export abstract class BaseService {
     }
   }
 
+  public getResourceGroup(): string {
+    return this.resourceGroup;
+  }
+  
   /**
    * Sends an API request using axios HTTP library
    * @param method The HTTP method
diff --git a/src/services/resourceService.ts b/src/services/resourceService.ts
index 8d47c01..e36dd90 100644
--- a/src/services/resourceService.ts
+++ b/src/services/resourceService.ts
@@ -11,6 +11,11 @@ export class ResourceService extends BaseService {
     this.resourceClient = new ResourceManagementClient(this.credentials, this.subscriptionId);
   }
 
+  public async getDeployments() {
+    this.log(`Listing deployments for resource group '${this.resourceGroup}':`);
+    return await this.resourceClient.deployments.listByResourceGroup(this.resourceGroup);
+  }
+
   public async deployResourceGroup() {
     this.log(`Creating resource group: ${this.resourceGroup}`);
 


Test patch:
diff --git a/src/plugins/deploy/azureDeployPlugin.test.ts b/src/plugins/deploy/azureDeployPlugin.test.ts
index 2818890..3c3db2f 100644
--- a/src/plugins/deploy/azureDeployPlugin.test.ts
+++ b/src/plugins/deploy/azureDeployPlugin.test.ts
@@ -10,6 +10,11 @@ import { ResourceService } from "../../services/resourceService";
 import { Site } from "@azure/arm-appservice/esm/models";
 
 describe("Deploy plugin", () => {
+
+  afterEach(() => {
+    jest.resetAllMocks();
+  })
+
   it("calls deploy hook", async () => {
     const deployResourceGroup = jest.fn();
     const functionAppStub: Site = MockFactory.createTestSite();
@@ -30,4 +35,33 @@ describe("Deploy plugin", () => {
     expect(deploy).toBeCalled();
     expect(uploadFunctions).toBeCalledWith(functionAppStub);
   });
+
+  it("lists deployments", async () => {
+    const deployments = MockFactory.createTestDeployments();
+    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";
+    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 += "-----------\n"
+    expect(sls.cli.log).lastCalledWith(expectedLogStatement);
+  });
+
+  it("logs empty deployment list", async () => {
+    const sls = MockFactory.createTestServerless();
+    const resourceGroup = "rg1";
+    ResourceService.prototype.getDeployments = jest.fn(() => Promise.resolve([])) as any;
+    ResourceService.prototype.getResourceGroup = jest.fn(() => resourceGroup);
+    const options = MockFactory.createTestServerlessOptions();
+    const plugin = new AzureDeployPlugin(sls, options);
+    await invokeHook(plugin, "deploy:list:list");
+    expect(sls.cli.log).lastCalledWith(`No deployments found for resource group '${resourceGroup}'`);
+  });
 });
diff --git a/src/services/resourceService.test.ts b/src/services/resourceService.test.ts
index f664b32..22ac5ad 100644
--- a/src/services/resourceService.test.ts
+++ b/src/services/resourceService.test.ts
@@ -6,6 +6,7 @@ jest.mock("@azure/arm-resources")
 import { ResourceManagementClient } from "@azure/arm-resources";
 
 describe("Resource Service", () => {
+  const deployments = MockFactory.createTestDeployments();
 
   beforeAll(() => {
     ResourceManagementClient.prototype.resourceGroups = {
@@ -14,7 +15,8 @@ describe("Resource Service", () => {
     } as any;
 
     ResourceManagementClient.prototype.deployments = {
-      deleteMethod: jest.fn()
+      deleteMethod: jest.fn(),
+      listByResourceGroup: jest.fn(() => Promise.resolve(deployments)),
     } as any;
   });
 
@@ -70,4 +72,15 @@ describe("Resource Service", () => {
     expect(ResourceManagementClient.prototype.resourceGroups.deleteMethod)
       .toBeCalledWith(resourceGroup);
   });
+
+  it("lists deployments", 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 deps = await service.getDeployments();
+    expect(deps).toEqual(deployments);
+  });
 });
\ No newline at end of file
diff --git a/src/test/mockFactory.ts b/src/test/mockFactory.ts
index ac72bc3..2a7ede2 100644
--- a/src/test/mockFactory.ts
+++ b/src/test/mockFactory.ts
@@ -13,6 +13,7 @@ import { ServerlessAzureConfig } from "../models/serverless";
 import { AzureServiceProvider, ServicePrincipalEnvVariables } from "../models/azureProvider"
 import { Logger } from "../models/generic";
 import { ApiCorsPolicy } from "../models/apiManagement";
+import { DeploymentsListByResourceGroupResponse } from "@azure/arm-resources/esm/models";
 
 function getAttribute(object: any, prop: string, defaultValue: any): any {
   if (object && object[prop]) {
@@ -140,6 +141,19 @@ export class MockFactory {
     return credentials;
   }
 
+  public static createTestDeployments(count: number = 5): DeploymentsListByResourceGroupResponse {
+    const result = [];
+    for (let i = 0; i < count; i++) {
+      result.push({
+        name: `deployment${i+1}`,
+        properties: {
+          timestamp: new Date(),
+        }
+      })
+    }
+    return result as DeploymentsListByResourceGroupResponse
+  }
+
   public static createTestAxiosResponse<T>(
     config: AxiosRequestConfig,
     responseJson: T,