Repository: serverless/serverless (JavaScript) — Feature

Issue:
Add additional hooks/entry points to the info plugin
# This is a Feature Proposal

## Description

Currently the info plugin writes its output en bloc. 

```
Service Information
service: auth
stage: dev
region: us-east-1
api keys:
  None
endpoints:
  None
functions:
  postauth: auth-dev-postauth
  authenticate: auth-dev-authenticate
  authorize: auth-dev-authorize
aliases:
  dev
```

The problem is, that plugins only can add their output at the end of the full info output (see _aliases_).

It would be better, if the info plugin would offer additional hooks and entrypoints per info section, that

1. Enable plugins to print their information within the info section by hooking after the section event

2. Lets plugins output a section via the info plugin (via pluginManager.spawn('info:print:_section-name_') )

3. Lets plugins overwrite original section outputs by overwriting the events hook implementation.

## Summary

With adding new events and entrypoints to the info plugin all actions mentioned above should be available to plugins. The effort to add this is minimal and non-risky.
Add additional hooks/entry points to the info plugin
# This is a Feature Proposal

## Description

Currently the info plugin writes its output en bloc. 

```
Service Information
service: auth
stage: dev
region: us-east-1
api keys:
  None
endpoints:
  None
functions:
  postauth: auth-dev-postauth
  authenticate: auth-dev-authenticate
  authorize: auth-dev-authorize
aliases:
  dev
```

The problem is, that plugins only can add their output at the end of the full info output (see _aliases_).

It would be better, if the info plugin would offer additional hooks and entrypoints per info section, that

1. Enable plugins to print their information within the info section by hooking after the section event

2. Lets plugins output a section via the info plugin (via pluginManager.spawn('info:print:_section-name_') )

3. Lets plugins overwrite original section outputs by overwriting the events hook implementation.

## Summary

With adding new events and entrypoints to the info plugin all actions mentioned above should be available to plugins. The effort to add this is minimal and non-risky.


Discussion:

Resolving patch:
diff --git a/docs/providers/aws/guide/plugins.md b/docs/providers/aws/guide/plugins.md
index 464dd49e5f6..ce15147aeee 100644
--- a/docs/providers/aws/guide/plugins.md
+++ b/docs/providers/aws/guide/plugins.md
@@ -319,3 +319,24 @@ module.exports = MyPlugin;
 ### Command Naming
 
 Command names need to be unique. If we load two commands and both want to specify the same command (e.g. we have an integrated command `deploy` and an external command also wants to use `deploy`) the Serverless CLI will print an error and exit. If you want to have your own `deploy` command you need to name it something different like `myCompanyDeploy` so they don't clash with existing plugins.
+
+### Extending the `info` command
+
+The `info` command which is used to display information about the deployment has detailed `lifecycleEvents` you can hook into to add and display custom information.
+
+Here's an example overview of the info lifecycle events the AWS implementation exposes:
+
+```
+-> info:info
+  -> aws:info:validate
+  -> aws:info:gatherData
+  -> aws:info:displayServiceInfo
+  -> aws:info:displayApiKeys
+  -> aws:info:displayEndpoints
+  -> aws:info:displayFunctions
+  -> aws:info:displayStackOutputs
+```
+
+Here you could e.g. hook into `after:aws:info:gatherData` and implement your own data collection and display it to the user.
+
+**Note:** Every provider implements its own `info` plugin so you might want to take a look into the `lifecycleEvents` the provider `info` plugin exposes.
diff --git a/docs/providers/azure/guide/plugins.md b/docs/providers/azure/guide/plugins.md
index 28b96b4d504..109dce1218f 100644
--- a/docs/providers/azure/guide/plugins.md
+++ b/docs/providers/azure/guide/plugins.md
@@ -319,3 +319,24 @@ module.exports = MyPlugin;
 ### Command Naming
 
 Command names need to be unique. If we load two commands and both want to specify the same command (e.g. we have an integrated command `deploy` and an external command also wants to use `deploy`) the Serverless CLI will print an error and exit. If you want to have your own `deploy` command you need to name it something different like `myCompanyDeploy` so they don't clash with existing plugins.
