Compare commits

..

No commits in common. "main" and "0.4.0" have entirely different histories.
main ... 0.4.0

306 changed files with 2184 additions and 11173 deletions

View file

@ -1,9 +1,6 @@
.git
.github
test
.dockerignore
.gitignore
*.yml
Dockerfile*
circle.yml
Makefile
README.md
test

View file

@ -1,35 +0,0 @@
# ⚠️ PLEASE READ ⚠️
## Questions or Features
If you have a question or want to request a feature, please **DO NOT SUBMIT** a new issue.
Instead please use the relevant Discussions section's category:
- 🙏 [Ask a question](https://github.com/nginx-proxy/nginx-proxy/discussions/categories/q-a)
- 💡 [Request a feature](https://github.com/nginx-proxy/nginx-proxy/discussions/categories/ideas)
## Bugs
If you are logging a bug, please search the current open issues first to see if there is already a bug opened.
For bugs, the easier you make it to reproduce the issue you see and the more initial information you provide, the easier and faster the bug can be identified and can get fixed.
Please at least provide:
- the exact nginx-proxy version you're using (if using `latest` please make sure it is up to date and provide the version number printed at container startup).
- complete configuration (compose file, command line, etc) of both your nginx-proxy container(s) and proxied containers. You should redact sensitive info if needed but please provide **full** configurations.
- generated nginx configuration obtained with `docker exec nameofyournginxproxycontainer nginx -T`
If you can provide a script or docker-compose file that reproduces the problems, that is very helpful.
## General advice about `latest`
Do not use the `latest` tag for production setups.
`latest` is nothing more than a convenient default used by Docker if no specific tag is provided, there isn't any strict convention on what goes into this tag over different projects, and it does not carry any promise of stability.
Using `latest` will most certainly put you at risk of experiencing uncontrolled updates to non backward compatible versions (or versions with breaking changes) and makes it harder for maintainers to track which exact version of the container you are experiencing an issue with.
This recommendation stands for pretty much every Docker image in existence, not just nginx-proxy's ones.
Thanks,
Nicolas

View file

@ -1,32 +0,0 @@
version: 2
updates:
# Maintain dependencies for Docker
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "daily"
commit-message:
prefix: "build"
labels:
- "type/build"
- "scope/dockerfile"
# Maintain Python dependencies (test suite)
- package-ecosystem: "pip"
directory: "/test/requirements"
schedule:
interval: "weekly"
commit-message:
prefix: "ci"
labels:
- "type/ci"
# Maintain GitHub Actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
commit-message:
prefix: "ci"
labels:
- "type/ci"

View file

@ -1,85 +0,0 @@
name: Build and publish Docker images on demand
on:
workflow_dispatch:
inputs:
image_tag:
description: "Image tag"
type: string
required: true
jobs:
multiarch-build:
name: Build and publish ${{ matrix.base }} image with tag ${{ inputs.image_tag }}
strategy:
matrix:
base: [alpine, debian]
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Retrieve nginx-proxy version
id: nginx-proxy_version
run: echo "VERSION=$(git describe --tags)" >> "$GITHUB_OUTPUT"
- 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"
- name: Get Docker tags
id: docker_meta
uses: docker/metadata-action@v5
with:
images: |
nginxproxy/nginx-proxy
tags: |
type=raw,value=${{ inputs.image_tag }},enable=${{ matrix.base == 'debian' }}
type=raw,value=${{ inputs.image_tag }},suffix=-alpine,enable=${{ matrix.base == 'alpine' }}
labels: |
org.opencontainers.image.authors=Nicolas Duchon <nicolas.duchon@gmail.com> (@buchdag), Jason Wilder
org.opencontainers.image.version=${{ steps.nginx-proxy_version.outputs.VERSION }}
flavor: |
latest=false
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push the image
id: docker_build
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile.${{ matrix.base }}
build-args: |
NGINX_PROXY_VERSION=${{ steps.nginx-proxy_version.outputs.VERSION }}
DOCKER_GEN_VERSION=${{ steps.docker-gen_version.outputs.VERSION }}
platforms: linux/amd64,linux/arm64,linux/s390x,linux/arm/v7
sbom: true
push: true
provenance: mode=max
tags: ${{ steps.docker_meta.outputs.tags }}
labels: ${{ steps.docker_meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Images digests
run: echo ${{ steps.docker_build.outputs.digest }}

View file

@ -1,101 +0,0 @@
name: Build and publish Docker images
on:
workflow_dispatch:
schedule:
- cron: "0 0 * * 1"
push:
branches:
- main
tags:
- "*.*.*"
paths-ignore:
- "test/*"
- ".gitignore"
- "docker-compose-separate-containers.yml"
- "docker-compose.yml"
- "LICENSE"
- "Makefile"
- "*.md"
jobs:
multiarch-build:
name: Build and publish image
strategy:
matrix:
base: [alpine, debian]
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Retrieve nginx-proxy version
id: nginx-proxy_version
run: echo "VERSION=$(git describe --tags)" >> "$GITHUB_OUTPUT"
- 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"
- name: Get Docker tags
id: docker_meta
uses: docker/metadata-action@v5
with:
images: |
ghcr.io/nginx-proxy/nginx-proxy
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' }}
labels: |
org.opencontainers.image.authors=Nicolas Duchon <nicolas.duchon@gmail.com> (@buchdag), Jason Wilder
org.opencontainers.image.version=${{ steps.nginx-proxy_version.outputs.VERSION }}
flavor: |
latest=false
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push the image
id: docker_build
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile.${{ matrix.base }}
build-args: |
NGINX_PROXY_VERSION=${{ steps.nginx-proxy_version.outputs.VERSION }}
DOCKER_GEN_VERSION=${{ steps.docker-gen_version.outputs.VERSION }}
platforms: linux/amd64,linux/arm64,linux/s390x,linux/arm/v7
sbom: true
push: true
provenance: mode=max
tags: ${{ steps.docker_meta.outputs.tags }}
labels: ${{ steps.docker_meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Images digests
run: echo ${{ steps.docker_build.outputs.digest }}

View file

@ -1,27 +0,0 @@
name: Update Docker Hub Description
on:
push:
branches:
- main
paths:
- README.md
- .github/workflows/dockerhub-description.yml
jobs:
dockerHubDescription:
name: Update Docker Hub Description
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Docker Hub Description
uses: peter-evans/dockerhub-description@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN_RWD }}
repository: nginxproxy/nginx-proxy
short-description: ${{ github.event.repository.description }}
enable-url-completion: true

View file

@ -1,50 +0,0 @@
name: Tests
on:
workflow_dispatch:
push:
branches:
- main
paths-ignore:
- "LICENSE"
- "**.md"
pull_request:
paths-ignore:
- "LICENSE"
- "**.md"
jobs:
unit:
name: Unit Tests
runs-on: ubuntu-latest
strategy:
matrix:
base_docker_image: [alpine, debian]
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.12
uses: actions/setup-python@v5
with:
python-version: 3.12
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r python-requirements.txt
working-directory: test/requirements
- name: Pull nginx:alpine image
run: docker pull nginx:alpine
- name: Build Docker web server image
run: make build-webserver
- name: Build Docker nginx proxy test image
run: make build-nginx-proxy-test-${{ matrix.base_docker_image }}
- name: Run tests
run: pytest
working-directory: test

4
.gitignore vendored
View file

@ -1,4 +0,0 @@
**/__pycache__/
**/.cache/
.idea/
wip

22
.travis.yml Normal file
View file

@ -0,0 +1,22 @@
sudo: required
services:
- docker
env:
global:
- DOCKER_VERSION=1.10.1-0~trusty
before_install:
# list docker-engine versions
- apt-cache madison docker-engine
# upgrade docker-engine to specific version
- sudo apt-get -o Dpkg::Options::="--force-confnew" install -y docker-engine=${DOCKER_VERSION}
- docker version
- docker info
- sudo add-apt-repository ppa:duggan/bats --yes
- sudo apt-get update -qq
- sudo apt-get install -qq bats
- make update-dependencies
script:
- make test

34
Dockerfile Normal file
View file

@ -0,0 +1,34 @@
FROM nginx:1.9.12
MAINTAINER Jason Wilder mail@jasonwilder.com
# Install wget and install/updates certificates
RUN apt-get update \
&& apt-get install -y -q --no-install-recommends \
ca-certificates \
wget \
&& apt-get clean \
&& rm -r /var/lib/apt/lists/*
# Configure Nginx and apply fix for very long server names
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.1
RUN wget https://github.com/jwilder/docker-gen/releases/download/$DOCKER_GEN_VERSION/docker-gen-linux-amd64-$DOCKER_GEN_VERSION.tar.gz \
&& tar -C /usr/local/bin -xvzf docker-gen-linux-amd64-$DOCKER_GEN_VERSION.tar.gz \
&& rm /docker-gen-linux-amd64-$DOCKER_GEN_VERSION.tar.gz
COPY . /app/
WORKDIR /app/
ENV DOCKER_HOST unix:///tmp/docker.sock
VOLUME ["/etc/nginx/certs"]
ENTRYPOINT ["/app/docker-entrypoint.sh"]
CMD ["forego", "start", "-r"]

View file

@ -1,38 +0,0 @@
FROM docker.io/nginxproxy/docker-gen:0.14.5 AS docker-gen
FROM docker.io/nginxproxy/forego:0.18.2 AS forego
# Build the final image
FROM docker.io/library/nginx:1.27.3-alpine
ARG NGINX_PROXY_VERSION
# Add DOCKER_GEN_VERSION environment variable because
# acme-companion rely on it (but the actual value is not important)
ARG DOCKER_GEN_VERSION="unknown"
ENV NGINX_PROXY_VERSION=${NGINX_PROXY_VERSION} \
DOCKER_GEN_VERSION=${DOCKER_GEN_VERSION} \
DOCKER_HOST=unix:///tmp/docker.sock
# Install dependencies
RUN apk add --no-cache --virtual .run-deps bash openssl
# Configure Nginx
RUN echo -e "\ninclude /etc/nginx/toplevel.conf.d/*.conf;" >> /etc/nginx/nginx.conf \
&& sed -i 's/worker_connections.*;$/worker_connections 10240;/' /etc/nginx/nginx.conf \
&& sed -i -e '/^\}$/{s//\}\nworker_rlimit_nofile 20480;/;:a' -e '$!N;$!ba' -e '}' /etc/nginx/nginx.conf \
&& mkdir -p '/etc/nginx/toplevel.conf.d' \
&& mkdir -p '/etc/nginx/dhparam' \
&& mkdir -p '/etc/nginx/certs' \
&& mkdir -p '/usr/share/nginx/html/errors'
# Install Forego + docker-gen
COPY --from=forego /usr/local/bin/forego /usr/local/bin/forego
COPY --from=docker-gen /usr/local/bin/docker-gen /usr/local/bin/docker-gen
COPY network_internal.conf /etc/nginx/
COPY app nginx.tmpl LICENSE /app/
WORKDIR /app/
ENTRYPOINT ["/app/docker-entrypoint.sh"]
CMD ["forego", "start", "-r"]

View file

@ -1,35 +0,0 @@
FROM docker.io/nginxproxy/docker-gen:0.14.5-debian AS docker-gen
FROM docker.io/nginxproxy/forego:0.18.2-debian AS forego
# Build the final image
FROM docker.io/library/nginx:1.27.3
ARG NGINX_PROXY_VERSION
# Add DOCKER_GEN_VERSION environment variable because
# acme-companion rely on it (but the actual value is not important)
ARG DOCKER_GEN_VERSION="unknown"
ENV NGINX_PROXY_VERSION=${NGINX_PROXY_VERSION} \
DOCKER_GEN_VERSION=${DOCKER_GEN_VERSION} \
DOCKER_HOST=unix:///tmp/docker.sock
# Configure Nginx
RUN echo "\ninclude /etc/nginx/toplevel.conf.d/*.conf;" >> /etc/nginx/nginx.conf \
&& sed -i 's/worker_connections.*;$/worker_connections 10240;/' /etc/nginx/nginx.conf \
&& sed -i -e '/^\}$/{s//\}\nworker_rlimit_nofile 20480;/;:a' -e '$!N;$!ba' -e '}' /etc/nginx/nginx.conf \
&& mkdir -p '/etc/nginx/toplevel.conf.d' \
&& mkdir -p '/etc/nginx/dhparam' \
&& mkdir -p '/etc/nginx/certs' \
&& mkdir -p '/usr/share/nginx/html/errors'
# Install Forego + docker-gen
COPY --from=forego /usr/local/bin/forego /usr/local/bin/forego
COPY --from=docker-gen /usr/local/bin/docker-gen /usr/local/bin/docker-gen
COPY network_internal.conf /etc/nginx/
COPY app nginx.tmpl LICENSE /app/
WORKDIR /app/
ENTRYPOINT ["/app/docker-entrypoint.sh"]
CMD ["forego", "start", "-r"]

View file

@ -1,7 +1,6 @@
The MIT License (MIT)
Copyright (c) 2014-2020 Jason Wilder
Copyright (c) 2021-2022 Nicolas Duchon
Copyright (c) 2014 Jason Wilder
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View file

@ -1,20 +1,14 @@
.SILENT :
.PHONY : test-debian test-alpine test
.PHONY : test
update-dependencies:
docker pull jwilder/docker-gen:0.7.1
docker pull nginx:1.9.12
docker pull python:3
docker pull rancher/socat-docker:latest
docker pull appropriate/curl:latest
docker pull docker:1.10
build-webserver:
docker build --pull -t web test/requirements/web
build-nginx-proxy-test-debian:
docker build --pull --build-arg NGINX_PROXY_VERSION="test" -f Dockerfile.debian -t nginxproxy/nginx-proxy:test .
build-nginx-proxy-test-alpine:
docker build --pull --build-arg NGINX_PROXY_VERSION="test" -f Dockerfile.alpine -t nginxproxy/nginx-proxy:test .
test-debian: build-webserver build-nginx-proxy-test-debian
test/pytest.sh
test-alpine: build-webserver build-nginx-proxy-test-alpine
test/pytest.sh
test: test-debian test-alpine
test:
docker build -t jwilder/nginx-proxy:bats .
bats test

2
Procfile Normal file
View file

@ -0,0 +1,2 @@
nginx: nginx
dockergen: docker-gen -watch -only-exposed -notify "nginx -s reload" /app/nginx.tmpl /etc/nginx/conf.d/default.conf

307
README.md
View file

@ -1,92 +1,253 @@
[![Test](https://github.com/nginx-proxy/nginx-proxy/actions/workflows/test.yml/badge.svg)](https://github.com/nginx-proxy/nginx-proxy/actions/workflows/test.yml)
[![GitHub release](https://img.shields.io/github/v/release/nginx-proxy/nginx-proxy)](https://github.com/nginx-proxy/nginx-proxy/releases)
[![nginx 1.27.3](https://img.shields.io/badge/nginx-1.27.3-brightgreen.svg?logo=nginx)](https://nginx.org/en/CHANGES)
[![Docker Image Size](https://img.shields.io/docker/image-size/nginxproxy/nginx-proxy?sort=semver)](https://hub.docker.com/r/nginxproxy/nginx-proxy "Click to view the image on Docker Hub")
[![Docker stars](https://img.shields.io/docker/stars/nginxproxy/nginx-proxy.svg)](https://hub.docker.com/r/nginxproxy/nginx-proxy "DockerHub")
[![Docker pulls](https://img.shields.io/docker/pulls/nginxproxy/nginx-proxy.svg)](https://hub.docker.com/r/nginxproxy/nginx-proxy "DockerHub")
![nginx 1.9.12](https://img.shields.io/badge/nginx-1.9.12-brightgreen.svg) ![License MIT](https://img.shields.io/badge/license-MIT-blue.svg) [![Build](https://circleci.com/gh/jwilder/nginx-proxy.svg?&style=shield&circle-token=2da3ee844076a47371bd45da81cf27409ca7306a)](https://circleci.com/gh/jwilder/nginx-proxy) [![Build Status](https://travis-ci.org/jwilder/nginx-proxy.svg?branch=master)](https://travis-ci.org/jwilder/nginx-proxy) [![](https://img.shields.io/docker/stars/jwilder/nginx-proxy.svg)](https://hub.docker.com/r/jwilder/nginx-proxy 'DockerHub') [![](https://img.shields.io/docker/pulls/jwilder/nginx-proxy.svg)](https://hub.docker.com/r/jwilder/nginx-proxy 'DockerHub')
nginx-proxy sets up a container running nginx and [docker-gen](https://github.com/nginx-proxy/docker-gen). docker-gen generates reverse proxy configs for nginx and reloads nginx when containers are started and stopped.
See [Automated Nginx Reverse Proxy for Docker](http://jasonwilder.com/blog/2014/03/25/automated-nginx-reverse-proxy-for-docker/) for why you might want to use this.
nginx-proxy sets up a container running nginx and [docker-gen][1]. docker-gen generates reverse proxy configs for nginx and reloads nginx when containers are started and stopped.
See [Automated Nginx Reverse Proxy for Docker][2] for why you might want to use this.
### Usage
To run it:
```console
docker run --detach \
--name nginx-proxy \
--publish 80:80 \
--volume /var/run/docker.sock:/tmp/docker.sock:ro \
nginxproxy/nginx-proxy:1.6
```
docker-compose
```docker-compose
services:
nginx-proxy:
image: nginxproxy/nginx-proxy
restart: always
ports:
- "80:80"
volumes:
- "/var/run/docker.sock:/tmp/docker.sock"
```
Then start any containers (here an nginx container) you want proxied with an env var `VIRTUAL_HOST=subdomain.yourdomain.com`
$ docker run -d -p 80:80 -v /var/run/docker.sock:/tmp/docker.sock:ro jwilder/nginx-proxy
Then start any containers you want proxied with an env var `VIRTUAL_HOST=subdomain.youdomain.com`
$ docker run -e VIRTUAL_HOST=foo.bar.com ...
The containers being proxied must [expose](https://docs.docker.com/reference/run/#expose-incoming-ports) the port to be proxied, either by using the `EXPOSE` directive in their `Dockerfile` or by using the `--expose` flag to `docker run` or `docker create`.
Provided your DNS is setup to forward foo.bar.com to the a host running nginx-proxy, the request will be routed to a container with the VIRTUAL_HOST env var set.
### Docker-compose
Currently this does not work with the new v2 syntax of docker-compose (due to not being compatible with the new network overlay see [#304](https://github.com/jwilder/nginx-proxy/issues/304)). It does work when using the old docker-composer syntax.
### Multiple Ports
If your container exposes multiple ports, nginx-proxy will default to the service running on port 80. If you need to specify a different port, you can set a VIRTUAL_PORT env var to select a different one. If your container only exposes one port and it has a VIRTUAL_HOST env var set, that port will be selected.
[1]: https://github.com/jwilder/docker-gen
[2]: http://jasonwilder.com/blog/2014/03/25/automated-nginx-reverse-proxy-for-docker/
### Multiple Hosts
If you need to support multiple virtual hosts for a container, you can separate each entry with commas. For example, `foo.bar.com,baz.bar.com,bar.com` and each host will be setup the same.
### Wildcard Hosts
You can also use wildcards at the beginning and the end of host name, like `*.bar.com` or `foo.bar.*`. Or even a regular expression, which can be very useful in conjunction with a wildcard DNS service like [xip.io](http://xip.io), using `~^foo\.bar\..*\.xip\.io` will match `foo.bar.127.0.0.1.xip.io`, `foo.bar.10.0.2.2.xip.io` and all other given IPs. More information about this topic can be found in the nginx documentation about [`server_names`](http://nginx.org/en/docs/http/server_names.html).
### Multiple Networks
With the addition of [overlay networking](https://docs.docker.com/engine/userguide/networking/get-started-overlay/) in Docker 1.9, your `nginx-proxy` container may need to connect to backend containers on multiple networks. By default, if you don't pass the `--net` flag when your `nginx-proxy` container is created, it will only be attached to the default `bridge` network. This means that it will not be able to connect to containers on networks other than `bridge`.
If you want your `nginx-proxy` container to be attached to a different network, you must pass the `--net=my-network` option in your `docker create` or `docker run` command. At the time of this writing, only a single network can be specified at container creation time. To attach to other networks, you can use the `docker network connect` command after your container is created:
```console
docker run --detach \
--name your-proxied-app \
--env VIRTUAL_HOST=foo.bar.com \
nginx
```
docker-compose
```docker-compose
environment:
- VIRTUAL_HOST=git.patachina.casacam.net
- VIRTUAL_PORT=3000
```
Provided your DNS is setup to resolve `foo.bar.com` to the host running nginx-proxy, a request to `http://foo.bar.com` will then be routed to a container with the `VIRTUAL_HOST` env var set to `foo.bar.com` (in this case, the **your-proxied-app** container).
The containers being proxied must :
- [expose](https://docs.docker.com/engine/reference/run/#expose-incoming-ports) the port to be proxied, either by using the `EXPOSE` directive in their `Dockerfile` or by using the `--expose` flag to `docker run` or `docker create`.
- share at least one Docker network with the nginx-proxy container: by default, if you don't pass the `--net` flag when your nginx-proxy container is created, it will only be attached to the default bridge network. This means that it will not be able to connect to containers on networks other than bridge.
Note: providing a port number in `VIRTUAL_HOST` isn't suported, please see [virtual ports](https://github.com/nginx-proxy/nginx-proxy/tree/main/docs#virtual-ports) or [custom external HTTP/HTTPS ports](https://github.com/nginx-proxy/nginx-proxy/tree/main/docs#custom-external-httphttps-ports) depending on what you want to achieve.
### Image variants
The nginx-proxy images are available in two flavors.
#### Debian based version
This image is based on the nginx:mainline image, itself based on the debian slim image.
```console
docker pull nginxproxy/nginx-proxy:1.6
$ docker run -d -p 80:80 -v /var/run/docker.sock:/tmp/docker.sock:ro \
--name my-nginx-proxy --net my-network jwilder/nginx-proxy
$ docker network connect my-other-network my-nginx-proxy
```
#### Alpine based version (`-alpine` suffix)
In this example, the `my-nginx-proxy` container will be connected to `my-network` and `my-other-network` and will be able to proxy to other containers attached to those networks.
This image is based on the nginx:alpine image.
### SSL Backends
```console
docker pull nginxproxy/nginx-proxy:1.6-alpine
If you would like to connect to your backend using HTTPS instead of HTTP, set `VIRTUAL_PROTO=https` on the backend container.
### Default Host
To set the default host for nginx use the env var `DEFAULT_HOST=foo.bar.com` for example
$ docker run -d -p 80:80 -e DEFAULT_HOST=foo.bar.com -v /var/run/docker.sock:/tmp/docker.sock:ro jwilder/nginx-proxy
### Separate Containers
nginx-proxy can also be run as two separate containers using the [jwilder/docker-gen](https://index.docker.io/u/jwilder/docker-gen/)
image and the official [nginx](https://registry.hub.docker.com/_/nginx/) image.
You may want to do this to prevent having the docker socket bound to a publicly exposed container service.
To run nginx proxy as a separate container you'll need to have [nginx.tmpl](https://github.com/jwilder/nginx-proxy/blob/master/nginx.tmpl) on your host system.
First start nginx with a volume:
$ docker run -d -p 80:80 --name nginx -v /tmp/nginx:/etc/nginx/conf.d -t nginx
Then start the docker-gen container with the shared volume and template:
```
$ docker run --volumes-from nginx \
-v /var/run/docker.sock:/tmp/docker.sock:ro \
-v $(pwd):/etc/docker-gen/templates \
-t jwilder/docker-gen -notify-sighup nginx -watch -only-exposed /etc/docker-gen/templates/nginx.tmpl /etc/nginx/conf.d/default.conf
```
> [!IMPORTANT]
>
> #### A note on `latest` and `alpine`:
>
> It is not recommended to use the `latest` (`nginxproxy/nginx-proxy`, `nginxproxy/nginx-proxy:latest`) or `alpine` (`nginxproxy/nginx-proxy:alpine`) tag for production setups.
>
> [Those tags point](https://hub.docker.com/r/nginxproxy/nginx-proxy/tags) to the latest commit in the `main` branch. They do not carry any promise of stability, and using them will probably put your nginx-proxy setup at risk of experiencing uncontrolled updates to non backward compatible versions (or versions with breaking changes). You should always specify the version you want to use explicitly to ensure your setup doesn't break when the image is updated.
Finally, start your containers with `VIRTUAL_HOST` environment variables.
### Additional documentation
$ docker run -e VIRTUAL_HOST=foo.bar.com ...
Please check the [docs section](https://github.com/nginx-proxy/nginx-proxy/tree/main/docs).
### SSL Support
### Powered by
SSL is supported using single host, wildcard and SNI certificates using naming conventions for
certificates or optionally specifying a cert name (for SNI) as an environment variable.
[![GoLand logo](https://resources.jetbrains.com/storage/products/company/brand/logos/GoLand_icon.svg)](https://www.jetbrains.com/go/)
[![PyCharm logo](https://resources.jetbrains.com/storage/products/company/brand/logos/PyCharm_icon.svg)](https://www.jetbrains.com/pycharm/)
To enable SSL:
$ docker run -d -p 80:80 -p 443:443 -v /path/to/certs:/etc/nginx/certs -v /var/run/docker.sock:/tmp/docker.sock:ro jwilder/nginx-proxy
The contents of `/path/to/certs` should contain the certificates and private keys for any virtual
hosts in use. The certificate and keys should be named after the virtual host with a `.crt` and
`.key` extension. For example, a container with `VIRTUAL_HOST=foo.bar.com` should have a
`foo.bar.com.crt` and `foo.bar.com.key` file in the certs directory.
#### Diffie-Hellman Groups
If you have Diffie-Hellman groups enabled, the files should be named after the virtual host with a
`dhparam` suffix and `.pem` extension. For example, a container with `VIRTUAL_HOST=foo.bar.com`
should have a `foo.bar.com.dhparam.pem` file in the certs directory.
#### Wildcard Certificates
Wildcard certificates and keys should be named after the domain name with a `.crt` and `.key` extension.
For example `VIRTUAL_HOST=foo.bar.com` would use cert name `bar.com.crt` and `bar.com.key`.
#### SNI
If your certificate(s) supports multiple domain names, you can start a container with `CERT_NAME=<name>`
to identify the certificate to be used. For example, a certificate for `*.foo.com` and `*.bar.com`
could be named `shared.crt` and `shared.key`. A container running with `VIRTUAL_HOST=foo.bar.com`
and `CERT_NAME=shared` will then use this shared cert.
#### How SSL Support Works
The SSL cipher configuration is based on [mozilla nginx intermediate profile](https://wiki.mozilla.org/Security/Server_Side_TLS#Nginx) which
should provide compatibility with clients back to Firefox 1, Chrome 1, IE 7, Opera 5, Safari 1,
Windows XP IE8, Android 2.3, Java 7. The configuration also enables HSTS, and SSL
session caches.
The default behavior for the proxy when port 80 and 443 are exposed is as follows:
* If a container has a usable cert, port 80 will redirect to 443 for that container so that HTTPS
is always preferred when available.
* If the container does not have a usable cert, a 503 will be returned.
Note that in the latter case, a browser may get an connection error as no certificate is available
to establish a connection. A self-signed or generic cert named `default.crt` and `default.key`
will allow a client browser to make a SSL connection (likely w/ a warning) and subsequently receive
a 503.
To serve traffic in both SSL and non-SSL modes without redirecting to SSL, you can include the
environment variable `HTTPS_METHOD=noredirect` (the default is `HTTPS_METHOD=redirect`). You can also
disable the non-SSL site entirely with `HTTPS_METHOD=nohttp`.
### Basic Authentication Support
In order to be able to secure your virtual host, you have to create a file named as its equivalent VIRTUAL_HOST variable on directory
/etc/nginx/htpasswd/$VIRTUAL_HOST
```
$ docker run -d -p 80:80 -p 443:443 \
-v /path/to/htpasswd:/etc/nginx/htpasswd \
-v /path/to/certs:/etc/nginx/certs \
-v /var/run/docker.sock:/tmp/docker.sock:ro \
jwilder/nginx-proxy
```
You'll need apache2-utils on the machine where you plan to create the htpasswd file. Follow these [instructions](http://httpd.apache.org/docs/2.2/programs/htpasswd.html)
### Custom Nginx Configuration
If you need to configure Nginx beyond what is possible using environment variables, you can provide custom configuration files on either a proxy-wide or per-`VIRTUAL_HOST` basis.
#### Replacing default proxy settings
If you want to replace the default proxy settings for the nginx container, add a configuration file at `/etc/nginx/proxy.conf`. A file with the default settings would
look like this:
```Nginx
# HTTP 1.1 support
proxy_http_version 1.1;
proxy_buffering off;
proxy_set_header Host $http_host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $proxy_connection;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
```
***NOTE***: If you provide this file it will replace the defaults; you may want to check the .tmpl file to make sure you have all of the needed options.
#### Proxy-wide
To add settings on a proxy-wide basis, add your configuration file under `/etc/nginx/conf.d` using a name ending in `.conf`.
This can be done in a derived image by creating the file in a `RUN` command or by `COPY`ing the file into `conf.d`:
```Dockerfile
FROM jwilder/nginx-proxy
RUN { \
echo 'server_tokens off;'; \
echo 'client_max_body_size 100m;'; \
} > /etc/nginx/conf.d/my_proxy.conf
```
Or it can be done by mounting in your custom configuration in your `docker run` command:
$ docker run -d -p 80:80 -p 443:443 -v /path/to/my_proxy.conf:/etc/nginx/conf.d/my_proxy.conf:ro -v /var/run/docker.sock:/tmp/docker.sock:ro jwilder/nginx-proxy
#### Per-VIRTUAL_HOST
To add settings on a per-`VIRTUAL_HOST` basis, add your configuration file under `/etc/nginx/vhost.d`. Unlike in the proxy-wide case, which allows multiple config files with any name ending in `.conf`, the per-`VIRTUAL_HOST` file must be named exactly after the `VIRTUAL_HOST`.
In order to allow virtual hosts to be dynamically configured as backends are added and removed, it makes the most sense to mount an external directory as `/etc/nginx/vhost.d` as opposed to using derived images or mounting individual configuration files.
For example, if you have a virtual host named `app.example.com`, you could provide a custom configuration for that host as follows:
$ docker run -d -p 80:80 -p 443:443 -v /path/to/vhost.d:/etc/nginx/vhost.d:ro -v /var/run/docker.sock:/tmp/docker.sock:ro jwilder/nginx-proxy
$ { echo 'server_tokens off;'; echo 'client_max_body_size 100m;'; } > /path/to/vhost.d/app.example.com
If you are using multiple hostnames for a single container (e.g. `VIRTUAL_HOST=example.com,www.example.com`), the virtual host configuration file must exist for each hostname. If you would like to use the same configuration for multiple virtual host names, you can use a symlink:
$ { echo 'server_tokens off;'; echo 'client_max_body_size 100m;'; } > /path/to/vhost.d/www.example.com
$ ln -s /path/to/vhost.d/www.example.com /path/to/vhost.d/example.com
#### Per-VIRTUAL_HOST default configuration
If you want most of your virtual hosts to use a default single configuration and then override on a few specific ones, add those settings to the `/etc/nginx/vhost.d/default` file. This file
will be used on any virtual host which does not have a `/etc/nginx/vhost.d/{VIRTUAL_HOST}` file associated with it.
#### Per-VIRTUAL_HOST location configuration
To add settings to the "location" block on a per-`VIRTUAL_HOST` basis, add your configuration file under `/etc/nginx/vhost.d`
just like the previous section except with the suffix `_location`.
For example, if you have a virtual host named `app.example.com` and you have configured a proxy_cache `my-cache` in another custom file, you could tell it to use a proxy cache as follows:
$ docker run -d -p 80:80 -p 443:443 -v /path/to/vhost.d:/etc/nginx/vhost.d:ro -v /var/run/docker.sock:/tmp/docker.sock:ro jwilder/nginx-proxy
$ { echo 'proxy_cache my-cache;'; echo 'proxy_cache_valid 200 302 60m;'; echo 'proxy_cache_valid 404 1m;' } > /path/to/vhost.d/app.example.com_location
If you are using multiple hostnames for a single container (e.g. `VIRTUAL_HOST=example.com,www.example.com`), the virtual host configuration file must exist for each hostname. If you would like to use the same configuration for multiple virtual host names, you can use a symlink:
$ { echo 'proxy_cache my-cache;'; echo 'proxy_cache_valid 200 302 60m;'; echo 'proxy_cache_valid 404 1m;' } > /path/to/vhost.d/app.example.com_location
$ ln -s /path/to/vhost.d/www.example.com /path/to/vhost.d/example.com
#### Per-VIRTUAL_HOST location default configuration
If you want most of your virtual hosts to use a default single `location` block configuration and then override on a few specific ones, add those settings to the `/etc/nginx/vhost.d/default_location` file. This file
will be used on any virtual host which does not have a `/etc/nginx/vhost.d/{VIRTUAL_HOST}` file associated with it.
### Contributing
Before submitting pull requests or issues, please check github to make sure an existing issue or pull request is not already open.
#### Running Tests Locally
To run tests, you'll need to install [bats 0.4.0](https://github.com/sstephenson/bats).
make test

View file

@ -1,2 +0,0 @@
dockergen: docker-gen -watch -notify "nginx -s reload" /app/nginx.tmpl /etc/nginx/conf.d/default.conf
nginx: nginx -g "daemon off;"

View file

@ -1,8 +0,0 @@
-----BEGIN DH PARAMETERS-----
MIIBCAKCAQEA//////////+t+FRYortKmq/cViAnPTzx2LnFg84tNpWp4TZBFGQz
+8yTnc4kmz75fS/jY2MMddj2gbICrsRhetPfHtXV/WVhJDP1H18GbtCFY2VVPe0a
87VXE15/V8k1mE8McODmi3fipona8+/och3xWKE2rec1MKzKT0g6eXq8CrGCsyT7
YdEIqUuyyOP7uWrat2DX9GgdT0Kj3jlN9K5W7edjcrsZCwenyO4KbXCeAvzhzffi
7MA0BM0oNC9hkXL+nOmFg/+OTxIy7vKBg8P+OxtMb61zO7X8vC7CIAXFjvGDfRaD
ssbzSibBsu/6iGtCOGEoXJf//////////wIBAg==
-----END DH PARAMETERS-----

View file

@ -1,11 +0,0 @@
-----BEGIN DH PARAMETERS-----
MIIBiAKCAYEA//////////+t+FRYortKmq/cViAnPTzx2LnFg84tNpWp4TZBFGQz
+8yTnc4kmz75fS/jY2MMddj2gbICrsRhetPfHtXV/WVhJDP1H18GbtCFY2VVPe0a
87VXE15/V8k1mE8McODmi3fipona8+/och3xWKE2rec1MKzKT0g6eXq8CrGCsyT7
YdEIqUuyyOP7uWrat2DX9GgdT0Kj3jlN9K5W7edjcrsZCwenyO4KbXCeAvzhzffi
7MA0BM0oNC9hkXL+nOmFg/+OTxIy7vKBg8P+OxtMb61zO7X8vC7CIAXFjvGDfRaD
ssbzSibBsu/6iGtCOGEfz9zeNVs7ZRkDW7w09N75nAI4YbRvydbmyQd62R0mkff3
7lmMsPrBhtkcrv4TCYUTknC0EwyTvEN5RPT9RFLi103TZPLiHnH1S/9croKrnJ32
nuhtK8UiNjoNq8Uhl5sN6todv5pC1cRITgq80Gv6U93vPBsg7j/VnXwl5B0rZsYu
N///////////AgEC
-----END DH PARAMETERS-----

View file

@ -1,13 +0,0 @@
-----BEGIN DH PARAMETERS-----
MIICCAKCAgEA//////////+t+FRYortKmq/cViAnPTzx2LnFg84tNpWp4TZBFGQz
+8yTnc4kmz75fS/jY2MMddj2gbICrsRhetPfHtXV/WVhJDP1H18GbtCFY2VVPe0a
87VXE15/V8k1mE8McODmi3fipona8+/och3xWKE2rec1MKzKT0g6eXq8CrGCsyT7
YdEIqUuyyOP7uWrat2DX9GgdT0Kj3jlN9K5W7edjcrsZCwenyO4KbXCeAvzhzffi
7MA0BM0oNC9hkXL+nOmFg/+OTxIy7vKBg8P+OxtMb61zO7X8vC7CIAXFjvGDfRaD
ssbzSibBsu/6iGtCOGEfz9zeNVs7ZRkDW7w09N75nAI4YbRvydbmyQd62R0mkff3
7lmMsPrBhtkcrv4TCYUTknC0EwyTvEN5RPT9RFLi103TZPLiHnH1S/9croKrnJ32
nuhtK8UiNjoNq8Uhl5sN6todv5pC1cRITgq80Gv6U93vPBsg7j/VnXwl5B0rZp4e
8W5vUsMWTfT7eTDp5OWIV7asfV9C1p9tGHdjzx1VA0AEh/VbpX4xzHpxNciG77Qx
iu1qHgEtnmgyqQdgCpGBMMRtx3j5ca0AOAkpmaMzy4t6Gh25PXFAADwqTs6p+Y0K
zAqCkc3OyX3Pjsm1Wn+IpGtNtahR9EGC4caKAH5eZV9q//////////8CAQI=
-----END DH PARAMETERS-----

View file

@ -1,121 +0,0 @@
#!/bin/bash
set -e
function _parse_true() {
case "$1" in
true | True | TRUE | 1)
return 0
;;
*)
return 1
;;
esac
}
function _parse_false() {
case "$1" in
false | False | FALSE | 0)
return 0
;;
*)
return 1
;;
esac
}
function _print_version {
if [[ -n "${NGINX_PROXY_VERSION:-}" ]]; then
echo "Info: running nginx-proxy version ${NGINX_PROXY_VERSION}"
fi
}
function _check_unix_socket() {
# Warn if the DOCKER_HOST socket does not exist
if [[ ${DOCKER_HOST} == unix://* ]]; then
local SOCKET_FILE="${DOCKER_HOST#unix://}"
if [[ ! -S ${SOCKET_FILE} ]]; then
cat >&2 <<-EOT
ERROR: you need to share your Docker host socket with a volume at ${SOCKET_FILE}
Typically you should run your nginxproxy/nginx-proxy with: \`-v /var/run/docker.sock:${SOCKET_FILE}:ro\`
See the documentation at: https://github.com/nginx-proxy/nginx-proxy/#usage
EOT
exit 1
fi
fi
}
function _resolvers() {
# Compute the DNS resolvers for use in the templates - if the IP contains ":", it's IPv6 and must be enclosed in []
RESOLVERS=$(awk '$1 == "nameserver" {print ($2 ~ ":")? "["$2"]": $2}' ORS=' ' /etc/resolv.conf | sed 's/ *$//g'); export RESOLVERS
SCOPED_IPV6_REGEX='\[fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}\]'
if [[ -z ${RESOLVERS} ]]; then
echo 'Warning: unable to determine DNS resolvers for nginx' >&2
unset RESOLVERS
elif [[ ${RESOLVERS} =~ ${SCOPED_IPV6_REGEX} ]]; then
echo -n 'Warning: Scoped IPv6 addresses removed from resolvers: ' >&2
echo "${RESOLVERS}" | grep -Eo "$SCOPED_IPV6_REGEX" | paste -s -d ' ' >&2
RESOLVERS=$(echo "${RESOLVERS}" | sed -r "s/${SCOPED_IPV6_REGEX}//g" | xargs echo -n); export RESOLVERS
fi
}
function _setup_dhparam() {
# DH params will be supplied for nginx here:
local DHPARAM_FILE='/etc/nginx/dhparam/dhparam.pem'
# Should be 2048, 3072, or 4096 (default):
local FFDHE_GROUP="${DHPARAM_BITS:=4096}"
# DH params may be provided by the user (rarely necessary)
if [[ -f ${DHPARAM_FILE} ]]; then
echo 'Warning: A custom dhparam.pem file was provided. Best practice is to use standardized RFC7919 DHE groups instead.' >&2
return 0
elif _parse_true "${DHPARAM_SKIP:=false}"; then
echo 'Skipping Diffie-Hellman parameters setup.'
return 0
elif _parse_false "${DHPARAM_GENERATION:=true}"; then
echo 'Warning: The DHPARAM_GENERATION environment variable is deprecated, please consider using DHPARAM_SKIP set to true instead.' >&2
echo 'Skipping Diffie-Hellman parameters setup.'
return 0
elif [[ ! ${DHPARAM_BITS} =~ ^(2048|3072|4096)$ ]]; then
echo "ERROR: Unsupported DHPARAM_BITS size: ${DHPARAM_BITS}. Use: 2048, 3072, or 4096 (default)." >&2
exit 1
fi
echo 'Setting up DH Parameters..'
# Use an existing pre-generated DH group from RFC7919 (https://datatracker.ietf.org/doc/html/rfc7919#appendix-A):
local RFC7919_DHPARAM_FILE="/app/dhparam/ffdhe${FFDHE_GROUP}.pem"
# Provide the DH params file to nginx:
cp "${RFC7919_DHPARAM_FILE}" "${DHPARAM_FILE}"
}
# Run the init logic if the default CMD was provided
if [[ $* == 'forego start -r' ]]; then
_print_version
_check_unix_socket
_resolvers
_setup_dhparam
if [ -z "${TRUST_DOWNSTREAM_PROXY}" ]; then
cat >&2 <<-EOT
Warning: TRUST_DOWNSTREAM_PROXY is not set; defaulting to "true". For security, you should explicitly set TRUST_DOWNSTREAM_PROXY to "false" if there is not a trusted reverse proxy in front of this proxy.
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
fi
exec "$@"

View file

@ -1,24 +0,0 @@
volumes:
nginx_conf:
services:
nginx:
image: nginx
container_name: nginx
ports:
- "80:80"
volumes:
- nginx_conf:/etc/nginx/conf.d:ro
dockergen:
image: nginxproxy/docker-gen
command: -notify-sighup nginx -watch /etc/docker-gen/templates/nginx.tmpl /etc/nginx/conf.d/default.conf
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
- ./nginx.tmpl:/etc/docker-gen/templates/nginx.tmpl
- nginx_conf:/etc/nginx/conf.d
whoami:
image: jwilder/whoami
environment:
- VIRTUAL_HOST=whoami.example

View file

@ -1,16 +0,0 @@
services:
nginx-proxy:
image: nginxproxy/nginx-proxy
container_name: nginx-proxy
ports:
- "80:80"
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
# if you want to proxy based on host ports, you'll want to use the host network
# network_mode: "host"
whoami:
image: jwilder/whoami
environment:
- VIRTUAL_HOST=whoami.example

22
docker-entrypoint.sh Executable file
View file

@ -0,0 +1,22 @@
#!/bin/bash
set -e
# Warn if the DOCKER_HOST socket does not exist
if [[ $DOCKER_HOST == unix://* ]]; then
socket_file=${DOCKER_HOST#unix://}
if ! [ -S $socket_file ]; then
cat >&2 <<-EOT
ERROR: you need to share your Docker host socket with a volume at $socket_file
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
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 "$@"

File diff suppressed because it is too large Load diff

View file

@ -1,7 +0,0 @@
# Only allow traffic from internal clients
allow 127.0.0.0/8;
allow 10.0.0.0/8;
allow 192.168.0.0/16;
allow 172.16.0.0/12;
allow fc00::/7; # IPv6 local address range
deny all;

1163
nginx.tmpl

File diff suppressed because it is too large Load diff

View file

@ -1,143 +1,14 @@
Nginx proxy test suite
======================
Test suite
==========
Install requirements
--------------------
This test suite is implemented on top of the [Bats](https://github.com/sstephenson/bats/blob/master/README.md) test framework.
You need [Docker Compose v2](https://docs.docker.com/compose/install/linux/), [python 3.9](https://www.python.org/) and [pip](https://pip.pypa.io/en/stable/installation/) installed. Then run the commands:
It is intended to verify the correct behavior of the Docker image `jwilder/nginx-proxy:bats`.
pip install -r requirements/python-requirements.txt
Running the test suite
----------------------
Prepare the nginx-proxy test image
----------------------------------
Make sure you have Bats installed, then run:
make build-nginx-proxy-test-debian
or if you want to test the alpine flavor:
make build-nginx-proxy-test-alpine
Run the test suite
------------------
pytest
need more verbosity ?
pytest -s
Note: By default this test suite relies on Docker Compose v2 with the command `docker compose`. It still supports Docker Compose v1 via the `DOCKER_COMPOSE` environment variable:
DOCKER_COMPOSE=docker-compose pytest
Run one single test module
--------------------------
pytest test_nominal.py
Run the test suite from a Docker container
------------------------------------------
If you cannot (or don't want to) install pytest and its requirements on your computer. You can use the nginx-proxy-tester docker image to run the test suite from a Docker container.
make test-debian
or if you want to test the alpine flavor:
make test-alpine
Write a test module
-------------------
This test suite uses [pytest](http://doc.pytest.org/en/latest/). The [conftest.py](conftest.py) file will be automatically loaded by pytest and will provide you with two useful pytest [fixtures](https://docs.pytest.org/en/latest/explanation/fixtures.html):
- docker_compose
- nginxproxy
### docker_compose fixture
When using the `docker_compose` fixture in a test, pytest will try to start the [Docker Compose](https://docs.docker.com/compose/) services corresponding to the current test module, based on the test module filename.
By default, if your test module file is `test/test_subdir/test_example.py`, then the `docker_compose` fixture will try to load the following files, [merging them](https://docs.docker.com/reference/compose-file/merge/) in this order:
1. `test/compose.base.yml`
2. `test/test_subdir/compose.base.override.yml` (if it exists)
3. `test/test_subdir/test_example.yml`
The fixture will run the _docker compose_ command with the `-f` option to load the given compose files. So you can test your docker compose file syntax by running it yourself with:
docker compose -f test/compose.base.yml -f test/test_subdir/test_example.yml up -d
The first file contains the base configuration of the nginx-proxy container common to most tests:
```yaml
services:
nginx-proxy:
image: nginxproxy/nginx-proxy:test
container_name: nginx-proxy
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
ports:
- "80:80"
- "443:443"
```
The second optional file allow you to override this base configuration for all test modules in a subfolder.
The third file contains the services and overrides specific to a given test module.
This automatic merge can be bypassed by using a file named `test_example.base.yml` (instead of `test_example.yml`). When this file exist, it will be the only one used by the test and no merge with other compose files will automatically occur.
The `docker_compose` fixture also set the `PYTEST_MODULE_PATH` environment variable to the absolute path of the current test module directory, so it can be used to mount files or directory relatives to the current test.
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/4.4.4/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 `127.0.0.1` if the tests are executed on a Darwin (macOS) system, otherwise the IP address of the container that was created from the `nginxproxy/nginx-proxy:test` image.
So, in tests, all the following domain names will resolve to either localhost or the nginx-proxy container's IP:
- `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 `127.0.0.1` if the tests are executed on a Darwin (macOS) system, otherwise the IP address of the container named `XXX`.
So, on a non-Darwin system:
- `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
The `nginxproxy` fixture will provide you with a replacement for the python [requests](https://pypi.python.org/pypi/requests/) module. This replacement will just repeat up to 30 times a requests if it receives the HTTP error 404 or 502. This error occurs when you try to send queries to nginx-proxy too early after the container creation.
Also this requests replacement is preconfigured to use the Certificate Authority root certificate [certs/ca-root.crt](certs/) to validate https connections.
Furthermore, the nginxproxy methods accept an additional keyword parameter: `ipv6` which forces requests made against containers to use the containers IPv6 address when set to `True`. If IPv6 is not supported by the system or docker, that particular test will be skipped.
def test_forwards_to_web1_ipv6(docker_compose, nginxproxy):
r = nginxproxy.get("http://web1.nginx-proxy.tld/port", ipv6=True)
assert r.status_code == 200
assert r.text == "answer from port 81\n"
### The web docker image
When you run the `make build-webserver` command, you built a [`web`](requirements/README.md) docker image which is convenient for running a small web server in a container. This image can produce containers that listens on multiple ports at the same time.
### Testing TLS
If you need to create server certificates, use the [`certs/create_server_certificate.sh`](certs/) script. Pytest will be able to validate any certificate issued from this script.
docker build -t jwilder/nginx-proxy:bats .
bats test/

View file

@ -1,81 +0,0 @@
create_server_certificate.sh
============================
`create_server_certificate.sh` is a script helping with issuing server certificates that can be used to provide TLS on web servers.
It also creates a Certificate Authority (CA) root key and certificate. This CA root certificate can be used to validate the server certificates it generates.
For instance, with _curl_:
curl --cacert /somewhere/ca-root.crt https://www.example.com/
or with _wget_:
wget --certificate=/somewhere/ca-root.crt https://www.example.com/
or with the python _requests_ module:
import requests
r = requests.get("https://www.example.com", verify="/somewhere/ca-root.crt")
Usage
-----
### Simple domain
Create a server certificate for domain `www.example.com`:
./create_server_certificate.sh www.example.com
Will produce:
- `www.example.com.key`
- `www.example.com.crt`
### Multiple domains
Create a server certificate for main domain `www.example.com` and alternative domains `example.com`, `foo.com` and `bar.com`:
./create_server_certificate.sh www.example.com foo.com bar.com
Will produce:
- `www.example.com.key`
- `www.example.com.crt`
### Wildcard domain
Create a server certificate for wildcard domain `*.example.com`:
./create_server_certificate.sh "*.example.com"
Note that you need to use quotes around the domain string or the shell would expand `*`.
Will produce:
- `*.example.com.key`
- `*.example.com.crt`
Again, to prevent your shell from expanding `*`, use quotes. i.e.: `cat "*.example.com.crt"`.
Such a server certificate would be valid for domains:
- `foo.example.com`
- `bar.example.com`
but not for domains:
- `example.com`
- `foo.bar.example.com`
### Wildcard domain on multiple levels
While you can technically create a server certificate for wildcard domain `*.example.com` and alternative name `*.*.example.com`, client implementations generally do not support multiple wildcards in a domain name.
For instance, a python script using urllib3 would fail to validate domain `foo.bar.example.com` presenting a certificate with name `*.*.example.com`. It is advised to stay away from producing such certificates.
If you want to give it a try:
./create_server_certificate.sh "*.example.com" "*.*.example.com"
Such a server certificate would be valid for domains:
- `foo.example.com`
- `bar.example.com`
- `foo.bar.example.com`

View file

@ -1,21 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIDZDCCAkygAwIBAgIJAJmW6Ju6iJNNMA0GCSqGSIb3DQEBCwUAMD8xHzAdBgNV
BAoMFm5naW54LXByb3h5IHRlc3Qgc3VpdGUxHDAaBgNVBAMME3d3dy5uZ2lueC1w
cm94eS50bGQwHhcNMTcwMTEwMDAwODUxWhcNMjcwMTA4MDAwODUxWjA/MR8wHQYD
VQQKDBZuZ2lueC1wcm94eSB0ZXN0IHN1aXRlMRwwGgYDVQQDDBN3d3cubmdpbngt
cHJveHkudGxkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAndjE3OPr
48hIOQigk/HejrowsQDLNfkkc6vej0J983rJitGTgBfxqq27fOPfqhE5bi1M5JDk
KkrOrSitxCJLgpq+4Ls9/RXg8skZFHRAQbNwuKBehaDkPdamJ0i3dv6e4kZy41oI
RqxQ/MKdminC4LShFZvPoKeh9ae7w1MgB2/4E68LO66bYiHlLNL7ENViSHhLyCmt
qIE7kdV9jgn2NuVJ37m6/6SNQ3GBiIjEW+ooRQ3HEVKBCismcwq80+BD5VS/yW18
KqX8m4sBM+IgZbcOqrV+APMbGvd8iNJgQSSQC/r0Wscgt7UeggVYKDazjDSPvLUE
FUN5wEmydkP2AQIDAQABo2MwYTAdBgNVHQ4EFgQUJL59pHomt+8dUNxv8HgrYjKf
OA8wHwYDVR0jBBgwFoAUJL59pHomt+8dUNxv8HgrYjKfOA8wDwYDVR0TAQH/BAUw
AwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQADggEBABALxY96YqsZ
CL2crzY0FIGhfjLE7P3mtUGklUpFu7xyI6mGUyL1nJYSnHB5IEV6QLhVVUE/CojI
crXorQWBDkx26AgCt/eIOdvPYC0JDeXiIhH6sld3yH7JGwGqJkfXaUUfUkuwMae7
mMIEG9e6vfSh/YNTRxs0KBjBcXHHl5K+Dz4h9r14OqnQFqVFZaR6T6td44tDDNhn
beW8iIfCWRqDsnvIcJzLa2QR4onmJSw5DaSeFFaKefhdHEzEBZntLfyFbjRYHT/O
+BRdewhg6rSDkGLcL8n/ZnRLOa+xmegjQ/Op94OmWO3TfXOITJAtkaO2YVZoyek8
T6ckVovq4zU=
-----END CERTIFICATE-----

View file

@ -1,27 +0,0 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEAndjE3OPr48hIOQigk/HejrowsQDLNfkkc6vej0J983rJitGT
gBfxqq27fOPfqhE5bi1M5JDkKkrOrSitxCJLgpq+4Ls9/RXg8skZFHRAQbNwuKBe
haDkPdamJ0i3dv6e4kZy41oIRqxQ/MKdminC4LShFZvPoKeh9ae7w1MgB2/4E68L
O66bYiHlLNL7ENViSHhLyCmtqIE7kdV9jgn2NuVJ37m6/6SNQ3GBiIjEW+ooRQ3H
EVKBCismcwq80+BD5VS/yW18KqX8m4sBM+IgZbcOqrV+APMbGvd8iNJgQSSQC/r0
Wscgt7UeggVYKDazjDSPvLUEFUN5wEmydkP2AQIDAQABAoIBAQCDM4zetix6lx1B
GuSuVFrTc/vJBInkgQRFiVRi67fZS/R+CJl73WsonWO7+YUNzWdZJxpE2hJs/OUx
lSBqaL8u/gUuszRhS3BBHdpU4BQRCF/ndpVaqVNN+z78ZDrrE9Vo63nPdCRw6gYf
MnzhiVjMghdq6Kn6NZwvno45WrzCsIbrrQ4zU+S2PhG8MTA53jzqqQ8mUSJX0lAl
6b9+1aWA0d0Jnk3M3doaFU/Dlnz3n6kkx0AdqNe8bdsFrPfwsrF+dwGx04SGgLmK
V2OjIDFYYGtiHp3PJ9IYIA32ij+UloSDDZ2BxXkma8Zilw04ytY5l8tlk2ZDWTD9
U2MXxjmBAoGBAMmmI19I/asTPjljlqzrOsrdRkklJvnCHgy/yw9u3nMfkJ0lLGAp
mZoCqJIEsAqlLGM5bOjKy3KQ3n2SBX3mz7/RajnpJRTnNLeJIPAAXHN9TDyKcWRo
Los6xHN7YMSLYKs4HMihXp9Yu4Ms88/8nO/01nufjN0rTgFnWdL0WfxJAoGBAMhk
Qm92ukMmbrXSrV0WF+eFooHwgPmUWZ1oZY5ZHmO3FCuSBHiICGrWKmdbcG6H5zmZ
oFZ0unsvk2Yjl+/+tntxr/dwp6Q+chsqkLms8GE76NWEO8qn4hQNywkFgpKlPci3
n5IqpuQ2DpJ1PAQkwgZD/5rSscNidNMezXO5Uvv5AoGBALR291kjXcJpKlr6AbMn
oipD9c8obMVBMNuAGh7pvjORoD7DMf+tu0XV8z8a6uHcCOmUTx/XvlP9yuDeegO/
OVYV+NdzDDi04r0PAGdKK3NAQ6Y60Fhn1J/OLFqdpHDBu/X/9eKoaKJ7KvWumVUe
YuVtXTauB8c4JkujTwQ4ov/hAoGAHxvhbGhkFhSbT0K7gx3w7BJE3iM2AojTOKqC
SYzwOM6tJO5wHz4PAHbq8kyxsZcLgFenGoTYhlMmcM7JwYorThKiHKmyfL7s++ap
vQlp785bIPp8RcO2RyK1CFuAn79jTgujjA9vBTKXJIlqncIPFOXtgl1/FzPrqvK3
NmXoyhECgYEAje9hM9RYO0jbfmTZoQh+onMRz34SM9XWLH+NQGgfvsGtjeRnrUKK
GuWQz/GQGJLy/Uc1KHIdrfPDjvQhZXmPL1v7pNfCrqyj+EnKCNDPPnYq5Zq4WLsB
x1hKPH0LmfEBkXOiFGrD3h3KAuBK5nb0/EFBDR4JuMaySC5CpbOds9o=
-----END RSA PRIVATE KEY-----

View file

@ -1,183 +0,0 @@
#!/bin/bash
set -u
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
if [[ "$#" -eq 0 ]]; then
cat <<-EOF
To generate a server certificate, provide the domain name as a parameter:
$(basename $0) www.my-domain.tdl
$(basename $0) www.my-domain.tdl alternate.domain.tld
You can also create certificates for wildcard domains:
$(basename $0) '*.my-domain.tdl'
EOF
exit 0
else
DOMAIN="$1"
ALTERNATE_DOMAINS="DNS:$( echo "$@" | sed 's/ /,DNS:/g')"
fi
###############################################################################
# Create a nginx container (which conveniently provides the `openssl` command)
###############################################################################
CONTAINER=$(docker run -d -v $DIR:/work -w /work -e SAN="$ALTERNATE_DOMAINS" nginx:1.27.3)
# Configure openssl
docker exec $CONTAINER bash -c '
mkdir -p /ca/{certs,crl,private,newcerts} 2>/dev/null
echo 1000 > /ca/serial
touch /ca/index.txt
cat > /ca/openssl.cnf <<-"OESCRIPT"
[ ca ]
# `man ca`
default_ca = CA_default
[ CA_default ]
# Directory and file locations.
dir = /ca
certs = $dir/certs
crl_dir = $dir/crl
new_certs_dir = $dir/newcerts
database = $dir/index.txt
serial = $dir/serial
RANDFILE = $dir/private/.rand
# The root key and root certificate.
private_key = /work/ca-root.key
certificate = /work/ca-root.crt
# SHA-1 is deprecated, so use SHA-2 instead.
default_md = sha256
name_opt = ca_default
cert_opt = ca_default
default_days = 10000
preserve = no
policy = policy_loose
[ policy_loose ]
countryName = optional
stateOrProvinceName = optional
localityName = optional
organizationName = optional
organizationalUnitName = optional
commonName = supplied
emailAddress = optional
[ req ]
# Options for the `req` tool (`man req`).
default_bits = 2048
distinguished_name = req_distinguished_name
string_mask = utf8only
# SHA-1 is deprecated, so use SHA-2 instead.
default_md = sha256
# Extension to add when the -x509 option is used.
x509_extensions = v3_ca
[ req_distinguished_name ]
# See <https://en.wikipedia.org/wiki/Certificate_signing_request>.
countryName = Country Name (2 letter code)
stateOrProvinceName = State or Province Name
localityName = Locality Name
0.organizationName = Organization Name
organizationalUnitName = Organizational Unit Name
commonName = Common Name
emailAddress = Email Address
[ v3_ca ]
# Extensions for a typical CA (`man x509v3_config`).
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = critical, CA:true
keyUsage = critical, digitalSignature, cRLSign, keyCertSign
[ server_cert ]
# Extensions for server certificates (`man x509v3_config`).
basicConstraints = CA:FALSE
nsCertType = server
nsComment = server certificate generated for test purpose (nginx-proxy test suite)
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer:always
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
[ san_env ]
subjectAltName=${ENV::SAN}
OESCRIPT
'
# shortcut for calling `openssl` inside the container
function openssl {
docker exec $CONTAINER openssl "$@"
}
function exitfail {
echo
echo ERROR: "$@"
docker rm -f $CONTAINER
exit 1
}
###############################################################################
# Setup Certificate authority
###############################################################################
if ! [[ -f "$DIR/ca-root.key" ]]; then
echo
echo "> Create a Certificate Authority root key: $DIR/ca-root.key"
openssl genrsa -out ca-root.key 2048
[[ $? -eq 0 ]] || exitfail failed to generate CA root key
fi
# Create a CA root certificate
if ! [[ -f "$DIR/ca-root.crt" ]]; then
echo
echo "> Create a CA root certificate: $DIR/ca-root.crt"
openssl req -config /ca/openssl.cnf \
-key ca-root.key \
-new -x509 -days 3650 -subj "/O=nginx-proxy test suite/CN=www.nginx-proxy.tld" -extensions v3_ca \
-out ca-root.crt
[[ $? -eq 0 ]] || exitfail failed to generate CA root certificate
# Verify certificate
openssl x509 -noout -text -in ca-root.crt
fi
###############################################################################
# create server key and certificate signed by the certificate authority
###############################################################################
echo
echo "> Create a host key: $DIR/$DOMAIN.key"
openssl genrsa -out "$DOMAIN.key" 2048
echo
echo "> Create a host certificate signing request"
SAN="$ALTERNATE_DOMAINS" openssl req -config /ca/openssl.cnf \
-key "$DOMAIN.key" \
-new -out "/ca/$DOMAIN.csr" -days 1000 -extensions san_env -subj "/CN=$DOMAIN"
[[ $? -eq 0 ]] || exitfail failed to generate server certificate signing request
echo
echo "> Create server certificate: $DIR/$DOMAIN.crt"
SAN="$ALTERNATE_DOMAINS" openssl ca -config /ca/openssl.cnf -batch \
-extensions server_cert \
-extensions san_env \
-in "/ca/$DOMAIN.csr" \
-out "$DOMAIN.crt"
[[ $? -eq 0 ]] || exitfail failed to generate server certificate
# Verify host certificate
#openssl x509 -noout -text -in "$DOMAIN.crt"
docker rm -f $CONTAINER >/dev/null

View file

@ -1,9 +0,0 @@
services:
nginx-proxy:
image: nginxproxy/nginx-proxy:test
container_name: nginx-proxy
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
ports:
- "80:80"
- "443:443"

View file

@ -1,620 +0,0 @@
import contextlib
import logging
import os
import pathlib
import platform
import re
import shlex
import socket
import subprocess
import time
from io import StringIO
from typing import Iterator, List, Optional
import backoff
import docker.errors
import pytest
import requests
from _pytest.fixtures import FixtureRequest
from docker import DockerClient
from docker.models.containers import Container
from docker.models.networks import Network
from packaging.version import Version
from requests import Response
from 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('requests.packages.urllib3.connectionpool').setLevel(logging.WARN)
CA_ROOT_CERTIFICATE = pathlib.Path(__file__).parent.joinpath("certs/ca-root.crt")
PYTEST_RUNNING_IN_CONTAINER = os.environ.get('PYTEST_RUNNING_IN_CONTAINER') == "1"
FORCE_CONTAINER_IPV6 = False # ugly global state to consider containers' IPv6 address instead of IPv4
DOCKER_COMPOSE = os.environ.get('DOCKER_COMPOSE', 'docker compose')
docker_client = docker.from_env()
# Name of pytest container to reference if it's being used for running tests
test_container = 'nginx-proxy-pytest'
###############################################################################
#
# utilities
#
###############################################################################
@contextlib.contextmanager
def ipv6(force_ipv6: bool = True):
"""
Meant to be used as a context manager to force IPv6 sockets:
with ipv6():
nginxproxy.get("http://something.nginx-proxy.example") # force use of IPv6
with ipv6(False):
nginxproxy.get("http://something.nginx-proxy.example") # legacy behavior
"""
global FORCE_CONTAINER_IPV6
FORCE_CONTAINER_IPV6 = force_ipv6
yield
FORCE_CONTAINER_IPV6 = False
class RequestsForDocker:
"""
Proxy for calling methods of the requests module.
When an HTTP response failed due to HTTP Error 404 or 502, retry a few times.
Provides method `get_conf` to extract the nginx-proxy configuration content.
"""
def __init__(self):
self.session = requests.Session()
if CA_ROOT_CERTIFICATE.is_file():
self.session.verify = CA_ROOT_CERTIFICATE.as_posix()
@staticmethod
def get_nginx_proxy_container() -> Container:
"""
Return list of containers
"""
nginx_proxy_containers = docker_client.containers.list(filters={"ancestor": "nginxproxy/nginx-proxy:test"})
if len(nginx_proxy_containers) > 1:
pytest.fail("Too many running nginxproxy/nginx-proxy:test containers", pytrace=False)
elif len(nginx_proxy_containers) == 0:
pytest.fail("No running nginxproxy/nginx-proxy:test container", pytrace=False)
return nginx_proxy_containers.pop()
def get_conf(self) -> bytes:
"""
Return the nginx config file
"""
nginx_proxy_container = self.get_nginx_proxy_container()
return get_nginx_conf_from_container(nginx_proxy_container)
def get_ip(self) -> str:
"""
Return the nginx container ip address
"""
nginx_proxy_container = self.get_nginx_proxy_container()
return container_ip(nginx_proxy_container)
def get(self, *args, **kwargs) -> Response:
with ipv6(kwargs.pop('ipv6', False)):
@backoff.on_predicate(backoff.constant, lambda r: r.status_code in (404, 502), interval=.3, max_tries=30, jitter=None)
def _get(*_args, **_kwargs):
return self.session.get(*_args, **_kwargs)
return _get(*args, **kwargs)
def post(self, *args, **kwargs) -> Response:
with ipv6(kwargs.pop('ipv6', False)):
@backoff.on_predicate(backoff.constant, lambda r: r.status_code in (404, 502), interval=.3, max_tries=30, jitter=None)
def _post(*_args, **_kwargs):
return self.session.post(*_args, **_kwargs)
return _post(*args, **kwargs)
def put(self, *args, **kwargs) -> Response:
with ipv6(kwargs.pop('ipv6', False)):
@backoff.on_predicate(backoff.constant, lambda r: r.status_code in (404, 502), interval=.3, max_tries=30, jitter=None)
def _put(*_args, **_kwargs):
return self.session.put(*_args, **_kwargs)
return _put(*args, **kwargs)
def head(self, *args, **kwargs) -> Response:
with ipv6(kwargs.pop('ipv6', False)):
@backoff.on_predicate(backoff.constant, lambda r: r.status_code in (404, 502), interval=.3, max_tries=30, jitter=None)
def _head(*_args, **_kwargs):
return self.session.head(*_args, **_kwargs)
return _head(*args, **kwargs)
def delete(self, *args, **kwargs) -> Response:
with ipv6(kwargs.pop('ipv6', False)):
@backoff.on_predicate(backoff.constant, lambda r: r.status_code in (404, 502), interval=.3, max_tries=30, jitter=None)
def _delete(*_args, **_kwargs):
return self.session.delete(*_args, **_kwargs)
return _delete(*args, **kwargs)
def options(self, *args, **kwargs) -> Response:
with ipv6(kwargs.pop('ipv6', False)):
@backoff.on_predicate(backoff.constant, lambda r: r.status_code in (404, 502), interval=.3, max_tries=30, jitter=None)
def _options(*_args, **_kwargs):
return self.session.options(*_args, **_kwargs)
return _options(*args, **kwargs)
def __getattr__(self, name):
return getattr(requests, name)
def container_ip(container: Container) -> str:
"""
return the IP address of a container.
If the global FORCE_CONTAINER_IPV6 flag is set, return the IPv6 address
"""
global FORCE_CONTAINER_IPV6
if FORCE_CONTAINER_IPV6:
if not HAS_IPV6:
pytest.skip("This system does not support IPv6")
ip = container_ipv6(container)
if ip == '':
pytest.skip(f"Container {container.name} has no IPv6 address")
else:
return ip
else:
net_info = container.attrs["NetworkSettings"]["Networks"]
if "bridge" in net_info:
return net_info["bridge"]["IPAddress"]
# container is running in host network mode
if "host" in net_info:
return "127.0.0.1"
# not default bridge network, fallback on first network defined
network_name = list(net_info.keys())[0]
return net_info[network_name]["IPAddress"]
def container_ipv6(container: Container) -> str:
"""
return the IPv6 address of a container.
"""
net_info = container.attrs["NetworkSettings"]["Networks"]
if "bridge" in net_info:
return net_info["bridge"]["GlobalIPv6Address"]
# container is running in host network mode
if "host" in net_info:
return "::1"
# not default bridge network, fallback on first network defined
network_name = list(net_info.keys())[0]
return net_info[network_name]["GlobalIPv6Address"]
def nginx_proxy_dns_resolver(domain_name: str) -> Optional[str]:
"""
if "nginx-proxy" if found in host, return the ip address of the docker container
issued from the docker image nginxproxy/nginx-proxy:test.
:return: IP or None
"""
log = logging.getLogger('DNS')
log.debug(f"nginx_proxy_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.warning(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.warning(f"nginxproxy/nginx-proxy:test container might have exited unexpectedly. Container logs: " + "\n" + exited_nginxproxy_container_logs.decode())
return None
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 docker_container_dns_resolver(domain_name: str) -> Optional[str]:
"""
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(f"docker_container_dns_resolver({domain_name!r})")
match = re.search(r'(^|.+\.)(?P<container>[^.]+)\.container\.docker$', domain_name)
if not match:
log.debug(f"{domain_name!r} does not match")
return None
container_name = match.group('container')
log.debug(f"looking for container {container_name!r}")
try:
container = docker_client.containers.get(container_name)
except docker.errors.NotFound:
log.warning(f"container named {container_name!r} not found while resolving {domain_name!r}")
return None
log.debug(f"container {container.name!r} found ({container.short_id})")
ip = container_ip(container)
log.info(f"resolving domain name {domain_name!r} as IP address {ip} of container {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 'nginxproxy/nginx-proxy:test',
or to 127.0.0.1 on Darwin.
see https://docs.docker.com/desktop/features/networking/#i-want-to-connect-to-a-container-from-the-host
"""
prv_getaddrinfo = socket.getaddrinfo
dns_cache = {}
def new_getaddrinfo(*args):
logging.getLogger('DNS').debug(f"resolving domain name {repr(args)}")
_args = list(args)
# Fail early when querying IP directly, and it is forced ipv6 when not supported,
# Otherwise a pytest container not using the host network fails to pass `test_raw-ip-vhost`.
if FORCE_CONTAINER_IPV6 and not HAS_IPV6:
pytest.skip("This system does not support IPv6")
# custom DNS resolvers
ip = None
# Docker Desktop can't route traffic directly to Linux containers.
if platform.system() == "Darwin":
ip = "127.0.0.1"
if ip is None:
ip = nginx_proxy_dns_resolver(args[0])
if ip is None:
ip = docker_container_dns_resolver(args[0])
if ip is not None:
_args[0] = ip
# call on original DNS resolver, with eventually the original host changed to the wanted IP address
try:
return dns_cache[tuple(_args)]
except KeyError:
res = prv_getaddrinfo(*_args)
dns_cache[tuple(_args)] = res
return res
socket.getaddrinfo = new_getaddrinfo
return prv_getaddrinfo
def restore_urllib_dns_resolver(getaddrinfo_func):
socket.getaddrinfo = getaddrinfo_func
def get_nginx_conf_from_container(container: Container) -> bytes:
"""
return the nginx /etc/nginx/conf.d/default.conf file content from a container
"""
import tarfile
from io import BytesIO
strm_generator, stat = container.get_archive('/etc/nginx/conf.d/default.conf')
strm_fileobj = BytesIO(b"".join(strm_generator))
with tarfile.open(fileobj=strm_fileobj) as tf:
conffile = tf.extractfile('default.conf')
return conffile.read()
def __prepare_and_execute_compose_cmd(compose_files: List[str], project_name: str, cmd: str):
"""
Prepare and execute the Docker Compose command with the provided compose files and project name.
"""
compose_cmd = StringIO()
compose_cmd.write(DOCKER_COMPOSE)
compose_cmd.write(f" --project-name {project_name}")
for compose_file in compose_files:
compose_cmd.write(f" --file {compose_file}")
compose_cmd.write(f" {cmd}")
logging.info(compose_cmd.getvalue())
try:
subprocess.check_output(shlex.split(compose_cmd.getvalue()), stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as e:
pytest.fail(f"Error while running '{compose_cmd.getvalue()}':\n{e.output}", pytrace=False)
def docker_compose_up(compose_files: List[str], project_name: str):
"""
Execute compose up --detach with the provided compose files and project name.
"""
if compose_files is None or len(compose_files) == 0:
pytest.fail(f"No compose file passed to docker_compose_up", pytrace=False)
__prepare_and_execute_compose_cmd(compose_files, project_name, cmd="up --detach")
def docker_compose_down(compose_files: List[str], project_name: str):
"""
Execute compose down --volumes with the provided compose files and project name.
"""
if compose_files is None or len(compose_files) == 0:
pytest.fail(f"No compose file passed to docker_compose_up", pytrace=False)
__prepare_and_execute_compose_cmd(compose_files, project_name, cmd="down --volumes")
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"
"""
containers = docker_client.containers.list(filters={"ancestor": "nginxproxy/nginx-proxy:test"})
if len(containers) != 1:
return
container = containers[0]
for line in container.logs(stream=True):
if b"Watching docker events" in line:
logging.debug("nginx-proxy ready")
break
@pytest.fixture
def docker_compose_files(request: FixtureRequest) -> List[str]:
"""Fixture returning the docker compose files to consider:
If a YAML file exists with the same name as the test module (with the `.py` extension
replaced with `.base.yml`, ie `test_foo.py`-> `test_foo.base.yml`) and in the same
directory as the test module, use only that file.
Otherwise, merge the following files in this order:
- the `compose.base.yml` file in the parent `test` directory.
- if present in the same directory as the test module, the `compose.base.override.yml` file.
- the YAML file named after the current test module (ie `test_foo.py`-> `test_foo.yml`)
Tests can override this fixture to specify a custom location.
"""
compose_files: List[str] = []
test_module_path = pathlib.Path(request.module.__file__).parent
module_base_file = test_module_path.joinpath(f"{request.module.__name__}.base.yml")
if module_base_file.is_file():
return [module_base_file.as_posix()]
global_base_file = test_module_path.parent.joinpath("compose.base.yml")
if global_base_file.is_file():
compose_files.append(global_base_file.as_posix())
module_base_override_file = test_module_path.joinpath("compose.base.override.yml")
if module_base_override_file.is_file():
compose_files.append(module_base_override_file.as_posix())
module_compose_file = test_module_path.joinpath(f"{request.module.__name__}.yml")
if module_compose_file.is_file():
compose_files.append(module_compose_file.as_posix())
if not module_base_file.is_file() and not module_compose_file.is_file():
logging.error(
f"Could not find any docker compose file named '{module_base_file.name}' or '{module_compose_file.name}'"
)
logging.debug(f"using docker compose files {compose_files}")
return compose_files
def connect_to_network(network: Network) -> Optional[Network]:
"""
If we are running from a container, connect our container to the given network
:return: the name of the network we were connected to, or None
"""
if PYTEST_RUNNING_IN_CONTAINER:
try:
my_container = docker_client.containers.get(test_container)
except docker.errors.NotFound:
logging.warning(f"container {test_container} not found")
return None
# figure out our container networks
my_networks = list(my_container.attrs["NetworkSettings"]["Networks"].keys())
# If the pytest container is using host networking, it cannot connect to container networks (not required with host network)
if 'host' in my_networks:
return None
# Make sure our container is connected to the nginx-proxy's network,
# but avoid connecting to `none` network (not valid) with `test_server-down` tests
if network.name not in my_networks and network.name != 'none':
logging.info(f"Connecting to docker network: {network.name}")
network.connect(my_container)
return network
def disconnect_from_network(network: Network = None):
"""
If we are running from a container, disconnect our container from the given network.
:param network: name of a docker network to disconnect from
"""
if PYTEST_RUNNING_IN_CONTAINER and network is not None:
try:
my_container = docker_client.containers.get(test_container)
except docker.errors.NotFound:
logging.warning(f"container {test_container} not found")
return
# figure out our container networks
my_networks_names = list(my_container.attrs["NetworkSettings"]["Networks"].keys())
# disconnect our container from the given network
if network.name in my_networks_names:
logging.info(f"Disconnecting from network {network.name}")
network.disconnect(my_container)
def connect_to_all_networks() -> List[Network]:
"""
If we are running from a container, connect our container to all current docker networks.
:return: a list of networks we connected to
"""
if not PYTEST_RUNNING_IN_CONTAINER:
return []
else:
# find the list of docker networks
networks = [network for network in docker_client.networks.list(greedy=True) if len(network.containers) > 0 and network.name != 'bridge']
return [connect_to_network(network) for network in networks]
class DockerComposer(contextlib.AbstractContextManager):
def __init__(self):
self._networks = None
self._docker_compose_files = None
self._project_name = None
def __exit__(self, *exc_info):
self._down()
def _down(self):
if self._docker_compose_files is None:
return
for network in self._networks:
disconnect_from_network(network)
docker_compose_down(self._docker_compose_files, self._project_name)
self._docker_compose_file = None
self._project_name = None
def compose(self, docker_compose_files: List[str], project_name: str):
if docker_compose_files == self._docker_compose_files and project_name == self._project_name:
return
self._down()
if docker_compose_files is None or project_name is None:
return
docker_compose_up(docker_compose_files, project_name)
self._networks = connect_to_all_networks()
wait_for_nginxproxy_to_be_ready()
time.sleep(3) # give time to containers to be ready
self._docker_compose_files = docker_compose_files
self._project_name = project_name
###############################################################################
#
# Py.test fixtures
#
###############################################################################
@pytest.fixture(scope="module")
def docker_composer() -> Iterator[DockerComposer]:
with DockerComposer() as d:
yield d
@pytest.fixture
def ca_root_certificate() -> str:
return CA_ROOT_CERTIFICATE.as_posix()
@pytest.fixture
def monkey_patched_dns():
original_dns_resolver = monkey_patch_urllib_dns_resolver()
yield
restore_urllib_dns_resolver(original_dns_resolver)
@pytest.fixture
def docker_compose(
request: FixtureRequest,
monkeypatch,
monkey_patched_dns,
docker_composer,
docker_compose_files
) -> Iterator[DockerClient]:
"""
Ensures containers necessary for the test module are started in a compose project,
and set the environment variable `PYTEST_MODULE_PATH` to the test module's parent folder.
A list of custom docker compose files path can be specified by overriding
the `docker_compose_file` fixture.
Also, in the case where pytest is running from a docker container, this fixture
makes sure our container will be attached to all the docker networks.
"""
pytest_module_path = pathlib.Path(request.module.__file__).parent
monkeypatch.setenv("PYTEST_MODULE_PATH", pytest_module_path.as_posix())
project_name = request.module.__name__
docker_composer.compose(docker_compose_files, project_name)
yield docker_client
@pytest.fixture
def nginxproxy() -> Iterator[RequestsForDocker]:
"""
Provides the `nginxproxy` object that can be used in the same way the requests module is:
r = nginxproxy.get("https://foo.com")
The difference is that in case an HTTP requests has status code 404 or 502 (which mostly
indicates that nginx has just reloaded), we retry up to 30 times the query.
Also, the nginxproxy methods accept an additional keyword parameter: `ipv6` which forces requests
made against containers to use the containers IPv6 address when set to `True`. If IPv6 is not
supported by the system or docker, that particular test will be skipped.
"""
yield RequestsForDocker()
@pytest.fixture
def acme_challenge_path() -> str:
"""
Provides fake Let's Encrypt ACME challenge path used in certain tests
"""
return ".well-known/acme-challenge/test-filename"
###############################################################################
#
# Py.test hooks
#
###############################################################################
# pytest hook to display additional stuff in test report
def pytest_runtest_logreport(report):
if report.failed:
test_containers = docker_client.containers.list(all=True, filters={"ancestor": "nginxproxy/nginx-proxy:test"})
for container in test_containers:
report.longrepr.addsection('nginx-proxy logs', container.logs().decode())
report.longrepr.addsection('nginx-proxy conf', get_nginx_conf_from_container(container).decode())
# Py.test `incremental` marker, see http://stackoverflow.com/a/12579625/107049
def pytest_runtest_makereport(item, call):
if "incremental" in item.keywords:
if call.excinfo is not None:
parent = item.parent
parent._previousfailed = item
def pytest_runtest_setup(item):
previousfailed = getattr(item.parent, "_previousfailed", None)
if previousfailed is not None:
pytest.xfail(f"previous test failed ({previousfailed.name})")
###############################################################################
#
# Check requirements
#
###############################################################################
try:
docker_client.images.get('nginxproxy/nginx-proxy:test')
except docker.errors.ImageNotFound:
pytest.exit("The docker image 'nginxproxy/nginx-proxy:test' 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")

33
test/default-host.bats Normal file
View file

@ -0,0 +1,33 @@
#!/usr/bin/env bats
load test_helpers
function setup {
# make sure to stop any web container before each test so we don't
# have any unexpected contaiener running with VIRTUAL_HOST or VIRUTAL_PORT set
stop_bats_containers web
}
@test "[$TEST_FILE] DEFAULT_HOST=web1.bats" {
SUT_CONTAINER=bats-nginx-proxy-${TEST_FILE}-1
# GIVEN a webserver with VIRTUAL_HOST set to web.bats
prepare_web_container bats-web 80 -e VIRTUAL_HOST=web.bats
# WHEN nginx-proxy runs with DEFAULT_HOST set to web.bats
run nginxproxy $SUT_CONTAINER -v /var/run/docker.sock:/tmp/docker.sock:ro -e DEFAULT_HOST=web.bats
assert_success
docker_wait_for_log $SUT_CONTAINER 9 "Watching docker events"
# THEN querying the proxy without Host header → 200
run curl_container $SUT_CONTAINER / --head
assert_output -l 0 $'HTTP/1.1 200 OK\r'
# THEN querying the proxy with any other Host header → 200
run curl_container $SUT_CONTAINER / --head --header "Host: something.I.just.made.up"
assert_output -l 0 $'HTTP/1.1 200 OK\r'
}
@test "[$TEST_FILE] stop all bats containers" {
stop_bats_containers
}

123
test/docker.bats Normal file
View file

@ -0,0 +1,123 @@
#!/usr/bin/env bats
load test_helpers
@test "[$TEST_FILE] start 2 web containers" {
prepare_web_container bats-web1 81 -e VIRTUAL_HOST=web1.bats
prepare_web_container bats-web2 82 -e VIRTUAL_HOST=web2.bats
}
@test "[$TEST_FILE] -v /var/run/docker.sock:/tmp/docker.sock:ro" {
SUT_CONTAINER=bats-nginx-proxy-${TEST_FILE}-1
# WHEN nginx-proxy runs on our docker host using the default unix socket
run nginxproxy $SUT_CONTAINER -v /var/run/docker.sock:/tmp/docker.sock:ro
assert_success
docker_wait_for_log $SUT_CONTAINER 9 "Watching docker events"
# THEN
assert_nginxproxy_behaves $SUT_CONTAINER
}
@test "[$TEST_FILE] -v /var/run/docker.sock:/f00.sock:ro -e DOCKER_HOST=unix:///f00.sock" {
SUT_CONTAINER=bats-nginx-proxy-${TEST_FILE}-2
# WHEN nginx-proxy runs on our docker host using a custom unix socket
run nginxproxy $SUT_CONTAINER -v /var/run/docker.sock:/f00.sock:ro -e DOCKER_HOST=unix:///f00.sock
assert_success
docker_wait_for_log $SUT_CONTAINER 9 "Watching docker events"
# THEN
assert_nginxproxy_behaves $SUT_CONTAINER
}
@test "[$TEST_FILE] -e DOCKER_HOST=tcp://..." {
SUT_CONTAINER=bats-nginx-proxy-${TEST_FILE}-3
# GIVEN a container exposing our docker host over TCP
run docker_tcp bats-docker-tcp
assert_success
sleep 1s
# WHEN nginx-proxy runs on our docker host using tcp to connect to our docker host
run nginxproxy $SUT_CONTAINER -e DOCKER_HOST="tcp://bats-docker-tcp:2375" --link bats-docker-tcp:bats-docker-tcp
assert_success
docker_wait_for_log $SUT_CONTAINER 9 "Watching docker events"
# THEN
assert_nginxproxy_behaves $SUT_CONTAINER
}
@test "[$TEST_FILE] separated containers (nginx + docker-gen + nginx.tmpl)" {
docker_clean bats-nginx
docker_clean bats-docker-gen
# GIVEN a simple nginx container
run docker run -d \
--label bats-type="nginx" \
--name bats-nginx \
-v /etc/nginx/conf.d/ \
-v /etc/nginx/certs/ \
nginx:latest
assert_success
run retry 5 1s docker run --label bats-type="curl" appropriate/curl --silent --fail --head http://$(docker_ip bats-nginx)/
assert_output -l 0 $'HTTP/1.1 200 OK\r'
# WHEN docker-gen runs on our docker host
run docker run -d \
--label bats-type="docker-gen" \
--name bats-docker-gen \
-v /var/run/docker.sock:/tmp/docker.sock:ro \
-v $BATS_TEST_DIRNAME/../nginx.tmpl:/etc/docker-gen/templates/nginx.tmpl:ro \
--volumes-from bats-nginx \
--expose 80 \
jwilder/docker-gen:0.7.1 \
-notify-sighup bats-nginx \
-watch \
-only-exposed \
/etc/docker-gen/templates/nginx.tmpl \
/etc/nginx/conf.d/default.conf
assert_success
docker_wait_for_log bats-docker-gen 9 "Watching docker events"
# Give some time to the docker-gen container to notify bats-nginx so it
# reloads its config
sleep 2s
run docker_running_state bats-nginx
assert_output "true" || {
docker logs bats-docker-gen
false
} >&2
# THEN
assert_nginxproxy_behaves bats-nginx
}
@test "[$TEST_FILE] stop all bats containers" {
stop_bats_containers
}
# $1 nginx-proxy container
function assert_nginxproxy_behaves {
local -r container=$1
# Querying the proxy without Host header → 503
run curl_container $container / --head
assert_output -l 0 $'HTTP/1.1 503 Service Temporarily Unavailable\r'
# Querying the proxy with Host header → 200
run curl_container $container /data --header "Host: web1.bats"
assert_output "answer from port 81"
run curl_container $container /data --header "Host: web2.bats"
assert_output "answer from port 82"
# Querying the proxy with unknown Host header → 503
run curl_container $container /data --header "Host: webFOO.bats" --head
assert_output -l 0 $'HTTP/1.1 503 Service Temporarily Unavailable\r'
}

6
test/lib/README.md Normal file
View file

@ -0,0 +1,6 @@
bats lib
========
found on https://github.com/sstephenson/bats/pull/110
When that pull request will be merged, the `test/lib/bats` won't be necessary anymore.

596
test/lib/bats/batslib.bash Normal file
View file

@ -0,0 +1,596 @@
#
# batslib.bash
# ------------
#
# The Standard Library is a collection of test helpers intended to
# simplify testing. It contains the following types of test helpers.
#
# - Assertions are functions that perform a test and output relevant
# information on failure to help debugging. They return 1 on failure
# and 0 otherwise.
#
# All output is formatted for readability using the functions of
# `output.bash' and sent to the standard error.
#
source "${BATS_LIB}/batslib/output.bash"
########################################################################
# ASSERTIONS
########################################################################
# Fail and display a message. When no parameters are specified, the
# message is read from the standard input. Other functions use this to
# report failure.
#
# Globals:
# none
# Arguments:
# $@ - [=STDIN] message
# Returns:
# 1 - always
# Inputs:
# STDIN - [=$@] message
# Outputs:
# STDERR - message
fail() {
(( $# == 0 )) && batslib_err || batslib_err "$@"
return 1
}
# Fail and display details if the expression evaluates to false. Details
# include the expression, `$status' and `$output'.
#
# NOTE: The expression must be a simple command. Compound commands, such
# as `[[', can be used only when executed with `bash -c'.
#
# Globals:
# status
# output
# Arguments:
# $1 - expression
# Returns:
# 0 - expression evaluates to TRUE
# 1 - otherwise
# Outputs:
# STDERR - details, on failure
assert() {
if ! "$@"; then
{ local -ar single=(
'expression' "$*"
'status' "$status"
)
local -ar may_be_multi=(
'output' "$output"
)
local -ir width="$( batslib_get_max_single_line_key_width \
"${single[@]}" "${may_be_multi[@]}" )"
batslib_print_kv_single "$width" "${single[@]}"
batslib_print_kv_single_or_multi "$width" "${may_be_multi[@]}"
} | batslib_decorate 'assertion failed' \
| fail
fi
}
# Fail and display details if the expected and actual values do not
# equal. Details include both values.
#
# Globals:
# none
# Arguments:
# $1 - actual value
# $2 - expected value
# Returns:
# 0 - values equal
# 1 - otherwise
# Outputs:
# STDERR - details, on failure
assert_equal() {
if [[ $1 != "$2" ]]; then
batslib_print_kv_single_or_multi 8 \
'expected' "$2" \
'actual' "$1" \
| batslib_decorate 'values do not equal' \
| fail
fi
}
# Fail and display details if `$status' is not 0. Details include
# `$status' and `$output'.
#
# Globals:
# status
# output
# Arguments:
# none
# Returns:
# 0 - `$status' is 0
# 1 - otherwise
# Outputs:
# STDERR - details, on failure
assert_success() {
if (( status != 0 )); then
{ local -ir width=6
batslib_print_kv_single "$width" 'status' "$status"
batslib_print_kv_single_or_multi "$width" 'output' "$output"
} | batslib_decorate 'command failed' \
| fail
fi
}
# Fail and display details if `$status' is 0. Details include `$output'.
#
# Optionally, when the expected status is specified, fail when it does
# not equal `$status'. In this case, details include the expected and
# actual status, and `$output'.
#
# Globals:
# status
# output
# Arguments:
# $1 - [opt] expected status
# Returns:
# 0 - `$status' is not 0, or
# `$status' equals the expected status
# 1 - otherwise
# Outputs:
# STDERR - details, on failure
assert_failure() {
(( $# > 0 )) && local -r expected="$1"
if (( status == 0 )); then
batslib_print_kv_single_or_multi 6 'output' "$output" \
| batslib_decorate 'command succeeded, but it was expected to fail' \
| fail
elif (( $# > 0 )) && (( status != expected )); then
{ local -ir width=8
batslib_print_kv_single "$width" \
'expected' "$expected" \
'actual' "$status"
batslib_print_kv_single_or_multi "$width" \
'output' "$output"
} | batslib_decorate 'command failed as expected, but status differs' \
| fail
fi
}
# Fail and display details if the expected does not match the actual
# output or a fragment of it.
#
# By default, the entire output is matched. The assertion fails if the
# expected output does not equal `$output'. Details include both values.
#
# When `-l <index>' is used, only the <index>-th line is matched. The
# assertion fails if the expected line does not equal
# `${lines[<index>}'. Details include the compared lines and <index>.
#
# When `-l' is used without the <index> argument, the output is searched
# for the expected line. The expected line is matched against each line
# in `${lines[@]}'. If no match is found the assertion fails. Details
# include the expected line and `$output'.
#
# By default, literal matching is performed. Options `-p' and `-r'
# enable partial (i.e. substring) and extended regular expression
# matching, respectively. Specifying an invalid extended regular
# expression with `-r' displays an error.
#
# Options `-p' and `-r' are mutually exclusive. When used
# simultaneously, an error is displayed.
#
# Globals:
# output
# lines
# Options:
# -l <index> - match against the <index>-th element of `${lines[@]}'
# -l - search `${lines[@]}' for the expected line
# -p - partial matching
# -r - extended regular expression matching
# Arguments:
# $1 - expected output
# Returns:
# 0 - expected matches the actual output
# 1 - otherwise
# Outputs:
# STDERR - details, on failure
# error message, on error
assert_output() {
local -i is_match_line=0
local -i is_match_contained=0
local -i is_mode_partial=0
local -i is_mode_regex=0
# Handle options.
while (( $# > 0 )); do
case "$1" in
-l)
if (( $# > 2 )) && [[ $2 =~ ^([0-9]|[1-9][0-9]+)$ ]]; then
is_match_line=1
local -ri idx="$2"
shift
else
is_match_contained=1;
fi
shift
;;
-p) is_mode_partial=1; shift ;;
-r) is_mode_regex=1; shift ;;
--) break ;;
*) break ;;
esac
done
if (( is_match_line )) && (( is_match_contained )); then
echo "\`-l' and \`-l <index>' are mutually exclusive" \
| batslib_decorate 'ERROR: assert_output' \
| fail
return $?
fi
if (( is_mode_partial )) && (( is_mode_regex )); then
echo "\`-p' and \`-r' are mutually exclusive" \
| batslib_decorate 'ERROR: assert_output' \
| fail
return $?
fi
# Arguments.
local -r expected="$1"
if (( is_mode_regex == 1 )) && [[ '' =~ $expected ]] || (( $? == 2 )); then
echo "Invalid extended regular expression: \`$expected'" \
| batslib_decorate 'ERROR: assert_output' \
| fail
return $?
fi
# Matching.
if (( is_match_contained )); then
# Line contained in output.
if (( is_mode_regex )); then
local -i idx
for (( idx = 0; idx < ${#lines[@]}; ++idx )); do
[[ ${lines[$idx]} =~ $expected ]] && return 0
done
{ local -ar single=(
'regex' "$expected"
)
local -ar may_be_multi=(
'output' "$output"
)
local -ir width="$( batslib_get_max_single_line_key_width \
"${single[@]}" "${may_be_multi[@]}" )"
batslib_print_kv_single "$width" "${single[@]}"
batslib_print_kv_single_or_multi "$width" "${may_be_multi[@]}"
} | batslib_decorate 'no output line matches regular expression' \
| fail
elif (( is_mode_partial )); then
local -i idx
for (( idx = 0; idx < ${#lines[@]}; ++idx )); do
[[ ${lines[$idx]} == *"$expected"* ]] && return 0
done
{ local -ar single=(
'substring' "$expected"
)
local -ar may_be_multi=(
'output' "$output"
)
local -ir width="$( batslib_get_max_single_line_key_width \
"${single[@]}" "${may_be_multi[@]}" )"
batslib_print_kv_single "$width" "${single[@]}"
batslib_print_kv_single_or_multi "$width" "${may_be_multi[@]}"
} | batslib_decorate 'no output line contains substring' \
| fail
else
local -i idx
for (( idx = 0; idx < ${#lines[@]}; ++idx )); do
[[ ${lines[$idx]} == "$expected" ]] && return 0
done
{ local -ar single=(
'line' "$expected"
)
local -ar may_be_multi=(
'output' "$output"
)
local -ir width="$( batslib_get_max_single_line_key_width \
"${single[@]}" "${may_be_multi[@]}" )"
batslib_print_kv_single "$width" "${single[@]}"
batslib_print_kv_single_or_multi "$width" "${may_be_multi[@]}"
} | batslib_decorate 'output does not contain line' \
| fail
fi
elif (( is_match_line )); then
# Specific line.
if (( is_mode_regex )); then
if ! [[ ${lines[$idx]} =~ $expected ]]; then
batslib_print_kv_single 5 \
'index' "$idx" \
'regex' "$expected" \
'line' "${lines[$idx]}" \
| batslib_decorate 'regular expression does not match line' \
| fail
fi
elif (( is_mode_partial )); then
if [[ ${lines[$idx]} != *"$expected"* ]]; then
batslib_print_kv_single 9 \
'index' "$idx" \
'substring' "$expected" \
'line' "${lines[$idx]}" \
| batslib_decorate 'line does not contain substring' \
| fail
fi
else
if [[ ${lines[$idx]} != "$expected" ]]; then
batslib_print_kv_single 8 \
'index' "$idx" \
'expected' "$expected" \
'actual' "${lines[$idx]}" \
| batslib_decorate 'line differs' \
| fail
fi
fi
else
# Entire output.
if (( is_mode_regex )); then
if ! [[ $output =~ $expected ]]; then
batslib_print_kv_single_or_multi 6 \
'regex' "$expected" \
'output' "$output" \
| batslib_decorate 'regular expression does not match output' \
| fail
fi
elif (( is_mode_partial )); then
if [[ $output != *"$expected"* ]]; then
batslib_print_kv_single_or_multi 9 \
'substring' "$expected" \
'output' "$output" \
| batslib_decorate 'output does not contain substring' \
| fail
fi
else
if [[ $output != "$expected" ]]; then
batslib_print_kv_single_or_multi 8 \
'expected' "$expected" \
'actual' "$output" \
| batslib_decorate 'output differs' \
| fail
fi
fi
fi
}
# Fail and display details if the unexpected matches the actual output
# or a fragment of it.
#
# By default, the entire output is matched. The assertion fails if the
# unexpected output equals `$output'. Details include `$output'.
#
# When `-l <index>' is used, only the <index>-th line is matched. The
# assertion fails if the unexpected line equals `${lines[<index>}'.
# Details include the compared line and <index>.
#
# When `-l' is used without the <index> argument, the output is searched
# for the unexpected line. The unexpected line is matched against each
# line in `${lines[<index>]}'. If a match is found the assertion fails.
# Details include the unexpected line, the index where it was found and
# `$output' (with the unexpected line highlighted in it if `$output` is
# longer than one line).
#
# By default, literal matching is performed. Options `-p' and `-r'
# enable partial (i.e. substring) and extended regular expression
# matching, respectively. On failure, the substring or the regular
# expression is added to the details (if not already displayed).
# Specifying an invalid extended regular expression with `-r' displays
# an error.
#
# Options `-p' and `-r' are mutually exclusive. When used
# simultaneously, an error is displayed.
#
# Globals:
# output
# lines
# Options:
# -l <index> - match against the <index>-th element of `${lines[@]}'
# -l - search `${lines[@]}' for the unexpected line
# -p - partial matching
# -r - extended regular expression matching
# Arguments:
# $1 - unexpected output
# Returns:
# 0 - unexpected matches the actual output
# 1 - otherwise
# Outputs:
# STDERR - details, on failure
# error message, on error
refute_output() {
local -i is_match_line=0
local -i is_match_contained=0
local -i is_mode_partial=0
local -i is_mode_regex=0
# Handle options.
while (( $# > 0 )); do
case "$1" in
-l)
if (( $# > 2 )) && [[ $2 =~ ^([0-9]|[1-9][0-9]+)$ ]]; then
is_match_line=1
local -ri idx="$2"
shift
else
is_match_contained=1;
fi
shift
;;
-L) is_match_contained=1; shift ;;
-p) is_mode_partial=1; shift ;;
-r) is_mode_regex=1; shift ;;
--) break ;;
*) break ;;
esac
done
if (( is_match_line )) && (( is_match_contained )); then
echo "\`-l' and \`-l <index>' are mutually exclusive" \
| batslib_decorate 'ERROR: refute_output' \
| fail
return $?
fi
if (( is_mode_partial )) && (( is_mode_regex )); then
echo "\`-p' and \`-r' are mutually exclusive" \
| batslib_decorate 'ERROR: refute_output' \
| fail
return $?
fi
# Arguments.
local -r unexpected="$1"
if (( is_mode_regex == 1 )) && [[ '' =~ $unexpected ]] || (( $? == 2 )); then
echo "Invalid extended regular expression: \`$unexpected'" \
| batslib_decorate 'ERROR: refute_output' \
| fail
return $?
fi
# Matching.
if (( is_match_contained )); then
# Line contained in output.
if (( is_mode_regex )); then
local -i idx
for (( idx = 0; idx < ${#lines[@]}; ++idx )); do
if [[ ${lines[$idx]} =~ $unexpected ]]; then
{ local -ar single=(
'regex' "$unexpected"
'index' "$idx"
)
local -a may_be_multi=(
'output' "$output"
)
local -ir width="$( batslib_get_max_single_line_key_width \
"${single[@]}" "${may_be_multi[@]}" )"
batslib_print_kv_single "$width" "${single[@]}"
if batslib_is_single_line "${may_be_multi[1]}"; then
batslib_print_kv_single "$width" "${may_be_multi[@]}"
else
may_be_multi[1]="$( printf '%s' "${may_be_multi[1]}" \
| batslib_prefix \
| batslib_mark '>' "$idx" )"
batslib_print_kv_multi "${may_be_multi[@]}"
fi
} | batslib_decorate 'no line should match the regular expression' \
| fail
return $?
fi
done
elif (( is_mode_partial )); then
local -i idx
for (( idx = 0; idx < ${#lines[@]}; ++idx )); do
if [[ ${lines[$idx]} == *"$unexpected"* ]]; then
{ local -ar single=(
'substring' "$unexpected"
'index' "$idx"
)
local -a may_be_multi=(
'output' "$output"
)
local -ir width="$( batslib_get_max_single_line_key_width \
"${single[@]}" "${may_be_multi[@]}" )"
batslib_print_kv_single "$width" "${single[@]}"
if batslib_is_single_line "${may_be_multi[1]}"; then
batslib_print_kv_single "$width" "${may_be_multi[@]}"
else
may_be_multi[1]="$( printf '%s' "${may_be_multi[1]}" \
| batslib_prefix \
| batslib_mark '>' "$idx" )"
batslib_print_kv_multi "${may_be_multi[@]}"
fi
} | batslib_decorate 'no line should contain substring' \
| fail
return $?
fi
done
else
local -i idx
for (( idx = 0; idx < ${#lines[@]}; ++idx )); do
if [[ ${lines[$idx]} == "$unexpected" ]]; then
{ local -ar single=(
'line' "$unexpected"
'index' "$idx"
)
local -a may_be_multi=(
'output' "$output"
)
local -ir width="$( batslib_get_max_single_line_key_width \
"${single[@]}" "${may_be_multi[@]}" )"
batslib_print_kv_single "$width" "${single[@]}"
if batslib_is_single_line "${may_be_multi[1]}"; then
batslib_print_kv_single "$width" "${may_be_multi[@]}"
else
may_be_multi[1]="$( printf '%s' "${may_be_multi[1]}" \
| batslib_prefix \
| batslib_mark '>' "$idx" )"
batslib_print_kv_multi "${may_be_multi[@]}"
fi
} | batslib_decorate 'line should not be in output' \
| fail
return $?
fi
done
fi
elif (( is_match_line )); then
# Specific line.
if (( is_mode_regex )); then
if [[ ${lines[$idx]} =~ $unexpected ]] || (( $? == 0 )); then
batslib_print_kv_single 5 \
'index' "$idx" \
'regex' "$unexpected" \
'line' "${lines[$idx]}" \
| batslib_decorate 'regular expression should not match line' \
| fail
fi
elif (( is_mode_partial )); then
if [[ ${lines[$idx]} == *"$unexpected"* ]]; then
batslib_print_kv_single 9 \
'index' "$idx" \
'substring' "$unexpected" \
'line' "${lines[$idx]}" \
| batslib_decorate 'line should not contain substring' \
| fail
fi
else
if [[ ${lines[$idx]} == "$unexpected" ]]; then
batslib_print_kv_single 5 \
'index' "$idx" \
'line' "${lines[$idx]}" \
| batslib_decorate 'line should differ' \
| fail
fi
fi
else
# Entire output.
if (( is_mode_regex )); then
if [[ $output =~ $unexpected ]] || (( $? == 0 )); then
batslib_print_kv_single_or_multi 6 \
'regex' "$unexpected" \
'output' "$output" \
| batslib_decorate 'regular expression should not match output' \
| fail
fi
elif (( is_mode_partial )); then
if [[ $output == *"$unexpected"* ]]; then
batslib_print_kv_single_or_multi 9 \
'substring' "$unexpected" \
'output' "$output" \
| batslib_decorate 'output should not contain substring' \
| fail
fi
else
if [[ $output == "$unexpected" ]]; then
batslib_print_kv_single_or_multi 6 \
'output' "$output" \
| batslib_decorate 'output equals, but it was expected to differ' \
| fail
fi
fi
fi
}

View file

@ -0,0 +1,264 @@
#
# output.bash
# -----------
#
# Private functions implementing output formatting. Used by public
# helper functions.
#
# Print a message to the standard error. When no parameters are
# specified, the message is read from the standard input.
#
# Globals:
# none
# Arguments:
# $@ - [=STDIN] message
# Returns:
# none
# Inputs:
# STDIN - [=$@] message
# Outputs:
# STDERR - message
batslib_err() {
{ if (( $# > 0 )); then
echo "$@"
else
cat -
fi
} >&2
}
# Count the number of lines in the given string.
#
# TODO(ztombol): Fix tests and remove this note after #93 is resolved!
# NOTE: Due to a bug in Bats, `batslib_count_lines "$output"' does not
# give the same result as `${#lines[@]}' when the output contains
# empty lines.
# See PR #93 (https://github.com/sstephenson/bats/pull/93).
#
# Globals:
# none
# Arguments:
# $1 - string
# Returns:
# none
# Outputs:
# STDOUT - number of lines
batslib_count_lines() {
local -i n_lines=0
local line
while IFS='' read -r line || [[ -n $line ]]; do
(( ++n_lines ))
done < <(printf '%s' "$1")
echo "$n_lines"
}
# Determine whether all strings are single-line.
#
# Globals:
# none
# Arguments:
# $@ - strings
# Returns:
# 0 - all strings are single-line
# 1 - otherwise
batslib_is_single_line() {
for string in "$@"; do
(( $(batslib_count_lines "$string") > 1 )) && return 1
done
return 0
}
# Determine the length of the longest key that has a single-line value.
#
# This function is useful in determining the correct width of the key
# column in two-column format when some keys may have multi-line values
# and thus should be excluded.
#
# Globals:
# none
# Arguments:
# $odd - key
# $even - value of the previous key
# Returns:
# none
# Outputs:
# STDOUT - length of longest key
batslib_get_max_single_line_key_width() {
local -i max_len=-1
while (( $# != 0 )); do
local -i key_len="${#1}"
batslib_is_single_line "$2" && (( key_len > max_len )) && max_len="$key_len"
shift 2
done
echo "$max_len"
}
# Print key-value pairs in two-column format.
#
# Keys are displayed in the first column, and their corresponding values
# in the second. To evenly line up values, the key column is fixed-width
# and its width is specified with the first parameter (possibly computed
# using `batslib_get_max_single_line_key_width').
#
# Globals:
# none
# Arguments:
# $1 - width of key column
# $even - key
# $odd - value of the previous key
# Returns:
# none
# Outputs:
# STDOUT - formatted key-value pairs
batslib_print_kv_single() {
local -ir col_width="$1"; shift
while (( $# != 0 )); do
printf '%-*s : %s\n' "$col_width" "$1" "$2"
shift 2
done
}
# Print key-value pairs in multi-line format.
#
# The key is displayed first with the number of lines of its
# corresponding value in parenthesis. Next, starting on the next line,
# the value is displayed. For better readability, it is recommended to
# indent values using `batslib_prefix'.
#
# Globals:
# none
# Arguments:
# $odd - key
# $even - value of the previous key
# Returns:
# none
# Outputs:
# STDOUT - formatted key-value pairs
batslib_print_kv_multi() {
while (( $# != 0 )); do
printf '%s (%d lines):\n' "$1" "$( batslib_count_lines "$2" )"
printf '%s\n' "$2"
shift 2
done
}
# Print all key-value pairs in either two-column or multi-line format
# depending on whether all values are single-line.
#
# If all values are single-line, print all pairs in two-column format
# with the specified key column width (identical to using
# `batslib_print_kv_single').
#
# Otherwise, print all pairs in multi-line format after indenting values
# with two spaces for readability (identical to using `batslib_prefix'
# and `batslib_print_kv_multi')
#
# Globals:
# none
# Arguments:
# $1 - width of key column (for two-column format)
# $even - key
# $odd - value of the previous key
# Returns:
# none
# Outputs:
# STDOUT - formatted key-value pairs
batslib_print_kv_single_or_multi() {
local -ir width="$1"; shift
local -a pairs=( "$@" )
local -a values=()
local -i i
for (( i=1; i < ${#pairs[@]}; i+=2 )); do
values+=( "${pairs[$i]}" )
done
if batslib_is_single_line "${values[@]}"; then
batslib_print_kv_single "$width" "${pairs[@]}"
else
local -i i
for (( i=1; i < ${#pairs[@]}; i+=2 )); do
pairs[$i]="$( batslib_prefix < <(printf '%s' "${pairs[$i]}") )"
done
batslib_print_kv_multi "${pairs[@]}"
fi
}
# Prefix each line read from the standard input with the given string.
#
# Globals:
# none
# Arguments:
# $1 - [= ] prefix string
# Returns:
# none
# Inputs:
# STDIN - lines
# Outputs:
# STDOUT - prefixed lines
batslib_prefix() {
local -r prefix="${1:- }"
local line
while IFS='' read -r line || [[ -n $line ]]; do
printf '%s%s\n' "$prefix" "$line"
done
}
# Mark select lines of the text read from the standard input by
# overwriting their beginning with the given string.
#
# Usually the input is indented by a few spaces using `batslib_prefix'
# first.
#
# Globals:
# none
# Arguments:
# $1 - marking string
# $@ - indices (zero-based) of lines to mark
# Returns:
# none
# Inputs:
# STDIN - lines
# Outputs:
# STDOUT - lines after marking
batslib_mark() {
local -r symbol="$1"; shift
# Sort line numbers.
set -- $( sort -nu <<< "$( printf '%d\n' "$@" )" )
local line
local -i idx=0
while IFS='' read -r line || [[ -n $line ]]; do
if (( ${1:--1} == idx )); then
printf '%s\n' "${symbol}${line:${#symbol}}"
shift
else
printf '%s\n' "$line"
fi
(( ++idx ))
done
}
# Enclose the input text in header and footer lines.
#
# The header contains the given string as title. The output is preceded
# and followed by an additional newline to make it stand out more.
#
# Globals:
# none
# Arguments:
# $1 - title
# Returns:
# none
# Inputs:
# STDIN - text
# Outputs:
# STDOUT - decorated text
batslib_decorate() {
echo
echo "-- $1 --"
cat -
echo '--'
echo
}

View file

@ -0,0 +1,66 @@
## functions to help deal with docker
# Removes container $1
function docker_clean {
docker kill $1 &>/dev/null ||:
sleep .25s
docker rm -vf $1 &>/dev/null ||:
sleep .25s
}
# get the ip of docker container $1
function docker_ip {
docker inspect --format '{{ .NetworkSettings.IPAddress }}' $1
}
# get the ip of docker container $1
function docker_id {
docker inspect --format '{{ .ID }}' $1
}
# get the running state of container $1
# → true/false
# fails if the container does not exist
function docker_running_state {
docker inspect -f {{.State.Running}} $1
}
# get the docker container $1 PID
function docker_pid {
docker inspect --format {{.State.Pid}} $1
}
# asserts logs from container $1 contains $2
function docker_assert_log {
local -r container=$1
shift
run docker logs $container
assert_output -p "$*"
}
# wait for a container to produce a given text in its log
# $1 container
# $2 timeout in second
# $* text to wait for
function docker_wait_for_log {
local -r container=$1
local -ir timeout_sec=$2
shift 2
retry $(( $timeout_sec * 2 )) .5s docker_assert_log $container "$*"
}
# Create a docker container named $1 which exposes the docker host unix
# socket over tcp on port 2375.
#
# $1 container name
function docker_tcp {
local container_name="$1"
docker_clean $container_name
docker run -d \
--label bats-type="socat" \
--name $container_name \
--expose 2375 \
-v /var/run/docker.sock:/var/run/docker.sock \
rancher/socat-docker
docker run --label bats-type="docker" --link "$container_name:docker" docker:1.10 version
}

22
test/lib/helpers.bash Normal file
View file

@ -0,0 +1,22 @@
## add the retry function to bats
# Retry a command $1 times until it succeeds. Wait $2 seconds between retries.
function retry {
local attempts=$1
shift
local delay=$1
shift
local i
for ((i=0; i < attempts; i++)); do
run "$@"
if [ "$status" -eq 0 ]; then
echo "$output"
return 0
fi
sleep $delay
done
echo "Command \"$@\" failed $attempts times. Status: $status. Output: $output" >&2
false
}

View file

@ -0,0 +1,24 @@
-----BEGIN CERTIFICATE-----
MIID7TCCAtWgAwIBAgIJAOGkf5EnexJVMA0GCSqGSIb3DQEBCwUAMIGMMQswCQYD
VQQGEwJVUzERMA8GA1UECAwIVmlyZ2luaWExDzANBgNVBAcMBlJlc3RvbjERMA8G
A1UECgwIRmFrZSBPcmcxGzAZBgNVBAMMEioubmdpbngtcHJveHkuYmF0czEpMCcG
CSqGSIb3DQEJARYad2VibWFzdGVyQG5naW54LXByb3h5LmJhdHMwHhcNMTYwNDIw
MTUzOTUxWhcNMjYwNDE4MTUzOTUxWjCBjDELMAkGA1UEBhMCVVMxETAPBgNVBAgM
CFZpcmdpbmlhMQ8wDQYDVQQHDAZSZXN0b24xETAPBgNVBAoMCEZha2UgT3JnMRsw
GQYDVQQDDBIqLm5naW54LXByb3h5LmJhdHMxKTAnBgkqhkiG9w0BCQEWGndlYm1h
c3RlckBuZ2lueC1wcm94eS5iYXRzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
CgKCAQEA0Amkj3iaQn8Z2CW6n24zSuWu2OoLCkHZAk8eprkI4kKoPBvjusynkm8E
phq65jebToHoldfuQ0wM61DzhD15bHwS3x9CrOVbShsmdnGALz+wdR0/4Likx50I
YZdecTOAlkoZudnX5FZ4ngOxjqcym7p5T8TrSS97a0fx99gitZY0p+Nu2tip4o3t
WBMs+SoPWTlQ1SrSmL8chC8O2knyBl/w1nHmDnMuR6FGcHdhLncApw9t5spgfv7p
OrMF4tQxJQNk10TnflmEMkGmy+pfk2e0cQ1Kwp3Nmzm7ECkggxxyjU3ihKiFK+09
8aSCi7gDAY925+mV6LZ5oLMpO3KJvQIDAQABo1AwTjAdBgNVHQ4EFgQU+NvFo37z
9Dyq8Mu82SPtV7q1gYQwHwYDVR0jBBgwFoAU+NvFo37z9Dyq8Mu82SPtV7q1gYQw
DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAI1ityDV0UsCVHSpB2LN+
QXlk8XS0ACIJ8Q0hbOj3BmYrdAVglG4P6upDEueaaxwsaBTagkTP8nxZ9dhfZHyZ
5YLNwYsiG5iqb8e0ecHx3uJT/0YiXn/8rBvxEZna4Fl8seGdp7BjOWUAS2Nv8tn4
EJJvRdfX/O8XgPc95DM4lwQ/dvyWmavMI4lnl0n1IQV9WPGaIQhYPU9WEQK6iMUB
o1kx8YbOJQD0ZBRfqpriNt1/8ylkkSYYav8QT9JFvQFCWEvaX71QF+cuOwC7ZYBH
4ElXwEUrYBHKiPo0q0VsTtMvLh7h/T5czrIhG/NpfVJPtQOk8aVwNScL3/n+TGU8
6g==
-----END CERTIFICATE-----

View file

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDQCaSPeJpCfxnY
JbqfbjNK5a7Y6gsKQdkCTx6muQjiQqg8G+O6zKeSbwSmGrrmN5tOgeiV1+5DTAzr
UPOEPXlsfBLfH0Ks5VtKGyZ2cYAvP7B1HT/guKTHnQhhl15xM4CWShm52dfkVnie
A7GOpzKbunlPxOtJL3trR/H32CK1ljSn427a2Knije1YEyz5Kg9ZOVDVKtKYvxyE
Lw7aSfIGX/DWceYOcy5HoUZwd2EudwCnD23mymB+/uk6swXi1DElA2TXROd+WYQy
QabL6l+TZ7RxDUrCnc2bObsQKSCDHHKNTeKEqIUr7T3xpIKLuAMBj3bn6ZXotnmg
syk7com9AgMBAAECggEAa7wCp3XqVPNjW+c1/ShhkbDeWmDhtL8i9aopkmeSbTHd
07sRtQQU56Vsf+Sp010KpZ5q52Z6cglpS1eRtHLtdbvPPhL/QXBJVVg4E/B1VIKk
DBJIqUSVuPXeiEOOWgs01R+ssO1ae1o4foQlKF33vGPWPPQacL0RKh6I9TPNzcD7
n4rujlHk72N/bNydyK2rnyKB4vAI5TbZPLps+Xe123CmgZnW3JClcWV9B4foRmiu
a5Iq1WYAK2GYKbYwgqDRyYBC27m91a7U31pE4GQD+xQdlz6kcOlCU5hAcPK3h7j0
fLQqn8g+YAtc0nBKKB4NZe3QEzTiVMorT0VitxI71QKBgQDnirardZaXOFzYGzB3
j+FGB9BUW54hnHr5BxOYrfmEJ5umJjJWaGupfYrQsPArrJP1//WbqVZIPvdQParD
mQhLmSp1r/VNzGB6pISmzU1ZGDHsmBxYseh366om5YBQUFU2vmbil9VkrkM4fsJG
tcS9V/nVY/EM7Yp3PzjfLlhC1wKBgQDmA1YJmnZvIbLp3PoKqM69QiCLKztVm7nX
xpu3b3qbXEzXkt2sP5PHmr+s13hOPQFKRJ2hk4UN9WqpnFoHw5E5eWWhSa/peUZm
r10Y5XspiFtRHHiu6ABXB49eB4fen+vHEZHKyRJ4rFthKjjBHdNPC8bmwnT3jE85
/8a26FLZiwKBgQDXEi8JZslBn9YF2oOTm28KCLoHka551AsaA+u892T8z3mxxGsf
fhD7N6TYonIEb2Jkr6OpOortwqcgvpc+5oghCJ27AX2fDUdUxDp/YdYF+wZsmQJD
lMW1lo7PYIBmmaf9mLCiq5xIz+GauYul+LNNmUl0YEgI1SC4EV63WCodswKBgDMX
GJxHd/kVViVGFTAa8NjvAEWJU8OfNHduQRZMp8IsjVDw6VYiRRP4Fo0wyyMtv8Sc
WxsRpmNEWO3VsdW5pd9LTLy3nmBQtMeIOjiWeHXwOMBaf5/yHmk2X6z2JULY6Mkt
6OFPKlAtkJqTg0m58z7Ckeqd1NdLjimG27+y+PwjAoGAFt0cbC1Ust2BE6YEspSX
ofpAnJsyKrbF9iVUyXDUP99sdqYQfPJ5uqPGkP59lJGkTLtebuitqi6FCyrsT6Fq
AWLiExbqebAqcuAZw2S+iuK27S4rrkjVGF53J7vH3rOzCBUXaRx6GKfTjUqedHdg
9Kw+LP6IFnMTb+EGLo+GqHs=
-----END PRIVATE KEY-----

43
test/multiple-hosts.bats Normal file
View file

@ -0,0 +1,43 @@
#!/usr/bin/env bats
load test_helpers
SUT_CONTAINER=bats-nginx-proxy-${TEST_FILE}
function setup {
# make sure to stop any web container before each test so we don't
# have any unexpected contaiener running with VIRTUAL_HOST or VIRUTAL_PORT set
stop_bats_containers web
}
@test "[$TEST_FILE] start a nginx-proxy container" {
run nginxproxy $SUT_CONTAINER -v /var/run/docker.sock:/tmp/docker.sock:ro
assert_success
docker_wait_for_log $SUT_CONTAINER 9 "Watching docker events"
}
@test "[$TEST_FILE] nginx-proxy forwards requests for 2 hosts" {
# WHEN a container runs a web server with VIRTUAL_HOST set for multiple hosts
prepare_web_container bats-multiple-hosts-1 80 -e VIRTUAL_HOST=multiple-hosts-1-A.bats,multiple-hosts-1-B.bats
dockergen_wait_for_event $SUT_CONTAINER start bats-multiple-hosts-1
sleep 1
# THEN querying the proxy without Host header → 503
run curl_container $SUT_CONTAINER / --head
assert_output -l 0 $'HTTP/1.1 503 Service Temporarily Unavailable\r'
# THEN querying the proxy with unknown Host header → 503
run curl_container $SUT_CONTAINER /data --header "Host: webFOO.bats" --head
assert_output -l 0 $'HTTP/1.1 503 Service Temporarily Unavailable\r'
# THEN
run curl_container $SUT_CONTAINER /data --header 'Host: multiple-hosts-1-A.bats'
assert_output "answer from port 80"
# THEN
run curl_container $SUT_CONTAINER /data --header 'Host: multiple-hosts-1-B.bats'
assert_output "answer from port 80"
}
@test "[$TEST_FILE] stop all bats containers" {
stop_bats_containers
}

64
test/multiple-ports.bats Normal file
View file

@ -0,0 +1,64 @@
#!/usr/bin/env bats
load test_helpers
SUT_CONTAINER=bats-nginx-proxy-${TEST_FILE}
function setup {
# make sure to stop any web container before each test so we don't
# have any unexpected contaiener running with VIRTUAL_HOST or VIRUTAL_PORT set
stop_bats_containers web
}
@test "[$TEST_FILE] start a nginx-proxy container" {
# GIVEN nginx-proxy
run nginxproxy $SUT_CONTAINER -v /var/run/docker.sock:/tmp/docker.sock:ro
assert_success
docker_wait_for_log $SUT_CONTAINER 9 "Watching docker events"
}
@test "[$TEST_FILE] nginx-proxy defaults to the service running on port 80" {
# WHEN
prepare_web_container bats-web-${TEST_FILE}-1 "80 90" -e VIRTUAL_HOST=web.bats
dockergen_wait_for_event $SUT_CONTAINER start bats-web-${TEST_FILE}-1
sleep 1
# THEN
assert_response_is_from_port 80
}
@test "[$TEST_FILE] VIRTUAL_PORT=90 while port 80 is also exposed" {
# GIVEN
prepare_web_container bats-web-${TEST_FILE}-2 "80 90" -e VIRTUAL_HOST=web.bats -e VIRTUAL_PORT=90
dockergen_wait_for_event $SUT_CONTAINER start bats-web-${TEST_FILE}-2
sleep 1
# THEN
assert_response_is_from_port 90
}
@test "[$TEST_FILE] single exposed port != 80" {
# GIVEN
prepare_web_container bats-web-${TEST_FILE}-3 1234 -e VIRTUAL_HOST=web.bats
dockergen_wait_for_event $SUT_CONTAINER start bats-web-${TEST_FILE}-3
sleep 1
# THEN
assert_response_is_from_port 1234
}
@test "[$TEST_FILE] stop all bats containers" {
stop_bats_containers
}
# assert querying nginx-proxy provides a response from the expected port of the web container
# $1 port we are expecting an response from
function assert_response_is_from_port {
local -r port=$1
run curl_container $SUT_CONTAINER /data --header "Host: web.bats"
assert_output "answer from port $port"
}

View file

@ -1,5 +0,0 @@
[pytest]
# disable the creation of the `.cache` folders
addopts = -p no:cacheprovider --ignore=requirements --ignore=certs --color=yes -v
markers =
incremental: mark a test as incremental.

View file

@ -1,28 +0,0 @@
#!/bin/sh
###############################################################################
# #
# This script is meant to run the test suite from a Docker container. #
# #
# This is useful when you want to run the test suite from Mac or #
# Docker Toolbox. #
# #
###############################################################################
# Returns the absolute directory path to this script
TESTDIR=$(cd "${0%/*}" && pwd) || exit 1
DIR=$(cd "${TESTDIR}/.." && pwd) || exit 1
# check requirements
echo "> Building nginx-proxy-tester image..."
docker build --pull -t nginx-proxy-tester \
-f "${TESTDIR}/requirements/Dockerfile-nginx-proxy-tester" \
"${TESTDIR}/requirements" \
|| exit 1
# run the nginx-proxy-tester container setting the correct value for the working dir
# in order for docker compose to work properly when run from within that container.
exec docker run --rm -it --name "nginx-proxy-pytest" \
--volume "/var/run/docker.sock:/var/run/docker.sock" \
--volume "${DIR}:${DIR}" \
--workdir "${TESTDIR}" \
nginx-proxy-tester "$@"

View file

@ -1,35 +0,0 @@
FROM python:3.12
ENV PYTEST_RUNNING_IN_CONTAINER=1
COPY python-requirements.txt /requirements.txt
RUN pip install -r /requirements.txt
# Add Docker's official GPG key
RUN apt-get update \
&& apt-get install -y \
ca-certificates \
curl \
&& install -m 0755 -d /etc/apt/keyrings \
&& curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc \
&& chmod a+r /etc/apt/keyrings/docker.asc
# Add the Docker repository to Apt sources
RUN echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
tee /etc/apt/sources.list.d/docker.list > /dev/null
# Install docker-ce-cli and docker-compose-plugin requirements for Pytest docker_compose fixture
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
docker-ce-cli \
docker-compose-plugin \
&& apt-get clean \
&& rm -r /var/lib/apt/lists/*
# Check if docker compose is available
RUN docker compose version
WORKDIR /test
ENTRYPOINT ["pytest"]

View file

@ -1,52 +0,0 @@
This directory contains resources to build Docker images tests depend on
# Build images
make build-webserver
# python-requirements.txt
If you want to run the test suite from your computer, you need python and a few python modules.
The _python-requirements.txt_ file describes the python modules required. To install them, use
pip:
pip install -r python-requirements.txt
If you don't want to run the test from your computer, you can run the tests from a docker container, see the _pytest.sh_ script.
# Images
## web
This container will run one or many webservers, each of them listening on a single port.
Ports are specified using the `WEB_PORTS` environment variable:
docker run -d -e WEB_PORTS=80 web # will create a container running one webserver listening on port 80
docker run -d -e WEB_PORTS="80 81" web # will create a container running two webservers, one listening on port 80 and a second one listening on port 81
The webserver answers on two paths:
- `/headers`
- `/port`
```
$ docker run -d -e WEB_PORTS=80 -p 80:80 web
$ curl http://127.0.0.1:80/headers
Host: 127.0.0.1
User-Agent: curl/7.47.0
Accept: */*
$ curl http://127.0.0.1:80/port
answer from port 80
```
## nginx-proxy-tester
This is an optional requirement which is usefull if you cannot (or don't want to) install pytest and its requirements on your computer. In this case, you can use the `nginx-proxy-tester` docker image to run the test suite from a Docker container.
To use this image, it is mandatory to run the container using the `pytest.sh` shell script. The script will build the image and run a container from it with the appropriate volumes and settings.

View file

@ -1,6 +0,0 @@
backoff==2.2.1
docker==7.1.0
packaging==24.2
pytest==8.3.4
requests==2.32.3
urllib3==2.3.0

View file

@ -1,9 +0,0 @@
# Docker Image running one (or multiple) webservers listening on all given ports from WEB_PORTS environment variable
FROM python:3-alpine
RUN apk add --no-cache bash
COPY ./webserver.py /
COPY ./entrypoint.sh /
WORKDIR /opt
ENTRYPOINT ["/bin/bash", "/entrypoint.sh"]

View file

@ -1,15 +0,0 @@
#!/bin/bash
set -u
trap '[ ${#PIDS[@]} -gt 0 ] && kill -TERM ${PIDS[@]}' TERM
declare -a PIDS
for port in $WEB_PORTS; do
echo starting a web server listening on port "$port";
/webserver.py "$port" &
PIDS+=($!)
done
wait "${PIDS[@]}"
trap - TERM
wait "${PIDS[@]}"

View file

@ -1,38 +0,0 @@
#!/usr/bin/env python3
import os, sys, re
import http.server
import socketserver
class Handler(http.server.SimpleHTTPRequestHandler):
def do_GET(self):
response_body = ""
response_code = 200
if self.path == "/headers":
response_body += self.headers.as_string()
elif self.path == "/port":
response_body += f"answer from port {PORT}\n"
elif re.match(r"/status/(\d+)", self.path):
result = re.match(r"/status/(\d+)", self.path)
response_code = int(result.group(1))
response_body += f"answer with response code {response_code}\n"
elif self.path == "/":
response_body += f"I'm {os.environ['HOSTNAME']}\n"
else:
response_body += "No route for this path!\n"
response_code = 404
self.send_response(response_code)
self.send_header("Content-Type", "text/plain")
self.end_headers()
if len(response_body):
self.wfile.write(response_body.encode())
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()

117
test/ssl.bats Normal file
View file

@ -0,0 +1,117 @@
#!/usr/bin/env bats
load test_helpers
SUT_CONTAINER=bats-nginx-proxy-${TEST_FILE}
function setup {
# make sure to stop any web container before each test so we don't
# have any unexpected contaiener running with VIRTUAL_HOST or VIRUTAL_PORT set
stop_bats_containers web
}
@test "[$TEST_FILE] start a nginx-proxy container" {
run nginxproxy $SUT_CONTAINER -v /var/run/docker.sock:/tmp/docker.sock:ro -v ${DIR}/lib/ssl:/etc/nginx/certs:ro
assert_success
docker_wait_for_log $SUT_CONTAINER 9 "Watching docker events"
}
@test "[$TEST_FILE] test SSL for VIRTUAL_HOST=*.nginx-proxy.bats" {
# WHEN
prepare_web_container bats-ssl-hosts-1 "80 443" \
-e VIRTUAL_HOST=*.nginx-proxy.bats \
-e CERT_NAME=nginx-proxy.bats
dockergen_wait_for_event $SUT_CONTAINER start bats-ssl-hosts-1
sleep 1
# THEN
assert_301 test.nginx-proxy.bats
assert_200_https test.nginx-proxy.bats
}
@test "[$TEST_FILE] test HTTPS_METHOD=nohttp" {
# WHEN
prepare_web_container bats-ssl-hosts-2 "80 443" \
-e VIRTUAL_HOST=*.nginx-proxy.bats \
-e CERT_NAME=nginx-proxy.bats \
-e HTTPS_METHOD=nohttp
dockergen_wait_for_event $SUT_CONTAINER start bats-ssl-hosts-2
sleep 1
# THEN
assert_503 test.nginx-proxy.bats
assert_200_https test.nginx-proxy.bats
}
@test "[$TEST_FILE] test HTTPS_METHOD=noredirect" {
# WHEN
prepare_web_container bats-ssl-hosts-3 "80 443" \
-e VIRTUAL_HOST=*.nginx-proxy.bats \
-e CERT_NAME=nginx-proxy.bats \
-e HTTPS_METHOD=noredirect
dockergen_wait_for_event $SUT_CONTAINER start bats-ssl-hosts-3
sleep 1
# THEN
assert_200 test.nginx-proxy.bats
assert_200_https test.nginx-proxy.bats
}
@test "[$TEST_FILE] stop all bats containers" {
stop_bats_containers
}
# assert that querying nginx-proxy with the given Host header produces a `HTTP 200` response
# $1 Host HTTP header to use when querying nginx-proxy
function assert_200 {
local -r host=$1
run curl_container $SUT_CONTAINER / --head --header "Host: $host"
assert_output -l 0 $'HTTP/1.1 200 OK\r'
}
# assert that querying nginx-proxy with the given Host header produces a `HTTP 503` response
# $1 Host HTTP header to use when querying nginx-proxy
function assert_503 {
local -r host=$1
run curl_container $SUT_CONTAINER / --head --header "Host: $host"
assert_output -l 0 $'HTTP/1.1 503 Service Temporarily Unavailable\r'
}
# assert that querying nginx-proxy with the given Host header produces a `HTTP 503` response
# $1 Host HTTP header to use when querying nginx-proxy
function assert_301 {
local -r host=$1
run curl_container $SUT_CONTAINER / --head --header "Host: $host"
assert_output -l 0 $'HTTP/1.1 301 Moved Permanently\r'
}
# assert that querying nginx-proxy with the given Host header produces a `HTTP 200` response
# $1 Host HTTP header to use when querying nginx-proxy
function assert_200_https {
local -r host=$1
run curl_container_https $SUT_CONTAINER / --head --header "Host: $host"
assert_output -l 0 $'HTTP/1.1 200 OK\r'
}
# assert that querying nginx-proxy with the given Host header produces a `HTTP 503` response
# $1 Host HTTP header to use when querying nginx-proxy
function assert_503_https {
local -r host=$1
run curl_container_https $SUT_CONTAINER / --head --header "Host: $host"
assert_output -l 0 $'HTTP/1.1 503 Service Temporarily Unavailable\r'
}
# assert that querying nginx-proxy with the given Host header produces a `HTTP 503` response
# $1 Host HTTP header to use when querying nginx-proxy
function assert_301_https {
local -r host=$1
run curl_container_https $SUT_CONTAINER / --head --header "Host: $host"
assert_output -l 0 $'HTTP/1.1 301 Moved Permanently\r'
}

View file

@ -1,70 +0,0 @@
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 4096 (0x1000)
Signature Algorithm: sha256WithRSAEncryption
Issuer: O=nginx-proxy test suite, CN=www.nginx-proxy.tld
Validity
Not Before: Jan 10 00:08:52 2017 GMT
Not After : May 28 00:08:52 2044 GMT
Subject: CN=*.nginx-proxy.tld
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (2048 bit)
Modulus:
00:cb:45:f4:14:9b:fe:64:85:79:4a:36:8d:3d:d1:
27:d0:7c:36:28:30:e6:73:80:6f:7c:49:23:d0:6c:
17:e4:44:c0:77:4d:9a:c2:bc:24:84:e3:a5:4d:ba:
d2:da:51:7b:a1:2a:12:d4:c0:19:55:69:2c:22:27:
2d:1a:f6:fc:4b:7f:e9:cb:a8:3c:e8:69:b8:d2:4f:
de:4e:50:e2:d0:74:30:7c:42:5a:ae:aa:85:a5:b1:
71:4d:c9:7e:86:8b:62:8c:3e:0d:e3:3b:c3:f5:81:
0b:8c:68:79:fe:bf:10:fb:ae:ec:11:49:6d:64:5e:
1a:7d:b3:92:93:4e:96:19:3a:98:04:a7:66:b2:74:
61:2d:41:13:0c:a4:54:0d:2c:78:fd:b4:a3:e8:37:
78:9a:de:fa:bc:2e:a8:0f:67:14:58:ce:c3:87:d5:
14:0e:8b:29:7d:48:19:b2:a9:f5:b4:e8:af:32:21:
67:15:7e:43:52:8b:20:cf:9f:38:43:bf:fd:c8:24:
7f:52:a3:88:f2:f1:4a:14:91:2a:6e:91:6f:fb:7d:
6a:78:c6:6d:2e:dd:1e:4c:2b:63:bb:3a:43:9c:91:
f9:df:d3:08:13:63:86:7d:ce:e8:46:cf:f1:6c:1f:
ca:f7:4c:de:d8:4b:e0:da:bc:06:d9:87:0f:ff:96:
45:85
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Subject Alternative Name:
DNS:*.nginx-proxy.tld
Signature Algorithm: sha256WithRSAEncryption
6e:a5:0e:e4:d3:cc:d5:b7:fc:34:75:89:4e:98:8c:e7:08:06:
a8:5b:ec:13:7d:83:99:a2:61:b8:d5:12:6e:c5:b4:53:4e:9a:
22:cd:ad:14:30:6a:7d:58:d7:23:d9:a4:2a:96:a0:40:9e:50:
9f:ce:f2:fe:8c:dd:9a:ac:99:39:5b:89:2d:ca:e5:3e:c3:bc:
03:04:1c:12:d9:6e:b8:9f:f0:3a:be:12:44:7e:a4:21:86:73:
af:d5:00:51:3f:2c:56:70:34:8f:26:b0:7f:b0:cf:cf:7f:f9:
40:6f:00:29:c4:cf:c3:b7:c2:49:3d:3f:b0:26:78:87:b9:c7:
6c:1b:aa:6a:1a:dd:c5:eb:f2:69:ba:6d:46:0b:92:49:b5:11:
3c:eb:48:c7:2f:fb:33:a6:6a:82:a2:ab:f8:1e:5f:7d:e3:b7:
f2:fd:f5:88:a5:09:4d:a0:bc:f4:3b:cd:d2:8b:d7:57:1f:86:
3b:d2:3e:a4:92:21:b0:02:0b:e9:e0:c4:1c:f1:78:e2:58:a7:
26:5f:4c:29:c8:23:f0:6e:12:3f:bd:ad:44:7b:0b:bd:db:ba:
63:8d:07:c6:9d:dc:46:cc:63:40:ba:5e:45:82:dd:9a:e5:50:
e8:e7:d7:27:88:fc:6f:1d:8a:e7:5c:49:28:aa:10:29:75:28:
c7:52:de:f9
-----BEGIN CERTIFICATE-----
MIIC9zCCAd+gAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwPzEfMB0GA1UECgwWbmdp
bngtcHJveHkgdGVzdCBzdWl0ZTEcMBoGA1UEAwwTd3d3Lm5naW54LXByb3h5LnRs
ZDAeFw0xNzAxMTAwMDA4NTJaFw00NDA1MjgwMDA4NTJaMBwxGjAYBgNVBAMMESou
bmdpbngtcHJveHkudGxkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
y0X0FJv+ZIV5SjaNPdEn0Hw2KDDmc4BvfEkj0GwX5ETAd02awrwkhOOlTbrS2lF7
oSoS1MAZVWksIictGvb8S3/py6g86Gm40k/eTlDi0HQwfEJarqqFpbFxTcl+hoti
jD4N4zvD9YELjGh5/r8Q+67sEUltZF4afbOSk06WGTqYBKdmsnRhLUETDKRUDSx4
/bSj6Dd4mt76vC6oD2cUWM7Dh9UUDospfUgZsqn1tOivMiFnFX5DUosgz584Q7/9
yCR/UqOI8vFKFJEqbpFv+31qeMZtLt0eTCtjuzpDnJH539MIE2OGfc7oRs/xbB/K
90ze2Evg2rwG2YcP/5ZFhQIDAQABoyAwHjAcBgNVHREEFTATghEqLm5naW54LXBy
b3h5LnRsZDANBgkqhkiG9w0BAQsFAAOCAQEAbqUO5NPM1bf8NHWJTpiM5wgGqFvs
E32DmaJhuNUSbsW0U06aIs2tFDBqfVjXI9mkKpagQJ5Qn87y/ozdmqyZOVuJLcrl
PsO8AwQcEtluuJ/wOr4SRH6kIYZzr9UAUT8sVnA0jyawf7DPz3/5QG8AKcTPw7fC
ST0/sCZ4h7nHbBuqahrdxevyabptRguSSbURPOtIxy/7M6ZqgqKr+B5ffeO38v31
iKUJTaC89DvN0ovXVx+GO9I+pJIhsAIL6eDEHPF44linJl9MKcgj8G4SP72tRHsL
vdu6Y40Hxp3cRsxjQLpeRYLdmuVQ6OfXJ4j8bx2K51xJKKoQKXUox1Le+Q==
-----END CERTIFICATE-----

View file

@ -1,27 +0,0 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAy0X0FJv+ZIV5SjaNPdEn0Hw2KDDmc4BvfEkj0GwX5ETAd02a
wrwkhOOlTbrS2lF7oSoS1MAZVWksIictGvb8S3/py6g86Gm40k/eTlDi0HQwfEJa
rqqFpbFxTcl+hotijD4N4zvD9YELjGh5/r8Q+67sEUltZF4afbOSk06WGTqYBKdm
snRhLUETDKRUDSx4/bSj6Dd4mt76vC6oD2cUWM7Dh9UUDospfUgZsqn1tOivMiFn
FX5DUosgz584Q7/9yCR/UqOI8vFKFJEqbpFv+31qeMZtLt0eTCtjuzpDnJH539MI
E2OGfc7oRs/xbB/K90ze2Evg2rwG2YcP/5ZFhQIDAQABAoIBAQCjAro2PNLJMfCO
fyjNRgmzu6iCmpR0U68T8GN0JPsT576g7e8J828l0pkhuIyW33lRSThIvLSUNf9a
dChL032H3lBTLduKVh4NKleQXnVFzaeEPoISSFVdButiAhAhPW4OIUVp0OfY3V+x
fac3j2nDLAfL5SKAtqZv363Py9m66EBYm5BmGTQqT/frQWeCEBvlErQef5RIaU8p
e2zMWgSNNojVai8U3nKNRvYHWeWXM6Ck7lCvkHhMF+RpbmCZuqhbEARVnehU/Jdn
QHJ3nxeA2OWpoWKXvAHtSnno49yxq1UIstiQvY+ng5C5i56UlB60UiU2NJ6doZkB
uQ7/1MaBAoGBAORdcFtgdgRALjXngFWhpCp0CseyUehn1KhxDCG+D1pJ142/ymcf
oJOzKJPMRNDdDUBMnR1GBfy7rmwvYevI/SMNy2Qs7ofcXPbdtwwvTCToZ1V9/54k
VfuPBFT+3QzWRvG1tjTV3E4L2VV3nrl2qNPhE5DlfIaU3nQq5Fl0HprJAoGBAOPf
MWOTGev61CdODO5KN3pLAoamiPs5lEUlz3kM3L1Q52YLITxNDjRj9hWBUATJZOS2
pLOoYRwmhD7vrnimMc41+NuuFX+4T7hWPc8uSuOxX0VijYtULyNRK57mncG1Fq9M
RMLbOJ7FD+8jdXNsSMqpQ+pxLJRX/A10O2fOQnbdAoGAL5hV4YWSM0KZHvz332EI
ER0MXiCJN7HkPZMKH0I4eu3m8hEmAyYxVndBnsQ1F37q0xrkqAQ/HTSUntGlS/og
4Bxw5pkCwegoq/77tpto+ExDtSrEitYx4XMmSPyxX4qNULU5m3tzJgUML+b1etwD
Rd2kMU/TC02dq4KBAy/TbRkCgYAl1xN5iJz+XenLGR/2liZ+TWR+/bqzlU006mF4
pZUmbv/uJxz+yYD5XDwqOA4UrWjuvhG9r9FoflDprp2XdWnB556KxG7XhcDfSJr9
A5/2DadXe1Ur9O/a+oi2228JEsxQkea9QPA3FVxfBtFjOHEiDlez39VaUP4PMeUH
iO3qlQKBgFQhdTb7HeYnApYIDHLmd1PvjRvp8XKR1CpEN0nkw8HpHcT1q1MUjQCr
iT6FQupULEvGmO3frQsgVeRIQDbEdZK3C5xCtn6qOw70sYATVf361BbTtidmU9yV
THFxwDSVLiVZgFryoY/NtAc27sVdJnGsPRjjaeVgALAsLbmZ1K/H
-----END RSA PRIVATE KEY-----

View file

@ -1,6 +0,0 @@
services:
nginx-proxy:
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
- ${PYTEST_MODULE_PATH}/certs:/etc/nginx/certs:ro
- ${PYTEST_MODULE_PATH}/acme_root:/usr/share/nginx/html:ro

View file

@ -1,27 +0,0 @@
def test_redirect_acme_challenge_location_disabled(docker_compose, nginxproxy, acme_challenge_path):
r = nginxproxy.get(
f"http://web1.nginx-proxy.tld/{acme_challenge_path}",
allow_redirects=False
)
assert r.status_code == 301
def test_redirect_acme_challenge_location_enabled(docker_compose, nginxproxy, acme_challenge_path):
r = nginxproxy.get(
f"http://web2.nginx-proxy.tld/{acme_challenge_path}",
allow_redirects=False
)
assert r.status_code == 200
def test_noredirect_acme_challenge_location_disabled(docker_compose, nginxproxy, acme_challenge_path):
r = nginxproxy.get(
f"http://web3.nginx-proxy.tld/{acme_challenge_path}",
allow_redirects=False
)
assert r.status_code == 404
def test_noredirect_acme_challenge_location_enabled(docker_compose, nginxproxy, acme_challenge_path):
r = nginxproxy.get(
f"http://web4.nginx-proxy.tld/{acme_challenge_path}",
allow_redirects=False
)
assert r.status_code == 200

View file

@ -1,40 +0,0 @@
services:
nginx-proxy:
environment:
ACME_HTTP_CHALLENGE_LOCATION: "false"
web1:
image: web
expose:
- "81"
environment:
WEB_PORTS: "81"
VIRTUAL_HOST: "web1.nginx-proxy.tld"
web2:
image: web
expose:
- "82"
environment:
WEB_PORTS: "82"
VIRTUAL_HOST: "web2.nginx-proxy.tld"
ACME_HTTP_CHALLENGE_LOCATION: "true"
web3:
image: web
expose:
- "83"
environment:
WEB_PORTS: "83"
VIRTUAL_HOST: "web3.nginx-proxy.tld"
HTTPS_METHOD: noredirect
web4:
image: web
expose:
- "84"
environment:
WEB_PORTS: "84"
VIRTUAL_HOST: "web4.nginx-proxy.tld"
HTTPS_METHOD: noredirect
ACME_HTTP_CHALLENGE_LOCATION: "true"

View file

@ -1,27 +0,0 @@
def test_redirect_acme_challenge_location_enabled(docker_compose, nginxproxy, acme_challenge_path):
r = nginxproxy.get(
f"http://web1.nginx-proxy.tld/{acme_challenge_path}",
allow_redirects=False
)
assert r.status_code == 200
def test_redirect_acme_challenge_location_disabled(docker_compose, nginxproxy, acme_challenge_path):
r = nginxproxy.get(
f"http://web2.nginx-proxy.tld/{acme_challenge_path}",
allow_redirects=False
)
assert r.status_code == 301
def test_noredirect_acme_challenge_location_enabled(docker_compose, nginxproxy, acme_challenge_path):
r = nginxproxy.get(
f"http://web3.nginx-proxy.tld/{acme_challenge_path}",
allow_redirects=False
)
assert r.status_code == 200
def test_noredirect_acme_challenge_location_disabled(docker_compose, nginxproxy, acme_challenge_path):
r = nginxproxy.get(
f"http://web4.nginx-proxy.tld/{acme_challenge_path}",
allow_redirects=False
)
assert r.status_code == 404

View file

@ -1,36 +0,0 @@
services:
web1:
image: web
expose:
- "81"
environment:
WEB_PORTS: "81"
VIRTUAL_HOST: "web1.nginx-proxy.tld"
web2:
image: web
expose:
- "82"
environment:
WEB_PORTS: "82"
VIRTUAL_HOST: "web2.nginx-proxy.tld"
ACME_HTTP_CHALLENGE_LOCATION: "false"
web3:
image: web
expose:
- "83"
environment:
WEB_PORTS: "83"
VIRTUAL_HOST: "web3.nginx-proxy.tld"
HTTPS_METHOD: noredirect
web4:
image: web
expose:
- "84"
environment:
WEB_PORTS: "84"
VIRTUAL_HOST: "web4.nginx-proxy.tld"
HTTPS_METHOD: noredirect
ACME_HTTP_CHALLENGE_LOCATION: "false"

View file

@ -1,13 +0,0 @@
def test_redirect_acme_challenge_location_legacy(docker_compose, nginxproxy, acme_challenge_path):
r = nginxproxy.get(
f"http://web1.nginx-proxy.tld/{acme_challenge_path}",
allow_redirects=False
)
assert r.status_code == 200
def test_noredirect_acme_challenge_location_legacy(docker_compose, nginxproxy, acme_challenge_path):
r = nginxproxy.get(
f"http://web2.nginx-proxy.tld/{acme_challenge_path}",
allow_redirects=False
)
assert r.status_code == 404

View file

@ -1,21 +0,0 @@
services:
nginx-proxy:
environment:
ACME_HTTP_CHALLENGE_LOCATION: "legacy"
web1:
image: web
expose:
- "81"
environment:
WEB_PORTS: "81"
VIRTUAL_HOST: "web1.nginx-proxy.tld"
web2:
image: web
expose:
- "82"
environment:
WEB_PORTS: "82"
VIRTUAL_HOST: "web2.nginx-proxy.tld"
HTTPS_METHOD: noredirect

View file

@ -1,66 +0,0 @@
"""
Test that nginx-proxy-tester can build successfully
"""
import pathlib
import re
import docker
import pytest
client = docker.from_env()
@pytest.fixture(scope = "session")
def docker_build(request):
# Define Dockerfile path
current_file_path = pathlib.Path(__file__)
dockerfile_path = current_file_path.parent.parent.joinpath("requirements")
dockerfile_name = "Dockerfile-nginx-proxy-tester"
# Build the Docker image
image, logs = client.images.build(
path = dockerfile_path.as_posix(),
dockerfile = dockerfile_name,
rm = True, # Remove intermediate containers
tag = "nginx-proxy-tester-ci", # Tag for the built image
)
# Check for build success
for log in logs:
if "stream" in log:
print(log["stream"].strip())
if "error" in log:
raise Exception(log["error"])
def teardown():
# Clean up after teardown
client.images.remove(image.id, force=True)
request.addfinalizer(teardown)
# Return the image name
return "nginx-proxy-tester-ci"
def test_build_nginx_proxy_tester(docker_build):
assert docker_build == "nginx-proxy-tester-ci"
def test_run_nginx_proxy_tester(docker_build):
# Run the container with 'pytest -v' command to output version info
container = client.containers.run("nginx-proxy-tester-ci",
command = "pytest -V",
detach = True,
)
# Wait for the container to finish and get the exit code
result = container.wait()
exit_code = result.get("StatusCode", 1) # Default to 1 (error) if not found
# Get the output logs from the container
output = container.logs().decode("utf-8").strip()
# Clean up: Remove the container
container.remove()
# Assertions
assert exit_code == 0, "Container exited with a non-zero exit code"
assert re.search(r"pytest\s\d+\.\d+\.\d+", output)

View file

@ -1,23 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Maintenance</title>
<style>
html {
color-scheme: light dark;
}
body {
width: 35em;
margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif;
}
</style>
</head>
<body>
<h1>Damn, there's some maintenance in progress.</h1>
<p>
Our apologies for this temporary inconvenience. Regular service
performance will be re-established shortly.
</p>
</body>
</html>

View file

@ -1,7 +0,0 @@
import re
def test_custom_error_page(docker_compose, nginxproxy):
r = nginxproxy.get("http://unknown.nginx-proxy.tld")
assert r.status_code == 503
assert re.search(r"Damn, there's some maintenance in progress.", r.text)

View file

@ -1,5 +0,0 @@
services:
nginx-proxy:
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
- ${PYTEST_MODULE_PATH}/50x.html:/usr/share/nginx/html/errors/50x.html:ro

View file

@ -1 +0,0 @@
add_header X-test bar;

View file

@ -1 +0,0 @@
add_header X-test f00;

View file

@ -1,26 +0,0 @@
def test_custom_default_conf_does_not_apply_to_unknown_vhost(docker_compose, nginxproxy):
r = nginxproxy.get("http://nginx-proxy/")
assert r.status_code == 503
assert "X-test" not in r.headers
def test_custom_default_conf_applies_to_web1(docker_compose, nginxproxy):
r = nginxproxy.get("http://web1.nginx-proxy.example/port")
assert r.status_code == 200
assert r.text == "answer from port 81\n"
assert "X-test" in r.headers
assert "f00" == r.headers["X-test"]
def test_custom_default_conf_applies_to_web2(docker_compose, nginxproxy):
r = nginxproxy.get("http://web2.nginx-proxy.example/port")
assert r.status_code == 200
assert r.text == "answer from port 82\n"
assert "X-test" in r.headers
assert "f00" == r.headers["X-test"]
def test_custom_default_conf_is_overriden_for_web3(docker_compose, nginxproxy):
r = nginxproxy.get("http://web3.nginx-proxy.example/port")
assert r.status_code == 200
assert r.text == "answer from port 83\n"
assert "X-test" in r.headers
assert "bar" == r.headers["X-test"]

View file

@ -1,30 +0,0 @@
services:
nginx-proxy:
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
- ${PYTEST_MODULE_PATH}/my_custom_proxy_settings_f00.conf:/etc/nginx/vhost.d/default_location:ro
- ${PYTEST_MODULE_PATH}/my_custom_proxy_settings_bar.conf:/etc/nginx/vhost.d/web3.nginx-proxy.example_location:ro
web1:
image: web
expose:
- "81"
environment:
WEB_PORTS: "81"
VIRTUAL_HOST: web1.nginx-proxy.example
web2:
image: web
expose:
- "82"
environment:
WEB_PORTS: "82"
VIRTUAL_HOST: web2.nginx-proxy.example
web3:
image: web
expose:
- "83"
environment:
WEB_PORTS: "83"
VIRTUAL_HOST: web3.nginx-proxy.example

View file

@ -1,18 +0,0 @@
def test_custom_conf_does_not_apply_to_unknown_vhost(docker_compose, nginxproxy):
r = nginxproxy.get("http://nginx-proxy/")
assert r.status_code == 503
assert "X-test" not in r.headers
def test_custom_conf_applies_to_web1(docker_compose, nginxproxy):
r = nginxproxy.get("http://web1.nginx-proxy.example/port")
assert r.status_code == 200
assert r.text == "answer from port 81\n"
assert "X-test" in r.headers
assert "f00" == r.headers["X-test"]
def test_custom_conf_applies_to_web2(docker_compose, nginxproxy):
r = nginxproxy.get("http://web2.nginx-proxy.example/port")
assert r.status_code == 200
assert r.text == "answer from port 82\n"
assert "X-test" in r.headers
assert "f00" == r.headers["X-test"]

View file

@ -1,21 +0,0 @@
services:
nginx-proxy:
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
- ${PYTEST_MODULE_PATH}/my_custom_proxy_settings_f00.conf:/etc/nginx/proxy.conf:ro
web1:
image: web
expose:
- "81"
environment:
WEB_PORTS: "81"
VIRTUAL_HOST: web1.nginx-proxy.example
web2:
image: web
expose:
- "82"
environment:
WEB_PORTS: "82"
VIRTUAL_HOST: web2.nginx-proxy.example

View file

@ -1,27 +0,0 @@
def test_custom_conf_does_not_apply_to_unknown_vhost(docker_compose, nginxproxy):
r = nginxproxy.get("http://nginx-proxy/")
assert r.status_code == 503
assert "X-test" not in r.headers
def test_custom_conf_applies_to_web1(docker_compose, nginxproxy):
r = nginxproxy.get("http://web1.nginx-proxy.example/port")
assert r.status_code == 200
assert r.text == "answer from port 81\n"
assert "X-test" in r.headers
assert "f00" == r.headers["X-test"]
def test_custom_conf_applies_to_regex(docker_compose, nginxproxy):
r = nginxproxy.get("http://regex.foo.nginx-proxy.example/port")
assert r.status_code == 200
assert r.text == "answer from port 83\n"
assert "X-test" in r.headers
assert "bar" == r.headers["X-test"]
def test_custom_conf_does_not_apply_to_web2(docker_compose, nginxproxy):
r = nginxproxy.get("http://web2.nginx-proxy.example/port")
assert r.status_code == 200
assert r.text == "answer from port 82\n"
assert "X-test" not in r.headers
def test_custom_block_is_present_in_nginx_generated_conf(docker_compose, nginxproxy):
assert b"include /etc/nginx/vhost.d/web1.nginx-proxy.example_location;" in nginxproxy.get_conf()

View file

@ -1,30 +0,0 @@
services:
nginx-proxy:
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
- ${PYTEST_MODULE_PATH}/my_custom_proxy_settings_f00.conf:/etc/nginx/vhost.d/web1.nginx-proxy.example_location:ro
- ${PYTEST_MODULE_PATH}/my_custom_proxy_settings_bar.conf:/etc/nginx/vhost.d/561032515ede3ab3a015edfb244608b72409c430_location:ro
web1:
image: web
expose:
- "81"
environment:
WEB_PORTS: "81"
VIRTUAL_HOST: web1.nginx-proxy.example
web2:
image: web
expose:
- "82"
environment:
WEB_PORTS: "82"
VIRTUAL_HOST: web2.nginx-proxy.example
regex:
image: web
expose:
- "83"
environment:
WEB_PORTS: "83"
VIRTUAL_HOST: ~^regex.*\.nginx-proxy\.example$

View file

@ -1,24 +0,0 @@
def test_custom_conf_does_not_apply_to_unknown_vhost(docker_compose, nginxproxy):
r = nginxproxy.get("http://nginx-proxy/")
assert r.status_code == 503
assert "X-test" not in r.headers
def test_custom_conf_applies_to_web1(docker_compose, nginxproxy):
r = nginxproxy.get("http://web1.nginx-proxy.example/port")
assert r.status_code == 200
assert r.text == "answer from port 81\n"
assert "X-test" in r.headers
assert "f00" == r.headers["X-test"]
def test_custom_conf_applies_to_regex(docker_compose, nginxproxy):
r = nginxproxy.get("http://regex.foo.nginx-proxy.example/port")
assert r.status_code == 200
assert r.text == "answer from port 83\n"
assert "X-test" in r.headers
assert "bar" == r.headers["X-test"]
def test_custom_conf_does_not_apply_to_web2(docker_compose, nginxproxy):
r = nginxproxy.get("http://web2.nginx-proxy.example/port")
assert r.status_code == 200
assert r.text == "answer from port 82\n"
assert "X-test" not in r.headers

View file

@ -1,30 +0,0 @@
services:
nginx-proxy:
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
- ${PYTEST_MODULE_PATH}/my_custom_proxy_settings_f00.conf:/etc/nginx/vhost.d/web1.nginx-proxy.example:ro
- ${PYTEST_MODULE_PATH}/my_custom_proxy_settings_bar.conf:/etc/nginx/vhost.d/561032515ede3ab3a015edfb244608b72409c430:ro
web1:
image: web
expose:
- "81"
environment:
WEB_PORTS: "81"
VIRTUAL_HOST: web1.nginx-proxy.example
web2:
image: web
expose:
- "82"
environment:
WEB_PORTS: "82"
VIRTUAL_HOST: web2.nginx-proxy.example
regex:
image: web
expose:
- "83"
environment:
WEB_PORTS: "83"
VIRTUAL_HOST: ~^regex.*\.nginx-proxy\.example$

View file

@ -1,18 +0,0 @@
def test_custom_conf_does_not_apply_to_unknown_vhost(docker_compose, nginxproxy):
r = nginxproxy.get("http://nginx-proxy/")
assert r.status_code == 503
assert "X-test" not in r.headers
def test_custom_conf_applies_to_web1(docker_compose, nginxproxy):
r = nginxproxy.get("http://web1.nginx-proxy.example/port")
assert r.status_code == 200
assert r.text == "answer from port 81\n"
assert "X-test" in r.headers
assert "f00" == r.headers["X-test"]
def test_custom_conf_applies_to_web2(docker_compose, nginxproxy):
r = nginxproxy.get("http://web2.nginx-proxy.example/port")
assert r.status_code == 200
assert r.text == "answer from port 82\n"
assert "X-test" in r.headers
assert "f00" == r.headers["X-test"]

View file

@ -1,21 +0,0 @@
services:
nginx-proxy:
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
- ${PYTEST_MODULE_PATH}/my_custom_proxy_settings_f00.conf:/etc/nginx/conf.d/my_custom_proxy_settings_f00.conf:ro
web1:
image: web
expose:
- "81"
environment:
WEB_PORTS: "81"
VIRTUAL_HOST: web1.nginx-proxy.example
web2:
image: web
expose:
- "82"
environment:
WEB_PORTS: "82"
VIRTUAL_HOST: web2.nginx-proxy.example

View file

@ -1,48 +0,0 @@
import json
import pytest
def test_debug_endpoint_is_enabled_globally(docker_compose, nginxproxy):
r = nginxproxy.get("http://enabled.debug.nginx-proxy.example/nginx-proxy-debug")
assert r.status_code == 200
r = nginxproxy.get("http://stripped.debug.nginx-proxy.example/nginx-proxy-debug")
assert r.status_code == 200
def test_debug_endpoint_response_contains_expected_values(docker_compose, nginxproxy):
r = nginxproxy.get("http://enabled.debug.nginx-proxy.example/nginx-proxy-debug")
assert r.status_code == 200
try:
jsonResponse = json.loads(r.text)
except ValueError as err:
pytest.fail("Failed to parse debug endpoint response as JSON: %s" % err, pytrace=False)
assert jsonResponse["global"]["enable_debug_endpoint"] == "true"
assert jsonResponse["vhost"]["enable_debug_endpoint"] == True
def test_debug_endpoint_paths_stripped_if_response_too_long(docker_compose, nginxproxy):
r = nginxproxy.get("http://stripped.debug.nginx-proxy.example/nginx-proxy-debug")
assert r.status_code == 200
try:
jsonResponse = json.loads(r.text)
except ValueError as err:
pytest.fail("Failed to parse debug endpoint response as JSON: %s" % err, pytrace=False)
if "paths" in jsonResponse["vhost"]:
pytest.fail("Expected paths to be stripped from debug endpoint response", pytrace=False)
assert jsonResponse["warning"] == "Virtual paths configuration for this hostname is too large and has been stripped from response."
def test_debug_endpoint_hostname_replaced_by_warning_if_regexp(docker_compose, nginxproxy):
r = nginxproxy.get("http://regexp.foo.debug.nginx-proxy.example/nginx-proxy-debug")
assert r.status_code == 200
try:
jsonResponse = json.loads(r.text)
except ValueError as err:
pytest.fail("Failed to parse debug endpoint response as JSON: %s" % err, pytrace=False)
assert jsonResponse["vhost"]["hostname"] == "Hostname is a regexp and unsafe to include in the debug response."
def test_debug_endpoint_is_disabled_per_container(docker_compose, nginxproxy):
r = nginxproxy.get("http://disabled.debug.nginx-proxy.example/nginx-proxy-debug")
assert r.status_code == 404

View file

@ -1,59 +0,0 @@
services:
nginx-proxy:
environment:
DEBUG_ENDPOINT: "true"
debug_enabled:
image: web
expose:
- "81"
environment:
WEB_PORTS: "81"
VIRTUAL_HOST: enabled.debug.nginx-proxy.example
debug_stripped:
image: web
expose:
- "82"
environment:
WEB_PORTS: "82"
VIRTUAL_HOST_MULTIPORTS: |-
stripped.debug.nginx-proxy.example:
"/1":
"/2":
"/3":
"/4":
"/5":
"/6":
"/7":
"/8":
"/9":
"/10":
"/11":
"/12":
"/13":
"/14":
"/15":
"/16":
"/17":
"/18":
"/19":
"/20":
debug_regexp:
image: web
expose:
- "84"
environment:
WEB_PORTS: "84"
VIRTUAL_HOST: ~^regexp.*\.debug.nginx-proxy.example
debug_disabled:
image: web
expose:
- "83"
environment:
WEB_PORTS: "83"
VIRTUAL_HOST: disabled.debug.nginx-proxy.example
labels:
com.github.nginx-proxy.nginx-proxy.debug-endpoint: "false"

View file

@ -1,26 +0,0 @@
import json
import pytest
def test_debug_endpoint_is_disabled_globally(docker_compose, nginxproxy):
r = nginxproxy.get("http://disabled1.debug.nginx-proxy.example/nginx-proxy-debug")
assert r.status_code == 404
r = nginxproxy.get("http://disabled2.debug.nginx-proxy.example/nginx-proxy-debug")
assert r.status_code == 404
def test_debug_endpoint_is_enabled_per_container(docker_compose, nginxproxy):
r = nginxproxy.get("http://enabled.debug.nginx-proxy.example/nginx-proxy-debug")
assert r.status_code == 200
def test_debug_endpoint_response_contains_expected_values(docker_compose, nginxproxy):
r = nginxproxy.get("http://enabled.debug.nginx-proxy.example/nginx-proxy-debug")
assert r.status_code == 200
try:
jsonResponse = json.loads(r.text)
except ValueError as err:
pytest.fail("Failed to parse debug endpoint response as JSON:: %s" % err, pytrace=False)
assert jsonResponse["global"]["enable_debug_endpoint"] == "false"
assert jsonResponse["vhost"]["enable_debug_endpoint"] == True

View file

@ -1,27 +0,0 @@
services:
debug_disabled1:
image: web
expose:
- "81"
environment:
WEB_PORTS: "81"
VIRTUAL_HOST: disabled1.debug.nginx-proxy.example
debug_disabled2:
image: web
expose:
- "82"
environment:
WEB_PORTS: "82"
VIRTUAL_HOST: disabled2.debug.nginx-proxy.example
debug_enabled:
image: web
expose:
- "83"
environment:
WEB_PORTS: "83"
VIRTUAL_HOST: enabled.debug.nginx-proxy.example
labels:
com.github.nginx-proxy.nginx-proxy.debug-endpoint: "true"

View file

@ -1,4 +0,0 @@
def test_fallback_on_default(docker_compose, nginxproxy):
r = nginxproxy.get("http://unknown.nginx-proxy.tld/port")
assert r.status_code == 200
assert r.text == "answer from port 81\n"

View file

@ -1,12 +0,0 @@
services:
nginx-proxy:
environment:
DEFAULT_HOST: web1.tld
web1:
image: web
expose:
- "81"
environment:
WEB_PORTS: "81"
VIRTUAL_HOST: web1.tld

View file

@ -1,13 +0,0 @@
def test_unknown_virtual_host(docker_compose, nginxproxy):
r = nginxproxy.get("http://nginx-proxy/port")
assert r.status_code == 503
def test_forwards_to_web1(docker_compose, nginxproxy):
r = nginxproxy.get("http://web1.nginx-proxy.tld/port")
assert r.status_code == 200
assert r.text == "answer from port 81\n"
def test_forwards_to_web2(docker_compose, nginxproxy):
r = nginxproxy.get("http://web2.nginx-proxy.tld/port")
assert r.status_code == 200
assert r.text == "answer from port 82\n"

View file

@ -1,22 +0,0 @@
services:
nginx-proxy:
volumes:
- /var/run/docker.sock:/f00.sock:ro
environment:
DOCKER_HOST: unix:///f00.sock
web1:
image: web
expose:
- "81"
environment:
WEB_PORTS: "81"
VIRTUAL_HOST: web1.nginx-proxy.tld
web2:
image: web
expose:
- "82"
environment:
WEB_PORTS: "82"
VIRTUAL_HOST: web2.nginx-proxy.tld

View file

@ -1,30 +0,0 @@
volumes:
nginx_conf:
services:
nginx-proxy-nginx:
image: nginx
container_name: nginx
volumes:
- nginx_conf:/etc/nginx/conf.d:ro
ports:
- "80:80"
- "443:443"
nginx-proxy-dockergen:
image: nginxproxy/docker-gen
command: -notify-sighup nginx -watch /etc/docker-gen/templates/nginx.tmpl /etc/nginx/conf.d/default.conf
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
- ../../nginx.tmpl:/etc/docker-gen/templates/nginx.tmpl
- nginx_conf:/etc/nginx/conf.d
web:
image: web
container_name: whoami
expose:
- "80"
environment:
WEB_PORTS: "80"
VIRTUAL_HOST: whoami.nginx.container.docker

View file

@ -1,27 +0,0 @@
import docker
import pytest
from packaging.version import Version
raw_version = docker.from_env().version()["Version"]
pytestmark = pytest.mark.skipif(
Version(raw_version) < Version("1.13"),
reason="Docker compose syntax v3 requires docker engine v1.13 or later (got {raw_version})"
)
def test_unknown_virtual_host_is_503(docker_compose, nginxproxy):
r = nginxproxy.get("http://unknown.nginx.container.docker/")
assert r.status_code == 503
def test_forwards_to_whoami(docker_compose, nginxproxy):
r = nginxproxy.get("http://whoami.nginx.container.docker/")
assert r.status_code == 200
whoami_container = docker_compose.containers.get("whoami")
assert r.text == f"I'm {whoami_container.id[:12]}\n"
if __name__ == "__main__":
import doctest
doctest.testmod()

View file

@ -1,15 +0,0 @@
def test_nohttp_missing_cert_disabled(docker_compose, nginxproxy):
r = nginxproxy.get("http://nohttp-missing-cert-disabled.nginx-proxy.tld/", allow_redirects=False)
assert r.status_code == 503
def test_nohttp_missing_cert_enabled(docker_compose, nginxproxy):
r = nginxproxy.get("http://nohttp-missing-cert-enabled.nginx-proxy.tld/", allow_redirects=False)
assert r.status_code == 200
def test_redirect_missing_cert_disabled(docker_compose, nginxproxy):
r = nginxproxy.get("http://redirect-missing-cert-disabled.nginx-proxy.tld/", allow_redirects=False)
assert r.status_code == 301
def test_redirect_missing_cert_enabled(docker_compose, nginxproxy):
r = nginxproxy.get("http://redirect-missing-cert-enabled.nginx-proxy.tld/", allow_redirects=False)
assert r.status_code == 200

View file

@ -1,40 +0,0 @@
services:
nginx-proxy:
environment:
ENABLE_HTTP_ON_MISSING_CERT: "false"
nohttp-missing-cert-disabled:
image: web
expose:
- "81"
environment:
WEB_PORTS: "81"
VIRTUAL_HOST: nohttp-missing-cert-disabled.nginx-proxy.tld
HTTPS_METHOD: nohttp
nohttp-missing-cert-enabled:
image: web
expose:
- "82"
environment:
WEB_PORTS: "82"
VIRTUAL_HOST: nohttp-missing-cert-enabled.nginx-proxy.tld
HTTPS_METHOD: nohttp
ENABLE_HTTP_ON_MISSING_CERT: "true"
redirect-missing-cert-disabled:
image: web
expose:
- "83"
environment:
WEB_PORTS: "83"
VIRTUAL_HOST: redirect-missing-cert-disabled.nginx-proxy.tld
redirect-missing-cert-enabled:
image: web
expose:
- "84"
environment:
WEB_PORTS: "84"
VIRTUAL_HOST: redirect-missing-cert-enabled.nginx-proxy.tld
ENABLE_HTTP_ON_MISSING_CERT: "true"

View file

@ -1,84 +0,0 @@
"""
Test that nginx-proxy detects new containers
"""
from time import sleep
import pytest
from docker.errors import NotFound
@pytest.fixture
def web1(docker_compose):
"""
pytest fixture creating a web container with `VIRTUAL_HOST=web1.nginx-proxy` listening on port 81.
"""
container = docker_compose.containers.run(
name="web1",
image="web",
detach=True,
environment={
"WEB_PORTS": "81",
"VIRTUAL_HOST": "web1.nginx-proxy"
},
ports={"81/tcp": None}
)
docker_compose.networks.get("test_events-net").connect(container)
sleep(2) # give it some time to initialize and for docker-gen to detect it
yield container
try:
docker_compose.containers.get("web1").remove(force=True)
except NotFound:
pass
@pytest.fixture
def web2(docker_compose):
"""
pytest fixture creating a web container with `VIRTUAL_HOST=nginx-proxy`, `VIRTUAL_PATH=/web2/` and `VIRTUAL_DEST=/` listening on port 82.
"""
container = docker_compose.containers.run(
name="web2",
image="web",
detach=True,
environment={
"WEB_PORTS": "82",
"VIRTUAL_HOST": "nginx-proxy",
"VIRTUAL_PATH": "/web2/",
"VIRTUAL_DEST": "/",
},
ports={"82/tcp": None}
)
docker_compose.networks.get("test_events-net").connect(container)
sleep(2) # give it some time to initialize and for docker-gen to detect it
yield container
try:
docker_compose.containers.get("web2").remove(force=True)
except NotFound:
pass
def test_nginx_proxy_behavior_when_alone(docker_compose, nginxproxy):
r = nginxproxy.get("http://nginx-proxy/")
assert r.status_code == 503
def test_new_container_is_detected_vhost(web1, nginxproxy):
r = nginxproxy.get("http://web1.nginx-proxy/port")
assert r.status_code == 200
assert "answer from port 81\n" == r.text
web1.remove(force=True)
sleep(2)
r = nginxproxy.get("http://web1.nginx-proxy/port")
assert r.status_code == 503
def test_new_container_is_detected_vpath(web2, nginxproxy):
r = nginxproxy.get("http://nginx-proxy/web2/port")
assert r.status_code == 200
assert "answer from port 82\n" == r.text
r = nginxproxy.get("http://nginx-proxy/port")
assert r.status_code in [404, 503]
web2.remove(force=True)
sleep(2)
r = nginxproxy.get("http://nginx-proxy/web2/port")
assert r.status_code == 503

View file

@ -1,3 +0,0 @@
networks:
default:
name: test_events-net

View file

@ -1,9 +0,0 @@
services:
nginx-proxy:
image: nginxproxy/nginx-proxy:test
container_name: nginx-proxy
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
ports:
- "80:80"
- "443:443"

View file

@ -1,5 +0,0 @@
server {
server_name __;
listen 80 default_server;
return 418;
}

View file

@ -1,14 +0,0 @@
services:
nginx-proxy:
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
- ${PYTEST_MODULE_PATH}/test_fallback.data/custom-fallback.conf:/etc/nginx/conf.d/zzz-custom-fallback.conf:ro
http-only:
image: web
expose:
- "83"
environment:
WEB_PORTS: "83"
VIRTUAL_HOST: http-only.nginx-proxy.test
HTTPS_METHOD: nohttps

View file

@ -1,71 +0,0 @@
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 4096 (0x1000)
Signature Algorithm: sha256WithRSAEncryption
Issuer: O=nginx-proxy test suite, CN=www.nginx-proxy.tld
Validity
Not Before: Feb 7 21:54:16 2023 GMT
Not After : Jun 25 21:54:16 2050 GMT
Subject: CN=http-only.nginx-proxy.test
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
RSA Public-Key: (2048 bit)
Modulus:
00:b4:62:61:07:54:2e:6d:55:83:2d:24:b7:e2:15:
34:13:bd:79:21:e9:10:75:3f:4c:f8:ba:60:29:87:
e5:8e:2a:1e:fd:33:51:5a:8a:3a:6f:60:ff:24:f1:
1b:27:30:8c:ac:43:04:b7:79:cb:7a:ec:c6:08:a4:
a0:15:b0:0f:ee:6b:15:84:24:11:bc:85:2b:48:06:
04:0a:58:bb:8c:e8:4d:48:f5:06:c5:91:fe:5d:99:
0a:29:31:8a:f1:9b:0c:e0:39:75:a1:06:9b:d4:f5:
06:74:8f:46:5e:64:ba:2f:d0:3d:7c:3d:30:03:e9:
7c:35:17:69:04:f6:2e:29:d4:93:d6:d6:d2:6c:04:
38:06:21:06:05:30:8a:b9:9d:05:8d:12:6e:48:39:
bb:f6:93:4f:ba:a5:84:c7:96:2f:be:92:25:e9:d0:
95:2a:d9:23:8a:b3:28:0b:b6:19:1c:3b:be:a2:91:
70:44:a8:77:18:94:4b:df:61:f4:5c:c9:78:76:34:
b5:87:0f:c0:92:04:26:b6:ca:62:cd:9b:5d:eb:bf:
10:ac:df:af:72:5f:af:09:38:b1:dc:e1:3d:13:db:
a0:ac:b7:2e:ca:39:5c:4c:f1:1e:81:a8:b4:44:a2:
72:d5:3b:c0:71:cc:dc:16:0d:fa:38:96:44:b3:00:
d6:65
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Subject Alternative Name:
DNS:http-only.nginx-proxy.test
Signature Algorithm: sha256WithRSAEncryption
3b:54:95:48:4d:f6:93:38:42:40:02:ab:b7:17:3b:50:3b:ca:
c7:12:69:b0:da:cb:d7:3e:0e:1f:bf:a2:59:c7:fe:c2:5c:43:
84:92:b9:3a:be:8f:7e:2e:81:3c:ed:f3:a9:77:21:c2:35:f1:
da:cf:3a:1e:e2:ee:a2:ce:72:55:97:87:0e:ad:59:61:f7:75:
46:c0:2b:d4:88:b7:36:97:11:fb:5e:28:89:e9:2a:92:f1:15:
f1:43:8e:c1:38:85:8d:3a:26:7d:25:72:93:17:96:8d:5a:ed:
e8:73:3a:d5:8d:80:f2:af:38:84:ff:85:2e:d1:36:7d:2e:e1:
f0:2c:d8:15:5f:fc:c5:70:5d:25:6a:22:f3:2a:cd:0f:25:ad:
d4:93:d3:9a:3e:50:bc:da:a5:6c:86:ea:1d:d9:b9:c5:90:db:
f5:02:c8:c9:77:5c:ef:77:fe:74:60:41:33:d9:3c:a2:e1:73:
aa:14:18:5d:36:58:c8:41:63:4c:59:0e:4b:3d:c5:65:5a:01:
b0:16:50:0f:d0:4f:0d:ca:97:f6:11:47:06:6b:b1:ae:bb:26:
30:34:8b:7a:91:5d:8a:22:c7:f9:05:0d:bb:a5:b7:60:c0:20:
ce:d0:0e:c0:66:b3:e7:c4:61:ec:c5:40:e6:52:11:41:c3:11:
18:04:c7:1e
-----BEGIN CERTIFICATE-----
MIIDCzCCAfOgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwPzEfMB0GA1UECgwWbmdp
bngtcHJveHkgdGVzdCBzdWl0ZTEcMBoGA1UEAwwTd3d3Lm5naW54LXByb3h5LnRs
ZDAgFw0yMzAyMDcyMTU0MTZaGA8yMDUwMDYyNTIxNTQxNlowJTEjMCEGA1UEAwwa
aHR0cC1vbmx5Lm5naW54LXByb3h5LnRlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IB
DwAwggEKAoIBAQC0YmEHVC5tVYMtJLfiFTQTvXkh6RB1P0z4umAph+WOKh79M1Fa
ijpvYP8k8RsnMIysQwS3ect67MYIpKAVsA/uaxWEJBG8hStIBgQKWLuM6E1I9QbF
kf5dmQopMYrxmwzgOXWhBpvU9QZ0j0ZeZLov0D18PTAD6Xw1F2kE9i4p1JPW1tJs
BDgGIQYFMIq5nQWNEm5IObv2k0+6pYTHli++kiXp0JUq2SOKsygLthkcO76ikXBE
qHcYlEvfYfRcyXh2NLWHD8CSBCa2ymLNm13rvxCs369yX68JOLHc4T0T26Csty7K
OVxM8R6BqLREonLVO8BxzNwWDfo4lkSzANZlAgMBAAGjKTAnMCUGA1UdEQQeMByC
Gmh0dHAtb25seS5uZ2lueC1wcm94eS50ZXN0MA0GCSqGSIb3DQEBCwUAA4IBAQA7
VJVITfaTOEJAAqu3FztQO8rHEmmw2svXPg4fv6JZx/7CXEOEkrk6vo9+LoE87fOp
dyHCNfHazzoe4u6iznJVl4cOrVlh93VGwCvUiLc2lxH7XiiJ6SqS8RXxQ47BOIWN
OiZ9JXKTF5aNWu3oczrVjYDyrziE/4Uu0TZ9LuHwLNgVX/zFcF0laiLzKs0PJa3U
k9OaPlC82qVshuod2bnFkNv1AsjJd1zvd/50YEEz2Tyi4XOqFBhdNljIQWNMWQ5L
PcVlWgGwFlAP0E8Nypf2EUcGa7GuuyYwNIt6kV2KIsf5BQ27pbdgwCDO0A7AZrPn
xGHsxUDmUhFBwxEYBMce
-----END CERTIFICATE-----

View file

@ -1,27 +0,0 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAtGJhB1QubVWDLSS34hU0E715IekQdT9M+LpgKYfljioe/TNR
Woo6b2D/JPEbJzCMrEMEt3nLeuzGCKSgFbAP7msVhCQRvIUrSAYECli7jOhNSPUG
xZH+XZkKKTGK8ZsM4Dl1oQab1PUGdI9GXmS6L9A9fD0wA+l8NRdpBPYuKdST1tbS
bAQ4BiEGBTCKuZ0FjRJuSDm79pNPuqWEx5YvvpIl6dCVKtkjirMoC7YZHDu+opFw
RKh3GJRL32H0XMl4djS1hw/AkgQmtspizZtd678QrN+vcl+vCTix3OE9E9ugrLcu
yjlcTPEegai0RKJy1TvAcczcFg36OJZEswDWZQIDAQABAoIBAAfDA/HQyX6i41YZ
8l+kEe2XhZLT+IVTB/jb7C9dTZ9kaJj0kFeZAxKv1cq9JTH2gNcYuyc58muDrLHK
g6jrPoQ/z1k0RB8ci9Q5jgrz7n4NsOWmxXfS5GMaprlHDHeA+HjdgBZBtorfUDvL
vndpVimgiETETUCd115hd39jKHFcRcdV6yCix7ObywK3dMgLVpagCcnlyCWffS/r
nhhMfJ+VstW0nUtfZ7JEYwT6Cg7lLAVtDkqPX8zGjJiRwUKH808bUyqEw1y5Cc8U
U5hbmMgPWfXsKxsEC6FSVHBG9ZX2jymOMQXijLFcBSuWvADHmyU+ZxXcbtd1rv4E
cGFj3wECgYEA5cNrr5WjrpEin6MYYVWxiQ+xEWPU2R17eApagrDRLM41JJpv7a5m
TYuZRfIxb59CBPi718Gi168P3T2KMvo2/BTh9Lq5ZBYHx3aDqW2QvMFn7/tgamj8
0DBxccd2QWfGIBrT1rAF7lD8TC86wtDDVKrvhucRSEXVKF/jWFFRGfUCgYEAyPt6
48khr7sfNMVdkDLjQjZVV6H7ZUMoSn0FGybgKWxW+b0XCBPObUQWIpyCNTRr1+4A
1TAUS+F/OVVfwnLNgemeE2wd6CaduxwiK1U4pHbyXCElH1ifonHWV3MoXOefYsiY
q5z2jfJzUi0JZVUKsveu9rQsFLsc//1s/I5T1LECgYEAldY6fNg2VVp63OZsuNU8
oSiljbSwEyMh6Oe/nOkYkIKtr4AzrCoGt11piG7ohGW0lS9suMijnMqiquI+JP5+
KyinLoUy761aR17nf+9e62mpkZw6hUqQTGi7Irs0SHUXhMpaCfDi/Ua9MiW+yVuB
ds6+xBgeciZwWxMlXOwy2p0CgYEAm+YWiSK3Mq0fo7uEvBn9Fps2z+ciLoZNdppL
n6gkMX2MaeQ3PVi/wxoRYX+tsL+c973yf2vwEnw0R7Dlutt6dc9VgxNWj4GE0GMe
Tiao7Uom7Tf4p7wC9+r9rI/zOz2f8OxRIK18wtbShWfR5fx1dCWUXmGb3+jUse1O
4Qk2FcECgYAvSvGFoJb8tuHFEYYHBbjficmvTUsrTE+EhxPqWKFhKfF19fFFIupy
XBCrN6nwrh+/YMxZXeIRbbTTf814cOO7PjLeNhnfhJZkaJq1HzbYe3bOurna3qrm
Ra3xiM8Ld2PyGnZPXf8+AWhMhuPkLX1KFVTCAxwCpmTZCHtiGCmXMA==
-----END RSA PRIVATE KEY-----

View file

@ -1,71 +0,0 @@
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 4096 (0x1000)
Signature Algorithm: sha256WithRSAEncryption
Issuer: O=nginx-proxy test suite, CN=www.nginx-proxy.tld
Validity
Not Before: Feb 7 21:53:19 2023 GMT
Not After : Jun 25 21:53:19 2050 GMT
Subject: CN=https-and-http.nginx-proxy.test
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
RSA Public-Key: (2048 bit)
Modulus:
00:b7:97:85:d1:7f:6b:50:29:f3:87:b7:4e:f5:25:
40:6a:d8:fa:a1:63:3c:4a:2e:68:4a:c6:8b:38:df:
07:81:d0:08:9d:fc:17:f5:37:28:7f:31:e6:f3:81:
28:4e:22:b6:bd:a2:4e:f2:2f:e5:0f:dd:55:3c:e1:
04:84:4c:45:1b:1a:ae:b7:f0:2a:da:43:05:71:91:
92:b8:d1:49:fe:80:0a:53:b9:66:da:54:60:9a:fc:
e1:b2:e8:28:48:7f:96:94:3c:92:a3:b2:37:f6:7a:
c2:de:0b:12:f0:ae:4e:92:fe:2d:c1:b2:95:28:1f:
88:8d:79:99:81:19:ae:22:a4:95:f5:9f:db:25:8e:
1d:cf:43:cd:6f:85:93:5f:79:ee:f8:f3:d4:82:e1:
e9:4d:c9:ad:ae:5b:92:43:3a:3c:71:51:70:f7:3e:
bd:1b:24:52:6a:a3:cf:54:72:57:ed:fe:72:ea:96:
9b:5a:02:02:a7:df:85:b7:68:ae:1e:07:77:9f:59:
a5:a0:8b:28:c2:c8:b7:bb:8a:42:50:df:05:73:bf:
9c:55:13:b5:82:79:77:40:57:a4:8f:88:a5:71:50:
d7:70:b0:4d:0c:d9:86:b3:9b:db:8a:20:bd:19:68:
10:52:2d:53:ba:0e:2e:1c:ad:80:54:bb:b6:c9:ab:
11:39
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Subject Alternative Name:
DNS:https-and-http.nginx-proxy.test
Signature Algorithm: sha256WithRSAEncryption
2c:f3:e5:47:3f:8e:5a:28:b1:df:e5:95:50:85:6f:27:2f:a6:
8d:f1:5e:cf:df:e2:52:66:97:61:36:59:81:26:25:19:99:c9:
93:e5:85:cb:ca:69:af:4b:21:a3:d2:7a:bf:b5:5e:2d:42:fb:
99:f8:22:58:e5:bf:79:b8:8a:74:7e:c6:94:14:d9:f2:27:63:
b6:e5:74:21:5b:59:fb:f6:c8:a9:28:fb:60:f7:5e:bd:c2:e6:
74:24:14:96:61:95:6c:c2:66:b4:52:25:a1:85:5a:97:e5:68:
5c:62:cf:69:3b:b0:a9:56:d8:e3:5f:74:dc:84:18:d5:3e:4f:
c9:35:39:26:88:dc:9b:80:d9:40:e1:4f:09:27:8d:d2:89:55:
30:91:02:86:35:04:95:1e:1d:58:14:5b:c6:e0:2e:a7:bf:a8:
f6:2b:76:8a:4e:71:79:bc:c0:04:cd:db:81:73:46:ce:68:ed:
25:b0:0e:42:8d:96:64:77:3b:f4:9d:1a:c9:f6:78:4c:56:4f:
92:17:29:3d:80:50:71:77:4b:a8:29:c2:12:fc:ad:0a:37:81:
38:4c:fb:54:99:4d:12:5f:98:dc:d1:a9:7b:08:45:c4:6f:7e:
fe:00:e0:db:79:fe:d1:28:e3:8e:82:d1:fb:bc:0a:c4:42:93:
c9:5e:eb:ba
-----BEGIN CERTIFICATE-----
MIIDFTCCAf2gAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwPzEfMB0GA1UECgwWbmdp
bngtcHJveHkgdGVzdCBzdWl0ZTEcMBoGA1UEAwwTd3d3Lm5naW54LXByb3h5LnRs
ZDAgFw0yMzAyMDcyMTUzMTlaGA8yMDUwMDYyNTIxNTMxOVowKjEoMCYGA1UEAwwf
aHR0cHMtYW5kLWh0dHAubmdpbngtcHJveHkudGVzdDCCASIwDQYJKoZIhvcNAQEB
BQADggEPADCCAQoCggEBALeXhdF/a1Ap84e3TvUlQGrY+qFjPEouaErGizjfB4HQ
CJ38F/U3KH8x5vOBKE4itr2iTvIv5Q/dVTzhBIRMRRsarrfwKtpDBXGRkrjRSf6A
ClO5ZtpUYJr84bLoKEh/lpQ8kqOyN/Z6wt4LEvCuTpL+LcGylSgfiI15mYEZriKk
lfWf2yWOHc9DzW+Fk1957vjz1ILh6U3Jra5bkkM6PHFRcPc+vRskUmqjz1RyV+3+
cuqWm1oCAqffhbdorh4Hd59ZpaCLKMLIt7uKQlDfBXO/nFUTtYJ5d0BXpI+IpXFQ
13CwTQzZhrOb24ogvRloEFItU7oOLhytgFS7tsmrETkCAwEAAaMuMCwwKgYDVR0R
BCMwIYIfaHR0cHMtYW5kLWh0dHAubmdpbngtcHJveHkudGVzdDANBgkqhkiG9w0B
AQsFAAOCAQEALPPlRz+OWiix3+WVUIVvJy+mjfFez9/iUmaXYTZZgSYlGZnJk+WF
y8ppr0sho9J6v7VeLUL7mfgiWOW/ebiKdH7GlBTZ8idjtuV0IVtZ+/bIqSj7YPde
vcLmdCQUlmGVbMJmtFIloYVal+VoXGLPaTuwqVbY41903IQY1T5PyTU5Jojcm4DZ
QOFPCSeN0olVMJEChjUElR4dWBRbxuAup7+o9it2ik5xebzABM3bgXNGzmjtJbAO
Qo2WZHc79J0ayfZ4TFZPkhcpPYBQcXdLqCnCEvytCjeBOEz7VJlNEl+Y3NGpewhF
xG9+/gDg23n+0SjjjoLR+7wKxEKTyV7rug==
-----END CERTIFICATE-----

Some files were not shown because too many files have changed in this diff Show more