1
0
Fork 0

Merge pull request #6 from byjg/2.2

Release 2.2
This commit is contained in:
Joao Gilberto Magalhaes 2021-08-10 18:01:23 -05:00 committed by GitHub
commit 7057965da4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 905 additions and 149 deletions

1
.gitignore vendored
View file

@ -3,3 +3,4 @@
venv venv
.docker_data .docker_data
__pycache__ __pycache__
.pytest_cache

26
.travis.yml Normal file
View file

@ -0,0 +1,26 @@
language: python
services:
- docker
jobs:
include:
- stage: test
if: (type IN (pull_request))
install:
- pip install -r requirements.txt
script:
- pytest -s tests/
- stage: build docker
if: (branch = master) AND (NOT (type IN (pull_request)))
install:
- docker pull byjg/k8s-ci
script:
- docker run --privileged -v /tmp/z:/var/lib/containers -it --rm -v $PWD:/work -w /work -e DOCKER_USERNAME=$DOCKER_USERNAME -e DOCKER_PASSWORD=$DOCKER_PASSWORD -e DOCKER_REGISTRY=$DOCKER_REGISTRY byjg/k8s-ci /work/build-multiarch.sh
- stage: documentation
if: (branch = master) AND (NOT (type IN (pull_request)))
install: skip
script: "curl https://opensource.byjg.com/add-doc.sh | bash /dev/stdin devops docker-easy-haproxy"

View file

@ -1,18 +1,21 @@
FROM haproxy:2.1-alpine FROM alpine:3.14
WORKDIR /scripts WORKDIR /scripts
RUN apk add --no-cache bash python3 py-yaml supervisor docker RUN apk add --no-cache haproxy bash python3 py3-pip py-yaml supervisor docker \
&& ln -s /usr/bin/python3 /usr/bin/python
COPY requirements.txt /scripts COPY requirements.txt /scripts
RUN pip3 install --upgrade pip \ RUN pip3 install --upgrade pip \
&& pip install -r requirements.txt && pip install -r requirements.txt
COPY swarm.* /scripts/
COPY static.* /scripts/
COPY templates /scripts/templates/ COPY templates /scripts/templates/
COPY easymapping /scripts/easymapping/ COPY easymapping /scripts/easymapping/
COPY tests/ /scripts/tests/
COPY assets / COPY assets /
RUN pytest -s tests/
CMD ["/usr/bin/supervisord", "-n", "-c", "/etc/supervisord.conf" ] CMD ["/usr/bin/supervisord", "-n", "-c", "/etc/supervisord.conf" ]

7
Makefile Normal file
View file

@ -0,0 +1,7 @@
.PHONY: build
build:
docker build -t byjg/easy-haproxy -t byjg/easy-haproxy:local .
.PHONY: test
test:
pytest tests/

View file

