1
0
Fork 0

Merge pull request #35 from byjg/issues

Issues
This commit is contained in:
Joao M 2023-02-08 21:39:25 -06:00 committed by GitHub
commit d2a0c248fa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 173 additions and 64 deletions

16
.vscode/launch.json vendored
View file

@ -4,6 +4,22 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{
"name": "PyTest Current File",
"type": "python",
"request": "launch",
"module": "pytest",
"justMyCode": true,
"console": "integratedTerminal",
"cwd": "${workspaceFolder}/src",
"args": [
"-vv",
"${file}"
],
"env": {
"PYTHONPATH": "${cwd}/src"
}
},
{ {
"name": "Python: Current File", "name": "Python: Current File",
"type": "python", "type": "python",

6
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,6 @@
{
"cSpell.words": [
"certonly",
"letsencrypt"
]
}

View file

@ -1,10 +1,11 @@
# Docker environment variables # Docker environment variables
| Environment Variable | Description | Default | | Environment Variable | Description | Default |
|-------------------------------|-------------------------------------------------------------------------------------------------|------------------| |---------------------------------|-------------------------------------------------------------------------------------------------|------------------|
| EASYHAPROXY_DISCOVER | How the services will be discovered to create `haproxy.cfg`: `static`, `docker`, `swarm` or `kubernetes` | **required** | | EASYHAPROXY_DISCOVER | How the services will be discovered to create `haproxy.cfg`: `static`, `docker`, `swarm` or `kubernetes` | **required** |
| EASYHAPROXY_LABEL_PREFIX | (Optional) The key will search for matching resources. | `easyhaproxy` | | EASYHAPROXY_LABEL_PREFIX | (Optional) The key will search for matching resources. | `easyhaproxy` |
| EASYHAPROXY_LETSENCRYPT_EMAIL | (Optional) The email will be used to request the certificate to Letsencrypt | *empty* | | EASYHAPROXY_LETSENCRYPT_EMAIL | (Optional) The email will be used to request the certificate to Letsencrypt | *empty* |
| EASYHAPROXY_LETSENCRYPT_SERVER | (Optional) Can be `staging` or 'schema://domain.tld'. If set, will try to connect to the Letsencrypt test server | *empty* |
| EASYHAPROXY_SSL_MODE | (Optional) `strict` supports only the most recent TLS version; `default` good SSL integration with recent browsers; `loose` supports all old SSL protocols for old browsers (not recommended). | `default`| | EASYHAPROXY_SSL_MODE | (Optional) `strict` supports only the most recent TLS version; `default` good SSL integration with recent browsers; `loose` supports all old SSL protocols for old browsers (not recommended). | `default`|
| EASYHAPROXY_REFRESH_CONF | (Optional) Check configuration every N seconds. | 10 | | EASYHAPROXY_REFRESH_CONF | (Optional) Check configuration every N seconds. | 10 |
| EASYHAPROXY_LOG_LEVEL | (Optional) The log level for EasyHAproxy messages. Available: TRACE,DEBUG,INFO,WARN,ERROR,FATAL | DEBUG | | EASYHAPROXY_LOG_LEVEL | (Optional) The log level for EasyHAproxy messages. Available: TRACE,DEBUG,INFO,WARN,ERROR,FATAL | DEBUG |

View file

@ -5,6 +5,7 @@
This method will use a docker standalone installation to discover the containers and configure the HAProxy. This method will use a docker standalone installation to discover the containers and configure the HAProxy.
The only requirement is that containers and EasyHAProxy must be in the same docker network. The only requirement is that containers and EasyHAProxy must be in the same docker network.
If not, EasyHAProxy will connect the container with the EasyHAProxy network.
e.g.: e.g.:

View file

@ -6,6 +6,7 @@ This method will use a docker swarm installation to discover the containers and
The advantage of this method is that you can discover containers in other nodes from the cluster. The advantage of this method is that you can discover containers in other nodes from the cluster.
The only requirement is that containers and EasyHAProxy must be in the same docker swarm network. The only requirement is that containers and EasyHAProxy must be in the same docker swarm network.
If not, EasyHAProxy will connect the service with the EasyHAProxy service network.
e.g.: e.g.:

View file

@ -13,7 +13,7 @@
# location: https://host1.local/ # location: https://host1.local/
# #
# Test SSL: # Test SSL:
# openssl s_client -showcerts -connect 127.0.0.1:443 --servername host1.local # openssl s_client -showcerts -connect 127.0.0.1:443 -servername host1.local
version: "3" version: "3"

View file

@ -13,7 +13,7 @@
# location: https://host1.local/ # location: https://host1.local/
# #
# Test SSL: # Test SSL:
# openssl s_client -showcerts -connect 127.0.0.1:443 --servername host1.local # openssl s_client -showcerts -connect 127.0.0.1:443 -servername host1.local
version: "3" version: "3"

View file

@ -49,7 +49,7 @@ class HaproxyConfigGenerator:
def __init__(self, mapping): def __init__(self, mapping):
self.mapping = mapping self.mapping = mapping
self.mapping.setdefault("ssl_mode", 'default') self.mapping.setdefault("ssl_mode", 'default')
self.mapping.setdefault("letsencrypt", {"email": ""}) self.mapping.setdefault("letsencrypt", {"email": "", "staging": False})
self.mapping["ssl_mode"] = self.mapping["ssl_mode"].lower() self.mapping["ssl_mode"] = self.mapping["ssl_mode"].lower()
self.label = DockerLabelHandler(mapping['lookup_label'] if 'lookup_label' in mapping else "easyhaproxy") self.label = DockerLabelHandler(mapping['lookup_label'] if 'lookup_label' in mapping else "easyhaproxy")
self.letsencrypt_hosts = [] self.letsencrypt_hosts = []

View file

@ -174,9 +174,18 @@ class DaemonizeHAProxy:
class Certbot: class Certbot:
def __init__(self, certs, email): def __init__(self, certs, email, test_server):
self.certs = certs self.certs = certs
self.email = email self.email = email
self.test_server = self.set_test_server(test_server)
def set_test_server(self, test_server):
if test_server.lower() == "staging":
return "--staging"
elif test_server.lower().startswith("http"):
return "--server " + test_server
else:
return ""
def check_certificates(self, hosts): def check_certificates(self, hosts):
if self.email == "" or len(hosts) == 0: if self.email == "" or len(hosts) == 0:
@ -201,7 +210,7 @@ class Certbot:
Functions.log(Functions.CERTBOT_LOG, Functions.DEBUG, "Renew certificate for %s" % (host)) Functions.log(Functions.CERTBOT_LOG, Functions.DEBUG, "Renew certificate for %s" % (host))
renew_certs.append(host_arg) renew_certs.append(host_arg)
certbot_certonly = ('/usr/bin/certbot certonly ' certbot_certonly = ('/usr/bin/certbot certonly {test_server}'
' --standalone' ' --standalone'
' --preferred-challenges http' ' --preferred-challenges http'
' --http-01-port 2080' ' --http-01-port 2080'
@ -210,7 +219,9 @@ class Certbot:
' --no-eff-email' ' --no-eff-email'
' --non-interactive' ' --non-interactive'
' --max-log-backups=0' ' --max-log-backups=0'
' %s --email %s' % (' '.join(request_certs), self.email) ' {certs} --email {email}'.format(certs = ' '.join(request_certs),
email = self.email,
test_server = self.test_server)
) )
ret_reload = False ret_reload = False
@ -235,6 +246,8 @@ class Certbot:
def find_live_certificates(self): def find_live_certificates(self):
letsencrypt_certs = "/etc/letsencrypt/live/" letsencrypt_certs = "/etc/letsencrypt/live/"
if not os.path.exists(letsencrypt_certs):
return
for item in os.listdir(letsencrypt_certs): for item in os.listdir(letsencrypt_certs):
path = os.path.join(letsencrypt_certs, item) path = os.path.join(letsencrypt_certs, item)
if os.path.isdir(path): if os.path.isdir(path):

View file

@ -22,7 +22,7 @@ def start():
haproxy.haproxy("start") haproxy.haproxy("start")
haproxy.sleep() haproxy.sleep()
certbot = Certbot(Consts.certs_letsencrypt, os.getenv("EASYHAPROXY_LETSENCRYPT_EMAIL")) certbot = Certbot(Consts.certs_letsencrypt, os.getenv("EASYHAPROXY_LETSENCRYPT_EMAIL"), os.getenv("EASYHAPROXY_LETSENCRYPT_SERVER", "").lower())
while True: while True:
if old_haproxy is not None: if old_haproxy is not None:

View file

@ -6,6 +6,7 @@ import os
import json import json
import base64 import base64
import docker import docker
import socket
from kubernetes import client, config from kubernetes import client, config
from kubernetes.client.rest import ApiException from kubernetes.client.rest import ApiException
@ -27,7 +28,8 @@ class ContainerEnv:
env_vars["lookup_label"] = os.getenv("EASYHAPROXY_LABEL_PREFIX") if os.getenv("EASYHAPROXY_LABEL_PREFIX") else "easyhaproxy" env_vars["lookup_label"] = os.getenv("EASYHAPROXY_LABEL_PREFIX") if os.getenv("EASYHAPROXY_LABEL_PREFIX") else "easyhaproxy"
if (os.getenv("EASYHAPROXY_LETSENCRYPT_EMAIL")): if (os.getenv("EASYHAPROXY_LETSENCRYPT_EMAIL")):
env_vars["letsencrypt"] = { env_vars["letsencrypt"] = {
"email": os.getenv("EASYHAPROXY_LETSENCRYPT_EMAIL") "email": os.getenv("EASYHAPROXY_LETSENCRYPT_EMAIL"),
"server": os.getenv("EASYHAPROXY_LETSENCRYPT_SERVER", "false").lower() in ["true", "1", "yes"]
} }
return env_vars return env_vars
@ -126,9 +128,25 @@ class Docker(ProcessorInterface):
super().__init__() super().__init__()
def inspect_network(self): def inspect_network(self):
try:
ha_proxy_network_name = next(iter(self.client.containers.get(socket.gethostname()).attrs["NetworkSettings"]["Networks"]))
except:
# HAProxy is not running in a container, get first container network
if len(self.client.containers.list()) == 0:
return
ha_proxy_network_name = next(iter(self.client.containers.get(self.client.containers.list()[0].name).attrs["NetworkSettings"]["Networks"]))
ha_proxy_network = self.client.networks.get(ha_proxy_network_name)
self.parsed_object = {} self.parsed_object = {}
for container in self.client.containers.list(): for container in self.client.containers.list():
self.parsed_object[container.name] = container.labels # Issue 32 - Docker container cannot connect to containers in different network.
if ha_proxy_network_name not in container.attrs["NetworkSettings"]["Networks"].keys():
ha_proxy_network.connect(container.name)
container = self.client.containers.get(container.name) # refresh object
ip_address = container.attrs["NetworkSettings"]["Networks"][ha_proxy_network_name]["IPAddress"]
self.parsed_object[ip_address] = container.labels
class Swarm(ProcessorInterface): class Swarm(ProcessorInterface):
@ -137,9 +155,28 @@ class Swarm(ProcessorInterface):
super().__init__() super().__init__()
def inspect_network(self): def inspect_network(self):
ha_proxy_service_name = self.client.containers.get(socket.gethostname()).name.split('.')[0]
for endpoint in self.client.services.get(ha_proxy_service_name).attrs['Endpoint']["VirtualIPs"]:
ha_proxy_network_id = endpoint["NetworkID"]
if self.client.networks.get(ha_proxy_network_id).name != 'ingress':
break
self.parsed_object = {} self.parsed_object = {}
for container in self.client.services.list(): for service in self.client.services.list():
self.parsed_object[container.attrs["Spec"]["Name"]] = container.attrs["Spec"]["Labels"] ip_address = None
network_list = []
for endpoint in service.attrs["Endpoint"]["VirtualIPs"]:
if ha_proxy_network_id == endpoint["NetworkID"]:
ip_address = endpoint["Addr"].split("/")[0]
break
network_list.append(endpoint["NetworkID"])
if ip_address is None:
network_list.append(ha_proxy_network_id)
service.update(networks = network_list)
continue # skip to the next service to give time to update the network
self.parsed_object[ip_address] = service.attrs["Spec"]["Labels"]
class Kubernetes(ProcessorInterface): class Kubernetes(ProcessorInterface):
@ -162,6 +199,8 @@ class Kubernetes(ProcessorInterface):
self.parsed_object = {} self.parsed_object = {}
for ingress in ret.items: for ingress in ret.items:
if 'kubernetes.io/ingress.class' not in ingress.metadata.annotations:
continue
if ingress.metadata.annotations['kubernetes.io/ingress.class'] != "easyhaproxy-ingress": if ingress.metadata.annotations['kubernetes.io/ingress.class'] != "easyhaproxy-ingress":
continue continue

View file

@ -95,8 +95,24 @@ def test_container_env_stats_password():
"lookup_label": "easyhaproxy", "lookup_label": "easyhaproxy",
"letsencrypt": { "letsencrypt": {
"email": "acme@example.org", "email": "acme@example.org",
"server": False
} }
} == ContainerEnv.read() } == ContainerEnv.read()
finally: finally:
os.environ['EASYHAPROXY_LETSENCRYPT_EMAIL'] = '' os.environ['EASYHAPROXY_LETSENCRYPT_EMAIL'] = ''
def test_container_env_letsencrypt():
os.environ['EASYHAPROXY_LETSENCRYPT_EMAIL'] = 'acme@example.org'
os.environ['EASYHAPROXY_LETSENCRYPT_SERVER'] = 'true'
try:
assert {
"customerrors": False,
"ssl_mode": "default",
"lookup_label": "easyhaproxy",
"letsencrypt": {
"email": "acme@example.org",
"server": True
}
} == ContainerEnv.read()
finally:
os.environ['EASYHAPROXY_LETSENCRYPT_EMAIL'] = ''

