Add multiport syntax for variable VIRTUAL_PORT

Syntax:
  VIRTUAL_PORT = [ <virtual_port> | <multiport> ];
  multiport    = port, { ",",  port };
  port         = <virtual_port> [ ":", <virtual_path> [ ":", <virtual_dest> ]];

Example with multiport syntax:
  VIRTUAL_HOST: "multiport.example.com"
  VIRTUAL_PORT: "9220:~ ^/(admin|fonts?|images|webmin)/,10901,20901:/ws2p,30901:/gva/playground"

Produces:
  # multiport.example.com:10901
  upstream multiport.example.com-10901 {
      # Exposed ports: [{   10901  tcp } {   20901  tcp } {   30901  tcp } {   9220  tcp }]
      # Default virtual port: 80
      # VIRTUAL_PORT: 10901
      ## Can be connected with "docker-gen-bridge" network
      # blah
      server 172.29.0.5:10901;
  }
  # multiport.example.com:20901/ws2p
  upstream multiport.example.com-5c7ebef820fe004e45e3af1d0c47971594d028b2-20901 {
      # Exposed ports: [{   10901  tcp } {   20901  tcp } {   30901  tcp } {   9220  tcp }]
      # Default virtual port: 80
      # VIRTUAL_PORT: 20901
      ## Can be connected with "docker-gen-bridge" network
      # blah
      server 172.29.0.5:20901;
  }
  # multiport.example.com:30901/gva/playground
  upstream multiport.example.com-1f02ce2421b17d828edaabfc3014360891bb0be3-30901 {
      # Exposed ports: [{   10901  tcp } {   20901  tcp } {   30901  tcp } {   9220  tcp }]
      # Default virtual port: 80
      # VIRTUAL_PORT: 30901
      ## Can be connected with "docker-gen-bridge" network
      # blah
      server 172.29.0.5:30901;
  }
  # multiport.example.com:9220~ ^/(admin|fonts?|images|webmin)/
  upstream multiport.example.com-cae8bfc2ea1fe6bb6fda08727ab065e8fed98aa2-9220 {
      # Exposed ports: [{   10901  tcp } {   20901  tcp } {   30901  tcp } {   9220  tcp }]
      # Default virtual port: 80
      # VIRTUAL_PORT: 9220
      ## Can be connected with "docker-gen-bridge" network
      # blah
      server 172.29.0.5:9220;
  }
  server {
      server_name multiport.example.com;
      access_log /var/log/nginx/access.log vhost;
      listen 80 ;
      location / {
          proxy_pass http://multiport.example.com-10901;
      }
      location /ws2p {
          proxy_pass http://multiport.example.com-5c7ebef820fe004e45e3af1d0c47971594d028b2-20901/;
      }
      location /gva/playground {
          proxy_pass http://multiport.example.com-1f02ce2421b17d828edaabfc3014360891bb0be3-30901;
      }
      location ~ ^/(admin|fonts?|images|webmin)/ {
          proxy_pass http://multiport.example.com-cae8bfc2ea1fe6bb6fda08727ab065e8fed98aa2-9220;
      }
  }

This feature is discussed in that upstream issue:
https://github.com/nginx-proxy/nginx-proxy/issues/1504
This commit is contained in:
Gilles Filippini 2023-01-09 19:32:52 +01:00
parent 94fb8459cd
commit bd1aeda540
7 changed files with 268 additions and 17 deletions

View file

