diff --git a/.gitignore b/.gitignore index 7a63f2c..d760fc3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ .idea *~ -venv \ No newline at end of file +venv +.docker_data +__pycache__ diff --git a/Dockerfile b/Dockerfile index 5a6e50a..616f926 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,18 @@ -FROM haproxy:1.9-alpine +FROM haproxy:2.0.2-alpine -RUN apk add --no-cache bash python3 py-yaml +WORKDIR /scripts -COPY entrypoint.* / -COPY conf.d /etc/haproxy/conf.d -COPY assets/errors-custom/* /etc/haproxy/errors-custom/ +RUN apk add --no-cache bash python3 py-yaml supervisor docker -ENTRYPOINT [ "/entrypoint.sh" ] -CMD ["haproxy", "-f", "/etc/haproxy/haproxy.cfg"] \ No newline at end of file +COPY requirements.txt /scripts +RUN pip3 install --upgrade pip \ + && pip install -r requirements.txt + +COPY swarm.* /scripts/ +COPY static.* /scripts/ +COPY templates /scripts/templates/ +COPY easymapping /scripts/easymapping/ + +COPY assets / + +CMD ["/usr/bin/supervisord", "-n", "-c", "/etc/supervisord.conf" ] \ No newline at end of file diff --git a/README.md b/README.md index 92e8c48..0916837 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,141 @@ # Easy HAProxy -This Docker image will create dynamically the `haproxy.cfg` based on very simple Yaml. +This Docker image will create dynamically the `haproxy.cfg` based on the labels defined in docker containers or from +a simple Yaml instead docker # Features - Enable or disable Stats on port 1936 with custom password -- Simple mapping host => host:port -- Simple redirect host => host:port +- Discover and setup haproxy from Docker Tag +- Discover and setup haproxy redirect from Docker Tag +- Setup HAProxy CFG from a Yaml file. # Basic Usage -Create a yaml file in your machine called `easyconfig.cfg` and put the contents: +The Easy HAProxy will create the `haproxy.cfg` automatically based on the containers or from a YAML provided. + +The basic command line to run is: + +```bash +docker run -d \ + --name easy-haproxy-container \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -e DISCOVER="swarm|docker|static" \ + # + Environment Variables \ + # + ports mapped to the host \ + byjg/easy-haproxy +``` + +The mapping to `/var/run/docker.sock` is necessary to discover the docker containers and get the labels; + +The environment variables will setup the HAProxy. + +| Environment Variable | Description | +|----------------------|-------------------------------------------------------------------------------| +| DISCOVER | How `haproxy.cfg` will be created: `static`, `docker` or `swarm` | +| HAPROXY_USERNAME | (Optional) The HAProxy username to the statistics. Default: `admin` | +| HAPROXY_PASSWORD | The HAProxy password to the statistics. If not set disable stats. | +| HAPROXY_STATS_PORT | (Optional) The HAProxy port to the statistics. Default: `1936` | +| HAPROXY_CUSTOMERRORS | (Optional) If HAProxy will use custom HTML errors. true/false. Default: false | + + + +The environment variable `DISCOVER` will define where is located your containers (see below more details): +- docker +- swarm +- static + +# DISCOVER: `docker` + +This method will use a regular 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 discover will occur every minute. + +e.g.: + +```bash +docker create networkd easyhaproxy + +docker run --network easyhaproxy byjg/easyhaproxy + +docker run --network easyhaproxy myimage +``` + +# 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 discover will occur every minute. + +Important: easyhaproxy needs to be in the same network of the containers or otherwise will not access. + +## Tags to be attached in the Docker Container + +| Tag | Description | +|---------------------------------------------|---------------------------------------------------------------------------------------------------------| +| com.byjg.easyhaproxy.definitions | A Comma delimited list with the definitions. Each name requires the definition of the parameters below. | +| com.byjg.easyhaproxy.port.[definition] | (Optional) What is the port that the HAProxy will listen to. (Defaults to 80) | +| com.byjg.easyhaproxy.localport.[definition] | (Optional) What is the port that the container is listening. (Defaults to 80) | +| com.byjg.easyhaproxy.host.[definition] | What is the host that the HAProxy will listen to. | +| com.byjg.easyhaproxy.redirect.[definition] | (Optional) Host redirects from connections in the port defined above. | +| com.byjg.easyhaproxy.sslcert.[definition] | (Optional) Cert PEM Base64 encoded. | + + +Note: if you are deploying a stack set labels at the `deploy` level: + +```yaml +services: + foo: + deploy: + labels: + com.byjg.easyhaproxy.definitions: "http,https" + ... +``` + + +### Single Definition: + +```bash +docker run \ + -l com.byjg.easyhaproxy.definitions=http \ + -l com.byjg.easyhaproxy.port.http=80\ + -l com.byjg.easyhaproxy.host.http=byjg.com.br \ + .... +``` + +### Multiples Definitions on the same container: + +```bash +docker run \ + -l com.byjg.easyhaproxy.definitions=express,admin \ + + -l com.byjg.easyhaproxy.port.express=80 \ + -l com.byjg.easyhaproxy.localport.express=3000 \ + -l com.byjg.easyhaproxy.host.express=express.byjg.com.br \ + + -l com.byjg.easyhaproxy.port.admin=80 \ + -l com.byjg.easyhaproxy.localport.admin=3001 \ + -l com.byjg.easyhaproxy.host.admin=admin.byjg.com.br \ + .... \ + some/myimage +``` + +### Redirect Example: + +```bash +docker run \ + -l com.byjg.easyhaproxy.redirect.=www.byjg.com.br--http://byjg.com.br,byjg.com--http://byjg.com.br +``` + +# 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: @@ -30,7 +154,7 @@ easymapping: www.host1.com.br: http://host1.com.br - port: 443 - ssl_cert: /etc/easyconfig/mycert.pem + ssl_cert: BASE64_PEM_CERTIFICATE hosts: host1.com.br: container:80 @@ -39,24 +163,15 @@ easymapping: host3.com.br: domain:8181 ``` -Then run (remember to enable the proper ports): +Running: ```bash -docker run \ - -v /path/to/local:/etc/easyconfig \ - -p 80:80 \ - -p 8080:8080 \ - -p 1936:1936 \ - --name easy-haproxy-instance \ - -d byjg/easy-haproxy +docker run -v /my/config.yml:/etc/haproxy/easyconfig.yml .... byjg/easyhaproxy ``` - - # Mapping custom .cfg files -Just create a folder and put files with the extension .cfg. and map the volume to the container. -This will concatenate your config into the main haproxy.cfg +Map a folder containing valid HAProxy `.cfg` files to `/etc/haproxy/conf.d`. It will be concatenated to your HAProxy CFG. ```bash docker run \ @@ -65,48 +180,41 @@ docker run \ -d byjg/easy-haproxy ``` -Check if your config is ok: - -```bash -docker run \ - /* other parameters */ - -v /your/local/conf.d:/etc/haproxy/conf.d \ - -byjg/easy-haproxy -c -f /etc/haproxy/haproxy.cfg -``` - -# Docker Compose - -```yaml -version: "3.4" -services: - front: - image: byjg/easy-haproxy - volume: - - /path/to/local:/etc/easyconfig - ports: - - 80:80 - - 8080:8080 - - 1936:1936 -``` # Handling SSL -HaProxy can handle SSL for you. in this case add the parameter pointing to file containing -the pem of certificates and key in only one file: +You can attach a valid SSL certificate to the request. -``` - - port: 443 - ssl_cert: /etc/easyconfig/mycert.pem - hosts: - host1.com.br: container:80 +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----- ``` -Important: Different certificates need to be handled in different entries. +2. Convert it to BASE64 in a single line: + +```bash +cat single.pem | base64 -w0 +``` + +3. Use this string to define the label `com.byjg.easyhaproxy.sslcert.[definition]` # Setting Custom Errors -Map the volume : `/etc/haproxy/errors-custom/` and put a file named `ERROR_NUMBER.http` where ERROR_NUMBER -is the http error code (e.g. 503.http) +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 @@ -114,3 +222,4 @@ is the http error code (e.g. 503.http) docker build -t byjg/easy-haproxy . ``` + diff --git a/assets/etc/crontabs/root b/assets/etc/crontabs/root new file mode 100644 index 0000000..cabb0aa --- /dev/null +++ b/assets/etc/crontabs/root @@ -0,0 +1,9 @@ +# do daily/weekly/monthly maintenance +# min hour day month weekday command +*/15 * * * * run-parts /etc/periodic/15min +0 * * * * run-parts /etc/periodic/hourly +0 2 * * * run-parts /etc/periodic/daily +0 3 * * 6 run-parts /etc/periodic/weekly +0 5 1 * * run-parts /etc/periodic/monthly +* * * * * /scripts/haproxy-reload.sh + diff --git a/assets/etc/haproxy/certs/README.md b/assets/etc/haproxy/certs/README.md new file mode 100644 index 0000000..828a1d8 --- /dev/null +++ b/assets/etc/haproxy/certs/README.md @@ -0,0 +1,3 @@ +# Certs Folder + +Docker Easy HAProxy will save the SSL Certs here. diff --git a/conf.d/00-simple-comment.cfg b/assets/etc/haproxy/conf.d/00-simple-comment.cfg similarity index 100% rename from conf.d/00-simple-comment.cfg rename to assets/etc/haproxy/conf.d/00-simple-comment.cfg diff --git a/conf.d/README.md b/assets/etc/haproxy/conf.d/README.md similarity index 100% rename from conf.d/README.md rename to assets/etc/haproxy/conf.d/README.md diff --git a/assets/errors-custom/400.http b/assets/etc/haproxy/errors-custom/400.http similarity index 100% rename from assets/errors-custom/400.http rename to assets/etc/haproxy/errors-custom/400.http diff --git a/assets/errors-custom/403.http b/assets/etc/haproxy/errors-custom/403.http similarity index 100% rename from assets/errors-custom/403.http rename to assets/etc/haproxy/errors-custom/403.http diff --git a/assets/errors-custom/408.http b/assets/etc/haproxy/errors-custom/408.http similarity index 100% rename from assets/errors-custom/408.http rename to assets/etc/haproxy/errors-custom/408.http diff --git a/assets/errors-custom/500.http b/assets/etc/haproxy/errors-custom/500.http similarity index 100% rename from assets/errors-custom/500.http rename to assets/etc/haproxy/errors-custom/500.http diff --git a/assets/errors-custom/502.http b/assets/etc/haproxy/errors-custom/502.http similarity index 100% rename from assets/errors-custom/502.http rename to assets/etc/haproxy/errors-custom/502.http diff --git a/assets/errors-custom/503.http b/assets/etc/haproxy/errors-custom/503.http similarity index 100% rename from assets/errors-custom/503.http rename to assets/etc/haproxy/errors-custom/503.http diff --git a/assets/errors-custom/504.http b/assets/etc/haproxy/errors-custom/504.http similarity index 100% rename from assets/errors-custom/504.http rename to assets/etc/haproxy/errors-custom/504.http diff --git a/assets/etc/supervisord.conf b/assets/etc/supervisord.conf new file mode 100644 index 0000000..29e789b --- /dev/null +++ b/assets/etc/supervisord.conf @@ -0,0 +1,45 @@ +[unix_http_server] +file=/dev/shm/supervisor.sock ; (the path to the socket file) + +[supervisord] +logfile=/tmp/supervisord.log ; (main log file;default $CWD/supervisord.log) +logfile_maxbytes=50MB ; (max main logfile bytes b4 rotation;default 50MB) +logfile_backups=10 ; (num of main logfile rotation backups;default 10) +loglevel=info ; (log level;default info; others: debug,warn,trace) +pidfile=/tmp/supervisord.pid ; (supervisord pidfile;default supervisord.pid) +nodaemon=false ; (start in foreground if true;default false) +minfds=1024 ; (min. avail startup file descriptors;default 1024) +minprocs=200 ; (min. avail process descriptors;default 200) +user=root ; + +; the below section must remain in the config file for RPC +; (supervisorctl/web interface) to work, additional interfaces may be +; added by defining them in separate rpcinterface: sections +[rpcinterface:supervisor] +supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface + +[supervisorctl] +serverurl=unix:///dev/shm/supervisor.sock ; use a unix:// URL for a unix socket + +[program:haproxy] +command = /scripts/haproxy.sh +autostart=true +autorestart=false +priority=5 +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 + +[program:crond] +command=/usr/sbin/crond -f +autostart=true +autorestart=false +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 + +[eventlistener:exit_on_any_fatal] +command=/scripts/exit-event-listener.py +events=PROCESS_STATE_FATAL,PROCESS_STATE_EXITED,PROCESS_STATE_STOPPED diff --git a/assets/scripts/exit-event-listener.py b/assets/scripts/exit-event-listener.py new file mode 100755 index 0000000..9b1826f --- /dev/null +++ b/assets/scripts/exit-event-listener.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python + +import os +import signal + +from supervisor import childutils + +def main(): + while True: + headers, payload = childutils.listener.wait() + childutils.listener.ok() + events = ['PROCESS_STATE_FATAL', 'PROCESS_STATE_EXITED', 'PROCESS_STATE_STOPPED'] + if not (headers['eventname'] in events): + continue + os.kill(os.getppid(), signal.SIGTERM) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/assets/scripts/haproxy-reload.sh b/assets/scripts/haproxy-reload.sh new file mode 100755 index 0000000..df9f603 --- /dev/null +++ b/assets/scripts/haproxy-reload.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash + +cd /scripts + +RELOAD="true" + +if [[ "$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 [[ "$DISCOVER" == "docker" ]]; then + CONTAINERS=$(docker ps -q) + LABEL_PATH=".Config.Labels" + else + CONTAINERS=$(docker node ps $(docker node ls -q) --format "{{ .Name }}" --filter desired-state=running | cut -d. -f1 | sort | uniq) + LABEL_PATH=".Spec.Labels" + fi + + for container in ${CONTAINERS}; do + docker inspect --format "{{ json $LABEL_PATH }}" ${container} | xargs -I % echo ${container}=% >> ${CONTROL_FILE} + done + + if cmp -s ${CONTROL_FILE} ${CONTROL_FILE}.old ; then + RELOAD="false" + else + python3 swarm.py > /etc/haproxy/haproxy.cfg + fi +fi + +if cmp -s ${CONTROL_FILE} ${CONTROL_FILE}.old ; then + RELOAD="false" +fi + +if [[ ! -z "$1" ]]; then + echo "Initial configuration" + RELOAD="false" +fi + +if [[ "$RELOAD" == "true" ]]; then + echo "Reloading..." + /usr/local/sbin/haproxy -W -f /etc/haproxy/haproxy.cfg -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 new file mode 100755 index 0000000..89dd3a9 --- /dev/null +++ b/assets/scripts/haproxy.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +source /scripts/haproxy-reload.sh initial + +/usr/local/sbin/haproxy -W -f /etc/haproxy/haproxy.cfg -p /run/haproxy.pid -S /var/run/haproxy.sock + +while true; do + sleep 60 +done diff --git a/easymapping/__init__.py b/easymapping/__init__.py new file mode 100644 index 0000000..1b54af2 --- /dev/null +++ b/easymapping/__init__.py @@ -0,0 +1,12 @@ +from jinja2 import Environment, FileSystemLoader + + +class HaproxyConfigGenerator: + def __init__(self, mapping): + self.mapping = mapping + + def generate(self): + file_loader = FileSystemLoader('templates') + env = Environment(loader=file_loader) + template = env.get_template('haproxy.cfg.j2') + return template.render(data=self.mapping) diff --git a/entrypoint.py b/entrypoint.py deleted file mode 100644 index 7962ed2..0000000 --- a/entrypoint.py +++ /dev/null @@ -1,109 +0,0 @@ -import yaml -import sys - -if len(sys.argv) != 2: - print("You need to pass the easyconfig.cfg path") - exit(1) - - -def defaults(custom): - result = """ -defaults - log global - - timeout connect 3s - timeout client 10s - timeout server 10m -""" - if custom: - result += """ - errorfile 400 /etc/haproxy/errors-custom/400.http - errorfile 403 /etc/haproxy/errors-custom/403.http - errorfile 408 /etc/haproxy/errors-custom/408.http - errorfile 500 /etc/haproxy/errors-custom/500.http - errorfile 502 /etc/haproxy/errors-custom/502.http - errorfile 503 /etc/haproxy/errors-custom/503.http - errorfile 504 /etc/haproxy/errors-custom/504.http -""" - result += """ -global - log /dev/log local0 - maxconn 2000 - tune.ssl.default-dh-param 2048 -""" - return result - - -def stats(map): - return """ -frontend stats - bind *:{2} - mode http - stats enable - stats hide-version - stats realm Haproxy\ Statistics - stats uri / - stats auth {0}:{1} -# acl is_proxystats hdr(host) -i some.host.com -# default_backend srv_stats -# use_backend srv_stats if is_proxystats - default_backend srv_stats - -backend srv_stats - mode http - server Local 127.0.0.1:{2} -""".format(map["username"], map["password"], map["port"] if "port" in map else 1936) - - -def easymapping(o, salt): - port = o["port"] - ssl = " ssl crt " + o["ssl_cert"] if "ssl_cert" in o else "" - hosts = o["hosts"] if "hosts" in o else dict() - redir = o["redirect"] if "redirect" in o else dict() - - result = """ -frontend http_in_{0}_{1} - bind *:{0} {2} - mode http - -""".format(port, salt, ssl) - - for k in redir: - result += " redirect prefix " + redir[k] + " code 301 if { hdr(host) -i " + k + " }\n" - - result += "\n" - for k in hosts: - host = k.replace(".", "_") + "_{0}_{1}".format(port, salt) - result += " acl is_rule_{0}_1 hdr(host) -i {1}\n".format(host, k) - result += " acl is_rule_{0}_2 hdr(host) -i {1}:{2}\n".format(host, k, port) - result += " use_backend srv_{0} if is_rule_{0}_1 OR is_rule_{0}_2\n\n".format(host) - - for k in hosts: - host = k.replace(".", "_") + "_{0}_{1}".format(port, salt) - result += """ -backend srv_{0} - balance roundrobin - mode http - option forwardfor - http-request set-header X-Forwarded-Port %[dst_port]""".format(host) + """ - http-request add-header X-Forwarded-Proto https if { ssl_fc }""" + """ - server srv {0} check weight 1 -""".format(hosts[k]) - - return result - - -with open(sys.argv[1], 'r') as content_file: - parsed = yaml.load(content_file.read()) - -n = 0 - -print(defaults(parsed["customerrors"] if "customerrors" in parsed else False)) -if "stats" in parsed: - print(stats(parsed["stats"])) -if "easymapping" in parsed: - for k in parsed["easymapping"]: - n = n + 1 - print(easymapping(k, n)) - - diff --git a/entrypoint.sh b/entrypoint.sh deleted file mode 100755 index 9eb6d31..0000000 --- a/entrypoint.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env bash - -set -e - -echo " ______ _ _ _____ " -echo "| ____| | | | | /\ | __ \ " -echo "| |__ __ _ ___ _ _ | |__| | / \ | |__) | __ _____ ___ _ " -echo "| __| / _\` / __| | | | | __ | / /\ \ | ___/ '__/ _ \ \/ / | | |" -echo "| |___| (_| \__ \ |_| | | | | |/ ____ \| | | | | (_) > <| |_| |" -echo "|______\__,_|___/\__, | |_| |_/_/ \_\_| |_| \___/_/\_\\__, |" -echo " __/ | __/ |" -echo " |___/ |___/ " -echo "" - -HAPROXY_CFG="/etc/haproxy/haproxy.cfg" -EASYCONFIG_CFG="/etc/easyconfig/easyconfig.cfg" - -if [ ! -f "$EASYCONFIG_CFG" ] -then - echo "File '$EASYCONFIG_CFG' does not exist" - exit 1 -fi - - - -python3 /entrypoint.py "$EASYCONFIG_CFG" > $HAPROXY_CFG - - -/sbin/syslogd -O /proc/1/fd/1 -/docker-entrypoint.sh "$@" diff --git a/requirements.txt b/requirements.txt index 4818cc5..fa90f6f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,3 @@ -pyyaml \ No newline at end of file +pyyaml +docker +jinja2 \ No newline at end of file diff --git a/static.py b/static.py new file mode 100644 index 0000000..07054ef --- /dev/null +++ b/static.py @@ -0,0 +1,16 @@ +import yaml +import sys +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()) + +exit(0) diff --git a/swarm.py b/swarm.py new file mode 100644 index 0000000..a74cef1 --- /dev/null +++ b/swarm.py @@ -0,0 +1,74 @@ +import os +import json +import time +import base64 +import hashlib +from easymapping import HaproxyConfigGenerator + +# path = os.path.dirname(os.path.realpath(__file__)) +with open("/tmp/.docker_data", 'r') as content_file: + lineList = content_file.readlines() + +result = { + "easymapping": [], + "customerrors": True if os.getenv("HAPROXY_CUSTOMERRORS") == "true" else False +} +easymapping = dict() + +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", + } + +for line in lineList: + line = line.strip() + i = line.find("=") + container = line[:i] + jsonStr = line[i+1:] + d = json.loads(jsonStr) + + if "com.byjg.easyhaproxy.definitions" in d.keys(): + definitions = d["com.byjg.easyhaproxy.definitions"].split(",") + + for definition in definitions: + if "com.byjg.easyhaproxy.host." + definition not in d: + continue + + port = d["com.byjg.easyhaproxy.port." + definition] if "com.byjg.easyhaproxy.port." + definition in d else "80" + hash = hashlib.md5(d["com.byjg.easyhaproxy.sslcert." + definition].encode('utf-8')).hexdigest() if "com.byjg.easyhaproxy.sslcert." + definition in d else "" + + key = port+hash + + if key not in easymapping: + easymapping[key] = { + "port": port, + "hosts": dict(), + "redirect": dict(), + # "ssl_cert": "" + } + + easymapping[key]["hosts"][d["com.byjg.easyhaproxy.host." + definition]] = container + ":" + (d["com.byjg.easyhaproxy.localport." + definition] if "com.byjg.easyhaproxy.localport." + definition in d else "80") + + if "com.byjg.easyhaproxy.sslcert." + definition in d: + filename = '/etc/haproxy/certs/' + d["com.byjg.easyhaproxy.host." + definition] + "." + str(time.time()) + ".pem" + easymapping[key]["ssl_cert"] = filename + with open(filename, 'wb') as file: + file.write(base64.b64decode(d["com.byjg.easyhaproxy.sslcert." + definition])) + + if "com.byjg.easyhaproxy.redirect." + definition in d: + redirect = d["com.byjg.easyhaproxy.redirect." + definition] if "com.byjg.easyhaproxy.redirect." + definition in d else "" + for r in redirect.split(","): + r_parts = r.split("--") + easymapping[key]["redirect"][r_parts[0]] = r_parts[1] + + result["easymapping"] = easymapping.values() + + +cfg = HaproxyConfigGenerator(result) +print(cfg.generate()) + +# print(jsonStr) + + diff --git a/templates/haproxy.cfg.j2 b/templates/haproxy.cfg.j2 new file mode 100644 index 0000000..96862f3 --- /dev/null +++ b/templates/haproxy.cfg.j2 @@ -0,0 +1,68 @@ +defaults + log global + + timeout connect 3s + timeout client 10s + timeout server 10m +{% if data["customerrors"] %} + errorfile 400 /etc/haproxy/errors-custom/400.http + errorfile 403 /etc/haproxy/errors-custom/403.http + errorfile 408 /etc/haproxy/errors-custom/408.http + errorfile 500 /etc/haproxy/errors-custom/500.http + errorfile 502 /etc/haproxy/errors-custom/502.http + errorfile 503 /etc/haproxy/errors-custom/503.http + errorfile 504 /etc/haproxy/errors-custom/504.http +{% endif %} + +global + log /dev/log local0 + maxconn 2000 + tune.ssl.default-dh-param 2048 + +{% if "stats" in data %} +frontend stats + bind *:{{ data["stats"]["port"] | default(1936) }} + mode http + stats enable + stats hide-version + stats realm Haproxy\ Statistics + stats uri / + stats auth {{ data["stats"]["username"] }}:{{ data["stats"]["password"] }} +# acl is_proxystats hdr(host) -i some.host.com +# default_backend srv_stats +# use_backend srv_stats if is_proxystats + default_backend srv_stats + +backend srv_stats + mode http + server Local 127.0.0.1:{{ data["stats"]["port"] | default(1936) }} +{% endif %} + +{% for o in data["easymapping"] %} + {% set salt = loop.index %} +frontend http_in_{{ o["port"] }}_{{ salt }} + bind *:{{ o["port"] }} {{ " ssl crt " + o["ssl_cert"] if "ssl_cert" in o else "" }} + mode http + + {% for k in o["redirect"] -%} + redirect prefix {{ o["redirect"][k] }} code 301 if { hdr(host) -i {{ k }} } + {% endfor -%} + + {% for k in o["hosts"] %} + {% set host = k.replace(".", "_") + "_{0}_{1}".format(o["port"], salt) %} + acl is_rule_{{ host }}_1 hdr(host) -i {{ k }} + acl is_rule_{{ host }}_2 hdr(host) -i {{ k }}:{{ o["port"] }} + use_backend srv_{{ host }} if is_rule_{{ host }}_1 OR is_rule_{{ host }}_2 + {% endfor %} + + {% for k in o["hosts"] %} + {% set host = k.replace(".", "_") + "_{0}_{1}".format(o["port"], salt) %} +backend srv_{{ host }} + 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 {{ o["hosts"][k] }} check weight 1 + {% endfor %} +{% endfor %}