├── .gitignore ├── Dockerfile ├── Makefile ├── README.md ├── config ├── crds │ └── exampleoperator_v1alpha1_immortalcontainer.yaml ├── default │ └── default.yaml ├── example-use.yaml ├── namespace.yaml └── rbac │ ├── rbac_role.yaml │ ├── rbac_role_binding.yaml │ └── service_account.yaml ├── docs ├── components_diagram.png └── components_diagram.xml ├── requirements.txt └── src ├── controller.py ├── defs.py ├── main.py └── threadedwatch.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | venv 3 | .vscode 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3-alpine3.9 as base 2 | 3 | FROM base as builder 4 | RUN mkdir /install 5 | WORKDIR /install 6 | COPY requirements.txt /requirements.txt 7 | RUN apk add --no-cache --virtual .build-deps gcc musl-dev libffi-dev openssl-dev 8 | RUN pip install --install-option="--prefix=/install" -r /requirements.txt 9 | 10 | FROM base 11 | COPY --from=builder /install /usr/local 12 | COPY src /exampleoperatorpy 13 | WORKDIR /exampleoperatorpy 14 | CMD ["python", "main.py"] -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | IMG?=exampleoperatorpy:dev 2 | 3 | dep: 4 | pip install -r requirements.txt 5 | 6 | docker-build: 7 | docker build . -t ${IMG} 8 | 9 | crds: 10 | kubectl apply -f config/crds 11 | 12 | namespace: 13 | kubectl apply -f config/namespace.yaml 14 | 15 | permissions: 16 | kubectl apply -f config/rbac 17 | 18 | run: crds 19 | python3 src/main.py --kubeconfig ~/.kube/config 20 | 21 | install: namespace crds permissions 22 | 23 | # Deploy controller in the configured Kubernetes cluster in ~/.kube/config 24 | deploy: install 25 | kubectl apply -f config/default --namespace=immortalcontainers-operator 26 | 27 | # Remove controller in the configured Kubernetes cluster in ~/.kube/config 28 | undeploy: 29 | kubectl delete -f config/default --namespace=immortalcontainers-operator 30 | kubectl delete -f config/crds || true 31 | kubectl delete -f config/rbac || true 32 | kubectl delete -f config/namespace.yaml || true 33 | 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # exampleoperatorpy 2 | 3 | This repository implements an example Kubernetes operator in Python 3, called "ImmortalContainers". This operator enables the user to define, using custom resources, containers that must run and if terminated must be restarted. 4 | 5 | The following diagram shows the main components of this operator controller: 6 | 7 | ![components diagram](https://github.com/flugel-it/k8s-python-operator/raw/master/docs/components_diagram.png "Components diagram") 8 | 9 | ## Venv and project dependencies 10 | 11 | To create a virtual env and install the project dependencies follow these steps: 12 | 13 | ```bash 14 | python3 -m venv venv 15 | . ./venv/bin/activate 16 | make dep 17 | ``` 18 | 19 | ## Install CRD and RBAC permissions 20 | 21 | To install CRDs and RBAC configurations to your currently set cluster use: 22 | 23 | ```bash 24 | make install 25 | ``` 26 | 27 | ## Running the operator outside the cluster 28 | 29 | ```bash 30 | . ./venv/bin/activate 31 | python src/main.py --kubeconfig ~/.kube/config 32 | ``` 33 | 34 | ## Running inside the cluster 35 | 36 | You must first generate the image using `make docker-build` and push it to your repo. 37 | 38 | If using **minikube** follow these steps: 39 | 40 | ```bash 41 | eval $(minikube docker-env) 42 | make docker-build 43 | ``` 44 | 45 | Then create the `system` namespace 46 | 47 | ```bash 48 | kubectl apply -f config/namespace.yaml 49 | ``` 50 | 51 | And then run `make deploy`. 52 | 53 | After this you should check that everything is running, ex: 54 | 55 | ```bash 56 | $ kubectl get pods --namespace system 57 | NAME READY STATUS RESTARTS AGE 58 | exampleoperatorpy-controller-7cb7f99658-97zjs 1/1 Running 0 24m 59 | 60 | $ kubectl logs exampleoperatorpy-controller-7cb7f99658-97zjs --namespace=system 61 | 62 | INFO:controller:Controller starting 63 | ``` 64 | 65 | ## Using the operator 66 | 67 | Once the operator is running you can create immortal containers using a custom resource like this one: 68 | 69 | ```yaml 70 | apiVersion: immortalcontainer.flugel.it/v1alpha1 71 | kind: ImmortalContainer 72 | metadata: 73 | name: example-immortal-container 74 | spec: 75 | image: nginx:latest 76 | ``` 77 | 78 | Run `kubectl apply -f config/example-use.yaml` to try it. 79 | 80 | Then run `kubectl get pods` and check the pod is created. If you kill the pod it will be recreated. 81 | 82 | ## Remove the operator 83 | 84 | To remove the operator, CDR and RBAC use `make undeploy` 85 | 86 | Pods created by the operator will not be deleted, but will not be restarted if deleted later. -------------------------------------------------------------------------------- /config/crds/exampleoperator_v1alpha1_immortalcontainer.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: immortalcontainers.immortalcontainer.flugel.it 5 | spec: 6 | group: immortalcontainer.flugel.it 7 | names: 8 | kind: ImmortalContainer 9 | listKind: ImmortalContainerList 10 | plural: immortalcontainers 11 | singular: immortalcontainer 12 | scope: Namespaced 13 | subresources: 14 | status: {} 15 | validation: 16 | openAPIV3Schema: 17 | properties: 18 | apiVersion: 19 | type: string 20 | kind: 21 | type: string 22 | metadata: 23 | type: object 24 | spec: 25 | properties: 26 | image: 27 | minLength: 1 28 | type: string 29 | required: 30 | - image 31 | type: object 32 | status: 33 | properties: 34 | currentPod: 35 | type: string 36 | startTimes: 37 | format: int64 38 | type: integer 39 | type: object 40 | version: v1alpha1 -------------------------------------------------------------------------------- /config/default/default.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: immortalcontainers-controller 5 | labels: 6 | app: immortalcontainers-controller 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: immortalcontainers-controller 12 | template: 13 | metadata: 14 | labels: 15 | app: immortalcontainers-controller 16 | spec: 17 | serviceAccountName: immortalcontainers-operator 18 | containers: 19 | - image: flugelit/immortalcontainer-operator-py:dev 20 | name: immortalcontainers-controller 21 | -------------------------------------------------------------------------------- /config/example-use.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: immortalcontainer.flugel.it/v1alpha1 2 | kind: ImmortalContainer 3 | metadata: 4 | name: example-immortal-container 5 | spec: 6 | image: nginx:latest 7 | -------------------------------------------------------------------------------- /config/namespace.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: immortalcontainers-operator 5 | labels: 6 | name: immortalcontainers-operator 7 | -------------------------------------------------------------------------------- /config/rbac/rbac_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | creationTimestamp: null 5 | name: immortalcontainers-operator 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - events 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - create 16 | - update 17 | - patch 18 | - delete 19 | - apiGroups: 20 | - "" 21 | resources: 22 | - pods 23 | verbs: 24 | - get 25 | - list 26 | - watch 27 | - create 28 | - update 29 | - patch 30 | - delete 31 | - apiGroups: 32 | - immortalcontainer.flugel.it 33 | resources: 34 | - immortalcontainers 35 | verbs: 36 | - get 37 | - list 38 | - watch 39 | - create 40 | - update 41 | - patch 42 | - delete 43 | -------------------------------------------------------------------------------- /config/rbac/rbac_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: immortalcontainers-operator-role-binding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: immortalcontainers-operator 9 | subjects: 10 | - kind: ServiceAccount 11 | name: immortalcontainers-operator 12 | namespace: immortalcontainers-operator 13 | -------------------------------------------------------------------------------- /config/rbac/service_account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: immortalcontainers-operator 5 | namespace: immortalcontainers-operator 6 | -------------------------------------------------------------------------------- /docs/components_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flugel-it/k8s-python-operator/da9a3f2a74e23ac2d76a0ac1193167f9e2bfac22/docs/components_diagram.png -------------------------------------------------------------------------------- /docs/components_diagram.xml: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | kubernetes==8.0.1 2 | -------------------------------------------------------------------------------- /src/controller.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import queue 3 | import threading 4 | 5 | from kubernetes.client.rest import ApiException 6 | from kubernetes.client import models 7 | import copy 8 | 9 | logger = logging.getLogger('controller') 10 | 11 | 12 | class Controller(threading.Thread): 13 | """Reconcile current and desired state by listening for events and making 14 | calls to Kubernetes API. 15 | """ 16 | 17 | def __init__(self, pods_watcher, immortalcontainers_watcher, corev1api, 18 | customsapi, custom_group, custom_version, custom_plural, 19 | custom_kind, workqueue_size=10): 20 | """Initializes the controller. 21 | 22 | :param pods_watcher: Watcher for pods events. 23 | :param immortalcontainers_watcher: Watcher for immortalcontainers custom 24 | resource events. 25 | :param corev1api: kubernetes.client.CoreV1Api() 26 | :param customsapi: kubernetes.client.CustomObjectsApi() 27 | :param custom_group: The custom resource's group name 28 | :param custom_version: The custom resource's version 29 | :param custom_plural: The custom resource's plural name. 30 | :param custom_kind: The custom resource's kind name. 31 | :param workqueue_size: queue size for resources that must be processed. 32 | """ 33 | super().__init__() 34 | # `workqueue` contains namespace/name of immortalcontainers whose status 35 | # must be reconciled 36 | self.workqueue = queue.Queue(workqueue_size) 37 | self.pods_watcher = pods_watcher 38 | self.immortalcontainers_watcher = immortalcontainers_watcher 39 | self.corev1api = corev1api 40 | self.customsapi = customsapi 41 | self.custom_group = custom_group 42 | self.custom_version = custom_version 43 | self.custom_plural = custom_plural 44 | self.custom_kind = custom_kind 45 | self.pods_watcher.add_handler(self._handle_pod_event) 46 | self.immortalcontainers_watcher.add_handler( 47 | self._handle_immortalcontainer_event) 48 | 49 | def _handle_pod_event(self, event): 50 | """Handle an event from the pods watcher putting the pod's corresponding 51 | immortalcontroller in the `workqueue`. """ 52 | obj = event['object'] 53 | owner_name = "" 54 | if obj.metadata.owner_references is not None: 55 | for owner_ref in obj.metadata.owner_references: 56 | if owner_ref.api_version == self.custom_group+"/"+self.custom_version and \ 57 | owner_ref.kind == self.custom_kind: 58 | owner_name = owner_ref.name 59 | if owner_name != "": 60 | self._queue_work(obj.metadata.namespace+"/"+owner_name) 61 | 62 | def _handle_immortalcontainer_event(self, event): 63 | """Handle an event from the immortalcontainers watcher putting the 64 | object name in the `workqueue`.""" 65 | self._queue_work(event['object']['metadata']['namespace'] + 66 | "/"+event['object']['metadata']['name']) 67 | 68 | def _queue_work(self, object_key): 69 | """Add a object name to the work queue.""" 70 | if len(object_key.split("/")) != 2: 71 | logger.error("Invalid object key: {:s}".format(object_key)) 72 | return 73 | self.workqueue.put(object_key) 74 | 75 | def run(self): 76 | """Dequeue and process objects from the `workqueue`. This method 77 | should not be called directly, but using `start()""" 78 | self.running = True 79 | logger.info('Controller starting') 80 | while self.running: 81 | e = self.workqueue.get() 82 | if not self.running: 83 | self.workqueue.task_done() 84 | break 85 | try: 86 | self._reconcile_state(e) 87 | self.workqueue.task_done() 88 | except Exception as ex: 89 | logger.error( 90 | "Error _reconcile state {:s}".format(e), 91 | exc_info=True) 92 | 93 | def stop(self): 94 | """Stops this controller thread""" 95 | self.running = False 96 | self.workqueue.put(None) 97 | 98 | def _reconcile_state(self, object_key): 99 | """Make changes to go from current state to desired state and updates 100 | object status.""" 101 | logger.info("Reconcile state: {:s}".format(object_key)) 102 | ns, name = object_key.split("/") 103 | 104 | # Get object if it exists 105 | try: 106 | immortalcontainer = self.customsapi.get_namespaced_custom_object( 107 | self.custom_group, self.custom_version, ns, self.custom_plural, name) 108 | except ApiException as e: 109 | if e.status == 404: 110 | logger.info( 111 | "Element {:s} in workqueue no longer exist".format(object_key)) 112 | return 113 | raise e 114 | 115 | # Create pod definition 116 | pod_definition = self._new_pod(immortalcontainer) 117 | pod = None 118 | try: 119 | pod = self.corev1api.read_namespaced_pod( 120 | pod_definition.metadata.name, ns) 121 | except ApiException as e: 122 | if e.status != 404: 123 | logger.info("Error retrieving pod {:s} for immortalcontainer {:s}".format( 124 | pod_definition.metadata.name, object_key)) 125 | raise e 126 | 127 | if pod is None: 128 | # If no pod exists create one 129 | pod = self.corev1api.create_namespaced_pod(ns, pod_definition) 130 | # update status 131 | self._update_status(immortalcontainer, pod) 132 | 133 | def _update_status(self, immortalcontainer, pod): 134 | """Updates an ImmortalContainer status""" 135 | new_status = self._calculate_status(immortalcontainer, pod) 136 | try: 137 | self.customsapi.patch_namespaced_custom_object_status( 138 | self.custom_group, self.custom_version, 139 | immortalcontainer['metadata']['namespace'], 140 | self.custom_plural, immortalcontainer['metadata']['name'], 141 | new_status 142 | ) 143 | except Exception as e: 144 | logger.error("Error updating status for ImmortalContainer {:s}/{:s}".format( 145 | immortalcontainer['metadata']['namespace'], immortalcontainer['metadata']['name'])) 146 | 147 | def _calculate_status(self, immortalcontainer, pod): 148 | """Calculates what the status of an ImmortalContainer should be """ 149 | new_status = copy.deepcopy(immortalcontainer) 150 | if 'status' in immortalcontainer and 'startTimes' in immortalcontainer['status']: 151 | startTimes = immortalcontainer['status']['startTimes']+1 152 | else: 153 | startTimes = 1 154 | new_status['status'] = dict( 155 | currentPod=pod.metadata.name, 156 | startTimes=startTimes 157 | ) 158 | return new_status 159 | 160 | def _new_pod(self, immortalcontainer): 161 | """Returns the pod definition to create the pod for an ImmortalContainer""" 162 | labels = dict(controller=immortalcontainer['metadata']['name']) 163 | return models.V1Pod( 164 | metadata=models.V1ObjectMeta( 165 | name=immortalcontainer['metadata']['name']+"-immortalpod", 166 | labels=labels, 167 | namespace=immortalcontainer['metadata']['namespace'], 168 | owner_references=[models.V1OwnerReference( 169 | api_version=self.custom_group+"/"+self.custom_version, 170 | controller=True, 171 | kind=self.custom_kind, 172 | name=immortalcontainer['metadata']['name'], 173 | uid=immortalcontainer['metadata']['uid'] 174 | )]), 175 | spec=models.V1PodSpec( 176 | containers=[ 177 | models.V1Container( 178 | name="acontainer", 179 | image=immortalcontainer['spec']['image'] 180 | ) 181 | ] 182 | ) 183 | ) 184 | -------------------------------------------------------------------------------- /src/defs.py: -------------------------------------------------------------------------------- 1 | CUSTOM_GROUP = 'immortalcontainer.flugel.it' 2 | CUSTOM_VERSION = 'v1alpha1' 3 | CUSTOM_PLURAL = 'immortalcontainers' 4 | CUSTOM_KIND = 'ImmortalContainer' 5 | -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import logging 3 | import sys 4 | 5 | from kubernetes import client, config 6 | 7 | import defs 8 | from controller import Controller 9 | from threadedwatch import ThreadedWatcher 10 | 11 | logging.basicConfig(level=logging.INFO) 12 | logger = logging.getLogger() 13 | 14 | 15 | def main(): 16 | parser = argparse.ArgumentParser() 17 | parser.add_argument( 18 | '--kubeconfig', help='path to kubeconfig file, only required if running outside of a cluster') 19 | args = parser.parse_args() 20 | if args.kubeconfig is not None: 21 | config.load_kube_config() 22 | else: 23 | config.load_incluster_config() 24 | 25 | corev1api = client.CoreV1Api() 26 | customsapi = client.CustomObjectsApi() 27 | 28 | # Changing this it's possible to work on all the namespaces or choose only one 29 | pods_watcher = ThreadedWatcher(corev1api.list_pod_for_all_namespaces) 30 | immortalcontainers_watcher = ThreadedWatcher( 31 | customsapi.list_cluster_custom_object, defs.CUSTOM_GROUP, 32 | defs.CUSTOM_VERSION, defs.CUSTOM_PLURAL 33 | ) 34 | controller = Controller(pods_watcher, immortalcontainers_watcher, corev1api, 35 | customsapi, defs.CUSTOM_GROUP, defs.CUSTOM_VERSION, 36 | defs.CUSTOM_PLURAL, defs.CUSTOM_KIND) 37 | 38 | controller.start() 39 | pods_watcher.start() 40 | immortalcontainers_watcher.start() 41 | try: 42 | controller.join() 43 | except (KeyboardInterrupt, SystemExit): 44 | print('\n! Received keyboard interrupt, quitting threads.\n') 45 | controller.stop() 46 | controller.join() 47 | 48 | 49 | if __name__ == '__main__': 50 | main() 51 | -------------------------------------------------------------------------------- /src/threadedwatch.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import threading 3 | 4 | from kubernetes import watch 5 | 6 | logger = logging.getLogger('threadedwatch') 7 | 8 | 9 | class ThreadedWatcher(threading.Thread): 10 | """Watches Kubernetes resources event in a separate thread. Handlers for 11 | events can be registered using `add_handler`. 12 | 13 | Example: 14 | v1 = kubernetes.client.CoreV1Api() 15 | watcher = ThreadedWatcher(v1.list_pod_for_all_namespaces) 16 | def on_event(event): 17 | print(event) 18 | watcher.add_handler(on_event) 19 | watcher.start() 20 | watcher.join() 21 | """ 22 | 23 | def __init__(self, func, *args, **kwargs): 24 | """Initialize this watcher. 25 | 26 | :param func: The API function pointer to watch. Any parameter to the 27 | function can be passed after this parameter. 28 | """ 29 | super().__init__(daemon=True) 30 | self.func = func 31 | self.func_args = args 32 | self.func_kwargs = kwargs 33 | self.handlers = [] 34 | self.watcher = None 35 | 36 | def add_handler(self, handler): 37 | """Adds a handler for all events seen by this watcher.""" 38 | self.handlers.append(handler) 39 | 40 | def run(self): 41 | """Listen and dispatch events, this method should not be called 42 | directly, but using `start()`. 43 | """ 44 | self.watcher = watch.Watch() 45 | stream = self.watcher.stream( 46 | self.func, *self.func_args, **self.func_kwargs) 47 | for event in stream: 48 | for handler in self.handlers: 49 | try: 50 | handler(event) 51 | except: 52 | logger.error("Error in event handler", exc_info=True) 53 | 54 | def stop(self): 55 | """Stops listening and dispatching events.""" 56 | if self.watcher is not None: 57 | self.watcher.stop() 58 | --------------------------------------------------------------------------------