@ -50,6 +50,8 @@ By default, docker uses IPv6-to-IPv4 NAT. This means all client connections from
If you need to support multiple virtual hosts for a container, you can separate each entry with commas. For example, `foo.bar.com,baz.bar.com,bar.com` and each host will be setup the same.
Do **not** put any space before of after each comma.
### Virtual Ports
When your container exposes only one port, nginx-proxy will default to this port, else to port 80.
@ -61,6 +63,71 @@ 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
### Multiport Syntax
There are services which expose more than one port. In this case you can set the `VIRTUAL_PORT` variable using multiport syntax:
```
VIRTUAL_PORT = [ <virtual_port> | <multiport> ];
multiport = port, { ",", port };
port = <virtual_port> [ ":", <virtual_path> [ ":", <virtual_dest> ]];
```
`<virtual_port>`, `<virtual_path>`, and `<virtual_dest>` accept the same values that `VIRTUAL_PORT`, `VIRTUAL_PATH`, and `VIRTUAL_DEST` do.
Example:
```
VIRTUAL_HOST: "multiport.example.com"
VIRTUAL_PORT: "9220:~ ^/(admin|fonts?|images|webmin)/,10901,20901:/ws2p,30901:/gva/playground"
```
would produce one nginx `upstream` definition per port, and as many `location` blocs:
```
# multiport.example.com:10901
upstream multiport.example.com-10901 {
## Can be connected with "docker-gen-bridge" network
# blah
server 172.29.0.5:10901;
}
# multiport.example.com:20901/ws2p
upstream multiport.example.com-5c7ebef820fe004e45e3af1d0c47971594d028b2-20901 {
## Can be connected with "docker-gen-bridge" network
# blah
server 172.29.0.5:20901;
}
# multiport.example.com:30901/gva/playground
upstream multiport.example.com-1f02ce2421b17d828edaabfc3014360891bb0be3-30901 {
## Can be connected with "docker-gen-bridge" network
# blah
server 172.29.0.5:30901;
}
# multiport.example.com:9220~ ^/(admin|fonts?|images|webmin)/
upstream multiport.example.com-cae8bfc2ea1fe6bb6fda08727ab065e8fed98aa2-9220 {
## Can be connected with "docker-gen-bridge" network
# blah
server 172.29.0.5:9220;
}
server {
server_name multiport.example.com;
listen 80 ;
access_log /var/log/nginx/access.log vhost;
location / {
proxy_pass http://multiport.example.com-10901;
}
location /ws2p {
proxy_pass http://multiport.example.com-5c7ebef820fe004e45e3af1d0c47971594d028b2-20901;
}
location /gva/playground {
proxy_pass http://multiport.example.com-1f02ce2421b17d828edaabfc3014360891bb0be3-30901;
}
location ~ ^/(admin|fonts?|images|webmin)/ {
proxy_pass http://multiport.example.com-cae8bfc2ea1fe6bb6fda08727ab065e8fed98aa2-9220;
}
}
```
As with the `VIRTUAL_PATH` it is possible to define per path location configuration files.
**Important note:** All `VIRTUAL_PATH` variables will be ignored for any virtual host appearing in a at least one container where `VIRTUAL_PORT` uses the multiport syntax, .
### Wildcard Hosts
You can also use wildcards at the beginning and the end of host name, like `*.bar.com` or `foo.bar.*`. Or even a regular expression, which can be very useful in conjunction with a wildcard DNS service like [nip.io](https://nip.io) or [sslip.io](https://sslip.io), using `~^foo\.bar\..*\.nip\.io` will match `foo.bar.127.0.0.1.nip.io`, `foo.bar.10.0.2.2.nip.io` and all other given IPs. More information about this topic can be found in the nginx documentation about [`server_names`](http://nginx.org/en/docs/http/server_names.html).

View file

@ -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 }}
@ -381,6 +388,30 @@ upstream {{ $vpath.upstream }} {
{{- $_ := set $ "keepalive" $keepalive }}
{{- end }}
{{- /*
* Template used as a function to parse a port specification from
* multiport syntax.
* The results (path, port, dest) are returned via the provided do dict.
*/}}
{{- define "parse_multiport_syntax_port_spec" }}
{{- $vp_data := split $.spec ":" }}
{{- $vp_port := index $vp_data 0 }}
{{- $vp_path := "/" }}
{{- $vp_dest := "" }}
{{- if (gt (len $vp_data) 1) }}
{{- $vp_path = index $vp_data 1 }}
{{- if (empty $vp_path) }}
{{- $vp_path = "/" }}
{{- end }}
{{- end }}
{{- if (gt (len $vp_data) 2) }}
{{- $vp_dest = index $vp_data 2 }}
{{- end }}
{{- $_ := set $ "path" $vp_path }}
{{- $_ := set $ "port" $vp_port }}
{{- $_ := set $ "dest" $vp_dest }}
{{- 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 {
@ -535,27 +566,73 @@ proxy_set_header Proxy "";
{{- /* Get the VIRTUAL_ROOT By containers w/ use fastcgi root */}}
{{- $vhost_root := or (first (groupByKeys $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 }}
{{- $vhost_multiport_syntax := false }}
{{- range $vp_spec, $_ := groupBy $containers "Env.VIRTUAL_PORT" }}
{{- if (or (gt (len (split $vp_spec ":")) 1) (gt (len (split $vp_spec ",")) 1)) }}
{{- $vhost_multiport_syntax = true }}
{{- break }}
{{- end }}
{{- end }}
{{- $paths := dict }}
{{- if $vhost_multiport_syntax }}
{{- range $vp_spec, $containers := groupByMulti $containers "Env.VIRTUAL_PORT" "," }}
{{- $vp_spec = trim $vp_spec }}
{{- if not $vp_spec }}
{{- continue }}
{{- end }}
{{- $args := dict "spec" $vp_spec }}
{{- template "parse_multiport_syntax_port_spec" $args }}
{{- $vp_path := $args.path }}
{{- $vp_port := $args.port }}
{{- $vp_dest := $args.dest }}
{{- 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
) }}
{{- $path_data := when (hasKey $paths $vp_path) (get $paths $vp_path) (dict) }}
{{- $path_ports := when (hasKey $path_data "ports") (get $path_data "ports") (dict) }}
{{- $path_port_containers := when (hasKey $path_ports $vp_port) (get $path_ports $vp_port) (list) }}
{{- $path_port_containers = concat $path_port_containers $containers }}
{{- $_ := set $path_ports $vp_port $path_port_containers }}
{{- $_ := set $path_data "ports" $path_ports }}
{{- if (not (hasKey $path_data "dest")) }}
{{- $_ := set $path_data "dest" $vp_dest }}
{{- end }}
{{- $_ := set $paths $vp_path $path_data }}
{{- end }}
{{- range $path, $path_data := $paths }}
{{- $path_containers := list }}
{{- range $port, $port_containers := $path_data.ports }}
{{ $path_containers = concat $path_containers $port_containers }}
{{- end }}
{{- $args := dict "Containers" $path_containers "Path" $path "Upstream_name" $upstream_name "Has_virtual_paths" false "Multiport_syntax" true }}
{{- template "get_path_info" $args }}
{{- $_ := set $path_data "proto" $args.proto }}
{{- $_ := set $path_data "network_tag" $args.network_tag }}
{{- $_ := set $path_data "upstream" $args.upstream }}
{{- $_ := set $path_data "loadbalance" $args.loadbalance }}
{{- $_ := set $path_data "keepalive" $args.keepalive }}
{{- $_ := set $paths $path $path_data }}
{{- end }}
{{- else }}
{{- $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 }}
{{- $args := dict "Containers" $containers "Path" $path "Upstream_name" $upstream_name "Has_virtual_paths" $has_virtual_paths "Multiport_syntax" false }}
{{- template "get_path_info" $args }}
{{- $_ := set $paths $path (dict
"ports" (dict "legacy" $containers)
"dest" (or (first (groupByKeys $containers "Env.VIRTUAL_DEST")) "")
"proto" $args.proto
"network_tag" $args.network_tag
"upstream" $args.upstream
"loadbalance" $args.loadbalance
"keepalive" $args.keepalive
) }}
{{- end }}
{{- end }}
{{- $_ := set $globals.vhosts $hostname (dict

View file

@ -0,0 +1,29 @@
import pytest
def test_web_no_slash_location(docker_compose, nginxproxy):
r = nginxproxy.get("http://web.nginx-proxy.tld/")
assert r.status_code == 405
def test_web_rout_to_slash_port(docker_compose, nginxproxy):
r = nginxproxy.get("http://web.nginx-proxy.tld/which-port")
assert r.status_code == 200
assert "answer from port 83\n" in r.text
def test_web1_answers_on_slash_location(docker_compose, nginxproxy):
r = nginxproxy.get("http://web1.nginx-proxy.tld/")
assert r.status_code == 200
def test_web1_no_virtual_path(docker_compose, nginxproxy):
r = nginxproxy.get("http://web1.nginx-proxy.tld/which-port")
assert r.status_code == 404
def test_web1_port_80_is_served_by_location_slash_80(docker_compose, nginxproxy):
r = nginxproxy.get("http://web1.nginx-proxy.tld/80/port")
assert r.status_code == 200
assert "answer from port 80\n" in r.text
def test_web1_port_81_is_served_by_location_slash_81(docker_compose, nginxproxy):
r = nginxproxy.get("http://web1.nginx-proxy.tld/81/port")
assert r.status_code == 200
assert "answer from port 81\n" in r.text

View file

@ -0,0 +1,31 @@
version: "2"
services:
web:
image: web
expose:
- "83"
environment:
WEB_PORTS: "83"
VIRTUAL_HOST: "web.nginx-proxy.tld,web1.nginx-proxy.tld"
VIRTUAL_PORT: "83"
VIRTUAL_PATH: "/which-port"
VIRTUAL_DEST: "/port"
web1:
image: web
expose:
- "80"
- "81"
environment:
WEB_PORTS: "80 81"
VIRTUAL_HOST: "web1.nginx-proxy.tld"
VIRTUAL_PORT: "80:/80:/,81:/81:/"
sut:
image: nginxproxy/nginx-proxy:test
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
- ./web.nginx-proxy.tld_022792f37cc2c58102bdf79316aebed215e0de21_location:/etc/nginx/vhost.d/web.nginx-proxy.tld_022792f37cc2c58102bdf79316aebed215e0de21_location
environment:
DEFAULT_ROOT: "405"

View file

@ -0,0 +1,26 @@
import pytest
def test_port_80_is_server_by_location_root(docker_compose, nginxproxy):
r = nginxproxy.get("http://web.nginx-proxy.tld/port")
assert r.status_code == 200
assert "answer from port 80\n" in r.text
def test_port_81_is_server_by_location_slash81(docker_compose, nginxproxy):
r = nginxproxy.get("http://web.nginx-proxy.tld/81/port")
assert r.status_code == 200
assert "answer from port 81\n" in r.text
def test_port_82_is_server_by_location_slash82_with_dest_slashport(docker_compose, nginxproxy):
r = nginxproxy.get("http://web.nginx-proxy.tld/82")
assert r.status_code == 200
assert "answer from port 82\n" in r.text
def test_port_83_is_server_by_regex_location_slash83_with_rewrite_in_custom_location_file(docker_compose, nginxproxy):
# The custom location file with rewrite is requested because when
# location is specified using a regex then proxy_pass should be
# specified without a URI
# see http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_pass
r = nginxproxy.get("http://web.nginx-proxy.tld/83/port")
assert r.status_code == 200
assert "answer from port 83\n" in r.text

View file

@ -0,0 +1,20 @@
version: "2"
services:
web:
image: web
expose:
- "80"
- "81"
- "82"
- "83"
environment:
WEB_PORTS: "80 81 82 83"
VIRTUAL_HOST: "web.nginx-proxy.tld"
VIRTUAL_PORT: "80,81:/81:/,82:/82:/port,83:~ ^/[8][3]"
sut:
image: nginxproxy/nginx-proxy:test
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
- ./web.nginx-proxy.tld_022792f37cc2c58102bdf79316aebed215e0de21_location:/etc/nginx/vhost.d/web.nginx-proxy.tld_022792f37cc2c58102bdf79316aebed215e0de21_location

View file

@ -0,0 +1 @@
rewrite ^/83/(.*)$ /$1 break;