diff --git a/.github/workflows/build-publish.yml b/.github/workflows/build-publish.yml index b714de7..7b5d6ec 100644 --- a/.github/workflows/build-publish.yml +++ b/.github/workflows/build-publish.yml @@ -23,7 +23,7 @@ jobs: name: Build and publish image strategy: matrix: - base: [alpine, debian] + flavor: [alpine, debian, dockergen] runs-on: ubuntu-latest steps: - name: Checkout @@ -37,7 +37,7 @@ jobs: - name: Retrieve docker-gen version id: docker-gen_version - run: sed -n -e 's;^FROM nginxproxy/docker-gen:\([0-9.]*\).*;VERSION=\1;p' Dockerfile.${{ matrix.base }} >> "$GITHUB_OUTPUT" + run: sed -n -e 's;^FROM docker.io/nginxproxy/docker-gen:\([0-9.]*\).*;VERSION=\1;p' Dockerfile.${{ matrix.flavor }} >> "$GITHUB_OUTPUT" - name: Get Docker tags id: docker_meta @@ -48,12 +48,15 @@ jobs: nginxproxy/nginx-proxy jwilder/nginx-proxy tags: | - type=semver,pattern={{version}},enable=${{ matrix.base == 'debian' }} - type=semver,pattern={{major}}.{{minor}},enable=${{ matrix.base == 'debian' }} - type=semver,suffix=-alpine,pattern={{version}},enable=${{ matrix.base == 'alpine' }} - type=semver,suffix=-alpine,pattern={{major}}.{{minor}},enable=${{ matrix.base == 'alpine' }} - type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' && matrix.base == 'debian' }} - type=raw,value=alpine,enable=${{ github.ref == 'refs/heads/main' && matrix.base == 'alpine' }} + type=semver,pattern={{version}},enable=${{ matrix.flavor == 'debian' }} + type=semver,pattern={{major}}.{{minor}},enable=${{ matrix.flavor == 'debian' }} + type=semver,suffix=-alpine,pattern={{version}},enable=${{ matrix.flavor == 'alpine' }} + type=semver,suffix=-alpine,pattern={{major}}.{{minor}},enable=${{ matrix.flavor == 'alpine' }} + type=semver,suffix=-dockergen,pattern={{version}},enable=${{ matrix.flavor == 'dockergen' }} + type=semver,suffix=-dockergen,pattern={{major}}.{{minor}},enable=${{ matrix.flavor == 'dockergen' }} + type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' && matrix.flavor == 'debian' }} + type=raw,value=alpine,enable=${{ github.ref == 'refs/heads/main' && matrix.flavor == 'alpine' }} + type=raw,value=dockergen,enable=${{ github.ref == 'refs/heads/main' && matrix.flavor == 'dockergen' }} labels: | org.opencontainers.image.authors=Nicolas Duchon (@buchdag), Jason Wilder org.opencontainers.image.version=${{ steps.nginx-proxy_version.outputs.VERSION }} @@ -84,7 +87,7 @@ jobs: uses: docker/build-push-action@v6 with: context: . - file: Dockerfile.${{ matrix.base }} + file: Dockerfile.${{ matrix.flavor }} build-args: | NGINX_PROXY_VERSION=${{ steps.nginx-proxy_version.outputs.VERSION }} DOCKER_GEN_VERSION=${{ steps.docker-gen_version.outputs.VERSION }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 60e8331..9a41a89 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,7 +20,7 @@ jobs: strategy: matrix: - base_docker_image: [alpine, debian] + flavor: [alpine, debian, dockergen] steps: - uses: actions/checkout@v4 @@ -43,8 +43,10 @@ jobs: run: make build-webserver - name: Build Docker nginx proxy test image - run: make build-nginx-proxy-test-${{ matrix.base_docker_image }} + run: make build-nginx-proxy-test-${{ matrix.flavor }} - name: Run tests run: pytest working-directory: test + env: + COMPOSE_PROFILES: ${{ matrix.flavor == 'dockergen' && 'separateContainers' || 'singleContainer' }} diff --git a/Dockerfile.dockergen b/Dockerfile.dockergen new file mode 100644 index 0000000..6ab7ec8 --- /dev/null +++ b/Dockerfile.dockergen @@ -0,0 +1,18 @@ +FROM docker.io/nginxproxy/docker-gen:0.14.4 + +ARG NGINX_PROXY_VERSION +ENV NGINX_PROXY_VERSION=${NGINX_PROXY_VERSION} \ + DOCKER_HOST=unix:///tmp/docker.sock \ + DHPARAM_SKIP=true \ + NGINX_CONTAINER_NAME=nginx-proxy + +# Install dependencies +RUN apk add --no-cache --virtual .run-deps bash + +RUN mkdir -p '/etc/nginx/conf.d' + +COPY app nginx.tmpl LICENSE /app/ +WORKDIR /app/ + +ENTRYPOINT ["/app/docker-entrypoint.sh"] +CMD ["docker-gen", "-notify-sighup", "$NGINX_CONTAINER_NAME", "-watch", "/app/nginx.tmpl", "/etc/nginx/conf.d/default.conf"] diff --git a/Makefile b/Makefile index e735e6a..7af3279 100644 --- a/Makefile +++ b/Makefile @@ -11,10 +11,19 @@ build-nginx-proxy-test-debian: build-nginx-proxy-test-alpine: docker build --pull --build-arg NGINX_PROXY_VERSION="test" -f Dockerfile.alpine -t nginxproxy/nginx-proxy:test . +build-nginx-proxy-test-dockergen: + docker build --pull --build-arg NGINX_PROXY_VERSION="test" -f Dockerfile.dockergen -t nginxproxy/nginx-proxy:test-dockergen . + +test-debian: export COMPOSE_PROFILES = singleContainer test-debian: build-webserver build-nginx-proxy-test-debian test/pytest.sh +test-alpine: export COMPOSE_PROFILES = singleContainer test-alpine: build-webserver build-nginx-proxy-test-alpine test/pytest.sh -test: test-debian test-alpine +test-dockergen: export COMPOSE_PROFILES = separateContainers +test-dockergen: build-webserver build-nginx-proxy-test-docker-gen + test/pytest.sh + +test: test-debian test-alpine test-dockergen diff --git a/app/docker-entrypoint.sh b/app/docker-entrypoint.sh index 0477dd2..bb12f67 100755 --- a/app/docker-entrypoint.sh +++ b/app/docker-entrypoint.sh @@ -101,7 +101,7 @@ function _setup_dhparam() { } # Run the init logic if the default CMD was provided -if [[ $* == 'forego start -r' ]]; then +if [[ $* == "forego start -r" ]] || [[ $* =~ "docker-gen -notify-sighup" ]]; then _print_version _check_unix_socket @@ -116,6 +116,11 @@ if [[ $* == 'forego start -r' ]]; then Warning: The default value of TRUST_DOWNSTREAM_PROXY might change to "false" in a future version of nginx-proxy. If you require TRUST_DOWNSTREAM_PROXY to be enabled, explicitly set it to "true". EOT fi + + if [[ $3 == "\$NGINX_CONTAINER_NAME" && -n "$NGINX_CONTAINER_NAME" ]]; then + # change the value of $3 to the expanded $NGINX_CONTAINER_NAME variable + set -- "${@:1:2}" "$NGINX_CONTAINER_NAME" "${@:4}" + fi fi exec "$@" diff --git a/test/conftest.py b/test/conftest.py index 0df348b..d2f7841 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -27,6 +27,9 @@ PYTEST_RUNNING_IN_CONTAINER = os.environ.get('PYTEST_RUNNING_IN_CONTAINER') == " FORCE_CONTAINER_IPV6 = False # ugly global state to consider containers' IPv6 address instead of IPv4 DOCKER_COMPOSE = os.environ.get('DOCKER_COMPOSE', 'docker compose') +COMPOSE_PROFILES = os.environ.get('COMPOSE_PROFILES', 'singleContainer') +IMAGE_TAG = "test-dockergen" if COMPOSE_PROFILES == "separateContainers" else "test" + docker_client = docker.from_env() @@ -75,11 +78,11 @@ class requests_for_docker(object): """ Return list of containers """ - nginx_proxy_containers = docker_client.containers.list(filters={"ancestor": "nginxproxy/nginx-proxy:test"}) + nginx_proxy_containers = docker_client.containers.list(filters={"ancestor": f"nginxproxy/nginx-proxy:{IMAGE_TAG}"}) if len(nginx_proxy_containers) > 1: - pytest.fail("Too many running nginxproxy/nginx-proxy:test containers", pytrace=False) + pytest.fail(f"Too many running nginxproxy/nginx-proxy:{IMAGE_TAG} containers", pytrace=False) elif len(nginx_proxy_containers) == 0: - pytest.fail("No running nginxproxy/nginx-proxy:test container", pytrace=False) + pytest.fail(f"No running nginxproxy/nginx-proxy:{IMAGE_TAG} container", pytrace=False) return nginx_proxy_containers def get_conf(self): @@ -188,7 +191,7 @@ def container_ipv6(container): return net_info[network_name]["GlobalIPv6Address"] -def nginx_proxy_dns_resolver(domain_name): +def nginx_proxy_single_container_dns_resolver(domain_name): """ if "nginx-proxy" if found in host, return the ip address of the docker container issued from the docker image nginxproxy/nginx-proxy:test. @@ -196,21 +199,44 @@ def nginx_proxy_dns_resolver(domain_name): :return: IP or None """ log = logging.getLogger('DNS') - log.debug(f"nginx_proxy_dns_resolver({domain_name!r})") + log.debug(f"nginx_proxy_single_container_dns_resolver({domain_name!r})") if 'nginx-proxy' in domain_name: nginxproxy_containers = docker_client.containers.list(filters={"status": "running", "ancestor": "nginxproxy/nginx-proxy:test"}) if len(nginxproxy_containers) == 0: - log.warn(f"no container found from image nginxproxy/nginx-proxy:test while resolving {domain_name!r}") + log.info(f"no container found from image nginxproxy/nginx-proxy:test while resolving {domain_name!r}") exited_nginxproxy_containers = docker_client.containers.list(filters={"status": "exited", "ancestor": "nginxproxy/nginx-proxy:test"}) if len(exited_nginxproxy_containers) > 0: exited_nginxproxy_container_logs = exited_nginxproxy_containers[0].logs() - log.warn(f"nginxproxy/nginx-proxy:test container might have exited unexpectedly. Container logs: " + "\n" + exited_nginxproxy_container_logs.decode()) + log.warning(f"nginxproxy/nginx-proxy:test container might have exited unexpectedly. Container logs: " + "\n" + exited_nginxproxy_container_logs.decode()) return nginxproxy_container = nginxproxy_containers[0] ip = container_ip(nginxproxy_container) log.info(f"resolving domain name {domain_name!r} as IP address {ip} of nginx-proxy container {nginxproxy_container.name}") return ip +def nginx_proxy_separate_containers_dns_resolver(domain_name): + """ + if "nginx-proxy" if found in host, return the ip address of the docker container + labeled with "com.github.nginx-proxy.nginx-proxy.nginx". + + :return: IP or None + """ + log = logging.getLogger('DNS') + log.debug(f"nginx_proxy_separate_containers_dns_resolver({domain_name!r})") + if 'nginx-proxy' in domain_name: + nginx_containers = docker_client.containers.list(filters={"status": "running", "label": "com.github.nginx-proxy.nginx-proxy.nginx"}) + if len(nginx_containers) == 0: + log.info(f"no container labeled with com.github.nginx-proxy.nginx-proxy.nginx found while resolving {domain_name!r}") + exited_nginx_containers = docker_client.containers.list(filters={"status": "exited", "label": "com.github.nginx-proxy.nginx-proxy.nginx"}) + if len(exited_nginx_containers) > 0: + exited_nginx_container_logs = exited_nginx_containers[0].logs() + log.warning(f"nginx container might have exited unexpectedly. Container logs: " + "\n" + exited_nginx_container_logs.decode()) + return + nginx_container = nginx_containers[0] + ip = container_ip(nginx_container) + log.info(f"resolving domain name {domain_name!r} as IP address {ip} of nginx container {nginx_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 @@ -231,7 +257,7 @@ def docker_container_dns_resolver(domain_name): try: container = docker_client.containers.get(container_name) except docker.errors.NotFound: - log.warn(f"container named {container_name!r} not found while resolving {domain_name!r}") + log.warning(f"container named {container_name!r} not found while resolving {domain_name!r}") return log.debug(f"container {container.name!r} found ({container.short_id})") @@ -244,7 +270,8 @@ 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 'nginxproxy/nginx-proxy:test'. + of the container created from image 'nginxproxy/nginx-proxy:test' or + labeled with 'com.github.nginx-proxy.nginx-proxy.nginx'. """ prv_getaddrinfo = socket.getaddrinfo dns_cache = {} @@ -258,7 +285,9 @@ def monkey_patch_urllib_dns_resolver(): pytest.skip("This system does not support IPv6") # custom DNS resolvers - ip = nginx_proxy_dns_resolver(args[0]) + ip = nginx_proxy_single_container_dns_resolver(args[0]) + if ip is None: + ip = nginx_proxy_separate_containers_dns_resolver(args[0]) if ip is None: ip = docker_container_dns_resolver(args[0]) if ip is not None: @@ -319,10 +348,11 @@ def docker_compose_down(compose_file='docker-compose.yml'): def wait_for_nginxproxy_to_be_ready(): """ - If one (and only one) container started from image nginxproxy/nginx-proxy:test is found, - wait for its log to contain substring "Watching docker events" + If one (and only one) container started from image nginxproxy/nginx-proxy:test + or nginxproxy/nginx-proxy:test-dockergen is found, wait for its log to contain + substring "Watching docker events" """ - containers = docker_client.containers.list(filters={"ancestor": "nginxproxy/nginx-proxy:test"}) + containers = docker_client.containers.list(filters={"ancestor": f"nginxproxy/nginx-proxy:{IMAGE_TAG}"}) if len(containers) != 1: return container = containers[0] @@ -371,7 +401,7 @@ def connect_to_network(network): try: my_container = docker_client.containers.get(test_container) except docker.errors.NotFound: - logging.warn(f"container {test_container} not found") + logging.warning(f"container {test_container} not found") return # figure out our container networks @@ -399,7 +429,7 @@ def disconnect_from_network(network=None): try: my_container = docker_client.containers.get(test_container) except docker.errors.NotFound: - logging.warn(f"container {test_container} not found") + logging.warning(f"container {test_container} not found") return # figure out our container networks @@ -527,7 +557,11 @@ def acme_challenge_path(): def pytest_runtest_logreport(report): if report.failed: if isinstance(report.longrepr, ReprExceptionInfo): - test_containers = docker_client.containers.list(all=True, filters={"ancestor": "nginxproxy/nginx-proxy:test"}) + nginx_containers = docker_client.containers.list(all=True, filters={"label": "com.github.nginx-proxy.nginx-proxy.nginx"}) + for container in nginx_containers: + report.longrepr.addsection('nginx container logs', container.logs()) + + test_containers = docker_client.containers.list(all=True, filters={"ancestor": f"nginxproxy/nginx-proxy:{IMAGE_TAG}"}) 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)) @@ -553,9 +587,9 @@ def pytest_runtest_setup(item): ############################################################################### try: - docker_client.images.get('nginxproxy/nginx-proxy:test') + docker_client.images.get(f"nginxproxy/nginx-proxy:{IMAGE_TAG}") except docker.errors.ImageNotFound: - pytest.exit("The docker image 'nginxproxy/nginx-proxy:test' is missing") + pytest.exit(f"The docker image 'nginxproxy/nginx-proxy:{IMAGE_TAG}' is missing") if Version(docker.__version__) < Version("7.0.0"): pytest.exit("This test suite is meant to work with the python docker module v7.0.0 or later") diff --git a/test/stress_tests/test_unreachable_network/docker-compose.yml b/test/stress_tests/test_unreachable_network/docker-compose.yml index b6407e1..177b83b 100644 --- a/test/stress_tests/test_unreachable_network/docker-compose.yml +++ b/test/stress_tests/test_unreachable_network/docker-compose.yml @@ -2,14 +2,43 @@ networks: netA: netB: +volumes: + nginx_conf: + services: reverseproxy: + profiles: + - singleContainer container_name: reverseproxy networks: - netA image: nginxproxy/nginx-proxy:test volumes: - /var/run/docker.sock:/tmp/docker.sock:ro + + reverseproxynginx: + profiles: + - separateContainers + container_name: reverseproxy + networks: + - netA + image: nginx:alpine + volumes: + - nginx_conf:/etc/nginx/conf.d:ro + labels: + - "com.github.nginx-proxy.nginx-proxy.nginx" + + docker-gen: + profiles: + - separateContainers + networks: + - netA + image: nginxproxy/nginx-proxy:test-dockergen + volumes: + - /var/run/docker.sock:/tmp/docker.sock:ro + - nginx_conf:/etc/nginx/conf.d + environment: + NGINX_CONTAINER_NAME: reverseproxy webA: networks: