diff --git a/k8s/base.py b/k8s/base.py index 006823e..ffe9bdd 100644 --- a/k8s/base.py +++ b/k8s/base.py @@ -137,8 +137,13 @@ def watch_list(cls, namespace=None): if line: try: event_json = json.loads(line) - event = WatchEvent(event_json, cls) - yield event + try: + event = WatchEvent(event_json, cls) + yield event + except TypeError: + LOG.exception( + "Unable to create instance of %s from watch event json, discarding event. event_json=%r", + cls.__name__, event_json) except ValueError: LOG.exception("Unable to parse JSON on watch event, discarding event. Line: %r", line) @@ -307,6 +312,9 @@ def __repr__(self): return "{cls}(type={type}, object={object})".format(cls=self.__class__.__name__, type=self.type, object=self.object) + def __eq__(self, other): + return self.type == other.type and self.object == other.object + class LabelSelector(object): """Base for label select operations""" diff --git a/tests/k8s/test_client.py b/tests/k8s/test_client.py index a0dc552..2447b12 100644 --- a/tests/k8s/test_client.py +++ b/tests/k8s/test_client.py @@ -2,13 +2,13 @@ # -*- coding: utf-8 -*- # Copyright 2017-2019 The FIAAS Authors -# +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -20,12 +20,14 @@ import pytest from k8s import config -from k8s.base import Model, Field +from k8s.base import Model, WatchEvent +from k8s.fields import Field, RequiredField from k8s.client import Client, SENSITIVE_HEADERS, _session_factory import requests +# pylint: disable=R0201 @pytest.mark.usefixtures("k8s_config") class TestClient(object): @pytest.fixture @@ -187,6 +189,124 @@ def test_redacts_sensitive_headers(self, key): assert sensitive_value not in text +# pylint: disable=R0201 +@pytest.mark.usefixtures("k8s_config") +class TestWatchListEvents(object): + + def test_watch_list_payload_ok(self, get): + """ + verify watch events of WatchListExample create WatchEvent with the appropriate type and object + """ + response = mock.create_autospec(requests.Response) + response.status_code = 200 + response.iter_lines.return_value = [''' +{ + "type": "ADDED", + "object": { + "value": 1, + "requiredValue": 2 + } +}''', ''' +{ + "type": "MODIFIED", + "object": { + "value": 3, + "requiredValue": 4 + } +} +'''] + get.return_value = response + + expected = [ + _create_watchevent(WatchEvent.ADDED, WatchListExample(value=1, requiredValue=2)), + _create_watchevent(WatchEvent.MODIFIED, WatchListExample(value=3, requiredValue=4)), + ] + + items = list(WatchListExample.watch_list()) + assert items == expected + + def test_watch_list_payload_invalid_json(self, get): + """ + verify event which does not cleanly unmarshal from json to dict is discarded + """ + response = mock.create_autospec(requests.Response) + response.status_code = 200 + response.iter_lines.return_value = [''' +{ + "type": "ADDED", + "object": { + "value": 1, + "requiredValue": 2 + } +} +''', ''' +definitely not valid json +''', ''' +{ + "type": "ADDED", + "object": { + "value": 5, + "requiredValue": 6 + } +}'''] + get.return_value = response + + expected = [ + _create_watchevent(WatchEvent.ADDED, WatchListExample(value=1, requiredValue=2)), + # "definitely not valid json" should be discarded + _create_watchevent(WatchEvent.ADDED, WatchListExample(value=5, requiredValue=6)), + ] + + items = list(WatchListExample.watch_list()) + assert items == expected + + def test_watch_list_payload_invalid_object(self, get): + """ + verify event which contains a resource not valid according to the Model class is discarded + """ + response = mock.create_autospec(requests.Response) + response.status_code = 200 + response.iter_lines.return_value = [''' +{ + "type": "ADDED", + "object": { + "value": 1, + "requiredValue": 2 + } +} +''', ''' +{ + "type": "ADDED", + "object": { + "value": 10, + } +} +''', ''' +{ + "type": "ADDED", + "object": { + "value": 5, + "requiredValue": 6 + } +}'''] + get.return_value = response + + expected = [ + _create_watchevent(WatchEvent.ADDED, WatchListExample(value=1, requiredValue=2)), + # event with value=10 and requiredValue missing should be discarded + _create_watchevent(WatchEvent.ADDED, WatchListExample(value=5, requiredValue=6)), + ] + + items = list(WatchListExample.watch_list()) + assert items == expected + + +def _create_watchevent(event_type, event_object): + """factory function for WatchEvent to make it easier to create test data from actual objects, as the constructor + takes a dict (unmarshaled json)""" + return WatchEvent({"type": event_type, "object": event_object.as_dict()}, event_object.__class__) + + def _absolute_url(url): return config.api_server + url @@ -199,6 +319,7 @@ class Meta: watch_list_url_template = "/watch/{namespace}/example" value = Field(int) + requiredValue = RequiredField(int) class WatchListExampleUnsupported(Model):