From 460d92e21c1fa9399a771384beaf9fa245ffb9ea Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Tue, 12 May 2026 18:37:06 -0400 Subject: [PATCH 1/5] feat: expose certs-set and certs-remove plugin triggers Adds `certs-set` and `certs-remove` plugin triggers so other plugins can install or remove an app's SSL cert/key pair without shelling out to the `dokku certs:add` / `dokku certs:remove` subcommands. Shared implementations live as `fn-certs-set` and `fn-certs-remove` in `plugins/certs/internal-functions`, with the subcommands and the new triggers calling `verify_app_name` before delegating. --- docs/development/plugin-triggers.md | 36 +++++++++++++++++++++++++ plugins/certs/certs-remove | 16 +++++++++++ plugins/certs/certs-set | 16 +++++++++++ plugins/certs/internal-functions | 42 +++++++++++++++++++++++++++++ plugins/certs/subcommands/add | 15 +++-------- plugins/certs/subcommands/remove | 12 ++------- tests/unit/certs.bats | 38 ++++++++++++++++++++++++++ 7 files changed, 154 insertions(+), 21 deletions(-) create mode 100755 plugins/certs/certs-remove create mode 100755 plugins/certs/certs-set diff --git a/docs/development/plugin-triggers.md b/docs/development/plugin-triggers.md index dc2399ace..5538a7d3b 100644 --- a/docs/development/plugin-triggers.md +++ b/docs/development/plugin-triggers.md @@ -411,6 +411,42 @@ elif [[ "$KEY_TYPE" == "key" ]]; then fi ``` +### `certs-remove` + +- Description: Removes the SSL cert/key pair from an app and fires the `post-certs-remove` and `post-domains-update` triggers. Fails if no app-specific SSL endpoint is defined. +- Invoked by: +- Arguments: `$APP` +- Example: + +```shell +#!/usr/bin/env bash +# Removes the SSL endpoint for an app + +set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x + +APP="$1" + +dokku certs:remove "$APP" +``` + +### `certs-set` + +- Description: Installs an SSL cert/key pair onto an app and fires the `post-certs-update` and `post-domains-update` triggers. `$CRT_FILE` and `$KEY_FILE` must be paths to readable PEM-encoded files. +- Invoked by: +- Arguments: `$APP $CRT_FILE $KEY_FILE` +- Example: + +```shell +#!/usr/bin/env bash +# Installs a cert/key pair for an app + +set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x + +APP="$1"; CRT_FILE="$2"; KEY_FILE="$3" + +dokku certs:add "$APP" "$CRT_FILE" "$KEY_FILE" +``` + ### `check-deploy` - Description: Allows you to run checks on a deploy before Dokku allows the container to handle requests. diff --git a/plugins/certs/certs-remove b/plugins/certs/certs-remove new file mode 100755 index 000000000..69eec4452 --- /dev/null +++ b/plugins/certs/certs-remove @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" +source "$PLUGIN_AVAILABLE_PATH/certs/internal-functions" +set -eo pipefail +[[ $DOKKU_TRACE ]] && set -x + +trigger-certs-certs-remove() { + declare desc="removes the SSL cert/key pair from an app" + declare trigger="certs-remove" + declare APP="$1" + + verify_app_name "$APP" + fn-certs-remove "$APP" +} + +trigger-certs-certs-remove "$@" diff --git a/plugins/certs/certs-set b/plugins/certs/certs-set new file mode 100755 index 000000000..a63684b30 --- /dev/null +++ b/plugins/certs/certs-set @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" +source "$PLUGIN_AVAILABLE_PATH/certs/internal-functions" +set -eo pipefail +[[ $DOKKU_TRACE ]] && set -x + +trigger-certs-certs-set() { + declare desc="installs an SSL cert/key pair onto an app" + declare trigger="certs-set" + declare APP="$1" CRT_FILE="$2" KEY_FILE="$3" + + verify_app_name "$APP" + fn-certs-set "$APP" "$CRT_FILE" "$KEY_FILE" +} + +trigger-certs-certs-set "$@" diff --git a/plugins/certs/internal-functions b/plugins/certs/internal-functions index a81bd92f2..2686c0a0c 100755 --- a/plugins/certs/internal-functions +++ b/plugins/certs/internal-functions @@ -4,6 +4,48 @@ source "$PLUGIN_AVAILABLE_PATH/certs/functions" set -eo pipefail [[ $DOKKU_TRACE ]] && set -x +fn-certs-set() { + declare desc="installs an SSL cert/key pair onto an app" + declare APP="$1" CRT_FILE="$2" KEY_FILE="$3" + local APP_SSL_PATH="$DOKKU_ROOT/$APP/tls" + + if [[ -z "$CRT_FILE" ]] || [[ -z "$KEY_FILE" ]]; then + dokku_log_fail "Both CRT and KEY file paths are required" + fi + + if [[ ! -r "$CRT_FILE" ]]; then + dokku_log_fail "CRT file specified not found, please check file paths" + fi + + if [[ ! -r "$KEY_FILE" ]]; then + dokku_log_fail "KEY file specified not found, please check file paths" + fi + + mkdir -p "$APP_SSL_PATH" + rm -f "$APP_SSL_PATH/server.crt" "$APP_SSL_PATH/server.key" + cp "$CRT_FILE" "$APP_SSL_PATH/server.crt" + cp "$KEY_FILE" "$APP_SSL_PATH/server.key" + chmod 750 "$APP_SSL_PATH" + chmod 640 "$APP_SSL_PATH/server.crt" "$APP_SSL_PATH/server.key" + plugn trigger post-certs-update "$APP" + plugn trigger post-domains-update "$APP" +} + +fn-certs-remove() { + declare desc="removes the SSL cert/key pair from an app" + declare APP="$1" + local APP_SSL_PATH="$DOKKU_ROOT/$APP/tls" + + if [[ ! -d "$APP_SSL_PATH" ]]; then + dokku_log_fail "An app-specific SSL endpoint is not defined" + fi + + dokku_log_info1 "Removing SSL endpoint from $APP" + rm -rf "$APP_SSL_PATH" + plugn trigger post-certs-remove "$APP" + plugn trigger post-domains-update "$APP" +} + cmd-certs-report() { declare desc="displays an ssl report for one or more apps" declare cmd="certs:report" diff --git a/plugins/certs/subcommands/add b/plugins/certs/subcommands/add index c6baddc1d..d95f215dc 100755 --- a/plugins/certs/subcommands/add +++ b/plugins/certs/subcommands/add @@ -3,6 +3,7 @@ set -eo pipefail [[ $DOKKU_TRACE ]] && set -x source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" source "$PLUGIN_AVAILABLE_PATH/certs/functions" +source "$PLUGIN_AVAILABLE_PATH/certs/internal-functions" is_tar_import() { declare desc="determines if we have STDIN open in an attempt to detect a streamed tar import" @@ -35,7 +36,6 @@ cmd-certs-set() { declare APP="$1" CRT_FILE="$2" KEY_FILE="$3" verify_app_name "$APP" - local APP_SSL_PATH="$DOKKU_ROOT/$APP/tls" if is_file_import "$CRT_FILE" "$KEY_FILE"; then # importing from file @@ -53,7 +53,7 @@ cmd-certs-set() { elif [[ $CRT_FILE_COUNT -gt 1 ]]; then dokku_log_fail "Tar archive contains more than one .crt file" else - local CRT_FILE=$CRT_FILE_SEARCH + CRT_FILE=$CRT_FILE_SEARCH fi local KEY_FILE_SEARCH=$(find . -not -path '*/\.*' -type f | grep ".key$") @@ -63,20 +63,13 @@ cmd-certs-set() { elif [[ $KEY_FILE_COUNT -gt 1 ]]; then dokku_log_fail "Tar archive contains more than one .key file" else - local KEY_FILE=$KEY_FILE_SEARCH + KEY_FILE=$KEY_FILE_SEARCH fi else dokku_log_fail "Tar archive containing server.crt and server.key expected on stdin" fi - mkdir -p "$APP_SSL_PATH" - rm -f "$APP_SSL_PATH/server.crt" "$APP_SSL_PATH/server.key" - cp "$CRT_FILE" "$APP_SSL_PATH/server.crt" - cp "$KEY_FILE" "$APP_SSL_PATH/server.key" - chmod 750 "$APP_SSL_PATH" - chmod 640 "$APP_SSL_PATH/server.crt" "$APP_SSL_PATH/server.key" - plugn trigger post-certs-update "$APP" - plugn trigger post-domains-update "$APP" + fn-certs-set "$APP" "$CRT_FILE" "$KEY_FILE" } cmd-certs-set "$@" diff --git a/plugins/certs/subcommands/remove b/plugins/certs/subcommands/remove index 87e419b72..ac5a4cbce 100755 --- a/plugins/certs/subcommands/remove +++ b/plugins/certs/subcommands/remove @@ -2,6 +2,7 @@ set -eo pipefail [[ $DOKKU_TRACE ]] && set -x source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" +source "$PLUGIN_AVAILABLE_PATH/certs/internal-functions" cmd-certs-remove() { declare desc="removes SSL cert/key from specified app" @@ -10,16 +11,7 @@ cmd-certs-remove() { declare APP="$1" verify_app_name "$APP" - local APP_SSL_PATH="$DOKKU_ROOT/$APP/tls" - - if [[ -d "$APP_SSL_PATH" ]]; then - dokku_log_info1 "Removing SSL endpoint from $APP" - rm -rf "$APP_SSL_PATH" - plugn trigger post-certs-remove "$APP" - plugn trigger post-domains-update "$APP" - else - dokku_log_fail "An app-specific SSL endpoint is not defined" - fi + fn-certs-remove "$APP" } cmd-certs-remove "$@" diff --git a/tests/unit/certs.bats b/tests/unit/certs.bats index d0df07235..c4ecd2916 100644 --- a/tests/unit/certs.bats +++ b/tests/unit/certs.bats @@ -131,6 +131,44 @@ teardown() { assert_success } +@test "(certs) certs-set plugin trigger" { + run /bin/bash -c "plugn trigger certs-set $TEST_APP $BATS_TMPDIR/tls/server.crt $BATS_TMPDIR/tls/server.key" + echo "output: $output" + echo "status: $status" + assert_success + [[ -f "$DOKKU_ROOT/$TEST_APP/tls/server.crt" ]] + [[ -f "$DOKKU_ROOT/$TEST_APP/tls/server.key" ]] +} + +@test "(certs) certs-set plugin trigger missing key file" { + run /bin/bash -c "plugn trigger certs-set $TEST_APP $BATS_TMPDIR/tls/server.crt /nonexistent/server.key" + echo "output: $output" + echo "status: $status" + assert_failure + assert_output_contains "KEY file specified not found" +} + +@test "(certs) certs-remove plugin trigger" { + run /bin/bash -c "dokku certs:add $TEST_APP $BATS_TMPDIR/tls/server.crt $BATS_TMPDIR/tls/server.key" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "plugn trigger certs-remove $TEST_APP" + echo "output: $output" + echo "status: $status" + assert_success + [[ ! -d "$DOKKU_ROOT/$TEST_APP/tls" ]] +} + +@test "(certs) certs-remove plugin trigger without endpoint" { + run /bin/bash -c "plugn trigger certs-remove $TEST_APP" + echo "output: $output" + echo "status: $status" + assert_failure + assert_output_contains "An app-specific SSL endpoint is not defined" +} + @test "(certs) certs:show" { run /bin/bash -c "dokku certs:show fake-app-name 2>&1" echo "output: $output" From 2b963884b42175f6965468f9c534cc86d43ee761 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Tue, 12 May 2026 20:21:07 -0400 Subject: [PATCH 2/5] test: invoke certs trigger tests via run_plugn_trigger helper The new certs-set / certs-remove tests called plugn directly via /bin/bash -c, which exits with 'PLUGIN_PATH is not set in environment' under the bats runner. Route the calls through the existing run_plugn_trigger helper so the required PLUGIN_PATH / PLUGIN_CORE_AVAILABLE_PATH / DOKKU_LIB_ROOT vars are set. --- tests/unit/certs.bats | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/unit/certs.bats b/tests/unit/certs.bats index c4ecd2916..042553725 100644 --- a/tests/unit/certs.bats +++ b/tests/unit/certs.bats @@ -132,7 +132,7 @@ teardown() { } @test "(certs) certs-set plugin trigger" { - run /bin/bash -c "plugn trigger certs-set $TEST_APP $BATS_TMPDIR/tls/server.crt $BATS_TMPDIR/tls/server.key" + run_plugn_trigger certs-set "$TEST_APP" "$BATS_TMPDIR/tls/server.crt" "$BATS_TMPDIR/tls/server.key" echo "output: $output" echo "status: $status" assert_success @@ -141,7 +141,7 @@ teardown() { } @test "(certs) certs-set plugin trigger missing key file" { - run /bin/bash -c "plugn trigger certs-set $TEST_APP $BATS_TMPDIR/tls/server.crt /nonexistent/server.key" + run_plugn_trigger certs-set "$TEST_APP" "$BATS_TMPDIR/tls/server.crt" "/nonexistent/server.key" echo "output: $output" echo "status: $status" assert_failure @@ -154,7 +154,7 @@ teardown() { echo "status: $status" assert_success - run /bin/bash -c "plugn trigger certs-remove $TEST_APP" + run_plugn_trigger certs-remove "$TEST_APP" echo "output: $output" echo "status: $status" assert_success @@ -162,7 +162,7 @@ teardown() { } @test "(certs) certs-remove plugin trigger without endpoint" { - run /bin/bash -c "plugn trigger certs-remove $TEST_APP" + run_plugn_trigger certs-remove "$TEST_APP" echo "output: $output" echo "status: $status" assert_failure From a9b2f56ee888a4b549c4251a934ed3e86a4347f0 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Tue, 12 May 2026 21:31:58 -0400 Subject: [PATCH 3/5] test: forward PLUGIN_AVAILABLE_PATH and PLUGIN_ENABLED_PATH from run_plugn_trigger The dokku entrypoint exports both vars before invoking plugn, so triggers that source `$PLUGIN_AVAILABLE_PATH/...` work in production. The helper only forwarded PLUGIN_PATH / PLUGIN_CORE_AVAILABLE_PATH / DOKKU_LIB_ROOT, so triggers that follow the same pattern fail under bats. --- tests/unit/test_helper.bash | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_helper.bash b/tests/unit/test_helper.bash index 0424c2a43..cd6caad20 100644 --- a/tests/unit/test_helper.bash +++ b/tests/unit/test_helper.bash @@ -239,7 +239,7 @@ run_plugn_trigger() { done local TRIGGER="$1" shift - run /bin/bash -c "PLUGIN_PATH=$PLUGIN_PATH PLUGIN_CORE_AVAILABLE_PATH=$PLUGIN_CORE_AVAILABLE_PATH DOKKU_LIB_ROOT=$DOKKU_LIB_ROOT${extra_env} plugn trigger $TRIGGER $* < /dev/null" + run /bin/bash -c "PLUGIN_PATH=$PLUGIN_PATH PLUGIN_AVAILABLE_PATH=$PLUGIN_AVAILABLE_PATH PLUGIN_ENABLED_PATH=$PLUGIN_ENABLED_PATH PLUGIN_CORE_AVAILABLE_PATH=$PLUGIN_CORE_AVAILABLE_PATH DOKKU_LIB_ROOT=$DOKKU_LIB_ROOT${extra_env} plugn trigger $TRIGGER $* < /dev/null" } # Run a single plugin's trigger script directly so other plugins' handlers do not fire. From 282b2df71a36f8cdcbcc0b09920db33ef12a9ac2 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Tue, 12 May 2026 22:30:49 -0400 Subject: [PATCH 4/5] test: forward DOKKU_ROOT from run_plugn_trigger Triggers that call verify_app_name or otherwise reference $DOKKU_ROOT fail under bats because the helper did not forward DOKKU_ROOT to the plugn subshell. The production dokku entrypoint exports DOKKU_ROOT before invoking plugn, so bring the helper to env-var parity. --- tests/unit/test_helper.bash | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_helper.bash b/tests/unit/test_helper.bash index cd6caad20..abb859a75 100644 --- a/tests/unit/test_helper.bash +++ b/tests/unit/test_helper.bash @@ -239,7 +239,7 @@ run_plugn_trigger() { done local TRIGGER="$1" shift - run /bin/bash -c "PLUGIN_PATH=$PLUGIN_PATH PLUGIN_AVAILABLE_PATH=$PLUGIN_AVAILABLE_PATH PLUGIN_ENABLED_PATH=$PLUGIN_ENABLED_PATH PLUGIN_CORE_AVAILABLE_PATH=$PLUGIN_CORE_AVAILABLE_PATH DOKKU_LIB_ROOT=$DOKKU_LIB_ROOT${extra_env} plugn trigger $TRIGGER $* < /dev/null" + run /bin/bash -c "DOKKU_ROOT=$DOKKU_ROOT PLUGIN_PATH=$PLUGIN_PATH PLUGIN_AVAILABLE_PATH=$PLUGIN_AVAILABLE_PATH PLUGIN_ENABLED_PATH=$PLUGIN_ENABLED_PATH PLUGIN_CORE_AVAILABLE_PATH=$PLUGIN_CORE_AVAILABLE_PATH DOKKU_LIB_ROOT=$DOKKU_LIB_ROOT${extra_env} plugn trigger $TRIGGER $* < /dev/null" } # Run a single plugin's trigger script directly so other plugins' handlers do not fire. From b4e3d8502438d0231aa11f8973d75a903b9485bc Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Tue, 12 May 2026 22:36:47 -0400 Subject: [PATCH 5/5] test: route run_plugn_trigger through dokku plugin:trigger The dokku entrypoint already exports DOKKU_ROOT, PLUGIN_PATH, PLUGIN_AVAILABLE_PATH, PLUGIN_ENABLED_PATH, PLUGIN_CORE_AVAILABLE_PATH and DOKKU_LIB_ROOT before invoking plugn. Delegating to `dokku plugin:trigger` keeps the helper at env-var parity with the production trigger path without enumerating every var the helper must forward. --- tests/unit/test_helper.bash | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_helper.bash b/tests/unit/test_helper.bash index abb859a75..fb35b626a 100644 --- a/tests/unit/test_helper.bash +++ b/tests/unit/test_helper.bash @@ -228,7 +228,7 @@ destroy_key() { rm -f /tmp/testkey* &>/dev/null || true } -# Run a plugn trigger with the env vars plugn needs, capturing $output/$status via bats `run`. +# Run a plugn trigger via `dokku plugin:trigger`, capturing $output/$status via bats `run`. # Optional VAR=VAL pairs before are forwarded as extra env vars on the bash command line. # Usage: run_plugn_trigger [VAR=VAL ...] run_plugn_trigger() { @@ -239,7 +239,7 @@ run_plugn_trigger() { done local TRIGGER="$1" shift - run /bin/bash -c "DOKKU_ROOT=$DOKKU_ROOT PLUGIN_PATH=$PLUGIN_PATH PLUGIN_AVAILABLE_PATH=$PLUGIN_AVAILABLE_PATH PLUGIN_ENABLED_PATH=$PLUGIN_ENABLED_PATH PLUGIN_CORE_AVAILABLE_PATH=$PLUGIN_CORE_AVAILABLE_PATH DOKKU_LIB_ROOT=$DOKKU_LIB_ROOT${extra_env} plugn trigger $TRIGGER $* < /dev/null" + run /bin/bash -c "${extra_env} dokku plugin:trigger $TRIGGER $* < /dev/null" } # Run a single plugin's trigger script directly so other plugins' handlers do not fire.