diff --git a/README.md b/README.md index 7cbb7c3..ec14cbf 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ The environment variables will setup the HAProxy. | Environment Variable | Description | |-------------------------------|---------------------------------------------------------------------------------------------------------------| -| EASYHAPROXY_DISCOVER | How `haproxy.cfg` will be created: `static`, `docker` or `swarm` | +| EASYHAPROXY_DISCOVER | How `haproxy.cfg` will be created: `static`, `docker`, `swarm` or `kubernetes` | | EASYHAPROXY_LABEL_PREFIX | (Optional) The key will search for matching resources. Default: `easyhaproxy`. | | EASYHAPROXY_LETSENCRYPT_EMAIL | (Optional) The email will be used to request the certificate to Letsencrypt | | 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). | @@ -100,7 +100,48 @@ The discovery will occur every minute. Important: easyhaproxy needs to be in the same network of the containers or otherwise will not access. -### Docker Container (Swarm or Docker) tags: +### EASYHAPROXY_DISCOVER: kubernetes (experimental and limited) + +This will query all `ingress` in the kubernetes cluster and check the annotation `kubernetes.io/ingress.class: easyhaproxy-ingress`. + +e.g.: +``` +kind: Ingress +metadata: + annotations: + kubernetes.io/ingress.class: easyhaproxy-ingress + name: example-ingress + namespace: example +spec: + rules: + - host: example.org + http: + paths: + - backend: + service: + name: example-service + port: + number: 8080 + pathType: ImplementationSpecific + - host: www.example.org + http: + paths: + - backend: + service: + name: example-service + port: + number: 8080 + pathType: ImplementationSpecific +``` + +At this point the implementation is very limited and doesn't support all ingress properties nor wildcard domains. + +The system will read only `host` and `port.number` + +There is no necessary to add labels or annotations. + + +### Container (Docker or Swarm) labels: | Tag | Description | Example | |---------------------------------------|---------------------------------------------------------------------------------------------------------|--------------| diff --git a/assets/scripts/haproxy-reload.sh b/assets/scripts/haproxy-reload.sh index 20fbad8..7cd3822 100755 --- a/assets/scripts/haproxy-reload.sh +++ b/assets/scripts/haproxy-reload.sh @@ -6,11 +6,6 @@ cd /scripts RELOAD="true" -if [[ "static|docker|swarm" != *"$EASYHAPROXY_DISCOVER"* ]];then - log "error" "CONF_CHECK" "EASYHAPROXY_DISCOVER should be 'static', 'docker', or 'swarm'. I got '$EASYHAPROXY_DISCOVER' instead." - exit 1 -fi - if [[ "$EASYHAPROXY_DISCOVER" == "static" ]]; then CONTROL_FILE="/etc/haproxy/haproxy.cfg" touch ${CONTROL_FILE} @@ -22,27 +17,40 @@ else mv ${CONTROL_FILE} ${CONTROL_FILE}.old touch ${CONTROL_FILE} - if [[ "$EASYHAPROXY_DISCOVER" == "docker" ]]; then - CONTAINERS=$(docker ps -q | sort | uniq) - LABEL_PATH=".Config.Labels" + case "$EASYHAPROXY_DISCOVER" in + docker) + CONTAINERS=$(docker ps -q | sort | uniq) + LABEL_PATH=".Config.Labels" - for container in ${CONTAINERS}; do - docker inspect --format "{{ json $LABEL_PATH }}" ${container} | xargs -I % echo ${container}=% >> ${CONTROL_FILE} - done - else - CONTAINERS=$(docker node ps $(docker node ls -q) --format "{{ .Name }}" --filter desired-state=running | cut -d. -f1 | sort | uniq) - LABEL_PATH=".Spec.Labels" + for container in ${CONTAINERS}; do + docker inspect --format "{{ json $LABEL_PATH }}" ${container} | xargs -I % echo ${container}=% >> ${CONTROL_FILE} + done + ;; - for container in ${CONTAINERS}; do - docker service inspect --format "{{ json $LABEL_PATH }}" ${container} | xargs -I % echo ${container}=% >> ${CONTROL_FILE} - done - fi + swarm) + CONTAINERS=$(docker node ps $(docker node ls -q) --format "{{ .Name }}" --filter desired-state=running | cut -d. -f1 | sort | uniq) + LABEL_PATH=".Spec.Labels" + + for container in ${CONTAINERS}; do + docker service inspect --format "{{ json $LABEL_PATH }}" ${container} | xargs -I % echo ${container}=% >> ${CONTROL_FILE} + done + ;; + + kubernetes) + python3 /scripts/k8s.py > ${CONTROL_FILE} + ;; + + *) + log "error" "CONF_CHECK" "EASYHAPROXY_DISCOVER should be 'static', 'docker', 'swarm' or kubernetes. I got '$EASYHAPROXY_DISCOVER' instead." + exit 1 + ;; + esac if cmp -s ${CONTROL_FILE} ${CONTROL_FILE}.old ; then RELOAD="false" else python3 swarm.py > /etc/haproxy/haproxy.cfg - log "info" "CONF_CHECK" "New configuration found" + log "info" "CONF_CHECK" "New configuration found" fi fi diff --git a/assets/scripts/k8s.py b/assets/scripts/k8s.py new file mode 100644 index 0000000..8ebc19e --- /dev/null +++ b/assets/scripts/k8s.py @@ -0,0 +1,50 @@ +from kubernetes import client, config +from kubernetes.client.rest import ApiException +import json + +# https://github.com/kubernetes-client/python/tree/master/kubernetes/docs + +def main(): + config.load_incluster_config() + + api_instance = client.CoreV1Api() + v1 = client.NetworkingV1Api() + + ret = v1.list_ingress_for_all_namespaces(watch=False) + + discover = {} + for i in ret.items: + if i.metadata.annotations['kubernetes.io/ingress.class'] != "easyhaproxy-ingress": + continue + + data = {} + #ingress_name = i.metadata.name + data["creation_timestamp"] = i.metadata.creation_timestamp.strftime("%x %X") + data["resource_version"] = i.metadata.resource_version + data["namespace"] = i.metadata.namespace + for rule in i.spec.rules: + rule_data = {} + port_number = rule.http.paths[0].backend.service.port.number + definition = rule.host.replace(".", "-") + rule_data["easyhaproxy.%s_%s.host" % (definition, port_number)] = rule.host + rule_data["easyhaproxy.%s_%s.port" % (definition, port_number)] = "80" + rule_data["easyhaproxy.%s_%s.localport" % (definition, port_number)] = port_number + service_name = rule.http.paths[0].backend.service.name + try: + api_response = api_instance.read_namespaced_service(service_name, i.metadata.namespace) + cluster_ip = api_response.spec.cluster_ip + except ApiException as e: + cluster_ip = None + # print("Exception when calling CoreV1Api->read_namespaced_service: %s\n" % e) + + if cluster_ip is not None: + if cluster_ip not in discover.keys(): + discover[cluster_ip] = data + discover[cluster_ip].update(rule_data) + + for k in discover.keys(): + print("%s=%s" % (k, json.dumps(discover[k]))) + +if __name__ == '__main__': + main() + diff --git a/kubernetes/easyhaproxy.yml b/kubernetes/easyhaproxy.yml new file mode 100644 index 0000000..c3b3b5e --- /dev/null +++ b/kubernetes/easyhaproxy.yml @@ -0,0 +1,61 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: easyhaproxy + +--- +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: easyhaproxy-ingress + namespace: easyhaproxy +spec: + selector: + matchLabels: + app: easyhaproxy-ingress + template: + metadata: + labels: + app: easyhaproxy-ingress + spec: + serviceAccountName: easyhaproxy-ingress + containers: + - image: byjg/easy-haproxy:kubernetes + imagePullPolicy: IfNotPresent + name: easyhaproxy-ingress + ports: + - name: http + containerPort: 80 + hostPort: 80 + - name: https + containerPort: 443 + hostPort: 443 + - name: stats + containerPort: 1943 + hostPort: 1943 + resources: + requests: + cpu: "100m" + memory: "128Mi" + #limits: + # cpu: "1" + # memory: "1Gi" + securityContext: + allowPrivilegeEscalation: true + #runAsUser: 101 #nginx + capabilities: + drop: + - ALL + add: + - NET_BIND_SERVICE + env: + - name: EASYHAPROXY_DISCOVER + value: kubernetes + - name: HAPROXY_USERNAME + value: admin + - name: HAPROXY_PASSWORD + value: password + - name: EASYHAPROXY_REFRESH_CONF + value: 10 + diff --git a/requirements.txt b/requirements.txt index 364b4b7..7959d6f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ pyyaml docker jinja2 -pytest \ No newline at end of file +pytest +kubernetes \ No newline at end of file