1
0
mirror of https://github.com/thib8956/nginx-proxy synced 2024-11-25 05:16:30 +00:00

TESTS: add tests for IPv6

This commit is contained in:
Thomas LEVEIL 2017-02-14 01:39:08 +01:00
parent e6b9d2f5e7
commit 9f26efdf86
4 changed files with 129 additions and 48 deletions

View File

@ -88,6 +88,14 @@ The `nginxproxy` fixture will provide you with a replacement for the python [req
Also this requests replacement is preconfigured to use the Certificate Authority root certificate [certs/ca-root.crt](certs/) to validate https connections. 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 ### The web docker image

View File

@ -1,5 +1,5 @@
from __future__ import print_function from __future__ import print_function
import json import contextlib
import logging import logging
import os import os
import shlex import shlex
@ -12,14 +12,18 @@ import backoff
import docker import docker
import pytest import pytest
import requests import requests
from _pytest._code.code import ReprExceptionInfo
from requests.packages.urllib3.util.connection import HAS_IPV6
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
logging.getLogger('backoff').setLevel(logging.INFO) logging.getLogger('backoff').setLevel(logging.INFO)
logging.getLogger('DNS').setLevel(logging.INFO) logging.getLogger('DNS').setLevel(logging.DEBUG)
logging.getLogger('requests.packages.urllib3.connectionpool').setLevel(logging.WARN) logging.getLogger('requests.packages.urllib3.connectionpool').setLevel(logging.WARN)
CA_ROOT_CERTIFICATE = os.path.join(os.path.dirname(__file__), 'certs/ca-root.crt') 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") 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() docker_client = docker.from_env()
@ -30,6 +34,24 @@ docker_client = docker.from_env()
# #
############################################################################### ###############################################################################
@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): class requests_for_docker(object):
""" """
@ -54,36 +76,42 @@ class requests_for_docker(object):
return get_nginx_conf_from_container(nginx_proxy_containers[0]) return get_nginx_conf_from_container(nginx_proxy_containers[0])
def get(self, *args, **kwargs): 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) @backoff.on_predicate(backoff.constant, lambda r: r.status_code in (404, 502), interval=.3, max_tries=30, jitter=None)
def _get(*args, **kwargs): def _get(*args, **kwargs):
return self.session.get(*args, **kwargs) return self.session.get(*args, **kwargs)
return _get(*args, **kwargs) return _get(*args, **kwargs)
def post(self, *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) @backoff.on_predicate(backoff.constant, lambda r: r.status_code in (404, 502), interval=.3, max_tries=30, jitter=None)
def _post(*args, **kwargs): def _post(*args, **kwargs):
return self.session.post(*args, **kwargs) return self.session.post(*args, **kwargs)
return _post(*args, **kwargs) return _post(*args, **kwargs)
def put(self, *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) @backoff.on_predicate(backoff.constant, lambda r: r.status_code in (404, 502), interval=.3, max_tries=30, jitter=None)
def _put(*args, **kwargs): def _put(*args, **kwargs):
return self.session.put(*args, **kwargs) return self.session.put(*args, **kwargs)
return _put(*args, **kwargs) return _put(*args, **kwargs)
def head(self, *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) @backoff.on_predicate(backoff.constant, lambda r: r.status_code in (404, 502), interval=.3, max_tries=30, jitter=None)
def _head(*args, **kwargs): def _head(*args, **kwargs):
return self.session.head(*args, **kwargs) return self.session.head(*args, **kwargs)
return _head(*args, **kwargs) return _head(*args, **kwargs)
def delete(self, *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) @backoff.on_predicate(backoff.constant, lambda r: r.status_code in (404, 502), interval=.3, max_tries=30, jitter=None)
def _delete(*args, **kwargs): def _delete(*args, **kwargs):
return self.session.delete(*args, **kwargs) return self.session.delete(*args, **kwargs)
return _delete(*args, **kwargs) return _delete(*args, **kwargs)
def options(self, *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) @backoff.on_predicate(backoff.constant, lambda r: r.status_code in (404, 502), interval=.3, max_tries=30, jitter=None)
def _options(*args, **kwargs): def _options(*args, **kwargs):
return self.session.options(*args, **kwargs) return self.session.options(*args, **kwargs)
@ -95,8 +123,20 @@ class requests_for_docker(object):
def container_ip(container): def container_ip(container):
""" """
return the IP address of a 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"] net_info = container.attrs["NetworkSettings"]["Networks"]
if "bridge" in net_info: if "bridge" in net_info:
return net_info["bridge"]["IPAddress"] return net_info["bridge"]["IPAddress"]
@ -106,9 +146,22 @@ def container_ip(container):
return net_info[network_name]["IPAddress"] 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): def nginx_proxy_dns_resolver(domain_name):
""" """
if "nginx-proxy" if found in domain_name, return the ip address of the docker container if "nginx-proxy" if found in host, return the ip address of the docker container
issued from the docker image jwilder/nginx-proxy:test. issued from the docker image jwilder/nginx-proxy:test.
:return: IP or None :return: IP or None
@ -164,24 +217,21 @@ def monkey_patch_urllib_dns_resolver():
dns_cache = {} dns_cache = {}
def new_getaddrinfo(*args): def new_getaddrinfo(*args):
logging.getLogger('DNS').debug("resolving domain name %s" % repr(args)) logging.getLogger('DNS').debug("resolving domain name %s" % repr(args))
_args = list(args)
# custom DNS resolvers # custom DNS resolvers
ip = nginx_proxy_dns_resolver(args[0]) ip = nginx_proxy_dns_resolver(args[0])
if ip is None: if ip is None:
ip = docker_container_dns_resolver(args[0]) ip = docker_container_dns_resolver(args[0])
if ip is not None: if ip is not None:
return [ _args[0] = ip
(socket.AF_INET, socket.SOCK_STREAM, 6, '', (ip, args[1])),
(socket.AF_INET, socket.SOCK_DGRAM, 17, '', (ip, args[1])),
(socket.AF_INET, socket.SOCK_RAW, 0, '', (ip, args[1]))
]
# fallback on original DNS resolver # call on original DNS resolver, with eventually the original host changed to the wanted IP address
try: try:
return dns_cache[args] return dns_cache[tuple(_args)]
except KeyError: except KeyError:
res = prv_getaddrinfo(*args) res = prv_getaddrinfo(*_args)
dns_cache[args] = res dns_cache[tuple(_args)] = res
return res return res
socket.getaddrinfo = new_getaddrinfo socket.getaddrinfo = new_getaddrinfo
return prv_getaddrinfo return prv_getaddrinfo
@ -370,7 +420,11 @@ def nginxproxy():
r = nginxproxy.get("http://foo.com") r = nginxproxy.get("http://foo.com")
The difference is that in case an HTTP requests has status code 404 or 502 (which mostly 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 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() yield requests_for_docker()
@ -384,6 +438,7 @@ def nginxproxy():
# pytest hook to display additionnal stuff in test report # pytest hook to display additionnal stuff in test report
def pytest_runtest_logreport(report): def pytest_runtest_logreport(report):
if report.failed: if report.failed:
if isinstance(report.longrepr, ReprExceptionInfo):
test_containers = docker_client.containers.list(all=True, filters={"ancestor": "jwilder/nginx-proxy:test"}) test_containers = docker_client.containers.list(all=True, filters={"ancestor": "jwilder/nginx-proxy:test"})
for container in test_containers: for container in test_containers:
report.longrepr.addsection('nginx-proxy logs', container.logs()) report.longrepr.addsection('nginx-proxy logs', container.logs())

View File

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

View File

@ -8,7 +8,5 @@ def test_http_is_forwarded(docker_compose, nginxproxy):
def test_https_is_disabled(docker_compose, nginxproxy): def test_https_is_disabled(docker_compose, nginxproxy):
with pytest.raises(ConnectionError) as excinfo: with pytest.raises(ConnectionError):
r = nginxproxy.get("https://web.nginx-proxy.tld/", allow_redirects=False) nginxproxy.get("https://web.nginx-proxy.tld/", allow_redirects=False)
assert "[Errno 93] Protocol not supported" in str(excinfo.value)