@ -1,24 +1,24 @@
# Easy HAProxy # Easy HAProxy
This Docker image will create dynamically the `haproxy.cfg` based on the labels defined in docker containers or from This Docker image will create dynamically the `haproxy.cfg` based on the labels defined in docker containers or from
a simple Yaml instead docker a simple Yaml instead docker
# Features ## Features
- Enable or disable Stats on port 1936 with custom password - Enable or disable Stats on port 1936 with custom password
- Discover and setup haproxy from Docker Tag - Discover and setup haproxy from Docker Tag
- Discover and setup haproxy redirect from Docker Tag - Discover and setup haproxy redirect from Docker Tag
- Setup HAProxy CFG from a Yaml file. - Setup HAProxy CFG from a Yaml file.
# Basic Usage ## Basic Usage
The Easy HAProxy will create the `haproxy.cfg` automatically based on the containers or from a YAML provided. The Easy HAProxy will create the `haproxy.cfg` automatically based on the containers or from a YAML provided.
The basic command line to run is: The basic command line to run is:
```bash ```bash
docker run -d \ docker run -d \
--name easy-haproxy-container \ --name easy-haproxy-container \
-v /var/run/docker.sock:/var/run/docker.sock \ -v /var/run/docker.sock:/var/run/docker.sock \
-e DISCOVER="swarm|docker|static" \ -e DISCOVER="swarm|docker|static" \
@ -29,9 +29,8 @@ docker run -d \
The mapping to `/var/run/docker.sock` is necessary to discover the docker containers and get the labels; The mapping to `/var/run/docker.sock` is necessary to discover the docker containers and get the labels;
The environment variables will setup the HAProxy. The environment variables will setup the HAProxy.
{:.table}
| Environment Variable | Description | | Environment Variable | Description |
|----------------------|-------------------------------------------------------------------------------| |----------------------|-------------------------------------------------------------------------------|
| DISCOVER | How `haproxy.cfg` will be created: `static`, `docker` or `swarm` | | DISCOVER | How `haproxy.cfg` will be created: `static`, `docker` or `swarm` |
@ -47,11 +46,11 @@ The environment variable `DISCOVER` will define where is located your containers
- swarm - swarm
- static - static
# DISCOVER: docker ## DISCOVER: docker
This method will use a regular docker installation to discover the containers and configure the HAProxy. This method will use a regular docker installation to discover the containers and configure the HAProxy.
The only requirement is that containers and easy-haproxy must be in the same docker network. The only requirement is that containers and easy-haproxy must be in the same docker network.
The discover will occur every minute. The discover will occur every minute.
@ -65,27 +64,27 @@ docker run --network easyhaproxy byjg/easyhaproxy
docker run --network easyhaproxy myimage docker run --network easyhaproxy myimage
``` ```
# DISCOVER: swarm ## DISCOVER: swarm
This method requires a functional Docker Swarm Cluster. The system will search for the labels in all containers on all This method requires a functional Docker Swarm Cluster. The system will search for the labels in all containers on all
swarm nodes. swarm nodes.
The discover will occur every minute. The discover will occur every minute.
Important: easyhaproxy needs to be in the same network of the containers or otherwise will not access. Important: easyhaproxy needs to be in the same network of the containers or otherwise will not access.
## Tags to be attached in the Docker Container ### Tags to be attached in the Docker Container (Swarm or Docker)
{:.table}
| Tag | Description |
|---------------------------------------------|---------------------------------------------------------------------------------------------------------|
| com.byjg.easyhaproxy.definitions | A Comma delimited list with the definitions. Each name requires the definition of the parameters below. |
| com.byjg.easyhaproxy.port.[definition] | (Optional) What is the port that the HAProxy will listen to. (Defaults to 80) |
| com.byjg.easyhaproxy.localport.[definition] | (Optional) What is the port that the container is listening. (Defaults to 80) |
| com.byjg.easyhaproxy.host.[definition] | What is the host that the HAProxy will listen to. |
| com.byjg.easyhaproxy.redirect.[definition] | (Optional) Host redirects from connections in the port defined above. |
| com.byjg.easyhaproxy.sslcert.[definition] | (Optional) Cert PEM Base64 encoded. |
| Tag | Description | Example |
|---------------------------------------------|---------------------------------------------------------------------------------------------------------|--------------|
| com.byjg.easyhaproxy.definitions | A Comma delimited list with the definitions. Each name requires the definition of the parameters below. | http,https |
| com.byjg.easyhaproxy.mode.[definition] | (Optional) Is this http or tcp mode in HAProxy. (Defaults to http) | http |
| com.byjg.easyhaproxy.port.[definition] | (Optional) What is the port that the HAProxy will listen to. (Defaults to 80) | 80 |
| com.byjg.easyhaproxy.localport.[definition] | (Optional) What is the port that the container is listening. (Defaults to 80) | 8080 |
| com.byjg.easyhaproxy.host.[definition] | What is the host that the HAProxy will listen to. | somehost.com |
| com.byjg.easyhaproxy.redirect.[definition] | (Optional) Host redirects from connections in the port defined above. | foo.com--https://bla.com,bar.com--https://bar.org |
| com.byjg.easyhaproxy.sslcert.[definition] | (Optional) Cert PEM Base64 encoded. | |
| com.byjg.easyhaproxy.health-check.[definition] | (Optional) `ssl`, enable health check via SSL in `mode tcp` (Defaults to "empty") | |
Note: if you are deploying a stack set labels at the `deploy` level: Note: if you are deploying a stack set labels at the `deploy` level:
@ -126,6 +125,22 @@ docker run \
some/myimage some/myimage
``` ```
### TLS passthrough
Used to pass on SSL-termination to a backend:
```bash
docker run \
-l com.byjg.easyhaproxy.defintions=tcp-service \
-l com.byjg.easyhaproxy.mode.tcp-service=tcp \
-l com.byjg.easyhaproxy.health-check.tcp-service=ssl \
-l com.byjg.easyhaproxy.port.tcp-service=443
.... \
some/tcp-service
```
- enable health-check via SSL on the backend with the optional `health-check` label
### Redirect Example: ### Redirect Example:
```bash ```bash
@ -133,7 +148,7 @@ docker run \
-l com.byjg.easyhaproxy.redirect.<defintion>=www.byjg.com.br--http://byjg.com.br,byjg.com--http://byjg.com.br -l com.byjg.easyhaproxy.redirect.<defintion>=www.byjg.com.br--http://byjg.com.br,byjg.com--http://byjg.com.br
``` ```
# DISCOVER: static ## DISCOVER: static
This method expects a YAML file to setup the `haproxy.cfg` This method expects a YAML file to setup the `haproxy.cfg`
@ -149,14 +164,14 @@ customerrors: true # Optional (default false)
easymapping: easymapping:
- port: 80 - port: 80
hosts: hosts:
host1.com.br: container:5000 host1.com.br: container:5000
host2.com.br: other:3000 host2.com.br: other:3000
redirect: redirect:
www.host1.com.br: http://host1.com.br www.host1.com.br: http://host1.com.br
- port: 443 - port: 443
ssl_cert: BASE64_PEM_CERTIFICATE ssl_cert: /path/to/ssl/certificate
hosts: hosts:
host1.com.br: container:80 host1.com.br: container:80
@ -173,10 +188,10 @@ docker run -v /my/config.yml:/etc/haproxy/easyconfig.yml .... byjg/easyhaproxy
# Mapping custom .cfg files # Mapping custom .cfg files
Map a folder containing valid HAProxy `.cfg` files to `/etc/haproxy/conf.d`. It will be concatenated to your HAProxy CFG. Map a folder containing valid HAProxy `.cfg` files to `/etc/haproxy/conf.d`. It will be concatenated to your HAProxy CFG.
```bash ```bash
docker run \ docker run \
/* other parameters */ /* other parameters */
-v /your/local/conf.d:/etc/haproxy/conf.d \ -v /your/local/conf.d:/etc/haproxy/conf.d \
-d byjg/easy-haproxy -d byjg/easy-haproxy
@ -185,9 +200,9 @@ docker run \
# Handling SSL # Handling SSL
You can attach a valid SSL certificate to the request. You can attach a valid SSL certificate to the request.
1. First Create a single PEM file including CA. 1. First Create a single PEM file including CA.
```bash ```bash
cat example.com.crt example.com.key > single.pem cat example.com.crt example.com.key > single.pem
@ -215,8 +230,8 @@ cat single.pem | base64 -w0
# Setting Custom Errors # Setting Custom Errors
If enabled, map the volume : `/etc/haproxy/errors-custom/` to your container and put a file named `ERROR_NUMBER.http` If enabled, map the volume : `/etc/haproxy/errors-custom/` to your container and put a file named `ERROR_NUMBER.http`
where ERROR_NUMBER is the http error code (e.g. 503.http) where ERROR_NUMBER is the http error code (e.g. 503.http)
# Build # Build

View file

@ -5,6 +5,7 @@ import signal
from supervisor import childutils from supervisor import childutils
def main(): def main():
while True: while True:
headers, payload = childutils.listener.wait() headers, payload = childutils.listener.wait()
@ -12,7 +13,11 @@ def main():
events = ['PROCESS_STATE_FATAL', 'PROCESS_STATE_EXITED', 'PROCESS_STATE_STOPPED'] events = ['PROCESS_STATE_FATAL', 'PROCESS_STATE_EXITED', 'PROCESS_STATE_STOPPED']
if not (headers['eventname'] in events): if not (headers['eventname'] in events):
continue continue
print(headers)
print(payload)
os.kill(os.getppid(), signal.SIGTERM) os.kill(os.getppid(), signal.SIGTERM)
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View file

