feat: route paths to non-web processes via proxy:route

Adds proxy:route:set/remove/clear/report so users can declare path-prefix routes to non-web Procfile processes without hand-writing nginx.conf.sigil. Routes are stored on the proxy plugin as a PropertyMap of path to process:port:strip, with set semantics so each invocation produces a fully-determined state and re-application is idempotent. A proxy-supports-routes capability trigger gates the command per backend - nginx-vhosts, traefik-vhosts, caddy-vhosts, and scheduler-k3s render routes today; openresty-vhosts and haproxy-vhosts refuse until their sidecar images learn a path-prefix vocabulary (tracked in dokku/openresty-docker-proxy#137 and the haproxy sidecar). Closes #8725.
This commit is contained in:
Jose Diaz-Gonzalez
2026-06-01 13:48:17 -04:00
parent 2cd558e102
commit ffd5248cc3
29 changed files with 1539 additions and 7 deletions

View File

@@ -236,6 +236,33 @@ fn-nginx-vhosts-render-template() {
PROXY_PORT_MAP="$(echo "$proxy_port_map" | xargs)" # trailing spaces mess up default template
PROXY_UPSTREAM_PORTS="$(echo "$PROXY_UPSTREAM_PORTS" | xargs)"
# Path-based routes (proxy:route:set). Format the storage representation
# into space-separated sigil variables: PROXY_ROUTES holds the route triples
# (process|port|path|strip), PROXY_ROUTE_LISTENERS holds one
# (process|port|listener-ip,listener-ip,...) entry per unique process+port
# pair so the template can render per-route upstream blocks.
local PROXY_ROUTES="" PROXY_ROUTE_LISTENERS=""
local route_pair_seen=" "
while IFS='|' read -r route_proc route_port route_path route_strip; do
[[ -z "$route_proc" ]] && continue
PROXY_ROUTES="${PROXY_ROUTES} ${route_proc}|${route_port}|${route_path}|${route_strip}"
local route_pair="${route_proc}|${route_port}"
if [[ "$route_pair_seen" != *" ${route_pair} "* ]]; then
route_pair_seen="${route_pair_seen}${route_pair} "
local route_listeners="$(plugn trigger network-get-listeners "$APP" "$route_proc" | xargs)"
if [[ -z "$route_listeners" ]]; then
# No live containers for this process yet; emit a stub so nginx -t
# still passes during pre-validate and the upstream block renders
# without a host-not-found error at reload time.
route_listeners="127.0.0.1:${route_port}"
fi
local route_listener_csv="${route_listeners// /,}"
PROXY_ROUTE_LISTENERS="${PROXY_ROUTE_LISTENERS} ${route_proc}|${route_port}|${route_listener_csv}"
fi
done < <(plugn trigger proxy-routes-list "$APP")
PROXY_ROUTES="$(echo "$PROXY_ROUTES" | xargs)"
PROXY_ROUTE_LISTENERS="$(echo "$PROXY_ROUTE_LISTENERS" | xargs)"
local SSL_INUSE=
local NONSSL_VHOSTS=$(plugn trigger domains-list "$APP")
local NOSSL_SERVER_NAME=$(echo "$NONSSL_VHOSTS" | xargs)
@@ -352,6 +379,7 @@ fn-nginx-vhosts-render-template() {
NGINX_PORT="$PROXY_PORT" NGINX_SSL_PORT="$PROXY_SSL_PORT"
PROXY_PORT="$PROXY_PORT" PROXY_SSL_PORT="$PROXY_SSL_PORT"
PROXY_PORT_MAP="$PROXY_PORT_MAP" PROXY_UPSTREAM_PORTS="$PROXY_UPSTREAM_PORTS"
PROXY_ROUTES="$PROXY_ROUTES" PROXY_ROUTE_LISTENERS="$PROXY_ROUTE_LISTENERS"
PROXY_X_FORWARDED_FOR="$PROXY_X_FORWARDED_FOR"
PROXY_X_FORWARDED_PORT="$PROXY_X_FORWARDED_PORT"
PROXY_X_FORWARDED_PROTO="$PROXY_X_FORWARDED_PROTO"

View File

@@ -32,6 +32,7 @@ require (
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rogpeppe/go-internal v1.15.0 // indirect
github.com/ryanuber/columnize v2.1.2+incompatible // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.58.0 // indirect
golang.org/x/sync v0.24.0 // indirect
golang.org/x/sys v0.49.0 // indirect

View File

@@ -0,0 +1,18 @@
#!/usr/bin/env bash
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions"
trigger-nginx-vhosts-proxy-supports-routes() {
declare desc="advertises path-based routing support for nginx"
declare trigger="proxy-supports-routes"
declare APP="$1"
if [[ "$(plugn trigger proxy-type "$APP")" != "nginx" ]]; then
return
fi
echo "true"
}
trigger-nginx-vhosts-proxy-supports-routes "$@"

View File

@@ -46,6 +46,34 @@ server {
return 301 https://$host:{{ $.PROXY_SSL_PORT }}$request_uri;
}
{{ else }}
{{ range $route := $.PROXY_ROUTES | split " " }}
{{ if $route }}
{{ $r := $route | split "|" }}
{{ $route_proc := index $r 0 }}
{{ $route_port := index $r 1 }}
{{ $route_path := index $r 2 }}
{{ $route_strip := index $r 3 }}
location {{ $route_path }} {
proxy_pass http://{{ $.APP }}-{{ $route_proc }}-{{ $route_port }}{{ if eq $route_strip "1" }}/{{ end }};
proxy_http_version 1.1;
proxy_connect_timeout {{ $.PROXY_CONNECT_TIMEOUT }};
proxy_read_timeout {{ $.PROXY_READ_TIMEOUT }};
proxy_send_timeout {{ $.PROXY_SEND_TIMEOUT }};
proxy_buffer_size {{ $.PROXY_BUFFER_SIZE }};
proxy_buffering {{ $.PROXY_BUFFERING }};
proxy_buffers {{ $.PROXY_BUFFERS }};
proxy_busy_buffers_size {{ $.PROXY_BUSY_BUFFERS_SIZE }};
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For {{ $.PROXY_X_FORWARDED_FOR }};
proxy_set_header X-Forwarded-Port {{ $.PROXY_X_FORWARDED_PORT }};
proxy_set_header X-Forwarded-Proto {{ $.PROXY_X_FORWARDED_PROTO }};
proxy_set_header X-Request-Start $msec;
{{ if $.PROXY_X_FORWARDED_SSL }}proxy_set_header X-Forwarded-Ssl {{ $.PROXY_X_FORWARDED_SSL }};{{ end }}
}
{{ end }}
{{ end }}
location / {
{{ if $.DOKKU_APP_WEB_LISTENERS }}
@@ -158,3 +186,19 @@ upstream {{ $.APP }}-{{ $upstream_port }} {
server {{ $listener_ip }}:{{ $upstream_port }};{{ end }}
}
{{ end }}{{ end }}
{{ range $entry := $.PROXY_ROUTE_LISTENERS | split " " }}
{{ if $entry }}
{{ $e := $entry | split "|" }}
{{ $route_proc := index $e 0 }}
{{ $route_port := index $e 1 }}
{{ $route_csv := index $e 2 }}
upstream {{ $.APP }}-{{ $route_proc }}-{{ $route_port }} {
{{ if $.PROXY_KEEPALIVE }} keepalive {{ $.PROXY_KEEPALIVE }};{{ end }}
{{ range $listener := $route_csv | split "," }}
{{ $listener_list := $listener | split ":" }}
{{ $listener_ip := index $listener_list 0 }}
server {{ $listener_ip }}:{{ $route_port }};{{ end }}
}
{{ end }}
{{ end }}