From 53066ec4ac8dabe484c17731ad0facb98117a3d1 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Tue, 2 Jun 2026 22:07:17 -0400 Subject: [PATCH] fix: traefik priority loss, caddy directive order, openresty cache, k3s diagnostic Traefik was setting an explicit priority equal to the path length, which is smaller than traefik's default priority (rule length) on the web container. Web's bare Host(...) rule has implicit priority ~40 while my explicit 7 lost the match, so /api/v0/Procfile always went to web. Dropping the explicit priority lets traefik use rule length, which already gives longest-prefix-wins because the route rule includes both Host and PathPrefix and so is always longer than web's bare Host(...). Caddy's route block was being preempted by web's bare reverse_proxy due to caddy's default directive order (reverse_proxy ~190, route ~210). Switching to handle, which terminates matched requests and isn't bypassed by reverse_proxy, makes /api/v0/* land on api instead of web's catch-all. Openresty T2 polls longer (60 retries) and dumps sidecar logs on failure so the upstream label-update cache delay can be reported. The k3s files dump kubectl get service/ingress/endpoints in T1 so the next CI run reveals whether the api Service has ports and whether the Ingress backend reference is correct. --- plugins/caddy-vhosts/internal-functions | 9 ++++++--- plugins/traefik-vhosts/internal-functions | 4 +++- tests/unit/proxy-route-k3s-nginx.bats | 9 +++++++++ tests/unit/proxy-route-k3s-traefik.bats | 9 +++++++++ tests/unit/proxy-route-openresty.bats | 12 +++++++++--- 5 files changed, 36 insertions(+), 7 deletions(-) diff --git a/plugins/caddy-vhosts/internal-functions b/plugins/caddy-vhosts/internal-functions index 76ce0f5bc..472a4b684 100755 --- a/plugins/caddy-vhosts/internal-functions +++ b/plugins/caddy-vhosts/internal-functions @@ -34,13 +34,16 @@ fn-caddy-route-labels() { # The named matcher accepts both the bare prefix and the prefix-glob form # so requests like /api/v0 and /api/v0/users/42 both match. output="$output --label 'caddy.${matcher_name}.path=${route_path} ${route_path}/*'" - output="$output --label 'caddy.${route_index}_route=${matcher_name}'" + # Use handle (not route) so the directive terminates the matched request. + # Otherwise web's bare `reverse_proxy` (at default-order ~190) runs before + # this route block (~210) and catches everything including /api/v0/*. + output="$output --label 'caddy.${route_index}_handle=${matcher_name}'" local sub_index=0 if [[ "$route_strip" == "1" ]]; then - output="$output --label 'caddy.${route_index}_route.${sub_index}_uri=strip_prefix ${route_path}'" + output="$output --label 'caddy.${route_index}_handle.${sub_index}_uri=strip_prefix ${route_path}'" sub_index=$((sub_index + 1)) fi - output="$output --label \"caddy.${route_index}_route.${sub_index}_reverse_proxy={{upstreams ${route_port}}}\"" + output="$output --label \"caddy.${route_index}_handle.${sub_index}_reverse_proxy={{upstreams ${route_port}}}\"" route_index=$((route_index + 1)) done < <(plugn trigger proxy-routes-list "$APP") diff --git a/plugins/traefik-vhosts/internal-functions b/plugins/traefik-vhosts/internal-functions index 8d4fa5229..10dbd601e 100755 --- a/plugins/traefik-vhosts/internal-functions +++ b/plugins/traefik-vhosts/internal-functions @@ -31,7 +31,9 @@ fn-traefik-route-labels() { output="$output --label \"traefik.http.routers.${router_name}.rule=${rule}\"" output="$output --label traefik.http.routers.${router_name}.entrypoints=$(fn-traefik-computed-http-entry-point)" - output="$output --label traefik.http.routers.${router_name}.priority=${path_len}" + # No explicit priority - traefik's default (rule length) gives + # longest-prefix-wins because our rule includes both Host(...) and + # PathPrefix(...), which is always longer than web's bare Host(...) rule. output="$output --label traefik.http.routers.${router_name}.service=${service_name}" output="$output --label traefik.http.services.${service_name}.loadbalancer.server.port=${route_port}" diff --git a/tests/unit/proxy-route-k3s-nginx.bats b/tests/unit/proxy-route-k3s-nginx.bats index ddcda9d0b..bf9b620dd 100644 --- a/tests/unit/proxy-route-k3s-nginx.bats +++ b/tests/unit/proxy-route-k3s-nginx.bats @@ -40,6 +40,15 @@ teardown() { echo "status: $status" assert_success + # Diagnostic: dump the cluster state so CI logs show whether the api + # Service and the Ingress are wired correctly. + run /bin/bash -c "kubectl get service -o yaml 2>&1" + echo "services: $output" + run /bin/bash -c "kubectl get ingress -o yaml 2>&1" + echo "ingresses: $output" + run /bin/bash -c "kubectl get endpoints -o yaml 2>&1" + echo "endpoints: $output" + # Without --strip-prefix, /api/v0/Procfile reaches api as /api/v0/Procfile # which python's http.server returns 404 for. 404 proves the route reaches # api (web's catch-all would have returned 200). diff --git a/tests/unit/proxy-route-k3s-traefik.bats b/tests/unit/proxy-route-k3s-traefik.bats index 89d6e0243..5e3661d3f 100644 --- a/tests/unit/proxy-route-k3s-traefik.bats +++ b/tests/unit/proxy-route-k3s-traefik.bats @@ -40,6 +40,15 @@ teardown() { echo "status: $status" assert_success + # Diagnostic: dump the cluster state so CI logs show whether the api + # Service and the IngressRoute are wired correctly. + run /bin/bash -c "kubectl get service -o yaml 2>&1" + echo "services: $output" + run /bin/bash -c "kubectl get ingressroute.traefik.io -o yaml 2>&1" + echo "ingressroutes: $output" + run /bin/bash -c "kubectl get endpoints -o yaml 2>&1" + echo "endpoints: $output" + # Without --strip-prefix, /api/v0/Procfile reaches api as /api/v0/Procfile # which python's http.server returns 404 for. 404 proves the route reaches # api (web's catch-all would have returned 200). diff --git a/tests/unit/proxy-route-openresty.bats b/tests/unit/proxy-route-openresty.bats index 3a43e14d4..98b02d5d3 100644 --- a/tests/unit/proxy-route-openresty.bats +++ b/tests/unit/proxy-route-openresty.bats @@ -86,9 +86,15 @@ teardown() { run /bin/bash -c "docker inspect ${TEST_APP}.api.1 --format '{{json .Config.Labels}}' 2>&1 | jq . 2>/dev/null || docker inspect ${TEST_APP}.api.1 --format '{{json .Config.Labels}}' 2>&1" echo "api labels: $output" - # Give the daemon a moment to read the new labels. - sleep 5 - assert_http_localhost_response_contains "http" "${TEST_APP}.${DOKKU_DOMAIN}" "80" "/api/v0/Procfile" "python3 -m http.server" + # openresty-docker-proxy 0.12.0 has a polling/caching delay before it + # re-renders the config after labels on an existing route key change. + # Give it a longer window than the default HTTP retry. If this still + # times out, dump the sidecar logs so we can file an upstream bug. + HTTP_ASSERT_RETRIES=60 assert_http_localhost_response_contains "http" "${TEST_APP}.${DOKKU_DOMAIN}" "80" "/api/v0/Procfile" "python3 -m http.server" || { + run /bin/bash -c "dokku openresty:logs --num 200 2>&1 || docker logs openresty-openresty-1 --tail 200 2>&1" + echo "openresty sidecar logs: $output" + return 1 + } } @test "(proxy-route:openresty) removing a route falls back to web" {