diff --git a/docs/development/plugin-creation.md b/docs/development/plugin-creation.md index 05468f134..096f059b6 100644 --- a/docs/development/plugin-creation.md +++ b/docs/development/plugin-creation.md @@ -2,56 +2,222 @@ A plugin can be a simple implementation of [triggers](/docs/development/plugin-triggers.md) or can implement a command structure of its own. Dokku has no restrictions on the language in which a plugin is implemented; it only cares that the plugin implements the appropriate [commands](/docs/development/plugin-creation.md#command-api) or [triggers](/docs/development/plugin-triggers.md) for the API. **NOTE:** any file that implements triggers or uses the command API must be executable. -If you create your own plugin: +When creating custom plugins: 1. Take a look at [the plugins shipped with Dokku](/docs/community/plugins.md) and hack away! -2. Check out the [list of triggers](/docs/development/plugin-triggers.md) your plugin can implement -3. Upload your plugin to GitHub with a repository name following the `dokku-` convention (e.g. `dokku-mariadb`) -4. Edit [this page](/docs/community/plugins.md) and add a link to your plugin +2. Check out the [list of triggers](/docs/development/plugin-triggers.md) the plugin can implement +3. Upload the plugin to GitHub with a repository name following the `dokku-` convention (e.g. `dokku-mariadb`) +4. Edit [this page](/docs/community/plugins.md) and add a link to the plugin 5. Subscribe to the [dokku development blog](http://progrium.com) to be notified about API changes and releases - ## Compilable plugins (Golang, Java(?), C, etc.) -When developing a plugin, you must implement the `install` trigger such that it outputs the built executable(s) using a directory structure that implements the plugin's desired command and/or triggers the API. See the [smoke-test-plugin](https://github.com/dokku/smoke-test-plugin) for an example. +When developing a plugin, the `install` trigger must be implemented such that it outputs the built executable(s) using a directory structure that implements the plugin's desired command and/or triggers the API. See the [smoke-test-plugin](https://github.com/dokku/smoke-test-plugin) for an example. ## Command API + There are 3 main integration points: `commands`, `subcommands/default`, and `subcommands/`. ### `commands` + Primarily used to supply the plugin's usage/help output. (i.e. [plugin help](https://github.com/dokku/dokku/tree/master/plugins/plugin/commands)). ### `subcommands/default` + Implements the plugin's default command behavior. (i.e. [`dokku plugin`](https://github.com/dokku/dokku/tree/master/plugins/plugin/subcommands/default)). ### `subcommands/` + Implements the additional command interface and will translate to `dokku plugin:cmd` on the command line. (i.e. [`dokku plugin:install`](https://github.com/dokku/dokku/tree/master/plugins/plugin/subcommands/install)). +# Plugin Building Tips + +## Always create a `plugin.toml` + +The `plugin.toml` file is used to describe the plugin in help output, and helps users understand the purpose of the plugin. This _must_ have a description and a version. The version _should_ be bumped at every plugin release. + +```toml +[plugin] +description = "dokku example plugin" +version = "0.1.0" +[plugin.config] +``` + +## Files should be executable + +Commands, subcommands, triggers and source shell scripts should all be executable. On a Unix-like machine, the following command can be used to make them executable: + +```shell +chmod +x path/to/file +``` + +Non-executable commands, subcommands, and triggers will be ignored. + +## Use the `pipefail` bash option + +Consider whether to include the `set -eo pipefail` option. Look at the following example: + +```shell +IMAGE=$(docker images | grep "user/repo" | awk '{print $3}') +if [[ -z $IMAGE ]]; then + dokku_log_fail "user/repo image not found... Did you run 'dokku plugin:install'?" +fi +``` + +If `user/repo` doesn't exist, Dokku exits just before the `awk` command and the `dokku_log_fail` message will never go to `STDOUT`. printed with echo. The `set -e` option should be used in this case. + +Here is the `help` entry for `set`: + +``` +help set +Options: + -e Exit immediately if a command exits with a non-zero status. + -o option-name + pipefail the return value of a pipeline is the status of + the last command to exit with a non-zero status, + or zero if no command exited with a non-zero status +``` + +## Support trace mode + +Trace mode is useful for getting debugging output from plugins when the `--trace` flag is specified or `dokku trace:on` is triggered. This should be done at the top of each shell script: + +```shell +#!/usr/bin/env bash +set -eo pipefail +[[ $DOKKU_TRACE ]] && set -x +``` + +In the above example, the third line enables bash's debug mode, which prints command traces before executing command. + +## Verify the existence of dependencies + +If a plugin depends on a specific command-line tool, check whether that tool exists before utilizing it. Either `command -v` or `which` may be used to do so: + +```shell +# `command -v` example +if ! command -v "nginx" &>/dev/null; then + log-fail "Missing nginx, install it" +fi + +# `which` example +if ! which nginx >/dev/null 2>&1; then + log-fail "Missing nginx, install it" +fi +``` + +In cases where a dependency should be installed before the plugin can be used at all, use the `dependencies` plugin trigger to install the dependency. + +## Implement a help command + +For plugins which expose commands, implement a `help` command. This may be empty, but should contain a listing of all available commands. + +Commas - `,` - are used in the help output for columnizing output. Verify that the plugin conforms to the spec by running `dokku help --all` and manually verifying the output. + +See the sample plugin below for an example. + +## Namespace commands + +All commands *should* be namespaced. In cases where a core plugin is overriden, the plugin _may_ utilize the a namespace in use by the core, but generally this should be avoided to reduce confusion as to where the command is implemented. + +## Implement a proper catch-all command + +As of 0.3.3, a catch-all should be implemented that exits with a `DOKKU_NOT_IMPLEMENTED_EXIT` code. This allows Dokku to output a `command not found` message. + +See the sample plugin below for an example. + +## Set app config without restarting + +In the case that a plugin needs to set app configuration settings and a restart should be avoided (default Heroku-style behavior) these "internal" commands provide this functionality: + +```shell +config_set --no-restart node-js-app KEY1=VALUE1 [KEY2=VALUE2 ...] +config_unset --no-restart node-js-app KEY1 [KEY2 ...] +``` + +## Expose functionality in a `functions` file + +To allow other plugins access to (some of) a plugin's functionality, functions can expose by including a `functions` file in the plugin for others to source. All functions in that file should be considered publicly accessible by other plugins. + +Any functions that must be kept private should reside in the plugin's `trigger/` or `commands/` directories. Other files may also be used to hide private functions; the official convention for hiding private functions is to place them an `internal-functions` file. + +## Use helper functions to fetch app images + +> New as of 0.4.0 + +Dokku allows image tagging and deployment of tagged images. This means hard-coding the `$IMAGE` as `dokku/$APP` is no longer sufficient. + +Plugins should use `get_running_image_tag()` and `get_app_image_name()` as sourced from `common/functions`. See the [plugin triggers](/docs/development/plugin-triggers.md) doc for examples. + +> **Note:** This is only for plugins that are not `pre/post-build-*` plugins + +## Use `$DOCKER_BIN` instead of `docker` directly + +> New as of 0.17.5 + +Certain systems may require a wrapper function around the `docker` binary for proper execution. Utilizing the `$DOCKER_BIN` environment variable when calling docker for those functions is preferred. + +```shell +# good +"$DOCKER_BIN" run -d $IMAGE /bin/bash -e -c "$COMMAND" + +# bad +docker run -d $IMAGE /bin/bash -e -c "$COMMAND" +``` + +## Include labels for all temporary containers + +> New as of 0.5.0 + +As of 0.5.0, labels are used to help cleanup intermediate containers with `dokku cleanup`. Plugins that create containers should add the correct labels to `run` docker commands. + +```shell +# `docker run` example +"$DOCKER_BIN" run "${DOKKU_GLOBAL_RUN_ARGS[@]}" ... +``` + +## Avoid calling the `dokku` binary directly + +> New as of 0.6.0 + +Plugins should **not** call the `dokku` binary directly from within plugins because clients using the `--app` argument are potentially broken when doing so. + +Plugins should instead source the `functions` file for a given plugin when attempting to call Dokku internal functions. # Sample plugin + The below plugin is a dummy `dokku hello` plugin. +Each plugin requires a `plugin.toml` descriptor file with the following required fields: + +```toml +[plugin] +description = "dokku hello plugin" +version = "0.1.0" +[plugin.config] +``` + `hello/subcommands/default` ```shell #!/usr/bin/env bash -set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x +set -eo pipefail +[[ $DOKKU_TRACE ]] && set -x source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" hello_main_cmd() { declare desc="prints Hello \$APP" - local cmd="hello" + declare cmd="hello" argv=("$@") + [[ ${argv[0]} == "$cmd" ]] && shift 1 # Support --app/$DOKKU_APP_NAME flag # Use the following lines to reorder args into "$cmd $DOKKU_APP_NAME $@"" - local argv=("$@") - [[ ${argv[0]} == "$cmd" ]] && shift 1 [[ -n $DOKKU_APP_NAME ]] && set -- $DOKKU_APP_NAME $@ set -- $cmd $@ - ## + # + declare APP="$1" - [[ -z $2 ]] && dokku_log_fail "Please specify an app to run the command on" - verify_app_name "$2" - local APP="$2"; + [[ -z "$APP" ]] && dokku_log_fail "Please specify an app to run the command on" + verify_app_name "$APP" echo "Hello $APP" } @@ -63,23 +229,14 @@ hello_main_cmd "$@" ```shell #!/usr/bin/env bash -set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x +set -eo pipefail +[[ $DOKKU_TRACE ]] && set -x source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" hello_world_cmd() { - declare desc="prints Hello World" - local cmd="hello:world" - # Support --app/$DOKKU_APP_NAME flag - # Use the following lines to reorder args into "$cmd $DOKKU_APP_NAME $@"" - local argv=("$@") + declare desc="prints Hello world" + declare cmd="hello:world" argv=("$@") [[ ${argv[0]} == "$cmd" ]] && shift 1 - [[ -n $DOKKU_APP_NAME ]] && set -- $DOKKU_APP_NAME $@ - set -- $cmd $@ - ## - - [[ -z $2 ]] && dokku_log_fail "Please specify an app to run the command on" - verify_app_name "$2" - local APP="$2"; echo "Hello world" } @@ -91,7 +248,8 @@ hello_world_cmd "$@" ```shell #!/usr/bin/env bash -set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x +set -eo pipefail +[[ $DOKKU_TRACE ]] && set -x case "$1" in help | hello:help) @@ -126,58 +284,3 @@ help_content esac ``` - -Each plugin requires a `plugin.toml` descriptor file with the following required fields: - -```toml -[plugin] -description = "dokku hello plugin" -version = "0.1.0" -[plugin.config] -``` - -# A few notes: - -- Remember to `chmod +x` your executable files -- Always support `DOKKU_TRACE` as per the 2nd line of the above example -- If your command depends on an application, include a check for whether that application exists (see the above example) -- You must implement a `help` command, though you may leave it empty. Also, you must use commas (`,`) in the command syntax to support output in columns -- Commands **should** be namespaced -- As of 0.3.3, a catch-all should be implemented that exits with a `DOKKU_NOT_IMPLEMENTED_EXIT` code. This allows Dokku to output a `command not found` message. -- Consider whether you want to include the `set -eo pipefail` option. Look at the following example : - - ```shell - IMAGE=$(docker images | grep "user/repo" | awk '{print $3}') - if [[ -z $IMAGE ]]; then - dokku_log_fail "user/repo image not found... Did you run 'dokku plugin:install'?" - fi - ``` - - If `user/repo` doesn't exist, Dokku exits just before the `awk` command and the `dokku_log_fail` message will never go to `STDOUT`. printed with echo. You would want to use `set -e` in this case. - - Here is the `help` entry for `set`: - ``` - help set - Options: - -e Exit immediately if a command exits with a non-zero status. - -o option-name - pipefail the return value of a pipeline is the status of - the last command to exit with a non-zero status, - or zero if no command exited with a non-zero status - ``` -- In the case that your plugin needs to set application configuration settings and you want to avoid having to restart (default Heroku-style behavior) these "internal" commands provide this functionality: - - ```shell - dokku config:set --no-restart node-js-app KEY1=VALUE1 [KEY2=VALUE2 ...] - dokku config:unset --no-restart node-js-app KEY1 [KEY2 ...] - ``` -- If you want to allow other plugins access to (some of) your plugin's functionality, you can expose this by including a `functions` file in your plugin for others to source - - You should consider all functions in that file to be publicly accessible by other plugins - - Any functions you want to keep private should reside in your plugin's `trigger/` or `commands/` directories -- As of 0.4.0, Dokku allows image tagging and deployment of tagged images - - This means hard-coding the `$IMAGE` as `dokku/$APP` is no longer sufficient - - You should now use `get_running_image_tag()` and `get_app_image_name()` as sourced from `common/functions` (see the [plugin triggers](/docs/development/plugin-triggers.md) doc for examples). **Note:** This is only for plugins that are not `pre/post-build-*` plugins -- As of 0.5.0, we use container labels to help cleanup intermediate containers with `dokku cleanup - - This means that if you manually call `docker run`, you should include `$DOKKU_GLOBAL_RUN_ARGS` to ensure your intermediate containers are labeled correctly -- As of 0.6.0, you should not **not** call the `dokku` binary directly from within plugins because clients using the `--app` argument are potentially broken when doing so (as well as other issues) - - You should instead source the `functions` file for a given plugin when attempting to call Dokku internal functions