129 lines
		
	
	
		
			4.3 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			129 lines
		
	
	
		
			4.3 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| """Base Kubernetes Reconciler"""
 | |
| from typing import TYPE_CHECKING, Generic, TypeVar
 | |
| 
 | |
| from kubernetes.client import V1ObjectMeta
 | |
| from kubernetes.client.rest import ApiException
 | |
| from structlog import get_logger
 | |
| 
 | |
| from passbook import __version__
 | |
| from passbook.lib.sentry import SentryIgnoredException
 | |
| 
 | |
| if TYPE_CHECKING:
 | |
|     from passbook.outposts.controllers.kubernetes import KubernetesController
 | |
| 
 | |
| # pylint: disable=invalid-name
 | |
| T = TypeVar("T")
 | |
| 
 | |
| 
 | |
| class ReconcileTrigger(SentryIgnoredException):
 | |
|     """Base trigger raised by child classes to notify us"""
 | |
| 
 | |
| 
 | |
| class NeedsRecreate(ReconcileTrigger):
 | |
|     """Exception to trigger a complete recreate of the Kubernetes Object"""
 | |
| 
 | |
| 
 | |
| class NeedsUpdate(ReconcileTrigger):
 | |
|     """Exception to trigger an update to the Kubernetes Object"""
 | |
| 
 | |
| 
 | |
| class KubernetesObjectReconciler(Generic[T]):
 | |
|     """Base Kubernetes Reconciler, handles the basic logic."""
 | |
| 
 | |
|     controller: "KubernetesController"
 | |
| 
 | |
|     def __init__(self, controller: "KubernetesController"):
 | |
|         self.controller = controller
 | |
|         self.namespace = controller.outpost.config.kubernetes_namespace
 | |
|         self.logger = get_logger(
 | |
|             controller=self.__class__.__name__, outpost=controller.outpost
 | |
|         )
 | |
| 
 | |
|     @property
 | |
|     def name(self) -> str:
 | |
|         """Get the name of the object this reconciler manages"""
 | |
|         raise NotImplementedError
 | |
| 
 | |
|     def up(self):
 | |
|         """Create object if it doesn't exist, update if needed or recreate if needed."""
 | |
|         current = None
 | |
|         reference = self.get_reference_object()
 | |
|         try:
 | |
|             try:
 | |
|                 current = self.retrieve()
 | |
|             except ApiException as exc:
 | |
|                 if exc.status == 404:
 | |
|                     self.logger.debug("Failed to get current, triggering recreate")
 | |
|                     raise NeedsRecreate from exc
 | |
|                 self.logger.debug("Other unhandled error", exc=exc)
 | |
|                 raise exc
 | |
|             else:
 | |
|                 self.logger.debug("Got current, running reconcile")
 | |
|                 self.reconcile(current, reference)
 | |
|         except NeedsRecreate:
 | |
|             self.logger.debug("Recreate requested")
 | |
|             if current:
 | |
|                 self.logger.debug("Deleted old")
 | |
|                 self.delete(current)
 | |
|             else:
 | |
|                 self.logger.debug("No old found, creating")
 | |
|             self.logger.debug("Created")
 | |
|             self.create(reference)
 | |
|         except NeedsUpdate:
 | |
|             self.logger.debug("Updating")
 | |
|             self.update(current, reference)
 | |
|         else:
 | |
|             self.logger.debug("Nothing to do...")
 | |
| 
 | |
|     def down(self):
 | |
|         """Delete object if found"""
 | |
|         try:
 | |
|             current = self.retrieve()
 | |
|             self.delete(current)
 | |
|             self.logger.debug("Removing")
 | |
|         except ApiException as exc:
 | |
|             if exc.status == 404:
 | |
|                 self.logger.debug("Failed to get current, assuming non-existant")
 | |
|                 return
 | |
|             self.logger.debug("Other unhandled error", exc=exc)
 | |
|             raise exc
 | |
| 
 | |
|     def get_reference_object(self) -> T:
 | |
|         """Return object as it should be"""
 | |
|         raise NotImplementedError
 | |
| 
 | |
|     def reconcile(self, current: T, reference: T):
 | |
|         """Check what operations should be done, should be raised as
 | |
|         ReconcileTrigger"""
 | |
|         raise NotImplementedError
 | |
| 
 | |
|     def create(self, reference: T):
 | |
|         """API Wrapper to create object"""
 | |
|         raise NotImplementedError
 | |
| 
 | |
|     def retrieve(self) -> T:
 | |
|         """API Wrapper to retrive object"""
 | |
|         raise NotImplementedError
 | |
| 
 | |
|     def delete(self, reference: T):
 | |
|         """API Wrapper to delete object"""
 | |
|         raise NotImplementedError
 | |
| 
 | |
|     def update(self, current: T, reference: T):
 | |
|         """API Wrapper to update object"""
 | |
|         raise NotImplementedError
 | |
| 
 | |
|     def get_object_meta(self, **kwargs) -> V1ObjectMeta:
 | |
|         """Get common object metadata"""
 | |
|         return V1ObjectMeta(
 | |
|             namespace=self.namespace,
 | |
|             labels={
 | |
|                 "app.kubernetes.io/name": f"passbook-{self.controller.outpost.type.lower()}",
 | |
|                 "app.kubernetes.io/instance": self.controller.outpost.name,
 | |
|                 "app.kubernetes.io/version": __version__,
 | |
|                 "app.kubernetes.io/managed-by": "passbook.beryju.org",
 | |
|                 "passbook.beryju.org/outpost-uuid": self.controller.outpost.uuid.hex,
 | |
|             },
 | |
|             **kwargs,
 | |
|         )
 | 
