diff --git a/test/conftest.py b/test/conftest.py index fcff6ea..43f83bb 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -445,6 +445,18 @@ def pytest_runtest_logreport(report): report.longrepr.addsection('nginx-proxy conf', get_nginx_conf_from_container(container)) +# Py.test `incremental` marker, see http://stackoverflow.com/a/12579625/107049 +def pytest_runtest_makereport(item, call): + if "incremental" in item.keywords: + if call.excinfo is not None: + parent = item.parent + parent._previousfailed = item + + +def pytest_runtest_setup(item): + previousfailed = getattr(item.parent, "_previousfailed", None) + if previousfailed is not None: + pytest.xfail("previous test failed (%s)" % previousfailed.name) ############################################################################### # diff --git a/test/stress_tests/README.md b/test/stress_tests/README.md new file mode 100644 index 0000000..ca20cc1 --- /dev/null +++ b/test/stress_tests/README.md @@ -0,0 +1 @@ +This directory contains tests that showcase scenarios known to break the expected behavior of nginx-proxy. \ No newline at end of file diff --git a/test/stress_tests/test_deleted_cert/README.md b/test/stress_tests/test_deleted_cert/README.md new file mode 100644 index 0000000..9fac0b9 --- /dev/null +++ b/test/stress_tests/test_deleted_cert/README.md @@ -0,0 +1,5 @@ +Test the behavior of nginx-proxy when restarted after deleting a certificate file is was using. + +1. nginx-proxy is created with a virtual host having a certificate +1. while nginx-proxy is running, the certificate file is deleted +1. nginx-proxy is then restarted (without removing the container) diff --git a/test/stress_tests/test_deleted_cert/certs/web.nginx-proxy.crt b/test/stress_tests/test_deleted_cert/certs/web.nginx-proxy.crt new file mode 100644 index 0000000..2c92efe --- /dev/null +++ b/test/stress_tests/test_deleted_cert/certs/web.nginx-proxy.crt @@ -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: Feb 17 23:20:54 2017 GMT + Not After : Jul 5 23:20:54 2044 GMT + Subject: CN=web.nginx-proxy + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + Public-Key: (2048 bit) + Modulus: + 00:b6:27:63:a5:c6:e8:f4:7a:94:0e:cc:a2:62:76: + 6d:5d:33:6f:cf:19:fc:e7:e5:bb:0e:0e:d0:7c:4f: + 73:4c:48:2b:17:d1:4d:d5:9f:42:08:73:84:54:8c: + 86:d2:c5:da:59:01:3f:42:22:e0:36:f0:dc:ab:de: + 0a:bd:26:2b:22:13:87:a6:1f:23:ef:0e:99:27:8b: + 15:4a:1b:ef:93:c9:6b:91:de:a0:02:0c:62:bb:cc: + 56:37:e8:25:92:c3:1f:f1:69:d8:7c:a8:33:e0:89: + ce:14:67:a0:39:77:88:91:e6:a3:07:97:90:22:88: + d0:79:18:63:fb:6f:7e:ee:2b:42:7e:23:f5:e7:da: + e9:ee:6a:fa:96:65:9f:e1:2b:15:49:c8:cd:2d:ce: + 86:4f:2c:2a:67:79:bf:41:30:14:cc:f6:0f:14:74: + 9e:b6:d3:d0:3b:f0:1b:b8:e8:19:2a:fd:d6:fd:dc: + 4b:4e:65:7d:9b:bf:37:7e:2d:35:22:2e:74:90:ce: + 41:35:3d:41:a0:99:db:97:1f:bf:3e:18:3c:48:fb: + da:df:c6:4e:4e:b9:67:b8:10:d5:a5:13:03:c4:b7: + 65:e7:aa:f0:14:4b:d3:4d:ea:fe:8f:69:cf:50:21: + 63:27:cf:9e:4c:67:15:7b:3f:3b:da:cb:17:80:61: + 1e:25 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Subject Alternative Name: + DNS:web.nginx-proxy + Signature Algorithm: sha256WithRSAEncryption + 09:31:be:db:4e:b0:b6:68:da:ae:5b:16:51:29:fc:9f:61:b6: + 5a:2f:3c:35:ef:67:76:97:b0:34:4e:3b:b4:d6:88:19:4f:84: + 2e:73:d3:c0:3a:4c:41:54:6c:bb:67:89:67:ad:25:55:d7:d4: + 80:fe:a7:3f:3d:9e:f1:34:96:d8:da:5a:78:51:c0:63:f1:52: + 29:35:55:f4:7d:70:1c:d3:96:62:7f:64:86:81:52:27:c4:c6: + 10:13:c6:73:56:4d:32:d0:b3:c3:c8:2c:25:83:e4:2b:1d:d4: + 74:30:e5:85:af:2d:b6:a5:6b:fe:5d:d3:3c:00:58:94:f4:6a: + f5:a6:1d:cf:f9:ed:d5:27:ed:13:24:b2:4f:2b:f3:b8:e4:af: + 0c:1d:fe:e0:6a:01:5e:a2:44:ff:3e:96:fa:6c:39:a3:51:37: + f3:72:55:d8:2d:29:6e:de:95:b9:d8:e3:1e:65:a5:9c:0d:79: + 2d:39:ab:c7:ac:16:b6:a5:71:4b:35:a4:6c:72:47:1b:72:9c: + 67:58:c1:fc:f6:7f:a7:73:50:7b:d6:27:57:74:a1:31:38:a7: + 31:e3:b9:d4:c9:45:33:ec:ed:16:cf:c5:bd:d0:03:b1:45:3f: + 68:0d:91:5c:26:4e:37:05:74:ed:3e:75:5e:ca:5e:ee:e2:51: + 4b:da:08:99 +-----BEGIN CERTIFICATE----- +MIIC8zCCAdugAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwPzEfMB0GA1UECgwWbmdp +bngtcHJveHkgdGVzdCBzdWl0ZTEcMBoGA1UEAwwTd3d3Lm5naW54LXByb3h5LnRs +ZDAeFw0xNzAyMTcyMzIwNTRaFw00NDA3MDUyMzIwNTRaMBoxGDAWBgNVBAMMD3dl +Yi5uZ2lueC1wcm94eTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALYn +Y6XG6PR6lA7MomJ2bV0zb88Z/Ofluw4O0HxPc0xIKxfRTdWfQghzhFSMhtLF2lkB +P0Ii4Dbw3KveCr0mKyITh6YfI+8OmSeLFUob75PJa5HeoAIMYrvMVjfoJZLDH/Fp +2HyoM+CJzhRnoDl3iJHmoweXkCKI0HkYY/tvfu4rQn4j9efa6e5q+pZln+ErFUnI +zS3Ohk8sKmd5v0EwFMz2DxR0nrbT0DvwG7joGSr91v3cS05lfZu/N34tNSIudJDO +QTU9QaCZ25cfvz4YPEj72t/GTk65Z7gQ1aUTA8S3Zeeq8BRL003q/o9pz1AhYyfP +nkxnFXs/O9rLF4BhHiUCAwEAAaMeMBwwGgYDVR0RBBMwEYIPd2ViLm5naW54LXBy +b3h5MA0GCSqGSIb3DQEBCwUAA4IBAQAJMb7bTrC2aNquWxZRKfyfYbZaLzw172d2 +l7A0Tju01ogZT4Quc9PAOkxBVGy7Z4lnrSVV19SA/qc/PZ7xNJbY2lp4UcBj8VIp +NVX0fXAc05Zif2SGgVInxMYQE8ZzVk0y0LPDyCwlg+QrHdR0MOWFry22pWv+XdM8 +AFiU9Gr1ph3P+e3VJ+0TJLJPK/O45K8MHf7gagFeokT/Ppb6bDmjUTfzclXYLSlu +3pW52OMeZaWcDXktOavHrBa2pXFLNaRsckcbcpxnWMH89n+nc1B71idXdKExOKcx +47nUyUUz7O0Wz8W90AOxRT9oDZFcJk43BXTtPnVeyl7u4lFL2giZ +-----END CERTIFICATE----- diff --git a/test/stress_tests/test_deleted_cert/certs/web.nginx-proxy.key b/test/stress_tests/test_deleted_cert/certs/web.nginx-proxy.key new file mode 100644 index 0000000..dca1c99 --- /dev/null +++ b/test/stress_tests/test_deleted_cert/certs/web.nginx-proxy.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAtidjpcbo9HqUDsyiYnZtXTNvzxn85+W7Dg7QfE9zTEgrF9FN +1Z9CCHOEVIyG0sXaWQE/QiLgNvDcq94KvSYrIhOHph8j7w6ZJ4sVShvvk8lrkd6g +Agxiu8xWN+glksMf8WnYfKgz4InOFGegOXeIkeajB5eQIojQeRhj+29+7itCfiP1 +59rp7mr6lmWf4SsVScjNLc6GTywqZ3m/QTAUzPYPFHSettPQO/AbuOgZKv3W/dxL +TmV9m783fi01Ii50kM5BNT1BoJnblx+/Phg8SPva38ZOTrlnuBDVpRMDxLdl56rw +FEvTTer+j2nPUCFjJ8+eTGcVez872ssXgGEeJQIDAQABAoIBAGQCMFW+ZfyEqHGP +rMA+oUEAkqy0agSwPwky3QjDXlxNa0uCYSeebtTRB6CcHxHuCzm+04puN4gyqhW6 +rU64fAoTivCMPGBuNWxekmvD9r+/YM4P2u4E+th9EgFT9f0kII+dO30FpKXtQzY0 +xuWGWXcxl+T9M+eiEkPKPmq4BoqgTDo5ty7qDv0ZqksGotKFmdYbtSvgBAueJdwu +VWJvenI9F42ExBRKOW1aldiRiaYBCLiCVPKJtOg9iuOP9RHUL1SE8xy5I5mm78g3 +a13ji3BNq3yS+VhGjQ7zDy1V1jGupLoJw4I7OThu8hy+B8Vt8EN/iqakufOkjlTN +xTJ33CkCgYEA5Iymg0NTjWk6aEkFa9pERjfUWqdVp9sWSpFFZZgi55n7LOx6ohi3 +vuLim3is/gYfK2kU/kHGZZLPnT0Rdx0MbOB4XK0CAUlqtUd0IyO4jMZ06g4/kn3N +e2jLdCCIBoEQuLk4ELxj2mHsLQhEvDrg7nzU2WpTHHhvJbIbDWOAxhsCgYEAzAgv +rKpanF+QDf4yeKHxAj2rrwRksTw4Pe7ZK/bog/i+HIVDA70vMapqftHbual/IRrB +JL7hxskoJ/h9c1w4xkWDjqkSKz8/Ihr4dyPfWyGINWbx/rarT/m5MU5SarScoK7o +Xgb25x+W+61rtI+2JhVRGO86+JiAeT4LkAX88L8CgYAwHHug/jdEeXZWJakCfzwI +HBCT1M3vO+uBXvtg25ndb0i0uENIhDOJ93EEkW65Osis9r34mBgPocwaqZRXosHO +2aH8wF6/rpjL+HK2QvrCh7Rs4Pr494qeA/1wQLjhxaGjgToQK9hJTHvPLwJpLWvU +SGr2Ka+9Oo0LPmb7dorRKQKBgQCLsNcjOodLJMp2KiHYIdfmlt6itzlRd09yZ8Nc +rHHJWVagJEUbnD1hnbHIHlp3pSqbObwfMmlWNoc9xo3tm6hrZ1CJLgx4e5b3/Ms8 +ltznge/F0DPDFsH3wZwfu+YFlJ7gDKCfL9l/qEsxCS0CtJobPOEHV1NivNbJK8ey +1ca19QKBgDTdMOUsobAmDEkPQIpxfK1iqYAB7hpRLi79OOhLp23NKeyRNu8FH9fo +G3DZ4xUi6hP2bwiYugMXDyLKfvxbsXwQC84kGF8j+bGazKNhHqEC1OpYwmaTB3kg +qL9cHbjWySeRdIsRY/eWmiKjUwmiO54eAe1HWUdcsuz8yM3xf636 +-----END RSA PRIVATE KEY----- diff --git a/test/stress_tests/test_deleted_cert/docker-compose.yml b/test/stress_tests/test_deleted_cert/docker-compose.yml new file mode 100644 index 0000000..06a61b9 --- /dev/null +++ b/test/stress_tests/test_deleted_cert/docker-compose.yml @@ -0,0 +1,17 @@ +web: + image: web + expose: + - "81" + environment: + WEB_PORTS: 81 + VIRTUAL_HOST: web.nginx-proxy + + +reverseproxy: + image: jwilder/nginx-proxy:test + container_name: reverseproxy + environment: + DEBUG: "true" + volumes: + - /var/run/docker.sock:/tmp/docker.sock:ro + - ./tmp_certs:/etc/nginx/certs:ro \ No newline at end of file diff --git a/test/stress_tests/test_deleted_cert/test_restart_while_missing_cert.py b/test/stress_tests/test_deleted_cert/test_restart_while_missing_cert.py new file mode 100644 index 0000000..2b74acd --- /dev/null +++ b/test/stress_tests/test_deleted_cert/test_restart_while_missing_cert.py @@ -0,0 +1,73 @@ +import logging +import os +from os.path import join, isfile +from shutil import copy +from time import sleep + +import pytest +from requests import ConnectionError + +script_dir = os.path.dirname(__file__) + +pytestmark = pytest.mark.xfail() # TODO delete this marker once those issues are fixed + + +@pytest.yield_fixture(scope="module", autouse=True) +def certs(): + """ + pytest fixture that provides cert and key files into the tmp_certs directory + """ + file_names = ("web.nginx-proxy.crt", "web.nginx-proxy.key") + logging.info("copying server cert and key files into tmp_certs") + for f_name in file_names: + copy(join(script_dir, "certs", f_name), join(script_dir, "tmp_certs")) + yield + logging.info("cleaning up the tmp_cert directory") + for f_name in file_names: + if isfile(join(script_dir, "tmp_certs", f_name)): + os.remove(join(script_dir, "tmp_certs", f_name)) + +############################################################################### + + +def test_unknown_virtual_host_is_503(docker_compose, nginxproxy): + r = nginxproxy.get("http://foo.nginx-proxy/") + assert r.status_code == 503 + + +def test_http_web_is_301(docker_compose, nginxproxy): + r = nginxproxy.get("http://web.nginx-proxy/port", allow_redirects=False) + assert r.status_code == 301 + + +def test_https_web_is_200(docker_compose, nginxproxy): + r = nginxproxy.get("https://web.nginx-proxy/port") + assert r.status_code == 200 + assert 'answer from port 81\n' in r.text + + +@pytest.mark.incremental +def test_delete_cert_and_restart_reverseproxy(docker_compose): + os.remove(join(script_dir, "tmp_certs", "web.nginx-proxy.crt")) + docker_compose.containers.get("reverseproxy").restart() + sleep(3) # give time for the container to initialize + assert "running" == docker_compose.containers.get("reverseproxy").status + + +@pytest.mark.incremental +def test_unknown_virtual_host_is_still_503(nginxproxy): + r = nginxproxy.get("http://foo.nginx-proxy/") + assert r.status_code == 503 + + +@pytest.mark.incremental +def test_http_web_is_now_200(nginxproxy): + r = nginxproxy.get("http://web.nginx-proxy/port", allow_redirects=False) + assert r.status_code == 200 + assert "answer from port 81\n" == r.text + + +@pytest.mark.incremental +def test_https_web_is_now_broken_since_there_is_no_cert(nginxproxy): + with pytest.raises(ConnectionError): + nginxproxy.get("https://web.nginx-proxy/port") diff --git a/test/stress_tests/test_deleted_cert/tmp_certs/.gitignore b/test/stress_tests/test_deleted_cert/tmp_certs/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/test/stress_tests/test_deleted_cert/tmp_certs/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/test/stress_tests/test_unreachable_network/README.md b/test/stress_tests/test_unreachable_network/README.md new file mode 100644 index 0000000..aa09c4d --- /dev/null +++ b/test/stress_tests/test_unreachable_network/README.md @@ -0,0 +1,59 @@ +# nginx-proxy template is not considered when a container is not reachable + +Having a container with the `VIRTUAL_HOST` environment variable set but on a network not reachable from the nginx-proxy container will result in nginx-proxy serving the default nginx welcome page for all requests. + +Furthermore, if the nginx-proxy in such state is restarted, the nginx process will crash and the container stops. + +In the generated nginx config file, we can notice the presence of an empty `upstream {}` block. + +This can be fixed by merging [PR-585](https://github.com/jwilder/nginx-proxy/pull/585). + +## How to reproduce + +1. a first web container is created on network `netA` +1. a second web container is created on network `netB` +1. nginx-proxy is created with access to `netA` only + + +## Erratic behavior + +- nginx serves the default welcome page for all requests to `/` and error 404 for any other path +- nginx-container crash on restart + +Log shows: + +``` +webB_1 | starting a web server listening on port 82 +webA_1 | starting a web server listening on port 81 +reverseproxy | forego | starting dockergen.1 on port 5000 +reverseproxy | forego | starting nginx.1 on port 5100 +reverseproxy | dockergen.1 | 2017/02/20 01:10:24 Generated '/etc/nginx/conf.d/default.conf' from 3 containers +reverseproxy | dockergen.1 | 2017/02/20 01:10:24 Running 'nginx -s reload' +reverseproxy | dockergen.1 | 2017/02/20 01:10:24 Error running notify command: nginx -s reload, exit status 1 +reverseproxy | dockergen.1 | 2017/02/20 01:10:24 Watching docker events +reverseproxy | dockergen.1 | 2017/02/20 01:10:24 Contents of /etc/nginx/conf.d/default.conf did not change. Skipping notification 'nginx -s reload' +reverseproxy | reverseproxy | forego | starting dockergen.1 on port 5000 <---- nginx-proxy container restarted +reverseproxy | forego | starting nginx.1 on port 5100 +reverseproxy | dockergen.1 | 2017/02/20 01:10:24 Generated '/etc/nginx/conf.d/default.conf' from 3 containers +reverseproxy | dockergen.1 | 2017/02/20 01:10:24 Running 'nginx -s reload' +reverseproxy | dockergen.1 | 2017/02/20 01:10:24 Error running notify command: nginx -s reload, exit status 1 +reverseproxy | dockergen.1 | 2017/02/20 01:10:24 Watching docker events +reverseproxy | dockergen.1 | 2017/02/20 01:10:24 Contents of /etc/nginx/conf.d/default.conf did not change. Skipping notification 'nginx -s reload' +reverseproxy | forego | starting dockergen.1 on port 5000 +reverseproxy | forego | starting nginx.1 on port 5100 +reverseproxy | nginx.1 | 2017/02/20 01:11:02 [emerg] 17#17: no servers are inside upstream in /etc/nginx/conf.d/default.conf:64 +reverseproxy | forego | starting nginx.1 on port 5200 +reverseproxy | forego | sending SIGTERM to nginx.1 +reverseproxy | forego | sending SIGTERM to dockergen.1 +reverseproxy exited with code 0 +reverseproxy exited with code 0 + +``` + +## Expected behavior + +- no default nginx welcome page should be served +- nginx is able to forward requests to containers of `netA` +- nginx respond with error 503 for unknown virtual hosts +- nginx is not able to forward requests to containers of `netB` and responds with an error +- nginx should survive restarts diff --git a/test/stress_tests/test_unreachable_network/docker-compose.yml b/test/stress_tests/test_unreachable_network/docker-compose.yml new file mode 100644 index 0000000..0ca4f99 --- /dev/null +++ b/test/stress_tests/test_unreachable_network/docker-compose.yml @@ -0,0 +1,35 @@ +version: "2" + +networks: + netA: + netB: + +services: + reverseproxy: + container_name: reverseproxy + networks: + - netA + image: jwilder/nginx-proxy:test + volumes: + - /var/run/docker.sock:/tmp/docker.sock:ro + + webA: + networks: + - netA + image: web + expose: + - 81 + environment: + WEB_PORTS: 81 + VIRTUAL_HOST: webA.nginx-proxy + + webB: + networks: + - netB + image: web + expose: + - 82 + environment: + WEB_PORTS: 82 + VIRTUAL_HOST: webB.nginx-proxy + diff --git a/test/stress_tests/test_unreachable_network/test_unreachable_net.py b/test/stress_tests/test_unreachable_network/test_unreachable_net.py new file mode 100644 index 0000000..dbcfb14 --- /dev/null +++ b/test/stress_tests/test_unreachable_network/test_unreachable_net.py @@ -0,0 +1,35 @@ +from time import sleep + +import pytest +import requests + +pytestmark = pytest.mark.xfail() # TODO delete this marker once #585 is merged + + +def test_default_nginx_welcome_page_should_not_be_served(docker_compose, nginxproxy): + r = nginxproxy.get("http://whatever.nginx-proxy/", allow_redirects=False) + assert "Welcome to nginx!" not in r.text + + +def test_unknown_virtual_host_is_503(docker_compose, nginxproxy): + r = nginxproxy.get("http://unknown.nginx-proxy/", allow_redirects=False) + assert r.status_code == 503 + + +def test_http_web_a_is_forwarded(docker_compose, nginxproxy): + r = nginxproxy.get("http://webA.nginx-proxy/port", allow_redirects=False) + assert r.status_code == 200 + assert "answer from port 81\n" == r.text + + +def test_http_web_b_gets_an_error(docker_compose, nginxproxy): + r = nginxproxy.get("http://webB.nginx-proxy/", allow_redirects=False) + assert "Welcome to nginx!" not in r.text + with pytest.raises(requests.exceptions.HTTPError): + r.raise_for_status() + + +def test_reverseproxy_survive_restart(docker_compose): + docker_compose.containers.get("reverseproxy").restart() + sleep(2) # give time for the container to initialize + assert docker_compose.containers.get("reverseproxy").status == "running"