Writing plugins for Navy

Navy allows you to customise the CLI and manipulate services at runtime by using plugins.

Some examples of why you might want to use plugins:

Using plugins

Plugins are defined in your projects Navyfile.js which lives in your project root. See Navyfile.js configuration reference.

Writing plugins

Plugins are just standard node modules which export a function which gets passed a Navy. Create a new folder with a package.json and index.js.

index.js:

module.exports = function (navy) {
  // plugin code here
}

Your plugin can now register middleware, provide custom commands for the CLI, or hook into various lifecycle events.

Custom commands

A plugin can provide custom commands which the user can run using navy run [command]. This can be useful for workflow related operations which might be specific to your team. This is not meant to be used for commands related to the build of your app, this is more for utilities.

You can register a custom command in your index.js:

function migrateData(navy) {
  console.log('Called migrate data command with navy %s', navy.name)
}

module.exports = function (navy) {
  navy.registerCommand('migrate-data', migrateData.bind(null, navy))
}

Now to test it, make sure you have a Navy set up called dev with a Navyfile.js in the project root with the plugin added to the plugins array.

Then run:

$ navy run migrate-data
Called migrate data command with navy dev

Success!

Middleware

Middleware is responsible for manipulating the compose config at runtime. You register middleware by passing in a function which will get called with the current compose config, as well as the state of the current Navy, and the function should return new compose configuration. You can think of it as a set of reducers:

// Pseudocode for how middleware gets run
const middleware = [/* registered middleware from plugins */]
const currentComposeConfig = /* your docker compose config */

const newComposeConfig = middleware.reduce(
  (composeConfig, middleware) => middleware(composeConfig),
  currentComposeConfig,
)

A middleware function looks like this:

index.js:

const {middlewareHelpers} = require('navy')

function newImageForService(service) {
  // Replace myorg/myimage with somelocalregistry.local/myorg/myimage
  if (service.image && service.image.indexOf('myorg/') !== -1) {
    return service.image.replace('myorg/', 'somelocalregistry.local/myorg/')
  }

  return service.image
}

function replaceImage(config) {
  return middlewareHelpers.rewriteServices(config, service => ({
    ...service,
    image: newImageForService(service),
  }))
}

module.exports = function (navy) {
  navy.registerMiddleware(replaceImage)
}

The replaceImage function takes in the current compose config, and returns a new config object. The middlewareHelpers.rewriteServices is a helper function provided by Navy which will return a new config object with new config for each service based on the return value of the map callback provided.

So replaceImage will return something like:

{
  version: 2,
  services: {
    myapp: {
      image: 'somelocalregistry.local/myorg/myimage',
      ports: ['80'],
    },
  },
}

Middleware functions should not mutate the config passed in and should instead return a new instance of the config.

The code snippet above will cause any service with the image myorg/[somename] to pull down somelocalregistry.local/myorg/[somename] instead.

Lifecycle hooks

You can easily hook into various lifecycle hooks in Navy to add functionality.

For example, in index.js:

function handleBeforeLaunch() {
  console.log('Before launch!')
}

module.exports = function (navy) {
  navy.on('cli.before.launch', handleBeforeLaunch)
}

Plugin hooks can be asynchronous and return a promise.