From 87e5b58b779a7aa6344cc56ed5e65d5b5d22bb26 Mon Sep 17 00:00:00 2001 From: Gilles Filippini Date: Sun, 24 Mar 2024 12:31:13 +0100 Subject: [PATCH 01/15] feat: multiports support using yaml syntax (See nginx-proxy/nginx-proxy#1504) Using variable VIRTUAL_HOST_MULTIPORTS as a dictionnary: key: hostname value: dictionnary: key: path value: struct port dest When the dictionnary associated with a hostname is empty, default values apply: path = "/" port = default port dest = "" For each path entry, port and dest are optionnal and are assigned default values when missing. Example: VIRTUAL_HOST_MULTIPORTS: | host1.example.org: "/": port: 8000 "/somewhere": port: 9000 dest: "/elsewhere" host2.example.org: host3.example.org: "/inner/path": --- nginx.tmpl | 149 ++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 112 insertions(+), 37 deletions(-) diff --git a/nginx.tmpl b/nginx.tmpl index fefb07f..966b9b0 100644 --- a/nginx.tmpl +++ b/nginx.tmpl @@ -128,7 +128,7 @@ # exposed ports:{{ range sortObjectsByKeysAsc $.container.Addresses "Port" }} {{ .Port }}/{{ .Proto }}{{ else }} (none){{ end }} {{- $default_port := when (eq (len $.container.Addresses) 1) (first $.container.Addresses).Port "80" }} # default port: {{ $default_port }} - {{- $port := when (eq $.port "legacy") (or $.container.Env.VIRTUAL_PORT $default_port) $.port }} + {{- $port := when (eq $.port "default") $default_port (when (eq $.port "legacy") (or $.container.Env.VIRTUAL_PORT $default_port) $.port) }} # using port: {{ $port }} {{- $addr_obj := where $.container.Addresses "Port" $port | first }} {{- if and $addr_obj $addr_obj.HostPort }} @@ -347,6 +347,7 @@ upstream {{ $vpath.upstream }} { * - "Containers": List of container's RuntimeContainer struct. * - "Upstream_name" * - "Has_virtual_paths": boolean + * - "Multiport_syntax": boolean * - "Path" * * The return values will be added to the dot dict with keys: @@ -373,6 +374,12 @@ upstream {{ $vpath.upstream }} { {{- $upstream = printf "%s-%s" $upstream $sum }} {{- $dest = or (first (groupByKeys $.Containers "Env.VIRTUAL_DEST")) "" }} {{- end }} + {{- if $.Multiport_syntax }} + {{- if (not (eq $.Path "/")) }} + {{- $sum := sha1 $.Path }} + {{- $upstream = printf "%s-%s" $upstream $sum }} + {{- end }} + {{- end }} {{- $_ := set $ "proto" $proto }} {{- $_ := set $ "network_tag" $network_tag }} {{- $_ := set $ "upstream" $upstream }} @@ -501,14 +508,107 @@ proxy_set_header Proxy ""; {{- end }} {{- /* Precompute some information about each vhost. */}} +{{- range $vhosts_yaml, $containers := groupBy $globals.containers "Env.VIRTUAL_HOST_YAML" }} + {{- range $hostname, $vhost := (fromYaml $vhosts_yaml) }} + {{- $vhost_data := when (hasKey $globals.vhosts $hostname) (get $globals.vhosts $hostname) (dict) }} + {{- $paths := coalesce $vhost_data.paths (dict) }} + {{- if (empty $vhost) }} + {{ $vhost = dict "/" (dict) }} + {{- end }} + {{- range $path, $vpath := $vhost }} + {{- $dest := coalesce $vpath.dest "" }} + {{- $port := when (hasKey $vpath "port") (toString $vpath.port) "default" }} + {{- $path_data := when (hasKey $paths $path) (get $paths $path) (dict) }} + {{- $path_ports := when (hasKey $path_data "ports") (get $path_data "ports") (dict) }} + {{- $path_port_containers := when (hasKey $path_ports $port) (get $path_ports $port) (list) }} + {{- $path_port_containers = concat $path_port_containers $containers }} + {{- $_ := set $path_ports $port $path_port_containers }} + {{- $_ := set $path_data "ports" $path_ports }} + {{- if (not (hasKey $path_data "dest")) }} + {{- $_ := set $path_data "dest" $dest }} + {{- end }} + {{- $_ := set $paths $path $path_data }} + {{- end }} + {{- $_ := set $vhost_data "paths" $paths }} + {{- $is_regexp := hasPrefix "~" $hostname }} + {{- $_ := set $vhost_data "upstream_name" (when (or $is_regexp $globals.sha1_upstream_name) (sha1 $hostname) $hostname) }} + {{- $_ := set $vhost_data "has_virtual_paths" false }} + {{- $_ := set $vhost_data "multiport_syntax" true }} + {{- $_ := set $globals.vhosts $hostname $vhost_data }} + {{- end }} +{{- end }} + {{- range $hostname, $containers := groupByMulti $globals.containers "Env.VIRTUAL_HOST" "," }} {{- $hostname = trim $hostname }} {{- if not $hostname }} {{- /* Ignore containers with VIRTUAL_HOST set to the empty string. */}} {{- continue }} {{- end }} + {{- range $_, $containers_to_drop := groupBy $containers "Env.VIRTUAL_HOST_YAML" }} + {{- range $container := $containers_to_drop }} + {{- $containers = without $containers $container }} + {{- end }} + {{- end }} + {{- if (eq (len $containers) 0) }} + {{- continue }} + {{- end }} + {{- $vhost_data := when (hasKey $globals.vhosts $hostname) (get $globals.vhosts $hostname) (dict) }} - {{- $certName := first (groupByKeys $containers "Env.CERT_NAME") }} + {{- $is_regexp := hasPrefix "~" $hostname }} + {{- $upstream_name := when (or $is_regexp $globals.sha1_upstream_name) (sha1 $hostname) $hostname }} + + {{- $has_virtual_paths := false }} + {{- $paths := coalesce $vhost_data.paths (dict) }} + {{- $tmp_paths := groupBy $containers "Env.VIRTUAL_PATH" }} + {{- $has_virtual_paths = gt (len $tmp_paths) 0}} + {{- if not $has_virtual_paths }} + {{- $tmp_paths = dict "/" $containers }} + {{- end }} + {{- range $path, $containers := $tmp_paths }} + {{- $path_data := when (hasKey $paths $path) (get $paths $path) (dict) }} + {{- $path_ports := when (hasKey $path_data "ports") (get $path_data "ports") (dict) }} + {{- $port := "legacy" }} + {{- $path_port_containers := when (hasKey $path_ports $port) (get $path_ports $port) (list) }} + {{- $path_port_containers = concat $path_port_containers $containers }} + + {{- $_ := set $path_ports $port $path_port_containers }} + {{- $_ := set $path_data "ports" $path_ports }} + {{- if (not (hasKey $path_data "dest")) }} + {{- $_ := set $path_data "dest" (or (first (groupByKeys $containers "Env.VIRTUAL_DEST")) "") }} + {{- end }} + {{- $_ := set $paths $path $path_data }} + {{- end }} + {{- $_ := set $vhost_data "paths" $paths }} + {{- $_ := set $vhost_data "upstream_name" $upstream_name }} + {{- $_ := set $vhost_data "has_virtual_paths" $has_virtual_paths }} + {{- $_ := set $vhost_data "multiport_syntax" false }} + {{- $_ := set $globals.vhosts $hostname $vhost_data }} +{{- end }} + +{{- range $hostname, $vhost_data := $globals.vhosts }} + {{- $vhost_containers := list }} + {{- range $path, $vpath_data := $vhost_data.paths }} + {{- $vpath_containers := list }} + {{- range $port, $vport_containers := $vpath_data.ports }} + {{ $vpath_containers = concat $vpath_containers $vport_containers }} + {{- end }} + + {{- $args := dict "Containers" $vpath_containers "Path" $path "Upstream_name" $vhost_data.upstream_name "Has_virtual_paths" $vhost_data.has_virtual_paths "Multiport_syntax" $vhost_data.multiport_syntax }} + {{- template "get_path_info" $args }} + {{- if $vhost_data.has_virtual_paths }} + {{- $_ := set $vpath_data "dest" $args.dest }} + {{- end }} + {{- $_ := set $vpath_data "proto" $args.proto }} + {{- $_ := set $vpath_data "network_tag" $args.network_tag }} + {{- $_ := set $vpath_data "upstream" $args.upstream }} + {{- $_ := set $vpath_data "loadbalance" $args.loadbalance }} + {{- $_ := set $vpath_data "keepalive" $args.keepalive }} + {{- $_ := set $vhost_data.paths $path $vpath_data }} + + {{ $vhost_containers = concat $vhost_containers $vpath_containers }} + {{- end }} + + {{- $certName := first (groupByKeys $vhost_containers "Env.CERT_NAME") }} {{- $vhostCert := closest (dir "/etc/nginx/certs") (printf "%s.crt" $hostname) }} {{- $vhostCert = trimSuffix ".crt" $vhostCert }} {{- $vhostCert = trimSuffix ".key" $vhostCert }} @@ -516,49 +616,23 @@ proxy_set_header Proxy ""; {{- $cert_ok := and (ne $cert "") (exists (printf "/etc/nginx/certs/%s.crt" $cert)) (exists (printf "/etc/nginx/certs/%s.key" $cert)) }} {{- $default := eq $globals.Env.DEFAULT_HOST $hostname }} - {{- $https_method := or (first (groupByKeys $containers "Env.HTTPS_METHOD")) $globals.Env.HTTPS_METHOD "redirect" }} - {{- $http2_enabled := parseBool (or (first (keys (groupByLabel $containers "com.github.nginx-proxy.nginx-proxy.http2.enable"))) $globals.Env.ENABLE_HTTP2 "true")}} - {{- $http3_enabled := parseBool (or (first (keys (groupByLabel $containers "com.github.nginx-proxy.nginx-proxy.http3.enable"))) $globals.Env.ENABLE_HTTP3 "false")}} - - {{- $is_regexp := hasPrefix "~" $hostname }} - {{- $upstream_name := when (or $is_regexp $globals.sha1_upstream_name) (sha1 $hostname) $hostname }} + {{- $https_method := or (first (groupByKeys $vhost_containers "Env.HTTPS_METHOD")) $globals.Env.HTTPS_METHOD "redirect" }} + {{- $http2_enabled := parseBool (or (first (keys (groupByLabel $vhost_containers "com.github.nginx-proxy.nginx-proxy.http2.enable"))) $globals.Env.ENABLE_HTTP2 "true")}} + {{- $http3_enabled := parseBool (or (first (keys (groupByLabel $vhost_containers "com.github.nginx-proxy.nginx-proxy.http3.enable"))) $globals.Env.ENABLE_HTTP3 "false")}} {{- /* Get the SERVER_TOKENS defined by containers w/ the same vhost, falling back to "". */}} - {{- $server_tokens := trim (or (first (groupByKeys $containers "Env.SERVER_TOKENS")) "") }} + {{- $server_tokens := trim (or (first (groupByKeys $vhost_containers "Env.SERVER_TOKENS")) "") }} {{- /* Get the SSL_POLICY defined by containers w/ the same vhost, falling back to empty string (use default). */}} - {{- $ssl_policy := or (first (groupByKeys $containers "Env.SSL_POLICY")) "" }} + {{- $ssl_policy := or (first (groupByKeys $vhost_containers "Env.SSL_POLICY")) "" }} {{- /* Get the HSTS defined by containers w/ the same vhost, falling back to "max-age=31536000". */}} - {{- $hsts := or (first (groupByKeys $containers "Env.HSTS")) (or $globals.Env.HSTS "max-age=31536000") }} + {{- $hsts := or (first (groupByKeys $vhost_containers "Env.HSTS")) (or $globals.Env.HSTS "max-age=31536000") }} {{- /* Get the VIRTUAL_ROOT By containers w/ use fastcgi root */}} - {{- $vhost_root := or (first (groupByKeys $containers "Env.VIRTUAL_ROOT")) "/var/www/public" }} + {{- $vhost_root := or (first (groupByKeys $vhost_containers "Env.VIRTUAL_ROOT")) "/var/www/public" }} - - {{- $tmp_paths := groupBy $containers "Env.VIRTUAL_PATH" }} - {{- $has_virtual_paths := gt (len $tmp_paths) 0}} - {{- if not $has_virtual_paths }} - {{- $tmp_paths = dict "/" $containers }} - {{- end }} - - {{- $paths := dict }} - - {{- range $path, $containers := $tmp_paths }} - {{- $args := dict "Containers" $containers "Path" $path "Upstream_name" $upstream_name "Has_virtual_paths" $has_virtual_paths }} - {{- template "get_path_info" $args }} - {{- $_ := set $paths $path (dict - "ports" (dict "legacy" $containers) - "dest" $args.dest - "proto" $args.proto - "network_tag" $args.network_tag - "upstream" $args.upstream - "loadbalance" $args.loadbalance - "keepalive" $args.keepalive - ) }} - {{- end }} - - {{- $_ := set $globals.vhosts $hostname (dict + {{- $vhost_data = merge $vhost_data (dict "cert" $cert "cert_ok" $cert_ok "default" $default @@ -566,13 +640,14 @@ proxy_set_header Proxy ""; "https_method" $https_method "http2_enabled" $http2_enabled "http3_enabled" $http3_enabled - "paths" $paths "server_tokens" $server_tokens "ssl_policy" $ssl_policy "vhost_root" $vhost_root ) }} + {{- $_ := set $globals.vhosts $hostname $vhost_data }} {{- end }} + {{- /* * If needed, create a catch-all fallback server to send an error code to * clients that request something from an unknown vhost. From fc98f4c953015c021f269829aabb76e02b3a801c Mon Sep 17 00:00:00 2001 From: Nicolas Duchon Date: Thu, 2 May 2024 11:37:20 +0200 Subject: [PATCH 02/15] refactor: cleanup template --- nginx.tmpl | 59 ++++++++++++++++++++++-------------------------------- 1 file changed, 24 insertions(+), 35 deletions(-) diff --git a/nginx.tmpl b/nginx.tmpl index 966b9b0..f554d0b 100644 --- a/nginx.tmpl +++ b/nginx.tmpl @@ -346,12 +346,9 @@ upstream {{ $vpath.upstream }} { * The provided dot dict is expected to have the following entries: * - "Containers": List of container's RuntimeContainer struct. * - "Upstream_name" - * - "Has_virtual_paths": boolean - * - "Multiport_syntax": boolean * - "Path" * * The return values will be added to the dot dict with keys: - * - "dest" * - "proto" * - "network_tag" * - "upstream" @@ -368,22 +365,13 @@ upstream {{ $vpath.upstream }} { {{- $keepalive := coalesce (first (keys (groupByLabel $.Containers "com.github.nginx-proxy.nginx-proxy.keepalive"))) "disabled" }} {{- $upstream := $.Upstream_name }} - {{- $dest := "" }} - {{- if $.Has_virtual_paths }} + {{- if (not (eq $.Path "/")) }} {{- $sum := sha1 $.Path }} {{- $upstream = printf "%s-%s" $upstream $sum }} - {{- $dest = or (first (groupByKeys $.Containers "Env.VIRTUAL_DEST")) "" }} - {{- end }} - {{- if $.Multiport_syntax }} - {{- if (not (eq $.Path "/")) }} - {{- $sum := sha1 $.Path }} - {{- $upstream = printf "%s-%s" $upstream $sum }} - {{- end }} {{- end }} {{- $_ := set $ "proto" $proto }} {{- $_ := set $ "network_tag" $network_tag }} {{- $_ := set $ "upstream" $upstream }} - {{- $_ := set $ "dest" $dest }} {{- $_ := set $ "loadbalance" $loadbalance }} {{- $_ := set $ "keepalive" $keepalive }} {{- end }} @@ -507,14 +495,16 @@ proxy_set_header X-Original-URI $request_uri; proxy_set_header Proxy ""; {{- end }} -{{- /* Precompute some information about each vhost. */}} +{{- /* Precompute some information about vhost that use VIRTUAL_HOST_YAML. */}} {{- range $vhosts_yaml, $containers := groupBy $globals.containers "Env.VIRTUAL_HOST_YAML" }} {{- range $hostname, $vhost := (fromYaml $vhosts_yaml) }} {{- $vhost_data := when (hasKey $globals.vhosts $hostname) (get $globals.vhosts $hostname) (dict) }} {{- $paths := coalesce $vhost_data.paths (dict) }} + {{- if (empty $vhost) }} {{ $vhost = dict "/" (dict) }} {{- end }} + {{- range $path, $vpath := $vhost }} {{- $dest := coalesce $vpath.dest "" }} {{- $port := when (hasKey $vpath "port") (toString $vpath.port) "default" }} @@ -532,18 +522,19 @@ proxy_set_header Proxy ""; {{- $_ := set $vhost_data "paths" $paths }} {{- $is_regexp := hasPrefix "~" $hostname }} {{- $_ := set $vhost_data "upstream_name" (when (or $is_regexp $globals.sha1_upstream_name) (sha1 $hostname) $hostname) }} - {{- $_ := set $vhost_data "has_virtual_paths" false }} - {{- $_ := set $vhost_data "multiport_syntax" true }} {{- $_ := set $globals.vhosts $hostname $vhost_data }} {{- end }} {{- end }} +{{- /* Precompute some information about vhost that use VIRTUAL_HOST. */}} {{- range $hostname, $containers := groupByMulti $globals.containers "Env.VIRTUAL_HOST" "," }} + {{- /* Ignore containers with VIRTUAL_HOST set to the empty string. */}} {{- $hostname = trim $hostname }} {{- if not $hostname }} - {{- /* Ignore containers with VIRTUAL_HOST set to the empty string. */}} {{- continue }} {{- end }} + + {{/* Drop containers with VIRTUAL_HOST_YAML set (VIRTUAL_HOST_YAML takes precedence). */}} {{- range $_, $containers_to_drop := groupBy $containers "Env.VIRTUAL_HOST_YAML" }} {{- range $container := $containers_to_drop }} {{- $containers = without $containers $container }} @@ -552,36 +543,32 @@ proxy_set_header Proxy ""; {{- if (eq (len $containers) 0) }} {{- continue }} {{- end }} + {{- $vhost_data := when (hasKey $globals.vhosts $hostname) (get $globals.vhosts $hostname) (dict) }} - - {{- $is_regexp := hasPrefix "~" $hostname }} - {{- $upstream_name := when (or $is_regexp $globals.sha1_upstream_name) (sha1 $hostname) $hostname }} - - {{- $has_virtual_paths := false }} {{- $paths := coalesce $vhost_data.paths (dict) }} + {{- $tmp_paths := groupBy $containers "Env.VIRTUAL_PATH" }} - {{- $has_virtual_paths = gt (len $tmp_paths) 0}} - {{- if not $has_virtual_paths }} + {{- if not (gt (len $tmp_paths) 0) }} {{- $tmp_paths = dict "/" $containers }} {{- end }} + {{- range $path, $containers := $tmp_paths }} + {{- $dest := or (first (groupByKeys $containers "Env.VIRTUAL_DEST")) "" }} + {{- $port := "legacy" }} {{- $path_data := when (hasKey $paths $path) (get $paths $path) (dict) }} {{- $path_ports := when (hasKey $path_data "ports") (get $path_data "ports") (dict) }} - {{- $port := "legacy" }} {{- $path_port_containers := when (hasKey $path_ports $port) (get $path_ports $port) (list) }} {{- $path_port_containers = concat $path_port_containers $containers }} - {{- $_ := set $path_ports $port $path_port_containers }} {{- $_ := set $path_data "ports" $path_ports }} {{- if (not (hasKey $path_data "dest")) }} - {{- $_ := set $path_data "dest" (or (first (groupByKeys $containers "Env.VIRTUAL_DEST")) "") }} + {{- $_ := set $path_data "dest" $dest }} {{- end }} {{- $_ := set $paths $path $path_data }} {{- end }} {{- $_ := set $vhost_data "paths" $paths }} - {{- $_ := set $vhost_data "upstream_name" $upstream_name }} - {{- $_ := set $vhost_data "has_virtual_paths" $has_virtual_paths }} - {{- $_ := set $vhost_data "multiport_syntax" false }} + {{- $is_regexp := hasPrefix "~" $hostname }} + {{- $_ := set $vhost_data "upstream_name" (when (or $is_regexp $globals.sha1_upstream_name) (sha1 $hostname) $hostname) }} {{- $_ := set $globals.vhosts $hostname $vhost_data }} {{- end }} @@ -593,11 +580,13 @@ proxy_set_header Proxy ""; {{ $vpath_containers = concat $vpath_containers $vport_containers }} {{- end }} - {{- $args := dict "Containers" $vpath_containers "Path" $path "Upstream_name" $vhost_data.upstream_name "Has_virtual_paths" $vhost_data.has_virtual_paths "Multiport_syntax" $vhost_data.multiport_syntax }} + {{- $args := (dict + "Containers" $vpath_containers + "Path" $path + "Upstream_name" $vhost_data.upstream_name + )}} + {{- template "get_path_info" $args }} - {{- if $vhost_data.has_virtual_paths }} - {{- $_ := set $vpath_data "dest" $args.dest }} - {{- end }} {{- $_ := set $vpath_data "proto" $args.proto }} {{- $_ := set $vpath_data "network_tag" $args.network_tag }} {{- $_ := set $vpath_data "upstream" $args.upstream }} @@ -883,4 +872,4 @@ server { } {{- end }} } -{{- end }} +{{- end }} \ No newline at end of file From 62d9c08474ba0490ed8664605be4eb6527543321 Mon Sep 17 00:00:00 2001 From: Nicolas Duchon Date: Thu, 2 May 2024 13:00:55 +0200 Subject: [PATCH 03/15] fix: default values if port and dest are missing --- nginx.tmpl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/nginx.tmpl b/nginx.tmpl index f554d0b..8c67ae9 100644 --- a/nginx.tmpl +++ b/nginx.tmpl @@ -506,6 +506,9 @@ proxy_set_header Proxy ""; {{- end }} {{- range $path, $vpath := $vhost }} + {{- if (empty $vpath) }} + {{- $vpath = dict "dest" "" "port" "default" }} + {{- end }} {{- $dest := coalesce $vpath.dest "" }} {{- $port := when (hasKey $vpath "port") (toString $vpath.port) "default" }} {{- $path_data := when (hasKey $paths $path) (get $paths $path) (dict) }} From 62212186ebaf2f39a3a1deaef5b5c88af0633c5e Mon Sep 17 00:00:00 2001 From: Nicolas Duchon Date: Thu, 2 May 2024 09:51:48 +0200 Subject: [PATCH 04/15] test: re-organize test files --- test/test_composev2.py | 10 ---------- test/test_composev2.yml | 15 --------------- test/{ => test_logs}/test_log_format.py | 0 test/{ => test_logs}/test_log_format.yml | 0 test/{ => test_logs}/test_log_json.py | 0 test/{ => test_logs}/test_log_json.yml | 0 test/{ => test_logs}/test_log_json_format.py | 0 test/{ => test_logs}/test_log_json_format.yml | 0 ...TUAL_PORT-single-different-from-single-port.py | 0 ...UAL_PORT-single-different-from-single-port.yml | 0 .../test_VIRTUAL_PORT.py | 0 .../test_VIRTUAL_PORT.yml | 0 .../test_default-80.py | 0 .../test_default-80.yml | 0 .../test_single-port-not-80.py | 0 .../test_single-port-not-80.yml | 0 .../test_default-root-none.py | 0 .../test_default-root-none.yml | 0 18 files changed, 25 deletions(-) delete mode 100644 test/test_composev2.py delete mode 100644 test/test_composev2.yml rename test/{ => test_logs}/test_log_format.py (100%) rename test/{ => test_logs}/test_log_format.yml (100%) rename test/{ => test_logs}/test_log_json.py (100%) rename test/{ => test_logs}/test_log_json.yml (100%) rename test/{ => test_logs}/test_log_json_format.py (100%) rename test/{ => test_logs}/test_log_json_format.yml (100%) rename test/{test_multiple-ports => test_ports}/test_VIRTUAL_PORT-single-different-from-single-port.py (100%) rename test/{test_multiple-ports => test_ports}/test_VIRTUAL_PORT-single-different-from-single-port.yml (100%) rename test/{test_multiple-ports => test_ports}/test_VIRTUAL_PORT.py (100%) rename test/{test_multiple-ports => test_ports}/test_VIRTUAL_PORT.yml (100%) rename test/{test_multiple-ports => test_ports}/test_default-80.py (100%) rename test/{test_multiple-ports => test_ports}/test_default-80.yml (100%) rename test/{test_multiple-ports => test_ports}/test_single-port-not-80.py (100%) rename test/{test_multiple-ports => test_ports}/test_single-port-not-80.yml (100%) rename test/{ => test_virtual-path}/test_default-root-none.py (100%) rename test/{ => test_virtual-path}/test_default-root-none.yml (100%) diff --git a/test/test_composev2.py b/test/test_composev2.py deleted file mode 100644 index 695857e..0000000 --- a/test/test_composev2.py +++ /dev/null @@ -1,10 +0,0 @@ -import pytest - -def test_unknown_virtual_host(docker_compose, nginxproxy): - r = nginxproxy.get("http://nginx-proxy/") - assert r.status_code == 503 - -def test_forwards_to_whoami(docker_compose, nginxproxy): - r = nginxproxy.get("http://web.nginx-proxy.example/port") - assert r.status_code == 200 - assert r.text == "answer from port 81\n" diff --git a/test/test_composev2.yml b/test/test_composev2.yml deleted file mode 100644 index 3c36022..0000000 --- a/test/test_composev2.yml +++ /dev/null @@ -1,15 +0,0 @@ -version: "2" - -services: - nginx-proxy: - image: nginxproxy/nginx-proxy:test - volumes: - - /var/run/docker.sock:/tmp/docker.sock:ro - - web: - image: web - expose: - - "81" - environment: - WEB_PORTS: 81 - VIRTUAL_HOST: web.nginx-proxy.example diff --git a/test/test_log_format.py b/test/test_logs/test_log_format.py similarity index 100% rename from test/test_log_format.py rename to test/test_logs/test_log_format.py diff --git a/test/test_log_format.yml b/test/test_logs/test_log_format.yml similarity index 100% rename from test/test_log_format.yml rename to test/test_logs/test_log_format.yml diff --git a/test/test_log_json.py b/test/test_logs/test_log_json.py similarity index 100% rename from test/test_log_json.py rename to test/test_logs/test_log_json.py diff --git a/test/test_log_json.yml b/test/test_logs/test_log_json.yml similarity index 100% rename from test/test_log_json.yml rename to test/test_logs/test_log_json.yml diff --git a/test/test_log_json_format.py b/test/test_logs/test_log_json_format.py similarity index 100% rename from test/test_log_json_format.py rename to test/test_logs/test_log_json_format.py diff --git a/test/test_log_json_format.yml b/test/test_logs/test_log_json_format.yml similarity index 100% rename from test/test_log_json_format.yml rename to test/test_logs/test_log_json_format.yml diff --git a/test/test_multiple-ports/test_VIRTUAL_PORT-single-different-from-single-port.py b/test/test_ports/test_VIRTUAL_PORT-single-different-from-single-port.py similarity index 100% rename from test/test_multiple-ports/test_VIRTUAL_PORT-single-different-from-single-port.py rename to test/test_ports/test_VIRTUAL_PORT-single-different-from-single-port.py diff --git a/test/test_multiple-ports/test_VIRTUAL_PORT-single-different-from-single-port.yml b/test/test_ports/test_VIRTUAL_PORT-single-different-from-single-port.yml similarity index 100% rename from test/test_multiple-ports/test_VIRTUAL_PORT-single-different-from-single-port.yml rename to test/test_ports/test_VIRTUAL_PORT-single-different-from-single-port.yml diff --git a/test/test_multiple-ports/test_VIRTUAL_PORT.py b/test/test_ports/test_VIRTUAL_PORT.py similarity index 100% rename from test/test_multiple-ports/test_VIRTUAL_PORT.py rename to test/test_ports/test_VIRTUAL_PORT.py diff --git a/test/test_multiple-ports/test_VIRTUAL_PORT.yml b/test/test_ports/test_VIRTUAL_PORT.yml similarity index 100% rename from test/test_multiple-ports/test_VIRTUAL_PORT.yml rename to test/test_ports/test_VIRTUAL_PORT.yml diff --git a/test/test_multiple-ports/test_default-80.py b/test/test_ports/test_default-80.py similarity index 100% rename from test/test_multiple-ports/test_default-80.py rename to test/test_ports/test_default-80.py diff --git a/test/test_multiple-ports/test_default-80.yml b/test/test_ports/test_default-80.yml similarity index 100% rename from test/test_multiple-ports/test_default-80.yml rename to test/test_ports/test_default-80.yml diff --git a/test/test_multiple-ports/test_single-port-not-80.py b/test/test_ports/test_single-port-not-80.py similarity index 100% rename from test/test_multiple-ports/test_single-port-not-80.py rename to test/test_ports/test_single-port-not-80.py diff --git a/test/test_multiple-ports/test_single-port-not-80.yml b/test/test_ports/test_single-port-not-80.yml similarity index 100% rename from test/test_multiple-ports/test_single-port-not-80.yml rename to test/test_ports/test_single-port-not-80.yml diff --git a/test/test_default-root-none.py b/test/test_virtual-path/test_default-root-none.py similarity index 100% rename from test/test_default-root-none.py rename to test/test_virtual-path/test_default-root-none.py diff --git a/test/test_default-root-none.yml b/test/test_virtual-path/test_default-root-none.yml similarity index 100% rename from test/test_default-root-none.yml rename to test/test_virtual-path/test_default-root-none.yml From 216eae9f70559637b392d3b223a25f9039fb3929 Mon Sep 17 00:00:00 2001 From: Nicolas Duchon Date: Thu, 2 May 2024 10:08:51 +0200 Subject: [PATCH 05/15] test: multiports base test --- test/test_multiports/test_multiports-base.py | 39 ++++++++++++ test/test_multiports/test_multiports-base.yml | 61 +++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 test/test_multiports/test_multiports-base.py create mode 100644 test/test_multiports/test_multiports-base.yml diff --git a/test/test_multiports/test_multiports-base.py b/test/test_multiports/test_multiports-base.py new file mode 100644 index 0000000..7f1ef0f --- /dev/null +++ b/test/test_multiports/test_multiports-base.py @@ -0,0 +1,39 @@ +import pytest + + +def test_virtual_host_is_dropped_when_using_multiports(docker_compose, nginxproxy): + r = nginxproxy.get("http://notskipped.nginx-proxy.tld/port") + assert r.status_code == 200 + assert "answer from port 81\n" in r.text + r = nginxproxy.get("http://skipped.nginx-proxy.tld/") + assert r.status_code == 503 + + +def test_answer_is_served_from_port_80_by_default(docker_compose, nginxproxy): + r = nginxproxy.get("http://port80.a.nginx-proxy.tld/port") + assert r.status_code == 200 + assert "answer from port 80\n" in r.text + r = nginxproxy.get("http://port80.b.nginx-proxy.tld/port") + assert r.status_code == 200 + assert "answer from port 80\n" in r.text + r = nginxproxy.get("http://port80.c.nginx-proxy.tld/port") + assert r.status_code == 200 + assert "answer from port 80\n" in r.text + + +def test_answer_is_served_from_chosen_ports(docker_compose, nginxproxy): + r = nginxproxy.get("http://port8080.nginx-proxy.tld/port") + assert r.status_code == 200 + assert "answer from port 8080\n" in r.text + r = nginxproxy.get("http://port9000.nginx-proxy.tld/port") + assert r.status_code == 200 + assert "answer from port 9000\n" in r.text + + +def test_answer_is_served_from_chosen_ports_and_dest(docker_compose, nginxproxy): + r = nginxproxy.get("http://virtualpaths.nginx-proxy.tld/rootdest/port") + assert r.status_code == 200 + assert "answer from port 10001\n" in r.text + r = nginxproxy.get("http://virtualpaths.nginx-proxy.tld/customdest") + assert r.status_code == 200 + assert "answer from port 10002\n" in r.text diff --git a/test/test_multiports/test_multiports-base.yml b/test/test_multiports/test_multiports-base.yml new file mode 100644 index 0000000..bfe4a01 --- /dev/null +++ b/test/test_multiports/test_multiports-base.yml @@ -0,0 +1,61 @@ +version: "2" + +services: + skipvirtualhost: + image: web + expose: + - "81" + environment: + WEB_PORTS: "81" + VIRTUAL_HOST: skipped.nginx-proxy.tld + VIRTUAL_HOST_YAML: |- + notskipped.nginx-proxy.tld: + + defaultport: + image: web + expose: + - "80" + - "8080" + environment: + WEB_PORTS: "80 8080" + VIRTUAL_HOST_YAML: |- + port80.a.nginx-proxy.tld: + port80.b.nginx-proxy.tld: + port80.c.nginx-proxy.tld: + "/": + + multiports: + image: web + expose: + - "8080" + - "9000" + environment: + WEB_PORTS: "8080 9000" + VIRTUAL_HOST_YAML: |- + port8080.nginx-proxy.tld: + "/": + port: 8080 + port9000.nginx-proxy.tld: + "/": + port: 9000 + + virtualpath: + image: web + expose: + - "10001" + - "10002" + environment: + WEB_PORTS: "10001 10002" + VIRTUAL_HOST_YAML: |- + virtualpaths.nginx-proxy.tld: + "/rootdest": + port: 10001 + dest: "/" + "/customdest": + port: 10002 + dest: "/port" + + sut: + image: nginxproxy/nginx-proxy:test + volumes: + - /var/run/docker.sock:/tmp/docker.sock:ro From 47e2838e615544adda3313ef576db4bacd94d0ac Mon Sep 17 00:00:00 2001 From: Nicolas Duchon Date: Fri, 3 May 2024 00:12:14 +0200 Subject: [PATCH 06/15] refactor: rename VIRTUAL_HOST_YAML -> VIRTUAL_HOST_MULTIPORTS --- nginx.tmpl | 13 ++++++++----- test/test_multiports/test_multiports-base.yml | 8 ++++---- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/nginx.tmpl b/nginx.tmpl index 8c67ae9..615526c 100644 --- a/nginx.tmpl +++ b/nginx.tmpl @@ -495,8 +495,8 @@ proxy_set_header X-Original-URI $request_uri; proxy_set_header Proxy ""; {{- end }} -{{- /* Precompute some information about vhost that use VIRTUAL_HOST_YAML. */}} -{{- range $vhosts_yaml, $containers := groupBy $globals.containers "Env.VIRTUAL_HOST_YAML" }} +{{- /* Precompute and store some information about vhost that use VIRTUAL_HOST_MULTIPORTS. */}} +{{- range $vhosts_yaml, $containers := groupBy $globals.containers "Env.VIRTUAL_HOST_MULTIPORTS" }} {{- range $hostname, $vhost := (fromYaml $vhosts_yaml) }} {{- $vhost_data := when (hasKey $globals.vhosts $hostname) (get $globals.vhosts $hostname) (dict) }} {{- $paths := coalesce $vhost_data.paths (dict) }} @@ -529,7 +529,7 @@ proxy_set_header Proxy ""; {{- end }} {{- end }} -{{- /* Precompute some information about vhost that use VIRTUAL_HOST. */}} +{{- /* Precompute and store some information about vhost that use VIRTUAL_HOST. */}} {{- range $hostname, $containers := groupByMulti $globals.containers "Env.VIRTUAL_HOST" "," }} {{- /* Ignore containers with VIRTUAL_HOST set to the empty string. */}} {{- $hostname = trim $hostname }} @@ -537,8 +537,10 @@ proxy_set_header Proxy ""; {{- continue }} {{- end }} - {{/* Drop containers with VIRTUAL_HOST_YAML set (VIRTUAL_HOST_YAML takes precedence). */}} - {{- range $_, $containers_to_drop := groupBy $containers "Env.VIRTUAL_HOST_YAML" }} + {{- /* Drop containers with both VIRTUAL_HOST and VIRTUAL_HOST_MULTIPORTS set + * (VIRTUAL_HOST_MULTIPORTS takes precedence thanks to the previous loop). + */}} + {{- range $_, $containers_to_drop := groupBy $containers "Env.VIRTUAL_HOST_MULTIPORTS" }} {{- range $container := $containers_to_drop }} {{- $containers = without $containers $container }} {{- end }} @@ -575,6 +577,7 @@ proxy_set_header Proxy ""; {{- $_ := set $globals.vhosts $hostname $vhost_data }} {{- end }} +{{- /* Loop over $globals.vhosts and update it with the remaining informations about each vhost. */}} {{- range $hostname, $vhost_data := $globals.vhosts }} {{- $vhost_containers := list }} {{- range $path, $vpath_data := $vhost_data.paths }} diff --git a/test/test_multiports/test_multiports-base.yml b/test/test_multiports/test_multiports-base.yml index bfe4a01..8bd5803 100644 --- a/test/test_multiports/test_multiports-base.yml +++ b/test/test_multiports/test_multiports-base.yml @@ -8,7 +8,7 @@ services: environment: WEB_PORTS: "81" VIRTUAL_HOST: skipped.nginx-proxy.tld - VIRTUAL_HOST_YAML: |- + VIRTUAL_HOST_MULTIPORTS: |- notskipped.nginx-proxy.tld: defaultport: @@ -18,7 +18,7 @@ services: - "8080" environment: WEB_PORTS: "80 8080" - VIRTUAL_HOST_YAML: |- + VIRTUAL_HOST_MULTIPORTS: |- port80.a.nginx-proxy.tld: port80.b.nginx-proxy.tld: port80.c.nginx-proxy.tld: @@ -31,7 +31,7 @@ services: - "9000" environment: WEB_PORTS: "8080 9000" - VIRTUAL_HOST_YAML: |- + VIRTUAL_HOST_MULTIPORTS: |- port8080.nginx-proxy.tld: "/": port: 8080 @@ -46,7 +46,7 @@ services: - "10002" environment: WEB_PORTS: "10001 10002" - VIRTUAL_HOST_YAML: |- + VIRTUAL_HOST_MULTIPORTS: |- virtualpaths.nginx-proxy.tld: "/rootdest": port: 10001 From 8359aa2089eb3a0f1c5a94d24f904d815a77484b Mon Sep 17 00:00:00 2001 From: Nicolas Duchon Date: Fri, 3 May 2024 00:52:33 +0200 Subject: [PATCH 07/15] docs: documentation for multiports support --- docs/README.md | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/docs/README.md b/docs/README.md index 6e29e03..77b4874 100644 --- a/docs/README.md +++ b/docs/README.md @@ -53,6 +53,62 @@ For each host defined into `VIRTUAL_HOST`, the associated virtual port is retrie 1. From the container's exposed port if there is only one 1. From the default port 80 when none of the above methods apply +### Multiple ports + +If your container expose more than one service on different ports and those services need to be proxied, you'll need to use the `VIRTUAL_HOST_MULTIPORTS` environment variable. This variable takes virtual host, path, port and dest definition in YAML (or JSON) form, and completely override the `VIRTUAL_HOST`, `VIRTUAL_PORT`, `VIRTUAL_PATH` and `VIRTUAL_DEST` environment variables on this container. + +The expected format is the following: + +```yaml +hostname: + path: + port: int + dest: string +``` + +For each hostname entry, `path`, `port` and `dest` are optionnal and are assigned default values when missing: + +- `path` = "/" +- `port` = default port +- `dest` = "" + +Docker compose example with an hypotetical container running services on port 80, 8000 and 9000: + +```yaml +services: + multiport-container: + image: somerepo/somecontainer + container_name: multiport-container + environment: + VIRTUAL_HOST_MULTIPORTS: |- + service1.example.org: + service2.example.org: + "/": + port: 8000 + "/foo": + port: 9000 + dest: "/" + service3.example.org: + "/bar": + dest: "/somewhere" +``` + +Command line equivalent using JSON formatting: + +```console +docker run --detach \ + --name multiport-container \ + --env 'VIRTUAL_HOST_MULTIPORTS={"service1.example.org": {}, "service2.example.org": {"/": {"port": 8000}, "/somewhere": {"port": 9000, "dest": "/elsewhere"}}, "service3.example.org": {"/foo": {"dest": "/bar"}}}' + somerepo/somecontainer +``` + +This would result in the following proxy config: + +- `host1.example.org` -> `multiport-container:80` +- `host2.example.org` -> `multiport-container:8000` +- `host2.example.org/foo` -> `multiport-container:9000` +- `host3.example.org/bar` -> `multiport-container:80/somewhere` + ⬆️ [back to table of contents](#table-of-contents) ## Path-based Routing From 0baff189bcdffbb4bf82b9d0ed940f2eda7642d2 Mon Sep 17 00:00:00 2001 From: Nicolas Duchon Date: Fri, 3 May 2024 11:21:45 +0200 Subject: [PATCH 08/15] refactor: get rid of get_path_info template --- nginx.tmpl | 67 +++++++++++++++--------------------------------------- 1 file changed, 18 insertions(+), 49 deletions(-) diff --git a/nginx.tmpl b/nginx.tmpl index 615526c..63bc67e 100644 --- a/nginx.tmpl +++ b/nginx.tmpl @@ -338,44 +338,6 @@ upstream {{ $vpath.upstream }} { } {{- end }} -{{- /* - * Template used as a function to collect virtual path properties from - * the given containers. These properties are "returned" by storing their - * values into the provided dot dict. - * - * The provided dot dict is expected to have the following entries: - * - "Containers": List of container's RuntimeContainer struct. - * - "Upstream_name" - * - "Path" - * - * The return values will be added to the dot dict with keys: - * - "proto" - * - "network_tag" - * - "upstream" - * - "loadbalance" - * - "keepalive" - */}} -{{- define "get_path_info" }} - {{- /* Get the VIRTUAL_PROTO defined by containers w/ the same vhost-vpath, falling back to "http". */}} - {{- $proto := trim (or (first (groupByKeys $.Containers "Env.VIRTUAL_PROTO")) "http") }} - {{- /* Get the NETWORK_ACCESS defined by containers w/ the same vhost, falling back to "external". */}} - {{- $network_tag := or (first (groupByKeys $.Containers "Env.NETWORK_ACCESS")) "external" }} - - {{- $loadbalance := first (keys (groupByLabel $.Containers "com.github.nginx-proxy.nginx-proxy.loadbalance")) }} - {{- $keepalive := coalesce (first (keys (groupByLabel $.Containers "com.github.nginx-proxy.nginx-proxy.keepalive"))) "disabled" }} - - {{- $upstream := $.Upstream_name }} - {{- if (not (eq $.Path "/")) }} - {{- $sum := sha1 $.Path }} - {{- $upstream = printf "%s-%s" $upstream $sum }} - {{- end }} - {{- $_ := set $ "proto" $proto }} - {{- $_ := set $ "network_tag" $network_tag }} - {{- $_ := set $ "upstream" $upstream }} - {{- $_ := set $ "loadbalance" $loadbalance }} - {{- $_ := set $ "keepalive" $keepalive }} -{{- end }} - # If we receive X-Forwarded-Proto, pass it through; otherwise, pass along the # scheme used to connect to this server map $http_x_forwarded_proto $proxy_x_forwarded_proto { @@ -586,18 +548,25 @@ proxy_set_header Proxy ""; {{ $vpath_containers = concat $vpath_containers $vport_containers }} {{- end }} - {{- $args := (dict - "Containers" $vpath_containers - "Path" $path - "Upstream_name" $vhost_data.upstream_name - )}} + {{- /* Get the VIRTUAL_PROTO defined by containers w/ the same vhost-vpath, falling back to "http". */}} + {{- $proto := trim (or (first (groupByKeys $vpath_containers "Env.VIRTUAL_PROTO")) "http") }} + {{- /* Get the NETWORK_ACCESS defined by containers w/ the same vhost, falling back to "external". */}} + {{- $network_tag := or (first (groupByKeys $vpath_containers "Env.NETWORK_ACCESS")) "external" }} - {{- template "get_path_info" $args }} - {{- $_ := set $vpath_data "proto" $args.proto }} - {{- $_ := set $vpath_data "network_tag" $args.network_tag }} - {{- $_ := set $vpath_data "upstream" $args.upstream }} - {{- $_ := set $vpath_data "loadbalance" $args.loadbalance }} - {{- $_ := set $vpath_data "keepalive" $args.keepalive }} + {{- $loadbalance := first (keys (groupByLabel $vpath_containers "com.github.nginx-proxy.nginx-proxy.loadbalance")) }} + {{- $keepalive := coalesce (first (keys (groupByLabel $vpath_containers "com.github.nginx-proxy.nginx-proxy.keepalive"))) "disabled" }} + + {{- $upstream := $vhost_data.upstream_name }} + {{- if (not (eq $path "/")) }} + {{- $sum := sha1 $path }} + {{- $upstream = printf "%s-%s" $upstream $sum }} + {{- end }} + + {{- $_ := set $vpath_data "proto" $proto }} + {{- $_ := set $vpath_data "network_tag" $network_tag }} + {{- $_ := set $vpath_data "upstream" $upstream }} + {{- $_ := set $vpath_data "loadbalance" $loadbalance }} + {{- $_ := set $vpath_data "keepalive" $keepalive }} {{- $_ := set $vhost_data.paths $path $vpath_data }} {{ $vhost_containers = concat $vhost_containers $vpath_containers }} From 53e9a03ac9d822d9664f14575a3ff69b625978f0 Mon Sep 17 00:00:00 2001 From: Nicolas Duchon Date: Fri, 3 May 2024 22:21:21 +0200 Subject: [PATCH 09/15] feat: print warning on unparsable VIRTUAL_HOST_MULTIPORTS --- nginx.tmpl | 14 +++++- .../test_multiports-invalid-syntax.py | 18 ++++++++ .../test_multiports-invalid-syntax.yml | 44 +++++++++++++++++++ 3 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 test/test_multiports/test_multiports-invalid-syntax.py create mode 100644 test/test_multiports/test_multiports-invalid-syntax.yml diff --git a/nginx.tmpl b/nginx.tmpl index 63bc67e..a5be0b0 100644 --- a/nginx.tmpl +++ b/nginx.tmpl @@ -459,7 +459,19 @@ proxy_set_header Proxy ""; {{- /* Precompute and store some information about vhost that use VIRTUAL_HOST_MULTIPORTS. */}} {{- range $vhosts_yaml, $containers := groupBy $globals.containers "Env.VIRTUAL_HOST_MULTIPORTS" }} - {{- range $hostname, $vhost := (fromYaml $vhosts_yaml) }} + {{- /* Print a warning in the config if VIRTUAL_HOST_MULTIPORTS can't be parsed. */}} + {{- $parsedVhosts := fromYaml $vhosts_yaml }} + {{- if (empty $parsedVhosts) }} + {{- $containerNames := list }} + {{- range $container := $containers }} + {{- $containerNames = append $containerNames $container.Name }} + {{- end }} +# /!\ WARNING: the VIRTUAL_HOST_MULTIPORTS environment variable used for {{ len $containerNames | plural "this container" "those containers" }} is not a valid YAML string: +# {{ $containerNames | join ", " }} + {{- continue }} + {{- end }} + + {{- range $hostname, $vhost := $parsedVhosts }} {{- $vhost_data := when (hasKey $globals.vhosts $hostname) (get $globals.vhosts $hostname) (dict) }} {{- $paths := coalesce $vhost_data.paths (dict) }} diff --git a/test/test_multiports/test_multiports-invalid-syntax.py b/test/test_multiports/test_multiports-invalid-syntax.py new file mode 100644 index 0000000..ed1c773 --- /dev/null +++ b/test/test_multiports/test_multiports-invalid-syntax.py @@ -0,0 +1,18 @@ +import pytest +import re + + +def test_virtual_hosts_with_syntax_error_should_not_be_reachable(docker_compose, nginxproxy): + r = nginxproxy.get("http://test1.nginx-proxy.tld") + assert r.status_code == 503 + r = nginxproxy.get("http://test2.nginx-proxy.tld") + assert r.status_code == 503 + + +def test_config_should_have_multiports_warning_comments(docker_compose, nginxproxy): + conf = nginxproxy.get_conf().decode('ASCII') + matches = re.findall(r"the VIRTUAL_HOST_MULTIPORTS environment variable used for this container is not a valid YAML string", conf) + assert len(matches) == 3 + assert "# invalidsyntax" in conf + assert "# hostnamerepeat" in conf + assert "# pathrepeat" in conf diff --git a/test/test_multiports/test_multiports-invalid-syntax.yml b/test/test_multiports/test_multiports-invalid-syntax.yml new file mode 100644 index 0000000..9f40220 --- /dev/null +++ b/test/test_multiports/test_multiports-invalid-syntax.yml @@ -0,0 +1,44 @@ +version: "2" + +services: + invalidsyntax: + image: web + container_name: invalidsyntax + expose: + - "80" + environment: + WEB_PORTS: "80" + VIRTUAL_HOST_MULTIPORTS: |- + test1.nginx-proxy.tld + test2.nginx-proxy.tld: + + hostnamerepeat: + image: web + container_name: hostnamerepeat + expose: + - "80" + environment: + WEB_PORTS: "80" + VIRTUAL_HOST_MULTIPORTS: |- + test1.nginx-proxy.tld: + test1.nginx-proxy.tld: + + pathrepeat: + image: web + container_name: pathrepeat + expose: + - "8080" + - "9000" + environment: + WEB_PORTS: "8080 9000" + VIRTUAL_HOST_MULTIPORTS: |- + test1.nginx-proxy.tld: + "/": + port: 8080 + "/": + port: 9000 + + sut: + image: nginxproxy/nginx-proxy:test + volumes: + - /var/run/docker.sock:/tmp/docker.sock:ro From d80ca7ec36b26b6d3059601d2611ba2d9361074e Mon Sep 17 00:00:00 2001 From: Nicolas Duchon Date: Sat, 4 May 2024 13:30:21 +0200 Subject: [PATCH 10/15] test: json syntax for multiports variable --- ...s-base.py => test_multiports-base-json.py} | 0 .../test_multiports-base-json.yml | 77 +++++++++++++++++++ .../test_multiports-base-yaml.py | 39 ++++++++++ ...base.yml => test_multiports-base-yaml.yml} | 0 4 files changed, 116 insertions(+) rename test/test_multiports/{test_multiports-base.py => test_multiports-base-json.py} (100%) create mode 100644 test/test_multiports/test_multiports-base-json.yml create mode 100644 test/test_multiports/test_multiports-base-yaml.py rename test/test_multiports/{test_multiports-base.yml => test_multiports-base-yaml.yml} (100%) diff --git a/test/test_multiports/test_multiports-base.py b/test/test_multiports/test_multiports-base-json.py similarity index 100% rename from test/test_multiports/test_multiports-base.py rename to test/test_multiports/test_multiports-base-json.py diff --git a/test/test_multiports/test_multiports-base-json.yml b/test/test_multiports/test_multiports-base-json.yml new file mode 100644 index 0000000..fc0d0fa --- /dev/null +++ b/test/test_multiports/test_multiports-base-json.yml @@ -0,0 +1,77 @@ +version: "2" + +services: + skipvirtualhost: + image: web + expose: + - "81" + environment: + WEB_PORTS: "81" + VIRTUAL_HOST: skipped.nginx-proxy.tld + VIRTUAL_HOST_MULTIPORTS: |- + { + "notskipped.nginx-proxy.tld": {} + } + + defaultport: + image: web + expose: + - "80" + - "8080" + environment: + WEB_PORTS: "80 8080" + VIRTUAL_HOST_MULTIPORTS: |- + { + "port80.a.nginx-proxy.tld": {}, + "port80.b.nginx-proxy.tld": {}, + "port80.c.nginx-proxy.tld": { + "/": {} + } + } + + multiports: + image: web + expose: + - "8080" + - "9000" + environment: + WEB_PORTS: "8080 9000" + VIRTUAL_HOST_MULTIPORTS: |- + { + "port8080.nginx-proxy.tld": { + "/": { + "port": 8080 + } + }, + "port9000.nginx-proxy.tld": { + "/": { + "port": 9000 + } + } + } + + virtualpath: + image: web + expose: + - "10001" + - "10002" + environment: + WEB_PORTS: "10001 10002" + VIRTUAL_HOST_MULTIPORTS: |- + { + "virtualpaths.nginx-proxy.tld": { + "/rootdest": { + "port": 10001, + "dest": "/" + }, + "/customdest": { + "port": 10002, + "dest": "/port" + } + } + } + + sut: + image: nginxproxy/nginx-proxy:test + volumes: + - /var/run/docker.sock:/tmp/docker.sock:ro diff --git a/test/test_multiports/test_multiports-base-yaml.py b/test/test_multiports/test_multiports-base-yaml.py new file mode 100644 index 0000000..7f1ef0f --- /dev/null +++ b/test/test_multiports/test_multiports-base-yaml.py @@ -0,0 +1,39 @@ +import pytest + + +def test_virtual_host_is_dropped_when_using_multiports(docker_compose, nginxproxy): + r = nginxproxy.get("http://notskipped.nginx-proxy.tld/port") + assert r.status_code == 200 + assert "answer from port 81\n" in r.text + r = nginxproxy.get("http://skipped.nginx-proxy.tld/") + assert r.status_code == 503 + + +def test_answer_is_served_from_port_80_by_default(docker_compose, nginxproxy): + r = nginxproxy.get("http://port80.a.nginx-proxy.tld/port") + assert r.status_code == 200 + assert "answer from port 80\n" in r.text + r = nginxproxy.get("http://port80.b.nginx-proxy.tld/port") + assert r.status_code == 200 + assert "answer from port 80\n" in r.text + r = nginxproxy.get("http://port80.c.nginx-proxy.tld/port") + assert r.status_code == 200 + assert "answer from port 80\n" in r.text + + +def test_answer_is_served_from_chosen_ports(docker_compose, nginxproxy): + r = nginxproxy.get("http://port8080.nginx-proxy.tld/port") + assert r.status_code == 200 + assert "answer from port 8080\n" in r.text + r = nginxproxy.get("http://port9000.nginx-proxy.tld/port") + assert r.status_code == 200 + assert "answer from port 9000\n" in r.text + + +def test_answer_is_served_from_chosen_ports_and_dest(docker_compose, nginxproxy): + r = nginxproxy.get("http://virtualpaths.nginx-proxy.tld/rootdest/port") + assert r.status_code == 200 + assert "answer from port 10001\n" in r.text + r = nginxproxy.get("http://virtualpaths.nginx-proxy.tld/customdest") + assert r.status_code == 200 + assert "answer from port 10002\n" in r.text diff --git a/test/test_multiports/test_multiports-base.yml b/test/test_multiports/test_multiports-base-yaml.yml similarity index 100% rename from test/test_multiports/test_multiports-base.yml rename to test/test_multiports/test_multiports-base-yaml.yml From 8b91f09a9b4dba8143f6483cfea28f4ac6e95fbb Mon Sep 17 00:00:00 2001 From: Nicolas Duchon Date: Sat, 4 May 2024 13:40:05 +0200 Subject: [PATCH 11/15] docs: use examples closer to real life scenarios --- docs/README.md | 79 ++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 61 insertions(+), 18 deletions(-) diff --git a/docs/README.md b/docs/README.md index 77b4874..b6ad130 100644 --- a/docs/README.md +++ b/docs/README.md @@ -57,6 +57,8 @@ For each host defined into `VIRTUAL_HOST`, the associated virtual port is retrie If your container expose more than one service on different ports and those services need to be proxied, you'll need to use the `VIRTUAL_HOST_MULTIPORTS` environment variable. This variable takes virtual host, path, port and dest definition in YAML (or JSON) form, and completely override the `VIRTUAL_HOST`, `VIRTUAL_PORT`, `VIRTUAL_PATH` and `VIRTUAL_DEST` environment variables on this container. +The YAML syntax should be easier to write on Docker compose files, while the JSON syntax can be used for CLI invocation. + The expected format is the following: ```yaml @@ -72,7 +74,9 @@ For each hostname entry, `path`, `port` and `dest` are optionnal and are assigne - `port` = default port - `dest` = "" -Docker compose example with an hypotetical container running services on port 80, 8000 and 9000: +The following examples use an hypotetical container running services on port 80, 8000 and 9000: + +#### Multiple ports routed to different hostnames ```yaml services: @@ -81,33 +85,72 @@ services: container_name: multiport-container environment: VIRTUAL_HOST_MULTIPORTS: |- + www.example.org: service1.example.org: - service2.example.org: "/": port: 8000 - "/foo": + service2.example.org: + "/": port: 9000 - dest: "/" - service3.example.org: - "/bar": - dest: "/somewhere" -``` -Command line equivalent using JSON formatting: +# There is no path dict specified for www.example.org, so it get the default values: +# www.example.org: +# "/": +# port: 80 (default port) +# dest: "" -```console -docker run --detach \ - --name multiport-container \ - --env 'VIRTUAL_HOST_MULTIPORTS={"service1.example.org": {}, "service2.example.org": {"/": {"port": 8000}, "/somewhere": {"port": 9000, "dest": "/elsewhere"}}, "service3.example.org": {"/foo": {"dest": "/bar"}}}' - somerepo/somecontainer +# JSON equivalent: +# VIRTUAL_HOST_MULTIPORTS: |- +# { +# "www.example.org": {}, +# "service1.example.org": { "/": { "port": 8000, "dest": "" } }, +# "service2.example.org": { "/": { "port": 9000, "dest": "" } } +# } ``` This would result in the following proxy config: -- `host1.example.org` -> `multiport-container:80` -- `host2.example.org` -> `multiport-container:8000` -- `host2.example.org/foo` -> `multiport-container:9000` -- `host3.example.org/bar` -> `multiport-container:80/somewhere` +- `www.example.org` -> `multiport-container:80` +- `service1.example.org` -> `multiport-container:8000` +- `service2.example.org` -> `multiport-container:9000` + +#### Multiple ports routed to same hostname and different paths + +```yaml +services: + multiport-container: + image: somerepo/somecontainer + container_name: multiport-container + environment: + VIRTUAL_HOST_MULTIPORTS: |- + www.example.org: + "/": + "/service1": + port: 8000 + dest: "/" + "/service2": + port: 9000 + dest: "/" + +# port and dest are not specified on the / path, so this path is routed +# to the default port with the default dest value (empty string) + +# JSON equivalent: +# VIRTUAL_HOST_MULTIPORTS: |- +# { +# "www.example.org": { +# "/": {}, +# "/service1": { "port": 8000, "dest": "/" }, +# "/service2": { "port": 9000, "dest": "/" } +# } +# } +``` + +This would result in the following proxy config: + +- `www.example.org` -> `multiport-container:80` +- `www.example.org/service1` -> `multiport-container:8000` +- `www.example.org/service2` -> `multiport-container:9000` ⬆️ [back to table of contents](#table-of-contents) From be7c4c8c850fe0e9e3eb863b0d2bf99c819881f4 Mon Sep 17 00:00:00 2001 From: Nicolas Duchon Date: Sun, 5 May 2024 16:31:03 +0200 Subject: [PATCH 12/15] fix: do not discard containers without VIRTUAL_PATH For containers grouped by identical VIRTUAL_HOST, those with no VIRTUAL_PATH variable were silently discarded when at least one container with VIRTUAL_PATH existed. --- nginx.tmpl | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/nginx.tmpl b/nginx.tmpl index a5be0b0..2b73158 100644 --- a/nginx.tmpl +++ b/nginx.tmpl @@ -526,10 +526,7 @@ proxy_set_header Proxy ""; {{- $vhost_data := when (hasKey $globals.vhosts $hostname) (get $globals.vhosts $hostname) (dict) }} {{- $paths := coalesce $vhost_data.paths (dict) }} - {{- $tmp_paths := groupBy $containers "Env.VIRTUAL_PATH" }} - {{- if not (gt (len $tmp_paths) 0) }} - {{- $tmp_paths = dict "/" $containers }} - {{- end }} + {{- $tmp_paths := groupByWithDefault $containers "Env.VIRTUAL_PATH" "/" }} {{- range $path, $containers := $tmp_paths }} {{- $dest := or (first (groupByKeys $containers "Env.VIRTUAL_DEST")) "" }} From 1bf7eff04f03db19d378d94120c45612af403001 Mon Sep 17 00:00:00 2001 From: Nicolas Duchon Date: Sat, 4 May 2024 14:48:08 +0200 Subject: [PATCH 13/15] test: multiport merge with legacy variable --- test/test_multiports/test_multiports-merge.py | 14 +++++++ .../test_multiports/test_multiports-merge.yml | 41 +++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 test/test_multiports/test_multiports-merge.py create mode 100644 test/test_multiports/test_multiports-merge.yml diff --git a/test/test_multiports/test_multiports-merge.py b/test/test_multiports/test_multiports-merge.py new file mode 100644 index 0000000..f5aa697 --- /dev/null +++ b/test/test_multiports/test_multiports-merge.py @@ -0,0 +1,14 @@ +import backoff +import pytest + + +def test_multiports_and_legacy_configs_should_be_merged(docker_compose, nginxproxy): + @backoff.on_predicate(backoff.constant, lambda r: r == False, interval=.5, max_tries=20, jitter=None) + def answer_contains(answer, url): + return answer in nginxproxy.get(url).text + + assert answer_contains("80", "http://merged.nginx-proxy.tld/port") + assert answer_contains("81", "http://merged.nginx-proxy.tld/port") + + assert answer_contains("9090", "http://merged.nginx-proxy.tld/foo/port") + assert answer_contains("9191", "http://merged.nginx-proxy.tld/foo/port") diff --git a/test/test_multiports/test_multiports-merge.yml b/test/test_multiports/test_multiports-merge.yml new file mode 100644 index 0000000..5c5cd8b --- /dev/null +++ b/test/test_multiports/test_multiports-merge.yml @@ -0,0 +1,41 @@ +version: "2" + +services: + merged-singleport: + image: web + expose: + - "80" + environment: + WEB_PORTS: "80" + VIRTUAL_HOST: merged.nginx-proxy.tld + + merged-singleport-virtual-path: + image: web + expose: + - "9090" + environment: + WEB_PORTS: "9090" + VIRTUAL_HOST: merged.nginx-proxy.tld + VIRTUAL_PORT: "9090" + VIRTUAL_PATH: "/foo" + VIRTUAL_DEST: "/" + + merged-multiports: + image: web + expose: + - "81" + - "9191" + environment: + WEB_PORTS: "81 9191" + VIRTUAL_HOST_MULTIPORTS: |- + merged.nginx-proxy.tld: + "/": + port: 81 + "/foo": + port: 9191 + dest: "/" + + sut: + image: nginxproxy/nginx-proxy:test + volumes: + - /var/run/docker.sock:/tmp/docker.sock:ro From 99645f104da77aece73a79e04ef37bdf81527809 Mon Sep 17 00:00:00 2001 From: Nicolas Duchon Date: Mon, 6 May 2024 14:40:32 +0200 Subject: [PATCH 14/15] docs: typo --- docs/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/README.md b/docs/README.md index b6ad130..3f3b17d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -74,7 +74,7 @@ For each hostname entry, `path`, `port` and `dest` are optionnal and are assigne - `port` = default port - `dest` = "" -The following examples use an hypotetical container running services on port 80, 8000 and 9000: +The following examples use an hypothetical container running services on port 80, 8000 and 9000: #### Multiple ports routed to different hostnames From be319e662977e13fc157960cb9b39adea663309d Mon Sep 17 00:00:00 2001 From: Nicolas Duchon Date: Mon, 6 May 2024 15:34:51 +0200 Subject: [PATCH 15/15] docs: typo Co-authored-by: Niek <100143256+SchoNie@users.noreply.github.com> --- docs/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/README.md b/docs/README.md index 3f3b17d..aea3556 100644 --- a/docs/README.md +++ b/docs/README.md @@ -68,7 +68,7 @@ hostname: dest: string ``` -For each hostname entry, `path`, `port` and `dest` are optionnal and are assigned default values when missing: +For each hostname entry, `path`, `port` and `dest` are optional and are assigned default values when missing: - `path` = "/" - `port` = default port