Преглед на файлове

TESTS: add tests for IPv6

Thomas LEVEIL преди 8 години
родител
ревизия
9f26efdf86
променени са 4 файла, в които са добавени 129 реда и са изтрити 48 реда
  1. 8 0
      test2/README.md
  2. 99 44
      test2/conftest.py
  3. 20 0
      test2/test_nominal.py
  4. 2 4
      test2/test_ssl/test_nohttps.py

+ 8 - 0
test2/README.md

@@ -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.
 
+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
 

+ 99 - 44
test2/conftest.py

@@ -1,5 +1,5 @@
 from __future__ import print_function
-import json
+import contextlib
 import logging
 import os
 import shlex
@@ -12,14 +12,18 @@ 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.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()
 
@@ -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):
     """
@@ -54,40 +76,46 @@ class requests_for_docker(object):
         return get_nginx_conf_from_container(nginx_proxy_containers[0])
 
     def get(self, *args, **kwargs):
-        @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)
+        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):
-        @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)
+        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):
-        @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)
+        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):
-        @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)
+        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):
-        @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)
+        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):
-        @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)
+        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)
@@ -95,20 +123,45 @@ class requests_for_docker(object):
 
 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"]
+        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"]["IPAddress"]
+        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]["IPAddress"]
+    return net_info[network_name]["GlobalIPv6Address"]
 
 
 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.
 
     :return: IP or None
@@ -164,24 +217,21 @@ def monkey_patch_urllib_dns_resolver():
     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:
-            return [
-                (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]))
-            ]
+            _args[0] = ip
 
-        # fallback on original DNS resolver
+        # call on original DNS resolver, with eventually the original host changed to the wanted IP address
         try:
-            return dns_cache[args]
+            return dns_cache[tuple(_args)]
         except KeyError:
-            res = prv_getaddrinfo(*args)
-            dns_cache[args] = res
+            res = prv_getaddrinfo(*_args)
+            dns_cache[tuple(_args)] = res
             return res
     socket.getaddrinfo = new_getaddrinfo
     return prv_getaddrinfo
@@ -370,7 +420,11 @@ def nginxproxy():
     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
+    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()
 
@@ -384,10 +438,11 @@ def nginxproxy():
 # pytest hook to display additionnal stuff in test report
 def pytest_runtest_logreport(report):
     if report.failed:
-        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))
+        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))
 
 
 

+ 20 - 0
test2/test_nominal.py

@@ -1,15 +1,35 @@
 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" 
+
+
+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" 

+ 2 - 4
test2/test_ssl/test_nohttps.py

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