1
0
Fork 0
mirror of https://github.com/nginx-proxy/nginx-proxy synced 2024-05-18 21:46:06 +02:00

Merged master, fixed BATS conflict

This commit is contained in:
Steve Kamerman 2017-03-06 10:48:12 -05:00
commit ad9af2884d
100 changed files with 2620 additions and 1904 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
**/__pycache__/
**/.cache/

View File

@ -4,8 +4,11 @@ services:
- docker
env:
global:
- DOCKER_VERSION=1.12.3-0~trusty
global:
- DOCKER_VERSION=1.13.1-0~ubuntu-trusty
matrix:
- TEST_TARGET: test-debian
- TEST_TARGET: test-alpine
before_install:
# list docker-engine versions
@ -14,15 +17,8 @@ before_install:
- sudo apt-get -o Dpkg::Options::="--force-confnew" install -y --force-yes docker-engine=${DOCKER_VERSION}
- docker version
- docker info
- sudo add-apt-repository ppa:duggan/bats --yes
- sudo apt-get update -qq
- sudo apt-get install -qq bats
# prepare docker test requirements
- make update-dependencies
matrix:
include:
- env: TEST_ID=test-debian
- env: TEST_ID=test-alpine
script:
- make $TEST_ID
- make $TEST_TARGET

View File

@ -1,4 +1,4 @@
FROM nginx:1.11.8
FROM nginx:1.11.10
MAINTAINER Jason Wilder mail@jasonwilder.com
# Install wget and install/updates certificates

View File

@ -1,4 +1,4 @@
FROM nginx:1.11.8-alpine
FROM nginx:1.11.10-alpine
MAINTAINER Jason Wilder mail@jasonwilder.com
# Install wget and install/updates certificates

View File

@ -1,21 +1,16 @@
.SILENT :
.PHONY : test
.PHONY : test-debian test-alpine test
update-dependencies:
docker pull jwilder/docker-gen:0.7.3
docker pull nginx:1.11.6
docker pull nginx:1.11.8-alpine
docker pull python:3
docker pull rancher/socat-docker:latest
docker pull appropriate/curl:latest
docker pull docker:1.10
test/requirements/build.sh
test-debian:
docker build -t jwilder/nginx-proxy:bats .
bats test
test-debian: update-dependencies
docker build -t jwilder/nginx-proxy:test .
test/pytest.sh
test-alpine:
docker build -f Dockerfile.alpine -t jwilder/nginx-proxy:bats .
bats test
test-alpine: update-dependencies
docker build -f Dockerfile.alpine -t jwilder/nginx-proxy:test .
test/pytest.sh
test: test-debian test-alpine

View File

@ -1,2 +1,2 @@
nginx: nginx
dockergen: docker-gen -watch -notify "nginx -s reload" /app/nginx.tmpl /etc/nginx/conf.d/default.conf
nginx: nginx

View File

