From f007f857dfa33794269bd67725721b5ce819885b Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Thu, 25 Aug 2022 19:37:02 -0500 Subject: [PATCH] Added more unit tests --- src/easymapping/__init__.py | 4 +- src/processor/__init__.py | 7 ++- src/tests/expected/docker.txt | 101 ++++++++++++++++++++++++++++++++ src/tests/test_containerenv.py | 102 +++++++++++++++++++++++++++++++++ src/tests/test_docker.py | 92 +++++++++++++++++++++++++++++ src/tests/test_static.py | 24 ++++++++ 6 files changed, 325 insertions(+), 5 deletions(-) create mode 100644 src/tests/expected/docker.txt create mode 100644 src/tests/test_containerenv.py create mode 100644 src/tests/test_docker.py create mode 100644 src/tests/test_static.py diff --git a/src/easymapping/__init__.py b/src/easymapping/__init__.py index 042ca61..94a893f 100644 --- a/src/easymapping/__init__.py +++ b/src/easymapping/__init__.py @@ -56,10 +56,10 @@ class HaproxyConfigGenerator: self.serving_hosts = [] self.certs = {} - def generate(self, container_metadata = None): + def generate(self, container_metadata = {}): self.mapping.setdefault("easymapping", []) - if container_metadata is not None: + if container_metadata != {}: self.mapping["easymapping"] = self.parse(container_metadata) file_loader = FileSystemLoader('templates') diff --git a/src/processor/__init__.py b/src/processor/__init__.py index 1a5ae9e..c92bf29 100644 --- a/src/processor/__init__.py +++ b/src/processor/__init__.py @@ -13,7 +13,7 @@ class ContainerEnv: def read(): env_vars = { "customerrors": True if os.getenv("HAPROXY_CUSTOMERRORS") == "true" else False, - "ssl_mode": os.getenv("EASYHAPROXY_SSL_MODE", "default") + "ssl_mode": os.getenv("EASYHAPROXY_SSL_MODE") if os.getenv("EASYHAPROXY_SSL_MODE") else 'default' } if os.getenv("HAPROXY_PASSWORD"): @@ -33,6 +33,8 @@ class ContainerEnv: class ProcessorInterface: + static_file = "/etc/haproxy/easyconfig.yml" + def __init__(self, filename = None): self.filename = filename self.refresh() @@ -40,7 +42,7 @@ class ProcessorInterface: @staticmethod def factory(mode): if mode == "static": - return Static("/etc/haproxy/easyconfig.yml") + return Static(ProcessorInterface.static_file) elif mode == "docker": return Docker() elif mode == "swarm": @@ -95,7 +97,6 @@ class ProcessorInterface: Functions.save("{0}/{1}".format(path, cert), self.get_certs(cert)) - class Static(ProcessorInterface): def inspect_network(self): self.parsed_object = {} diff --git a/src/tests/expected/docker.txt b/src/tests/expected/docker.txt new file mode 100644 index 0000000..7bcdedc --- /dev/null +++ b/src/tests/expected/docker.txt @@ -0,0 +1,101 @@ +global + log stdout format raw local0 info + maxconn 2000 + tune.ssl.default-dh-param 2048 + + # intermediate configuration + ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384 + ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 + ssl-default-bind-options prefer-client-ciphers no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets + + ssl-default-server-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384 + ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 + ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets + + ssl-dh-param-file /etc/haproxy/dhparam + +defaults + log global + option httplog + + timeout connect 3s + timeout client 10s + timeout server 10m + +frontend stats + bind *:1936 + mode http + stats enable + stats hide-version + stats realm Haproxy\ Statistics + stats uri / + default_backend srv_stats + +backend srv_stats + mode http + server Local 127.0.0.1:1936 + +frontend http_in_443 + bind *:443 ssl crt /certs/letsencrypt/ alpn http/1.1 crt /certs/haproxy/ alpn http/1.1 + mode http + + acl is_rule_hostssl_local_443_1 hdr(host) -i hostssl.local + acl is_rule_hostssl_local_443_2 hdr(host) -i hostssl.local:443 + use_backend srv_hostssl_local_443 if is_rule_hostssl_local_443_1 OR is_rule_hostssl_local_443_2 + + acl is_rule_host2_local_443_1 hdr(host) -i host2.local + acl is_rule_host2_local_443_2 hdr(host) -i host2.local:443 + use_backend srv_host2_local_443 if is_rule_host2_local_443_1 OR is_rule_host2_local_443_2 + +backend srv_hostssl_local_443 + balance roundrobin + mode http + option forwardfor + http-request set-header X-Forwarded-Port %[dst_port] + http-request add-header X-Forwarded-Proto https if { ssl_fc } + server srv-0 test2_processor_docker:8080 check weight 1 +backend srv_host2_local_443 + balance roundrobin + mode http + option forwardfor + http-request set-header X-Forwarded-Port %[dst_port] + http-request add-header X-Forwarded-Proto https if { ssl_fc } + server srv-0 test_processor_docker:9000 check weight 1 + +frontend http_in_80 + bind *:80 + mode http + + acl is_rule_host1_local_80_1 hdr(host) -i host1.local + acl is_rule_host1_local_80_2 hdr(host) -i host1.local:80 + use_backend srv_host1_local_80 if is_rule_host1_local_80_1 OR is_rule_host1_local_80_2 + +backend srv_host1_local_80 + balance roundrobin + mode http + option forwardfor + http-request set-header X-Forwarded-Port %[dst_port] + http-request add-header X-Forwarded-Proto https if { ssl_fc } + server srv-0 test_processor_docker:8080 check weight 1 + +frontend http_in_90 + bind *:90 + mode http + + acl is_rule_host2_local_90_1 hdr(host) -i host2.local + acl is_rule_host2_local_90_2 hdr(host) -i host2.local:90 + acl is_letsencrypt_host2_local_90 path_beg /.well-known/acme-challenge/ + use_backend letsencrypt_backend if is_letsencrypt_host2_local_90 is_rule_host2_local_90_1 OR is_letsencrypt_host2_local_90 is_rule_host2_local_90_2 + use_backend srv_host2_local_90 if is_rule_host2_local_90_1 OR is_rule_host2_local_90_2 + +backend srv_host2_local_90 + balance roundrobin + mode http + option forwardfor + http-request set-header X-Forwarded-Port %[dst_port] + http-request add-header X-Forwarded-Proto https if { ssl_fc } + server srv-0 test_processor_docker:9000 check weight 1 + +backend letsencrypt_backend + mode http + server certbot 127.0.0.1:2080 diff --git a/src/tests/test_containerenv.py b/src/tests/test_containerenv.py new file mode 100644 index 0000000..90f5225 --- /dev/null +++ b/src/tests/test_containerenv.py @@ -0,0 +1,102 @@ +import pytest +import os +from processor import ContainerEnv + +def test_container_env_empty(): + assert { + "customerrors": False, + "ssl_mode": "default", + "lookup_label": "easyhaproxy" + } == ContainerEnv.read() + + # os.environ['CERTBOT_LOG_LEVEL'] = 'warn' + +def test_container_env_customerrors(): + os.environ['HAPROXY_CUSTOMERRORS'] = 'true' + try: + assert { + "customerrors": True, + "ssl_mode": "default", + "lookup_label": "easyhaproxy" + } == ContainerEnv.read() + finally: + os.environ['HAPROXY_CUSTOMERRORS'] = '' + +def test_container_env_sslmode(): + os.environ['EASYHAPROXY_SSL_MODE'] = 'strict' + try: + assert { + "customerrors": False, + "ssl_mode": "strict", + "lookup_label": "easyhaproxy" + } == ContainerEnv.read() + finally: + os.environ['EASYHAPROXY_SSL_MODE'] = '' + +def test_container_env_stats(): + os.environ['HAPROXY_USERNAME'] = 'abc' + os.environ['HAPROXY_STATS_PORT'] = '2101' + try: + assert { + "customerrors": False, + "ssl_mode": "default", + "lookup_label": "easyhaproxy", + } == ContainerEnv.read() + finally: + os.environ['HAPROXY_USERNAME'] = '' + os.environ['HAPROXY_STATS_PORT'] = '' + +def test_container_env_stats_password(): + os.environ['HAPROXY_PASSWORD'] = 'xyz' + try: + assert { + "customerrors": False, + "ssl_mode": "default", + "lookup_label": "easyhaproxy", + "stats": { + "username": "admin", + "password": "xyz", + "port": "1936" + + } + } == ContainerEnv.read() + finally: + os.environ['HAPROXY_PASSWORD'] = '' + + +def test_container_env_stats_password(): + os.environ['HAPROXY_USERNAME'] = 'abc' + os.environ['HAPROXY_STATS_PORT'] = '2101' + os.environ['HAPROXY_PASSWORD'] = 'xyz' + try: + assert { + "customerrors": False, + "ssl_mode": "default", + "lookup_label": "easyhaproxy", + "stats": { + "username": "abc", + "password": "xyz", + "port": "2101" + + } + } == ContainerEnv.read() + finally: + os.environ['HAPROXY_USERNAME'] = '' + os.environ['HAPROXY_STATS_PORT'] = '' + os.environ['HAPROXY_PASSWORD'] = '' + + +def test_container_env_stats_password(): + os.environ['EASYHAPROXY_LETSENCRYPT_EMAIL'] = 'acme@example.org' + try: + assert { + "customerrors": False, + "ssl_mode": "default", + "lookup_label": "easyhaproxy", + "letsencrypt": { + "email": "acme@example.org", + } + } == ContainerEnv.read() + finally: + os.environ['EASYHAPROXY_LETSENCRYPT_EMAIL'] = '' + diff --git a/src/tests/test_docker.py b/src/tests/test_docker.py new file mode 100644 index 0000000..d65a08b --- /dev/null +++ b/src/tests/test_docker.py @@ -0,0 +1,92 @@ +import pytest +import os +import time +import docker +from functions import Functions +from processor import ProcessorInterface +from processor import Docker + +def _get_hydrated_object(parsed_objects, key): + assert key in parsed_objects.keys() + hydrated_object = {} + for keys in parsed_objects[key]: + if "easyhaproxy" in keys: + hydrated_object[keys] = parsed_objects[key][keys] + return hydrated_object + + +def test_processor_docker(): + try: + client = docker.from_env() + except docker.errors.DockerException: + pytest.skip("There is no docker environment") + + if len(client.containers.list()) > 0: + pytest.skip("I cannot run this test with other containers running.") + + container = client.containers.run("byjg/static-httpserver", + name="test_processor_docker", + detach=True, + auto_remove=True, + remove=True, + labels={ + "easyhaproxy.http.port": "80", + "easyhaproxy.http.localport": "8080", + "easyhaproxy.http.host": "host1.local", + + "easyhaproxy.http2.port": "90", + "easyhaproxy.http2.localport": "9000", + "easyhaproxy.http2.host": "host2.local", + "easyhaproxy.http2.letsencrypt": "true", + }) + container2 = client.containers.run("byjg/static-httpserver", + name="test2_processor_docker", + detach=True, + auto_remove=True, + remove=True, + labels={ + "easyhaproxy.ssl.port": "443", + "easyhaproxy.ssl.localport": "8080", + "easyhaproxy.ssl.host": "hostssl.local", + "easyhaproxy.ssl.sslcert": "U29tZSBQRU0gQ2VydGlmaWNhdGU=" + }) + try: + time.sleep(1) + + os.environ['EASYHAPROXY_LETSENCRYPT_EMAIL'] = 'docker@example.org' + + static = ProcessorInterface.factory("docker") + assert static.get_letsencrypt_hosts() is None + + assert { + 'easyhaproxy.http.host': 'host1.local', + 'easyhaproxy.http.localport': '8080', + 'easyhaproxy.http.port': '80', + 'easyhaproxy.http2.host': 'host2.local', + 'easyhaproxy.http2.localport': '9000', + 'easyhaproxy.http2.port': '90', + 'easyhaproxy.http2.letsencrypt': 'true', + } == _get_hydrated_object(static.get_parsed_object(), "test_processor_docker") + assert { + 'easyhaproxy.ssl.host': 'hostssl.local', + 'easyhaproxy.ssl.localport': '8080', + 'easyhaproxy.ssl.port': '443', + 'easyhaproxy.ssl.sslcert': 'U29tZSBQRU0gQ2VydGlmaWNhdGU=' + } == _get_hydrated_object(static.get_parsed_object(), "test2_processor_docker") + + assert static.get_hosts() is None + assert static.get_certs() == {} + + haproxy_cfg = static.get_haproxy_conf() + assert haproxy_cfg == Functions.load(os.path.join(os.path.dirname(os.path.realpath(__file__)), "./expected/docker.txt")) + + assert static.get_letsencrypt_hosts() == ['host2.local'] + assert static.get_hosts() == ['hostssl.local:443', 'host1.local:80', 'host2.local:90'] + assert static.get_certs() == {'hostssl.local.pem': 'Some PEM Certificate'} + finally: + os.environ['EASYHAPROXY_LETSENCRYPT_EMAIL'] = '' + container.stop() + container2.stop() + + +#test_processor_docker() \ No newline at end of file diff --git a/src/tests/test_static.py b/src/tests/test_static.py new file mode 100644 index 0000000..a68c2e4 --- /dev/null +++ b/src/tests/test_static.py @@ -0,0 +1,24 @@ +import pytest +import os +from functions import Functions +from processor import ProcessorInterface +from processor import Static + +def test_processor_static(): + ProcessorInterface.static_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), "./fixtures/static.yml") + static = ProcessorInterface.factory("static") + + assert static.get_letsencrypt_hosts() is None + assert static.get_parsed_object() == {} + assert static.get_hosts() is None + + haproxy_cfg = static.get_haproxy_conf() + + assert haproxy_cfg == Functions.load(os.path.join(os.path.dirname(os.path.realpath(__file__)), "./expected/static.txt")) + + # @todo: Static doesnt populate this fields + assert static.get_letsencrypt_hosts() == [] + assert static.get_parsed_object() == {} + assert static.get_hosts() == [] + +# test_processor_static() \ No newline at end of file