Skip to content

Latest commit

 

History

History
498 lines (376 loc) · 14 KB

01-creating-plugins.md

File metadata and controls

498 lines (376 loc) · 14 KB

Building Serverless plugins

The Serverless plugin System is at the core of the Serverless framework.

The main goals of the plugin system are:

  • Separation of CLI configuration and plugin logic
    • We want any plugin author to be able to easily create new commands within the Serverless framework and extend existing commands. To achieve this we've created a strong separation between CLI and plugins that has clear interfaces between each other.
  • Separation between logic of different plugins
    • Different plugins need to have an easy way to run independently and after each other without defining dependencies between each other.
  • Greater Extensibility
    • Plugins need to be able to easily integrate into the lifecycle of a command independent of other Plugins that are running and can extend the functionality of Serverless easily.

Concepts

Plugin

A plugin encapsulates commands (and corresponding lifecycle events) and hooks in a shareable way. A plugin is not forced to provide both, they can only consist of a list of commands and lifecycle events or only of hooks.

Command

A command represents a CLI command that can be called by a user, e.g. serverless deploy would be the deploy command.

A command has no logic, but simply defines the CLI configuration (e.g. command, subcommands, parameters) and the lifecycle events for this particular command. Every command defines its own lifecycle events, so different commands can have completely different lifecycles.

The commands that come with Serverless (e.g. deploy, remove, …) are implemented in the exact same way as commands built by other users. This means that lifecycle events we define for our commands do not have any special meaning in Serverless or for other plugins. Every command is free to have its own lifecycle events, none of them are more special than others.

'use strict';

class MyPlugin {
  constructor() {
    this.commands = {
      deploy: {
        lifecycleEvents: [
          'resources',
          'functions'
        ]
      },
    };
  }
}

module.exports = MyPlugin;

We automatically put the name of the command in front of lifecycle events when they are used for hooks. So in a hook the following syntax needs to be used.

CommandName:LifecycleEventName

Which would be deploy:resources, and deploy:functions in a hook definition (which we will show in more detail below).

In addition to the lifecycle events defined here we will create 2 additional events for each:

  1. before:CommandName:LifecycleEventName
  2. after:CommandName:LifecycleEventName

Following the above example we’ll have these lifecycle events for our myPlugin plugin:

For the deploy:resources lifecycleEvent:

  • before:deploy:resources
  • deploy:resources
  • after:deploy:resources

For the deploy:functions lifecycleEvent:

  • before:deploy:functions
  • deploy:functions
  • after:deploy:functions

These names will be used as hooks to include plugin logic. This allows to set up lifecycle events with generic names, but still make sure they are only executed for specific commands.

You can also nest commands, e.g. if you want to provide a command serverless deploy single you can simply nest commands as defined in the following example. Those nested commands have their own lifecycle events and do not inherit them from their parents.

'use strict';

class MyPlugin {
  constructor() {
    this.commands = {
      deploy: {
        lifecycleEvents: [
          'resources',
          'functions'
        ],
        commands: {
          single: {
            lifecycleEvents: [
              'resources',
              'functions'
            ],
          },
        },
      },
    }
  }
}

module.exports = MyPlugin;

Hook

Hooks allow to connect specific lifecycle events to functions in a plugin. In the constructor of your class you define a hooks variable that the Plugin System will use once a specific command is running. Any hook can bind to any lifecycle event from any command, not just from commands that the same plugin provides.

This allows to extend any command with additional functionality.

'use strict';

class Deploy {
  constructor() {
    this.commands = {
      deploy: {
        lifecycleEvents: [
          'resources',
          'functions'
        ]
      },
    };

    this.hooks = {
      'before:deploy:resources': this.beforeDeployResources,
      'deploy:resources': this.deployResources,
      'after:deploy:functions': this.afterDeployFunctions
    };
  }

  beforeDeployResources() {
    console.log('Before Deploy Resources');
  }

  deployResources() {
    console.log('Deploy Resources');
  }

  afterDeployFunctions() {
    console.log('After Deploy Functions');
  }
}

module.exports = Deploy;

Serverless instance

The serverless instance which enables access to the whole Serverless setup during runtime is passed in as the first parameter to the plugin constructor.

'use strict';

class MyPlugin {
  constructor(serverless, options) {
    this.serverless = serverless;
    this.options = options;

    this.commands = {
      log: {
        lifecycleEvents: [
          'serverless'
        ],
      },
    };

    this.hooks = {
      'log:serverless': this.logServerless.bind(this)
    }
  }

  logServerless() {
    console.log('Serverless instance: ', this.serverless);
  }
}

module.exports = MyPlugin;

Options and shortcuts

Each (sub)command can have multiple options (and corresponding shortcuts if available).

Options are passed in with a double dash (--) like this: serverless function deploy --function functionName.

Shortcuts are passed in with a single dash (-) like this: serverless function deploy -f functionName

The options object will be passed in as the second parameter to the constructor of your plugin.

'use strict';

class Deploy {
  constructor(serverless, options) {
    this.serverless = serverless;
    this.options = options;

    this.commands = {
      deploy: {
        lifecycleEvents: [
          'functions'
        ],
        options: {
          function: {
            usage: 'Specify the function you want to deploy (e.g. "--function myFunction")'
          }
        }
      },
    };

    this.hooks = {
      'deploy:functions': this.deployFunction.bind(this)
    }
  }

  deployFunction() {
    console.log('Deploying function: ', this.options.function);
  }
}

module.exports = Deploy;

Mark options as required

Options can be marked as required. This way the plugin manager will automatically raise an error if a required option is not passed in via the CLI. You can mark options as required with the help of required: true inside the options definition.

