From 0f7cddee4d2e13e85dc8cc59d979dfac3cd70305 Mon Sep 17 00:00:00 2001 From: Thomas LEVEIL Date: Tue, 21 Feb 2017 03:32:12 +0100 Subject: [PATCH 1/4] get rid of forego As a side effect, this fixes the crash on restart if files referenced in previous generated configuration are now missing --- Dockerfile | 5 --- Dockerfile.alpine | 5 --- Procfile | 2 - docker-entrypoint.sh | 97 +++++++++++++++++++++++++++++++++++++++---- test/test_restart.py | 34 +++++++++++++++ test/test_restart.yml | 5 +++ 6 files changed, 128 insertions(+), 20 deletions(-) delete mode 100644 Procfile create mode 100644 test/test_restart.py create mode 100644 test/test_restart.yml diff --git a/Dockerfile b/Dockerfile index 6da4b03..a42a544 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,10 +13,6 @@ RUN apt-get update \ RUN echo "daemon off;" >> /etc/nginx/nginx.conf \ && sed -i 's/^http {/&\n server_names_hash_bucket_size 128;/g' /etc/nginx/nginx.conf -# Install Forego -ADD https://github.com/jwilder/forego/releases/download/v0.16.1/forego /usr/local/bin/forego -RUN chmod u+x /usr/local/bin/forego - ENV DOCKER_GEN_VERSION 0.7.3 RUN wget https://github.com/jwilder/docker-gen/releases/download/$DOCKER_GEN_VERSION/docker-gen-linux-amd64-$DOCKER_GEN_VERSION.tar.gz \ @@ -31,4 +27,3 @@ ENV DOCKER_HOST unix:///tmp/docker.sock VOLUME ["/etc/nginx/certs"] ENTRYPOINT ["/app/docker-entrypoint.sh"] -CMD ["forego", "start", "-r"] diff --git a/Dockerfile.alpine b/Dockerfile.alpine index 8715a2a..9f1546c 100644 --- a/Dockerfile.alpine +++ b/Dockerfile.alpine @@ -10,10 +10,6 @@ RUN apk add --no-cache --virtual .run-deps \ RUN echo "daemon off;" >> /etc/nginx/nginx.conf \ && sed -i 's/^http {/&\n server_names_hash_bucket_size 128;/g' /etc/nginx/nginx.conf -# Install Forego -ADD https://github.com/jwilder/forego/releases/download/v0.16.1/forego /usr/local/bin/forego -RUN chmod u+x /usr/local/bin/forego - ENV DOCKER_GEN_VERSION 0.7.3 RUN wget --quiet https://github.com/jwilder/docker-gen/releases/download/$DOCKER_GEN_VERSION/docker-gen-alpine-linux-amd64-$DOCKER_GEN_VERSION.tar.gz \ @@ -28,4 +24,3 @@ ENV DOCKER_HOST unix:///tmp/docker.sock VOLUME ["/etc/nginx/certs"] ENTRYPOINT ["/app/docker-entrypoint.sh"] -CMD ["forego", "start", "-r"] diff --git a/Procfile b/Procfile deleted file mode 100644 index 29fe166..0000000 --- a/Procfile +++ /dev/null @@ -1,2 +0,0 @@ -dockergen: docker-gen -watch -notify "nginx -s reload" /app/nginx.tmpl /etc/nginx/conf.d/default.conf -nginx: nginx diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 6353314..1c3f110 100755 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -1,7 +1,86 @@ #!/bin/bash -set -e +############################################################################### +# +# Signals: +# - HUP: reload docker-gen +# - USR1: reload nginx +# +############################################################################### +set -u -# Warn if the DOCKER_HOST socket does not exist +function start_docker_gen { + echo "~~~~~ Starting docker-gen ~~~~~" + { + while true; do + docker-gen -watch -notify "kill -USR1 1" /app/nginx.tmpl /etc/nginx/conf.d/default.conf + echo "docker-gen exited" + echo "~~~~~ Restarting docker-gen ~~~~~~" + done + } & +} + +function start_nginx { + echo "~~~~~ Starting nginx ~~~~~" + { + while true; do + nginx + echo "nginx exited, checking config..." + if nginx -t; then + echo "~~~~~ Restarting nginx ~~~~~~" + else + exit 1 + fi + done + } & +} + + +function reload_nginx { + if pgrep nginx >/dev/null; then + # nginx is already running + echo "~~~~~ Reloading nginx ~~~~~" + pkill -HUP nginx + else + start_nginx + fi +} + +############################################################################### + +function handle_SIGHUP { + echo "~~~~~ Signal HUP received ~~~~~" + if ! pgrep nginx >/dev/null; then + echo "~~~~~ Starting nginx ~~~~~" + nginx & + fi + pkill -HUP docker-gen # forward SIGHUP to docker-gen + wait +} + +function handle_SIGUSR1 { + echo "~~~~~ Signal USR1 received ~~~~~" + reload_nginx + wait +} + +############################################################################### + +# If the user has run provided a command, run it instead +if [ $# -ne 0 ]; then + exec "$@" +fi + +cat <<-OEBANNER + ███╗ ██╗ ██████╗ ██╗███╗ ██╗██╗ ██╗ ██████╗ ██████╗ ██████╗ ██╗ ██╗██╗ ██╗ + ████╗ ██║██╔════╝ ██║████╗ ██║╚██╗██╔╝ ██╔══██╗██╔══██╗██╔═══██╗╚██╗██╔╝╚██╗ ██╔╝ + ██╔██╗ ██║██║ ███╗██║██╔██╗ ██║ ╚███╔╝█████╗██████╔╝██████╔╝██║ ██║ ╚███╔╝ ╚████╔╝ + ██║╚██╗██║██║ ██║██║██║╚██╗██║ ██╔██╗╚════╝██╔═══╝ ██╔══██╗██║ ██║ ██╔██╗ ╚██╔╝ + ██║ ╚████║╚██████╔╝██║██║ ╚████║██╔╝ ██╗ ██║ ██║ ██║╚██████╔╝██╔╝ ██╗ ██║ + ╚═╝ ╚═══╝ ╚═════╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ +OEBANNER + + +# Error if the DOCKER_HOST socket does not exist if [[ $DOCKER_HOST == unix://* ]]; then socket_file=${DOCKER_HOST#unix://} if ! [ -S $socket_file ]; then @@ -10,13 +89,15 @@ if [[ $DOCKER_HOST == unix://* ]]; then Typically you should run your jwilder/nginx-proxy with: \`-v /var/run/docker.sock:$socket_file:ro\` See the documentation at http://git.io/vZaGJ EOT - socketMissing=1 + exit 1 fi fi -# If the user has run the default command and the socket doesn't exist, fail -if [ "$socketMissing" = 1 -a "$1" = forego -a "$2" = start -a "$3" = '-r' ]; then - exit 1 -fi -exec "$@" +trap handle_SIGHUP HUP +trap handle_SIGUSR1 USR1 +trap "exit 0" TERM + +rm /etc/nginx/conf.d/default.conf +start_docker_gen +wait \ No newline at end of file diff --git a/test/test_restart.py b/test/test_restart.py new file mode 100644 index 0000000..9b8c522 --- /dev/null +++ b/test/test_restart.py @@ -0,0 +1,34 @@ +import pytest +import time + + +def test_dockergen_is_running(docker_compose): + assert docker_compose.containers.get("reverseproxy").exec_run("pgrep docker-gen") != '' + + +def test_nginx_is_running(docker_compose): + assert docker_compose.containers.get("reverseproxy").exec_run("pgrep nginx") != '' + + +def test_nginx_answers_with_503(docker_compose, nginxproxy): + r = nginxproxy.get("http://nginx-proxy/") + assert r.status_code == 503 + + +def test_survive_restart(docker_compose, nginxproxy): + docker_compose.containers.get("reverseproxy").restart() + time.sleep(2) # give time to eventually fail + assert docker_compose.containers.get("reverseproxy").status == "running" + + +def test_dockergen_is_still_running(docker_compose): + assert docker_compose.containers.get("reverseproxy").exec_run("pgrep -c docker-gen") != '' + + +def test_nginx_is_still_running(docker_compose): + assert docker_compose.containers.get("reverseproxy").exec_run("pgrep -c nginx") != '' + + +def test_nginx_still_answers_with_503(docker_compose, nginxproxy): + r = nginxproxy.get("http://nginx-proxy/") + assert r.status_code == 503 diff --git a/test/test_restart.yml b/test/test_restart.yml new file mode 100644 index 0000000..0457fb7 --- /dev/null +++ b/test/test_restart.yml @@ -0,0 +1,5 @@ +reverseproxy: + container_name: reverseproxy + image: jwilder/nginx-proxy:test + volumes: + - /var/run/docker.sock:/tmp/docker.sock:ro From 563cfedb980a2f75d43b37f7ea3a1dcc36dd3ec3 Mon Sep 17 00:00:00 2001 From: Thomas LEVEIL Date: Fri, 24 Feb 2017 00:35:33 +0100 Subject: [PATCH 2/4] TESTS: add test for the docker entrypoint script --- test/test_entrypoint/docker-compose.yml | 18 ++++++++ test/test_entrypoint/test_entrypoint.py | 60 +++++++++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 test/test_entrypoint/docker-compose.yml create mode 100644 test/test_entrypoint/test_entrypoint.py diff --git a/test/test_entrypoint/docker-compose.yml b/test/test_entrypoint/docker-compose.yml new file mode 100644 index 0000000..13563e7 --- /dev/null +++ b/test/test_entrypoint/docker-compose.yml @@ -0,0 +1,18 @@ +version: '2' + +services: + nginx-proxy: + image: jwilder/nginx-proxy:test + container_name: nginx-proxy + volumes: + - /var/run/docker.sock:/tmp/docker.sock:ro +# - ../../docker-entrypoint.sh:/app/docker-entrypoint.sh:ro + + web1: + container_name: web1 + image: web + expose: + - 81 + environment: + WEB_PORTS: "81" + VIRTUAL_HOST: web1.nginx-proxy diff --git a/test/test_entrypoint/test_entrypoint.py b/test/test_entrypoint/test_entrypoint.py new file mode 100644 index 0000000..d05401c --- /dev/null +++ b/test/test_entrypoint/test_entrypoint.py @@ -0,0 +1,60 @@ +from time import sleep + +import pytest +import re +import docker + + +docker_client = docker.from_env() +RE_PGREP_RESPONSE = re.compile("^(?P\d+)\n", re.MULTILINE) + + +def get_pid(process_name): + pgrep_response = docker_client.containers.get("nginx-proxy").exec_run("pgrep %s" % process_name) + pids = [] + for m in RE_PGREP_RESPONSE.finditer(pgrep_response): + pids.append(int(m.group("pid"))) + assert len(pids) > 0 + return set(pids) + + +def kill(process_name): + docker_client.containers.get("nginx-proxy").exec_run("pkill %s" % process_name) + sleep(2) + + +def assert_reverse_proxy_behavior(nginxproxy): + assert "answer from port 81\n" == nginxproxy.get("http://web1.nginx-proxy/port").text + docker_client.containers.get("web1").stop() + sleep(2) + assert nginxproxy.get("http://web1.nginx-proxy/").status_code == 503 + docker_client.containers.get("web1").start() + sleep(2) + assert "answer from port 81\n" == nginxproxy.get("http://web1.nginx-proxy/port").text + + +############################################################################### + +def test_dockergen_is_restarted_when_killed(docker_compose, nginxproxy): + assert_reverse_proxy_behavior(nginxproxy) + first_pids = get_pid("docker-gen") + kill("docker-gen") + assert_reverse_proxy_behavior(nginxproxy) + second_pids = get_pid("docker-gen") + assert first_pids != second_pids + + +def test_nginx_is_restarted_when_killed(docker_compose, nginxproxy): + assert_reverse_proxy_behavior(nginxproxy) + first_pids = get_pid("nginx") + kill("nginx") + assert_reverse_proxy_behavior(nginxproxy) + second_pids = get_pid("nginx") + assert len(first_pids.intersection(second_pids)) == 0 + + +def test_term_signal_exits_nginxproxy(docker_compose): + assert "running" == docker_compose.containers.get("nginx-proxy").status + print docker_compose.containers.get("nginx-proxy").exec_run("kill -TERM 1") + sleep(2) + assert "exited" == docker_compose.containers.get("nginx-proxy").status From d282c2481bdf473ed57df5c81471e286c129060b Mon Sep 17 00:00:00 2001 From: Thomas LEVEIL Date: Sat, 25 Feb 2017 02:15:37 +0100 Subject: [PATCH 3/4] TESTS: the web image can now handle concurrent connections Using the python web framework [Flask](http://flask.pocoo.org/). As a side effect, headers printed out from the `/headers` end point get capitalized. --- test/requirements/web/Dockerfile | 1 + test/requirements/web/webserver.py | 39 +++++++++++++----------------- test/test_headers/test_http.py | 2 +- test/test_headers/test_https.py | 2 +- 4 files changed, 20 insertions(+), 24 deletions(-) diff --git a/test/requirements/web/Dockerfile b/test/requirements/web/Dockerfile index 923ed79..0d57a93 100644 --- a/test/requirements/web/Dockerfile +++ b/test/requirements/web/Dockerfile @@ -1,6 +1,7 @@ # Docker Image running one (or multiple) webservers listening on all given ports from WEB_PORTS environment variable FROM python:3 +RUN pip install flask==0.12 COPY ./webserver.py / COPY ./entrypoint.sh / WORKDIR /opt diff --git a/test/requirements/web/webserver.py b/test/requirements/web/webserver.py index 305c207..cd4726e 100755 --- a/test/requirements/web/webserver.py +++ b/test/requirements/web/webserver.py @@ -1,31 +1,26 @@ #!/usr/bin/env python3 +import os -import os, sys -import http.server -import socketserver +import sys +from flask import Flask, Response, request +app = Flask(__name__) -class Handler(http.server.SimpleHTTPRequestHandler): - def do_GET(self): - - self.send_response(200) - self.send_header("Content-Type", "text/plain") - self.end_headers() +@app.route("/") +def root(): + return Response("I'm %s\n" % os.environ['HOSTNAME'], mimetype="text/plain") - if self.path == "/headers": - self.wfile.write(self.headers.as_string().encode()) - elif self.path == "/port": - response = "answer from port %s\n" % PORT - self.wfile.write(response.encode()) - elif self.path == "/": - response = "I'm %s\n" % os.environ['HOSTNAME'] - self.wfile.write(response.encode()) - else: - self.wfile.write("No route for this path!\n".encode()) + +@app.route("/headers") +def headers(): + return Response("".join(["%s: %s\n" % (header, value) for header, value in request.headers.items()]), mimetype="text/plain") + + +@app.route("/port") +def port(): + return Response("answer from port %s\n" % PORT, mimetype="text/plain") if __name__ == '__main__': PORT = int(sys.argv[1]) - socketserver.TCPServer.allow_reuse_address = True - httpd = socketserver.TCPServer(('0.0.0.0', PORT), Handler) - httpd.serve_forever() + app.run(host="0.0.0.0", port=PORT) diff --git a/test/test_headers/test_http.py b/test/test_headers/test_http.py index 2799262..4110680 100644 --- a/test/test_headers/test_http.py +++ b/test/test_headers/test_http.py @@ -63,7 +63,7 @@ def test_X_Forwarded_Ssl_is_overwritten(docker_compose, nginxproxy): def test_X_Real_IP_is_generated(docker_compose, nginxproxy): r = nginxproxy.get("http://web.nginx-proxy.tld/headers") assert r.status_code == 200 - assert "X-Real-IP: " in r.text + assert "X-Real-IP: ".upper() in r.text.upper() def test_Host_is_passed_on(docker_compose, nginxproxy): r = nginxproxy.get("http://web.nginx-proxy.tld/headers") diff --git a/test/test_headers/test_https.py b/test/test_headers/test_https.py index a1d434a..beb238a 100644 --- a/test/test_headers/test_https.py +++ b/test/test_headers/test_https.py @@ -64,7 +64,7 @@ def test_X_Forwarded_Ssl_is_overwritten(docker_compose, nginxproxy): def test_X_Real_IP_is_generated(docker_compose, nginxproxy): r = nginxproxy.get("https://web.nginx-proxy.tld/headers") assert r.status_code == 200 - assert "X-Real-IP: " in r.text + assert "X-Real-IP: ".upper() in r.text.upper() def test_Host_is_passed_on(docker_compose, nginxproxy): r = nginxproxy.get("https://web.nginx-proxy.tld/headers") From 41006c36a5dbaebb92500e0e12b0d9a7bae827a6 Mon Sep 17 00:00:00 2001 From: Thomas LEVEIL Date: Sat, 25 Feb 2017 02:16:38 +0100 Subject: [PATCH 4/4] TESTS: adjust logging levels --- test/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/conftest.py b/test/conftest.py index fcff6ea..7673e2c 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -17,7 +17,7 @@ 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.DEBUG) +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') @@ -287,7 +287,7 @@ def wait_for_nginxproxy_to_be_ready(): container = containers[0] for line in container.logs(stream=True): if "Watching docker events" in line: - logging.debug("nginx-proxy ready") + logging.info("nginx-proxy ready") break def find_docker_compose_file(request):