+
+### Extending the `info` command
+
+The `info` command which is used to display information about the deployment has detailed `lifecycleEvents` you can hook into to add and display custom information.
+
+Here's an example overview of the info lifecycle events the AWS implementation exposes:
+
+```
+-> info:info
+  -> aws:info:validate
+  -> aws:info:gatherData
+  -> aws:info:displayServiceInfo
+  -> aws:info:displayApiKeys
+  -> aws:info:displayEndpoints
+  -> aws:info:displayFunctions
+  -> aws:info:displayStackOutputs
+```
+
+Here you could e.g. hook into `after:aws:info:gatherData` and implement your own data collection and display it to the user.
+
+**Note:** Every provider implements its own `info` plugin so you might want to take a look into the `lifecycleEvents` the provider `info` plugin exposes.
diff --git a/docs/providers/openwhisk/guide/plugins.md b/docs/providers/openwhisk/guide/plugins.md
index aff502ae2b0..fa6cb4fd861 100644
--- a/docs/providers/openwhisk/guide/plugins.md
+++ b/docs/providers/openwhisk/guide/plugins.md
@@ -319,3 +319,24 @@ module.exports = MyPlugin;
 ### Command Naming
 
 Command names need to be unique. If we load two commands and both want to specify the same command (e.g. we have an integrated command `deploy` and an external command also wants to use `deploy`) the Serverless CLI will print an error and exit. If you want to have your own `deploy` command you need to name it something different like `myCompanyDeploy` so they don't clash with existing plugins.
