autoconf - basic ingress controller support for kubernetes

This commit is contained in:
bunkerity 2021-08-03 16:39:39 +02:00
parent 021147f9d9
commit 4e178b474c
No known key found for this signature in database
GPG key ID: 3D80806F12602A7C
10 changed files with 227 additions and 80 deletions

View file

@ -149,7 +149,14 @@ class Config :
except : except :
ret = False ret = False
elif self.__type == Controller.Type.KUBERNETES : elif self.__type == Controller.Type.KUBERNETES :
log("config", "ERROR", "TODO get urls for k8s") for instance in instances :
name = instance.metadata.name
try :
dns_result = dns.resolver.query(name + ".default.svc.cluster.local")
for ip in dns_result :
urls.append("http://" + ip.to_text() + ":8080" + self.__api_uri + path)
except :
ret = False
for url in urls : for url in urls :
req = None req = None

View file

@ -1,5 +1,6 @@
from kubernetes import client, config, watch from kubernetes import client, config, watch
from threading import Thread, Lock from threading import Thread, Lock
import time
import Controller import Controller
@ -13,20 +14,35 @@ class IngressController(Controller.Controller) :
self.__api = client.CoreV1Api() self.__api = client.CoreV1Api()
self.__extensions_api = client.ExtensionsV1beta1Api() self.__extensions_api = client.ExtensionsV1beta1Api()
self.__old_env = {} self.__old_env = {}
self.__internal_lock = Lock()
def __get_pods(self) :
return self.__api.list_pod_for_all_namespaces(watch=False, label_selector="bunkerized-nginx").items
def __get_ingresses(self) : def __get_ingresses(self) :
return self.__extensions_api.list_ingress_for_all_namespaces(watch=False).items return self.__extensions_api.list_ingress_for_all_namespaces(watch=False, label_selector="bunkerized-nginx").items
def __get_services(self) : def __get_services(self, autoconf=False) :
return self.__api.list_service_for_all_namespaces(watch=False).items services = self.__api.list_service_for_all_namespaces(watch=False, label_selector="bunkerized-nginx").items
if not autoconf :
return services
services_autoconf = []
for service in services :
if service.metadata.annotations != None and "bunkerized-nginx.AUTOCONF" in service.metadata.annotations :
services_autoconf.append(service)
return services_autoconf
def __annotations_to_env(self, annotations, service=False) : def __pod_to_env(self, pod_env) :
env = {} env = {}
prefix = "" for env_var in pod_env :
if service : env[env_var.name] = env_var.value
if not "bunkerized-nginx.SERVER_NAME" in annotations : if env_var.value == None :
raise Exception("Missing bunkerized-nginx.SERVER_NAME annotation in Service.") env[env_var.name] = ""
prefix = annotations["bunkerized-nginx.SERVER_NAME"].split(" ")[0] + "_" return env
def __annotations_to_env(self, annotations) :
env = {}
prefix = annotations["bunkerized-nginx.SERVER_NAME"].split(" ")[0] + "_"
for annotation in annotations : for annotation in annotations :
if annotation.startswith("bunkerized-nginx.") and annotation.replace("bunkerized-nginx.", "", 1) != "" and annotation.replace("bunkerized-nginx.", "", 1) != "AUTOCONF" : if annotation.startswith("bunkerized-nginx.") and annotation.replace("bunkerized-nginx.", "", 1) != "" and annotation.replace("bunkerized-nginx.", "", 1) != "AUTOCONF" :
env[prefix + annotation.replace("bunkerized-nginx.", "", 1)] = annotations[annotation] env[prefix + annotation.replace("bunkerized-nginx.", "", 1)] = annotations[annotation]
@ -34,64 +50,124 @@ class IngressController(Controller.Controller) :
def __rules_to_env(self, rules) : def __rules_to_env(self, rules) :
env = {} env = {}
first_servers = []
for rule in rules : for rule in rules :
rule = rule.to_dict()
prefix = "" prefix = ""
if "host" in rule : if "host" in rule :
prefix = rule["host"] + "_" prefix = rule["host"] + "_"
first_servers.append(rule["host"])
if not "http" in rule or not "paths" in rule["http"] : if not "http" in rule or not "paths" in rule["http"] :
continue continue
for path in rule["http"]["paths"] : for path in rule["http"]["paths"] :
env[prefix + "USE_REVERSE_PROXY"] = "yes" env[prefix + "USE_REVERSE_PROXY"] = "yes"
env[prefix + "REVERSE_PROXY_URL"] = path["path"] env[prefix + "REVERSE_PROXY_URL"] = path["path"]
env[prefix + "REVERSE_PROXY_HOST"] = "http://" + path["backend"]["serviceName"] + ":" + str(path["backend"]["servicePort"]) env[prefix + "REVERSE_PROXY_HOST"] = "http://" + path["backend"]["service_name"] + ":" + str(path["backend"]["service_port"])
env["SERVER_NAME"] = " ".join(first_servers)
return env return env
def get_env(self) : def get_env(self) :
pods = self.__get_pods()
ingresses = self.__get_ingresses() ingresses = self.__get_ingresses()
services = self.__get_services() services = self.__get_services()
env = {} env = {}
first_servers = []
for pod in pods :
env.update(self.__pod_to_env(pod.spec.containers[0].env))
if "SERVER_NAME" in env and env["SERVER_NAME"] != "" :
first_servers.extend(env["SERVER_NAME"].split(" "))
for ingress in ingresses : for ingress in ingresses :
if ingress.metadata.annotations == None : env.update(self.__rules_to_env(ingress.spec.rules))
continue if "SERVER_NAME" in env and env["SERVER_NAME"] != "" :
if "bunkerized-nginx.AUTOCONF" in ingress.metadata.annotations : first_servers.extend(env["SERVER_NAME"].split(" "))
env.update(self.__annotations_to_env(ingress.metadata.annotations))
env.update(self.__rules_to_env(ingress.spec.rules))
for service in services : for service in services :
if service.metadata.annotations == None : if service.metadata.annotations != None and "bunkerized-nginx.SERVER_NAME" in service.metadata.annotations :
continue env.update(self.__annotations_to_env(service.metadata.annotations))
if "bunkerized-nginx.AUTOCONF" in service.metadata.annotations : first_servers.append(service.metadata.annotations["SERVER_NAME"])
env.update(self.__annotations_to_env(service.metadata.annotations, service=True)) first_servers = list(dict.fromkeys(first_servers))
if len(first_servers) == 0 :
env["SERVER_NAME"] = ""
else :
env["SERVER_NAME"] = " ".join(first_servers)
return self._fix_env(env) return self._fix_env(env)
def process_events(self, current_env) : def process_events(self, current_env) :
self.__old_env = current_env self.__old_env = current_env
t_pod = Thread(target=self.__watch_pod)
t_ingress = Thread(target=self.__watch_ingress) t_ingress = Thread(target=self.__watch_ingress)
t_service = Thread(target=self.__watch_service) t_service = Thread(target=self.__watch_service)
t_pod.start()
t_ingress.start() t_ingress.start()
t_service.start() t_service.start()
t_pod.join()
t_ingress.join() t_ingress.join()
t_service.join() t_service.join()
def __watch_ingress(self) : def __watch_pod(self) :
w = watch.Watch() w = watch.Watch()
for event in w.stream(self.__extensions_api.list_ingress_for_all_namespaces) : for event in w.stream(self.__api.list_pod_for_all_namespaces, label_selector="bunkerized-nginx") :
self.__internal_lock.acquire()
new_env = self.get_env() new_env = self.get_env()
if new_env != self.__old_env() : if new_env != self.__old_env :
if self.gen_conf(new_env, lock=False) : if self.gen_conf(new_env) :
self.__old_env = new_env.copy() self.__old_env = new_env.copy()
log("CONTROLLER", "INFO", "successfully generated new configuration") log("CONTROLLER", "INFO", "successfully generated new configuration")
if self.reload() :
log("controller", "INFO", "successful reload")
else :
log("controller", "ERROR", "failed reload")
self.__internal_lock.release()
def __watch_ingress(self) :
w = watch.Watch()
for event in w.stream(self.__extensions_api.list_ingress_for_all_namespaces, label_selector="bunkerized-nginx") :
self.__internal_lock.acquire()
new_env = self.get_env()
if new_env != self.__old_env :
if self.gen_conf(new_env) :
self.__old_env = new_env.copy()
log("CONTROLLER", "INFO", "successfully generated new configuration")
if self.reload() :
log("controller", "INFO", "successful reload")
else :
log("controller", "ERROR", "failed reload")
self.__internal_lock.release()
def __watch_service(self) : def __watch_service(self) :
w = watch.Watch() w = watch.Watch()
for event in w.stream(self.__api.list_service_for_all_namespaces) : for event in w.stream(self.__api.list_service_for_all_namespaces, label_selector="bunkerized-nginx") :
self.__internal_lock.acquire()
new_env = self.get_env() new_env = self.get_env()
if new_env != self.__old_env() : if new_env != self.__old_env :
if self.gen_conf(new_env, lock=False) : if self.gen_conf(new_env) :
self.__old_env = new_env.copy() self.__old_env = new_env.copy()
log("CONTROLLER", "INFO", "successfully generated new configuration") log("CONTROLLER", "INFO", "successfully generated new configuration")
if self.reload() :
log("controller", "INFO", "successful reload")
else :
log("controller", "ERROR", "failed reload")
self.__internal_lock.release()
def reload(self) : def reload(self) :
return self._reload(self.__get_ingresses()) return self._reload(self.__get_services(autoconf=True))
def wait(self) : def wait(self) :
return self._config.wait(self.__get_ingresses()) # Wait for at least one bunkerized-nginx pod
pods = self.__get_pods()
while len(pods) == 0 :
time.sleep(1)
pods = self.__get_pods()
# Wait for at least one bunkerized-nginx service
services = self.__get_services(autoconf=True)
while len(services) == 0 :
time.sleep(1)
services = self.__get_services(autoconf=True)
# Generate first config
env = self.get_env()
if not self.gen_conf(env) :
return False, env
# Wait for bunkerized-nginx
return self._config.wait(services), env