View file

@ -6,15 +6,24 @@ from functions import Functions
from processor import ProcessorInterface from processor import ProcessorInterface
from processor import Docker from processor import Docker
def _get_hydrated_object(parsed_objects, key):
assert key in parsed_objects.keys() def _get_hydrated_object(parsed_objects, lookup_key):
hydrated_object = {} hydrated_object = {}
for key in parsed_objects:
for keys in parsed_objects[key]: for keys in parsed_objects[key]:
if "easyhaproxy" in keys: if lookup_key in keys:
hydrated_object[keys] = parsed_objects[key][keys] hydrated_object[keys] = parsed_objects[key][keys]
return hydrated_object return hydrated_object
def _get_ip_host(parsed_objects, lookup_key):
hydrated_object = {}
for key in parsed_objects:
for keys in parsed_objects[key]:
if lookup_key in keys:
return key
def test_processor_docker(): def test_processor_docker():
try: try:
client = docker.from_env() client = docker.from_env()
@ -66,27 +75,34 @@ def test_processor_docker():
'easyhaproxy.http2.localport': '9000', 'easyhaproxy.http2.localport': '9000',
'easyhaproxy.http2.port': '90', 'easyhaproxy.http2.port': '90',
'easyhaproxy.http2.letsencrypt': 'true', 'easyhaproxy.http2.letsencrypt': 'true',
} == _get_hydrated_object(static.get_parsed_object(), "test_processor_docker") } == _get_hydrated_object(static.get_parsed_object(), "easyhaproxy.http")
assert { assert {
'easyhaproxy.ssl.host': 'hostssl.local', 'easyhaproxy.ssl.host': 'hostssl.local',
'easyhaproxy.ssl.localport': '8080', 'easyhaproxy.ssl.localport': '8080',
'easyhaproxy.ssl.port': '443', 'easyhaproxy.ssl.port': '443',
'easyhaproxy.ssl.sslcert': 'U29tZSBQRU0gQ2VydGlmaWNhdGU=' 'easyhaproxy.ssl.sslcert': 'U29tZSBQRU0gQ2VydGlmaWNhdGU='
} == _get_hydrated_object(static.get_parsed_object(), "test2_processor_docker") } == _get_hydrated_object(static.get_parsed_object(), "easyhaproxy.ssl.")
assert static.get_hosts() is None assert static.get_hosts() is None
assert static.get_certs() == {} assert static.get_certs() == {}
haproxy_cfg = static.get_haproxy_conf() haproxy_cfg = static.get_haproxy_conf()
assert haproxy_cfg == Functions.load(os.path.join(os.path.dirname(os.path.realpath(__file__)), "./expected/docker.txt")) assert haproxy_cfg == Functions.load(os.path.join(os.path.dirname(os.path.realpath(__file__)), "./expected/docker.txt")).replace("test_processor_docker", _get_ip_host(
static.get_parsed_object(), "easyhaproxy.http")).replace("test2_processor_docker", _get_ip_host(static.get_parsed_object(), "easyhaproxy.ssl"))
assert static.get_letsencrypt_hosts() == ['host2.local'] assert static.get_letsencrypt_hosts() == ['host2.local']
assert static.get_hosts() == ['hostssl.local:443', 'host1.local:80', 'host2.local:90'] assert static.get_hosts() == [
assert static.get_certs() == {'hostssl.local.pem': 'Some PEM Certificate'} 'hostssl.local:443',
'host1.local:80',
'host2.local:90'
]
assert static.get_certs() == {
'hostssl.local.pem': 'Some PEM Certificate'
}
finally: finally:
os.environ['EASYHAPROXY_LETSENCRYPT_EMAIL'] = '' os.environ['EASYHAPROXY_LETSENCRYPT_EMAIL'] = ''
container.stop() container.stop()
container2.stop() container2.stop()
#test_processor_docker() # test_processor_docker()