'use strict';

class Deploy {
  constructor(serverless, options) {
    this.options = options;

    this.commands = {
      deploy: {
        lifecycleEvents: [
          'functions'
        ],
        options: {
          function: {
            usage: 'Specify the function you want to deploy (e.g. "--function myFunction")',
            required: true
          }
        }
      },
    };

    this.hooks = {
      'deploy:functions': this.deployFunction.bind(this)
    }
  }

  deployFunction() {
    console.log('Deploying function: ', this.options.function);
  }
}

module.exports = Deploy;

Define shortcuts for options

Options can also provide shortcuts. Shortcuts make it more convenient to enter long commands. Serverless will translate shortcuts into options under the hood which means that the option the shortcut belongs to will be replaced with the value of the shortcut (if the shortcut is given).

You can define shortcuts by setting the shortcut property in the options definition.

Note: A shortcut should be unique inside of a plugin.

'use strict';

class Deploy {
  constructor(serverless, options) {
    this.options = options;

    this.commands = {
      deploy: {
        lifecycleEvents: [
          'functions'
        ],
        options: {
          function: {
            usage: 'Specify the function you want to deploy (e.g. "--function myFunction" or "-f myFunction")',
            required: true,
            shortcut: 'f'
          }
        }
      },
    };

    this.hooks = {
      'deploy:functions': this.deployFunction.bind(this)
    }
  }

  deployFunction() {
    console.log('Deploying function: ', this.options.function);
  }
}

module.exports = Deploy;

Custom validations for options

You can provide a custom validation rule in form of a regular expression for your option. Serverless will raise an error you can specify if the input for the option does not match the regular expression.

In the following example we'll validate the retries option against a regular expression that checks if the value is a number.

'use strict';

class Deploy {
  constructor(serverless, options) {
    this.serverless = serverless;
    this.options = options;

    this.commands = {
      deploy: {
        lifecycleEvents: [
          'functions'
        ],
        options: {
          retries: {
            usage: 'Number of retries which should be performed',
            customValidation: {
              regularExpression: /^[0-9]+$/,
              errorMessage: 'Retries should be a number',
            },
          }
        }
      },
    };

    this.hooks = {
      'deploy:functions': this.deployFunction.bind(this)
    }
  }

  deployFunction() {
    console.log(`Deploying function… Will retry ${this.options.retries} times`);
  }
}

module.exports = Deploy;

Provider specific plugins

Plugins can be provider specific which means that they are bound to a provider.

Note: Binding a plugin to a provider is optional. Serverless will always consider your plugin if you don't specify a provider.

The provider definition should be added inside the plugins constructor and can be the name of the provider (as a string) or the provider plugin instance.

String representation

The string representation simply tells Serverless that this plugin should only be loaded if the provider defined in the service matches the one of the plugin.

'use strict';

class ProviderDeploy {
  constructor(serverless, options) {
    this.serverless = serverless;
    this.options = options;

    // set the providers name here
    this.provider = 'providerName';

    this.commands = {
      deploy: {
        lifecycleEvents: [
          'functions'
        ],
        options: {
          function: {
            usage: 'Specify the function you want to deploy (e.g. "--function myFunction")',
            required: true
          }
        }
      },
    };

    this.hooks = {
      'deploy:functions': this.deployFunction.bind(this)
    }
  }

  deployFunction() {
    console.log('Deploying function: ', this.options.function);
  }
}

module.exports = ProviderDeploy;

The provider plugin instance

If you want to get access to provider specific utilities such as the SDK you can get the provider plugin instance with the help of the this.serverless.getProvider('providerName') function and set it to the plugins provider property. You can access the wrapped methods via this.provider.<methodName>.

The usage of the plugins provider property will load the plugin only if the provider matches the one defined in the service.

'use strict';

class ProviderInvoke {
  constructor(serverless, options) {
    this.serverless = serverless;
    this.options = options;

    // get access to the provider plugin instance here
    this.provider = this.serverless.getProvider('providerName');

    this.commands = {
      invoke: {
        lifecycleEvents: [
          'function'
        ],
        options: {
          function: {
            usage: 'Specify the function you want to invoke (e.g. "--function myFunction")',
            required: true
          }
        }
      },
    };

    this.hooks = {
      'invoke:function': this.invokeFunction.bind(this)
    }
  }

  invokeFunction() {
    // use the "request" method from the provider plugin
    return this.provider.request('function', 'invoke', this.options.function);
  }
}

module.exports = ProviderInvoke;

Plugin registration process

A user has to define the plugins they want to use in the root level of the serverless.yml file:

plugins:
  - custom-plugin-1
  - custom-plugin-2

We do not auto-detect plugins from installed dependencies so users do not run into any surprises and we cut down on the startup time of the tool. Through JSON-REF users can share configuration for used plugins between serverless.yml files in one repository.

After the serverless.yml configuration file is loaded the plugin system will load all the commands and plugins and initialize the plugin system.

Plugin options

Sometimes your plugin needs to setup some custom options. The serverless.yml file provides the custom section where you can add options your plugin can use.

plugins:
  - my-greet-plugin

custom:
  greeting: hello

Plugin Order

Plugins are registered in the order they are defined through our system and the serverless.yml file. By default we will load the core plugins first, then we will load all plugins according to the order given in the serverless.yml file.

This means the Serverless core plugins will always be executed first for every lifecycle event before 3rd party plugins. If external plugins should be running before our plugins they should generally be able to hook into an earlier lifecycle event.

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. Commands need to be unique in the current service context they are executed. So 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.