diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..8dc3880 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +__pycache__ +.pytest_cache +*.pyc +.* diff --git a/.github/dependabot.yml b/.github/dependabot.yml index fff4485..6eda8b7 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -6,6 +6,6 @@ updates: schedule: interval: "daily" - package-ecosystem: "pip" - directory: "/" + directory: "/src" schedule: interval: "daily" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a33b545..3efaf4c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,9 +26,14 @@ jobs: - name: Checkout repository uses: actions/checkout@v2 - - run: | + - name: Install requirements + run: | cd src/ pip install -r requirements.txt + + - name: Run tests + run: | + cd src/ pytest -s tests/ -vv Build: @@ -87,7 +92,7 @@ jobs: uses: docker/build-push-action@v2 with: context: . - file: Dockerfile + file: build/Dockerfile build-args: | RELEASE_VERSION_ARG="${{ join(steps.tags.outputs.result, ',') }}" platforms: linux/amd64,linux/arm64 @@ -102,6 +107,16 @@ jobs: # chmod +x $HOME/.docker/cli-plugins/docker-pushrm # docker pushrm ${{ env.IMAGE_NAME }} + Helm: + runs-on: 'ubuntu-latest' + needs: Build + if: github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/v') + env: + DOC_GITHUB_TOKEN: '${{ secrets.DOC_TOKEN }}' + steps: + - uses: actions/checkout@v2 + - run: curl https://opensource.byjg.com/add-helm.sh | bash /dev/stdin helm easyhaproxy + Documentation: runs-on: 'ubuntu-latest' needs: Build @@ -110,4 +125,4 @@ jobs: DOC_GITHUB_TOKEN: '${{ secrets.DOC_TOKEN }}' steps: - uses: actions/checkout@v2 - - run: curl https://opensource.byjg.com/add-doc.sh | bash /dev/stdin devops docker-easy-haproxy + - run: curl https://opensource.byjg.com/add-doc.sh | bash /dev/stdin devops docker-easy-haproxy docs diff --git a/.gitignore b/.gitignore index 835d304..1610fa1 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ venv .docker_data __pycache__ .pytest_cache -*.pyc \ No newline at end of file +*.pyc +.env diff --git a/.gitpod.yml b/.gitpod.yml index 2b37d88..b763650 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -6,4 +6,4 @@ tasks: - command: | virtualenv -p /usr/bin/python3 venv source venv/bin/activate - pip install -r requirements.txt + pip install -r src/requirements.txt diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index ed1f6e6..0000000 --- a/.travis.yml +++ /dev/null @@ -1,26 +0,0 @@ -language: python - -services: - - docker - - -jobs: - include: - - stage: test - if: (type IN (pull_request)) - install: - - pip install -r requirements.txt - script: - - pytest -s tests/ - - - stage: build docker - if: (branch = master) AND (NOT (type IN (pull_request))) - install: - - docker pull byjg/k8s-ci - script: - - docker run --privileged -v /tmp/z:/var/lib/containers -it --rm -v $PWD:/work -w /work -e DOCKER_USERNAME=$DOCKER_USERNAME -e DOCKER_PASSWORD=$DOCKER_PASSWORD -e DOCKER_REGISTRY=$DOCKER_REGISTRY byjg/k8s-ci /work/build-multiarch.sh - - - stage: documentation - if: (branch = master) AND (NOT (type IN (pull_request))) - install: skip - script: "curl https://opensource.byjg.com/add-doc.sh | bash /dev/stdin devops docker-easy-haproxy" diff --git a/.vscode/launch.json b/.vscode/launch.json index 428608e..9cb10c6 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,9 +10,9 @@ "request": "launch", "program": "${file}", "console": "integratedTerminal", - "cwd": "${workspaceFolder}", + "cwd": "${workspaceFolder}/src", "env": { - "PYTHONPATH": "${cwd}" + "PYTHONPATH": "${cwd}/src" } } ] diff --git a/Makefile b/Makefile index 4c3b4e4..0b7e07f 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ VERSION := $(shell git rev-parse --short HEAD) .PHONY: build build: - docker build -t byjg/easy-haproxy --build-arg RELEASE_VERSION_ARG="$(VERSION)" -t byjg/easy-haproxy:local . + docker build -t byjg/easy-haproxy --build-arg RELEASE_VERSION_ARG="$(VERSION)" -t byjg/easy-haproxy:local -f build/Dockerfile . .PHONY: test test: diff --git a/README.md b/README.md index 7cbb7c3..23cdd36 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Easy HAProxy +# EasyHAProxy [![Opensource ByJG](https://img.shields.io/badge/opensource-byjg-success.svg)](http://opensource.byjg.com) [![Build Status](https://github.com/byjg/docker-easy-haproxy/actions/workflows/build.yml/badge.svg?branch=master)](https://github.com/byjg/docker-easy-haproxy/actions/workflows/build.yml) @@ -6,378 +6,48 @@ [![GitHub license](https://img.shields.io/github/license/byjg/docker-easy-haproxy.svg)](https://opensource.byjg.com/opensource/licensing.html) [![GitHub release](https://img.shields.io/github/release/byjg/docker-easy-haproxy.svg)](https://github.com/byjg/docker-easy-haproxy/releases/) -Service discovery for HAProxy. +![EasyHAProxy](easyhaproxy_logo.png) -This Docker image will dynamically create the `haproxy.cfg` based on the labels defined in docker containers or from -a simple Yaml. +## Service discovery for HAProxy + +The main objective of EasyHAProxy is dynamically create the `haproxy.cfg` based on the labels defined in docker containers. + +EasyHAProxy can detect and configure automatically HAProxy on the folowing platforms: + +- Docker +- Docker Swarm +- Kubernetes ## Features -EasyHAProxy will discover the services based on the Docker Tags of the running containers in a Docker host or Docker Swarm cluster and dynamically set up the `haproxy.cfg`. Below, EasyHAProxy main features:: +EasyHAProxy will discover the services based on the Docker Tags of the running containers in a Docker host or Docker Swarm cluster and dynamically set up the `haproxy.cfg`. Below, EasyHAProxy main features: + - Use Letsencrypt with HAProxy. +- Set your custom SSL certificates - Balance traffic between multiple replicas - Set SSL with three different levels of validations and according to the most recent definitions. -- Include your SSL certificate. - Setup HAProxy to listen to TCP. - Add redirects. - Enable/disable Stats on port 1936 with a custom password. - Enable/disable custom errors. -Also, it is possible to set up HAProxy from a simple Yaml file instead of creating `haproxy.cfg` file. +Also, it is possible to set up HAProxy from a simple Yaml file instead of creating `haproxy.cfg` file. -## Basic Usage +## How It Works? -The Easy HAProxy will automatically create the `haproxy.cfg` file based on the containers or a YAML provided. +You don't need to change your current infrastructure and don't need to learn the HAProxy configuration. -The basic command line to run is: +You need run the EasyHAProxy container, add some labels to your existing container and EasyHAProxy will +automatically detect them and setup HAProxy for you. -```bash -docker run -d \ - --name easy-haproxy-container \ - -v /var/run/docker.sock:/var/run/docker.sock \ - -e EASYHAPROXY_DISCOVER="swarm|docker|static" \ - # + Environment Variables \ - # + ports mapped to the host \ - byjg/easy-haproxy -``` +## Detailed Instructions -The mapping to `/var/run/docker.sock` is necessary to discover the docker containers and get the labels; +For detailed instructions on how to use EasyHAProxy follow the instructions for the platform you want to use: -The environment variables will setup the HAProxy. +| Kubernetes | Docker Swarm | Docker | Static +|:----------:|:------------:|:------:|:-------: +| [![Kubernetes](easyhaproxy_kubernetes.png)](docs/kubernetes.md) | [![Docker Swarm](easyhaproxy_swarm.png)](docs/swarm.md) | [![Docker](easyhaproxy_docker.png)](docs/docker.md) | [![Static](easyhaproxy_static.png)](docs/static.md) -| Environment Variable | Description | -|-------------------------------|---------------------------------------------------------------------------------------------------------------| -| EASYHAPROXY_DISCOVER | How `haproxy.cfg` will be created: `static`, `docker` or `swarm` | -| EASYHAPROXY_LABEL_PREFIX | (Optional) The key will search for matching resources. Default: `easyhaproxy`. | -| EASYHAPROXY_LETSENCRYPT_EMAIL | (Optional) The email will be used to request the certificate to Letsencrypt | -| EASYHAPROXY_SSL_MODE | (Optional) `STRICT` supports only the most recent TLS version; `DEFAULT` good SSL integration with recent browsers; `LOOSE` supports all old SSL protocols for old browsers (not recommended). | -| EASYHAPROXY_REFRESH_CONF | (Optional) Check configuration every N seconds. Default: 10 | -| HAPROXY_USERNAME | (Optional) The HAProxy username to the statistics. Default: `admin` | -| HAPROXY_PASSWORD | (Optional) The HAProxy password to the statistics. If not set, statistics will be available with no password | -| HAPROXY_STATS_PORT | (Optional) The HAProxy port to the statistics. Default: `1936`. If set to `false`, disable statistics | -| HAPROXY_CUSTOMERRORS | (Optional) If HAProxy will use custom HTML errors. true/false. Default: `false` | - -The environment variable `EASYHAPROXY_DISCOVER` will define where is located your containers (see below for more details): - -- docker -- swarm -- static - -## Automatic Discover Services - -Easy HAProxy can automatically discover the container services running in the same network of Docker or in a Docker Swarm cluster. - -### EASYHAPROXY_DISCOVER: docker - -This method will use a standard docker installation to discover the containers and configure the HAProxy. - -The only requirement is that containers and easy-haproxy must be in the same docker network. - -The discovery will occur every minute. - -e.g.: - -```bash -docker create networkd easyhaproxy - -docker run --network easyhaproxy byjg/easyhaproxy - -docker run --network easyhaproxy myimage -``` - -or, if the container is already created you can join it using the command: - -``` -docker network connect easyhaproxy mycontainer -``` - -### EASYHAPROXY_DISCOVER: swarm - -This method requires a functional Docker Swarm Cluster. The system will search for the labels in all containers on all -swarm nodes. - -The discovery will occur every minute. - -Important: easyhaproxy needs to be in the same network of the containers or otherwise will not access. - -### Docker Container (Swarm or Docker) tags: - -| Tag | Description | Example | -|---------------------------------------|---------------------------------------------------------------------------------------------------------|--------------| -| easyhaproxy.[definition].host | Host(s) HAProxy is listening. More than one host use comma as delimiter | somehost.com OR host1.com,host2.com | -| easyhaproxy.[definition].mode | (Optional) Is this `http` or `tcp` mode in HAProxy. (Defaults to http) | http | -| easyhaproxy.[definition].port | (Optional) Port HAProxy will listen for the host. (Defaults to 80) | 80 | -| easyhaproxy.[definition].localport | (Optional) Port container is listening. (Defaults to 80) | 8080 | -| easyhaproxy.[definition].redirect | (Optional) JSON containing key/value pair from host/to URL redirect. | {"foo.com":"https://bla.com", "bar.com":"https://bar.org"} | -| easyhaproxy.[definition].sslcert | (Optional) Cert PEM Base64 encoded. Do not use this if letsencrypt is enabled. | | -| easyhaproxy.[definition].ssl | (Optional) If `true` you need to provide certificate as a file. See below. Do not use with `sslcert`. | true | -| easyhaproxy.[definition].health-check | (Optional) `ssl`, enable health check via SSL in `mode tcp` (Defaults to "empty") | ssl | -| easyhaproxy.[definition].letsencrypt | (Optional) Generate certificate with letsencrypt. Do not use with `sslcert`. | true OR yes OR false OR no | -| easyhaproxy.[definition].redirect_ssl | (Optional) Redirect all requests to https | true OR yes OR false OR no | - -### Defining the labels in Docker Swarm - -if you are deploying a stack in a Docker Swarm cluster, set labels at the `deploy` level: - -```yaml -services: - foo: - deploy: - labels: - easyhaproxy.my.host: "www.example.org" - easyhaproxy.my.localport: 8080 - ... -``` - -### Single Definition - -```bash -docker run \ - -l easyhaproxy.webapi.port=80\ - -l easyhaproxy.webapi.host=byjg.com.br \ - .... -``` - -### Multiples Definitions on the same container - -```bash -docker run \ - -l easyhaproxy.express.port=80 \ - -l easyhaproxy.express.localport=3000 \ - -l easyhaproxy.express.host=express.byjg.com.br \ - - -l easyhaproxy.admin.port=80 \ - -l easyhaproxy.admin.localport=3001 \ - -l easyhaproxy.admin.host=admin.byjg.com.br \ - .... \ - some/myimage -``` - -### Multiples hosts on the same container - -```bash -docker run \ - -l easyhaproxy.express.port=80 \ - -l easyhaproxy.express.localport=3000 \ - -l easyhaproxy.express.host=express.byjg.com.br,admin.byjg.com.br \ - .... \ - some/myimage -``` - -If you are using docker-compose you can use this way: - -```yaml -version: "3" - -services: - mycontainer: - image: some/myimage - labels: - easyhaproxy.express.port: 80 - easyhaproxy.express.localport: 3000 - easyhaproxy.express.host: >- - express.byjg.com.br, - admin.byjg.com.br -``` - -### TLS passthrough - -Used to pass on SSL termination to a backend. Alternatively, you can enable health-check via SSL on the backend with the optional `health-check` label: - -```bash -docker run \ - -l easyhaproxy.example.mode=tcp \ - -l easyhaproxy.example.health-check=ssl \ - -l easyhaproxy.example.port=443 - .... \ - some/tcp-service -``` - -### Redirect Example - -```bash -docker run \ - -l easyhaproxy.[definition].redirect='{"www.byjg.com.br":"http://byjg.com.br","byjg.com":"http://byjg.com.br"}' -``` - -## EASYHAPROXY_DISCOVER: static - -This method expects a YAML file to setup the `haproxy.cfg` - -Create a YAML file and map to `/etc/haproxy/easyconfig.yml` - -```yaml -stats: - username: admin - password: password - port: 1936 # Optional (default 1936) - -customerrors: true # Optional (default false) - -ssl_mode: default - -letsencrypt: { - "email": "acme@example.org" -} - -easymapping: - - port: 80 - hosts: - host1.com.br: - containers: - - container:5000 - letsencrypt: true - redirect_ssl: true - host2.com.br: - containers: - - other:3000 - redirect: - www.host1.com.br: http://host1.com.br - - - port: 443 - hosts: - host1.com.br: - containers: - - container:80 - redirect_ssl: false - ssl: true - - - port: 8080 - hosts: - host3.com.br: - containers: - - domain:8181 -``` - -Running: - -```bash -docker run -v /my/config.yml:/etc/haproxy/easyconfig.yml .... byjg/easyhaproxy -``` - -## Letsencrypt - -This HAProxy can issue a letsencrypt certificate. The command is as below: - -Run the EasyHAProxy: - -```bash -docker run \ - -e EASYHAPROXY_LETSENCRYPT_EMAIL=john@doe.com - .... \ - byjg/easy-haproxy -``` - -Run your container: -```bash -docker run \ - -l easyhaproxy.express.port=80 \ - -l easyhaproxy.express.localport=3000 \ - -l easyhaproxy.express.host=example.org \ - -l easyhaproxy.express.letsencrypt=true \ - .... \ - some/myimage -``` - -Caveats: - -- Your container **must** listen to the port 80. Besides no error, the certificate won't be issued if in a different port. -- The port 2080 is reserved for the certbot and should not be exposed. -- You cannot set the port 443 for the container with the Letsencrypt because EasyHAProxy will handle this automatically once the certificate is issued. -- If you don't run the EasyHAProxy with the parameter `EASYHAPROXY_LETSENCRYPT_EMAIL` no certificate will be issued. -- Be aware of Letsencrypt issue limits - https://letsencrypt.org/docs/duplicate-certificate-limit/ and https://letsencrypt.org/docs/rate-limits/ - -## Exposing Ports - -You must expose some ports on the EasyHAProxy container and in the firewall. However, you don't need to expose the other container ports because EasyHAProxy will handle that. - -- The ports `80` and `443`. -- If you enable the HAProxy statistics, you must also expose the port defined in `HAPROXY_STATS_PORT` environment variable (default 1936). Be aware that statististics are enabled by default with no password. -- Every port defined in `easyhaproxy.[definitions].port` also should be exposed. - -e.g. - -```bash -docker run \ - /* other parameters */ - -p 80:80 \ - -p 443:443 \ - -p 1936:1936 \ - -d byjg/easy-haproxy -``` - - -## Mapping custom .cfg files - -You can concatenate valid HAProxy `.cfg` files to the dynamically generated `haproxy.cfg` by mapping the folder `/etc/haproxy/conf.d`. - -```bash -docker run \ - /* other parameters */ - -v /your/local/conf.d:/etc/haproxy/conf.d \ - -d byjg/easy-haproxy -``` - -## Mapping SSL certificates volumes - -EasyHAProxy stores the certificates inside the folder `/certs/haproxy` and `/certs/letsencrypt`. -- If you want to preserve the letsencrypt certificates between reloads, map the folder `/certs/letsencrypt` to your volume. -- If you want to provide your certificates as a file instead of a Base64 parameter, map the folder `/certs/haproxy` to your volume, and instead of use `easyhaproxy.[definition].sslcert`, use `easyhaproxy.[definition].ssl: true` - -```bash -docker run \ - /* other parameters */ - -v /your/certs/letsencrypt:/certs/letsencrypt \ - -d byjg/easy-haproxy -``` - -## Handling SSL - -You can attach a valid SSL certificate to the request. - -1. First, Create a single PEM file including CA. - -```bash -cat example.com.crt example.com.key > single.pem - -cat single.pem - ------BEGIN CERTIFICATE----- -MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC5ZheHqmBnEJP+ -U9r1gxYWKLzdqrMrcxtQN6M1hIH9n0peuJeIrybdcV7sMbStMXI= ------END CERTIFICATE----- - ------BEGIN PRIVATE KEY----- -MIIEojCCA4qgAwIBAgIUegW2BimwuL4RzRZ2WYkHA6U5nkAwDQYJKoZIhvcNAQEL -3j4wz8/I5fdsk090j4s5KA== ------END PRIVATE KEY----- -``` - -2. Convert it to BASE64 in a single line: - -```bash -cat single.pem | base64 -w0 -``` - -3. Use this string to define the label `easyhaproxy.[definition].sslcert` - -## Setting Custom Errors - -If enabled, map the volume : `/etc/haproxy/errors-custom/` to your container and put a file named `ERROR_NUMBER.http` -where ERROR_NUMBER is the HTTP error code (e.g., `503.http`) - -## Build - - -```bash -docker build -t byjg/easy-haproxy . -``` - -## Limitations - -EasyHAProxy has some limitations when there is more than one easy-haproxy container running: -- Replicas can be out-of-sync for a few seconds because each replica will discover the pods separately. -- Each replica will request a Letsencrypt certificate and can fail because the letsencrypt challenge can be directed to the other replica. ---- [Open source ByJG](http://opensource.byjg.com) diff --git a/_config.yml b/_config.yml deleted file mode 100644 index 3935400..0000000 --- a/_config.yml +++ /dev/null @@ -1,66 +0,0 @@ -name: docker-easy-haproxy - -project: - version: 1.0.0 - download_url: https://github.com/byjg/docker-easy-haproxy/releases - -license: - software: MIT - software_url: https://opensource.org/licenses/MIT - - docs: MIT - docs_url: https://opensource.org/licenses/MIT - -git_edit_address: https://github.com/byjg/docker-easy-haproxy/blob/master/ - -links: - header: - - title: GitHub - url: https://github.com/byjg/docker-easy-haproxy - - title: ByJG - url: https://opensource.byjg.com/ - footer: - - title: GitHub - url: https://github.com/byjg/docker-easy-haproxy - - title: Issues - url: https://github.com/byjg/docker-easy-haproxy/issues - -ui: - header: - color1: "#080331" - color2: "#0033cc" - trianglify: true - -social: - github: - user: byjg - repo: docker-easy-haproxy - twitter: - enabled: false - via: - hash: opensourcebyjg - account: - facebook: - enabled: true - url: https://opensource.byjg.com/ - profileUrl: - -author: - twitter: byjg - -twitter: - card: summary - username: byjg - -logo: https://opensource.byjg.com/images/logo_byjg.png - -analytics: - google: UA-130014324-1 - -plugins: - - jekyll-seo-tag - -# Build settings -markdown: kramdown -remote_theme: byjg/jekyll-docs-theme - diff --git a/assets/scripts/banner.txt b/assets/scripts/banner.txt deleted file mode 100644 index f897aab..0000000 --- a/assets/scripts/banner.txt +++ /dev/null @@ -1,5 +0,0 @@ - _ - ___ __ _ ____ _ ___| |_ __ _ _ __ _ _ _____ ___ _ -/ -_) _` (_-< || |___| ' \/ _` | '_ \ '_/ _ \ \ / || | -\___\__,_/__/\_, | |_||_\__,_| .__/_| \___/_\_\\_, | - |__/ |_| |__/ diff --git a/assets/scripts/certbot.sh b/assets/scripts/certbot.sh deleted file mode 100755 index 8678231..0000000 --- a/assets/scripts/certbot.sh +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env bash - -source /scripts/functions.sh - -if [ ! -f /scripts/letsencrypt_hosts.txt ]; then - exit 0 -fi - -# Semaphore -if [ -f /tmp/certbot-lock ]; then - log "notice" "CERTBOT_JOB" "Another process is running" - exit 0 -fi - -touch /tmp/certbot-lock - -mkdir -p /var/log/letsencrypt -ln -sf /dev/stdout /var/log/letsencrypt/letsencrypt.log - -REQUEST_CERTS="" -RENEW_CERTS="" - -for domain in $(cat /scripts/letsencrypt_hosts.txt); do - if [ ! -f "/certs/letsencrypt/$domain.pem" ]; then - REQUEST_CERTS="$REQUES_CERTS -d $domain" - continue - fi - - if [[ $(find "/certs/letsencrypt/$domain.pem" -mtime +30 -print) ]]; then - RENEW_CERTS="$RENEW_CERTS -d $domain" - fi -done - -if [ -n "$REQUEST_CERTS" ]; then - log "info" "CERTBOT_JOB" "Requesting certificates for $REQUEST_CERTS" - certbot certonly \ - --standalone \ - --preferred-challenges http \ - --http-01-port 2080 \ - --agree-tos \ - --issuance-timeout 90 \ - --no-eff-email \ - --non-interactive \ - --max-log-backups=0 \ - --post-hook "/scripts/certbot_to_haproxy.sh" \ - $REQUEST_CERTS --email $EASYHAPROXY_LETSENCRYPT_EMAIL -fi - -if [ -n "$RENEW_CERTS" ]; then - log "info" "CERTBOT_JOB" "Resquesting renew certificated fort $RENEW_CERTS" - certbot renew --post-hook "/scripts/certbot_to_haproxy.sh" -fi - -# Release semaphore -rm /tmp/certbot-lock diff --git a/assets/scripts/certbot_to_haproxy.sh b/assets/scripts/certbot_to_haproxy.sh deleted file mode 100755 index 532a265..0000000 --- a/assets/scripts/certbot_to_haproxy.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash - -source /scripts/functions.sh - -# Loop through all Let's Encrypt certificates -for CERTIFICATE in `find /etc/letsencrypt/live/* -type d`; do - CERTIFICATE=`basename $CERTIFICATE` - - # Combine certificate and private key to single file - cat /etc/letsencrypt/live/$CERTIFICATE/fullchain.pem /etc/letsencrypt/live/$CERTIFICATE/privkey.pem > /certs/letsencrypt/$CERTIFICATE.pem -done - -# It will be checked on haproxy-reload.sh -touch /tmp/force-reload \ No newline at end of file diff --git a/assets/scripts/functions.sh b/assets/scripts/functions.sh deleted file mode 100644 index e90e730..0000000 --- a/assets/scripts/functions.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -function log() { - # ARGS: - # - loglevel - # - app - # - message - echo [$2] $(date +"$EASYHAPROXY_DATEFORMAT") [$1]: $3 -} diff --git a/assets/scripts/haproxy-reload.sh b/assets/scripts/haproxy-reload.sh deleted file mode 100755 index 20fbad8..0000000 --- a/assets/scripts/haproxy-reload.sh +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env bash - -source /scripts/functions.sh - -cd /scripts - -RELOAD="true" - -if [[ "static|docker|swarm" != *"$EASYHAPROXY_DISCOVER"* ]];then - log "error" "CONF_CHECK" "EASYHAPROXY_DISCOVER should be 'static', 'docker', or 'swarm'. I got '$EASYHAPROXY_DISCOVER' instead." - exit 1 -fi - -if [[ "$EASYHAPROXY_DISCOVER" == "static" ]]; then - CONTROL_FILE="/etc/haproxy/haproxy.cfg" - touch ${CONTROL_FILE} - cp ${CONTROL_FILE} ${CONTROL_FILE}.old - python3 static.py /etc/haproxy/easyconfig.yml > ${CONTROL_FILE} -else - CONTROL_FILE="/tmp/.docker_data" - touch ${CONTROL_FILE} - mv ${CONTROL_FILE} ${CONTROL_FILE}.old - touch ${CONTROL_FILE} - - if [[ "$EASYHAPROXY_DISCOVER" == "docker" ]]; then - CONTAINERS=$(docker ps -q | sort | uniq) - LABEL_PATH=".Config.Labels" - - for container in ${CONTAINERS}; do - docker inspect --format "{{ json $LABEL_PATH }}" ${container} | xargs -I % echo ${container}=% >> ${CONTROL_FILE} - done - else - CONTAINERS=$(docker node ps $(docker node ls -q) --format "{{ .Name }}" --filter desired-state=running | cut -d. -f1 | sort | uniq) - LABEL_PATH=".Spec.Labels" - - for container in ${CONTAINERS}; do - docker service inspect --format "{{ json $LABEL_PATH }}" ${container} | xargs -I % echo ${container}=% >> ${CONTROL_FILE} - done - fi - - if cmp -s ${CONTROL_FILE} ${CONTROL_FILE}.old ; then - RELOAD="false" - else - python3 swarm.py > /etc/haproxy/haproxy.cfg - log "info" "CONF_CHECK" "New configuration found" - fi -fi - -if cmp -s ${CONTROL_FILE} ${CONTROL_FILE}.old ; then - RELOAD="false" -fi - -if [[ ! -z "$1" ]]; then - log "info" "CONF_CHECK" "Initial configuration. Skip certbot." -else - /scripts/certbot.sh -fi - -# If Certbot reloads successfully will create the file /tmp/force-reload -if [ -f /tmp/force-reload ]; then - log "info" "CONF_CHECK" "New certificates found..." - RELOAD="true" - rm /tmp/force-reload -fi - -if [[ ! -z "$1" ]]; then - log "info" "CONF_CHECK" "Starting haproxy..." - /usr/sbin/haproxy -W -f /etc/haproxy/haproxy.cfg $(ls /etc/haproxy/conf.d/*.cfg 2>/dev/null | xargs -I{} echo -f {}) -p /run/haproxy.pid -S /var/run/haproxy.sock & - -elif [[ "$RELOAD" == "true" ]]; then - log "info" "CONF_CHECK" "Reloading..." - /usr/sbin/haproxy -W -f /etc/haproxy/haproxy.cfg $(ls /etc/haproxy/conf.d/*.cfg 2>/dev/null | xargs -I{} echo -f {}) -p /run/haproxy.pid -x /var/run/haproxy.sock -sf $(cat /run/haproxy.pid) & -fi diff --git a/assets/scripts/haproxy.sh b/assets/scripts/haproxy.sh deleted file mode 100755 index 92c15e5..0000000 --- a/assets/scripts/haproxy.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env bash - -source /scripts/functions.sh - -/usr/sbin/haproxy -v - -if [ -z "$EASYHAPROXY_DATEFORMAT" ]; then - export EASYHAPROXY_DATEFORMAT="%Y-%m-%d %H:%M:%S %Z" -fi - -if [ -z "$EASYHAPROXY_REFRESH_CONF" ]; then - export EASYHAPROXY_REFRESH_CONF=10 -fi - - -cat banner.txt -echo Release: $RELEASE_VERSION -echo -echo "Environment" -env | sort | grep 'HAPROXY' | xargs -I{} echo " - {} " -echo - -/scripts/haproxy-reload.sh initial - -while true; do - sleep $EASYHAPROXY_REFRESH_CONF - log "info" "CONF_CHECK" "Heartbeat." - /scripts/haproxy-reload.sh -done diff --git a/assets/scripts/static.py b/assets/scripts/static.py deleted file mode 100644 index 2e36cc6..0000000 --- a/assets/scripts/static.py +++ /dev/null @@ -1,22 +0,0 @@ -import yaml -import sys -import os - -from easymapping import HaproxyConfigGenerator - -if len(sys.argv) != 2: - print("You need to pass the easyconfig.yml path") - exit(1) - - -with open(sys.argv[1], 'r') as content_file: - parsed = yaml.load(content_file.read(), Loader=yaml.FullLoader) - -cfg = HaproxyConfigGenerator(parsed) -print(cfg.generate()) - -path = os.path.dirname(os.path.realpath(__file__)) -with open(path + "/letsencrypt_hosts.txt", 'w') as fp: - fp.write('\n'.join(cfg.letsencrypt_hosts)) - -exit(0) diff --git a/assets/scripts/swarm.py b/assets/scripts/swarm.py deleted file mode 100644 index 52c1a73..0000000 --- a/assets/scripts/swarm.py +++ /dev/null @@ -1,34 +0,0 @@ -import os -from easymapping import HaproxyConfigGenerator - -with open("/tmp/.docker_data", 'r') as content_file: - line_list = content_file.readlines() - -result = { - "customerrors": True if os.getenv("HAPROXY_CUSTOMERRORS") == "true" else False, - "ssl_mode": os.getenv("EASYHAPROXY_SSL_MODE", "default") -} - -if os.getenv("HAPROXY_PASSWORD"): - result["stats"] = { - "username": os.getenv("HAPROXY_USERNAME") if os.getenv("HAPROXY_USERNAME") else "admin", - "password": os.getenv("HAPROXY_PASSWORD"), - "port": os.getenv("HAPROXY_STATS_PORT") if os.getenv("HAPROXY_STATS_PORT") else "1936", - } - -result["lookup_label"] = os.getenv("EASYHAPROXY_LABEL_PREFIX") if os.getenv("EASYHAPROXY_LABEL_PREFIX") else "easyhaproxy" -if (os.getenv("EASYHAPROXY_LETSENCRYPT_EMAIL")): - result["letsencrypt"] = { - "email": os.getenv("EASYHAPROXY_LETSENCRYPT_EMAIL") - } - - -cfg = HaproxyConfigGenerator(result) -print(cfg.generate(line_list)) - -path = os.path.dirname(os.path.realpath(__file__)) -with open(path + "/letsencrypt_hosts.txt", 'w') as fp: - fp.write('\n'.join(cfg.letsencrypt_hosts)) -# print(jsonStr) - - diff --git a/build-multiarch.sh b/build-multiarch.sh index 6a5ae0e..716412b 100755 --- a/build-multiarch.sh +++ b/build-multiarch.sh @@ -5,9 +5,9 @@ set -e # Start k8s-ci before run this command # docker run --privileged -v /tmp/z:/var/lib/containers -it --rm -v $PWD:/work -w /work byjg/k8s-ci -if [ -z "$DOCKER_USERNAME" ] || [ -z "$DOCKER_PASSWORD" ] || [ -z "$DOCKER_REGISTRY" ] +if [ -z "$DOCKER_USERNAME" ] || [ -z "$DOCKER_PASSWORD" ] || [ -z "$DOCKER_REGISTRY" ] || [ -z "$VERSIONS" ] then - echo You need to setup \$DOCKER_USERNAME, \$DOCKER_PASSWORD and \$DOCKER_REGISTRY before run this command. + echo You need to setup \$DOCKER_USERNAME, \$DOCKER_PASSWORD, \$DOCKER_REGISTRY and \$VERSIONS before run this command. exit 1 fi @@ -15,19 +15,17 @@ buildah login --username $DOCKER_USERNAME --password $DOCKER_PASSWORD $DOCKER_RE podman run --rm --events-backend=file --cgroup-manager=cgroupfs --privileged docker://multiarch/qemu-user-static --reset -p yes -VERSIONS="latest $TRAVIS_TAG" for VERSION in $VERSIONS do - DOCKERFILE=Dockerfile + DOCKERFILE=build/Dockerfile buildah manifest create byjg/easy-haproxy:$VERSION - buildah bud --arch arm64 --os linux --iidfile /tmp/iid-arm64 -f $DOCKERFILE -t byjg/easy-haproxy:$VERSION-arm64 . - buildah bud --arch amd64 --os linux --iidfile /tmp/iid-amd64 -f $DOCKERFILE -t byjg/easy-haproxy:$VERSION-amd64 . + buildah bud --arch arm64 --os linux --iidfile /tmp/iid-arm64 -f $DOCKERFILE --build-arg=RELEASE_VERSION_ARG="$VERSION-manual" -t byjg/easy-haproxy:$VERSION-arm64 . + buildah bud --arch amd64 --os linux --iidfile /tmp/iid-amd64 -f $DOCKERFILE --build-arg=RELEASE_VERSION_ARG="$VERSION-manual" -t byjg/easy-haproxy:$VERSION-amd64 . buildah manifest add byjg/easy-haproxy:$VERSION --arch arm64 --os linux --variant v8 $(cat /tmp/iid-arm64) buildah manifest add byjg/easy-haproxy:$VERSION --arch amd64 --os linux --os=linux $(cat /tmp/iid-amd64) buildah manifest push --all --format v2s2 byjg/easy-haproxy:$VERSION docker://byjg/easy-haproxy:$VERSION done - diff --git a/Dockerfile b/build/Dockerfile similarity index 52% rename from Dockerfile rename to build/Dockerfile index 9cb2339..20c3459 100644 --- a/Dockerfile +++ b/build/Dockerfile @@ -3,21 +3,19 @@ FROM alpine:3.16 ARG RELEASE_VERSION_ARG ENV RELEASE_VERSION=$RELEASE_VERSION_ARG +ENV TZ="Etc/UTC" WORKDIR /scripts -COPY requirements.txt /scripts -COPY templates /scripts/templates/ -COPY easymapping /scripts/easymapping/ -COPY tests/ /scripts/tests/ -COPY assets / +COPY src/ /scripts/ +COPY build/assets / -RUN apk add --no-cache haproxy bash python3 py3-pip py-yaml docker certbot openssl \ +RUN apk add --no-cache haproxy bash python3 py3-pip py-yaml certbot openssl \ && ln -s /usr/bin/python3 /usr/bin/python \ && pip3 install --upgrade pip \ && pip install -r requirements.txt \ - && pytest -s tests/ \ + && pytest -s -vv tests/ \ && openssl dhparam -out /etc/haproxy/dhparam 2048 \ && openssl dhparam -out /etc/haproxy/dhparam-1024 1024 -CMD ["/bin/bash", "-c", "/scripts/haproxy.sh" ] +CMD ["/usr/bin/python", "-u", "/scripts/main.py" ] diff --git a/assets/certs/haproxy/.place_holder_cert.pem b/build/assets/certs/haproxy/.place_holder_cert.pem similarity index 100% rename from assets/certs/haproxy/.place_holder_cert.pem rename to build/assets/certs/haproxy/.place_holder_cert.pem diff --git a/assets/etc/haproxy/conf.d/README.md b/build/assets/etc/haproxy/conf.d/README.md similarity index 100% rename from assets/etc/haproxy/conf.d/README.md rename to build/assets/etc/haproxy/conf.d/README.md diff --git a/assets/etc/haproxy/errors-custom/400.http b/build/assets/etc/haproxy/errors-custom/400.http similarity index 100% rename from assets/etc/haproxy/errors-custom/400.http rename to build/assets/etc/haproxy/errors-custom/400.http diff --git a/assets/etc/haproxy/errors-custom/403.http b/build/assets/etc/haproxy/errors-custom/403.http similarity index 100% rename from assets/etc/haproxy/errors-custom/403.http rename to build/assets/etc/haproxy/errors-custom/403.http diff --git a/assets/etc/haproxy/errors-custom/408.http b/build/assets/etc/haproxy/errors-custom/408.http similarity index 100% rename from assets/etc/haproxy/errors-custom/408.http rename to build/assets/etc/haproxy/errors-custom/408.http diff --git a/assets/etc/haproxy/errors-custom/500.http b/build/assets/etc/haproxy/errors-custom/500.http similarity index 100% rename from assets/etc/haproxy/errors-custom/500.http rename to build/assets/etc/haproxy/errors-custom/500.http diff --git a/assets/etc/haproxy/errors-custom/502.http b/build/assets/etc/haproxy/errors-custom/502.http similarity index 100% rename from assets/etc/haproxy/errors-custom/502.http rename to build/assets/etc/haproxy/errors-custom/502.http diff --git a/assets/etc/haproxy/errors-custom/503.http b/build/assets/etc/haproxy/errors-custom/503.http similarity index 100% rename from assets/etc/haproxy/errors-custom/503.http rename to build/assets/etc/haproxy/errors-custom/503.http diff --git a/assets/etc/haproxy/errors-custom/504.http b/build/assets/etc/haproxy/errors-custom/504.http similarity index 100% rename from assets/etc/haproxy/errors-custom/504.http rename to build/assets/etc/haproxy/errors-custom/504.http diff --git a/docker-compose.yml b/deploy/docker/docker-compose.yml similarity index 83% rename from docker-compose.yml rename to deploy/docker/docker-compose.yml index 2af38f1..ad01297 100644 --- a/docker-compose.yml +++ b/deploy/docker/docker-compose.yml @@ -2,7 +2,7 @@ version: "3" services: easyhaproxy: - image: byjg/easy-haproxy:4.1.0 + image: byjg/easy-haproxy:master volumes: - /var/run/docker.sock:/var/run/docker.sock - certs_letsencrypt:/certs/letsencrypt @@ -23,10 +23,15 @@ services: - "443:443/tcp" - "1936:1936/tcp" + networks: + - easyhaproxy + volumes: certs_letsencrypt: + external: true certs_haproxy: + external: true networks: easyhaproxy: - driver: bridge + external: true diff --git a/deploy/docker/install.sh b/deploy/docker/install.sh new file mode 100755 index 0000000..d5ab2ff --- /dev/null +++ b/deploy/docker/install.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +ASSETS_DIR="$(dirname "${BASH_SOURCE[0]}")"/../../build/assets/certs/haproxy + +docker network create easyhaproxy +docker volume create certs_letsencrypt +docker volume create certs_haproxy + +docker run -d --rm --name easyhaproxy_install -v certs_haproxy:/certs alpine tail -f /dev/null +docker cp $ASSETS_DIR/.place_holder_cert.pem easyhaproxy_install:/certs/.place_holder_cert.pem +docker stop easyhaproxy_install + +echo +echo +echo make sure to add to all of your containers: +echo +echo docker-compose +echo ============== +echo "networks:" +echo " default:" +echo " name: easyhaproxy" +echo " external: true" + +echo +echo +echo docker run +echo ============== +echo docker run ... --network easyhaproxy ... your_image:tag +echo \ No newline at end of file diff --git a/deploy/kubernetes/easyhaproxy-clusterip.yml b/deploy/kubernetes/easyhaproxy-clusterip.yml new file mode 100644 index 0000000..ee8d8a7 --- /dev/null +++ b/deploy/kubernetes/easyhaproxy-clusterip.yml @@ -0,0 +1,208 @@ +--- +# Source: easyhaproxy/templates/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: ingress-easyhaproxy + namespace: easyhaproxy + labels: + helm.sh/chart: easyhaproxy-0.1.3 + app.kubernetes.io/name: easyhaproxy + app.kubernetes.io/instance: ingress + app.kubernetes.io/version: "master" + app.kubernetes.io/managed-by: Helm +--- +# Source: easyhaproxy/templates/clusterrole.yaml +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: ingress-easyhaproxy + namespace: easyhaproxy + labels: + helm.sh/chart: easyhaproxy-0.1.3 + app.kubernetes.io/name: easyhaproxy + app.kubernetes.io/instance: ingress + app.kubernetes.io/version: "master" + app.kubernetes.io/managed-by: Helm +rules: +- apiGroups: + - "" + resources: + # - configmaps + # - endpoints + # - nodes + - pods + - services + - namespaces + # - events + - serviceaccounts + verbs: + - get + - list + - watch +- apiGroups: + - "extensions" + - "networking.k8s.io" + resources: + - ingresses + # - ingresses/status + # - ingressclasses + verbs: + - get + - list + - watch +# - apiGroups: +# - "extensions" +# - "networking.k8s.io" +# resources: +# - ingresses/status +# verbs: +# - update +- apiGroups: + - "" + resources: + - secrets + verbs: + - get + - list + # - watch + # - create + # - patch + # - update +# - apiGroups: +# - "discovery.k8s.io" +# resources: +# - endpointslices +# verbs: +# - get +# - list +# - watch +--- +# Source: easyhaproxy/templates/clusterrolebinding.yaml +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: ingress-easyhaproxy + namespace: easyhaproxy + labels: + helm.sh/chart: easyhaproxy-0.1.3 + app.kubernetes.io/name: easyhaproxy + app.kubernetes.io/instance: ingress + app.kubernetes.io/version: "master" + app.kubernetes.io/managed-by: Helm +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: ingress-easyhaproxy +subjects: +- kind: ServiceAccount + name: ingress-easyhaproxy + namespace: easyhaproxy +--- +# Source: easyhaproxy/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: +metadata: + name: ingress-easyhaproxy + namespace: easyhaproxy + labels: + helm.sh/chart: easyhaproxy-0.1.3 + app.kubernetes.io/name: easyhaproxy + app.kubernetes.io/instance: ingress + app.kubernetes.io/version: "master" + app.kubernetes.io/managed-by: Helm + annotations: + {} +spec: + type: ClusterIP + ports: + - name: http + port: 80 + + - name: https + port: 443 + + - name: stats + port: 1936 + + + + selector: + app.kubernetes.io/name: easyhaproxy + app.kubernetes.io/instance: ingress +--- +# Source: easyhaproxy/templates/deployment.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: ingress-easyhaproxy + namespace: easyhaproxy + labels: + helm.sh/chart: easyhaproxy-0.1.3 + app.kubernetes.io/name: easyhaproxy + app.kubernetes.io/instance: ingress + app.kubernetes.io/version: "master" + app.kubernetes.io/managed-by: Helm +spec: + selector: + matchLabels: + app.kubernetes.io/name: easyhaproxy + app.kubernetes.io/instance: ingress + template: + metadata: + labels: + app.kubernetes.io/name: easyhaproxy + app.kubernetes.io/instance: ingress + spec: + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: easyhaproxy/node + operator: In + values: + - master + serviceAccountName: ingress-easyhaproxy + securityContext: + {} + containers: + - name: easyhaproxy + securityContext: + {} + image: "byjg/easy-haproxy:master" + imagePullPolicy: Always + ports: + - name: http + containerPort: 80 + + - name: https + containerPort: 443 + + - name: stats + containerPort: 1936 + + resources: + requests: + cpu: 100m + memory: 128Mi + env: + - name: EASYHAPROXY_DISCOVER + value: kubernetes + - name: HAPROXY_USERNAME + value: admin + - name: HAPROXY_PASSWORD + value: password + - name: EASYHAPROXY_REFRESH_CONF + value: "10" + - name: HAPROXY_CUSTOMERRORS + value: "true" + - name: EASYHAPROXY_SSL_MODE + value: loose + - name: EASYHAPROXY_LOG_LEVEL + value: DEBUG + - name: HAPROXY_LOG_LEVEL + value: DEBUG + - name: CERTBOT_LOG_LEVEL + value: DEBUG diff --git a/deploy/kubernetes/easyhaproxy-daemonset.yml b/deploy/kubernetes/easyhaproxy-daemonset.yml new file mode 100644 index 0000000..7631fc0 --- /dev/null +++ b/deploy/kubernetes/easyhaproxy-daemonset.yml @@ -0,0 +1,175 @@ +--- +# Source: easyhaproxy/templates/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: ingress-easyhaproxy + namespace: easyhaproxy + labels: + helm.sh/chart: easyhaproxy-0.1.3 + app.kubernetes.io/name: easyhaproxy + app.kubernetes.io/instance: ingress + app.kubernetes.io/version: "master" + app.kubernetes.io/managed-by: Helm +--- +# Source: easyhaproxy/templates/clusterrole.yaml +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: ingress-easyhaproxy + namespace: easyhaproxy + labels: + helm.sh/chart: easyhaproxy-0.1.3 + app.kubernetes.io/name: easyhaproxy + app.kubernetes.io/instance: ingress + app.kubernetes.io/version: "master" + app.kubernetes.io/managed-by: Helm +rules: +- apiGroups: + - "" + resources: + # - configmaps + # - endpoints + # - nodes + - pods + - services + - namespaces + # - events + - serviceaccounts + verbs: + - get + - list + - watch +- apiGroups: + - "extensions" + - "networking.k8s.io" + resources: + - ingresses + # - ingresses/status + # - ingressclasses + verbs: + - get + - list + - watch +# - apiGroups: +# - "extensions" +# - "networking.k8s.io" +# resources: +# - ingresses/status +# verbs: +# - update +- apiGroups: + - "" + resources: + - secrets + verbs: + - get + - list + # - watch + # - create + # - patch + # - update +# - apiGroups: +# - "discovery.k8s.io" +# resources: +# - endpointslices +# verbs: +# - get +# - list +# - watch +--- +# Source: easyhaproxy/templates/clusterrolebinding.yaml +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: ingress-easyhaproxy + namespace: easyhaproxy + labels: + helm.sh/chart: easyhaproxy-0.1.3 + app.kubernetes.io/name: easyhaproxy + app.kubernetes.io/instance: ingress + app.kubernetes.io/version: "master" + app.kubernetes.io/managed-by: Helm +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: ingress-easyhaproxy +subjects: +- kind: ServiceAccount + name: ingress-easyhaproxy + namespace: easyhaproxy +--- +# Source: easyhaproxy/templates/deployment.yaml +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: ingress-easyhaproxy + namespace: easyhaproxy + labels: + helm.sh/chart: easyhaproxy-0.1.3 + app.kubernetes.io/name: easyhaproxy + app.kubernetes.io/instance: ingress + app.kubernetes.io/version: "master" + app.kubernetes.io/managed-by: Helm +spec: + selector: + matchLabels: + app.kubernetes.io/name: easyhaproxy + app.kubernetes.io/instance: ingress + template: + metadata: + labels: + app.kubernetes.io/name: easyhaproxy + app.kubernetes.io/instance: ingress + spec: + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: easyhaproxy/node + operator: In + values: + - master + serviceAccountName: ingress-easyhaproxy + securityContext: + {} + containers: + - name: easyhaproxy + securityContext: + {} + image: "byjg/easy-haproxy:master" + imagePullPolicy: Always + ports: + - name: http + containerPort: 80 + hostPort: 80 + - name: https + containerPort: 443 + hostPort: 443 + - name: stats + containerPort: 1936 + hostPort: 1936 + resources: + requests: + cpu: 100m + memory: 128Mi + env: + - name: EASYHAPROXY_DISCOVER + value: kubernetes + - name: HAPROXY_USERNAME + value: admin + - name: HAPROXY_PASSWORD + value: password + - name: EASYHAPROXY_REFRESH_CONF + value: "10" + - name: HAPROXY_CUSTOMERRORS + value: "true" + - name: EASYHAPROXY_SSL_MODE + value: loose + - name: EASYHAPROXY_LOG_LEVEL + value: DEBUG + - name: HAPROXY_LOG_LEVEL + value: DEBUG + - name: CERTBOT_LOG_LEVEL + value: DEBUG diff --git a/deploy/kubernetes/easyhaproxy-nodeport.yml b/deploy/kubernetes/easyhaproxy-nodeport.yml new file mode 100644 index 0000000..542d94b --- /dev/null +++ b/deploy/kubernetes/easyhaproxy-nodeport.yml @@ -0,0 +1,208 @@ +--- +# Source: easyhaproxy/templates/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: ingress-easyhaproxy + namespace: easyhaproxy + labels: + helm.sh/chart: easyhaproxy-0.1.3 + app.kubernetes.io/name: easyhaproxy + app.kubernetes.io/instance: ingress + app.kubernetes.io/version: "master" + app.kubernetes.io/managed-by: Helm +--- +# Source: easyhaproxy/templates/clusterrole.yaml +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: ingress-easyhaproxy + namespace: easyhaproxy + labels: + helm.sh/chart: easyhaproxy-0.1.3 + app.kubernetes.io/name: easyhaproxy + app.kubernetes.io/instance: ingress + app.kubernetes.io/version: "master" + app.kubernetes.io/managed-by: Helm +rules: +- apiGroups: + - "" + resources: + # - configmaps + # - endpoints + # - nodes + - pods + - services + - namespaces + # - events + - serviceaccounts + verbs: + - get + - list + - watch +- apiGroups: + - "extensions" + - "networking.k8s.io" + resources: + - ingresses + # - ingresses/status + # - ingressclasses + verbs: + - get + - list + - watch +# - apiGroups: +# - "extensions" +# - "networking.k8s.io" +# resources: +# - ingresses/status +# verbs: +# - update +- apiGroups: + - "" + resources: + - secrets + verbs: + - get + - list + # - watch + # - create + # - patch + # - update +# - apiGroups: +# - "discovery.k8s.io" +# resources: +# - endpointslices +# verbs: +# - get +# - list +# - watch +--- +# Source: easyhaproxy/templates/clusterrolebinding.yaml +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: ingress-easyhaproxy + namespace: easyhaproxy + labels: + helm.sh/chart: easyhaproxy-0.1.3 + app.kubernetes.io/name: easyhaproxy + app.kubernetes.io/instance: ingress + app.kubernetes.io/version: "master" + app.kubernetes.io/managed-by: Helm +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: ingress-easyhaproxy +subjects: +- kind: ServiceAccount + name: ingress-easyhaproxy + namespace: easyhaproxy +--- +# Source: easyhaproxy/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: +metadata: + name: ingress-easyhaproxy + namespace: easyhaproxy + labels: + helm.sh/chart: easyhaproxy-0.1.3 + app.kubernetes.io/name: easyhaproxy + app.kubernetes.io/instance: ingress + app.kubernetes.io/version: "master" + app.kubernetes.io/managed-by: Helm + annotations: + {} +spec: + type: NodePort + ports: + - name: http + port: 80 + nodePort: 31080 + - name: https + port: 443 + nodePort: 31443 + - name: stats + port: 1936 + nodePort: 31936 + + + selector: + app.kubernetes.io/name: easyhaproxy + app.kubernetes.io/instance: ingress +--- +# Source: easyhaproxy/templates/deployment.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: ingress-easyhaproxy + namespace: easyhaproxy + labels: + helm.sh/chart: easyhaproxy-0.1.3 + app.kubernetes.io/name: easyhaproxy + app.kubernetes.io/instance: ingress + app.kubernetes.io/version: "master" + app.kubernetes.io/managed-by: Helm +spec: + selector: + matchLabels: + app.kubernetes.io/name: easyhaproxy + app.kubernetes.io/instance: ingress + template: + metadata: + labels: + app.kubernetes.io/name: easyhaproxy + app.kubernetes.io/instance: ingress + spec: + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: easyhaproxy/node + operator: In + values: + - master + serviceAccountName: ingress-easyhaproxy + securityContext: + {} + containers: + - name: easyhaproxy + securityContext: + {} + image: "byjg/easy-haproxy:master" + imagePullPolicy: Always + ports: + - name: http + containerPort: 80 + + - name: https + containerPort: 443 + + - name: stats + containerPort: 1936 + + resources: + requests: + cpu: 100m + memory: 128Mi + env: + - name: EASYHAPROXY_DISCOVER + value: kubernetes + - name: HAPROXY_USERNAME + value: admin + - name: HAPROXY_PASSWORD + value: password + - name: EASYHAPROXY_REFRESH_CONF + value: "10" + - name: HAPROXY_CUSTOMERRORS + value: "true" + - name: EASYHAPROXY_SSL_MODE + value: loose + - name: EASYHAPROXY_LOG_LEVEL + value: DEBUG + - name: HAPROXY_LOG_LEVEL + value: DEBUG + - name: CERTBOT_LOG_LEVEL + value: DEBUG diff --git a/docs/container-labels.md b/docs/container-labels.md new file mode 100644 index 0000000..97988d5 --- /dev/null +++ b/docs/container-labels.md @@ -0,0 +1,97 @@ +# Container Labels + +## Container (Docker or Swarm) labels + +| Tag | Description | Default | Example | +|---------------------------------------|-------------------------------------------------------------------------------------------------------|----------------|--------------| +| easyhaproxy.[definition].host | Host(s) HAProxy is listening. More than one host use comma as delimiter | **required** | somehost.com OR host1.com,host2.com | +| easyhaproxy.[definition].mode | (Optional) Is this `http` or `tcp` mode in HAProxy. | http | http or tcp | +| easyhaproxy.[definition].port | (Optional) Port HAProxy will listen for the host. | 80 | 3000 | +| easyhaproxy.[definition].localport | (Optional) Port container is listening. | 80 | 8080 | +| easyhaproxy.[definition].redirect | (Optional) JSON containing key/value pair from host/to URL redirect. | *empty* | {"foo.com":"https://bla.com", "bar.com":"https://bar.org"} | +| easyhaproxy.[definition].sslcert | (Optional) Cert PEM Base64 encoded. Do not use this if `letsencrypt` is enabled. | *empty* | base64 cert + key | +| easyhaproxy.[definition].ssl | (Optional) If `true` you need to provide certificate as a file. See below. Do not use with `sslcert`. | false | true or false | +| easyhaproxy.[definition].health-check | (Optional) `ssl`, enable health check via SSL in `mode tcp` | *empty* | ssl | +| easyhaproxy.[definition].letsencrypt | (Optional) Generate certificate with letsencrypt. Do not use with `sslcert` parameter. | false | true OR false | +| easyhaproxy.[definition].redirect_ssl | (Optional) Redirect all requests to https | false | true OR false | +| easyhaproxy.[definition].clone_to_ssl | (Optional) It copies the configuration to HTTPS(443) and disable SSL from the current config. **Do not use* this with `ssl` or `letsencrypt` parameters | false | true OR false | + +The `definition` is a string that will group all configurations togethers. Different `definition` will create different configurations. + +The container can have more than one defintion. + +## Configuations + +### Single Definition + +```bash +docker run \ + -l easyhaproxy.webapi.port=80\ + -l easyhaproxy.webapi.host=byjg.com.br \ + .... +``` + +### Multiples Definitions on the same container + +```bash +docker run \ + -l easyhaproxy.express.port=80 \ + -l easyhaproxy.express.localport=3000 \ + -l easyhaproxy.express.host=express.byjg.com.br \ + + -l easyhaproxy.admin.port=80 \ + -l easyhaproxy.admin.localport=3001 \ + -l easyhaproxy.admin.host=admin.byjg.com.br \ + .... \ + some/myimage +``` + +### Multiples hosts on the same container + +```bash +docker run \ + -l easyhaproxy.express.port=80 \ + -l easyhaproxy.express.localport=3000 \ + -l easyhaproxy.express.host=express.byjg.com.br,admin.byjg.com.br \ + .... \ + some/myimage +``` + +If you are using docker-compose you can use this way: + +```yaml +version: "3" + +services: + mycontainer: + image: some/myimage + labels: + easyhaproxy.express.port: 80 + easyhaproxy.express.localport: 3000 + easyhaproxy.express.host: >- + express.byjg.com.br, + admin.byjg.com.br +``` + +### TCP Mode + +Set `easyhaproxy.[definition].mode=tcp` if your application uses TCP protocol instead of HTTP. + +```bash +docker run \ + -l easyhaproxy.example.mode=tcp \ + -l easyhaproxy.example.port=3306 + -l easyhaproxy.example.localport=3306 + .... \ + some/tcp-service +``` + +### Redirect Domains + +```bash +docker run \ + -l easyhaproxy.[definition].redirect='{"www.byjg.com.br":"http://byjg.com.br","byjg.com":"http://byjg.com.br"}' +``` + +---- +[Open source ByJG](http://opensource.byjg.com) diff --git a/docs/docker-environment.md b/docs/docker-environment.md new file mode 100644 index 0000000..1ed6288 --- /dev/null +++ b/docs/docker-environment.md @@ -0,0 +1,21 @@ +# Docker environment variables + +| Environment Variable | Description | Default | +|-------------------------------|-------------------------------------------------------------------------------------------------|------------------| +| EASYHAPROXY_DISCOVER | How the services will be discovered to create `haproxy.cfg`: `static`, `docker`, `swarm` or `kubernetes` | **required** | +| EASYHAPROXY_LABEL_PREFIX | (Optional) The key will search for matching resources. | `easyhaproxy` | +| EASYHAPROXY_LETSENCRYPT_EMAIL | (Optional) The email will be used to request the certificate to Letsencrypt | *empty* | +| EASYHAPROXY_SSL_MODE | (Optional) `strict` supports only the most recent TLS version; `default` good SSL integration with recent browsers; `loose` supports all old SSL protocols for old browsers (not recommended). | `default`| +| EASYHAPROXY_REFRESH_CONF | (Optional) Check configuration every N seconds. | 10 | +| EASYHAPROXY_LOG_LEVEL | (Optional) The log level for EasyHAproxy messages. Available: TRACE,DEBUG,INFO,WARN,ERROR,FATAL | DEBUG | +| CERTBOT_LOG_LEVEL | (Optional) The log level for Certbot messages. Available: TRACE,DEBUG,INFO,WARN,ERROR,FATAL | DEBUG | +| HAPROXY_LOG_LEVEL | (Optional) The log level for HAProxy messages. Available: TRACE,DEBUG,INFO,WARN,ERROR,FATAL | DEBUG | +| HAPROXY_USERNAME | (Optional) The HAProxy username to the statistics. | `admin` | +| HAPROXY_PASSWORD | (Optional) The HAProxy password to the statistics. If not set, statistics will be available with no password | *empty* | +| HAPROXY_STATS_PORT | (Optional) The HAProxy port to the statistics. If set to `false`, disable statistics | `1936` | +| HAPROXY_CUSTOMERRORS | (Optional) If HAProxy will use custom HTML errors. true/false. | `false` | + + + +---- +[Open source ByJG](http://opensource.byjg.com) diff --git a/docs/docker.md b/docs/docker.md new file mode 100644 index 0000000..a195129 --- /dev/null +++ b/docs/docker.md @@ -0,0 +1,65 @@ +# Docker + +## Setup Docker EasyHAProxy + +This method will use a docker standalone installation to discover the containers and configure the HAProxy. + +The only requirement is that containers and EasyHAProxy must be in the same docker network in order to HAProxy be able direct the traffic to the containers. + +e.g.: + +```bash +docker create network easyhaproxy +``` + +And then run the EasyHAProxy + +```bash +docker run -d \ + --name easy-haproxy-container \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -e EASYHAPROXY_DISCOVER="docker" \ + # + Environment Variables \ + -p 80:80 \ + -p 443:443 \ + -p 1936:1936 \ + --network easyhaproxy + byjg/easy-haproxy +``` + +The mapping to `/var/run/docker.sock` is necessary to discover the docker containers and get the labels; + +## Running containers + +To make your containers "discoverable" by EasyHAProxy that is minimum configuration you need: + +```bash +docker run -d \ + -e easyhaproxy.http.host=example.org \ + -e easyhaproxy.http.port=80 \ + -e easyhaproxy.http.localport=8080 \ + --network easyhaproxy + my/image:tag +``` + +Once the container is running EasyHAProxy will detect automatically and start to redirect all traffic from `example.org:80` to your container. + +You don't need to expose any port in your container. + +There a list of other parameters you can to configure your container. Please follow the [docker label configuration](container-labels.md) + +## Setup the EasyHAProxy container + +You can configure the behavior of the EasyHAProxy by setup specific environment variables. To get a list of the variables please follow the [docker container environment](docker-environment.md) + + +## Setup certificates with Letsencrypt + +Follow [this link](letsencrypt.md) + +## Setup your own certificates + +Follow [this link](ssl.md) + +---- +[Open source ByJG](http://opensource.byjg.com) diff --git a/docs/icons.xcf b/docs/icons.xcf new file mode 100644 index 0000000..dda1161 Binary files /dev/null and b/docs/icons.xcf differ diff --git a/docs/kubernetes.md b/docs/kubernetes.md new file mode 100644 index 0000000..7181454 --- /dev/null +++ b/docs/kubernetes.md @@ -0,0 +1,198 @@ +# Kubernetes + +## Setup Kubernetes EasyHAProxy + +EasyHAProxy query all ingress definitions with the annotation `kubernetes.io/ingress.class: easyhaproxy-ingress`.Once find the annotation, it will immediatelly setup HAProxy and start to serve it. + +There are three installation modes: + +- DaemonSet: It will expose the ports 80, 443 and 1936 +- NodePort: It will expose the ports 31080, 31443 and 31936 +- ClusterIP it will node expose any port. The HAProxy will be accessible only inside the cluster. + +To install the daemonset in your cluster follow these steps: + +### 1) Identify the node where your EasyHAProxy container will run. + +Doesn't matter if you choose DaemonSet or ClusterIP, EasyHAProxy will be limited to a single node. To understand that see [limitations](limitations.md) page. + +```bash +$ kubectl get nodes + +NAME STATUS ROLES AGE VERSION +node-01 Ready 561d v1.21.13-3 +node-02 Ready 561d v1.21.13-3 +``` + +Add the EasyHAProxy label to the node + +```bash +kubectl label nodes node-01 "easyhaproxy/node=master" +``` + +### 2) Install EasyHAProxy + +There are two ways to install EasyHAProxy in a Kubernetes cluster. You can use Kubernetes Manifest or Helm 3. + +#### 2.1.) Using Kubernetes Manifest + +```bash +kubectl create namespace easyhaproxy + +kubectl apply -f \ + https://raw.githubusercontent.com/byjg/docker-easy-haproxy/kubernetes/deploy/kubernetes/easyhaproxy-daemonset.yml +``` + +You can configure the behavior of the EasyHAProxy by setup specific environment variables. To get a list of the variables please follow the [docker container environment](docker-environment.md) + +#### 2.2) Using HELM 3 + +Minimal configuration + +```bash +helm repo add byjg https://opensource.byjg.com/helm +helm repo update byjg +kubectl create namespace easyhaproxy + +helm upgrade --install ingress byjg/easyhaproxy \ + --namespace easyhaproxy \ + --set resources.requests.cpu=100m \ + --set resources.requests.memory=128Mi +``` + +Customizing Helm Values: + +```yaml +easyhaproxy: + stats: + username: admin + password: password + refresh: "10" + customErrors: "true" + sslMode: loose + logLevel: + certbot: DEBUG + easyhaproxy: DEBUG + haproxy: DEBUG + +service: + create: false # If false, it will create a Daemonset with hostPort. The easiest. + type: ClusterIP # or NodePort + annotations: {} + +binding: + ports: + http: 80 + https: 443 + stats: 1936 + additionalPorts: [] + +# Make sure to create this +masterNode: + label: easyhaproxy/node + values: + - master +``` + +## Running containers + +The only requirement is that you have an ingress properly setup and with the annotation `kubernetes.io/ingress.class: easyhaproxy-ingress`. + +e.g. + +```yaml +kind: Ingress +metadata: + annotations: + kubernetes.io/ingress.class: easyhaproxy-ingress + name: example-ingress + namespace: example +spec: + rules: + - host: example.org + http: + paths: + - backend: + service: + name: example-service + port: + number: 8080 + pathType: ImplementationSpecific +``` + +Once the container is running EasyHAProxy will detect automatically and start to redirect all traffic from `example.org:80` to your container. + +You don't need to expose any port in your container. + +Caveats: + +- At this point, the implementation don't support all ingress properties nor wildcard domains. +- The ingress will publish externally only the ports 80 and 443, plus 1936 if stats is enable. +- EasyHAProxy will read all `spec.rules[].host` spec, however it will parse only the first path `spec.rules[].http.paths[0].port.number` for each rule, and ignore the other paths. + +## Kubernetes annotations + +| annotation | Description | Default | Example | +|-----------------------------|-----------------------------------------------------------------------------------------|--------------|--------------| +| kubernetes.io/ingress.class | (required) Activate EasyHAProxy. | **required** | easyhaproxy-ingress +| easyhaproxy.redirect_ssl | (optional) Boolean. Force redirect all endpoints to https. | false | true or false +| easyhaproxy.letsencrypt | (optional) Boolean. It will request letsencript certificates for the ingresses domains. | false | true or false +| easyhaproxy.redirect | (optional) Json. Specific a domain and its destination. | *empty* | {"domain":"redirect_url"} +| easyhaproxy.mode | (optional) Set the HTTP mode for that connection. | http | http or tcp +| easyhaproxy.listen_port | (optional) Set the an additional port for that ingress | http | http or tcp + +**Important**: The annotations are per ingress and applied to all hosts in that ingress configuration. + +## Letsencrypt + +It is necessary add the annotation `easyhaproxy.letsencrypt` to the ingress configuration: + +```yaml +kind: Ingress +metadata: + annotations: + kubernetes.io/ingress.class: easyhaproxy-ingress + easyhaproxy.letsencrypt: 'true' + name: example-ingress + namespace: example +spec: + .... +``` + +Make sure your cluster is accessible both through ports 80 and 443. + +## Custom SSL Certificates + +You need to create a secret with your certificate and key, and associate them in your ingress. + +```yaml +--- +apiVersion: v1 +kind: Secret +metadata: + name: host2-tls + namespace: default +data: + tls.crt: base64 of your certificate + tls.key: base64 of your certificate private key +type: kubernetes.io/tls + +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + annotations: + kubernetes.io/ingress.class: easyhaproxy-ingress + name: tls-example + namespace: default +spec: + tls: + - hosts: + - host2.local + secretName: host2-tls + rules: + ... +``` + +---- +[Open source ByJG](http://opensource.byjg.com) diff --git a/docs/letsencrypt.md b/docs/letsencrypt.md new file mode 100644 index 0000000..6dfcc38 --- /dev/null +++ b/docs/letsencrypt.md @@ -0,0 +1,52 @@ +# Letsencrypt + +EasyHAProxy can issue a letsencrypt certificate. The command is as below: + +Run the EasyHAProxy: + +```bash +docker run \ + -e EASYHAPROXY_LETSENCRYPT_EMAIL=john@doe.com + .... \ + byjg/easy-haproxy +``` + +Run your container: + +```bash +docker run \ + -l easyhaproxy.express.port=80 \ + -l easyhaproxy.express.localport=3000 \ + -l easyhaproxy.express.host=example.org \ + -l easyhaproxy.express.letsencrypt=true \ + .... \ + some/myimage +``` + +Requirements: + +- Your container **must** listen to the port 80. Besides no error, the certificate won't be issued if in a different port. +- You cannot set the port 443 for the container with the Letsencrypt because EasyHAProxy will handle this automatically once the certificate is issued. +- You have to setup the `EASYHAPROXY_LETSENCRYPT_EMAIL` environment variable on EasyHAProxy. If you don't setup, EasyHAProxy **will not request** a certificate. + +Be aware of Letsencrypt issue limits - https://letsencrypt.org/docs/duplicate-certificate-limit/ and https://letsencrypt.org/docs/rate-limits/ + +## Persist your Letsencrypt certificates + +It is a good idea to store the letsencrypt certificate in a persistent storage, even you knowing you can issue again in case your lost the certificate. + +However, there is a limit in how many certificates can be issue for the same domain in a period of time. + +To avoid this, map the folder `/certs/letsencrypt` to a docker volume. + +```bash +docker volume create certs_letsencrypt + +docker run \ + /* other parameters */ + -v certs_letsencrypt:/certs/letsencrypt \ + -d byjg/easy-haproxy +``` + +---- +[Open source ByJG](http://opensource.byjg.com) diff --git a/docs/limitations.md b/docs/limitations.md new file mode 100644 index 0000000..3f64701 --- /dev/null +++ b/docs/limitations.md @@ -0,0 +1,16 @@ +# Limitations + +EasyHAProxy currently expects to work in a single replica. + +If more than one replica is running, EasyHAProxy will continue to work, however each replica will discover +the services independently. + +It means replicas can be out-of-sync for a few seconds because each replica will discover the pods +separately. + +For Letsencrypt this is worse because each replica will a Letsencrypt certificate and can fail because the +letsencrypt challenge can be directed to the other replica. Also, you can hit the certificate issue limit. +So if you intend to run multiple replicas **do not** activate letsencrypt. + +---- +[Open source ByJG](http://opensource.byjg.com) \ No newline at end of file diff --git a/docs/logo.xcf b/docs/logo.xcf new file mode 100644 index 0000000..ab58b08 Binary files /dev/null and b/docs/logo.xcf differ diff --git a/docs/other.md b/docs/other.md new file mode 100644 index 0000000..5f93002 --- /dev/null +++ b/docs/other.md @@ -0,0 +1,39 @@ +# Other configurations + +## Exposing Ports + +You must expose some ports on the EasyHAProxy container and in the firewall. However, you don't need to expose the other container ports because EasyHAProxy will handle that. + +- The ports `80` and `443`. +- If you enable the HAProxy statistics, you must also expose the port defined in `HAPROXY_STATS_PORT` environment variable (default 1936). Be aware that statististics are enabled by default with no password. +- Every port defined in `easyhaproxy.[definitions].port` also should be exposed. + +e.g. + +```bash +docker run \ + /* other parameters */ + -p 80:80 \ + -p 443:443 \ + -p 1936:1936 \ + -d byjg/easy-haproxy +``` + +## Mapping custom .cfg files + +You can concatenate valid HAProxy `.cfg` files to the dynamically generated `haproxy.cfg` by mapping the folder `/etc/haproxy/conf.d`. + +```bash +docker run \ + /* other parameters */ + -v /your/local/conf.d:/etc/haproxy/conf.d \ + -d byjg/easy-haproxy +``` + +## Setting Custom Errors + +If enabled, map the volume : `/etc/haproxy/errors-custom/` to your container and put a file named `ERROR_NUMBER.http` +where ERROR_NUMBER is the HTTP error code (e.g., `503.http`) + +---- +[Open source ByJG](http://opensource.byjg.com) \ No newline at end of file diff --git a/docs/ssl.md b/docs/ssl.md new file mode 100644 index 0000000..9baffd3 --- /dev/null +++ b/docs/ssl.md @@ -0,0 +1,75 @@ +# Setup your own certificates + +You can use your certificates with EasyHAProxy. + +There is two ways to do that. + +## Setup certificate as a label definition in docker container + +1. First, Create a single PEM from the certificate and the key. + +```bash +cat example.com.crt example.com.key > single.pem + +cat single.pem + +-----BEGIN CERTIFICATE----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC5ZheHqmBnEJP+ +U9r1gxYWKLzdqrMrcxtQN6M1hIH9n0peuJeIrybdcV7sMbStMXI= +-----END CERTIFICATE----- + +-----BEGIN PRIVATE KEY----- +MIIEojCCA4qgAwIBAgIUegW2BimwuL4RzRZ2WYkHA6U5nkAwDQYJKoZIhvcNAQEL +3j4wz8/I5fdsk090j4s5KA== +-----END PRIVATE KEY----- +``` + +2. Convert the `single.pem` to BASE64 in a single line: + +```bash +cat single.pem | base64 -w0 +``` + +3. Use this string to define the label `easyhaproxy.[definition].sslcert` + +## Map the certificate as docker volume + +EasyHAProxy stores the certificates inside the folder `/certs/haproxy`. + +1. Run EasyHAProxy with the volume for the certificates: + +```bash +docker volume create certs_haproxy + +docker run \ + /* other parameters */ + -v certs_haproxy:/certs/haproxy \ + -d byjg/easy-haproxy +``` + +2. Create a single PEM from the certificate and the key. + +```bash +cat example.com.crt example.com.key > single.pem + +cat single.pem + +-----BEGIN CERTIFICATE----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC5ZheHqmBnEJP+ +U9r1gxYWKLzdqrMrcxtQN6M1hIH9n0peuJeIrybdcV7sMbStMXI= +-----END CERTIFICATE----- + +-----BEGIN PRIVATE KEY----- +MIIEojCCA4qgAwIBAgIUegW2BimwuL4RzRZ2WYkHA6U5nkAwDQYJKoZIhvcNAQEL +3j4wz8/I5fdsk090j4s5KA== +-----END PRIVATE KEY----- +``` + +3. Copy this certificate to EasyHAProxy volume + +```bash +docker cp single.pem easyhaproxy:/certs/haproxy +``` + +---- +[Open source ByJG](http://opensource.byjg.com) diff --git a/docs/static.md b/docs/static.md new file mode 100644 index 0000000..8506234 --- /dev/null +++ b/docs/static.md @@ -0,0 +1,109 @@ +# Docker + +## Setup Docker EasyHAProxy + +This method will use a static configuration, simpler and easier than HAProxy to create the `haproxy.cfg` + +You can use this configuration to setup external servers not related to docker or kubernetes. + +Another advantage is that EasyHAProxy will monitor for changes in this file and automatically reconfigure HAProxy and changes are detected. + +First, Create a YAML: + +```yaml +stats: + username: admin + password: password + port: 1936 # Optional (default 1936) + +customerrors: true # Optional (default false) + +ssl_mode: default + +letsencrypt: { + "email": "acme@example.org" +} + +easymapping: + - port: 80 + hosts: + host1.com.br: + containers: + - container:5000 + letsencrypt: true + redirect_ssl: true + host2.com.br: + containers: + - other:3000 + redirect: + www.host1.com.br: http://host1.com.br + + - port: 443 + hosts: + host1.com.br: + containers: + - container:80 + redirect_ssl: false + ssl: true + + - port: 8080 + hosts: + host3.com.br: + containers: + - domain:8181 +``` + +Then map this file to `/etc/haproxy/easyconfig.yml` in your EasyHAProxy container as: + +```bash +docker run -d \ + --name easy-haproxy-container \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v /my/config.yml:/etc/haproxy/easyconfig.yml + -e EASYHAPROXY_DISCOVER="static" \ + # + Environment Variables \ + -p 80:80 \ + -p 443:443 \ + -p 1936:1936 \ + --network easyhaproxy + byjg/easy-haproxy +``` + +You can find other informations on [docker label configuration](container-labels.md) and [docker container environment](docker-environment.md) + +## Yaml Definition + +```yaml +stats: + username: admin # Optional (default "admin") + password: password # If stats or stats.password is omitted, stats will be public with no password + port: 1936 # Optional (default 1936) + +customerrors: true # Optional (default false) + +ssl_mode: default # Optional + +letsencrypt: { # Optional. If you enable `letsencrypt` will need to setu0p this, + # otherwise the certificate will be issued + "email": "acme@example.org" +} + +easymapping: + - port: 80 # Listen port + mode: http # Optional. Default `http`. Can be http or tcp + hosts: + host1.com.br: # Hostname + containers: + - container:5000 # Endpoints of the hostname above (ip, dns, container, etc) + letsencrypt: true # Optional. it will request a letsencrypt certiticate + redirect_ssl: true # Optional. It will redirect this site to it SSL. + ssl: true # Optional. Inform this port will listen to SSL, instead of HTTP + clone_to_ssl: true # Optional. Default False. You clone these hosts to it equivalent SSL. + redirect: + www.host1.com.br: http://host1.com.br +``` + +*Note*: The only way to pass SSL certificates is to map the certificates to EasyHAProxy as a docker volume. Refer to the [SSL documentation](ssl.md) to learn how to do it. + +---- +[Open source ByJG](http://opensource.byjg.com) diff --git a/docs/swarm.md b/docs/swarm.md new file mode 100644 index 0000000..9d26245 --- /dev/null +++ b/docs/swarm.md @@ -0,0 +1,96 @@ +# Swarm + +## Setup Docker EasyHAProxy + +This method will use a docker swarm installation to discover the containers and configure the HAProxy. +The advantage of this method is that you can discover container in other nodes from cluster. + +The only requirement is that containers and EasyHAProxy must be in the same docker swarm network in order to HAProxy be able direct the traffic to the containers. + +e.g.: + +```bash +docker create network easyhaproxy +``` + +And then deploy the EasyHAProxy stack: + +```yaml +version: "3" + +services: + haproxy: + image: byjg/easy-haproxy + volumes: + - /var/run/docker.sock:/var/run/docker.sock + deploy: + replicas: 1 + environment: + EASYHAPROXY_DISCOVER: swarm + EASYHAPROXY_SSL_MODE: "loose" + HAPROXY_CUSTOMERRORS: "true" + HAPROXY_USERNAME: admin + HAPROXY_PASSWORD: password + HAPROXY_STATS_PORT: 1936 + ports: + - "80:80/tcp" + - "443:443/tcp" + - "1936:1936/tcp" + networks: + - easyhaproxy + +networks: + easyhaproxy: + external: true +``` + +and then: + +```bash +docker stack deploy --compose-file docker-compose.yml easyhaproxy +``` + +The mapping to `/var/run/docker.sock` is necessary to discover the docker containers and get the labels; + +**Do not** add more than one replica for EasyHAProxy. To understand that see [limitations](limitations.md) page. + +## Running containers + +To make your containers "discoverable" by EasyHAProxy that is minimum configuration you need: + +```yaml +version: "3" + +services: + container: + image: my/image:tag + deploy: + replicas: 1 + labels: + easyhaproxy.http.host: host1.local + easyhaproxy.http.port: 80 + easyhaproxy.http.localport: 8080 + networks: + - easyhaproxy + +networks: + easyhaproxy: + external: true +``` + +Once the container is running EasyHAProxy will detect automatically and start to redirect all traffic from `example.org:80` to your container. + +You don't need to expose any port in your container. + +There a list of other parameters you can to configure your container. Please follow the [docker label configuration](container-labels.md) + +## Setup the EasyHAProxy container + +You can configure the behavior of the EasyHAProxy by setup specific environment variables. To get a list of the variables please follow the [docker container environment](docker-environment.md) + +## More information + +You can refer the [Docker Documentation](docker.md) to get other detailed instructions. + +---- +[Open source ByJG](http://opensource.byjg.com) diff --git a/easyhaproxy_docker.png b/easyhaproxy_docker.png new file mode 100644 index 0000000..536bacf Binary files /dev/null and b/easyhaproxy_docker.png differ diff --git a/easyhaproxy_kubernetes.png b/easyhaproxy_kubernetes.png new file mode 100644 index 0000000..c78d327 Binary files /dev/null and b/easyhaproxy_kubernetes.png differ diff --git a/easyhaproxy_logo.png b/easyhaproxy_logo.png new file mode 100644 index 0000000..49f9bfb Binary files /dev/null and b/easyhaproxy_logo.png differ diff --git a/easyhaproxy_static.png b/easyhaproxy_static.png new file mode 100644 index 0000000..9666f88 Binary files /dev/null and b/easyhaproxy_static.png differ diff --git a/easyhaproxy_swarm.png b/easyhaproxy_swarm.png new file mode 100644 index 0000000..9978cc3 Binary files /dev/null and b/easyhaproxy_swarm.png differ diff --git a/examples/kubernetes/service.yml b/examples/kubernetes/service.yml new file mode 100644 index 0000000..2ac821a --- /dev/null +++ b/examples/kubernetes/service.yml @@ -0,0 +1,80 @@ +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + annotations: + kubernetes.io/ingress.class: easyhaproxy-ingress + name: container-example + namespace: default +spec: + rules: + - host: example.org + http: + paths: + - backend: + service: + name: container-example + port: + number: 8080 + pathType: ImplementationSpecific + - host: www.example.org + http: + paths: + - backend: + service: + name: container-example + port: + number: 8080 + pathType: ImplementationSpecific + +--- +apiVersion: v1 +kind: Service +metadata: + name: container-example + namespace: default +spec: + ports: + - name: http + port: 8080 + selector: + app: container-example + type: ClusterIP + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: container-example + namespace: default +spec: + replicas: 1 + revisionHistoryLimit: 10 + strategy: + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + type: RollingUpdate + selector: + matchLabels: + app: container-example + template: + metadata: + labels: + app: container-example + spec: + containers: + - name: container-example + image: byjg/static-httpserver + ports: + - containerPort: 8080 + resources: + limits: + cpu: '0.05' + memory: '20Mi' + requests: + cpu: '0.05' + memory: '20Mi' + env: + - name: TITLE + value: "My Host Example" diff --git a/examples/kubernetes/service_tls.yml b/examples/kubernetes/service_tls.yml new file mode 100644 index 0000000..e351fc2 --- /dev/null +++ b/examples/kubernetes/service_tls.yml @@ -0,0 +1,86 @@ +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + annotations: + kubernetes.io/ingress.class: easyhaproxy-ingress + name: tls-example + namespace: default +spec: + tls: + - hosts: + - host2.local + secretName: host2-tls + rules: + - host: host2.local + http: + paths: + - backend: + service: + name: tls-example + port: + number: 8080 + pathType: ImplementationSpecific + +--- +apiVersion: v1 +kind: Secret +metadata: + name: host2-tls + namespace: default +data: + tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURxVENDQXBHZ0F3SUJBZ0lVSWQ1Yjl0OXVxSDc4ZzAyRXpiV0Y2RktWdzNnd0RRWUpLb1pJaHZjTkFRRUwKQlFBd1pERUxNQWtHQTFVRUJoTUNRbEl4RnpBVkJnTlZCQWdNRGxKcGJ5QmtaU0JLWVc1bGFYSnZNUmN3RlFZRApWUVFIREE1U2FXOGdaR1VnU21GdVpXbHliekVOTUFzR0ExVUVDZ3dFUVVOTlJURVVNQklHQTFVRUF3d0xhRzl6CmRESXViRzlqWVd3d0hoY05Nakl3T0RFMU1EUXlOekExV2hjTk1qTXdPREUxTURReU56QTFXakJrTVFzd0NRWUQKVlFRR0V3SkNVakVYTUJVR0ExVUVDQXdPVW1sdklHUmxJRXBoYm1WcGNtOHhGekFWQmdOVkJBY01EbEpwYnlCawpaU0JLWVc1bGFYSnZNUTB3Q3dZRFZRUUtEQVJCUTAxRk1SUXdFZ1lEVlFRRERBdG9iM04wTWk1c2IyTmhiRENDCkFTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTVNLdnJPYWhhdkNYbnZTRjUxMzFocG8KNms2NUM1N2pnUlE4NEZhRGo1TWJKT1ZsWVFWRnRNRzBYT2s3YStoaDV2MWZlNHdIMFI3STZGRG8wVjlzUytzcwprbzVic0VsYzF4WWxnNUhidUtxODl2UlNLZzZFRGx6dHgzQktiaTkxMlBtdDV2RkdOSjE2emN3NzdEVXJRSVhvCjRJL2I0YTNwbUJpV2o0M05vVElybVNXSHRzR3d3T2ozaUR2U3dlcWRZWEpJcjNocEhINXU2cG9oakRvUXZxRHoKSzZNdThwNm1oQ1VLTnM3S0ZKbk5Jbk5HMjVvUVQ2TzBuNE9HdG1nUmpMV29wZEVuT2hNa0tzZklvSTFYdGxYQgpMQkR2N2h1SUNrM3Q1eXd0ZkNReU8wOWtYN2xGSWdkNXJuNytNandINVdOZXFiUUp4dWFxam9YUW5OWlVnVXNDCkF3RUFBYU5UTUZFd0hRWURWUjBPQkJZRUZOaE1CRzhxNmEraUsybkVDd1ZUbjZCOUVYWk9NQjhHQTFVZEl3UVkKTUJhQUZOaE1CRzhxNmEraUsybkVDd1ZUbjZCOUVYWk9NQThHQTFVZEV3RUIvd1FGTUFNQkFmOHdEUVlKS29aSQpodmNOQVFFTEJRQURnZ0VCQUptdWR2eDgrcDVpSVVzVDhmbS9mYlZNMERBNnFXQUxEWVVKblRuM2o2THE0dnBmClBGQytxMUxtdVdmQlFNeXFLckhyUDNlNDkzRWN0WG9pU0taTzZpTjVkVkpJdXIwMk9qR3VpQUVjc1l1WTFuTG4KczlwaWlJK1VFd3hINnV4MU5hSFVuenNXYXVvQnZSaHpqWHZPNlNBVlNaSllhOWRZNW1pelhrbER5RE51RzVVMApsWHY5ZWdNR0JzeTBkRzZlRlhrVTVDUGR4V1U1NDB5STJzQ3RTQWo3eitXUlVENWs3Z0o3dFZvWTMvL2pIUVpHCjVTVFRtbTV0OWtwSVpUV2twdHlKb3M5b1pKRllNSVhxVzJGYzZ0eUxacFJwMzFSNzh0RHM2RVRJa1RvRGMwUlIKano2NnRoNkhJK1psZ0lCUWh3MDkraFlBaEJEZTkrRG1kL1N6UVpjPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0t + tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2d0lCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktrd2dnU2xBZ0VBQW9JQkFRREVpcjZ6bW9XcndsNTcKMGhlZGQ5WWFhT3BPdVF1ZTQ0RVVQT0JXZzQrVEd5VGxaV0VGUmJUQnRGenBPMnZvWWViOVgzdU1COUVleU9oUQo2TkZmYkV2ckxKS09XN0JKWE5jV0pZT1IyN2lxdlBiMFVpb09oQTVjN2Nkd1NtNHZkZGo1cmVieFJqU2RlczNNCk8rdzFLMENGNk9DUDIrR3Q2WmdZbG8rTnphRXlLNWtsaDdiQnNNRG85NGc3MHNIcW5XRnlTSzk0YVJ4K2J1cWEKSVl3NkVMNmc4eXVqTHZLZXBvUWxDamJPeWhTWnpTSnpSdHVhRUUranRKK0RoclpvRVl5MXFLWFJKem9USkNySAp5S0NOVjdaVndTd1E3KzRiaUFwTjdlY3NMWHdrTWp0UFpGKzVSU0lIZWE1Ky9qSThCK1ZqWHFtMENjYm1xbzZGCjBKeldWSUZMQWdNQkFBRUNnZ0VCQUtjZWl0VlJPUVE1ZS9teFJSOUNmSzFzTkgvSDNOZTMvMVBrQjZYSXJGYWIKcUIzZXZFYXRaT3VvbjdBNk5LRWVUamwzN1NlK3BkU1ZaT1VYY3FDL0J6YnJhWnJlMytFaHJrcElqNzJBcFYrWQoyaXdaaVdWYWFKUWdJNHVaM21OQXc4UmFXSnNqNVMxYTlJOExET2lRNUlaNDVDbXZBQkRQSmVNU2N2SlN2UlJZCmU1TjBMNnN0cVM3WitJb3lHVktVZnAxaU5PMFl5eXdPVWlTa0lSWGdzY3VSWFpHWXBpR1BvbXNKK0pzMWVqelcKanlTdGxaSkVyNEwxMjg1ckdQcm1IcWpUd0ZkK2hHODBXYzQxNzl4TCtXUkU2SEJFVVpTaXk5NWZlNmtjUEhYWApCZ2lWWXRjRkttaUJpMmRUYnhsNGU5NHV0MjM5aTBIdGxKMVpKdExoK0JFQ2dZRUE4YTI5OEsyelhrSG9zeGhOCnRSckg3WGZNUFRrSEREZDNyeE0yMUxUK2ZJWHFpbkdVcDlMWWFEY2JqdVRQczhlMzN1S01kN1I2UTQweDg5eVcKSVhOa2EvVkwwUFhVZVY2N2FDVkxMcWdYRExHdWRsdUppbkgwWHZtSTBDbUJlY0ZTTXFJRm1RbGdxRVJvR0dzMwpVTWFjMHA4NzZUNFhrR1FRSmRmNjJiRnBFNFVDZ1lFQTBEQkgwUERsT3B3WGNjRGdYck1mYXlyOEhBaEkrRzVSCnlXUS8vOWlpcnRVODNjaHdJV3draDUzZUxMTXpMZ2RxSm5QaVd5YVVXNUJxem1ZdUQyM25oeGNRN1BOZElxT08KSDFzRTZ6cUxOc2h2NDZ0NVFLbGgxUTRxamQ3VXF0Z3JTclk2M1JYSkNNV1R3bk5NZURMdGo4Z2FLYmprckczUgpCTTJpbGR0NlVvOENnWUJYN05EVWxpMVNsb0gxWGxzdkQwNDdTOEZIYU03eWw5OTRGM0owVW1EZnBzemNqMVA0CjlwRjY0TW1xNC8zWXQwbGkwbU11VGIvSmdiM3hyWWdGSlhrY2VjS2FoRVZIM3JvcHVwK3Vtc0xBQUlpclVNUXEKVlNrRndKMFF0bmovZGVEVXdQTnVhT1g4Y2Q2NU81Q0ZWNnpJUjl4QkVERDhmQnNQMlpMT3ptZWZEUUtCZ1FERgptMjR2VnRoZC8xY0pkQ2dEKzBWeE5ZWERIZUlWWExGbzFTMGlMWUNOTG4zdGpabFJRQktVWHpaSmUzYXkwL3JmCnNOTkQ3YVNZSE1Za1R6eWRESmJjMVBvTnp4bXlEVWlUWHBPV3F5VUV4TS9mYkIxVlVQRTVoNDdBeHFkWjJvR04KRXRkZ2pwTVpMbUNJQzJTa0dzTCszTkpvazhVS0hkcHVFcm1tUUlNazVRS0JnUUNnRVdjWXRMWEMzWURZTUZkSQpVZ2NUZWJGcVNzM21MWWd1YjF4ZWtXM0lYUjJ5b200VjVmUVRMaUY3WWZuMmRwRFc0SWNNVTBVSkZZVnNVbGhLCmFHdGV0NFZtNU5uOCtNZ2hvdDV5QWpxTzl5QVVhdWI3d2dpZktJZTk5dFFLZDh1WnlDdkowaGh2bUREU2Z4NG0KQi9URWlGQU85OXlGNDlpU3hFVlNBUzZwcVE9PQotLS0tLUVORCBQUklWQVRFIEtFWS0tLS0t +type: kubernetes.io/tls + +--- +apiVersion: v1 +kind: Service +metadata: + name: tls-example + namespace: default +spec: + ports: + - name: http + port: 8080 + selector: + app: tls-example + type: ClusterIP + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: tls-example + namespace: default +spec: + replicas: 1 + revisionHistoryLimit: 10 + strategy: + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + type: RollingUpdate + selector: + matchLabels: + app: tls-example + template: + metadata: + labels: + app: tls-example + spec: + containers: + - name: tls-example + image: byjg/static-httpserver + ports: + - containerPort: 8080 + resources: + limits: + cpu: '0.05' + memory: '20Mi' + requests: + cpu: '0.05' + memory: '20Mi' + env: + - name: TITLE + value: "My Host Example" diff --git a/examples/swarm/docker-compose.yml b/examples/swarm/docker-compose.yml new file mode 100644 index 0000000..d9000bf --- /dev/null +++ b/examples/swarm/docker-compose.yml @@ -0,0 +1,67 @@ +# To test: +# curl -k -H "Host: host1.local" https://127.0.0.1/ +# curl -k -H "Host: host2.local" https://127.0.0.1/ +# +# curl -I -H Host:host1.local http://127.0.0.1 +# HTTP/1.1 301 Moved Permanently +# content-length: 0 +# location: https://host1.local/ +# +# curl -I -H Host:host2.local http://127.0.0.1 +# HTTP/1.1 301 Moved Permanently +# content-length: 0 +# location: https://host1.local/ +# +# Test SSL: +# openssl s_client -showcerts -connect 127.0.0.1:443 --servername host1.local + +version: "3" + +services: + haproxy: + image: byjg/easy-haproxy + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ./host2.local.pem:/certs/haproxy/host2.local.pem + deploy: + replicas: 1 + environment: + EASYHAPROXY_DISCOVER: swarm + EASYHAPROXY_SSL_MODE: "loose" + HAPROXY_CUSTOMERRORS: "true" + HAPROXY_USERNAME: admin + HAPROXY_PASSWORD: password + HAPROXY_STATS_PORT: 1936 + ports: + - "80:80/tcp" + - "443:443/tcp" + - "1936:1936/tcp" + + container: + image: byjg/static-httpserver + deploy: + replicas: 1 + labels: + easyhaproxy.http.redirect_ssl: "true" + easyhaproxy.http.host: host1.local + easyhaproxy.http.port: 80 + + easyhaproxy.https.port: 443 + easyhaproxy.https.localport: 8080 + easyhaproxy.https.host: host1.local + easyhaproxy.https.sslcert: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZEVENDQXZXZ0F3SUJBZ0lVUmkrdzFaVmdlZWRUbE5JQXdxUUJNSnY2ZFhzd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0ZqRVVNQklHQTFVRUF3d0xhRzl6ZERFdWJHOWpZV3d3SGhjTk1qRXdPREV3TVRnME9UQTJXaGNOTXpFdwpPREE0TVRnME9UQTJXakFXTVJRd0VnWURWUVFEREF0b2IzTjBNUzVzYjJOaGJEQ0NBaUl3RFFZSktvWklodmNOCkFRRUJCUUFEZ2dJUEFEQ0NBZ29DZ2dJQkFNQkRBaExBeWdKdWFXNnc2ZmZpZ3pUQUFHWHBtRXowdEl4bjFrNFoKeDV3TjVycHYvcXUwUU1ZeitBdjJ1MWVPS0VLWmVhRlJWcFQwcjkzZFg3SXZiRVpIdDI1R1BpQnZsTEdxaGpLUgpQblNrLzdVOFhtc250dFVBVjdyVkVLMVVyZEZ3OC9Jd3JpUUMrZGhyMG1uWWZTRE1rdkJvTUZwZGhWTlRyYkFaCjFUQjZyUWpFN0FyME10OG15OTZYSm13cmNqSzJUaitFMnJnUElVejFlNWNla0ZZSURTQmF0bXcrMyt2citUNXgKRk5Ga0oybzMwVzVvOFpmbENKSnpyVmFpaHFRaWNzNlpLRGdwZjdpcVhNRml3V0lsaGRRcEd2eDVHZi9LRlRLOQpVYU9uUlp6L1grMkNlYkFGYVRIUjNrL1BZcHBXVGdCQkJ1UnZscEN3K3dkbmttdGVDMFNRUkY5MVFXVnI3ZWpvCjdLYU9sR0k1VnR2TVVzV3ZUZUFabXBheW1JYUFURVR1T0phWTBKVTExT21MZUQ5RE9qNUUyU1E3cUlYL3BGY3AKeHB6RzVqNGMrTWxndnhQMlZBa05UZUFYQ2FZaVBCUUg1WlpnMEhFMlduQjFLaExSRmxIZDRpSFFEMkdKNXlOLwo2ZkNGQmZaZktTZUs4SmF1d3hnV2tyYTUzT2NEcS9tS2QrREEvZEsrL3J1Rzd0cXdWZ0lhMDRIT3Bsek03TFlSCkdCMElyczkrbHI1L1BKYlFabVUwNzNNZG42Y1hBZzNwKzZ3dndGbERrUzV2MTNnQkRZTkh0RjYyYmM1NTFlZEYKWjZrR3pKN3dtR1JvODRhQlA3TXVSWmVSZUxPclNTNjdhMXdMZHpac01uUDFUSjd4OUxmcjlNS2wydURuUWRuWQpleDhEQWdNQkFBR2pVekJSTUIwR0ExVWREZ1FXQkJTUS9tdFpkNmg4ZW45WVFWSDZITzFQbFdXaXF6QWZCZ05WCkhTTUVHREFXZ0JTUS9tdFpkNmg4ZW45WVFWSDZITzFQbFdXaXF6QVBCZ05WSFJNQkFmOEVCVEFEQVFIL01BMEcKQ1NxR1NJYjNEUUVCQ3dVQUE0SUNBUUNoUVlOdWFoMyttVHBJQkRZeEdyalRKTnVPVElNYVd6TXlpMXRrZitMMApzRUd3cGJtQU8ybVdXUVlGN1dWTHNpOThQVUxoM2FkanQyaml1ZDlWbGFhQzZnbnduNVpvMStQaWxvOXNOTExXCjZpajArck40a3dJbS9wTnFpK2pEdXUyY3ZBdUhJd1pXZWg4YkVlLzVVQ3hvNGlobVdGUU44ZUo2VFVLQ3BoUkMKNkVvci9TU1paQlFIZ1BsMEJjaHpIT2t3dTdSM0xDbmRScXhqaEFvVmI5eVFPVitac21UZUpYdWx3TnpKMXVMdApUOE9JZ0lpRHBtQm83SFNOMkgwazNjaHgwMEFzalV5SjltbUFXUGVqRmUvS1hMUlBjVlpSMTdqaHpnZklCRXpzCk01V3RXRm0xYUhEalZ2Nk02aXRlVm02MUU5VCtrL00xMXJ1MWUyWXdzeFREdmI2eDA0bWNyTnU5c29xZGRCYnIKVmZwbHV1b1EvaEVBYlh0Rk5Qb1R5U3B6MGN3T3djSENvd1ZPTG1kS2d2SW1zelppTXlISEc4VkdHbVBoODhuNwp3VnhiMGdWMFA0Uk1yY01MZGVUZG41NVlRcjFDcUJyMzRlQjZvbDZBc2JUbTNWekJIUlZtRk5rc2wxbzVKQjV0CnRYTGdGL0c4L3J6Si80bTFQYVZ1eHJCN0R4VW1JazhFUGJTSVZrdlp2ZDdMQnpLd1E2SWZWYXVjZXdIZkVhalEKVklpZXhTTWlGYzdsdzNLbnhqT0haamY2Rk05VllnM05vKytHZEM5OXM3TGtJdUp3QU1MTnFUUTdIdmhuN1l2UAo0RmxTSWdjNnhqMFlrR1pFUWxiNW8vNW5hdUVxUVUwQUJndzZqdEk0TnhyTkxUNmNwN0NPNE0weElERWcvM1lECmFBPT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQotLS0tLUJFR0lOIFBSSVZBVEUgS0VZLS0tLS0KTUlJSlF3SUJBREFOQmdrcWhraUc5dzBCQVFFRkFBU0NDUzB3Z2drcEFnRUFBb0lDQVFEQVF3SVN3TW9DYm1sdQpzT24zNG9NMHdBQmw2WmhNOUxTTVo5Wk9HY2VjRGVhNmIvNnJ0RURHTS9nTDlydFhqaWhDbVhtaFVWYVU5Sy9kCjNWK3lMMnhHUjdkdVJqNGdiNVN4cW9ZeWtUNTBwUCsxUEY1cko3YlZBRmU2MVJDdFZLM1JjUFB5TUs0a0F2blkKYTlKcDJIMGd6Skx3YURCYVhZVlRVNjJ3R2RVd2VxMEl4T3dLOURMZkpzdmVseVpzSzNJeXRrNC9oTnE0RHlGTQo5WHVYSHBCV0NBMGdXclpzUHQvcjYvaytjUlRSWkNkcU45RnVhUEdYNVFpU2M2MVdvb2FrSW5MT21TZzRLWCs0CnFsekJZc0ZpSllYVUtScjhlUm4veWhVeXZWR2pwMFdjLzEvdGdubXdCV2t4MGQ1UHoyS2FWazRBUVFia2I1YVEKc1BzSFo1SnJYZ3RFa0VSZmRVRmxhKzNvNk95bWpwUmlPVmJiekZMRnIwM2dHWnFXc3BpR2dFeEU3amlXbU5DVgpOZFRwaTNnL1F6bytSTmtrTzZpRi82UlhLY2FjeHVZK0hQakpZTDhUOWxRSkRVM2dGd21tSWp3VUIrV1dZTkJ4Ck5scHdkU29TMFJaUjNlSWgwQTloaWVjamYrbndoUVgyWHlrbml2Q1dyc01ZRnBLMnVkem5BNnY1aW5mZ3dQM1MKdnY2N2h1N2FzRllDR3RPQnpxWmN6T3kyRVJnZENLN1BmcGErZnp5VzBHWmxOTzl6SForbkZ3SU42ZnVzTDhCWgpRNUV1YjlkNEFRMkRSN1JldG0zT2VkWG5SV2VwQnN5ZThKaGthUE9HZ1QrekxrV1hrWGl6cTBrdXUydGNDM2MyCmJESno5VXllOGZTMzYvVENwZHJnNTBIWjJIc2ZBd0lEQVFBQkFvSUNBUUMveFpiWjBjY3RxYWdzcXZhVk5URWUKZXExcStoZmFHdlBFWVFhWUhJcklFKzJpNVhjbkdjTEtjS2ZvZHhEakFuOFIvemdkT3A2Y01YMENWbi9Qb2hIawpBRUR0RTgrQVZ3d0FNMUZzT3dnTEhWR2FHejhxcnhCbFlkUWdIY3BtdWVJdTJQWGJDOGVIVUJpYVVPSXVoYXc1Ci9SUk1EQUMvQWkyc3NmaTdnT2p2VkU0b1F4UVcwUUcxS0dPT0FVSm4vdVlIdzJSRlkyVXUxcGlteE8ya0RPNTMKZ2N4bUMxV09ueUNIbUhhaVcvVWg3ejZKYW1mU000ZFh0VEpac2x5aDM3ZGhIS05iZzlWa1A3Q1FLQTRoTHpvcApoYmY1cVk2cmFyZ09OaW55MUhnTVB4cm13S3VVb3VKeU90TjB5QnR4akRDVU5hWFVCd2l5N3NOR1MrSDR2c3lCCjVQOUhoSUhTdHUrRlp0M0hHN0VJcUNuZGlhU0tEUzRqV2FWUUFiYm80bloyWnMyQkQreERlUFJDUlVxWDdyTTQKNFh6UElSV1dYbW1XZi83SWcyOUhicnA0YTlMY09tUTJsZUNKdGJhVEZTTjk2T0xVSjVFK2hRMHVsQ1pnQlZtUQpSQ1VZa0pQNGxPemJhS2R6anhnSE1ySHptNDVlVUZmOExpck94aTJ1eXhYSFFtRE51NGIzWDE4a3QzUGdVbVVtCjNkWHBsM2ZxU3lKYTdTQ1Y4Wk5CcnNyRHExRSt0aFl0dTkxUWJWU0d4SGQ5SHJOVmUzWGRMYk9DZFU5Q3VDNjkKTmdsem5hYTdzWkxxbXlLZWpUZkdzWTd4cldkTmNNUGw0cDRmY0lEL080RXBBU1pmb3JwVGVLTlQwWklmWlpldwpiMG1BUWVZWnFRTThpL3FNWU4vdUFRS0NBUUVBNXFnMXNSTk1jNlZkTS90UmdsYXNHWW94amdSQzJPcUFEWmdzCm1BWE1VSjNrRXJweXh0K2VDaW15OGlidVlwelJUSVE4ZkJUV1JrQ3RSWlhKNytLY0xWdGs5UVpJb0xiaHlOd2QKNEl4RVFaRnVVbGpEYnZTalRMU3ljc0h2bzY1aWJXSWZUTDdiZ1dsTEdnR3EvVU96ZkdzZ0g2Uzl3THA1RzMwRwo4RUx5akk1ZVRJWUlDcmZUbVZMK2M0NU1ScEVNS28rY3Z6OFB5c2lhT0ZUbjNjeXN3UFZkWWFlRUVxTVFqVTh3CklHTnNHWkx5dFk3QkFCQmNZMGxkcnRiYS9PK0Z2LytSSDd1VXR6UDd4cENJd0ZDeDgwWnpOK1dSeTlOdkk2M1UKenEzeUlCb1c5R3lBcEQyK1BMYVBOeGY3UUxUVUNoWTFaei9kWVJsdEtPeHYyQWE1Z1FLQ0FRRUExV0xXTnFwMApmaEIvWnRmU0VTaHhGTU04OWNqTjZBYXoxV0tMN3VUQm91OW9TSm54amtoa2FWNzZhY25UL2lxWHR4TU5nSGkxCmZJbURwVTNQdk0wWTRVZDJUNDdvSGM2UDFCclpQTi9HbVh5L3M2QkFFZFB3TGU3Sis0blRJU0hBZEdtcmgrYS8KNXBrdHUzMmc5bFdxZnR4ZWNGSVZTTFBXa3hUMFhLaU14cDFmZmtMK09hdnBNZ01GWks0MWlLczNkTlNoS1BvZwpMOEdTUGNQOXgveW43OFAyZUszTitQR2psQTZwUHpyQU55V1U3TjAvYm1IY0I5VEtQK3VkWVdjalZocnU3TVlOCndOckU0a0tkQzh2OGk3eDd0RGJ2Yjc5VCtGbzZQSWg1M3AwT3NuWnpBOFVSMFFOUit2RFF1ZlF1eWFqOFJFQysKWkc4WXlDS3N2azh5Z3dLQ0FRQS9mc1N4QjBmL2VlRXJZeDZ3QzUzNnRlRW9ZQ0hxeHJzVGd2V2JyOVRyeUZzMQprSi95QVRMblIwMWNmYjBYNW1WemM5K1dwTUhMdXhnMzFLRXZhU2xuRHdhK3NNa2pmTlN3ejI5bUZoYmdHZUhOCngyT2RVcmoxYjdURUJJRXNoTi9SanJaaEVSVXFEY3MvMEgrNmtuMkJYWmdOUGZPQ2I1TFJMMXpPblE5YUJBTVAKZThJUStVUEZyR1FoZVdXajgxL3ZBM081N2VreUFJRDd5dHU5WWcrWVdyTW5JODhtdGo3ak40NWZEQitBOXNQYgptUDJtUDlxKzlqNVUyQTZXbkhVc1FuVTMwQktEVUVzYUFVV3o4MExaWG1adlY4SUg0eDl3S2ZVd0pCQklLQVp6CnFMN005N1k3em1Ha1gvU3BmbDMwbk9KOGxzY2hhTGQxRVlsRVphMkJBb0lCQVFDeWUyNVQ0VFY1TUpGdjB6dVoKTUd1TmcxU2MvTzRGa24yZkVVT2NlV2pod1VCSDRjUGpUL2YxRHdXRHNOYUo5TlJieENyNTkzMU9BclBEYzVjOApBNDA0K1k0ak01UkJRa0tabGk5NHRIQW9kK2pjOVVCQjZUVXZKbGw1OVNsTXdDOTY3OXdTMjFaT0tuZlBLR0NYClNzWkdRRXNaeGY2Wmhoc0hnWEozZ2wvbHpVSlBtUGVPQTVZVlIrT2Q5LzA5S0lGRlRvalNmb3luaFZDdUt4NDkKeGI0dVZZbjJIT0o0eEowZlBUZ2hkQ0hNdnJtWGVlUVJqdmI4OGVhTm1xVlVFSEhGRnRnYjRma2xBNWZFN1JUeApCaGxpUkRCd1o3YlVrSU5LNnlWazluNkJUbnM1bU12UkxtZ2RuSnBZdkU3S0MwMkxUYlpiM0krajhDMFpVYStOCnF5N0RBb0lCQUFpZXJpYlM3V1VjbDJhQmxrbTUrVzdxTm0vSU5tNXp2bm9TUG82VjN3YTVoczZmOStDL2tiZEYKODdqUVBBL1lGZTN1UjJzQUo3c2xYNWV1Wks4V21mcEZtZ3psdTBzRXo4MU1MUS9XeXBadFp5dHlWdFd6QjJQdQpYQ1cxdGRTSDllSTJCbWhYZ29rSE5UTTQ4TmsveE9FTnJQL3NlWHJJeDVMSzBobkRIWm90dS96NitZU2tCOWhGCmNtMmZaeWdEMWRNTFg2bGlSaW14eUZZK2RJQ0pOQjk1SmlmVExXWW5XZUdkZGt3UHRYVWVHWEUxb2x6dk5rTEQKek16RTA5dWhreC9sUkpudGVPQkVaYWY4ME9CLzA5T2k5YjkvcnhZNTlkd3NINkdheExvVGZFS3VQbnZCVk1OUgpZa1UxNFd6UUtsZUZraUJKSTlsVnZuZmdHbk9sZ2cwPQotLS0tLUVORCBQUklWQVRFIEtFWS0tLS0t + + container2: + image: byjg/static-httpserver + deploy: + replicas: 1 + labels: + easyhaproxy.http.host: host2.local + easyhaproxy.http.port: 80 + easyhaproxy.http.redirect_ssl: "true" + + easyhaproxy.https.port: 443 + easyhaproxy.https.localport: 8080 + easyhaproxy.https.host: host2.local + easyhaproxy.https.ssl: "true" + diff --git a/examples/swarm/host2.local.pem b/examples/swarm/host2.local.pem new file mode 100644 index 0000000..917062a --- /dev/null +++ b/examples/swarm/host2.local.pem @@ -0,0 +1,50 @@ +-----BEGIN CERTIFICATE----- +MIIDqTCCApGgAwIBAgIUId5b9t9uqH78g02EzbWF6FKVw3gwDQYJKoZIhvcNAQEL +BQAwZDELMAkGA1UEBhMCQlIxFzAVBgNVBAgMDlJpbyBkZSBKYW5laXJvMRcwFQYD +VQQHDA5SaW8gZGUgSmFuZWlybzENMAsGA1UECgwEQUNNRTEUMBIGA1UEAwwLaG9z +dDIubG9jYWwwHhcNMjIwODE1MDQyNzA1WhcNMjMwODE1MDQyNzA1WjBkMQswCQYD +VQQGEwJCUjEXMBUGA1UECAwOUmlvIGRlIEphbmVpcm8xFzAVBgNVBAcMDlJpbyBk +ZSBKYW5laXJvMQ0wCwYDVQQKDARBQ01FMRQwEgYDVQQDDAtob3N0Mi5sb2NhbDCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMSKvrOahavCXnvSF5131hpo +6k65C57jgRQ84FaDj5MbJOVlYQVFtMG0XOk7a+hh5v1fe4wH0R7I6FDo0V9sS+ss +ko5bsElc1xYlg5HbuKq89vRSKg6EDlztx3BKbi912Pmt5vFGNJ16zcw77DUrQIXo +4I/b4a3pmBiWj43NoTIrmSWHtsGwwOj3iDvSweqdYXJIr3hpHH5u6pohjDoQvqDz +K6Mu8p6mhCUKNs7KFJnNInNG25oQT6O0n4OGtmgRjLWopdEnOhMkKsfIoI1XtlXB +LBDv7huICk3t5ywtfCQyO09kX7lFIgd5rn7+MjwH5WNeqbQJxuaqjoXQnNZUgUsC +AwEAAaNTMFEwHQYDVR0OBBYEFNhMBG8q6a+iK2nECwVTn6B9EXZOMB8GA1UdIwQY +MBaAFNhMBG8q6a+iK2nECwVTn6B9EXZOMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI +hvcNAQELBQADggEBAJmudvx8+p5iIUsT8fm/fbVM0DA6qWALDYUJnTn3j6Lq4vpf +PFC+q1LmuWfBQMyqKrHrP3e493EctXoiSKZO6iN5dVJIur02OjGuiAEcsYuY1nLn +s9piiI+UEwxH6ux1NaHUnzsWauoBvRhzjXvO6SAVSZJYa9dY5mizXklDyDNuG5U0 +lXv9egMGBsy0dG6eFXkU5CPdxWU540yI2sCtSAj7z+WRUD5k7gJ7tVoY3//jHQZG +5STTmm5t9kpIZTWkptyJos9oZJFYMIXqW2Fc6tyLZpRp31R78tDs6ETIkToDc0RR +jz66th6HI+ZlgIBQhw09+hYAhBDe9+Dmd/SzQZc= +-----END CERTIFICATE----- +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDEir6zmoWrwl57 +0hedd9YaaOpOuQue44EUPOBWg4+TGyTlZWEFRbTBtFzpO2voYeb9X3uMB9EeyOhQ +6NFfbEvrLJKOW7BJXNcWJYOR27iqvPb0UioOhA5c7cdwSm4vddj5rebxRjSdes3M +O+w1K0CF6OCP2+Gt6ZgYlo+NzaEyK5klh7bBsMDo94g70sHqnWFySK94aRx+buqa +IYw6EL6g8yujLvKepoQlCjbOyhSZzSJzRtuaEE+jtJ+DhrZoEYy1qKXRJzoTJCrH +yKCNV7ZVwSwQ7+4biApN7ecsLXwkMjtPZF+5RSIHea5+/jI8B+VjXqm0Ccbmqo6F +0JzWVIFLAgMBAAECggEBAKceitVROQQ5e/mxRR9CfK1sNH/H3Ne3/1PkB6XIrFab +qB3evEatZOuon7A6NKEeTjl37Se+pdSVZOUXcqC/BzbraZre3+EhrkpIj72ApV+Y +2iwZiWVaaJQgI4uZ3mNAw8RaWJsj5S1a9I8LDOiQ5IZ45CmvABDPJeMScvJSvRRY +e5N0L6stqS7Z+IoyGVKUfp1iNO0YyywOUiSkIRXgscuRXZGYpiGPomsJ+Js1ejzW +jyStlZJEr4L1285rGPrmHqjTwFd+hG80Wc4179xL+WRE6HBEUZSiy95fe6kcPHXX +BgiVYtcFKmiBi2dTbxl4e94ut239i0HtlJ1ZJtLh+BECgYEA8a298K2zXkHosxhN +tRrH7XfMPTkHDDd3rxM21LT+fIXqinGUp9LYaDcbjuTPs8e33uKMd7R6Q40x89yW +IXNka/VL0PXUeV67aCVLLqgXDLGudluJinH0XvmI0CmBecFSMqIFmQlgqERoGGs3 +UMac0p876T4XkGQQJdf62bFpE4UCgYEA0DBH0PDlOpwXccDgXrMfayr8HAhI+G5R +yWQ//9iirtU83chwIWwkh53eLLMzLgdqJnPiWyaUW5BqzmYuD23nhxcQ7PNdIqOO +H1sE6zqLNshv46t5QKlh1Q4qjd7UqtgrSrY63RXJCMWTwnNMeDLtj8gaKbjkrG3R +BM2ildt6Uo8CgYBX7NDUli1SloH1XlsvD047S8FHaM7yl994F3J0UmDfpszcj1P4 +9pF64Mmq4/3Yt0li0mMuTb/Jgb3xrYgFJXkcecKahEVH3ropup+umsLAAIirUMQq +VSkFwJ0Qtnj/deDUwPNuaOX8cd65O5CFV6zIR9xBEDD8fBsP2ZLOzmefDQKBgQDF +m24vVthd/1cJdCgD+0VxNYXDHeIVXLFo1S0iLYCNLn3tjZlRQBKUXzZJe3ay0/rf +sNND7aSYHMYkTzydDJbc1PoNzxmyDUiTXpOWqyUExM/fbB1VUPE5h47AxqdZ2oGN +EtdgjpMZLmCIC2SkGsL+3NJok8UKHdpuErmmQIMk5QKBgQCgEWcYtLXC3YDYMFdI +UgcTebFqSs3mLYgub1xekW3IXR2yom4V5fQTLiF7Yfn2dpDW4IcMU0UJFYVsUlhK +aGtet4Vm5Nn8+Mghot5yAjqO9yAUaub7wgifKIe99tQKd8uZyCvJ0hhvmDDSfx4m +B/TEiFAO99yF49iSxEVSAS6pqQ== +-----END PRIVATE KEY----- \ No newline at end of file diff --git a/helm/easyhaproxy/.helmignore b/helm/easyhaproxy/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/helm/easyhaproxy/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/helm/easyhaproxy/Chart.yaml b/helm/easyhaproxy/Chart.yaml new file mode 100644 index 0000000..dcbbe33 --- /dev/null +++ b/helm/easyhaproxy/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: easyhaproxy +description: "EasyHAProxy - A service discovery backed on HAProxy" + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.3 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "master" diff --git a/helm/easyhaproxy/templates/_helpers.tpl b/helm/easyhaproxy/templates/_helpers.tpl new file mode 100644 index 0000000..d84556c --- /dev/null +++ b/helm/easyhaproxy/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "easyhaproxy.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "easyhaproxy.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "easyhaproxy.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "easyhaproxy.labels" -}} +helm.sh/chart: {{ include "easyhaproxy.chart" . }} +{{ include "easyhaproxy.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "easyhaproxy.selectorLabels" -}} +app.kubernetes.io/name: {{ include "easyhaproxy.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "easyhaproxy.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "easyhaproxy.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/helm/easyhaproxy/templates/clusterrole.yaml b/helm/easyhaproxy/templates/clusterrole.yaml new file mode 100644 index 0000000..04ac23e --- /dev/null +++ b/helm/easyhaproxy/templates/clusterrole.yaml @@ -0,0 +1,67 @@ +{{- if .Values.serviceAccount.create -}} +--- +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: {{ include "easyhaproxy.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "easyhaproxy.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +rules: +- apiGroups: + - "" + resources: + # - configmaps + # - endpoints + # - nodes + - pods + - services + - namespaces + # - events + - serviceaccounts + verbs: + - get + - list + - watch +- apiGroups: + - "extensions" + - "networking.k8s.io" + resources: + - ingresses + # - ingresses/status + # - ingressclasses + verbs: + - get + - list + - watch +# - apiGroups: +# - "extensions" +# - "networking.k8s.io" +# resources: +# - ingresses/status +# verbs: +# - update +- apiGroups: + - "" + resources: + - secrets + verbs: + - get + - list + # - watch + # - create + # - patch + # - update +# - apiGroups: +# - "discovery.k8s.io" +# resources: +# - endpointslices +# verbs: +# - get +# - list +# - watch +{{- end }} \ No newline at end of file diff --git a/helm/easyhaproxy/templates/clusterrolebinding.yaml b/helm/easyhaproxy/templates/clusterrolebinding.yaml new file mode 100644 index 0000000..d936de9 --- /dev/null +++ b/helm/easyhaproxy/templates/clusterrolebinding.yaml @@ -0,0 +1,22 @@ +{{- if .Values.serviceAccount.create -}} +--- +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: {{ include "easyhaproxy.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "easyhaproxy.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "easyhaproxy.fullname" . }} +subjects: +- kind: ServiceAccount + name: {{ include "easyhaproxy.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +{{- end }} diff --git a/helm/easyhaproxy/templates/deployment.yaml b/helm/easyhaproxy/templates/deployment.yaml new file mode 100644 index 0000000..86fb22c --- /dev/null +++ b/helm/easyhaproxy/templates/deployment.yaml @@ -0,0 +1,71 @@ +--- +apiVersion: apps/v1 +kind: {{ ternary "Deployment" "DaemonSet" .Values.service.create }} +metadata: + name: {{ include "easyhaproxy.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "easyhaproxy.labels" . | nindent 4 }} +spec: + selector: + matchLabels: + {{- include "easyhaproxy.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "easyhaproxy.selectorLabels" . | nindent 8 }} + spec: + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: {{ .Values.masterNode.label }} + operator: In + values: + {{- toYaml .Values.masterNode.values | nindent 18 }} + serviceAccountName: {{ include "easyhaproxy.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: 80 + {{ if not .Values.service.create }}hostPort: {{ .Values.binding.ports.http }}{{ end }} + - name: https + containerPort: 443 + {{ if not .Values.service.create }}hostPort: {{ .Values.binding.ports.https }}{{ end }} + - name: stats + containerPort: 1936 + {{ if not .Values.service.create }}hostPort: {{ .Values.binding.ports.stats }}{{ end }} + {{- range $port := .Values.binding.additionalPorts }} + - name: extra{{ $port }} + containerPort: {{ $port }} + {{ if not $.Values.service.create }}hostPort: {{ $port }}{{ end }} + {{- end }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + env: + - name: EASYHAPROXY_DISCOVER + value: kubernetes + - name: HAPROXY_USERNAME + value: {{ .Values.easyhaproxy.stats.username }} + - name: HAPROXY_PASSWORD + value: {{ .Values.easyhaproxy.stats.password }} + - name: EASYHAPROXY_REFRESH_CONF + value: {{ .Values.easyhaproxy.refresh | quote }} + - name: HAPROXY_CUSTOMERRORS + value: {{ .Values.easyhaproxy.customErrors | quote}} + - name: EASYHAPROXY_SSL_MODE + value: {{ .Values.easyhaproxy.sslMode }} + - name: EASYHAPROXY_LOG_LEVEL + value: {{ .Values.easyhaproxy.logLevel.easyhaproxy }} + - name: HAPROXY_LOG_LEVEL + value: {{ .Values.easyhaproxy.logLevel.haproxy }} + - name: CERTBOT_LOG_LEVEL + value: {{ .Values.easyhaproxy.logLevel.certbot }} diff --git a/helm/easyhaproxy/templates/service.yaml b/helm/easyhaproxy/templates/service.yaml new file mode 100644 index 0000000..81b2985 --- /dev/null +++ b/helm/easyhaproxy/templates/service.yaml @@ -0,0 +1,34 @@ +{{ if .Values.service.create }} +--- +apiVersion: v1 +kind: Service +metadata: +metadata: + name: {{ include "easyhaproxy.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "easyhaproxy.labels" . | nindent 4 }} + annotations: + {{- toYaml .Values.service.annotations | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - name: http + port: 80 + {{ if eq .Values.service.type "NodePort" }}nodePort: {{ .Values.binding.ports.http }}{{ end }} + - name: https + port: 443 + {{ if eq .Values.service.type "NodePort" }}nodePort: {{ .Values.binding.ports.https }}{{ end }} + - name: stats + port: 1936 + {{ if eq .Values.service.type "NodePort" }}nodePort: {{ .Values.binding.ports.stats }}{{ end }} + {{- range $port := .Values.binding.additionalPorts }} + - name: extra{{ $port }} + port: {{ $port }} + {{ if eq $.Values.service.type "NodePort" }}nodePort: {{ $port }}{{ end }} + {{- end }} + + + selector: + {{- include "easyhaproxy.selectorLabels" . | nindent 4 }} +{{ end }} \ No newline at end of file diff --git a/helm/easyhaproxy/templates/serviceaccount.yaml b/helm/easyhaproxy/templates/serviceaccount.yaml new file mode 100644 index 0000000..5182461 --- /dev/null +++ b/helm/easyhaproxy/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "easyhaproxy.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "easyhaproxy.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/helm/easyhaproxy/values.schema.json b/helm/easyhaproxy/values.schema.json new file mode 100644 index 0000000..9018523 --- /dev/null +++ b/helm/easyhaproxy/values.schema.json @@ -0,0 +1,19 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "required": [], + "properties": { + "service": { + "type": "object", + "required": [], + "properties": { + "create": { + "type": "boolean" + }, + "type": { + "type": "string", + "enum": ["ClusterIP", "NodePort"] + } + } + } + } +} \ No newline at end of file diff --git a/helm/easyhaproxy/values.yaml b/helm/easyhaproxy/values.yaml new file mode 100644 index 0000000..9f1b413 --- /dev/null +++ b/helm/easyhaproxy/values.yaml @@ -0,0 +1,77 @@ +# Default values for easyhaproxy. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: byjg/easy-haproxy + pullPolicy: Always # IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +service: + create: false # If false, it will create a Daemonset with hostPort. The easiest. + type: ClusterIP # or NodePort + annotations: {} + +binding: + ports: + http: 80 + https: 443 + stats: 1936 + additionalPorts: [] + +serviceAccount: + create: true + annotations: {} + name: "" + +podAnnotations: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +resources: {} +# requests: +# cpu: "100m" +# memory: "128Mi" +# limits: +# cpu: 100m +# memory: 128Mi + +nodeSelector: {} + +tolerations: [] + +affinity: {} + +easyhaproxy: + stats: + username: admin + password: password + refresh: "10" + customErrors: "true" + sslMode: loose + logLevel: + certbot: DEBUG + easyhaproxy: DEBUG + haproxy: DEBUG + +# Make sure to create this +masterNode: + label: easyhaproxy/node + values: + - master diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 364b4b7..0000000 --- a/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -pyyaml -docker -jinja2 -pytest \ No newline at end of file diff --git a/easymapping/__init__.py b/src/easymapping/__init__.py similarity index 79% rename from easymapping/__init__.py rename to src/easymapping/__init__.py index a10fa2f..6ad8af7 100644 --- a/easymapping/__init__.py +++ b/src/easymapping/__init__.py @@ -27,7 +27,7 @@ class DockerLabelHandler: def get_bool(self, label, default_value = False): if self.has_label(label): - return self.__data[label].lower() in ["True", "true", "1", "yes"] + return self.__data[label].lower() in ["true", "1", "yes"] return default_value def get_json(self, label, default_value = {}): @@ -46,24 +46,21 @@ class DockerLabelHandler: class HaproxyConfigGenerator: - def __init__(self, mapping, ssl_cert_folder="/certs"): + def __init__(self, mapping): self.mapping = mapping self.mapping.setdefault("ssl_mode", 'default') self.mapping.setdefault("letsencrypt", {"email": ""}) self.mapping["ssl_mode"] = self.mapping["ssl_mode"].lower() self.label = DockerLabelHandler(mapping['lookup_label'] if 'lookup_label' in mapping else "easyhaproxy") - self.ssl_cert_haproxy = ssl_cert_folder + "/haproxy" - self.ssl_cert_letsecncrypt = ssl_cert_folder + "/letsencrypt" self.letsencrypt_hosts = [] - os.makedirs(self.ssl_cert_haproxy, exist_ok=True) - os.makedirs(self.ssl_cert_letsecncrypt, exist_ok=True) + self.serving_hosts = [] + self.certs = {} - def generate(self, line_list = []): + def generate(self, container_metadata = {}): self.mapping.setdefault("easymapping", []) - # static? - if len(line_list) > 0: - self.mapping["easymapping"] = self.parse(line_list) + if container_metadata != {}: + self.mapping["easymapping"] = self.parse(container_metadata) file_loader = FileSystemLoader('templates') env = Environment(loader=file_loader) @@ -74,15 +71,11 @@ class HaproxyConfigGenerator: return template.render(data=self.mapping) - def parse(self, line_list): + def parse(self, container_metadata): easymapping = dict() - for line in line_list: - line = line.strip() - i = line.find("=") - container = line[:i] - json_str = line[i+1:] - d = json.loads(json_str) + for container in container_metadata: + d = container_metadata[container] # Extract the definitions dynamically definitions = {} @@ -97,7 +90,7 @@ class HaproxyConfigGenerator: self.label.set_data(d) # Parse each definition found. - for definition in definitions.keys(): + for definition in sorted(definitions.keys()): mode = self.label.get( self.label.create([definition, "mode"]), "http" @@ -117,6 +110,9 @@ class HaproxyConfigGenerator: self.label.create([definition, "letsencrypt"]), False ) and self.mapping["letsencrypt"]["email"] != "" + clone_to_ssl = self.label.get_bool( + self.label.create([definition, "clone_to_ssl"]) + ) if port not in easymapping: easymapping[port] = { @@ -138,8 +134,9 @@ class HaproxyConfigGenerator: "" ) - for hostname in d[host_label].split(","): + for hostname in sorted(d[host_label].split(",")): hostname = hostname.strip() + self.serving_hosts.append("%s:%s" % (hostname, port)) easymapping[port]["hosts"].setdefault(hostname, {}) easymapping[port]["hosts"][hostname].setdefault("containers", []) easymapping[port]["hosts"][hostname].setdefault("letsencrypt", False) @@ -153,7 +150,7 @@ class HaproxyConfigGenerator: self.label.create([definition, "redirect"]) ) - if letsencrypt: + if letsencrypt or clone_to_ssl: if "443" not in easymapping: easymapping["443"] = { "mode": "http", @@ -166,21 +163,17 @@ class HaproxyConfigGenerator: easymapping["443"]["hosts"][hostname]["letsencrypt"] = False easymapping["443"]["hosts"][hostname]["redirect_ssl"] = False easymapping["443"]["ssl"] = True - self.letsencrypt_hosts.append(hostname) if hostname not in self.letsencrypt_hosts else self.letsencrypt_hosts + self.letsencrypt_hosts.append(hostname) if letsencrypt and hostname not in self.letsencrypt_hosts else self.letsencrypt_hosts # handle SSL ssl_label = self.label.create([definition, "sslcert"]) if self.label.has_label(ssl_label): - filename = "{}/{}.pem".format( - self.ssl_cert_haproxy, d[host_label] - ) - easymapping[port]["ssl"] = True - with open(filename, 'wb') as file: - file.write( - base64.b64decode(d[ssl_label]) - ) + filename = "{}.pem".format(d[host_label]) + easymapping[port]["ssl"] = True if not clone_to_ssl else False + self.certs[filename] = base64.b64decode(d[ssl_label]).decode('ascii') + if self.label.get_bool(self.label.create([definition, "ssl"])): - easymapping[port]["ssl"] = True + easymapping[port]["ssl"] = True if not clone_to_ssl else False return easymapping.values() diff --git a/src/functions/__init__.py b/src/functions/__init__.py new file mode 100644 index 0000000..4267f73 --- /dev/null +++ b/src/functions/__init__.py @@ -0,0 +1,232 @@ +from datetime import datetime +from multiprocessing import Process, Lock +import subprocess +import shlex +import time +import os + +class Functions: + HAPROXY_LOG="HAPROXY" + EASYHAPROXY_LOG="EASYHAPROXY" + CERTBOT_LOG="CERTBOT" + INIT_LOG="INIT" + + TRACE = "TRACE" + DEBUG = "DEBUG" + INFO = "INFO" + WARN = "WARN" + ERROR = "ERROR" + FATAL = "FATAL" + + debug_log = None + + @staticmethod + def skip_log(source, log_level_str): + level = os.getenv("%s_LOG_LEVEL" % (source.upper()), "").upper() + level_importance = { + Functions.TRACE: 0, + Functions.DEBUG: 1, + Functions.INFO: 2, + Functions.WARN: 3, + Functions.ERROR: 4, + Functions.FATAL: 5 + } + level_required = 1 if level not in level_importance else level_importance[level] + level_asked = 1 if log_level_str.upper() not in level_importance else level_importance[log_level_str.upper()] + return level_asked < level_required + + @staticmethod + def load(filename): + with open(filename, 'r') as content_file: + return content_file.read() + + @staticmethod + def save(filename, contents): + with open(filename, 'w') as file: + file.write(contents) + + @staticmethod + def log(source, level, message): + if message is None or message == "": + return + + if Functions.skip_log(source, level): + return + + if not isinstance(message, (list, tuple)): + message = [message] + + for line in message: + log = "[%s] %s [%s]: %s" % (source, datetime.now().strftime("%x %X"), level, line.rstrip()) + print(log) + if Functions.debug_log is not None: + Functions.debug_log.append(log) + + @staticmethod + def run_bash(source, command, log_output=True, return_result=True): + if not isinstance(command, (list, tuple)): + command = shlex.split(command) + + try: + process = subprocess.Popen(command, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True) + + output = [] + + while True: + line = process.stdout.readline().rstrip() + output.append(line) if return_result else None + Functions.log(source, Functions.INFO, line) if log_output else None + Functions.log(source, Functions.WARN, process.stderr.readline()) + return_code = process.poll() + if return_code is not None: + lines = [] + for line in process.stdout.readlines(): + output.append(line.rstrip()) if return_result else None + lines.append(line.rstrip()) + Functions.log(source, Functions.INFO, lines) if log_output else None + Functions.log(source, Functions.WARN, process.stderr.readlines()) + break + + return output + except Exception as e: + Functions.log(source, Functions.ERROR, "%s" % (e)) + + +class Consts: + easyhaproxy_config = "/etc/haproxy/easyconfig.yml" + haproxy_config = "/etc/haproxy/haproxy.cfg" + certs_letsencrypt = "/certs/letsencrypt" + certs_haproxy = "/certs/haproxy" + +class DaemonizeHAProxy: + def __init__(self): + self.process = None + self.thread = None + + def haproxy(self, action): + if action == "start": + self.__prepare("/usr/sbin/haproxy -W -f /etc/haproxy/haproxy.cfg -p /run/haproxy.pid -S /var/run/haproxy.sock") + else: + pid = "".join(Functions().run_bash(Functions.HAPROXY_LOG, "cat /run/haproxy.pid", log_output=False)) + self.__prepare("/usr/sbin/haproxy -W -f /etc/haproxy/haproxy.cfg -p /run/haproxy.pid -x /var/run/haproxy.sock -sf %s" % (pid)) + + if self.process is None: + return + + self.thread = Process(target=self.__start, args=()) + self.thread.start() + + def __prepare(self, command): + source = Functions.HAPROXY_LOG + if not isinstance(command, (list, tuple)): + command = shlex.split(command) + + try: + self.process = subprocess.Popen(command, + shell=False, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + bufsize=-1, + universal_newlines=True) + + except Exception as e: + Functions.log(source, Functions.ERROR, "%s" % (e)) + + + def __start(self): + source = Functions.HAPROXY_LOG + try: + with self.process.stdout: + for line in iter(self.process.stdout.readline, b''): + Functions.log(source, Functions.INFO, line) + + returncode = self.process.wait() + Functions.log(source, Functions.DEBUG, "Return code %s" % (returncode)) + + except Exception as e: + Functions.log(source, Functions.ERROR, "%s" % (e)) + + def is_alive(self): + return self.thread.is_alive() + + def kill(self): + self.process.kill() + self.thread.kill() + + def terminate(self): + self.process.terminate() + self.thread.terminate() + + +class Certbot: + def __init__(self, certs, email): + self.certs = certs + self.email = email + + def check_certificates(self, hosts): + if self.email == "" or len(hosts) == 0: + return False + + try: + request_certs = [] + renew_certs = [] + current_time = time.time() + for host in hosts: + filename = "%s/%s.pem" % (self.certs, host) + host_arg = '-d %s' % (host) + if not os.path.exists(filename): + Functions.log(Functions.CERTBOT_LOG, Functions.DEBUG, "Request new certificate for %s" % (host)) + request_certs.append(host_arg) + else: + creation_time = os.path.getctime(filename) + if (current_time - creation_time) // (24 * 3600) > 90: + Functions.log(Functions.CERTBOT_LOG, Functions.DEBUG, "Request expired certificate for %s" % (host)) + request_certs.append(host_arg) + if (current_time - creation_time) // (24 * 3600) >= 45: + Functions.log(Functions.CERTBOT_LOG, Functions.DEBUG, "Renew certificate for %s" % (host)) + renew_certs.append(host_arg) + + certbot_certonly = ('/usr/bin/certbot certonly ' + ' --standalone' + ' --preferred-challenges http' + ' --http-01-port 2080' + ' --agree-tos' + ' --issuance-timeout 90' + ' --no-eff-email' + ' --non-interactive' + ' --max-log-backups=0' + ' %s --email %s' % (' '.join(request_certs), self.email) + ) + + ret_reload = False + if len(request_certs) > 0: + Functions.run_bash(Functions.CERTBOT_LOG, certbot_certonly, return_result=False) + ret_reload = True + + if len(renew_certs) > 0: + Functions.run_bash(Functions.CERTBOT_LOG, "/usb/bin/certbot renew", return_result=False) + ret_reload = True + + if ret_reload: + self.find_live_certificates() + + return ret_reload + except Exception as e: + Functions.log(Functions.CERTBOT_LOG, Functions.ERROR, "%s" % (e)) + return False + + def merge_certificate(self, cert, key, filename): + Functions.save(filename, cert + key) + + def find_live_certificates(self): + letsencrypt_certs = "/etc/letsencrypt/live/" + for item in os.listdir(letsencrypt_certs): + path = os.path.join(letsencrypt_certs, item) + if os.path.isdir(path): + cert = Functions.load(os.path.join(path, "fullchain.pem")) + key = Functions.load(os.path.join(path, "privkey.pem")) + filename = "%s/%s.pem" % (self.certs, item) + self.merge_certificate(cert, key, filename) diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..bb5305f --- /dev/null +++ b/src/main.py @@ -0,0 +1,73 @@ +from functions import Functions, DaemonizeHAProxy, Certbot, Consts +from processor import ProcessorInterface +import os +import time +from deepdiff import DeepDiff + +def start(): + processor_obj = ProcessorInterface.factory(os.getenv("EASYHAPROXY_DISCOVER")) + if processor_obj is None: + exit(1) + + os.makedirs(Consts.certs_letsencrypt, exist_ok=True) + os.makedirs(Consts.certs_haproxy, exist_ok=True) + + processor_obj.save_config(Consts.haproxy_config) + processor_obj.save_certs(Consts.certs_haproxy) + letsencrypt_certs_found = processor_obj.get_letsencrypt_hosts() + Functions.log(Functions.EASYHAPROXY_LOG, Functions.DEBUG, 'Found hosts: %s' % ", ".join(processor_obj.get_hosts())) # Needs to after save_config + Functions.log(Functions.EASYHAPROXY_LOG, Functions.TRACE, 'Object Found: %s' % (processor_obj.get_parsed_object())) + + old_haproxy = None + haproxy = DaemonizeHAProxy() + haproxy.haproxy("start") + + certbot = Certbot(Consts.certs_letsencrypt, os.getenv("EASYHAPROXY_LETSENCRYPT_EMAIL")) + + while True: + time.sleep(10) + if old_haproxy is not None: + old_haproxy.kill() + old_haproxy = None + try: + old_parsed = processor_obj.get_parsed_object() + processor_obj.refresh() + if DeepDiff(old_parsed, processor_obj.get_parsed_object()) != {} or not haproxy.is_alive(): + Functions.log(Functions.EASYHAPROXY_LOG, Functions.DEBUG, 'New configuration found. Reloading...') + Functions.log(Functions.EASYHAPROXY_LOG, Functions.TRACE, 'Object Found: %s' % (processor_obj.get_parsed_object())) + processor_obj.save_config(Consts.haproxy_config) + processor_obj.save_certs(Consts.certs_haproxy) + letsencrypt_certs_found = processor_obj.get_letsencrypt_hosts() + Functions.log(Functions.EASYHAPROXY_LOG, Functions.DEBUG, 'Found hosts: %s' % ", ".join(processor_obj.get_hosts())) # Needs to after save_config + old_haproxy = haproxy + haproxy = DaemonizeHAProxy() + haproxy.haproxy("reload") + old_haproxy.terminate() + + certbot.check_certificates(letsencrypt_certs_found) + except Exception as e: + Functions.log(Functions.EASYHAPROXY_LOG, Functions.FATAL, "Err: %s" % (e)) + Functions.log(Functions.EASYHAPROXY_LOG, Functions.DEBUG, 'Heartbeat') + + + + +def main(): + Functions.run_bash(Functions.INIT_LOG, '/usr/sbin/haproxy -v') + + Functions.log(Functions.INIT_LOG, Functions.INFO, " _ ") + Functions.log(Functions.INIT_LOG, Functions.INFO, " ___ __ _ ____ _ ___| |_ __ _ _ __ _ _ _____ ___ _ ") + Functions.log(Functions.INIT_LOG, Functions.INFO, "/ -_) _` (_-< || |___| ' \/ _` | '_ \ '_/ _ \ \ / || |") + Functions.log(Functions.INIT_LOG, Functions.INFO, "\___\__,_/__/\_, | |_||_\__,_| .__/_| \___/_\_\\_, |") + Functions.log(Functions.INIT_LOG, Functions.INFO, " |__/ |_| |__/ ") + + Functions.log(Functions.INIT_LOG, Functions.INFO, "Release: %s" % (os.getenv("RELEASE_VERSION"))) + Functions.log(Functions.INIT_LOG, Functions.DEBUG, 'Environment:') + for name, value in os.environ.items(): + if "HAPROXY" in name: + Functions.log(Functions.INIT_LOG, Functions.DEBUG, "- {0}: {1}".format(name, value)) + + start() + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/src/processor/__init__.py b/src/processor/__init__.py new file mode 100644 index 0000000..eacebbd --- /dev/null +++ b/src/processor/__init__.py @@ -0,0 +1,236 @@ +from easymapping import HaproxyConfigGenerator +from functions import Functions, Consts +import yaml +import sys +import os +import json +import base64 +import docker +from kubernetes import client, config +from kubernetes.client.rest import ApiException + +class ContainerEnv: + @staticmethod + def read(): + env_vars = { + "customerrors": True if os.getenv("HAPROXY_CUSTOMERRORS") == "true" else False, + "ssl_mode": os.getenv("EASYHAPROXY_SSL_MODE").lower() if os.getenv("EASYHAPROXY_SSL_MODE") else 'default' + } + + if os.getenv("HAPROXY_PASSWORD"): + env_vars["stats"] = { + "username": os.getenv("HAPROXY_USERNAME") if os.getenv("HAPROXY_USERNAME") else "admin", + "password": os.getenv("HAPROXY_PASSWORD"), + "port": os.getenv("HAPROXY_STATS_PORT") if os.getenv("HAPROXY_STATS_PORT") else "1936", + } + + env_vars["lookup_label"] = os.getenv("EASYHAPROXY_LABEL_PREFIX") if os.getenv("EASYHAPROXY_LABEL_PREFIX") else "easyhaproxy" + if (os.getenv("EASYHAPROXY_LETSENCRYPT_EMAIL")): + env_vars["letsencrypt"] = { + "email": os.getenv("EASYHAPROXY_LETSENCRYPT_EMAIL") + } + + return env_vars + + +class ProcessorInterface: + static_file = "/etc/haproxy/easyconfig.yml" + + def __init__(self, filename = None): + self.filename = filename + self.refresh() + + @staticmethod + def factory(mode): + if mode == "static": + return Static(ProcessorInterface.static_file) + elif mode == "docker": + return Docker() + elif mode == "swarm": + return Swarm() + elif mode == "kubernetes": + return Kubernetes() + else: + Functions.log("EASYHAPROXY", Functions.FATAL, "Expected mode to be 'static', 'docker', 'swarm' or 'kubernetes'. I got '%s'" % (mode)) + return None + + def refresh(self): + self.letsencrypt_hosts = None + self.parsed_object = None + self.cfg = None + self.hosts = None + self.inspect_network() + self.parse() + + def inspect_network(self): + #Abstract + pass + + def parse(self): + self.cfg = HaproxyConfigGenerator(ContainerEnv.read()) + + def get_letsencrypt_hosts(self): + return self.letsencrypt_hosts + + def get_hosts(self): + return self.hosts + + def get_parsed_object(self): + return self.parsed_object + + def get_certs(self, key = None): + if key is None: + return self.cfg.certs + else: + return None if key not in self.cfg.certs else self.cfg.certs[key] + + def get_haproxy_conf(self): + conf = self.cfg.generate(self.parsed_object) + self.letsencrypt_hosts = self.cfg.letsencrypt_hosts + self.hosts = self.cfg.serving_hosts + return conf + + def save_config(self, filename): + Functions.save(filename, self.get_haproxy_conf()) + + def save_certs(self, path): + for cert in self.get_certs(): + Functions.save("{0}/{1}".format(path, cert), self.get_certs(cert)) + + +class Static(ProcessorInterface): + def inspect_network(self): + self.parsed_object = {} + self.static_content = None + + def get_parsed_object(self): + return self.static_content["easymapping"] if "easymapping" in self.static_content else [] + + def get_hosts(self): + hosts = [] + for object in self.get_parsed_object(): + for host in object["hosts"].keys(): + hosts.append("%s:%s" % (host, object["port"])) + return hosts + + def parse(self): + self.static_content = yaml.load(Functions.load(self.filename), Loader=yaml.FullLoader) + self.cfg = HaproxyConfigGenerator(self.static_content) + + +class Docker(ProcessorInterface): + def __init__(self, filename = None): + self.client = docker.from_env() + super().__init__() + + def inspect_network(self): + self.parsed_object = {} + for container in self.client.containers.list(): + self.parsed_object[container.name] = container.labels + + +class Swarm(ProcessorInterface): + def __init__(self, filename = None): + self.client = docker.from_env() + super().__init__() + + def inspect_network(self): + self.parsed_object = {} + for container in self.client.services.list(): + self.parsed_object[container.attrs["Spec"]["Name"]] = container.attrs["Spec"]["Labels"] + + +class Kubernetes(ProcessorInterface): + def __init__(self, filename = None): + config.load_incluster_config() + config.verify_ssl = False + self.api_instance = client.CoreV1Api() + self.v1 = client.NetworkingV1Api() + self.cert_cache = {} + super().__init__() + + def _check_annotation(self, annotations, key): + if key not in annotations: + return None + return annotations[key] + + def inspect_network(self): + + ret = self.v1.list_ingress_for_all_namespaces(watch=False) + + self.parsed_object = {} + for ingress in ret.items: + if ingress.metadata.annotations['kubernetes.io/ingress.class'] != "easyhaproxy-ingress": + continue + + ssl_hosts = [] + + letsencrypt = self._check_annotation(ingress.metadata.annotations, "easyhaproxy.letsencrypt") + redirect_ssl = self._check_annotation(ingress.metadata.annotations, "easyhaproxy.redirect_ssl") + redirect = self._check_annotation(ingress.metadata.annotations, "easyhaproxy.redirect") + mode = self._check_annotation(ingress.metadata.annotations, "easyhaproxy.mode") + listen_port = self._check_annotation(ingress.metadata.annotations, "easyhaproxy.listen_port") + if listen_port is None: + listen_port = 80 + + data = {} + data["creation_timestamp"] = ingress.metadata.creation_timestamp.strftime("%x %X") + data["resource_version"] = ingress.metadata.resource_version + data["namespace"] = ingress.metadata.namespace + + ingress_name = ingress.metadata.namespace + + if ingress.spec.tls is not None: + for tls in ingress.spec.tls: + try: + secret = self.api_instance.read_namespaced_secret(tls.secret_name, ingress.metadata.namespace) + if "tls.crt" not in secret.data or "tls.key" not in secret.data: + continue + + if tls.secret_name not in self.cert_cache or self.cert_cache[tls.secret_name] != secret.data: + self.cert_cache[tls.secret_name] = secret.data + Functions.save( + "{0}/{1}.pem".format(Consts.certs_haproxy, tls.secret_name), + base64.b64decode(secret.data["tls.crt"]).decode('ascii') + "\n" + base64.b64decode(secret.data["tls.key"]).decode('ascii') + ) + + ssl_hosts.extend(tls.hosts) + except Exception as e: + Functions.log("EASYHAPROXY", Functions.WARN, "Ingress %s - Get secret failed: '%s'" % (ingress_name, e)) + + Functions.log("EASYHAPROXY", Functions.TRACE, "Ingress %s - SSL Hosts found '%s'" % (ingress_name, ssl_hosts)) + + for rule in ingress.spec.rules: + rule_data = {} + port_number = rule.http.paths[0].backend.service.port.number + definition = "easyhaproxy.%s_%s" % (rule.host.replace(".", "-"), port_number) + rule_data["%s.host" % (definition)] = rule.host + rule_data["%s.port" % (definition)] = listen_port + rule_data["%s.localport" % (definition)] = port_number + if rule.host in ssl_hosts: + rule_data["%s.clone_to_ssl" % (definition)] = 'true' + if redirect_ssl is not None: + rule_data["%s.redirect_ssl" % (definition)] = redirect_ssl + if letsencrypt is not None: + rule_data["%s.letsencrypt" % (definition)] = letsencrypt + if redirect is not None: + rule_data["%s.redirect" % (definition)] = redirect + if mode is not None: + rule_data["%s.mode" % (definition)] = mode + + service_name = rule.http.paths[0].backend.service.name + try: + api_response = self.api_instance.read_namespaced_service(service_name, ingress.metadata.namespace) + cluster_ip = api_response.spec.cluster_ip + except ApiException as e: + cluster_ip = None + Functions.log("EASYHAPROXY", Functions.WARN, "Ingress %s - Service %s - Failed: '%s'" % (ingress_name, service_name, e)) + + if cluster_ip is not None: + if cluster_ip not in self.parsed_object.keys(): + self.parsed_object[cluster_ip] = data + self.parsed_object[cluster_ip].update(rule_data) + + + + diff --git a/pytest.ini b/src/pytest.ini similarity index 100% rename from pytest.ini rename to src/pytest.ini diff --git a/src/requirements.txt b/src/requirements.txt new file mode 100644 index 0000000..5c4ba98 --- /dev/null +++ b/src/requirements.txt @@ -0,0 +1,7 @@ +pyyaml +docker +jinja2 +pytest +docker +kubernetes +deepdiff \ No newline at end of file diff --git a/templates/bind.j2 b/src/templates/bind.j2 similarity index 100% rename from templates/bind.j2 rename to src/templates/bind.j2 diff --git a/templates/frontend-mode-http.j2 b/src/templates/frontend-mode-http.j2 similarity index 100% rename from templates/frontend-mode-http.j2 rename to src/templates/frontend-mode-http.j2 diff --git a/templates/frontend-mode-tcp.j2 b/src/templates/frontend-mode-tcp.j2 similarity index 100% rename from templates/frontend-mode-tcp.j2 rename to src/templates/frontend-mode-tcp.j2 diff --git a/templates/haproxy.cfg.j2 b/src/templates/haproxy.cfg.j2 similarity index 99% rename from templates/haproxy.cfg.j2 rename to src/templates/haproxy.cfg.j2 index eda4b8a..a1842b1 100644 --- a/templates/haproxy.cfg.j2 +++ b/src/templates/haproxy.cfg.j2 @@ -78,3 +78,4 @@ backend srv_{{ host }} backend letsencrypt_backend mode http server certbot 127.0.0.1:2080 + diff --git a/templates/ssl_default.j2 b/src/templates/ssl_default.j2 similarity index 100% rename from templates/ssl_default.j2 rename to src/templates/ssl_default.j2 diff --git a/templates/ssl_loose.j2 b/src/templates/ssl_loose.j2 similarity index 100% rename from templates/ssl_loose.j2 rename to src/templates/ssl_loose.j2 diff --git a/templates/ssl_strict.j2 b/src/templates/ssl_strict.j2 similarity index 100% rename from templates/ssl_strict.j2 rename to src/templates/ssl_strict.j2 diff --git a/tests/__init__.py b/src/tests/__init__.py similarity index 100% rename from tests/__init__.py rename to src/tests/__init__.py diff --git a/tests/context.py b/src/tests/context.py similarity index 100% rename from tests/context.py rename to src/tests/context.py diff --git a/src/tests/expected/docker.txt b/src/tests/expected/docker.txt new file mode 100644 index 0000000..7bcdedc --- /dev/null +++ b/src/tests/expected/docker.txt @@ -0,0 +1,101 @@ +global + log stdout format raw local0 info + maxconn 2000 + tune.ssl.default-dh-param 2048 + + # intermediate configuration + ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384 + ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 + ssl-default-bind-options prefer-client-ciphers no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets + + ssl-default-server-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384 + ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 + ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets + + ssl-dh-param-file /etc/haproxy/dhparam + +defaults + log global + option httplog + + timeout connect 3s + timeout client 10s + timeout server 10m + +frontend stats + bind *:1936 + mode http + stats enable + stats hide-version + stats realm Haproxy\ Statistics + stats uri / + default_backend srv_stats + +backend srv_stats + mode http + server Local 127.0.0.1:1936 + +frontend http_in_443 + bind *:443 ssl crt /certs/letsencrypt/ alpn http/1.1 crt /certs/haproxy/ alpn http/1.1 + mode http + + acl is_rule_hostssl_local_443_1 hdr(host) -i hostssl.local + acl is_rule_hostssl_local_443_2 hdr(host) -i hostssl.local:443 + use_backend srv_hostssl_local_443 if is_rule_hostssl_local_443_1 OR is_rule_hostssl_local_443_2 + + acl is_rule_host2_local_443_1 hdr(host) -i host2.local + acl is_rule_host2_local_443_2 hdr(host) -i host2.local:443 + use_backend srv_host2_local_443 if is_rule_host2_local_443_1 OR is_rule_host2_local_443_2 + +backend srv_hostssl_local_443 + balance roundrobin + mode http + option forwardfor + http-request set-header X-Forwarded-Port %[dst_port] + http-request add-header X-Forwarded-Proto https if { ssl_fc } + server srv-0 test2_processor_docker:8080 check weight 1 +backend srv_host2_local_443 + balance roundrobin + mode http + option forwardfor + http-request set-header X-Forwarded-Port %[dst_port] + http-request add-header X-Forwarded-Proto https if { ssl_fc } + server srv-0 test_processor_docker:9000 check weight 1 + +frontend http_in_80 + bind *:80 + mode http + + acl is_rule_host1_local_80_1 hdr(host) -i host1.local + acl is_rule_host1_local_80_2 hdr(host) -i host1.local:80 + use_backend srv_host1_local_80 if is_rule_host1_local_80_1 OR is_rule_host1_local_80_2 + +backend srv_host1_local_80 + balance roundrobin + mode http + option forwardfor + http-request set-header X-Forwarded-Port %[dst_port] + http-request add-header X-Forwarded-Proto https if { ssl_fc } + server srv-0 test_processor_docker:8080 check weight 1 + +frontend http_in_90 + bind *:90 + mode http + + acl is_rule_host2_local_90_1 hdr(host) -i host2.local + acl is_rule_host2_local_90_2 hdr(host) -i host2.local:90 + acl is_letsencrypt_host2_local_90 path_beg /.well-known/acme-challenge/ + use_backend letsencrypt_backend if is_letsencrypt_host2_local_90 is_rule_host2_local_90_1 OR is_letsencrypt_host2_local_90 is_rule_host2_local_90_2 + use_backend srv_host2_local_90 if is_rule_host2_local_90_1 OR is_rule_host2_local_90_2 + +backend srv_host2_local_90 + balance roundrobin + mode http + option forwardfor + http-request set-header X-Forwarded-Port %[dst_port] + http-request add-header X-Forwarded-Proto https if { ssl_fc } + server srv-0 test_processor_docker:9000 check weight 1 + +backend letsencrypt_backend + mode http + server certbot 127.0.0.1:2080 diff --git a/tests/expected/no-services.txt b/src/tests/expected/no-services.txt similarity index 97% rename from tests/expected/no-services.txt rename to src/tests/expected/no-services.txt index 428cbca..a95c434 100644 --- a/tests/expected/no-services.txt +++ b/src/tests/expected/no-services.txt @@ -25,4 +25,4 @@ defaults backend letsencrypt_backend mode http - server certbot 127.0.0.1:2080 \ No newline at end of file + server certbot 127.0.0.1:2080 diff --git a/tests/expected/services-letsencrypt.txt b/src/tests/expected/services-letsencrypt.txt similarity index 99% rename from tests/expected/services-letsencrypt.txt rename to src/tests/expected/services-letsencrypt.txt index e0093d1..3e2eafb 100644 --- a/tests/expected/services-letsencrypt.txt +++ b/src/tests/expected/services-letsencrypt.txt @@ -92,4 +92,4 @@ backend srv_test_example_org_443 backend letsencrypt_backend mode http - server certbot 127.0.0.1:2080 \ No newline at end of file + server certbot 127.0.0.1:2080 diff --git a/tests/expected/services-multi-containers.txt b/src/tests/expected/services-multi-containers.txt similarity index 98% rename from tests/expected/services-multi-containers.txt rename to src/tests/expected/services-multi-containers.txt index 00e9196..5910691 100644 --- a/tests/expected/services-multi-containers.txt +++ b/src/tests/expected/services-multi-containers.txt @@ -42,4 +42,4 @@ backend srv_www_helloworld_com_19901 backend letsencrypt_backend mode http - server certbot 127.0.0.1:2080 \ No newline at end of file + server certbot 127.0.0.1:2080 diff --git a/tests/expected/services-multiple-hosts.txt b/src/tests/expected/services-multiple-hosts.txt similarity index 98% rename from tests/expected/services-multiple-hosts.txt rename to src/tests/expected/services-multiple-hosts.txt index cc9c39e..954dde1 100644 --- a/tests/expected/services-multiple-hosts.txt +++ b/src/tests/expected/services-multiple-hosts.txt @@ -48,15 +48,15 @@ frontend http_in_19901 mode http redirect prefix www.google.com code 301 if { hdr(host) -i google.helloworld.com } - acl is_rule_hello_com_19901_1 hdr(host) -i hello.com - acl is_rule_hello_com_19901_2 hdr(host) -i hello.com:19901 - use_backend srv_hello_com_19901 if is_rule_hello_com_19901_1 OR is_rule_hello_com_19901_2 - acl is_rule_www_helloworld_com_19901_1 hdr(host) -i www.helloworld.com acl is_rule_www_helloworld_com_19901_2 hdr(host) -i www.helloworld.com:19901 use_backend srv_www_helloworld_com_19901 if is_rule_www_helloworld_com_19901_1 OR is_rule_www_helloworld_com_19901_2 -backend srv_hello_com_19901 + acl is_rule_hello_com_19901_1 hdr(host) -i hello.com + acl is_rule_hello_com_19901_2 hdr(host) -i hello.com:19901 + use_backend srv_hello_com_19901 if is_rule_hello_com_19901_1 OR is_rule_hello_com_19901_2 + +backend srv_www_helloworld_com_19901 balance roundrobin mode http option forwardfor @@ -64,7 +64,7 @@ backend srv_hello_com_19901 http-request add-header X-Forwarded-Proto https if { ssl_fc } server srv-0 3e63154954b0:80 check weight 1 server srv-1 eb294c110eb1:80 check weight 1 -backend srv_www_helloworld_com_19901 +backend srv_hello_com_19901 balance roundrobin mode http option forwardfor @@ -75,4 +75,4 @@ backend srv_www_helloworld_com_19901 backend letsencrypt_backend mode http - server certbot 127.0.0.1:2080 \ No newline at end of file + server certbot 127.0.0.1:2080 diff --git a/tests/expected/services-redirect-ssl.txt b/src/tests/expected/services-redirect-ssl.txt similarity index 99% rename from tests/expected/services-redirect-ssl.txt rename to src/tests/expected/services-redirect-ssl.txt index d0dbb07..077123b 100644 --- a/tests/expected/services-redirect-ssl.txt +++ b/src/tests/expected/services-redirect-ssl.txt @@ -77,4 +77,4 @@ backend srv_host1_local_443 backend letsencrypt_backend mode http - server certbot 127.0.0.1:2080 \ No newline at end of file + server certbot 127.0.0.1:2080 diff --git a/tests/expected/services-tcp.txt b/src/tests/expected/services-tcp.txt similarity index 97% rename from tests/expected/services-tcp.txt rename to src/tests/expected/services-tcp.txt index 6df1a26..fa8fd7a 100644 --- a/tests/expected/services-tcp.txt +++ b/src/tests/expected/services-tcp.txt @@ -39,4 +39,4 @@ backend srv_agent_quantum_local_31339 backend letsencrypt_backend mode http - server certbot 127.0.0.1:2080 \ No newline at end of file + server certbot 127.0.0.1:2080 diff --git a/tests/expected/services.txt b/src/tests/expected/services.txt similarity index 99% rename from tests/expected/services.txt rename to src/tests/expected/services.txt index 43765e6..bf2cecd 100644 --- a/tests/expected/services.txt +++ b/src/tests/expected/services.txt @@ -121,4 +121,4 @@ backend srv_www_somehost_com_br_80 backend letsencrypt_backend mode http - server certbot 127.0.0.1:2080 \ No newline at end of file + server certbot 127.0.0.1:2080 diff --git a/tests/expected/ssl-loose.txt b/src/tests/expected/ssl-loose.txt similarity index 98% rename from tests/expected/ssl-loose.txt rename to src/tests/expected/ssl-loose.txt index 674c9c6..45ac853 100644 --- a/tests/expected/ssl-loose.txt +++ b/src/tests/expected/ssl-loose.txt @@ -35,4 +35,4 @@ backend srv_stats backend letsencrypt_backend mode http - server certbot 127.0.0.1:2080 \ No newline at end of file + server certbot 127.0.0.1:2080 diff --git a/tests/expected/ssl-strict.txt b/src/tests/expected/ssl-strict.txt similarity index 95% rename from tests/expected/ssl-strict.txt rename to src/tests/expected/ssl-strict.txt index 815fd2f..f4e00b4 100644 --- a/tests/expected/ssl-strict.txt +++ b/src/tests/expected/ssl-strict.txt @@ -20,4 +20,4 @@ defaults backend letsencrypt_backend mode http - server certbot 127.0.0.1:2080 \ No newline at end of file + server certbot 127.0.0.1:2080 diff --git a/tests/expected/static.txt b/src/tests/expected/static.txt similarity index 99% rename from tests/expected/static.txt rename to src/tests/expected/static.txt index f3cd653..ea0362f 100644 --- a/tests/expected/static.txt +++ b/src/tests/expected/static.txt @@ -107,4 +107,4 @@ backend srv_host3_com_br_8080 backend letsencrypt_backend mode http - server certbot 127.0.0.1:2080 \ No newline at end of file + server certbot 127.0.0.1:2080 diff --git a/src/tests/fixtures/no-services b/src/tests/fixtures/no-services new file mode 100644 index 0000000..9e1b3c9 --- /dev/null +++ b/src/tests/fixtures/no-services @@ -0,0 +1,5 @@ +{"swarm-prom_caddy": {"com.docker.stack.image":"stefanprodan/caddy","com.docker.stack.namespace":"swarm-prom"}, +"swarm-prom_cadvisor": {"com.docker.stack.image":"google/cadvisor","com.docker.stack.namespace":"swarm-prom"}, +"swarm-prom_dockerd-exporter": {"com.docker.stack.image":"stefanprodan/caddy","com.docker.stack.namespace":"swarm-prom"}, +"swarm-prom_unsee": {"com.docker.stack.image":"cloudflare/unsee:v0.8.0","com.docker.stack.namespace":"swarm-prom"}, +"test_proxy": {"com.docker.stack.image":"byjg/easy-haproxy","com.docker.stack.namespace":"test"}} diff --git a/src/tests/fixtures/services b/src/tests/fixtures/services new file mode 100644 index 0000000..56ef60a --- /dev/null +++ b/src/tests/fixtures/services @@ -0,0 +1,6 @@ +{"portainer-agent_agent": {"com.docker.stack.image":"portainer/agent:1.5.1","com.docker.stack.namespace":"portainer-agent"}, +"my-stack_agent": {"easyhaproxy.agent.host":"agent.quantum.example.org","easyhaproxy.agent.localport":"9001","easyhaproxy.agent.mode":"tcp","easyhaproxy.agent.port":"31339","com.docker.stack.image":"portainer/agent:1.5.1","com.docker.stack.namespace":"my-stack","com.planetary-quantum":"monitoring"}, +"my-stack_cadvisor": {"easyhaproxy.cadvisor.host":"cadvisor.quantum.example.org","easyhaproxy.cadvisor.localport":"8080","easyhaproxy.cadvisor.port":"31337","com.docker.stack.image":"gcr.io/google-containers/cadvisor:v0.34.0","com.docker.stack.namespace":"my-stack","com.planetary-quantum":"monitoring"}, +"my-stack_node-exporter": {"easyhaproxy.exp.host":"node-exporter.quantum.example.org","easyhaproxy.exp.localport":"9100","easyhaproxy.exp.port":"31337","com.docker.stack.image":"stefanprodan/swarmprom-node-exporter:v0.16.0","com.docker.stack.namespace":"my-stack","com.planetary-quantum":"monitoring","easyhaproxy.exp.letsencrypt":"true"}, +"my-stack_reverse-proxy": {"com.docker.stack.image":"quay.io/pngmbh/easy-haproxy:tcp-mode","com.docker.stack.namespace":"my-stack","com.planetary-quantum":"monitoring"}, +"some-service": {"easyhaproxy.http.port":"80","easyhaproxy.http.host":"www.somehost.com.br","easyhaproxy.http.localport":"80","easyhaproxy.http.redirect":"{\"somehost.com.br\":\"https://www.somehost.com.br\",\"somehost.com\":\"https://www.somehost.com.br\",\"www.somehost.com\":\"https://www.somehost.com.br\",\"byjg.ca\":\"https://www.somehost.com.br\",\"www.byjg.ca\":\"https://www.somehost.com.br\"}","easyhaproxy.https.port":"443","easyhaproxy.https.host":"www.somehost.com.br","easyhaproxy.https.localport":"80","easyhaproxy.https.redirect":"{\"somehost.com.br\":\"https://www.somehost.com.br\",\"somehost.com\":\"https://www.somehost.com.br\",\"www.somehost.com\":\"https://www.somehost.com.br\",\"byjg.ca\":\"https://www.somehost.com.br\",\"www.byjg.ca\":\"https://www.somehost.com.br\"}","easyhaproxy.https.sslcert":"U29tZSBQRU0gQ2VydGlmaWNhdGU="}} \ No newline at end of file diff --git a/src/tests/fixtures/services-changed-label b/src/tests/fixtures/services-changed-label new file mode 100644 index 0000000..2a23c24 --- /dev/null +++ b/src/tests/fixtures/services-changed-label @@ -0,0 +1,6 @@ +{"portainer-agent_agent": {"com.docker.stack.image":"portainer/agent:1.5.1","com.docker.stack.namespace":"portainer-agent"}, +"my-stack_agent": {"haproxy.agent.host":"agent.quantum.example.org","haproxy.agent.localport":"9001","haproxy.agent.mode":"tcp","haproxy.agent.port":"31339","com.docker.stack.image":"portainer/agent:1.5.1","com.docker.stack.namespace":"my-stack","com.planetary-quantum":"monitoring"}, +"my-stack_cadvisor": {"haproxy.cadvisor.host":"cadvisor.quantum.example.org","haproxy.cadvisor.localport":"8080","haproxy.cadvisor.port":"31337","com.docker.stack.image":"gcr.io/google-containers/cadvisor:v0.34.0","com.docker.stack.namespace":"my-stack","com.planetary-quantum":"monitoring"}, +"my-stack_node-exporter": {"haproxy.exp.host":"node-exporter.quantum.example.org","haproxy.exp.localport":"9100","haproxy.exp.port":"31337","com.docker.stack.image":"stefanprodan/swarmprom-node-exporter:v0.16.0","com.docker.stack.namespace":"my-stack","com.planetary-quantum":"monitoring","haproxy.exp.letsencrypt":"yes"}, +"my-stack_reverse-proxy": {"com.docker.stack.image":"quay.io/pngmbh/easy-haproxy:tcp-mode","com.docker.stack.namespace":"my-stack","com.planetary-quantum":"monitoring"}, +"some-service": {"haproxy.http.port":"80","haproxy.http.host":"www.somehost.com.br","haproxy.http.localport":"80","haproxy.http.redirect":"{\"somehost.com.br\":\"https://www.somehost.com.br\",\"somehost.com\":\"https://www.somehost.com.br\",\"www.somehost.com\":\"https://www.somehost.com.br\",\"byjg.ca\":\"https://www.somehost.com.br\",\"www.byjg.ca\":\"https://www.somehost.com.br\"}","haproxy.https.port":"443","haproxy.https.host":"www.somehost.com.br","haproxy.https.localport":"80","haproxy.https.redirect":"{\"somehost.com.br\":\"https://www.somehost.com.br\",\"somehost.com\":\"https://www.somehost.com.br\",\"www.somehost.com\":\"https://www.somehost.com.br\",\"byjg.ca\":\"https://www.somehost.com.br\",\"www.byjg.ca\":\"https://www.somehost.com.br\"}","haproxy.https.sslcert":"U29tZSBQRU0gQ2VydGlmaWNhdGU="}} \ No newline at end of file diff --git a/src/tests/fixtures/services-clone-to-ssl b/src/tests/fixtures/services-clone-to-ssl new file mode 100644 index 0000000..e9b9239 --- /dev/null +++ b/src/tests/fixtures/services-clone-to-ssl @@ -0,0 +1,2 @@ +{"10.152.183.62": {"creation_timestamp": "08/24/22 02:59:44", "resource_version": "72517156", "namespace": "parking", "easyhaproxy.valida-me_8080.host": "valida.me", "easyhaproxy.valida-me_8080.port": "80", "easyhaproxy.valida-me_8080.localport": 8080, "easyhaproxy.valida-me_8080.redirect": "{\"www.valida.me\": \"https://valida.me\"}", "easyhaproxy.www-valida-me_8080.host": "www.valida.me", "easyhaproxy.www-valida-me_8080.port": "80", "easyhaproxy.www-valida-me_8080.localport": 8080, "easyhaproxy.www-valida-me_8080.redirect": "{\"www.valida.me\": \"https://valida.me\"}"}, +"10.152.183.215": {"creation_timestamp": "08/26/22 03:06:01", "resource_version": "72522999", "namespace": "default", "easyhaproxy.host2-local_8080.host": "host2.local", "easyhaproxy.host2-local_8080.port": "80", "easyhaproxy.host2-local_8080.localport": 8080, "easyhaproxy.host2-local_8080.clone_to_ssl": "true"}} \ No newline at end of file diff --git a/src/tests/fixtures/services-letsencrypt b/src/tests/fixtures/services-letsencrypt new file mode 100644 index 0000000..9c83781 --- /dev/null +++ b/src/tests/fixtures/services-letsencrypt @@ -0,0 +1,4 @@ +{"f5c645a0dfc6": {"com.docker.compose.config-hash":"b95ebc27d0e61caa418cdfa632e05a656da9bbc3ea0d4603651971015f10a1f0","com.docker.compose.container-number":"1","com.docker.compose.depends_on":"","com.docker.compose.image":"sha256:bea3509d6fdc8d7f9ec95563a5a226dc977ee74fb3e980e0de70e892c2d38dde","com.docker.compose.oneoff":"False","com.docker.compose.project":"docker","com.docker.compose.project.config_files":"/workspace/docker-easy-haproxy/examples/docker/docker-compose-test.yml","com.docker.compose.project.working_dir":"/workspace/docker-easy-haproxy/examples/docker","com.docker.compose.service":"nginx","com.docker.compose.version":"2.8.0","easyhaproxy.http.host":"test.example.org","easyhaproxy.http.letsencrypt":"true","easyhaproxy.http.localport":"80","easyhaproxy.http.port":"80","easyhaproxy.http.redirect":"{\"google.helloworld.com\": \"www.google.com\"}","easyhaproxy.http.redirect_ssl":"true"}, +"bbd4d1854155": {"com.docker.compose.config-hash":"3dc790bf2bea944359c75a40c45655bd868f1d85beb599d1ca797e8ea2c95ee4","com.docker.compose.container-number":"1","com.docker.compose.depends_on":"","com.docker.compose.image":"sha256:0fd95b1512c207048ab3fcc74032354f38143fbb8235ac2a47da903c98a58205","com.docker.compose.oneoff":"False","com.docker.compose.project":"docker","com.docker.compose.project.config_files":"/workspace/docker-easy-haproxy/examples/docker/docker-compose-test.yml","com.docker.compose.project.working_dir":"/workspace/docker-easy-haproxy/examples/docker","com.docker.compose.service":"haproxy","com.docker.compose.version":"2.8.0"}, +"b63438410b6a": {"com.docker.compose.config-hash":"b95ebc27d0e61caa418cdfa632e05a656da9bbc3ea0d4603651971015f10a1f0","com.docker.compose.container-number":"2","com.docker.compose.depends_on":"","com.docker.compose.image":"sha256:bea3509d6fdc8d7f9ec95563a5a226dc977ee74fb3e980e0de70e892c2d38dde","com.docker.compose.oneoff":"False","com.docker.compose.project":"docker","com.docker.compose.project.config_files":"/workspace/docker-easy-haproxy/examples/docker/docker-compose-test.yml","com.docker.compose.project.working_dir":"/workspace/docker-easy-haproxy/examples/docker","com.docker.compose.service":"nginx","com.docker.compose.version":"2.8.0","easyhaproxy.http.host":"test.example.org","easyhaproxy.http.letsencrypt":"true","easyhaproxy.http.localport":"80","easyhaproxy.http.port":"80","easyhaproxy.http.redirect":"{\"google.helloworld.com\": \"www.google.com\"}","easyhaproxy.http.redirect_ssl":"true"}, +"83d57d592e26": {"com.docker.compose.config-hash":"8c5871144f1e8a3aeca037207c02f011ab2c6e6c311a3773602b63541762dab5","com.docker.compose.container-number":"1","com.docker.compose.depends_on":"","com.docker.compose.image":"sha256:c4232396c715f3d568816c666e6d9b4a68ef6c36f6243b4007c4ee1d8335fd65","com.docker.compose.oneoff":"False","com.docker.compose.project":"docker","com.docker.compose.project.config_files":"/workspace/docker-easy-haproxy/examples/docker/docker-compose-test.yml","com.docker.compose.project.working_dir":"/workspace/docker-easy-haproxy/examples/docker","com.docker.compose.service":"static","com.docker.compose.version":"2.8.0","easyhaproxy.http.host":"test2.example.org","easyhaproxy.http.localport":"8080","easyhaproxy.http.port":"80","io.buildah.version":"1.21.0"}} \ No newline at end of file diff --git a/src/tests/fixtures/services-multi-containers b/src/tests/fixtures/services-multi-containers new file mode 100644 index 0000000..4257caa --- /dev/null +++ b/src/tests/fixtures/services-multi-containers @@ -0,0 +1,2 @@ +{"test_nginx.2.t5r94mjlced7m3t5orfjbowmm": {"easyhaproxy.http.host":"www.helloworld.com","easyhaproxy.http.localport":"80","easyhaproxy.http.port":"19901","com.docker.stack.image":"stenote/nginx-hostname","com.docker.stack.namespace":"test"}, +"test_nginx.1.p552hqxkdx88narjrp5kouwb2": {"easyhaproxy.http.host":"www.helloworld.com","easyhaproxy.http.localport":"80","easyhaproxy.http.port":"19901","com.docker.stack.image":"stenote/nginx-hostname","com.docker.stack.namespace":"test"}} \ No newline at end of file diff --git a/src/tests/fixtures/services-multiple-hosts b/src/tests/fixtures/services-multiple-hosts new file mode 100644 index 0000000..66db013 --- /dev/null +++ b/src/tests/fixtures/services-multiple-hosts @@ -0,0 +1,3 @@ +{"db79d3a910f4": {"com.docker.compose.config-hash":"5bde40f52451521ad201e70de1291397376a0498a7c955624a609da3b60e7e8e","com.docker.compose.container-number":"1","com.docker.compose.depends_on":"","com.docker.compose.image":"sha256:ea39067705590557dd0cd951664a10970ceefcb725a3c1f43690d6d6d4ed5fce","com.docker.compose.oneoff":"False","com.docker.compose.project":"docker","com.docker.compose.project.config_files":"/workspace/docker-easy-haproxy/examples/docker/docker-compose-multi-containers.yml","com.docker.compose.project.working_dir":"/workspace/docker-easy-haproxy/examples/docker","com.docker.compose.service":"haproxy","com.docker.compose.version":"2.8.0"}, +"3e63154954b0": {"com.docker.compose.config-hash":"4e0cbdd8372c6779863799e5021ed8178f74b55bd8e070abcdffaf87eb7baa36","com.docker.compose.container-number":"1","com.docker.compose.depends_on":"","com.docker.compose.image":"sha256:bea3509d6fdc8d7f9ec95563a5a226dc977ee74fb3e980e0de70e892c2d38dde","com.docker.compose.oneoff":"False","com.docker.compose.project":"docker","com.docker.compose.project.config_files":"/workspace/docker-easy-haproxy/examples/docker/docker-compose-multi-containers.yml","com.docker.compose.project.working_dir":"/workspace/docker-easy-haproxy/examples/docker","com.docker.compose.service":"nginx","com.docker.compose.version":"2.8.0","easyhaproxy.http.host":"hello.com\n, www.helloworld.com\n","easyhaproxy.http.localport":"80","easyhaproxy.http.port":"19901","easyhaproxy.http.redirect":"{\"google.helloworld.com\": \"www.google.com\"}"}, +"eb294c110eb1": {"com.docker.compose.config-hash":"4e0cbdd8372c6779863799e5021ed8178f74b55bd8e070abcdffaf87eb7baa36","com.docker.compose.container-number":"2","com.docker.compose.depends_on":"","com.docker.compose.image":"sha256:bea3509d6fdc8d7f9ec95563a5a226dc977ee74fb3e980e0de70e892c2d38dde","com.docker.compose.oneoff":"False","com.docker.compose.project":"docker","com.docker.compose.project.config_files":"/workspace/docker-easy-haproxy/examples/docker/docker-compose-multi-containers.yml","com.docker.compose.project.working_dir":"/workspace/docker-easy-haproxy/examples/docker","com.docker.compose.service":"nginx","com.docker.compose.version":"2.8.0","easyhaproxy.http.host":"hello.com\n, www.helloworld.com\n","easyhaproxy.http.localport":"80","easyhaproxy.http.port":"19901","easyhaproxy.http.redirect":"{\"google.helloworld.com\": \"www.google.com\"}"}} \ No newline at end of file diff --git a/src/tests/fixtures/services-redirect-ssl b/src/tests/fixtures/services-redirect-ssl new file mode 100644 index 0000000..1e4a539 --- /dev/null +++ b/src/tests/fixtures/services-redirect-ssl @@ -0,0 +1,3 @@ +{"3571640c480a": {"com.docker.compose.config-hash":"b3089bebef089144accf08bc189c2dbd527cb2ff2b44f0fd57f031b3258d9389","com.docker.compose.container-number":"1","com.docker.compose.depends_on":"","com.docker.compose.image":"sha256:c4232396c715f3d568816c666e6d9b4a68ef6c36f6243b4007c4ee1d8335fd65","com.docker.compose.oneoff":"False","com.docker.compose.project":"docker","com.docker.compose.project.config_files":"/workspace/docker-easy-haproxy/examples/docker/docker-compose.yml","com.docker.compose.project.working_dir":"/workspace/docker-easy-haproxy/examples/docker","com.docker.compose.service":"container2","com.docker.compose.version":"2.8.0","easyhaproxy.http.host":"host2.local","easyhaproxy.http.port":"80","easyhaproxy.http.redirect_ssl":"true","easyhaproxy.https.host":"host2.local","easyhaproxy.https.localport":"8080","easyhaproxy.https.port":"443","easyhaproxy.https.ssl":"true","io.buildah.version":"1.21.0"}, +"5b69bc7fea1b": {"com.docker.compose.config-hash":"e2e9424b6ef2adfa33f750e3894446c9200f24674f5cf601ad60a87430e14314","com.docker.compose.container-number":"1","com.docker.compose.depends_on":"","com.docker.compose.image":"sha256:c4232396c715f3d568816c666e6d9b4a68ef6c36f6243b4007c4ee1d8335fd65","com.docker.compose.oneoff":"False","com.docker.compose.project":"docker","com.docker.compose.project.config_files":"/workspace/docker-easy-haproxy/examples/docker/docker-compose.yml","com.docker.compose.project.working_dir":"/workspace/docker-easy-haproxy/examples/docker","com.docker.compose.service":"container","com.docker.compose.version":"2.8.0","easyhaproxy.http.host":"host1.local","easyhaproxy.http.port":"80","easyhaproxy.http.redirect_ssl":"true","easyhaproxy.https.host":"host1.local","easyhaproxy.https.localport":"8080","easyhaproxy.https.port":"443","easyhaproxy.https.sslcert":"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZEVENDQXZXZ0F3SUJBZ0lVUmkrdzFaVmdlZWRUbE5JQXdxUUJNSnY2ZFhzd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0ZqRVVNQklHQTFVRUF3d0xhRzl6ZERFdWJHOWpZV3d3SGhjTk1qRXdPREV3TVRnME9UQTJXaGNOTXpFdwpPREE0TVRnME9UQTJXakFXTVJRd0VnWURWUVFEREF0b2IzTjBNUzVzYjJOaGJEQ0NBaUl3RFFZSktvWklodmNOCkFRRUJCUUFEZ2dJUEFEQ0NBZ29DZ2dJQkFNQkRBaExBeWdKdWFXNnc2ZmZpZ3pUQUFHWHBtRXowdEl4bjFrNFoKeDV3TjVycHYvcXUwUU1ZeitBdjJ1MWVPS0VLWmVhRlJWcFQwcjkzZFg3SXZiRVpIdDI1R1BpQnZsTEdxaGpLUgpQblNrLzdVOFhtc250dFVBVjdyVkVLMVVyZEZ3OC9Jd3JpUUMrZGhyMG1uWWZTRE1rdkJvTUZwZGhWTlRyYkFaCjFUQjZyUWpFN0FyME10OG15OTZYSm13cmNqSzJUaitFMnJnUElVejFlNWNla0ZZSURTQmF0bXcrMyt2citUNXgKRk5Ga0oybzMwVzVvOFpmbENKSnpyVmFpaHFRaWNzNlpLRGdwZjdpcVhNRml3V0lsaGRRcEd2eDVHZi9LRlRLOQpVYU9uUlp6L1grMkNlYkFGYVRIUjNrL1BZcHBXVGdCQkJ1UnZscEN3K3dkbmttdGVDMFNRUkY5MVFXVnI3ZWpvCjdLYU9sR0k1VnR2TVVzV3ZUZUFabXBheW1JYUFURVR1T0phWTBKVTExT21MZUQ5RE9qNUUyU1E3cUlYL3BGY3AKeHB6RzVqNGMrTWxndnhQMlZBa05UZUFYQ2FZaVBCUUg1WlpnMEhFMlduQjFLaExSRmxIZDRpSFFEMkdKNXlOLwo2ZkNGQmZaZktTZUs4SmF1d3hnV2tyYTUzT2NEcS9tS2QrREEvZEsrL3J1Rzd0cXdWZ0lhMDRIT3Bsek03TFlSCkdCMElyczkrbHI1L1BKYlFabVUwNzNNZG42Y1hBZzNwKzZ3dndGbERrUzV2MTNnQkRZTkh0RjYyYmM1NTFlZEYKWjZrR3pKN3dtR1JvODRhQlA3TXVSWmVSZUxPclNTNjdhMXdMZHpac01uUDFUSjd4OUxmcjlNS2wydURuUWRuWQpleDhEQWdNQkFBR2pVekJSTUIwR0ExVWREZ1FXQkJTUS9tdFpkNmg4ZW45WVFWSDZITzFQbFdXaXF6QWZCZ05WCkhTTUVHREFXZ0JTUS9tdFpkNmg4ZW45WVFWSDZITzFQbFdXaXF6QVBCZ05WSFJNQkFmOEVCVEFEQVFIL01BMEcKQ1NxR1NJYjNEUUVCQ3dVQUE0SUNBUUNoUVlOdWFoMyttVHBJQkRZeEdyalRKTnVPVElNYVd6TXlpMXRrZitMMApzRUd3cGJtQU8ybVdXUVlGN1dWTHNpOThQVUxoM2FkanQyaml1ZDlWbGFhQzZnbnduNVpvMStQaWxvOXNOTExXCjZpajArck40a3dJbS9wTnFpK2pEdXUyY3ZBdUhJd1pXZWg4YkVlLzVVQ3hvNGlobVdGUU44ZUo2VFVLQ3BoUkMKNkVvci9TU1paQlFIZ1BsMEJjaHpIT2t3dTdSM0xDbmRScXhqaEFvVmI5eVFPVitac21UZUpYdWx3TnpKMXVMdApUOE9JZ0lpRHBtQm83SFNOMkgwazNjaHgwMEFzalV5SjltbUFXUGVqRmUvS1hMUlBjVlpSMTdqaHpnZklCRXpzCk01V3RXRm0xYUhEalZ2Nk02aXRlVm02MUU5VCtrL00xMXJ1MWUyWXdzeFREdmI2eDA0bWNyTnU5c29xZGRCYnIKVmZwbHV1b1EvaEVBYlh0Rk5Qb1R5U3B6MGN3T3djSENvd1ZPTG1kS2d2SW1zelppTXlISEc4VkdHbVBoODhuNwp3VnhiMGdWMFA0Uk1yY01MZGVUZG41NVlRcjFDcUJyMzRlQjZvbDZBc2JUbTNWekJIUlZtRk5rc2wxbzVKQjV0CnRYTGdGL0c4L3J6Si80bTFQYVZ1eHJCN0R4VW1JazhFUGJTSVZrdlp2ZDdMQnpLd1E2SWZWYXVjZXdIZkVhalEKVklpZXhTTWlGYzdsdzNLbnhqT0haamY2Rk05VllnM05vKytHZEM5OXM3TGtJdUp3QU1MTnFUUTdIdmhuN1l2UAo0RmxTSWdjNnhqMFlrR1pFUWxiNW8vNW5hdUVxUVUwQUJndzZqdEk0TnhyTkxUNmNwN0NPNE0weElERWcvM1lECmFBPT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQotLS0tLUJFR0lOIFBSSVZBVEUgS0VZLS0tLS0KTUlJSlF3SUJBREFOQmdrcWhraUc5dzBCQVFFRkFBU0NDUzB3Z2drcEFnRUFBb0lDQVFEQVF3SVN3TW9DYm1sdQpzT24zNG9NMHdBQmw2WmhNOUxTTVo5Wk9HY2VjRGVhNmIvNnJ0RURHTS9nTDlydFhqaWhDbVhtaFVWYVU5Sy9kCjNWK3lMMnhHUjdkdVJqNGdiNVN4cW9ZeWtUNTBwUCsxUEY1cko3YlZBRmU2MVJDdFZLM1JjUFB5TUs0a0F2blkKYTlKcDJIMGd6Skx3YURCYVhZVlRVNjJ3R2RVd2VxMEl4T3dLOURMZkpzdmVseVpzSzNJeXRrNC9oTnE0RHlGTQo5WHVYSHBCV0NBMGdXclpzUHQvcjYvaytjUlRSWkNkcU45RnVhUEdYNVFpU2M2MVdvb2FrSW5MT21TZzRLWCs0CnFsekJZc0ZpSllYVUtScjhlUm4veWhVeXZWR2pwMFdjLzEvdGdubXdCV2t4MGQ1UHoyS2FWazRBUVFia2I1YVEKc1BzSFo1SnJYZ3RFa0VSZmRVRmxhKzNvNk95bWpwUmlPVmJiekZMRnIwM2dHWnFXc3BpR2dFeEU3amlXbU5DVgpOZFRwaTNnL1F6bytSTmtrTzZpRi82UlhLY2FjeHVZK0hQakpZTDhUOWxRSkRVM2dGd21tSWp3VUIrV1dZTkJ4Ck5scHdkU29TMFJaUjNlSWgwQTloaWVjamYrbndoUVgyWHlrbml2Q1dyc01ZRnBLMnVkem5BNnY1aW5mZ3dQM1MKdnY2N2h1N2FzRllDR3RPQnpxWmN6T3kyRVJnZENLN1BmcGErZnp5VzBHWmxOTzl6SForbkZ3SU42ZnVzTDhCWgpRNUV1YjlkNEFRMkRSN1JldG0zT2VkWG5SV2VwQnN5ZThKaGthUE9HZ1QrekxrV1hrWGl6cTBrdXUydGNDM2MyCmJESno5VXllOGZTMzYvVENwZHJnNTBIWjJIc2ZBd0lEQVFBQkFvSUNBUUMveFpiWjBjY3RxYWdzcXZhVk5URWUKZXExcStoZmFHdlBFWVFhWUhJcklFKzJpNVhjbkdjTEtjS2ZvZHhEakFuOFIvemdkT3A2Y01YMENWbi9Qb2hIawpBRUR0RTgrQVZ3d0FNMUZzT3dnTEhWR2FHejhxcnhCbFlkUWdIY3BtdWVJdTJQWGJDOGVIVUJpYVVPSXVoYXc1Ci9SUk1EQUMvQWkyc3NmaTdnT2p2VkU0b1F4UVcwUUcxS0dPT0FVSm4vdVlIdzJSRlkyVXUxcGlteE8ya0RPNTMKZ2N4bUMxV09ueUNIbUhhaVcvVWg3ejZKYW1mU000ZFh0VEpac2x5aDM3ZGhIS05iZzlWa1A3Q1FLQTRoTHpvcApoYmY1cVk2cmFyZ09OaW55MUhnTVB4cm13S3VVb3VKeU90TjB5QnR4akRDVU5hWFVCd2l5N3NOR1MrSDR2c3lCCjVQOUhoSUhTdHUrRlp0M0hHN0VJcUNuZGlhU0tEUzRqV2FWUUFiYm80bloyWnMyQkQreERlUFJDUlVxWDdyTTQKNFh6UElSV1dYbW1XZi83SWcyOUhicnA0YTlMY09tUTJsZUNKdGJhVEZTTjk2T0xVSjVFK2hRMHVsQ1pnQlZtUQpSQ1VZa0pQNGxPemJhS2R6anhnSE1ySHptNDVlVUZmOExpck94aTJ1eXhYSFFtRE51NGIzWDE4a3QzUGdVbVVtCjNkWHBsM2ZxU3lKYTdTQ1Y4Wk5CcnNyRHExRSt0aFl0dTkxUWJWU0d4SGQ5SHJOVmUzWGRMYk9DZFU5Q3VDNjkKTmdsem5hYTdzWkxxbXlLZWpUZkdzWTd4cldkTmNNUGw0cDRmY0lEL080RXBBU1pmb3JwVGVLTlQwWklmWlpldwpiMG1BUWVZWnFRTThpL3FNWU4vdUFRS0NBUUVBNXFnMXNSTk1jNlZkTS90UmdsYXNHWW94amdSQzJPcUFEWmdzCm1BWE1VSjNrRXJweXh0K2VDaW15OGlidVlwelJUSVE4ZkJUV1JrQ3RSWlhKNytLY0xWdGs5UVpJb0xiaHlOd2QKNEl4RVFaRnVVbGpEYnZTalRMU3ljc0h2bzY1aWJXSWZUTDdiZ1dsTEdnR3EvVU96ZkdzZ0g2Uzl3THA1RzMwRwo4RUx5akk1ZVRJWUlDcmZUbVZMK2M0NU1ScEVNS28rY3Z6OFB5c2lhT0ZUbjNjeXN3UFZkWWFlRUVxTVFqVTh3CklHTnNHWkx5dFk3QkFCQmNZMGxkcnRiYS9PK0Z2LytSSDd1VXR6UDd4cENJd0ZDeDgwWnpOK1dSeTlOdkk2M1UKenEzeUlCb1c5R3lBcEQyK1BMYVBOeGY3UUxUVUNoWTFaei9kWVJsdEtPeHYyQWE1Z1FLQ0FRRUExV0xXTnFwMApmaEIvWnRmU0VTaHhGTU04OWNqTjZBYXoxV0tMN3VUQm91OW9TSm54amtoa2FWNzZhY25UL2lxWHR4TU5nSGkxCmZJbURwVTNQdk0wWTRVZDJUNDdvSGM2UDFCclpQTi9HbVh5L3M2QkFFZFB3TGU3Sis0blRJU0hBZEdtcmgrYS8KNXBrdHUzMmc5bFdxZnR4ZWNGSVZTTFBXa3hUMFhLaU14cDFmZmtMK09hdnBNZ01GWks0MWlLczNkTlNoS1BvZwpMOEdTUGNQOXgveW43OFAyZUszTitQR2psQTZwUHpyQU55V1U3TjAvYm1IY0I5VEtQK3VkWVdjalZocnU3TVlOCndOckU0a0tkQzh2OGk3eDd0RGJ2Yjc5VCtGbzZQSWg1M3AwT3NuWnpBOFVSMFFOUit2RFF1ZlF1eWFqOFJFQysKWkc4WXlDS3N2azh5Z3dLQ0FRQS9mc1N4QjBmL2VlRXJZeDZ3QzUzNnRlRW9ZQ0hxeHJzVGd2V2JyOVRyeUZzMQprSi95QVRMblIwMWNmYjBYNW1WemM5K1dwTUhMdXhnMzFLRXZhU2xuRHdhK3NNa2pmTlN3ejI5bUZoYmdHZUhOCngyT2RVcmoxYjdURUJJRXNoTi9SanJaaEVSVXFEY3MvMEgrNmtuMkJYWmdOUGZPQ2I1TFJMMXpPblE5YUJBTVAKZThJUStVUEZyR1FoZVdXajgxL3ZBM081N2VreUFJRDd5dHU5WWcrWVdyTW5JODhtdGo3ak40NWZEQitBOXNQYgptUDJtUDlxKzlqNVUyQTZXbkhVc1FuVTMwQktEVUVzYUFVV3o4MExaWG1adlY4SUg0eDl3S2ZVd0pCQklLQVp6CnFMN005N1k3em1Ha1gvU3BmbDMwbk9KOGxzY2hhTGQxRVlsRVphMkJBb0lCQVFDeWUyNVQ0VFY1TUpGdjB6dVoKTUd1TmcxU2MvTzRGa24yZkVVT2NlV2pod1VCSDRjUGpUL2YxRHdXRHNOYUo5TlJieENyNTkzMU9BclBEYzVjOApBNDA0K1k0ak01UkJRa0tabGk5NHRIQW9kK2pjOVVCQjZUVXZKbGw1OVNsTXdDOTY3OXdTMjFaT0tuZlBLR0NYClNzWkdRRXNaeGY2Wmhoc0hnWEozZ2wvbHpVSlBtUGVPQTVZVlIrT2Q5LzA5S0lGRlRvalNmb3luaFZDdUt4NDkKeGI0dVZZbjJIT0o0eEowZlBUZ2hkQ0hNdnJtWGVlUVJqdmI4OGVhTm1xVlVFSEhGRnRnYjRma2xBNWZFN1JUeApCaGxpUkRCd1o3YlVrSU5LNnlWazluNkJUbnM1bU12UkxtZ2RuSnBZdkU3S0MwMkxUYlpiM0krajhDMFpVYStOCnF5N0RBb0lCQUFpZXJpYlM3V1VjbDJhQmxrbTUrVzdxTm0vSU5tNXp2bm9TUG82VjN3YTVoczZmOStDL2tiZEYKODdqUVBBL1lGZTN1UjJzQUo3c2xYNWV1Wks4V21mcEZtZ3psdTBzRXo4MU1MUS9XeXBadFp5dHlWdFd6QjJQdQpYQ1cxdGRTSDllSTJCbWhYZ29rSE5UTTQ4TmsveE9FTnJQL3NlWHJJeDVMSzBobkRIWm90dS96NitZU2tCOWhGCmNtMmZaeWdEMWRNTFg2bGlSaW14eUZZK2RJQ0pOQjk1SmlmVExXWW5XZUdkZGt3UHRYVWVHWEUxb2x6dk5rTEQKek16RTA5dWhreC9sUkpudGVPQkVaYWY4ME9CLzA5T2k5YjkvcnhZNTlkd3NINkdheExvVGZFS3VQbnZCVk1OUgpZa1UxNFd6UUtsZUZraUJKSTlsVnZuZmdHbk9sZ2cwPQotLS0tLUVORCBQUklWQVRFIEtFWS0tLS0t","io.buildah.version":"1.21.0"}, +"77094604be33": {"com.docker.compose.config-hash":"b671dd783e367918c604400fe83cd2450c845a555bda6769bd1351bdcce3c565","com.docker.compose.container-number":"1","com.docker.compose.depends_on":"","com.docker.compose.image":"sha256:4e24fb65b607dc3b105ab5ec5945707ccbf50ac32605e7f4935440b453b7c45c","com.docker.compose.oneoff":"False","com.docker.compose.project":"docker","com.docker.compose.project.config_files":"/workspace/docker-easy-haproxy/examples/docker/docker-compose.yml","com.docker.compose.project.working_dir":"/workspace/docker-easy-haproxy/examples/docker","com.docker.compose.service":"haproxy","com.docker.compose.version":"2.8.0"}} \ No newline at end of file diff --git a/src/tests/fixtures/services-tcp b/src/tests/fixtures/services-tcp new file mode 100644 index 0000000..5b5c2b0 --- /dev/null +++ b/src/tests/fixtures/services-tcp @@ -0,0 +1,2 @@ +{"test_agent": {"easyhaproxy.agent.host":"agent.quantum.local","easyhaproxy.agent.localport":"9001","easyhaproxy.agent.mode":"tcp","easyhaproxy.agent.port":"31339","com.docker.stack.image":"portainer/agent:1.5.1","com.docker.stack.namespace":"test", "easyhaproxy.agent.health-check":"ssl"}, +"test_proxy": {"com.docker.stack.image":"byjg/easy-haproxy:local","com.docker.stack.namespace":"test"}} \ No newline at end of file diff --git a/tests/fixtures/static.yml b/src/tests/fixtures/static.yml similarity index 100% rename from tests/fixtures/static.yml rename to src/tests/fixtures/static.yml diff --git a/src/tests/test_containerenv.py b/src/tests/test_containerenv.py new file mode 100644 index 0000000..5e64b0f --- /dev/null +++ b/src/tests/test_containerenv.py @@ -0,0 +1,102 @@ +import pytest +import os +from processor import ContainerEnv + +def test_container_env_empty(): + assert { + "customerrors": False, + "ssl_mode": "default", + "lookup_label": "easyhaproxy" + } == ContainerEnv.read() + + # os.environ['CERTBOT_LOG_LEVEL'] = 'warn' + +def test_container_env_customerrors(): + os.environ['HAPROXY_CUSTOMERRORS'] = 'true' + try: + assert { + "customerrors": True, + "ssl_mode": "default", + "lookup_label": "easyhaproxy" + } == ContainerEnv.read() + finally: + os.environ['HAPROXY_CUSTOMERRORS'] = '' + +def test_container_env_sslmode(): + os.environ['EASYHAPROXY_SSL_MODE'] = 'STRICT' + try: + assert { + "customerrors": False, + "ssl_mode": "strict", + "lookup_label": "easyhaproxy" + } == ContainerEnv.read() + finally: + os.environ['EASYHAPROXY_SSL_MODE'] = '' + +def test_container_env_stats(): + os.environ['HAPROXY_USERNAME'] = 'abc' + os.environ['HAPROXY_STATS_PORT'] = '2101' + try: + assert { + "customerrors": False, + "ssl_mode": "default", + "lookup_label": "easyhaproxy", + } == ContainerEnv.read() + finally: + os.environ['HAPROXY_USERNAME'] = '' + os.environ['HAPROXY_STATS_PORT'] = '' + +def test_container_env_stats_password(): + os.environ['HAPROXY_PASSWORD'] = 'xyz' + try: + assert { + "customerrors": False, + "ssl_mode": "default", + "lookup_label": "easyhaproxy", + "stats": { + "username": "admin", + "password": "xyz", + "port": "1936" + + } + } == ContainerEnv.read() + finally: + os.environ['HAPROXY_PASSWORD'] = '' + + +def test_container_env_stats_password(): + os.environ['HAPROXY_USERNAME'] = 'abc' + os.environ['HAPROXY_STATS_PORT'] = '2101' + os.environ['HAPROXY_PASSWORD'] = 'xyz' + try: + assert { + "customerrors": False, + "ssl_mode": "default", + "lookup_label": "easyhaproxy", + "stats": { + "username": "abc", + "password": "xyz", + "port": "2101" + + } + } == ContainerEnv.read() + finally: + os.environ['HAPROXY_USERNAME'] = '' + os.environ['HAPROXY_STATS_PORT'] = '' + os.environ['HAPROXY_PASSWORD'] = '' + + +def test_container_env_stats_password(): + os.environ['EASYHAPROXY_LETSENCRYPT_EMAIL'] = 'acme@example.org' + try: + assert { + "customerrors": False, + "ssl_mode": "default", + "lookup_label": "easyhaproxy", + "letsencrypt": { + "email": "acme@example.org", + } + } == ContainerEnv.read() + finally: + os.environ['EASYHAPROXY_LETSENCRYPT_EMAIL'] = '' + diff --git a/src/tests/test_docker.py b/src/tests/test_docker.py new file mode 100644 index 0000000..d65a08b --- /dev/null +++ b/src/tests/test_docker.py @@ -0,0 +1,92 @@ +import pytest +import os +import time +import docker +from functions import Functions +from processor import ProcessorInterface +from processor import Docker + +def _get_hydrated_object(parsed_objects, key): + assert key in parsed_objects.keys() + hydrated_object = {} + for keys in parsed_objects[key]: + if "easyhaproxy" in keys: + hydrated_object[keys] = parsed_objects[key][keys] + return hydrated_object + + +def test_processor_docker(): + try: + client = docker.from_env() + except docker.errors.DockerException: + pytest.skip("There is no docker environment") + + if len(client.containers.list()) > 0: + pytest.skip("I cannot run this test with other containers running.") + + container = client.containers.run("byjg/static-httpserver", + name="test_processor_docker", + detach=True, + auto_remove=True, + remove=True, + labels={ + "easyhaproxy.http.port": "80", + "easyhaproxy.http.localport": "8080", + "easyhaproxy.http.host": "host1.local", + + "easyhaproxy.http2.port": "90", + "easyhaproxy.http2.localport": "9000", + "easyhaproxy.http2.host": "host2.local", + "easyhaproxy.http2.letsencrypt": "true", + }) + container2 = client.containers.run("byjg/static-httpserver", + name="test2_processor_docker", + detach=True, + auto_remove=True, + remove=True, + labels={ + "easyhaproxy.ssl.port": "443", + "easyhaproxy.ssl.localport": "8080", + "easyhaproxy.ssl.host": "hostssl.local", + "easyhaproxy.ssl.sslcert": "U29tZSBQRU0gQ2VydGlmaWNhdGU=" + }) + try: + time.sleep(1) + + os.environ['EASYHAPROXY_LETSENCRYPT_EMAIL'] = 'docker@example.org' + + static = ProcessorInterface.factory("docker") + assert static.get_letsencrypt_hosts() is None + + assert { + 'easyhaproxy.http.host': 'host1.local', + 'easyhaproxy.http.localport': '8080', + 'easyhaproxy.http.port': '80', + 'easyhaproxy.http2.host': 'host2.local', + 'easyhaproxy.http2.localport': '9000', + 'easyhaproxy.http2.port': '90', + 'easyhaproxy.http2.letsencrypt': 'true', + } == _get_hydrated_object(static.get_parsed_object(), "test_processor_docker") + assert { + 'easyhaproxy.ssl.host': 'hostssl.local', + 'easyhaproxy.ssl.localport': '8080', + 'easyhaproxy.ssl.port': '443', + 'easyhaproxy.ssl.sslcert': 'U29tZSBQRU0gQ2VydGlmaWNhdGU=' + } == _get_hydrated_object(static.get_parsed_object(), "test2_processor_docker") + + assert static.get_hosts() is None + assert static.get_certs() == {} + + haproxy_cfg = static.get_haproxy_conf() + assert haproxy_cfg == Functions.load(os.path.join(os.path.dirname(os.path.realpath(__file__)), "./expected/docker.txt")) + + assert static.get_letsencrypt_hosts() == ['host2.local'] + assert static.get_hosts() == ['hostssl.local:443', 'host1.local:80', 'host2.local:90'] + assert static.get_certs() == {'hostssl.local.pem': 'Some PEM Certificate'} + finally: + os.environ['EASYHAPROXY_LETSENCRYPT_EMAIL'] = '' + container.stop() + container2.stop() + + +#test_processor_docker() \ No newline at end of file diff --git a/src/tests/test_functions.py b/src/tests/test_functions.py new file mode 100644 index 0000000..5f40267 --- /dev/null +++ b/src/tests/test_functions.py @@ -0,0 +1,101 @@ +import json +import pytest +import os +import re +import random +import string +from functions import Functions + +def test_functions_check_local_level(): + assert Functions.skip_log('CERTBOT', Functions.INFO) == False + assert Functions.skip_log('HAPOROXY', Functions.INFO) == False + assert Functions.skip_log('EASYHAPROXY', Functions.INFO) == False + + os.environ['CERTBOT_LOG_LEVEL'] = 'warn' + assert Functions.skip_log('CERTBOT', Functions.INFO) == True + os.environ['CERTBOT_LOG_LEVEL'] = '' + + os.environ['HAPROXY_LOG_LEVEL'] = 'warn' + assert Functions.skip_log('HAPROXY', Functions.INFO) == True + os.environ['HAPROXY_LOG_LEVEL'] = '' + + os.environ['EASYHAPROXY_LOG_LEVEL'] = 'warn' + assert Functions.skip_log('EASYHAPROXY', Functions.INFO) == True + os.environ['EASYHAPROXY_LOG_LEVEL'] = '' + +def test_function_load_and_save(): + filename = '/tmp/x.txt' + try: + assert os.path.exists(filename) == False + text = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(50)) + Functions.save(filename, text) + assert os.path.exists(filename) == True + assert Functions.load(filename) == text + finally: + os.unlink(filename) + +def test_functions_check_log_sanity(): + print() + Functions.log(Functions.EASYHAPROXY_LOG, Functions.INFO, "Test 1") + assert Functions.debug_log is None + + Functions.debug_log = [] + try: + Functions.log(Functions.EASYHAPROXY_LOG, Functions.INFO, "Test 2") + assert len(Functions.debug_log) == 1 + assert re.match("\[EASYHAPROXY\] .* \[INFO\]: Test 2", Functions.debug_log[0]) + + os.environ['CERTBOT_LOG_LEVEL'] = 'DEBUG' + Functions.log(Functions.EASYHAPROXY_LOG, Functions.INFO, "Test 3") + assert len(Functions.debug_log) == 2 + assert re.match("\[EASYHAPROXY\] .* \[INFO\]: Test 3", Functions.debug_log[1]) + + os.environ['EASYHAPROXY_LOG_LEVEL'] = 'warn' + Functions.log(Functions.EASYHAPROXY_LOG, Functions.INFO, "Test 4") # Should not log to debug + assert len(Functions.debug_log) == 2 + + finally: + os.environ['EASYHAPROXY_LOG_LEVEL'] = '' + Functions.debug_log = None + +def test_functions_run_bash_log_output(): + print() + Functions.debug_log = [] + try: + result = Functions.run_bash(Functions.EASYHAPROXY_LOG, "echo 'test run 1'", log_output=True, return_result=False) + assert result == [] + assert len(Functions.debug_log) == 1 + assert re.match("\[EASYHAPROXY\] .* \[INFO\]: test run 1", Functions.debug_log[0]) + finally: + Functions.debug_log = None + +def test_functions_run_bash_no_log_output(): + print() + Functions.debug_log = [] + try: + result = Functions.run_bash(Functions.EASYHAPROXY_LOG, "echo 'test run 2'", log_output=False, return_result=False) + assert result == [] + assert len(Functions.debug_log) == 0 + finally: + Functions.debug_log = None + +def test_functions_run_bash_return(): + print() + Functions.debug_log = [] + try: + result = Functions.run_bash(Functions.EASYHAPROXY_LOG, "echo 'test run 3'", log_output=False, return_result=True) + assert len(Functions.debug_log) == 0 + assert "".join(result) == 'test run 3' + finally: + Functions.debug_log = None + +def test_functions_run_bash_log_and_return_output(): + print() + Functions.debug_log = [] + try: + result = Functions.run_bash(Functions.EASYHAPROXY_LOG, "echo 'test run 4'", log_output=True, return_result=True) + assert "".join(result) == 'test run 4' + assert len(Functions.debug_log) == 1 + assert re.match("\[EASYHAPROXY\] .* \[INFO\]: test run 4", Functions.debug_log[0]) + finally: + Functions.debug_log = None \ No newline at end of file diff --git a/tests/test_labels.py b/src/tests/test_labels.py similarity index 100% rename from tests/test_labels.py rename to src/tests/test_labels.py diff --git a/tests/test_parser.py b/src/tests/test_parser.py similarity index 80% rename from tests/test_parser.py rename to src/tests/test_parser.py index 81b4dfc..a3d35f0 100644 --- a/tests/test_parser.py +++ b/src/tests/test_parser.py @@ -2,6 +2,7 @@ import easymapping import pytest import os import yaml +import json CERTS_FOLDER="/tmp/certs" CERT_FILE="/tmp/certs/haproxy/www.somehost.com.br.pem" @@ -10,7 +11,7 @@ LETSENCRYPT_EMAIL="some@email.com" def load_fixture(file): path = os.path.dirname(os.path.realpath(__file__)) with open(path + "/fixtures/" + file, 'r') as content_file: - line_list = content_file.readlines() + line_list = json.loads("".join(content_file.readlines())) return line_list @@ -25,7 +26,7 @@ def test_parser_doesnt_crash(): } } - cfg = easymapping.HaproxyConfigGenerator(result, CERTS_FOLDER) + cfg = easymapping.HaproxyConfigGenerator(result) haproxy_config = cfg.generate(line_list) assert len(haproxy_config) > 0 @@ -47,10 +48,7 @@ def test_parser_finds_services(): } } - if os.path.exists(CERT_FILE): - os.remove(CERT_FILE) - - cfg = easymapping.HaproxyConfigGenerator(result, CERTS_FOLDER) + cfg = easymapping.HaproxyConfigGenerator(result) haproxy_config = cfg.generate(line_list) assert len(haproxy_config) > 0 @@ -58,8 +56,7 @@ def test_parser_finds_services(): with open(path + "/expected/services.txt", 'r') as expected_file: assert expected_file.read() == haproxy_config - with open(CERT_FILE, 'r') as expected_file: - assert expected_file.read() == "Some PEM Certificate" + assert {"www.somehost.com.br.pem":"Some PEM Certificate"} == cfg.certs assert ['node-exporter.quantum.example.org'] == cfg.letsencrypt_hosts @@ -80,7 +77,7 @@ def test_parser_finds_services_changed_label(): if os.path.exists(CERT_FILE): os.remove(CERT_FILE) - cfg = easymapping.HaproxyConfigGenerator(result, CERTS_FOLDER) + cfg = easymapping.HaproxyConfigGenerator(result) haproxy_config = cfg.generate(line_list) assert len(haproxy_config) > 0 @@ -88,8 +85,7 @@ def test_parser_finds_services_changed_label(): with open(path + "/expected/services.txt", 'r') as expected_file: assert expected_file.read() == haproxy_config - with open(CERT_FILE, 'r') as expected_file: - assert expected_file.read() == "Some PEM Certificate" + assert {"www.somehost.com.br.pem":"Some PEM Certificate"} == cfg.certs assert ['node-exporter.quantum.example.org'] == cfg.letsencrypt_hosts @@ -109,7 +105,7 @@ def test_parser_finds_services_raw(): if os.path.exists(CERT_FILE): os.remove(CERT_FILE) - cfg = easymapping.HaproxyConfigGenerator(result, CERTS_FOLDER) + cfg = easymapping.HaproxyConfigGenerator(result) parsed_object = [ { @@ -217,7 +213,7 @@ def test_parser_static(): with open(path + "/fixtures/static.yml", 'r') as content_file: parsed = yaml.load(content_file.read(), Loader=yaml.FullLoader) - cfg = easymapping.HaproxyConfigGenerator(parsed, CERTS_FOLDER) + cfg = easymapping.HaproxyConfigGenerator(parsed) haproxy_config = cfg.generate() assert len(haproxy_config) > 0 @@ -295,7 +291,7 @@ def test_parser_tcp(): } } - cfg = easymapping.HaproxyConfigGenerator(result, CERTS_FOLDER) + cfg = easymapping.HaproxyConfigGenerator(result) haproxy_config = cfg.generate(line_list) # print(haproxy_config) @@ -315,7 +311,7 @@ def test_parser_multi_containers(): } } - cfg = easymapping.HaproxyConfigGenerator(result, CERTS_FOLDER) + cfg = easymapping.HaproxyConfigGenerator(result) haproxy_config = cfg.generate(line_list) assert len(haproxy_config) > 0 @@ -337,7 +333,7 @@ def test_parser_multiple_hosts(): } } - cfg = easymapping.HaproxyConfigGenerator(result, CERTS_FOLDER) + cfg = easymapping.HaproxyConfigGenerator(result) haproxy_config = cfg.generate(line_list) assert len(haproxy_config) > 0 @@ -358,7 +354,7 @@ def test_parser_redirect_ssl(): } } - cfg = easymapping.HaproxyConfigGenerator(result, CERTS_FOLDER) + cfg = easymapping.HaproxyConfigGenerator(result) haproxy_config = cfg.generate(line_list) assert len(haproxy_config) > 0 @@ -379,7 +375,7 @@ def test_parser_ssl_strict(): } } - cfg = easymapping.HaproxyConfigGenerator(result, CERTS_FOLDER) + cfg = easymapping.HaproxyConfigGenerator(result) haproxy_config = cfg.generate(line_list) assert len(haproxy_config) > 0 @@ -396,7 +392,7 @@ def test_parser_ssl_loose(): "ssl_mode": "loose", } - cfg = easymapping.HaproxyConfigGenerator(result, CERTS_FOLDER) + cfg = easymapping.HaproxyConfigGenerator(result) haproxy_config = cfg.generate(line_list) assert len(haproxy_config) > 0 @@ -418,7 +414,7 @@ def test_parser_ssl_letsencrypt(): } } - cfg = easymapping.HaproxyConfigGenerator(result, CERTS_FOLDER) + cfg = easymapping.HaproxyConfigGenerator(result) haproxy_config = cfg.generate(line_list) assert len(haproxy_config) > 0 @@ -427,7 +423,85 @@ def test_parser_ssl_letsencrypt(): assert expected_file.read() == haproxy_config assert ["test.example.org"] == cfg.letsencrypt_hosts + +def test_parser_finds_services_clone_to_ssl_raw(): + line_list = load_fixture("services-clone-to-ssl") + + result = { + "customerrors": False, + "letsencrypt": { + "email": LETSENCRYPT_EMAIL + }, + "stats": { + "port": 0 + } + } + + if os.path.exists(CERT_FILE): + os.remove(CERT_FILE) + + cfg = easymapping.HaproxyConfigGenerator(result) + + parsed_object = [ + { + "health-check":"", + "hosts":{ + "host2.local":{ + "containers":[ + "10.152.183.215:8080" + ], + "letsencrypt": False, + "redirect_ssl": False + }, + "valida.me":{ + "containers":[ + "10.152.183.62:8080" + ], + "letsencrypt": False, + "redirect_ssl": False + }, + "www.valida.me":{ + "containers":[ + "10.152.183.62:8080" + ], + "letsencrypt": False, + "redirect_ssl": False + } + }, + "mode":"http", + "port":"80", + "redirect":{ + + } + }, + { + "health-check":"ssl", + "hosts":{ + "host2.local":{ + "containers":[ + "10.152.183.215:8080" + ], + "letsencrypt": False, + "redirect_ssl": False + } + }, + "mode":"http", + "port":"443", + "redirect":{ + + }, + "ssl": True + } + ] + processed = list(cfg.parse(line_list)) + + assert parsed_object == processed + assert [] == cfg.letsencrypt_hosts + + + #test_parser_finds_services_raw() #test_parser_tcp() #test_parser_multiple_hosts() -#test_parser_ssl_letsencrypt() \ No newline at end of file +#test_parser_ssl_letsencrypt() +#test_parser_finds_services() \ No newline at end of file diff --git a/src/tests/test_static.py b/src/tests/test_static.py new file mode 100644 index 0000000..c21593c --- /dev/null +++ b/src/tests/test_static.py @@ -0,0 +1,73 @@ +import pytest +import os +from functions import Functions +from processor import ProcessorInterface +from processor import Static + +def test_processor_static(): + ProcessorInterface.static_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), "./fixtures/static.yml") + static = ProcessorInterface.factory("static") + + parsed_object = [ + { + "hosts":{ + "host1.com.br":{ + "containers":[ + "container:5000" + ], + "letsencrypt": True + }, + "host2.com.br":{ + "containers":[ + "other:3000" + ] + } + }, + "port":80, + "redirect":{ + "www.host1.com.br":"http://host1.com.br" + } + }, + { + "hosts":{ + "host1.com.br":{ + "containers":[ + "container:80" + ] + } + }, + "port":443, + "ssl": True + }, + { + "hosts":{ + "host3.com.br":{ + "containers":[ + "domain:8181" + ] + } + }, + "port":8080 + } + ] + hosts = [ + 'host1.com.br:80', + 'host2.com.br:80', + 'host1.com.br:443', + 'host3.com.br:8080' + ] + + assert static.get_letsencrypt_hosts() is None + assert static.get_parsed_object() == parsed_object + assert static.get_hosts() == hosts + + haproxy_cfg = static.get_haproxy_conf() + + assert haproxy_cfg == Functions.load(os.path.join(os.path.dirname(os.path.realpath(__file__)), "./expected/static.txt")) + + # @todo: Static doesnt populate this fields + assert static.get_letsencrypt_hosts() == [] + assert static.get_parsed_object() == parsed_object + assert static.get_hosts() == hosts + +# test_processor_static() \ No newline at end of file diff --git a/tests/fixtures/no-services b/tests/fixtures/no-services deleted file mode 100644 index a0f782d..0000000 --- a/tests/fixtures/no-services +++ /dev/null @@ -1,5 +0,0 @@ -swarm-prom_caddy={"com.docker.stack.image":"stefanprodan/caddy","com.docker.stack.namespace":"swarm-prom"} -swarm-prom_cadvisor={"com.docker.stack.image":"google/cadvisor","com.docker.stack.namespace":"swarm-prom"} -swarm-prom_dockerd-exporter={"com.docker.stack.image":"stefanprodan/caddy","com.docker.stack.namespace":"swarm-prom"} -swarm-prom_unsee={"com.docker.stack.image":"cloudflare/unsee:v0.8.0","com.docker.stack.namespace":"swarm-prom"} -test_proxy={"com.docker.stack.image":"byjg/easy-haproxy","com.docker.stack.namespace":"test"} diff --git a/tests/fixtures/services b/tests/fixtures/services deleted file mode 100644 index c77af21..0000000 --- a/tests/fixtures/services +++ /dev/null @@ -1,6 +0,0 @@ -portainer-agent_agent={"com.docker.stack.image":"portainer/agent:1.5.1","com.docker.stack.namespace":"portainer-agent"} -my-stack_agent={"easyhaproxy.agent.host":"agent.quantum.example.org","easyhaproxy.agent.localport":"9001","easyhaproxy.agent.mode":"tcp","easyhaproxy.agent.port":"31339","com.docker.stack.image":"portainer/agent:1.5.1","com.docker.stack.namespace":"my-stack","com.planetary-quantum":"monitoring"} -my-stack_cadvisor={"easyhaproxy.cadvisor.host":"cadvisor.quantum.example.org","easyhaproxy.cadvisor.localport":"8080","easyhaproxy.cadvisor.port":"31337","com.docker.stack.image":"gcr.io/google-containers/cadvisor:v0.34.0","com.docker.stack.namespace":"my-stack","com.planetary-quantum":"monitoring"} -my-stack_node-exporter={"easyhaproxy.exp.host":"node-exporter.quantum.example.org","easyhaproxy.exp.localport":"9100","easyhaproxy.exp.port":"31337","com.docker.stack.image":"stefanprodan/swarmprom-node-exporter:v0.16.0","com.docker.stack.namespace":"my-stack","com.planetary-quantum":"monitoring","easyhaproxy.exp.letsencrypt":"true"} -my-stack_reverse-proxy={"com.docker.stack.image":"quay.io/pngmbh/easy-haproxy:tcp-mode","com.docker.stack.namespace":"my-stack","com.planetary-quantum":"monitoring"} -some-service={"easyhaproxy.http.port":"80","easyhaproxy.http.host":"www.somehost.com.br","easyhaproxy.http.localport":"80","easyhaproxy.http.redirect":"{\"somehost.com.br\":\"https://www.somehost.com.br\",\"somehost.com\":\"https://www.somehost.com.br\",\"www.somehost.com\":\"https://www.somehost.com.br\",\"byjg.ca\":\"https://www.somehost.com.br\",\"www.byjg.ca\":\"https://www.somehost.com.br\"}","easyhaproxy.https.port":"443","easyhaproxy.https.host":"www.somehost.com.br","easyhaproxy.https.localport":"80","easyhaproxy.https.redirect":"{\"somehost.com.br\":\"https://www.somehost.com.br\",\"somehost.com\":\"https://www.somehost.com.br\",\"www.somehost.com\":\"https://www.somehost.com.br\",\"byjg.ca\":\"https://www.somehost.com.br\",\"www.byjg.ca\":\"https://www.somehost.com.br\"}","easyhaproxy.https.sslcert":"U29tZSBQRU0gQ2VydGlmaWNhdGU="} \ No newline at end of file diff --git a/tests/fixtures/services-changed-label b/tests/fixtures/services-changed-label deleted file mode 100644 index f4b0b14..0000000 --- a/tests/fixtures/services-changed-label +++ /dev/null @@ -1,6 +0,0 @@ -portainer-agent_agent={"com.docker.stack.image":"portainer/agent:1.5.1","com.docker.stack.namespace":"portainer-agent"} -my-stack_agent={"haproxy.agent.host":"agent.quantum.example.org","haproxy.agent.localport":"9001","haproxy.agent.mode":"tcp","haproxy.agent.port":"31339","com.docker.stack.image":"portainer/agent:1.5.1","com.docker.stack.namespace":"my-stack","com.planetary-quantum":"monitoring"} -my-stack_cadvisor={"haproxy.cadvisor.host":"cadvisor.quantum.example.org","haproxy.cadvisor.localport":"8080","haproxy.cadvisor.port":"31337","com.docker.stack.image":"gcr.io/google-containers/cadvisor:v0.34.0","com.docker.stack.namespace":"my-stack","com.planetary-quantum":"monitoring"} -my-stack_node-exporter={"haproxy.exp.host":"node-exporter.quantum.example.org","haproxy.exp.localport":"9100","haproxy.exp.port":"31337","com.docker.stack.image":"stefanprodan/swarmprom-node-exporter:v0.16.0","com.docker.stack.namespace":"my-stack","com.planetary-quantum":"monitoring","haproxy.exp.letsencrypt":"yes"} -my-stack_reverse-proxy={"com.docker.stack.image":"quay.io/pngmbh/easy-haproxy:tcp-mode","com.docker.stack.namespace":"my-stack","com.planetary-quantum":"monitoring"} -some-service={"haproxy.http.port":"80","haproxy.http.host":"www.somehost.com.br","haproxy.http.localport":"80","haproxy.http.redirect":"{\"somehost.com.br\":\"https://www.somehost.com.br\",\"somehost.com\":\"https://www.somehost.com.br\",\"www.somehost.com\":\"https://www.somehost.com.br\",\"byjg.ca\":\"https://www.somehost.com.br\",\"www.byjg.ca\":\"https://www.somehost.com.br\"}","haproxy.https.port":"443","haproxy.https.host":"www.somehost.com.br","haproxy.https.localport":"80","haproxy.https.redirect":"{\"somehost.com.br\":\"https://www.somehost.com.br\",\"somehost.com\":\"https://www.somehost.com.br\",\"www.somehost.com\":\"https://www.somehost.com.br\",\"byjg.ca\":\"https://www.somehost.com.br\",\"www.byjg.ca\":\"https://www.somehost.com.br\"}","haproxy.https.sslcert":"U29tZSBQRU0gQ2VydGlmaWNhdGU="} \ No newline at end of file diff --git a/tests/fixtures/services-letsencrypt b/tests/fixtures/services-letsencrypt deleted file mode 100644 index 2d616ff..0000000 --- a/tests/fixtures/services-letsencrypt +++ /dev/null @@ -1,4 +0,0 @@ -f5c645a0dfc6={"com.docker.compose.config-hash":"b95ebc27d0e61caa418cdfa632e05a656da9bbc3ea0d4603651971015f10a1f0","com.docker.compose.container-number":"1","com.docker.compose.depends_on":"","com.docker.compose.image":"sha256:bea3509d6fdc8d7f9ec95563a5a226dc977ee74fb3e980e0de70e892c2d38dde","com.docker.compose.oneoff":"False","com.docker.compose.project":"docker","com.docker.compose.project.config_files":"/workspace/docker-easy-haproxy/examples/docker/docker-compose-test.yml","com.docker.compose.project.working_dir":"/workspace/docker-easy-haproxy/examples/docker","com.docker.compose.service":"nginx","com.docker.compose.version":"2.8.0","easyhaproxy.http.host":"test.example.org","easyhaproxy.http.letsencrypt":"true","easyhaproxy.http.localport":"80","easyhaproxy.http.port":"80","easyhaproxy.http.redirect":"{\"google.helloworld.com\": \"www.google.com\"}","easyhaproxy.http.redirect_ssl":"true"} -bbd4d1854155={"com.docker.compose.config-hash":"3dc790bf2bea944359c75a40c45655bd868f1d85beb599d1ca797e8ea2c95ee4","com.docker.compose.container-number":"1","com.docker.compose.depends_on":"","com.docker.compose.image":"sha256:0fd95b1512c207048ab3fcc74032354f38143fbb8235ac2a47da903c98a58205","com.docker.compose.oneoff":"False","com.docker.compose.project":"docker","com.docker.compose.project.config_files":"/workspace/docker-easy-haproxy/examples/docker/docker-compose-test.yml","com.docker.compose.project.working_dir":"/workspace/docker-easy-haproxy/examples/docker","com.docker.compose.service":"haproxy","com.docker.compose.version":"2.8.0"} -b63438410b6a={"com.docker.compose.config-hash":"b95ebc27d0e61caa418cdfa632e05a656da9bbc3ea0d4603651971015f10a1f0","com.docker.compose.container-number":"2","com.docker.compose.depends_on":"","com.docker.compose.image":"sha256:bea3509d6fdc8d7f9ec95563a5a226dc977ee74fb3e980e0de70e892c2d38dde","com.docker.compose.oneoff":"False","com.docker.compose.project":"docker","com.docker.compose.project.config_files":"/workspace/docker-easy-haproxy/examples/docker/docker-compose-test.yml","com.docker.compose.project.working_dir":"/workspace/docker-easy-haproxy/examples/docker","com.docker.compose.service":"nginx","com.docker.compose.version":"2.8.0","easyhaproxy.http.host":"test.example.org","easyhaproxy.http.letsencrypt":"true","easyhaproxy.http.localport":"80","easyhaproxy.http.port":"80","easyhaproxy.http.redirect":"{\"google.helloworld.com\": \"www.google.com\"}","easyhaproxy.http.redirect_ssl":"true"} -83d57d592e26={"com.docker.compose.config-hash":"8c5871144f1e8a3aeca037207c02f011ab2c6e6c311a3773602b63541762dab5","com.docker.compose.container-number":"1","com.docker.compose.depends_on":"","com.docker.compose.image":"sha256:c4232396c715f3d568816c666e6d9b4a68ef6c36f6243b4007c4ee1d8335fd65","com.docker.compose.oneoff":"False","com.docker.compose.project":"docker","com.docker.compose.project.config_files":"/workspace/docker-easy-haproxy/examples/docker/docker-compose-test.yml","com.docker.compose.project.working_dir":"/workspace/docker-easy-haproxy/examples/docker","com.docker.compose.service":"static","com.docker.compose.version":"2.8.0","easyhaproxy.http.host":"test2.example.org","easyhaproxy.http.localport":"8080","easyhaproxy.http.port":"80","io.buildah.version":"1.21.0"} diff --git a/tests/fixtures/services-multi-containers b/tests/fixtures/services-multi-containers deleted file mode 100644 index 7e58c70..0000000 --- a/tests/fixtures/services-multi-containers +++ /dev/null @@ -1,2 +0,0 @@ -test_nginx.2.t5r94mjlced7m3t5orfjbowmm={"easyhaproxy.http.host":"www.helloworld.com","easyhaproxy.http.localport":"80","easyhaproxy.http.port":"19901","com.docker.stack.image":"stenote/nginx-hostname","com.docker.stack.namespace":"test"} -test_nginx.1.p552hqxkdx88narjrp5kouwb2={"easyhaproxy.http.host":"www.helloworld.com","easyhaproxy.http.localport":"80","easyhaproxy.http.port":"19901","com.docker.stack.image":"stenote/nginx-hostname","com.docker.stack.namespace":"test"} \ No newline at end of file diff --git a/tests/fixtures/services-multiple-hosts b/tests/fixtures/services-multiple-hosts deleted file mode 100644 index c04b29a..0000000 --- a/tests/fixtures/services-multiple-hosts +++ /dev/null @@ -1,3 +0,0 @@ -db79d3a910f4={"com.docker.compose.config-hash":"5bde40f52451521ad201e70de1291397376a0498a7c955624a609da3b60e7e8e","com.docker.compose.container-number":"1","com.docker.compose.depends_on":"","com.docker.compose.image":"sha256:ea39067705590557dd0cd951664a10970ceefcb725a3c1f43690d6d6d4ed5fce","com.docker.compose.oneoff":"False","com.docker.compose.project":"docker","com.docker.compose.project.config_files":"/workspace/docker-easy-haproxy/examples/docker/docker-compose-multi-containers.yml","com.docker.compose.project.working_dir":"/workspace/docker-easy-haproxy/examples/docker","com.docker.compose.service":"haproxy","com.docker.compose.version":"2.8.0"} -3e63154954b0={"com.docker.compose.config-hash":"4e0cbdd8372c6779863799e5021ed8178f74b55bd8e070abcdffaf87eb7baa36","com.docker.compose.container-number":"1","com.docker.compose.depends_on":"","com.docker.compose.image":"sha256:bea3509d6fdc8d7f9ec95563a5a226dc977ee74fb3e980e0de70e892c2d38dde","com.docker.compose.oneoff":"False","com.docker.compose.project":"docker","com.docker.compose.project.config_files":"/workspace/docker-easy-haproxy/examples/docker/docker-compose-multi-containers.yml","com.docker.compose.project.working_dir":"/workspace/docker-easy-haproxy/examples/docker","com.docker.compose.service":"nginx","com.docker.compose.version":"2.8.0","easyhaproxy.http.host":"hello.com\n, www.helloworld.com\n","easyhaproxy.http.localport":"80","easyhaproxy.http.port":"19901","easyhaproxy.http.redirect":"{\"google.helloworld.com\": \"www.google.com\"}"} -eb294c110eb1={"com.docker.compose.config-hash":"4e0cbdd8372c6779863799e5021ed8178f74b55bd8e070abcdffaf87eb7baa36","com.docker.compose.container-number":"2","com.docker.compose.depends_on":"","com.docker.compose.image":"sha256:bea3509d6fdc8d7f9ec95563a5a226dc977ee74fb3e980e0de70e892c2d38dde","com.docker.compose.oneoff":"False","com.docker.compose.project":"docker","com.docker.compose.project.config_files":"/workspace/docker-easy-haproxy/examples/docker/docker-compose-multi-containers.yml","com.docker.compose.project.working_dir":"/workspace/docker-easy-haproxy/examples/docker","com.docker.compose.service":"nginx","com.docker.compose.version":"2.8.0","easyhaproxy.http.host":"hello.com\n, www.helloworld.com\n","easyhaproxy.http.localport":"80","easyhaproxy.http.port":"19901","easyhaproxy.http.redirect":"{\"google.helloworld.com\": \"www.google.com\"}"} \ No newline at end of file diff --git a/tests/fixtures/services-redirect-ssl b/tests/fixtures/services-redirect-ssl deleted file mode 100644 index 718450b..0000000 --- a/tests/fixtures/services-redirect-ssl +++ /dev/null @@ -1,3 +0,0 @@ -3571640c480a={"com.docker.compose.config-hash":"b3089bebef089144accf08bc189c2dbd527cb2ff2b44f0fd57f031b3258d9389","com.docker.compose.container-number":"1","com.docker.compose.depends_on":"","com.docker.compose.image":"sha256:c4232396c715f3d568816c666e6d9b4a68ef6c36f6243b4007c4ee1d8335fd65","com.docker.compose.oneoff":"False","com.docker.compose.project":"docker","com.docker.compose.project.config_files":"/workspace/docker-easy-haproxy/examples/docker/docker-compose.yml","com.docker.compose.project.working_dir":"/workspace/docker-easy-haproxy/examples/docker","com.docker.compose.service":"container2","com.docker.compose.version":"2.8.0","easyhaproxy.http.host":"host2.local","easyhaproxy.http.port":"80","easyhaproxy.http.redirect_ssl":"true","easyhaproxy.https.host":"host2.local","easyhaproxy.https.localport":"8080","easyhaproxy.https.port":"443","easyhaproxy.https.ssl":"true","io.buildah.version":"1.21.0"} -5b69bc7fea1b={"com.docker.compose.config-hash":"e2e9424b6ef2adfa33f750e3894446c9200f24674f5cf601ad60a87430e14314","com.docker.compose.container-number":"1","com.docker.compose.depends_on":"","com.docker.compose.image":"sha256:c4232396c715f3d568816c666e6d9b4a68ef6c36f6243b4007c4ee1d8335fd65","com.docker.compose.oneoff":"False","com.docker.compose.project":"docker","com.docker.compose.project.config_files":"/workspace/docker-easy-haproxy/examples/docker/docker-compose.yml","com.docker.compose.project.working_dir":"/workspace/docker-easy-haproxy/examples/docker","com.docker.compose.service":"container","com.docker.compose.version":"2.8.0","easyhaproxy.http.host":"host1.local","easyhaproxy.http.port":"80","easyhaproxy.http.redirect_ssl":"true","easyhaproxy.https.host":"host1.local","easyhaproxy.https.localport":"8080","easyhaproxy.https.port":"443","easyhaproxy.https.sslcert":"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZEVENDQXZXZ0F3SUJBZ0lVUmkrdzFaVmdlZWRUbE5JQXdxUUJNSnY2ZFhzd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0ZqRVVNQklHQTFVRUF3d0xhRzl6ZERFdWJHOWpZV3d3SGhjTk1qRXdPREV3TVRnME9UQTJXaGNOTXpFdwpPREE0TVRnME9UQTJXakFXTVJRd0VnWURWUVFEREF0b2IzTjBNUzVzYjJOaGJEQ0NBaUl3RFFZSktvWklodmNOCkFRRUJCUUFEZ2dJUEFEQ0NBZ29DZ2dJQkFNQkRBaExBeWdKdWFXNnc2ZmZpZ3pUQUFHWHBtRXowdEl4bjFrNFoKeDV3TjVycHYvcXUwUU1ZeitBdjJ1MWVPS0VLWmVhRlJWcFQwcjkzZFg3SXZiRVpIdDI1R1BpQnZsTEdxaGpLUgpQblNrLzdVOFhtc250dFVBVjdyVkVLMVVyZEZ3OC9Jd3JpUUMrZGhyMG1uWWZTRE1rdkJvTUZwZGhWTlRyYkFaCjFUQjZyUWpFN0FyME10OG15OTZYSm13cmNqSzJUaitFMnJnUElVejFlNWNla0ZZSURTQmF0bXcrMyt2citUNXgKRk5Ga0oybzMwVzVvOFpmbENKSnpyVmFpaHFRaWNzNlpLRGdwZjdpcVhNRml3V0lsaGRRcEd2eDVHZi9LRlRLOQpVYU9uUlp6L1grMkNlYkFGYVRIUjNrL1BZcHBXVGdCQkJ1UnZscEN3K3dkbmttdGVDMFNRUkY5MVFXVnI3ZWpvCjdLYU9sR0k1VnR2TVVzV3ZUZUFabXBheW1JYUFURVR1T0phWTBKVTExT21MZUQ5RE9qNUUyU1E3cUlYL3BGY3AKeHB6RzVqNGMrTWxndnhQMlZBa05UZUFYQ2FZaVBCUUg1WlpnMEhFMlduQjFLaExSRmxIZDRpSFFEMkdKNXlOLwo2ZkNGQmZaZktTZUs4SmF1d3hnV2tyYTUzT2NEcS9tS2QrREEvZEsrL3J1Rzd0cXdWZ0lhMDRIT3Bsek03TFlSCkdCMElyczkrbHI1L1BKYlFabVUwNzNNZG42Y1hBZzNwKzZ3dndGbERrUzV2MTNnQkRZTkh0RjYyYmM1NTFlZEYKWjZrR3pKN3dtR1JvODRhQlA3TXVSWmVSZUxPclNTNjdhMXdMZHpac01uUDFUSjd4OUxmcjlNS2wydURuUWRuWQpleDhEQWdNQkFBR2pVekJSTUIwR0ExVWREZ1FXQkJTUS9tdFpkNmg4ZW45WVFWSDZITzFQbFdXaXF6QWZCZ05WCkhTTUVHREFXZ0JTUS9tdFpkNmg4ZW45WVFWSDZITzFQbFdXaXF6QVBCZ05WSFJNQkFmOEVCVEFEQVFIL01BMEcKQ1NxR1NJYjNEUUVCQ3dVQUE0SUNBUUNoUVlOdWFoMyttVHBJQkRZeEdyalRKTnVPVElNYVd6TXlpMXRrZitMMApzRUd3cGJtQU8ybVdXUVlGN1dWTHNpOThQVUxoM2FkanQyaml1ZDlWbGFhQzZnbnduNVpvMStQaWxvOXNOTExXCjZpajArck40a3dJbS9wTnFpK2pEdXUyY3ZBdUhJd1pXZWg4YkVlLzVVQ3hvNGlobVdGUU44ZUo2VFVLQ3BoUkMKNkVvci9TU1paQlFIZ1BsMEJjaHpIT2t3dTdSM0xDbmRScXhqaEFvVmI5eVFPVitac21UZUpYdWx3TnpKMXVMdApUOE9JZ0lpRHBtQm83SFNOMkgwazNjaHgwMEFzalV5SjltbUFXUGVqRmUvS1hMUlBjVlpSMTdqaHpnZklCRXpzCk01V3RXRm0xYUhEalZ2Nk02aXRlVm02MUU5VCtrL00xMXJ1MWUyWXdzeFREdmI2eDA0bWNyTnU5c29xZGRCYnIKVmZwbHV1b1EvaEVBYlh0Rk5Qb1R5U3B6MGN3T3djSENvd1ZPTG1kS2d2SW1zelppTXlISEc4VkdHbVBoODhuNwp3VnhiMGdWMFA0Uk1yY01MZGVUZG41NVlRcjFDcUJyMzRlQjZvbDZBc2JUbTNWekJIUlZtRk5rc2wxbzVKQjV0CnRYTGdGL0c4L3J6Si80bTFQYVZ1eHJCN0R4VW1JazhFUGJTSVZrdlp2ZDdMQnpLd1E2SWZWYXVjZXdIZkVhalEKVklpZXhTTWlGYzdsdzNLbnhqT0haamY2Rk05VllnM05vKytHZEM5OXM3TGtJdUp3QU1MTnFUUTdIdmhuN1l2UAo0RmxTSWdjNnhqMFlrR1pFUWxiNW8vNW5hdUVxUVUwQUJndzZqdEk0TnhyTkxUNmNwN0NPNE0weElERWcvM1lECmFBPT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQotLS0tLUJFR0lOIFBSSVZBVEUgS0VZLS0tLS0KTUlJSlF3SUJBREFOQmdrcWhraUc5dzBCQVFFRkFBU0NDUzB3Z2drcEFnRUFBb0lDQVFEQVF3SVN3TW9DYm1sdQpzT24zNG9NMHdBQmw2WmhNOUxTTVo5Wk9HY2VjRGVhNmIvNnJ0RURHTS9nTDlydFhqaWhDbVhtaFVWYVU5Sy9kCjNWK3lMMnhHUjdkdVJqNGdiNVN4cW9ZeWtUNTBwUCsxUEY1cko3YlZBRmU2MVJDdFZLM1JjUFB5TUs0a0F2blkKYTlKcDJIMGd6Skx3YURCYVhZVlRVNjJ3R2RVd2VxMEl4T3dLOURMZkpzdmVseVpzSzNJeXRrNC9oTnE0RHlGTQo5WHVYSHBCV0NBMGdXclpzUHQvcjYvaytjUlRSWkNkcU45RnVhUEdYNVFpU2M2MVdvb2FrSW5MT21TZzRLWCs0CnFsekJZc0ZpSllYVUtScjhlUm4veWhVeXZWR2pwMFdjLzEvdGdubXdCV2t4MGQ1UHoyS2FWazRBUVFia2I1YVEKc1BzSFo1SnJYZ3RFa0VSZmRVRmxhKzNvNk95bWpwUmlPVmJiekZMRnIwM2dHWnFXc3BpR2dFeEU3amlXbU5DVgpOZFRwaTNnL1F6bytSTmtrTzZpRi82UlhLY2FjeHVZK0hQakpZTDhUOWxRSkRVM2dGd21tSWp3VUIrV1dZTkJ4Ck5scHdkU29TMFJaUjNlSWgwQTloaWVjamYrbndoUVgyWHlrbml2Q1dyc01ZRnBLMnVkem5BNnY1aW5mZ3dQM1MKdnY2N2h1N2FzRllDR3RPQnpxWmN6T3kyRVJnZENLN1BmcGErZnp5VzBHWmxOTzl6SForbkZ3SU42ZnVzTDhCWgpRNUV1YjlkNEFRMkRSN1JldG0zT2VkWG5SV2VwQnN5ZThKaGthUE9HZ1QrekxrV1hrWGl6cTBrdXUydGNDM2MyCmJESno5VXllOGZTMzYvVENwZHJnNTBIWjJIc2ZBd0lEQVFBQkFvSUNBUUMveFpiWjBjY3RxYWdzcXZhVk5URWUKZXExcStoZmFHdlBFWVFhWUhJcklFKzJpNVhjbkdjTEtjS2ZvZHhEakFuOFIvemdkT3A2Y01YMENWbi9Qb2hIawpBRUR0RTgrQVZ3d0FNMUZzT3dnTEhWR2FHejhxcnhCbFlkUWdIY3BtdWVJdTJQWGJDOGVIVUJpYVVPSXVoYXc1Ci9SUk1EQUMvQWkyc3NmaTdnT2p2VkU0b1F4UVcwUUcxS0dPT0FVSm4vdVlIdzJSRlkyVXUxcGlteE8ya0RPNTMKZ2N4bUMxV09ueUNIbUhhaVcvVWg3ejZKYW1mU000ZFh0VEpac2x5aDM3ZGhIS05iZzlWa1A3Q1FLQTRoTHpvcApoYmY1cVk2cmFyZ09OaW55MUhnTVB4cm13S3VVb3VKeU90TjB5QnR4akRDVU5hWFVCd2l5N3NOR1MrSDR2c3lCCjVQOUhoSUhTdHUrRlp0M0hHN0VJcUNuZGlhU0tEUzRqV2FWUUFiYm80bloyWnMyQkQreERlUFJDUlVxWDdyTTQKNFh6UElSV1dYbW1XZi83SWcyOUhicnA0YTlMY09tUTJsZUNKdGJhVEZTTjk2T0xVSjVFK2hRMHVsQ1pnQlZtUQpSQ1VZa0pQNGxPemJhS2R6anhnSE1ySHptNDVlVUZmOExpck94aTJ1eXhYSFFtRE51NGIzWDE4a3QzUGdVbVVtCjNkWHBsM2ZxU3lKYTdTQ1Y4Wk5CcnNyRHExRSt0aFl0dTkxUWJWU0d4SGQ5SHJOVmUzWGRMYk9DZFU5Q3VDNjkKTmdsem5hYTdzWkxxbXlLZWpUZkdzWTd4cldkTmNNUGw0cDRmY0lEL080RXBBU1pmb3JwVGVLTlQwWklmWlpldwpiMG1BUWVZWnFRTThpL3FNWU4vdUFRS0NBUUVBNXFnMXNSTk1jNlZkTS90UmdsYXNHWW94amdSQzJPcUFEWmdzCm1BWE1VSjNrRXJweXh0K2VDaW15OGlidVlwelJUSVE4ZkJUV1JrQ3RSWlhKNytLY0xWdGs5UVpJb0xiaHlOd2QKNEl4RVFaRnVVbGpEYnZTalRMU3ljc0h2bzY1aWJXSWZUTDdiZ1dsTEdnR3EvVU96ZkdzZ0g2Uzl3THA1RzMwRwo4RUx5akk1ZVRJWUlDcmZUbVZMK2M0NU1ScEVNS28rY3Z6OFB5c2lhT0ZUbjNjeXN3UFZkWWFlRUVxTVFqVTh3CklHTnNHWkx5dFk3QkFCQmNZMGxkcnRiYS9PK0Z2LytSSDd1VXR6UDd4cENJd0ZDeDgwWnpOK1dSeTlOdkk2M1UKenEzeUlCb1c5R3lBcEQyK1BMYVBOeGY3UUxUVUNoWTFaei9kWVJsdEtPeHYyQWE1Z1FLQ0FRRUExV0xXTnFwMApmaEIvWnRmU0VTaHhGTU04OWNqTjZBYXoxV0tMN3VUQm91OW9TSm54amtoa2FWNzZhY25UL2lxWHR4TU5nSGkxCmZJbURwVTNQdk0wWTRVZDJUNDdvSGM2UDFCclpQTi9HbVh5L3M2QkFFZFB3TGU3Sis0blRJU0hBZEdtcmgrYS8KNXBrdHUzMmc5bFdxZnR4ZWNGSVZTTFBXa3hUMFhLaU14cDFmZmtMK09hdnBNZ01GWks0MWlLczNkTlNoS1BvZwpMOEdTUGNQOXgveW43OFAyZUszTitQR2psQTZwUHpyQU55V1U3TjAvYm1IY0I5VEtQK3VkWVdjalZocnU3TVlOCndOckU0a0tkQzh2OGk3eDd0RGJ2Yjc5VCtGbzZQSWg1M3AwT3NuWnpBOFVSMFFOUit2RFF1ZlF1eWFqOFJFQysKWkc4WXlDS3N2azh5Z3dLQ0FRQS9mc1N4QjBmL2VlRXJZeDZ3QzUzNnRlRW9ZQ0hxeHJzVGd2V2JyOVRyeUZzMQprSi95QVRMblIwMWNmYjBYNW1WemM5K1dwTUhMdXhnMzFLRXZhU2xuRHdhK3NNa2pmTlN3ejI5bUZoYmdHZUhOCngyT2RVcmoxYjdURUJJRXNoTi9SanJaaEVSVXFEY3MvMEgrNmtuMkJYWmdOUGZPQ2I1TFJMMXpPblE5YUJBTVAKZThJUStVUEZyR1FoZVdXajgxL3ZBM081N2VreUFJRDd5dHU5WWcrWVdyTW5JODhtdGo3ak40NWZEQitBOXNQYgptUDJtUDlxKzlqNVUyQTZXbkhVc1FuVTMwQktEVUVzYUFVV3o4MExaWG1adlY4SUg0eDl3S2ZVd0pCQklLQVp6CnFMN005N1k3em1Ha1gvU3BmbDMwbk9KOGxzY2hhTGQxRVlsRVphMkJBb0lCQVFDeWUyNVQ0VFY1TUpGdjB6dVoKTUd1TmcxU2MvTzRGa24yZkVVT2NlV2pod1VCSDRjUGpUL2YxRHdXRHNOYUo5TlJieENyNTkzMU9BclBEYzVjOApBNDA0K1k0ak01UkJRa0tabGk5NHRIQW9kK2pjOVVCQjZUVXZKbGw1OVNsTXdDOTY3OXdTMjFaT0tuZlBLR0NYClNzWkdRRXNaeGY2Wmhoc0hnWEozZ2wvbHpVSlBtUGVPQTVZVlIrT2Q5LzA5S0lGRlRvalNmb3luaFZDdUt4NDkKeGI0dVZZbjJIT0o0eEowZlBUZ2hkQ0hNdnJtWGVlUVJqdmI4OGVhTm1xVlVFSEhGRnRnYjRma2xBNWZFN1JUeApCaGxpUkRCd1o3YlVrSU5LNnlWazluNkJUbnM1bU12UkxtZ2RuSnBZdkU3S0MwMkxUYlpiM0krajhDMFpVYStOCnF5N0RBb0lCQUFpZXJpYlM3V1VjbDJhQmxrbTUrVzdxTm0vSU5tNXp2bm9TUG82VjN3YTVoczZmOStDL2tiZEYKODdqUVBBL1lGZTN1UjJzQUo3c2xYNWV1Wks4V21mcEZtZ3psdTBzRXo4MU1MUS9XeXBadFp5dHlWdFd6QjJQdQpYQ1cxdGRTSDllSTJCbWhYZ29rSE5UTTQ4TmsveE9FTnJQL3NlWHJJeDVMSzBobkRIWm90dS96NitZU2tCOWhGCmNtMmZaeWdEMWRNTFg2bGlSaW14eUZZK2RJQ0pOQjk1SmlmVExXWW5XZUdkZGt3UHRYVWVHWEUxb2x6dk5rTEQKek16RTA5dWhreC9sUkpudGVPQkVaYWY4ME9CLzA5T2k5YjkvcnhZNTlkd3NINkdheExvVGZFS3VQbnZCVk1OUgpZa1UxNFd6UUtsZUZraUJKSTlsVnZuZmdHbk9sZ2cwPQotLS0tLUVORCBQUklWQVRFIEtFWS0tLS0t","io.buildah.version":"1.21.0"} -77094604be33={"com.docker.compose.config-hash":"b671dd783e367918c604400fe83cd2450c845a555bda6769bd1351bdcce3c565","com.docker.compose.container-number":"1","com.docker.compose.depends_on":"","com.docker.compose.image":"sha256:4e24fb65b607dc3b105ab5ec5945707ccbf50ac32605e7f4935440b453b7c45c","com.docker.compose.oneoff":"False","com.docker.compose.project":"docker","com.docker.compose.project.config_files":"/workspace/docker-easy-haproxy/examples/docker/docker-compose.yml","com.docker.compose.project.working_dir":"/workspace/docker-easy-haproxy/examples/docker","com.docker.compose.service":"haproxy","com.docker.compose.version":"2.8.0"} diff --git a/tests/fixtures/services-tcp b/tests/fixtures/services-tcp deleted file mode 100644 index 41f7410..0000000 --- a/tests/fixtures/services-tcp +++ /dev/null @@ -1,2 +0,0 @@ -test_agent={"easyhaproxy.agent.host":"agent.quantum.local","easyhaproxy.agent.localport":"9001","easyhaproxy.agent.mode":"tcp","easyhaproxy.agent.port":"31339","com.docker.stack.image":"portainer/agent:1.5.1","com.docker.stack.namespace":"test", "easyhaproxy.agent.health-check":"ssl"} -test_proxy={"com.docker.stack.image":"byjg/easy-haproxy:local","com.docker.stack.namespace":"test"}