+
+### Extending the `info` command
+
+The `info` command which is used to display information about the deployment has detailed `lifecycleEvents` you can hook into to add and display custom information.
+
+Here's an example overview of the info lifecycle events the AWS implementation exposes:
+
+```
+-> info:info
+  -> aws:info:validate
+  -> aws:info:gatherData
+  -> aws:info:displayServiceInfo
+  -> aws:info:displayApiKeys
+  -> aws:info:displayEndpoints
+  -> aws:info:displayFunctions
+  -> aws:info:displayStackOutputs
+```
+
+Here you could e.g. hook into `after:aws:info:gatherData` and implement your own data collection and display it to the user.
+
+**Note:** Every provider implements its own `info` plugin so you might want to take a look into the `lifecycleEvents` the provider `info` plugin exposes.
diff --git a/lib/plugins/aws/info/display.js b/lib/plugins/aws/info/display.js
index dae5059c9eb..0ba36cbf407 100644
--- a/lib/plugins/aws/info/display.js
+++ b/lib/plugins/aws/info/display.js
@@ -4,18 +4,22 @@ const chalk = require('chalk');
 const _ = require('lodash');
 
 module.exports = {
-  display() {
+  displayServiceInfo() {
     const info = this.gatheredData.info;
 
     let message = '';
-
     message += `${chalk.yellow.underline('Service Information')}\n`;
     message += `${chalk.yellow('service:')} ${info.service}\n`;
     message += `${chalk.yellow('stage:')} ${info.stage}\n`;
     message += `${chalk.yellow('region:')} ${info.region}`;
 
-    // Display API Keys
-    let apiKeysMessage = `\n${chalk.yellow('api keys:')}`;
+    this.serverless.cli.consoleLog(message);
+    return message;
+  },
+
+  displayApiKeys() {
+    const info = this.gatheredData.info;
+    let apiKeysMessage = `${chalk.yellow('api keys:')}`;
 
     if (info.apiKeys && info.apiKeys.length > 0) {
       info.apiKeys.forEach((apiKeyInfo) => {
@@ -25,10 +29,13 @@ module.exports = {
       apiKeysMessage += '\n  None';
     }
 
-    message += apiKeysMessage;
+    this.serverless.cli.consoleLog(apiKeysMessage);
+    return apiKeysMessage;
+  },
 
-    // Display Endpoints
-    let endpointsMessage = `\n${chalk.yellow('endpoints:')}`;
+  displayEndpoints() {
+    const info = this.gatheredData.info;
+    let endpointsMessage = `${chalk.yellow('endpoints:')}`;
 
     if (info.endpoint) {
       _.forEach(this.serverless.service.functions, (functionObject) => {
@@ -53,10 +60,13 @@ module.exports = {
       endpointsMessage += '\n  None';
     }
 
-    message += endpointsMessage;
+    this.serverless.cli.consoleLog(endpointsMessage);
+    return endpointsMessage;
+  },
 
-    // Display function information
-    let functionsMessage = `\n${chalk.yellow('functions:')}`;
+  displayFunctions() {
+    const info = this.gatheredData.info;
+    let functionsMessage = `${chalk.yellow('functions:')}`;
 
     if (info.functions && info.functions.length > 0) {
       info.functions.forEach((f) => {
@@ -66,17 +76,21 @@ module.exports = {
       functionsMessage += '\n  None';
     }
 
-    message += functionsMessage;
+    this.serverless.cli.consoleLog(functionsMessage);
+    return functionsMessage;
+  },
 
-    // when verbose info is requested, add the stack outputs to the output
+  displayStackOutputs() {
+    let message = '';
     if (this.options.verbose) {
-      message += `${chalk.yellow.underline('\n\nStack Outputs\n')}`;
+      message = `${chalk.yellow.underline('\nStack Outputs\n')}`;
       _.forEach(this.gatheredData.outputs, (output) => {
         message += `${chalk.yellow(output.OutputKey)}: ${output.OutputValue}\n`;
       });
+
+      this.serverless.cli.consoleLog(message);
     }
 
-    this.serverless.cli.consoleLog(message);
     return message;
   },
 };
diff --git a/lib/plugins/aws/info/index.js b/lib/plugins/aws/info/index.js
index 6766b422884..ebad3db233d 100644
--- a/lib/plugins/aws/info/index.js
+++ b/lib/plugins/aws/info/index.js
@@ -19,24 +19,57 @@ class AwsInfo {
       display
     );
 
+    this.commands = {
+      aws: {
+        type: 'entrypoint',
+        commands: {
+          info: {
+            lifecycleEvents: [
+              'validate',
+              'gatherData',
+              'displayServiceInfo',
+              'displayApiKeys',
+              'displayEndpoints',
+              'displayFunctions',
+              'displayStackOutputs',
+            ],
+          },
+        },
+      },
+    };
+
     this.hooks = {
-      'info:info': () => BbPromise.bind(this)
-        .then(this.validate)
-        .then(this.getStackInfo)
-        .then(this.getApiKeyValues)
-        .then(this.display),
+      'info:info': () => this.serverless.pluginManager.spawn('aws:info'),
 
       'deploy:deploy': () => BbPromise.bind(this)
         .then(() => {
           if (this.options.noDeploy) {
             return BbPromise.resolve();
           }
-          return BbPromise.resolve().bind(this)
-            .then(this.validate)
-            .then(this.getStackInfo)
-            .then(this.getApiKeyValues)
-            .then(this.display);
+          return this.serverless.pluginManager.spawn('aws:info');
         }),
+
+      'aws:info:validate': () => BbPromise.bind(this)
+        .then(this.validate),
+
+      'aws:info:gatherData': () => BbPromise.bind(this)
+        .then(this.getStackInfo)
+        .then(this.getApiKeyValues),
+
+      'aws:info:displayServiceInfo': () => BbPromise.bind(this)
+        .then(this.displayServiceInfo),
+
+      'aws:info:displayApiKeys': () => BbPromise.bind(this)
+        .then(this.displayApiKeys),
+
+      'aws:info:displayEndpoints': () => BbPromise.bind(this)
+        .then(this.displayEndpoints),
+
+      'aws:info:displayFunctions': () => BbPromise.bind(this)
+        .then(this.displayFunctions),
+
+      'aws:info:displayStackOutputs': () => BbPromise.bind(this)
+        .then(this.displayStackOutputs),
     };
   }
 }


Test patch:
diff --git a/lib/plugins/aws/info/display.test.js b/lib/plugins/aws/info/display.test.js
index 273290d4779..84f64f58649 100644
--- a/lib/plugins/aws/info/display.test.js
+++ b/lib/plugins/aws/info/display.test.js
@@ -47,14 +47,8 @@ describe('#display()', () => {
     expectedMessage += `${chalk.yellow('service:')} my-first\n`;
     expectedMessage += `${chalk.yellow('stage:')} dev\n`;
     expectedMessage += `${chalk.yellow('region:')} eu-west-1`;
-    expectedMessage += `\n${chalk.yellow('api keys:')}`;
-    expectedMessage += '\n  None';
-    expectedMessage += `\n${chalk.yellow('endpoints:')}`;
-    expectedMessage += '\n  None';
-    expectedMessage += `\n${chalk.yellow('functions:')}`;
-    expectedMessage += '\n  None';
 
-    const message = awsInfo.display();
+    const message = awsInfo.displayServiceInfo();
     expect(consoleLogStub.calledOnce).to.equal(true);
     expect(message).to.equal(expectedMessage);
   });
@@ -64,20 +58,18 @@ describe('#display()', () => {
 
     let expectedMessage = '';
 
-    expectedMessage += `${chalk.yellow.underline('Service Information')}\n`;
-    expectedMessage += `${chalk.yellow('service:')} my-first\n`;
-    expectedMessage += `${chalk.yellow('stage:')} dev\n`;
-    expectedMessage += `${chalk.yellow('region:')} eu-west-1`;
-    expectedMessage += `\n${chalk.yellow('api keys:')}`;
+    expectedMessage += `${chalk.yellow('api keys:')}`;
     expectedMessage += '\n  keyOne: 1234';
-    expectedMessage += `\n${chalk.yellow('endpoints:')}`;
-    expectedMessage += '\n  None';
-    expectedMessage += `\n${chalk.yellow('functions:')}`;
-    expectedMessage += '\n  None';
 
-    const message = awsInfo.display();
+    const message = awsInfo.displayApiKeys();
     expect(consoleLogStub.calledOnce).to.equal(true);
     expect(message).to.equal(expectedMessage);
+
+    delete awsInfo.gatheredData.info.apiKeys;
+    const missingMessage = awsInfo.displayApiKeys();
+    expectedMessage = `${chalk.yellow('api keys:')}`;
+    expectedMessage += '\n  None';
+    expect(missingMessage).to.equal(expectedMessage);
   });
 
   it('should display endpoints if given', () => {
@@ -130,24 +122,22 @@ describe('#display()', () => {
 
     let expectedMessage = '';
 
-    expectedMessage += `${chalk.yellow.underline('Service Information')}\n`;
-    expectedMessage += `${chalk.yellow('service:')} my-first\n`;
-    expectedMessage += `${chalk.yellow('stage:')} dev\n`;
-    expectedMessage += `${chalk.yellow('region:')} eu-west-1`;
-    expectedMessage += `\n${chalk.yellow('api keys:')}`;
-    expectedMessage += '\n  None';
-    expectedMessage += `\n${chalk.yellow('endpoints:')}`;
+    expectedMessage += `${chalk.yellow('endpoints:')}`;
     expectedMessage += '\n  POST - ab12cd34ef.execute-api.us-east-1.amazonaws.com/dev';
     expectedMessage += '\n  POST - ab12cd34ef.execute-api.us-east-1.amazonaws.com/dev/both';
     expectedMessage += '\n  POST - ab12cd34ef.execute-api.us-east-1.amazonaws.com/dev/both/add';
     expectedMessage += '\n  POST - ab12cd34ef.execute-api.us-east-1.amazonaws.com/dev/e';
     expectedMessage += '\n  GET - ab12cd34ef.execute-api.us-east-1.amazonaws.com/dev/function1';
-    expectedMessage += `\n${chalk.yellow('functions:')}`;
-    expectedMessage += '\n  None';
 
-    const message = awsInfo.display();
+    const message = awsInfo.displayEndpoints();
     expect(consoleLogStub.calledOnce).to.equal(true);
     expect(message).to.equal(expectedMessage);
+
+    delete awsInfo.gatheredData.info.endpoint;
+    const missingMessage = awsInfo.displayEndpoints();
+    expectedMessage = `${chalk.yellow('endpoints:')}`;
+    expectedMessage += '\n  None';
+    expect(missingMessage).to.equal(expectedMessage);
   });
 
   it('should display functions if given', () => {
@@ -168,22 +158,20 @@ describe('#display()', () => {
 
     let expectedMessage = '';
 
-    expectedMessage += `${chalk.yellow.underline('Service Information')}\n`;
-    expectedMessage += `${chalk.yellow('service:')} my-first\n`;
-    expectedMessage += `${chalk.yellow('stage:')} dev\n`;
-    expectedMessage += `${chalk.yellow('region:')} eu-west-1`;
-    expectedMessage += `\n${chalk.yellow('api keys:')}`;
-    expectedMessage += '\n  None';
-    expectedMessage += `\n${chalk.yellow('endpoints:')}`;
-    expectedMessage += '\n  None';
-    expectedMessage += `\n${chalk.yellow('functions:')}`;
+    expectedMessage += `${chalk.yellow('functions:')}`;
     expectedMessage += '\n  function1: my-first-dev-function1';
     expectedMessage += '\n  function2: my-first-dev-function2';
     expectedMessage += '\n  function3: my-first-dev-function3';
 
-    const message = awsInfo.display();
+    const message = awsInfo.displayFunctions();
     expect(consoleLogStub.calledOnce).to.equal(true);
     expect(message).to.equal(expectedMessage);
+
+    delete awsInfo.gatheredData.info.functions;
+    const missingMessage = awsInfo.displayFunctions();
+    expectedMessage = `${chalk.yellow('functions:')}`;
+    expectedMessage += '\n  None';
+    expect(missingMessage).to.equal(expectedMessage);
   });
 
   it('should display CloudFormation outputs when verbose output is requested', () => {
@@ -204,22 +192,16 @@ describe('#display()', () => {
 
     let expectedMessage = '';
 
-    expectedMessage += `${chalk.yellow.underline('Service Information')}\n`;
-    expectedMessage += `${chalk.yellow('service:')} my-first\n`;
-    expectedMessage += `${chalk.yellow('stage:')} dev\n`;
-    expectedMessage += `${chalk.yellow('region:')} eu-west-1`;
-    expectedMessage += `\n${chalk.yellow('api keys:')}`;
-    expectedMessage += '\n  None';
-    expectedMessage += `\n${chalk.yellow('endpoints:')}`;
-    expectedMessage += '\n  None';
-    expectedMessage += `\n${chalk.yellow('functions:')}`;
-    expectedMessage += '\n  None';
-    expectedMessage += `${chalk.yellow.underline('\n\nStack Outputs\n')}`;
+    expectedMessage += `${chalk.yellow.underline('\nStack Outputs\n')}`;
     expectedMessage += `${chalk.yellow('Function1FunctionArn')}: ${'arn:function1'}\n`;
     expectedMessage += `${chalk.yellow('Function2FunctionArn')}: ${'arn:function2'}\n`;
 
-    const message = awsInfo.display();
+    const message = awsInfo.displayStackOutputs();
     expect(consoleLogStub.calledOnce).to.equal(true);
     expect(message).to.equal(expectedMessage);
+
+    awsInfo.options.verbose = false;
+    const nonVerboseMessage = awsInfo.displayStackOutputs();
+    expect(nonVerboseMessage).to.equal('');
   });
 });
diff --git a/lib/plugins/aws/info/index.test.js b/lib/plugins/aws/info/index.test.js
index cc95ae6462b..54f1dfe6d3c 100644
--- a/lib/plugins/aws/info/index.test.js
+++ b/lib/plugins/aws/info/index.test.js
@@ -12,7 +12,11 @@ describe('AwsInfo', () => {
   let validateStub;
   let getStackInfoStub;
   let getApiKeyValuesStub;
-  let displayStub;
+  let displayServiceInfoStub;
+  let displayApiKeysStub;
+  let displayEndpointsStub;
+  let displayFunctionsStub;
+  let displayStackOutputsStub;
 
   beforeEach(() => {
     serverless = new Serverless();
@@ -22,21 +26,36 @@ describe('AwsInfo', () => {
       region: 'us-east-1',
     };
     awsInfo = new AwsInfo(serverless, options);
+    // Load commands and hooks into pluginManager
+    serverless.pluginManager.loadCommands(awsInfo);
+    serverless.pluginManager.loadHooks(awsInfo);
     validateStub = sinon
       .stub(awsInfo, 'validate').resolves();
     getStackInfoStub = sinon
       .stub(awsInfo, 'getStackInfo').resolves();
     getApiKeyValuesStub = sinon
       .stub(awsInfo, 'getApiKeyValues').resolves();
-    displayStub = sinon
-      .stub(awsInfo, 'display').resolves();
+    displayServiceInfoStub = sinon
+      .stub(awsInfo, 'displayServiceInfo').resolves();
+    displayApiKeysStub = sinon
+      .stub(awsInfo, 'displayApiKeys').resolves();
+    displayEndpointsStub = sinon
+      .stub(awsInfo, 'displayEndpoints').resolves();
+    displayFunctionsStub = sinon
+      .stub(awsInfo, 'displayFunctions').resolves();
+    displayStackOutputsStub = sinon
+      .stub(awsInfo, 'displayStackOutputs').resolves();
   });
 
   afterEach(() => {
     awsInfo.validate.restore();
     awsInfo.getStackInfo.restore();
     awsInfo.getApiKeyValues.restore();
-    awsInfo.display.restore();
+    awsInfo.displayServiceInfo.restore();
+    awsInfo.displayApiKeys.restore();
+    awsInfo.displayEndpoints.restore();
+    awsInfo.displayFunctions.restore();
+    awsInfo.displayStackOutputs.restore();
   });
 
   describe('#constructor()', () => {
@@ -55,7 +74,11 @@ describe('AwsInfo', () => {
       awsInfo.hooks['info:info']().then(() => {
         expect(validateStub.calledOnce).to.equal(true);
         expect(getStackInfoStub.calledAfter(validateStub)).to.equal(true);
-        expect(displayStub.calledAfter(getApiKeyValuesStub)).to.equal(true);
+        expect(displayServiceInfoStub.calledAfter(getApiKeyValuesStub)).to.equal(true);
+        expect(displayApiKeysStub.calledAfter(getApiKeyValuesStub)).to.equal(true);
+        expect(displayEndpointsStub.calledAfter(getApiKeyValuesStub)).to.equal(true);
+        expect(displayFunctionsStub.calledAfter(getApiKeyValuesStub)).to.equal(true);
+        expect(displayStackOutputsStub.calledAfter(getApiKeyValuesStub)).to.equal(true);
       })
     );
 
@@ -64,7 +87,11 @@ describe('AwsInfo', () => {
         awsInfo.hooks['deploy:deploy']().then(() => {
           expect(validateStub.calledOnce).to.equal(true);
           expect(getStackInfoStub.calledAfter(validateStub)).to.equal(true);
-          expect(displayStub.calledAfter(getApiKeyValuesStub)).to.equal(true);
+          expect(displayServiceInfoStub.calledAfter(getApiKeyValuesStub)).to.equal(true);
+          expect(displayApiKeysStub.calledAfter(getApiKeyValuesStub)).to.equal(true);
+          expect(displayEndpointsStub.calledAfter(getApiKeyValuesStub)).to.equal(true);
+          expect(displayFunctionsStub.calledAfter(getApiKeyValuesStub)).to.equal(true);
+          expect(displayStackOutputsStub.calledAfter(getApiKeyValuesStub)).to.equal(true);
         })
       );
     });