├── .gitignore ├── DEV_GUIDE.md ├── Dockerfile ├── QUICKSTART.md ├── README.md ├── bin └── start.sh ├── event-controller-template.yml └── src ├── IPAClient └── __init__.py ├── OpenShiftWatcher └── __init__.py ├── conf └── config.ini.sample ├── config └── __init__.py ├── constants └── __init__.py ├── errors └── __init__.py ├── plugin_dns ├── README.md └── __init__.py ├── plugin_ipa ├── README.md └── __init__.py ├── plugin_simple └── __init__.py └── watch.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | kubernetes.io 3 | -------------------------------------------------------------------------------- /DEV_GUIDE.md: -------------------------------------------------------------------------------- 1 | # Event Controller Dev Guide 2 | 3 | The OpenShift Event Controller is a utility used as a service integrator for OpenShift and other third party components 4 | 5 | ## Setup Dev Environment 6 | 7 | Before we can test locally, we need to install some dependencies and gather some information about our Cluster. 8 | 9 | ``` 10 | yum -y install libffi-devel 11 | pip3 install requests pkiutils pyopenssl 12 | ``` 13 | 14 | Now, create a directory in which to store local configs. The directory name below coincides with the directory that would be mounted into a pod. 15 | 16 | ``` 17 | mkdir -p ./kubernetes.io/serviceaccount 18 | oc login # Interactive step 19 | oc whoami -t > kubernetes.io/serviceaccount/token 20 | # Place OpenShift CA file at kubernetes.io/serviceaccount/ca.crt 21 | echo QUIT | openssl s_client -showcerts -connect :8443 2>&1 | openssl x509 -text | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > kubernetes.io/serviceaccount/ca.crt 22 | ``` 23 | 24 | ## Testing Source Locally 25 | 26 | 27 | ``` 28 | K8S_TOKEN=`oc whoami -t` K8S_API_ENDPOINT='master.example.com:8443' K8S_NAMESPACE=event-controller K8S_CA=./kubernetes.io/serviceaccount/ca.crt K8S_RESOURCE=routes python3 watch.py 29 | ``` 30 | 31 | You should see a log of data about the Namespace you passed in. 32 | 33 | ## Testing The Image Locally 34 | 35 | ``` 36 | docker run -v /home/esauer/src/oc-watcher-skel/kubernetes.io/:/etc/config:z -v /home/esauer/src/oc-watcher-skel/kubernetes.io/serviceaccount:/var/run/secrets/kubernetes.io/serviceaccount:z -e CONFIG_FILE=/etc/config/config.ini event-controller 37 | ``` 38 | 39 | If you want to debug the running image itself: 40 | 41 | ``` 42 | docker run -it --entrypoint=/bin/bash -v /path/to/kubernetes.io/conf:/etc/watcher/:z -v /path/tokubernetes.io/serviceaccount:/var/run/secrets/kubernetes.io/serviceaccount:z -e CONFIG_FILE=/etc/watcher/config.ini event-controller 43 | ``` 44 | 45 | ## Configuration 46 | 47 | The event controller can be configured via either Environment variables or an ini file. A sample config file can be found at `conf/config.ini.sample`. To run the watcher with a config file, run: 48 | 49 | `python3 watch.py --config conf/config.ini` 50 | 51 | ## Plugin Architecture 52 | 53 | The event controller is designed to be pluggable. New plugins can be created by simply creating a python module that implements a single `handle_event()` method, which takes a single `dict` object as an argument (the `event` object). 54 | 55 | A plugin is invoked like so: 56 | 57 | ```python 58 | plugin = load_plugin(plugin_name) 59 | for k8s_event in getEvents(): 60 | result,level = plugin.handle_event(self, k8s_event, self.config.getPluginConfig(), *args, **kwargs) 61 | log(result, level) 62 | ``` 63 | 64 | Let's look at a very simple plugin, called `plugin_simple`. This plugin is a single file init file loaded from a directory, `plugin_simple/__init__.py`: 65 | 66 | ```python 67 | def handle_event(watcher, event, config): 68 | message = "Kind: {0}; Name: {1}".format(event['object']['kind'], event['object']['metadata']['name']) 69 | log_level = "INFO" 70 | return message, log_level 71 | ``` 72 | 73 | This plugin takes in the event object generated by the Kubernetes API, grabs a few choice fields, and creates a log message which is returned to the watcher. 74 | 75 | From here, we can easily write plugins for the watcher that do things like: 76 | 77 | - Create Alerts 78 | - Integrate with Third party systems like DNS or PKI infrastructures 79 | 80 | Happy Integrating! 81 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM centos/python-35-centos7 2 | 3 | USER root 4 | 5 | ENV PATH=/opt/app-root/bin:/opt/rh/rh-python35/root/usr/bin:/opt/app-root/src/.local/bin/:/opt/app-root/src/bin:/opt/app-root/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \ 6 | LD_LIBRARY_PATH=/opt/rh/rh-python35/root/usr/lib64 7 | 8 | RUN yum -y install libffi-devel; \ 9 | pip install --upgrade pip; \ 10 | pip install requests pkiutils pyopenssl; \ 11 | yum clean all; 12 | 13 | RUN mkdir -p /opt/event-controller 14 | 15 | COPY ./src/ /opt/event-controller/ 16 | 17 | COPY ./bin/ /opt/event-controller/bin 18 | 19 | RUN chown -R 1001:1001 /opt/event-controller 20 | 21 | USER 1001 22 | 23 | ENTRYPOINT ["/opt/event-controller/bin/start.sh"] 24 | -------------------------------------------------------------------------------- /QUICKSTART.md: -------------------------------------------------------------------------------- 1 | # Event Controller Quickstart Tutorial 2 | 3 | Let's walk through a simple example of how the Event Controller works. 4 | 5 | ``` 6 | oc new-project event-controller 7 | oc create -f event-controller-template.yml 8 | oc new-app --template=event-controller 9 | ``` 10 | 11 | The Event Controller is now deployed in a project called `event-controller`. Let's look at what was created: 12 | 13 | ``` 14 | $ oc get all | grep event-controller 15 | bc/event-controller Docker Git@containerize 2 16 | builds/event-controller-1 Docker Git@86e5d9b Complete About an hour ago 2m1s 17 | is/event-controller 172.30.226.234:5000/event-controller/event-controller latest 41 minutes ago 18 | is/python-35-centos7 172.30.226.234:5000/event-controller/python-35-centos7 latest About an hour ago 19 | dc/event-controller 3 1 1 config,image(event-controller:latest) 20 | rc/event-controller-1 0 0 1h 21 | po/event-controller-1-tkkk3 1/1 Running 0 32m 22 | ``` 23 | 24 | That covers most of it, but we also created a `ConfigMap`. Let's look at that really quick. 25 | 26 | ``` 27 | $ oc get configmap event-controller --template={{.data}} 28 | map[config.ini: 29 | [global] 30 | k8s_resource=routes 31 | watcher_plugin=simple 32 | 33 | [plugin_simple] 34 | ] 35 | ``` 36 | 37 | From this config we can see that the Event Controller is watching `Route` resources and has a `simple` plugin enabled. 38 | 39 | Now let's put it into action! First, open a new tab and start watching the pod logs. 40 | 41 | ``` 42 | $ oc get pods 43 | NAME READY STATUS RESTARTS AGE 44 | event-controller-1-build 0/1 Completed 0 1h 45 | event-controller-3-tkkk3 1/1 Running 0 29m <-- Make sure to pick the one that's running 46 | $ oc logs -f event-controller-3-tkkk3 47 | 2017-03-17 04:42:51,576 [INFO] Loading config file from /etc/config/config.ini 48 | ``` 49 | There. We can see that the watcher has started and is waiting for new events. 50 | 51 | ``` 52 | oc new-app https://github.com/openshift/nodejs-ex.git 53 | ``` 54 | 55 | Notice no new logs yet. That's because a route has not been created yet. So let's do that. 56 | 57 | ``` 58 | $ oc expose svc/nodejs-ex 59 | route "nodejs-ex" exposed 60 | ``` 61 | 62 | Check out the logs now! 63 | ``` 64 | 2017-03-17 05:24:38,339 [INFO] Kind: Route; Name: nodejs-ex; Event Type:ADDED 65 | ``` 66 | 67 | Cool, so at a very basic level, with the `simple` plugin enabled, the watcher will detect when a route gets created, and log it. 68 | 69 | Think about the other ways we could use that information... 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenShift Event Controller 2 | 3 | The OpenShift Event Controller is a utility used as a service integrator for OpenShift and other third party components. 4 | 5 | ## Getting Started 6 | 7 | To skip right to a first deployment, check out our [Quickstart Tutorial](./QUICKSTART.md) 8 | 9 | ## Plugins 10 | 11 | We currently support the following plugins: 12 | 13 | * [Simple Plugin](./src/plugin_simple) 14 | * Watches for new resources and logs those events to the console 15 | * [DNS Plugin](./src/plugin_dns) 16 | * Creates DNS records for new routes 17 | * [Certificates Plugin](./src/plugin_ipa) 18 | * Creates certificates and automatically secures new routes as they get created. Works against an IPA or IDM server 19 | 20 | ## Configuration 21 | 22 | The event controller can be configured either through Environment Variables or a Config Files. We recommend the config file. 23 | 24 | A sample config file looks like: 25 | 26 | ``` 27 | [global] 28 | k8s_resource=routes 29 | watcher_plugin=simple 30 | log_level=INFO 31 | 32 | [plugin_simple] 33 | #message_log_level=WARNING 34 | 35 | [plugin_ipa] 36 | need_cert_annotation=openshift.io/managed.cert 37 | ipa_user=ldap-user 38 | ipa_password=mypassword 39 | ipa_url=https://idm.example.com/ipa/ 40 | ipa_realm=MYREALM.EXAMPLE.COM 41 | ca_trust=/etc/ldap-ca/ca.crt 42 | 43 | [plugin_dns] 44 | application_router_ip=192.168.2.3 45 | dns_server=192.168.5.6 46 | dns_key_file=/path/to/cloudapps.example.com.key 47 | resolv_conf=/path/to/tmp_resolv.conf 48 | ``` 49 | 50 | ### Global Config Options 51 | 52 | | Environment Variable | ini Variable | Required | Description | 53 | | ------------- | ------------- | -------| --------- | 54 | | K8S_API_ENDPOINT | k8s_api_endpoint | True | OpenShift/Kubernetes API hostname:port | 55 | | K8S_TOKEN | k8s_token | True; will be pulled from Pod | Login token (`oc whoami -t`) | 56 | | K8S_NAMESPACED | k8s_namespaced | True | Whether the resource is namespace scoped | 57 | | K8S_NAMESPACE | k8s_namespace | When `K8S_NAMESPACED` is `True`; will be pulled from Pod | Namespace you want to listen watch resources in | 58 | | K8S_API_PATH | k8s_api_path | False | The full API resource path. Override API path construction based on other values | 59 | | K8S_API_GROUP | k8s_api_group | False | Kubernetes API group | 60 | | K8S_API_VERSION | k8s_api_version | False | Kubernetes API Version | 61 | | K8S_RESOURCE | k8s_resource | True | The `Kind` of the Kubernetes or OpenShift resource | 62 | | K8S_CA | k8s_ca | False; will be pulled from Pod | Path to the `ca.crt` file for the cluster | 63 | | LOG_LEVEL | log_level | False | Logging threshold to be output. Options: DEBUG, INFO, WARNING, ERROR, CRITICAL; Default: INFO 64 | | WATCHER_PLUGIN | watcher_plugin | False | Name of the Plugin you want to run in the Watcher. Default: 'simple' | 65 | 66 | ### Configuring A Plugin 67 | 68 | Check the documentation for the individual plugins for more details on how they are configured. 69 | -------------------------------------------------------------------------------- /bin/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ -z ${CONFIG_FILE+x} ]; then 4 | python /opt/event-controller/watch.py 5 | else 6 | python /opt/event-controller/watch.py --config ${CONFIG_FILE} 7 | fi 8 | -------------------------------------------------------------------------------- /event-controller-template.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Template 3 | labels: 4 | template: event-controller 5 | metadata: 6 | annotations: 7 | name: event-controller 8 | objects: 9 | - apiVersion: v1 10 | kind: ImageStream 11 | metadata: 12 | labels: 13 | template: ${APPLICATION_NAME} 14 | name: python-35-centos7 15 | spec: 16 | tags: 17 | - annotations: 18 | openshift.io/imported-from: centos/python-35-centos7 19 | from: 20 | kind: DockerImage 21 | name: centos/python-35-centos7 22 | generation: 2 23 | importPolicy: {} 24 | name: latest 25 | - apiVersion: v1 26 | kind: ImageStream 27 | metadata: 28 | labels: 29 | template: ${APPLICATION_NAME} 30 | name: ${APPLICATION_NAME} 31 | - apiVersion: v1 32 | kind: BuildConfig 33 | metadata: 34 | annotations: 35 | labels: 36 | template: ${APPLICATION_NAME} 37 | name: ${APPLICATION_NAME} 38 | spec: 39 | output: 40 | to: 41 | kind: ImageStreamTag 42 | name: ${APPLICATION_NAME}:latest 43 | postCommit: {} 44 | resources: {} 45 | runPolicy: Serial 46 | source: 47 | type: Git 48 | git: 49 | uri: ${SOURCE_CODE_URL} 50 | ref: ${SOURCE_CODE_REF} 51 | contextDir: ${SOURCE_CODE_CONTEXT_DIR} 52 | strategy: 53 | dockerStrategy: 54 | from: 55 | kind: ImageStreamTag 56 | name: python-35-centos7:latest 57 | type: Docker 58 | triggers: 59 | - github: 60 | secret: A_yyouzqlQq97DLR9MEn 61 | type: GitHub 62 | - generic: 63 | secret: gJXJoTgAMPBPbofHusDR 64 | type: Generic 65 | - type: ConfigChange 66 | - apiVersion: v1 67 | kind: DeploymentConfig 68 | metadata: 69 | labels: 70 | template: ${APPLICATION_NAME} 71 | name: ${APPLICATION_NAME} 72 | spec: 73 | replicas: 1 74 | selector: 75 | app: ${APPLICATION_NAME} 76 | deploymentconfig: ${APPLICATION_NAME} 77 | strategy: 78 | resources: {} 79 | rollingParams: 80 | intervalSeconds: 1 81 | maxSurge: 25% 82 | maxUnavailable: 25% 83 | timeoutSeconds: 600 84 | updatePeriodSeconds: 1 85 | type: Rolling 86 | template: 87 | metadata: 88 | annotations: 89 | labels: 90 | app: ${APPLICATION_NAME} 91 | deploymentconfig: ${APPLICATION_NAME} 92 | spec: 93 | volumes: 94 | - name: config-volume 95 | configMap: 96 | name: ${APPLICATION_NAME} 97 | containers: 98 | - env: 99 | - name: CONFIG_FILE 100 | value: /etc/config/config.ini 101 | image: ${APPLICATION_NAME} 102 | imagePullPolicy: Always 103 | name: ${APPLICATION_NAME} 104 | ports: 105 | - containerPort: 8080 106 | protocol: TCP 107 | resources: {} 108 | terminationMessagePath: /dev/termination-log 109 | volumeMounts: 110 | - name: config-volume 111 | mountPath: /etc/config 112 | dnsPolicy: ClusterFirst 113 | restartPolicy: Always 114 | serviceAccountName: ${APPLICATION_NAME} 115 | securityContext: {} 116 | terminationGracePeriodSeconds: 30 117 | test: false 118 | triggers: 119 | - type: ConfigChange 120 | - imageChangeParams: 121 | automatic: true 122 | containerNames: 123 | - ${APPLICATION_NAME} 124 | from: 125 | kind: ImageStreamTag 126 | name: ${APPLICATION_NAME}:latest 127 | type: ImageChange 128 | - apiVersion: v1 129 | data: 130 | config.ini: | 131 | [global] 132 | k8s_resource=routes 133 | watcher_plugin=simple 134 | 135 | [plugin_simple] 136 | kind: ConfigMap 137 | metadata: 138 | name: ${APPLICATION_NAME} 139 | labels: 140 | template: ${APPLICATION_NAME} 141 | - apiVersion: v1 142 | kind: ServiceAccount 143 | metadata: 144 | labels: 145 | template: ${APPLICATION_NAME} 146 | name: ${APPLICATION_NAME} 147 | - apiVersion: v1 148 | groupNames: null 149 | kind: RoleBinding 150 | metadata: 151 | labels: 152 | template: ${APPLICATION_NAME} 153 | name: ${APPLICATION_NAME}_edit 154 | roleRef: 155 | name: edit 156 | subjects: 157 | - kind: ServiceAccount 158 | name: ${APPLICATION_NAME} 159 | parameters: 160 | - description: The name for the application. 161 | name: APPLICATION_NAME 162 | required: true 163 | value: event-controller 164 | - description: Source code repo URL 165 | name: SOURCE_CODE_URL 166 | required: true 167 | value: https://github.com/redhat-cop/openshift-event-controller.git 168 | - description: Source code branch 169 | name: SOURCE_CODE_REF 170 | required: true 171 | value: master 172 | - description: Directory in your source code repo 173 | name: SOURCE_CODE_CONTEXT_DIR 174 | required: false 175 | -------------------------------------------------------------------------------- /src/IPAClient/__init__.py: -------------------------------------------------------------------------------- 1 | import json 2 | import requests 3 | import pkiutils 4 | from OpenSSL import crypto 5 | 6 | 7 | class IPAClient(object): 8 | def __init__(self, ipa_user, ipa_password, ipa_url, bits=2048, ca_trust=False): 9 | #ipaurl="https://idm-1.etl.lab.eng.rdu2.redhat.com/ipa/" 10 | #realm="ETL.LAB.ENG.RDU2.REDHAT.COM" 11 | 12 | self.session = requests.Session() 13 | self.ipa_url = ipa_url 14 | self.bits = bits 15 | self.ca_trust = ca_trust 16 | 17 | #TODO: Sign Request with Dynamic CA (IPA) 18 | # authenticate to IPA Server 19 | try: 20 | resp = self.session.post('{0}session/login_password'.format(self.ipa_url), 21 | params="", 22 | data = {'user': ipa_user, 23 | 'password': ipa_password}, 24 | verify=self.ca_trust, 25 | headers={'Content-Type':'application/x-www-form-urlencoded', 26 | 'Accept':'applicaton/json'}) 27 | except Exception as e: 28 | raise Exception("IPA Auth Exception: {0}".format(e)) 29 | 30 | self.header = {'referer': self.ipa_url, 'Content-Type':'application/json', 'Accept':'application/json'} 31 | 32 | def create_host(self, host): 33 | try: 34 | # CREATE HOST [event['object']['spec']['host']] 35 | request_payload = {'id': 0, 'method': 'host_add', 'params': [[host],{}]} 36 | create_host = self.session.post('{0}session/json'.format(self.ipa_url), headers=self.header, 37 | data=json.dumps(request_payload), verify=self.ca_trust) 38 | if create_host.json()['error']: 39 | if create_host.json()['error']['name'] == 'DuplicateEntry': 40 | # Host already created, we can continue 41 | pass 42 | else: 43 | raise Exception("Create Host Failed: {0}\n{1}".format(create_host.json(), json.dumps(request_payload, indent=2))) 44 | except Exception as e: 45 | raise Exception("Create Host Exception: {0}".format(e)) 46 | 47 | def delete_host(self, host): 48 | try: 49 | resp = self.session.post('{0}session/json'.format(self.ipa_url), headers=self.header, 50 | data=json.dumps({'id': 0, 'method': 'host_del', 'params': [host, {'force': True}]}), verify=self.ca_trust) 51 | except Exception as e: 52 | raise Exception("Delete Host Exception: {0}".format(e)) 53 | 54 | 55 | def create_cert(self, host, realm): 56 | try: 57 | key = pkiutils.create_rsa_key(bits=self.bits, 58 | keyfile=None, 59 | format='PEM', 60 | passphrase=None) 61 | csr = pkiutils.create_csr(key, 62 | "/CN={0}/C=US/O=Test organisation/".format(host), 63 | csrfilename=None, 64 | attributes=None) 65 | except Exception as e: 66 | raise Exception("Create CSR Exception: {0}".format(e)) 67 | 68 | 69 | try: 70 | # CREATE CERT 71 | cert_request = self.session.post('{0}session/json'.format(self.ipa_url), headers=self.header, 72 | data=json.dumps({'id': 0, 73 | 'method': 'cert_request', 74 | 'params': [[csr], 75 | {'principal': 'host/{0}@{1}'.format(host, realm), 76 | 'request_type': 'pkcs10', 77 | 'add': False}]}), 78 | verify=self.ca_trust) 79 | cert_resp = cert_request.json() 80 | except Exception as e: 81 | raise Exception("Cert Create Exception: {0}\n{1}".format(e, cert_request)) 82 | 83 | try: 84 | return cert_resp['result']['result']['certificate'], key 85 | except TypeError as e: 86 | if cert_resp['error']: 87 | raise Exception("Key Error: {0}\nCert Request Body:{1}\nCert Response: {2}\nKey: {3}".format(e, cert_request, cert_resp, key)) 88 | else: 89 | raise Exception("Unknown Exception {0}".format(e)) 90 | -------------------------------------------------------------------------------- /src/OpenShiftWatcher/__init__.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | 4 | class OpenShiftWatcher(object): 5 | def __init__(self, os_api_endpoint, os_auth_token, os_namespaced, os_namespace, os_api_path, os_api_group, os_api_version, os_resource, ca_trust='/etc/ssl/certs/ca-bundle.trust.crt'): 6 | ''' os_auth_token generated from `oc whoami -t` 7 | 8 | Example: 9 | watcher = OpenShiftWatcher(os_api_endpoint="master1.example.com:8443", 10 | os_auth_token="lTBiDnvYlHhuOl3C9Tj_Mb-FvL0hcMMONIua0E0D5CE", 11 | os_namespaced='True', 12 | os_namespace="joe", 13 | os_api_path="", 14 | os_api_group="oapi", 15 | os_api_version="v1", 16 | os_resource="routes") 17 | ''' 18 | self.os_api_url = self.generate_url_resource(os_api_endpoint, os_namespaced, os_namespace, os_api_path, os_api_group, os_api_version, os_resource) 19 | self.os_auth_token = os_auth_token 20 | self.session = requests.Session() 21 | self.ca_trust = ca_trust 22 | 23 | def stream(self): 24 | req = requests.Request("GET", self.os_api_url, 25 | headers={'Authorization': 'Bearer {0}'.format(self.os_auth_token)}, 26 | params="" 27 | ).prepare() 28 | 29 | resp = self.session.send(req, stream=True, verify=self.ca_trust) 30 | 31 | if resp.status_code != 200: 32 | raise Exception("Unable to contact OpenShift API at {0}. Message from server: {1}".format(self.os_api_url, resp.text)) 33 | 34 | for line in resp.iter_lines(): 35 | if line: 36 | try: 37 | yield json.loads(line.decode('utf-8')) 38 | # TODO: Use the specific exception type here. 39 | # TODO: Logging -> "No Json Object could be decoded." 40 | except Exception as e: 41 | raise Exception("Watcher error: {0}".format(e)) 42 | 43 | def generate_url_resource(self, os_api_endpoint, os_namespaced, os_namespace, os_api_path, os_api_group, os_api_version, os_resource): 44 | if os_api_path: 45 | return "https://{0}/{1}?watch=true".format(os_api_endpoint, os_api_path) 46 | else: 47 | if os_namespaced == 'True': 48 | return "https://{0}/{1}/{2}/namespaces/{3}/{4}?watch=true".format(os_api_endpoint, os_api_group, os_api_version, os_namespace, os_resource) 49 | else: 50 | return "https://{0}/{1}/{2}/{3}?watch=true".format(os_api_endpoint, os_api_group, os_api_version, os_resource) 51 | -------------------------------------------------------------------------------- /src/conf/config.ini.sample: -------------------------------------------------------------------------------- 1 | [global] 2 | k8s_ca= 3 | k8s_token= 4 | k8s_api_endpoint= 5 | k8s_api_path= 6 | k8s_api_group= 7 | k8s_api_version= 8 | k8s_namespaced= 9 | k8s_namespace= 10 | k8s_resource= 11 | watcher_plugin= 12 | 13 | [plugin_simple] 14 | 15 | 16 | [plugin_dns] 17 | application_router_ip= # IP address for the entry point to the OpenShift application - e.g.: load balancer, router, etc. 18 | resolv_conf= # Optional custom resolv.conf file to use for custom resolvers 19 | dns_server= # DNS server holding records 20 | dns_key_file= # nsupdate key file 21 | -------------------------------------------------------------------------------- /src/config/__init__.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import configparser 3 | import constants 4 | import os 5 | from errors import * 6 | class WatcherConfig(object): 7 | def __init__(self): 8 | 9 | # Parse Arguments from the command line 10 | parser = argparse.ArgumentParser(description=constants.DESCRIPTION) 11 | parser.add_argument('-c', '--config') 12 | args = parser.parse_args() 13 | self.config_file = args.config 14 | 15 | # First validate arguments 16 | self.validated = self.validateArgs(args) 17 | if self.validated['is_valid']: 18 | self.config = configparser.ConfigParser() 19 | if self.config_file is not None: 20 | self.config.read(self.config_file) 21 | 22 | # Get Plugin so we know how to parse the rest of the args 23 | self.plugin = self.getPlugin() 24 | 25 | self.log_level = self.getParam(constants.ENV_LOG_LEVEL, '', constants.DEFAULT_LOG_LEVEL) 26 | 27 | self.k8s_token = self.getParam(constants.ENV_K8S_TOKEN, constants.DEFAULT_K8S_TOKEN) 28 | self.k8s_namespace = self.getParam(constants.ENV_K8S_NAMESPACE, constants.DEFAULT_K8S_NAMESPACE) 29 | self.k8s_endpoint = self.getParam(constants.ENV_K8S_API, '', constants.DEFAULT_K8S_API) 30 | self.k8s_ca = self.getParam(constants.ENV_K8S_CA, '', constants.DEFAULT_K8S_CA) 31 | self.k8s_namespaced = self.getParam(constants.ENV_K8S_NAMESPACED, '', constants.DEFAULT_K8S_NAMESPACED) 32 | self.k8s_api_path = self.getParam(constants.ENV_K8S_API_PATH) 33 | self.k8s_api_group = self.getParam(constants.ENV_K8S_API_GROUP, '', constants.DEFAULT_K8S_API_GROUP) 34 | self.k8s_api_version = self.getParam(constants.ENV_K8S_API_VERSION, '', constants.DEFAULT_K8S_API_VERSION) 35 | self.k8s_resource = self.getParam(constants.ENV_K8S_RESOURCE) 36 | 37 | def getPlugin(self): 38 | try: 39 | return os.environ[constants.ENV_PLUGIN.upper()] 40 | except KeyError: 41 | try: 42 | return self.config['global'][constants.ENV_PLUGIN.lower()] 43 | except KeyError: 44 | return constants.DEFAULT_WATCHER_PLUGIN 45 | 46 | def getPluginConfig(self): 47 | if self.config_file is not None: 48 | return self.config["plugin_{0}".format(self.plugin)] 49 | else: 50 | return {} 51 | 52 | def getParam(self, env = '', file='', default = ''): 53 | try: 54 | return os.environ[env.upper()] 55 | except KeyError: 56 | try: 57 | if 'global' in self.config: 58 | if self.config['global'].get(env.lower()) == None or not self.config['global'].get(env.lower()): 59 | raise KeyError('No Log Level Set') 60 | return self.config['global'].get(env.lower()) 61 | except KeyError: 62 | try: 63 | return open(file, 'r').read().strip() 64 | except IOError: 65 | return default 66 | 67 | def validateArgs(self, args): 68 | validated = {} 69 | if (self.config_file is not None) and (not os.path.exists(self.config_file)): 70 | validated['is_valid'] = False 71 | validated['reason'] = 'Config file not found: {0}'.format(self.config_file) 72 | validated['log_level'] = 'CRITICAL' 73 | else: 74 | validated['is_valid'] = True 75 | return validated 76 | 77 | def validateConfig(self): 78 | # Kube resource should be set when API path not set 79 | if not self.k8s_api_path and not self.k8s_resource: 80 | raise InvalidResourceError( 81 | 'Kubernetes resource not set. Either export {0}=, or set {1}= in {2}'.format( 82 | constants.ENV_K8S_RESOURCE.upper(), 83 | constants.ENV_K8S_RESOURCE.lower(), 84 | self.config_file 85 | ) 86 | ) 87 | # Namespace should be set when namespace is expected 88 | if self.k8s_namespaced and not self.k8s_namespace: 89 | raise InvalidNamespaceError( 90 | 'Namespace is not set. Either export {0}=, or set {1}= in {2}'.format( 91 | constants.ENV_K8S_NAMESPACE.upper(), 92 | constants.ENV_K8S_NAMESPACE.lower(), 93 | self.config_file 94 | ) 95 | ) 96 | # API URL should be set 97 | if not self.k8s_endpoint: 98 | raise InvalidEndpointError( 99 | 'Kubeneretes API Endpoint is not set. Either export {0}=, or set {1}= in {2}'.format( 100 | constants.ENV_K8S_API.upper(), 101 | constants.ENV_K8S_API.lower(), 102 | self.config_file 103 | ) 104 | ) 105 | # Token should be set 106 | if not self.k8s_token: 107 | raise InvalidTokenError( 108 | 'Kubeneretes Token is not set. Either export {0}=, or set {1}= in {2}'.format( 109 | constants.ENV_K8S_TOKEN.upper(), 110 | constants.ENV_K8S_TOKEN.lower(), 111 | self.config_file 112 | ) 113 | ) 114 | # We should warn if CA is not set 115 | if not self.k8s_ca: 116 | raise InsecureError( 117 | 'No Kubernetes CA file was loaded. Errors are likely. To remove this warning, export {0}=/path/to/ca.crt or set {1}=/path/to/ca.crt in {2}'.format( 118 | constants.ENV_K8S_CA.upper(), 119 | constants.ENV_K8S_CA.lower(), 120 | self.config_file 121 | ) 122 | ) 123 | -------------------------------------------------------------------------------- /src/constants/__init__.py: -------------------------------------------------------------------------------- 1 | ENV_K8S_API = 'K8S_API_ENDPOINT' 2 | ENV_K8S_TOKEN = 'K8S_TOKEN' 3 | ENV_K8S_NAMESPACE = 'K8S_NAMESPACE' 4 | ENV_K8S_CA = 'K8S_CA' 5 | ENV_K8S_RESOURCE = 'K8S_RESOURCE' 6 | ENV_K8S_API_PATH = 'K8S_API_PATH' 7 | ENV_K8S_API_VERSION = 'K8S_API_VERSION' 8 | ENV_K8S_API_GROUP = 'K8S_API_GROUP' 9 | ENV_K8S_NAMESPACED = 'K8S_NAMESPACED' 10 | ENV_LOG_LEVEL = 'LOG_LEVEL' 11 | ENV_PLUGIN = 'WATCHER_PLUGIN' 12 | 13 | DEFAULT_K8S_NAMESPACE = '/var/run/secrets/kubernetes.io/serviceaccount/namespace' 14 | DEFAULT_K8S_TOKEN = '/var/run/secrets/kubernetes.io/serviceaccount/token' 15 | DEFAULT_K8S_CA = '/var/run/secrets/kubernetes.io/serviceaccount/ca.crt' 16 | DEFAULT_K8S_API = 'kubernetes.default.svc.cluster.local' 17 | DEFAULT_K8S_API_VERSION = 'v1' 18 | DEFAULT_K8S_API_GROUP = 'oapi' 19 | DEFAULT_K8S_NAMESPACED = 'True' 20 | DEFAULT_WATCHER_PLUGIN = 'simple' 21 | DEFAULT_LOG_LEVEL = 'INFO' 22 | 23 | DESCRIPTION = 'OpenShift/kubernetes API Watcher Utility. Used to integrate with Third part systems' 24 | -------------------------------------------------------------------------------- /src/errors/__init__.py: -------------------------------------------------------------------------------- 1 | class Error(Exception): 2 | """Base class for exceptions in this module.""" 3 | def __init__(self, message): 4 | self.message = message 5 | self.exit_code = 10 6 | 7 | class FatalError(Error): 8 | """Error that should cause program to terminate""" 9 | def __init__(self, message): 10 | Error.__init__(self, message) 11 | self.fatal = True 12 | self.exit_code = 21 13 | 14 | class WarningError(Error): 15 | """Error that should result in a Warning message""" 16 | def __init__(self, message): 17 | Error.__init__(self, message) 18 | self.fatal = False 19 | self.exit_code = 23 20 | 21 | class InvalidResourceError(FatalError): 22 | """Exception indicating that user did not set a Resource Kind or set it Improperly""" 23 | def __init__(self, message): 24 | FatalError.__init__(self, message) 25 | self.exit_code = 31 26 | 27 | class InvalidNamespaceError(FatalError): 28 | """Exception indicating that user did not set a Namespace or set it Improperly""" 29 | def __init__(self, message): 30 | FatalError.__init__(self, message) 31 | self.exit_code = 32 32 | 33 | class InvalidEndpointError(FatalError): 34 | """Exception indicating that user did not set an API Endpoint or set it Improperly""" 35 | def __init__(self, message): 36 | FatalError.__init__(self, message) 37 | self.exit_code = 33 38 | 39 | class InvalidTokenError(FatalError): 40 | """Exception indicating that user did not set a Token or set it Improperly""" 41 | def __init__(self, message): 42 | FatalError.__init__(self, message) 43 | self.exit_code = 34 44 | 45 | class InsecureError(WarningError): 46 | """Exception indicating that user did not set a Resource Kind or set it Improperly""" 47 | def __init__(self, message): 48 | WarningError.__init__(self, message) 49 | self.exit_code = 40 50 | -------------------------------------------------------------------------------- /src/plugin_dns/README.md: -------------------------------------------------------------------------------- 1 | # DNS plugin 2 | 3 | The DNS plugin is used to update a target DNS server with application route specific configuration. This allows for application specific DNS records rather than using wildcard dns records for resolution. 4 | 5 | ## Testing Locally 6 | 7 | In addition to the instructions part of the top-level README, please add the following `plugin_dns` specific parameters. 8 | 9 | - A "nsupdate" enabled DNS server, such as `named` 10 | - A valid nsupdate key that allows for add/delete of DNS records on the target DNS server 11 | 12 | *Note* that if the DNS server targeted is not part of the regular resolver chain, you may override the resolvers with a custom resolv.conf by supplying the `resolv_conf` configuration option. This is needed in case you want to test with a zone that is not part of the normal resolvers or for some other reason this plugin cannot obtain the SOA information for the targeted zone through the normal DNS servers. In that event, just copy your current resolv.conf to a new location and ensure it has at least one valid `nameserver` entry in it (e.g.: the dns_server). 13 | 14 | ``` 15 | cat ~/myresolv.conf 16 | nameserver 192.168.1.10 17 | ``` 18 | 19 | Once the above requirements are met, and the below listed configuration has been populated, updates to routes in OpenShift will be pushed to the DNS server. 20 | 21 | ## Configuration 22 | 23 | ### Global Configuration Options 24 | 25 | | Environment Variable | ini Variable | Required | Description | 26 | | ------------- | ------------- | ------------- | ------------- | 27 | | K8S_API_ENDPOINT | k8s_api_endpoint | True | OpenShift/Kubernetes API hostname:port | 28 | | K8S_TOKEN | k8s_token | True | Login token (`oc whoami -t`) | 29 | | K8S_NAMESPACE | k8s_namespace | True | Namespace you want to watch for route changes in | 30 | | K8S_RESOURCE | k8s_resource | True | Needs to be set to `routes` for this plugin | 31 | | WATCHER_PLUGIN | watcher_plugin | True | Needs to be set to `dns` for this plugin | 32 | | LOG_LEVEL | log_level | False | set to DEBUG for more detailed output during troubleshooting | 33 | 34 | 35 | ### DNS Plugin Configuration Options 36 | 37 | | ini Variable | Required | Description | 38 | | ------------- | ------------- | ------------- | 39 | | application_router_ip | True | IP address for the DNS A-record to be pointed to, for example a load balancer or the OpenShift router | 40 | | resolv_conf | False | If the target DNS server is not connected up with your standard resolvers, use this to override with a different resolver file (e.g.: pointing to the dns_server below) | 41 | | dns_server | True | The target DNS server (FQDN or IP) | 42 | | dns_key_file | True | The DNS server key used with nsupdate | 43 | 44 | 45 | ## Developer Local Setup 46 | 47 | ### Dependencies 48 | 49 | Install the following python modules 50 | ``` 51 | sudo pip3 install dnspython 52 | ``` 53 | -------------------------------------------------------------------------------- /src/plugin_dns/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import os.path 4 | import requests 5 | 6 | import sys 7 | import dns.name 8 | import dns.resolver 9 | import dns.update 10 | import dns.tsigkeyring 11 | 12 | def handle_event(watcher, event, config): 13 | logger = watcher.logger 14 | message = "Event received ({0}), but no action processed".format( 15 | event['type'] 16 | ) 17 | 18 | logger.debug("Event Received: Kind {0}; Name {1}; Type{2};" 19 | .format(event['object']['kind'], 20 | event['object']['metadata']['name'], 21 | event['type'] 22 | ) 23 | ) 24 | 25 | if type(event) is dict and 'type' in event: 26 | if event['type'] == 'ADDED': 27 | remove_dns(event, config, logger) 28 | if add_dns(event, config, logger): 29 | message = 'DNS record \'{0}\' added for route \'{1}\''.format( 30 | event['object']['spec']['host'], 31 | event['object']['metadata']['name'] 32 | ) 33 | elif event['type'] == 'DELETED': 34 | if remove_dns(event, config, logger): 35 | message = 'DNS record \'{0}\' removed for route \'{1}\''.format( 36 | event['object']['spec']['host'], 37 | event['object']['metadata']['name'] 38 | ) 39 | elif event['type'] == 'MODIFIED': 40 | route = get_route(watcher, event, config) 41 | if route.status_code == 404: 42 | if remove_dns(event, config, logger): 43 | message = 'DNS record \'{0}\' removed for route \'{1}\''.format( 44 | event['object']['spec']['host'], 45 | event['object']['metadata']['name'] 46 | ) 47 | 48 | log_level = config.get('message_log_level','INFO') 49 | return message, log_level 50 | 51 | 52 | def get_route(watcher, event, config): 53 | route_name = event['object']['metadata']['name'] 54 | k8s_endpoint = watcher.config.k8s_endpoint 55 | k8s_namespace = watcher.config.k8s_namespace 56 | k8s_token = watcher.config.k8s_token 57 | k8s_ca = watcher.config.k8s_ca 58 | 59 | req = requests.get( 60 | 'https://{0}/oapi/v1/namespaces/{1}/routes/{2}' 61 | .format(k8s_endpoint, k8s_namespace, route_name), 62 | headers={ 63 | 'Authorization': 'Bearer {0}'.format(k8s_token), 64 | 'Content-Type':'application/strategic-merge-patch+json' 65 | }, 66 | verify=k8s_ca) 67 | 68 | return req 69 | 70 | # This function initially sourced from https://gist.github.com/pklaus/4619865 71 | # - modifications have been made afterwards 72 | def get_key(file_name, logger): 73 | if os.path.exists(file_name) == False: 74 | logger.error("Specified key file does not exist. Please correct") 75 | return False 76 | 77 | f = open(file_name) 78 | keyfile = f.read().splitlines() 79 | f.close() 80 | 81 | hostname = keyfile[0].rsplit(' ')[1].replace('"', '').strip() 82 | algo = keyfile[1].split()[1].replace(';','').replace('-','_').upper().strip() 83 | key = keyfile[2].split()[1].replace('}','').replace(';','').replace('"', '').strip() 84 | 85 | k = {hostname:key} 86 | 87 | try: 88 | key_ring = dns.tsigkeyring.from_text(k) 89 | except: 90 | logger.error( 91 | '\'{0}\' is not a valid key. ' 92 | 'The file should be in DNS KEY record format. See dnssec-keygen(8)' 93 | .format(k) 94 | ) 95 | return False 96 | return [key_ring, algo] 97 | 98 | 99 | def get_zone(dns_a_record, config, logger): 100 | resolv_conf = config.get('resolv_conf') 101 | if not resolv_conf: 102 | return None 103 | 104 | if os.path.exists(resolv_conf) == False: 105 | logger.warning("Specified resolv.conf does not exist. Please correct") 106 | else: 107 | try: 108 | resolver = dns.resolver.Resolver(resolv_conf) 109 | except Exception as e: 110 | logger.error("Failed to create a valid Resolver. (error: {0})".format(e)) 111 | 112 | dns_zone = dns.resolver.zone_for_name(dns_a_record, resolver=resolver) 113 | 114 | try: 115 | dns_zone 116 | except NameError: 117 | dns_zone = dns.resolver.zone_for_name(dns_a_record) 118 | 119 | return dns_zone 120 | 121 | 122 | def modify_dns(action, event, config, logger): 123 | dns_server = config.get('dns_server') 124 | dns_key_file = config.get('dns_key_file') 125 | application_router_ip = config.get('application_router_ip') 126 | 127 | dns_a_record = event['object']['spec']['host'] 128 | 129 | try: 130 | name_object = dns.name.from_text(dns_a_record) 131 | except Exception as e: 132 | logger.error("Not a valid DNS name: {0} (error: {1})".format(dns_a_record, e)); 133 | return False 134 | 135 | dns_zone = get_zone(dns_a_record, config, logger) 136 | dns_name = name_object.relativize(dns_zone) 137 | 138 | logger.debug('Zone: {0}, DNS Name: {1}'.format(dns_zone, dns_name)) 139 | 140 | key_ring, key_algorithm = get_key(dns_key_file, logger) 141 | dns_update = dns.update.Update( 142 | dns_zone, 143 | keyring=key_ring, 144 | keyalgorithm=getattr(dns.tsig, key_algorithm) 145 | ) 146 | 147 | if action == 'add': 148 | dns_update.add(dns_name, '180', 'A', application_router_ip) 149 | elif action == 'del': 150 | dns_update.delete(dns_name, 'A', application_router_ip) 151 | 152 | logger.debug('dns_update {0}'.format(dns_update)) 153 | 154 | try: 155 | dns_response = dns.query.tcp(dns_update, dns_server) 156 | except dns.tsig.PeerBadKey: 157 | logger.error("Invalid key for server {0}".format(dns_server)) 158 | return False 159 | except dns.tsig.PeerBadSignature: 160 | logger.error("Invalid key signature for server {0}".format(dns_server)) 161 | except Exception as e: 162 | logger.error("Failed to update DNS with error {0}".format(e)) 163 | return False 164 | return True 165 | 166 | 167 | def remove_dns(event, config, logger): 168 | return modify_dns('del', event, config, logger) 169 | 170 | 171 | def add_dns(event, config, logger): 172 | return modify_dns('add', event, config, logger) 173 | 174 | 175 | -------------------------------------------------------------------------------- /src/plugin_ipa/README.md: -------------------------------------------------------------------------------- 1 | # Event Watcher PKI Plugin 2 | 3 | This plugin provides functionality to automatically secure routes with certificates generated from an external PKI system. 4 | 5 | ## Quickstart 6 | 7 | Write the following to your config.ini: 8 | ``` 9 | ...(see watcher readme for global settings)... 10 | 11 | [plugin_ipa] 12 | need_cert_annotation=openshift.io/managed.cert 13 | ipa_user= 14 | ipa_password= 15 | ipa_url= 16 | ipa_realm= 17 | ca_trust= 18 | ``` 19 | 20 | Then run the following command: 21 | 22 | ``` 23 | python3 watch.py --config conf/config.ini 24 | ``` 25 | 26 | ## Plugin Configuration Options 27 | 28 | | ini Variable | Required | Description | 29 | | ------------- | ------------- | -------------| 30 | | need_cert_annotation | true | Name of the annotation used to designate routes that need certificates | 31 | | ipa_user | true | IPA/IDM user with permissions to create hosts & certificates | 32 | | ipa_password | true | IPA/IDM password | 33 | | ipa_url | true | Url to IPA/IDM API (include path) | 34 | | ipa_realm | true | Realm under which new hosts/certs should be created | 35 | | ipa_ca_cert | false | CA certificate for IPA server trust | 36 | ## Developer local Setup 37 | 38 | ### Dependencies 39 | 40 | First, install the following packages: 41 | 42 | ``` 43 | sudo dnf group install "C Development Tools and Libraries" 44 | sudo dnf install openssl-devel 45 | sudo dnf install python3-pip 46 | sudo dnf install python3-devel 47 | sudo dnf install redhat-rpm-config 48 | ``` 49 | 50 | Then, install the following python modules 51 | ``` 52 | sudo pip3 install requests 53 | sudo pip3 install pyOpenSSL 54 | sudo pip3 install cryptography 55 | sudo pip3 install pkiutils 56 | sudo pip3 install six 57 | sudo pip3 install traceback 58 | sudo pip3 install json 59 | ``` 60 | -------------------------------------------------------------------------------- /src/plugin_ipa/__init__.py: -------------------------------------------------------------------------------- 1 | import traceback, sys, os, requests, json, six 2 | from IPAClient import IPAClient 3 | 4 | def handle_event(watcher, event, config): 5 | 6 | route_fqdn = event['object']['spec']['host'] 7 | route_name = event['object']['metadata']['name'] 8 | 9 | if need_cert(event, config, watcher.logger): 10 | watcher.logger.info("[ROUTE NEEDS CERT]: {0}".format(route_fqdn)) 11 | if event['type'] == 'ADDED': 12 | watcher.logger.info("[ROUTE ADDED]: {0}".format(route_fqdn)) 13 | watcher.logger.debug("[ROUTE ADDED]: {0}".format(event)) 14 | update_route(route_fqdn, route_name, config, watcher.logger, watcher) 15 | elif event['type'] == 'MODIFIED': 16 | update_route(route_fqdn, route_name, config, watcher.logger, watcher) 17 | watcher.logger.info("[ROUTE MODIFIED]: {0}".format(route_fqdn)) 18 | watcher.logger.debug("[ROUTE MODIFIED]: {0}".format(event)) 19 | 20 | #TODO: Get modified cert route and make sure cert data matches hostname. If not, regenerate cert 21 | elif event['type'] == 'DELETED': 22 | delete_route(route_fqdn, config, watcher.logger) 23 | watcher.logger.info("[ROUTE DELETED]: {0}".format(route_fqdn)) 24 | else: 25 | watcher.logger.debug("[UNKNOWN EVENT TYPE]: {0}".format(event)) 26 | else: 27 | watcher.logger.debug("No cert needed") 28 | 29 | 30 | message = "Kind: {0}; Name: {1}".format(event['object']['kind'], event['object']['metadata']['name']) 31 | log_level = "INFO" 32 | return message, log_level 33 | 34 | def get_route(route_name, logger, watcher): 35 | req = requests.get('https://{0}/oapi/v1/namespaces/{1}/routes/{2}'.format(watcher.config.k8s_endpoint, watcher.config.k8s_namespace, route_name), 36 | headers={'Authorization': 'Bearer {0}'.format(watcher.config.k8s_token), 'Content-Type':'application/strategic-merge-patch+json'}, 37 | verify=watcher.config.k8s_ca) 38 | return req 39 | 40 | def need_cert(event, config, logger): 41 | try: 42 | route_annotation = event['object']['metadata']['annotations'][config.get('need_cert_annotation')] 43 | try: 44 | route_annotation_state = event['object']['metadata']['annotations']['{0}.state'.format(config.get('need_cert_annotation'))] 45 | except KeyError: 46 | route_annotation_state = False 47 | logger.debug("Found annotation: {0}={1}".format(config.get('need_cert_annotation'),route_annotation)) 48 | return route_annotation == "true" and route_annotation_state != 'created' 49 | except KeyError: 50 | exc_type, exc_value, exc_traceback = sys.exc_info() 51 | logger.debug("Error message: {0}".format(repr(traceback.format_exception(exc_type, exc_value, exc_traceback, 52 | limit=2)))) 53 | logger.debug("Got an event with no annotation, so nothing to do: {0}".format(event)) 54 | return False 55 | else: 56 | logger.debug("Unknown error") 57 | return False 58 | 59 | def update_route(route_fqdn, route_name, config, logger, watcher): 60 | ipa_client = IPAClient( 61 | ipa_user = config.get('ipa_user'), 62 | ipa_password = config.get('ipa_password'), 63 | ipa_url = config.get('ipa_url'), 64 | ca_trust = config.get('ipa_ca_cert', False) 65 | ) 66 | ipa_client.create_host(route_fqdn) 67 | certificate, key = ipa_client.create_cert(route_fqdn, config.get('ipa_realm')) 68 | logger.info("[CERT CREATED]: {0}".format(route_fqdn)) 69 | logger.debug("Cert: {0}\nKey: {1}\n".format(certificate, key.exportKey().decode('UTF-8'))) 70 | 71 | req = requests.patch('https://{0}/oapi/v1/namespaces/{1}/routes/{2}'.format(watcher.config.k8s_endpoint, watcher.config.k8s_namespace, route_name), 72 | headers={'Authorization': 'Bearer {0}'.format(watcher.config.k8s_token), 'Content-Type':'application/strategic-merge-patch+json'}, 73 | data=json.dumps({'metadata': {'annotations': {'{0}.state'.format(config.get('need_cert_annotation')): 'created'}}, 'spec': {'tls': {'certificate': '-----BEGIN CERTIFICATE-----\n{0}\n-----END CERTIFICATE-----'.format( 74 | '\n'.join(certificate[i:i+65] for i in six.moves.range(0, len(certificate), 65))), 75 | 'key': '{0}'.format(key.exportKey('PEM').decode('UTF-8'))}}}), 76 | params="", verify=watcher.config.k8s_ca) 77 | logger.info("[ROUTE UPDATED]: {0}".format(route_fqdn)) 78 | 79 | def delete_route(route_fqdn, config, logger): 80 | ipa_client = IPAClient( 81 | ipa_user = config.get('ipa_user'), 82 | ipa_password = config.get('ipa_password'), 83 | ipa_url = config.get('ipa_url'), 84 | ca_trust = config.get('ipa_ca_cert', False) 85 | ) 86 | ipa_client.delete_host(route_fqdn) 87 | logger.info("[CERT DELETED]: {0}".format(route_fqdn)) 88 | -------------------------------------------------------------------------------- /src/plugin_simple/__init__.py: -------------------------------------------------------------------------------- 1 | def handle_event(watcher, event, config): 2 | message = "Kind: {0}; Name: {1}; Event Type:{2}".format(event['object']['kind'], event['object']['metadata']['name'], event['type']) 3 | log_level = config.get('message_log_level','INFO') 4 | return message, log_level 5 | -------------------------------------------------------------------------------- /src/watch.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import requests 5 | import json 6 | import logging 7 | import signal 8 | import sys 9 | from config import WatcherConfig 10 | from errors import * 11 | from OpenShiftWatcher import OpenShiftWatcher 12 | 13 | class EventWatcher(object): 14 | def __init__(self): 15 | 16 | logging.basicConfig(format="%(asctime)s [%(levelname)s] %(message)s") 17 | self.logger = logging.getLogger('watcher') 18 | self.config = self.getConfig() 19 | 20 | self.logger.setLevel(self.config.log_level) 21 | self.logger.debug(json.dumps(dict(os.environ), indent=2, sort_keys=True)) 22 | self.logger.debug("CA Trust: {0}".format(self.config.k8s_ca)) 23 | self.logger.debug("Namespace: {0}".format(self.config.k8s_namespace)) 24 | self.logger.debug("Token: {0}".format(self.config.k8s_token)) 25 | self.logger.info("Loading config file from {0}".format(self.config.config_file)) 26 | 27 | self.watch(self.config.k8s_resource, self.config.plugin) 28 | 29 | def watch(self, resource_type, plugin_name, *args, **kwargs): 30 | plugin = self.load_plugin(plugin_name) 31 | watcher = OpenShiftWatcher(os_api_endpoint=self.config.k8s_endpoint, 32 | os_auth_token=self.config.k8s_token, 33 | os_namespaced=self.config.k8s_namespaced, 34 | os_namespace=self.config.k8s_namespace, 35 | os_api_path=self.config.k8s_api_path, 36 | os_api_group=self.config.k8s_api_group, 37 | os_api_version=self.config.k8s_api_version, 38 | os_resource=resource_type, 39 | ca_trust=self.config.k8s_ca) 40 | 41 | for event in watcher.stream(): 42 | if type(event) is dict and 'type' in event: 43 | result,level = plugin.handle_event(self, event, self.config.getPluginConfig(), *args, **kwargs) 44 | self.log(result, level) 45 | 46 | def getConfig(self): 47 | config = WatcherConfig() 48 | 49 | # Check that args are properly set 50 | if not config.validated['is_valid']: 51 | log_level = config.validated['log_level'] 52 | self.log(config.validated['reason'], log_level) 53 | sys.exit(1) 54 | 55 | try: 56 | config.validateConfig() 57 | except Error as error: 58 | if isinstance(error, FatalError): 59 | self.log(error.message, 'CRITICAL') 60 | sys.exit(error.exit_code) 61 | elif isinstance(error, WarningError): 62 | self.log(error.message, 'WARNING') 63 | else: 64 | self.log(error.message, 'ERROR') 65 | 66 | return config 67 | 68 | 69 | def log(self, message, log_level): 70 | self.logger.debug('Log level: {0}'.format(log_level)) 71 | self.logger.log(logging.getLevelName(log_level), message) 72 | 73 | def load_plugin(self, name): 74 | mod = __import__("plugin_%s" % name) 75 | return mod 76 | 77 | if __name__ == '__main__': 78 | EventWatcher() 79 | --------------------------------------------------------------------------------