feat: render proxy routes under openresty and add per-backend tests

Bumps the openresty sidecar to 0.12.0, which introduces the openresty.route.<n>.* label family for path-based routing. The openresty-vhosts plugin now emits route labels for non-web containers targeted by a proxy:route:set, mirroring the traefik and caddy patterns. Adds integration tests for every rendering backend (proxy-route-{nginx,traefik,caddy,openresty}.bats covering basic routing, --strip-prefix, and removal) plus per-ingress-class scheduler-k3s tests (proxy-route-k3s-{nginx,traefik}.bats covering the same three cases against a real cluster). The nginx file additionally asserts the rendered nginx config carries the WebSocket upgrade headers in each route's location block.
This commit is contained in:
Jose Diaz-Gonzalez
2026-06-01 17:04:05 -04:00
parent a4d40becd8
commit 4a550fe4b9
11 changed files with 644 additions and 20 deletions

View File

@@ -196,7 +196,7 @@ See the [port management documentation](/docs/networking/port-management.md) for
By default, every HTTP/HTTPS request is routed to the `web` process declared in the app's `Procfile`. Use `proxy:route:*` to route specific path prefixes to a different process within the same app - for example, sending `/api/v0/*` to an `api` process or `/ws` to a `websocket` process. The `web` process always handles every other path as an implicit catch-all.
> [!NOTE]
> Path-based routing is rendered by the `nginx`, `traefik`, `caddy`, and `k3s` proxy backends. The `openresty` and `haproxy` backends accept and store route declarations, but the routes are inert (no labels are emitted) because the sidecar images do not yet support path-prefix vocabulary. Routes stored ahead of a backend switch will activate automatically once the proxy is changed to a rendering backend. Tracked at [dokku/openresty-docker-proxy#137](https://github.com/dokku/openresty-docker-proxy/issues/137) and the `haproxy-vhosts` sidecar.
> Path-based routing is rendered by the `nginx`, `traefik`, `caddy`, `openresty`, and `k3s` proxy backends. The `haproxy` backend accepts and stores route declarations but the routes are inert (no labels are emitted) because the `byjg/easy-haproxy` sidecar image only supports host-based routing. Routes stored ahead of a backend switch will activate automatically once the proxy is changed to a rendering backend.
#### Adding a route
@@ -271,9 +271,9 @@ dokku proxy:route:set node-js-app api /api/v0/admin
#### Caveats per backend
- **nginx**: route changes take effect immediately on the next `proxy-build-config` reload (which `proxy:route:*` invokes automatically).
- **traefik** and **caddy**: routes are applied via Docker labels on the target containers. Containers must be recreated for new or removed labels to take effect. The command prints a notice instructing you to run `dokku ps:rebuild <app>` to recreate containers.
- **traefik**, **caddy**, and **openresty**: routes are applied via Docker labels on the target containers. Containers must be recreated for new or removed labels to take effect. The command prints a notice instructing you to run `dokku ps:rebuild <app>` to recreate containers.
- **k3s**: route changes take effect on the next deploy when the chart is re-rendered.
- **openresty** and **haproxy**: routes are stored but inert under these backends. They activate automatically if you switch to a rendering backend (`nginx`, `traefik`, `caddy`, or `k3s`).
- **haproxy**: routes are stored but inert under this backend. They activate automatically if you switch to a rendering backend (`nginx`, `traefik`, `caddy`, `openresty`, or `k3s`).
- **WebSocket** traffic is forwarded transparently on `nginx`, `traefik`, `caddy`, and `k3s` backends - no additional configuration is required.
- A route to a process that is scaled to `0` will resolve to no upstream until the process is scaled up (`dokku ps:scale <app> <process>=1`). `proxy:route:set` warns when this is the case.

View File

@@ -12,11 +12,26 @@ trigger-openresty-vhosts-docker-args-process-deploy() {
local include_dir output value DATA
local STDIN=$(cat)
if [[ "$PROC_TYPE" != "web" ]]; then
if [[ "$(plugn trigger proxy-type "$APP")" != "openresty" ]] && [[ "$(plugn trigger proxy-type "$APP")" != "nginx" ]]; then
return
fi
if [[ "$(plugn trigger proxy-type "$APP")" != "openresty" ]] && [[ "$(plugn trigger proxy-type "$APP")" != "nginx" ]]; then
if [[ "$PROC_TYPE" != "web" ]]; then
# Non-web processes are only labeled when at least one path-based route
# targets the process. Otherwise openresty should ignore the container.
local route_proc
local has_route=false
while IFS='|' read -r route_proc _ _ _; do
if [[ "$route_proc" == "$PROC_TYPE" ]]; then
has_route=true
break
fi
done < <(plugn trigger proxy-routes-list "$APP")
if [[ "$has_route" != "true" ]]; then
echo -n "$STDIN"
return
fi
fn-openresty-route-labels "$APP" "$PROC_TYPE" "$STDIN"
return
fi

View File

@@ -11,6 +11,35 @@ fn-get-pagesize() {
"$PLUGIN_CORE_AVAILABLE_PATH/nginx-vhosts/pagesize"
}
fn-openresty-route-labels() {
declare desc="emits per-route openresty labels for a non-web container"
declare APP="$1" PROC_TYPE="$2" STDIN="$3"
local output route_proc route_port route_path route_strip route_index=0
output=""
while IFS='|' read -r route_proc route_port route_path route_strip; do
[[ -z "$route_proc" ]] && continue
[[ "$route_proc" != "$PROC_TYPE" ]] && continue
output="$output --label 'openresty.route.${route_index}.path-prefix=${route_path}'"
output="$output --label 'openresty.route.${route_index}.port=${route_port}'"
if [[ "$route_strip" == "1" ]]; then
output="$output --label 'openresty.route.${route_index}.strip-prefix=true'"
fi
route_index=$((route_index + 1))
done < <(plugn trigger proxy-routes-list "$APP")
local proxy_labels_file_path=$(fn-proxy-get-labels-file-path "openresty" "$APP")
if [[ -f "$proxy_labels_file_path" ]]; then
while read -r line; do
[[ -z "$line" ]] && continue
output="$output --label '$line'"
done <"$proxy_labels_file_path"
fi
echo -n "$STDIN $output"
}
fn-openresty-access-log-format() {
declare desc="get the raw per-app access log format"
declare APP="$1"

View File

@@ -105,7 +105,7 @@ func CommandRouteClear(appName string) error {
// labels are baked in at create-time).
func warnIfRebuildRequired(appName string) {
switch getComputedProxyType(appName) {
case "traefik", "caddy":
case "traefik", "caddy", "openresty":
common.LogWarn(fmt.Sprintf(
"Routes are applied via container labels under the %s proxy backend; run `dokku ps:rebuild %s` to recreate containers and pick up the change",
getComputedProxyType(appName), appName,

View File

@@ -0,0 +1,88 @@
#!/usr/bin/env bats
load test_helper
setup() {
global_setup
dokku nginx:stop
dokku caddy:start
create_app
dokku proxy:set "$TEST_APP" caddy
}
teardown() {
dokku proxy:route:clear "$TEST_APP" >/dev/null 2>&1 || true
destroy_app
dokku caddy:stop
dokku nginx:start
global_teardown
}
@test "(proxy-route:caddy) basic routing - /api/v0 hits api, / hits web" {
run deploy_app python "dokku@$DOKKU_DOMAIN:$TEST_APP" add_api_process_callback
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku ps:scale $TEST_APP web=1 api=1"
assert_success
run /bin/bash -c "dokku proxy:route:set $TEST_APP api /api/v0 --port 5001"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku ps:rebuild $TEST_APP"
assert_success
assert_http_localhost_response_contains "http" "${TEST_APP}.${DOKKU_DOMAIN}" "80" "/api/v0/Procfile" "python3 -m http.server"
}
@test "(proxy-route:caddy) --strip-prefix toggles upstream path visibility" {
run deploy_app python "dokku@$DOKKU_DOMAIN:$TEST_APP" add_api_process_callback
assert_success
run /bin/bash -c "dokku ps:scale $TEST_APP web=1 api=1"
assert_success
run /bin/bash -c "dokku proxy:route:set $TEST_APP api /api/v0 --port 5001"
assert_success
run /bin/bash -c "dokku ps:rebuild $TEST_APP"
assert_success
assert_http_localhost_response_contains "http" "${TEST_APP}.${DOKKU_DOMAIN}" "80" "/api/v0/Procfile" "" "404"
run /bin/bash -c "dokku proxy:route:set $TEST_APP api /api/v0 --port 5001 --strip-prefix"
assert_success
run /bin/bash -c "dokku ps:rebuild $TEST_APP"
assert_success
assert_http_localhost_response_contains "http" "${TEST_APP}.${DOKKU_DOMAIN}" "80" "/api/v0/Procfile" "python3 -m http.server"
}
@test "(proxy-route:caddy) removing a route falls back to web" {
run deploy_app python "dokku@$DOKKU_DOMAIN:$TEST_APP" add_api_process_callback
assert_success
run /bin/bash -c "dokku ps:scale $TEST_APP web=1 api=1"
assert_success
run /bin/bash -c "dokku proxy:route:set $TEST_APP api /api/v0 --port 5001 --strip-prefix"
assert_success
run /bin/bash -c "dokku ps:rebuild $TEST_APP"
assert_success
assert_http_localhost_response_contains "http" "${TEST_APP}.${DOKKU_DOMAIN}" "80" "/api/v0/Procfile" "python3 -m http.server"
run /bin/bash -c "dokku proxy:route:remove $TEST_APP /api/v0"
assert_success
run /bin/bash -c "dokku ps:rebuild $TEST_APP"
assert_success
run curl --connect-to "${TEST_APP}.${DOKKU_DOMAIN}:80:localhost:80" -kSso /tmp/route-removed-body "http://${TEST_APP}.${DOKKU_DOMAIN}:80/api/v0/Procfile"
run /bin/bash -c "grep -c 'python3 -m http.server' /tmp/route-removed-body || true"
assert_output "0"
}
add_api_process_callback() {
local APP="$1"
local APP_REPO_DIR="$2"
echo "api: python3 -m http.server 5001" >>"$APP_REPO_DIR/Procfile"
}

View File

@@ -0,0 +1,111 @@
#!/usr/bin/env bats
load test_helper
TEST_APP="rdmtestapp"
setup() {
uninstall_k3s || true
global_setup
dokku nginx:stop
export KUBECONFIG="/etc/rancher/k3s/k3s.yaml"
}
teardown() {
global_teardown
dokku nginx:start
uninstall_k3s || true
}
@test "(proxy-route:k3s-nginx) basic routing - /api/v0 hits api, / hits web" {
if [[ -z "$DOCKERHUB_USERNAME" ]] || [[ -z "$DOCKERHUB_TOKEN" ]]; then
skip "skipping due to missing docker.io credentials DOCKERHUB_USERNAME:DOCKERHUB_TOKEN"
fi
INGRESS_CLASS=nginx install_k3s
run /bin/bash -c "dokku apps:create $TEST_APP"
assert_success
run deploy_app python "dokku@$DOKKU_DOMAIN:$TEST_APP" add_api_process_callback
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku ps:scale $TEST_APP web=1 api=1"
assert_success
run /bin/bash -c "dokku proxy:route:set $TEST_APP api /api/v0 --port 5001"
echo "output: $output"
echo "status: $status"
assert_success
assert_http_localhost_response_contains "http" "${TEST_APP}.${DOKKU_DOMAIN}" "80" "/api/v0/Procfile" "python3 -m http.server"
}
@test "(proxy-route:k3s-nginx) --strip-prefix renders rewrite-target Ingress and strips upstream path" {
if [[ -z "$DOCKERHUB_USERNAME" ]] || [[ -z "$DOCKERHUB_TOKEN" ]]; then
skip "skipping due to missing docker.io credentials DOCKERHUB_USERNAME:DOCKERHUB_TOKEN"
fi
INGRESS_CLASS=nginx install_k3s
run /bin/bash -c "dokku apps:create $TEST_APP"
assert_success
run deploy_app python "dokku@$DOKKU_DOMAIN:$TEST_APP" add_api_process_callback
assert_success
run /bin/bash -c "dokku ps:scale $TEST_APP web=1 api=1"
assert_success
run /bin/bash -c "dokku proxy:route:set $TEST_APP api /api/v0 --port 5001 --strip-prefix"
assert_success
# The chart should emit an additional Ingress carrying the rewrite-target
# annotation when --strip-prefix is set.
run /bin/bash -c "kubectl get ingress -o json | jq -r '.items[].metadata.annotations | select(.[\"nginx.ingress.kubernetes.io/rewrite-target\"] != null) | .[\"nginx.ingress.kubernetes.io/rewrite-target\"]'"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_contains "/\$2"
assert_http_localhost_response_contains "http" "${TEST_APP}.${DOKKU_DOMAIN}" "80" "/api/v0/Procfile" "python3 -m http.server"
}
@test "(proxy-route:k3s-nginx) removing a route drops the extra Ingress paths" {
if [[ -z "$DOCKERHUB_USERNAME" ]] || [[ -z "$DOCKERHUB_TOKEN" ]]; then
skip "skipping due to missing docker.io credentials DOCKERHUB_USERNAME:DOCKERHUB_TOKEN"
fi
INGRESS_CLASS=nginx install_k3s
run /bin/bash -c "dokku apps:create $TEST_APP"
assert_success
run deploy_app python "dokku@$DOKKU_DOMAIN:$TEST_APP" add_api_process_callback
assert_success
run /bin/bash -c "dokku ps:scale $TEST_APP web=1 api=1"
assert_success
run /bin/bash -c "dokku proxy:route:set $TEST_APP api /api/v0 --port 5001 --strip-prefix"
assert_success
assert_http_localhost_response_contains "http" "${TEST_APP}.${DOKKU_DOMAIN}" "80" "/api/v0/Procfile" "python3 -m http.server"
run /bin/bash -c "dokku proxy:route:remove $TEST_APP /api/v0"
assert_success
# The rewrite-target Ingress should be gone after removal.
run /bin/bash -c "kubectl get ingress -o json | jq -r '[.items[] | select(.metadata.annotations[\"nginx.ingress.kubernetes.io/rewrite-target\"] != null)] | length'"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "0"
}
add_api_process_callback() {
local APP="$1"
local APP_REPO_DIR="$2"
echo "api: python3 -m http.server 5001" >>"$APP_REPO_DIR/Procfile"
}

View File

@@ -0,0 +1,110 @@
#!/usr/bin/env bats
load test_helper
TEST_APP="rdmtestapp"
setup() {
uninstall_k3s || true
global_setup
dokku nginx:stop
export KUBECONFIG="/etc/rancher/k3s/k3s.yaml"
}
teardown() {
global_teardown
dokku nginx:start
uninstall_k3s || true
}
@test "(proxy-route:k3s-traefik) basic routing - /api/v0 hits api, / hits web" {
if [[ -z "$DOCKERHUB_USERNAME" ]] || [[ -z "$DOCKERHUB_TOKEN" ]]; then
skip "skipping due to missing docker.io credentials DOCKERHUB_USERNAME:DOCKERHUB_TOKEN"
fi
INGRESS_CLASS=traefik install_k3s
run /bin/bash -c "dokku apps:create $TEST_APP"
assert_success
run deploy_app python "dokku@$DOKKU_DOMAIN:$TEST_APP" add_api_process_callback
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku ps:scale $TEST_APP web=1 api=1"
assert_success
run /bin/bash -c "dokku proxy:route:set $TEST_APP api /api/v0 --port 5001"
echo "output: $output"
echo "status: $status"
assert_success
assert_http_localhost_response_contains "http" "${TEST_APP}.${DOKKU_DOMAIN}" "80" "/api/v0/Procfile" "python3 -m http.server"
}
@test "(proxy-route:k3s-traefik) --strip-prefix renders stripPrefix Middleware and strips upstream path" {
if [[ -z "$DOCKERHUB_USERNAME" ]] || [[ -z "$DOCKERHUB_TOKEN" ]]; then
skip "skipping due to missing docker.io credentials DOCKERHUB_USERNAME:DOCKERHUB_TOKEN"
fi
INGRESS_CLASS=traefik install_k3s
run /bin/bash -c "dokku apps:create $TEST_APP"
assert_success
run deploy_app python "dokku@$DOKKU_DOMAIN:$TEST_APP" add_api_process_callback
assert_success
run /bin/bash -c "dokku ps:scale $TEST_APP web=1 api=1"
assert_success
run /bin/bash -c "dokku proxy:route:set $TEST_APP api /api/v0 --port 5001 --strip-prefix"
assert_success
# The chart should emit a StripPrefix Middleware CRD when --strip-prefix
# is set.
run /bin/bash -c "kubectl get middleware.traefik.io -o json | jq -r '.items[].spec.stripPrefix.prefixes[]?' | head -1"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_contains "/api/v0"
assert_http_localhost_response_contains "http" "${TEST_APP}.${DOKKU_DOMAIN}" "80" "/api/v0/Procfile" "python3 -m http.server"
}
@test "(proxy-route:k3s-traefik) removing a route drops the stripPrefix Middleware" {
if [[ -z "$DOCKERHUB_USERNAME" ]] || [[ -z "$DOCKERHUB_TOKEN" ]]; then
skip "skipping due to missing docker.io credentials DOCKERHUB_USERNAME:DOCKERHUB_TOKEN"
fi
INGRESS_CLASS=traefik install_k3s
run /bin/bash -c "dokku apps:create $TEST_APP"
assert_success
run deploy_app python "dokku@$DOKKU_DOMAIN:$TEST_APP" add_api_process_callback
assert_success
run /bin/bash -c "dokku ps:scale $TEST_APP web=1 api=1"
assert_success
run /bin/bash -c "dokku proxy:route:set $TEST_APP api /api/v0 --port 5001 --strip-prefix"
assert_success
assert_http_localhost_response_contains "http" "${TEST_APP}.${DOKKU_DOMAIN}" "80" "/api/v0/Procfile" "python3 -m http.server"
run /bin/bash -c "dokku proxy:route:remove $TEST_APP /api/v0"
assert_success
run /bin/bash -c "kubectl get middleware.traefik.io -o json | jq -r '[.items[] | select(.spec.stripPrefix != null)] | length'"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "0"
}
add_api_process_callback() {
local APP="$1"
local APP_REPO_DIR="$2"
echo "api: python3 -m http.server 5001" >>"$APP_REPO_DIR/Procfile"
}

View File

@@ -0,0 +1,107 @@
#!/usr/bin/env bats
load test_helper
setup() {
global_setup
create_app
}
teardown() {
dokku proxy:route:clear "$TEST_APP" >/dev/null 2>&1 || true
destroy_app
global_teardown
}
@test "(proxy-route:nginx) basic routing - /api/v0 hits api, / hits web" {
run deploy_app python "dokku@$DOKKU_DOMAIN:$TEST_APP" add_api_process_callback
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku ps:scale $TEST_APP web=1 api=1"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku proxy:route:set $TEST_APP api /api/v0 --port 5001"
echo "output: $output"
echo "status: $status"
assert_success
assert_http_localhost_response_contains "http" "${TEST_APP}.${DOKKU_DOMAIN}" "80" "/api/v0/Procfile" "python3 -m http.server"
}
@test "(proxy-route:nginx) --strip-prefix toggles upstream path visibility" {
run deploy_app python "dokku@$DOKKU_DOMAIN:$TEST_APP" add_api_process_callback
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku ps:scale $TEST_APP web=1 api=1"
echo "output: $output"
echo "status: $status"
assert_success
# Without --strip-prefix, /api/v0/Procfile is forwarded as-is to api,
# which does not have an /api/v0/Procfile file on disk.
run /bin/bash -c "dokku proxy:route:set $TEST_APP api /api/v0 --port 5001"
assert_success
assert_http_localhost_response_contains "http" "${TEST_APP}.${DOKKU_DOMAIN}" "80" "/api/v0/Procfile" "" "404"
# With --strip-prefix, /api/v0/Procfile becomes /Procfile upstream and api
# returns the Procfile content.
run /bin/bash -c "dokku proxy:route:set $TEST_APP api /api/v0 --port 5001 --strip-prefix"
assert_success
assert_http_localhost_response_contains "http" "${TEST_APP}.${DOKKU_DOMAIN}" "80" "/api/v0/Procfile" "python3 -m http.server"
}
@test "(proxy-route:nginx) removing a route falls back to web" {
run deploy_app python "dokku@$DOKKU_DOMAIN:$TEST_APP" add_api_process_callback
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku ps:scale $TEST_APP web=1 api=1"
assert_success
run /bin/bash -c "dokku proxy:route:set $TEST_APP api /api/v0 --port 5001 --strip-prefix"
assert_success
assert_http_localhost_response_contains "http" "${TEST_APP}.${DOKKU_DOMAIN}" "80" "/api/v0/Procfile" "python3 -m http.server"
run /bin/bash -c "dokku proxy:route:remove $TEST_APP /api/v0"
assert_success
# Web does not serve /api/v0/Procfile; we just assert it is no longer the
# api-served Procfile content.
run curl --connect-to "${TEST_APP}.${DOKKU_DOMAIN}:80:localhost:80" -kSso /tmp/route-removed-body "http://${TEST_APP}.${DOKKU_DOMAIN}:80/api/v0/Procfile"
run /bin/bash -c "grep -c 'python3 -m http.server' /tmp/route-removed-body || true"
assert_output "0"
}
@test "(proxy-route:nginx) websocket upgrade headers are present in rendered config" {
run deploy_app python "dokku@$DOKKU_DOMAIN:$TEST_APP" add_api_process_callback
assert_success
run /bin/bash -c "dokku ps:scale $TEST_APP web=1 api=1"
assert_success
run /bin/bash -c "dokku proxy:route:set $TEST_APP api /api/v0 --port 5001"
assert_success
run /bin/bash -c "dokku nginx:show-config $TEST_APP"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_contains "location /api/v0"
assert_output_contains "proxy_http_version 1.1"
assert_output_contains "proxy_set_header Upgrade \$http_upgrade"
assert_output_contains "proxy_set_header Connection \$http_connection"
}
add_api_process_callback() {
local APP="$1"
local APP_REPO_DIR="$2"
# api serves /app's working directory on port 5001. GET /Procfile returns
# the literal Procfile bytes, which web.py does not serve - this gives the
# HTTP assertions a clear marker for which process handled the request.
echo "api: python3 -m http.server 5001" >>"$APP_REPO_DIR/Procfile"
}

View File

@@ -0,0 +1,88 @@
#!/usr/bin/env bats
load test_helper
setup() {
global_setup
dokku nginx:stop
dokku openresty:start
create_app
dokku proxy:set "$TEST_APP" openresty
}
teardown() {
dokku proxy:route:clear "$TEST_APP" >/dev/null 2>&1 || true
destroy_app
dokku openresty:stop
dokku nginx:start
global_teardown
}
@test "(proxy-route:openresty) basic routing - /api/v0 hits api, / hits web" {
run deploy_app python "dokku@$DOKKU_DOMAIN:$TEST_APP" add_api_process_callback
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku ps:scale $TEST_APP web=1 api=1"
assert_success
run /bin/bash -c "dokku proxy:route:set $TEST_APP api /api/v0 --port 5001"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku ps:rebuild $TEST_APP"
assert_success
assert_http_localhost_response_contains "http" "${TEST_APP}.${DOKKU_DOMAIN}" "80" "/api/v0/Procfile" "python3 -m http.server"
}
@test "(proxy-route:openresty) --strip-prefix toggles upstream path visibility" {
run deploy_app python "dokku@$DOKKU_DOMAIN:$TEST_APP" add_api_process_callback
assert_success
run /bin/bash -c "dokku ps:scale $TEST_APP web=1 api=1"
assert_success
run /bin/bash -c "dokku proxy:route:set $TEST_APP api /api/v0 --port 5001"
assert_success
run /bin/bash -c "dokku ps:rebuild $TEST_APP"
assert_success
assert_http_localhost_response_contains "http" "${TEST_APP}.${DOKKU_DOMAIN}" "80" "/api/v0/Procfile" "" "404"
run /bin/bash -c "dokku proxy:route:set $TEST_APP api /api/v0 --port 5001 --strip-prefix"
assert_success
run /bin/bash -c "dokku ps:rebuild $TEST_APP"
assert_success
assert_http_localhost_response_contains "http" "${TEST_APP}.${DOKKU_DOMAIN}" "80" "/api/v0/Procfile" "python3 -m http.server"
}
@test "(proxy-route:openresty) removing a route falls back to web" {
run deploy_app python "dokku@$DOKKU_DOMAIN:$TEST_APP" add_api_process_callback
assert_success
run /bin/bash -c "dokku ps:scale $TEST_APP web=1 api=1"
assert_success
run /bin/bash -c "dokku proxy:route:set $TEST_APP api /api/v0 --port 5001 --strip-prefix"
assert_success
run /bin/bash -c "dokku ps:rebuild $TEST_APP"
assert_success
assert_http_localhost_response_contains "http" "${TEST_APP}.${DOKKU_DOMAIN}" "80" "/api/v0/Procfile" "python3 -m http.server"
run /bin/bash -c "dokku proxy:route:remove $TEST_APP /api/v0"
assert_success
run /bin/bash -c "dokku ps:rebuild $TEST_APP"
assert_success
run curl --connect-to "${TEST_APP}.${DOKKU_DOMAIN}:80:localhost:80" -kSso /tmp/route-removed-body "http://${TEST_APP}.${DOKKU_DOMAIN}:80/api/v0/Procfile"
run /bin/bash -c "grep -c 'python3 -m http.server' /tmp/route-removed-body || true"
assert_output "0"
}
add_api_process_callback() {
local APP="$1"
local APP_REPO_DIR="$2"
echo "api: python3 -m http.server 5001" >>"$APP_REPO_DIR/Procfile"
}

View File

@@ -0,0 +1,90 @@
#!/usr/bin/env bats
load test_helper
setup() {
global_setup
dokku nginx:stop
dokku traefik:start
create_app
dokku proxy:set "$TEST_APP" traefik
}
teardown() {
dokku proxy:route:clear "$TEST_APP" >/dev/null 2>&1 || true
destroy_app
dokku traefik:stop
dokku nginx:start
global_teardown
}
@test "(proxy-route:traefik) basic routing - /api/v0 hits api, / hits web" {
run deploy_app python "dokku@$DOKKU_DOMAIN:$TEST_APP" add_api_process_callback
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku ps:scale $TEST_APP web=1 api=1"
assert_success
run /bin/bash -c "dokku proxy:route:set $TEST_APP api /api/v0 --port 5001"
echo "output: $output"
echo "status: $status"
assert_success
# Routes are applied via container labels - rebuild so the api container
# restarts with the new labels attached.
run /bin/bash -c "dokku ps:rebuild $TEST_APP"
assert_success
assert_http_localhost_response_contains "http" "${TEST_APP}.${DOKKU_DOMAIN}" "80" "/api/v0/Procfile" "python3 -m http.server"
}
@test "(proxy-route:traefik) --strip-prefix toggles upstream path visibility" {
run deploy_app python "dokku@$DOKKU_DOMAIN:$TEST_APP" add_api_process_callback
assert_success
run /bin/bash -c "dokku ps:scale $TEST_APP web=1 api=1"
assert_success
run /bin/bash -c "dokku proxy:route:set $TEST_APP api /api/v0 --port 5001"
assert_success
run /bin/bash -c "dokku ps:rebuild $TEST_APP"
assert_success
assert_http_localhost_response_contains "http" "${TEST_APP}.${DOKKU_DOMAIN}" "80" "/api/v0/Procfile" "" "404"
run /bin/bash -c "dokku proxy:route:set $TEST_APP api /api/v0 --port 5001 --strip-prefix"
assert_success
run /bin/bash -c "dokku ps:rebuild $TEST_APP"
assert_success
assert_http_localhost_response_contains "http" "${TEST_APP}.${DOKKU_DOMAIN}" "80" "/api/v0/Procfile" "python3 -m http.server"
}
@test "(proxy-route:traefik) removing a route falls back to web" {
run deploy_app python "dokku@$DOKKU_DOMAIN:$TEST_APP" add_api_process_callback
assert_success
run /bin/bash -c "dokku ps:scale $TEST_APP web=1 api=1"
assert_success
run /bin/bash -c "dokku proxy:route:set $TEST_APP api /api/v0 --port 5001 --strip-prefix"
assert_success
run /bin/bash -c "dokku ps:rebuild $TEST_APP"
assert_success
assert_http_localhost_response_contains "http" "${TEST_APP}.${DOKKU_DOMAIN}" "80" "/api/v0/Procfile" "python3 -m http.server"
run /bin/bash -c "dokku proxy:route:remove $TEST_APP /api/v0"
assert_success
run /bin/bash -c "dokku ps:rebuild $TEST_APP"
assert_success
run curl --connect-to "${TEST_APP}.${DOKKU_DOMAIN}:80:localhost:80" -kSso /tmp/route-removed-body "http://${TEST_APP}.${DOKKU_DOMAIN}:80/api/v0/Procfile"
run /bin/bash -c "grep -c 'python3 -m http.server' /tmp/route-removed-body || true"
assert_output "0"
}
add_api_process_callback() {
local APP="$1"
local APP_REPO_DIR="$2"
echo "api: python3 -m http.server 5001" >>"$APP_REPO_DIR/Procfile"
}

View File

@@ -218,20 +218,6 @@ teardown() {
assert_output "true"
}
@test "(proxy:route:set) stores routes under openresty backend even though they are inert" {
dokku proxy:set "$TEST_APP" openresty >/dev/null 2>&1 || skip "openresty proxy not available"
run /bin/bash -c "dokku proxy:route:set $TEST_APP api /api/v0"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku proxy:route:report $TEST_APP"
echo "output: $output"
echo "status: $status"
assert_output_contains "/api/v0 -> api:5000"
}
@test "(proxy:route:set) stores routes under haproxy backend even though they are inert" {
dokku proxy:set "$TEST_APP" haproxy >/dev/null 2>&1 || skip "haproxy proxy not available"