diff --git a/README.md b/README.md index d9824d1..0eeb50d 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,14 @@ EasyHAProxy can detect and configure HAProxy automatically on the following plat - Docker Swarm - Kubernetes +## Who is using? + +EasyHAProxy is part of some projects: +- Dokku +- MicroK8s + +See detailed instructions on how to install below. + ## EasyHAProxy Mission Easy to set up and low configuration to numerous features. @@ -38,7 +46,7 @@ EasyHAProxy will discover the services based on the Docker Tags of the container Also, it is possible to set up HAProxy from a simple Yaml file instead of creating `haproxy.cfg` file. -## How Does It Works? +## How Does It Work? You don't need to change your current infrastructure and don't need to learn the HAProxy configuration. diff --git a/build/Dockerfile b/build/Dockerfile index 20c3459..334f3cb 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.16 +FROM alpine:3.18 ARG RELEASE_VERSION_ARG @@ -11,7 +11,6 @@ COPY src/ /scripts/ COPY build/assets / 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 \ && pip install -r requirements.txt \ && pytest -s -vv tests/ \ diff --git a/docs/container-labels.md b/docs/container-labels.md index 41e89b4..daa1b51 100644 --- a/docs/container-labels.md +++ b/docs/container-labels.md @@ -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].sslcert | (Optional) Cert PEM Base64 encoded. Do not use this if `certbot` 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].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].certbot | (Optional) Generate certificate with certbot. Do not use with `sslcert` parameter. More info [here](acme.md). | 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 `certbot` 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. diff --git a/docs/docker-environment.md b/docs/docker-environment.md index 2415e82..b9f4d33 100644 --- a/docs/docker-environment.md +++ b/docs/docker-environment.md @@ -20,6 +20,5 @@ | HAPROXY_CUSTOMERRORS | (Optional) If HAProxy will use custom HTML errors. true/false. | `false` | - ---- [Open source ByJG](http://opensource.byjg.com) diff --git a/docs/docker.md b/docs/docker.md index 643ad48..ce0c5cd 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -4,8 +4,12 @@ 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. -If not, EasyHAProxy will connect the container with the EasyHAProxy network. +You cannot mix docker containers with swarm containers. + +The only request is that containers and EasyHAProxy must be in the same docker network. +If you don't add to your services the same network EasyHAProxy is connected to, EasyHAProxy will attach it network to your container. + +Also, it is highly recommended you create a network external to EasyHAProxy. e.g.: @@ -47,13 +51,12 @@ Once the container is running, EasyHAProxy will detect automatically and start t You don't need to expose any port in your container. -Please follow the [docker label configuration](container-labels.md) to see other configurations available. +Please follow the [docker label configuration](container-labels.md) to see other configurations available. ## Setup the EasyHAProxy container You can configure the behavior of the EasyHAProxy by setup specific environment variables. To get a list of the variables, please follow the [docker container environment](docker-environment.md) - ## Setup certificates with ACME (e.g. Letsencrypt) Follow [this link](acme.md) diff --git a/docs/kubernetes.md b/docs/kubernetes.md index 12d5c5d..b3fac3c 100644 --- a/docs/kubernetes.md +++ b/docs/kubernetes.md @@ -71,7 +71,7 @@ Once the container is running, EasyHAProxy will detect automatically and start t You don't need to expose any port in your container. -Caveats: +Notes: - At this point, the implementation doesn't support all ingress properties or wildcard domains. - The ingress will publish the ports 80 and 443, plus 1936 if stats are enabled. @@ -79,14 +79,14 @@ Caveats: ## Kubernetes annotations -| annotation | Description | Default | Example | -|-----------------------------|-------------------------------------------------------------------------------------|--------------|---------------------------| -| kubernetes.io/ingress.class | (required) Activate EasyHAProxy. | **required** | easyhaproxy-ingress | -| easyhaproxy.redirect_ssl | (optional) Boolean. Force redirect all endpoints to HTTPS. | false | true or false | -| easyhaproxy.certbot | (optional) Boolean. It will request certbot certificates for the ingresses domains. | false | true or false | -| easyhaproxy.redirect | (optional) JSON. Key pair with a domain and its destination. | *empty* | {"domain":"redirect_url"} | -| easyhaproxy.mode | (optional) Set the HTTP mode for that connection. | http | http or tcp | -| easyhaproxy.listen_port | (optional) Set the an additional port for that ingress | http | http or tcp | +| annotation | Description | Default | Example | +|----------------------------------|-------------------------------------------------------------------------------------|--------------|---------------------------------------| +| kubernetes.io/ingress.class | (required) Activate EasyHAProxy. | **required** | easyhaproxy-ingress | +| easyhaproxy.redirect_ssl | (optional) Boolean. Force redirect all endpoints to HTTPS. | false | true or false | +| easyhaproxy.certbot | (optional) Boolean. It will request certbot certificates for the ingresses domains. | false | true or false | +| easyhaproxy.redirect | (optional) JSON. Key pair with a domain and its destination. | *empty* | {"domain":"redirect_url"} | +| easyhaproxy.mode | (optional) Set the HTTP mode for that connection. | http | http or tcp | +| easyhaproxy.listen_port | (optional) Set the an additional port for that ingress | http | http or tcp | **Important**: The annotations are per ingress and applied to all hosts in that ingress configuration. diff --git a/docs/static.md b/docs/static.md index bf7b76e..ff6ee32 100644 --- a/docs/static.md +++ b/docs/static.md @@ -20,6 +20,9 @@ customerrors: true # Optional (default false) ssl_mode: default +logLevel: + haproxy: INFO + certbot: { "email": "acme@example.org" } @@ -83,8 +86,13 @@ customerrors: true # Optional (default false) ssl_mode: default # Optional +logLevel: + certbot: DEBUG # Optional (default: DEBUG). Can be: TRACE,DEBUG,INFO,WARN,ERROR,FATAL + easyhaproxy: DEBUG # Optional (default: DEBUG). Can be: TRACE,DEBUG,INFO,WARN,ERROR,FATAL + haproxy: INFO # Optional (default: INFO). Can be: TRACE,DEBUG,INFO,WARN,ERROR,FATAL + certbot: { # Optional. If you enable `certbot` will need to setu0p this, - # otherwise the certificate will be issued + # otherwise the certificate will be issued "email": "acme@example.org" } diff --git a/docs/swarm.md b/docs/swarm.md index fc671d1..9ab29c4 100644 --- a/docs/swarm.md +++ b/docs/swarm.md @@ -5,13 +5,17 @@ This method will use a docker swarm installation to discover the containers and configure the HAProxy. 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. -If not, EasyHAProxy will connect the service with the EasyHAProxy service network. +You cannot mix docker containers with swarm containers. + +The only request is that containers and EasyHAProxy must be in the same docker swarm network. +If you don't add to your services the same network EasyHAProxy is connected to, EasyHAProxy will attach it network to your container. + +Also, it is highly recommended you create a network external to EasyHAProxy. e.g.: ```bash -docker network create -d overlay easyhaproxy +docker network create -d overlay --attachable easyhaproxy ``` And then deploy the EasyHAProxy stack: @@ -79,13 +83,11 @@ networks: external: true ``` -Note: The services to be discovered **don't need** to be in the same network as EasyHAProxy is. - Once the container is running, EasyHAProxy will detect automatically and start to redirect all traffic from `example.org:80` to your container. You don't need to expose any port in your container. -Please follow the [docker label configuration](container-labels.md) to see other configurations available. +Please follow the [docker label configuration](container-labels.md) to see other configurations available. ## Setup the EasyHAProxy container diff --git a/docs/volumes.md b/docs/volumes.md new file mode 100644 index 0000000..47daa02 --- /dev/null +++ b/docs/volumes.md @@ -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/certbot/ | The folder that will contain the certificates (`PEM`) processed by Certbot (e.g. Let's Encrypt). More info: [acme](acme.md). | +| /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) html files. | + +---- +[Open source ByJG](http://opensource.byjg.com) diff --git a/examples/swarm/certs/host1.local.pem b/examples/swarm/certs/host1.local.pem new file mode 100644 index 0000000..0d7eb39 --- /dev/null +++ b/examples/swarm/certs/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----- +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 +YkU14WzQKleFkiBJI9lVvnfgGnOlgg0= +-----END PRIVATE KEY----- \ No newline at end of file diff --git a/examples/swarm/host2.local.pem b/examples/swarm/certs/host2.local.pem similarity index 100% rename from examples/swarm/host2.local.pem rename to examples/swarm/certs/host2.local.pem diff --git a/examples/swarm/easyhaproxy.yml b/examples/swarm/easyhaproxy.yml new file mode 100644 index 0000000..a15260b --- /dev/null +++ b/examples/swarm/easyhaproxy.yml @@ -0,0 +1,40 @@ +# To Install +# docker network create --driver overlay --attachable easyhaproxy +# docker stack deploy -c easyhaproxy.yml easyhaproxy + + +version: "3" + +services: + haproxy: + image: byjg/easy-haproxy:4.3.1-rc2 + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ./certs:/certs/haproxy + - certs_certbot:/certs/certbot + deploy: + replicas: 1 + environment: + EASYHAPROXY_DISCOVER: swarm + EASYHAPROXY_SSL_MODE: "loose" + EASYHAPROXY_LETSENCRYPT_EMAIL: changeme@example.org + HAPROXY_CUSTOMERRORS: "true" + HAPROXY_USERNAME: admin + HAPROXY_PASSWORD: password + HAPROXY_STATS_PORT: 1936 + ports: + - "80:80/tcp" + - "443:443/tcp" + - "1936:1936/tcp" + networks: + - easyhaproxy + +networks: + easyhaproxy: + external: true + +volumes: + certs_certbot: + # external: true + # certs_haproxy: + # external: true \ No newline at end of file diff --git a/examples/swarm/portainer.yml b/examples/swarm/portainer.yml new file mode 100644 index 0000000..d259988 --- /dev/null +++ b/examples/swarm/portainer.yml @@ -0,0 +1,28 @@ +# To install: +# docker stack deploy -c portainer.yml portainer + +version: "3" + +services: + portainer: + image: portainer/portainer-ce:latest + volumes: + - portainer_data:/data portainer + - /var/run/docker.sock:/var/run/docker.sock + deploy: + replicas: 1 + labels: + # easyhaproxy.http.redirect_ssl: true + # easyhaproxy.http.certbot: true + easyhaproxy.http.host: portainer.local + easyhaproxy.http.port: 80 + easyhaproxy.http.localport: 9000 + +volumes: + certs_certbot: + external: true + # certs_haproxy: + # external: true + portainer_data: + # external: true + diff --git a/examples/swarm/docker-compose.yml b/examples/swarm/services.yml similarity index 94% rename from examples/swarm/docker-compose.yml rename to examples/swarm/services.yml index d5fa394..ad16ce2 100644 --- a/examples/swarm/docker-compose.yml +++ b/examples/swarm/services.yml @@ -1,3 +1,6 @@ +# To install: +# docker stack deploy -c services.yml services +# # To test: # curl -k -H "Host: host1.local" https://127.0.0.1/ # curl -k -H "Host: host2.local" https://127.0.0.1/ @@ -18,25 +21,6 @@ version: "3" services: - haproxy: - image: byjg/easy-haproxy - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - ./host2.local.pem:/certs/haproxy/host2.local.pem - deploy: - replicas: 1 - environment: - EASYHAPROXY_DISCOVER: swarm - EASYHAPROXY_SSL_MODE: "loose" - 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 deploy: diff --git a/src/easymapping/__init__.py b/src/easymapping/__init__.py index 34fa05c..bb1c833 100644 --- a/src/easymapping/__init__.py +++ b/src/easymapping/__init__.py @@ -114,7 +114,7 @@ class HaproxyConfigGenerator: if port not in easymapping: easymapping[port] = { "mode": mode, - "health-check": "", + "ssl-check": "", "port": port, "hosts": dict(), "redirect": dict(), @@ -126,8 +126,8 @@ class HaproxyConfigGenerator: "80" ) - easymapping[port]["health-check"] = self.label.get( - self.label.create([definition, "health-check"]), + easymapping[port]["ssl-check"] = self.label.get( + self.label.create([definition, "ssl-check"]), "" ) @@ -142,6 +142,10 @@ class HaproxyConfigGenerator: easymapping[port]["hosts"][hostname]["redirect_ssl"] = self.label.get_bool( 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( self.label.create([definition, "redirect"]) @@ -151,7 +155,7 @@ class HaproxyConfigGenerator: if "443" not in easymapping: easymapping["443"] = { "mode": "http", - "health-check": "ssl", + "ssl-check": "ssl", "port": "443", "hosts": dict(), "redirect": dict(), diff --git a/src/functions/__init__.py b/src/functions/__init__.py index a331697..1b3b1c0 100644 --- a/src/functions/__init__.py +++ b/src/functions/__init__.py @@ -27,6 +27,13 @@ class ContainerEnv: env_vars["lookup_label"] = os.getenv("EASYHAPROXY_LABEL_PREFIX") if os.getenv( "EASYHAPROXY_LABEL_PREFIX") else "easyhaproxy" + env_vars["logLevel"] = { + "easyhaproxy": os.getenv("EASYHAPROXY_LOG_LEVEL") if os.getenv( + "EASYHAPROXY_LOG_LEVEL") else Functions.DEBUG, + "haproxy": os.getenv("HAPROXY_LOG_LEVEL") if os.getenv("HAPROXY_LOG_LEVEL") else Functions.INFO, + "certbot": os.getenv("CERTBOT_LOG_LEVEL") if os.getenv("CERTBOT_LOG_LEVEL") else Functions.DEBUG, + } + env_vars["certbot"] = { "autoconfig": os.getenv("EASYHAPROXY_CERTBOT_AUTOCONFIG", ""), "email": os.getenv("EASYHAPROXY_CERTBOT_EMAIL", ""), @@ -72,7 +79,7 @@ class ContainerEnv: env_vars["certbot"]["eab_kid"] = os.environ['EASYHAPROXY_CERTBOT_EAB_KID'] = resp["eab_kid"] env_vars["certbot"]["eab_hmac_key"] = os.environ['EASYHAPROXY_CERTBOT_EAB_HMAC_KEY'] = resp["eab_hmac_key"] else: - os.environ["EASYHAPROXY_CERTBOT_EMAIL"] = "" + del os.environ["EASYHAPROXY_CERTBOT_EMAIL"] Functions.log(Functions.CERTBOT_LOG, Functions.ERROR, "Could not obtain ZeroSSL credentials " + resp["error"]["type"]) os.environ['EASYHAPROXY_CERTBOT_SERVER'] = env_vars["certbot"]["server"] @@ -174,26 +181,20 @@ class Functions: class Consts: easyhaproxy_config = "/etc/haproxy/static/config.yml" haproxy_config = "/etc/haproxy/haproxy.cfg" + custom_config_folder = "/etc/haproxy/conf.d" certs_certbot = "/certs/certbot" certs_haproxy = "/certs/haproxy" class DaemonizeHAProxy: - def __init__(self): + def __init__(self, custom_config_folder = None): self.process = None self.thread = 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): - if action == "start": - self.__prepare( - "/usr/sbin/haproxy -W -f /etc/haproxy/haproxy.cfg -p /run/haproxy.pid -S /var/run/haproxy.sock") - else: - return_code, pid_tmp = Functions().run_bash(Functions.HAPROXY_LOG, "cat /run/haproxy.pid", log_output=False) - pid = "".join(pid_tmp) - self.__prepare( - "/usr/sbin/haproxy -W -f /etc/haproxy/haproxy.cfg -p /run/haproxy.pid -x /var/run/haproxy.sock -sf %s" % - pid) + self.__prepare(self.get_haproxy_command(action)) if self.process is None: return @@ -201,6 +202,18 @@ class DaemonizeHAProxy: self.thread = Process(target=self.__start, args=()) self.thread.start() + def get_haproxy_command(self, action, pid_file="/run/haproxy.pid"): + 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 %s -S /var/run/haproxy.sock" % (custom_config_files, pid_file) + else: + return_code, output = Functions().run_bash(Functions.HAPROXY_LOG, "cat %s" % pid_file, log_output=False) + pid = "".join(output) + return "/usr/sbin/haproxy -W -f /etc/haproxy/haproxy.cfg %s -p %s -x /var/run/haproxy.sock -sf %s" % (custom_config_files, pid_file, pid) + def __prepare(self, command): source = Functions.HAPROXY_LOG if not isinstance(command, (list, tuple)): @@ -250,6 +263,16 @@ class DaemonizeHAProxy: 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: def __init__(self, certs): diff --git a/src/main.py b/src/main.py index d55c148..24769d8 100644 --- a/src/main.py +++ b/src/main.py @@ -23,6 +23,7 @@ def start(): old_haproxy = None haproxy = DaemonizeHAProxy() + current_custom_config_files = haproxy.get_custom_config_files() haproxy.haproxy("start") haproxy.sleep() @@ -35,8 +36,7 @@ def start(): try: old_parsed = processor_obj.get_parsed_object() processor_obj.refresh() - if certbot.check_certificates(certbot_certs_found) or DeepDiff(old_parsed, - processor_obj.get_parsed_object()) != {} or not haproxy.is_alive(): + if certbot.check_certificates(certbot_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.TRACE, 'Object Found: %s' % (processor_obj.get_parsed_object())) @@ -47,6 +47,7 @@ def start(): 'Found hosts: %s' % ", ".join(processor_obj.get_hosts())) # Needs to after save_config old_haproxy = haproxy haproxy = DaemonizeHAProxy() + current_custom_config_files = haproxy.get_custom_config_files() haproxy.haproxy("reload") old_haproxy.terminate() diff --git a/src/processor/__init__.py b/src/processor/__init__.py index 68d55c6..0d3f8a5 100644 --- a/src/processor/__init__.py +++ b/src/processor/__init__.py @@ -22,6 +22,7 @@ class ProcessorInterface: self.certbot_hosts = None self.hosts = None self.filename = filename + self.label = ContainerEnv.read()['lookup_label'] self.refresh() @staticmethod @@ -151,28 +152,38 @@ class Swarm(ProcessorInterface): def inspect_network(self): ha_proxy_service_name = self.client.containers.get(socket.gethostname()).name.split('.')[0] ha_proxy_network_id = None + swarm_ingress_id = None + + # Get the HAProxy network and the ingress network 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': + network_name = self.client.networks.get(endpoint["NetworkID"]).name + if swarm_ingress_id is None and network_name == 'ingress': + swarm_ingress_id = endpoint["NetworkID"] + if ha_proxy_network_id is None and network_name != 'ingress': + ha_proxy_network_id = endpoint["NetworkID"] + if ha_proxy_network_id is not None and swarm_ingress_id is not None: break - if ha_proxy_network_id is None: - raise "Could not find ingress network" - + # Check if the service is attached to the HAProxy network self.parsed_object = {} for service in self.client.services.list(): + if not any(self.label in key for key in service.attrs["Spec"]["Labels"]): + continue + 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"]) + elif swarm_ingress_id != endpoint["NetworkID"]: + network_list.append(endpoint["NetworkID"]) + # Attach the service to the HAProxy network 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 + 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"] @@ -187,10 +198,9 @@ class Kubernetes(ProcessorInterface): self.cert_cache = {} super().__init__() - @staticmethod - def _check_annotation(annotations, key): + def _check_annotation(self, annotations, key, default=None): if key not in annotations: - return None + return default return annotations[key] def inspect_network(self): @@ -210,9 +220,7 @@ class Kubernetes(ProcessorInterface): redirect_ssl = self._check_annotation(ingress.metadata.annotations, "easyhaproxy.redirect_ssl") redirect = self._check_annotation(ingress.metadata.annotations, "easyhaproxy.redirect") mode = self._check_annotation(ingress.metadata.annotations, "easyhaproxy.mode") - listen_port = self._check_annotation(ingress.metadata.annotations, "easyhaproxy.listen_port") - if listen_port is None: - listen_port = 80 + listen_port = self._check_annotation(ingress.metadata.annotations, "easyhaproxy.listen_port", 80) data = {"creation_timestamp": ingress.metadata.creation_timestamp.strftime("%x %X"), "resource_version": ingress.metadata.resource_version, "namespace": ingress.metadata.namespace} @@ -259,6 +267,7 @@ class Kubernetes(ProcessorInterface): rule_data["%s.redirect" % definition] = redirect if mode is not None: 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 try: diff --git a/src/templates/bind.j2 b/src/templates/bind.j2 index ba947f4..d69de8f 100644 --- a/src/templates/bind.j2 +++ b/src/templates/bind.j2 @@ -1,5 +1,5 @@ {% if "ssl" in o %} - bind *:{{ o["port"] }} ssl crt /certs/certbot/ alpn http/1.1 crt /certs/haproxy/ alpn http/1.1 + bind *:{{ o["port"] }} ssl crt /certs/certbot/ alpn h2,http/1.1 crt /certs/haproxy/ alpn h2,http/1.1 {% elif "h2" in o and o["h2"] %} bind *:{{ o["port"] }} proto h2 option http-use-htx diff --git a/src/templates/haproxy.cfg.j2 b/src/templates/haproxy.cfg.j2 index 5639b25..ef72f98 100644 --- a/src/templates/haproxy.cfg.j2 +++ b/src/templates/haproxy.cfg.j2 @@ -1,5 +1,18 @@ +{% set log_definition = data["logLevel"] | default({}) %} +{% set log_level = log_definition["haproxy"] | default("INFO") | upper %} +{% if log_level == "TRACE" or log_level == "DEBUG" %} +{% set haproxy_log_level = "debug" %} +{% elif log_level == "INFO" %} +{% set haproxy_log_level = "info" %} +{% elif log_level == "WARN" %} +{% set haproxy_log_level = "warning" %} +{% elif log_level == "ERROR" %} +{% set haproxy_log_level = "err" %} +{% elif log_level == "FATAL" %} +{% set haproxy_log_level = "crit" %} +{% endif %} global - log stdout format raw local0 info + log stdout format raw local0 {{ haproxy_log_level }} maxconn 2000 {% if data["ssl_mode"] == "strict" %} {% include "ssl_strict.j2" %} @@ -59,7 +72,7 @@ frontend {{ mode }}_in_{{ o["port"] }} {% for k in o["hosts"] -%} {% set host = k.replace(".", "_") + "_{0}".format(o["port"]) %} backend srv_{{ host }} - balance roundrobin + balance {{ o["balance"] | default("roundrobin") }} mode {{ mode }} {% if mode == "http" %} option forwardfor @@ -67,10 +80,10 @@ 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["ssl-check"] == "ssl" }} {% endif %} {% 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 %} diff --git a/src/tests/expected/docker.txt b/src/tests/expected/docker.txt index 0775f93..91f928b 100644 --- a/src/tests/expected/docker.txt +++ b/src/tests/expected/docker.txt @@ -36,7 +36,7 @@ backend srv_stats server Local 127.0.0.1:1936 frontend http_in_443 - bind *:443 ssl crt /certs/certbot/ alpn http/1.1 crt /certs/haproxy/ alpn http/1.1 + bind *:443 ssl crt /certs/certbot/ alpn h2,http/1.1 crt /certs/haproxy/ alpn h2,http/1.1 mode http acl is_rule_hostssl_local_443_1 hdr(host) -i hostssl.local diff --git a/src/tests/expected/services-letsencrypt.txt b/src/tests/expected/services-letsencrypt.txt index c72e401..8d5da5d 100644 --- a/src/tests/expected/services-letsencrypt.txt +++ b/src/tests/expected/services-letsencrypt.txt @@ -74,7 +74,7 @@ backend srv_test2_example_org_80 server srv-0 83d57d592e26:8080 check weight 1 frontend http_in_443 - bind *:443 ssl crt /certs/certbot/ alpn http/1.1 crt /certs/haproxy/ alpn http/1.1 + bind *:443 ssl crt /certs/certbot/ alpn h2,http/1.1 crt /certs/haproxy/ alpn h2,http/1.1 mode http acl is_rule_test_example_org_443_1 hdr(host) -i test.example.org diff --git a/src/tests/expected/services-redirect-ssl.txt b/src/tests/expected/services-redirect-ssl.txt index ce59c19..ad841cc 100644 --- a/src/tests/expected/services-redirect-ssl.txt +++ b/src/tests/expected/services-redirect-ssl.txt @@ -49,7 +49,7 @@ backend srv_host1_local_80 server srv-0 5b69bc7fea1b:80 check weight 1 frontend http_in_443 - bind *:443 ssl crt /certs/certbot/ alpn http/1.1 crt /certs/haproxy/ alpn http/1.1 + bind *:443 ssl crt /certs/certbot/ alpn h2,http/1.1 crt /certs/haproxy/ alpn h2,http/1.1 mode http acl is_rule_host2_local_443_1 hdr(host) -i host2.local diff --git a/src/tests/expected/services.txt b/src/tests/expected/services.txt index 81a4c99..82f8a6c 100644 --- a/src/tests/expected/services.txt +++ b/src/tests/expected/services.txt @@ -67,7 +67,7 @@ backend srv_node-exporter_quantum_example_org_31337 server srv-0 my-stack_node-exporter:9100 check weight 1 frontend http_in_443 - bind *:443 ssl crt /certs/certbot/ alpn http/1.1 crt /certs/haproxy/ alpn http/1.1 + bind *:443 ssl crt /certs/certbot/ alpn h2,http/1.1 crt /certs/haproxy/ alpn h2,http/1.1 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 } diff --git a/src/tests/expected/static.txt b/src/tests/expected/static.txt index c582286..a5af1ec 100644 --- a/src/tests/expected/static.txt +++ b/src/tests/expected/static.txt @@ -74,7 +74,7 @@ backend srv_host2_com_br_80 server srv-0 other:3000 check weight 1 frontend http_in_443 - bind *:443 ssl crt /certs/certbot/ alpn http/1.1 crt /certs/haproxy/ alpn http/1.1 + bind *:443 ssl crt /certs/certbot/ alpn h2,http/1.1 crt /certs/haproxy/ alpn h2,http/1.1 mode http acl is_rule_host1_com_br_443_1 hdr(host) -i host1.com.br diff --git a/src/tests/fixtures/00_haproxy.cfg b/src/tests/fixtures/00_haproxy.cfg new file mode 100644 index 0000000..eaec319 --- /dev/null +++ b/src/tests/fixtures/00_haproxy.cfg @@ -0,0 +1,2 @@ +global + maxconn 4000 \ No newline at end of file diff --git a/src/tests/fixtures/10_haproxy.cfg b/src/tests/fixtures/10_haproxy.cfg new file mode 100644 index 0000000..9a855a9 --- /dev/null +++ b/src/tests/fixtures/10_haproxy.cfg @@ -0,0 +1,2 @@ +global + maxconn 5000 \ No newline at end of file diff --git a/src/tests/fixtures/services-tcp b/src/tests/fixtures/services-tcp index 5b5c2b0..bc924ab 100644 --- a/src/tests/fixtures/services-tcp +++ b/src/tests/fixtures/services-tcp @@ -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"}} \ No newline at end of file diff --git a/src/tests/test_containerenv.py b/src/tests/test_containerenv.py index beac0c1..30c2680 100644 --- a/src/tests/test_containerenv.py +++ b/src/tests/test_containerenv.py @@ -1,6 +1,6 @@ import os -from functions import ContainerEnv +from functions import Functions, ContainerEnv def test_container_env_empty(): @@ -8,6 +8,11 @@ def test_container_env_empty(): "customerrors": False, "ssl_mode": "default", "lookup_label": "easyhaproxy", + "logLevel": { + "easyhaproxy": Functions.DEBUG, + "haproxy": Functions.INFO, + "certbot": Functions.DEBUG, + }, "certbot": {"autoconfig": "", "eab_hmac_key": "", "eab_kid": "", @@ -26,6 +31,11 @@ def test_container_env_customerrors(): "customerrors": True, "ssl_mode": "default", "lookup_label": "easyhaproxy", + "logLevel": { + "easyhaproxy": Functions.DEBUG, + "haproxy": Functions.INFO, + "certbot": Functions.DEBUG, + }, "certbot": {"autoconfig": "", "eab_hmac_key": "", "eab_kid": "", @@ -34,7 +44,7 @@ def test_container_env_customerrors(): "retry_count": 60} } == ContainerEnv.read() finally: - os.environ['HAPROXY_CUSTOMERRORS'] = '' + del os.environ['HAPROXY_CUSTOMERRORS'] def test_container_env_sslmode(): @@ -44,6 +54,11 @@ def test_container_env_sslmode(): "customerrors": False, "ssl_mode": "strict", "lookup_label": "easyhaproxy", + "logLevel": { + "easyhaproxy": Functions.DEBUG, + "haproxy": Functions.INFO, + "certbot": Functions.DEBUG, + }, "certbot": {"autoconfig": "", "eab_hmac_key": "", "eab_kid": "", @@ -52,7 +67,7 @@ def test_container_env_sslmode(): "retry_count": 60} } == ContainerEnv.read() finally: - os.environ['EASYHAPROXY_SSL_MODE'] = '' + del os.environ['EASYHAPROXY_SSL_MODE'] def test_container_env_stats(): @@ -63,6 +78,11 @@ def test_container_env_stats(): "customerrors": False, "ssl_mode": "default", "lookup_label": "easyhaproxy", + "logLevel": { + "easyhaproxy": Functions.DEBUG, + "haproxy": Functions.INFO, + "certbot": Functions.DEBUG, + }, "certbot": {"autoconfig": "", "eab_hmac_key": "", "eab_kid": "", @@ -71,8 +91,8 @@ def test_container_env_stats(): "retry_count": 60} } == ContainerEnv.read() finally: - os.environ['HAPROXY_USERNAME'] = '' - os.environ['HAPROXY_STATS_PORT'] = '' + del os.environ['HAPROXY_USERNAME'] + del os.environ['HAPROXY_STATS_PORT'] def test_container_env_stats_password(): @@ -88,6 +108,11 @@ def test_container_env_stats_password(): "port": "1936" }, + "logLevel": { + "easyhaproxy": Functions.DEBUG, + "haproxy": Functions.INFO, + "certbot": Functions.DEBUG, + }, "certbot": {"autoconfig": "", "eab_hmac_key": "", "eab_kid": "", @@ -96,7 +121,7 @@ def test_container_env_stats_password(): "retry_count": 60} } == ContainerEnv.read() finally: - os.environ['HAPROXY_PASSWORD'] = '' + del os.environ['HAPROXY_PASSWORD'] def test_container_env_stats_password_2(): @@ -113,6 +138,11 @@ def test_container_env_stats_password_2(): "password": "xyz", "port": "2101" }, + "logLevel": { + "easyhaproxy": Functions.DEBUG, + "haproxy": Functions.INFO, + "certbot": Functions.DEBUG, + }, "certbot": {"autoconfig": "", "eab_hmac_key": "", "eab_kid": "", @@ -121,9 +151,9 @@ def test_container_env_stats_password_2(): "retry_count": 60} } == ContainerEnv.read() finally: - os.environ['HAPROXY_USERNAME'] = '' - os.environ['HAPROXY_STATS_PORT'] = '' - os.environ['HAPROXY_PASSWORD'] = '' + del os.environ['HAPROXY_USERNAME'] + del os.environ['HAPROXY_STATS_PORT'] + del os.environ['HAPROXY_PASSWORD'] def test_container_env_certbot_email(): @@ -133,6 +163,11 @@ def test_container_env_certbot_email(): "customerrors": False, "ssl_mode": "default", "lookup_label": "easyhaproxy", + "logLevel": { + "easyhaproxy": Functions.DEBUG, + "haproxy": Functions.INFO, + "certbot": Functions.DEBUG, + }, "certbot": { "autoconfig": "", 'eab_hmac_key': "", @@ -143,7 +178,7 @@ def test_container_env_certbot_email(): } } == ContainerEnv.read() finally: - os.environ['EASYHAPROXY_CERTBOT_EMAIL'] = '' + del os.environ['EASYHAPROXY_CERTBOT_EMAIL'] def test_container_env_certbot_full(): @@ -154,17 +189,55 @@ def test_container_env_certbot_full(): os.environ['EASYHAPROXY_CERTBOT_RETRY_COUNT'] = "10" try: assert { - "customerrors": False, - "ssl_mode": "default", - "lookup_label": "easyhaproxy", - "certbot": { - "autoconfig": "", - "email": "acme@example.org", - "server": "schema://url/a", - 'eab_hmac_key': 'eab_hmac_key', - 'eab_kid': 'eab_kid', - 'retry_count': 10 - } - } == ContainerEnv.read() + "customerrors": False, + "ssl_mode": "default", + "lookup_label": "easyhaproxy", + "logLevel": { + "easyhaproxy": Functions.DEBUG, + "haproxy": Functions.INFO, + "certbot": Functions.DEBUG, + }, + "certbot": { + "autoconfig": "", + "email": "acme@example.org", + "server": "schema://url/a", + 'eab_hmac_key': 'eab_hmac_key', + 'eab_kid': 'eab_kid', + 'retry_count': 10 + } + } == ContainerEnv.read() finally: - os.environ['EASYHAPROXY_CERTBOT_EMAIL'] = '' + del os.environ['EASYHAPROXY_CERTBOT_EMAIL'] + del os.environ['EASYHAPROXY_CERTBOT_SERVER'] + del os.environ['EASYHAPROXY_CERTBOT_EAB_KID'] + del os.environ['EASYHAPROXY_CERTBOT_EAB_HMAC_KEY'] + del os.environ['EASYHAPROXY_CERTBOT_RETRY_COUNT'] + + +def test_container_log_level(): + os.environ['CERTBOT_LOG_LEVEL'] = Functions.TRACE + os.environ['EASYHAPROXY_LOG_LEVEL'] = Functions.ERROR + os.environ['HAPROXY_LOG_LEVEL'] = Functions.FATAL + try: + assert { + "customerrors": False, + "ssl_mode": "default", + "lookup_label": "easyhaproxy", + "logLevel": { + "easyhaproxy": Functions.ERROR, + "haproxy": Functions.FATAL, + "certbot": Functions.TRACE, + }, + "certbot": { + "autoconfig": "", + 'eab_hmac_key': "", + 'eab_kid': "", + "email": "", + "server": False, + "retry_count": 60 + } + } == ContainerEnv.read() + finally: + del os.environ['CERTBOT_LOG_LEVEL'] + del os.environ['EASYHAPROXY_LOG_LEVEL'] + del os.environ['HAPROXY_LOG_LEVEL'] diff --git a/src/tests/test_daemonize.py b/src/tests/test_daemonize.py new file mode 100644 index 0000000..e5d87c9 --- /dev/null +++ b/src/tests/test_daemonize.py @@ -0,0 +1,48 @@ +import os + +from functions import DaemonizeHAProxy, Functions + + +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(): + tmp_pid_file = "/tmp/tmp_pid.txt" + Functions.save(tmp_pid_file, "10") + + try: + daemon = DaemonizeHAProxy(os.path.abspath(os.path.dirname(__file__)) + '/fixtures') + command = daemon.get_haproxy_command("reload", tmp_pid_file) + assert command == "/usr/sbin/haproxy -W -f /etc/haproxy/haproxy.cfg -f %s -p %s -x /var/run/haproxy.sock -sf %s" % (os.path.dirname(__file__) + "/fixtures", tmp_pid_file, 10) + finally: + os.remove(tmp_pid_file) diff --git a/src/tests/test_docker.py b/src/tests/test_docker.py index b6b95e1..e31063e 100644 --- a/src/tests/test_docker.py +++ b/src/tests/test_docker.py @@ -100,7 +100,7 @@ def test_processor_docker(): 'hostssl.local.pem': 'Some PEM Certificate' } finally: - os.environ['EASYHAPROXY_CERTBOT_EMAIL'] = '' + del os.environ['EASYHAPROXY_CERTBOT_EMAIL'] container.stop() container2.stop() diff --git a/src/tests/test_functions.py b/src/tests/test_functions.py index e31a0ea..770cd55 100644 --- a/src/tests/test_functions.py +++ b/src/tests/test_functions.py @@ -13,15 +13,15 @@ def test_functions_check_local_level(): os.environ['CERTBOT_LOG_LEVEL'] = 'warn' assert Functions.skip_log('CERTBOT', Functions.INFO) == True - os.environ['CERTBOT_LOG_LEVEL'] = '' + del os.environ['CERTBOT_LOG_LEVEL'] os.environ['HAPROXY_LOG_LEVEL'] = 'warn' assert Functions.skip_log('HAPROXY', Functions.INFO) == True - os.environ['HAPROXY_LOG_LEVEL'] = '' + del os.environ['HAPROXY_LOG_LEVEL'] os.environ['EASYHAPROXY_LOG_LEVEL'] = 'warn' assert Functions.skip_log('EASYHAPROXY', Functions.INFO) == True - os.environ['EASYHAPROXY_LOG_LEVEL'] = '' + del os.environ['EASYHAPROXY_LOG_LEVEL'] def test_function_load_and_save(): @@ -57,7 +57,7 @@ def test_functions_check_log_sanity(): assert len(Functions.debug_log) == 2 finally: - os.environ['EASYHAPROXY_LOG_LEVEL'] = '' + del os.environ['EASYHAPROXY_LOG_LEVEL'] Functions.debug_log = None diff --git a/src/tests/test_parser.py b/src/tests/test_parser.py index 6ab72cb..3f6f1fa 100644 --- a/src/tests/test_parser.py +++ b/src/tests/test_parser.py @@ -114,11 +114,12 @@ def test_parser_finds_services_raw(): parsed_object = [ { - "mode": "tcp", - "health-check": "", - "port": "31339", - "hosts": { + "mode":"tcp", + "ssl-check":"", + "port":"31339", + "hosts":{ "agent.quantum.example.org": { + "balance": "roundrobin", "containers": [ "my-stack_agent:9001" ], @@ -131,18 +132,20 @@ def test_parser_finds_services_raw(): } }, { - "mode": "http", - "health-check": "", - "port": "31337", - "hosts": { - "cadvisor.quantum.example.org": { + "mode":"http", + "ssl-check":"", + "port":"31337", + "hosts":{ + "cadvisor.quantum.example.org":{ + "balance": "roundrobin", "containers": [ "my-stack_cadvisor:8080" ], "certbot": False, "redirect_ssl": False }, - "node-exporter.quantum.example.org": { + "node-exporter.quantum.example.org":{ + "balance": "roundrobin", "containers": [ "my-stack_node-exporter:9100" ], @@ -155,18 +158,20 @@ def test_parser_finds_services_raw(): }, }, { - "mode": "http", - "health-check": "", - "port": "443", - "hosts": { + "mode":"http", + "ssl-check":"", + "port":"443", + "hosts":{ "node-exporter.quantum.example.org": { + "balance": "roundrobin", "containers": [ "my-stack_node-exporter:9100" ], "certbot": False, "redirect_ssl": False }, - "www.somehost.com.br": { + "www.somehost.com.br":{ + "balance": "roundrobin", "containers": [ "some-service:80" ], @@ -184,11 +189,12 @@ def test_parser_finds_services_raw(): "ssl": True }, { - "mode": "http", - "health-check": "", - "port": "80", - "hosts": { - "www.somehost.com.br": { + "mode":"http", + "ssl-check":"", + "port":"80", + "hosts":{ + "www.somehost.com.br":{ + "balance": "roundrobin", "containers": [ "some-service:80" ], @@ -451,25 +457,28 @@ def test_parser_finds_services_clone_to_ssl_raw(): parsed_object = [ { - "health-check": "", - "hosts": { - "host2.local": { - "containers": [ - "10.152.183.215:8080" + "ssl-check":"", + "hosts":{ + "host2.local":{ + "balance":"roundrobin", + "containers":[ + "10.152.183.215:8080" ], "certbot": False, "redirect_ssl": False }, - "valida.me": { - "containers": [ - "10.152.183.62:8080" + "valida.me":{ + "balance":"roundrobin", + "containers":[ + "10.152.183.62:8080" ], "certbot": False, "redirect_ssl": False }, - "www.valida.me": { - "containers": [ - "10.152.183.62:8080" + "www.valida.me":{ + "balance":"roundrobin", + "containers":[ + "10.152.183.62:8080" ], "certbot": False, "redirect_ssl": False @@ -482,11 +491,12 @@ def test_parser_finds_services_clone_to_ssl_raw(): } }, { - "health-check": "ssl", - "hosts": { - "host2.local": { - "containers": [ - "10.152.183.215:8080" + "ssl-check":"ssl", + "hosts":{ + "host2.local":{ + "balance":"roundrobin", + "containers":[ + "10.152.183.215:8080" ], "certbot": False, "redirect_ssl": False