@ -18,14 +18,18 @@ else
if [[ "$DISCOVER" == "docker" ]]; then if [[ "$DISCOVER" == "docker" ]]; then
CONTAINERS=$(docker ps -q) CONTAINERS=$(docker ps -q)
LABEL_PATH=".Config.Labels" LABEL_PATH=".Config.Labels"
for container in ${CONTAINERS}; do
docker inspect --format "{{ json $LABEL_PATH }}" ${container} | xargs -I % echo ${container}=% >> ${CONTROL_FILE}
done
else else
CONTAINERS=$(docker node ps $(docker node ls -q) --format "{{ .Name }}" --filter desired-state=running | cut -d. -f1 | sort | uniq) CONTAINERS=$(docker node ps $(docker node ls -q) --format "{{ .Name }}" --filter desired-state=running | cut -d. -f1 | sort | uniq)
LABEL_PATH=".Spec.Labels" LABEL_PATH=".Spec.Labels"
fi
for container in ${CONTAINERS}; do for container in ${CONTAINERS}; do
docker inspect --format "{{ json $LABEL_PATH }}" ${container} | xargs -I % echo ${container}=% >> ${CONTROL_FILE} docker service inspect --format "{{ json $LABEL_PATH }}" ${container} | xargs -I % echo ${container}=% >> ${CONTROL_FILE}
done done
fi
if cmp -s ${CONTROL_FILE} ${CONTROL_FILE}.old ; then if cmp -s ${CONTROL_FILE} ${CONTROL_FILE}.old ; then
RELOAD="false" RELOAD="false"

View file

@ -2,7 +2,7 @@
source /scripts/haproxy-reload.sh initial source /scripts/haproxy-reload.sh initial
/usr/local/sbin/haproxy -W -f /etc/haproxy/haproxy.cfg -p /run/haproxy.pid -S /var/run/haproxy.sock /usr/sbin/haproxy -W -f /etc/haproxy/haproxy.cfg -p /run/haproxy.pid -S /var/run/haproxy.sock
while true; do while true; do
sleep 60 sleep 60

24
assets/scripts/swarm.py Normal file
View file

@ -0,0 +1,24 @@
import os
from easymapping import HaproxyConfigGenerator
# path = os.path.dirname(os.path.realpath(__file__))
with open("/tmp/.docker_data", 'r') as content_file:
lineList = content_file.readlines()
result = {
"customerrors": True if os.getenv("HAPROXY_CUSTOMERRORS") == "true" else False
}
if os.getenv("HAPROXY_PASSWORD"):
result["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",
}
cfg = HaproxyConfigGenerator(result)
print(cfg.generate(lineList))
# print(jsonStr)

33
build-multiarch.sh Executable file
View file

@ -0,0 +1,33 @@
#!/bin/bash
set -e
# Start k8s-ci before run this command
# docker run --privileged -v /tmp/z:/var/lib/containers -it --rm -v $PWD:/work -w /work byjg/k8s-ci
if [ -z "$DOCKER_USERNAME" ] || [ -z "$DOCKER_PASSWORD" ] || [ -z "$DOCKER_REGISTRY" ]
then
echo You need to setup \$DOCKER_USERNAME, \$DOCKER_PASSWORD and \$DOCKER_REGISTRY before run this command.
exit 1
fi
buildah login --username $DOCKER_USERNAME --password $DOCKER_PASSWORD $DOCKER_REGISTRY
podman run --rm --events-backend=file --cgroup-manager=cgroupfs --privileged docker://multiarch/qemu-user-static --reset -p yes
VERSIONS="latest $TRAVIS_TAG"
for VERSION in $VERSIONS
do
DOCKERFILE=Dockerfile
buildah manifest create byjg/easy-haproxy:$VERSION
buildah bud --arch arm64 --os linux --iidfile /tmp/iid-arm64 -f $DOCKERFILE -t byjg/easy-haproxy:$VERSION-arm64 .
buildah bud --arch amd64 --os linux --iidfile /tmp/iid-amd64 -f $DOCKERFILE -t byjg/easy-haproxy:$VERSION-amd64 .
buildah manifest add byjg/easy-haproxy:$VERSION --arch arm64 --os linux --variant v8 $(cat /tmp/iid-arm64)
buildah manifest add byjg/easy-haproxy:$VERSION --arch amd64 --os linux --os=linux $(cat /tmp/iid-amd64)
buildah manifest push --all --format v2s2 byjg/easy-haproxy:$VERSION docker://byjg/easy-haproxy:$VERSION
done

View file

