From 869fdead3d7b3aba1bf3ae09db5936e34b2386fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joao=20Gilberto=20Magalh=C3=A3es?= Date: Fri, 5 Jun 2020 09:11:51 -0500 Subject: [PATCH 01/14] Added .travis.yml file --- .travis.yml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..e870c1e --- /dev/null +++ b/.travis.yml @@ -0,0 +1,9 @@ +language: python +python: + - "3.7" + +install: + - pip install -r requirements.txt + +script: + - pytest -s tests/ \ No newline at end of file From f5b097616ecb3917b0eb87f43a2e7e7109790721 Mon Sep 17 00:00:00 2001 From: till Date: Sat, 30 May 2020 15:50:22 +0200 Subject: [PATCH 02/14] Update: support tcp-mode (in frontend/backend) --- README.md | 14 ++++++++++++++ swarm.py | 2 ++ templates/haproxy.cfg.j2 | 17 ++++++++++++++--- 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 28cb482..a03d9ea 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,7 @@ Important: easyhaproxy needs to be in the same network of the containers or othe | 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.mode.[definition] | (Optional) Is this http or tcp mode in HAProxy. (Defaults to http) | | 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. | @@ -126,6 +127,19 @@ docker run \ some/myimage ``` +### TLS passthrough + +Used to pass on SSL-termination to a backend: + +```bash +docker run \ + -l com.byjg.easyhaproxy.defintions=tcp-service \ + -l com.byjg.easyhaproxy.mode.tcp-service=tcp \ + -l com.byjg.easyhaproxy.port.tcp-service=443 + .... \ + some/tcp-service +``` + ### Redirect Example: ```bash diff --git a/swarm.py b/swarm.py index a74cef1..961f634 100644 --- a/swarm.py +++ b/swarm.py @@ -36,6 +36,7 @@ for line in lineList: if "com.byjg.easyhaproxy.host." + definition not in d: continue + mode = d["com.byjg.easyhaproxy.mode." + definition] if "com.byjg.easyhaproxy.mode." + definition in d else "http" 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 "" @@ -43,6 +44,7 @@ for line in lineList: if key not in easymapping: easymapping[key] = { + "mode": mode, "port": port, "hosts": dict(), "redirect": dict(), diff --git a/templates/haproxy.cfg.j2 b/templates/haproxy.cfg.j2 index 96862f3..732e48b 100644 --- a/templates/haproxy.cfg.j2 +++ b/templates/haproxy.cfg.j2 @@ -39,10 +39,17 @@ backend srv_stats {% endif %} {% for o in data["easymapping"] %} + {% set mode = o["mode"] or "http" %} {% set salt = loop.index %} -frontend http_in_{{ o["port"] }}_{{ salt }} +frontend {{ mode }}_in_{{ o["port"] }}_{{ salt }} bind *:{{ o["port"] }} {{ " ssl crt " + o["ssl_cert"] if "ssl_cert" in o else "" }} - mode http + mode {{ mode }} + + {% if mode == "tcp" -%} + option tcplog + tcp-request inspect-delay 5s + tcp-request content accept if { req.ssl_hello_type 1 } + {% endif -%} {% for k in o["redirect"] -%} redirect prefix {{ o["redirect"][k] }} code 301 if { hdr(host) -i {{ k }} } @@ -59,10 +66,14 @@ frontend http_in_{{ o["port"] }}_{{ salt }} {% set host = k.replace(".", "_") + "_{0}_{1}".format(o["port"], salt) %} backend srv_{{ host }} balance roundrobin - mode http + mode {{ mode }} + +{% if mode == "http" %} option forwardfor http-request set-header X-Forwarded-Port %[dst_port] http-request add-header X-Forwarded-Proto https if { ssl_fc } +{% endif %} + server srv {{ o["hosts"][k] }} check weight 1 {% endfor %} {% endfor %} From fcc79806020bce1eca7d9e1b036ce4acf6bdc9d6 Mon Sep 17 00:00:00 2001 From: till Date: Sat, 30 May 2020 17:08:49 +0200 Subject: [PATCH 03/14] Fix: inspect services (vs. containers/tasks) --- assets/scripts/haproxy-reload.sh | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/assets/scripts/haproxy-reload.sh b/assets/scripts/haproxy-reload.sh index df9f603..ecee161 100755 --- a/assets/scripts/haproxy-reload.sh +++ b/assets/scripts/haproxy-reload.sh @@ -18,14 +18,18 @@ else if [[ "$DISCOVER" == "docker" ]]; then CONTAINERS=$(docker ps -q) 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" - fi - for container in ${CONTAINERS}; do - docker inspect --format "{{ json $LABEL_PATH }}" ${container} | xargs -I % echo ${container}=% >> ${CONTROL_FILE} - done + 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" From 5f9eac4b8a459ec3ea6f9290f1d9375166c20ca5 Mon Sep 17 00:00:00 2001 From: till Date: Sat, 30 May 2020 19:37:12 +0200 Subject: [PATCH 04/14] Fix: send haproxy logs to stdout --- templates/haproxy.cfg.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/haproxy.cfg.j2 b/templates/haproxy.cfg.j2 index 732e48b..f0293e3 100644 --- a/templates/haproxy.cfg.j2 +++ b/templates/haproxy.cfg.j2 @@ -15,7 +15,7 @@ defaults {% endif %} global - log /dev/log local0 + log stdout format raw local0 info maxconn 2000 tune.ssl.default-dh-param 2048 From 08de132450434d8dc426c31880bb87deb1223be0 Mon Sep 17 00:00:00 2001 From: till Date: Sat, 30 May 2020 22:24:40 +0200 Subject: [PATCH 05/14] Refactor: tried to refactor code - move scripts to assets/scripts (and updated build) - put most logic into HaproxyConfigGenerator - created DockerLabelHandler for all label handling from previous code - added unit tests and fixtures to cover HaproxyConfigGenerator and DockerLabelHandler - added a Makefile with build (for docker build) and test targets --- Dockerfile | 2 - Makefile | 7 ++ README.md | 42 ++++----- static.py => assets/scripts/static.py | 0 assets/scripts/swarm.py | 24 +++++ easymapping/__init__.py | 128 +++++++++++++++++++++++++- pytest.ini | 2 + setup.py | 20 ++++ swarm.py | 76 --------------- templates/bind.j2 | 8 ++ templates/frontend-mode-http.j2 | 11 +++ templates/frontend-mode-tcp.j2 | 6 ++ templates/haproxy.cfg.j2 | 50 ++++------ tests/__init__.py | 0 tests/context.py | 5 + tests/fixtures/no-services | 5 + tests/fixtures/services | 5 + tests/fixtures/services-tcp | 2 + tests/fixtures/static.yml | 23 +++++ tests/test_labels.py | 32 +++++++ tests/test_parser.py | 70 ++++++++++++++ 21 files changed, 387 insertions(+), 131 deletions(-) create mode 100644 Makefile rename static.py => assets/scripts/static.py (100%) create mode 100644 assets/scripts/swarm.py create mode 100644 pytest.ini create mode 100644 setup.py delete mode 100644 swarm.py create mode 100644 templates/bind.j2 create mode 100644 templates/frontend-mode-http.j2 create mode 100644 templates/frontend-mode-tcp.j2 create mode 100644 tests/__init__.py create mode 100644 tests/context.py create mode 100644 tests/fixtures/no-services create mode 100644 tests/fixtures/services create mode 100644 tests/fixtures/services-tcp create mode 100644 tests/fixtures/static.yml create mode 100644 tests/test_labels.py create mode 100644 tests/test_parser.py diff --git a/Dockerfile b/Dockerfile index 4827b7d..9720c23 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,8 +8,6 @@ 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/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d721ef9 --- /dev/null +++ b/Makefile @@ -0,0 +1,7 @@ +.PHONY: build +build: + docker build -t byjg/easy-haproxy -t byjg/easy-haproxy:local . + +.PHONY: test +test: + pytest tests/ diff --git a/README.md b/README.md index a03d9ea..09373a8 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,24 @@ -# Easy HAProxy +# Easy HAProxy -This Docker image will create dynamically the `haproxy.cfg` based on the labels defined in docker containers or from -a simple Yaml instead docker +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 -- Discover and setup haproxy from Docker Tag +- Discover and setup haproxy from Docker Tag - Discover and setup haproxy redirect from Docker Tag -- Setup HAProxy CFG from a Yaml file. +- Setup HAProxy CFG from a Yaml file. # Basic Usage -The Easy HAProxy will create the `haproxy.cfg` automatically based on the containers or from a YAML provided. +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 \ +docker run -d \ --name easy-haproxy-container \ -v /var/run/docker.sock:/var/run/docker.sock \ -e DISCOVER="swarm|docker|static" \ @@ -29,7 +29,7 @@ docker run -d \ 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. +The environment variables will setup the HAProxy. {:.table} | Environment Variable | Description | @@ -49,9 +49,9 @@ The environment variable `DISCOVER` will define where is located your containers # DISCOVER: docker -This method will use a regular docker installation to discover the containers and configure the HAProxy. +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 only requirement is that containers and easy-haproxy must be in the same docker network. The discover will occur every minute. @@ -67,12 +67,12 @@ 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. +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. +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 @@ -163,12 +163,12 @@ customerrors: true # Optional (default false) easymapping: - port: 80 - hosts: + hosts: host1.com.br: container:5000 host2.com.br: other:3000 redirect: www.host1.com.br: http://host1.com.br - + - port: 443 ssl_cert: BASE64_PEM_CERTIFICATE hosts: @@ -187,10 +187,10 @@ docker run -v /my/config.yml:/etc/haproxy/easyconfig.yml .... byjg/easyhaproxy # Mapping custom .cfg files -Map a folder containing valid HAProxy `.cfg` files to `/etc/haproxy/conf.d`. It will be concatenated to your 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 \ +docker run \ /* other parameters */ -v /your/local/conf.d:/etc/haproxy/conf.d \ -d byjg/easy-haproxy @@ -199,9 +199,9 @@ docker run \ # Handling SSL -You can attach a valid SSL certificate to the request. +You can attach a valid SSL certificate to the request. -1. First Create a single PEM file including CA. +1. First Create a single PEM file including CA. ```bash cat example.com.crt example.com.key > single.pem @@ -229,8 +229,8 @@ cat single.pem | base64 -w0 # 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) +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 diff --git a/static.py b/assets/scripts/static.py similarity index 100% rename from static.py rename to assets/scripts/static.py diff --git a/assets/scripts/swarm.py b/assets/scripts/swarm.py new file mode 100644 index 0000000..c7741ec --- /dev/null +++ b/assets/scripts/swarm.py @@ -0,0 +1,24 @@ +import os +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 = { + "customerrors": True if os.getenv("HAPROXY_CUSTOMERRORS") == "true" else False +} + +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", + } + +cfg = HaproxyConfigGenerator(result) +print(cfg.generate(lineList)) + +# print(jsonStr) + + diff --git a/easymapping/__init__.py b/easymapping/__init__.py index 1b54af2..d0d0e69 100644 --- a/easymapping/__init__.py +++ b/easymapping/__init__.py @@ -1,12 +1,138 @@ +import base64 +import hashlib from jinja2 import Environment, FileSystemLoader +import json +import time + + +class DockerLabelHandler: + def __init__(self, label): + self.__label_base = label + + + def create(self, key): + if isinstance(key, str): + return "{}.{}".format(self.__label_base, key) + + return "{}.{}".format(self.__label_base, ".".join(key)) + + + def get(self, label, default_value = ""): + if self.has_label(label): + return self.__data[label] + return default_value + + + def set_data(self, data): + self.__data = data + + + def has_label(self, label): + if label in self.__data: + return True + return False class HaproxyConfigGenerator: def __init__(self, mapping): self.mapping = mapping + self.label = DockerLabelHandler("com.byjg.easyhaproxy") + + + def generate(self, lineList = []): + # static? + if len(lineList) > 0: + self.mapping["easymapping"] = self.__parse(lineList) + + # still 'None' -> default to [] for jinja2 + if self.mapping["easymapping"] is None: + self.mapping["easymapping"] = [] - def generate(self): file_loader = FileSystemLoader('templates') env = Environment(loader=file_loader) + env.trim_blocks = True + env.lstrip_blocks = True + env.rstrip_blocks = True template = env.get_template('haproxy.cfg.j2') return template.render(data=self.mapping) + + + def __parse(self, lineList): + easymapping = dict() + + for line in lineList: + line = line.strip() + i = line.find("=") + container = line[:i] + jsonStr = line[i+1:] + d = json.loads(jsonStr) + + if self.label.create("definitions") not in d.keys(): + continue + + self.label.set_data(d) + + definitions = d[self.label.create("definitions")].split(",") + for definition in definitions: + mode = self.label.get( + self.label.create(["mode", definition]), + "http" + ) + + # TODO: we can ignore "host" in TCP, but it would break the template + host_label = self.label.create(["host", definition]) + if not self.label.has_label(host_label): + continue + + port = self.label.get( + self.label.create(["port", definition]), + "80" + ) + + if self.label.create(["sslcert", definition]) in d: + hash = hashlib.md5( + d[self.label.create(["sslcert", definition])].encode('utf-8') + ).hexdigest() + else: + hash = "" + + key = port+hash + + if key not in easymapping: + easymapping[key] = { + "mode": mode, + "port": port, + "hosts": dict(), + "redirect": dict(), + } + + # TODO: this could use `EXPOSE` from `Dockerfile`? + ct_port = self.label.get( + self.label.create(["localport", definition]), + "80" + ) + + easymapping[key]["hosts"][d[host_label]] = "{}:{}".format(container, ct_port) + + # handle SSL + ssl_label = self.label.create(["sslcert", definition]) + if self.label.has_label(ssl_label): + filename = "/etc/haproxy/certs/{}.{}.pem".format( + d[ssl_label], str(time.time()) + ) + easymapping[key]["ssl_cert"] = filename + with open(filename, 'wb') as file: + file.write( + base64.b64decode(d[ssl_label]) + ) + + # handle redirects + redirect = self.label.get( + self.label.create(["redirect", definition]) + ) + if len(redirect) > 0: + for r in redirect.split(","): + r_parts = r.split("--") + easymapping[key]["redirect"][r_parts[0]] = r_parts[1] + + return easymapping.values() diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..3acaa4f --- /dev/null +++ b/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +addopts = -v -p no:warnings diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..f19e91e --- /dev/null +++ b/setup.py @@ -0,0 +1,20 @@ +from setuptools import setup, find_packages + + +with open('README.md') as f: + readme = f.read() + +with open('LICENSE') as f: + license = f.read() + +setup( + name='easymapping', + version='0.1.0', + description='HAProxy label based routing', + long_description=readme, + author='', + author_email='', + url='', + license=license, + packages=find_packages(exclude=('tests', 'docs')) +) diff --git a/swarm.py b/swarm.py deleted file mode 100644 index 961f634..0000000 --- a/swarm.py +++ /dev/null @@ -1,76 +0,0 @@ -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 - - mode = d["com.byjg.easyhaproxy.mode." + definition] if "com.byjg.easyhaproxy.mode." + definition in d else "http" - 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] = { - "mode": mode, - "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/bind.j2 b/templates/bind.j2 new file mode 100644 index 0000000..abbd254 --- /dev/null +++ b/templates/bind.j2 @@ -0,0 +1,8 @@ + {% if "ssl_cert" in o %} + bind *:{{ o["port"] }} ssl crt {{ o["ssl_cert"] }} + {% elif "h2" in o and o["h2"] %} + bind *:{{ o["port"] }} proto h2 + option http-use-htx + {% else %} + bind *:{{ o["port"] }} + {% endif %} diff --git a/templates/frontend-mode-http.j2 b/templates/frontend-mode-http.j2 new file mode 100644 index 0000000..5d903e6 --- /dev/null +++ b/templates/frontend-mode-http.j2 @@ -0,0 +1,11 @@ + 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 %} diff --git a/templates/frontend-mode-tcp.j2 b/templates/frontend-mode-tcp.j2 new file mode 100644 index 0000000..012d865 --- /dev/null +++ b/templates/frontend-mode-tcp.j2 @@ -0,0 +1,6 @@ + mode tcp + option tcplog + log global +{% set backend = (o["hosts"]|first) %} + default_backend srv_{{ backend.replace(".", "_") + "_{0}_{1}".format(o["port"], salt) }} + diff --git a/templates/haproxy.cfg.j2 b/templates/haproxy.cfg.j2 index f0293e3..6b502d6 100644 --- a/templates/haproxy.cfg.j2 +++ b/templates/haproxy.cfg.j2 @@ -1,3 +1,8 @@ +global + log stdout format raw local0 info + maxconn 2000 + tune.ssl.default-dh-param 2048 + defaults log global @@ -14,11 +19,6 @@ defaults errorfile 504 /etc/haproxy/errors-custom/504.http {% endif %} -global - log stdout format raw local0 info - maxconn 2000 - tune.ssl.default-dh-param 2048 - {% if "stats" in data %} frontend stats bind *:{{ data["stats"]["port"] | default(1936) }} @@ -37,43 +37,31 @@ backend srv_stats mode http server Local 127.0.0.1:{{ data["stats"]["port"] | default(1936) }} {% endif %} - -{% for o in data["easymapping"] %} +{% for o in data["easymapping"] -%} {% set mode = o["mode"] or "http" %} {% set salt = loop.index %} + frontend {{ mode }}_in_{{ o["port"] }}_{{ salt }} - bind *:{{ o["port"] }} {{ " ssl crt " + o["ssl_cert"] if "ssl_cert" in o else "" }} - mode {{ mode }} + {% include "bind.j2" %} + {% if mode == "http" %} + {% include "frontend-mode-http.j2" %} + {% else %} + {% include "frontend-mode-tcp.j2" %} + {% endif %} - {% if mode == "tcp" -%} - option tcplog - tcp-request inspect-delay 5s - tcp-request content accept if { req.ssl_hello_type 1 } - {% endif -%} - - {% 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"] %} + {% for k in o["hosts"] -%} {% set host = k.replace(".", "_") + "_{0}_{1}".format(o["port"], salt) %} backend srv_{{ host }} balance roundrobin mode {{ mode }} - -{% if mode == "http" %} + {% if mode == "http" %} option forwardfor http-request set-header X-Forwarded-Port %[dst_port] http-request add-header X-Forwarded-Proto https if { ssl_fc } -{% endif %} - + {% elif mode == "tcp" %} + option tcp-check + tcp-check connect + {% endif %} server srv {{ o["hosts"][k] }} check weight 1 {% endfor %} {% endfor %} diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/context.py b/tests/context.py new file mode 100644 index 0000000..67dda65 --- /dev/null +++ b/tests/context.py @@ -0,0 +1,5 @@ +import os +import sys +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) + +import easymapping diff --git a/tests/fixtures/no-services b/tests/fixtures/no-services new file mode 100644 index 0000000..a0f782d --- /dev/null +++ b/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/tests/fixtures/services b/tests/fixtures/services new file mode 100644 index 0000000..2d832e7 --- /dev/null +++ b/tests/fixtures/services @@ -0,0 +1,5 @@ +portainer-agent_agent={"com.docker.stack.image":"portainer/agent:1.5.1","com.docker.stack.namespace":"portainer-agent"} +my-stack_agent={"com.byjg.easyhaproxy.definitions":"agent","com.byjg.easyhaproxy.host.agent":"agent.quantum.example.org","com.byjg.easyhaproxy.localport.agent":"9001","com.byjg.easyhaproxy.mode.agent":"tcp","com.byjg.easyhaproxy.port.agent":"31339","com.docker.stack.image":"portainer/agent:1.5.1","com.docker.stack.namespace":"my-stack","com.planetary-quantum":"monitoring"} +my-stack_cadvisor={"com.byjg.easyhaproxy.definitions":"cadvisor","com.byjg.easyhaproxy.host.cadvisor":"cadvisor.quantum.example.org","com.byjg.easyhaproxy.localport.cadvisor":"8080","com.byjg.easyhaproxy.port.cadvisor":"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={"com.byjg.easyhaproxy.definitions":"exp","com.byjg.easyhaproxy.host.exp":"node-exporter.quantum.example.org","com.byjg.easyhaproxy.localport.exp":"9100","com.byjg.easyhaproxy.port.exp":"31337","com.docker.stack.image":"stefanprodan/swarmprom-node-exporter:v0.16.0","com.docker.stack.namespace":"my-stack","com.planetary-quantum":"monitoring"} +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"} diff --git a/tests/fixtures/services-tcp b/tests/fixtures/services-tcp new file mode 100644 index 0000000..8520b59 --- /dev/null +++ b/tests/fixtures/services-tcp @@ -0,0 +1,2 @@ +test_agent={"com.byjg.easyhaproxy.definitions":"agent","com.byjg.easyhaproxy.host.agent":"agent.quantum.local","com.byjg.easyhaproxy.localport.agent":"9001","com.byjg.easyhaproxy.mode.agent":"tcp","com.byjg.easyhaproxy.port.agent":"31339","com.docker.stack.image":"portainer/agent:1.5.1","com.docker.stack.namespace":"test","com.planetary-quantum":"monitoring"} +test_proxy={"com.docker.stack.image":"byjg/easy-haproxy:local","com.docker.stack.namespace":"test"} diff --git a/tests/fixtures/static.yml b/tests/fixtures/static.yml new file mode 100644 index 0000000..dd660d5 --- /dev/null +++ b/tests/fixtures/static.yml @@ -0,0 +1,23 @@ +stats: + username: admin + password: test123 + port: 1936 # Optional (default 1936) + +customerrors: true # Optional (default false) + +easymapping: + - port: 80 + hosts: + host1.com.br: container:5000 + host2.com.br: other:3000 + redirect: + www.host1.com.br: http://host1.com.br + + - port: 443 + ssl_cert: BASE64_PEM_CERTIFICATE + hosts: + host1.com.br: container:80 + + - port: 8080 + hosts: + host3.com.br: domain:8181 diff --git a/tests/test_labels.py b/tests/test_labels.py new file mode 100644 index 0000000..f56c212 --- /dev/null +++ b/tests/test_labels.py @@ -0,0 +1,32 @@ +from .context import easymapping +import json +import pytest + +def test_label_generation(): + label = easymapping.DockerLabelHandler("foo") + + assert label.create("bar") == "foo.bar" + assert label.create(["bar", "foobar"]) == "foo.bar.foobar" + + +def test_label_data(): + label = easymapping.DockerLabelHandler("base") + label.set_data(json.loads('{"base.definitions":"h2"}')) + + label_name = label.create("definitions") + assert label_name == "base.definitions" + assert label.has_label(label_name) + assert label.get(label_name) == "h2" + + +def test_label_complex_key(): + label = easymapping.DockerLabelHandler("till") + + data = dict() + data["till.definitions"] = "h2" + data["till.host.h2"] = "fqdn.example.org" + data["till.mode.h2"] = "tcp" + label.set_data(json.loads(json.dumps(data))) + + assert label.get(label.create(["host", "h2"])) == "fqdn.example.org" + assert label.get(label.create(["mode", "h2"])) == "tcp" diff --git a/tests/test_parser.py b/tests/test_parser.py new file mode 100644 index 0000000..edf0357 --- /dev/null +++ b/tests/test_parser.py @@ -0,0 +1,70 @@ +from .context import easymapping +import pytest +import os +import yaml + + +def load_fixture(file): + path = os.path.dirname(os.path.realpath(__file__)) + with open(path + "/fixtures/" + file, 'r') as content_file: + lineList = content_file.readlines() + + return lineList + + +def test_parser_doesnt_crash(): + lineList = load_fixture("no-services") + + result = { + "customerrors": False + } + + cfg = easymapping.HaproxyConfigGenerator(result) + haproxy_config = cfg.generate(lineList) + assert len(haproxy_config) > 0 + assert "frontend" not in haproxy_config + assert "backend" not in haproxy_config + + +def test_parser_finds_services(): + lineList = load_fixture("services") + + result = { + "customerrors": False + } + + cfg = easymapping.HaproxyConfigGenerator(result) + haproxy_config = cfg.generate(lineList) + assert len(haproxy_config) > 0 + assert "mode tcp" in haproxy_config + assert "mode http" in haproxy_config + + assert "frontend tcp_in_31339_1" in haproxy_config + assert "frontend http_in_31337_2" in haproxy_config + + +def test_parser_static(): + path = os.path.dirname(os.path.realpath(__file__)) + with open(path + "/fixtures/static.yml", 'r') as content_file: + parsed = yaml.load(content_file.read(), Loader=yaml.FullLoader) + + cfg = easymapping.HaproxyConfigGenerator(parsed) + haproxy_config = cfg.generate() + assert len(haproxy_config) > 0 + + # assert on auth on stats + assert "stats auth admin:test123" in haproxy_config + + # assert that we found redirect + assert "redirect prefix http://host1.com.br code 301 if { hdr(host) -i www.host1.com.br }" in haproxy_config + + # assert that we found the services + assert "frontend http_in_80_1" in haproxy_config + assert "bind *:80" + assert "frontend http_in_443_2" in haproxy_config + assert "bind *:443" + assert "frontend http_in_8080_3" in haproxy_config + assert "bind :*8080" + + # verify ssl config + assert "frontend http_in_443_2\n bind *:443 ssl crt BASE64_PEM_CERTIFICATE" in haproxy_config From 75deebc783f9174fad857eafe285a03d13b78694 Mon Sep 17 00:00:00 2001 From: till Date: Fri, 5 Jun 2020 16:27:02 +0200 Subject: [PATCH 06/14] Update: more tests and ssl health-check - restructure more tests for the "mode http" - add tests tests for "mode tcp" - add the ability to ssl health check on a backend (in mode tcp) --- README.md | 5 +++- easymapping/__init__.py | 6 +++++ templates/haproxy.cfg.j2 | 4 +-- tests/fixtures/services-tcp | 2 +- tests/test_parser.py | 54 +++++++++++++++++++++++++++++++------ 5 files changed, 59 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 09373a8..7417dc7 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ Important: easyhaproxy needs to be in the same network of the containers or othe | 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. | - +| com.byjg.easyhaproxy.health-check.[definition] | (Optional) `ssl`, enable health check via SSL in `mode tcp` (Defaults to "empty") | Note: if you are deploying a stack set labels at the `deploy` level: @@ -135,11 +135,14 @@ Used to pass on SSL-termination to a backend: docker run \ -l com.byjg.easyhaproxy.defintions=tcp-service \ -l com.byjg.easyhaproxy.mode.tcp-service=tcp \ + -l com.byjg.easyhaproxy.health-check.tcp-service=ssl \ -l com.byjg.easyhaproxy.port.tcp-service=443 .... \ some/tcp-service ``` + - enable health-check via SSL on the backend with the optional `health-check` label + ### Redirect Example: ```bash diff --git a/easymapping/__init__.py b/easymapping/__init__.py index d0d0e69..a26b99f 100644 --- a/easymapping/__init__.py +++ b/easymapping/__init__.py @@ -101,6 +101,7 @@ class HaproxyConfigGenerator: if key not in easymapping: easymapping[key] = { "mode": mode, + "health-check": "", "port": port, "hosts": dict(), "redirect": dict(), @@ -112,6 +113,11 @@ class HaproxyConfigGenerator: "80" ) + easymapping[key]["health-check"] = self.label.get( + self.label.create(["health-check", definition]), + "" + ) + easymapping[key]["hosts"][d[host_label]] = "{}:{}".format(container, ct_port) # handle SSL diff --git a/templates/haproxy.cfg.j2 b/templates/haproxy.cfg.j2 index 6b502d6..192bbd4 100644 --- a/templates/haproxy.cfg.j2 +++ b/templates/haproxy.cfg.j2 @@ -60,8 +60,8 @@ backend srv_{{ host }} http-request add-header X-Forwarded-Proto https if { ssl_fc } {% elif mode == "tcp" %} option tcp-check - tcp-check connect + tcp-check connect{{ " ssl" if o["health-check"] == "ssl" }} {% endif %} - server srv {{ o["hosts"][k] }} check weight 1 + server srv {{ o["hosts"][k] }} check weight 1{{ " verify none" if o["health-check"] == "ssl" }} {% endfor %} {% endfor %} diff --git a/tests/fixtures/services-tcp b/tests/fixtures/services-tcp index 8520b59..64bb7a6 100644 --- a/tests/fixtures/services-tcp +++ b/tests/fixtures/services-tcp @@ -1,2 +1,2 @@ -test_agent={"com.byjg.easyhaproxy.definitions":"agent","com.byjg.easyhaproxy.host.agent":"agent.quantum.local","com.byjg.easyhaproxy.localport.agent":"9001","com.byjg.easyhaproxy.mode.agent":"tcp","com.byjg.easyhaproxy.port.agent":"31339","com.docker.stack.image":"portainer/agent:1.5.1","com.docker.stack.namespace":"test","com.planetary-quantum":"monitoring"} +test_agent={"com.byjg.easyhaproxy.definitions":"agent","com.byjg.easyhaproxy.host.agent":"agent.quantum.local","com.byjg.easyhaproxy.localport.agent":"9001","com.byjg.easyhaproxy.mode.agent":"tcp","com.byjg.easyhaproxy.port.agent":"31339","com.docker.stack.image":"portainer/agent:1.5.1","com.docker.stack.namespace":"test", "com.byjg.easyhaproxy.health-check.agent":"ssl"} test_proxy={"com.docker.stack.image":"byjg/easy-haproxy:local","com.docker.stack.namespace":"test"} diff --git a/tests/test_parser.py b/tests/test_parser.py index edf0357..0851833 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -59,12 +59,50 @@ def test_parser_static(): assert "redirect prefix http://host1.com.br code 301 if { hdr(host) -i www.host1.com.br }" in haproxy_config # assert that we found the services - assert "frontend http_in_80_1" in haproxy_config - assert "bind *:80" - assert "frontend http_in_443_2" in haproxy_config - assert "bind *:443" - assert "frontend http_in_8080_3" in haproxy_config - assert "bind :*8080" + frontend_http_cfg = "frontend http_in_80_1\n" + frontend_http_cfg += " bind *:80" + assert frontend_http_cfg in haproxy_config - # verify ssl config - assert "frontend http_in_443_2\n bind *:443 ssl crt BASE64_PEM_CERTIFICATE" in haproxy_config + frontend_https_cfg = "frontend http_in_443_2\n" + frontend_https_cfg += " bind *:443" + assert frontend_https_cfg in haproxy_config + + # print(haproxy_config) + frontend_http8080_cfg = "frontend http_in_8080_3\n" + frontend_http8080_cfg += " bind *:8080" + assert frontend_http8080_cfg in haproxy_config + + + # verify ssl config with certificate + frontend_ssl_cfg = "frontend http_in_443_2\n" + frontend_ssl_cfg += " bind *:443 ssl crt BASE64_PEM_CERTIFICATE" + assert frontend_ssl_cfg in haproxy_config + + +def test_parser_tcp(): + lineList = load_fixture("services-tcp") + + result = { + "customerrors": False + } + + cfg = easymapping.HaproxyConfigGenerator(result) + haproxy_config = cfg.generate(lineList) + # print(haproxy_config) + + frontend_cfg = "frontend tcp_in_31339_1\n" + frontend_cfg += " bind *:31339\n" + frontend_cfg += " mode tcp\n" + frontend_cfg += " option tcplog\n" + frontend_cfg += " log global\n" + frontend_cfg += " default_backend srv_agent_quantum_local_31339_1\n\n" + assert frontend_cfg in haproxy_config + + backend_cfg = "backend srv_agent_quantum_local_31339_1\n" + backend_cfg += " balance roundrobin\n" + backend_cfg += " mode tcp\n" + backend_cfg += " option tcp-check\n" + backend_cfg += " tcp-check connect ssl\n" + backend_cfg += " server srv test_agent:9001 check weight 1 verify none" + + assert backend_cfg in haproxy_config From ddfa2e34ae91fb27a86ad8811a0852c81c6310d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joao=20Gilberto=20Magalh=C3=A3es?= Date: Sun, 7 Jun 2020 21:26:37 -0500 Subject: [PATCH 07/14] Minor adjustment in Static unittest. --- tests/expected/static.txt | 95 +++++++++++++++++++++++++++++++++++++++ tests/fixtures/static.yml | 2 +- tests/test_parser.py | 28 +----------- 3 files changed, 98 insertions(+), 27 deletions(-) create mode 100644 tests/expected/static.txt diff --git a/tests/expected/static.txt b/tests/expected/static.txt new file mode 100644 index 0000000..52dd0b6 --- /dev/null +++ b/tests/expected/static.txt @@ -0,0 +1,95 @@ +global + log stdout format raw local0 info + maxconn 2000 + tune.ssl.default-dh-param 2048 + +defaults + log global + + timeout connect 3s + timeout client 10s + timeout server 10m + 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 + +frontend stats + bind *:1936 + mode http + stats enable + stats hide-version + stats realm Haproxy\ Statistics + stats uri / + stats auth admin:test123 +# 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:1936 + +frontend http_in_80_1 + bind *:80 + mode http + redirect prefix http://host1.com.br code 301 if { hdr(host) -i www.host1.com.br } + + acl is_rule_host1_com_br_80_1_1 hdr(host) -i host1.com.br + acl is_rule_host1_com_br_80_1_2 hdr(host) -i host1.com.br:80 + use_backend srv_host1_com_br_80_1 if is_rule_host1_com_br_80_1_1 OR is_rule_host1_com_br_80_1_2 + + acl is_rule_host2_com_br_80_1_1 hdr(host) -i host2.com.br + acl is_rule_host2_com_br_80_1_2 hdr(host) -i host2.com.br:80 + use_backend srv_host2_com_br_80_1 if is_rule_host2_com_br_80_1_1 OR is_rule_host2_com_br_80_1_2 + +backend srv_host1_com_br_80_1 + 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 container:5000 check weight 1 +backend srv_host2_com_br_80_1 + 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 other:3000 check weight 1 + +frontend http_in_443_2 + bind *:443 ssl crt /etc/haproxy/certs/mycert.pem + mode http + + acl is_rule_host1_com_br_443_2_1 hdr(host) -i host1.com.br + acl is_rule_host1_com_br_443_2_2 hdr(host) -i host1.com.br:443 + use_backend srv_host1_com_br_443_2 if is_rule_host1_com_br_443_2_1 OR is_rule_host1_com_br_443_2_2 + +backend srv_host1_com_br_443_2 + 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 container:80 check weight 1 + +frontend http_in_8080_3 + bind *:8080 + mode http + + acl is_rule_host3_com_br_8080_3_1 hdr(host) -i host3.com.br + acl is_rule_host3_com_br_8080_3_2 hdr(host) -i host3.com.br:8080 + use_backend srv_host3_com_br_8080_3 if is_rule_host3_com_br_8080_3_1 OR is_rule_host3_com_br_8080_3_2 + +backend srv_host3_com_br_8080_3 + 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 domain:8181 check weight 1 diff --git a/tests/fixtures/static.yml b/tests/fixtures/static.yml index dd660d5..82ea2e2 100644 --- a/tests/fixtures/static.yml +++ b/tests/fixtures/static.yml @@ -14,7 +14,7 @@ easymapping: www.host1.com.br: http://host1.com.br - port: 443 - ssl_cert: BASE64_PEM_CERTIFICATE + ssl_cert: /etc/haproxy/certs/mycert.pem hosts: host1.com.br: container:80 diff --git a/tests/test_parser.py b/tests/test_parser.py index 0851833..c9ff08e 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -52,32 +52,8 @@ def test_parser_static(): haproxy_config = cfg.generate() assert len(haproxy_config) > 0 - # assert on auth on stats - assert "stats auth admin:test123" in haproxy_config - - # assert that we found redirect - assert "redirect prefix http://host1.com.br code 301 if { hdr(host) -i www.host1.com.br }" in haproxy_config - - # assert that we found the services - frontend_http_cfg = "frontend http_in_80_1\n" - frontend_http_cfg += " bind *:80" - assert frontend_http_cfg in haproxy_config - - frontend_https_cfg = "frontend http_in_443_2\n" - frontend_https_cfg += " bind *:443" - assert frontend_https_cfg in haproxy_config - - # print(haproxy_config) - frontend_http8080_cfg = "frontend http_in_8080_3\n" - frontend_http8080_cfg += " bind *:8080" - assert frontend_http8080_cfg in haproxy_config - - - # verify ssl config with certificate - frontend_ssl_cfg = "frontend http_in_443_2\n" - frontend_ssl_cfg += " bind *:443 ssl crt BASE64_PEM_CERTIFICATE" - assert frontend_ssl_cfg in haproxy_config - + with open(path + "/expected/static.txt", 'r') as expected_file: + assert expected_file.read() == haproxy_config def test_parser_tcp(): lineList = load_fixture("services-tcp") From 74413882fa7fc13b12d3e7a416cece3d7eca6a17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joao=20Gilberto=20Magalh=C3=A3es?= Date: Sun, 7 Jun 2020 22:31:00 -0500 Subject: [PATCH 08/14] Fix SSL Parsing; Changed all tests; --- easymapping/__init__.py | 18 ++++--- tests/expected/no-services.txt | 12 +++++ tests/expected/services-tcp.txt | 26 +++++++++ tests/expected/services.txt | 95 +++++++++++++++++++++++++++++++++ tests/fixtures/services | 1 + tests/test_parser.py | 53 +++++++++--------- 6 files changed, 169 insertions(+), 36 deletions(-) create mode 100644 tests/expected/no-services.txt create mode 100644 tests/expected/services-tcp.txt create mode 100644 tests/expected/services.txt diff --git a/easymapping/__init__.py b/easymapping/__init__.py index a26b99f..d570085 100644 --- a/easymapping/__init__.py +++ b/easymapping/__init__.py @@ -2,8 +2,7 @@ import base64 import hashlib from jinja2 import Environment, FileSystemLoader import json -import time - +import os class DockerLabelHandler: def __init__(self, label): @@ -34,9 +33,12 @@ class DockerLabelHandler: class HaproxyConfigGenerator: - def __init__(self, mapping): + def __init__(self, mapping, ssl_cert_folder="/etc/haproxy/certs"): self.mapping = mapping self.label = DockerLabelHandler("com.byjg.easyhaproxy") + self.ssl_cert_folder = ssl_cert_folder + self.ssl_cert_increment = 0 + os.makedirs(self.ssl_cert_folder, exist_ok=True) def generate(self, lineList = []): @@ -89,14 +91,13 @@ class HaproxyConfigGenerator: "80" ) + hash = "" if self.label.create(["sslcert", definition]) in d: hash = hashlib.md5( d[self.label.create(["sslcert", definition])].encode('utf-8') ).hexdigest() - else: - hash = "" - key = port+hash + key = port if not hash else port + "_" + hash if key not in easymapping: easymapping[key] = { @@ -123,8 +124,9 @@ class HaproxyConfigGenerator: # handle SSL ssl_label = self.label.create(["sslcert", definition]) if self.label.has_label(ssl_label): - filename = "/etc/haproxy/certs/{}.{}.pem".format( - d[ssl_label], str(time.time()) + self.ssl_cert_increment += 1 + filename = "{}/{}.{}.pem".format( + self.ssl_cert_folder, d[host_label], str(self.ssl_cert_increment) ) easymapping[key]["ssl_cert"] = filename with open(filename, 'wb') as file: diff --git a/tests/expected/no-services.txt b/tests/expected/no-services.txt new file mode 100644 index 0000000..f5ecac3 --- /dev/null +++ b/tests/expected/no-services.txt @@ -0,0 +1,12 @@ +global + log stdout format raw local0 info + maxconn 2000 + tune.ssl.default-dh-param 2048 + +defaults + log global + + timeout connect 3s + timeout client 10s + timeout server 10m + diff --git a/tests/expected/services-tcp.txt b/tests/expected/services-tcp.txt new file mode 100644 index 0000000..66cb16d --- /dev/null +++ b/tests/expected/services-tcp.txt @@ -0,0 +1,26 @@ +global + log stdout format raw local0 info + maxconn 2000 + tune.ssl.default-dh-param 2048 + +defaults + log global + + timeout connect 3s + timeout client 10s + timeout server 10m + + +frontend tcp_in_31339_1 + bind *:31339 + mode tcp + option tcplog + log global + default_backend srv_agent_quantum_local_31339_1 + +backend srv_agent_quantum_local_31339_1 + balance roundrobin + mode tcp + option tcp-check + tcp-check connect ssl + server srv test_agent:9001 check weight 1 verify none diff --git a/tests/expected/services.txt b/tests/expected/services.txt new file mode 100644 index 0000000..3e38ed6 --- /dev/null +++ b/tests/expected/services.txt @@ -0,0 +1,95 @@ +global + log stdout format raw local0 info + maxconn 2000 + tune.ssl.default-dh-param 2048 + +defaults + log global + + timeout connect 3s + timeout client 10s + timeout server 10m + + +frontend tcp_in_31339_1 + bind *:31339 + mode tcp + option tcplog + log global + default_backend srv_agent_quantum_example_org_31339_1 + +backend srv_agent_quantum_example_org_31339_1 + balance roundrobin + mode tcp + option tcp-check + tcp-check connect + server srv my-stack_agent:9001 check weight 1 + +frontend http_in_31337_2 + bind *:31337 + mode http + + acl is_rule_cadvisor_quantum_example_org_31337_2_1 hdr(host) -i cadvisor.quantum.example.org + acl is_rule_cadvisor_quantum_example_org_31337_2_2 hdr(host) -i cadvisor.quantum.example.org:31337 + use_backend srv_cadvisor_quantum_example_org_31337_2 if is_rule_cadvisor_quantum_example_org_31337_2_1 OR is_rule_cadvisor_quantum_example_org_31337_2_2 + + acl is_rule_node-exporter_quantum_example_org_31337_2_1 hdr(host) -i node-exporter.quantum.example.org + acl is_rule_node-exporter_quantum_example_org_31337_2_2 hdr(host) -i node-exporter.quantum.example.org:31337 + use_backend srv_node-exporter_quantum_example_org_31337_2 if is_rule_node-exporter_quantum_example_org_31337_2_1 OR is_rule_node-exporter_quantum_example_org_31337_2_2 + +backend srv_cadvisor_quantum_example_org_31337_2 + 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 my-stack_cadvisor:8080 check weight 1 +backend srv_node-exporter_quantum_example_org_31337_2 + 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 my-stack_node-exporter:9100 check weight 1 + +frontend http_in_80_3 + bind *:80 + mode http + redirect prefix https://www.somehost.com.br code 301 if { hdr(host) -i somehost.com.br } + redirect prefix https://www.somehost.com.br code 301 if { hdr(host) -i somehost.com } + redirect prefix https://www.somehost.com.br code 301 if { hdr(host) -i www.somehost.com } + redirect prefix https://www.somehost.com.br code 301 if { hdr(host) -i byjg.ca } + redirect prefix https://www.somehost.com.br code 301 if { hdr(host) -i www.byjg.ca } + + acl is_rule_www_somehost_com_br_80_3_1 hdr(host) -i www.somehost.com.br + acl is_rule_www_somehost_com_br_80_3_2 hdr(host) -i www.somehost.com.br:80 + use_backend srv_www_somehost_com_br_80_3 if is_rule_www_somehost_com_br_80_3_1 OR is_rule_www_somehost_com_br_80_3_2 + +backend srv_www_somehost_com_br_80_3 + 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 some-service:80 check weight 1 + +frontend http_in_443_4 + bind *:443 ssl crt /tmp/www.somehost.com.br.1.pem + mode http + redirect prefix https://www.somehost.com.br code 301 if { hdr(host) -i somehost.com.br } + redirect prefix https://www.somehost.com.br code 301 if { hdr(host) -i somehost.com } + redirect prefix https://www.somehost.com.br code 301 if { hdr(host) -i www.somehost.com } + redirect prefix https://www.somehost.com.br code 301 if { hdr(host) -i byjg.ca } + redirect prefix https://www.somehost.com.br code 301 if { hdr(host) -i www.byjg.ca } + + acl is_rule_www_somehost_com_br_443_4_1 hdr(host) -i www.somehost.com.br + acl is_rule_www_somehost_com_br_443_4_2 hdr(host) -i www.somehost.com.br:443 + use_backend srv_www_somehost_com_br_443_4 if is_rule_www_somehost_com_br_443_4_1 OR is_rule_www_somehost_com_br_443_4_2 + +backend srv_www_somehost_com_br_443_4 + 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 some-service:80 check weight 1 diff --git a/tests/fixtures/services b/tests/fixtures/services index 2d832e7..c213a8d 100644 --- a/tests/fixtures/services +++ b/tests/fixtures/services @@ -3,3 +3,4 @@ my-stack_agent={"com.byjg.easyhaproxy.definitions":"agent","com.byjg.easyhaproxy my-stack_cadvisor={"com.byjg.easyhaproxy.definitions":"cadvisor","com.byjg.easyhaproxy.host.cadvisor":"cadvisor.quantum.example.org","com.byjg.easyhaproxy.localport.cadvisor":"8080","com.byjg.easyhaproxy.port.cadvisor":"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={"com.byjg.easyhaproxy.definitions":"exp","com.byjg.easyhaproxy.host.exp":"node-exporter.quantum.example.org","com.byjg.easyhaproxy.localport.exp":"9100","com.byjg.easyhaproxy.port.exp":"31337","com.docker.stack.image":"stefanprodan/swarmprom-node-exporter:v0.16.0","com.docker.stack.namespace":"my-stack","com.planetary-quantum":"monitoring"} 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={"com.byjg.easyhaproxy.definitions":"http,https","com.byjg.easyhaproxy.port.http":"80","com.byjg.easyhaproxy.host.http":"www.somehost.com.br","com.byjg.easyhaproxy.localport.http":"80","com.byjg.easyhaproxy.redirect.http":"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","com.byjg.easyhaproxy.port.https":"443","com.byjg.easyhaproxy.host.https":"www.somehost.com.br","com.byjg.easyhaproxy.localport.https":"80","com.byjg.easyhaproxy.redirect.https":"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","com.byjg.easyhaproxy.sslcert.https":"U29tZSBQRU0gQ2VydGlmaWNhdGU="} \ No newline at end of file diff --git a/tests/test_parser.py b/tests/test_parser.py index c9ff08e..4f076be 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -19,11 +19,13 @@ def test_parser_doesnt_crash(): "customerrors": False } - cfg = easymapping.HaproxyConfigGenerator(result) + cfg = easymapping.HaproxyConfigGenerator(result, "/tmp") haproxy_config = cfg.generate(lineList) + assert len(haproxy_config) > 0 - assert "frontend" not in haproxy_config - assert "backend" not in haproxy_config + path = os.path.dirname(os.path.realpath(__file__)) + with open(path + "/expected/no-services.txt", 'r') as expected_file: + assert expected_file.read() == haproxy_config def test_parser_finds_services(): @@ -33,14 +35,20 @@ def test_parser_finds_services(): "customerrors": False } - cfg = easymapping.HaproxyConfigGenerator(result) - haproxy_config = cfg.generate(lineList) - assert len(haproxy_config) > 0 - assert "mode tcp" in haproxy_config - assert "mode http" in haproxy_config + cert_file = "/tmp/www.somehost.com.br.1.pem" + if os.path.exists(cert_file): + os.remove(cert_file) - assert "frontend tcp_in_31339_1" in haproxy_config - assert "frontend http_in_31337_2" in haproxy_config + cfg = easymapping.HaproxyConfigGenerator(result, "/tmp") + haproxy_config = cfg.generate(lineList) + + assert len(haproxy_config) > 0 + path = os.path.dirname(os.path.realpath(__file__)) + 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" def test_parser_static(): @@ -48,13 +56,14 @@ 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) + cfg = easymapping.HaproxyConfigGenerator(parsed, "/tmp") haproxy_config = cfg.generate() assert len(haproxy_config) > 0 with open(path + "/expected/static.txt", 'r') as expected_file: assert expected_file.read() == haproxy_config + def test_parser_tcp(): lineList = load_fixture("services-tcp") @@ -62,23 +71,11 @@ def test_parser_tcp(): "customerrors": False } - cfg = easymapping.HaproxyConfigGenerator(result) + cfg = easymapping.HaproxyConfigGenerator(result, "/tmp") haproxy_config = cfg.generate(lineList) # print(haproxy_config) - frontend_cfg = "frontend tcp_in_31339_1\n" - frontend_cfg += " bind *:31339\n" - frontend_cfg += " mode tcp\n" - frontend_cfg += " option tcplog\n" - frontend_cfg += " log global\n" - frontend_cfg += " default_backend srv_agent_quantum_local_31339_1\n\n" - assert frontend_cfg in haproxy_config - - backend_cfg = "backend srv_agent_quantum_local_31339_1\n" - backend_cfg += " balance roundrobin\n" - backend_cfg += " mode tcp\n" - backend_cfg += " option tcp-check\n" - backend_cfg += " tcp-check connect ssl\n" - backend_cfg += " server srv test_agent:9001 check weight 1 verify none" - - assert backend_cfg in haproxy_config + assert len(haproxy_config) > 0 + path = os.path.dirname(os.path.realpath(__file__)) + with open(path + "/expected/services-tcp.txt", 'r') as expected_file: + assert expected_file.read() == haproxy_config From 53010bb1be4e20636b715ae339c0b39af2c22fc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joao=20Gilberto=20Magalh=C3=A3es?= Date: Sun, 7 Jun 2020 23:03:08 -0500 Subject: [PATCH 09/14] Add more verbose on exit-event-listener.py. Fix python. --- Dockerfile | 3 ++- assets/scripts/exit-event-listener.py | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9720c23..eb7267c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,8 @@ FROM haproxy:2.1-alpine WORKDIR /scripts -RUN apk add --no-cache bash python3 py-yaml supervisor docker +RUN apk add --no-cache bash python3 py-yaml supervisor docker \ + && ln -s /usr/bin/python3 /usr/bin/python COPY requirements.txt /scripts RUN pip3 install --upgrade pip \ diff --git a/assets/scripts/exit-event-listener.py b/assets/scripts/exit-event-listener.py index 9b1826f..4f64778 100755 --- a/assets/scripts/exit-event-listener.py +++ b/assets/scripts/exit-event-listener.py @@ -5,6 +5,7 @@ import signal from supervisor import childutils + def main(): while True: headers, payload = childutils.listener.wait() @@ -12,7 +13,11 @@ def main(): events = ['PROCESS_STATE_FATAL', 'PROCESS_STATE_EXITED', 'PROCESS_STATE_STOPPED'] if not (headers['eventname'] in events): continue + + print(headers) + print(payload) os.kill(os.getppid(), signal.SIGTERM) + if __name__ == "__main__": - main() \ No newline at end of file + main() From 61d8ea2360ca199901a876def7185744b01907ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joao=20Gilberto=20Magalh=C3=A3es?= Date: Sun, 7 Jun 2020 23:35:08 -0500 Subject: [PATCH 10/14] Fix README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7417dc7..c457c2d 100644 --- a/README.md +++ b/README.md @@ -173,7 +173,7 @@ easymapping: www.host1.com.br: http://host1.com.br - port: 443 - ssl_cert: BASE64_PEM_CERTIFICATE + ssl_cert: /path/to/ssl/certificate hosts: host1.com.br: container:80 From 364fce10d1376a6e8c6043bccbc5098d16bf2f2c Mon Sep 17 00:00:00 2001 From: Joao Gilberto Date: Tue, 10 Aug 2021 15:17:00 -0500 Subject: [PATCH 11/14] Upgrade HAProxy to 2.4.2; Added examples --- .gitignore | 1 + Dockerfile | 5 +- README.md | 34 ++++++------- assets/scripts/haproxy.sh | 2 +- examples/docker/docker-compose.yml | 32 ++++++++++++ examples/static/config.yml | 17 +++++++ examples/static/docker-compose.yml | 18 +++++++ examples/static/host1.local.pem | 82 ++++++++++++++++++++++++++++++ 8 files changed, 170 insertions(+), 21 deletions(-) create mode 100644 examples/docker/docker-compose.yml create mode 100644 examples/static/config.yml create mode 100644 examples/static/docker-compose.yml create mode 100644 examples/static/host1.local.pem diff --git a/.gitignore b/.gitignore index d760fc3..d2768a5 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ venv .docker_data __pycache__ +.pytest_cache \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index eb7267c..6dcc7c0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,12 @@ -FROM haproxy:2.1-alpine +FROM alpine:3.14 WORKDIR /scripts -RUN apk add --no-cache bash python3 py-yaml supervisor docker \ +RUN apk add --no-cache haproxy bash python3 py3-pip py-yaml supervisor docker \ && ln -s /usr/bin/python3 /usr/bin/python COPY requirements.txt /scripts + RUN pip3 install --upgrade pip \ && pip install -r requirements.txt diff --git a/README.md b/README.md index 7417dc7..56e644f 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ 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 +## Features - Enable or disable Stats on port 1936 with custom password - Discover and setup haproxy from Docker Tag @@ -11,7 +11,7 @@ a simple Yaml instead docker - Setup HAProxy CFG from a Yaml file. -# Basic Usage +## Basic Usage The Easy HAProxy will create the `haproxy.cfg` automatically based on the containers or from a YAML provided. @@ -31,7 +31,6 @@ The mapping to `/var/run/docker.sock` is necessary to discover the docker contai The environment variables will setup the HAProxy. -{:.table} | Environment Variable | Description | |----------------------|-------------------------------------------------------------------------------| | DISCOVER | How `haproxy.cfg` will be created: `static`, `docker` or `swarm` | @@ -47,7 +46,7 @@ The environment variable `DISCOVER` will define where is located your containers - swarm - static -# DISCOVER: docker +## DISCOVER: docker This method will use a regular docker installation to discover the containers and configure the HAProxy. @@ -65,7 +64,7 @@ docker run --network easyhaproxy byjg/easyhaproxy docker run --network easyhaproxy myimage ``` -# DISCOVER: swarm +## DISCOVER: swarm This method requires a functional Docker Swarm Cluster. The system will search for the labels in all containers on all swarm nodes. @@ -74,19 +73,18 @@ 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 +### Tags to be attached in the Docker Container (Swarm or Docker) -{:.table} -| 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.mode.[definition] | (Optional) Is this http or tcp mode in HAProxy. (Defaults to http) | -| 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. | -| com.byjg.easyhaproxy.health-check.[definition] | (Optional) `ssl`, enable health check via SSL in `mode tcp` (Defaults to "empty") | +| Tag | Description | Example | +|---------------------------------------------|---------------------------------------------------------------------------------------------------------|--------------| +| com.byjg.easyhaproxy.definitions | A Comma delimited list with the definitions. Each name requires the definition of the parameters below. | http,https | +| com.byjg.easyhaproxy.mode.[definition] | (Optional) Is this http or tcp mode in HAProxy. (Defaults to http) | http | +| com.byjg.easyhaproxy.port.[definition] | (Optional) What is the port that the HAProxy will listen to. (Defaults to 80) | 80 | +| com.byjg.easyhaproxy.localport.[definition] | (Optional) What is the port that the container is listening. (Defaults to 80) | 8080 | +| com.byjg.easyhaproxy.host.[definition] | What is the host that the HAProxy will listen to. | somehost.com | +| com.byjg.easyhaproxy.redirect.[definition] | (Optional) Host redirects from connections in the port defined above. | foo.com--https://bla.com,bar.com--https://bar.org | +| com.byjg.easyhaproxy.sslcert.[definition] | (Optional) Cert PEM Base64 encoded. | | +| com.byjg.easyhaproxy.health-check.[definition] | (Optional) `ssl`, enable health check via SSL in `mode tcp` (Defaults to "empty") | | Note: if you are deploying a stack set labels at the `deploy` level: @@ -150,7 +148,7 @@ docker run \ -l com.byjg.easyhaproxy.redirect.=www.byjg.com.br--http://byjg.com.br,byjg.com--http://byjg.com.br ``` -# DISCOVER: static +## DISCOVER: static This method expects a YAML file to setup the `haproxy.cfg` diff --git a/assets/scripts/haproxy.sh b/assets/scripts/haproxy.sh index 89dd3a9..2af313f 100755 --- a/assets/scripts/haproxy.sh +++ b/assets/scripts/haproxy.sh @@ -2,7 +2,7 @@ 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 +/usr/sbin/haproxy -W -f /etc/haproxy/haproxy.cfg -p /run/haproxy.pid -S /var/run/haproxy.sock while true; do sleep 60 diff --git a/examples/docker/docker-compose.yml b/examples/docker/docker-compose.yml new file mode 100644 index 0000000..e1c697b --- /dev/null +++ b/examples/docker/docker-compose.yml @@ -0,0 +1,32 @@ +version: "3" + +services: + haproxy: + image: byjg/easy-haproxy + volumes: + - /var/run/docker.sock:/var/run/docker.sock + environment: + DISCOVER: docker + 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 + labels: + com.byjg.easyhaproxy.definitions: "http,https" + + com.byjg.easyhaproxy.redirect.http: host1.local--https://host1.local + com.byjg.easyhaproxy.host.http: host1.local + com.byjg.easyhaproxy.port.http: 80 + + com.byjg.easyhaproxy.port.https: 443 + com.byjg.easyhaproxy.localport.https: 8080 + com.byjg.easyhaproxy.host.https: host1.local + com.byjg.easyhaproxy.sslcert.https: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZEVENDQXZXZ0F3SUJBZ0lVUmkrdzFaVmdlZWRUbE5JQXdxUUJNSnY2ZFhzd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0ZqRVVNQklHQTFVRUF3d0xhRzl6ZERFdWJHOWpZV3d3SGhjTk1qRXdPREV3TVRnME9UQTJXaGNOTXpFdwpPREE0TVRnME9UQTJXakFXTVJRd0VnWURWUVFEREF0b2IzTjBNUzVzYjJOaGJEQ0NBaUl3RFFZSktvWklodmNOCkFRRUJCUUFEZ2dJUEFEQ0NBZ29DZ2dJQkFNQkRBaExBeWdKdWFXNnc2ZmZpZ3pUQUFHWHBtRXowdEl4bjFrNFoKeDV3TjVycHYvcXUwUU1ZeitBdjJ1MWVPS0VLWmVhRlJWcFQwcjkzZFg3SXZiRVpIdDI1R1BpQnZsTEdxaGpLUgpQblNrLzdVOFhtc250dFVBVjdyVkVLMVVyZEZ3OC9Jd3JpUUMrZGhyMG1uWWZTRE1rdkJvTUZwZGhWTlRyYkFaCjFUQjZyUWpFN0FyME10OG15OTZYSm13cmNqSzJUaitFMnJnUElVejFlNWNla0ZZSURTQmF0bXcrMyt2citUNXgKRk5Ga0oybzMwVzVvOFpmbENKSnpyVmFpaHFRaWNzNlpLRGdwZjdpcVhNRml3V0lsaGRRcEd2eDVHZi9LRlRLOQpVYU9uUlp6L1grMkNlYkFGYVRIUjNrL1BZcHBXVGdCQkJ1UnZscEN3K3dkbmttdGVDMFNRUkY5MVFXVnI3ZWpvCjdLYU9sR0k1VnR2TVVzV3ZUZUFabXBheW1JYUFURVR1T0phWTBKVTExT21MZUQ5RE9qNUUyU1E3cUlYL3BGY3AKeHB6RzVqNGMrTWxndnhQMlZBa05UZUFYQ2FZaVBCUUg1WlpnMEhFMlduQjFLaExSRmxIZDRpSFFEMkdKNXlOLwo2ZkNGQmZaZktTZUs4SmF1d3hnV2tyYTUzT2NEcS9tS2QrREEvZEsrL3J1Rzd0cXdWZ0lhMDRIT3Bsek03TFlSCkdCMElyczkrbHI1L1BKYlFabVUwNzNNZG42Y1hBZzNwKzZ3dndGbERrUzV2MTNnQkRZTkh0RjYyYmM1NTFlZEYKWjZrR3pKN3dtR1JvODRhQlA3TXVSWmVSZUxPclNTNjdhMXdMZHpac01uUDFUSjd4OUxmcjlNS2wydURuUWRuWQpleDhEQWdNQkFBR2pVekJSTUIwR0ExVWREZ1FXQkJTUS9tdFpkNmg4ZW45WVFWSDZITzFQbFdXaXF6QWZCZ05WCkhTTUVHREFXZ0JTUS9tdFpkNmg4ZW45WVFWSDZITzFQbFdXaXF6QVBCZ05WSFJNQkFmOEVCVEFEQVFIL01BMEcKQ1NxR1NJYjNEUUVCQ3dVQUE0SUNBUUNoUVlOdWFoMyttVHBJQkRZeEdyalRKTnVPVElNYVd6TXlpMXRrZitMMApzRUd3cGJtQU8ybVdXUVlGN1dWTHNpOThQVUxoM2FkanQyaml1ZDlWbGFhQzZnbnduNVpvMStQaWxvOXNOTExXCjZpajArck40a3dJbS9wTnFpK2pEdXUyY3ZBdUhJd1pXZWg4YkVlLzVVQ3hvNGlobVdGUU44ZUo2VFVLQ3BoUkMKNkVvci9TU1paQlFIZ1BsMEJjaHpIT2t3dTdSM0xDbmRScXhqaEFvVmI5eVFPVitac21UZUpYdWx3TnpKMXVMdApUOE9JZ0lpRHBtQm83SFNOMkgwazNjaHgwMEFzalV5SjltbUFXUGVqRmUvS1hMUlBjVlpSMTdqaHpnZklCRXpzCk01V3RXRm0xYUhEalZ2Nk02aXRlVm02MUU5VCtrL00xMXJ1MWUyWXdzeFREdmI2eDA0bWNyTnU5c29xZGRCYnIKVmZwbHV1b1EvaEVBYlh0Rk5Qb1R5U3B6MGN3T3djSENvd1ZPTG1kS2d2SW1zelppTXlISEc4VkdHbVBoODhuNwp3VnhiMGdWMFA0Uk1yY01MZGVUZG41NVlRcjFDcUJyMzRlQjZvbDZBc2JUbTNWekJIUlZtRk5rc2wxbzVKQjV0CnRYTGdGL0c4L3J6Si80bTFQYVZ1eHJCN0R4VW1JazhFUGJTSVZrdlp2ZDdMQnpLd1E2SWZWYXVjZXdIZkVhalEKVklpZXhTTWlGYzdsdzNLbnhqT0haamY2Rk05VllnM05vKytHZEM5OXM3TGtJdUp3QU1MTnFUUTdIdmhuN1l2UAo0RmxTSWdjNnhqMFlrR1pFUWxiNW8vNW5hdUVxUVUwQUJndzZqdEk0TnhyTkxUNmNwN0NPNE0weElERWcvM1lECmFBPT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQotLS0tLUJFR0lOIFBSSVZBVEUgS0VZLS0tLS0KTUlJSlF3SUJBREFOQmdrcWhraUc5dzBCQVFFRkFBU0NDUzB3Z2drcEFnRUFBb0lDQVFEQVF3SVN3TW9DYm1sdQpzT24zNG9NMHdBQmw2WmhNOUxTTVo5Wk9HY2VjRGVhNmIvNnJ0RURHTS9nTDlydFhqaWhDbVhtaFVWYVU5Sy9kCjNWK3lMMnhHUjdkdVJqNGdiNVN4cW9ZeWtUNTBwUCsxUEY1cko3YlZBRmU2MVJDdFZLM1JjUFB5TUs0a0F2blkKYTlKcDJIMGd6Skx3YURCYVhZVlRVNjJ3R2RVd2VxMEl4T3dLOURMZkpzdmVseVpzSzNJeXRrNC9oTnE0RHlGTQo5WHVYSHBCV0NBMGdXclpzUHQvcjYvaytjUlRSWkNkcU45RnVhUEdYNVFpU2M2MVdvb2FrSW5MT21TZzRLWCs0CnFsekJZc0ZpSllYVUtScjhlUm4veWhVeXZWR2pwMFdjLzEvdGdubXdCV2t4MGQ1UHoyS2FWazRBUVFia2I1YVEKc1BzSFo1SnJYZ3RFa0VSZmRVRmxhKzNvNk95bWpwUmlPVmJiekZMRnIwM2dHWnFXc3BpR2dFeEU3amlXbU5DVgpOZFRwaTNnL1F6bytSTmtrTzZpRi82UlhLY2FjeHVZK0hQakpZTDhUOWxRSkRVM2dGd21tSWp3VUIrV1dZTkJ4Ck5scHdkU29TMFJaUjNlSWgwQTloaWVjamYrbndoUVgyWHlrbml2Q1dyc01ZRnBLMnVkem5BNnY1aW5mZ3dQM1MKdnY2N2h1N2FzRllDR3RPQnpxWmN6T3kyRVJnZENLN1BmcGErZnp5VzBHWmxOTzl6SForbkZ3SU42ZnVzTDhCWgpRNUV1YjlkNEFRMkRSN1JldG0zT2VkWG5SV2VwQnN5ZThKaGthUE9HZ1QrekxrV1hrWGl6cTBrdXUydGNDM2MyCmJESno5VXllOGZTMzYvVENwZHJnNTBIWjJIc2ZBd0lEQVFBQkFvSUNBUUMveFpiWjBjY3RxYWdzcXZhVk5URWUKZXExcStoZmFHdlBFWVFhWUhJcklFKzJpNVhjbkdjTEtjS2ZvZHhEakFuOFIvemdkT3A2Y01YMENWbi9Qb2hIawpBRUR0RTgrQVZ3d0FNMUZzT3dnTEhWR2FHejhxcnhCbFlkUWdIY3BtdWVJdTJQWGJDOGVIVUJpYVVPSXVoYXc1Ci9SUk1EQUMvQWkyc3NmaTdnT2p2VkU0b1F4UVcwUUcxS0dPT0FVSm4vdVlIdzJSRlkyVXUxcGlteE8ya0RPNTMKZ2N4bUMxV09ueUNIbUhhaVcvVWg3ejZKYW1mU000ZFh0VEpac2x5aDM3ZGhIS05iZzlWa1A3Q1FLQTRoTHpvcApoYmY1cVk2cmFyZ09OaW55MUhnTVB4cm13S3VVb3VKeU90TjB5QnR4akRDVU5hWFVCd2l5N3NOR1MrSDR2c3lCCjVQOUhoSUhTdHUrRlp0M0hHN0VJcUNuZGlhU0tEUzRqV2FWUUFiYm80bloyWnMyQkQreERlUFJDUlVxWDdyTTQKNFh6UElSV1dYbW1XZi83SWcyOUhicnA0YTlMY09tUTJsZUNKdGJhVEZTTjk2T0xVSjVFK2hRMHVsQ1pnQlZtUQpSQ1VZa0pQNGxPemJhS2R6anhnSE1ySHptNDVlVUZmOExpck94aTJ1eXhYSFFtRE51NGIzWDE4a3QzUGdVbVVtCjNkWHBsM2ZxU3lKYTdTQ1Y4Wk5CcnNyRHExRSt0aFl0dTkxUWJWU0d4SGQ5SHJOVmUzWGRMYk9DZFU5Q3VDNjkKTmdsem5hYTdzWkxxbXlLZWpUZkdzWTd4cldkTmNNUGw0cDRmY0lEL080RXBBU1pmb3JwVGVLTlQwWklmWlpldwpiMG1BUWVZWnFRTThpL3FNWU4vdUFRS0NBUUVBNXFnMXNSTk1jNlZkTS90UmdsYXNHWW94amdSQzJPcUFEWmdzCm1BWE1VSjNrRXJweXh0K2VDaW15OGlidVlwelJUSVE4ZkJUV1JrQ3RSWlhKNytLY0xWdGs5UVpJb0xiaHlOd2QKNEl4RVFaRnVVbGpEYnZTalRMU3ljc0h2bzY1aWJXSWZUTDdiZ1dsTEdnR3EvVU96ZkdzZ0g2Uzl3THA1RzMwRwo4RUx5akk1ZVRJWUlDcmZUbVZMK2M0NU1ScEVNS28rY3Z6OFB5c2lhT0ZUbjNjeXN3UFZkWWFlRUVxTVFqVTh3CklHTnNHWkx5dFk3QkFCQmNZMGxkcnRiYS9PK0Z2LytSSDd1VXR6UDd4cENJd0ZDeDgwWnpOK1dSeTlOdkk2M1UKenEzeUlCb1c5R3lBcEQyK1BMYVBOeGY3UUxUVUNoWTFaei9kWVJsdEtPeHYyQWE1Z1FLQ0FRRUExV0xXTnFwMApmaEIvWnRmU0VTaHhGTU04OWNqTjZBYXoxV0tMN3VUQm91OW9TSm54amtoa2FWNzZhY25UL2lxWHR4TU5nSGkxCmZJbURwVTNQdk0wWTRVZDJUNDdvSGM2UDFCclpQTi9HbVh5L3M2QkFFZFB3TGU3Sis0blRJU0hBZEdtcmgrYS8KNXBrdHUzMmc5bFdxZnR4ZWNGSVZTTFBXa3hUMFhLaU14cDFmZmtMK09hdnBNZ01GWks0MWlLczNkTlNoS1BvZwpMOEdTUGNQOXgveW43OFAyZUszTitQR2psQTZwUHpyQU55V1U3TjAvYm1IY0I5VEtQK3VkWVdjalZocnU3TVlOCndOckU0a0tkQzh2OGk3eDd0RGJ2Yjc5VCtGbzZQSWg1M3AwT3NuWnpBOFVSMFFOUit2RFF1ZlF1eWFqOFJFQysKWkc4WXlDS3N2azh5Z3dLQ0FRQS9mc1N4QjBmL2VlRXJZeDZ3QzUzNnRlRW9ZQ0hxeHJzVGd2V2JyOVRyeUZzMQprSi95QVRMblIwMWNmYjBYNW1WemM5K1dwTUhMdXhnMzFLRXZhU2xuRHdhK3NNa2pmTlN3ejI5bUZoYmdHZUhOCngyT2RVcmoxYjdURUJJRXNoTi9SanJaaEVSVXFEY3MvMEgrNmtuMkJYWmdOUGZPQ2I1TFJMMXpPblE5YUJBTVAKZThJUStVUEZyR1FoZVdXajgxL3ZBM081N2VreUFJRDd5dHU5WWcrWVdyTW5JODhtdGo3ak40NWZEQitBOXNQYgptUDJtUDlxKzlqNVUyQTZXbkhVc1FuVTMwQktEVUVzYUFVV3o4MExaWG1adlY4SUg0eDl3S2ZVd0pCQklLQVp6CnFMN005N1k3em1Ha1gvU3BmbDMwbk9KOGxzY2hhTGQxRVlsRVphMkJBb0lCQVFDeWUyNVQ0VFY1TUpGdjB6dVoKTUd1TmcxU2MvTzRGa24yZkVVT2NlV2pod1VCSDRjUGpUL2YxRHdXRHNOYUo5TlJieENyNTkzMU9BclBEYzVjOApBNDA0K1k0ak01UkJRa0tabGk5NHRIQW9kK2pjOVVCQjZUVXZKbGw1OVNsTXdDOTY3OXdTMjFaT0tuZlBLR0NYClNzWkdRRXNaeGY2Wmhoc0hnWEozZ2wvbHpVSlBtUGVPQTVZVlIrT2Q5LzA5S0lGRlRvalNmb3luaFZDdUt4NDkKeGI0dVZZbjJIT0o0eEowZlBUZ2hkQ0hNdnJtWGVlUVJqdmI4OGVhTm1xVlVFSEhGRnRnYjRma2xBNWZFN1JUeApCaGxpUkRCd1o3YlVrSU5LNnlWazluNkJUbnM1bU12UkxtZ2RuSnBZdkU3S0MwMkxUYlpiM0krajhDMFpVYStOCnF5N0RBb0lCQUFpZXJpYlM3V1VjbDJhQmxrbTUrVzdxTm0vSU5tNXp2bm9TUG82VjN3YTVoczZmOStDL2tiZEYKODdqUVBBL1lGZTN1UjJzQUo3c2xYNWV1Wks4V21mcEZtZ3psdTBzRXo4MU1MUS9XeXBadFp5dHlWdFd6QjJQdQpYQ1cxdGRTSDllSTJCbWhYZ29rSE5UTTQ4TmsveE9FTnJQL3NlWHJJeDVMSzBobkRIWm90dS96NitZU2tCOWhGCmNtMmZaeWdEMWRNTFg2bGlSaW14eUZZK2RJQ0pOQjk1SmlmVExXWW5XZUdkZGt3UHRYVWVHWEUxb2x6dk5rTEQKek16RTA5dWhreC9sUkpudGVPQkVaYWY4ME9CLzA5T2k5YjkvcnhZNTlkd3NINkdheExvVGZFS3VQbnZCVk1OUgpZa1UxNFd6UUtsZUZraUJKSTlsVnZuZmdHbk9sZ2cwPQotLS0tLUVORCBQUklWQVRFIEtFWS0tLS0t \ No newline at end of file diff --git a/examples/static/config.yml b/examples/static/config.yml new file mode 100644 index 0000000..2e75011 --- /dev/null +++ b/examples/static/config.yml @@ -0,0 +1,17 @@ +stats: + username: admin + password: password + port: 1936 # Optional (default 1936) + +customerrors: true # Optional (default false) + +easymapping: + - port: 80 + redirect: + host1.local: https://host1.local + www.host1.local: https://host1.local + + - port: 443 + ssl_cert: /etc/certs/host1.local.pem + hosts: + host1.local: container:8080 diff --git a/examples/static/docker-compose.yml b/examples/static/docker-compose.yml new file mode 100644 index 0000000..59aaeb5 --- /dev/null +++ b/examples/static/docker-compose.yml @@ -0,0 +1,18 @@ +version: "3" + +services: + haproxy: + image: byjg/easy-haproxy + volumes: + - ./config.yml:/etc/haproxy/easyconfig.yml + - ./host1.local.pem:/etc/certs/host1.local.pem + - /var/run/docker.sock:/var/run/docker.sock + environment: + DISCOVER: static + ports: + - "80:80/tcp" + - "443:443/tcp" + - "1936:1936/tcp" + + container: + image: byjg/static-httpserver diff --git a/examples/static/host1.local.pem b/examples/static/host1.local.pem new file mode 100644 index 0000000..0d7eb39 --- /dev/null +++ b/examples/static/host1.local.pem @@ -0,0 +1,82 @@ +-----BEGIN CERTIFICATE----- +MIIFDTCCAvWgAwIBAgIURi+w1ZVgeedTlNIAwqQBMJv6dXswDQYJKoZIhvcNAQEL +BQAwFjEUMBIGA1UEAwwLaG9zdDEubG9jYWwwHhcNMjEwODEwMTg0OTA2WhcNMzEw +ODA4MTg0OTA2WjAWMRQwEgYDVQQDDAtob3N0MS5sb2NhbDCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBAMBDAhLAygJuaW6w6ffigzTAAGXpmEz0tIxn1k4Z +x5wN5rpv/qu0QMYz+Av2u1eOKEKZeaFRVpT0r93dX7IvbEZHt25GPiBvlLGqhjKR +PnSk/7U8XmsnttUAV7rVEK1UrdFw8/IwriQC+dhr0mnYfSDMkvBoMFpdhVNTrbAZ +1TB6rQjE7Ar0Mt8my96XJmwrcjK2Tj+E2rgPIUz1e5cekFYIDSBatmw+3+vr+T5x +FNFkJ2o30W5o8ZflCJJzrVaihqQics6ZKDgpf7iqXMFiwWIlhdQpGvx5Gf/KFTK9 +UaOnRZz/X+2CebAFaTHR3k/PYppWTgBBBuRvlpCw+wdnkmteC0SQRF91QWVr7ejo +7KaOlGI5VtvMUsWvTeAZmpaymIaATETuOJaY0JU11OmLeD9DOj5E2SQ7qIX/pFcp +xpzG5j4c+MlgvxP2VAkNTeAXCaYiPBQH5ZZg0HE2WnB1KhLRFlHd4iHQD2GJ5yN/ +6fCFBfZfKSeK8JauwxgWkra53OcDq/mKd+DA/dK+/ruG7tqwVgIa04HOplzM7LYR +GB0Irs9+lr5/PJbQZmU073Mdn6cXAg3p+6wvwFlDkS5v13gBDYNHtF62bc551edF +Z6kGzJ7wmGRo84aBP7MuRZeReLOrSS67a1wLdzZsMnP1TJ7x9Lfr9MKl2uDnQdnY +ex8DAgMBAAGjUzBRMB0GA1UdDgQWBBSQ/mtZd6h8en9YQVH6HO1PlWWiqzAfBgNV +HSMEGDAWgBSQ/mtZd6h8en9YQVH6HO1PlWWiqzAPBgNVHRMBAf8EBTADAQH/MA0G +CSqGSIb3DQEBCwUAA4ICAQChQYNuah3+mTpIBDYxGrjTJNuOTIMaWzMyi1tkf+L0 +sEGwpbmAO2mWWQYF7WVLsi98PULh3adjt2jiud9VlaaC6gnwn5Zo1+Pilo9sNLLW +6ij0+rN4kwIm/pNqi+jDuu2cvAuHIwZWeh8bEe/5UCxo4ihmWFQN8eJ6TUKCphRC +6Eor/SSZZBQHgPl0BchzHOkwu7R3LCndRqxjhAoVb9yQOV+ZsmTeJXulwNzJ1uLt +T8OIgIiDpmBo7HSN2H0k3chx00AsjUyJ9mmAWPejFe/KXLRPcVZR17jhzgfIBEzs +M5WtWFm1aHDjVv6M6iteVm61E9T+k/M11ru1e2YwsxTDvb6x04mcrNu9soqddBbr +VfpluuoQ/hEAbXtFNPoTySpz0cwOwcHCowVOLmdKgvImszZiMyHHG8VGGmPh88n7 +wVxb0gV0P4RMrcMLdeTdn55YQr1CqBr34eB6ol6AsbTm3VzBHRVmFNksl1o5JB5t +tXLgF/G8/rzJ/4m1PaVuxrB7DxUmIk8EPbSIVkvZvd7LBzKwQ6IfVaucewHfEajQ +VIiexSMiFc7lw3KnxjOHZjf6FM9VYg3No++GdC99s7LkIuJwAMLNqTQ7Hvhn7YvP +4FlSIgc6xj0YkGZEQlb5o/5nauEqQU0ABgw6jtI4NxrNLT6cp7CO4M0xIDEg/3YD +aA== +-----END CERTIFICATE----- +-----BEGIN PRIVATE KEY----- +MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQDAQwISwMoCbmlu +sOn34oM0wABl6ZhM9LSMZ9ZOGcecDea6b/6rtEDGM/gL9rtXjihCmXmhUVaU9K/d +3V+yL2xGR7duRj4gb5SxqoYykT50pP+1PF5rJ7bVAFe61RCtVK3RcPPyMK4kAvnY +a9Jp2H0gzJLwaDBaXYVTU62wGdUweq0IxOwK9DLfJsvelyZsK3Iytk4/hNq4DyFM +9XuXHpBWCA0gWrZsPt/r6/k+cRTRZCdqN9FuaPGX5QiSc61WooakInLOmSg4KX+4 +qlzBYsFiJYXUKRr8eRn/yhUyvVGjp0Wc/1/tgnmwBWkx0d5Pz2KaVk4AQQbkb5aQ +sPsHZ5JrXgtEkERfdUFla+3o6OymjpRiOVbbzFLFr03gGZqWspiGgExE7jiWmNCV +NdTpi3g/Qzo+RNkkO6iF/6RXKcacxuY+HPjJYL8T9lQJDU3gFwmmIjwUB+WWYNBx +NlpwdSoS0RZR3eIh0A9hiecjf+nwhQX2XyknivCWrsMYFpK2udznA6v5infgwP3S +vv67hu7asFYCGtOBzqZczOy2ERgdCK7Pfpa+fzyW0GZlNO9zHZ+nFwIN6fusL8BZ +Q5Eub9d4AQ2DR7Retm3OedXnRWepBsye8JhkaPOGgT+zLkWXkXizq0kuu2tcC3c2 +bDJz9Uye8fS36/TCpdrg50HZ2HsfAwIDAQABAoICAQC/xZbZ0cctqagsqvaVNTEe +eq1q+hfaGvPEYQaYHIrIE+2i5XcnGcLKcKfodxDjAn8R/zgdOp6cMX0CVn/PohHk +AEDtE8+AVwwAM1FsOwgLHVGaGz8qrxBlYdQgHcpmueIu2PXbC8eHUBiaUOIuhaw5 +/RRMDAC/Ai2ssfi7gOjvVE4oQxQW0QG1KGOOAUJn/uYHw2RFY2Uu1pimxO2kDO53 +gcxmC1WOnyCHmHaiW/Uh7z6JamfSM4dXtTJZslyh37dhHKNbg9VkP7CQKA4hLzop +hbf5qY6rargONiny1HgMPxrmwKuUouJyOtN0yBtxjDCUNaXUBwiy7sNGS+H4vsyB +5P9HhIHStu+FZt3HG7EIqCndiaSKDS4jWaVQAbbo4nZ2Zs2BD+xDePRCRUqX7rM4 +4XzPIRWWXmmWf/7Ig29Hbrp4a9LcOmQ2leCJtbaTFSN96OLUJ5E+hQ0ulCZgBVmQ +RCUYkJP4lOzbaKdzjxgHMrHzm45eUFf8LirOxi2uyxXHQmDNu4b3X18kt3PgUmUm +3dXpl3fqSyJa7SCV8ZNBrsrDq1E+thYtu91QbVSGxHd9HrNVe3XdLbOCdU9CuC69 +Nglznaa7sZLqmyKejTfGsY7xrWdNcMPl4p4fcID/O4EpASZforpTeKNT0ZIfZZew +b0mAQeYZqQM8i/qMYN/uAQKCAQEA5qg1sRNMc6VdM/tRglasGYoxjgRC2OqADZgs +mAXMUJ3kErpyxt+eCimy8ibuYpzRTIQ8fBTWRkCtRZXJ7+KcLVtk9QZIoLbhyNwd +4IxEQZFuUljDbvSjTLSycsHvo65ibWIfTL7bgWlLGgGq/UOzfGsgH6S9wLp5G30G +8ELyjI5eTIYICrfTmVL+c45MRpEMKo+cvz8PysiaOFTn3cyswPVdYaeEEqMQjU8w +IGNsGZLytY7BABBcY0ldrtba/O+Fv/+RH7uUtzP7xpCIwFCx80ZzN+WRy9NvI63U +zq3yIBoW9GyApD2+PLaPNxf7QLTUChY1Zz/dYRltKOxv2Aa5gQKCAQEA1WLWNqp0 +fhB/ZtfSEShxFMM89cjN6Aaz1WKL7uTBou9oSJnxjkhkaV76acnT/iqXtxMNgHi1 +fImDpU3PvM0Y4Ud2T47oHc6P1BrZPN/GmXy/s6BAEdPwLe7J+4nTISHAdGmrh+a/ +5pktu32g9lWqftxecFIVSLPWkxT0XKiMxp1ffkL+OavpMgMFZK41iKs3dNShKPog +L8GSPcP9x/yn78P2eK3N+PGjlA6pPzrANyWU7N0/bmHcB9TKP+udYWcjVhru7MYN +wNrE4kKdC8v8i7x7tDbvb79T+Fo6PIh53p0OsnZzA8UR0QNR+vDQufQuyaj8REC+ +ZG8YyCKsvk8ygwKCAQA/fsSxB0f/eeErYx6wC536teEoYCHqxrsTgvWbr9TryFs1 +kJ/yATLnR01cfb0X5mVzc9+WpMHLuxg31KEvaSlnDwa+sMkjfNSwz29mFhbgGeHN +x2OdUrj1b7TEBIEshN/RjrZhERUqDcs/0H+6kn2BXZgNPfOCb5LRL1zOnQ9aBAMP +e8IQ+UPFrGQheWWj81/vA3O57ekyAID7ytu9Yg+YWrMnI88mtj7jN45fDB+A9sPb +mP2mP9q+9j5U2A6WnHUsQnU30BKDUEsaAUWz80LZXmZvV8IH4x9wKfUwJBBIKAZz +qL7M97Y7zmGkX/Spfl30nOJ8lschaLd1EYlEZa2BAoIBAQCye25T4TV5MJFv0zuZ +MGuNg1Sc/O4Fkn2fEUOceWjhwUBH4cPjT/f1DwWDsNaJ9NRbxCr5931OArPDc5c8 +A404+Y4jM5RBQkKZli94tHAod+jc9UBB6TUvJll59SlMwC9679wS21ZOKnfPKGCX +SsZGQEsZxf6ZhhsHgXJ3gl/lzUJPmPeOA5YVR+Od9/09KIFFTojSfoynhVCuKx49 +xb4uVYn2HOJ4xJ0fPTghdCHMvrmXeeQRjvb88eaNmqVUEHHFFtgb4fklA5fE7RTx +BhliRDBwZ7bUkINK6yVk9n6BTns5mMvRLmgdnJpYvE7KC02LTbZb3I+j8C0ZUa+N +qy7DAoIBAAieribS7WUcl2aBlkm5+W7qNm/INm5zvnoSPo6V3wa5hs6f9+C/kbdF +87jQPA/YFe3uR2sAJ7slX5euZK8WmfpFmgzlu0sEz81MLQ/WypZtZytyVtWzB2Pu +XCW1tdSH9eI2BmhXgokHNTM48Nk/xOENrP/seXrIx5LK0hnDHZotu/z6+YSkB9hF +cm2fZygD1dMLX6liRimxyFY+dICJNB95JifTLWYnWeGddkwPtXUeGXE1olzvNkLD +zMzE09uhkx/lRJnteOBEZaf80OB/09Oi9b9/rxY59dwsH6GaxLoTfEKuPnvBVMNR +YkU14WzQKleFkiBJI9lVvnfgGnOlgg0= +-----END PRIVATE KEY----- \ No newline at end of file From af3529a3731a9b94a0be2b86dbd3690dee89b77c Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Tue, 10 Aug 2021 17:11:38 -0500 Subject: [PATCH 12/14] Removing special char. --- templates/haproxy.cfg.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/haproxy.cfg.j2 b/templates/haproxy.cfg.j2 index 192bbd4..6214a4c 100644 --- a/templates/haproxy.cfg.j2 +++ b/templates/haproxy.cfg.j2 @@ -38,7 +38,7 @@ backend srv_stats server Local 127.0.0.1:{{ data["stats"]["port"] | default(1936) }} {% endif %} {% for o in data["easymapping"] -%} - {% set mode = o["mode"] or "http" %} + {% set mode = o["mode"] or "http" %} {% set salt = loop.index %} frontend {{ mode }}_in_{{ o["port"] }}_{{ salt }} From 44f8d2756f9207d3870460d2e1fe1b00fc3bb0af Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Tue, 10 Aug 2021 17:56:09 -0500 Subject: [PATCH 13/14] Minor Fixes; Multi-Arch. --- .travis.yml | 29 +++++++++++++++++++++++------ Dockerfile | 3 +++ README.md | 2 +- build-multiarch.sh | 33 +++++++++++++++++++++++++++++++++ requirements.txt | 3 ++- templates/bind.j2 | 2 +- templates/haproxy.cfg.j2 | 4 ++-- 7 files changed, 65 insertions(+), 11 deletions(-) create mode 100755 build-multiarch.sh diff --git a/.travis.yml b/.travis.yml index e870c1e..17752ea 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,26 @@ language: python -python: - - "3.7" -install: - - pip install -r requirements.txt +services: + - docker -script: - - pytest -s tests/ \ No newline at end of file + +jobs: + include: + - stage: test + if: (type IN (pull_request)) + install: + - pip install -r requirements + 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/Dockerfile b/Dockerfile index 6dcc7c0..86f11ea 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,10 @@ RUN pip3 install --upgrade pip \ COPY templates /scripts/templates/ COPY easymapping /scripts/easymapping/ +COPY tests/ /scripts/tests/ COPY assets / +RUN pytest -s tests/ + CMD ["/usr/bin/supervisord", "-n", "-c", "/etc/supervisord.conf" ] diff --git a/README.md b/README.md index 2af85c4..8f040f2 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ Important: easyhaproxy needs to be in the same network of the containers or othe | com.byjg.easyhaproxy.host.[definition] | What is the host that the HAProxy will listen to. | somehost.com | | com.byjg.easyhaproxy.redirect.[definition] | (Optional) Host redirects from connections in the port defined above. | foo.com--https://bla.com,bar.com--https://bar.org | | com.byjg.easyhaproxy.sslcert.[definition] | (Optional) Cert PEM Base64 encoded. | | -| com.byjg.easyhaproxy.health-check.[definition] | (Optional) `ssl`, enable health check via SSL in `mode tcp` (Defaults to "empty") | | +| com.byjg.easyhaproxy.health-check.[definition] | (Optional) `ssl`, enable health check via SSL in `mode tcp` (Defaults to "empty") | | Note: if you are deploying a stack set labels at the `deploy` level: diff --git a/build-multiarch.sh b/build-multiarch.sh new file mode 100755 index 0000000..6a5ae0e --- /dev/null +++ b/build-multiarch.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +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" ] +then + echo You need to setup \$DOCKER_USERNAME, \$DOCKER_PASSWORD and \$DOCKER_REGISTRY before run this command. + exit 1 +fi + +buildah login --username $DOCKER_USERNAME --password $DOCKER_PASSWORD $DOCKER_REGISTRY + +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 + + 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 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/requirements.txt b/requirements.txt index fa90f6f..364b4b7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ pyyaml docker -jinja2 \ No newline at end of file +jinja2 +pytest \ No newline at end of file diff --git a/templates/bind.j2 b/templates/bind.j2 index abbd254..6cad5ff 100644 --- a/templates/bind.j2 +++ b/templates/bind.j2 @@ -1,5 +1,5 @@ {% if "ssl_cert" in o %} - bind *:{{ o["port"] }} ssl crt {{ o["ssl_cert"] }} + bind *:{{ o["port"] }} ssl crt {{ o["ssl_cert"] }} {% elif "h2" in o and o["h2"] %} bind *:{{ o["port"] }} proto h2 option http-use-htx diff --git a/templates/haproxy.cfg.j2 b/templates/haproxy.cfg.j2 index 6214a4c..e04b1e5 100644 --- a/templates/haproxy.cfg.j2 +++ b/templates/haproxy.cfg.j2 @@ -60,8 +60,8 @@ backend srv_{{ host }} http-request add-header X-Forwarded-Proto https if { ssl_fc } {% elif mode == "tcp" %} option tcp-check - tcp-check connect{{ " ssl" if o["health-check"] == "ssl" }} + tcp-check connect{{ " ssl" if o["health-check"] == "ssl" }} {% endif %} - server srv {{ o["hosts"][k] }} check weight 1{{ " verify none" if o["health-check"] == "ssl" }} + server srv {{ o["hosts"][k] }} check weight 1{{ " verify none" if o["health-check"] == "ssl" }} {% endfor %} {% endfor %} From 7e242d1215fb64cd4990e6369719d8e21ff8f5fd Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Tue, 10 Aug 2021 17:57:29 -0500 Subject: [PATCH 14/14] Minor Fixes; Multi-Arch. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 17752ea..ed1f6e6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,7 @@ jobs: - stage: test if: (type IN (pull_request)) install: - - pip install -r requirements + - pip install -r requirements.txt script: - pytest -s tests/