From 3cf02f9904b7b19d8e096f33817f9f9a280fe4d9 Mon Sep 17 00:00:00 2001 From: Matt Farmer Date: Sat, 28 Feb 2026 18:07:07 -0500 Subject: [PATCH] feat(traefik-vhosts): support custom entrypoint for API/dashboard Add two new global-only properties to configure a dedicated Traefik entrypoint for the API and dashboard, enabling network isolation on private interfaces. Includes comprehensive test coverage for property management and compose config. Co-Authored-By: Claude Haiku 4.5 --- plugins/traefik-vhosts/command-functions | 2 + plugins/traefik-vhosts/internal-functions | 17 +++ plugins/traefik-vhosts/subcommands/set | 8 +- .../templates/compose.yml.sigil | 10 +- tests/unit/traefik.bats | 116 ++++++++++++++++++ 5 files changed, 147 insertions(+), 6 deletions(-) diff --git a/plugins/traefik-vhosts/command-functions b/plugins/traefik-vhosts/command-functions index d2d28da0c..f37149b72 100755 --- a/plugins/traefik-vhosts/command-functions +++ b/plugins/traefik-vhosts/command-functions @@ -69,6 +69,8 @@ cmd-traefik-report-single() { verify_app_name "$APP" local flag_map=( "--traefik-api-enabled: $(fn-traefik-api-enabled)" + "--traefik-api-entry-point: $(fn-traefik-api-entry-point)" + "--traefik-api-entry-point-address: $(fn-traefik-api-entry-point-address)" "--traefik-api-vhost: $(fn-traefik-api-vhost)" "--traefik-basic-auth-password: $(fn-traefik-basic-auth-password)" "--traefik-basic-auth-username: $(fn-traefik-basic-auth-username)" diff --git a/plugins/traefik-vhosts/internal-functions b/plugins/traefik-vhosts/internal-functions index c12dd9981..fcb570fc5 100755 --- a/plugins/traefik-vhosts/internal-functions +++ b/plugins/traefik-vhosts/internal-functions @@ -49,7 +49,16 @@ fn-traefik-template-compose-file() { dns_provider_env_vars="$(fn-traefik-dns-provider-env-vars)" fi + local api_entry_point_address api_entry_point_port="" + api_entry_point_address="$(fn-traefik-api-entry-point-address)" + if [[ -n "$api_entry_point_address" ]]; then + api_entry_point_port="${api_entry_point_address##*:}" + fi + local SIGIL_PARAMS=(TRAEFIK_API_ENABLED="$(fn-traefik-api-enabled)" + TRAEFIK_API_ENTRY_POINT="$(fn-traefik-api-entry-point)" + TRAEFIK_API_ENTRY_POINT_ADDRESS="$api_entry_point_address" + TRAEFIK_API_ENTRY_POINT_PORT="$api_entry_point_port" TRAEFIK_API_VHOST="$(fn-traefik-api-vhost)" TRAEFIK_BASIC_AUTH="$basic_auth" TRAEFIK_CHALLENGE_MODE="$(fn-traefik-challenge-mode)" @@ -111,6 +120,14 @@ fn-traefik-https-entry-point() { fn-plugin-property-get-default "traefik" "--global" "https-entry-point" "https" } +fn-traefik-api-entry-point() { + fn-plugin-property-get-default "traefik" "--global" "api-entry-point" "" +} + +fn-traefik-api-entry-point-address() { + fn-plugin-property-get-default "traefik" "--global" "api-entry-point-address" "" +} + fn-traefik-challenge-mode() { fn-plugin-property-get-default "traefik" "--global" "challenge-mode" "tls" } diff --git a/plugins/traefik-vhosts/subcommands/set b/plugins/traefik-vhosts/subcommands/set index 1e8baa5fc..e7bb9b0e1 100755 --- a/plugins/traefik-vhosts/subcommands/set +++ b/plugins/traefik-vhosts/subcommands/set @@ -9,9 +9,9 @@ cmd-traefik-set() { declare cmd="traefik:set" [[ "$1" == "$cmd" ]] && shift 1 declare APP="$1" KEY="$2" VALUE="$3" - local VALID_KEYS=("api-enabled" "api-vhost" "challenge-mode" "dashboard-enabled" "basic-auth-username" "basic-auth-password" "dns-provider" "image" "letsencrypt-email" "letsencrypt-server" "log-level" "http-entry-point" "https-entry-point") - local GLOBAL_KEYS=("api-enabled" "api-vhost" "challenge-mode" "dashboard-enabled" "basic-auth-username" "basic-auth-password" "dns-provider" "image" "letsencrypt-email" "letsencrypt-server" "log-level" "http-entry-point" "https-entry-point") - local GLOBAL_ONLY_KEYS=("challenge-mode" "dns-provider") + local VALID_KEYS=("api-enabled" "api-entry-point" "api-entry-point-address" "api-vhost" "challenge-mode" "dashboard-enabled" "basic-auth-username" "basic-auth-password" "dns-provider" "image" "letsencrypt-email" "letsencrypt-server" "log-level" "http-entry-point" "https-entry-point") + local GLOBAL_KEYS=("api-enabled" "api-entry-point" "api-entry-point-address" "api-vhost" "challenge-mode" "dashboard-enabled" "basic-auth-username" "basic-auth-password" "dns-provider" "image" "letsencrypt-email" "letsencrypt-server" "log-level" "http-entry-point" "https-entry-point") + local GLOBAL_ONLY_KEYS=("api-entry-point" "api-entry-point-address" "challenge-mode" "dns-provider") [[ -z "$KEY" ]] && dokku_log_fail "No key specified" @@ -22,7 +22,7 @@ cmd-traefik-set() { fi if ! fn-in-array "$KEY" "${VALID_KEYS[@]}" && [[ "$is_dns_provider_env_var" != "true" ]]; then - dokku_log_fail "Invalid key specified, valid keys include: api-enabled api-vhost challenge-mode dashboard-enabled basic-auth-username basic-auth-password dns-provider dns-provider- image letsencrypt-email letsencrypt-server log-level http-entry-point https-entry-point" + dokku_log_fail "Invalid key specified, valid keys include: api-enabled api-entry-point api-entry-point-address api-vhost challenge-mode dashboard-enabled basic-auth-username basic-auth-password dns-provider dns-provider- image letsencrypt-email letsencrypt-server log-level http-entry-point https-entry-point" fi if ! fn-in-array "$KEY" "${GLOBAL_KEYS[@]}" && [[ "$is_dns_provider_env_var" != "true" ]]; then diff --git a/plugins/traefik-vhosts/templates/compose.yml.sigil b/plugins/traefik-vhosts/templates/compose.yml.sigil index 2b277b966..064438df9 100644 --- a/plugins/traefik-vhosts/templates/compose.yml.sigil +++ b/plugins/traefik-vhosts/templates/compose.yml.sigil @@ -9,6 +9,9 @@ services: - --entrypoints.http.http.redirections.entrypoint.scheme=https - --entrypoints.https.address=:443 {{ end }} + {{ if and ($.TRAEFIK_API_ENTRY_POINT) ($.TRAEFIK_API_ENTRY_POINT_ADDRESS) }} + - --entrypoints.{{ $.TRAEFIK_API_ENTRY_POINT }}.address={{ $.TRAEFIK_API_ENTRY_POINT_ADDRESS }} + {{ end }} - --providers.docker - --providers.docker.exposedByDefault=false - --api={{ $.TRAEFIK_API_ENABLED }} @@ -52,7 +55,7 @@ services: - "traefik.enable=true" - "traefik.http.routers.api.rule=Host(`{{ $.TRAEFIK_API_VHOST }}`)" - "traefik.http.routers.api.service=api@internal" - - "traefik.http.routers.api.entrypoints={{ if $.TRAEFIK_LETSENCRYPT_EMAIL }}https{{ else }}http{{ end }}" + - "traefik.http.routers.api.entrypoints={{ if $.TRAEFIK_API_ENTRY_POINT }}{{ $.TRAEFIK_API_ENTRY_POINT }}{{ else if $.TRAEFIK_LETSENCRYPT_EMAIL }}https{{ else }}http{{ end }}" {{ end }} {{ if $.TRAEFIK_BASIC_AUTH }} @@ -60,7 +63,7 @@ services: - "traefik.http.middlewares.auth.basicauth.users={{ $.TRAEFIK_BASIC_AUTH }}" {{ end }} - {{ if $.TRAEFIK_LETSENCRYPT_EMAIL }} + {{ if and ($.TRAEFIK_LETSENCRYPT_EMAIL) (not $.TRAEFIK_API_ENTRY_POINT) }} - "traefik.http.routers.api.tls.certresolver=leresolver" {{ end }} @@ -71,6 +74,9 @@ services: {{ if $.TRAEFIK_LETSENCRYPT_EMAIL }} - "443:443" {{ end }} + {{ if and ($.TRAEFIK_API_ENTRY_POINT) ($.TRAEFIK_API_ENTRY_POINT_ADDRESS) }} + - "{{ $.TRAEFIK_API_ENTRY_POINT_ADDRESS }}:{{ $.TRAEFIK_API_ENTRY_POINT_PORT }}" + {{ end }} restart: unless-stopped diff --git a/tests/unit/traefik.bats b/tests/unit/traefik.bats index c9a69a251..b627a8968 100644 --- a/tests/unit/traefik.bats +++ b/tests/unit/traefik.bats @@ -8,6 +8,8 @@ setup() { dokku traefik:set --global letsencrypt-server https://acme-staging-v02.api.letsencrypt.org/directory dokku traefik:set --global letsencrypt-email dokku traefik:set --global api-enabled + dokku traefik:set --global api-entry-point + dokku traefik:set --global api-entry-point-address dokku traefik:set --global challenge-mode dokku traefik:set --global dns-provider dokku traefik:start @@ -556,6 +558,120 @@ teardown() { assert_output_not_exists } +@test "(traefik) api-entry-point property" { + run /bin/bash -c "dokku traefik:report $TEST_APP --traefik-api-entry-point" + echo "output: $output" + echo "status: $status" + assert_success + assert_output_not_exists + + run /bin/bash -c "dokku traefik:set --global api-entry-point private" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku traefik:report $TEST_APP --traefik-api-entry-point" + echo "output: $output" + echo "status: $status" + assert_success + assert_output "private" + + run /bin/bash -c "dokku traefik:set --global api-entry-point" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku traefik:report $TEST_APP --traefik-api-entry-point" + echo "output: $output" + echo "status: $status" + assert_success + assert_output_not_exists +} + +@test "(traefik) api-entry-point can only be set globally" { + run /bin/bash -c "dokku traefik:set $TEST_APP api-entry-point private" + echo "output: $output" + echo "status: $status" + assert_failure + assert_output_contains "can only be set globally" +} + +@test "(traefik) api-entry-point-address can only be set globally" { + run /bin/bash -c "dokku traefik:set $TEST_APP api-entry-point-address :8080" + echo "output: $output" + echo "status: $status" + assert_failure + assert_output_contains "can only be set globally" +} + +@test "(traefik) show-config with api-entry-point" { + run /bin/bash -c "dokku traefik:set --global api-enabled true" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku traefik:set --global api-entry-point private" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku traefik:set --global api-entry-point-address :8080" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku traefik:show-config | yq -r '.services.traefik.command[] | select(. == \"--entrypoints.private.address=:8080\")'" + echo "output: $output" + echo "status: $status" + assert_success + assert_output "--entrypoints.private.address=:8080" + + run /bin/bash -c "dokku traefik:show-config | yq -r '.services.traefik.labels[] | select(. == \"traefik.http.routers.api.entrypoints=private\")'" + echo "output: $output" + echo "status: $status" + assert_success + assert_output "traefik.http.routers.api.entrypoints=private" + + run /bin/bash -c "dokku traefik:show-config | yq -r '.services.traefik.ports[] | select(. == \":8080:8080\")'" + echo "output: $output" + echo "status: $status" + assert_success + assert_output ":8080:8080" + + # Cleanup + run /bin/bash -c "dokku traefik:set --global api-enabled" + run /bin/bash -c "dokku traefik:set --global api-entry-point" + run /bin/bash -c "dokku traefik:set --global api-entry-point-address" +} + +@test "(traefik) show-config with api-entry-point does not set tls cert resolver" { + run /bin/bash -c "dokku traefik:set --global letsencrypt-email test@example.com" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku traefik:set --global api-enabled true" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku traefik:set --global api-entry-point private" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku traefik:show-config | yq -r '.services.traefik.labels[] | select(contains(\"certresolver\"))'" + echo "output: $output" + echo "status: $status" + assert_success + assert_output_not_exists + + # Cleanup + run /bin/bash -c "dokku traefik:set --global letsencrypt-email" + run /bin/bash -c "dokku traefik:set --global api-enabled" + run /bin/bash -c "dokku traefik:set --global api-entry-point" +} + @test "(traefik) [dns-01] show-config with dns challenge" { run /bin/bash -c "dokku traefik:set --global letsencrypt-email test@example.com" echo "output: $output"