@ -1,12 +1,146 @@
import base64
import hashlib
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
import json
import os
class DockerLabelHandler:
def __init__(self, label):
self.__label_base = label
def create(self, key):
if isinstance(key, str):
return "{}.{}".format(self.__label_base, key)
return "{}.{}".format(self.__label_base, ".".join(key))
def get(self, label, default_value = ""):
if self.has_label(label):
return self.__data[label]
return default_value
def set_data(self, data):
self.__data = data
def has_label(self, label):
if label in self.__data:
return True
return False
class HaproxyConfigGenerator: class HaproxyConfigGenerator:
def __init__(self, mapping): def __init__(self, mapping, ssl_cert_folder="/etc/haproxy/certs"):
self.mapping = mapping self.mapping = mapping
self.label = DockerLabelHandler("com.byjg.easyhaproxy")
self.ssl_cert_folder = ssl_cert_folder
self.ssl_cert_increment = 0
os.makedirs(self.ssl_cert_folder, exist_ok=True)
def generate(self, lineList = []):
# static?
if len(lineList) > 0:
self.mapping["easymapping"] = self.__parse(lineList)
# still 'None' -> default to [] for jinja2
if self.mapping["easymapping"] is None:
self.mapping["easymapping"] = []
def generate(self):
file_loader = FileSystemLoader('templates') file_loader = FileSystemLoader('templates')
env = Environment(loader=file_loader) env = Environment(loader=file_loader)
env.trim_blocks = True
env.lstrip_blocks = True
env.rstrip_blocks = True
template = env.get_template('haproxy.cfg.j2') template = env.get_template('haproxy.cfg.j2')
return template.render(data=self.mapping) return template.render(data=self.mapping)
def __parse(self, lineList):
easymapping = dict()
for line in lineList:
line = line.strip()
i = line.find("=")
container = line[:i]
jsonStr = line[i+1:]
d = json.loads(jsonStr)
if self.label.create("definitions") not in d.keys():
continue
self.label.set_data(d)
definitions = d[self.label.create("definitions")].split(",")
for definition in definitions:
mode = self.label.get(
self.label.create(["mode", definition]),
"http"
)
# TODO: we can ignore "host" in TCP, but it would break the template
host_label = self.label.create(["host", definition])
if not self.label.has_label(host_label):
continue
port = self.label.get(
self.label.create(["port", definition]),
"80"
)
hash = ""
if self.label.create(["sslcert", definition]) in d:
hash = hashlib.md5(
d[self.label.create(["sslcert", definition])].encode('utf-8')
).hexdigest()
key = port if not hash else port + "_" + hash
if key not in easymapping:
easymapping[key] = {
"mode": mode,
"health-check": "",
"port": port,
"hosts": dict(),
"redirect": dict(),
}
# TODO: this could use `EXPOSE` from `Dockerfile`?
ct_port = self.label.get(
self.label.create(["localport", definition]),
"80"
)
easymapping[key]["health-check"] = self.label.get(
self.label.create(["health-check", definition]),
""
)
easymapping[key]["hosts"][d[host_label]] = "{}:{}".format(container, ct_port)
# handle SSL
ssl_label = self.label.create(["sslcert", definition])
if self.label.has_label(ssl_label):
self.ssl_cert_increment += 1
filename = "{}/{}.{}.pem".format(
self.ssl_cert_folder, d[host_label], str(self.ssl_cert_increment)
)
easymapping[key]["ssl_cert"] = filename
with open(filename, 'wb') as file:
file.write(
base64.b64decode(d[ssl_label])
)
# handle redirects
redirect = self.label.get(
self.label.create(["redirect", definition])
)
if len(redirect) > 0:
for r in redirect.split(","):
r_parts = r.split("--")
easymapping[key]["redirect"][r_parts[0]] = r_parts[1]
return easymapping.values()

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,17 @@
stats:
username: admin
password: password
port: 1936 # Optional (default 1936)
customerrors: true # Optional (default false)
easymapping:
- port: 80
redirect:
host1.local: https://host1.local
www.host1.local: https://host1.local
- port: 443
ssl_cert: /etc/certs/host1.local.pem
hosts:
host1.local: container:8080

View file

@ -0,0 +1,18 @@
version: "3"
services:
haproxy:
image: byjg/easy-haproxy
volumes:
- ./config.yml:/etc/haproxy/easyconfig.yml
- ./host1.local.pem:/etc/certs/host1.local.pem
- /var/run/docker.sock:/var/run/docker.sock
environment:
DISCOVER: static
ports:
- "80:80/tcp"
- "443:443/tcp"
- "1936:1936/tcp"
container:
image: byjg/static-httpserver

View file

@ -0,0 +1,82 @@
-----BEGIN CERTIFICATE-----
MIIFDTCCAvWgAwIBAgIURi+w1ZVgeedTlNIAwqQBMJv6dXswDQYJKoZIhvcNAQEL
BQAwFjEUMBIGA1UEAwwLaG9zdDEubG9jYWwwHhcNMjEwODEwMTg0OTA2WhcNMzEw
ODA4MTg0OTA2WjAWMRQwEgYDVQQDDAtob3N0MS5sb2NhbDCCAiIwDQYJKoZIhvcN
AQEBBQADggIPADCCAgoCggIBAMBDAhLAygJuaW6w6ffigzTAAGXpmEz0tIxn1k4Z
x5wN5rpv/qu0QMYz+Av2u1eOKEKZeaFRVpT0r93dX7IvbEZHt25GPiBvlLGqhjKR
PnSk/7U8XmsnttUAV7rVEK1UrdFw8/IwriQC+dhr0mnYfSDMkvBoMFpdhVNTrbAZ
1TB6rQjE7Ar0Mt8my96XJmwrcjK2Tj+E2rgPIUz1e5cekFYIDSBatmw+3+vr+T5x
FNFkJ2o30W5o8ZflCJJzrVaihqQics6ZKDgpf7iqXMFiwWIlhdQpGvx5Gf/KFTK9
UaOnRZz/X+2CebAFaTHR3k/PYppWTgBBBuRvlpCw+wdnkmteC0SQRF91QWVr7ejo
7KaOlGI5VtvMUsWvTeAZmpaymIaATETuOJaY0JU11OmLeD9DOj5E2SQ7qIX/pFcp
xpzG5j4c+MlgvxP2VAkNTeAXCaYiPBQH5ZZg0HE2WnB1KhLRFlHd4iHQD2GJ5yN/
6fCFBfZfKSeK8JauwxgWkra53OcDq/mKd+DA/dK+/ruG7tqwVgIa04HOplzM7LYR
GB0Irs9+lr5/PJbQZmU073Mdn6cXAg3p+6wvwFlDkS5v13gBDYNHtF62bc551edF
Z6kGzJ7wmGRo84aBP7MuRZeReLOrSS67a1wLdzZsMnP1TJ7x9Lfr9MKl2uDnQdnY
ex8DAgMBAAGjUzBRMB0GA1UdDgQWBBSQ/mtZd6h8en9YQVH6HO1PlWWiqzAfBgNV
HSMEGDAWgBSQ/mtZd6h8en9YQVH6HO1PlWWiqzAPBgNVHRMBAf8EBTADAQH/MA0G
CSqGSIb3DQEBCwUAA4ICAQChQYNuah3+mTpIBDYxGrjTJNuOTIMaWzMyi1tkf+L0
sEGwpbmAO2mWWQYF7WVLsi98PULh3adjt2jiud9VlaaC6gnwn5Zo1+Pilo9sNLLW
6ij0+rN4kwIm/pNqi+jDuu2cvAuHIwZWeh8bEe/5UCxo4ihmWFQN8eJ6TUKCphRC
6Eor/SSZZBQHgPl0BchzHOkwu7R3LCndRqxjhAoVb9yQOV+ZsmTeJXulwNzJ1uLt
T8OIgIiDpmBo7HSN2H0k3chx00AsjUyJ9mmAWPejFe/KXLRPcVZR17jhzgfIBEzs
M5WtWFm1aHDjVv6M6iteVm61E9T+k/M11ru1e2YwsxTDvb6x04mcrNu9soqddBbr
VfpluuoQ/hEAbXtFNPoTySpz0cwOwcHCowVOLmdKgvImszZiMyHHG8VGGmPh88n7
wVxb0gV0P4RMrcMLdeTdn55YQr1CqBr34eB6ol6AsbTm3VzBHRVmFNksl1o5JB5t
tXLgF/G8/rzJ/4m1PaVuxrB7DxUmIk8EPbSIVkvZvd7LBzKwQ6IfVaucewHfEajQ
VIiexSMiFc7lw3KnxjOHZjf6FM9VYg3No++GdC99s7LkIuJwAMLNqTQ7Hvhn7YvP
4FlSIgc6xj0YkGZEQlb5o/5nauEqQU0ABgw6jtI4NxrNLT6cp7CO4M0xIDEg/3YD
aA==
-----END CERTIFICATE-----
-----BEGIN PRIVATE KEY-----
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
YkU14WzQKleFkiBJI9lVvnfgGnOlgg0=
-----END PRIVATE KEY-----