View file

@ -17,7 +17,7 @@ trap "trap_exit" TERM INT QUIT
# trap SIGHUP # trap SIGHUP
function trap_reload() { function trap_reload() {
log "reload" "INFO" "catched reload operation" log "reload" "INFO" "catched reload operation"
if [ "$SWARM_MODE" != "yes" ] ; then if [ "$SWARM_MODE" != "yes" ] && [ "$KUBERNETES_MODE" != "yes" ] ; then
/opt/bunkerized-nginx/entrypoint/jobs.sh /opt/bunkerized-nginx/entrypoint/jobs.sh
fi fi
if [ -f /tmp/nginx.pid ] ; then if [ -f /tmp/nginx.pid ] ; then
@ -40,10 +40,10 @@ if [ ! -f "/etc/nginx/global.env" ] ; then
log "entrypoint" "INFO" "configuring bunkerized-nginx ..." log "entrypoint" "INFO" "configuring bunkerized-nginx ..."
# check permissions # check permissions
if [ "$SWARM_MODE" != "yes" ] ; then if [ "$SWARM_MODE" != "yes" ] && [ "$KUBERNETES_MODE" != "yes" ] ; then
/opt/bunkerized-nginx/entrypoint/permissions.sh /opt/bunkerized-nginx/entrypoint/permissions.sh
else else
/opt/bunkerized-nginx/entrypoint/permissions-swarm.sh /opt/bunkerized-nginx/entrypoint/permissions-cluster.sh
fi fi
if [ "$?" -ne 0 ] ; then if [ "$?" -ne 0 ] ; then
exit 1 exit 1

View file

@ -4,7 +4,7 @@
. /opt/bunkerized-nginx/entrypoint/utils.sh . /opt/bunkerized-nginx/entrypoint/utils.sh
# start nginx with temp conf for let's encrypt challenges and API # start nginx with temp conf for let's encrypt challenges and API
if [ "$(has_value AUTO_LETS_ENCRYPT yes)" != "" ] || [ "$SWARM_MODE" = "yes" ] || [ "$AUTO_LETS_ENCRYPT" = "yes" ] ; then if [ "$(has_value AUTO_LETS_ENCRYPT yes)" != "" ] || [ "$SWARM_MODE" = "yes" ] || [ "$AUTO_LETS_ENCRYPT" = "yes" ] || [ "$KUBERNETES_MODE" = "yes" ] ; then
cp /opt/bunkerized-nginx/confs/global/nginx-temp.conf /tmp/nginx-temp.conf cp /opt/bunkerized-nginx/confs/global/nginx-temp.conf /tmp/nginx-temp.conf
cp /opt/bunkerized-nginx/confs/global/api-temp.conf /tmp/api.conf cp /opt/bunkerized-nginx/confs/global/api-temp.conf /tmp/api.conf
if [ "$SWARM_MODE" = "yes" ] ; then if [ "$SWARM_MODE" = "yes" ] ; then

View file

@ -1,4 +1,4 @@
import json, re import json, re, sys
class Configurator : class Configurator :

View file

@ -2,11 +2,10 @@ apiVersion: networking.k8s.io/v1
kind: Ingress kind: Ingress
metadata: metadata:
name: bunkerized-nginx-ingress name: bunkerized-nginx-ingress
labels:
bunkerized-nginx: "yes"
annotations: annotations:
# mandatory, keep this annotation
bunkerized-nginx.AUTOCONF: "yes"
# add any global and default environment variables here as annotations with the "bunkerized-nginx." prefix # add any global and default environment variables here as annotations with the "bunkerized-nginx." prefix
# if the scope is "multisite", they will be applied to all services unless overriden by the service
# examples : # examples :
#bunkerized-nginx.AUTO_LETS_ENCRYPT: "yes" #bunkerized-nginx.AUTO_LETS_ENCRYPT: "yes"
#bunkerized-nginx.USE_ANTIBOT: "javascript" #bunkerized-nginx.USE_ANTIBOT: "javascript"
@ -25,7 +24,39 @@ spec:
path: "/" path: "/"
backend: backend:
service: service:
name: app-service name: myapp
port: port:
number: 80 number: 80
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
labels:
app: myapp
spec:
replicas: 1
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: myapp
image: containous/whoami
---
apiVersion: v1
kind: Service
metadata:
name: myapp
spec:
type: ClusterIP
selector:
app: myapp
ports:
- protocol: TCP
port: 80
targetPort: 80

View file

@ -17,51 +17,56 @@ spec:
serviceAccountName: bunkerized-nginx-ingress-controller serviceAccountName: bunkerized-nginx-ingress-controller
containers: containers:
- name: bunkerized-nginx-autoconf - name: bunkerized-nginx-autoconf
image: bunkerity/bunkerized-nginx-autoconf:1.3.0 image: bunkerity/bunkerized-nginx-autoconf:testing
imagePullPolicy: Always
env: env:
- name: KUBERNETES_MODE - name: KUBERNETES_MODE
value: "yes" value: "yes"
- name: API_URI - name: API_URI
value: "/ChangeMeToSomethingHardToGuess" value: "/ChangeMeToSomethingHardToGuess"
- name: SERVER_NAME
value: ""
- name: MULTISITE
value: "yes"
volumeMounts: volumeMounts:
- name: config - name: confs
mountPath: /etc/nginx mountPath: /etc/nginx
- name: certs - name: letsencrypt
mountPath: /etc/letsencrypt mountPath: /etc/letsencrypt
- name: challenges - name: acme-challenge
mountPath: /acme-challenge mountPath: /acme-challenge
- name: cache - name: cache
mountPath: /cache mountPath: /cache
- name: custom-modsec - name: modsec-confs
mountPath: /modsec-confs mountPath: /modsec-confs
readOnly: true readOnly: true
- name: custom-modsec-crs - name: modsec-crs-confs
mountPath: /modsec-crs-confs mountPath: /modsec-crs-confs
readOnly: true readOnly: true
volumes: volumes:
- name: config - name: confs
hostPath: hostPath:
path: /shared/config path: /shared/confs
type: Directory type: Directory
- name: certs - name: letsencrypt
hostPath: hostPath:
path: /shared/certs path: /shared/letsencrypt
type: Directory type: Directory
- name: challenges - name: acme-challenge
hostPath: hostPath:
path: /shared/challenges path: /shared/acme-challenge
type: Directory type: Directory
- name: cache - name: cache
hostPath: hostPath:
path: /shared/cache path: /shared/cache
type: Directory type: Directory
- name: custom-modsec - name: modsec-confs
hostPath: hostPath:
path: /shared/custom-modsec path: /shared/modsec-confs
type: Directory type: Directory
- name: custom-modsec-crs - name: modsec-crs-confs
hostPath: hostPath:
path: /shared/custom-modsec-crs path: /shared/modsec-crs-confs
type: Directory type: Directory
--- ---
apiVersion: apps/v1 apiVersion: apps/v1
@ -78,12 +83,14 @@ spec:
metadata: metadata:
labels: labels:
name: bunkerized-nginx name: bunkerized-nginx
bunkerized-nginx: "yes"
spec: spec:
hostNetwork: true hostNetwork: true
dnsPolicy: ClusterFirstWithHostNet dnsPolicy: ClusterFirstWithHostNet
containers: containers:
- name: bunkerized-nginx - name: bunkerized-nginx
image: bunkerity/bunkerized-nginx:1.3.0 image: bunkerity/bunkerized-nginx:testing
imagePullPolicy: Always
env: env:
- name: KUBERNETES_MODE - name: KUBERNETES_MODE
value: "yes" value: "yes"
@ -91,61 +98,78 @@ spec:
value: "yes" value: "yes"
- name: API_URI - name: API_URI
value: "/ChangeMeToSomethingHardToGuess" value: "/ChangeMeToSomethingHardToGuess"
- name: SERVER_NAME
value: ""
- name: MULTISITE
value: "yes"
volumeMounts: volumeMounts:
- name: config - name: confs
mountPath: /etc/nginx mountPath: /etc/nginx
readOnly: true readOnly: true
- name: certs - name: letsencrypt
mountPath: /etc/letsencrypt mountPath: /etc/letsencrypt
readOnly: true readOnly: true
- name: challenges - name: acme-challenge
mountPath: /acme-challenge mountPath: /acme-challenge
readOnly: true readOnly: true
- name: www - name: www
mountPath: /www mountPath: /www
readOnly: true readOnly: true
- name: custom-http - name: http-confs
mountPath: /http-confs mountPath: /http-confs
readOnly: true readOnly: true
- name: custom-server - name: server-confs
mountPath: /server-confs mountPath: /server-confs
readOnly: true readOnly: true
- name: custom-modsec - name: modsec-confs
mountPath: /modsec-confs mountPath: /modsec-confs
readOnly: true readOnly: true
- name: custom-modsec-crs - name: modsec-crs-confs
mountPath: /modsec-crs-confs mountPath: /modsec-crs-confs
readOnly: true readOnly: true
volumes: volumes:
- name: config - name: confs
hostPath: hostPath:
path: /shared/config path: /shared/confs
type: Directory type: Directory
- name: certs - name: letsencrypt
hostPath: hostPath:
path: /shared/certs path: /shared/letsencrypt
type: Directory type: Directory
- name: challenges - name: acme-challenge
hostPath: hostPath:
path: /shared/challenges path: /shared/acme-challenge
type: Directory type: Directory
- name: www - name: www
hostPath: hostPath:
path: /shared/www path: /shared/www
type: Directory type: Directory
- name: custom-http - name: http-confs
hostPath: hostPath:
path: /shared/custom-http path: /shared/http-confs
type: Directory type: Directory
- name: custom-server - name: server-confs
hostPath: hostPath:
path: /shared/custom-server path: /shared/server-confs
type: Directory type: Directory
- name: custom-modsec - name: modsec-confs
hostPath: hostPath:
path: /shared/custom-modsec path: /shared/modsec-confs
type: Directory type: Directory
- name: custom-modsec-crs - name: modsec-crs-confs
hostPath: hostPath:
path: /shared/custom-modsec-crs path: /shared/modsec-crs-confs
type: Directory type: Directory
---
apiVersion: v1
kind: Service
metadata:
name: bunkerized-nginx-service
labels:
bunkerized-nginx: "yes"
annotations:
bunkerized-nginx.AUTOCONF: "yes"
spec:
clusterIP: None
selector:
name: bunkerized-nginx

View file

@ -4,7 +4,7 @@ metadata:
name: bunkerized-nginx-ingress-controller name: bunkerized-nginx-ingress-controller
rules: rules:
- apiGroups: [""] - apiGroups: [""]
resources: ["services"] resources: ["services", "pods"]
verbs: ["get", "watch", "list"] verbs: ["get", "watch", "list"]
- apiGroups: ["extensions"] - apiGroups: ["extensions"]
resources: ["ingresses"] resources: ["ingresses"]
@ -27,4 +27,4 @@ subjects:
roleRef: roleRef:
kind: ClusterRole kind: ClusterRole
name: bunkerized-nginx-ingress-controller name: bunkerized-nginx-ingress-controller
apiGroup: rbac.authorization.k8s.io apiGroup: rbac.authorization.k8s.io

View file

@ -1201,6 +1201,15 @@
"regex": "^(yes|no)$", "regex": "^(yes|no)$",
"type": "checkbox" "type": "checkbox"
}, },
{
"context": "global",
"default": "no",
"env": "KUBERNETES_MODE",
"id": "kubernetes-mode",
"label": "Kubernetes mode",
"regex": "^(yes|no)$",
"type": "checkbox"
},
{ {
"context": "global", "context": "global",
"default": "no", "default": "no",