From da27ab3d405feef4fcbe8f6191c8d683bf4b21c0 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Fri, 19 Jul 2019 15:31:44 -0400 Subject: [PATCH] docs: cleanup plugin creation docs The plugin creation docs were hard to follow, unnecessarily complicating issues for developers. The new format should make the recommendations clear, and align examples with the core code. [ci skip] --- docs/development/plugin-creation.md | 269 +++++++++++++++++++--------- 1 file changed, 186 insertions(+), 83 deletions(-) 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