2
pytest.ini Normal file
View file

@ -0,0 +1,2 @@
[pytest]
addopts = -v -p no:warnings

View file

@ -1,3 +1,4 @@
pyyaml pyyaml
docker docker
jinja2 jinja2
pytest

20
setup.py Normal file
View file

@ -0,0 +1,20 @@
from setuptools import setup, find_packages
with open('README.md') as f:
readme = f.read()
with open('LICENSE') as f:
license = f.read()
setup(
name='easymapping',
version='0.1.0',
description='HAProxy label based routing',
long_description=readme,
author='',
author_email='',
url='',
license=license,
packages=find_packages(exclude=('tests', 'docs'))
)

View file

@ -1,74 +0,0 @@
import os
import json
import time
import base64
import hashlib
from easymapping import HaproxyConfigGenerator
# path = os.path.dirname(os.path.realpath(__file__))
with open("/tmp/.docker_data", 'r') as content_file:
lineList = content_file.readlines()
result = {
"easymapping": [],
"customerrors": True if os.getenv("HAPROXY_CUSTOMERRORS") == "true" else False
}
easymapping = dict()
if os.getenv("HAPROXY_PASSWORD"):
result["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",
}
for line in lineList:
line = line.strip()
i = line.find("=")
container = line[:i]
jsonStr = line[i+1:]
d = json.loads(jsonStr)
if "com.byjg.easyhaproxy.definitions" in d.keys():
definitions = d["com.byjg.easyhaproxy.definitions"].split(",")
for definition in definitions:
if "com.byjg.easyhaproxy.host." + definition not in d:
continue
port = d["com.byjg.easyhaproxy.port." + definition] if "com.byjg.easyhaproxy.port." + definition in d else "80"
hash = hashlib.md5(d["com.byjg.easyhaproxy.sslcert." + definition].encode('utf-8')).hexdigest() if "com.byjg.easyhaproxy.sslcert." + definition in d else ""
key = port+hash
if key not in easymapping:
easymapping[key] = {
"port": port,
"hosts": dict(),
"redirect": dict(),
# "ssl_cert": ""
}
easymapping[key]["hosts"][d["com.byjg.easyhaproxy.host." + definition]] = container + ":" + (d["com.byjg.easyhaproxy.localport." + definition] if "com.byjg.easyhaproxy.localport." + definition in d else "80")
if "com.byjg.easyhaproxy.sslcert." + definition in d:
filename = '/etc/haproxy/certs/' + d["com.byjg.easyhaproxy.host." + definition] + "." + str(time.time()) + ".pem"
easymapping[key]["ssl_cert"] = filename
with open(filename, 'wb') as file:
file.write(base64.b64decode(d["com.byjg.easyhaproxy.sslcert." + definition]))
if "com.byjg.easyhaproxy.redirect." + definition in d:
redirect = d["com.byjg.easyhaproxy.redirect." + definition] if "com.byjg.easyhaproxy.redirect." + definition in d else ""
for r in redirect.split(","):
r_parts = r.split("--")
easymapping[key]["redirect"][r_parts[0]] = r_parts[1]
result["easymapping"] = easymapping.values()
cfg = HaproxyConfigGenerator(result)
print(cfg.generate())
# print(jsonStr)

8
templates/bind.j2 Normal file
View file

@ -0,0 +1,8 @@
{% if "ssl_cert" in o %}
bind *:{{ o["port"] }} ssl crt {{ o["ssl_cert"] }}
{% elif "h2" in o and o["h2"] %}
bind *:{{ o["port"] }} proto h2
option http-use-htx
{% else %}
bind *:{{ o["port"] }}
{% endif %}

View file

@ -0,0 +1,11 @@
mode http
{% for k in o["redirect"] %}
redirect prefix {{ o["redirect"][k] }} code 301 if { hdr(host) -i {{ k }} }
{% endfor %}
{% for k in o["hosts"] %}
{% set host = k.replace(".", "_") + "_{0}_{1}".format(o["port"], salt) %}
acl is_rule_{{ host }}_1 hdr(host) -i {{ k }}
acl is_rule_{{ host }}_2 hdr(host) -i {{ k }}:{{ o["port"] }}
use_backend srv_{{ host }} if is_rule_{{ host }}_1 OR is_rule_{{ host }}_2
{% endfor %}

View file

@ -0,0 +1,6 @@
mode tcp
option tcplog
log global
{% set backend = (o["hosts"]|first) %}
default_backend srv_{{ backend.replace(".", "_") + "_{0}_{1}".format(o["port"], salt) }}

View file

