diff --git a/README.md b/README.md index ac4d522..fe8a32b 100644 --- a/README.md +++ b/README.md @@ -19,11 +19,23 @@ 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. + ## Features EasyHAProxy will discover the services based on the Docker Tags of the containers running on a Docker host or Docker Swarm cluster and dynamically set up the `haproxy.cfg`. Below, EasyHAProxy main features: -- Use Letsencrypt with HAProxy. +- Support Automatic Certificate Management Environment (ACME) protocol compatible with Let's encrypt and others CA. - Set your custom SSL certificates - Balance traffic between multiple replicas - Set SSL with three different levels of validations and according to the most recent definitions. @@ -34,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. @@ -59,6 +71,13 @@ Or you can install using tools: [![MicroK8s](docs/easyhaproxy_microk8s.png)](docs/microk8s.md) [![Dokku](docs/easyhaproxy_dokku.png)](docs/dokku.md) +## Special Topics + +If you already set up the EasyHAProxy, is time to go deeper: + +- [Custom SSL](docs/ssl.md) +- [Automatic Certificate Issuing](docs/acme.md) (e.g. Letsencrypt) + ## See EasyHAProxy in action Click on the image to see the videos (use HD for better visualization) 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/deploy/docker/docker-compose.yml b/deploy/docker/docker-compose.yml index 2586815..2c3284f 100644 --- a/deploy/docker/docker-compose.yml +++ b/deploy/docker/docker-compose.yml @@ -5,13 +5,13 @@ services: image: byjg/easy-haproxy:4.3.1-rc1 volumes: - /var/run/docker.sock:/var/run/docker.sock - - certs_letsencrypt:/certs/letsencrypt + - certs_certbot:/certs/certbot - certs_haproxy:/certs/haproxy environment: EASYHAPROXY_DISCOVER: docker EASYHAPROXY_LABEL_PREFIX: easyhaproxy - EASYHAPROXY_LETSENCRYPT_EMAIL: changeme@example.org + EASYHAPROXY_CERTBOT_EMAIL: changeme@example.org EASYHAPROXY_SSL_MODE: "default" HAPROXY_CUSTOMERRORS: "true" HAPROXY_USERNAME: admin @@ -27,7 +27,7 @@ services: - easyhaproxy volumes: - certs_letsencrypt: + certs_certbot: external: true certs_haproxy: external: true diff --git a/deploy/docker/install.sh b/deploy/docker/install.sh index d5ab2ff..366b146 100755 --- a/deploy/docker/install.sh +++ b/deploy/docker/install.sh @@ -3,7 +3,7 @@ ASSETS_DIR="$(dirname "${BASH_SOURCE[0]}")"/../../build/assets/certs/haproxy docker network create easyhaproxy -docker volume create certs_letsencrypt +docker volume create certs_certbot docker volume create certs_haproxy docker run -d --rm --name easyhaproxy_install -v certs_haproxy:/certs alpine tail -f /dev/null diff --git a/docs/acme.md b/docs/acme.md new file mode 100644 index 0000000..f695726 --- /dev/null +++ b/docs/acme.md @@ -0,0 +1,99 @@ +# SSL - Automatic Certificate Management Environment (ACME) + +The Automatic Certificate Management Environment (ACME) protocol +allow automating interactions between certificate authorities and their users' servers, +allowing the automated deployment of public key infrastructure. + +Most of the issuers offers Automatic Issuing free of cost. + +## Environment Variables + +To enable the ACME protocol we need to enable Certbot in EasyHAProxy by setting up to 4 environment variables: + +| Environment Variable | Required? | Description | +|----------------------------------|-----------|----------------------------------------------------------------------------------------------------------------------------------| +| EASYHAPROXY_CERTBOT_EMAIL | YES | Your email in the certificate authority. | +| EASYHAPROXY_CERTBOT_AUTOCONFIG | - | Will use pre-sets for your Certificate Authority (CA). See table below. | +| EASYHAPROXY_CERTBOT_SERVER | - | The ACME Endpoint of your certificate authority. If you use AUTOCONFIG, it is set automatically. See table below. | +| EASYHAPROXY_CERTBOT_EAB_KID | - | External Account Binding (EAB) Key Identifier (KID) provided by your certificate authority. Some CA require it. See table below. | +| EASYHAPROXY_CERTBOT_EAB_HMAC_KEY | - | External Account Binding (EAB) HMAC Key provided by your certificate authority. Some CA require it. See table below. | +| EASYHAPROXY_CERTBOT_RETRY_COUNT | - | Wait 'n' requests before retrying issue invalid requests. Default 60. | + +## Auto Config Certificate Authority (CA) + +Here are detailed instructions per Certificate Authority (CA). If anyone is missing, please let's know. + +| CA | Auto Config | Free? | Account Required? | EAB KID? | EAB HMAC Key? | More Info | +|----------------------|------------------|-------|--------------------|----------|---------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Let's Encrypt | - | Yes | No | No | No | - | +| Let's Encrypt (Test) | letsencrypt_test | Yes | No | No | No | - | +| ZeroSSL | zerossl | Yes | No | No | No | [Link](https://zerossl.com/documentation/acme/) | +| BuyPass | buypass | Yes | No | No | No | [Link](https://community.buypass.com/t/63d4ay/buypass-go-ssl-endpoints-updated-14-05-2020) | +| BuyPass (test) | buypass_test | Yes | No | No | No | [Link](https://community.buypass.com/t/63d4ay/buypass-go-ssl-endpoints-updated-14-05-2020) | +| Google | google | Yes | Yes | Yes | Yes | [Link](https://cloud.google.com/blog/products/identity-security/automate-public-certificate-lifecycle-management-via--acme-client-api) | +| Google Test | google_test | Yes | Yes | Yes | Yes | [Link](https://cloud.google.com/blog/products/identity-security/automate-public-certificate-lifecycle-management-via--acme-client-api) | +| SSLCOM RCA | sslcom_rca | Trial | EAB Keys by email. | Yes | Yes | [Link](https://www.ssl.com/blogs/sslcom-supports-acme-protocol-ssl-tls-certificate-automation/) | +| SSLCOM ECC | sslcom_ecc | Trial | EAB Keys by email. | Yes | Yes | [Link](https://www.ssl.com/blogs/sslcom-supports-acme-protocol-ssl-tls-certificate-automation/) | +| Digicert | - | No | Yes | Yes | Yes | [Link](https://docs.digicert.com/en/certcentral/certificate-tools/certificate-lifecycle-automation-guides/use-a-third-party-acme-client-for-host-automations.html) | +| Entrust | - | No | Yes | Yes | Yes | [Link](https://www.entrust.com/knowledgebase/ssl/how-to-use-acme-to-install-ssl-tls-certificates-in-entrust-certificate-services-apache) | +| Sectigo | - | No | Yes | Yes | Yes | [Link](https://www.sectigo.com/resource-library/sectigos-acme-automation) | + +This configuration is global. After set up ACME properly, is necessary enable for each domain the certificate request. + +To do that add the label: `easyhaproxy..certbot=true`. See the method of installation you are using to learn how to set up properly. + +## Example + +### Setting up EasyHAProxy + +Run the EasyHAProxy container: + +```bash +docker run \ + ... \ + -e EASYHAPROXY_CERTBOT_AUTOCONFIG=zerossl \ + -e EASYHAPROXY_CERTBOT_EMAIL=john@doe.com \ + -p 80:80 \ + -p 443:443 \ + -v /path/to/guest/certbot/certs:/certs/certbot \ + ... \ + byjg/easy-haproxy +``` + +Notes: + +- The `EASYHAPROXY_CERTBOT_AUTOCONFIG` is not required for Let's Encrypt. In this example, the certificate will be issued by ZeroSSL. +- If you don't setup `EASYHAPROXY_CERTBOT_EMAIL` environment variable, EasyHAProxy will fail silently and **will not request** a certificate. +- The ports 80 and 443 needs to accessible through the internet as [Let's Encrypt requirement](https://letsencrypt.org/docs/allow-port-80/) + +In order to avoid several certificate issuing, +**It is required you to persist the container folder `/certs/certbot` outside the container.** +You cannot delete or change it contents. +If you do not persist, or change/delete the contents, Issue a certificate might not work properly and hit rate limit. + +If you are using Let's Encrypt, be aware of it rate limits: + +- https://letsencrypt.org/docs/duplicate-certificate-limit/ +- https://letsencrypt.org/docs/rate-limits/ + + +## Setting up your container to use the ACME CA + +```bash +docker run \ + ... \ + --label easyhaproxy.express.port=80 \ + --label easyhaproxy.express.localport=3000 \ + --label easyhaproxy.express.host=example.org \ + --label easyhaproxy.express.certbot=true \ + ... \ + some/myimage +``` + +Requirements: + +- Your container **must** listen to port 80. The CA will not issue the certificate if `easyhaproxy..port` is in another port, and EasyHAProxy will fail silently. +- You cannot set port 443 for the container with the Letsencrypt because EasyHAProxy will create this port automatically once the certificate is issued. + +---- +[Open source ByJG](http://opensource.byjg.com) \ No newline at end of file diff --git a/docs/container-labels.md b/docs/container-labels.md index b71cecb..daa1b51 100644 --- a/docs/container-labels.md +++ b/docs/container-labels.md @@ -2,19 +2,20 @@ ## Container (Docker or Swarm) labels -| Tag | Description | Default | Example | -|---------------------------------------|-------------------------------------------------------------------------------------------------------|----------------|--------------| -| easyhaproxy.[definition].host | Host(s) HAProxy is listening. More than one host use comma as delimiter | **required** | somehost.com OR host1.com,host2.com | -| easyhaproxy.[definition].mode | (Optional) Is this `http` or `tcp` mode in HAProxy. | http | http or tcp | -| easyhaproxy.[definition].port | (Optional) Port HAProxy will listen for the host. | 80 | 3000 | -| easyhaproxy.[definition].localport | (Optional) Port container is listening. | 80 | 8080 | -| 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].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].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].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 | +| Tag | Description | Default | Example | +|---------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------|--------------|------------------------------------------------------------| +| easyhaproxy.[definition].host | Host(s) HAProxy is listening. More than one host use comma as delimiter | **required** | somehost.com OR host1.com,host2.com | +| easyhaproxy.[definition].mode | (Optional) Is this `http` or `tcp` mode in HAProxy. | http | http or tcp | +| easyhaproxy.[definition].port | (Optional) Port HAProxy will listen for the host. | 80 | 3000 | +| easyhaproxy.[definition].localport | (Optional) Port container is listening. | 80 | 8080 | +| 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].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 deleted file mode 100644 index 0b9ec11..0000000 --- a/docs/docker-environment.md +++ /dev/null @@ -1,22 +0,0 @@ -# Docker environment variables - -| Environment Variable | Description | Default | -|---------------------------------|-------------------------------------------------------------------------------------------------|------------------| -| 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_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_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 | -| CERTBOT_LOG_LEVEL | (Optional) The log level for Certbot messages. Available: TRACE,DEBUG,INFO,WARN,ERROR,FATAL | DEBUG | -| HAPROXY_LOG_LEVEL | (Optional) The log level for HAProxy messages. Available: TRACE,DEBUG,INFO,WARN,ERROR,FATAL | DEBUG | -| HAPROXY_USERNAME | (Optional) The HAProxy username to the statistics. | `admin` | -| HAPROXY_PASSWORD | (Optional) The HAProxy password to the statistics. If not set, statistics will be available with no password | *empty* | -| HAPROXY_STATS_PORT | (Optional) The HAProxy port to the statistics. If set to `false`, disable statistics | `1936` | -| 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 e8d85e4..362b309 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,16 +51,15 @@ 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) +You can configure the behavior of the EasyHAProxy by setup specific environment variables. To get a list of the variables, please follow the [environment variable guide](environment-variable.md) +## Setup certificates with ACME (e.g. Letsencrypt) -## Setup certificates with Letsencrypt - -Follow [this link](letsencrypt.md) +Follow [this link](acme.md) ## Setup your own certificates diff --git a/docs/environment-variable.md b/docs/environment-variable.md new file mode 100644 index 0000000..b00bacf --- /dev/null +++ b/docs/environment-variable.md @@ -0,0 +1,20 @@ +# Docker environment variables + +| Environment Variable | Description | Default | +|----------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------| +| 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_CERTBOT_* | (Optional) Enable Let's Encrypt or any other ACME certificate. See more: [acme](acme.md) | *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_REFRESH_CONF | (Optional) Check for new containers/services every N seconds. | 10 | +| EASYHAPROXY_LOG_LEVEL | (Optional) The log level for EasyHAproxy messages. Available: TRACE,DEBUG,INFO,WARN,ERROR,FATAL | DEBUG | +| CERTBOT_LOG_LEVEL | (Optional) The log level for Certbot messages. Available: TRACE,DEBUG,INFO,WARN,ERROR,FATAL | DEBUG | +| HAPROXY_LOG_LEVEL | (Optional) The log level for HAProxy messages. Available: TRACE,DEBUG,INFO,WARN,ERROR,FATAL | DEBUG | +| HAPROXY_USERNAME | (Optional) The HAProxy username to the statistics. | `admin` | +| HAPROXY_PASSWORD | (Optional) The HAProxy password to the statistics. If not set, statistics will be available with no password | *empty* | +| HAPROXY_STATS_PORT | (Optional) The HAProxy port to the statistics. If set to `false`, disable statistics | `1936` | +| HAPROXY_CUSTOMERRORS | (Optional) If HAProxy will use custom HTML errors. true/false. | `false` | + + +---- +[Open source ByJG](http://opensource.byjg.com) diff --git a/docs/helm.md b/docs/helm.md index 376d5ff..ae35f59 100644 --- a/docs/helm.md +++ b/docs/helm.md @@ -51,7 +51,7 @@ easyhaproxy: certbot: DEBUG easyhaproxy: DEBUG haproxy: DEBUG - letsencrypt: + certbot: email: "" service: diff --git a/docs/kubernetes.md b/docs/kubernetes.md index 8b65f6f..a7f4e6f 100644 --- a/docs/kubernetes.md +++ b/docs/kubernetes.md @@ -39,7 +39,7 @@ kubectl apply -f \ https://raw.githubusercontent.com/byjg/docker-easy-haproxy/4.3.1-rc1/deploy/kubernetes/easyhaproxy-daemonset.yml ``` -If necessary, you can configure environment variables. To get a list of the variables, please follow the [docker container environment](docker-environment.md) +If necessary, you can configure environment variables. To get a list of the variables, please follow the [environment variable guide](environment-variable.md) ## Running containers @@ -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,34 +79,36 @@ 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.letsencrypt | (optional) Boolean. It will request letsencrypt 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. -## Letsencrypt +## Certbot / ACME / Letsencrypt -It is necessary add the annotation `easyhaproxy.letsencrypt` to the ingress configuration: +It is necessary add the annotation `easyhaproxy.certbot` to the ingress configuration: ```yaml kind: Ingress metadata: annotations: kubernetes.io/ingress.class: easyhaproxy-ingress - easyhaproxy.letsencrypt: 'true' + easyhaproxy.certbot: 'true' name: example-ingress namespace: example spec: .... ``` -Make sure your cluster is accessible both through ports 80 and 443. +More info [here](acme.md). + +Make sure your cluster is accessible both through ports 80 and 443. ## Custom SSL Certificates diff --git a/docs/letsencrypt.md b/docs/letsencrypt.md deleted file mode 100644 index 0e8c86b..0000000 --- a/docs/letsencrypt.md +++ /dev/null @@ -1,50 +0,0 @@ -# Letsencrypt - -EasyHAProxy can issue a letsencrypt certificate. Follow the steps below: - -Run the EasyHAProxy: - -```bash -docker run \ - -e EASYHAPROXY_LETSENCRYPT_EMAIL=john@doe.com - .... \ - byjg/easy-haproxy -``` - -Run your container: - -```bash -docker run \ - --label easyhaproxy.express.port=80 \ - --label easyhaproxy.express.localport=3000 \ - --label easyhaproxy.express.host=example.org \ - --label easyhaproxy.express.letsencrypt=true \ - .... \ - some/myimage -``` - -Requirements: - -- Your container **must** listen to port 80. Letsencrypt will not issue the certificate if `easyhaproxy.express.port` is in another port, and EasyHAProxy will fail silently. -- You cannot set port 443 for the container with the Letsencrypt because EasyHAProxy will create this port automatically once the certificate is issued. -- `EASYHAPROXY_LETSENCRYPT_EMAIL` environment variable is required to be set. If you don't set it up, EasyHAProxy **will not request** a certificate. - -Be aware of Letsencrypt issue limits - https://letsencrypt.org/docs/duplicate-certificate-limit/ and https://letsencrypt.org/docs/rate-limits/ - -## Persist your Letsencrypt certificates - -It is a good idea to store the letsencrypt certificate in persistent storage because of the limit on how many certificates can be issued for the same domain in a period. - -To do this, map the folder `/certs/letsencrypt` to a docker volume. - -```bash -docker volume create certs_letsencrypt - -docker run \ - /* other parameters */ - -v certs_letsencrypt:/certs/letsencrypt \ - -d byjg/easy-haproxy -``` - ----- -[Open source ByJG](http://opensource.byjg.com) diff --git a/docs/ssl.md b/docs/ssl.md index 4384bbf..9b49e0c 100644 --- a/docs/ssl.md +++ b/docs/ssl.md @@ -1,12 +1,15 @@ # Setup custom certificates -You can use your certificates with EasyHAProxy. +You can use your own certificates with EasyHAProxy. You just need to let EasyHAProxy know that certificate. There are two ways to do that. +- [Setup certificate as a label definition in docker container](#setup-certificate-as-a-label-definition-in-docker-container) +- [Map the certificate as a docker volume](#map-the-certificate-as-a-docker-volume) + ## Setup certificate as a label definition in docker container -### Create a single PEM from the certificate and key. +1. Create a single PEM from the certificate and key. ```bash cat example.com.crt example.com.key > single.pem @@ -24,21 +27,21 @@ MIIEojCCA4qgAwIBAgIUegW2BimwuL4RzRZ2WYkHA6U5nkAwDQYJKoZIhvcNAQEL -----END PRIVATE KEY----- ``` -### Convert the `single.pem` to BASE64 in a single line: +2. Convert the `single.pem` to BASE64 in a single line: ```bash cat single.pem | base64 -w0 ``` -### Map the BASE64 to your container +3. Define a label in yout container Add the Base64 string you generated before to the label `easyhaproxy.[definition].sslcert` ## Map the certificate as a docker volume -EasyHAProxy stores the certificates inside the folder `/certs/haproxy`. +EasyHAProxy stores the certificates inside the container folder `/certs/haproxy`. -### Run EasyHAProxy with the volume for the certificates: +1. Run EasyHAProxy with the volume for the certificates: ```bash docker volume create certs_haproxy @@ -49,7 +52,7 @@ docker run \ -d byjg/easy-haproxy ``` -### Create a single PEM from the certificate and the key. +2. Create a single PEM from the certificate and the key. ```bash cat example.com.crt example.com.key > single.pem @@ -67,7 +70,7 @@ MIIEojCCA4qgAwIBAgIUegW2BimwuL4RzRZ2WYkHA6U5nkAwDQYJKoZIhvcNAQEL -----END PRIVATE KEY----- ``` -### Copy this certificate to EasyHAProxy volume +3. Copy this certificate to EasyHAProxy volume ```bash docker cp single.pem easyhaproxy:/certs/haproxy diff --git a/docs/static.md b/docs/static.md index 6a35bda..4f7c3a1 100644 --- a/docs/static.md +++ b/docs/static.md @@ -20,7 +20,10 @@ customerrors: true # Optional (default false) ssl_mode: default -letsencrypt: { +logLevel: + haproxy: INFO + +certbot: { "email": "acme@example.org" } @@ -30,7 +33,7 @@ easymapping: host1.com.br: containers: - container:5000 - letsencrypt: true + certbot: true redirect_ssl: true host2.com.br: containers: @@ -69,7 +72,7 @@ docker run -d \ byjg/easy-haproxy ``` -You can find other informations on [docker label configuration](container-labels.md) and [docker container environment](docker-environment.md) +You can find other informations on [docker label configuration](container-labels.md) and [environment variable guide](environment-variable.md) ## Yaml Definition @@ -83,9 +86,18 @@ customerrors: true # Optional (default false) ssl_mode: default # Optional -letsencrypt: { # Optional. If you enable `letsencrypt` will need to setu0p this, - # otherwise the certificate will be issued - "email": "acme@example.org" +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: + email: "acme@example.org" # If email is defined enable ACME/Certbot + autoconfig: "" # If empty use letsencrypt, otherwise try to set the CA defined. + eab_hmac_key: "" # If required by the CA, set here. + eab_kid: "" # If required by the CA, set here. + server: False # If empty/False uses Letsencrypt, otherwise the CA Endpoint defined here + retry_count: 60 # If the certificate reaches the Rate Limit, try again after 'n' iterations. } easymapping: @@ -95,7 +107,7 @@ easymapping: host1.com.br: # Hostname containers: - container:5000 # Endpoints of the hostname above (ip, dns, container, etc) - letsencrypt: true # Optional. it will request a letsencrypt certiticate + certbot: true # Optional. it will request a certbot certificate. Needs certbot.email set. redirect_ssl: true # Optional. It will redirect this site to it SSL. ssl: true # Optional. Inform this port will listen to SSL, instead of HTTP clone_to_ssl: true # Optional. Default False. You clone these hosts to its equivalent SSL. @@ -103,7 +115,8 @@ easymapping: www.host1.com.br: http://host1.com.br ``` -**Note**: The only way to pass SSL certificates is to map the certificates to EasyHAProxy as a docker volume. Refer to the [SSL documentation](ssl.md) to learn how to do it. +**Note**: The only way to pass SSL certificates in the static configuration file is to map the certificates +to EasyHAProxy as a docker volume. Refer to the [SSL documentation](ssl.md) to learn how to do it. ---- [Open source ByJG](http://opensource.byjg.com) diff --git a/docs/swarm.md b/docs/swarm.md index fc671d1..16fe71c 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,17 +83,15 @@ 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 -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) +You can configure the behavior of the EasyHAProxy by setup specific environment variables. To get a list of the variables, please follow the [environment variable guide](environment-variable.md) ## More information 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/docker/docker-compose-portainer-app-example.yml b/examples/docker/docker-compose-portainer-app-example.yml index 70f6290..99c9ed1 100644 --- a/examples/docker/docker-compose-portainer-app-example.yml +++ b/examples/docker/docker-compose-portainer-app-example.yml @@ -8,7 +8,7 @@ services: easyhaproxy.http.host: test.xpto.us easyhaproxy.http.port: 80 easyhaproxy.http.localport: 8080 - easyhaproxy.http.letsencrypt: true + easyhaproxy.http.certbot: true networks: diff --git a/examples/docker/docker-compose-portainer.yml b/examples/docker/docker-compose-portainer.yml index ec9fd3a..9263b50 100644 --- a/examples/docker/docker-compose-portainer.yml +++ b/examples/docker/docker-compose-portainer.yml @@ -1,4 +1,4 @@ -# docker volume create certs_letsencrypt +# docker volume create certs_certbot # docker volume create certs_haproxy # docker volume create portainer_data # docker network create easyhaproxy @@ -11,13 +11,13 @@ services: image: byjg/easy-haproxy volumes: - /var/run/docker.sock:/var/run/docker.sock - - certs_letsencrypt:/certs/letsencrypt + - certs_certbot:/certs/certbot # - certs_haproxy:/certs/haproxy environment: EASYHAPROXY_DISCOVER: docker EASYHAPROXY_LABEL_PREFIX: easyhaproxy - EASYHAPROXY_LETSENCRYPT_EMAIL: changeme@example.org + EASYHAPROXY_CERTBOT_EMAIL: changeme@example.org EASYHAPROXY_SSL_MODE: "default" HAPROXY_CUSTOMERRORS: "true" HAPROXY_USERNAME: admin @@ -32,18 +32,18 @@ services: portainer: image: portainer/portainer-ce:latest volumes: - - portainer_data:/data portainer + - portainer_data:/data - /var/run/docker.sock:/var/run/docker.sock labels: easyhaproxy.http.redirect_ssl: true - easyhaproxy.http.letsencrypt: true + easyhaproxy.http.certbot: true easyhaproxy.http.host: portainer.xpto.us easyhaproxy.http.port: 80 easyhaproxy.http.localport: 9000 volumes: - certs_letsencrypt: + certs_certbot: external: true certs_haproxy: external: true 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----- +MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQDAQwISwMoCbmlu +sOn34oM0wABl6ZhM9LSMZ9ZOGcecDea6b/6rtEDGM/gL9rtXjihCmXmhUVaU9K/d +3V+yL2xGR7duRj4gb5SxqoYykT50pP+1PF5rJ7bVAFe61RCtVK3RcPPyMK4kAvnY +a9Jp2H0gzJLwaDBaXYVTU62wGdUweq0IxOwK9DLfJsvelyZsK3Iytk4/hNq4DyFM +9XuXHpBWCA0gWrZsPt/r6/k+cRTRZCdqN9FuaPGX5QiSc61WooakInLOmSg4KX+4 +qlzBYsFiJYXUKRr8eRn/yhUyvVGjp0Wc/1/tgnmwBWkx0d5Pz2KaVk4AQQbkb5aQ +sPsHZ5JrXgtEkERfdUFla+3o6OymjpRiOVbbzFLFr03gGZqWspiGgExE7jiWmNCV +NdTpi3g/Qzo+RNkkO6iF/6RXKcacxuY+HPjJYL8T9lQJDU3gFwmmIjwUB+WWYNBx +NlpwdSoS0RZR3eIh0A9hiecjf+nwhQX2XyknivCWrsMYFpK2udznA6v5infgwP3S +vv67hu7asFYCGtOBzqZczOy2ERgdCK7Pfpa+fzyW0GZlNO9zHZ+nFwIN6fusL8BZ +Q5Eub9d4AQ2DR7Retm3OedXnRWepBsye8JhkaPOGgT+zLkWXkXizq0kuu2tcC3c2 +bDJz9Uye8fS36/TCpdrg50HZ2HsfAwIDAQABAoICAQC/xZbZ0cctqagsqvaVNTEe +eq1q+hfaGvPEYQaYHIrIE+2i5XcnGcLKcKfodxDjAn8R/zgdOp6cMX0CVn/PohHk +AEDtE8+AVwwAM1FsOwgLHVGaGz8qrxBlYdQgHcpmueIu2PXbC8eHUBiaUOIuhaw5 +/RRMDAC/Ai2ssfi7gOjvVE4oQxQW0QG1KGOOAUJn/uYHw2RFY2Uu1pimxO2kDO53 +gcxmC1WOnyCHmHaiW/Uh7z6JamfSM4dXtTJZslyh37dhHKNbg9VkP7CQKA4hLzop +hbf5qY6rargONiny1HgMPxrmwKuUouJyOtN0yBtxjDCUNaXUBwiy7sNGS+H4vsyB +5P9HhIHStu+FZt3HG7EIqCndiaSKDS4jWaVQAbbo4nZ2Zs2BD+xDePRCRUqX7rM4 +4XzPIRWWXmmWf/7Ig29Hbrp4a9LcOmQ2leCJtbaTFSN96OLUJ5E+hQ0ulCZgBVmQ +RCUYkJP4lOzbaKdzjxgHMrHzm45eUFf8LirOxi2uyxXHQmDNu4b3X18kt3PgUmUm +3dXpl3fqSyJa7SCV8ZNBrsrDq1E+thYtu91QbVSGxHd9HrNVe3XdLbOCdU9CuC69 +Nglznaa7sZLqmyKejTfGsY7xrWdNcMPl4p4fcID/O4EpASZforpTeKNT0ZIfZZew +b0mAQeYZqQM8i/qMYN/uAQKCAQEA5qg1sRNMc6VdM/tRglasGYoxjgRC2OqADZgs +mAXMUJ3kErpyxt+eCimy8ibuYpzRTIQ8fBTWRkCtRZXJ7+KcLVtk9QZIoLbhyNwd +4IxEQZFuUljDbvSjTLSycsHvo65ibWIfTL7bgWlLGgGq/UOzfGsgH6S9wLp5G30G +8ELyjI5eTIYICrfTmVL+c45MRpEMKo+cvz8PysiaOFTn3cyswPVdYaeEEqMQjU8w +IGNsGZLytY7BABBcY0ldrtba/O+Fv/+RH7uUtzP7xpCIwFCx80ZzN+WRy9NvI63U +zq3yIBoW9GyApD2+PLaPNxf7QLTUChY1Zz/dYRltKOxv2Aa5gQKCAQEA1WLWNqp0 +fhB/ZtfSEShxFMM89cjN6Aaz1WKL7uTBou9oSJnxjkhkaV76acnT/iqXtxMNgHi1 +fImDpU3PvM0Y4Ud2T47oHc6P1BrZPN/GmXy/s6BAEdPwLe7J+4nTISHAdGmrh+a/ +5pktu32g9lWqftxecFIVSLPWkxT0XKiMxp1ffkL+OavpMgMFZK41iKs3dNShKPog +L8GSPcP9x/yn78P2eK3N+PGjlA6pPzrANyWU7N0/bmHcB9TKP+udYWcjVhru7MYN +wNrE4kKdC8v8i7x7tDbvb79T+Fo6PIh53p0OsnZzA8UR0QNR+vDQufQuyaj8REC+ +ZG8YyCKsvk8ygwKCAQA/fsSxB0f/eeErYx6wC536teEoYCHqxrsTgvWbr9TryFs1 +kJ/yATLnR01cfb0X5mVzc9+WpMHLuxg31KEvaSlnDwa+sMkjfNSwz29mFhbgGeHN +x2OdUrj1b7TEBIEshN/RjrZhERUqDcs/0H+6kn2BXZgNPfOCb5LRL1zOnQ9aBAMP +e8IQ+UPFrGQheWWj81/vA3O57ekyAID7ytu9Yg+YWrMnI88mtj7jN45fDB+A9sPb +mP2mP9q+9j5U2A6WnHUsQnU30BKDUEsaAUWz80LZXmZvV8IH4x9wKfUwJBBIKAZz +qL7M97Y7zmGkX/Spfl30nOJ8lschaLd1EYlEZa2BAoIBAQCye25T4TV5MJFv0zuZ +MGuNg1Sc/O4Fkn2fEUOceWjhwUBH4cPjT/f1DwWDsNaJ9NRbxCr5931OArPDc5c8 +A404+Y4jM5RBQkKZli94tHAod+jc9UBB6TUvJll59SlMwC9679wS21ZOKnfPKGCX +SsZGQEsZxf6ZhhsHgXJ3gl/lzUJPmPeOA5YVR+Od9/09KIFFTojSfoynhVCuKx49 +xb4uVYn2HOJ4xJ0fPTghdCHMvrmXeeQRjvb88eaNmqVUEHHFFtgb4fklA5fE7RTx +BhliRDBwZ7bUkINK6yVk9n6BTns5mMvRLmgdnJpYvE7KC02LTbZb3I+j8C0ZUa+N +qy7DAoIBAAieribS7WUcl2aBlkm5+W7qNm/INm5zvnoSPo6V3wa5hs6f9+C/kbdF +87jQPA/YFe3uR2sAJ7slX5euZK8WmfpFmgzlu0sEz81MLQ/WypZtZytyVtWzB2Pu +XCW1tdSH9eI2BmhXgokHNTM48Nk/xOENrP/seXrIx5LK0hnDHZotu/z6+YSkB9hF +cm2fZygD1dMLX6liRimxyFY+dICJNB95JifTLWYnWeGddkwPtXUeGXE1olzvNkLD +zMzE09uhkx/lRJnteOBEZaf80OB/09Oi9b9/rxY59dwsH6GaxLoTfEKuPnvBVMNR +YkU14WzQKleFkiBJI9lVvnfgGnOlgg0= +-----END PRIVATE KEY----- \ No newline at end of file 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/helm/easyhaproxy/templates/deployment.yaml b/helm/easyhaproxy/templates/deployment.yaml index ecaa538..07cae85 100644 --- a/helm/easyhaproxy/templates/deployment.yaml +++ b/helm/easyhaproxy/templates/deployment.yaml @@ -69,7 +69,7 @@ spec: value: {{ .Values.easyhaproxy.logLevel.haproxy }} - name: CERTBOT_LOG_LEVEL value: {{ .Values.easyhaproxy.logLevel.certbot }} - {{- if .Values.easyhaproxy.letsencrypt.email }} - - name: EASYHAPROXY_LETSENCRYPT_EMAIL - value: {{ .Values.easyhaproxy.letsencrypt.email }} + {{- if .Values.easyhaproxy.certbot.email }} + - name: EASYHAPROXY_CERTBOT_EMAIL + value: {{ .Values.easyhaproxy.certbot.email }} {{ end }} diff --git a/helm/easyhaproxy/values.yaml b/helm/easyhaproxy/values.yaml index c3bb6a6..2ed4dfd 100644 --- a/helm/easyhaproxy/values.yaml +++ b/helm/easyhaproxy/values.yaml @@ -69,7 +69,7 @@ easyhaproxy: certbot: DEBUG easyhaproxy: DEBUG haproxy: DEBUG - letsencrypt: + certbot: email: "" # Make sure to create this diff --git a/src/easymapping/__init__.py b/src/easymapping/__init__.py index c2626a6..bb1c833 100644 --- a/src/easymapping/__init__.py +++ b/src/easymapping/__init__.py @@ -1,12 +1,13 @@ import base64 -import hashlib -from jinja2 import Environment, FileSystemLoader import json -import os import re +from jinja2 import Environment, FileSystemLoader + + class DockerLabelHandler: def __init__(self, label): + self.__data = None self.__label_base = label def get_lookup_label(self): @@ -18,19 +19,17 @@ class DockerLabelHandler: return "{}.{}".format(self.__label_base, ".".join(key)) - - def get(self, label, default_value = ""): + def get(self, label, default_value=""): if self.has_label(label): return self.__data[label] return default_value - - def get_bool(self, label, default_value = False): + def get_bool(self, label, default_value=False): if self.has_label(label): return self.__data[label].lower() in ["true", "1", "yes"] return default_value - def get_json(self, label, default_value = {}): + def get_json(self, label, default_value={}): if self.has_label(label): return json.loads(self.__data[label]) return default_value @@ -38,7 +37,6 @@ class DockerLabelHandler: def set_data(self, data): self.__data = data - def has_label(self, label): if label in self.__data: return True @@ -49,16 +47,16 @@ class HaproxyConfigGenerator: def __init__(self, mapping): self.mapping = mapping self.mapping.setdefault("ssl_mode", 'default') - self.mapping.setdefault("letsencrypt", {"email": "", "staging": False}) + self.mapping.setdefault("certbot", {"email": "", "server": False, "eab_kid": False, "eab_hmac_key": False}) self.mapping["ssl_mode"] = self.mapping["ssl_mode"].lower() self.label = DockerLabelHandler(mapping['lookup_label'] if 'lookup_label' in mapping else "easyhaproxy") - self.letsencrypt_hosts = [] + self.certbot_hosts = [] self.serving_hosts = [] self.certs = {} - - def generate(self, container_metadata = {}): + + def generate(self, container_metadata={}): self.mapping.setdefault("easymapping", []) - + if container_metadata != {}: self.mapping["easymapping"] = self.parse(container_metadata) @@ -70,7 +68,6 @@ class HaproxyConfigGenerator: template = env.get_template('haproxy.cfg.j2') return template.render(data=self.mapping) - def parse(self, container_metadata): easymapping = dict() @@ -106,10 +103,10 @@ class HaproxyConfigGenerator: "80" ) - letsencrypt = self.label.get_bool( - self.label.create([definition, "letsencrypt"]), + certbot = self.label.get_bool( + self.label.create([definition, "certbot"]), False - ) and self.mapping["letsencrypt"]["email"] != "" + ) and self.mapping["certbot"]["email"] != "" clone_to_ssl = self.label.get_bool( self.label.create([definition, "clone_to_ssl"]) ) @@ -117,7 +114,7 @@ class HaproxyConfigGenerator: if port not in easymapping: easymapping[port] = { "mode": mode, - "health-check": "", + "ssl-check": "", "port": port, "hosts": dict(), "redirect": dict(), @@ -129,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"]), "" ) @@ -139,32 +136,36 @@ class HaproxyConfigGenerator: self.serving_hosts.append("%s:%s" % (hostname, port)) easymapping[port]["hosts"].setdefault(hostname, {}) easymapping[port]["hosts"][hostname].setdefault("containers", []) - easymapping[port]["hosts"][hostname].setdefault("letsencrypt", False) + easymapping[port]["hosts"][hostname].setdefault("certbot", False) easymapping[port]["hosts"][hostname]["containers"] += ["{}:{}".format(container, ct_port)] - easymapping[port]["hosts"][hostname]["letsencrypt"] = letsencrypt + easymapping[port]["hosts"][hostname]["certbot"] = certbot 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"]) ) - if letsencrypt or clone_to_ssl: + if certbot or clone_to_ssl: if "443" not in easymapping: easymapping["443"] = { "mode": "http", - "health-check": "ssl", + "ssl-check": "ssl", "port": "443", "hosts": dict(), "redirect": dict(), } easymapping["443"]["hosts"][hostname] = dict(easymapping[port]["hosts"][hostname]) - easymapping["443"]["hosts"][hostname]["letsencrypt"] = False + easymapping["443"]["hosts"][hostname]["certbot"] = False easymapping["443"]["hosts"][hostname]["redirect_ssl"] = False easymapping["443"]["ssl"] = True - self.letsencrypt_hosts.append(hostname) if letsencrypt and hostname not in self.letsencrypt_hosts else self.letsencrypt_hosts - + self.certbot_hosts.append( + hostname) if certbot and hostname not in self.certbot_hosts else self.certbot_hosts # handle SSL ssl_label = self.label.create([definition, "sslcert"]) diff --git a/src/functions/__init__.py b/src/functions/__init__.py index d174dcf..1b3b1c0 100644 --- a/src/functions/__init__.py +++ b/src/functions/__init__.py @@ -1,17 +1,97 @@ -from datetime import datetime -from multiprocessing import Process, Lock -import subprocess -import shlex -import time import os -import re +import shlex +import subprocess import time +from datetime import datetime +from multiprocessing import Process + +import requests +from OpenSSL import crypto + + +class ContainerEnv: + @staticmethod + def read(): + env_vars = { + "customerrors": True if os.getenv("HAPROXY_CUSTOMERRORS") == "true" else False, + "ssl_mode": os.getenv("EASYHAPROXY_SSL_MODE").lower() if os.getenv("EASYHAPROXY_SSL_MODE") else 'default' + } + + if os.getenv("HAPROXY_PASSWORD"): + env_vars["stats"] = { + "username": os.getenv("HAPROXY_USERNAME") if os.getenv("HAPROXY_USERNAME") else "admin", + "password": os.getenv("HAPROXY_PASSWORD"), + "port": os.getenv("HAPROXY_STATS_PORT") if os.getenv("HAPROXY_STATS_PORT") else "1936", + } + + 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", ""), + "server": os.getenv("EASYHAPROXY_CERTBOT_SERVER", False), + "eab_kid": os.getenv("EASYHAPROXY_CERTBOT_EAB_KID", ""), + "eab_hmac_key": os.getenv("EASYHAPROXY_CERTBOT_EAB_HMAC_KEY", ""), + "retry_count": int(os.getenv("EASYHAPROXY_CERTBOT_RETRY_COUNT", 60)), + } + + if env_vars["certbot"]["autoconfig"] != "" and not env_vars["certbot"]["server"] and env_vars["certbot"]["email"] != "": + if env_vars["certbot"]["autoconfig"] == "letsencrypt": + env_vars["certbot"]["server"] = "https://acme-v02.api.letsencrypt.org/directory" + + if env_vars["certbot"]["autoconfig"] == "letsencrypt_test": + env_vars["certbot"]["server"] = "https://acme-staging-v02.api.letsencrypt.org/directory" + + if env_vars["certbot"]["autoconfig"] == "buypass": + env_vars["certbot"]["server"] = "https://api.buypass.com/acme/directory" + + if env_vars["certbot"]["autoconfig"] == "buypass_test": + env_vars["certbot"]["server"] = "https://api.test4.buypass.no/acme/directory" + + if env_vars["certbot"]["autoconfig"] == "sslcom_rca": + env_vars["certbot"]["server"] = "https://acme.ssl.com/sslcom-dv-rsa" + + if env_vars["certbot"]["autoconfig"] == "sslcom_ecc": + env_vars["certbot"]["server"] = "https://acme.ssl.com/sslcom-dv-ecc" + + if env_vars["certbot"]["autoconfig"] == "google": + env_vars["certbot"]["server"] = "https://dv.acme-v02.api.pki.goog/directory" + + if env_vars["certbot"]["autoconfig"] == "google_test": + env_vars["certbot"]["server"] = "https://dv.acme-v02.test-api.pki.goog/directory" + + if env_vars["certbot"]["autoconfig"] == "zerossl": + url = "https://api.zerossl.com/acme/eab-credentials-email" + headers = {"Content-Type": "application/x-www-form-urlencoded"} + data = "email=" + env_vars["certbot"]["email"] + resp = requests.post(url, headers=headers, data=data).json() + + if resp["success"]: + env_vars["certbot"]["server"] = "https://acme.zerossl.com/v2/DV90" + 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: + 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"] + + return env_vars + class Functions: - HAPROXY_LOG="HAPROXY" - EASYHAPROXY_LOG="EASYHAPROXY" - CERTBOT_LOG="CERTBOT" - INIT_LOG="INIT" + HAPROXY_LOG = "HAPROXY" + EASYHAPROXY_LOG = "EASYHAPROXY" + CERTBOT_LOG = "CERTBOT" + INIT_LOG = "INIT" TRACE = "TRACE" DEBUG = "DEBUG" @@ -28,7 +108,7 @@ class Functions: level_importance = { Functions.TRACE: 0, Functions.DEBUG: 1, - Functions.INFO: 2, + Functions.INFO: 2, Functions.WARN: 3, Functions.ERROR: 4, Functions.FATAL: 5 @@ -57,7 +137,7 @@ class Functions: if not isinstance(message, (list, tuple)): message = [message] - + for line in message: log = "[%s] %s [%s]: %s" % (source, datetime.now().strftime("%x %X"), level, line.rstrip()) print(log) @@ -70,10 +150,10 @@ class Functions: command = shlex.split(command) try: - process = subprocess.Popen(command, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - universal_newlines=True) + process = subprocess.Popen(command, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True) output = [] @@ -92,29 +172,29 @@ class Functions: Functions.log(source, Functions.WARN, process.stderr.readlines()) break - return output + return [return_code, output] except Exception as e: - Functions.log(source, Functions.ERROR, "%s" % (e)) + Functions.log(source, Functions.ERROR, "%s" % e) + return [-99, e] class Consts: easyhaproxy_config = "/etc/haproxy/static/config.yml" haproxy_config = "/etc/haproxy/haproxy.cfg" - certs_letsencrypt = "/certs/letsencrypt" + 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: - 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)) + self.__prepare(self.get_haproxy_command(action)) if self.process is None: return @@ -122,35 +202,46 @@ 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)): command = shlex.split(command) try: - self.process = subprocess.Popen(command, - shell=False, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - bufsize=-1, - universal_newlines=True) + self.process = subprocess.Popen(command, + shell=False, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + bufsize=-1, + universal_newlines=True) except Exception as e: - Functions.log(source, Functions.ERROR, "%s" % (e)) - + Functions.log(source, Functions.ERROR, "%s" % e) def __start(self): source = Functions.HAPROXY_LOG try: with self.process.stdout: - for line in iter(self.process.stdout.readline, b''): + for line in iter(self.process.stdout.readline, b''): Functions.log(source, Functions.INFO, line) - returncode = self.process.wait() - Functions.log(source, Functions.DEBUG, "Return code %s" % (returncode)) + return_code = self.process.wait() + Functions.log(source, Functions.DEBUG, "Return code %s" % return_code) except Exception as e: - Functions.log(source, Functions.ERROR, "%s" % (e)) + Functions.log(source, Functions.ERROR, "%s" % e) def is_alive(self): return self.thread.is_alive() @@ -165,25 +256,58 @@ class DaemonizeHAProxy: def sleep(self): if self.sleep_secs is None: - try: + try: self.sleep_secs = int(os.getenv("EASYHAPROXY_REFRESH_CONF", "10")) except ValueError: self.sleep_secs = 10 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, email, test_server): - self.certs = certs - self.email = email - self.test_server = self.set_test_server(test_server) + def __init__(self, certs): + env = ContainerEnv.read() - def set_test_server(self, test_server): - if test_server.lower() == "staging": + self.certs = certs + self.email = env["certbot"]["email"] + self.acme_server = self.set_acme_server(env["certbot"]["server"]) + self.eab_kid = self.set_eab_kid(env["certbot"]["eab_kid"]) + self.eab_hmac_key = self.set_eab_hmac_key(env["certbot"]["eab_hmac_key"]) + self.freeze_issue = {} + self.retry_count = env["certbot"]["retry_count"] + + @staticmethod + def set_acme_server(acme_server): + if not acme_server: + return "" + if acme_server.lower() == "staging": return "--staging" - elif test_server.lower().startswith("http"): - return "--server " + test_server + elif acme_server.lower().startswith("http"): + return "--server " + acme_server + else: + return "" + + @staticmethod + def set_eab_kid(eab_kid): + if eab_kid != "": + return "--eab-kid \"%s\"" % eab_kid + else: + return "" + + @staticmethod + def set_eab_hmac_key(eab_hmac_key): + if eab_hmac_key != "": + return "--eab-hmac-key \"%s\"" % eab_hmac_key else: return "" @@ -194,23 +318,25 @@ class Certbot: try: request_certs = [] renew_certs = [] - current_time = time.time() for host in hosts: - filename = "%s/%s.pem" % (self.certs, host) - host_arg = '-d %s' % (host) - if not os.path.exists(filename): - Functions.log(Functions.CERTBOT_LOG, Functions.DEBUG, "Request new certificate for %s" % (host)) + cert_status = self.get_certificate_status(host) + host_arg = '-d %s' % host + if cert_status == "ok" or cert_status == "error": + continue + elif host in self.freeze_issue: + freeze_count = self.freeze_issue.pop(host, 0) + if freeze_count > 0: + Functions.log(Functions.CERTBOT_LOG, Functions.DEBUG, + "Waiting freezing period (%d) for %s due previous errors" % (freeze_count, host)) + self.freeze_issue[host] = freeze_count-1 + elif cert_status == "not_found" or cert_status == "expired": + Functions.log(Functions.CERTBOT_LOG, Functions.DEBUG, "[%s] Request new certificate for %s" % (cert_status, host)) request_certs.append(host_arg) - else: - creation_time = os.path.getctime(filename) - if (current_time - creation_time) // (24 * 3600) > 90: - Functions.log(Functions.CERTBOT_LOG, Functions.DEBUG, "Request expired certificate for %s" % (host)) - request_certs.append(host_arg) - if (current_time - creation_time) // (24 * 3600) >= 45: - Functions.log(Functions.CERTBOT_LOG, Functions.DEBUG, "Renew certificate for %s" % (host)) - renew_certs.append(host_arg) + elif cert_status == "expiring": + Functions.log(Functions.CERTBOT_LOG, Functions.DEBUG, "[%s] Renew certificate for %s" % (cert_status, host)) + renew_certs.append(host_arg) - certbot_certonly = ('/usr/bin/certbot certonly {test_server}' + certbot_certonly = ('/usr/bin/certbot certonly {acme_server}' ' --standalone' ' --preferred-challenges http' ' --http-01-port 2080' @@ -219,39 +345,80 @@ class Certbot: ' --no-eff-email' ' --non-interactive' ' --max-log-backups=0' - ' {certs} --email {email}'.format(certs = ' '.join(request_certs), - email = self.email, - test_server = self.test_server) - ) + ' {eab_kid} {eab_hmac_key}' + ' {certs} --email {email}'.format(eab_kid=self.eab_kid, + eab_hmac_key=self.eab_hmac_key, + certs=' '.join(request_certs), + email=self.email, + acme_server=self.acme_server) + ) ret_reload = False + return_code_issue = 0 + return_code_renew = 0 if len(request_certs) > 0: - Functions.run_bash(Functions.CERTBOT_LOG, certbot_certonly, return_result=False) + return_code_issue, output = Functions.run_bash(Functions.CERTBOT_LOG, certbot_certonly, return_result=False) ret_reload = True if len(renew_certs) > 0: - Functions.run_bash(Functions.CERTBOT_LOG, "/usb/bin/certbot renew", return_result=False) + return_code_renew, output = Functions.run_bash(Functions.CERTBOT_LOG, "/usr/bin/certbot renew", return_result=False) ret_reload = True if ret_reload: self.find_live_certificates() + if return_code_issue != 0: + self.find_missing_certificates(request_certs) + if return_code_renew != 0: + self.find_missing_certificates(renew_certs) + return ret_reload except Exception as e: - Functions.log(Functions.CERTBOT_LOG, Functions.ERROR, "%s" % (e)) + Functions.log(Functions.CERTBOT_LOG, Functions.ERROR, "%s" % e) return False - - def merge_certificate(self, cert, key, filename): + + @staticmethod + def merge_certificate(cert, key, filename): Functions.save(filename, cert + key) - + def find_live_certificates(self): - letsencrypt_certs = "/etc/letsencrypt/live/" - if not os.path.exists(letsencrypt_certs): + certbot_certs = "/etc/letsencrypt/live/" + if not os.path.exists(certbot_certs): return - for item in os.listdir(letsencrypt_certs): - path = os.path.join(letsencrypt_certs, item) + for item in os.listdir(certbot_certs): + path = os.path.join(certbot_certs, item) if os.path.isdir(path): cert = Functions.load(os.path.join(path, "cert.pem")) key = Functions.load(os.path.join(path, "privkey.pem")) filename = "%s/%s.pem" % (self.certs, item) self.merge_certificate(cert, key, filename) + + def get_certificate_status(self, host): + current_time = time.time() + filename = "%s/%s.pem" % (self.certs, host) + if not os.path.exists(filename): + return "not_found" + + try: + with open(filename, 'rb') as file: + certificate_str = file.read() + certificate = crypto.load_certificate(crypto.FILETYPE_PEM, certificate_str) + expiration_after = datetime.strptime(certificate.get_notAfter().decode()[:-1], '%Y%m%d%H%M%S').timestamp() + if current_time >= expiration_after: + return "expired" + elif (expiration_after - current_time) // (24 * 3600) <= 15: + return "expiring" + except Exception as e: + Functions.log(Functions.CERTBOT_LOG, Functions.ERROR, "Certificate %s error %s" % (host, e)) + return "error" + + return "ok" + + def find_missing_certificates(self, hosts): + for host in hosts: + if host.startswith("-d "): + host = host[3:] + cert_status = self.get_certificate_status(host) + if cert_status != "ok": + self.freeze_issue[host] = self.retry_count + Functions.log(Functions.CERTBOT_LOG, Functions.DEBUG, "Freeze issuing ssl for %s due failure. The certificate is %s" % (host, cert_status)) diff --git a/src/main.py b/src/main.py index 34e29d8..24769d8 100644 --- a/src/main.py +++ b/src/main.py @@ -1,28 +1,33 @@ +import os + +from deepdiff import DeepDiff + from functions import Functions, DaemonizeHAProxy, Certbot, Consts from processor import ProcessorInterface -import os -from deepdiff import DeepDiff + def start(): processor_obj = ProcessorInterface.factory(os.getenv("EASYHAPROXY_DISCOVER")) if processor_obj is None: exit(1) - os.makedirs(Consts.certs_letsencrypt, exist_ok=True) + os.makedirs(Consts.certs_certbot, exist_ok=True) os.makedirs(Consts.certs_haproxy, exist_ok=True) processor_obj.save_config(Consts.haproxy_config) processor_obj.save_certs(Consts.certs_haproxy) - letsencrypt_certs_found = processor_obj.get_letsencrypt_hosts() - Functions.log(Functions.EASYHAPROXY_LOG, Functions.DEBUG, 'Found hosts: %s' % ", ".join(processor_obj.get_hosts())) # Needs to run after save_config + certbot_certs_found = processor_obj.get_certbot_hosts() + Functions.log(Functions.EASYHAPROXY_LOG, Functions.DEBUG, + 'Found hosts: %s' % ", ".join(processor_obj.get_hosts())) # Needs to run after save_config Functions.log(Functions.EASYHAPROXY_LOG, Functions.TRACE, 'Object Found: %s' % (processor_obj.get_parsed_object())) old_haproxy = None haproxy = DaemonizeHAProxy() + current_custom_config_files = haproxy.get_custom_config_files() haproxy.haproxy("start") haproxy.sleep() - certbot = Certbot(Consts.certs_letsencrypt, os.getenv("EASYHAPROXY_LETSENCRYPT_EMAIL"), os.getenv("EASYHAPROXY_LETSENCRYPT_SERVER", "").lower()) + certbot = Certbot(Consts.certs_certbot) while True: if old_haproxy is not None: @@ -31,27 +36,28 @@ def start(): try: old_parsed = processor_obj.get_parsed_object() 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(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())) + 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_certs(Consts.certs_haproxy) - letsencrypt_certs_found = processor_obj.get_letsencrypt_hosts() - Functions.log(Functions.EASYHAPROXY_LOG, Functions.DEBUG, 'Found hosts: %s' % ", ".join(processor_obj.get_hosts())) # Needs to after save_config + certbot_certs_found = processor_obj.get_certbot_hosts() + Functions.log(Functions.EASYHAPROXY_LOG, Functions.DEBUG, + '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() except Exception as e: - Functions.log(Functions.EASYHAPROXY_LOG, Functions.FATAL, "Err: %s" % (e)) + Functions.log(Functions.EASYHAPROXY_LOG, Functions.FATAL, "Err: %s" % e) Functions.log(Functions.EASYHAPROXY_LOG, Functions.DEBUG, 'Heartbeat') haproxy.sleep() - - def main(): Functions.run_bash(Functions.INIT_LOG, '/usr/sbin/haproxy -v') @@ -69,5 +75,6 @@ def main(): start() + if __name__ == '__main__': main() diff --git a/src/processor/__init__.py b/src/processor/__init__.py index 748ad1e..0d3f8a5 100644 --- a/src/processor/__init__.py +++ b/src/processor/__init__.py @@ -1,45 +1,28 @@ -from easymapping import HaproxyConfigGenerator -from functions import Functions, Consts -import yaml -import sys -import os -import json import base64 -import docker import socket + +import docker +import yaml from kubernetes import client, config from kubernetes.client.rest import ApiException -class ContainerEnv: - @staticmethod - def read(): - env_vars = { - "customerrors": True if os.getenv("HAPROXY_CUSTOMERRORS") == "true" else False, - "ssl_mode": os.getenv("EASYHAPROXY_SSL_MODE").lower() if os.getenv("EASYHAPROXY_SSL_MODE") else 'default' - } - - if os.getenv("HAPROXY_PASSWORD"): - env_vars["stats"] = { - "username": os.getenv("HAPROXY_USERNAME") if os.getenv("HAPROXY_USERNAME") else "admin", - "password": os.getenv("HAPROXY_PASSWORD"), - "port": os.getenv("HAPROXY_STATS_PORT") if os.getenv("HAPROXY_STATS_PORT") else "1936", - } - - env_vars["lookup_label"] = os.getenv("EASYHAPROXY_LABEL_PREFIX") if os.getenv("EASYHAPROXY_LABEL_PREFIX") else "easyhaproxy" - if (os.getenv("EASYHAPROXY_LETSENCRYPT_EMAIL")): - env_vars["letsencrypt"] = { - "email": os.getenv("EASYHAPROXY_LETSENCRYPT_EMAIL"), - "server": os.getenv("EASYHAPROXY_LETSENCRYPT_SERVER", "false").lower() in ["true", "1", "yes"] - } - - return env_vars +from easymapping import HaproxyConfigGenerator +from functions import Functions, Consts, ContainerEnv class ProcessorInterface: static_file = Consts.easyhaproxy_config - def __init__(self, filename = None): + def __init__(self, filename=None): + self.certbot_hosts = None + self.parsed_object = None + self.cfg = None + self.hosts = None + self.cfg = None + self.certbot_hosts = None + self.hosts = None self.filename = filename + self.label = ContainerEnv.read()['lookup_label'] self.refresh() @staticmethod @@ -53,11 +36,12 @@ class ProcessorInterface: elif mode == "kubernetes": return Kubernetes() else: - Functions.log("EASYHAPROXY", Functions.FATAL, "Expected mode to be 'static', 'docker', 'swarm' or 'kubernetes'. I got '%s'" % (mode)) + Functions.log("EASYHAPROXY", Functions.FATAL, + "Expected mode to be 'static', 'docker', 'swarm' or 'kubernetes'. I got '%s'" % mode) return None def refresh(self): - self.letsencrypt_hosts = None + self.certbot_hosts = None self.parsed_object = None self.cfg = None self.hosts = None @@ -65,14 +49,14 @@ class ProcessorInterface: self.parse() def inspect_network(self): - #Abstract + # Abstract pass def parse(self): self.cfg = HaproxyConfigGenerator(ContainerEnv.read()) - def get_letsencrypt_hosts(self): - return self.letsencrypt_hosts + def get_certbot_hosts(self): + return self.certbot_hosts def get_hosts(self): return self.hosts @@ -80,7 +64,7 @@ class ProcessorInterface: def get_parsed_object(self): return self.parsed_object - def get_certs(self, key = None): + def get_certs(self, key=None): if key is None: return self.cfg.certs else: @@ -88,7 +72,7 @@ class ProcessorInterface: def get_haproxy_conf(self): conf = self.cfg.generate(self.parsed_object) - self.letsencrypt_hosts = self.cfg.letsencrypt_hosts + self.certbot_hosts = self.cfg.certbot_hosts self.hosts = self.cfg.serving_hosts return conf @@ -101,20 +85,27 @@ class ProcessorInterface: class Static(ProcessorInterface): + def __init__(self, filename=None): + self.parsed_object = None + self.static_content = None + self.static_content = None + self.cfg = None + super().__init__(filename) + def inspect_network(self): self.parsed_object = {} self.static_content = None - + def get_parsed_object(self): return self.static_content["easymapping"] if "easymapping" in self.static_content else [] def get_hosts(self): hosts = [] - for object in self.get_parsed_object(): - if "hosts" not in object: + for obj in self.get_parsed_object(): + if "hosts" not in obj: continue - for host in object["hosts"].keys(): - hosts.append("%s:%s" % (host, object["port"])) + for host in obj["hosts"].keys(): + hosts.append("%s:%s" % (host, obj["port"])) return hosts def parse(self): @@ -123,18 +114,21 @@ class Static(ProcessorInterface): class Docker(ProcessorInterface): - def __init__(self, filename = None): + def __init__(self, filename=None): + self.parsed_object = None self.client = docker.from_env() super().__init__() - + def inspect_network(self): try: - ha_proxy_network_name = next(iter(self.client.containers.get(socket.gethostname()).attrs["NetworkSettings"]["Networks"])) + 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_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) @@ -150,37 +144,53 @@ class Docker(ProcessorInterface): class Swarm(ProcessorInterface): - def __init__(self, filename = None): + def __init__(self, filename=None): + self.parsed_object = None self.client = docker.from_env() super().__init__() 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': + 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"]: + 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 + # 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 - + self.parsed_object[ip_address] = service.attrs["Spec"]["Labels"] class Kubernetes(ProcessorInterface): - def __init__(self, filename = None): + def __init__(self, filename=None): + self.parsed_object = None config.load_incluster_config() config.verify_ssl = False self.api_instance = client.CoreV1Api() @@ -188,13 +198,13 @@ class Kubernetes(ProcessorInterface): self.cert_cache = {} super().__init__() - def _check_annotation(self, 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): - + ret = self.v1.list_ingress_for_all_namespaces(watch=False) self.parsed_object = {} @@ -206,18 +216,14 @@ class Kubernetes(ProcessorInterface): ssl_hosts = [] - letsencrypt = self._check_annotation(ingress.metadata.annotations, "easyhaproxy.letsencrypt") + certbot = self._check_annotation(ingress.metadata.annotations, "easyhaproxy.certbot") 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 = {} - data["creation_timestamp"] = ingress.metadata.creation_timestamp.strftime("%x %X") - data["resource_version"] = ingress.metadata.resource_version - data["namespace"] = ingress.metadata.namespace + data = {"creation_timestamp": ingress.metadata.creation_timestamp.strftime("%x %X"), + "resource_version": ingress.metadata.resource_version, "namespace": ingress.metadata.namespace} ingress_name = ingress.metadata.namespace @@ -231,33 +237,37 @@ class Kubernetes(ProcessorInterface): if tls.secret_name not in self.cert_cache or self.cert_cache[tls.secret_name] != secret.data: self.cert_cache[tls.secret_name] = secret.data Functions.save( - "{0}/{1}.pem".format(Consts.certs_haproxy, tls.secret_name), - base64.b64decode(secret.data["tls.crt"]).decode('ascii') + "\n" + base64.b64decode(secret.data["tls.key"]).decode('ascii') + "{0}/{1}.pem".format(Consts.certs_haproxy, tls.secret_name), + base64.b64decode(secret.data["tls.crt"]).decode('ascii') + "\n" + base64.b64decode( + secret.data["tls.key"]).decode('ascii') ) ssl_hosts.extend(tls.hosts) except Exception as e: - Functions.log("EASYHAPROXY", Functions.WARN, "Ingress %s - Get secret failed: '%s'" % (ingress_name, e)) + Functions.log("EASYHAPROXY", Functions.WARN, + "Ingress %s - Get secret failed: '%s'" % (ingress_name, e)) - Functions.log("EASYHAPROXY", Functions.TRACE, "Ingress %s - SSL Hosts found '%s'" % (ingress_name, ssl_hosts)) + Functions.log("EASYHAPROXY", Functions.TRACE, + "Ingress %s - SSL Hosts found '%s'" % (ingress_name, ssl_hosts)) for rule in ingress.spec.rules: rule_data = {} port_number = rule.http.paths[0].backend.service.port.number definition = "easyhaproxy.%s_%s" % (rule.host.replace(".", "-"), port_number) - rule_data["%s.host" % (definition)] = rule.host - rule_data["%s.port" % (definition)] = listen_port - rule_data["%s.localport" % (definition)] = port_number + rule_data["%s.host" % definition] = rule.host + rule_data["%s.port" % definition] = listen_port + rule_data["%s.localport" % definition] = port_number if rule.host in ssl_hosts: - rule_data["%s.clone_to_ssl" % (definition)] = 'true' + rule_data["%s.clone_to_ssl" % definition] = 'true' if redirect_ssl is not None: - rule_data["%s.redirect_ssl" % (definition)] = redirect_ssl - if letsencrypt is not None: - rule_data["%s.letsencrypt" % (definition)] = letsencrypt + rule_data["%s.redirect_ssl" % definition] = redirect_ssl + if certbot is not None: + rule_data["%s.certbot" % definition] = certbot if redirect is not None: - rule_data["%s.redirect" % (definition)] = redirect + rule_data["%s.redirect" % definition] = redirect 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 try: @@ -265,13 +275,10 @@ class Kubernetes(ProcessorInterface): cluster_ip = api_response.spec.cluster_ip except ApiException as e: cluster_ip = None - Functions.log("EASYHAPROXY", Functions.WARN, "Ingress %s - Service %s - Failed: '%s'" % (ingress_name, service_name, e)) - + Functions.log("EASYHAPROXY", Functions.WARN, + "Ingress %s - Service %s - Failed: '%s'" % (ingress_name, service_name, e)) + if cluster_ip is not None: if cluster_ip not in self.parsed_object.keys(): self.parsed_object[cluster_ip] = data self.parsed_object[cluster_ip].update(rule_data) - - - - diff --git a/src/requirements.txt b/src/requirements.txt index 5c4ba98..6a6cc6b 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -4,4 +4,5 @@ jinja2 pytest docker kubernetes -deepdiff \ No newline at end of file +deepdiff +pyopenssl \ No newline at end of file diff --git a/src/templates/bind.j2 b/src/templates/bind.j2 index d33d4bd..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/letsencrypt/ 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/frontend-mode-http.j2 b/src/templates/frontend-mode-http.j2 index a3ed56d..5f9b0b0 100644 --- a/src/templates/frontend-mode-http.j2 +++ b/src/templates/frontend-mode-http.j2 @@ -5,16 +5,16 @@ {%- for k in o["hosts"] %} {% set host = k.replace(".", "_") + "_{0}".format(o["port"]) %} - {% set letsencrypt = o["hosts"][k]["letsencrypt"] %} + {% set certbot = o["hosts"][k]["certbot"] %} acl is_rule_{{ host }}_1 hdr(host) -i {{ k }} acl is_rule_{{ host }}_2 hdr(host) -i {{ k }}:{{ o["port"] }} - {% if letsencrypt %} - acl is_letsencrypt_{{ host }} path_beg /.well-known/acme-challenge/ - use_backend letsencrypt_backend if is_letsencrypt_{{ host }} is_rule_{{ host }}_1 OR is_letsencrypt_{{ host }} is_rule_{{ host }}_2 + {% if certbot %} + acl is_certbot_{{ host }} path_beg /.well-known/acme-challenge/ + use_backend certbot_backend if is_certbot_{{ host }} is_rule_{{ host }}_1 OR is_certbot_{{ host }} is_rule_{{ host }}_2 {% endif %} {% if o["hosts"][k]["redirect_ssl"] %} - http-request redirect scheme https code 301 if {% if letsencrypt %}!is_letsencrypt_{{ host }} {% endif %}is_rule_{{ host }}_1 OR {% if letsencrypt %}!is_letsencrypt_{{ host }} {% endif %}is_rule_{{ host }}_2 + http-request redirect scheme https code 301 if {% if certbot %}!is_certbot_{{ host }} {% endif %}is_rule_{{ host }}_1 OR {% if certbot %}!is_certbot_{{ host }} {% endif %}is_rule_{{ host }}_2 {% else %} use_backend srv_{{ host }} if is_rule_{{ host }}_1 OR is_rule_{{ host }}_2 {% endif %} diff --git a/src/templates/haproxy.cfg.j2 b/src/templates/haproxy.cfg.j2 index a1842b1..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,15 +80,15 @@ 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 %} -backend letsencrypt_backend +backend certbot_backend mode http server certbot 127.0.0.1:2080 diff --git a/src/tests/context.py b/src/tests/context.py index 67dda65..66ad651 100644 --- a/src/tests/context.py +++ b/src/tests/context.py @@ -1,5 +1,5 @@ import os import sys -sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) -import easymapping + +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) diff --git a/src/tests/expected/docker.txt b/src/tests/expected/docker.txt index 7bcdedc..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/letsencrypt/ 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 @@ -84,8 +84,8 @@ frontend http_in_90 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 + acl is_certbot_host2_local_90 path_beg /.well-known/acme-challenge/ + use_backend certbot_backend if is_certbot_host2_local_90 is_rule_host2_local_90_1 OR is_certbot_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 @@ -96,6 +96,6 @@ backend srv_host2_local_90 http-request add-header X-Forwarded-Proto https if { ssl_fc } server srv-0 test_processor_docker:9000 check weight 1 -backend letsencrypt_backend +backend certbot_backend mode http server certbot 127.0.0.1:2080 diff --git a/src/tests/expected/no-services.txt b/src/tests/expected/no-services.txt index a95c434..7624237 100644 --- a/src/tests/expected/no-services.txt +++ b/src/tests/expected/no-services.txt @@ -23,6 +23,6 @@ defaults timeout server 10m -backend letsencrypt_backend +backend certbot_backend mode http server certbot 127.0.0.1:2080 diff --git a/src/tests/expected/services-letsencrypt.txt b/src/tests/expected/services-letsencrypt.txt index 3e2eafb..8d5da5d 100644 --- a/src/tests/expected/services-letsencrypt.txt +++ b/src/tests/expected/services-letsencrypt.txt @@ -49,9 +49,9 @@ frontend http_in_80 acl is_rule_test_example_org_80_1 hdr(host) -i test.example.org acl is_rule_test_example_org_80_2 hdr(host) -i test.example.org:80 - acl is_letsencrypt_test_example_org_80 path_beg /.well-known/acme-challenge/ - use_backend letsencrypt_backend if is_letsencrypt_test_example_org_80 is_rule_test_example_org_80_1 OR is_letsencrypt_test_example_org_80 is_rule_test_example_org_80_2 - http-request redirect scheme https code 301 if !is_letsencrypt_test_example_org_80 is_rule_test_example_org_80_1 OR !is_letsencrypt_test_example_org_80 is_rule_test_example_org_80_2 + acl is_certbot_test_example_org_80 path_beg /.well-known/acme-challenge/ + use_backend certbot_backend if is_certbot_test_example_org_80 is_rule_test_example_org_80_1 OR is_certbot_test_example_org_80 is_rule_test_example_org_80_2 + http-request redirect scheme https code 301 if !is_certbot_test_example_org_80 is_rule_test_example_org_80_1 OR !is_certbot_test_example_org_80 is_rule_test_example_org_80_2 acl is_rule_test2_example_org_80_1 hdr(host) -i test2.example.org acl is_rule_test2_example_org_80_2 hdr(host) -i test2.example.org:80 @@ -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/letsencrypt/ 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 @@ -90,6 +90,6 @@ backend srv_test_example_org_443 server srv-0 f5c645a0dfc6:80 check weight 1 verify none server srv-1 b63438410b6a:80 check weight 1 verify none -backend letsencrypt_backend +backend certbot_backend mode http server certbot 127.0.0.1:2080 diff --git a/src/tests/expected/services-multi-containers.txt b/src/tests/expected/services-multi-containers.txt index 5910691..3b1644d 100644 --- a/src/tests/expected/services-multi-containers.txt +++ b/src/tests/expected/services-multi-containers.txt @@ -40,6 +40,6 @@ backend srv_www_helloworld_com_19901 server srv-0 test_nginx.2.t5r94mjlced7m3t5orfjbowmm:80 check weight 1 server srv-1 test_nginx.1.p552hqxkdx88narjrp5kouwb2:80 check weight 1 -backend letsencrypt_backend +backend certbot_backend mode http server certbot 127.0.0.1:2080 diff --git a/src/tests/expected/services-multiple-hosts.txt b/src/tests/expected/services-multiple-hosts.txt index 954dde1..e5caf4b 100644 --- a/src/tests/expected/services-multiple-hosts.txt +++ b/src/tests/expected/services-multiple-hosts.txt @@ -73,6 +73,6 @@ backend srv_hello_com_19901 server srv-0 3e63154954b0:80 check weight 1 server srv-1 eb294c110eb1:80 check weight 1 -backend letsencrypt_backend +backend certbot_backend mode http server certbot 127.0.0.1:2080 diff --git a/src/tests/expected/services-redirect-ssl.txt b/src/tests/expected/services-redirect-ssl.txt index 077123b..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/letsencrypt/ 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 @@ -75,6 +75,6 @@ backend srv_host1_local_443 http-request add-header X-Forwarded-Proto https if { ssl_fc } server srv-0 5b69bc7fea1b:8080 check weight 1 -backend letsencrypt_backend +backend certbot_backend mode http server certbot 127.0.0.1:2080 diff --git a/src/tests/expected/services-tcp.txt b/src/tests/expected/services-tcp.txt index fa8fd7a..2c19128 100644 --- a/src/tests/expected/services-tcp.txt +++ b/src/tests/expected/services-tcp.txt @@ -37,6 +37,6 @@ backend srv_agent_quantum_local_31339 tcp-check connect ssl server srv-0 test_agent:9001 check weight 1 verify none -backend letsencrypt_backend +backend certbot_backend mode http server certbot 127.0.0.1:2080 diff --git a/src/tests/expected/services.txt b/src/tests/expected/services.txt index bf2cecd..82f8a6c 100644 --- a/src/tests/expected/services.txt +++ b/src/tests/expected/services.txt @@ -47,8 +47,8 @@ frontend http_in_31337 acl is_rule_node-exporter_quantum_example_org_31337_1 hdr(host) -i node-exporter.quantum.example.org acl is_rule_node-exporter_quantum_example_org_31337_2 hdr(host) -i node-exporter.quantum.example.org:31337 - acl is_letsencrypt_node-exporter_quantum_example_org_31337 path_beg /.well-known/acme-challenge/ - use_backend letsencrypt_backend if is_letsencrypt_node-exporter_quantum_example_org_31337 is_rule_node-exporter_quantum_example_org_31337_1 OR is_letsencrypt_node-exporter_quantum_example_org_31337 is_rule_node-exporter_quantum_example_org_31337_2 + acl is_certbot_node-exporter_quantum_example_org_31337 path_beg /.well-known/acme-challenge/ + use_backend certbot_backend if is_certbot_node-exporter_quantum_example_org_31337 is_rule_node-exporter_quantum_example_org_31337_1 OR is_certbot_node-exporter_quantum_example_org_31337 is_rule_node-exporter_quantum_example_org_31337_2 use_backend srv_node-exporter_quantum_example_org_31337 if is_rule_node-exporter_quantum_example_org_31337_1 OR is_rule_node-exporter_quantum_example_org_31337_2 backend srv_cadvisor_quantum_example_org_31337 @@ -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/letsencrypt/ 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 } @@ -119,6 +119,6 @@ backend srv_www_somehost_com_br_80 http-request add-header X-Forwarded-Proto https if { ssl_fc } server srv-0 some-service:80 check weight 1 -backend letsencrypt_backend +backend certbot_backend mode http server certbot 127.0.0.1:2080 diff --git a/src/tests/expected/ssl-loose.txt b/src/tests/expected/ssl-loose.txt index 45ac853..e4a6322 100644 --- a/src/tests/expected/ssl-loose.txt +++ b/src/tests/expected/ssl-loose.txt @@ -33,6 +33,6 @@ backend srv_stats mode http server Local 127.0.0.1:1936 -backend letsencrypt_backend +backend certbot_backend mode http server certbot 127.0.0.1:2080 diff --git a/src/tests/expected/ssl-strict.txt b/src/tests/expected/ssl-strict.txt index f4e00b4..7c3306f 100644 --- a/src/tests/expected/ssl-strict.txt +++ b/src/tests/expected/ssl-strict.txt @@ -18,6 +18,6 @@ defaults timeout server 10m -backend letsencrypt_backend +backend certbot_backend mode http server certbot 127.0.0.1:2080 diff --git a/src/tests/expected/static.txt b/src/tests/expected/static.txt index ea0362f..a5af1ec 100644 --- a/src/tests/expected/static.txt +++ b/src/tests/expected/static.txt @@ -50,8 +50,8 @@ frontend http_in_80 acl is_rule_host1_com_br_80_1 hdr(host) -i host1.com.br acl is_rule_host1_com_br_80_2 hdr(host) -i host1.com.br:80 - acl is_letsencrypt_host1_com_br_80 path_beg /.well-known/acme-challenge/ - use_backend letsencrypt_backend if is_letsencrypt_host1_com_br_80 is_rule_host1_com_br_80_1 OR is_letsencrypt_host1_com_br_80 is_rule_host1_com_br_80_2 + acl is_certbot_host1_com_br_80 path_beg /.well-known/acme-challenge/ + use_backend certbot_backend if is_certbot_host1_com_br_80 is_rule_host1_com_br_80_1 OR is_certbot_host1_com_br_80 is_rule_host1_com_br_80_2 use_backend srv_host1_com_br_80 if is_rule_host1_com_br_80_1 OR is_rule_host1_com_br_80_2 acl is_rule_host2_com_br_80_1 hdr(host) -i host2.com.br @@ -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/letsencrypt/ 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 @@ -105,6 +105,6 @@ backend srv_host3_com_br_8080 http-request add-header X-Forwarded-Proto https if { ssl_fc } server srv-0 domain:8181 check weight 1 -backend letsencrypt_backend +backend certbot_backend mode http server certbot 127.0.0.1:2080 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/run_bash.sh b/src/tests/fixtures/run_bash.sh new file mode 100755 index 0000000..75a66bb --- /dev/null +++ b/src/tests/fixtures/run_bash.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +# Just return the exit code +echo "Processing run_bash.sh" +if [ -n "$1" ]; then + exit "$1" +fi diff --git a/src/tests/fixtures/services b/src/tests/fixtures/services index 56ef60a..5c0aa68 100644 --- a/src/tests/fixtures/services +++ b/src/tests/fixtures/services @@ -1,6 +1,6 @@ {"portainer-agent_agent": {"com.docker.stack.image":"portainer/agent:1.5.1","com.docker.stack.namespace":"portainer-agent"}, "my-stack_agent": {"easyhaproxy.agent.host":"agent.quantum.example.org","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":"my-stack","com.planetary-quantum":"monitoring"}, "my-stack_cadvisor": {"easyhaproxy.cadvisor.host":"cadvisor.quantum.example.org","easyhaproxy.cadvisor.localport":"8080","easyhaproxy.cadvisor.port":"31337","com.docker.stack.image":"gcr.io/google-containers/cadvisor:v0.34.0","com.docker.stack.namespace":"my-stack","com.planetary-quantum":"monitoring"}, -"my-stack_node-exporter": {"easyhaproxy.exp.host":"node-exporter.quantum.example.org","easyhaproxy.exp.localport":"9100","easyhaproxy.exp.port":"31337","com.docker.stack.image":"stefanprodan/swarmprom-node-exporter:v0.16.0","com.docker.stack.namespace":"my-stack","com.planetary-quantum":"monitoring","easyhaproxy.exp.letsencrypt":"true"}, +"my-stack_node-exporter": {"easyhaproxy.exp.host":"node-exporter.quantum.example.org","easyhaproxy.exp.localport":"9100","easyhaproxy.exp.port":"31337","com.docker.stack.image":"stefanprodan/swarmprom-node-exporter:v0.16.0","com.docker.stack.namespace":"my-stack","com.planetary-quantum":"monitoring","easyhaproxy.exp.certbot":"true"}, "my-stack_reverse-proxy": {"com.docker.stack.image":"quay.io/pngmbh/easy-haproxy:tcp-mode","com.docker.stack.namespace":"my-stack","com.planetary-quantum":"monitoring"}, "some-service": {"easyhaproxy.http.port":"80","easyhaproxy.http.host":"www.somehost.com.br","easyhaproxy.http.localport":"80","easyhaproxy.http.redirect":"{\"somehost.com.br\":\"https://www.somehost.com.br\",\"somehost.com\":\"https://www.somehost.com.br\",\"www.somehost.com\":\"https://www.somehost.com.br\",\"byjg.ca\":\"https://www.somehost.com.br\",\"www.byjg.ca\":\"https://www.somehost.com.br\"}","easyhaproxy.https.port":"443","easyhaproxy.https.host":"www.somehost.com.br","easyhaproxy.https.localport":"80","easyhaproxy.https.redirect":"{\"somehost.com.br\":\"https://www.somehost.com.br\",\"somehost.com\":\"https://www.somehost.com.br\",\"www.somehost.com\":\"https://www.somehost.com.br\",\"byjg.ca\":\"https://www.somehost.com.br\",\"www.byjg.ca\":\"https://www.somehost.com.br\"}","easyhaproxy.https.sslcert":"U29tZSBQRU0gQ2VydGlmaWNhdGU="}} \ No newline at end of file diff --git a/src/tests/fixtures/services-changed-label b/src/tests/fixtures/services-changed-label index 2a23c24..af934de 100644 --- a/src/tests/fixtures/services-changed-label +++ b/src/tests/fixtures/services-changed-label @@ -1,6 +1,6 @@ {"portainer-agent_agent": {"com.docker.stack.image":"portainer/agent:1.5.1","com.docker.stack.namespace":"portainer-agent"}, "my-stack_agent": {"haproxy.agent.host":"agent.quantum.example.org","haproxy.agent.localport":"9001","haproxy.agent.mode":"tcp","haproxy.agent.port":"31339","com.docker.stack.image":"portainer/agent:1.5.1","com.docker.stack.namespace":"my-stack","com.planetary-quantum":"monitoring"}, "my-stack_cadvisor": {"haproxy.cadvisor.host":"cadvisor.quantum.example.org","haproxy.cadvisor.localport":"8080","haproxy.cadvisor.port":"31337","com.docker.stack.image":"gcr.io/google-containers/cadvisor:v0.34.0","com.docker.stack.namespace":"my-stack","com.planetary-quantum":"monitoring"}, -"my-stack_node-exporter": {"haproxy.exp.host":"node-exporter.quantum.example.org","haproxy.exp.localport":"9100","haproxy.exp.port":"31337","com.docker.stack.image":"stefanprodan/swarmprom-node-exporter:v0.16.0","com.docker.stack.namespace":"my-stack","com.planetary-quantum":"monitoring","haproxy.exp.letsencrypt":"yes"}, +"my-stack_node-exporter": {"haproxy.exp.host":"node-exporter.quantum.example.org","haproxy.exp.localport":"9100","haproxy.exp.port":"31337","com.docker.stack.image":"stefanprodan/swarmprom-node-exporter:v0.16.0","com.docker.stack.namespace":"my-stack","com.planetary-quantum":"monitoring","haproxy.exp.certbot":"yes"}, "my-stack_reverse-proxy": {"com.docker.stack.image":"quay.io/pngmbh/easy-haproxy:tcp-mode","com.docker.stack.namespace":"my-stack","com.planetary-quantum":"monitoring"}, "some-service": {"haproxy.http.port":"80","haproxy.http.host":"www.somehost.com.br","haproxy.http.localport":"80","haproxy.http.redirect":"{\"somehost.com.br\":\"https://www.somehost.com.br\",\"somehost.com\":\"https://www.somehost.com.br\",\"www.somehost.com\":\"https://www.somehost.com.br\",\"byjg.ca\":\"https://www.somehost.com.br\",\"www.byjg.ca\":\"https://www.somehost.com.br\"}","haproxy.https.port":"443","haproxy.https.host":"www.somehost.com.br","haproxy.https.localport":"80","haproxy.https.redirect":"{\"somehost.com.br\":\"https://www.somehost.com.br\",\"somehost.com\":\"https://www.somehost.com.br\",\"www.somehost.com\":\"https://www.somehost.com.br\",\"byjg.ca\":\"https://www.somehost.com.br\",\"www.byjg.ca\":\"https://www.somehost.com.br\"}","haproxy.https.sslcert":"U29tZSBQRU0gQ2VydGlmaWNhdGU="}} \ No newline at end of file diff --git a/src/tests/fixtures/services-letsencrypt b/src/tests/fixtures/services-letsencrypt index 9c83781..ca1d03a 100644 --- a/src/tests/fixtures/services-letsencrypt +++ b/src/tests/fixtures/services-letsencrypt @@ -1,4 +1,4 @@ -{"f5c645a0dfc6": {"com.docker.compose.config-hash":"b95ebc27d0e61caa418cdfa632e05a656da9bbc3ea0d4603651971015f10a1f0","com.docker.compose.container-number":"1","com.docker.compose.depends_on":"","com.docker.compose.image":"sha256:bea3509d6fdc8d7f9ec95563a5a226dc977ee74fb3e980e0de70e892c2d38dde","com.docker.compose.oneoff":"False","com.docker.compose.project":"docker","com.docker.compose.project.config_files":"/workspace/docker-easy-haproxy/examples/docker/docker-compose-test.yml","com.docker.compose.project.working_dir":"/workspace/docker-easy-haproxy/examples/docker","com.docker.compose.service":"nginx","com.docker.compose.version":"2.8.0","easyhaproxy.http.host":"test.example.org","easyhaproxy.http.letsencrypt":"true","easyhaproxy.http.localport":"80","easyhaproxy.http.port":"80","easyhaproxy.http.redirect":"{\"google.helloworld.com\": \"www.google.com\"}","easyhaproxy.http.redirect_ssl":"true"}, +{"f5c645a0dfc6": {"com.docker.compose.config-hash":"b95ebc27d0e61caa418cdfa632e05a656da9bbc3ea0d4603651971015f10a1f0","com.docker.compose.container-number":"1","com.docker.compose.depends_on":"","com.docker.compose.image":"sha256:bea3509d6fdc8d7f9ec95563a5a226dc977ee74fb3e980e0de70e892c2d38dde","com.docker.compose.oneoff":"False","com.docker.compose.project":"docker","com.docker.compose.project.config_files":"/workspace/docker-easy-haproxy/examples/docker/docker-compose-test.yml","com.docker.compose.project.working_dir":"/workspace/docker-easy-haproxy/examples/docker","com.docker.compose.service":"nginx","com.docker.compose.version":"2.8.0","easyhaproxy.http.host":"test.example.org","easyhaproxy.http.certbot":"true","easyhaproxy.http.localport":"80","easyhaproxy.http.port":"80","easyhaproxy.http.redirect":"{\"google.helloworld.com\": \"www.google.com\"}","easyhaproxy.http.redirect_ssl":"true"}, "bbd4d1854155": {"com.docker.compose.config-hash":"3dc790bf2bea944359c75a40c45655bd868f1d85beb599d1ca797e8ea2c95ee4","com.docker.compose.container-number":"1","com.docker.compose.depends_on":"","com.docker.compose.image":"sha256:0fd95b1512c207048ab3fcc74032354f38143fbb8235ac2a47da903c98a58205","com.docker.compose.oneoff":"False","com.docker.compose.project":"docker","com.docker.compose.project.config_files":"/workspace/docker-easy-haproxy/examples/docker/docker-compose-test.yml","com.docker.compose.project.working_dir":"/workspace/docker-easy-haproxy/examples/docker","com.docker.compose.service":"haproxy","com.docker.compose.version":"2.8.0"}, -"b63438410b6a": {"com.docker.compose.config-hash":"b95ebc27d0e61caa418cdfa632e05a656da9bbc3ea0d4603651971015f10a1f0","com.docker.compose.container-number":"2","com.docker.compose.depends_on":"","com.docker.compose.image":"sha256:bea3509d6fdc8d7f9ec95563a5a226dc977ee74fb3e980e0de70e892c2d38dde","com.docker.compose.oneoff":"False","com.docker.compose.project":"docker","com.docker.compose.project.config_files":"/workspace/docker-easy-haproxy/examples/docker/docker-compose-test.yml","com.docker.compose.project.working_dir":"/workspace/docker-easy-haproxy/examples/docker","com.docker.compose.service":"nginx","com.docker.compose.version":"2.8.0","easyhaproxy.http.host":"test.example.org","easyhaproxy.http.letsencrypt":"true","easyhaproxy.http.localport":"80","easyhaproxy.http.port":"80","easyhaproxy.http.redirect":"{\"google.helloworld.com\": \"www.google.com\"}","easyhaproxy.http.redirect_ssl":"true"}, +"b63438410b6a": {"com.docker.compose.config-hash":"b95ebc27d0e61caa418cdfa632e05a656da9bbc3ea0d4603651971015f10a1f0","com.docker.compose.container-number":"2","com.docker.compose.depends_on":"","com.docker.compose.image":"sha256:bea3509d6fdc8d7f9ec95563a5a226dc977ee74fb3e980e0de70e892c2d38dde","com.docker.compose.oneoff":"False","com.docker.compose.project":"docker","com.docker.compose.project.config_files":"/workspace/docker-easy-haproxy/examples/docker/docker-compose-test.yml","com.docker.compose.project.working_dir":"/workspace/docker-easy-haproxy/examples/docker","com.docker.compose.service":"nginx","com.docker.compose.version":"2.8.0","easyhaproxy.http.host":"test.example.org","easyhaproxy.http.certbot":"true","easyhaproxy.http.localport":"80","easyhaproxy.http.port":"80","easyhaproxy.http.redirect":"{\"google.helloworld.com\": \"www.google.com\"}","easyhaproxy.http.redirect_ssl":"true"}, "83d57d592e26": {"com.docker.compose.config-hash":"8c5871144f1e8a3aeca037207c02f011ab2c6e6c311a3773602b63541762dab5","com.docker.compose.container-number":"1","com.docker.compose.depends_on":"","com.docker.compose.image":"sha256:c4232396c715f3d568816c666e6d9b4a68ef6c36f6243b4007c4ee1d8335fd65","com.docker.compose.oneoff":"False","com.docker.compose.project":"docker","com.docker.compose.project.config_files":"/workspace/docker-easy-haproxy/examples/docker/docker-compose-test.yml","com.docker.compose.project.working_dir":"/workspace/docker-easy-haproxy/examples/docker","com.docker.compose.service":"static","com.docker.compose.version":"2.8.0","easyhaproxy.http.host":"test2.example.org","easyhaproxy.http.localport":"8080","easyhaproxy.http.port":"80","io.buildah.version":"1.21.0"}} \ 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/fixtures/static.yml b/src/tests/fixtures/static.yml index c83d61c..53d3705 100644 --- a/src/tests/fixtures/static.yml +++ b/src/tests/fixtures/static.yml @@ -11,7 +11,7 @@ easymapping: host1.com.br: containers: - container:5000 - letsencrypt: true + certbot: true host2.com.br: containers: - other:3000 diff --git a/src/tests/test_containerenv.py b/src/tests/test_containerenv.py index c338871..30c2680 100644 --- a/src/tests/test_containerenv.py +++ b/src/tests/test_containerenv.py @@ -1,118 +1,243 @@ -import pytest import os -from processor import ContainerEnv + +from functions import Functions, ContainerEnv + def test_container_env_empty(): assert { - "customerrors": False, - "ssl_mode": "default", - "lookup_label": "easyhaproxy" - } == ContainerEnv.read() + "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": "", + "email": "", + "server": False, + "retry_count": 60} + } == 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() + "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": "", + "email": "", + "server": False, + "retry_count": 60} + } == ContainerEnv.read() finally: - os.environ['HAPROXY_CUSTOMERRORS'] = '' + del 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() + "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": "", + "email": "", + "server": False, + "retry_count": 60} + } == ContainerEnv.read() finally: - os.environ['EASYHAPROXY_SSL_MODE'] = '' + del 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() + "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": "", + "email": "", + "server": False, + "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(): os.environ['HAPROXY_PASSWORD'] = 'xyz' try: assert { - "customerrors": False, - "ssl_mode": "default", - "lookup_label": "easyhaproxy", - "stats": { - "username": "admin", - "password": "xyz", - "port": "1936" + "customerrors": False, + "ssl_mode": "default", + "lookup_label": "easyhaproxy", + "stats": { + "username": "admin", + "password": "xyz", + "port": "1936" - } - } == ContainerEnv.read() + }, + "logLevel": { + "easyhaproxy": Functions.DEBUG, + "haproxy": Functions.INFO, + "certbot": Functions.DEBUG, + }, + "certbot": {"autoconfig": "", + "eab_hmac_key": "", + "eab_kid": "", + "email": "", + "server": False, + "retry_count": 60} + } == ContainerEnv.read() finally: - os.environ['HAPROXY_PASSWORD'] = '' + del os.environ['HAPROXY_PASSWORD'] -def test_container_env_stats_password(): +def test_container_env_stats_password_2(): 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() + "customerrors": False, + "ssl_mode": "default", + "lookup_label": "easyhaproxy", + "stats": { + "username": "abc", + "password": "xyz", + "port": "2101" + }, + "logLevel": { + "easyhaproxy": Functions.DEBUG, + "haproxy": Functions.INFO, + "certbot": Functions.DEBUG, + }, + "certbot": {"autoconfig": "", + "eab_hmac_key": "", + "eab_kid": "", + "email": "", + "server": False, + "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_stats_password(): - os.environ['EASYHAPROXY_LETSENCRYPT_EMAIL'] = 'acme@example.org' +def test_container_env_certbot_email(): + os.environ['EASYHAPROXY_CERTBOT_EMAIL'] = 'acme@example.org' + try: + assert { + "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': "", + "email": "acme@example.org", + "server": False, + "retry_count": 60 + } + } == ContainerEnv.read() + finally: + del os.environ['EASYHAPROXY_CERTBOT_EMAIL'] + + +def test_container_env_certbot_full(): + os.environ['EASYHAPROXY_CERTBOT_EMAIL'] = 'acme@example.org' + os.environ['EASYHAPROXY_CERTBOT_SERVER'] = 'schema://url/a' + os.environ['EASYHAPROXY_CERTBOT_EAB_KID'] = 'eab_kid' + os.environ['EASYHAPROXY_CERTBOT_EAB_HMAC_KEY'] = 'eab_hmac_key' + os.environ['EASYHAPROXY_CERTBOT_RETRY_COUNT'] = "10" try: assert { "customerrors": False, "ssl_mode": "default", "lookup_label": "easyhaproxy", - "letsencrypt": { - "email": "acme@example.org", - "server": False - } + "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_LETSENCRYPT_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_env_letsencrypt(): - os.environ['EASYHAPROXY_LETSENCRYPT_EMAIL'] = 'acme@example.org' - os.environ['EASYHAPROXY_LETSENCRYPT_SERVER'] = 'true' + +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", - "letsencrypt": { - "email": "acme@example.org", - "server": True - } - } == ContainerEnv.read() + "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: - os.environ['EASYHAPROXY_LETSENCRYPT_EMAIL'] = '' \ No newline at end of file + 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 da0c60d..e31063e 100644 --- a/src/tests/test_docker.py +++ b/src/tests/test_docker.py @@ -1,10 +1,11 @@ -import pytest import os import time + import docker +import pytest + from functions import Functions from processor import ProcessorInterface -from processor import Docker def _get_hydrated_object(parsed_objects, lookup_key): @@ -17,7 +18,6 @@ def _get_hydrated_object(parsed_objects, lookup_key): 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: @@ -46,7 +46,7 @@ def test_processor_docker(): "easyhaproxy.http2.port": "90", "easyhaproxy.http2.localport": "9000", "easyhaproxy.http2.host": "host2.local", - "easyhaproxy.http2.letsencrypt": "true", + "easyhaproxy.http2.certbot": "true", }) container2 = client.containers.run("byjg/static-httpserver", name="test2_processor_docker", @@ -62,10 +62,10 @@ def test_processor_docker(): try: time.sleep(1) - os.environ['EASYHAPROXY_LETSENCRYPT_EMAIL'] = 'docker@example.org' + os.environ['EASYHAPROXY_CERTBOT_EMAIL'] = 'docker@example.org' static = ProcessorInterface.factory("docker") - assert static.get_letsencrypt_hosts() is None + assert static.get_certbot_hosts() is None assert { 'easyhaproxy.http.host': 'host1.local', @@ -74,7 +74,7 @@ def test_processor_docker(): 'easyhaproxy.http2.host': 'host2.local', 'easyhaproxy.http2.localport': '9000', 'easyhaproxy.http2.port': '90', - 'easyhaproxy.http2.letsencrypt': 'true', + 'easyhaproxy.http2.certbot': 'true', } == _get_hydrated_object(static.get_parsed_object(), "easyhaproxy.http") assert { 'easyhaproxy.ssl.host': 'hostssl.local', @@ -90,7 +90,7 @@ def test_processor_docker(): 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_certbot_hosts() == ['host2.local'] assert static.get_hosts() == [ 'hostssl.local:443', 'host1.local:80', @@ -100,7 +100,7 @@ def test_processor_docker(): 'hostssl.local.pem': 'Some PEM Certificate' } finally: - os.environ['EASYHAPROXY_LETSENCRYPT_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 5f40267..770cd55 100644 --- a/src/tests/test_functions.py +++ b/src/tests/test_functions.py @@ -1,11 +1,11 @@ -import json -import pytest import os -import re import random +import re import string + from functions import Functions + def test_functions_check_local_level(): assert Functions.skip_log('CERTBOT', Functions.INFO) == False assert Functions.skip_log('HAPOROXY', Functions.INFO) == False @@ -13,15 +13,16 @@ 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(): filename = '/tmp/x.txt' @@ -34,6 +35,7 @@ def test_function_load_and_save(): finally: os.unlink(filename) + def test_functions_check_log_sanity(): print() Functions.log(Functions.EASYHAPROXY_LOG, Functions.INFO, "Test 1") @@ -51,51 +53,104 @@ def test_functions_check_log_sanity(): assert re.match("\[EASYHAPROXY\] .* \[INFO\]: Test 3", Functions.debug_log[1]) os.environ['EASYHAPROXY_LOG_LEVEL'] = 'warn' - Functions.log(Functions.EASYHAPROXY_LOG, Functions.INFO, "Test 4") # Should not log to debug + Functions.log(Functions.EASYHAPROXY_LOG, Functions.INFO, "Test 4") # Should not log to debug assert len(Functions.debug_log) == 2 finally: - os.environ['EASYHAPROXY_LOG_LEVEL'] = '' + del os.environ['EASYHAPROXY_LOG_LEVEL'] Functions.debug_log = None + def test_functions_run_bash_log_output(): print() Functions.debug_log = [] try: - result = Functions.run_bash(Functions.EASYHAPROXY_LOG, "echo 'test run 1'", log_output=True, return_result=False) + return_code, result = Functions.run_bash(Functions.EASYHAPROXY_LOG, "echo 'test run 1'", log_output=True, + return_result=False) + assert return_code == 0 assert result == [] assert len(Functions.debug_log) == 1 assert re.match("\[EASYHAPROXY\] .* \[INFO\]: test run 1", Functions.debug_log[0]) finally: Functions.debug_log = None + def test_functions_run_bash_no_log_output(): print() Functions.debug_log = [] try: - result = Functions.run_bash(Functions.EASYHAPROXY_LOG, "echo 'test run 2'", log_output=False, return_result=False) + return_code, result = Functions.run_bash(Functions.EASYHAPROXY_LOG, "echo 'test run 2'", log_output=False, + return_result=False) + assert return_code == 0 assert result == [] assert len(Functions.debug_log) == 0 finally: Functions.debug_log = None + def test_functions_run_bash_return(): print() Functions.debug_log = [] try: - result = Functions.run_bash(Functions.EASYHAPROXY_LOG, "echo 'test run 3'", log_output=False, return_result=True) + return_code, result = Functions.run_bash(Functions.EASYHAPROXY_LOG, "echo 'test run 3'", log_output=False, + return_result=True) + assert return_code == 0 assert len(Functions.debug_log) == 0 assert "".join(result) == 'test run 3' finally: Functions.debug_log = None + def test_functions_run_bash_log_and_return_output(): print() Functions.debug_log = [] try: - result = Functions.run_bash(Functions.EASYHAPROXY_LOG, "echo 'test run 4'", log_output=True, return_result=True) + return_code, result = Functions.run_bash(Functions.EASYHAPROXY_LOG, "echo 'test run 4'", log_output=True, return_result=True) + assert return_code == 0 assert "".join(result) == 'test run 4' assert len(Functions.debug_log) == 1 assert re.match("\[EASYHAPROXY\] .* \[INFO\]: test run 4", Functions.debug_log[0]) finally: - Functions.debug_log = None \ No newline at end of file + Functions.debug_log = None + + +def test_functions_run_bash_ok(): + print() + Functions.debug_log = [] + try: + return_code, result = Functions.run_bash(Functions.EASYHAPROXY_LOG, "%s/fixtures/run_bash.sh" % os.path.dirname(__file__), log_output=True, + return_result=False) + assert return_code == 0 + assert result == [] + assert len(Functions.debug_log) == 1 + assert re.match("\[EASYHAPROXY\] .* \[INFO\]: Processing run_bash.sh", Functions.debug_log[0]) + finally: + Functions.debug_log = None + + +def test_functions_run_bash_fail(): + print() + Functions.debug_log = [] + try: + return_code, result = Functions.run_bash(Functions.EASYHAPROXY_LOG, "%s/fixtures/run_bash.sh 15" % os.path.dirname(__file__), log_output=True, + return_result=False) + assert return_code == 15 + assert result == [] + assert len(Functions.debug_log) == 1 + assert re.match("\[EASYHAPROXY\] .* \[INFO\]: Processing run_bash.sh", Functions.debug_log[0]) + finally: + Functions.debug_log = None + + +def test_functions_run_command_not_found(): + print() + Functions.debug_log = [] + try: + return_code, result = Functions.run_bash(Functions.EASYHAPROXY_LOG, "no_command_here", log_output=True, + return_result=False) + assert return_code == -99 + assert str(result) == "[Errno 2] No such file or directory: 'no_command_here'" + assert len(Functions.debug_log) == 1 + assert re.match("\[EASYHAPROXY\] .* \[ERROR\]: \[Errno 2\] No such file or directory: 'no_command_here'", Functions.debug_log[0]) + finally: + Functions.debug_log = None diff --git a/src/tests/test_labels.py b/src/tests/test_labels.py index f56c212..14ce279 100644 --- a/src/tests/test_labels.py +++ b/src/tests/test_labels.py @@ -1,16 +1,17 @@ -from .context import easymapping import json -import pytest + +from easymapping import DockerLabelHandler + def test_label_generation(): - label = easymapping.DockerLabelHandler("foo") + label = DockerLabelHandler("foo") assert label.create("bar") == "foo.bar" assert label.create(["bar", "foobar"]) == "foo.bar.foobar" def test_label_data(): - label = easymapping.DockerLabelHandler("base") + label = DockerLabelHandler("base") label.set_data(json.loads('{"base.definitions":"h2"}')) label_name = label.create("definitions") @@ -20,7 +21,7 @@ def test_label_data(): def test_label_complex_key(): - label = easymapping.DockerLabelHandler("till") + label = DockerLabelHandler("till") data = dict() data["till.definitions"] = "h2" diff --git a/src/tests/test_parser.py b/src/tests/test_parser.py index a3d35f0..3f6f1fa 100644 --- a/src/tests/test_parser.py +++ b/src/tests/test_parser.py @@ -1,12 +1,14 @@ -import easymapping -import pytest -import os -import yaml import json +import os + +import yaml + +import easymapping + +CERTS_FOLDER = "/tmp/certs" +CERT_FILE = "/tmp/certs/haproxy/www.somehost.com.br.pem" +CERTBOT_EMAIL = "some@email.com" -CERTS_FOLDER="/tmp/certs" -CERT_FILE="/tmp/certs/haproxy/www.somehost.com.br.pem" -LETSENCRYPT_EMAIL="some@email.com" def load_fixture(file): path = os.path.dirname(os.path.realpath(__file__)) @@ -33,15 +35,16 @@ def test_parser_doesnt_crash(): path = os.path.dirname(os.path.realpath(__file__)) with open(path + "/expected/no-services.txt", 'r') as expected_file: assert expected_file.read() == haproxy_config - assert [] == cfg.letsencrypt_hosts + assert [] == cfg.certbot_hosts + def test_parser_finds_services(): line_list = load_fixture("services") result = { "customerrors": False, - "letsencrypt": { - "email": LETSENCRYPT_EMAIL + "certbot": { + "email": CERTBOT_EMAIL }, "stats": { "port": 0 @@ -56,9 +59,10 @@ def test_parser_finds_services(): with open(path + "/expected/services.txt", 'r') as expected_file: assert expected_file.read() == haproxy_config - assert {"www.somehost.com.br.pem":"Some PEM Certificate"} == cfg.certs + assert {"www.somehost.com.br.pem": "Some PEM Certificate"} == cfg.certs + + assert ['node-exporter.quantum.example.org'] == cfg.certbot_hosts - assert ['node-exporter.quantum.example.org'] == cfg.letsencrypt_hosts def test_parser_finds_services_changed_label(): line_list = load_fixture("services-changed-label") @@ -66,8 +70,8 @@ def test_parser_finds_services_changed_label(): result = { "customerrors": False, "lookup_label": "haproxy", - "letsencrypt": { - "email": LETSENCRYPT_EMAIL + "certbot": { + "email": CERTBOT_EMAIL }, "stats": { "port": 0 @@ -85,17 +89,18 @@ def test_parser_finds_services_changed_label(): with open(path + "/expected/services.txt", 'r') as expected_file: assert expected_file.read() == haproxy_config - assert {"www.somehost.com.br.pem":"Some PEM Certificate"} == cfg.certs + assert {"www.somehost.com.br.pem": "Some PEM Certificate"} == cfg.certs + + assert ['node-exporter.quantum.example.org'] == cfg.certbot_hosts - assert ['node-exporter.quantum.example.org'] == cfg.letsencrypt_hosts def test_parser_finds_services_raw(): line_list = load_fixture("services") result = { "customerrors": False, - "letsencrypt": { - "email": LETSENCRYPT_EMAIL + "certbot": { + "email": CERTBOT_EMAIL }, "stats": { "port": 0 @@ -110,93 +115,99 @@ def test_parser_finds_services_raw(): parsed_object = [ { "mode":"tcp", - "health-check":"", + "ssl-check":"", "port":"31339", "hosts":{ "agent.quantum.example.org": { + "balance": "roundrobin", "containers": [ "my-stack_agent:9001" ], - "letsencrypt": False, + "certbot": False, "redirect_ssl": False } }, - "redirect":{ - + "redirect": { + } }, { "mode":"http", - "health-check":"", + "ssl-check":"", "port":"31337", "hosts":{ "cadvisor.quantum.example.org":{ + "balance": "roundrobin", "containers": [ "my-stack_cadvisor:8080" ], - "letsencrypt": False, + "certbot": False, "redirect_ssl": False }, "node-exporter.quantum.example.org":{ + "balance": "roundrobin", "containers": [ "my-stack_node-exporter:9100" ], - "letsencrypt": True, + "certbot": True, "redirect_ssl": False } }, - "redirect":{ - + "redirect": { + }, }, { "mode":"http", - "health-check":"", + "ssl-check":"", "port":"443", "hosts":{ "node-exporter.quantum.example.org": { + "balance": "roundrobin", "containers": [ "my-stack_node-exporter:9100" ], - "letsencrypt": False, + "certbot": False, "redirect_ssl": False }, "www.somehost.com.br":{ + "balance": "roundrobin", "containers": [ "some-service:80" ], - "letsencrypt": False, + "certbot": False, "redirect_ssl": False } }, - "redirect":{ - "somehost.com.br":"https://www.somehost.com.br", - "somehost.com":"https://www.somehost.com.br", - "www.somehost.com":"https://www.somehost.com.br", - "byjg.ca":"https://www.somehost.com.br", - "www.byjg.ca":"https://www.somehost.com.br" + "redirect": { + "somehost.com.br": "https://www.somehost.com.br", + "somehost.com": "https://www.somehost.com.br", + "www.somehost.com": "https://www.somehost.com.br", + "byjg.ca": "https://www.somehost.com.br", + "www.byjg.ca": "https://www.somehost.com.br" }, "ssl": True }, { "mode":"http", - "health-check":"", + "ssl-check":"", "port":"80", "hosts":{ "www.somehost.com.br":{ + "balance": "roundrobin", "containers": [ "some-service:80" ], - "letsencrypt": False, + "certbot": False, "redirect_ssl": False } }, - "redirect":{ - "somehost.com.br":"https://www.somehost.com.br", - "somehost.com":"https://www.somehost.com.br", - "www.somehost.com":"https://www.somehost.com.br", - "byjg.ca":"https://www.somehost.com.br", - "www.byjg.ca":"https://www.somehost.com.br" + "redirect": { + "somehost.com.br": "https://www.somehost.com.br", + "somehost.com": "https://www.somehost.com.br", + "www.somehost.com": "https://www.somehost.com.br", + "byjg.ca": "https://www.somehost.com.br", + "www.byjg.ca": "https://www.somehost.com.br" }, } ] @@ -204,8 +215,7 @@ def test_parser_finds_services_raw(): processed = list(cfg.parse(line_list)) assert parsed_object == processed - assert ['node-exporter.quantum.example.org'] == cfg.letsencrypt_hosts - + assert ['node-exporter.quantum.example.org'] == cfg.certbot_hosts def test_parser_static(): @@ -219,7 +229,8 @@ def test_parser_static(): with open(path + "/expected/static.txt", 'r') as expected_file: assert expected_file.read() == haproxy_config - assert [] == cfg.letsencrypt_hosts + assert [] == cfg.certbot_hosts + def test_parser_static_raw(): path = os.path.dirname(os.path.realpath(__file__)) @@ -241,7 +252,7 @@ def test_parser_static_raw(): "containers": [ "container:5000" ], - "letsencrypt": True + "certbot": True }, "host2.com.br": { "containers": [ @@ -280,7 +291,6 @@ def test_parser_static_raw(): assert expected == parsed - def test_parser_tcp(): line_list = load_fixture("services-tcp") @@ -299,7 +309,8 @@ def test_parser_tcp(): path = os.path.dirname(os.path.realpath(__file__)) with open(path + "/expected/services-tcp.txt", 'r') as expected_file: assert expected_file.read() == haproxy_config - assert [] == cfg.letsencrypt_hosts + assert [] == cfg.certbot_hosts + def test_parser_multi_containers(): line_list = load_fixture("services-multi-containers") @@ -318,7 +329,7 @@ def test_parser_multi_containers(): path = os.path.dirname(os.path.realpath(__file__)) with open(path + "/expected/services-multi-containers.txt", 'r') as expected_file: assert expected_file.read() == haproxy_config - assert [] == cfg.letsencrypt_hosts + assert [] == cfg.certbot_hosts def test_parser_multiple_hosts(): @@ -340,7 +351,7 @@ def test_parser_multiple_hosts(): path = os.path.dirname(os.path.realpath(__file__)) with open(path + "/expected/services-multiple-hosts.txt", 'r') as expected_file: assert expected_file.read() == haproxy_config - assert [] == cfg.letsencrypt_hosts + assert [] == cfg.certbot_hosts def test_parser_redirect_ssl(): @@ -361,7 +372,7 @@ def test_parser_redirect_ssl(): path = os.path.dirname(os.path.realpath(__file__)) with open(path + "/expected/services-redirect-ssl.txt", 'r') as expected_file: assert expected_file.read() == haproxy_config - assert [] == cfg.letsencrypt_hosts + assert [] == cfg.certbot_hosts def test_parser_ssl_strict(): @@ -382,7 +393,8 @@ def test_parser_ssl_strict(): path = os.path.dirname(os.path.realpath(__file__)) with open(path + "/expected/ssl-strict.txt", 'r') as expected_file: assert expected_file.read() == haproxy_config - assert [] == cfg.letsencrypt_hosts + assert [] == cfg.certbot_hosts + def test_parser_ssl_loose(): line_list = load_fixture("no-services") @@ -399,7 +411,8 @@ def test_parser_ssl_loose(): path = os.path.dirname(os.path.realpath(__file__)) with open(path + "/expected/ssl-loose.txt", 'r') as expected_file: assert expected_file.read() == haproxy_config - assert [] == cfg.letsencrypt_hosts + assert [] == cfg.certbot_hosts + def test_parser_ssl_letsencrypt(): line_list = load_fixture("services-letsencrypt") @@ -409,8 +422,8 @@ def test_parser_ssl_letsencrypt(): "stats": { "password": "password" }, - "letsencrypt": { - "email": LETSENCRYPT_EMAIL + "certbot": { + "email": CERTBOT_EMAIL } } @@ -421,7 +434,7 @@ def test_parser_ssl_letsencrypt(): path = os.path.dirname(os.path.realpath(__file__)) with open(path + "/expected/services-letsencrypt.txt", 'r') as expected_file: assert expected_file.read() == haproxy_config - assert ["test.example.org"] == cfg.letsencrypt_hosts + assert ["test.example.org"] == cfg.certbot_hosts def test_parser_finds_services_clone_to_ssl_raw(): @@ -429,8 +442,8 @@ def test_parser_finds_services_clone_to_ssl_raw(): result = { "customerrors": False, - "letsencrypt": { - "email": LETSENCRYPT_EMAIL + "certbot": { + "email": CERTBOT_EMAIL }, "stats": { "port": 0 @@ -444,51 +457,55 @@ def test_parser_finds_services_clone_to_ssl_raw(): parsed_object = [ { - "health-check":"", + "ssl-check":"", "hosts":{ "host2.local":{ + "balance":"roundrobin", "containers":[ "10.152.183.215:8080" ], - "letsencrypt": False, + "certbot": False, "redirect_ssl": False }, "valida.me":{ + "balance":"roundrobin", "containers":[ "10.152.183.62:8080" ], - "letsencrypt": False, + "certbot": False, "redirect_ssl": False }, "www.valida.me":{ + "balance":"roundrobin", "containers":[ "10.152.183.62:8080" ], - "letsencrypt": False, + "certbot": False, "redirect_ssl": False } }, - "mode":"http", - "port":"80", - "redirect":{ - + "mode": "http", + "port": "80", + "redirect": { + } }, { - "health-check":"ssl", + "ssl-check":"ssl", "hosts":{ "host2.local":{ + "balance":"roundrobin", "containers":[ "10.152.183.215:8080" ], - "letsencrypt": False, + "certbot": False, "redirect_ssl": False } }, - "mode":"http", - "port":"443", - "redirect":{ - + "mode": "http", + "port": "443", + "redirect": { + }, "ssl": True } @@ -496,12 +513,10 @@ def test_parser_finds_services_clone_to_ssl_raw(): processed = list(cfg.parse(line_list)) assert parsed_object == processed - assert [] == cfg.letsencrypt_hosts + assert [] == cfg.certbot_hosts - - -#test_parser_finds_services_raw() -#test_parser_tcp() -#test_parser_multiple_hosts() -#test_parser_ssl_letsencrypt() -#test_parser_finds_services() \ No newline at end of file +# test_parser_finds_services_raw() +# test_parser_tcp() +# test_parser_multiple_hosts() +# test_parser_ssl_certbot() +# test_parser_finds_services() diff --git a/src/tests/test_static.py b/src/tests/test_static.py index c21593c..7e6998c 100644 --- a/src/tests/test_static.py +++ b/src/tests/test_static.py @@ -1,8 +1,8 @@ -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") @@ -10,44 +10,44 @@ def test_processor_static(): parsed_object = [ { - "hosts":{ - "host1.com.br":{ - "containers":[ - "container:5000" + "hosts": { + "host1.com.br": { + "containers": [ + "container:5000" ], - "letsencrypt": True + "certbot": True }, - "host2.com.br":{ - "containers":[ - "other:3000" + "host2.com.br": { + "containers": [ + "other:3000" ] } }, - "port":80, - "redirect":{ - "www.host1.com.br":"http://host1.com.br" + "port": 80, + "redirect": { + "www.host1.com.br": "http://host1.com.br" } }, { - "hosts":{ - "host1.com.br":{ - "containers":[ - "container:80" + "hosts": { + "host1.com.br": { + "containers": [ + "container:80" ] } }, - "port":443, + "port": 443, "ssl": True }, { - "hosts":{ - "host3.com.br":{ - "containers":[ - "domain:8181" + "hosts": { + "host3.com.br": { + "containers": [ + "domain:8181" ] } }, - "port":8080 + "port": 8080 } ] hosts = [ @@ -57,17 +57,18 @@ def test_processor_static(): 'host3.com.br:8080' ] - assert static.get_letsencrypt_hosts() is None + assert static.get_certbot_hosts() is None assert static.get_parsed_object() == parsed_object assert static.get_hosts() == hosts haproxy_cfg = static.get_haproxy_conf() - assert haproxy_cfg == Functions.load(os.path.join(os.path.dirname(os.path.realpath(__file__)), "./expected/static.txt")) + 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_certbot_hosts() == [] assert static.get_parsed_object() == parsed_object assert static.get_hosts() == hosts -# test_processor_static() \ No newline at end of file +# test_processor_static()