diff --git a/Dockerfile-test-healthcheck b/Dockerfile-test-healthcheck new file mode 100644 index 0000000..caf0b22 --- /dev/null +++ b/Dockerfile-test-healthcheck @@ -0,0 +1,16 @@ +FROM nginx:1.19.3 + +LABEL maintainer="Daniel Gonçalves " + +COPY docker-dns-entrypoint.sh / + +RUN chmod 755 /docker-dns-entrypoint.sh + +ENTRYPOINT ["/docker-dns-entrypoint.sh"] + +HEALTHCHECK --interval=3s --timeout=1s \ + CMD curl -f http://127.0.0.1/ || exit 1 + +EXPOSE 80 + +STOPSIGNAL SIGTERM diff --git a/Makefile b/Makefile index f81a802..3f0de82 100644 --- a/Makefile +++ b/Makefile @@ -1,2 +1,5 @@ +prepare-test: + @docker build -f Dockerfile-test-healthcheck -t docker-dns:test-healthcheck-1.0 . + test: @python -m tests diff --git a/README.md b/README.md index af52d8a..f84d15a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Docker DNS Docker DNS is a DNS server that resolve Docker's container name into -A,AAAA or PTR records to retrieve IPv4, IPv6 associated to a container +A, AAAA or PTR records to retrieve IPv4, IPv6 associated to a container name or the container associated to an IP address. ## Installation configuration and execution @@ -45,6 +45,7 @@ There are three options you can use to customize execution: ``` poetry shell +make prepare-test make test ``` @@ -62,7 +63,7 @@ This first version do the job, but there are still some things to do like: - [x] add AAAA queries support - [x] add PTR queries support - [ ] ~~add SRV queries support~~ Not relevant -- [ ] add HEALTH CHECK support (do not resolve a container that is not healthy) +- [x] add HEALTH CHECK support (do not resolve a container that is not healthy) - [ ] automate installation - [ ] improve documentation - [ ] add Windows and Mac OS support (tested only on Linux for now) diff --git a/docker-dns-entrypoint.sh b/docker-dns-entrypoint.sh new file mode 100644 index 0000000..924570c --- /dev/null +++ b/docker-dns-entrypoint.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +set -e + +sleep 5 + +sh /docker-entrypoint.sh nginx -g "daemon off;" & + +sleep 5 && nginx -s stop diff --git a/dockerDNS/__init__.py b/dockerDNS/__init__.py index 3632e8b..adc178a 100755 --- a/dockerDNS/__init__.py +++ b/dockerDNS/__init__.py @@ -6,4 +6,4 @@ "DockerDNS" ] -__version__ = 0.5 +__version__ = 0.6 diff --git a/dockerDNS/dockerDNS.py b/dockerDNS/dockerDNS.py index bcea4bf..dc82ec3 100755 --- a/dockerDNS/dockerDNS.py +++ b/dockerDNS/dockerDNS.py @@ -26,7 +26,15 @@ def __init__(self, dockerClient, servers=None): if not containerIPv6: containerIPv6 = None - self.addContainer(containerName, containerIPv4, containerIPv6) + shouldAddContainer = False + if (("Health" in c.attrs["State"] and + c.attrs["State"]["Health"]["Status"] == "healthy") or + "Health" not in c.attrs["State"]): + shouldAddContainer = True + if shouldAddContainer: + self.addContainer(containerName, + containerIPv4, + containerIPv6) def addContainer(self, containerName, containerIPv4, containerIPv6=None): if containerName not in self.runningContainers: @@ -134,20 +142,22 @@ def __init__(self, resolver): def run(self): self.eventListener = self.resolver.dockerClient.events( - filters={"event": ["connect", "disconnect"]}, + filters={"event": ["connect", + "disconnect", + "health_status"]}, decode=True) for e in self.eventListener: - callback = getattr(self, e["Action"] + "Callback") + callback_prefix = e["Action"] + if "health_status:" in e["Action"]: + callback_prefix = e["Action"][:(e["Action"].index(':'))] + callback = getattr(self, callback_prefix + "Callback") callback(e) def join(self, timeout=None): self.eventListener.close() super().join(timeout) - def connectCallback(self, event): - containerID = event["Actor"]["Attributes"]["container"] - api = self.resolver.dockerClient.api - container = api.inspect_container(containerID) + def __add_container(self, container): containerName = container["Name"].lstrip('/') containerNetworks = container["NetworkSettings"]["Networks"] @@ -172,6 +182,15 @@ def connectCallback(self, event): containerIPv4, containerIPv6) + def connectCallback(self, event): + containerID = event["Actor"]["Attributes"]["container"] + api = self.resolver.dockerClient.api + container = api.inspect_container(containerID) + + if ("Health" not in container["State"] or + container["State"]["Health"]["Status"] == "healthy"): + self.__add_container(container) + def disconnectCallback(self, event): containerID = event["Actor"]["Attributes"]["container"] api = self.resolver.dockerClient.api @@ -182,6 +201,14 @@ def disconnectCallback(self, event): except docker.errors.NotFound: pass + def health_statusCallback(self, event): + api = self.resolver.dockerClient.api + container = api.inspect_container(event["id"]) + + if ("Health" in container["State"] and + container["State"]["Health"]["Status"] == "healthy"): + self.__add_container(container) + class DockerDNS(): """Start and stop DockerDNS Service""" diff --git a/pyproject.toml b/pyproject.toml index 18b3341..4b4aeca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "docker-dns" -version = "0.5.0" +version = "0.6.0" description = "A DNS server for docker containers" authors = ["Daniel Gonçalves "] license = "WTFPL" diff --git a/tests/__main__.py b/tests/__main__.py index e9a9cc3..c9b71a5 100644 --- a/tests/__main__.py +++ b/tests/__main__.py @@ -1,4 +1,7 @@ -from .test_dockerDNS import TestDockerDNSIPv4, TestDockerDNSIPv6 +from .test_dockerDNS import TestDockerDNSIPv4NoHealthCheck +from .test_dockerDNS import TestDockerDNSIPv6NoHealthCheck +from .test_dockerDNS import TestDockerDNSIPv4HealthCheck +from .test_dockerDNS import TestDockerDNSIPv6HealthCheck from twisted.trial import runner, reporter import unittest @@ -6,10 +9,14 @@ def suite(): suite = unittest.TestSuite() - suite.addTest(TestDockerDNSIPv4('test_basic_dns_request')) - suite.addTest(TestDockerDNSIPv4('test_docker_network')) - suite.addTest(TestDockerDNSIPv6('test_basic_dns_request')) - suite.addTest(TestDockerDNSIPv6('test_docker_network')) + suite.addTest(TestDockerDNSIPv4NoHealthCheck('test_basic_dns_request')) + suite.addTest(TestDockerDNSIPv4NoHealthCheck('test_docker_network')) + suite.addTest(TestDockerDNSIPv6NoHealthCheck('test_basic_dns_request')) + suite.addTest(TestDockerDNSIPv6NoHealthCheck('test_docker_network')) + suite.addTest(TestDockerDNSIPv4HealthCheck('test_basic_dns_request')) + suite.addTest(TestDockerDNSIPv4HealthCheck('test_docker_network')) + suite.addTest(TestDockerDNSIPv6HealthCheck('test_basic_dns_request')) + suite.addTest(TestDockerDNSIPv6HealthCheck('test_docker_network')) return suite diff --git a/tests/test_dockerDNS.py b/tests/test_dockerDNS.py index fb945fb..bbaae38 100755 --- a/tests/test_dockerDNS.py +++ b/tests/test_dockerDNS.py @@ -52,9 +52,11 @@ def __init__(self, *args, **kwargs): self.networks = ["bridge"] - self.sleepTimeStart = 2 + self.sleepTimeBootstrap = 2 + self.sleepTimeBeforeRunning = 0 self.sleepTimeDestroy = 2 + self.defaultContainerImage = "debian:buster" self.defaultContainerCommand = "sleep 3" self.defaultContainerName = "test.dockerdns.io" @@ -85,7 +87,7 @@ def tearDown(self): def start_container(self): self.dockerClient.containers.run( - "debian:buster", + self.defaultContainerImage, self.defaultContainerCommand, remove=True, detach=True, @@ -93,6 +95,9 @@ def start_container(self): network=self.networks[0] ) + def while_bootstraping(self): + raise NotImplementedError + def while_container_is_running(self, networks): raise NotImplementedError @@ -103,7 +108,12 @@ def test_basic_dns_request(self): self.start_container() # While we detach the container, # we have to wait it has fully started. - time.sleep(self.sleepTimeStart) + time.sleep(self.sleepTimeBootstrap) + self.while_bootstraping() + # The container is started now but we + # may need to wait the healthcheck returns + # success + time.sleep(self.sleepTimeBeforeRunning) self.while_container_is_running() # We wait again to ensure the DNS entry # was removed after container has stopped. @@ -135,7 +145,12 @@ def test_docker_network(self): dockerNetwork.connect(self.defaultContainerName) # While we detach the container, # we have to wait it has fully started. - time.sleep(self.sleepTimeStart) + time.sleep(self.sleepTimeBootstrap) + self.while_bootstraping() + # The container is started now but we + # may need to wait the healthcheck returns + # success + time.sleep(self.sleepTimeBeforeRunning) self.while_container_is_running(self.networks) # We wait again to ensure the DNS entry # was removed after container has stopped. @@ -147,20 +162,24 @@ def test_docker_network(self): self.network = oldNetworks -class TestDockerDNSIPv4(BaseTest): +class TestDockerDNSIPv4NoHealthCheck(BaseTest): def __init__(self, *args, **kwargs): - super(TestDockerDNSIPv4, self).__init__(*args, **kwargs) + super(TestDockerDNSIPv4NoHealthCheck, self).__init__(*args, **kwargs) self.port = 35353 self.listenAddress = "127.0.0.1" self.forwarders = ["8.8.8.8"] + def while_bootstraping(self): + pass + def while_container_is_running(self, networks=None): if networks is None: networks = self.networks dnsAnswer = resolveDNS(self.defaultContainerName, self.listenAddress, - self.port) + self.port, + "A") self.assertTrue(areAnswersInNetworks(dnsAnswer, networks)) for ip in dnsAnswer: @@ -175,18 +194,22 @@ def while_container_is_running(self, networks=None): def when_container_has_gone(self): dnsAnswer = resolveDNS(self.defaultContainerName, self.listenAddress, - self.port) + self.port, + "A") self.assertTrue(len(dnsAnswer) == 0) -class TestDockerDNSIPv6(BaseTest): +class TestDockerDNSIPv6NoHealthCheck(BaseTest): def __init__(self, *args, **kwargs): - super(TestDockerDNSIPv6, self).__init__(*args, **kwargs) + super(TestDockerDNSIPv6NoHealthCheck, self).__init__(*args, **kwargs) self.port = 35353 self.listenAddress = "::1" self.forwarders = ["2001:4860:4860::8888"] + def while_bootstraping(self): + pass + def while_container_is_running(self, networks=None): if networks is None: networks = self.networks @@ -211,3 +234,37 @@ def when_container_has_gone(self): self.port, type="AAAA") self.assertTrue(len(dnsAnswer) == 0) + + +class TestDockerDNSIPv4HealthCheck(TestDockerDNSIPv4NoHealthCheck): + + def __init__(self, *args, **kwargs): + super(TestDockerDNSIPv4HealthCheck, self).__init__(*args, **kwargs) + self.defaultContainerImage = "docker-dns:test-healthcheck-1.0" + self.sleepTimeBootstrap = 3 + self.sleepTimeBeforeRunning = 5 + self.sleepTimeDestroy = 5 + + def while_bootstraping(self): + dnsAnswer = resolveDNS(self.defaultContainerName, + self.listenAddress, + self.port, + type="A") + self.assertTrue(len(dnsAnswer) == 0) + + +class TestDockerDNSIPv6HealthCheck(TestDockerDNSIPv6NoHealthCheck): + + def __init__(self, *args, **kwargs): + super(TestDockerDNSIPv6HealthCheck, self).__init__(*args, **kwargs) + self.defaultContainerImage = "docker-dns:test-healthcheck-1.0" + self.sleepTimeBootstrap = 3 + self.sleepTimeBeforeRunning = 5 + self.sleepTimeDestroy = 5 + + def while_bootstraping(self): + dnsAnswer = resolveDNS(self.defaultContainerName, + self.listenAddress, + self.port, + type="AAAA") + self.assertTrue(len(dnsAnswer) == 0)