@ -1,3 +1,8 @@
global
log stdout format raw local0 info
maxconn 2000
tune.ssl.default-dh-param 2048
defaults defaults
log global log global
@ -14,11 +19,6 @@ defaults
errorfile 504 /etc/haproxy/errors-custom/504.http errorfile 504 /etc/haproxy/errors-custom/504.http
{% endif %} {% endif %}
global
log /dev/log local0
maxconn 2000
tune.ssl.default-dh-param 2048
{% if "stats" in data %} {% if "stats" in data %}
frontend stats frontend stats
bind *:{{ data["stats"]["port"] | default(1936) }} bind *:{{ data["stats"]["port"] | default(1936) }}
@ -37,32 +37,31 @@ backend srv_stats
mode http mode http
server Local 127.0.0.1:{{ data["stats"]["port"] | default(1936) }} server Local 127.0.0.1:{{ data["stats"]["port"] | default(1936) }}
{% endif %} {% endif %}
{% for o in data["easymapping"] -%}
{% for o in data["easymapping"] %} {% set mode = o["mode"] or "http" %}
{% set salt = loop.index %} {% set salt = loop.index %}
frontend http_in_{{ o["port"] }}_{{ salt }}
bind *:{{ o["port"] }} {{ " ssl crt " + o["ssl_cert"] if "ssl_cert" in o else "" }}
mode http
{% for k in o["redirect"] -%} frontend {{ mode }}_in_{{ o["port"] }}_{{ salt }}
redirect prefix {{ o["redirect"][k] }} code 301 if { hdr(host) -i {{ k }} } {% include "bind.j2" %}
{% endfor -%} {% if mode == "http" %}
{% include "frontend-mode-http.j2" %}
{% else %}
{% include "frontend-mode-tcp.j2" %}
{% endif %}
{% for k in o["hosts"] %} {% for k in o["hosts"] -%}
{% set host = k.replace(".", "_") + "_{0}_{1}".format(o["port"], salt) %}
acl is_rule_{{ host }}_1 hdr(host) -i {{ k }}
acl is_rule_{{ host }}_2 hdr(host) -i {{ k }}:{{ o["port"] }}
use_backend srv_{{ host }} if is_rule_{{ host }}_1 OR is_rule_{{ host }}_2
{% endfor %}
{% for k in o["hosts"] %}
{% set host = k.replace(".", "_") + "_{0}_{1}".format(o["port"], salt) %} {% set host = k.replace(".", "_") + "_{0}_{1}".format(o["port"], salt) %}
backend srv_{{ host }} backend srv_{{ host }}
balance roundrobin balance roundrobin
mode http mode {{ mode }}
{% if mode == "http" %}
option forwardfor option forwardfor
http-request set-header X-Forwarded-Port %[dst_port] http-request set-header X-Forwarded-Port %[dst_port]
http-request add-header X-Forwarded-Proto https if { ssl_fc } http-request add-header X-Forwarded-Proto https if { ssl_fc }
server srv {{ o["hosts"][k] }} check weight 1 {% elif mode == "tcp" %}
option tcp-check
tcp-check connect{{ " ssl" if o["health-check"] == "ssl" }}
{% endif %}
server srv {{ o["hosts"][k] }} check weight 1{{ " verify none" if o["health-check"] == "ssl" }}
{% endfor %} {% endfor %}
{% endfor %} {% endfor %}

0
tests/__init__.py Normal file
View file

5
tests/context.py Normal file
View file

@ -0,0 +1,5 @@
import os
import sys
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
import easymapping

View file

@ -0,0 +1,12 @@
global
log stdout format raw local0 info
maxconn 2000
tune.ssl.default-dh-param 2048
defaults
log global
timeout connect 3s
timeout client 10s
timeout server 10m

View file

@ -0,0 +1,26 @@
global
log stdout format raw local0 info
maxconn 2000
tune.ssl.default-dh-param 2048
defaults
log global
timeout connect 3s
timeout client 10s
timeout server 10m
frontend tcp_in_31339_1
bind *:31339
mode tcp
option tcplog
log global
default_backend srv_agent_quantum_local_31339_1
backend srv_agent_quantum_local_31339_1
balance roundrobin
mode tcp
option tcp-check
tcp-check connect ssl
server srv test_agent:9001 check weight 1 verify none

View file

