/k8s_snapshots/kube.py

https://github.com/miracle2k/k8s-snapshots · Python · 181 lines · 120 code · 32 blank · 29 comment · 13 complexity · 8de521bca8c0afbaf2fc5edb00c618d2 MD5 · raw file

  1. import asyncio
  2. import threading
  3. from typing import (Optional, Iterable, AsyncGenerator, TypeVar, Type,
  4. NamedTuple, Callable)
  5. import pykube
  6. import structlog
  7. from aiochannel import Channel
  8. from k8s_snapshots.context import Context
  9. _logger = structlog.get_logger(__name__)
  10. Resource = TypeVar(
  11. 'Resource',
  12. bound=pykube.objects.APIObject,
  13. )
  14. ClientFactory = Callable[[], pykube.HTTPClient]
  15. # Copy of a locally-defined namedtuple in
  16. # pykube.query.WatchQuery.object_stream()
  17. _WatchEvent = NamedTuple('_WatchEvent', [
  18. ('type', str),
  19. ('object', Resource),
  20. ])
  21. class SnapshotRule(pykube.objects.APIObject):
  22. version = "k8s-snapshots.elsdoerfer.com/v1"
  23. endpoint = "snapshotrules"
  24. kind = "SnapshotRule"
  25. class Kubernetes:
  26. """
  27. Allows for easier mocking of Kubernetes resources.
  28. """
  29. def __init__(self, client_factory: Optional[ClientFactory] = None):
  30. """
  31. Parameters
  32. ----------
  33. client_factory
  34. Used in threaded operations to create a local
  35. :any:`pykube.HTTPClient` instance.
  36. """
  37. # Used for threaded operations
  38. self.client_factory = client_factory
  39. def get_or_none(self,
  40. resource_type: Type[Resource],
  41. name: str,
  42. namespace: Optional[str] = None) -> Optional[Resource]:
  43. """
  44. Sync wrapper for :any:`pykube.query.Query().get_or_none`
  45. """
  46. resource_query = resource_type.objects(self.client_factory())
  47. if namespace is not None:
  48. resource_query = resource_query.filter(namespace=namespace)
  49. return resource_query.get_or_none(name=name)
  50. def watch(
  51. self,
  52. resource_type: Type[Resource],
  53. ) -> Iterable[_WatchEvent]:
  54. """
  55. Sync wrapper for :any:`pykube.query.Query().watch().object_stream()`
  56. """
  57. return resource_type.objects(self.client_factory())\
  58. .filter(namespace=pykube.all).watch().object_stream()
  59. def get_resource_or_none_sync(
  60. client_factory: ClientFactory,
  61. resource_type: Type[Resource],
  62. name: str,
  63. namespace: Optional[str] = None) -> Optional[Resource]:
  64. return Kubernetes(client_factory).get_or_none(
  65. resource_type,
  66. name,
  67. namespace,
  68. )
  69. async def get_resource_or_none(client_factory: ClientFactory,
  70. resource_type: Type[Resource],
  71. name: str,
  72. namespace: Optional[str] = None,
  73. *,
  74. loop=None) -> Optional[Resource]:
  75. loop = loop or asyncio.get_event_loop()
  76. def _get():
  77. return get_resource_or_none_sync(
  78. client_factory=client_factory,
  79. resource_type=resource_type,
  80. name=name,
  81. namespace=namespace,
  82. )
  83. return await loop.run_in_executor(
  84. None,
  85. _get,
  86. )
  87. def watch_resources_sync(
  88. client_factory: ClientFactory,
  89. resource_type: pykube.objects.APIObject,
  90. ) -> Iterable:
  91. return Kubernetes(client_factory).watch(resource_type=resource_type)
  92. async def watch_resources(ctx: Context,
  93. resource_type: Resource,
  94. *,
  95. delay: int,
  96. allow_missing: bool = False,
  97. loop=None) -> AsyncGenerator[_WatchEvent, None]:
  98. """ Asynchronously watch Kubernetes resources """
  99. async_gen = _watch_resources_thread_wrapper(
  100. ctx.kube_client, resource_type, allow_missing=allow_missing, loop=loop)
  101. # Workaround a race condition in pykube:
  102. # https: // github.com / kelproject / pykube / issues / 138
  103. await asyncio.sleep(delay)
  104. async for item in async_gen:
  105. yield item
  106. async def _watch_resources_thread_wrapper(
  107. client_factory: Callable[[], pykube.HTTPClient],
  108. resource_type: Type[Resource],
  109. allow_missing: bool = False,
  110. *,
  111. loop=None) -> AsyncGenerator[_WatchEvent, None]:
  112. """ Async wrapper for pykube.watch().object_stream() """
  113. loop = loop or asyncio.get_event_loop()
  114. _log = _logger.bind(resource_type_name=resource_type.__name__, )
  115. channel = Channel()
  116. def worker():
  117. try:
  118. _log.debug('watch-resources.worker.start')
  119. while True:
  120. sync_iterator = watch_resources_sync(
  121. client_factory=client_factory, resource_type=resource_type)
  122. _log.debug('watch-resources.worker.watch-opened')
  123. for event in sync_iterator:
  124. # only put_nowait seems to cause SIGSEGV
  125. loop.call_soon_threadsafe(channel.put_nowait, event)
  126. _log.debug('watch-resources.worker.watch-closed')
  127. except pykube.exceptions.HTTPError as e:
  128. # TODO: It's possible that the user creates the resource
  129. # while we are already running. We should pick this up
  130. # automatically, i.e. watch ThirdPartyResource, or just
  131. # check every couple of seconds.
  132. if e.code == 404 and allow_missing:
  133. _log.info('watch-resources.worker.skipped')
  134. else:
  135. _log.exception('watch-resources.worker.error')
  136. except:
  137. _log.exception('watch-resources.worker.error')
  138. finally:
  139. _log.debug('watch-resources.worker.finalized')
  140. channel.close()
  141. thread = threading.Thread(
  142. target=worker,
  143. daemon=True,
  144. )
  145. thread.start()
  146. async for channel_event in channel:
  147. yield channel_event
  148. _log.debug('watch-resources.done')