From 123fdc700c7b99723afb8b325fa7ac6721508e44 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Wed, 3 Jun 2026 03:56:37 -0400 Subject: [PATCH] fix: drop k3s-traefik priority, wrap caddy web reverse_proxy in handle The k3s-traefik IngressRoute was emitting priority: len(path) on the route rule but no priority on the bare-Host catch-all. Traefik defaults missing priority to rule length, so the catch-all rule got ~32 while the path rule got 7, and the catch-all always won. Same exact priority bug we fixed for docker-traefik. Dropping the priority line lets traefik use its rule-length default which gives longest-prefix-wins naturally because Host && PathPrefix is always longer than bare Host. The caddy web container's emission was a bare reverse_proxy directive that runs at caddy default order ~190, well before the handle directive at ~210 where my route emission lives. The bare reverse_proxy caught every request including paths intended for the route. When at least one route exists for the app, the web container now wraps its reverse_proxy in a catch-all handle block via caddy.9_handle.0_reverse_proxy=...; both the route's handle (at 0_) and web's catch-all (at 9_) live at the same directive order ~210 and caddy's mutually-exclusive handle logic picks the route handle for matched paths and the catch-all for everything else. The "9_" prefix orders web's catch-all after the route handles in the docker-proxy generated Caddyfile. Apps without routes keep the original bare reverse_proxy behaviour. --- .../caddy-vhosts/docker-args-process-deploy | 30 +++++++++++++++++-- .../templates/chart/ingress-route.yaml | 4 ++- tests/unit/proxy-route-caddy.bats | 8 ----- 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/plugins/caddy-vhosts/docker-args-process-deploy b/plugins/caddy-vhosts/docker-args-process-deploy index 655b94c3e..8d0076f0e 100755 --- a/plugins/caddy-vhosts/docker-args-process-deploy +++ b/plugins/caddy-vhosts/docker-args-process-deploy @@ -100,6 +100,21 @@ trigger-caddy-vhosts-docker-args-process-deploy() { caddy_domains="${caddy_domains// /, }" fi + # If any path-based route exists for this app, the route emission on the + # non-web container puts a `handle @` block in the site. For caddy to + # honor that, web's `reverse_proxy` must also be wrapped in a `handle` + # block (both at directive order ~210) so caddy's mutually-exclusive + # handle logic picks the route handle for matched paths and falls through + # to the catch-all handle for everything else. Without this, web's bare + # reverse_proxy (~190) catches every request before any handle runs. + local has_routes=false + while IFS='|' read -r route_proc _ _ _; do + if [[ -n "$route_proc" ]]; then + has_routes=true + break + fi + done < <(plugn trigger proxy-routes-list "$APP") + # add the labels for caddy here # prefer the https:443 mapping to http:80 mapping if [[ "$is_app_listening" == "true" ]] && [[ -n "$caddy_domains" ]]; then @@ -133,7 +148,14 @@ trigger-caddy-vhosts-docker-args-process-deploy() { proxy_container_https_port="$proxy_container_https_port_candidate" fi - output="$output --label \"caddy.reverse_proxy={{ upstreams $proxy_container_https_port }}\"" + if [[ "$has_routes" == "true" ]]; then + # The "9_" prefix orders web's catch-all handle after the route + # handles (which the non-web container emits with "0_") in the + # docker-proxy generated Caddyfile. + output="$output --label \"caddy.9_handle.0_reverse_proxy={{ upstreams $proxy_container_https_port }}\"" + else + output="$output --label \"caddy.reverse_proxy={{ upstreams $proxy_container_https_port }}\"" + fi elif [[ -n "$proxy_container_http_port" ]] || [[ -n "$proxy_container_http_port_candidate" ]]; then caddy_domains="${caddy_domains//, /:80, }" output="--label 'caddy=${caddy_domains}:80'" @@ -143,7 +165,11 @@ trigger-caddy-vhosts-docker-args-process-deploy() { proxy_container_http_port="$proxy_container_http_port_candidate" fi - output="$output --label \"caddy.reverse_proxy={{ upstreams $proxy_container_http_port }}\"" + if [[ "$has_routes" == "true" ]]; then + output="$output --label \"caddy.9_handle.0_reverse_proxy={{ upstreams $proxy_container_http_port }}\"" + else + output="$output --label \"caddy.reverse_proxy={{ upstreams $proxy_container_http_port }}\"" + fi fi if [[ "$output" == *"caddy.reverse_proxy="* ]]; then diff --git a/plugins/scheduler-k3s/templates/chart/ingress-route.yaml b/plugins/scheduler-k3s/templates/chart/ingress-route.yaml index 2977d3782..55ef5f0ea 100644 --- a/plugins/scheduler-k3s/templates/chart/ingress-route.yaml +++ b/plugins/scheduler-k3s/templates/chart/ingress-route.yaml @@ -41,7 +41,9 @@ spec: {{- range $rdx, $route := $.Values.global.network.routes }} - kind: Rule match: Host(`{{ $domain.name }}`) && PathPrefix(`{{ $route.path }}`) - priority: {{ len $route.path }} + # No explicit priority - traefik's default (rule length) gives + # longest-prefix-wins because Host(...) && PathPrefix(...) is always + # longer than web's bare Host(...) rule below. middlewares: - name: {{ $.Values.global.app_name}}-{{ $processName }}-compression namespace: {{ $.Values.global.namespace }} diff --git a/tests/unit/proxy-route-caddy.bats b/tests/unit/proxy-route-caddy.bats index 584cf7c7f..f64f6f412 100644 --- a/tests/unit/proxy-route-caddy.bats +++ b/tests/unit/proxy-route-caddy.bats @@ -45,14 +45,6 @@ teardown() { # Give the daemon a moment to read the new labels. sleep 5 - # Surface the generated Caddyfile so we can see how caddy-docker-proxy - # merged the web + api containers' labels and figure out why /api/v0/* - # routing isn't taking effect. - run /bin/bash -c "docker exec caddy-caddy-1 cat /etc/caddy/Caddyfile 2>&1" - echo "caddy config: $output" - run /bin/bash -c "docker exec caddy-caddy-1 caddy adapt --config /etc/caddy/Caddyfile 2>&1 | head -200" - echo "caddy adapt json: $output" - # Without --strip-prefix, /api/v0/Procfile reaches api as /api/v0/Procfile # which python's http.server returns 404 for. A 404 here proves the route # reaches api - web's catch-all would have returned 200.