def test_unmarshal_with_float_object(self): w = Watch() event = w.unmarshal_event('{"type": "ADDED", "object": 1}', 'float') self.assertEqual("ADDED", event['type']) self.assertEqual(1.0, event['object']) self.assertTrue(isinstance(event['object'], float)) self.assertEqual(1, event['raw_object'])
def test_unmarshal_with_no_return_type(self): w = Watch() event = w.unmarshal_event('{"type": "ADDED", "object": ["test1"]}', None) self.assertEqual("ADDED", event['type']) self.assertEqual(["test1"], event['object']) self.assertEqual(["test1"], event['raw_object'])
def test_unmarshal_with_empty_return_type(self): # empty string as a return_type is a default value # if watch can't detect object by function's name w = Watch() event = w.unmarshal_event('{"type": "ADDED", "object": ["test1"]}', '') self.assertEqual("ADDED", event['type']) self.assertEqual(["test1"], event['object']) self.assertEqual(["test1"], event['raw_object'])
def test_unmarshal_with_custom_object(self): w = Watch() event = w.unmarshal_event( '{"type": "ADDED", "object": {"apiVersion":' '"test.com/v1beta1","kind":"foo","metadata":' '{"name": "bar", "resourceVersion": "1"}}}', 'object') self.assertEqual("ADDED", event['type']) # make sure decoder deserialized json into dictionary and updated # Watch.resource_version self.assertTrue(isinstance(event['object'], dict)) self.assertEqual("1", event['object']['metadata']['resourceVersion']) self.assertEqual("1", w.resource_version)
async def run(self): options = {} if self.arg: options = json.loads(self.arg) release = options.get('release_name') pod = options.get('pod_name') container = options.get('container_name') tail_lines = options.get('tail_lines', 500) limit_bytes = options.get('limit_bytes') await self.middleware.call('chart.release.validate_pod_log_args', release, pod, container) if tail_lines is not None and tail_lines < 1: raise CallError('Tail lines must be null or greater then 0.') elif limit_bytes is not None and limit_bytes < 1: raise CallError('Limit bytes must be null or greater then 0.') release_data = await self.middleware.call('chart.release.get_instance', release) async with api_client() as (api, context): self.watch = Watch() try: async with self.watch.stream( context['core_api'].read_namespaced_pod_log, name=pod, container=container, namespace=release_data['namespace'], tail_lines=tail_lines, limit_bytes=limit_bytes, timestamps=True, ) as stream: async for event in stream: # Event should contain a timestamp in RFC3339 format, we should parse it and supply it # separately so UI can highlight the timestamp giving us a cleaner view of the logs timestamp = event.split(maxsplit=1)[0].strip() try: timestamp = str(parse(timestamp)) except (TypeError, ParserError): timestamp = None else: event = event.split(maxsplit=1)[-1].lstrip() self.send_event('ADDED', fields={ 'data': event, 'timestamp': timestamp }) except ClientConnectionError: pass
async def test_unmarshall_k8s_error_response(self): """Never parse messages of type ERROR. This test uses an actually recorded error, in this case for an outdated resource version. """ # An actual error response sent by K8s during testing. k8s_err = { 'type': 'ERROR', 'object': { 'kind': 'Status', 'apiVersion': 'v1', 'metadata': {}, 'status': 'Failure', 'message': 'too old resource version: 1 (8146471)', 'reason': 'Gone', 'code': 410 } } ret = Watch().unmarshal_event(json.dumps(k8s_err), None) self.assertEqual(ret['type'], k8s_err['type']) self.assertEqual(ret['object'], k8s_err['object']) self.assertEqual(ret['object'], k8s_err['object'])
async def test_unmarshall_k8s_error_response(self): """Never parse messages of type ERROR. This test uses an actually recorded error, in this case for an outdated resource version. """ # An actual error response sent by K8s during testing. k8s_err = { 'type': 'ERROR', 'object': { 'kind': 'Status', 'apiVersion': 'v1', 'metadata': {}, 'status': 'Failure', 'message': 'too old resource version: 1 (8146471)', 'reason': 'Gone', 'code': 410 } } with self.assertRaisesRegex( kubernetes_asyncio.client.exceptions.ApiException, r'\(410\)\nReason: Gone: too old resource version: 1 \(8146471\)' ): Watch().unmarshal_event(json.dumps(k8s_err), None)
async def run(self) -> None: """Watch for changes to the configured custom object. This method is intended to be run as a background async task. It will run forever, adding any custom object changes to the associated queue. """ self._logger.debug("Starting Kubernetes watcher") consecutive_failures = 0 watch_call = ( self._api.list_cluster_custom_object, "gafaelfawr.lsst.io", "v1alpha1", self._plural, ) while True: try: async with Watch().stream(*watch_call) as stream: async for raw_event in stream: event = self._parse_raw_event(raw_event) if event: await self._queue.put(event) consecutive_failures = 0 except ApiException as e: msg = "ApiException from watch" consecutive_failures += 1 if consecutive_failures > 10: raise else: self._logger.exception(msg, error=str(e)) msg = "Pausing 10s before attempting to continue" self._logger.info() await asyncio.sleep(10)
async def run(self): release = self.arg['release_name'] pod = self.arg['pod_name'] container = self.arg['container_name'] tail_lines = self.arg['tail_lines'] limit_bytes = self.arg['limit_bytes'] await self.middleware.call('chart.release.validate_pod_log_args', release, pod, container) release_data = await self.middleware.call('chart.release.get_instance', release) async with api_client() as (api, context): self.watch = Watch() try: async with self.watch.stream( context['core_api'].read_namespaced_pod_log, name=pod, container=container, namespace=release_data['namespace'], tail_lines=tail_lines, limit_bytes=limit_bytes, timestamps=True, ) as stream: async for event in stream: # Event should contain a timestamp in RFC3339 format, we should parse it and supply it # separately so UI can highlight the timestamp giving us a cleaner view of the logs timestamp = event.split(maxsplit=1)[0].strip() try: timestamp = str(parse(timestamp)) except (TypeError, ParserError): timestamp = None else: event = event.split(maxsplit=1)[-1].lstrip() self.send_event('ADDED', fields={ 'data': event, 'timestamp': timestamp }) except ClientConnectionError: pass
async def test_unmarshall_k8s_error_response_401_gke(self): """Never parse messages of type ERROR. This test uses an actually recorded error returned by GKE. """ # An actual error response sent by K8s during testing. k8s_err = { 'kind': 'Status', 'apiVersion': 'v1', 'metadata': {}, 'status': 'Failure', 'message': 'Unauthorized', 'reason': 'Unauthorized', 'code': 401 } with self.assertRaisesRegex( kubernetes_asyncio.client.exceptions.ApiException, r'\(401\)\nReason: Unauthorized: Unauthorized'): Watch().unmarshal_event(json.dumps(k8s_err), None)
class KubernetesPodLogsFollowTailEventSource(EventSource): """ Retrieve logs of a container in a pod in a chart release. Name of chart release, name of pod and name of container is required. Optionally `tail_lines` and `limit_bytes` can be specified. `tail_lines` is an option to select how many lines of logs to retrieve for the said container. It defaults to 500. If set to `null`, it will retrieve complete logs of the container. `limit_bytes` is an option to select how many bytes to retrieve from the tail lines selected. If set to null ( which is the default ), it will not limit the bytes returned. To clarify, `tail_lines` is applied first and the required number of lines are retrieved and then `limit_bytes` is applied. """ ACCEPTS = Dict( Int('tail_lines', default=500, validators=[Range(min=1)]), Int('limit_bytes', default=None, null=True, validators=[Range(min=1)]), Str('release_name', required=True), Str('pod_name', required=True), Str('container_name', required=True), ) RETURNS = Dict(Str('data', required=True), Str('timestamp', required=True, null=True)) def __init__(self, *args, **kwargs): super(KubernetesPodLogsFollowTailEventSource, self).__init__(*args, **kwargs) self.watch = None async def run(self): release = self.arg['release_name'] pod = self.arg['pod_name'] container = self.arg['container_name'] tail_lines = self.arg['tail_lines'] limit_bytes = self.arg['limit_bytes'] await self.middleware.call('chart.release.validate_pod_log_args', release, pod, container) release_data = await self.middleware.call('chart.release.get_instance', release) async with api_client() as (api, context): self.watch = Watch() try: async with self.watch.stream( context['core_api'].read_namespaced_pod_log, name=pod, container=container, namespace=release_data['namespace'], tail_lines=tail_lines, limit_bytes=limit_bytes, timestamps=True, ) as stream: async for event in stream: # Event should contain a timestamp in RFC3339 format, we should parse it and supply it # separately so UI can highlight the timestamp giving us a cleaner view of the logs timestamp = event.split(maxsplit=1)[0].strip() try: timestamp = str(parse(timestamp)) except (TypeError, ParserError): timestamp = None else: event = event.split(maxsplit=1)[-1].lstrip() self.send_event('ADDED', fields={ 'data': event, 'timestamp': timestamp }) except ClientConnectionError: pass async def cancel(self): await super().cancel() if self.watch: await self.watch.close() async def on_finish(self): self.watch = None