@ -0,0 +1,95 @@
global
log stdout format raw local0 info
maxconn 2000
tune.ssl.default-dh-param 2048
defaults
log global
timeout connect 3s
timeout client 10s
timeout server 10m
frontend tcp_in_31339_1
bind *:31339
mode tcp
option tcplog
log global
default_backend srv_agent_quantum_example_org_31339_1
backend srv_agent_quantum_example_org_31339_1
balance roundrobin
mode tcp
option tcp-check
tcp-check connect
server srv my-stack_agent:9001 check weight 1
frontend http_in_31337_2
bind *:31337
mode http
acl is_rule_cadvisor_quantum_example_org_31337_2_1 hdr(host) -i cadvisor.quantum.example.org
acl is_rule_cadvisor_quantum_example_org_31337_2_2 hdr(host) -i cadvisor.quantum.example.org:31337
use_backend srv_cadvisor_quantum_example_org_31337_2 if is_rule_cadvisor_quantum_example_org_31337_2_1 OR is_rule_cadvisor_quantum_example_org_31337_2_2
acl is_rule_node-exporter_quantum_example_org_31337_2_1 hdr(host) -i node-exporter.quantum.example.org
acl is_rule_node-exporter_quantum_example_org_31337_2_2 hdr(host) -i node-exporter.quantum.example.org:31337
use_backend srv_node-exporter_quantum_example_org_31337_2 if is_rule_node-exporter_quantum_example_org_31337_2_1 OR is_rule_node-exporter_quantum_example_org_31337_2_2
backend srv_cadvisor_quantum_example_org_31337_2
balance roundrobin
mode http
option forwardfor
http-request set-header X-Forwarded-Port %[dst_port]
http-request add-header X-Forwarded-Proto https if { ssl_fc }
server srv my-stack_cadvisor:8080 check weight 1
backend srv_node-exporter_quantum_example_org_31337_2
balance roundrobin
mode http
option forwardfor
http-request set-header X-Forwarded-Port %[dst_port]
http-request add-header X-Forwarded-Proto https if { ssl_fc }
server srv my-stack_node-exporter:9100 check weight 1
frontend http_in_80_3
bind *:80
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 }
redirect prefix https://www.somehost.com.br code 301 if { hdr(host) -i www.somehost.com }
redirect prefix https://www.somehost.com.br code 301 if { hdr(host) -i byjg.ca }
redirect prefix https://www.somehost.com.br code 301 if { hdr(host) -i www.byjg.ca }
acl is_rule_www_somehost_com_br_80_3_1 hdr(host) -i www.somehost.com.br
acl is_rule_www_somehost_com_br_80_3_2 hdr(host) -i www.somehost.com.br:80
use_backend srv_www_somehost_com_br_80_3 if is_rule_www_somehost_com_br_80_3_1 OR is_rule_www_somehost_com_br_80_3_2
backend srv_www_somehost_com_br_80_3
balance roundrobin
mode http
option forwardfor
http-request set-header X-Forwarded-Port %[dst_port]
http-request add-header X-Forwarded-Proto https if { ssl_fc }
server srv some-service:80 check weight 1
frontend http_in_443_4
bind *:443 ssl crt /tmp/www.somehost.com.br.1.pem
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 }
redirect prefix https://www.somehost.com.br code 301 if { hdr(host) -i www.somehost.com }
redirect prefix https://www.somehost.com.br code 301 if { hdr(host) -i byjg.ca }
redirect prefix https://www.somehost.com.br code 301 if { hdr(host) -i www.byjg.ca }
acl is_rule_www_somehost_com_br_443_4_1 hdr(host) -i www.somehost.com.br
acl is_rule_www_somehost_com_br_443_4_2 hdr(host) -i www.somehost.com.br:443
use_backend srv_www_somehost_com_br_443_4 if is_rule_www_somehost_com_br_443_4_1 OR is_rule_www_somehost_com_br_443_4_2
backend srv_www_somehost_com_br_443_4
balance roundrobin
mode http
option forwardfor
http-request set-header X-Forwarded-Port %[dst_port]
http-request add-header X-Forwarded-Proto https if { ssl_fc }
server srv some-service:80 check weight 1

95
tests/expected/static.txt Normal file
View file

@ -0,0 +1,95 @@
global
log stdout format raw local0 info
maxconn 2000
tune.ssl.default-dh-param 2048
defaults
log global
timeout connect 3s
timeout client 10s
timeout server 10m
errorfile 400 /etc/haproxy/errors-custom/400.http
errorfile 403 /etc/haproxy/errors-custom/403.http
errorfile 408 /etc/haproxy/errors-custom/408.http
errorfile 500 /etc/haproxy/errors-custom/500.http
errorfile 502 /etc/haproxy/errors-custom/502.http
errorfile 503 /etc/haproxy/errors-custom/503.http
errorfile 504 /etc/haproxy/errors-custom/504.http
frontend stats
bind *:1936
mode http
stats enable
stats hide-version
stats realm Haproxy\ Statistics
stats uri /
stats auth admin:test123
# acl is_proxystats hdr(host) -i some.host.com
# default_backend srv_stats
# use_backend srv_stats if is_proxystats
default_backend srv_stats
backend srv_stats
mode http
server Local 127.0.0.1:1936
frontend http_in_80_1
bind *:80
mode http
redirect prefix http://host1.com.br code 301 if { hdr(host) -i www.host1.com.br }
acl is_rule_host1_com_br_80_1_1 hdr(host) -i host1.com.br
acl is_rule_host1_com_br_80_1_2 hdr(host) -i host1.com.br:80
use_backend srv_host1_com_br_80_1 if is_rule_host1_com_br_80_1_1 OR is_rule_host1_com_br_80_1_2
acl is_rule_host2_com_br_80_1_1 hdr(host) -i host2.com.br
acl is_rule_host2_com_br_80_1_2 hdr(host) -i host2.com.br:80
use_backend srv_host2_com_br_80_1 if is_rule_host2_com_br_80_1_1 OR is_rule_host2_com_br_80_1_2
backend srv_host1_com_br_80_1
balance roundrobin
mode http
option forwardfor
http-request set-header X-Forwarded-Port %[dst_port]
http-request add-header X-Forwarded-Proto https if { ssl_fc }
server srv container:5000 check weight 1
backend srv_host2_com_br_80_1
balance roundrobin
mode http
option forwardfor
http-request set-header X-Forwarded-Port %[dst_port]
http-request add-header X-Forwarded-Proto https if { ssl_fc }
server srv other:3000 check weight 1
frontend http_in_443_2
bind *:443 ssl crt /etc/haproxy/certs/mycert.pem
mode http
acl is_rule_host1_com_br_443_2_1 hdr(host) -i host1.com.br
acl is_rule_host1_com_br_443_2_2 hdr(host) -i host1.com.br:443
use_backend srv_host1_com_br_443_2 if is_rule_host1_com_br_443_2_1 OR is_rule_host1_com_br_443_2_2
backend srv_host1_com_br_443_2
balance roundrobin
mode http
option forwardfor
http-request set-header X-Forwarded-Port %[dst_port]
http-request add-header X-Forwarded-Proto https if { ssl_fc }
server srv container:80 check weight 1
frontend http_in_8080_3
bind *:8080
mode http
acl is_rule_host3_com_br_8080_3_1 hdr(host) -i host3.com.br
acl is_rule_host3_com_br_8080_3_2 hdr(host) -i host3.com.br:8080
use_backend srv_host3_com_br_8080_3 if is_rule_host3_com_br_8080_3_1 OR is_rule_host3_com_br_8080_3_2
backend srv_host3_com_br_8080_3
balance roundrobin
mode http
option forwardfor
http-request set-header X-Forwarded-Port %[dst_port]
http-request add-header X-Forwarded-Proto https if { ssl_fc }
server srv domain:8181 check weight 1

5
tests/fixtures/no-services vendored Normal file
View file