@ -1,4 +1,4 @@
![nginx 1.11.8](https://img.shields.io/badge/nginx-1.11.8-brightgreen.svg) ![License MIT](https://img.shields.io/badge/license-MIT-blue.svg) [![Build Status](https://travis-ci.org/jwilder/nginx-proxy.svg?branch=master)](https://travis-ci.org/jwilder/nginx-proxy) [![](https://img.shields.io/docker/stars/jwilder/nginx-proxy.svg)](https://hub.docker.com/r/jwilder/nginx-proxy 'DockerHub') [![](https://img.shields.io/docker/pulls/jwilder/nginx-proxy.svg)](https://hub.docker.com/r/jwilder/nginx-proxy 'DockerHub')
![nginx 1.11.10](https://img.shields.io/badge/nginx-1.11.10-brightgreen.svg) ![License MIT](https://img.shields.io/badge/license-MIT-blue.svg) [![Build Status](https://travis-ci.org/jwilder/nginx-proxy.svg?branch=master)](https://travis-ci.org/jwilder/nginx-proxy) [![](https://img.shields.io/docker/stars/jwilder/nginx-proxy.svg)](https://hub.docker.com/r/jwilder/nginx-proxy 'DockerHub') [![](https://img.shields.io/docker/pulls/jwilder/nginx-proxy.svg)](https://hub.docker.com/r/jwilder/nginx-proxy 'DockerHub')
nginx-proxy sets up a container running nginx and [docker-gen][1]. docker-gen generates reverse proxy configs for nginx and reloads nginx when containers are started and stopped.
@ -15,10 +15,26 @@ Then start any containers you want proxied with an env var `VIRTUAL_HOST=subdoma
$ docker run -e VIRTUAL_HOST=foo.bar.com ...
The containers being proxied must [expose](https://docs.docker.com/reference/run/#expose-incoming-ports) the port to be proxied, either by using the `EXPOSE` directive in their `Dockerfile` or by using the `--expose` flag to `docker run` or `docker create`.
The containers being proxied must [expose](https://docs.docker.com/engine/reference/run/#expose-incoming-ports) the port to be proxied, either by using the `EXPOSE` directive in their `Dockerfile` or by using the `--expose` flag to `docker run` or `docker create`.
Provided your DNS is setup to forward foo.bar.com to the a host running nginx-proxy, the request will be routed to a container with the VIRTUAL_HOST env var set.
### Image variants
The nginx-proxy images are available in two flavors.
#### jwilder/nginx-proxy:latest
This image uses the debian:jessie based nginx image.
$ docker pull jwilder/nginx-proxy:latest
#### jwilder/nginx-proxy:alpine
This image is based on the nginx:alpine image.
$ docker pull jwilder/nginx-proxy:alpine
### Docker Compose
```yaml
@ -45,6 +61,12 @@ $ curl -H "Host: whoami.local" localhost
I'm 5b129ab83266
```
### IPv6 support
You can activate the IPv6 support for the nginx-proxy container by passing the value `true` to the `ENABLE_IPV6` environment variable:
$ docker run -d -p 80:80 -e ENABLE_IPV6=true -v /var/run/docker.sock:/tmp/docker.sock:ro jwilder/nginx-proxy
### Multiple Ports
If your container exposes multiple ports, nginx-proxy will default to the service running on port 80. If you need to specify a different port, you can set a VIRTUAL_PORT env var to select a different one. If your container only exposes one port and it has a VIRTUAL_HOST env var set, that port will be selected.
@ -129,7 +151,7 @@ Finally, start your containers with `VIRTUAL_HOST` environment variables.
$ docker run -e VIRTUAL_HOST=foo.bar.com ...
### SSL Support using letsencrypt
[letsencrypt-nginx-proxy-companion](https://github.com/JrCs/docker-letsencrypt-nginx-proxy-companion)is a lightweight companion container for the nginx-proxy. It allow the creation/renewal of Let's Encrypt certificates automatically.
[letsencrypt-nginx-proxy-companion](https://github.com/JrCs/docker-letsencrypt-nginx-proxy-companion) is a lightweight companion container for the nginx-proxy. It allow the creation/renewal of Let's Encrypt certificates automatically.
### SSL Support
@ -307,7 +329,7 @@ If you are using multiple hostnames for a single container (e.g. `VIRTUAL_HOST=e
#### Per-VIRTUAL_HOST location default configuration
If you want most of your virtual hosts to use a default single `location` block configuration and then override on a few specific ones, add those settings to the `/etc/nginx/vhost.d/default_location` file. This file
will be used on any virtual host which does not have a `/etc/nginx/vhost.d/{VIRTUAL_HOST}` file associated with it.
will be used on any virtual host which does not have a `/etc/nginx/vhost.d/{VIRTUAL_HOST}_location` file associated with it.
### Contributing
@ -315,6 +337,22 @@ Before submitting pull requests or issues, please check github to make sure an e
#### Running Tests Locally
To run tests, you'll need to install [bats 0.4.0](https://github.com/sstephenson/bats).
To run tests, you need to prepare the docker image to test which must be tagged `jwilder/nginx-proxy:test`:
docker build -t jwilder/nginx-proxy:test . # build the Debian variant image
and call the [test/pytest.sh](test/pytest.sh) script.
Then build the Alpine variant of the image:
docker build -f Dockerfile.alpine -t jwilder/nginx-proxy:test . # build the Alpline variant image
and call the [test/pytest.sh](test/pytest.sh) script again.
If your system has the `make` command, you can automate those tasks by calling:
make test
You can learn more about how the test suite works and how to write new tests in the [test/README.md](test/README.md) file.

View File

@ -12,4 +12,3 @@ services:
image: jwilder/whoami
environment:
- VIRTUAL_HOST=whoami.local

View File

@ -77,9 +77,13 @@ proxy_set_header X-Forwarded-Port $proxy_x_forwarded_port;
proxy_set_header Proxy "";
{{ end }}
{{ $enable_ipv6 := eq (or ($.Env.ENABLE_IPV6) "") "true" }}
server {
server_name _; # This is just an invalid value which will never trigger on a real hostname.
listen 80;
{{ if $enable_ipv6 }}
listen [::]:80;
{{ end }}
access_log /var/log/nginx/access.log vhost;
return 503;
}
@ -88,6 +92,9 @@ server {
server {
server_name _; # This is just an invalid value which will never trigger on a real hostname.
listen 443 ssl http2;
{{ if $enable_ipv6 }}
listen [::]:443 ssl http2;
{{ end }}
access_log /var/log/nginx/access.log vhost;
return 503;
@ -98,7 +105,8 @@ server {
{{ end }}
{{ range $host, $containers := groupByMulti $ "Env.VIRTUAL_HOST" "," }}
{{ $upstream_name := sha1 $host }}
{{ $is_regexp := hasPrefix "~" $host }}
{{ $upstream_name := when $is_regexp (sha1 $host) $host }}
# {{ $host }}
upstream {{ $upstream_name }} {
{{ range $container := $containers }}
@ -155,6 +163,9 @@ upstream {{ $upstream_name }} {
server {
server_name {{ $host }};
listen 80 {{ $default_server }};
{{ if $enable_ipv6 }}
listen [::]:80 {{ $default_server }};
{{ end }}
access_log /var/log/nginx/access.log vhost;
return 301 https://$host$request_uri;
}
@ -163,6 +174,9 @@ server {
server {
server_name {{ $host }};
listen 443 ssl http2 {{ $default_server }};
{{ if $enable_ipv6 }}
listen [::]:443 ssl http2 {{ $default_server }};
{{ end }}
access_log /var/log/nginx/access.log vhost;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
@ -216,6 +230,9 @@ server {
server {
server_name {{ $host }};
listen 80 {{ $default_server }};
{{ if $enable_ipv6 }}
listen [::]:80 {{ $default_server }};
{{ end }}
access_log /var/log/nginx/access.log vhost;
{{ if (exists (printf "/etc/nginx/vhost.d/%s" $host)) }}
@ -247,6 +264,9 @@ server {
server {
server_name {{ $host }};
listen 443 ssl http2 {{ $default_server }};
{{ if $enable_ipv6 }}
listen [::]:443 ssl http2 {{ $default_server }};
{{ end }}
access_log /var/log/nginx/access.log vhost;
return 500;

View File

@ -1,14 +1,107 @@
Test suite
==========
This test suite is implemented on top of the [Bats](https://github.com/sstephenson/bats/blob/master/README.md) test framework.
It is intended to verify the correct behavior of the Docker image `jwilder/nginx-proxy:bats`.
Running the test suite
----------------------
Make sure you have Bats installed, then run:
docker build -t jwilder/nginx-proxy:bats .
bats test/
Nginx proxy test suite
======================
Install requirements
--------------------
You need [python 2.7](https://www.python.org/) and [pip](https://pip.pypa.io/en/stable/installing/) installed. Then run the commands:
requirements/build.sh
pip install -r requirements/python-requirements.txt
If you can't install those requirements on your computer, you can alternatively use the _pytest.sh_ script which will run the tests from a Docker container which has those requirements.
Prepare the nginx-proxy test image
----------------------------------
docker build -t jwilder/nginx-proxy:test ..
or if you want to test the alpine flavor:
docker build -t jwilder/nginx-proxy:test -f Dockerfile.alpine ..
make sure to tag that test image exactly `jwilder/nginx-proxy:test` or the test suite won't work.
Run the test suite
------------------
pytest
need more verbosity ?
pytest -s
Run one single test module
--------------------------
pytest test_nominal.py
Write a test module
-------------------
This test suite uses [pytest](http://doc.pytest.org/en/latest/). The [conftest.py](conftest.py) file will be automatically loaded by pytest and will provide you with two useful pytest [fixtures](http://doc.pytest.org/en/latest/fixture.html#fixture):
- docker_compose
- nginxproxy
### docker_compose fixture
When using the `docker_compose` fixture in a test, pytest will try to find a yml file named after your test module filename. For instance, if your test module is `test_example.py`, then the `docker_compose` fixture will try to load a `test_example.yml` [docker compose file](https://docs.docker.com/compose/compose-file/).
Once the docker compose file found, the fixture will remove all containers, run `docker-compose up`, and finally your test will be executed.
The fixture will run the _docker-compose_ command with the `-f` option to load the given compose file. So you can test your docker compose file syntax by running it yourself with:
docker-compose -f test_example.yml up -d
In the case you are running pytest from within a docker container, the `docker_compose` fixture will make sure the container running pytest is attached to all docker networks. That way, your test will be able to reach any of them.
In your tests, you can use the `docker_compose` variable to query and command the docker daemon as it provides you with a [client from the docker python module](https://docker-py.readthedocs.io/en/2.0.2/client.html#client-reference).
Also this fixture alters the way the python interpreter resolves domain names to IP addresses in the following ways:
Any domain name containing the substring `nginx-proxy` will resolve to the IP address of the container that was created from the `jwilder/nginx-proxy:test` image. So all the following domain names will resolve to the nginx-proxy container in tests:
- `nginx-proxy`
- `nginx-proxy.com`
- `www.nginx-proxy.com`
- `www.nginx-proxy.test`
- `www.nginx-proxy`
- `whatever.nginx-proxyooooooo`
- ...
Any domain name ending with `XXX.container.docker` will resolve to the IP address of the XXX container.
- `web1.container.docker` will resolve to the IP address of the `web1` container
- `f00.web1.container.docker` will resolve to the IP address of the `web1` container
- `anything.whatever.web2.container.docker` will resolve to the IP address of the `web2` container
Otherwise, domain names are resoved as usual using your system DNS resolver.
### nginxproxy fixture
The `nginxproxy` fixture will provide you with a replacement for the python [requests](https://pypi.python.org/pypi/requests/) module. This replacement will just repeat up to 30 times a requests if it receives the HTTP error 404 or 502. This error occurs when you try to send queries to nginx-proxy too early after the container creation.
Also this requests replacement is preconfigured to use the Certificate Authority root certificate [certs/ca-root.crt](certs/) to validate https connections.
Furthermore, the nginxproxy methods accept an additional keyword parameter: `ipv6` which forces requests made against containers to use the containers IPv6 address when set to `True`. If IPv6 is not supported by the system or docker, that particular test will be skipped.
def test_forwards_to_web1_ipv6(docker_compose, nginxproxy):
r = nginxproxy.get("http://web1.nginx-proxy.tld/port", ipv6=True)
assert r.status_code == 200
assert r.text == "answer from port 81\n"
### The web docker image
When you ran the `requirements/build.sh` script earlier, you built a [`web`](requirements/README.md) docker image which is convenient for running a small web server in a container. This image can produce containers that listens on multiple ports at the same time.
### Testing TLS
If you need to create server certificates, use the [`certs/create_server_certificate.sh`](certs/) script. Pytest will be able to validate any certificate issued from this script.

81
test/certs/README.md Normal file
View File

@ -0,0 +1,81 @@
create_server_certificate.sh
============================
`create_server_certificate.sh` is a script helping with issuing server certificates that can be used to provide TLS on web servers.
It also creates a Certificate Authority (CA) root key and certificate. This CA root certificate can be used to validate the server certificates it generates.
For instance, with _curl_:
curl --cacert /somewhere/ca-root.crt https://www.example.com/
or with _wget_:
wget --certificate=/somewhere/ca-root.crt https://www.example.com/
or with the python _requests_ module:
import requests
r = requests.get("https://www.example.com", verify="/somewhere/ca-root.crt")
Usage
-----
### Simple domain
Create a server certificate for domain `www.example.com`:
./create_server_certificate.sh www.example.com
Will produce:
- `www.example.com.key`
- `www.example.com.crt`
### Multiple domains
Create a server certificate for main domain `www.example.com` and alternative domains `example.com`, `foo.com` and `bar.com`:
./create_server_certificate.sh www.example.com foo.com bar.com
Will produce:
- `www.example.com.key`
- `www.example.com.crt`
### Wildcard domain
Create a server certificate for wildcard domain `*.example.com`:
./create_server_certificate.sh "*.example.com"
Note that you need to use quotes around the domain string or the shell would expand `*`.
Will produce:
- `*.example.com.key`
- `*.example.com.crt`
Again, to prevent your shell from expanding `*`, use quotes. i.e.: `cat "*.example.com.crt"`.
Such a server certificate would be valid for domains:
- `foo.example.com`
- `bar.example.com`
but not for domains:
- `example.com`
- `foo.bar.example.com`
### Wildcard domain on multiple levels
While you can technically create a server certificate for wildcard domain `*.example.com` and alternative name `*.*.example.com`, client implementations generally do not support multiple wildcards in a domain name.
For instance, a python script using urllib3 would fail to validate domain `foo.bar.example.com` presenting a certificate with name `*.*.example.com`. It is advised to stay away from producing such certificates.
If you want to give it a try:
./create_server_certificate.sh "*.example.com" "*.*.example.com"
Such a server certificate would be valid for domains:
- `foo.example.com`
- `bar.example.com`
- `foo.bar.example.com`

21
test/certs/ca-root.crt Normal file
View File

@ -0,0 +1,21 @@
-----BEGIN CERTIFICATE-----
MIIDZDCCAkygAwIBAgIJAJmW6Ju6iJNNMA0GCSqGSIb3DQEBCwUAMD8xHzAdBgNV
BAoMFm5naW54LXByb3h5IHRlc3Qgc3VpdGUxHDAaBgNVBAMME3d3dy5uZ2lueC1w
cm94eS50bGQwHhcNMTcwMTEwMDAwODUxWhcNMjcwMTA4MDAwODUxWjA/MR8wHQYD
VQQKDBZuZ2lueC1wcm94eSB0ZXN0IHN1aXRlMRwwGgYDVQQDDBN3d3cubmdpbngt
cHJveHkudGxkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAndjE3OPr
48hIOQigk/HejrowsQDLNfkkc6vej0J983rJitGTgBfxqq27fOPfqhE5bi1M5JDk
KkrOrSitxCJLgpq+4Ls9/RXg8skZFHRAQbNwuKBehaDkPdamJ0i3dv6e4kZy41oI
RqxQ/MKdminC4LShFZvPoKeh9ae7w1MgB2/4E68LO66bYiHlLNL7ENViSHhLyCmt
qIE7kdV9jgn2NuVJ37m6/6SNQ3GBiIjEW+ooRQ3HEVKBCismcwq80+BD5VS/yW18
KqX8m4sBM+IgZbcOqrV+APMbGvd8iNJgQSSQC/r0Wscgt7UeggVYKDazjDSPvLUE
FUN5wEmydkP2AQIDAQABo2MwYTAdBgNVHQ4EFgQUJL59pHomt+8dUNxv8HgrYjKf
OA8wHwYDVR0jBBgwFoAUJL59pHomt+8dUNxv8HgrYjKfOA8wDwYDVR0TAQH/BAUw
AwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQADggEBABALxY96YqsZ
CL2crzY0FIGhfjLE7P3mtUGklUpFu7xyI6mGUyL1nJYSnHB5IEV6QLhVVUE/CojI
crXorQWBDkx26AgCt/eIOdvPYC0JDeXiIhH6sld3yH7JGwGqJkfXaUUfUkuwMae7
mMIEG9e6vfSh/YNTRxs0KBjBcXHHl5K+Dz4h9r14OqnQFqVFZaR6T6td44tDDNhn
beW8iIfCWRqDsnvIcJzLa2QR4onmJSw5DaSeFFaKefhdHEzEBZntLfyFbjRYHT/O
+BRdewhg6rSDkGLcL8n/ZnRLOa+xmegjQ/Op94OmWO3TfXOITJAtkaO2YVZoyek8
T6ckVovq4zU=
-----END CERTIFICATE-----

27
test/certs/ca-root.key Normal file
View File

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEAndjE3OPr48hIOQigk/HejrowsQDLNfkkc6vej0J983rJitGT
gBfxqq27fOPfqhE5bi1M5JDkKkrOrSitxCJLgpq+4Ls9/RXg8skZFHRAQbNwuKBe
haDkPdamJ0i3dv6e4kZy41oIRqxQ/MKdminC4LShFZvPoKeh9ae7w1MgB2/4E68L
O66bYiHlLNL7ENViSHhLyCmtqIE7kdV9jgn2NuVJ37m6/6SNQ3GBiIjEW+ooRQ3H
EVKBCismcwq80+BD5VS/yW18KqX8m4sBM+IgZbcOqrV+APMbGvd8iNJgQSSQC/r0
Wscgt7UeggVYKDazjDSPvLUEFUN5wEmydkP2AQIDAQABAoIBAQCDM4zetix6lx1B
GuSuVFrTc/vJBInkgQRFiVRi67fZS/R+CJl73WsonWO7+YUNzWdZJxpE2hJs/OUx
lSBqaL8u/gUuszRhS3BBHdpU4BQRCF/ndpVaqVNN+z78ZDrrE9Vo63nPdCRw6gYf
MnzhiVjMghdq6Kn6NZwvno45WrzCsIbrrQ4zU+S2PhG8MTA53jzqqQ8mUSJX0lAl
6b9+1aWA0d0Jnk3M3doaFU/Dlnz3n6kkx0AdqNe8bdsFrPfwsrF+dwGx04SGgLmK
V2OjIDFYYGtiHp3PJ9IYIA32ij+UloSDDZ2BxXkma8Zilw04ytY5l8tlk2ZDWTD9
U2MXxjmBAoGBAMmmI19I/asTPjljlqzrOsrdRkklJvnCHgy/yw9u3nMfkJ0lLGAp
mZoCqJIEsAqlLGM5bOjKy3KQ3n2SBX3mz7/RajnpJRTnNLeJIPAAXHN9TDyKcWRo
Los6xHN7YMSLYKs4HMihXp9Yu4Ms88/8nO/01nufjN0rTgFnWdL0WfxJAoGBAMhk
Qm92ukMmbrXSrV0WF+eFooHwgPmUWZ1oZY5ZHmO3FCuSBHiICGrWKmdbcG6H5zmZ
oFZ0unsvk2Yjl+/+tntxr/dwp6Q+chsqkLms8GE76NWEO8qn4hQNywkFgpKlPci3
n5IqpuQ2DpJ1PAQkwgZD/5rSscNidNMezXO5Uvv5AoGBALR291kjXcJpKlr6AbMn
oipD9c8obMVBMNuAGh7pvjORoD7DMf+tu0XV8z8a6uHcCOmUTx/XvlP9yuDeegO/
OVYV+NdzDDi04r0PAGdKK3NAQ6Y60Fhn1J/OLFqdpHDBu/X/9eKoaKJ7KvWumVUe
YuVtXTauB8c4JkujTwQ4ov/hAoGAHxvhbGhkFhSbT0K7gx3w7BJE3iM2AojTOKqC
SYzwOM6tJO5wHz4PAHbq8kyxsZcLgFenGoTYhlMmcM7JwYorThKiHKmyfL7s++ap
vQlp785bIPp8RcO2RyK1CFuAn79jTgujjA9vBTKXJIlqncIPFOXtgl1/FzPrqvK3
NmXoyhECgYEAje9hM9RYO0jbfmTZoQh+onMRz34SM9XWLH+NQGgfvsGtjeRnrUKK
GuWQz/GQGJLy/Uc1KHIdrfPDjvQhZXmPL1v7pNfCrqyj+EnKCNDPPnYq5Zq4WLsB
x1hKPH0LmfEBkXOiFGrD3h3KAuBK5nb0/EFBDR4JuMaySC5CpbOds9o=
-----END RSA PRIVATE KEY-----

View File

@ -0,0 +1,183 @@
#!/bin/bash
set -u
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
if [[ "$#" -eq 0 ]]; then
cat <<-EOF
To generate a server certificate, provide the domain name as a parameter:
$(basename $0) www.my-domain.tdl
$(basename $0) www.my-domain.tdl alternate.domain.tld
You can also create certificates for wildcard domains:
$(basename $0) '*.my-domain.tdl'
EOF
exit 0
else
DOMAIN="$1"
ALTERNATE_DOMAINS="DNS:$( echo "$@" | sed 's/ /,DNS:/g')"
fi
###############################################################################
# Create a nginx container (which conveniently provides the `openssl` command)
###############################################################################
CONTAINER=$(docker run -d -v $DIR:/work -w /work -e SAN="$ALTERNATE_DOMAINS" nginx:1.11.8)
# Configure openssl
docker exec $CONTAINER bash -c '
mkdir -p /ca/{certs,crl,private,newcerts} 2>/dev/null
echo 1000 > /ca/serial
touch /ca/index.txt
cat > /ca/openssl.cnf <<-"OESCRIPT"
[ ca ]
# `man ca`
default_ca = CA_default
[ CA_default ]
# Directory and file locations.
dir = /ca
certs = $dir/certs
crl_dir = $dir/crl
new_certs_dir = $dir/newcerts
database = $dir/index.txt
serial = $dir/serial
RANDFILE = $dir/private/.rand
# The root key and root certificate.
private_key = /work/ca-root.key
certificate = /work/ca-root.crt
# SHA-1 is deprecated, so use SHA-2 instead.
default_md = sha256
name_opt = ca_default
cert_opt = ca_default
default_days = 10000
preserve = no
policy = policy_loose
[ policy_loose ]
countryName = optional
stateOrProvinceName = optional
localityName = optional
organizationName = optional
organizationalUnitName = optional
commonName = supplied
emailAddress = optional
[ req ]
# Options for the `req` tool (`man req`).
default_bits = 2048
distinguished_name = req_distinguished_name
string_mask = utf8only
# SHA-1 is deprecated, so use SHA-2 instead.
default_md = sha256
# Extension to add when the -x509 option is used.
x509_extensions = v3_ca
[ req_distinguished_name ]
# See <https://en.wikipedia.org/wiki/Certificate_signing_request>.
countryName = Country Name (2 letter code)
stateOrProvinceName = State or Province Name
localityName = Locality Name
0.organizationName = Organization Name
organizationalUnitName = Organizational Unit Name
commonName = Common Name
emailAddress = Email Address
[ v3_ca ]
# Extensions for a typical CA (`man x509v3_config`).
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = critical, CA:true
keyUsage = critical, digitalSignature, cRLSign, keyCertSign
[ server_cert ]
# Extensions for server certificates (`man x509v3_config`).
basicConstraints = CA:FALSE
nsCertType = server
nsComment = server certificate generated for test purpose (nginx-proxy test suite)
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer:always
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
[ san_env ]
subjectAltName=${ENV::SAN}
OESCRIPT
'
# shortcut for calling `openssl` inside the container
function openssl {
docker exec $CONTAINER openssl "$@"
}
function exitfail {
echo
echo ERROR: "$@"
docker rm -f $CONTAINER
exit 1
}
###############################################################################
# Setup Certificate authority
###############################################################################
if ! [[ -f "$DIR/ca-root.key" ]]; then
echo
echo "> Create a Certificate Authority root key: $DIR/ca-root.key"
openssl genrsa -out ca-root.key 2048
[[ $? -eq 0 ]] || exitfail failed to generate CA root key
fi
# Create a CA root certificate
if ! [[ -f "$DIR/ca-root.crt" ]]; then
echo
echo "> Create a CA root certificate: $DIR/ca-root.crt"
openssl req -config /ca/openssl.cnf \
-key ca-root.key \
-new -x509 -days 3650 -subj "/O=nginx-proxy test suite/CN=www.nginx-proxy.tld" -extensions v3_ca \
-out ca-root.crt
[[ $? -eq 0 ]] || exitfail failed to generate CA root certificate
# Verify certificate
openssl x509 -noout -text -in ca-root.crt
fi
###############################################################################
# create server key and certificate signed by the certificate authority
###############################################################################
echo
echo "> Create a host key: $DIR/$DOMAIN.key"
openssl genrsa -out "$DOMAIN.key" 2048
echo
echo "> Create a host certificate signing request"
SAN="$ALTERNATE_DOMAINS" openssl req -config /ca/openssl.cnf \
-key "$DOMAIN.key" \
-new -out "/ca/$DOMAIN.csr" -days 1000 -extensions san_env -subj "/CN=$DOMAIN"
[[ $? -eq 0 ]] || exitfail failed to generate server certificate signing request
echo
echo "> Create server certificate: $DIR/$DOMAIN.crt"
SAN="$ALTERNATE_DOMAINS" openssl ca -config /ca/openssl.cnf -batch \
-extensions server_cert \
-extensions san_env \
-in "/ca/$DOMAIN.csr" \
-out "$DOMAIN.crt"
[[ $? -eq 0 ]] || exitfail failed to generate server certificate
# Verify host certificate
#openssl x509 -noout -text -in "$DOMAIN.crt"
docker rm -f $CONTAINER >/dev/null

461
test/conftest.py Normal file
View File

@ -0,0 +1,461 @@
from __future__ import print_function
import contextlib
import logging
import os
import shlex
import socket
import subprocess
import time
import re
import backoff
import docker
import pytest
import requests
from _pytest._code.code import ReprExceptionInfo
from requests.packages.urllib3.util.connection import HAS_IPV6
logging.basicConfig(level=logging.INFO)
logging.getLogger('backoff').setLevel(logging.INFO)
logging.getLogger('DNS').setLevel(logging.DEBUG)
logging.getLogger('requests.packages.urllib3.connectionpool').setLevel(logging.WARN)
CA_ROOT_CERTIFICATE = os.path.join(os.path.dirname(__file__), 'certs/ca-root.crt')
I_AM_RUNNING_INSIDE_A_DOCKER_CONTAINER = os.path.isfile("/.dockerenv")
FORCE_CONTAINER_IPV6 = False # ugly global state to consider containers' IPv6 address instead of IPv4
docker_client = docker.from_env()
###############################################################################
#
# utilities
#
###############################################################################
@contextlib.contextmanager
def ipv6(force_ipv6=True):
"""
Meant to be used as a context manager to force IPv6 sockets:
with ipv6():
nginxproxy.get("http://something.nginx-proxy.local") # force use of IPv6
with ipv6(False):
nginxproxy.get("http://something.nginx-proxy.local") # legacy behavior
"""
global FORCE_CONTAINER_IPV6
FORCE_CONTAINER_IPV6 = force_ipv6
yield
FORCE_CONTAINER_IPV6 = False
class requests_for_docker(object):
"""
Proxy for calling methods of the requests module.
When a HTTP response failed due to HTTP Error 404 or 502, retry a few times.
Provides method `get_conf` to extract the nginx-proxy configuration content.
"""
def __init__(self):
self.session = requests.Session()
if os.path.isfile(CA_ROOT_CERTIFICATE):
self.session.verify = CA_ROOT_CERTIFICATE
def get_conf(self):
"""
Return the nginx config file
"""
nginx_proxy_containers = docker_client.containers.list(filters={"ancestor": "jwilder/nginx-proxy:test"})
if len(nginx_proxy_containers) > 1:
pytest.fail("Too many running jwilder/nginx-proxy:test containers", pytrace=False)
elif len(nginx_proxy_containers) == 0:
pytest.fail("No running jwilder/nginx-proxy:test container", pytrace=False)
return get_nginx_conf_from_container(nginx_proxy_containers[0])
def get(self, *args, **kwargs):
with ipv6(kwargs.pop('ipv6', False)):
@backoff.on_predicate(backoff.constant, lambda r: r.status_code in (404, 502), interval=.3, max_tries=30, jitter=None)
def _get(*args, **kwargs):
return self.session.get(*args, **kwargs)
return _get(*args, **kwargs)
def post(self, *args, **kwargs):
with ipv6(kwargs.pop('ipv6', False)):
@backoff.on_predicate(backoff.constant, lambda r: r.status_code in (404, 502), interval=.3, max_tries=30, jitter=None)
def _post(*args, **kwargs):
return self.session.post(*args, **kwargs)
return _post(*args, **kwargs)
def put(self, *args, **kwargs):
with ipv6(kwargs.pop('ipv6', False)):
@backoff.on_predicate(backoff.constant, lambda r: r.status_code in (404, 502), interval=.3, max_tries=30, jitter=None)
def _put(*args, **kwargs):
return self.session.put(*args, **kwargs)
return _put(*args, **kwargs)
def head(self, *args, **kwargs):
with ipv6(kwargs.pop('ipv6', False)):
@backoff.on_predicate(backoff.constant, lambda r: r.status_code in (404, 502), interval=.3, max_tries=30, jitter=None)
def _head(*args, **kwargs):
return self.session.head(*args, **kwargs)
return _head(*args, **kwargs)
def delete(self, *args, **kwargs):
with ipv6(kwargs.pop('ipv6', False)):
@backoff.on_predicate(backoff.constant, lambda r: r.status_code in (404, 502), interval=.3, max_tries=30, jitter=None)
def _delete(*args, **kwargs):
return self.session.delete(*args, **kwargs)
return _delete(*args, **kwargs)
def options(self, *args, **kwargs):
with ipv6(kwargs.pop('ipv6', False)):
@backoff.on_predicate(backoff.constant, lambda r: r.status_code in (404, 502), interval=.3, max_tries=30, jitter=None)
def _options(*args, **kwargs):
return self.session.options(*args, **kwargs)
return _options(*args, **kwargs)
def __getattr__(self, name):
return getattr(requests, name)
def container_ip(container):
"""
return the IP address of a container.
If the global FORCE_CONTAINER_IPV6 flag is set, return the IPv6 address
"""
global FORCE_CONTAINER_IPV6
if FORCE_CONTAINER_IPV6:
if not HAS_IPV6:
pytest.skip("This system does not support IPv6")
ip = container_ipv6(container)
if ip == '':
pytest.skip("Container %s has no IPv6 address" % container.name)
else:
return ip
else:
net_info = container.attrs["NetworkSettings"]["Networks"]
if "bridge" in net_info:
return net_info["bridge"]["IPAddress"]
# not default bridge network, fallback on first network defined
network_name = net_info.keys()[0]
return net_info[network_name]["IPAddress"]
def container_ipv6(container):
"""
return the IPv6 address of a container.
"""
net_info = container.attrs["NetworkSettings"]["Networks"]
if "bridge" in net_info:
return net_info["bridge"]["GlobalIPv6Address"]
# not default bridge network, fallback on first network defined
network_name = net_info.keys()[0]
return net_info[network_name]["GlobalIPv6Address"]
def nginx_proxy_dns_resolver(domain_name):
"""
if "nginx-proxy" if found in host, return the ip address of the docker container
issued from the docker image jwilder/nginx-proxy:test.
:return: IP or None
"""
log = logging.getLogger('DNS')
log.debug("nginx_proxy_dns_resolver(%r)" % domain_name)
if 'nginx-proxy' in domain_name:
nginxproxy_containers = docker_client.containers.list(filters={"status": "running", "ancestor": "jwilder/nginx-proxy:test"})
if len(nginxproxy_containers) == 0:
log.warn("no container found from image jwilder/nginx-proxy:test while resolving %r", domain_name)
return
nginxproxy_container = nginxproxy_containers[0]
ip = container_ip(nginxproxy_container)
log.info("resolving domain name %r as IP address %s of nginx-proxy container %s" % (domain_name, ip, nginxproxy_container.name))
return ip
def docker_container_dns_resolver(domain_name):
"""
if domain name is of the form "XXX.container.docker" or "anything.XXX.container.docker", return the ip address of the docker container
named XXX.
:return: IP or None
"""
log = logging.getLogger('DNS')
log.debug("docker_container_dns_resolver(%r)" % domain_name)
match = re.search('(^|.+\.)(?P<container>[^.]+)\.container\.docker$', domain_name)
if not match:
log.debug("%r does not match" % domain_name)
return
container_name = match.group('container')
log.debug("looking for container %r" % container_name)
try:
container = docker_client.containers.get(container_name)
except docker.errors.NotFound:
log.warn("container named %r not found while resolving %r" % (container_name, domain_name))
return
log.debug("container %r found (%s)" % (container.name, container.short_id))
ip = container_ip(container)
log.info("resolving domain name %r as IP address %s of container %s" % (domain_name, ip, container.name))
return ip
def monkey_patch_urllib_dns_resolver():
"""
Alter the behavior of the urllib DNS resolver so that any domain name
containing substring 'nginx-proxy' will resolve to the IP address
of the container created from image 'jwilder/nginx-proxy:test'.
"""
prv_getaddrinfo = socket.getaddrinfo
dns_cache = {}
def new_getaddrinfo(*args):
logging.getLogger('DNS').debug("resolving domain name %s" % repr(args))
_args = list(args)
# custom DNS resolvers
ip = nginx_proxy_dns_resolver(args[0])
if ip is None:
ip = docker_container_dns_resolver(args[0])
if ip is not None:
_args[0] = ip
# call on original DNS resolver, with eventually the original host changed to the wanted IP address
try:
return dns_cache[tuple(_args)]
except KeyError:
res = prv_getaddrinfo(*_args)
dns_cache[tuple(_args)] = res
return res
socket.getaddrinfo = new_getaddrinfo
return prv_getaddrinfo
def restore_urllib_dns_resolver(getaddrinfo_func):
socket.getaddrinfo = getaddrinfo_func
def remove_all_containers():
for container in docker_client.containers.list(all=True):
if I_AM_RUNNING_INSIDE_A_DOCKER_CONTAINER and container.id.startswith(socket.gethostname()):
continue # pytest is running within a Docker container, so we do not want to remove that particular container
logging.info("removing container %s" % container.name)
container.remove(v=True, force=True)
def get_nginx_conf_from_container(container):
"""
return the nginx /etc/nginx/conf.d/default.conf file content from a container
"""
import tarfile
from cStringIO import StringIO
strm, stat = container.get_archive('/etc/nginx/conf.d/default.conf')
with tarfile.open(fileobj=StringIO(strm.read())) as tf:
conffile = tf.extractfile('default.conf')
return conffile.read()
def docker_compose_up(compose_file='docker-compose.yml'):
logging.info('docker-compose -f %s up -d' % compose_file)
try:
subprocess.check_output(shlex.split('docker-compose -f %s up -d' % compose_file), stderr=subprocess.STDOUT)
except subprocess.CalledProcessError, e:
pytest.fail("Error while runninng 'docker-compose -f %s up -d':\n%s" % (compose_file, e.output), pytrace=False)
def docker_compose_down(compose_file='docker-compose.yml'):
logging.info('docker-compose -f %s down' % compose_file)
try:
subprocess.check_output(shlex.split('docker-compose -f %s down' % compose_file), stderr=subprocess.STDOUT)
except subprocess.CalledProcessError, e:
pytest.fail("Error while runninng 'docker-compose -f %s down':\n%s" % (compose_file, e.output), pytrace=False)
def wait_for_nginxproxy_to_be_ready():
"""
If one (and only one) container started from image jwilder/nginx-proxy:test is found,
wait for its log to contain substring "Watching docker events"
"""
containers = docker_client.containers.list(filters={"ancestor": "jwilder/nginx-proxy:test"})
if len(containers) != 1:
return
container = containers[0]
for line in container.logs(stream=True):
if "Watching docker events" in line:
logging.debug("nginx-proxy ready")
break
def find_docker_compose_file(request):
"""
helper for fixture functions to figure out the name of the docker-compose file to consider.
- if the test module provides a `docker_compose_file` variable, take that
- else, if a yaml file exists with the same name as the test module (but for the `.yml` extension), use that
- otherwise use `docker-compose.yml`.
"""
test_module_dir = os.path.dirname(request.module.__file__)
yml_file = os.path.join(test_module_dir, request.module.__name__ + '.yml')
yaml_file = os.path.join(test_module_dir, request.module.__name__ + '.yaml')
default_file = os.path.join(test_module_dir, 'docker-compose.yml')
docker_compose_file_module_variable = getattr(request.module, "docker_compose_file", None)
if docker_compose_file_module_variable is not None:
docker_compose_file = os.path.join( test_module_dir, docker_compose_file_module_variable)
if not os.path.isfile(docker_compose_file):
raise ValueError("docker compose file %r could not be found. Check your test module `docker_compose_file` variable value." % docker_compose_file)
else:
if os.path.isfile(yml_file):
docker_compose_file = yml_file
elif os.path.isfile(yaml_file):
docker_compose_file = yaml_file
else:
docker_compose_file = default_file
if not os.path.isfile(docker_compose_file):
logging.error("Could not find any docker-compose file named either '{0}.yml', '{0}.yaml' or 'docker-compose.yml'".format(request.module.__name__))
logging.debug("using docker compose file %s" % docker_compose_file)
return docker_compose_file
def connect_to_network(network):
"""
If we are running from a container, connect our container to the given network
:return: the name of the network we were connected to, or None
"""
if I_AM_RUNNING_INSIDE_A_DOCKER_CONTAINER:
try:
my_container = docker_client.containers.get(socket.gethostname())
except docker.errors.NotFound:
logging.warn("container %r not found" % socket.gethostname())
return
# figure out our container networks
my_networks = my_container.attrs["NetworkSettings"]["Networks"].keys()
# make sure our container is connected to the nginx-proxy's network
if network not in my_networks:
logging.info("Connecting to docker network: %s" % network.name)
network.connect(my_container)
return network
def disconnect_from_network(network=None):
"""
If we are running from a container, disconnect our container from the given network.
:param network: name of a docker network to disconnect from
"""
if I_AM_RUNNING_INSIDE_A_DOCKER_CONTAINER and network is not None:
try:
my_container = docker_client.containers.get(socket.gethostname())
except docker.errors.NotFound:
logging.warn("container %r not found" % socket.gethostname())
return
# figure out our container networks
my_networks_names = my_container.attrs["NetworkSettings"]["Networks"].keys()
# disconnect our container from the given network
if network.name in my_networks_names:
logging.info("Disconnecting from network %s" % network.name)
network.disconnect(my_container)
def connect_to_all_networks():
"""
If we are running from a container, connect our container to all current docker networks.
:return: a list of networks we connected to
"""
if not I_AM_RUNNING_INSIDE_A_DOCKER_CONTAINER:
return []
else:
# find the list of docker networks
networks = filter(lambda network: len(network.containers) > 0 and network.name != 'bridge', docker_client.networks.list())
return [connect_to_network(network) for network in networks]
###############################################################################
#
# Py.test fixtures
#
###############################################################################
@pytest.yield_fixture(scope="module")
def docker_compose(request):
"""
pytest fixture providing containers described in a docker compose file. After the tests, remove the created containers
A custom docker compose file name can be defined in a variable named `docker_compose_file`.
Also, in the case where pytest is running from a docker container, this fixture makes sure
our container will be attached to all the docker networks.
"""
docker_compose_file = find_docker_compose_file(request)
original_dns_resolver = monkey_patch_urllib_dns_resolver()
remove_all_containers()
docker_compose_up(docker_compose_file)
networks = connect_to_all_networks()
wait_for_nginxproxy_to_be_ready()
time.sleep(3) # give time to containers to be ready
yield docker_client
for network in networks:
disconnect_from_network(network)
docker_compose_down(docker_compose_file)
restore_urllib_dns_resolver(original_dns_resolver)
@pytest.yield_fixture()
def nginxproxy():
"""
Provides the `nginxproxy` object that can be used in the same way the requests module is:
r = nginxproxy.get("http://foo.com")
The difference is that in case an HTTP requests has status code 404 or 502 (which mostly
indicates that nginx has just reloaded), we retry up to 30 times the query.
Also, the nginxproxy methods accept an additional keyword parameter: `ipv6` which forces requests
made against containers to use the containers IPv6 address when set to `True`. If IPv6 is not
supported by the system or docker, that particular test will be skipped.
"""
yield requests_for_docker()
###############################################################################
#
# Py.test hooks
#
###############################################################################
# pytest hook to display additionnal stuff in test report
def pytest_runtest_logreport(report):
if report.failed:
if isinstance(report.longrepr, ReprExceptionInfo):
test_containers = docker_client.containers.list(all=True, filters={"ancestor": "jwilder/nginx-proxy:test"})
for container in test_containers:
report.longrepr.addsection('nginx-proxy logs', container.logs())
report.longrepr.addsection('nginx-proxy conf', get_nginx_conf_from_container(container))
###############################################################################
#
# Check requirements
#
###############################################################################
try:
docker_client.images.get('jwilder/nginx-proxy:test')
except docker.errors.ImageNotFound:
pytest.exit("The docker image 'jwilder/nginx-proxy:test' is missing")
if docker.__version__ != "2.0.2":
pytest.exit("This test suite is meant to work with the python docker module v2.0.2")

View File

@ -1,33 +0,0 @@
#!/usr/bin/env bats
load test_helpers
function setup {
# make sure to stop any web container before each test so we don't
# have any unexpected contaiener running with VIRTUAL_HOST or VIRUTAL_PORT set
stop_bats_containers web
}
@test "[$TEST_FILE] DEFAULT_HOST=web1.bats" {
SUT_CONTAINER=bats-nginx-proxy-${TEST_FILE}-1
# GIVEN a webserver with VIRTUAL_HOST set to web.bats
prepare_web_container bats-web 80 -e VIRTUAL_HOST=web.bats
# WHEN nginx-proxy runs with DEFAULT_HOST set to web.bats
run nginxproxy $SUT_CONTAINER -v /var/run/docker.sock:/tmp/docker.sock:ro -e DEFAULT_HOST=web.bats
assert_success
docker_wait_for_log $SUT_CONTAINER 9 "Watching docker events"
# THEN querying the proxy without Host header → 200
run curl_container $SUT_CONTAINER / --head
assert_output -l 0 $'HTTP/1.1 200 OK\r'
# THEN querying the proxy with any other Host header → 200
run curl_container $SUT_CONTAINER / --head --header "Host: something.I.just.made.up"
assert_output -l 0 $'HTTP/1.1 200 OK\r'
}
@test "[$TEST_FILE] stop all bats containers" {
stop_bats_containers
}

View File

@ -1,123 +0,0 @@
#!/usr/bin/env bats
load test_helpers
@test "[$TEST_FILE] start 2 web containers" {
prepare_web_container bats-web1 81 -e VIRTUAL_HOST=web1.bats
prepare_web_container bats-web2 82 -e VIRTUAL_HOST=web2.bats
}
@test "[$TEST_FILE] -v /var/run/docker.sock:/tmp/docker.sock:ro" {
SUT_CONTAINER=bats-nginx-proxy-${TEST_FILE}-1
# WHEN nginx-proxy runs on our docker host using the default unix socket
run nginxproxy $SUT_CONTAINER -v /var/run/docker.sock:/tmp/docker.sock:ro
assert_success
docker_wait_for_log $SUT_CONTAINER 9 "Watching docker events"
# THEN
assert_nginxproxy_behaves $SUT_CONTAINER
}
@test "[$TEST_FILE] -v /var/run/docker.sock:/f00.sock:ro -e DOCKER_HOST=unix:///f00.sock" {
SUT_CONTAINER=bats-nginx-proxy-${TEST_FILE}-2
# WHEN nginx-proxy runs on our docker host using a custom unix socket
run nginxproxy $SUT_CONTAINER -v /var/run/docker.sock:/f00.sock:ro -e DOCKER_HOST=unix:///f00.sock
assert_success
docker_wait_for_log $SUT_CONTAINER 9 "Watching docker events"
# THEN
assert_nginxproxy_behaves $SUT_CONTAINER
}
@test "[$TEST_FILE] -e DOCKER_HOST=tcp://..." {
SUT_CONTAINER=bats-nginx-proxy-${TEST_FILE}-3
# GIVEN a container exposing our docker host over TCP
run docker_tcp bats-docker-tcp
assert_success
sleep 1s
# WHEN nginx-proxy runs on our docker host using tcp to connect to our docker host
run nginxproxy $SUT_CONTAINER -e DOCKER_HOST="tcp://bats-docker-tcp:2375" --link bats-docker-tcp:bats-docker-tcp
assert_success
docker_wait_for_log $SUT_CONTAINER 9 "Watching docker events"
# THEN
assert_nginxproxy_behaves $SUT_CONTAINER
}
@test "[$TEST_FILE] separated containers (nginx + docker-gen + nginx.tmpl)" {
docker_clean bats-nginx
docker_clean bats-docker-gen
# GIVEN a simple nginx container
run docker run -d \
--label bats-type="nginx" \
--name bats-nginx \
-v /etc/nginx/conf.d/ \
-v /etc/nginx/certs/ \
nginx:latest
assert_success
run retry 5 1s docker run --label bats-type="curl" appropriate/curl --silent --fail --head http://$(docker_ip bats-nginx)/
assert_output -l 0 $'HTTP/1.1 200 OK\r'
# WHEN docker-gen runs on our docker host
run docker run -d \
--label bats-type="docker-gen" \
--name bats-docker-gen \
-v /var/run/docker.sock:/tmp/docker.sock:ro \
-v $BATS_TEST_DIRNAME/../nginx.tmpl:/etc/docker-gen/templates/nginx.tmpl:ro \
--volumes-from bats-nginx \
--expose 80 \
jwilder/docker-gen:0.7.3 \
-notify-sighup bats-nginx \
-watch \
-only-exposed \
/etc/docker-gen/templates/nginx.tmpl \
/etc/nginx/conf.d/default.conf
assert_success
docker_wait_for_log bats-docker-gen 9 "Watching docker events"
# Give some time to the docker-gen container to notify bats-nginx so it
# reloads its config
sleep 2s
run docker_running_state bats-nginx
assert_output "true" || {
docker logs bats-docker-gen
false
} >&2
# THEN
assert_nginxproxy_behaves bats-nginx
}
@test "[$TEST_FILE] stop all bats containers" {
stop_bats_containers
}
# $1 nginx-proxy container
function assert_nginxproxy_behaves {
local -r container=$1
# Querying the proxy without Host header → 503
run curl_container $container / --head
assert_output -l 0 $'HTTP/1.1 503 Service Temporarily Unavailable\r'
# Querying the proxy with Host header → 200
run curl_container $container /port --header "Host: web1.bats"
assert_output "answer from port 81"
run curl_container $container /port --header "Host: web2.bats"
assert_output "answer from port 82"
# Querying the proxy with unknown Host header → 503
run curl_container $container /port --header "Host: webFOO.bats" --head
assert_output -l 0 $'HTTP/1.1 503 Service Temporarily Unavailable\r'
}

View File

@ -1,139 +0,0 @@
#!/usr/bin/env bats
load test_helpers
SUT_CONTAINER=bats-nginx-proxy-${TEST_FILE}
function setup {
# make sure to stop any web container before each test so we don't
# have any unexpected container running with VIRTUAL_HOST or VIRUTAL_PORT set
stop_bats_containers web
}
@test "[$TEST_FILE] start a nginx-proxy container" {
# GIVEN
run nginxproxy $SUT_CONTAINER -v /var/run/docker.sock:/tmp/docker.sock:ro
assert_success
docker_wait_for_log $SUT_CONTAINER 9 "Watching docker events"
}
@test "[$TEST_FILE] nginx-proxy passes arbitrary header" {
# WHEN
prepare_web_container bats-host-1 80 -e VIRTUAL_HOST=web.bats
dockergen_wait_for_event $SUT_CONTAINER start bats-host-1
sleep 1
# THEN
run curl_container $SUT_CONTAINER /headers -H "Foo: Bar" -H "Host: web.bats"
assert_output -l 'Foo: Bar'
}
##### Testing the handling of X-Forwarded-For #####
@test "[$TEST_FILE] nginx-proxy generates X-Forwarded-For" {
# WHEN
prepare_web_container bats-host-2 80 -e VIRTUAL_HOST=web.bats
dockergen_wait_for_event $SUT_CONTAINER start bats-host-2
sleep 1
# THEN
run curl_container $SUT_CONTAINER /headers -H "Host: web.bats"
assert_output -p 'X-Forwarded-For:'
}
@test "[$TEST_FILE] nginx-proxy passes X-Forwarded-For" {
# WHEN
prepare_web_container bats-host-3 80 -e VIRTUAL_HOST=web.bats
dockergen_wait_for_event $SUT_CONTAINER start bats-host-3
sleep 1
# THEN
run curl_container $SUT_CONTAINER /headers -H "X-Forwarded-For: 1.2.3.4" -H "Host: web.bats"
assert_output -p 'X-Forwarded-For: 1.2.3.4, '
}
##### Testing the handling of X-Forwarded-Proto #####
@test "[$TEST_FILE] nginx-proxy generates X-Forwarded-Proto" {
# WHEN
prepare_web_container bats-host-4 80 -e VIRTUAL_HOST=web.bats
dockergen_wait_for_event $SUT_CONTAINER start bats-host-4
sleep 1
# THEN
run curl_container $SUT_CONTAINER /headers -H "Host: web.bats"
assert_output -l 'X-Forwarded-Proto: http'
}
@test "[$TEST_FILE] nginx-proxy passes X-Forwarded-Proto" {
# WHEN
prepare_web_container bats-host-5 80 -e VIRTUAL_HOST=web.bats
dockergen_wait_for_event $SUT_CONTAINER start bats-host-5
sleep 1
# THEN
run curl_container $SUT_CONTAINER /headers -H "X-Forwarded-Proto: https" -H "Host: web.bats"
assert_output -l 'X-Forwarded-Proto: https'
}
##### Testing the handling of X-Forwarded-Port #####
@test "[$TEST_FILE] nginx-proxy generates X-Forwarded-Port" {
# WHEN
prepare_web_container bats-host-6 80 -e VIRTUAL_HOST=web.bats
dockergen_wait_for_event $SUT_CONTAINER start bats-host-6
sleep 1
# THEN
run curl_container $SUT_CONTAINER /headers -H "Host: web.bats"
assert_output -l 'X-Forwarded-Port: 80'
}
@test "[$TEST_FILE] nginx-proxy passes X-Forwarded-Port" {
# WHEN
prepare_web_container bats-host-7 80 -e VIRTUAL_HOST=web.bats
dockergen_wait_for_event $SUT_CONTAINER start bats-host-7
sleep 1
# THEN
run curl_container $SUT_CONTAINER /headers -H "X-Forwarded-Port: 1234" -H "Host: web.bats"
assert_output -l 'X-Forwarded-Port: 1234'
}
##### Other headers
@test "[$TEST_FILE] nginx-proxy generates X-Real-IP" {
# WHEN
prepare_web_container bats-host-8 80 -e VIRTUAL_HOST=web.bats
dockergen_wait_for_event $SUT_CONTAINER start bats-host-8
sleep 1
# THEN
run curl_container $SUT_CONTAINER /headers -H "Host: web.bats"
assert_output -p 'X-Real-IP: '
}
@test "[$TEST_FILE] nginx-proxy passes Host" {
# WHEN
prepare_web_container bats-host-9 80 -e VIRTUAL_HOST=web.bats
dockergen_wait_for_event $SUT_CONTAINER start bats-host-9
sleep 1
# THEN
run curl_container $SUT_CONTAINER /headers -H "Host: web.bats"
assert_output -l 'Host: web.bats'
}
@test "[$TEST_FILE] nginx-proxy supresses Proxy for httpoxy protection" {
# WHEN
prepare_web_container bats-host-10 80 -e VIRTUAL_HOST=web.bats
dockergen_wait_for_event $SUT_CONTAINER start bats-host-10
sleep 1
# THEN
run curl_container $SUT_CONTAINER /headers -H "Proxy: tcp://foo.com" -H "Host: web.bats"
refute_output -l 'Proxy: tcp://foo.com'
}
@test "[$TEST_FILE] stop all bats containers" {
stop_bats_containers
}

View File

@ -1,6 +0,0 @@
bats lib
========
found on https://github.com/sstephenson/bats/pull/110
When that pull request will be merged, the `test/lib/bats` won't be necessary anymore.

View File

@ -1,596 +0,0 @@
#
# batslib.bash
# ------------
#
# The Standard Library is a collection of test helpers intended to
# simplify testing. It contains the following types of test helpers.
#
# - Assertions are functions that perform a test and output relevant
# information on failure to help debugging. They return 1 on failure
# and 0 otherwise.
#
# All output is formatted for readability using the functions of
# `output.bash' and sent to the standard error.
#
source "${BATS_LIB}/batslib/output.bash"
########################################################################
# ASSERTIONS
########################################################################
# Fail and display a message. When no parameters are specified, the
# message is read from the standard input. Other functions use this to
# report failure.
#
# Globals:
# none
# Arguments:
# $@ - [=STDIN] message
# Returns:
# 1 - always
# Inputs:
# STDIN - [=$@] message
# Outputs:
# STDERR - message
fail() {
(( $# == 0 )) && batslib_err || batslib_err "$@"
return 1
}
# Fail and display details if the expression evaluates to false. Details
# include the expression, `$status' and `$output'.
#
# NOTE: The expression must be a simple command. Compound commands, such
# as `[[', can be used only when executed with `bash -c'.
#
# Globals:
# status
# output
# Arguments:
# $1 - expression
# Returns:
# 0 - expression evaluates to TRUE
# 1 - otherwise
# Outputs:
# STDERR - details, on failure
assert() {
if ! "$@"; then
{ local -ar single=(
'expression' "$*"
'status' "$status"
)
local -ar may_be_multi=(
'output' "$output"
)
local -ir width="$( batslib_get_max_single_line_key_width \
"${single[@]}" "${may_be_multi[@]}" )"
batslib_print_kv_single "$width" "${single[@]}"
batslib_print_kv_single_or_multi "$width" "${may_be_multi[@]}"
} | batslib_decorate 'assertion failed' \
| fail
fi
}
# Fail and display details if the expected and actual values do not
# equal. Details include both values.
#
# Globals:
# none
# Arguments:
# $1 - actual value
# $2 - expected value
# Returns:
# 0 - values equal
# 1 - otherwise
# Outputs:
# STDERR - details, on failure
assert_equal() {
if [[ $1 != "$2" ]]; then
batslib_print_kv_single_or_multi 8 \
'expected' "$2" \
'actual' "$1" \
| batslib_decorate 'values do not equal' \
| fail
fi
}
# Fail and display details if `$status' is not 0. Details include
# `$status' and `$output'.
#
# Globals:
# status
# output
# Arguments:
# none
# Returns:
# 0 - `$status' is 0
# 1 - otherwise
# Outputs:
# STDERR - details, on failure
assert_success() {
if (( status != 0 )); then
{ local -ir width=6
batslib_print_kv_single "$width" 'status' "$status"
batslib_print_kv_single_or_multi "$width" 'output' "$output"
} | batslib_decorate 'command failed' \
| fail
fi
}
# Fail and display details if `$status' is 0. Details include `$output'.
#
# Optionally, when the expected status is specified, fail when it does
# not equal `$status'. In this case, details include the expected and
# actual status, and `$output'.
#
# Globals:
# status
# output
# Arguments:
# $1 - [opt] expected status
# Returns:
# 0 - `$status' is not 0, or
# `$status' equals the expected status
# 1 - otherwise
# Outputs:
# STDERR - details, on failure
assert_failure() {
(( $# > 0 )) && local -r expected="$1"
if (( status == 0 )); then
batslib_print_kv_single_or_multi 6 'output' "$output" \
| batslib_decorate 'command succeeded, but it was expected to fail' \
| fail
elif (( $# > 0 )) && (( status != expected )); then
{ local -ir width=8
batslib_print_kv_single "$width" \
'expected' "$expected" \
'actual' "$status"
batslib_print_kv_single_or_multi "$width" \
'output' "$output"
} | batslib_decorate 'command failed as expected, but status differs' \
| fail
fi
}
# Fail and display details if the expected does not match the actual
# output or a fragment of it.
#
# By default, the entire output is matched. The assertion fails if the
# expected output does not equal `$output'. Details include both values.
#
# When `-l <index>' is used, only the <index>-th line is matched. The
# assertion fails if the expected line does not equal
# `${lines[<index>}'. Details include the compared lines and <index>.
#
# When `-l' is used without the <index> argument, the output is searched
# for the expected line. The expected line is matched against each line
# in `${lines[@]}'. If no match is found the assertion fails. Details
# include the expected line and `$output'.
#
# By default, literal matching is performed. Options `-p' and `-r'
# enable partial (i.e. substring) and extended regular expression
# matching, respectively. Specifying an invalid extended regular
# expression with `-r' displays an error.
#
# Options `-p' and `-r' are mutually exclusive. When used
# simultaneously, an error is displayed.
#
# Globals:
# output
# lines
# Options:
# -l <index> - match against the <index>-th element of `${lines[@]}'
# -l - search `${lines[@]}' for the expected line
# -p - partial matching
# -r - extended regular expression matching
# Arguments:
# $1 - expected output
# Returns:
# 0 - expected matches the actual output
# 1 - otherwise
# Outputs:
# STDERR - details, on failure
# error message, on error
assert_output() {
local -i is_match_line=0
local -i is_match_contained=0
local -i is_mode_partial=0
local -i is_mode_regex=0
# Handle options.
while (( $# > 0 )); do
case "$1" in
-l)
if (( $# > 2 )) && [[ $2 =~ ^([0-9]|[1-9][0-9]+)$ ]]; then
is_match_line=1
local -ri idx="$2"
shift
else
is_match_contained=1;
fi
shift
;;
-p) is_mode_partial=1; shift ;;
-r) is_mode_regex=1; shift ;;
--) break ;;
*) break ;;
esac
done
if (( is_match_line )) && (( is_match_contained )); then
echo "\`-l' and \`-l <index>' are mutually exclusive" \
| batslib_decorate 'ERROR: assert_output' \
| fail
return $?
fi
if (( is_mode_partial )) && (( is_mode_regex )); then
echo "\`-p' and \`-r' are mutually exclusive" \
| batslib_decorate 'ERROR: assert_output' \
| fail
return $?
fi
# Arguments.
local -r expected="$1"
if (( is_mode_regex == 1 )) && [[ '' =~ $expected ]] || (( $? == 2 )); then
echo "Invalid extended regular expression: \`$expected'" \
| batslib_decorate 'ERROR: assert_output' \
| fail
return $?
fi
# Matching.
if (( is_match_contained )); then
# Line contained in output.
if (( is_mode_regex )); then
local -i idx
for (( idx = 0; idx < ${#lines[@]}; ++idx )); do
[[ ${lines[$idx]} =~ $expected ]] && return 0
done
{ local -ar single=(
'regex' "$expected"
)
local -ar may_be_multi=(
'output' "$output"
)
local -ir width="$( batslib_get_max_single_line_key_width \
"${single[@]}" "${may_be_multi[@]}" )"
batslib_print_kv_single "$width" "${single[@]}"
batslib_print_kv_single_or_multi "$width" "${may_be_multi[@]}"
} | batslib_decorate 'no output line matches regular expression' \
| fail
elif (( is_mode_partial )); then
local -i idx
for (( idx = 0; idx < ${#lines[@]}; ++idx )); do
[[ ${lines[$idx]} == *"$expected"* ]] && return 0
done
{ local -ar single=(
'substring' "$expected"
)
local -ar may_be_multi=(
'output' "$output"
)
local -ir width="$( batslib_get_max_single_line_key_width \
"${single[@]}" "${may_be_multi[@]}" )"
batslib_print_kv_single "$width" "${single[@]}"
batslib_print_kv_single_or_multi "$width" "${may_be_multi[@]}"
} | batslib_decorate 'no output line contains substring' \
| fail
else
local -i idx
for (( idx = 0; idx < ${#lines[@]}; ++idx )); do
[[ ${lines[$idx]} == "$expected" ]] && return 0
done
{ local -ar single=(
'line' "$expected"
)
local -ar may_be_multi=(
'output' "$output"
)
local -ir width="$( batslib_get_max_single_line_key_width \
"${single[@]}" "${may_be_multi[@]}" )"
batslib_print_kv_single "$width" "${single[@]}"
batslib_print_kv_single_or_multi "$width" "${may_be_multi[@]}"
} | batslib_decorate 'output does not contain line' \
| fail
fi
elif (( is_match_line )); then
# Specific line.
if (( is_mode_regex )); then
if ! [[ ${lines[$idx]} =~ $expected ]]; then
batslib_print_kv_single 5 \
'index' "$idx" \
'regex' "$expected" \
'line' "${lines[$idx]}" \
| batslib_decorate 'regular expression does not match line' \
| fail
fi
elif (( is_mode_partial )); then
if [[ ${lines[$idx]} != *"$expected"* ]]; then
batslib_print_kv_single 9 \
'index' "$idx" \
'substring' "$expected" \
'line' "${lines[$idx]}" \
| batslib_decorate 'line does not contain substring' \
| fail
fi
else
if [[ ${lines[$idx]} != "$expected" ]]; then
batslib_print_kv_single 8 \
'index' "$idx" \
'expected' "$expected" \
'actual' "${lines[$idx]}" \
| batslib_decorate 'line differs' \
| fail
fi
fi
else
# Entire output.
if (( is_mode_regex )); then
if ! [[ $output =~ $expected ]]; then
batslib_print_kv_single_or_multi 6 \
'regex' "$expected" \
'output' "$output" \
| batslib_decorate 'regular expression does not match output' \
| fail
fi
elif (( is_mode_partial )); then
if [[ $output != *"$expected"* ]]; then
batslib_print_kv_single_or_multi 9 \
'substring' "$expected" \
'output' "$output" \
| batslib_decorate 'output does not contain substring' \
| fail
fi
else
if [[ $output != "$expected" ]]; then
batslib_print_kv_single_or_multi 8 \
'expected' "$expected" \
'actual' "$output" \
| batslib_decorate 'output differs' \
| fail
fi
fi
fi
}
# Fail and display details if the unexpected matches the actual output
# or a fragment of it.
#
# By default, the entire output is matched. The assertion fails if the
# unexpected output equals `$output'. Details include `$output'.
#
# When `-l <index>' is used, only the <index>-th line is matched. The
# assertion fails if the unexpected line equals `${lines[<index>}'.
# Details include the compared line and <index>.
#
# When `-l' is used without the <index> argument, the output is searched
# for the unexpected line. The unexpected line is matched against each
# line in `${lines[<index>]}'. If a match is found the assertion fails.
# Details include the unexpected line, the index where it was found and
# `$output' (with the unexpected line highlighted in it if `$output` is
# longer than one line).
#
# By default, literal matching is performed. Options `-p' and `-r'
# enable partial (i.e. substring) and extended regular expression
# matching, respectively. On failure, the substring or the regular
# expression is added to the details (if not already displayed).
# Specifying an invalid extended regular expression with `-r' displays
# an error.
#
# Options `-p' and `-r' are mutually exclusive. When used
# simultaneously, an error is displayed.
#
# Globals:
# output
# lines
# Options:
# -l <index> - match against the <index>-th element of `${lines[@]}'
# -l - search `${lines[@]}' for the unexpected line
# -p - partial matching
# -r - extended regular expression matching
# Arguments:
# $1 - unexpected output
# Returns:
# 0 - unexpected matches the actual output
# 1 - otherwise
# Outputs:
# STDERR - details, on failure
# error message, on error
refute_output() {
local -i is_match_line=0
local -i is_match_contained=0
local -i is_mode_partial=0
local -i is_mode_regex=0
# Handle options.
while (( $# > 0 )); do
case "$1" in
-l)
if (( $# > 2 )) && [[ $2 =~ ^([0-9]|[1-9][0-9]+)$ ]]; then
is_match_line=1
local -ri idx="$2"
shift
else
is_match_contained=1;
fi
shift
;;
-L) is_match_contained=1; shift ;;
-p) is_mode_partial=1; shift ;;
-r) is_mode_regex=1; shift ;;
--) break ;;
*) break ;;
esac
done
if (( is_match_line )) && (( is_match_contained )); then
echo "\`-l' and \`-l <index>' are mutually exclusive" \
| batslib_decorate 'ERROR: refute_output' \
| fail
return $?
fi
if (( is_mode_partial )) && (( is_mode_regex )); then
echo "\`-p' and \`-r' are mutually exclusive" \
| batslib_decorate 'ERROR: refute_output' \
| fail
return $?
fi
# Arguments.
local -r unexpected="$1"
if (( is_mode_regex == 1 )) && [[ '' =~ $unexpected ]] || (( $? == 2 )); then
echo "Invalid extended regular expression: \`$unexpected'" \
| batslib_decorate 'ERROR: refute_output' \
| fail
return $?
fi
# Matching.
if (( is_match_contained )); then
# Line contained in output.
if (( is_mode_regex )); then
local -i idx
for (( idx = 0; idx < ${#lines[@]}; ++idx )); do
if [[ ${lines[$idx]} =~ $unexpected ]]; then
{ local -ar single=(
'regex' "$unexpected"
'index' "$idx"
)
local -a may_be_multi=(
'output' "$output"
)
local -ir width="$( batslib_get_max_single_line_key_width \
"${single[@]}" "${may_be_multi[@]}" )"
batslib_print_kv_single "$width" "${single[@]}"
if batslib_is_single_line "${may_be_multi[1]}"; then
batslib_print_kv_single "$width" "${may_be_multi[@]}"
else
may_be_multi[1]="$( printf '%s' "${may_be_multi[1]}" \
| batslib_prefix \
| batslib_mark '>' "$idx" )"
batslib_print_kv_multi "${may_be_multi[@]}"
fi
} | batslib_decorate 'no line should match the regular expression' \
| fail
return $?
fi
done
elif (( is_mode_partial )); then
local -i idx
for (( idx = 0; idx < ${#lines[@]}; ++idx )); do
if [[ ${lines[$idx]} == *"$unexpected"* ]]; then
{ local -ar single=(
'substring' "$unexpected"
'index' "$idx"
)
local -a may_be_multi=(
'output' "$output"
)
local -ir width="$( batslib_get_max_single_line_key_width \
"${single[@]}" "${may_be_multi[@]}" )"
batslib_print_kv_single "$width" "${single[@]}"
if batslib_is_single_line "${may_be_multi[1]}"; then
batslib_print_kv_single "$width" "${may_be_multi[@]}"
else
may_be_multi[1]="$( printf '%s' "${may_be_multi[1]}" \
| batslib_prefix \
| batslib_mark '>' "$idx" )"
batslib_print_kv_multi "${may_be_multi[@]}"
fi
} | batslib_decorate 'no line should contain substring' \
| fail
return $?
fi
done
else
local -i idx
for (( idx = 0; idx < ${#lines[@]}; ++idx )); do
if [[ ${lines[$idx]} == "$unexpected" ]]; then
{ local -ar single=(
'line' "$unexpected"
'index' "$idx"
)
local -a may_be_multi=(
'output' "$output"
)
local -ir width="$( batslib_get_max_single_line_key_width \
"${single[@]}" "${may_be_multi[@]}" )"
batslib_print_kv_single "$width" "${single[@]}"
if batslib_is_single_line "${may_be_multi[1]}"; then
batslib_print_kv_single "$width" "${may_be_multi[@]}"
else
may_be_multi[1]="$( printf '%s' "${may_be_multi[1]}" \
| batslib_prefix \
| batslib_mark '>' "$idx" )"
batslib_print_kv_multi "${may_be_multi[@]}"
fi
} | batslib_decorate 'line should not be in output' \
| fail
return $?
fi
done
fi
elif (( is_match_line )); then
# Specific line.
if (( is_mode_regex )); then
if [[ ${lines[$idx]} =~ $unexpected ]] || (( $? == 0 )); then
batslib_print_kv_single 5 \
'index' "$idx" \
'regex' "$unexpected" \
'line' "${lines[$idx]}" \
| batslib_decorate 'regular expression should not match line' \
| fail
fi
elif (( is_mode_partial )); then
if [[ ${lines[$idx]} == *"$unexpected"* ]]; then
batslib_print_kv_single 9 \
'index' "$idx" \
'substring' "$unexpected" \
'line' "${lines[$idx]}" \
| batslib_decorate 'line should not contain substring' \
| fail
fi
else
if [[ ${lines[$idx]} == "$unexpected" ]]; then
batslib_print_kv_single 5 \
'index' "$idx" \
'line' "${lines[$idx]}" \
| batslib_decorate 'line should differ' \
| fail
fi
fi
else
# Entire output.
if (( is_mode_regex )); then
if [[ $output =~ $unexpected ]] || (( $? == 0 )); then
batslib_print_kv_single_or_multi 6 \
'regex' "$unexpected" \
'output' "$output" \
| batslib_decorate 'regular expression should not match output' \
| fail
fi
elif (( is_mode_partial )); then
if [[ $output == *"$unexpected"* ]]; then
batslib_print_kv_single_or_multi 9 \
'substring' "$unexpected" \
'output' "$output" \
| batslib_decorate 'output should not contain substring' \
| fail
fi
else
if [[ $output == "$unexpected" ]]; then
batslib_print_kv_single_or_multi 6 \
'output' "$output" \
| batslib_decorate 'output equals, but it was expected to differ' \
| fail
fi
fi
fi
}

View File

@ -1,264 +0,0 @@
#
# output.bash
# -----------
#
# Private functions implementing output formatting. Used by public
# helper functions.
#
# Print a message to the standard error. When no parameters are
# specified, the message is read from the standard input.
#
# Globals:
# none
# Arguments:
# $@ - [=STDIN] message
# Returns:
# none
# Inputs:
# STDIN - [=$@] message
# Outputs:
# STDERR - message
batslib_err() {
{ if (( $# > 0 )); then
echo "$@"
else
cat -
fi
} >&2
}
# Count the number of lines in the given string.
#
# TODO(ztombol): Fix tests and remove this note after #93 is resolved!
# NOTE: Due to a bug in Bats, `batslib_count_lines "$output"' does not
# give the same result as `${#lines[@]}' when the output contains
# empty lines.
# See PR #93 (https://github.com/sstephenson/bats/pull/93).
#
# Globals:
# none
# Arguments:
# $1 - string
# Returns:
# none
# Outputs:
# STDOUT - number of lines
batslib_count_lines() {
local -i n_lines=0
local line
while IFS='' read -r line || [[ -n $line ]]; do
(( ++n_lines ))
done < <(printf '%s' "$1")
echo "$n_lines"
}
# Determine whether all strings are single-line.
#
# Globals:
# none
# Arguments:
# $@ - strings
# Returns:
# 0 - all strings are single-line
# 1 - otherwise
batslib_is_single_line() {
for string in "$@"; do
(( $(batslib_count_lines "$string") > 1 )) && return 1
done
return 0
}
# Determine the length of the longest key that has a single-line value.
#
# This function is useful in determining the correct width of the key
# column in two-column format when some keys may have multi-line values
# and thus should be excluded.
#
# Globals:
# none
# Arguments:
# $odd - key
# $even - value of the previous key
# Returns:
# none
# Outputs:
# STDOUT - length of longest key
batslib_get_max_single_line_key_width() {
local -i max_len=-1
while (( $# != 0 )); do
local -i key_len="${#1}"
batslib_is_single_line "$2" && (( key_len > max_len )) && max_len="$key_len"
shift 2
done
echo "$max_len"
}
# Print key-value pairs in two-column format.
#
# Keys are displayed in the first column, and their corresponding values
# in the second. To evenly line up values, the key column is fixed-width
# and its width is specified with the first parameter (possibly computed
# using `batslib_get_max_single_line_key_width').
#
# Globals:
# none
# Arguments:
# $1 - width of key column
# $even - key
# $odd - value of the previous key
# Returns:
# none
# Outputs:
# STDOUT - formatted key-value pairs
batslib_print_kv_single() {
local -ir col_width="$1"; shift
while (( $# != 0 )); do
printf '%-*s : %s\n' "$col_width" "$1" "$2"
shift 2
done
}
# Print key-value pairs in multi-line format.
#
# The key is displayed first with the number of lines of its
# corresponding value in parenthesis. Next, starting on the next line,
# the value is displayed. For better readability, it is recommended to
# indent values using `batslib_prefix'.
#
# Globals:
# none
# Arguments:
# $odd - key
# $even - value of the previous key
# Returns:
# none
# Outputs:
# STDOUT - formatted key-value pairs
batslib_print_kv_multi() {
while (( $# != 0 )); do
printf '%s (%d lines):\n' "$1" "$( batslib_count_lines "$2" )"
printf '%s\n' "$2"
shift 2
done
}
# Print all key-value pairs in either two-column or multi-line format
# depending on whether all values are single-line.
#
# If all values are single-line, print all pairs in two-column format
# with the specified key column width (identical to using
# `batslib_print_kv_single').
#
# Otherwise, print all pairs in multi-line format after indenting values
# with two spaces for readability (identical to using `batslib_prefix'
# and `batslib_print_kv_multi')
#
# Globals:
# none
# Arguments:
# $1 - width of key column (for two-column format)
# $even - key
# $odd - value of the previous key
# Returns:
# none
# Outputs:
# STDOUT - formatted key-value pairs
batslib_print_kv_single_or_multi() {
local -ir width="$1"; shift
local -a pairs=( "$@" )
local -a values=()
local -i i
for (( i=1; i < ${#pairs[@]}; i+=2 )); do
values+=( "${pairs[$i]}" )
done
if batslib_is_single_line "${values[@]}"; then
batslib_print_kv_single "$width" "${pairs[@]}"
else
local -i i
for (( i=1; i < ${#pairs[@]}; i+=2 )); do
pairs[$i]="$( batslib_prefix < <(printf '%s' "${pairs[$i]}") )"
done
batslib_print_kv_multi "${pairs[@]}"
fi
}
# Prefix each line read from the standard input with the given string.
#
# Globals:
# none
# Arguments:
# $1 - [= ] prefix string
# Returns:
# none
# Inputs:
# STDIN - lines
# Outputs:
# STDOUT - prefixed lines
batslib_prefix() {
local -r prefix="${1:- }"
local line
while IFS='' read -r line || [[ -n $line ]]; do
printf '%s%s\n' "$prefix" "$line"
done
}
# Mark select lines of the text read from the standard input by
# overwriting their beginning with the given string.
#
# Usually the input is indented by a few spaces using `batslib_prefix'
# first.
#
# Globals:
# none
# Arguments:
# $1 - marking string
# $@ - indices (zero-based) of lines to mark
# Returns:
# none
# Inputs:
# STDIN - lines
# Outputs:
# STDOUT - lines after marking
batslib_mark() {
local -r symbol="$1"; shift
# Sort line numbers.
set -- $( sort -nu <<< "$( printf '%d\n' "$@" )" )
local line
local -i idx=0
while IFS='' read -r line || [[ -n $line ]]; do
if (( ${1:--1} == idx )); then
printf '%s\n' "${symbol}${line:${#symbol}}"
shift
else
printf '%s\n' "$line"
fi
(( ++idx ))
done
}
# Enclose the input text in header and footer lines.
#
# The header contains the given string as title. The output is preceded
# and followed by an additional newline to make it stand out more.
#
# Globals:
# none
# Arguments:
# $1 - title
# Returns:
# none
# Inputs:
# STDIN - text
# Outputs:
# STDOUT - decorated text
batslib_decorate() {
echo
echo "-- $1 --"
cat -
echo '--'
echo
}

View File

@ -1,66 +0,0 @@
## functions to help deal with docker
# Removes container $1
function docker_clean {
docker kill $1 &>/dev/null ||:
sleep .25s
docker rm -vf $1 &>/dev/null ||:
sleep .25s
}
# get the ip of docker container $1
function docker_ip {
docker inspect --format '{{ .NetworkSettings.IPAddress }}' $1
}
# get the ip of docker container $1
function docker_id {
docker inspect --format '{{ .ID }}' $1
}
# get the running state of container $1
# → true/false
# fails if the container does not exist
function docker_running_state {
docker inspect -f {{.State.Running}} $1
}
# get the docker container $1 PID
function docker_pid {
docker inspect --format {{.State.Pid}} $1
}
# asserts logs from container $1 contains $2
function docker_assert_log {
local -r container=$1
shift
run docker logs $container
assert_output -p "$*"
}
# wait for a container to produce a given text in its log
# $1 container
# $2 timeout in second
# $* text to wait for
function docker_wait_for_log {
local -r container=$1
local -ir timeout_sec=$2
shift 2
retry $(( $timeout_sec * 2 )) .5s docker_assert_log $container "$*"
}
# Create a docker container named $1 which exposes the docker host unix
# socket over tcp on port 2375.
#
# $1 container name
function docker_tcp {
local container_name="$1"
docker_clean $container_name
docker run -d \
--label bats-type="socat" \
--name $container_name \
--expose 2375 \
-v /var/run/docker.sock:/var/run/docker.sock \
rancher/socat-docker
docker run --label bats-type="docker" --link "$container_name:docker" docker:1.10 version
}

View File

@ -1,22 +0,0 @@
## add the retry function to bats
# Retry a command $1 times until it succeeds. Wait $2 seconds between retries.
function retry {
local attempts=$1
shift
local delay=$1
shift
local i
for ((i=0; i < attempts; i++)); do
run "$@"
if [ "$status" -eq 0 ]; then
echo "$output"
return 0
fi
sleep $delay
done
echo "Command \"$@\" failed $attempts times. Status: $status. Output: $output" >&2
false
}

View File

@ -1,24 +0,0 @@
-----BEGIN CERTIFICATE-----
MIID7TCCAtWgAwIBAgIJAOGkf5EnexJVMA0GCSqGSIb3DQEBCwUAMIGMMQswCQYD
VQQGEwJVUzERMA8GA1UECAwIVmlyZ2luaWExDzANBgNVBAcMBlJlc3RvbjERMA8G
A1UECgwIRmFrZSBPcmcxGzAZBgNVBAMMEioubmdpbngtcHJveHkuYmF0czEpMCcG
CSqGSIb3DQEJARYad2VibWFzdGVyQG5naW54LXByb3h5LmJhdHMwHhcNMTYwNDIw
MTUzOTUxWhcNMjYwNDE4MTUzOTUxWjCBjDELMAkGA1UEBhMCVVMxETAPBgNVBAgM
CFZpcmdpbmlhMQ8wDQYDVQQHDAZSZXN0b24xETAPBgNVBAoMCEZha2UgT3JnMRsw
GQYDVQQDDBIqLm5naW54LXByb3h5LmJhdHMxKTAnBgkqhkiG9w0BCQEWGndlYm1h
c3RlckBuZ2lueC1wcm94eS5iYXRzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
CgKCAQEA0Amkj3iaQn8Z2CW6n24zSuWu2OoLCkHZAk8eprkI4kKoPBvjusynkm8E
phq65jebToHoldfuQ0wM61DzhD15bHwS3x9CrOVbShsmdnGALz+wdR0/4Likx50I
YZdecTOAlkoZudnX5FZ4ngOxjqcym7p5T8TrSS97a0fx99gitZY0p+Nu2tip4o3t
WBMs+SoPWTlQ1SrSmL8chC8O2knyBl/w1nHmDnMuR6FGcHdhLncApw9t5spgfv7p
OrMF4tQxJQNk10TnflmEMkGmy+pfk2e0cQ1Kwp3Nmzm7ECkggxxyjU3ihKiFK+09
8aSCi7gDAY925+mV6LZ5oLMpO3KJvQIDAQABo1AwTjAdBgNVHQ4EFgQU+NvFo37z
9Dyq8Mu82SPtV7q1gYQwHwYDVR0jBBgwFoAU+NvFo37z9Dyq8Mu82SPtV7q1gYQw
DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAI1ityDV0UsCVHSpB2LN+
QXlk8XS0ACIJ8Q0hbOj3BmYrdAVglG4P6upDEueaaxwsaBTagkTP8nxZ9dhfZHyZ
5YLNwYsiG5iqb8e0ecHx3uJT/0YiXn/8rBvxEZna4Fl8seGdp7BjOWUAS2Nv8tn4
EJJvRdfX/O8XgPc95DM4lwQ/dvyWmavMI4lnl0n1IQV9WPGaIQhYPU9WEQK6iMUB
o1kx8YbOJQD0ZBRfqpriNt1/8ylkkSYYav8QT9JFvQFCWEvaX71QF+cuOwC7ZYBH
4ElXwEUrYBHKiPo0q0VsTtMvLh7h/T5czrIhG/NpfVJPtQOk8aVwNScL3/n+TGU8
6g==
-----END CERTIFICATE-----

View File

@ -1,28 +0,0 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDQCaSPeJpCfxnY
JbqfbjNK5a7Y6gsKQdkCTx6muQjiQqg8G+O6zKeSbwSmGrrmN5tOgeiV1+5DTAzr
UPOEPXlsfBLfH0Ks5VtKGyZ2cYAvP7B1HT/guKTHnQhhl15xM4CWShm52dfkVnie
A7GOpzKbunlPxOtJL3trR/H32CK1ljSn427a2Knije1YEyz5Kg9ZOVDVKtKYvxyE
Lw7aSfIGX/DWceYOcy5HoUZwd2EudwCnD23mymB+/uk6swXi1DElA2TXROd+WYQy
QabL6l+TZ7RxDUrCnc2bObsQKSCDHHKNTeKEqIUr7T3xpIKLuAMBj3bn6ZXotnmg
syk7com9AgMBAAECggEAa7wCp3XqVPNjW+c1/ShhkbDeWmDhtL8i9aopkmeSbTHd
07sRtQQU56Vsf+Sp010KpZ5q52Z6cglpS1eRtHLtdbvPPhL/QXBJVVg4E/B1VIKk
DBJIqUSVuPXeiEOOWgs01R+ssO1ae1o4foQlKF33vGPWPPQacL0RKh6I9TPNzcD7
n4rujlHk72N/bNydyK2rnyKB4vAI5TbZPLps+Xe123CmgZnW3JClcWV9B4foRmiu
a5Iq1WYAK2GYKbYwgqDRyYBC27m91a7U31pE4GQD+xQdlz6kcOlCU5hAcPK3h7j0
fLQqn8g+YAtc0nBKKB4NZe3QEzTiVMorT0VitxI71QKBgQDnirardZaXOFzYGzB3
j+FGB9BUW54hnHr5BxOYrfmEJ5umJjJWaGupfYrQsPArrJP1//WbqVZIPvdQParD
mQhLmSp1r/VNzGB6pISmzU1ZGDHsmBxYseh366om5YBQUFU2vmbil9VkrkM4fsJG
tcS9V/nVY/EM7Yp3PzjfLlhC1wKBgQDmA1YJmnZvIbLp3PoKqM69QiCLKztVm7nX
xpu3b3qbXEzXkt2sP5PHmr+s13hOPQFKRJ2hk4UN9WqpnFoHw5E5eWWhSa/peUZm
r10Y5XspiFtRHHiu6ABXB49eB4fen+vHEZHKyRJ4rFthKjjBHdNPC8bmwnT3jE85
/8a26FLZiwKBgQDXEi8JZslBn9YF2oOTm28KCLoHka551AsaA+u892T8z3mxxGsf
fhD7N6TYonIEb2Jkr6OpOortwqcgvpc+5oghCJ27AX2fDUdUxDp/YdYF+wZsmQJD
lMW1lo7PYIBmmaf9mLCiq5xIz+GauYul+LNNmUl0YEgI1SC4EV63WCodswKBgDMX
GJxHd/kVViVGFTAa8NjvAEWJU8OfNHduQRZMp8IsjVDw6VYiRRP4Fo0wyyMtv8Sc
WxsRpmNEWO3VsdW5pd9LTLy3nmBQtMeIOjiWeHXwOMBaf5/yHmk2X6z2JULY6Mkt
6OFPKlAtkJqTg0m58z7Ckeqd1NdLjimG27+y+PwjAoGAFt0cbC1Ust2BE6YEspSX
ofpAnJsyKrbF9iVUyXDUP99sdqYQfPJ5uqPGkP59lJGkTLtebuitqi6FCyrsT6Fq
AWLiExbqebAqcuAZw2S+iuK27S4rrkjVGF53J7vH3rOzCBUXaRx6GKfTjUqedHdg
9Kw+LP6IFnMTb+EGLo+GqHs=
-----END PRIVATE KEY-----

View File

@ -1,43 +0,0 @@
#!/usr/bin/env bats
load test_helpers
SUT_CONTAINER=bats-nginx-proxy-${TEST_FILE}
function setup {
# make sure to stop any web container before each test so we don't
# have any unexpected contaiener running with VIRTUAL_HOST or VIRUTAL_PORT set
stop_bats_containers web
}
@test "[$TEST_FILE] start a nginx-proxy container" {
run nginxproxy $SUT_CONTAINER -v /var/run/docker.sock:/tmp/docker.sock:ro
assert_success
docker_wait_for_log $SUT_CONTAINER 9 "Watching docker events"
}
@test "[$TEST_FILE] nginx-proxy forwards requests for 2 hosts" {
# WHEN a container runs a web server with VIRTUAL_HOST set for multiple hosts
prepare_web_container bats-multiple-hosts-1 80 -e VIRTUAL_HOST=multiple-hosts-1-A.bats,multiple-hosts-1-B.bats
dockergen_wait_for_event $SUT_CONTAINER start bats-multiple-hosts-1
sleep 1
# THEN querying the proxy without Host header → 503
run curl_container $SUT_CONTAINER / --head
assert_output -l 0 $'HTTP/1.1 503 Service Temporarily Unavailable\r'
# THEN querying the proxy with unknown Host header → 503
run curl_container $SUT_CONTAINER /port --header "Host: webFOO.bats" --head
assert_output -l 0 $'HTTP/1.1 503 Service Temporarily Unavailable\r'
# THEN
run curl_container $SUT_CONTAINER /port --header 'Host: multiple-hosts-1-A.bats'
assert_output "answer from port 80"
# THEN
run curl_container $SUT_CONTAINER /port --header 'Host: multiple-hosts-1-B.bats'
assert_output "answer from port 80"
}
@test "[$TEST_FILE] stop all bats containers" {
stop_bats_containers
}

View File

@ -1,64 +0,0 @@
#!/usr/bin/env bats
load test_helpers
SUT_CONTAINER=bats-nginx-proxy-${TEST_FILE}
function setup {
# make sure to stop any web container before each test so we don't
# have any unexpected contaiener running with VIRTUAL_HOST or VIRUTAL_PORT set
stop_bats_containers web
}
@test "[$TEST_FILE] start a nginx-proxy container" {
# GIVEN nginx-proxy
run nginxproxy $SUT_CONTAINER -v /var/run/docker.sock:/tmp/docker.sock:ro
assert_success
docker_wait_for_log $SUT_CONTAINER 9 "Watching docker events"
}
@test "[$TEST_FILE] nginx-proxy defaults to the service running on port 80" {
# WHEN
prepare_web_container bats-web-${TEST_FILE}-1 "80 90" -e VIRTUAL_HOST=web.bats
dockergen_wait_for_event $SUT_CONTAINER start bats-web-${TEST_FILE}-1
sleep 1
# THEN
assert_response_is_from_port 80
}
@test "[$TEST_FILE] VIRTUAL_PORT=90 while port 80 is also exposed" {
# GIVEN
prepare_web_container bats-web-${TEST_FILE}-2 "80 90" -e VIRTUAL_HOST=web.bats -e VIRTUAL_PORT=90
dockergen_wait_for_event $SUT_CONTAINER start bats-web-${TEST_FILE}-2
sleep 1
# THEN
assert_response_is_from_port 90
}
@test "[$TEST_FILE] single exposed port != 80" {
# GIVEN
prepare_web_container bats-web-${TEST_FILE}-3 1234 -e VIRTUAL_HOST=web.bats
dockergen_wait_for_event $SUT_CONTAINER start bats-web-${TEST_FILE}-3
sleep 1
# THEN
assert_response_is_from_port 1234
}
@test "[$TEST_FILE] stop all bats containers" {
stop_bats_containers
}
# assert querying nginx-proxy provides a response from the expected port of the web container
# $1 port we are expecting an response from
function assert_response_is_from_port {
local -r port=$1
run curl_container $SUT_CONTAINER /port --header "Host: web.bats"
assert_output "answer from port $port"
}

3
test/pytest.ini Normal file
View File

@ -0,0 +1,3 @@
[pytest]
# disable the creation of the `.cache` folders
addopts = -p no:cacheprovider --ignore=requirements --ignore=certs -r s -v

24
test/pytest.sh Executable file
View File

@ -0,0 +1,24 @@
#!/bin/bash
###############################################################################
# #
# This script is meant to run the test suite from a Docker container. #
# #
# This is usefull when you want to run the test suite from Mac or #
# Docker Toolbox. #
# #
###############################################################################
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
ARGS="$@"
# check requirements
echo "> Building nginx-proxy-tester image..."
docker build -t nginx-proxy-tester -f $DIR/requirements/Dockerfile-nginx-proxy-tester $DIR/requirements
# run the nginx-proxy-tester container setting the correct value for the working dir in order for
# docker-compose to work properly when run from within that container.
exec docker run --rm -it \
-v ${DIR}:/${DIR} \
-w ${DIR} \
-v /var/run/docker.sock:/var/run/docker.sock \
nginx-proxy-tester ${ARGS}

View File

@ -0,0 +1,5 @@
FROM python:2.7
COPY python-requirements.txt /requirements.txt
RUN pip install -r /requirements.txt
WORKDIR /test
ENTRYPOINT ["pytest"]

View File

@ -0,0 +1,52 @@
This directory contains resources to build Docker images tests depend on
# Build images
./build.sh
# python-requirements.txt
If you want to run the test suite from your computer, you need python and a few python modules.
The _python-requirements.txt_ file describes the python modules required. To install them, use
pip:
pip install -r python-requirements.txt
If you don't want to run the test from your computer, you can run the tests from a docker container, see the _pytest.sh_ script.
# Images
## web
This container will run one or many webservers, each of them listening on a single port.
Ports are specified using the `WEB_PORTS` environment variable:
docker run -d -e WEB_PORTS=80 web # will create a container running one webserver listening on port 80
docker run -d -e WEB_PORTS="80 81" web # will create a container running two webservers, one listening on port 80 and a second one listening on port 81
The webserver answers on two paths:
- `/headers`
- `/port`
```
$ docker run -d -e WEB_PORTS=80 -p 80:80 web
$ curl http://127.0.0.1:80/headers
Host: 127.0.0.1
User-Agent: curl/7.47.0
Accept: */*
$ curl http://127.0.0.1:80/port
answer from port 80
```
## nginx-proxy-tester
This is an optional requirement which is usefull if you cannot (or don't want to) install pytest and its requirements on your computer. In this case, you can use the `nginx-proxy-tester` docker image to run the test suite from a Docker container.
To use this image, it is mandatory to run the container using the `pytest.sh` shell script. The script will build the image and run a container from it with the appropriate volumes and settings.

6
test/requirements/build.sh Executable file
View File

@ -0,0 +1,6 @@
#!/bin/bash
set -e
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
docker build -t web $DIR/web

View File

@ -0,0 +1,5 @@
backoff==1.3.2
docker-compose==1.11.1
docker==2.0.2
pytest==3.0.5
requests==2.11.1

View File

@ -0,0 +1,8 @@
# Docker Image running one (or multiple) webservers listening on all given ports from WEB_PORTS environment variable
FROM python:3
COPY ./webserver.py /
COPY ./entrypoint.sh /
WORKDIR /opt
ENTRYPOINT ["/bin/bash", "/entrypoint.sh"]

View File

@ -0,0 +1,15 @@
#!/bin/bash
set -u
trap '[ ${#PIDS[@]} -gt 0 ] && kill -TERM ${PIDS[@]}' TERM
declare -a PIDS
for port in $WEB_PORTS; do
echo starting a web server listening on port $port;
/webserver.py $port &
PIDS+=($!)
done
wait ${PIDS[@]}
trap - TERM
wait ${PIDS[@]}

View File

@ -4,9 +4,9 @@ import os, sys
import http.server
import socketserver
class BatsHandler(http.server.SimpleHTTPRequestHandler):
class Handler(http.server.SimpleHTTPRequestHandler):
def do_GET(self):
root = os.getcwd()
self.send_response(200)
self.send_header("Content-Type", "text/plain")
@ -17,11 +17,15 @@ class BatsHandler(http.server.SimpleHTTPRequestHandler):
elif self.path == "/port":
response = "answer from port %s\n" % PORT
self.wfile.write(response.encode())
elif self.path == "/":
response = "I'm %s\n" % os.environ['HOSTNAME']
self.wfile.write(response.encode())
else:
self.wfile.write("No route for this path!\n".encode())
if __name__ == '__main__':
PORT = int(sys.argv[1])
socketserver.TCPServer.allow_reuse_address = True
httpd = socketserver.TCPServer(('0.0.0.0', PORT), BatsHandler)
httpd = socketserver.TCPServer(('0.0.0.0', PORT), Handler)
httpd.serve_forever()

View File

@ -1,168 +0,0 @@
#!/usr/bin/env bats
load test_helpers
SUT_CONTAINER=bats-nginx-proxy-${TEST_FILE}
function setup {
# make sure to stop any web container before each test so we don't
# have any unexpected contaiener running with VIRTUAL_HOST or VIRUTAL_PORT set
stop_bats_containers web
}
@test "[$TEST_FILE] start a nginx-proxy container" {
run nginxproxy $SUT_CONTAINER -v /var/run/docker.sock:/tmp/docker.sock:ro -v ${DIR}/lib/ssl:/etc/nginx/certs:ro
assert_success
docker_wait_for_log $SUT_CONTAINER 9 "Watching docker events"
}
@test "[$TEST_FILE] test SSL for VIRTUAL_HOST=*.nginx-proxy.bats" {
# WHEN
prepare_web_container bats-ssl-hosts-1 "80" \
-e VIRTUAL_HOST=*.nginx-proxy.bats \
-e CERT_NAME=nginx-proxy.bats
dockergen_wait_for_event $SUT_CONTAINER start bats-ssl-hosts-1
sleep 1
# THEN
assert_301 test.nginx-proxy.bats
assert_200_https test.nginx-proxy.bats
}
@test "[$TEST_FILE] test HTTPS_METHOD=nohttp" {
# WHEN
prepare_web_container bats-ssl-hosts-2 "80" \
-e VIRTUAL_HOST=*.nginx-proxy.bats \
-e CERT_NAME=nginx-proxy.bats \
-e HTTPS_METHOD=nohttp
dockergen_wait_for_event $SUT_CONTAINER start bats-ssl-hosts-2
sleep 1
# THEN
assert_503 test.nginx-proxy.bats
assert_200_https test.nginx-proxy.bats
}
@test "[$TEST_FILE] test HTTPS_METHOD=noredirect" {
# WHEN
prepare_web_container bats-ssl-hosts-3 "80" \
-e VIRTUAL_HOST=*.nginx-proxy.bats \
-e CERT_NAME=nginx-proxy.bats \
-e HTTPS_METHOD=noredirect
dockergen_wait_for_event $SUT_CONTAINER start bats-ssl-hosts-3
sleep 1
# THEN
assert_200 test.nginx-proxy.bats
assert_200_https test.nginx-proxy.bats
}
@test "[$TEST_FILE] test SSL Strict-Transport-Security" {
# WHEN
prepare_web_container bats-ssl-hosts-4 "80" \
-e VIRTUAL_HOST=*.nginx-proxy.bats \
-e CERT_NAME=nginx-proxy.bats
dockergen_wait_for_event $SUT_CONTAINER start bats-ssl-hosts-4
sleep 1
# THEN
assert_301 test.nginx-proxy.bats
assert_200_https test.nginx-proxy.bats
assert_output -p "Strict-Transport-Security: max-age=31536000"
}
@test "[$TEST_FILE] test HTTPS_METHOD=noredirect disables Strict-Transport-Security" {
# WHEN
prepare_web_container bats-ssl-hosts-5 "80" \
-e VIRTUAL_HOST=*.nginx-proxy.bats \
-e CERT_NAME=nginx-proxy.bats \
-e HTTPS_METHOD=noredirect
dockergen_wait_for_event $SUT_CONTAINER start bats-ssl-hosts-5
sleep 1
# THEN
assert_200 test.nginx-proxy.bats
assert_200_https test.nginx-proxy.bats
refute_output -p "Strict-Transport-Security: max-age=31536000"
}
@test "[$TEST_FILE] test HTTPS_METHOD=nohttps" {
# WHEN
prepare_web_container bats-ssl-hosts-6 "80" \
-e VIRTUAL_HOST=*.nginx-proxy.bats \
-e CERT_NAME=nginx-proxy.bats \
-e HTTPS_METHOD=nohttps
dockergen_wait_for_event $SUT_CONTAINER start bats-ssl-hosts-6
sleep 1
# THEN
assert_down_https test.nginx-proxy.bats
assert_200 test.nginx-proxy.bats
}
@test "[$TEST_FILE] stop all bats containers" {
stop_bats_containers
}
# assert that querying nginx-proxy with the given Host header produces a `HTTP 200` response
# $1 Host HTTP header to use when querying nginx-proxy
function assert_200 {
local -r host=$1
run curl_container $SUT_CONTAINER / --head --header "Host: $host"
assert_output -l 0 $'HTTP/1.1 200 OK\r'
}
# assert that querying nginx-proxy with the given Host header produces a `HTTP 503` response
# $1 Host HTTP header to use when querying nginx-proxy
function assert_503 {
local -r host=$1
run curl_container $SUT_CONTAINER / --head --header "Host: $host"
assert_output -l 0 $'HTTP/1.1 503 Service Temporarily Unavailable\r'
}
# assert that querying nginx-proxy with the given Host header produces a `HTTP 503` response
# $1 Host HTTP header to use when querying nginx-proxy
function assert_301 {
local -r host=$1
run curl_container $SUT_CONTAINER / --head --header "Host: $host"
assert_output -l 0 $'HTTP/1.1 301 Moved Permanently\r'
}
# assert that querying nginx-proxy with the given Host header fails because the host is down
# $1 Host HTTP header to use when querying nginx-proxy
function assert_down_https {
local -r host=$1
run curl_container_https $SUT_CONTAINER / --head --header "Host: $host"
assert_failure
}
# assert that querying nginx-proxy with the given Host header produces a `HTTP 200` response
# $1 Host HTTP header to use when querying nginx-proxy
function assert_200_https {
local -r host=$1
run curl_container_https $SUT_CONTAINER / --head --header "Host: $host"
assert_output -l 0 $'HTTP/1.1 200 OK\r'
}
# assert that querying nginx-proxy with the given Host header produces a `HTTP 503` response
# $1 Host HTTP header to use when querying nginx-proxy
function assert_503_https {
local -r host=$1
run curl_container_https $SUT_CONTAINER / --head --header "Host: $host"
assert_output -l 0 $'HTTP/1.1 503 Service Temporarily Unavailable\r'
}
# assert that querying nginx-proxy with the given Host header produces a `HTTP 503` response
# $1 Host HTTP header to use when querying nginx-proxy
function assert_301_https {
local -r host=$1
run curl_container_https $SUT_CONTAINER / --head --header "Host: $host"
assert_output -l 0 $'HTTP/1.1 301 Moved Permanently\r'
}

View File

@ -0,0 +1,15 @@
import pytest
def test_unknown_virtual_host(docker_compose, nginxproxy):
r = nginxproxy.get("http://nginx-proxy/port")
assert r.status_code == 503
def test_forwards_to_web1(docker_compose, nginxproxy):
r = nginxproxy.get("http://web1.nginx-proxy.tld/port")
assert r.status_code == 200
assert r.text == "answer from port 81\n"
def test_forwards_to_web2(docker_compose, nginxproxy):
r = nginxproxy.get("http://web2.nginx-proxy.tld/port")
assert r.status_code == 200
assert r.text == "answer from port 82\n"

View File

@ -0,0 +1,24 @@
web1:
image: web
expose:
- "81"
environment:
WEB_PORTS: 81
VIRTUAL_HOST: web1.nginx-proxy.tld
web2:
image: web
expose:
- "82"
environment:
WEB_PORTS: 82
VIRTUAL_HOST: web2.nginx-proxy.tld
sut:
image: jwilder/nginx-proxy:test
volumes:
- /var/run/docker.sock:/f00.sock:ro
environment:
DOCKER_HOST: unix:///f00.sock

10
test/test_composev2.py Normal file
View File

@ -0,0 +1,10 @@
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.local/port")
assert r.status_code == 200
assert r.text == "answer from port 81\n"

14
test/test_composev2.yml Normal file
View File

@ -0,0 +1,14 @@
version: '2'
services:
nginx-proxy:
image: jwilder/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.local

View File

@ -0,0 +1 @@
add_header X-test f00;

View File

@ -0,0 +1 @@
add_header X-test bar;

View File

@ -0,0 +1,28 @@
import pytest
def test_custom_default_conf_does_not_apply_to_unknown_vhost(docker_compose, nginxproxy):
r = nginxproxy.get("http://nginx-proxy/")
assert r.status_code == 503
assert "X-test" not in r.headers
def test_custom_default_conf_applies_to_web1(docker_compose, nginxproxy):
r = nginxproxy.get("http://web1.nginx-proxy.local/port")
assert r.status_code == 200
assert r.text == "answer from port 81\n"
assert "X-test" in r.headers
assert "f00" == r.headers["X-test"]
def test_custom_default_conf_applies_to_web2(docker_compose, nginxproxy):
r = nginxproxy.get("http://web2.nginx-proxy.local/port")
assert r.status_code == 200
assert r.text == "answer from port 82\n"
assert "X-test" in r.headers
assert "f00" == r.headers["X-test"]
def test_custom_default_conf_is_overriden_for_web3(docker_compose, nginxproxy):
r = nginxproxy.get("http://web3.nginx-proxy.local/port")
assert r.status_code == 200
assert r.text == "answer from port 83\n"
assert "X-test" in r.headers
assert "bar" == r.headers["X-test"]

View File

@ -0,0 +1,30 @@
nginx-proxy:
image: jwilder/nginx-proxy:test
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
- ./my_custom_proxy_settings.conf:/etc/nginx/vhost.d/default_location:ro
- ./my_custom_proxy_settings_bar.conf:/etc/nginx/vhost.d/web3.nginx-proxy.local_location:ro
web1:
image: web
expose:
- "81"
environment:
WEB_PORTS: 81
VIRTUAL_HOST: web1.nginx-proxy.local
web2:
image: web
expose:
- "82"
environment:
WEB_PORTS: 82
VIRTUAL_HOST: web2.nginx-proxy.local
web3:
image: web
expose:
- "83"
environment:
WEB_PORTS: 83
VIRTUAL_HOST: web3.nginx-proxy.local

View File

@ -0,0 +1,20 @@
import pytest
def test_custom_conf_does_not_apply_to_unknown_vhost(docker_compose, nginxproxy):
r = nginxproxy.get("http://nginx-proxy/")
assert r.status_code == 503
assert "X-test" not in r.headers
def test_custom_conf_applies_to_web1(docker_compose, nginxproxy):
r = nginxproxy.get("http://web1.nginx-proxy.local/port")
assert r.status_code == 200
assert r.text == "answer from port 81\n"
assert "X-test" in r.headers
assert "f00" == r.headers["X-test"]
def test_custom_conf_applies_to_web2(docker_compose, nginxproxy):
r = nginxproxy.get("http://web2.nginx-proxy.local/port")
assert r.status_code == 200
assert r.text == "answer from port 82\n"
assert "X-test" in r.headers
assert "f00" == r.headers["X-test"]

View File

@ -0,0 +1,23 @@
version: '2'
services:
nginx-proxy:
image: jwilder/nginx-proxy:test
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
- ./my_custom_proxy_settings.conf:/etc/nginx/proxy.conf:ro
web1:
image: web
expose:
- "81"
environment:
WEB_PORTS: 81
VIRTUAL_HOST: web1.nginx-proxy.local
web2:
image: web
expose:
- "82"
environment:
WEB_PORTS: 82
VIRTUAL_HOST: web2.nginx-proxy.local

View File

@ -0,0 +1,22 @@
import pytest
def test_custom_conf_does_not_apply_to_unknown_vhost(docker_compose, nginxproxy):
r = nginxproxy.get("http://nginx-proxy/")
assert r.status_code == 503
assert "X-test" not in r.headers
def test_custom_conf_applies_to_web1(docker_compose, nginxproxy):
r = nginxproxy.get("http://web1.nginx-proxy.local/port")
assert r.status_code == 200
assert r.text == "answer from port 81\n"
assert "X-test" in r.headers
assert "f00" == r.headers["X-test"]
def test_custom_conf_does_not_apply_to_web2(docker_compose, nginxproxy):
r = nginxproxy.get("http://web2.nginx-proxy.local/port")
assert r.status_code == 200
assert r.text == "answer from port 82\n"
assert "X-test" not in r.headers
def test_custom_block_is_present_in_nginx_generated_conf(docker_compose, nginxproxy):
assert "include /etc/nginx/vhost.d/web1.nginx-proxy.local_location;" in nginxproxy.get_conf()

View File

@ -0,0 +1,23 @@
version: '2'
services:
nginx-proxy:
image: jwilder/nginx-proxy:test
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
- ./my_custom_proxy_settings.conf:/etc/nginx/vhost.d/web1.nginx-proxy.local_location:ro
web1:
image: web
expose:
- "81"
environment:
WEB_PORTS: 81
VIRTUAL_HOST: web1.nginx-proxy.local
web2:
image: web
expose:
- "82"
environment:
WEB_PORTS: 82
VIRTUAL_HOST: web2.nginx-proxy.local

View File

@ -0,0 +1,19 @@
import pytest
def test_custom_conf_does_not_apply_to_unknown_vhost(docker_compose, nginxproxy):
r = nginxproxy.get("http://nginx-proxy/")
assert r.status_code == 503
assert "X-test" not in r.headers
def test_custom_conf_applies_to_web1(docker_compose, nginxproxy):
r = nginxproxy.get("http://web1.nginx-proxy.local/port")
assert r.status_code == 200
assert r.text == "answer from port 81\n"
assert "X-test" in r.headers
assert "f00" == r.headers["X-test"]
def test_custom_conf_does_not_apply_to_web2(docker_compose, nginxproxy):
r = nginxproxy.get("http://web2.nginx-proxy.local/port")
assert r.status_code == 200
assert r.text == "answer from port 82\n"
assert "X-test" not in r.headers

View File

@ -0,0 +1,23 @@
version: '2'
services:
nginx-proxy:
image: jwilder/nginx-proxy:test
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
- ./my_custom_proxy_settings.conf:/etc/nginx/vhost.d/web1.nginx-proxy.local:ro
web1:
image: web
expose:
- "81"
environment:
WEB_PORTS: 81
VIRTUAL_HOST: web1.nginx-proxy.local
web2:
image: web
expose:
- "82"
environment:
WEB_PORTS: 82
VIRTUAL_HOST: web2.nginx-proxy.local

View File

@ -0,0 +1,20 @@
import pytest
def test_custom_conf_does_not_apply_to_unknown_vhost(docker_compose, nginxproxy):
r = nginxproxy.get("http://nginx-proxy/")
assert r.status_code == 503
assert "X-test" not in r.headers
def test_custom_conf_applies_to_web1(docker_compose, nginxproxy):
r = nginxproxy.get("http://web1.nginx-proxy.local/port")
assert r.status_code == 200
assert r.text == "answer from port 81\n"
assert "X-test" in r.headers
assert "f00" == r.headers["X-test"]
def test_custom_conf_applies_to_web2(docker_compose, nginxproxy):
r = nginxproxy.get("http://web2.nginx-proxy.local/port")
assert r.status_code == 200
assert r.text == "answer from port 82\n"
assert "X-test" in r.headers
assert "f00" == r.headers["X-test"]

View File

@ -0,0 +1,23 @@
version: '2'
services:
nginx-proxy:
image: jwilder/nginx-proxy:test
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
- ./my_custom_proxy_settings.conf:/etc/nginx/conf.d/my_custom_proxy_settings.conf:ro
web1:
image: web
expose:
- "81"
environment:
WEB_PORTS: 81
VIRTUAL_HOST: web1.nginx-proxy.local
web2:
image: web
expose:
- "82"
environment:
WEB_PORTS: 82
VIRTUAL_HOST: web2.nginx-proxy.local

View File

@ -0,0 +1,7 @@
import pytest
def test_fallback_on_default(docker_compose, nginxproxy):
r = nginxproxy.get("http://unknown.nginx-proxy.tld/port")
assert r.status_code == 200
assert r.text == "answer from port 81\n"

View File

@ -0,0 +1,17 @@
# GIVEN a webserver with VIRTUAL_HOST set to web1.tld
web1:
image: web
expose:
- "81"
environment:
WEB_PORTS: 81
VIRTUAL_HOST: web1.tld
# WHEN nginx-proxy runs with DEFAULT_HOST set to web1.tld
sut:
image: jwilder/nginx-proxy:test
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
environment:
DEFAULT_HOST: web1.tld

1
test/test_dockergen/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
nginx.tmpl

View File

@ -0,0 +1,38 @@
import os
import docker
import logging
import pytest
@pytest.yield_fixture(scope="module")
def nginx_tmpl():
"""
pytest fixture which extracts the the nginx config template from
the jwilder/nginx-proxy:test image
"""
script_dir = os.path.dirname(__file__)
logging.info("extracting nginx.tmpl from jwilder/nginx-proxy:test")
docker_client = docker.from_env()
print(docker_client.containers.run(
image='jwilder/nginx-proxy:test',
remove=True,
volumes=['{current_dir}:{current_dir}'.format(current_dir=script_dir)],
entrypoint='sh',
command='-xc "cp /app/nginx.tmpl {current_dir} && chmod 777 {current_dir}/nginx.tmpl"'.format(
current_dir=script_dir),
stderr=True))
yield
logging.info("removing nginx.tmpl")
os.remove(os.path.join(script_dir, "nginx.tmpl"))
def test_unknown_virtual_host_is_503(nginx_tmpl, docker_compose, nginxproxy):
r = nginxproxy.get("http://unknown.nginx.container.docker/")
assert r.status_code == 503
def test_forwards_to_whoami(nginx_tmpl, docker_compose, nginxproxy):
r = nginxproxy.get("http://whoami.nginx.container.docker/")
assert r.status_code == 200
whoami_container = docker_compose.containers.get("whoami")
assert r.text == "I'm %s\n" % whoami_container.id[:12]

View File

@ -0,0 +1,26 @@
version: '2'
services:
nginx:
image: nginx
container_name: nginx
volumes:
- /etc/nginx/conf.d
dockergen:
image: jwilder/docker-gen
command: -notify-sighup nginx -watch /etc/docker-gen/templates/nginx.tmpl /etc/nginx/conf.d/default.conf
volumes_from:
- nginx
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
- ./nginx.tmpl:/etc/docker-gen/templates/nginx.tmpl
web:
image: web
container_name: whoami
expose:
- "80"
environment:
WEB_PORTS: 80
VIRTUAL_HOST: whoami.nginx.container.docker

View File

@ -0,0 +1,47 @@
import os
import docker
import logging
import pytest
def versiontuple(v):
return tuple(map(int, (v.split("."))))
docker_version = docker.from_env().version()['Version']
pytestmark = pytest.mark.skipif(versiontuple(docker_version) < versiontuple('1.13'),
reason="Docker compose syntax v3 requires docker engine v1.13")
@pytest.yield_fixture(scope="module")
def nginx_tmpl():
"""
pytest fixture which extracts the the nginx config template from
the jwilder/nginx-proxy:test image
"""
script_dir = os.path.dirname(__file__)
logging.info("extracting nginx.tmpl from jwilder/nginx-proxy:test")
docker_client = docker.from_env()
print(docker_client.containers.run(
image='jwilder/nginx-proxy:test',
remove=True,
volumes=['{current_dir}:{current_dir}'.format(current_dir=script_dir)],
entrypoint='sh',
command='-xc "cp /app/nginx.tmpl {current_dir} && chmod 777 {current_dir}/nginx.tmpl"'.format(
current_dir=script_dir),
stderr=True))
yield
logging.info("removing nginx.tmpl")
os.remove(os.path.join(script_dir, "nginx.tmpl"))
def test_unknown_virtual_host_is_503(nginx_tmpl, docker_compose, nginxproxy):
r = nginxproxy.get("http://unknown.nginx.container.docker/")
assert r.status_code == 503
def test_forwards_to_whoami(nginx_tmpl, docker_compose, nginxproxy):
r = nginxproxy.get("http://whoami.nginx.container.docker/")
assert r.status_code == 200
whoami_container = docker_compose.containers.get("whoami")
assert r.text == "I'm %s\n" % whoami_container.id[:12]

View File

@ -0,0 +1,27 @@
version: '3'
services:
nginx:
image: nginx
container_name: nginx
volumes:
- nginx_conf:/etc/nginx/conf.d
dockergen:
image: jwilder/docker-gen
command: -notify-sighup nginx -watch /etc/docker-gen/templates/nginx.tmpl /etc/nginx/conf.d/default.conf
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
- ./nginx.tmpl:/etc/docker-gen/templates/nginx.tmpl
- nginx_conf:/etc/nginx/conf.d
web:
image: web
container_name: whoami
expose:
- "80"
environment:
WEB_PORTS: 80
VIRTUAL_HOST: whoami.nginx.container.docker
volumes:
nginx_conf: {}

46
test/test_events.py Normal file
View File

@ -0,0 +1,46 @@
"""
Test that nginx-proxy detects new containers
"""
from time import sleep
import pytest
from docker.errors import NotFound
@pytest.yield_fixture()
def web1(docker_compose):
"""
pytest fixture creating a web container with `VIRTUAL_HOST=web1.nginx-proxy` listening on port 81.
"""
container = docker_compose.containers.run(
name="web1",
image="web",
detach=True,
environment={
"WEB_PORTS": "81",
"VIRTUAL_HOST": "web1.nginx-proxy"
},
ports={"81/tcp": None}
)
sleep(2) # give it some time to initialize and for docker-gen to detect it
yield container
try:
docker_compose.containers.get("web1").remove(force=True)
except NotFound:
pass
def test_nginx_proxy_behavior_when_alone(docker_compose, nginxproxy):
r = nginxproxy.get("http://nginx-proxy/")
assert r.status_code == 503
def test_new_container_is_detected(web1, nginxproxy):
r = nginxproxy.get("http://web1.nginx-proxy/port")
assert r.status_code == 200
assert "answer from port 81\n" == r.text
web1.remove(force=True)
sleep(2)
r = nginxproxy.get("http://web1.nginx-proxy/port")
assert r.status_code == 503

4
test/test_events.yml Normal file
View File

@ -0,0 +1,4 @@
nginxproxy:
image: jwilder/nginx-proxy:test
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro

View File

@ -0,0 +1,70 @@
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 4096 (0x1000)
Signature Algorithm: sha256WithRSAEncryption
Issuer: O=nginx-proxy test suite, CN=www.nginx-proxy.tld
Validity
Not Before: Jan 13 03:06:39 2017 GMT
Not After : May 31 03:06:39 2044 GMT
Subject: CN=web.nginx-proxy.tld
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (2048 bit)
Modulus:
00:95:56:c7:0d:48:a5:2b:3c:65:49:3f:26:e1:38:
2b:61:30:56:e4:92:d7:63:e0:eb:ad:ac:f9:33:9b:
b2:31:f1:39:13:0b:e5:43:7b:c5:bd:8a:85:c8:d9:
3d:d8:ac:71:ba:16:e7:81:96:b2:ab:ae:c6:c0:bd:
be:a7:d1:96:8f:b2:9b:df:ba:f9:4d:a1:3b:7e:21:
4a:cd:b6:45:f9:6d:79:50:bf:24:8f:c1:6b:c1:09:
19:5b:62:cb:96:e8:04:14:20:e8:d4:16:62:6a:f2:
37:c1:96:e2:9d:53:05:0b:52:1d:e7:68:92:db:8b:
36:68:cd:8d:5b:02:ff:12:f0:ac:5d:0c:c4:e0:7a:
55:a2:49:60:9f:ff:47:1f:52:73:55:4d:d4:f2:d1:
62:a2:f4:50:9d:c9:f6:f1:43:b3:dc:57:e1:31:76:
b4:e0:a4:69:7e:f2:6d:34:ae:b9:8d:74:26:7b:d9:
f6:07:00:ef:4b:36:61:b3:ef:7a:a1:36:3a:b6:d0:
9e:f8:b8:a9:0d:4c:30:a2:ed:eb:ab:6b:eb:2e:e2:
0b:28:be:f7:04:b1:e9:e0:84:d6:5d:31:77:7c:dc:
d2:1f:d4:1d:71:6f:6f:6c:6d:1b:bf:31:e2:5b:c3:
52:d0:14:fc:8b:fb:45:ea:41:ec:ca:c7:3b:67:12:
c4:df
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Subject Alternative Name:
DNS:web.nginx-proxy.tld
Signature Algorithm: sha256WithRSAEncryption
4e:48:7d:81:66:ba:2f:50:3d:24:42:61:3f:1f:de:cf:ec:1b:
1b:bd:0a:67:b6:62:c8:79:9d:31:a0:fd:a9:61:ce:ff:69:bf:
0e:f4:f7:e6:15:2b:b0:f0:e4:f2:f4:d2:8f:74:02:b1:1e:4a:
a8:6f:26:0a:77:32:29:cf:dc:b5:61:82:3e:58:47:61:92:f0:
0c:20:25:f8:41:4d:34:09:44:bc:39:9e:aa:82:06:83:13:8b:
1e:2c:3d:cf:cd:1a:f7:77:39:38:e0:a3:a7:f3:09:da:02:8d:
73:75:38:b4:dd:24:a7:f9:03:db:98:c6:88:54:87:dc:e0:65:
4c:95:c5:39:9c:00:30:dc:f0:d3:2c:19:ca:f1:f4:6c:c6:d9:
b5:c4:4a:c7:bc:a1:2e:88:7b:b5:33:d0:ff:fb:48:5e:3e:29:
fa:58:e5:03:de:d8:17:de:ed:96:fc:7e:1f:fe:98:f6:be:99:
38:87:51:c0:d3:b7:9a:0f:26:92:e5:53:1b:d6:25:4c:ac:48:
f3:29:fc:74:64:9d:07:6a:25:57:24:aa:a7:70:fa:8f:6c:a7:
2b:b7:9d:81:46:10:32:93:b9:45:6d:0f:16:18:b2:21:1f:f3:
30:24:62:3f:e1:6c:07:1d:71:28:cb:4c:bb:f5:39:05:f9:b2:
5b:a0:05:1b
-----BEGIN CERTIFICATE-----
MIIC+zCCAeOgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwPzEfMB0GA1UECgwWbmdp
bngtcHJveHkgdGVzdCBzdWl0ZTEcMBoGA1UEAwwTd3d3Lm5naW54LXByb3h5LnRs
ZDAeFw0xNzAxMTMwMzA2MzlaFw00NDA1MzEwMzA2MzlaMB4xHDAaBgNVBAMME3dl
Yi5uZ2lueC1wcm94eS50bGQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
AQCVVscNSKUrPGVJPybhOCthMFbkktdj4OutrPkzm7Ix8TkTC+VDe8W9ioXI2T3Y
rHG6FueBlrKrrsbAvb6n0ZaPspvfuvlNoTt+IUrNtkX5bXlQvySPwWvBCRlbYsuW
6AQUIOjUFmJq8jfBluKdUwULUh3naJLbizZozY1bAv8S8KxdDMTgelWiSWCf/0cf
UnNVTdTy0WKi9FCdyfbxQ7PcV+ExdrTgpGl+8m00rrmNdCZ72fYHAO9LNmGz73qh
Njq20J74uKkNTDCi7eura+su4gsovvcEsenghNZdMXd83NIf1B1xb29sbRu/MeJb
w1LQFPyL+0XqQezKxztnEsTfAgMBAAGjIjAgMB4GA1UdEQQXMBWCE3dlYi5uZ2lu
eC1wcm94eS50bGQwDQYJKoZIhvcNAQELBQADggEBAE5IfYFmui9QPSRCYT8f3s/s
Gxu9Cme2Ysh5nTGg/alhzv9pvw709+YVK7Dw5PL00o90ArEeSqhvJgp3MinP3LVh
gj5YR2GS8AwgJfhBTTQJRLw5nqqCBoMTix4sPc/NGvd3OTjgo6fzCdoCjXN1OLTd
JKf5A9uYxohUh9zgZUyVxTmcADDc8NMsGcrx9GzG2bXESse8oS6Ie7Uz0P/7SF4+
KfpY5QPe2Bfe7Zb8fh/+mPa+mTiHUcDTt5oPJpLlUxvWJUysSPMp/HRknQdqJVck
qqdw+o9spyu3nYFGEDKTuUVtDxYYsiEf8zAkYj/hbAcdcSjLTLv1OQX5slugBRs=
-----END CERTIFICATE-----

View File

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEAlVbHDUilKzxlST8m4TgrYTBW5JLXY+Drraz5M5uyMfE5Ewvl
Q3vFvYqFyNk92KxxuhbngZayq67GwL2+p9GWj7Kb37r5TaE7fiFKzbZF+W15UL8k
j8FrwQkZW2LLlugEFCDo1BZiavI3wZbinVMFC1Id52iS24s2aM2NWwL/EvCsXQzE
4HpVoklgn/9HH1JzVU3U8tFiovRQncn28UOz3FfhMXa04KRpfvJtNK65jXQme9n2
BwDvSzZhs+96oTY6ttCe+LipDUwwou3rq2vrLuILKL73BLHp4ITWXTF3fNzSH9Qd
cW9vbG0bvzHiW8NS0BT8i/tF6kHsysc7ZxLE3wIDAQABAoIBAEmK7IecKMq7+V0y
3mC3GpXICmKR9cRX9XgX4LkLiZuSoXrBtuuevmhzGSMp6I0VjwQHV4a3wdFORs6Q
Ip3eVvj5Ck4Jc9BJAFVC6+WWR6tnwACFwOmSZRAw/O3GH2B3bdrDwiT/yQPFuLN7
LKoxQiCrFdLp6rh3PBosb9pMBXU7k/HUazIdgmSKg6/JIoo/4Gwyid04TF/4MI2l
RscxtP5/ANtS8VgwBEqhgdafRJ4KnLEpgvswgIQvUKmduVhZQlzd0LMY8FbhKVqz
Utg8gsXaTyH6df/nmgUIInxLMz/MKPnMkv99fS6Sp/hvYlGpLZFWBJ6unMq3lKEr
LMbHfIECgYEAxB+5QWdVqG2r9loJlf8eeuNeMPml4P8Jmi5RKyJC7Cww6DMlMxOS
78ZJfl4b3ZrWuyvhjOfX/aTq7kQaF1BI9o3KJBH8k6EtO4gI8KeNmDONyQk9zsrn
ru8Zwr7hVbAo8fCXxCnmPzhDLsYg6f3BVOsQWoX2SFYKZ1GvkPfIReECgYEAwu6G
qtgFb57Vim10ecfWGM6vrPxvyfqP+zlH/p4nR+aQ+2sFbt27D0B1byWBRZe4KQyw
Vq6XiQ09Fk6MJr8E8iAr9GXPPHcqlYI6bbNc6YOP3jVSKut0tQdTUOHll4kYIY+h
RS3VA3+BA//ADpWpywu+7RZRbaIECA+U2a224r8CgYB5PCMIixgoRaNHZeEHF+1/
iY1wOOKRcxY8eOU0BLnZxHd3EiasrCzoi2pi80nGczDKAxYqRCcAZDHVl8OJJdf0
kTGjmnrHx5pucmkUWn7s1vGOlGfgrQ0K1kLWX6hrj7m/1Tn7yOrLqbvd7hvqiTI5
jBVP3/+eN5G2zIf61TC4AQKBgCX2Q92jojNhsF58AHHy+/vqzIWYx8CC/mVDe4TX
kfjLqzJ7XhyAK/zFZdlWaX1/FYtRAEpxR+uV226rr1mgW7s3jrfS1/ADmRRyvyQ8
CP0k9PCmW7EmF51lptEanRbMyRlIGnUZfuFmhF6eAO4WMXHsgKs1bHg4VCapuihG
T1aLAoGACRGn1UxFuBGqtsh2zhhsBZE7GvXKJSk/eP7QJeEXUNpNjCpgm8kIZM5K
GorpL7PSB8mwVlDl18TpMm3P7nz6YkJYte+HdjO7pg59H39Uvtg3tZnIrFxNxVNb
YF62/yHfk2AyTgjQZQUSmDS84jq1zUK4oS90lxr+u8qwELTniMs=
-----END RSA PRIVATE KEY-----

View File

@ -0,0 +1,81 @@
import pytest
def test_arbitrary_headers_are_passed_on(docker_compose, nginxproxy):
r = nginxproxy.get("http://web.nginx-proxy.tld/headers", headers={'Foo': 'Bar'})
assert r.status_code == 200
assert "Foo: Bar\n" in r.text
##### Testing the handling of X-Forwarded-For #####
def test_X_Forwarded_For_is_generated(docker_compose, nginxproxy):
r = nginxproxy.get("http://web.nginx-proxy.tld/headers")
assert r.status_code == 200
assert "X-Forwarded-For:" in r.text
def test_X_Forwarded_For_is_passed_on(docker_compose, nginxproxy):
r = nginxproxy.get("http://web.nginx-proxy.tld/headers", headers={'X-Forwarded-For': '1.2.3.4'})
assert r.status_code == 200
assert "X-Forwarded-For: 1.2.3.4, " in r.text
##### Testing the handling of X-Forwarded-Proto #####
def test_X_Forwarded_Proto_is_generated(docker_compose, nginxproxy):
r = nginxproxy.get("http://web.nginx-proxy.tld/headers")
assert r.status_code == 200
assert "X-Forwarded-Proto: http" in r.text
def test_X_Forwarded_Proto_is_passed_on(docker_compose, nginxproxy):
r = nginxproxy.get("http://web.nginx-proxy.tld/headers", headers={'X-Forwarded-Proto': 'f00'})
assert r.status_code == 200
assert "X-Forwarded-Proto: f00\n" in r.text
##### Testing the handling of X-Forwarded-Port #####
def test_X_Forwarded_Port_is_generated(docker_compose, nginxproxy):
r = nginxproxy.get("http://web.nginx-proxy.tld/headers")
assert r.status_code == 200
assert "X-Forwarded-Port: 80\n" in r.text
def test_X_Forwarded_Port_is_passed_on(docker_compose, nginxproxy):
r = nginxproxy.get("http://web.nginx-proxy.tld/headers", headers={'X-Forwarded-Port': '1234'})
assert r.status_code == 200
assert "X-Forwarded-Port: 1234\n" in r.text
##### Testing the handling of X-Forwarded-Ssl #####
def test_X_Forwarded_Ssl_is_generated(docker_compose, nginxproxy):
r = nginxproxy.get("http://web.nginx-proxy.tld/headers")
assert r.status_code == 200
assert "X-Forwarded-Ssl: off\n" in r.text
def test_X_Forwarded_Ssl_is_overwritten(docker_compose, nginxproxy):
r = nginxproxy.get("http://web.nginx-proxy.tld/headers", headers={'X-Forwarded-Ssl': 'f00'})
assert r.status_code == 200
assert "X-Forwarded-Ssl: off\n" in r.text
##### Other headers
def test_X_Real_IP_is_generated(docker_compose, nginxproxy):
r = nginxproxy.get("http://web.nginx-proxy.tld/headers")
assert r.status_code == 200
assert "X-Real-IP: " in r.text
def test_Host_is_passed_on(docker_compose, nginxproxy):
r = nginxproxy.get("http://web.nginx-proxy.tld/headers")
assert r.status_code == 200
assert "Host: web.nginx-proxy.tld" in r.text
def test_httpoxy_safe(docker_compose, nginxproxy):
"""
See https://httpoxy.org/
nginx-proxy should suppress the `Proxy` header
"""
r = nginxproxy.get("http://web.nginx-proxy.tld/headers", headers={'Proxy': 'tcp://some.hacker.com'})
assert r.status_code == 200
assert "Proxy:" not in r.text

View File

@ -0,0 +1,13 @@
web:
image: web
expose:
- "80"
environment:
WEB_PORTS: 80
VIRTUAL_HOST: web.nginx-proxy.tld
sut:
image: jwilder/nginx-proxy:test
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro

View File

@ -0,0 +1,82 @@
import pytest
def test_arbitrary_headers_are_passed_on(docker_compose, nginxproxy):
r = nginxproxy.get("https://web.nginx-proxy.tld/headers", headers={'Foo': 'Bar'})
assert r.status_code == 200
assert "Foo: Bar\n" in r.text
##### Testing the handling of X-Forwarded-For #####
def test_X_Forwarded_For_is_generated(docker_compose, nginxproxy):
r = nginxproxy.get("https://web.nginx-proxy.tld/headers")
assert r.status_code == 200
assert "X-Forwarded-For:" in r.text
def test_X_Forwarded_For_is_passed_on(docker_compose, nginxproxy):
r = nginxproxy.get("https://web.nginx-proxy.tld/headers", headers={'X-Forwarded-For': '1.2.3.4'})
assert r.status_code == 200
assert "X-Forwarded-For: 1.2.3.4, " in r.text
##### Testing the handling of X-Forwarded-Proto #####
def test_X_Forwarded_Proto_is_generated(docker_compose, nginxproxy):
r = nginxproxy.get("https://web.nginx-proxy.tld/headers")
assert r.status_code == 200
assert "X-Forwarded-Proto: https" in r.text
def test_X_Forwarded_Proto_is_passed_on(docker_compose, nginxproxy):
r = nginxproxy.get("https://web.nginx-proxy.tld/headers", headers={'X-Forwarded-Proto': 'f00'})
assert r.status_code == 200
assert "X-Forwarded-Proto: f00\n" in r.text
##### Testing the handling of X-Forwarded-Port #####
def test_X_Forwarded_Port_is_generated(docker_compose, nginxproxy):
r = nginxproxy.get("https://web.nginx-proxy.tld/headers")
assert r.status_code == 200
assert "X-Forwarded-Port: 443\n" in r.text
def test_X_Forwarded_Port_is_passed_on(docker_compose, nginxproxy):
r = nginxproxy.get("https://web.nginx-proxy.tld/headers", headers={'X-Forwarded-Port': '1234'})
assert r.status_code == 200
assert "X-Forwarded-Port: 1234\n" in r.text
##### Testing the handling of X-Forwarded-Ssl #####
def test_X_Forwarded_Ssl_is_generated(docker_compose, nginxproxy):
r = nginxproxy.get("https://web.nginx-proxy.tld/headers")
assert r.status_code == 200
assert "X-Forwarded-Ssl: on\n" in r.text
def test_X_Forwarded_Ssl_is_overwritten(docker_compose, nginxproxy):
r = nginxproxy.get("https://web.nginx-proxy.tld/headers", headers={'X-Forwarded-Ssl': 'f00'})
assert r.status_code == 200
assert "X-Forwarded-Ssl: on\n" in r.text
##### Other headers
def test_X_Real_IP_is_generated(docker_compose, nginxproxy):
r = nginxproxy.get("https://web.nginx-proxy.tld/headers")
assert r.status_code == 200
assert "X-Real-IP: " in r.text
def test_Host_is_passed_on(docker_compose, nginxproxy):
r = nginxproxy.get("https://web.nginx-proxy.tld/headers")
assert r.status_code == 200
assert "Host: web.nginx-proxy.tld" in r.text
def test_httpoxy_safe(docker_compose, nginxproxy):
"""
See https://httpoxy.org/
nginx-proxy should suppress the `Proxy` header
"""
r = nginxproxy.get("https://web.nginx-proxy.tld/headers", headers={'Proxy': 'tcp://some.hacker.com'})
assert r.status_code == 200
assert "Proxy:" not in r.text

View File

@ -0,0 +1,15 @@
web:
image: web
expose:
- "80"
environment:
WEB_PORTS: 80
VIRTUAL_HOST: web.nginx-proxy.tld
sut:
image: jwilder/nginx-proxy:test
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
- ./certs/web.nginx-proxy.tld.crt:/etc/nginx/certs/web.nginx-proxy.tld.crt:ro
- ./certs/web.nginx-proxy.tld.key:/etc/nginx/certs/web.nginx-proxy.tld.key:ro

View File

@ -1,183 +0,0 @@
# Test if requirements are met
(
type docker &>/dev/null || ( echo "docker is not available"; exit 1 )
)>&2
# set a few global variables
SUT_IMAGE=jwilder/nginx-proxy:bats
TEST_FILE=$(basename $BATS_TEST_FILENAME .bats)
# load the Bats stdlib (see https://github.com/sstephenson/bats/pull/110)
DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
export BATS_LIB="${DIR}/lib/bats"
load "${BATS_LIB}/batslib.bash"
# load additional bats helpers
load ${DIR}/lib/helpers.bash
load ${DIR}/lib/docker_helpers.bash
# Define functions specific to our test suite
# run the SUT docker container
# and makes sure it remains started
# and displays the nginx-proxy start logs
#
# $1 container name
# $@ other options for the `docker run` command
function nginxproxy {
local -r container_name=$1
shift
docker_clean $container_name \
&& docker run -d \
--label bats-type="nginx-proxy" \
--name $container_name \
-v $DIR/lib/ssl/dhparam.pem:/etc/nginx/dhparam/dhparam.pem:ro \
"$@" \
$SUT_IMAGE \
&& wait_for_nginxproxy_container_to_start $container_name \
&& docker logs $container_name
}
# wait until the nginx-proxy container is ready to operate
#
# $1 container name
function wait_for_nginxproxy_container_to_start {
local -r container_name=$1
sleep .5s # give time to eventually fail to initialize
function is_running {
run docker_running_state $container_name
assert_output "true"
}
retry 3 1 is_running
}
# Send a HTTP request to container $1 for path $2 and
# Additional curl options can be passed as $@
#
# $1 container name
# $2 HTTP path to query
# $@ additional options to pass to the curl command
function curl_container {
local -r container=$1
local -r path=$2
shift 2
docker run --label bats-type="curl" appropriate/curl --silent \
--connect-timeout 5 \
--max-time 20 \
"$@" \
http://$(docker_ip $container)${path}
}
# Send a HTTPS request to container $1 for path $2 and
# Additional curl options can be passed as $@
#
# $1 container name
# $2 HTTPS path to query
# $@ additional options to pass to the curl command
function curl_container_https {
local -r container=$1
local -r path=$2
shift 2
docker run --label bats-type="curl" appropriate/curl --silent \
--connect-timeout 5 \
--max-time 20 \
--insecure \
"$@" \
https://$(docker_ip $container)${path}
}
# start a container running (one or multiple) webservers listening on given ports
#
# $1 container name
# $2 container port(s). If multiple ports, provide them as a string: "80 90" with a space as a separator
# $@ `docker run` additional options
function prepare_web_container {
local -r container_name=$1
local -r ports=$2
shift 2
local -r options="$@"
local expose_option=""
IFS=$' \t\n' # See https://github.com/sstephenson/bats/issues/89
for port in $ports; do
expose_option="${expose_option}--expose=$port "
done
( # used for debugging purpose. Will be display if test fails
echo "container_name: $container_name"
echo "ports: $ports"
echo "options: $options"
echo "expose_option: $expose_option"
)>&2
docker_clean $container_name
# GIVEN a container exposing 1 webserver on ports 1234
run docker run -d \
--label bats-type="web" \
--name $container_name \
$expose_option \
-w /var/www/ \
-v $DIR/web_helpers:/var/www:ro \
$options \
-e PYTHON_PORTS="$ports" \
python:3 bash -c "
trap '[ \${#PIDS[@]} -gt 0 ] && kill -TERM \${PIDS[@]}' TERM
declare -a PIDS
for port in \$PYTHON_PORTS; do
echo starting a web server listening on port \$port;
./webserver.py \$port &
PIDS+=(\$!)
done
wait \${PIDS[@]}
trap - TERM
wait \${PIDS[@]}
"
assert_success
# THEN querying directly port works
IFS=$' \t\n' # See https://github.com/sstephenson/bats/issues/89
for port in $ports; do
run retry 5 1s docker run --label bats-type="curl" appropriate/curl --silent --fail http://$(docker_ip $container_name):$port/port
assert_output "answer from port $port"
done
}
# stop all containers with the "bats-type" label (matching the optionally supplied value)
#
# $1 optional label value
function stop_bats_containers {
local -r value=$1
if [ -z "$value" ]; then
CIDS=( $(docker ps -q --filter "label=bats-type") )
else
CIDS=( $(docker ps -q --filter "label=bats-type=$value") )
fi
if [ ${#CIDS[@]} -gt 0 ]; then
docker stop ${CIDS[@]} >&2
fi
}
# wait for a docker-gen container to receive a specified event from a
# container with the specified ID/name
#
# $1 docker-gen container name
# $2 event
# $3 ID/name of container to receive event from
function dockergen_wait_for_event {
local -r container=$1
local -r event=$2
local -r other=$3
local -r did=$(docker_id "$other")
docker_wait_for_log "$container" 9 "Received event $event for container ${did:0:12}"
}

35
test/test_ipv6.py Normal file
View File

@ -0,0 +1,35 @@
import pytest
def test_unknown_virtual_host_ipv4(docker_compose, nginxproxy):
r = nginxproxy.get("http://nginx-proxy/port")
assert r.status_code == 503
def test_forwards_to_web1_ipv4(docker_compose, nginxproxy):
r = nginxproxy.get("http://web1.nginx-proxy.tld/port")
assert r.status_code == 200
assert r.text == "answer from port 81\n"
def test_forwards_to_web2_ipv4(docker_compose, nginxproxy):
r = nginxproxy.get("http://web2.nginx-proxy.tld/port")
assert r.status_code == 200
assert r.text == "answer from port 82\n"
def test_unknown_virtual_host_ipv6(docker_compose, nginxproxy):
r = nginxproxy.get("http://nginx-proxy/port", ipv6=True)
assert r.status_code == 503
def test_forwards_to_web1_ipv6(docker_compose, nginxproxy):
r = nginxproxy.get("http://web1.nginx-proxy.tld/port", ipv6=True)
assert r.status_code == 200
assert r.text == "answer from port 81\n"
def test_forwards_to_web2_ipv6(docker_compose, nginxproxy):
r = nginxproxy.get("http://web2.nginx-proxy.tld/port", ipv6=True)
assert r.status_code == 200
assert r.text == "answer from port 82\n"

23
test/test_ipv6.yml Normal file
View File

@ -0,0 +1,23 @@
web1:
image: web
expose:
- "81"
environment:
WEB_PORTS: 81
VIRTUAL_HOST: web1.nginx-proxy.tld
web2:
image: web
expose:
- "82"
environment:
WEB_PORTS: 82
VIRTUAL_HOST: web2.nginx-proxy.tld
sut:
image: jwilder/nginx-proxy:test
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
environment:
ENABLE_IPV6: "true"

View File

@ -0,0 +1,16 @@
import pytest
def test_unknown_virtual_host_is_503(docker_compose, nginxproxy):
r = nginxproxy.get("http://unknown.nginx-proxy.tld/port")
assert r.status_code == 503
def test_webA_is_forwarded(docker_compose, nginxproxy):
r = nginxproxy.get("http://webA.nginx-proxy.tld/port")
assert r.status_code == 200
assert r.text == "answer from port 81\n"
def test_webB_is_forwarded(docker_compose, nginxproxy):
r = nginxproxy.get("http://webB.nginx-proxy.tld/port")
assert r.status_code == 200
assert r.text == "answer from port 81\n"

View File

@ -0,0 +1,13 @@
web:
image: web
expose:
- "81"
environment:
WEB_PORTS: 81
VIRTUAL_HOST: webA.nginx-proxy.tld,webB.nginx-proxy.tld
sut:
image: jwilder/nginx-proxy:test
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro

View File

@ -0,0 +1,15 @@
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_web1(docker_compose, nginxproxy):
r = nginxproxy.get("http://web1.nginx-proxy.local/port")
assert r.status_code == 200
assert r.text == "answer from port 81\n"
def test_forwards_to_web2(docker_compose, nginxproxy):
r = nginxproxy.get("http://web2.nginx-proxy.local/port")
assert r.status_code == 200
assert r.text == "answer from port 82\n"

View File

@ -0,0 +1,34 @@
version: '2'
networks:
net1: {}
net2: {}
services:
nginx-proxy:
image: jwilder/nginx-proxy:test
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
networks:
- net1
- net2
web1:
image: web
expose:
- "81"
environment:
WEB_PORTS: 81
VIRTUAL_HOST: web1.nginx-proxy.local
networks:
- net1
web2:
image: web
expose:
- "82"
environment:
WEB_PORTS: 82
VIRTUAL_HOST: web2.nginx-proxy.local
networks:
- net2

View File

@ -0,0 +1,7 @@
import pytest
def test_answer_is_served_from_chosen_port(docker_compose, nginxproxy):
r = nginxproxy.get("http://web.nginx-proxy.tld/port")
assert r.status_code == 200
assert "answer from port 90\n" in r.text

View File

@ -0,0 +1,14 @@
web:
image: web
expose:
- "80"
- "90"
environment:
WEB_PORTS: "80 90"
VIRTUAL_HOST: "web.nginx-proxy.tld"
VIRTUAL_PORT: 90
sut:
image: jwilder/nginx-proxy:test
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro

View File

@ -0,0 +1,7 @@
import pytest
def test_answer_is_served_from_port_80_by_default(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

View File

@ -0,0 +1,13 @@
web:
image: web
expose:
- "80"
- "81"
environment:
WEB_PORTS: "80 81"
VIRTUAL_HOST: "web.nginx-proxy.tld"
sut:
image: jwilder/nginx-proxy:test
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro

View File

@ -0,0 +1,7 @@
import pytest
def test_answer_is_served_from_exposed_port_even_if_not_80(docker_compose, nginxproxy):
r = nginxproxy.get("http://web.nginx-proxy.tld/port")
assert r.status_code == 200
assert "answer from port 81\n" in r.text

View File

@ -0,0 +1,13 @@
web:
image: web
expose:
- "81"
environment:
WEB_PORTS: "81"
VIRTUAL_HOST: "web.nginx-proxy.tld"
sut:
image: jwilder/nginx-proxy:test
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro

24
test/test_nominal.py Normal file
View File

@ -0,0 +1,24 @@
import pytest
from requests import ConnectionError
def test_unknown_virtual_host(docker_compose, nginxproxy):
r = nginxproxy.get("http://nginx-proxy/port")
assert r.status_code == 503
def test_forwards_to_web1(docker_compose, nginxproxy):
r = nginxproxy.get("http://web1.nginx-proxy.tld/port")
assert r.status_code == 200
assert r.text == "answer from port 81\n"
def test_forwards_to_web2(docker_compose, nginxproxy):
r = nginxproxy.get("http://web2.nginx-proxy.tld/port")
assert r.status_code == 200
assert r.text == "answer from port 82\n"
def test_ipv6_is_disabled_by_default(docker_compose, nginxproxy):
with pytest.raises(ConnectionError):
nginxproxy.get("http://nginx-proxy/port", ipv6=True)

21
test/test_nominal.yml Normal file
View File

@ -0,0 +1,21 @@
web1:
image: web
expose:
- "81"
environment:
WEB_PORTS: 81
VIRTUAL_HOST: web1.nginx-proxy.tld
web2:
image: web
expose:
- "82"
environment:
WEB_PORTS: 82
VIRTUAL_HOST: web2.nginx-proxy.tld
sut:
image: jwilder/nginx-proxy:test
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro

View File

@ -0,0 +1,70 @@
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 4096 (0x1000)
Signature Algorithm: sha256WithRSAEncryption
Issuer: O=nginx-proxy test suite, CN=www.nginx-proxy.tld
Validity
Not Before: Jan 10 00:08:52 2017 GMT
Not After : May 28 00:08:52 2044 GMT
Subject: CN=*.nginx-proxy.tld
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (2048 bit)
Modulus:
00:cb:45:f4:14:9b:fe:64:85:79:4a:36:8d:3d:d1:
27:d0:7c:36:28:30:e6:73:80:6f:7c:49:23:d0:6c:
17:e4:44:c0:77:4d:9a:c2:bc:24:84:e3:a5:4d:ba:
d2:da:51:7b:a1:2a:12:d4:c0:19:55:69:2c:22:27:
2d:1a:f6:fc:4b:7f:e9:cb:a8:3c:e8:69:b8:d2:4f:
de:4e:50:e2:d0:74:30:7c:42:5a:ae:aa:85:a5:b1:
71:4d:c9:7e:86:8b:62:8c:3e:0d:e3:3b:c3:f5:81:
0b:8c:68:79:fe:bf:10:fb:ae:ec:11:49:6d:64:5e:
1a:7d:b3:92:93:4e:96:19:3a:98:04:a7:66:b2:74:
61:2d:41:13:0c:a4:54:0d:2c:78:fd:b4:a3:e8:37:
78:9a:de:fa:bc:2e:a8:0f:67:14:58:ce:c3:87:d5:
14:0e:8b:29:7d:48:19:b2:a9:f5:b4:e8:af:32:21:
67:15:7e:43:52:8b:20:cf:9f:38:43:bf:fd:c8:24:
7f:52:a3:88:f2:f1:4a:14:91:2a:6e:91:6f:fb:7d:
6a:78:c6:6d:2e:dd:1e:4c:2b:63:bb:3a:43:9c:91:
f9:df:d3:08:13:63:86:7d:ce:e8:46:cf:f1:6c:1f:
ca:f7:4c:de:d8:4b:e0:da:bc:06:d9:87:0f:ff:96:
45:85
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Subject Alternative Name:
DNS:*.nginx-proxy.tld
Signature Algorithm: sha256WithRSAEncryption
6e:a5:0e:e4:d3:cc:d5:b7:fc:34:75:89:4e:98:8c:e7:08:06:
a8:5b:ec:13:7d:83:99:a2:61:b8:d5:12:6e:c5:b4:53:4e:9a:
22:cd:ad:14:30:6a:7d:58:d7:23:d9:a4:2a:96:a0:40:9e:50:
9f:ce:f2:fe:8c:dd:9a:ac:99:39:5b:89:2d:ca:e5:3e:c3:bc:
03:04:1c:12:d9:6e:b8:9f:f0:3a:be:12:44:7e:a4:21:86:73:
af:d5:00:51:3f:2c:56:70:34:8f:26:b0:7f:b0:cf:cf:7f:f9:
40:6f:00:29:c4:cf:c3:b7:c2:49:3d:3f:b0:26:78:87:b9:c7:
6c:1b:aa:6a:1a:dd:c5:eb:f2:69:ba:6d:46:0b:92:49:b5:11:
3c:eb:48:c7:2f:fb:33:a6:6a:82:a2:ab:f8:1e:5f:7d:e3:b7:
f2:fd:f5:88:a5:09:4d:a0:bc:f4:3b:cd:d2:8b:d7:57:1f:86:
3b:d2:3e:a4:92:21:b0:02:0b:e9:e0:c4:1c:f1:78:e2:58:a7:
26:5f:4c:29:c8:23:f0:6e:12:3f:bd:ad:44:7b:0b:bd:db:ba:
63:8d:07:c6:9d:dc:46:cc:63:40:ba:5e:45:82:dd:9a:e5:50:
e8:e7:d7:27:88:fc:6f:1d:8a:e7:5c:49:28:aa:10:29:75:28:
c7:52:de:f9
-----BEGIN CERTIFICATE-----
MIIC9zCCAd+gAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwPzEfMB0GA1UECgwWbmdp
bngtcHJveHkgdGVzdCBzdWl0ZTEcMBoGA1UEAwwTd3d3Lm5naW54LXByb3h5LnRs
ZDAeFw0xNzAxMTAwMDA4NTJaFw00NDA1MjgwMDA4NTJaMBwxGjAYBgNVBAMMESou
bmdpbngtcHJveHkudGxkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
y0X0FJv+ZIV5SjaNPdEn0Hw2KDDmc4BvfEkj0GwX5ETAd02awrwkhOOlTbrS2lF7
oSoS1MAZVWksIictGvb8S3/py6g86Gm40k/eTlDi0HQwfEJarqqFpbFxTcl+hoti
jD4N4zvD9YELjGh5/r8Q+67sEUltZF4afbOSk06WGTqYBKdmsnRhLUETDKRUDSx4
/bSj6Dd4mt76vC6oD2cUWM7Dh9UUDospfUgZsqn1tOivMiFnFX5DUosgz584Q7/9
yCR/UqOI8vFKFJEqbpFv+31qeMZtLt0eTCtjuzpDnJH539MIE2OGfc7oRs/xbB/K
90ze2Evg2rwG2YcP/5ZFhQIDAQABoyAwHjAcBgNVHREEFTATghEqLm5naW54LXBy
b3h5LnRsZDANBgkqhkiG9w0BAQsFAAOCAQEAbqUO5NPM1bf8NHWJTpiM5wgGqFvs
E32DmaJhuNUSbsW0U06aIs2tFDBqfVjXI9mkKpagQJ5Qn87y/ozdmqyZOVuJLcrl
PsO8AwQcEtluuJ/wOr4SRH6kIYZzr9UAUT8sVnA0jyawf7DPz3/5QG8AKcTPw7fC
ST0/sCZ4h7nHbBuqahrdxevyabptRguSSbURPOtIxy/7M6ZqgqKr+B5ffeO38v31
iKUJTaC89DvN0ovXVx+GO9I+pJIhsAIL6eDEHPF44linJl9MKcgj8G4SP72tRHsL
vdu6Y40Hxp3cRsxjQLpeRYLdmuVQ6OfXJ4j8bx2K51xJKKoQKXUox1Le+Q==
-----END CERTIFICATE-----

View File

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAy0X0FJv+ZIV5SjaNPdEn0Hw2KDDmc4BvfEkj0GwX5ETAd02a
wrwkhOOlTbrS2lF7oSoS1MAZVWksIictGvb8S3/py6g86Gm40k/eTlDi0HQwfEJa
rqqFpbFxTcl+hotijD4N4zvD9YELjGh5/r8Q+67sEUltZF4afbOSk06WGTqYBKdm
snRhLUETDKRUDSx4/bSj6Dd4mt76vC6oD2cUWM7Dh9UUDospfUgZsqn1tOivMiFn
FX5DUosgz584Q7/9yCR/UqOI8vFKFJEqbpFv+31qeMZtLt0eTCtjuzpDnJH539MI
E2OGfc7oRs/xbB/K90ze2Evg2rwG2YcP/5ZFhQIDAQABAoIBAQCjAro2PNLJMfCO
fyjNRgmzu6iCmpR0U68T8GN0JPsT576g7e8J828l0pkhuIyW33lRSThIvLSUNf9a
dChL032H3lBTLduKVh4NKleQXnVFzaeEPoISSFVdButiAhAhPW4OIUVp0OfY3V+x
fac3j2nDLAfL5SKAtqZv363Py9m66EBYm5BmGTQqT/frQWeCEBvlErQef5RIaU8p
e2zMWgSNNojVai8U3nKNRvYHWeWXM6Ck7lCvkHhMF+RpbmCZuqhbEARVnehU/Jdn
QHJ3nxeA2OWpoWKXvAHtSnno49yxq1UIstiQvY+ng5C5i56UlB60UiU2NJ6doZkB
uQ7/1MaBAoGBAORdcFtgdgRALjXngFWhpCp0CseyUehn1KhxDCG+D1pJ142/ymcf
oJOzKJPMRNDdDUBMnR1GBfy7rmwvYevI/SMNy2Qs7ofcXPbdtwwvTCToZ1V9/54k
VfuPBFT+3QzWRvG1tjTV3E4L2VV3nrl2qNPhE5DlfIaU3nQq5Fl0HprJAoGBAOPf
MWOTGev61CdODO5KN3pLAoamiPs5lEUlz3kM3L1Q52YLITxNDjRj9hWBUATJZOS2
pLOoYRwmhD7vrnimMc41+NuuFX+4T7hWPc8uSuOxX0VijYtULyNRK57mncG1Fq9M
RMLbOJ7FD+8jdXNsSMqpQ+pxLJRX/A10O2fOQnbdAoGAL5hV4YWSM0KZHvz332EI
ER0MXiCJN7HkPZMKH0I4eu3m8hEmAyYxVndBnsQ1F37q0xrkqAQ/HTSUntGlS/og
4Bxw5pkCwegoq/77tpto+ExDtSrEitYx4XMmSPyxX4qNULU5m3tzJgUML+b1etwD
Rd2kMU/TC02dq4KBAy/TbRkCgYAl1xN5iJz+XenLGR/2liZ+TWR+/bqzlU006mF4
pZUmbv/uJxz+yYD5XDwqOA4UrWjuvhG9r9FoflDprp2XdWnB556KxG7XhcDfSJr9
A5/2DadXe1Ur9O/a+oi2228JEsxQkea9QPA3FVxfBtFjOHEiDlez39VaUP4PMeUH
iO3qlQKBgFQhdTb7HeYnApYIDHLmd1PvjRvp8XKR1CpEN0nkw8HpHcT1q1MUjQCr
iT6FQupULEvGmO3frQsgVeRIQDbEdZK3C5xCtn6qOw70sYATVf361BbTtidmU9yV
THFxwDSVLiVZgFryoY/NtAc27sVdJnGsPRjjaeVgALAsLbmZ1K/H
-----END RSA PRIVATE KEY-----

View File

@ -0,0 +1,71 @@
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 4096 (0x1000)
Signature Algorithm: sha256WithRSAEncryption
Issuer: O=nginx-proxy test suite, CN=www.nginx-proxy.tld
Validity
Not Before: Jan 10 00:37:02 2017 GMT
Not After : May 28 00:37:02 2044 GMT
Subject: CN=web2.nginx-proxy.tld
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (2048 bit)
Modulus:
00:da:ee:46:2d:44:7c:f1:e6:91:11:bf:34:d6:02:
4e:fe:43:23:fb:6d:20:f7:8d:1b:c6:9c:cd:1c:1a:
07:95:c2:ed:b9:23:73:c1:02:2b:50:51:3f:33:cf:
8e:aa:f1:13:58:4c:ff:7f:7d:4a:87:fc:f0:0f:54:
af:8c:eb:ba:b4:0f:71:5e:12:1f:64:b1:3d:83:88:
dd:9c:09:50:2d:37:1d:03:3b:18:30:36:f4:82:94:
87:7f:31:27:28:84:0c:99:6d:77:b5:b8:4f:ca:83:
58:d5:d8:4e:36:73:1c:1a:5c:ed:05:ef:95:60:03:
28:0c:9f:d8:6f:98:a8:cd:08:be:af:b1:95:5a:60:
96:fe:2a:d0:98:74:9b:4a:c0:48:66:73:67:54:33:
11:22:20:ea:11:05:ba:a6:ed:74:12:05:d8:de:4f:
01:46:39:74:d8:34:1a:f2:2c:c2:df:6d:94:57:52:
c1:e4:2e:1b:8e:12:0e:43:e7:6f:1f:da:51:80:35:
c2:8a:9b:b6:2a:30:39:6b:a0:77:fa:37:11:b7:ec:
de:6e:8c:6f:93:81:5e:2d:90:69:1b:4b:a4:80:ca:
f4:e5:5b:c0:13:45:b9:76:70:ed:d3:4e:dd:66:98:
99:9f:9d:f0:1e:fd:dd:04:4f:9a:55:bc:38:ad:42:
b9:dd
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Subject Alternative Name:
DNS:web2.nginx-proxy.tld
Signature Algorithm: sha256WithRSAEncryption
38:d6:8c:be:3c:5e:5d:61:02:77:eb:5b:6e:a7:1d:4f:69:0d:
54:bd:dd:3a:1a:8e:9d:a0:c2:f3:a5:31:91:3e:ec:7a:69:48:
40:27:45:a5:c6:b9:af:6d:d9:76:24:97:ec:c5:cf:4d:cc:49:
93:ab:bc:4f:01:7e:7a:57:73:4d:27:62:a6:68:bf:4c:00:2c:
c0:f3:7b:b3:32:81:6b:96:20:0a:73:a0:85:b5:f8:07:10:88:
e8:62:85:41:63:df:43:c5:f9:4b:90:89:6a:16:d9:a6:85:4a:
04:1b:5e:75:d8:84:ae:69:c7:62:8f:f1:53:c8:c6:31:71:6d:
0c:05:2d:bf:6e:b8:b8:7a:8c:73:6f:17:bb:5c:5a:67:51:12:
af:e2:17:9c:40:0e:35:f6:59:93:69:45:14:74:33:c6:aa:04:
76:8e:3c:a6:ac:f4:60:cb:53:eb:d6:63:1a:47:7b:be:11:8d:
74:de:e8:e5:bc:de:1b:09:db:1b:87:89:b7:6a:20:0a:5e:fb:
35:04:ab:b2:f7:4d:a1:86:65:1d:c7:c3:aa:30:15:3a:6e:66:
f8:43:33:63:ac:fc:c1:58:48:5b:ec:a0:00:bf:d4:f1:06:03:
79:ca:d5:b6:f2:39:0b:62:b4:fd:27:27:e9:37:52:21:ce:a4:
32:8a:bb:c7
-----BEGIN CERTIFICATE-----
MIIC/TCCAeWgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwPzEfMB0GA1UECgwWbmdp
bngtcHJveHkgdGVzdCBzdWl0ZTEcMBoGA1UEAwwTd3d3Lm5naW54LXByb3h5LnRs
ZDAeFw0xNzAxMTAwMDM3MDJaFw00NDA1MjgwMDM3MDJaMB8xHTAbBgNVBAMMFHdl
YjIubmdpbngtcHJveHkudGxkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
AQEA2u5GLUR88eaREb801gJO/kMj+20g940bxpzNHBoHlcLtuSNzwQIrUFE/M8+O
qvETWEz/f31Kh/zwD1SvjOu6tA9xXhIfZLE9g4jdnAlQLTcdAzsYMDb0gpSHfzEn
KIQMmW13tbhPyoNY1dhONnMcGlztBe+VYAMoDJ/Yb5iozQi+r7GVWmCW/irQmHSb
SsBIZnNnVDMRIiDqEQW6pu10EgXY3k8BRjl02DQa8izC322UV1LB5C4bjhIOQ+dv
H9pRgDXCipu2KjA5a6B3+jcRt+zeboxvk4FeLZBpG0ukgMr05VvAE0W5dnDt007d
ZpiZn53wHv3dBE+aVbw4rUK53QIDAQABoyMwITAfBgNVHREEGDAWghR3ZWIyLm5n
aW54LXByb3h5LnRsZDANBgkqhkiG9w0BAQsFAAOCAQEAONaMvjxeXWECd+tbbqcd
T2kNVL3dOhqOnaDC86UxkT7semlIQCdFpca5r23ZdiSX7MXPTcxJk6u8TwF+eldz
TSdipmi/TAAswPN7szKBa5YgCnOghbX4BxCI6GKFQWPfQ8X5S5CJahbZpoVKBBte
ddiErmnHYo/xU8jGMXFtDAUtv264uHqMc28Xu1xaZ1ESr+IXnEAONfZZk2lFFHQz
xqoEdo48pqz0YMtT69ZjGkd7vhGNdN7o5bzeGwnbG4eJt2ogCl77NQSrsvdNoYZl
HcfDqjAVOm5m+EMzY6z8wVhIW+ygAL/U8QYDecrVtvI5C2K0/Scn6TdSIc6kMoq7
xw==
-----END CERTIFICATE-----

View File

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEA2u5GLUR88eaREb801gJO/kMj+20g940bxpzNHBoHlcLtuSNz
wQIrUFE/M8+OqvETWEz/f31Kh/zwD1SvjOu6tA9xXhIfZLE9g4jdnAlQLTcdAzsY
MDb0gpSHfzEnKIQMmW13tbhPyoNY1dhONnMcGlztBe+VYAMoDJ/Yb5iozQi+r7GV
WmCW/irQmHSbSsBIZnNnVDMRIiDqEQW6pu10EgXY3k8BRjl02DQa8izC322UV1LB
5C4bjhIOQ+dvH9pRgDXCipu2KjA5a6B3+jcRt+zeboxvk4FeLZBpG0ukgMr05VvA
E0W5dnDt007dZpiZn53wHv3dBE+aVbw4rUK53QIDAQABAoIBABeTCsl7E30017Ay
h6z3yKvGbQx43tDpR/FmFwwMnX555AFImQFSi3l1ljmtAu7TUML0X5rJ0gm8qdjs
xI6HH66d7xYzG2BLWZVdWoef1RtZUO11IpCmikO5XLHMiCvrtDOdPwO5WhYzeJBm
X12rnX4VPYyjFNGm5Vwepj62EI8rS9G3NG00pDYPmN/vUQJiV/iTRIlvXgFm4Hl2
zJhVi+NhyiptFEycdg65JwVfLKtmUXRmwGFiSxQi1FX8YS5EdIV2S8yDwXlWSxmq
4R1eSID7pKxtzyRtBqZJggzfqtY8cMpoOC12FbLAvzagOavs4ntMgAVy5k2T15G2
syQyLSECgYEA+1NIRF3CxNUaPvpcR8Y4PWhwDEzqn5ZscnXaFrUp1W72f3bpwSCa
/t9lXe9O53R5/yt4nCbwvVM0UWYPHGZGQr+5AS7WWDVWVcwkXX1NIjALi0TXQ0Ty
zeuViXDofUS31yhwFFmgGa52pd+edXaGRvxzGyPVdtwpSLZP/gBLQykCgYEA3wC9
sHNPKMxi6vI2VBvmBXHoCSDAkuRLmQEGDmjjt0Ve4fmwGRbBJxniOlNtufNQRfRg
67qaeM4BTrP03cilZ71yXvLN5G2opY5c/I0dYTXRhV3IV6XwlCC+0xmn+ro7kB8x
J4wAj/h/tJ8T0+0TpnbyjmPH4KTJ9GjRTKwe65UCgYBszIHlbr2JXkONbe6S98GS
++o9uPJ9Aa6S4mf2Gpkwl2fIiF7rR0Ux/t2wC5AZ7Ld/en8tAkKHg0SL1GXIQpI6
BSt+0prh9r0YSVaYzkyc9zWYJcYWjfuan1jN9f3/dMctMolKlf4UAA3HAwZjDVtV
0aW24w1e9jI9EweQCuqJ+QKBgBwZec18GiNn7abxMktS4J8bBUPxLpLT1XrIGD1E
lj0HrrcGwVvH9Dq7FjiHPrJJqHnIG1ZYwxIp0xxZrKctmzoBMyInsi3wa2nBEJJ6
LZOMNoR5lr8El9XyclkjSHldchfs9kKnb4K0q1LVIKh5nRpCrrmmdQ8ndJMpigYB
QjwpAoGAWIhFrN0Acdq7Xc9pScqnAohPMWCTBUbeKrOm0ZwN4Q9D+lLeeggbWlWy
AqR4WHQMHc6B+p4Ncg3XBCFw0V62PkYhSCdaLNH3CPyFT0qoeY8VuMjmqS7yoKvp
uMMHfzMmyGg0dyplVGgANafb/it6Dp8T0pnCmzxhe57jf9xsVBo=
-----END RSA PRIVATE KEY-----

View File

@ -0,0 +1,71 @@
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 4096 (0x1000)
Signature Algorithm: sha256WithRSAEncryption
Issuer: O=nginx-proxy test suite, CN=www.nginx-proxy.tld
Validity
Not Before: Jan 10 00:58:11 2017 GMT
Not After : May 28 00:58:11 2044 GMT
Subject: CN=web3.nginx-proxy.tld
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (2048 bit)
Modulus:
00:ac:9b:90:dc:2c:0d:c3:ae:f4:a0:cc:40:15:d2:
c4:c2:2c:8c:15:b6:70:28:cf:32:c4:03:ce:b3:87:
30:5d:a6:12:96:69:7a:fe:67:29:1c:8c:24:bb:6a:
c8:86:13:19:91:00:3e:ef:00:67:50:b0:ea:c9:93:
c6:8a:73:82:d8:37:9f:8e:6e:12:13:ec:fa:08:0f:
ac:73:6e:42:96:67:9f:20:c5:1c:a3:b1:4a:83:36:
0e:0a:31:93:76:b1:b6:37:4f:e0:88:3c:46:dc:c1:
53:60:df:28:ae:3e:20:d8:d9:53:a2:be:09:38:f0:
ff:4a:91:45:cb:cb:b5:b3:3d:7d:09:98:47:dc:0d:
5e:83:73:b6:c9:f3:fb:9a:f2:bb:b0:62:ca:aa:af:
6b:42:e5:08:b2:14:87:f4:fc:f1:14:f8:cc:76:b3:
c0:49:df:66:c6:21:a0:bc:5e:0c:bb:de:e9:9c:e7:
fb:31:a1:cf:c4:e9:bb:bd:c3:5a:0d:23:52:c6:b3:
84:77:f1:0c:3d:ca:c3:60:48:f9:7e:a6:dc:4f:f7:
d2:5b:7c:02:4d:38:09:64:33:7e:bb:b1:65:bb:e2:
2b:1d:9a:49:d4:34:21:42:7a:49:3e:11:6c:10:66:
b4:91:db:bd:3a:c2:8d:f4:e4:03:b1:b4:6e:5c:98:
bf:7d
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Subject Alternative Name:
DNS:web3.nginx-proxy.tld
Signature Algorithm: sha256WithRSAEncryption
9a:f6:b9:c2:08:a4:b4:d7:e4:b2:d3:22:e9:fe:69:4a:e8:76:
18:60:11:1b:3b:7c:4b:c3:72:66:95:b7:7c:de:c7:34:32:58:
aa:5d:e0:12:f0:df:27:b6:3f:dd:f1:8c:ed:ce:bd:73:50:fc:
9b:e1:8c:c2:7f:ac:6b:54:9d:23:0a:d9:a6:25:cc:99:94:73:
2b:69:e8:f7:07:40:37:d3:d4:0b:14:86:6a:a1:01:53:4b:ae:
85:2d:12:13:bd:23:1e:09:cf:20:50:24:76:a6:5f:b4:d6:d6:
e1:34:40:93:4d:42:f7:d0:95:98:77:53:16:e7:ce:cc:4c:35:
b8:30:3b:62:95:e2:40:0c:a1:73:84:92:93:63:df:c6:21:d5:
eb:1d:a1:d0:f2:ec:a5:cf:d6:10:c9:8f:ac:11:16:39:cd:b0:
38:04:29:cf:e1:1c:dd:21:df:1f:95:35:a5:a4:61:2b:3d:8b:
8a:76:02:6d:31:a1:e8:6b:c5:3b:eb:90:40:34:16:5a:07:93:
96:56:cd:8b:52:ca:65:51:78:d3:f2:af:40:44:43:ec:fe:a2:
c8:d4:6d:21:c7:1f:d2:45:28:0d:d2:51:0f:d1:a5:db:00:2a:
3a:10:88:9e:5a:76:a2:f7:e2:f0:fe:14:a3:e8:ec:ef:00:f0:
35:87:eb:7c
-----BEGIN CERTIFICATE-----
MIIC/TCCAeWgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwPzEfMB0GA1UECgwWbmdp
bngtcHJveHkgdGVzdCBzdWl0ZTEcMBoGA1UEAwwTd3d3Lm5naW54LXByb3h5LnRs
ZDAeFw0xNzAxMTAwMDU4MTFaFw00NDA1MjgwMDU4MTFaMB8xHTAbBgNVBAMMFHdl
YjMubmdpbngtcHJveHkudGxkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
AQEArJuQ3CwNw670oMxAFdLEwiyMFbZwKM8yxAPOs4cwXaYSlml6/mcpHIwku2rI
hhMZkQA+7wBnULDqyZPGinOC2Defjm4SE+z6CA+sc25ClmefIMUco7FKgzYOCjGT
drG2N0/giDxG3MFTYN8orj4g2NlTor4JOPD/SpFFy8u1sz19CZhH3A1eg3O2yfP7
mvK7sGLKqq9rQuUIshSH9PzxFPjMdrPASd9mxiGgvF4Mu97pnOf7MaHPxOm7vcNa
DSNSxrOEd/EMPcrDYEj5fqbcT/fSW3wCTTgJZDN+u7Flu+IrHZpJ1DQhQnpJPhFs
EGa0kdu9OsKN9OQDsbRuXJi/fQIDAQABoyMwITAfBgNVHREEGDAWghR3ZWIzLm5n
aW54LXByb3h5LnRsZDANBgkqhkiG9w0BAQsFAAOCAQEAmva5wgiktNfkstMi6f5p
Suh2GGARGzt8S8NyZpW3fN7HNDJYql3gEvDfJ7Y/3fGM7c69c1D8m+GMwn+sa1Sd
IwrZpiXMmZRzK2no9wdAN9PUCxSGaqEBU0uuhS0SE70jHgnPIFAkdqZftNbW4TRA
k01C99CVmHdTFufOzEw1uDA7YpXiQAyhc4SSk2PfxiHV6x2h0PLspc/WEMmPrBEW
Oc2wOAQpz+Ec3SHfH5U1paRhKz2LinYCbTGh6GvFO+uQQDQWWgeTllbNi1LKZVF4
0/KvQERD7P6iyNRtIccf0kUoDdJRD9Gl2wAqOhCInlp2ovfi8P4Uo+js7wDwNYfr
fA==
-----END CERTIFICATE-----

View File

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEArJuQ3CwNw670oMxAFdLEwiyMFbZwKM8yxAPOs4cwXaYSlml6
/mcpHIwku2rIhhMZkQA+7wBnULDqyZPGinOC2Defjm4SE+z6CA+sc25ClmefIMUc
o7FKgzYOCjGTdrG2N0/giDxG3MFTYN8orj4g2NlTor4JOPD/SpFFy8u1sz19CZhH
3A1eg3O2yfP7mvK7sGLKqq9rQuUIshSH9PzxFPjMdrPASd9mxiGgvF4Mu97pnOf7
MaHPxOm7vcNaDSNSxrOEd/EMPcrDYEj5fqbcT/fSW3wCTTgJZDN+u7Flu+IrHZpJ
1DQhQnpJPhFsEGa0kdu9OsKN9OQDsbRuXJi/fQIDAQABAoIBAA14DjPAFEriyiAK
EC4jxkrIox3GoLXuhS2ahnSn5fRI00Z9cKWNcz3RCcS+LmuX7fTMqhyIUYeQZqHY
MDP5k4o/vOmmWS7I3THn1zMitXt7FoW+G+ACI6hdfXb6K2GluGxUhVbcLUNoqpLy
lwARxQpm2wnl/l49IA63i1S9zq3vy5HSvxBv3jq8xp2PX3Sho33LhsvW+miCJe+R
etKSV4mAjvR62XVgUGJ40FciVMK3pzwwIKPLI7k9sa7WHZr6fNHeDxIWA6AsVBTH
O+2l8Ufd79KesOD6VqdHlxg2a79s3NoCOflQQAbOSSR9ioCE7Ykgvw0wVl57xNoB
WZVAY0ECgYEA2I6+a56NMzkEMxr0w9ZRqgocXCgbqxZx1v7newDyO5J3X4jYhmMJ
abNZtnVs1Pz0IAgCMCf+N4D+RAi9/ahYxq7jmhIkT/IUHseh93XLgd/x9Q9ZJOGm
9+NI3aIBgWOsy4orKxfwzRAVEakOCChYUCy/GUDDD1MPcjxC8ma5abUCgYEAzAua
15Nayr9Sg0QsHqNvgTVkVlA6u+TwN+vfI8cH5nmXIMm5ShwCc9+Pm8mpcFwUo4uE
vOzQ+NwG9CBbVu6/i1/aR+ZlbctdhpsW51v18B9eFVXSZUvEv1ONGdoJZhq0tW7W
V4Zjf+UwdTcrSZKVpd5woUbRkByROPdir/3Ie6kCgYBhf5LX3SBxSWBMqfw9F4bY
6YhvLVeXpZlHVKhfRsPIcl7wUio6Bui8ABWKAkAnfGNk8HYbvEXGM3tGojD3vQ2L
Fj4+paBXpgPM/9A6G3yuUmcbD/fwlO+Zd2jc8A2BdaDcWq6ozjSJ/o2dz+ETZyar
ohm/gtrPUXQI2HzDqeAcaQKBgEBMd+LvAHFbkPjkhrKw9fZViOTaK2gCYOB+Z7ay
hX7PWhxu9QCxiuRQ0sRY7BgILEjNMmsGhWOmklpjx+TBH4MgFX0K0XOj3jkIrlMB
26JrgA5hGQfqtHlGLvSyjLusNr3ly42RP9GRu490byOkGZxHWF66Hle3aNv2uRaU
dpThAoGBAMIPpf4E6xGzurhgYdXMit3jGAYD85BbNUIm9jOym2lxS63ipoF08QhH
NoMVRf/AUoS4VDGXsuABjMffTZbE8L+DaL1cWSuPJAoF9AXUXtz6A8LRc9Mn2WjS
L8BIs9xZCzrJ5XzY4PSnjjyAU81z6E80azWglkmh8rRDzi/o9O79
-----END RSA PRIVATE KEY-----

View File

@ -0,0 +1,18 @@
import pytest
def test_web2_http_is_not_forwarded(docker_compose, nginxproxy):
r = nginxproxy.get("http://web2.nginx-proxy.tld/", allow_redirects=False)
assert r.status_code == 503
def test_web2_https_is_forwarded(docker_compose, nginxproxy):
r = nginxproxy.get("https://web2.nginx-proxy.tld/port", allow_redirects=False)
assert r.status_code == 200
assert "answer from port 82\n" in r.text
def test_web2_HSTS_policy_is_active(docker_compose, nginxproxy):
r = nginxproxy.get("https://web2.nginx-proxy.tld/port", allow_redirects=False)
assert "answer from port 82\n" in r.text
assert "Strict-Transport-Security" in r.headers

View File

@ -0,0 +1,15 @@
web2:
image: web
expose:
- "82"
environment:
WEB_PORTS: "82"
VIRTUAL_HOST: "web2.nginx-proxy.tld"
HTTPS_METHOD: nohttp
sut:
image: jwilder/nginx-proxy:test
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
- ./certs:/etc/nginx/certs:ro

View File

@ -0,0 +1,12 @@
import pytest
from requests import ConnectionError
def test_http_is_forwarded(docker_compose, nginxproxy):
r = nginxproxy.get("http://web.nginx-proxy.tld/port", allow_redirects=False)
assert r.status_code == 200
assert "answer from port 83\n" in r.text
def test_https_is_disabled(docker_compose, nginxproxy):
with pytest.raises(ConnectionError):
nginxproxy.get("https://web.nginx-proxy.tld/", allow_redirects=False)

View File

@ -0,0 +1,14 @@
web:
image: web
expose:
- "83"
environment:
WEB_PORTS: "83"
VIRTUAL_HOST: "web.nginx-proxy.tld"
HTTPS_METHOD: nohttps
sut:
image: jwilder/nginx-proxy:test
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro

View File

@ -0,0 +1,19 @@
import pytest
def test_web3_http_is_forwarded(docker_compose, nginxproxy):
r = nginxproxy.get("http://web3.nginx-proxy.tld/port", allow_redirects=False)
assert r.status_code == 200
assert "answer from port 83\n" in r.text
def test_web3_https_is_forwarded(docker_compose, nginxproxy):
r = nginxproxy.get("https://web3.nginx-proxy.tld/port", allow_redirects=False)
assert r.status_code == 200
assert "answer from port 83\n" in r.text
def test_web2_HSTS_policy_is_inactive(docker_compose, nginxproxy):
r = nginxproxy.get("https://web3.nginx-proxy.tld/port", allow_redirects=False)
assert "answer from port 83\n" in r.text
assert "Strict-Transport-Security" not in r.headers

View File

@ -0,0 +1,15 @@
web3:
image: web
expose:
- "83"
environment:
WEB_PORTS: "83"
VIRTUAL_HOST: "web3.nginx-proxy.tld"
HTTPS_METHOD: noredirect
sut:
image: jwilder/nginx-proxy:test
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
- ./certs:/etc/nginx/certs:ro

View File

@ -0,0 +1,23 @@
import pytest
@pytest.mark.parametrize("subdomain", ["foo", "bar"])
def test_web1_http_redirects_to_https(docker_compose, nginxproxy, subdomain):
r = nginxproxy.get("http://%s.nginx-proxy.tld/" % subdomain, allow_redirects=False)
assert r.status_code == 301
assert "Location" in r.headers
assert "https://%s.nginx-proxy.tld/" % subdomain == r.headers['Location']
@pytest.mark.parametrize("subdomain", ["foo", "bar"])
def test_web1_https_is_forwarded(docker_compose, nginxproxy, subdomain):
r = nginxproxy.get("https://%s.nginx-proxy.tld/port" % subdomain, allow_redirects=False)
assert r.status_code == 200
assert "answer from port 81\n" in r.text
@pytest.mark.parametrize("subdomain", ["foo", "bar"])
def test_web1_HSTS_policy_is_active(docker_compose, nginxproxy, subdomain):
r = nginxproxy.get("https://%s.nginx-proxy.tld/port" % subdomain, allow_redirects=False)
assert "answer from port 81\n" in r.text
assert "Strict-Transport-Security" in r.headers

View File

@ -0,0 +1,13 @@
web1:
image: web
expose:
- "81"
environment:
WEB_PORTS: "81"
VIRTUAL_HOST: "*.nginx-proxy.tld"
sut:
image: jwilder/nginx-proxy:test
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
- ./certs:/etc/nginx/certs:ro

View File

@ -0,0 +1,32 @@
import pytest
@pytest.mark.parametrize("host,expected_port", [
("f00.nginx-proxy.test", 81),
("bar.nginx-proxy.test", 81),
("test.nginx-proxy.f00", 82),
("test.nginx-proxy.bar", 82),
("web3.123.nginx-proxy.regexp", 83),
("web3.ABC.nginx-proxy.regexp", 83),
("web3.123.ABC.nginx-proxy.regexp", 83),
("web3.123-ABC.nginx-proxy.regexp", 83),
("web3.whatever.nginx-proxy.regexp-to-infinity-and-beyond", 83),
("web4.123.nginx-proxy.regexp", 84),
("web4.ABC.nginx-proxy.regexp", 84),
("web4.123.ABC.nginx-proxy.regexp", 84),
("web4.123-ABC.nginx-proxy.regexp", 84),
("web4.whatever.nginx-proxy.regexp", 84),
])
def test_wildcard_prefix(docker_compose, nginxproxy, host, expected_port):
r = nginxproxy.get("http://%s/port" % host)
assert r.status_code == 200
assert r.text == "answer from port %s\n" % expected_port
@pytest.mark.parametrize("host", [
"unexpected.nginx-proxy.tld",
"web4.whatever.nginx-proxy.regexp-to-infinity-and-beyond"
])
def test_non_matching_host_is_503(docker_compose, nginxproxy, host):
r = nginxproxy.get("http://%s/port" % host)
assert r.status_code == 503, r.text

View File

@ -0,0 +1,37 @@
web1:
image: web
expose:
- "81"
environment:
WEB_PORTS: "81"
VIRTUAL_HOST: "*.nginx-proxy.test"
web2:
image: web
expose:
- "82"
environment:
WEB_PORTS: "82"
VIRTUAL_HOST: "test.nginx-proxy.*"
web3:
image: web
expose:
- "83"
environment:
WEB_PORTS: "83"
VIRTUAL_HOST: ~^web3\..*\.nginx-proxy\.regexp
web4:
image: web
expose:
- "84"
environment:
WEB_PORTS: "84"
VIRTUAL_HOST: ~^web4\..*\.nginx-proxy\.regexp$$ # we need to double the `$` because of docker-compose variable interpolation
sut:
image: jwilder/nginx-proxy:test
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro

View File

@ -1,93 +0,0 @@
#!/usr/bin/env bats
load test_helpers
SUT_CONTAINER=bats-nginx-proxy-${TEST_FILE}
function setup {
# make sure to stop any web container before each test so we don't
# have any unexpected contaiener running with VIRTUAL_HOST or VIRUTAL_PORT set
stop_bats_containers web
}
@test "[$TEST_FILE] start a nginx-proxy container" {
# GIVEN
run nginxproxy $SUT_CONTAINER -v /var/run/docker.sock:/tmp/docker.sock:ro
assert_success
docker_wait_for_log $SUT_CONTAINER 9 "Watching docker events"
}
@test "[$TEST_FILE] VIRTUAL_HOST=*.wildcard.bats" {
# WHEN
prepare_web_container bats-wildcard-hosts-1 80 -e VIRTUAL_HOST=*.wildcard.bats
dockergen_wait_for_event $SUT_CONTAINER start bats-wildcard-hosts-1
sleep 1
# THEN
assert_200 f00.wildcard.bats
assert_200 bar.wildcard.bats
assert_503 unexpected.host.bats
}
@test "[$TEST_FILE] VIRTUAL_HOST=wildcard.bats.*" {
# WHEN
prepare_web_container bats-wildcard-hosts-2 80 -e VIRTUAL_HOST=wildcard.bats.*
dockergen_wait_for_event $SUT_CONTAINER start bats-wildcard-hosts-2
sleep 1
# THEN
assert_200 wildcard.bats.f00
assert_200 wildcard.bats.bar
assert_503 unexpected.host.bats
}
@test "[$TEST_FILE] VIRTUAL_HOST=~^foo\.bar\..*\.bats" {
# WHEN
prepare_web_container bats-wildcard-hosts-3 80 -e VIRTUAL_HOST=~^foo\.bar\..*\.bats
dockergen_wait_for_event $SUT_CONTAINER start bats-wildcard-hosts-3
sleep 1
# THEN
assert_200 foo.bar.whatever.bats
assert_200 foo.bar.why.not.bats
assert_200 foo.bar.why.not.bats-to-infinity-and-beyond
assert_503 unexpected.host.bats
}
@test "[$TEST_FILE] VIRTUAL_HOST=~^foo\.bar\..*\.bats$" {
# WHEN
prepare_web_container bats-wildcard-hosts-4 80 -e VIRTUAL_HOST=~^foo\.bar\..*\.bats$
dockergen_wait_for_event $SUT_CONTAINER start bats-wildcard-hosts-4
sleep 1
# THEN
assert_200 foo.bar.whatever.bats
assert_200 foo.bar.why.not.bats
assert_503 foo.bar.why.not.bats-to-infinity-and-beyond
assert_503 unexpected.host.bats
}
@test "[$TEST_FILE] stop all bats containers" {
stop_bats_containers
}
# assert that querying nginx-proxy with the given Host header produces a `HTTP 200` response
# $1 Host HTTP header to use when querying nginx-proxy
function assert_200 {
local -r host=$1
run curl_container $SUT_CONTAINER / --head --header "Host: $host"
assert_output -l 0 $'HTTP/1.1 200 OK\r'
}
# assert that querying nginx-proxy with the given Host header produces a `HTTP 503` response
# $1 Host HTTP header to use when querying nginx-proxy
function assert_503 {
local -r host=$1
run curl_container $SUT_CONTAINER / --head --header "Host: $host"
assert_output -l 0 $'HTTP/1.1 503 Service Temporarily Unavailable\r'
}