Skip to content

Commit

Permalink
Add healthcheck support
Browse files Browse the repository at this point in the history
 * Add dockerfile and entrypoint to create test container
 * Add makefile target to help prepare tests
 * Add healthcheck's tests
 * Add healthcheck support
 * Update documentation
 * Update version
  • Loading branch information
dangoncalves committed Jan 27, 2022
1 parent f446e34 commit d9d4c83
Show file tree
Hide file tree
Showing 9 changed files with 146 additions and 26 deletions.
16 changes: 16 additions & 0 deletions Dockerfile-test-healthcheck
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
FROM nginx:1.19.3

LABEL maintainer="Daniel Gonçalves <[email protected]>"

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
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
prepare-test:
@docker build -f Dockerfile-test-healthcheck -t docker-dns:test-healthcheck-1.0 .

test:
@python -m tests
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -45,6 +45,7 @@ There are three options you can use to customize execution:

```
poetry shell
make prepare-test
make test
```

Expand All @@ -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)
9 changes: 9 additions & 0 deletions docker-dns-entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/bin/sh

set -e

sleep 5

sh /docker-entrypoint.sh nginx -g "daemon off;" &

sleep 5 && nginx -s stop
2 changes: 1 addition & 1 deletion dockerDNS/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@
"DockerDNS"
]

__version__ = 0.5
__version__ = 0.6
41 changes: 34 additions & 7 deletions dockerDNS/dockerDNS.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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"]

Expand All @@ -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
Expand All @@ -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"""
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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 <[email protected]>"]
license = "WTFPL"
Expand Down
17 changes: 12 additions & 5 deletions tests/__main__.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
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


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


Expand Down
77 changes: 67 additions & 10 deletions tests/test_dockerDNS.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -85,14 +87,17 @@ def tearDown(self):

def start_container(self):
self.dockerClient.containers.run(
"debian:buster",
self.defaultContainerImage,
self.defaultContainerCommand,
remove=True,
detach=True,
name=self.defaultContainerName,
network=self.networks[0]
)

def while_bootstraping(self):
raise NotImplementedError

def while_container_is_running(self, networks):
raise NotImplementedError

Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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:
Expand All @@ -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
Expand All @@ -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)

0 comments on commit d9d4c83

Please sign in to comment.