@ -0,0 +1,5 @@
swarm-prom_caddy={"com.docker.stack.image":"stefanprodan/caddy","com.docker.stack.namespace":"swarm-prom"}
swarm-prom_cadvisor={"com.docker.stack.image":"google/cadvisor","com.docker.stack.namespace":"swarm-prom"}
swarm-prom_dockerd-exporter={"com.docker.stack.image":"stefanprodan/caddy","com.docker.stack.namespace":"swarm-prom"}
swarm-prom_unsee={"com.docker.stack.image":"cloudflare/unsee:v0.8.0","com.docker.stack.namespace":"swarm-prom"}
test_proxy={"com.docker.stack.image":"byjg/easy-haproxy","com.docker.stack.namespace":"test"}

6
tests/fixtures/services vendored Normal file
View file

@ -0,0 +1,6 @@
portainer-agent_agent={"com.docker.stack.image":"portainer/agent:1.5.1","com.docker.stack.namespace":"portainer-agent"}
my-stack_agent={"com.byjg.easyhaproxy.definitions":"agent","com.byjg.easyhaproxy.host.agent":"agent.quantum.example.org","com.byjg.easyhaproxy.localport.agent":"9001","com.byjg.easyhaproxy.mode.agent":"tcp","com.byjg.easyhaproxy.port.agent":"31339","com.docker.stack.image":"portainer/agent:1.5.1","com.docker.stack.namespace":"my-stack","com.planetary-quantum":"monitoring"}
my-stack_cadvisor={"com.byjg.easyhaproxy.definitions":"cadvisor","com.byjg.easyhaproxy.host.cadvisor":"cadvisor.quantum.example.org","com.byjg.easyhaproxy.localport.cadvisor":"8080","com.byjg.easyhaproxy.port.cadvisor":"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={"com.byjg.easyhaproxy.definitions":"exp","com.byjg.easyhaproxy.host.exp":"node-exporter.quantum.example.org","com.byjg.easyhaproxy.localport.exp":"9100","com.byjg.easyhaproxy.port.exp":"31337","com.docker.stack.image":"stefanprodan/swarmprom-node-exporter:v0.16.0","com.docker.stack.namespace":"my-stack","com.planetary-quantum":"monitoring"}
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={"com.byjg.easyhaproxy.definitions":"http,https","com.byjg.easyhaproxy.port.http":"80","com.byjg.easyhaproxy.host.http":"www.somehost.com.br","com.byjg.easyhaproxy.localport.http":"80","com.byjg.easyhaproxy.redirect.http":"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","com.byjg.easyhaproxy.port.https":"443","com.byjg.easyhaproxy.host.https":"www.somehost.com.br","com.byjg.easyhaproxy.localport.https":"80","com.byjg.easyhaproxy.redirect.https":"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","com.byjg.easyhaproxy.sslcert.https":"U29tZSBQRU0gQ2VydGlmaWNhdGU="}

2
tests/fixtures/services-tcp vendored Normal file
View file

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

23
tests/fixtures/static.yml vendored Normal file
View file

@ -0,0 +1,23 @@
stats:
username: admin
password: test123
port: 1936 # Optional (default 1936)
customerrors: true # Optional (default false)
easymapping:
- port: 80
hosts:
host1.com.br: container:5000
host2.com.br: other:3000
redirect:
www.host1.com.br: http://host1.com.br
- port: 443
ssl_cert: /etc/haproxy/certs/mycert.pem
hosts:
host1.com.br: container:80
- port: 8080
hosts:
host3.com.br: domain:8181

32
tests/test_labels.py Normal file
View file

@ -0,0 +1,32 @@
from .context import easymapping
import json
import pytest
def test_label_generation():
label = easymapping.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.set_data(json.loads('{"base.definitions":"h2"}'))
label_name = label.create("definitions")
assert label_name == "base.definitions"
assert label.has_label(label_name)
assert label.get(label_name) == "h2"
def test_label_complex_key():
label = easymapping.DockerLabelHandler("till")
data = dict()
data["till.definitions"] = "h2"
data["till.host.h2"] = "fqdn.example.org"
data["till.mode.h2"] = "tcp"
label.set_data(json.loads(json.dumps(data)))
assert label.get(label.create(["host", "h2"])) == "fqdn.example.org"
assert label.get(label.create(["mode", "h2"])) == "tcp"

81
tests/test_parser.py Normal file
View file

@ -0,0 +1,81 @@
from .context import easymapping
import pytest
import os
import yaml
def load_fixture(file):
path = os.path.dirname(os.path.realpath(__file__))
with open(path + "/fixtures/" + file, 'r') as content_file:
lineList = content_file.readlines()
return lineList
def test_parser_doesnt_crash():
lineList = load_fixture("no-services")
result = {
"customerrors": False
}
cfg = easymapping.HaproxyConfigGenerator(result, "/tmp")
haproxy_config = cfg.generate(lineList)
assert len(haproxy_config) > 0
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
def test_parser_finds_services():
lineList = load_fixture("services")
result = {
"customerrors": False
}
cert_file = "/tmp/www.somehost.com.br.1.pem"
if os.path.exists(cert_file):
os.remove(cert_file)
cfg = easymapping.HaproxyConfigGenerator(result, "/tmp")
haproxy_config = cfg.generate(lineList)
assert len(haproxy_config) > 0
path = os.path.dirname(os.path.realpath(__file__))
with open(path + "/expected/services.txt", 'r') as expected_file:
assert expected_file.read() == haproxy_config
with open(cert_file, 'r') as expected_file:
assert expected_file.read() == "Some PEM Certificate"
def test_parser_static():
path = os.path.dirname(os.path.realpath(__file__))
with open(path + "/fixtures/static.yml", 'r') as content_file:
parsed = yaml.load(content_file.read(), Loader=yaml.FullLoader)
cfg = easymapping.HaproxyConfigGenerator(parsed, "/tmp")
haproxy_config = cfg.generate()
assert len(haproxy_config) > 0
with open(path + "/expected/static.txt", 'r') as expected_file:
assert expected_file.read() == haproxy_config
def test_parser_tcp():
lineList = load_fixture("services-tcp")
result = {
"customerrors": False
}
cfg = easymapping.HaproxyConfigGenerator(result, "/tmp")
haproxy_config = cfg.generate(lineList)
# print(haproxy_config)
assert len(haproxy_config) > 0
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