mirror of
				https://github.com/thib8956/nginx-proxy
				synced 2025-11-04 11:09:20 +00:00 
			
		
		
		
	TESTS: add support for resolving containers IP address from domain names in the form *.<container name>.container.docker
This commit is contained in:
		@@ -17,6 +17,10 @@ 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.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -44,17 +48,6 @@ This test suite uses [pytest](http://doc.pytest.org/en/latest/). The [conftest.p
 | 
			
		||||
- docker_compose
 | 
			
		||||
- nginxproxy
 | 
			
		||||
 | 
			
		||||
Also _conftest.py_ alters the way the python interpreter resolves domain names to IP addresses in such a way that 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`
 | 
			
		||||
- ...
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### docker_compose fixture
 | 
			
		||||
 | 
			
		||||
@@ -68,6 +61,26 @@ The fixture will run the _docker-compose_ command with the `-f` option to load t
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@ import shlex
 | 
			
		||||
import socket
 | 
			
		||||
import subprocess
 | 
			
		||||
import time
 | 
			
		||||
import re
 | 
			
		||||
 | 
			
		||||
import backoff
 | 
			
		||||
import docker
 | 
			
		||||
@@ -14,7 +15,7 @@ import requests
 | 
			
		||||
 | 
			
		||||
logging.basicConfig(level=logging.INFO)
 | 
			
		||||
logging.getLogger('backoff').setLevel(logging.INFO)
 | 
			
		||||
logging.getLogger('patched DNS').setLevel(logging.INFO)
 | 
			
		||||
logging.getLogger('DNS').setLevel(logging.INFO)
 | 
			
		||||
logging.getLogger('requests.packages.urllib3.connectionpool').setLevel(logging.WARN)
 | 
			
		||||
 | 
			
		||||
CA_ROOT_CERTIFICATE = os.path.join(os.path.dirname(__file__), 'certs/ca-root.crt')
 | 
			
		||||
@@ -92,33 +93,90 @@ class requests_for_docker(object):
 | 
			
		||||
        return getattr(requests, name)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def container_ip(container):
 | 
			
		||||
    """
 | 
			
		||||
    return the IP address of a container
 | 
			
		||||
    """
 | 
			
		||||
    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 nginx_proxy_dns_resolver(domain_name):
 | 
			
		||||
    """
 | 
			
		||||
    if "nginx-proxy" if found in domain_name, 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'.
 | 
			
		||||
    """
 | 
			
		||||
    log = logging.getLogger("patched DNS")
 | 
			
		||||
    prv_getaddrinfo = socket.getaddrinfo
 | 
			
		||||
    dns_cache = {}
 | 
			
		||||
    def new_getaddrinfo(*args):
 | 
			
		||||
        log.debug("resolving domain name %s" % repr(args))
 | 
			
		||||
        if 'nginx-proxy' in args[0]:
 | 
			
		||||
            nginxproxy_container = docker_client.containers.list(filters={"status": "running", "ancestor": "jwilder/nginx-proxy:test"})[0]
 | 
			
		||||
            net_info = nginxproxy_container.attrs["NetworkSettings"]["Networks"]
 | 
			
		||||
            if "bridge" in net_info:
 | 
			
		||||
                ip = net_info["bridge"]["IPAddress"]
 | 
			
		||||
            else:
 | 
			
		||||
                # not default bridge network, fallback on first network defined
 | 
			
		||||
                network_name = net_info.keys()[0]
 | 
			
		||||
                ip = net_info[network_name]["IPAddress"]
 | 
			
		||||
            log.info("resolving domain name %r as IP address is %s" % (args[0], ip))
 | 
			
		||||
        logging.getLogger('DNS').debug("resolving domain name %s" % repr(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]))
 | 
			
		||||
            ]
 | 
			
		||||
 | 
			
		||||
        # fallback on original DNS resolver
 | 
			
		||||
        try:
 | 
			
		||||
            return dns_cache[args]
 | 
			
		||||
        except KeyError:
 | 
			
		||||
@@ -179,7 +237,6 @@ def wait_for_nginxproxy_to_be_ready():
 | 
			
		||||
    container = containers[0]
 | 
			
		||||
    for line in container.logs(stream=True):
 | 
			
		||||
        if "Watching docker events" in line:
 | 
			
		||||
            time.sleep(1)  # give time to docker-gen to produce the new nginx config and reload nginx
 | 
			
		||||
            logging.debug("nginx-proxy ready")
 | 
			
		||||
            break
 | 
			
		||||
 | 
			
		||||
@@ -223,7 +280,11 @@ def connect_to_network(network):
 | 
			
		||||
    :return: the name of the network we were connected to, or None
 | 
			
		||||
    """
 | 
			
		||||
    if I_AM_RUNNING_INSIDE_A_DOCKER_CONTAINER:
 | 
			
		||||
        my_container = docker_client.containers.get(socket.gethostname())
 | 
			
		||||
        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()
 | 
			
		||||
@@ -242,7 +303,11 @@ def disconnect_from_network(network=None):
 | 
			
		||||
    :param network: name of a docker network to disconnect from
 | 
			
		||||
    """
 | 
			
		||||
    if I_AM_RUNNING_INSIDE_A_DOCKER_CONTAINER and network is not None:
 | 
			
		||||
        my_container = docker_client.containers.get(socket.gethostname())
 | 
			
		||||
        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()
 | 
			
		||||
@@ -289,7 +354,8 @@ def docker_compose(request):
 | 
			
		||||
    docker_compose_up(docker_compose_file)
 | 
			
		||||
    networks = connect_to_all_networks()
 | 
			
		||||
    wait_for_nginxproxy_to_be_ready()
 | 
			
		||||
    yield
 | 
			
		||||
    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)
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user