1
0
Fork 0

Merge branch '4.4.0' into issue/38

This commit is contained in:
Joao M 2023-06-27 00:20:55 -05:00 committed by GitHub
commit c41cc42d47
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 125 additions and 26 deletions

View file

@ -1,4 +1,4 @@
FROM alpine:3.16 FROM alpine:3.18
ARG RELEASE_VERSION_ARG ARG RELEASE_VERSION_ARG
@ -11,7 +11,6 @@ COPY src/ /scripts/
COPY build/assets / COPY build/assets /
RUN apk add --no-cache haproxy bash python3 py3-pip py-yaml certbot openssl \ RUN apk add --no-cache haproxy bash python3 py3-pip py-yaml certbot openssl \
&& ln -s /usr/bin/python3 /usr/bin/python \
&& pip3 install --upgrade pip \ && pip3 install --upgrade pip \
&& pip install -r requirements.txt \ && pip install -r requirements.txt \
&& pytest -s -vv tests/ \ && pytest -s -vv tests/ \

View file

@ -11,10 +11,11 @@
| easyhaproxy.[definition].redirect | (Optional) JSON containing key/value pair from host/to URL redirect. | *empty* | {"foo.com":"https://bla.com", "bar.com":"https://bar.org"} | | easyhaproxy.[definition].redirect | (Optional) JSON containing key/value pair from host/to URL redirect. | *empty* | {"foo.com":"https://bla.com", "bar.com":"https://bar.org"} |
| easyhaproxy.[definition].sslcert | (Optional) Cert PEM Base64 encoded. Do not use this if `letsencrypt` is enabled. | *empty* | base64 cert + key | | easyhaproxy.[definition].sslcert | (Optional) Cert PEM Base64 encoded. Do not use this if `letsencrypt` is enabled. | *empty* | base64 cert + key |
| easyhaproxy.[definition].ssl | (Optional) If `true` you need to provide certificate as a file. See below. Do not use with `sslcert`. | false | true or false | | easyhaproxy.[definition].ssl | (Optional) If `true` you need to provide certificate as a file. See below. Do not use with `sslcert`. | false | true or false |
| easyhaproxy.[definition].health-check | (Optional) `ssl`, enable health check via SSL in `mode tcp` | *empty* | ssl | | easyhaproxy.[definition].ssl-check | (Optional) `ssl`, enable health check via SSL in `mode tcp` | *empty* | ssl |
| easyhaproxy.[definition].letsencrypt | (Optional) Generate certificate with letsencrypt. Do not use with `sslcert` parameter. | false | true OR false | | easyhaproxy.[definition].letsencrypt | (Optional) Generate certificate with letsencrypt. Do not use with `sslcert` parameter. | false | true OR false |
| easyhaproxy.[definition].redirect_ssl | (Optional) Redirect all requests to https | false | true OR false | | easyhaproxy.[definition].redirect_ssl | (Optional) Redirect all requests to https | false | true OR false |
| easyhaproxy.[definition].clone_to_ssl | (Optional) It copies the configuration to HTTPS(443) and disable SSL from the current config. **Do not use* this with `ssl` or `letsencrypt` parameters | false | true OR false | | easyhaproxy.[definition].clone_to_ssl | (Optional) It copies the configuration to HTTPS(443) and disable SSL from the current config. **Do not use* this with `ssl` or `letsencrypt` parameters | false | true OR false |
| easyhaproxy.[definition].balance | (Optional) HAProxy balance algorithm. See [HAProxy documentation](https://cbonte.github.io/haproxy-dconv/1.8/configuration.html#4.2-balance) | roundrobin | roundrobin, source, uri, url_param, hdr, rdp-cookie, leastconn, first, static-rr, rdp-cookie, hdr_dom, map-based |
The `definition` is a string that will group all configurations togethers. Different `definition` will create different configurations. The `definition` is a string that will group all configurations togethers. Different `definition` will create different configurations.

14
docs/volumes.md Normal file
View file

@ -0,0 +1,14 @@
# Volumes
You can map the following volumes:
| Volume | Description |
|------|------|
| /etc/haproxy/static/ | The folder that will contain the [config.yml](static.md) file for static configuration |
| /certs/haproxy/ | The folder that will contain the certificates (`PEM`) for the [SSL](ssl.md) |
| /certs/letsencrypt/ | The folder that will contain the certificates (`PEM`) for the SSL. Use this volume to cache the [letsencrypt](letsencrypt.md) certificate and avoid re-issue certificates between restarts. |
| /etc/haproxy/conf.d/ | The folder that will contain the [custom configuration](other.md) files. |
| /etc/haproxy/errors-custom/ | The folder that will contain the [custom error](other.md) files. |
----
[Open source ByJG](http://opensource.byjg.com)

View file

@ -117,7 +117,7 @@ class HaproxyConfigGenerator:
if port not in easymapping: if port not in easymapping:
easymapping[port] = { easymapping[port] = {
"mode": mode, "mode": mode,
"health-check": "", "ssl-check": "",
"port": port, "port": port,
"hosts": dict(), "hosts": dict(),
"redirect": dict(), "redirect": dict(),
@ -129,8 +129,8 @@ class HaproxyConfigGenerator:
"80" "80"
) )
easymapping[port]["health-check"] = self.label.get( easymapping[port]["ssl-check"] = self.label.get(
self.label.create([definition, "health-check"]), self.label.create([definition, "ssl-check"]),
"" ""
) )
@ -145,6 +145,10 @@ class HaproxyConfigGenerator:
easymapping[port]["hosts"][hostname]["redirect_ssl"] = self.label.get_bool( easymapping[port]["hosts"][hostname]["redirect_ssl"] = self.label.get_bool(
self.label.create([definition, "redirect_ssl"]) self.label.create([definition, "redirect_ssl"])
) )
easymapping[port]["hosts"][hostname]["balance"] = self.label.get(
self.label.create([definition, "balance"]),
"roundrobin"
)
easymapping[port]["redirect"] = self.label.get_json( easymapping[port]["redirect"] = self.label.get_json(
self.label.create([definition, "redirect"]) self.label.create([definition, "redirect"])
@ -154,7 +158,7 @@ class HaproxyConfigGenerator:
if "443" not in easymapping: if "443" not in easymapping:
easymapping["443"] = { easymapping["443"] = {
"mode": "http", "mode": "http",
"health-check": "ssl", "ssl-check": "ssl",
"port": "443", "port": "443",
"hosts": dict(), "hosts": dict(),
"redirect": dict(), "redirect": dict(),

View file

@ -100,21 +100,19 @@ class Functions:
class Consts: class Consts:
easyhaproxy_config = "/etc/haproxy/static/config.yml" easyhaproxy_config = "/etc/haproxy/static/config.yml"
haproxy_config = "/etc/haproxy/haproxy.cfg" haproxy_config = "/etc/haproxy/haproxy.cfg"
custom_config_folder = "/etc/haproxy/conf.d"
certs_letsencrypt = "/certs/letsencrypt" certs_letsencrypt = "/certs/letsencrypt"
certs_haproxy = "/certs/haproxy" certs_haproxy = "/certs/haproxy"
class DaemonizeHAProxy: class DaemonizeHAProxy:
def __init__(self): def __init__(self, custom_config_folder = None):
self.process = None self.process = None
self.thread = None self.thread = None
self.sleep_secs = None self.sleep_secs = None
self.custom_config_folder = custom_config_folder if custom_config_folder is not None else Consts.custom_config_folder
def haproxy(self, action): def haproxy(self, action):
if action == "start": self.__prepare(self.get_haproxy_command(action))
self.__prepare("/usr/sbin/haproxy -W -f /etc/haproxy/haproxy.cfg -p /run/haproxy.pid -S /var/run/haproxy.sock")
else:
pid = "".join(Functions().run_bash(Functions.HAPROXY_LOG, "cat /run/haproxy.pid", log_output=False))
self.__prepare("/usr/sbin/haproxy -W -f /etc/haproxy/haproxy.cfg -p /run/haproxy.pid -x /var/run/haproxy.sock -sf %s" % (pid))
if self.process is None: if self.process is None:
return return
@ -122,6 +120,17 @@ class DaemonizeHAProxy:
self.thread = Process(target=self.__start, args=()) self.thread = Process(target=self.__start, args=())
self.thread.start() self.thread.start()
def get_haproxy_command(self, action):
custom_config_files = ""
if len(list(self.get_custom_config_files().keys())) != 0:
custom_config_files = "-f %s" % (self.custom_config_folder)
if action == "start":
return "/usr/sbin/haproxy -W -f /etc/haproxy/haproxy.cfg %s -p /run/haproxy.pid -S /var/run/haproxy.sock" % (custom_config_files)
else:
pid = "".join(Functions().run_bash(Functions.HAPROXY_LOG, "cat /run/haproxy.pid", log_output=False))
return "/usr/sbin/haproxy -W -f /etc/haproxy/haproxy.cfg %s -p /run/haproxy.pid -x /var/run/haproxy.sock -sf %s" % (custom_config_files, pid)
def __prepare(self, command): def __prepare(self, command):
source = Functions.HAPROXY_LOG source = Functions.HAPROXY_LOG
if not isinstance(command, (list, tuple)): if not isinstance(command, (list, tuple)):
@ -172,6 +181,16 @@ class DaemonizeHAProxy:
time.sleep(self.sleep_secs) time.sleep(self.sleep_secs)
def get_custom_config_files(self):
if not os.path.exists(self.custom_config_folder):
return {}
files = {}
for file in os.listdir(self.custom_config_folder):
if file.endswith(".cfg"):
files[os.path.join(self.custom_config_folder, file)] = os.path.getmtime(os.path.join(self.custom_config_folder, file))
return dict(sorted(files.items(), key=lambda t: t[0]))
class Certbot: class Certbot:
def __init__(self, certs, email, test_server): def __init__(self, certs, email, test_server):

View file

@ -19,6 +19,7 @@ def start():
old_haproxy = None old_haproxy = None
haproxy = DaemonizeHAProxy() haproxy = DaemonizeHAProxy()
current_custom_config_files = haproxy.get_custom_config_files()
haproxy.haproxy("start") haproxy.haproxy("start")
haproxy.sleep() haproxy.sleep()
@ -31,7 +32,7 @@ def start():
try: try:
old_parsed = processor_obj.get_parsed_object() old_parsed = processor_obj.get_parsed_object()
processor_obj.refresh() processor_obj.refresh()
if certbot.check_certificates(letsencrypt_certs_found) or DeepDiff(old_parsed, processor_obj.get_parsed_object()) != {} or not haproxy.is_alive(): if certbot.check_certificates(letsencrypt_certs_found) or DeepDiff(old_parsed, processor_obj.get_parsed_object()) != {} or not haproxy.is_alive() or DeepDiff(current_custom_config_files, haproxy.get_custom_config_files()) != {}:
Functions.log(Functions.EASYHAPROXY_LOG, Functions.DEBUG, 'New configuration found. Reloading...') Functions.log(Functions.EASYHAPROXY_LOG, Functions.DEBUG, 'New configuration found. Reloading...')
Functions.log(Functions.EASYHAPROXY_LOG, Functions.TRACE, 'Object Found: %s' % (processor_obj.get_parsed_object())) Functions.log(Functions.EASYHAPROXY_LOG, Functions.TRACE, 'Object Found: %s' % (processor_obj.get_parsed_object()))
processor_obj.save_config(Consts.haproxy_config) processor_obj.save_config(Consts.haproxy_config)
@ -40,6 +41,7 @@ def start():
Functions.log(Functions.EASYHAPROXY_LOG, Functions.DEBUG, 'Found hosts: %s' % ", ".join(processor_obj.get_hosts())) # Needs to after save_config Functions.log(Functions.EASYHAPROXY_LOG, Functions.DEBUG, 'Found hosts: %s' % ", ".join(processor_obj.get_hosts())) # Needs to after save_config
old_haproxy = haproxy old_haproxy = haproxy
haproxy = DaemonizeHAProxy() haproxy = DaemonizeHAProxy()
current_custom_config_files = haproxy.get_custom_config_files()
haproxy.haproxy("reload") haproxy.haproxy("reload")
old_haproxy.terminate() old_haproxy.terminate()

View file

@ -194,9 +194,9 @@ class Kubernetes(ProcessorInterface):
self.cert_cache = {} self.cert_cache = {}
super().__init__() super().__init__()
def _check_annotation(self, annotations, key): def _check_annotation(self, annotations, key, default = None):
if key not in annotations: if key not in annotations:
return None return default
return annotations[key] return annotations[key]
def inspect_network(self): def inspect_network(self):
@ -264,6 +264,8 @@ class Kubernetes(ProcessorInterface):
rule_data["%s.redirect" % (definition)] = redirect rule_data["%s.redirect" % (definition)] = redirect
if mode is not None: if mode is not None:
rule_data["%s.mode" % (definition)] = mode rule_data["%s.mode" % (definition)] = mode
rule_data["%s.balance" % (definition)] = self._check_annotation(ingress.metadata.annotations, "easyhaproxy.balance", "roundrobin")
service_name = rule.http.paths[0].backend.service.name service_name = rule.http.paths[0].backend.service.name
try: try:

View file

@ -72,7 +72,7 @@ frontend {{ mode }}_in_{{ o["port"] }}
{% for k in o["hosts"] -%} {% for k in o["hosts"] -%}
{% set host = k.replace(".", "_") + "_{0}".format(o["port"]) %} {% set host = k.replace(".", "_") + "_{0}".format(o["port"]) %}
backend srv_{{ host }} backend srv_{{ host }}
balance roundrobin balance {{ o["balance"] | default("roundrobin") }}
mode {{ mode }} mode {{ mode }}
{% if mode == "http" %} {% if mode == "http" %}
option forwardfor option forwardfor
@ -80,10 +80,10 @@ backend srv_{{ host }}
http-request add-header X-Forwarded-Proto https if { ssl_fc } http-request add-header X-Forwarded-Proto https if { ssl_fc }
{% elif mode == "tcp" %} {% elif mode == "tcp" %}
option tcp-check option tcp-check
tcp-check connect{{ " ssl" if o["health-check"] == "ssl" }} tcp-check connect{{ " ssl" if o["ssl-check"] == "ssl" }}
{% endif %} {% endif %}
{% for c in o["hosts"][k]["containers"] %} {% for c in o["hosts"][k]["containers"] %}
server srv-{{ loop.index0 }} {{ c }} check weight 1{{ " verify none" if o["health-check"] == "ssl" }} server srv-{{ loop.index0 }} {{ c }} check weight 1{{ " verify none" if o["ssl-check"] == "ssl" }}
{% endfor %} {% endfor %}
{% endfor %} {% endfor %}
{% endfor %} {% endfor %}

2
src/tests/fixtures/00_haproxy.cfg vendored Normal file
View file

@ -0,0 +1,2 @@
global
maxconn 4000

2
src/tests/fixtures/10_haproxy.cfg vendored Normal file
View file

@ -0,0 +1,2 @@
global
maxconn 5000

View file

@ -1,2 +1,2 @@
{"test_agent": {"easyhaproxy.agent.host":"agent.quantum.local","easyhaproxy.agent.localport":"9001","easyhaproxy.agent.mode":"tcp","easyhaproxy.agent.port":"31339","com.docker.stack.image":"portainer/agent:1.5.1","com.docker.stack.namespace":"test", "easyhaproxy.agent.health-check":"ssl"}, {"test_agent": {"easyhaproxy.agent.host":"agent.quantum.local","easyhaproxy.agent.localport":"9001","easyhaproxy.agent.mode":"tcp","easyhaproxy.agent.port":"31339","com.docker.stack.image":"portainer/agent:1.5.1","com.docker.stack.namespace":"test", "easyhaproxy.agent.ssl-check":"ssl"},
"test_proxy": {"com.docker.stack.image":"byjg/easy-haproxy:local","com.docker.stack.namespace":"test"}} "test_proxy": {"com.docker.stack.image":"byjg/easy-haproxy:local","com.docker.stack.namespace":"test"}}

View file

@ -0,0 +1,44 @@
import json
import pytest
import os
import re
import random
import string
from functions import DaemonizeHAProxy
def test_daemonize_haproxy():
daemon = DaemonizeHAProxy()
assert daemon is not None
def test_daemonize_haproxy_check_config():
daemon = DaemonizeHAProxy()
filed = daemon.get_custom_config_files()
assert filed == {}
def test_daemonize_haproxy_get_haproxy_command_start():
daemon = DaemonizeHAProxy()
command = daemon.get_haproxy_command("start")
assert command == "/usr/sbin/haproxy -W -f /etc/haproxy/haproxy.cfg -p /run/haproxy.pid -S /var/run/haproxy.sock"
def test_daemonize_haproxy_get_haproxy_command_reload():
daemon = DaemonizeHAProxy()
command = daemon.get_haproxy_command("reload")
assert command == "/usr/sbin/haproxy -W -f /etc/haproxy/haproxy.cfg -p /run/haproxy.pid -x /var/run/haproxy.sock -sf "
def test_daemonize_haproxy_check_config():
daemon = DaemonizeHAProxy(os.path.abspath(os.path.dirname(__file__)) + '/fixtures')
filed = daemon.get_custom_config_files()
assert filed == {
os.path.dirname(__file__) + "/fixtures/00_haproxy.cfg": os.path.getmtime(os.path.dirname(__file__) + "/fixtures/00_haproxy.cfg"),
os.path.dirname(__file__) + "/fixtures/10_haproxy.cfg": os.path.getmtime(os.path.dirname(__file__) + "/fixtures/10_haproxy.cfg")
}
def test_daemonize_haproxy_get_haproxy_command_start():
daemon = DaemonizeHAProxy(os.path.abspath(os.path.dirname(__file__)) + '/fixtures')
command = daemon.get_haproxy_command("start")
assert command == "/usr/sbin/haproxy -W -f /etc/haproxy/haproxy.cfg -f %s -p /run/haproxy.pid -S /var/run/haproxy.sock" % (os.path.dirname(__file__) + "/fixtures")
def test_daemonize_haproxy_get_haproxy_command_reload():
daemon = DaemonizeHAProxy(os.path.abspath(os.path.dirname(__file__)) + '/fixtures')
command = daemon.get_haproxy_command("reload")
assert command == "/usr/sbin/haproxy -W -f /etc/haproxy/haproxy.cfg -f %s -p /run/haproxy.pid -x /var/run/haproxy.sock -sf " % (os.path.dirname(__file__) + "/fixtures")

View file

@ -110,10 +110,11 @@ def test_parser_finds_services_raw():
parsed_object = [ parsed_object = [
{ {
"mode":"tcp", "mode":"tcp",
"health-check":"", "ssl-check":"",
"port":"31339", "port":"31339",
"hosts":{ "hosts":{
"agent.quantum.example.org": { "agent.quantum.example.org": {
"balance": "roundrobin",
"containers": [ "containers": [
"my-stack_agent:9001" "my-stack_agent:9001"
], ],
@ -127,10 +128,11 @@ def test_parser_finds_services_raw():
}, },
{ {
"mode":"http", "mode":"http",
"health-check":"", "ssl-check":"",
"port":"31337", "port":"31337",
"hosts":{ "hosts":{
"cadvisor.quantum.example.org":{ "cadvisor.quantum.example.org":{
"balance": "roundrobin",
"containers": [ "containers": [
"my-stack_cadvisor:8080" "my-stack_cadvisor:8080"
], ],
@ -138,6 +140,7 @@ def test_parser_finds_services_raw():
"redirect_ssl": False "redirect_ssl": False
}, },
"node-exporter.quantum.example.org":{ "node-exporter.quantum.example.org":{
"balance": "roundrobin",
"containers": [ "containers": [
"my-stack_node-exporter:9100" "my-stack_node-exporter:9100"
], ],
@ -151,10 +154,11 @@ def test_parser_finds_services_raw():
}, },
{ {
"mode":"http", "mode":"http",
"health-check":"", "ssl-check":"",
"port":"443", "port":"443",
"hosts":{ "hosts":{
"node-exporter.quantum.example.org": { "node-exporter.quantum.example.org": {
"balance": "roundrobin",
"containers": [ "containers": [
"my-stack_node-exporter:9100" "my-stack_node-exporter:9100"
], ],
@ -162,6 +166,7 @@ def test_parser_finds_services_raw():
"redirect_ssl": False "redirect_ssl": False
}, },
"www.somehost.com.br":{ "www.somehost.com.br":{
"balance": "roundrobin",
"containers": [ "containers": [
"some-service:80" "some-service:80"
], ],
@ -180,10 +185,11 @@ def test_parser_finds_services_raw():
}, },
{ {
"mode":"http", "mode":"http",
"health-check":"", "ssl-check":"",
"port":"80", "port":"80",
"hosts":{ "hosts":{
"www.somehost.com.br":{ "www.somehost.com.br":{
"balance": "roundrobin",
"containers": [ "containers": [
"some-service:80" "some-service:80"
], ],
@ -444,9 +450,10 @@ def test_parser_finds_services_clone_to_ssl_raw():
parsed_object = [ parsed_object = [
{ {
"health-check":"", "ssl-check":"",
"hosts":{ "hosts":{
"host2.local":{ "host2.local":{
"balance":"roundrobin",
"containers":[ "containers":[
"10.152.183.215:8080" "10.152.183.215:8080"
], ],
@ -454,6 +461,7 @@ def test_parser_finds_services_clone_to_ssl_raw():
"redirect_ssl": False "redirect_ssl": False
}, },
"valida.me":{ "valida.me":{
"balance":"roundrobin",
"containers":[ "containers":[
"10.152.183.62:8080" "10.152.183.62:8080"
], ],
@ -461,6 +469,7 @@ def test_parser_finds_services_clone_to_ssl_raw():
"redirect_ssl": False "redirect_ssl": False
}, },
"www.valida.me":{ "www.valida.me":{
"balance":"roundrobin",
"containers":[ "containers":[
"10.152.183.62:8080" "10.152.183.62:8080"
], ],
@ -475,9 +484,10 @@ def test_parser_finds_services_clone_to_ssl_raw():
} }
}, },
{ {
"health-check":"ssl", "ssl-check":"ssl",
"hosts":{ "hosts":{
"host2.local":{ "host2.local":{
"balance":"roundrobin",
"containers":[ "containers":[
"10.152.183.215:8080" "10.152.183.215:8080"
], ],